| Index: tools/telemetry/telemetry/core/bitmapiter.py
|
| diff --git a/tools/telemetry/telemetry/core/bitmapiter.py b/tools/telemetry/telemetry/core/bitmapiter.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..22444b7d7ff424dbb2e6c72e8c9fb0b6a0a028ef
|
| --- /dev/null
|
| +++ b/tools/telemetry/telemetry/core/bitmapiter.py
|
| @@ -0,0 +1,175 @@
|
| +# Copyright 2014 The Chromium Authors. All rights reserved.
|
| +# Use of this source code is governed by a BSD-style license that can be
|
| +# found in the LICENSE file.
|
| +
|
| +import array
|
| +import os
|
| +import struct
|
| +import subprocess
|
| +
|
| +from telemetry.core import bitmap
|
| +from telemetry.core import util
|
| +
|
| +
|
| +def _GetDimensions(video):
|
| + proc = subprocess.Popen(['avconv', '-i', video], stderr=subprocess.PIPE)
|
| + dimensions = None
|
| + output = ''
|
| + for line in proc.stderr.readlines():
|
| + output += line
|
| + if 'Video:' in line:
|
| + dimensions = line.split(',')[2]
|
| + dimensions = map(int, dimensions.split()[0].split('x'))
|
| + break
|
| + proc.wait()
|
| + assert dimensions, ('Failed to determine video dimensions. output=%s' %
|
| + output)
|
| + return dimensions
|
| +
|
| +
|
| +def _GetFrameTimestampMs(stderr):
|
| + """Returns the frame timestamp in integer milliseconds from the avconv log.
|
| +
|
| + The expected line format is:
|
| + ' dts=1.715 pts=1.715\n'
|
| +
|
| + We have to be careful to only read a single timestamp per call to avoid
|
| + deadlock because avconv interleaves its writes to stdout and stderr.
|
| + """
|
| + while True:
|
| + line = ''
|
| + next_char = ''
|
| + while next_char != '\n':
|
| + next_char = stderr.read(1)
|
| + line += next_char
|
| + if 'pts=' in line:
|
| + return int(1000 * float(line.split('=')[-1]))
|
| +
|
| +
|
| +class _Commands(object):
|
| + """Command types for the external bitmapiter tool."""
|
| + NEXT = 1
|
| + PIXELS = 2
|
| + BOUNDING_BOX = 3
|
| + HISTOGRAM = 4
|
| + CROP = 5
|
| + MAGIC = 0x2751912F
|
| +
|
| +
|
| +class BitmapIter(object):
|
| + """Extracts Bitmap from video files and supports fast per-pixel operations.
|
| +
|
| + It communicates with the external bitmapiter tool to process a stream of
|
| + uncompressed pixels and perform expensive operations: GetBoundingBox and
|
| + ColorHistogram.
|
| + """
|
| +
|
| + def __init__(self, mp4_file):
|
| + bitmapiter_binary = util.FindSupportBinary('bitmapiter')
|
| + assert bitmapiter_binary, 'You must build bitmapiter first!'
|
| +
|
| + self._bpp = 3
|
| + self._width, self._height = _GetDimensions(mp4_file)
|
| +
|
| + # Current bitmap, if available.
|
| + self._bitmap = None
|
| + # Last timestamp.
|
| + self._timestamp = None
|
| +
|
| + # Use rawvideo so that we don't need any external library to parse pixels.
|
| + command = ['avconv', '-i', mp4_file, '-vcodec', 'rawvideo',
|
| + '-pix_fmt', 'rgb24', '-dump', '-loglevel', 'debug',
|
| + '-f', 'rawvideo', '-']
|
| +
|
| + avconv = subprocess.Popen(command, stdout=subprocess.PIPE,
|
| + stderr=subprocess.PIPE)
|
| + self._dumpfile = avconv.stderr
|
| + # Command pipe to communicate to bitmapiter.
|
| + pipe = os.pipe()
|
| + bmpit = subprocess.Popen([bitmapiter_binary, str(pipe[0])],
|
| + stdin=avconv.stdout,
|
| + stdout=subprocess.PIPE,
|
| + stderr=subprocess.PIPE)
|
| + self._cmdpipe = pipe[1]
|
| + self._outpipe = bmpit.stdout.fileno()
|
| + self._errfile = bmpit.stderr
|
| + self._RunCommand(_Commands.MAGIC, self._bpp, self._width, self._height)
|
| +
|
| + def _RunCommand(self, *command):
|
| + # Command is a packed bunch of ints.
|
| + packed_command = struct.pack('i' * len(command), *command)
|
| + os.write(self._cmdpipe, packed_command)
|
| + # Response is length + bytes.
|
| + length_response = os.read(self._outpipe, struct.calcsize('i'))
|
| + if not length_response:
|
| + msg = self._errfile.read()
|
| + if msg == b'STOP\n':
|
| + raise StopIteration
|
| + raise Exception(msg)
|
| +
|
| + length = struct.unpack('i', length_response)[0]
|
| + if length:
|
| + return os.read(self._outpipe, length)
|
| +
|
| + def __iter__(self):
|
| + return self
|
| +
|
| + def next(self):
|
| + self._RunCommand(_Commands.NEXT)
|
| + self._timestamp = _GetFrameTimestampMs(self._dumpfile)
|
| + return self
|
| +
|
| + @property
|
| + def bpp(self):
|
| + return self._bpp
|
| +
|
| + @property
|
| + def width(self):
|
| + return self._width
|
| +
|
| + @property
|
| + def height(self):
|
| + return self._height
|
| +
|
| + @property
|
| + def timestamp(self):
|
| + return self._timestamp
|
| +
|
| + @property
|
| + def bitmap(self):
|
| + if not self._bitmap:
|
| + pixels = self._RunCommand(_Commands.PIXELS)
|
| + self._bitmap = bitmap.Bitmap(self._bpp, self._width, self._height, pixels)
|
| + return self._bitmap
|
| +
|
| + def GetBoundingBox(self, color, tolerance=0):
|
| + """Finds the minimum box surrounding all occurences of |color|.
|
| + Returns: (top, left, width, height), match_count
|
| + Ignores the alpha channel."""
|
| + response = self._RunCommand(_Commands.BOUNDING_BOX, color, tolerance)
|
| + top, left, width, height, match_count = struct.unpack('iiiii', response)
|
| + return (top, left, width, height), match_count
|
| +
|
| + def Crop(self, left, top, width, height):
|
| + """Crops the current bitmap down to the specified box."""
|
| + if (left < 0 or top < 0 or
|
| + (left + width) > self.width or
|
| + (top + height) > self.height):
|
| + raise ValueError('Invalid dimensions')
|
| + self._RunCommand(_Commands.CROP, left, top, width, height)
|
| + return self
|
| +
|
| + def ColorHistogram(self, ignore_color=-1, tolerance=0):
|
| + """Computes a histogram of the pixel colors in this Bitmap.
|
| + Args:
|
| + ignore_color: An RgbaColor to exclude from the bucket counts.
|
| + tolerance: A tolerance for the ignore_color.
|
| +
|
| + Returns:
|
| + A list of 3x256 integers formatted as
|
| + [r0, r1, ..., g0, g1, ..., b0, b1, ...].
|
| + """
|
| + response = self._RunCommand(_Commands.HISTOGRAM, ignore_color, tolerance)
|
| + out = array.array('i')
|
| + out.fromstring(response)
|
| + return out
|
|
|