diff options
-rw-r--r-- | tools/telemetry/telemetry/core/bitmap.py | 109 | ||||
-rw-r--r-- | tools/telemetry/telemetry/core/bitmap_unittest.py | 30 | ||||
-rw-r--r-- | tools/telemetry/telemetry/core/bitmaptools/__init__.py | 72 | ||||
-rw-r--r-- | tools/telemetry/telemetry/core/bitmaptools/bitmaptools.cc | 263 | ||||
-rw-r--r-- | tools/telemetry/telemetry/core/tab.py | 19 |
5 files changed, 73 insertions, 420 deletions
diff --git a/tools/telemetry/telemetry/core/bitmap.py b/tools/telemetry/telemetry/core/bitmap.py index e22211f..471fe5f 100644 --- a/tools/telemetry/telemetry/core/bitmap.py +++ b/tools/telemetry/telemetry/core/bitmap.py @@ -4,7 +4,6 @@ import base64 import cStringIO -from telemetry.core import bitmaptools from telemetry.core import util util.AddDirToPythonPath(util.GetTelemetryDir(), 'third_party', 'png') @@ -52,7 +51,6 @@ class Bitmap(object): self._height = height self._pixels = pixels self._metadata = metadata or {} - self._crop_box = None @property def bpp(self): @@ -62,35 +60,16 @@ class Bitmap(object): @property def width(self): """Width of the bitmap.""" - if self._crop_box: - return self._crop_box[2] return self._width @property def height(self): """Height of the bitmap.""" - if self._crop_box: - return self._crop_box[3] return self._height @property - def _as_tuple(self): - # If we got a list of ints, we need to convert it into a byte buffer. - pixels = self._pixels - if type(pixels) is not bytearray: - pixels = bytearray(pixels) - if type(pixels) is not bytes: - pixels = bytes(pixels) - crop_box = self._crop_box or (0, 0, self._width, self._height) - return pixels, self._width, self._bpp, crop_box - - @property def pixels(self): """Flat pixel array of the bitmap.""" - if self._crop_box: - self._pixels = bitmaptools.Crop(self._as_tuple) - _, _, 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 @@ -104,13 +83,12 @@ 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(pixels[base + 0], pixels[base + 1], - pixels[base + 2], pixels[base + 3]) - return RgbaColor(pixels[base + 0], pixels[base + 1], - pixels[base + 2]) + 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]) def WritePngFile(self, path): with open(path, "wb") as f: @@ -131,10 +109,24 @@ class Bitmap(object): return Bitmap.FromPng(base64.b64decode(base64_png)) def IsEqual(self, other, tolerance=0): - """Determines whether two Bitmaps are identical within a given tolerance. - Ignores alpha channel.""" - # pylint: disable=W0212 - return bitmaptools.Equal(self._as_tuple, other._as_tuple, tolerance) + """Determines whether two Bitmaps are identical within a given tolerance.""" + + # Dimensions must be equal + if self.width != other.width or self.height != other.height: + return False + + # Loop over each pixel and test for equality + if tolerance or self.bpp != other.bpp: + for y in range(self.height): + for x in range(self.width): + c0 = self.GetPixelColor(x, y) + c1 = other.GetPixelColor(x, y) + if not c0.IsEqual(c1, tolerance): + return False + else: + return self.pixels == other.pixels + + return True def Diff(self, other): """Returns a new Bitmap that represents the difference between this image @@ -177,26 +169,55 @@ class Bitmap(object): return diff 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.""" - int_color = (color.r << 16) | (color.g << 8) | color.b - return bitmaptools.BoundingBox(self._as_tuple, int_color, tolerance) + """Returns a (top, left, width, height) tuple of the minimum box + surrounding all occurences of |color|.""" + # TODO(szym): Implement this. + raise NotImplementedError("GetBoundingBox not yet implemented.") def Crop(self, top, left, width, height): - """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 + """Crops the current bitmap down to the specified box. + TODO(szym): Make this O(1). + """ if (left < 0 or top < 0 or - (left + width) > cur_width or - (top + height) > cur_height): + (left + width) > self.width or + (top + height) > self.height): raise ValueError('Invalid dimensions') - self._crop_box = cur_left + left, cur_top + top, width, height + 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() + return self def ColorHistogram(self): - """Computes a histogram of the pixel colors in this Bitmap. - Returns a list of 3x256 integers.""" - return bitmaptools.Histogram(self._as_tuple) + """Returns a histogram of the pixel colors in this Bitmap.""" + # TODO(szym): Implement this. + raise NotImplementedError("ColorHistogram not yet implemented.") diff --git a/tools/telemetry/telemetry/core/bitmap_unittest.py b/tools/telemetry/telemetry/core/bitmap_unittest.py index 912aa3c..618faa0 100644 --- a/tools/telemetry/telemetry/core/bitmap_unittest.py +++ b/tools/telemetry/telemetry/core/bitmap_unittest.py @@ -70,8 +70,6 @@ class BitmapTest(unittest.TestCase): def testIsEqual(self): bmp = bitmap.Bitmap.FromBase64Png(test_png) file_bmp = bitmap.Bitmap.FromPngFile(test_png_path) - self.assertTrue(bmp.IsEqual(file_bmp, tolerance=1)) - # Zero tolerance IsEqual has a different implementation. self.assertTrue(bmp.IsEqual(file_bmp)) def testDiff(self): @@ -104,19 +102,6 @@ class BitmapTest(unittest.TestCase): diff_bmp.GetPixelColor(2, 1).AssertIsRGB(255, 255, 255) diff_bmp.GetPixelColor(2, 2).AssertIsRGB(255, 255, 255) - def testGetBoundingBox(self): - pixels = [0,0,0, 0,0,0, 0,0,0, 0,0,0, - 0,0,0, 1,0,0, 1,0,0, 0,0,0, - 0,0,0, 0,0,0, 0,0,0, 0,0,0] - bmp = bitmap.Bitmap(3, 4, 3, pixels) - box, count = bmp.GetBoundingBox(bitmap.RgbaColor(1, 0, 0)) - self.assertEquals(box, (1, 1, 2, 1)) - self.assertEquals(count, 2) - - box, count = bmp.GetBoundingBox(bitmap.RgbaColor(0, 1, 0)) - self.assertEquals(box, None) - self.assertEquals(count, 0) - def testCrop(self): pixels = [0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 1,0,0, 1,0,0, 0,0,0, @@ -129,18 +114,3 @@ class BitmapTest(unittest.TestCase): bmp.GetPixelColor(0, 0).AssertIsRGB(1, 0, 0) bmp.GetPixelColor(1, 0).AssertIsRGB(1, 0, 0) self.assertEquals(bmp.pixels, bytearray([1,0,0, 1,0,0])) - - def testHistogram(self): - pixels = [1,2,3, 1,2,3, 1,2,3, 1,2,3, - 1,2,3, 8,7,6, 5,4,6, 1,2,3, - 1,2,3, 8,7,6, 5,4,6, 1,2,3] - bmp = bitmap.Bitmap(3, 4, 3, pixels) - bmp.Crop(1, 1, 2, 2) - - histogram = bmp.ColorHistogram() - self.assertEquals(sum(histogram), bmp.width * bmp.height * 3) - self.assertEquals(histogram[5], 2) - self.assertEquals(histogram[8], 2) - self.assertEquals(histogram[4 + 256], 2) - self.assertEquals(histogram[7 + 256], 2) - self.assertEquals(histogram[6 + 512], 4)
\ No newline at end of file diff --git a/tools/telemetry/telemetry/core/bitmaptools/__init__.py b/tools/telemetry/telemetry/core/bitmaptools/__init__.py deleted file mode 100644 index 30a351f..0000000 --- a/tools/telemetry/telemetry/core/bitmaptools/__init__.py +++ /dev/null @@ -1,72 +0,0 @@ -# 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 processing routines. - -All functions accept a tuple of (pixels, width, channels) as the first argument. -Bounding box is a tuple (left, right, width, height). -""" - - -def _FixDistutilsMsvcCompiler(): - # To avoid runtime mismatch, distutils should use the compiler which was used - # to build python. But our module does not use the runtime much, so it should - # be fine to build within a different environment. - # See also: http://bugs.python.org/issue7511 - from distutils import msvc9compiler - for version in [msvc9compiler.get_build_version(), 9.0, 10.0, 11.0, 12.0]: - msvc9compiler.VERSION = version - try: - msvc9compiler.MSVCCompiler().initialize() - break - except Exception: - pass - - -def _BuildExtension(): - """Builds the extension library on demand.""" - from distutils import log - from distutils.core import Distribution, Extension - import os - import tempfile - - if os.name == 'nt': - _FixDistutilsMsvcCompiler() - - build_dir = tempfile.mkdtemp() - dirname = os.path.dirname(__file__) - # Source file paths must be relative to current path. - relpath = os.path.relpath(dirname, os.getcwd()) - src_files = [ - os.path.join(relpath, 'bitmaptools.cc') - ] - dist = Distribution({ - 'ext_modules': [Extension('bitmaptools', src_files)] - }) - dist.script_args = ['build_ext', '--build-temp', build_dir, - '--build-lib', dirname] - dist.parse_command_line() - log.set_threshold(log.ERROR) - dist.run_commands() - dist.script_args = ['clean', '--build-temp', build_dir, '--all'] - dist.parse_command_line() - log.set_threshold(log.ERROR) - dist.run_commands() - - -try: - # Always re-build from source. No-op if source file older than the library. - _BuildExtension() -except Exception: - # TODO(tonyg): fetch from cloudstorage - pass - - -try: - # pylint: disable=W0401,F0401 - from .bitmaptools import * -except ImportError: - raise NotImplementedError( - 'The bitmaptools module is not available for this platform.' - ) diff --git a/tools/telemetry/telemetry/core/bitmaptools/bitmaptools.cc b/tools/telemetry/telemetry/core/bitmaptools/bitmaptools.cc deleted file mode 100644 index 8ba4300..0000000 --- a/tools/telemetry/telemetry/core/bitmaptools/bitmaptools.cc +++ /dev/null @@ -1,263 +0,0 @@ -// 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. - -#include <Python.h> -#include <string.h> - - -struct Box { - Box() : left(), top(), right(), bottom() {} - - bool ParseArg(PyObject* obj) { - int width; - int height; - if (!PyArg_ParseTuple(obj, "iiii", &left, &top, &width, &height)) - return false; - if (left < 0 || top < 0 || width < 0 || height < 0) { - PyErr_SetString(PyExc_ValueError, "Box dimensions must be non-negative."); - return false; - } - right = left + width; - bottom = top + height; - return true; - } - - PyObject* MakeObject() const { - if (right <= left || bottom <= top) - return Py_None; - return Py_BuildValue("iiii", left, top, right - left, bottom - top); - } - - void Union(int x, int y) { - if (left > x) left = x; - if (right <= x) right = x + 1; - if (top > y) top = y; - if (bottom <= y) bottom = y + 1; - } - - int width() const { return right - left; } - int height() const { return bottom - top; } - - int left; - int top; - int right; - int bottom; -}; - - -// Represents a bitmap buffer with a crop box. -struct Bitmap { - Bitmap() {} - - ~Bitmap() { - if (pixels.buf) - PyBuffer_Release(&pixels); - } - - bool ParseArg(PyObject* obj) { - int width; - int bpp; - PyObject* box_object; - if (!PyArg_ParseTuple(obj, "s*iiO", &pixels, &width, &bpp, &box_object)) - return false; - if (width <= 0 || bpp <= 0) { - PyErr_SetString(PyExc_ValueError, "Width and bpp must be positive."); - return false; - } - - row_stride = width * bpp; - pixel_stride = bpp; - total_size = pixels.len; - row_size = row_stride; - - if (pixels.len % row_stride != 0) { - PyErr_SetString(PyExc_ValueError, "Length must be a multiple of width " - "and bpp."); - return false; - } - - if (!box.ParseArg(box_object)) - return false; - - if (box.bottom * row_stride > total_size || - box.right * pixel_stride > row_size) { - PyErr_SetString(PyExc_ValueError, "Crop box overflows the bitmap."); - return false; - } - - total_size = (box.bottom - box.top) * row_stride; - row_size = (box.right - box.left) * pixel_stride; - data = reinterpret_cast<const unsigned char*>(pixels.buf) + - box.top * row_stride + box.left * pixel_stride; - return true; - } - - Py_buffer pixels; - Box box; - // Points at the top-left pixel in |pixels.buf|. - const unsigned char* data; - // These counts are in bytes. - int row_stride; - int pixel_stride; - int total_size; - int row_size; -}; - - -static -PyObject* Histogram(PyObject* self, PyObject* bmp_object) { - Bitmap bmp; - if (!bmp.ParseArg(bmp_object)) - return NULL; - - const int kLength = 3 * 256; - int counts[kLength] = {}; - - for (const unsigned char* row = bmp.data; row < bmp.data + bmp.total_size; - row += bmp.row_stride) { - for (const unsigned char* pixel = row; pixel < row + bmp.row_size; - pixel += bmp.pixel_stride) { - ++(counts[256 * 0 + pixel[0]]); - ++(counts[256 * 1 + pixel[1]]); - ++(counts[256 * 2 + pixel[2]]); - } - } - - PyObject* list = PyList_New(kLength); - if (!list) - return NULL; - - for (int i = 0; i < kLength; ++i) - PyList_SetItem(list, i, PyInt_FromLong(counts[i])); - - return list; -} - - -static inline -bool PixelsEqual(const unsigned char* pixel1, const unsigned char* pixel2, - int tolerance) { - // Note: this works for both RGB and RGBA. Alpha channel is ignored. - return (abs(pixel1[0] - pixel2[0]) <= tolerance) && - (abs(pixel1[1] - pixel2[1]) <= tolerance) && - (abs(pixel1[2] - pixel2[2]) <= tolerance); -} - -static inline -bool PixelsEqual(const unsigned char* pixel, int color, int tolerance) { - unsigned char pixel2[3] = { color >> 16, color >> 8, color }; - return PixelsEqual(pixel, pixel2, tolerance); -} - -static -PyObject* Equal(PyObject* self, PyObject* args) { - PyObject* bmp_obj1; - PyObject* bmp_obj2; - int tolerance; - if (!PyArg_ParseTuple(args, "OOi", &bmp_obj1, &bmp_obj2, &tolerance)) - return NULL; - - Bitmap bmp1, bmp2; - if (!bmp1.ParseArg(bmp_obj1) || !bmp2.ParseArg(bmp_obj2)) - return NULL; - - if (bmp1.box.width() != bmp2.box.width() || - bmp1.box.height() != bmp2.box.height()) { - PyErr_SetString(PyExc_ValueError, "Bitmap dimensions don't match."); - return NULL; - } - - bool simple_match = (tolerance == 0) && - (bmp1.pixel_stride == 3) && - (bmp2.pixel_stride == 3); - for (const unsigned char *row1 = bmp1.data, *row2 = bmp2.data; - row1 < bmp1.data + bmp1.total_size; - row1 += bmp1.row_stride, row2 += bmp2.row_stride) { - if (simple_match) { - if (memcmp(row1, row2, bmp1.row_size) != 0) - return Py_False; - continue; - } - for (const unsigned char *pixel1 = row1, *pixel2 = row2; - pixel1 < row1 + bmp1.row_size; - pixel1 += bmp1.pixel_stride, pixel2 += bmp2.pixel_stride) { - if (!PixelsEqual(pixel1, pixel2, tolerance)) - return Py_False; - } - } - - return Py_True; -} - -static -PyObject* BoundingBox(PyObject* self, PyObject* args) { - PyObject* bmp_object; - int color; - int tolerance; - if (!PyArg_ParseTuple(args, "Oii", &bmp_object, &color, &tolerance)) - return NULL; - - Bitmap bmp; - if (!bmp.ParseArg(bmp_object)) - return NULL; - - Box box; - box.left = bmp.pixels.len; - box.top = bmp.pixels.len; - box.right = 0; - box.bottom = 0; - - int count = 0; - int y = 0; - for (const unsigned char* row = bmp.data; row < bmp.data + bmp.total_size; - row += bmp.row_stride, ++y) { - int x = 0; - for (const unsigned char* pixel = row; pixel < row + bmp.row_size; - pixel += bmp.pixel_stride, ++x) { - if (!PixelsEqual(pixel, color, tolerance)) - continue; - box.Union(x, y); - ++count; - } - } - - return Py_BuildValue("Oi", box.MakeObject(), count); -} - -static -PyObject* Crop(PyObject* self, PyObject* bmp_object) { - Bitmap bmp; - if (!bmp.ParseArg(bmp_object)) - return NULL; - - int out_size = bmp.row_size * bmp.box.height(); - unsigned char* out = new unsigned char[out_size]; - unsigned char* dst = out; - for (const unsigned char* row = bmp.data; - row < bmp.data + bmp.total_size; - row += bmp.row_stride, dst += bmp.row_size) { - // No change in pixel_stride, so we can copy whole rows. - memcpy(dst, row, bmp.row_size); - } - - PyObject* result = Py_BuildValue("s#", out, out_size); - delete[] out; - return result; -} - -static PyMethodDef module_methods[] = { - {"Histogram", Histogram, METH_O, - "Calculates histogram of bitmap colors. Returns a list of 3x256 ints."}, - {"Equal", Equal, METH_VARARGS, - "Checks if the two bmps are equal."}, - {"BoundingBox", BoundingBox, METH_VARARGS, - "Calculates bounding box of matching color."}, - {"Crop", Crop, METH_O, - "Crops the bmp to crop box."}, - {NULL, NULL, 0, NULL} /* sentinel */ -}; - -PyMODINIT_FUNC initbitmaptools(void) { - Py_InitModule("bitmaptools", module_methods); -} diff --git a/tools/telemetry/telemetry/core/tab.py b/tools/telemetry/telemetry/core/tab.py index a4fa2a6..11bed68 100644 --- a/tools/telemetry/telemetry/core/tab.py +++ b/tools/telemetry/telemetry/core/tab.py @@ -135,10 +135,9 @@ class Tab(web_contents.WebContents): bitmap is a telemetry.core.Bitmap. """ content_box = None - start_time = None for timestamp, bmp in self.browser.platform.StopVideoCapture(): if not content_box: - content_box, pixel_count = bmp.GetBoundingBox( + content_box = bmp.GetBoundingBox( bitmap.RgbaColor(*_CONTENT_FLASH_COLOR), tolerance=4) assert content_box, 'Failed to find tab contents in first video frame.' @@ -146,25 +145,23 @@ class Tab(web_contents.WebContents): # We assume arbitrarily that tabs are all larger than 200x200. If this # fails it either means that assumption has changed or something is # awry with our bounding box calculation. - assert content_box[2] > 200 and content_box[3] > 200, \ - 'Unexpectedly small tab contents.' - assert pixel_count > 0.75 * bmp.width * bmp.height, \ - 'Low count of pixels in tab contents matching expected color.' + assert content_box.width > 200 and content_box.height > 200, \ + 'Unexpectedly small tab contents' # Since Telemetry doesn't know how to resize the window, we assume # that we should always get the same content box for a tab. If this - # fails, it means either that assumption has changed or something is + # fails, it meas either that assumption has changed or something is # awry with our bounding box calculation. if self._previous_tab_contents_bounding_box: assert self._previous_tab_contents_bounding_box == content_box, \ 'Unexpected change in tab contents box.' self._previous_tab_contents_bounding_box = content_box - continue - elif not start_time: - start_time = timestamp + continue - yield timestamp - start_time, bmp.Crop(*content_box) + bmp.Crop(content_box) + # TODO(tonyg): Translate timestamp into navigation timing space. + yield timestamp, bmp def PerformActionAndWaitForNavigate( self, action_function, timeout=DEFAULT_TAB_TIMEOUT): |