blob: cbe737aa20afd1e0c0c62079016db1cd0ae5a2bb [file] [log] [blame]
#!/usr/bin/python
#
# Copyright 2013 Google Inc.
# All Rights Reserved.
#
# Tweak options.
#
# Command line:
# tweak-options.py [--options] <bitrate> <video>
#
# Algorithm:
# - Find runs with good performance for this bitrate and video
# - Create configs that are like this, but novel.
#
__author__ = "hta@google.com (Harald Alvestrand)"
import glob
import md5
import os
import random
import re
import stat
import sys
from optparse import OptionParser
class Option(object):
def __init__(self, name, values):
self.name = name
self.values = values
def PickAnother(self, not_this):
index = random.randint(0, len(self.values)-1)
if self.values[index] == not_this:
return self.values[(index + 1) % len(self.values)]
return self.values[index]
def OptionString(self, value):
return '--' + self.name + '=' + value
def ChangedOption(self, matchval):
print "ChangedOption called on ", matchval.group(1)
result = self.OptionString(self.PickAnother(matchval.group(1)))
print "Returning", result
return result
def PatchConfig(self, config):
""" Modify a configuration by changing the value of this parameter."""
newconfig = re.sub('--' + self.name + '=' + '(\S+)', self.ChangedOption,
config)
assert(config != newconfig)
return newconfig
class ChoiceOption(Option):
def __init__(self, flags):
self.values = flags
def PatchConfig(self, config):
""" Modify a configuration by replacing the instance of this option."""
current_flag = ''
for flag in self.values:
if config.find(' --' + flag + ' ') >= 0:
current_flag = flag
break
if current_flag == '':
raise Exception('No choice option alternative given')
next_flag = self.PickAnother(current_flag)
print "Changing from", current_flag, "to", next_flag
newconfig = re.sub('--' + current_flag + ' ',
'--' + next_flag + ' ', config)
assert(config != newconfig)
return newconfig
options = [
Option('overshoot-pct', ['0', '15', '30', '45']),
Option('undershoot-pct', ['0', '25', '50', '75', '100']),
# CQ mode is not considered for end-usage at the moment.
Option('end-usage', ['cbr', 'vbr']),
# End-usage cq doesn't really make sense unless we also set q to something
# between min and max. This is being checked.
# Option('end-usage', ['cbr', 'vbr', 'cq']),
Option('end-usage', ['cbr', 'vbr']),
Option('cpu-used', ['-16', '0', '16']),
Option('min-q', ['0', '2', '4', '8', '16', '24']),
Option('max-q', ['32', '56', '63']),
Option('buf-sz', ['200', '500', '1000', '2000', '4000', '8000', '16000']),
Option('buf-initial-sz', ['200', '400', '800', '1000', '2000', '4000', '8000', '16000']),
Option('max-intra-rate', ['100', '200', '400', '600', '800', '1200']),
Option('resize-allowed', ['0', '1']),
Option('lag-in-frames', ['0', '1', '2', '4', '8']),
ChoiceOption(['good', 'best', 'rt']),
]
class Config(object):
def __init__(self, dirname, results=''):
self.dirname = dirname
self.results = results
# These 2 values are read when needed.
self.config = ''
self.measurer = ''
def EncoderFileName(self):
return self.dirname + '/encoder'
def MeasurerFileName(self):
return self.dirname + '/measurer'
def Score(self):
"""Computes the "quality" of the encoding.
This function should be changed to fit the purpose of the experiment;
this version works well for finding the best PSNR encoding below
some bandwidth bound.
"""
if self.results == '':
return 0.0
m = re.match(r'target_rate=(\d+)\nencoded_rate=(\d+)\npsnr=([\d\.]+)',
self.results)
if m:
target_rate = int(m.group(1))
encoded_rate = int(m.group(2))
psnr=float(m.group(3))
score = psnr
# Penalize by 0.1 dB per 1 kbits/sec overshoot.
# TODO(hta): Consider whether this should be adjusted based on old runs.
if target_rate < encoded_rate:
score = score - (encoded_rate - target_rate) * 0.1
return score
def FetchEncoder(self):
if self.config == '':
with open(self.EncoderFileName()) as encoder:
self.config = encoder.read()
def Tweak(self):
"""Returns a Config object that's a tweaked version of this object."""
self.FetchEncoder()
with open(self.MeasurerFileName()) as measurerfile:
self.measurer = measurerfile.read()
newconfig = options[random.randint(0, len(options)-1)].PatchConfig(self.config)
m = md5.new()
m.update(newconfig)
hashname = m.hexdigest()[:12]
newconf = Config('vp8/' + hashname)
newconf.config = newconfig
newconf.measurer = self.measurer
return newconf
def Save(self):
if os.path.isdir(self.dirname):
print "Config ", self.dirname, " already exists"
return False
else:
print "Emitting new config ", self.dirname
os.mkdir(self.dirname)
with open(self.EncoderFileName(), 'w') as encoder:
encoder.write(self.config)
os.chmod(self.EncoderFileName(), stat.S_IXUSR | stat.S_IRUSR)
with open(self.MeasurerFileName(), 'w') as measurerfile:
measurerfile.write(self.measurer)
os.chmod(self.MeasurerFileName(), stat.S_IXUSR | stat.S_IRUSR)
return True
def Diff(self, otherconfig):
""" Show config differences. Assumes that words are the same. """
self.FetchEncoder()
otherconfig.FetchEncoder()
words1 = self.config.split()
words2 = otherconfig.config.split()
if len(words1) != len(words2):
return ['Different lengths']
difflist = []
for i in xrange(0,len(words1)):
if words1[i] != words2[i]:
difflist.append(words1[i] + " -> " + words2[i])
return difflist
def AllSingleHopTweaks(self):
""" Return all configs that differ by a single config change. """
configs = []
for option in options:
for alternative in option.names:
# do something intelligent
pass
return configs
def findConfigsWithResult(bitrate, videofilename):
candidates = []
basename = os.path.splitext(os.path.basename(videofilename))[0]
files = glob.glob('vp8/*/' + basename + '.results')
for file in files:
with open(file, 'r') as result:
data = result.read()
if re.search('target_rate=' + bitrate + '\n', data):
candidates.append(Config(os.path.dirname(file), data))
return candidates
def findMatchingConfigs(bitrate):
candidates = []
files = glob.glob('vp8/*/' + 'measurer')
for file in files:
with open(file, 'r') as result:
data = result.read()
if re.search('target_rate=' + bitrate + '\n', data):
candidates.append(Config(os.path.dirname(file), data))
return candidates
def rankCandidateConfigs(candidates):
print "Ranking candidates"
candidates.sort(key=lambda foo: foo.Score())
trail = None
if len(candidates) > 1:
trail = candidates[-2]
candidate = candidates[-1]
print "Changes from", trail.dirname, "to", candidate.dirname
print '\n'.join(trail.Diff(candidate))
print candidate.EncoderFileName(), ': ', candidate.Score()
def emitNewConfig(candidates):
"""Tweak the last configuration (highest score) and save it."""
for i in xrange(10):
# Since the tweaks are random, and may return a previously tried
# configuration, try a few times.
for i in xrange(10):
if candidates[-1].Tweak().Save():
return True
# If there are equivalent candidates, try varying another one.
if (len(candidates) > 1 and
candidates[-1].Score() == candidates[-2].Score()):
candidates.pop()
print "Skipping to configuration", candidates[-1].dirname
else:
return False
return False
def main():
parser = OptionParser()
parser.add_option("--foo", action="store_true", dest="foo")
(cmdline_options, args) = parser.parse_args()
print "Finding candidates"
candidates = findConfigsWithResult(args[0], args[1])
if len(candidates) == 0:
print >> sys.stderr, "No candidates found"
return 1
rankCandidateConfigs(candidates)
if emitNewConfig(candidates):
return 0
print >> sys.stderr, "Unable to change configuration"
return 1
if __name__ == '__main__':
sys.exit(main())