Commit 82c92567 authored by thestig@chromium.org's avatar thestig@chromium.org

Revert 243469 "[telemetry] Implement per-pixel algorithms in Bit..."

> [telemetry] Implement per-pixel algorithms in Bitmap as a C++ extension.
> 
> The extension provides fast bitmap operations with no external
> dependencies. However, it is not available on all platforms.
> 
> BUG=323813
> TEST=telemetry bitmap_unittest
> R=bulach@chromium.org, tonyg@chromium.org
> 
> Review URL: https://codereview.chromium.org/121493004

TBR=szym@chromium.org

Review URL: https://codereview.chromium.org/127533002

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@243478 0039d316-1c4b-4281-b951-d872f2087c98
parent 8b440501
......@@ -265,7 +265,6 @@
'../third_party/cacheinvalidation/cacheinvalidation.gyp:cacheinvalidation_unittests',
'../third_party/libaddressinput/libaddressinput.gyp:libaddressinput_unittests',
'../third_party/libphonenumber/libphonenumber.gyp:libphonenumber_unittests',
'../tools/telemetry/telemetry.gyp:*',
'../webkit/renderer/compositor_bindings/compositor_bindings_tests.gyp:webkit_compositor_bindings_unittests',
],
}],
......
......@@ -36,19 +36,5 @@
],
},
}],
['OS=="android" or OS=="linux" or OS=="mac"', {
'variables': {
'isolate_dependency_tracked': [
'<(PRODUCT_DIR)/bitmaptools.so',
],
},
}],
['OS=="win"', {
'variables': {
'isolate_dependency_tracked': [
'<(PRODUCT_DIR)/bitmaptools.pyd',
],
},
}],
]
}
# 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.
{
'targets' : [
{
'target_name': 'bitmaptools',
'type': 'none',
'variables': {
'output_path': '<(PRODUCT_DIR)',
},
'conditions': [
['OS=="linux" or OS=="mac" or OS=="android"', {
'variables': {
'python_extension': '.so'
},
}],
['OS=="win"', {
'variables': {
'python_extension': '.pyd'
},
}],
['OS=="win" or OS=="linux" or OS=="mac" or OS=="android"', {
'actions': [{
'action_name': 'bitmaptools',
'inputs': [
'telemetry/core/build_extension.py',
'telemetry/core/bitmaptools/bitmaptools.cc',
],
'outputs': [
'<(output_path)/bitmaptools>(python_extension)'
],
'action': [
'python',
'<@(_inputs)',
'<(output_path)',
'bitmaptools',
]
}],
}],
]
},
],
}
......@@ -51,7 +51,6 @@ class Bitmap(object):
self._height = height
self._pixels = pixels
self._metadata = metadata or {}
self._crop_box = None
@property
def bpp(self):
......@@ -61,36 +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:
from telemetry.core import bitmaptools
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,11 +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."""
from telemetry.core import bitmaptools
# 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
......@@ -178,28 +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."""
from telemetry.core import bitmaptools
int_color = (color.r << 16) | (color.g << 8) | color.b
return bitmaptools.BoundingBox(self._as_tuple, int_color, tolerance)
def Crop(self, left, top, 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
"""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.
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."""
from telemetry.core import bitmaptools
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.")
......@@ -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,43 +102,15 @@ 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):
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,
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, 1,0,0, 2,0,0, 3,0,0,
0,1,0, 1,1,0, 2,1,0, 3,1,0,
0,2,0, 1,2,0, 2,2,0, 3,2,0]
bmp = bitmap.Bitmap(3, 4, 3, pixels)
bmp.Crop(1, 2, 2, 1)
bmp.Crop(1, 1, 2, 1)
self.assertEquals(bmp.width, 2)
self.assertEquals(bmp.height, 1)
bmp.GetPixelColor(0, 0).AssertIsRGB(1, 2, 0)
bmp.GetPixelColor(1, 0).AssertIsRGB(2, 2, 0)
self.assertEquals(bmp.pixels, bytearray([1,2,0, 2,2,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
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]))
# 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.
""" 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).
"""
import imp
import os
import sys
from telemetry.core import build_extension, util
def _BuildModule(module_name):
# Build the extension for telemetry users who don't use all.gyp.
path = os.path.dirname(__file__)
src_files = [os.path.join(path, 'bitmaptools.cc')]
build_extension.BuildExtension(src_files, path, module_name)
return imp.find_module(module_name, [path])
def _FindAndImport():
found = util.FindSupportModule('bitmaptools')
if not found:
found = _BuildModule('bitmaptools')
if not found:
raise NotImplementedError('The bitmaptools module is not available.')
return imp.load_module('bitmaptools', *found)
sys.modules['bitmaptools_ext'] = _FindAndImport()
# pylint: disable=W0401,F0401
from bitmaptools_ext import *
// 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.
#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);
}
# 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.
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()
return
except Exception:
pass
raise Exception('Could not initialize MSVC compiler for distutils.')
def BuildExtension(sources, output_dir, extension_name):
from distutils import log
from distutils.core import Distribution, Extension
import os
import tempfile
if os.name == 'nt':
_FixDistutilsMsvcCompiler()
build_dir = tempfile.mkdtemp()
# Source file paths must be relative to current path.
cwd = os.getcwd()
src_files = [os.path.relpath(filename, cwd) for filename in sources]
dist = Distribution({
'ext_modules': [Extension(extension_name, src_files)]
})
dist.script_args = ['build_ext', '--build-temp', build_dir,
'--build-lib', output_dir]
dist.parse_command_line()
log.set_threshold(log.DEBUG)
dist.run_commands()
dist.script_args = ['clean', '--build-temp', build_dir, '--all']
dist.parse_command_line()
log.set_threshold(log.DEBUG)
dist.run_commands()
if __name__ == '__main__':
import sys
assert len(sys.argv) > 3, 'Usage: build.py source-files output-dir ext-name'
BuildExtension(sys.argv[1:-2], sys.argv[-2], sys.argv[-1])
......@@ -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):
......
# Copyright (c) 2012 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 imp
import inspect
import logging
import os
......@@ -146,7 +145,6 @@ def GetBuildDirectories():
for build_type in build_types:
yield build_dir, build_type
def FindSupportBinary(binary_name, executable=True):
"""Returns the path to the given binary name."""
# TODO(tonyg/dtu): This should support finding binaries in cloud storage.
......@@ -166,23 +164,3 @@ def FindSupportBinary(binary_name, executable=True):
command_mtime = candidate_mtime
return command
def FindSupportModule(module_name):
"""Like FindSupportBinary but uses imp.find_module to find a Python module."""
module = None
module_mtime = 0
chrome_root = GetChromiumSrcDir()
for build_dir, build_type in GetBuildDirectories():
path = os.path.join(chrome_root, build_dir, build_type)
try:
candidate = imp.find_module(module_name, [path])
except ImportError:
continue
candidate_mtime = os.stat(candidate[1]).st_mtime
if candidate_mtime > module_mtime:
module = candidate
module_mtime = candidate_mtime
return module
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment