Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(167)

Unified Diff: tools/telemetry/telemetry/core/bitmap.py

Issue 668753002: [Telemetry] Migrate bitmap.py from bitmaptools.cc to numpy (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: rebase Created 6 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: tools/telemetry/telemetry/core/bitmap.py
diff --git a/tools/telemetry/telemetry/core/bitmap.py b/tools/telemetry/telemetry/core/bitmap.py
deleted file mode 100644
index ead38b1c97c7b0dbc8996b0c2fd051a0f1ab0057..0000000000000000000000000000000000000000
--- a/tools/telemetry/telemetry/core/bitmap.py
+++ /dev/null
@@ -1,353 +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 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 collections
-import struct
-import subprocess
-
-from telemetry.core import util
-from telemetry.core import platform
-from telemetry.util import support_binaries
-
-util.AddDirToPythonPath(util.GetTelemetryDir(), 'third_party', 'png')
-import png # pylint: disable=F0401
-
-
-def HistogramDistance(hist1, hist2):
- """Earth mover's distance.
-
- http://en.wikipedia.org/wiki/Earth_mover's_distance
- First, normalize the two histograms. Then, treat the two histograms as
- piles of dirt, and calculate the cost of turning one pile into the other.
-
- To do this, calculate the difference in one bucket between the two
- histograms. Then carry it over in the calculation for the next bucket.
- In this way, the difference is weighted by how far it has to move."""
- if len(hist1) != len(hist2):
- raise ValueError('Trying to compare histograms '
- 'of different sizes, %s != %s' % (len(hist1), len(hist2)))
-
- n1 = sum(hist1)
- n2 = sum(hist2)
- if n1 == 0:
- raise ValueError('First histogram has 0 pixels in it.')
- if n2 == 0:
- raise ValueError('Second histogram has 0 pixels in it.')
-
- total = 0
- remainder = 0
- for value1, value2 in zip(hist1, hist2):
- remainder += value1 * n2 - value2 * n1
- total += abs(remainder)
- assert remainder == 0, (
- '%s pixel(s) left over after computing histogram distance.'
- % abs(remainder))
- return abs(float(total) / n1 / n2)
-
-
-class ColorHistogram(
- collections.namedtuple('ColorHistogram', ['r', 'g', 'b', 'default_color'])):
- # pylint: disable=W0232
- # pylint: disable=E1002
-
- def __new__(cls, r, g, b, default_color=None):
- return super(ColorHistogram, cls).__new__(cls, r, g, b, default_color)
-
- def Distance(self, other):
- total = 0
- for i in xrange(3):
- hist1 = self[i]
- hist2 = other[i]
-
- if sum(self[i]) == 0:
- if not self.default_color:
- raise ValueError('Histogram has no data and no default color.')
- hist1 = [0] * 256
- hist1[self.default_color[i]] = 1
- if sum(other[i]) == 0:
- if not other.default_color:
- raise ValueError('Histogram has no data and no default color.')
- hist2 = [0] * 256
- hist2[other.default_color[i]] = 1
-
- total += HistogramDistance(hist1, hist2)
- return total
-
-
-class RgbaColor(collections.namedtuple('RgbaColor', ['r', 'g', 'b', 'a'])):
- """Encapsulates an RGBA color retreived from a Bitmap"""
- # pylint: disable=W0232
- # pylint: disable=E1002
-
- def __new__(cls, r, g, b, a=255):
- return super(RgbaColor, cls).__new__(cls, r, g, b, a)
-
- def __int__(self):
- return (self.r << 16) | (self.g << 8) | self.b
-
- def IsEqual(self, expected_color, tolerance=0):
- """Verifies that the color is within a given tolerance of
- the expected color"""
- r_diff = abs(self.r - expected_color.r)
- g_diff = abs(self.g - expected_color.g)
- b_diff = abs(self.b - expected_color.b)
- a_diff = abs(self.a - expected_color.a)
- return (r_diff <= tolerance and g_diff <= tolerance
- and b_diff <= tolerance and a_diff <= tolerance)
-
- def AssertIsRGB(self, r, g, b, tolerance=0):
- assert self.IsEqual(RgbaColor(r, g, b), tolerance)
-
- def AssertIsRGBA(self, r, g, b, a, tolerance=0):
- assert self.IsEqual(RgbaColor(r, g, b, a), tolerance)
-
-
-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):
- binary = support_binaries.FindPath(
- 'bitmaptools',
- platform.GetHostPlatform().GetArchName(),
- platform.GetHostPlatform().GetOSName())
- 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_int = -1 if ignore_color is None else int(ignore_color)
- response = self._RunCommand(_BitmapTools.HISTOGRAM,
- ignore_color_int, tolerance)
- out = array.array('i')
- out.fromstring(response)
- assert len(out) == 768, (
- 'The ColorHistogram has the wrong number of buckets: %s' % len(out))
- return ColorHistogram(out[:256], out[256:512], out[512:], ignore_color)
-
- 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."""
-
- def __init__(self, bpp, width, height, pixels, metadata=None):
- assert bpp in [3, 4], 'Invalid bytes per pixel'
- assert width > 0, 'Invalid width'
- assert height > 0, 'Invalid height'
- assert pixels, 'Must specify pixels'
- assert bpp * width * height == len(pixels), 'Dimensions and pixels mismatch'
-
- self._bpp = bpp
- self._width = width
- self._height = height
- self._pixels = pixels
- self._metadata = metadata or {}
- self._crop_box = None
-
- @property
- def bpp(self):
- """Bytes per pixel."""
- return self._bpp
-
- @property
- def width(self):
- """Width of the bitmap."""
- return self._crop_box[2] if self._crop_box else self._width
-
- @property
- def height(self):
- """Height of the bitmap."""
- 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()
- # pylint: disable=unpacking-non-sequence
- _, _, 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
-
- @property
- def metadata(self):
- self._metadata['size'] = (self.width, self.height)
- self._metadata['alpha'] = self.bpp == 4
- self._metadata['bitdepth'] = 8
- return self._metadata
-
- 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])
-
- def WritePngFile(self, path):
- with open(path, "wb") as f:
- png.Writer(**self.metadata).write_array(f, self.pixels)
-
- @staticmethod
- def FromPng(png_data):
- width, height, pixels, meta = png.Reader(bytes=png_data).read_flat()
- return Bitmap(4 if meta['alpha'] else 3, width, height, pixels, meta)
-
- @staticmethod
- def FromPngFile(path):
- with open(path, "rb") as f:
- return Bitmap.FromPng(f.read())
-
- @staticmethod
- def FromBase64Png(base64_png):
- return Bitmap.FromPng(base64.b64decode(base64_png))
-
- def IsEqual(self, other, tolerance=0):
- """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
- and another Bitmap."""
-
- # Output dimensions will be the maximum of the two input dimensions
- out_width = max(self.width, other.width)
- out_height = max(self.height, other.height)
-
- diff = [[0 for x in xrange(out_width * 3)] for x in xrange(out_height)]
-
- # Loop over each pixel and write out the difference
- for y in range(out_height):
- for x in range(out_width):
- if x < self.width and y < self.height:
- c0 = self.GetPixelColor(x, y)
- else:
- c0 = RgbaColor(0, 0, 0, 0)
-
- if x < other.width and y < other.height:
- c1 = other.GetPixelColor(x, y)
- else:
- c1 = RgbaColor(0, 0, 0, 0)
-
- offset = x * 3
- diff[y][offset] = abs(c0.r - c1.r)
- diff[y][offset+1] = abs(c0.g - c1.g)
- diff[y][offset+2] = abs(c0.b - c1.b)
-
- # 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
- diff_img = png.from_array(diff, mode='RGB')
- output = cStringIO.StringIO()
- try:
- diff_img.save(output)
- diff = Bitmap.FromPng(output.getvalue())
- finally:
- output.close()
-
- 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."""
- return self._PrepareTools().BoundingBox(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
-
- if (left < 0 or top < 0 or
- (left + width) > cur_width or
- (top + height) > cur_height):
- raise ValueError('Invalid dimensions')
-
- self._crop_box = cur_left + left, cur_top + top, width, height
- return self
-
- def ColorHistogram(self, ignore_color=None, 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 ColorHistogram namedtuple with 256 integers in each field: r, g, and b.
- """
- return self._PrepareTools().Histogram(ignore_color, tolerance)

Powered by Google App Engine
This is Rietveld 408576698