Index: tools/telemetry/telemetry/core/bitmap.py |
diff --git a/tools/telemetry/telemetry/core/bitmap.py b/tools/telemetry/telemetry/core/bitmap.py |
index 8c52d3b2848649b2a279c24f7c472ce14ea90711..9b4c163b94bb2c75d17860d5c5ae02584a6f5d56 100644 |
--- a/tools/telemetry/telemetry/core/bitmap.py |
+++ b/tools/telemetry/telemetry/core/bitmap.py |
@@ -1,8 +1,18 @@ |
# Copyright 2013 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. |
+ |
+""" |
+Bitmap is a basic wrapper for image pixels. It includes some basic processing |
+tools: crop, find bounding box of a color and compute histogram of color values. |
+""" |
+ |
+import array |
import base64 |
import cStringIO |
+import struct |
+import subprocess |
+import sys |
from telemetry.core import util |
@@ -43,6 +53,62 @@ WEB_PAGE_TEST_ORANGE = RgbaColor(222, 100, 13) |
WHITE = RgbaColor(255, 255, 255) |
+class _BitmapTools(object): |
+ """Wraps a child process of bitmaptools and allows for one command.""" |
+ CROP_PIXELS = 0 |
+ HISTOGRAM = 1 |
+ BOUNDING_BOX = 2 |
+ |
+ def __init__(self, dimensions, pixels): |
+ suffix = '.exe' if sys.platform == 'win32' else '' |
+ binary = util.FindSupportBinary('bitmaptools' + suffix) |
+ assert binary, 'You must build bitmaptools first!' |
+ |
+ self._popen = subprocess.Popen([binary], |
+ stdin=subprocess.PIPE, |
+ stdout=subprocess.PIPE, |
+ stderr=subprocess.PIPE) |
+ |
+ # dimensions are: bpp, width, height, boxleft, boxtop, boxwidth, boxheight |
+ packed_dims = struct.pack('iiiiiii', *dimensions) |
+ self._popen.stdin.write(packed_dims) |
+ # If we got a list of ints, we need to convert it into a byte buffer. |
+ if type(pixels) is not bytearray: |
+ pixels = bytearray(pixels) |
+ self._popen.stdin.write(pixels) |
+ |
+ def _RunCommand(self, *command): |
+ assert not self._popen.stdin.closed, ( |
+ 'Exactly one command allowed per instance of tools.') |
+ packed_command = struct.pack('i' * len(command), *command) |
+ self._popen.stdin.write(packed_command) |
+ self._popen.stdin.close() |
+ length_packed = self._popen.stdout.read(struct.calcsize('i')) |
+ if not length_packed: |
+ raise Exception(self._popen.stderr.read()) |
+ length = struct.unpack('i', length_packed)[0] |
+ return self._popen.stdout.read(length) |
+ |
+ def CropPixels(self): |
+ return self._RunCommand(_BitmapTools.CROP_PIXELS) |
+ |
+ def Histogram(self, ignore_color, tolerance): |
+ ignore_color = -1 if ignore_color is None else int(ignore_color) |
+ response = self._RunCommand(_BitmapTools.HISTOGRAM, ignore_color, tolerance) |
+ out = array.array('i') |
+ out.fromstring(response) |
+ return out |
+ |
+ def BoundingBox(self, color, tolerance): |
+ response = self._RunCommand(_BitmapTools.BOUNDING_BOX, int(color), |
+ tolerance) |
+ unpacked = struct.unpack('iiiii', response) |
+ box, count = unpacked[:4], unpacked[-1] |
+ if box[2] < 0 or box[3] < 0: |
+ box = None |
+ return box, count |
+ |
+ |
class Bitmap(object): |
"""Utilities for parsing and inspecting a bitmap.""" |
@@ -58,6 +124,7 @@ class Bitmap(object): |
self._height = height |
self._pixels = pixels |
self._metadata = metadata or {} |
+ self._crop_box = None |
@property |
def bpp(self): |
@@ -67,16 +134,27 @@ class Bitmap(object): |
@property |
def width(self): |
"""Width of the bitmap.""" |
- return self._width |
+ return self._crop_box[2] if self._crop_box else self._width |
@property |
def height(self): |
"""Height of the bitmap.""" |
- return self._height |
+ return self._crop_box[3] if self._crop_box else self._height |
+ |
+ def _PrepareTools(self): |
+ """Prepares an instance of _BitmapTools which allows exactly one command. |
+ """ |
+ crop_box = self._crop_box or (0, 0, self._width, self._height) |
+ return _BitmapTools((self._bpp, self._width, self._height) + crop_box, |
+ self._pixels) |
@property |
def pixels(self): |
"""Flat pixel array of the bitmap.""" |
+ if self._crop_box: |
+ self._pixels = self._PrepareTools().CropPixels() |
+ _, _, self._width, self._height = self._crop_box |
+ self._crop_box = None |
if type(self._pixels) is not bytearray: |
self._pixels = bytearray(self._pixels) |
return self._pixels |
@@ -90,12 +168,13 @@ class Bitmap(object): |
def GetPixelColor(self, x, y): |
"""Returns a RgbaColor for the pixel at (x, y).""" |
+ pixels = self.pixels |
base = self._bpp * (y * self._width + x) |
if self._bpp == 4: |
- return RgbaColor(self._pixels[base + 0], self._pixels[base + 1], |
- self._pixels[base + 2], self._pixels[base + 3]) |
- return RgbaColor(self._pixels[base + 0], self._pixels[base + 1], |
- self._pixels[base + 2]) |
+ return RgbaColor(pixels[base + 0], pixels[base + 1], |
+ pixels[base + 2], pixels[base + 3]) |
+ return RgbaColor(pixels[base + 0], pixels[base + 1], |
+ pixels[base + 2]) |
def WritePngFile(self, path): |
with open(path, "wb") as f: |
@@ -179,49 +258,19 @@ class Bitmap(object): |
"""Finds the minimum box surrounding all occurences of |color|. |
Returns: (top, left, width, height), match_count |
Ignores the alpha channel.""" |
- # TODO(szym): Implement this. |
- raise NotImplementedError("GetBoundingBox not yet implemented.") |
+ return self._PrepareTools().BoundingBox(color, tolerance) |
def Crop(self, left, top, width, height): |
- """Crops the current bitmap down to the specified box. |
- TODO(szym): Make this O(1). |
- """ |
+ """Crops the current bitmap down to the specified box.""" |
+ cur_box = self._crop_box or (0, 0, self._width, self._height) |
+ cur_left, cur_top, cur_width, cur_height = cur_box |
+ |
if (left < 0 or top < 0 or |
- (left + width) > self.width or |
- (top + height) > self.height): |
+ (left + width) > cur_width or |
+ (top + height) > cur_height): |
raise ValueError('Invalid dimensions') |
- img_data = [[0 for x in xrange(width * self.bpp)] |
- for y in xrange(height)] |
- |
- # Copy each pixel in the sub-rect. |
- # TODO(tonyg): Make this faster by avoiding the copy and artificially |
- # restricting the dimensions. |
- for y in range(height): |
- for x in range(width): |
- c = self.GetPixelColor(x + left, y + top) |
- offset = x * self.bpp |
- img_data[y][offset] = c.r |
- img_data[y][offset + 1] = c.g |
- img_data[y][offset + 2] = c.b |
- if self.bpp == 4: |
- img_data[y][offset + 3] = c.a |
- |
- # This particular method can only save to a file, so the result will be |
- # written into an in-memory buffer and read back into a Bitmap |
- crop_img = png.from_array(img_data, mode='RGBA' if self.bpp == 4 else 'RGB') |
- output = cStringIO.StringIO() |
- try: |
- crop_img.save(output) |
- width, height, pixels, meta = png.Reader( |
- bytes=output.getvalue()).read_flat() |
- self._width = width |
- self._height = height |
- self._pixels = pixels |
- self._metadata = meta |
- finally: |
- output.close() |
- |
+ self._crop_box = cur_left + left, cur_top + top, width, height |
return self |
def ColorHistogram(self, ignore_color=None, tolerance=0): |
@@ -234,5 +283,4 @@ class Bitmap(object): |
A list of 3x256 integers formatted as |
[r0, r1, ..., g0, g1, ..., b0, b1, ...]. |
""" |
- # TODO(szym): Implement this. |
- raise NotImplementedError("ColorHistogram not yet implemented.") |
+ return self._PrepareTools().Histogram(ignore_color, tolerance) |