1a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)# Copyright 2013 The Chromium Authors. All rights reserved. 2a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)# Use of this source code is governed by a BSD-style license that can be 3a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)# found in the LICENSE file. 45d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 55d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)""" 65d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)Bitmap is a basic wrapper for image pixels. It includes some basic processing 75d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)tools: crop, find bounding box of a color and compute histogram of color values. 85d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)""" 95d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 105d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)import array 11a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)import base64 12a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)import cStringIO 13a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)import collections 145d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)import struct 155d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)import subprocess 16a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 17a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)from telemetry.core import util 18cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)from telemetry.core import platform 190529e5d033099cbfc42635f6f6183833b09dff6eBen Murdochfrom telemetry.util import support_binaries 20a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 21a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)util.AddDirToPythonPath(util.GetTelemetryDir(), 'third_party', 'png') 22a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)import png # pylint: disable=F0401 23a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 24a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 25a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)def HistogramDistance(hist1, hist2): 26a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) """Earth mover's distance. 27a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) 28a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) http://en.wikipedia.org/wiki/Earth_mover's_distance 29a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) First, normalize the two histograms. Then, treat the two histograms as 30a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) piles of dirt, and calculate the cost of turning one pile into the other. 31a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) 32a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) To do this, calculate the difference in one bucket between the two 33a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) histograms. Then carry it over in the calculation for the next bucket. 34a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) In this way, the difference is weighted by how far it has to move.""" 35a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) if len(hist1) != len(hist2): 36a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) raise ValueError('Trying to compare histograms ' 37a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) 'of different sizes, %s != %s' % (len(hist1), len(hist2))) 38a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) 39a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) n1 = sum(hist1) 40a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) n2 = sum(hist2) 41a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) if n1 == 0: 42a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) raise ValueError('First histogram has 0 pixels in it.') 43a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) if n2 == 0: 44a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) raise ValueError('Second histogram has 0 pixels in it.') 45a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) 46a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) total = 0 47a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) remainder = 0 48a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) for value1, value2 in zip(hist1, hist2): 49a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) remainder += value1 * n2 - value2 * n1 50a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) total += abs(remainder) 51a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) assert remainder == 0, ( 52a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) '%s pixel(s) left over after computing histogram distance.' 53a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) % abs(remainder)) 54a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) return abs(float(total) / n1 / n2) 55a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) 56a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) 57a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)class ColorHistogram( 58a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) collections.namedtuple('ColorHistogram', ['r', 'g', 'b', 'default_color'])): 59a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) # pylint: disable=W0232 60a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) # pylint: disable=E1002 61a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) 62a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) def __new__(cls, r, g, b, default_color=None): 63a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) return super(ColorHistogram, cls).__new__(cls, r, g, b, default_color) 64a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) 65a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) def Distance(self, other): 66a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) total = 0 67a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) for i in xrange(3): 68a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) hist1 = self[i] 69a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) hist2 = other[i] 70a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) 71a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) if sum(self[i]) == 0: 72a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) if not self.default_color: 73a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) raise ValueError('Histogram has no data and no default color.') 74a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) hist1 = [0] * 256 75a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) hist1[self.default_color[i]] = 1 76a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) if sum(other[i]) == 0: 77a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) if not other.default_color: 78a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) raise ValueError('Histogram has no data and no default color.') 79a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) hist2 = [0] * 256 80a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) hist2[other.default_color[i]] = 1 81a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) 82a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) total += HistogramDistance(hist1, hist2) 83a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) return total 84a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) 85a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) 86a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)class RgbaColor(collections.namedtuple('RgbaColor', ['r', 'g', 'b', 'a'])): 87a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) """Encapsulates an RGBA color retreived from a Bitmap""" 88a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) # pylint: disable=W0232 89a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) # pylint: disable=E1002 90a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 91a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) def __new__(cls, r, g, b, a=255): 92a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) return super(RgbaColor, cls).__new__(cls, r, g, b, a) 93a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 945d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) def __int__(self): 955d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) return (self.r << 16) | (self.g << 8) | self.b 965d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 97a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) def IsEqual(self, expected_color, tolerance=0): 98a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) """Verifies that the color is within a given tolerance of 99a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) the expected color""" 100a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) r_diff = abs(self.r - expected_color.r) 101a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) g_diff = abs(self.g - expected_color.g) 102a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) b_diff = abs(self.b - expected_color.b) 103a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) a_diff = abs(self.a - expected_color.a) 104a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) return (r_diff <= tolerance and g_diff <= tolerance 105a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) and b_diff <= tolerance and a_diff <= tolerance) 106a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 107a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) def AssertIsRGB(self, r, g, b, tolerance=0): 108a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) assert self.IsEqual(RgbaColor(r, g, b), tolerance) 109a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 110a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) def AssertIsRGBA(self, r, g, b, a, tolerance=0): 111a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) assert self.IsEqual(RgbaColor(r, g, b, a), tolerance) 112a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 113a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 1145d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)WEB_PAGE_TEST_ORANGE = RgbaColor(222, 100, 13) 1155d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)WHITE = RgbaColor(255, 255, 255) 1165d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 1175d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 1185d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)class _BitmapTools(object): 1195d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) """Wraps a child process of bitmaptools and allows for one command.""" 1205d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) CROP_PIXELS = 0 1215d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) HISTOGRAM = 1 1225d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) BOUNDING_BOX = 2 1235d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 1245d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) def __init__(self, dimensions, pixels): 1250529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch binary = support_binaries.FindPath( 126cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles) 'bitmaptools', platform.GetHostPlatform().GetOSName()) 1275d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) assert binary, 'You must build bitmaptools first!' 1285d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 1295d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) self._popen = subprocess.Popen([binary], 1305d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) stdin=subprocess.PIPE, 1315d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) stdout=subprocess.PIPE, 1325d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) stderr=subprocess.PIPE) 1335d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 1345d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) # dimensions are: bpp, width, height, boxleft, boxtop, boxwidth, boxheight 1355d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) packed_dims = struct.pack('iiiiiii', *dimensions) 1365d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) self._popen.stdin.write(packed_dims) 1375d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) # If we got a list of ints, we need to convert it into a byte buffer. 1385d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) if type(pixels) is not bytearray: 1395d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) pixels = bytearray(pixels) 1405d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) self._popen.stdin.write(pixels) 1415d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 1425d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) def _RunCommand(self, *command): 1435d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) assert not self._popen.stdin.closed, ( 1445d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 'Exactly one command allowed per instance of tools.') 1455d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) packed_command = struct.pack('i' * len(command), *command) 1465d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) self._popen.stdin.write(packed_command) 1475d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) self._popen.stdin.close() 1485d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) length_packed = self._popen.stdout.read(struct.calcsize('i')) 1495d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) if not length_packed: 1505d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) raise Exception(self._popen.stderr.read()) 1515d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) length = struct.unpack('i', length_packed)[0] 1525d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) return self._popen.stdout.read(length) 1535d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 1545d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) def CropPixels(self): 1555d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) return self._RunCommand(_BitmapTools.CROP_PIXELS) 1565d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 1575d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) def Histogram(self, ignore_color, tolerance): 158a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) ignore_color_int = -1 if ignore_color is None else int(ignore_color) 159a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) response = self._RunCommand(_BitmapTools.HISTOGRAM, 160a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) ignore_color_int, tolerance) 1615d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) out = array.array('i') 1625d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) out.fromstring(response) 163a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) assert len(out) == 768, ( 164a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) 'The ColorHistogram has the wrong number of buckets: %s' % len(out)) 165a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) return ColorHistogram(out[:256], out[256:512], out[512:], ignore_color) 1665d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 1675d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) def BoundingBox(self, color, tolerance): 1685d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) response = self._RunCommand(_BitmapTools.BOUNDING_BOX, int(color), 1695d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) tolerance) 1705d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) unpacked = struct.unpack('iiiii', response) 1715d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) box, count = unpacked[:4], unpacked[-1] 1725d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) if box[2] < 0 or box[3] < 0: 1735d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) box = None 1745d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) return box, count 1755d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 1765d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 177a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)class Bitmap(object): 178a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) """Utilities for parsing and inspecting a bitmap.""" 179a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 180a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) def __init__(self, bpp, width, height, pixels, metadata=None): 181a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) assert bpp in [3, 4], 'Invalid bytes per pixel' 182a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) assert width > 0, 'Invalid width' 183a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) assert height > 0, 'Invalid height' 184a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) assert pixels, 'Must specify pixels' 185a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) assert bpp * width * height == len(pixels), 'Dimensions and pixels mismatch' 186a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 187a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) self._bpp = bpp 188a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) self._width = width 189a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) self._height = height 190a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) self._pixels = pixels 1915d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) self._metadata = metadata or {} 1925d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) self._crop_box = None 1935d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 1945d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) @property 1955d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) def bpp(self): 1965d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) """Bytes per pixel.""" 1975d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) return self._bpp 198a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 199a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) @property 200a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) def width(self): 2015d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) """Width of the bitmap.""" 2025d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) return self._crop_box[2] if self._crop_box else self._width 203a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 204a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) @property 205a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) def height(self): 2065d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) """Height of the bitmap.""" 2075d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) return self._crop_box[3] if self._crop_box else self._height 2085d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 2095d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) def _PrepareTools(self): 2105d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) """Prepares an instance of _BitmapTools which allows exactly one command. 2115d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) """ 2125d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) crop_box = self._crop_box or (0, 0, self._width, self._height) 2135d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) return _BitmapTools((self._bpp, self._width, self._height) + crop_box, 2145d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) self._pixels) 2155d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 2165d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) @property 2175d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) def pixels(self): 2185d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) """Flat pixel array of the bitmap.""" 2195d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) if self._crop_box: 2205d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) self._pixels = self._PrepareTools().CropPixels() 2215d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) _, _, self._width, self._height = self._crop_box 2225d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) self._crop_box = None 2235d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) if type(self._pixels) is not bytearray: 2245d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) self._pixels = bytearray(self._pixels) 2255d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) return self._pixels 2265d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 2275d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) @property 2285d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) def metadata(self): 2295d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) self._metadata['size'] = (self.width, self.height) 2305d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) self._metadata['alpha'] = self.bpp == 4 2315d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) self._metadata['bitdepth'] = 8 2325d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) return self._metadata 233a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 234a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) def GetPixelColor(self, x, y): 2355d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) """Returns a RgbaColor for the pixel at (x, y).""" 2365d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) pixels = self.pixels 237a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) base = self._bpp * (y * self._width + x) 238a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) if self._bpp == 4: 2395d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) return RgbaColor(pixels[base + 0], pixels[base + 1], 2405d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) pixels[base + 2], pixels[base + 3]) 2415d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) return RgbaColor(pixels[base + 0], pixels[base + 1], 2425d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) pixels[base + 2]) 243a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 244a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) def WritePngFile(self, path): 245a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) with open(path, "wb") as f: 2465d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) png.Writer(**self.metadata).write_array(f, self.pixels) 247a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 248a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) @staticmethod 249a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) def FromPng(png_data): 250a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) width, height, pixels, meta = png.Reader(bytes=png_data).read_flat() 251a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) return Bitmap(4 if meta['alpha'] else 3, width, height, pixels, meta) 252a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 253a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) @staticmethod 254a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) def FromPngFile(path): 255a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) with open(path, "rb") as f: 256a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) return Bitmap.FromPng(f.read()) 257a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 258a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) @staticmethod 259a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) def FromBase64Png(base64_png): 260a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) return Bitmap.FromPng(base64.b64decode(base64_png)) 261a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 262a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) def IsEqual(self, other, tolerance=0): 2635d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) """Determines whether two Bitmaps are identical within a given tolerance.""" 264a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 265a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) # Dimensions must be equal 266a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) if self.width != other.width or self.height != other.height: 267a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) return False 268a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 269a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) # Loop over each pixel and test for equality 2705d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) if tolerance or self.bpp != other.bpp: 271a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) for y in range(self.height): 272a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) for x in range(self.width): 273a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) c0 = self.GetPixelColor(x, y) 274a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) c1 = other.GetPixelColor(x, y) 275a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) if not c0.IsEqual(c1, tolerance): 276a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) return False 277a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) else: 2785d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) return self.pixels == other.pixels 279a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 280a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) return True 281a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 282a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) def Diff(self, other): 283a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) """Returns a new Bitmap that represents the difference between this image 284a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) and another Bitmap.""" 285a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 286a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) # Output dimensions will be the maximum of the two input dimensions 287a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) out_width = max(self.width, other.width) 288a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) out_height = max(self.height, other.height) 289a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 290a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) diff = [[0 for x in xrange(out_width * 3)] for x in xrange(out_height)] 291a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 292a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) # Loop over each pixel and write out the difference 293a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) for y in range(out_height): 294a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) for x in range(out_width): 295a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) if x < self.width and y < self.height: 296a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) c0 = self.GetPixelColor(x, y) 297a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) else: 298a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) c0 = RgbaColor(0, 0, 0, 0) 299a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 300a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) if x < other.width and y < other.height: 301a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) c1 = other.GetPixelColor(x, y) 302a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) else: 303a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) c1 = RgbaColor(0, 0, 0, 0) 304a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 305a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) offset = x * 3 306a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) diff[y][offset] = abs(c0.r - c1.r) 307a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) diff[y][offset+1] = abs(c0.g - c1.g) 308a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) diff[y][offset+2] = abs(c0.b - c1.b) 309a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 310a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) # This particular method can only save to a file, so the result will be 311a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) # written into an in-memory buffer and read back into a Bitmap 312a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) diff_img = png.from_array(diff, mode='RGB') 313a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) output = cStringIO.StringIO() 314a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) try: 315a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) diff_img.save(output) 316a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) diff = Bitmap.FromPng(output.getvalue()) 317a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) finally: 318a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) output.close() 319a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 320a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) return diff 321a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 3225d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) def GetBoundingBox(self, color, tolerance=0): 3235d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) """Finds the minimum box surrounding all occurences of |color|. 3245d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) Returns: (top, left, width, height), match_count 3255d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) Ignores the alpha channel.""" 3265d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) return self._PrepareTools().BoundingBox(color, tolerance) 3275d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 328a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) def Crop(self, left, top, width, height): 3295d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) """Crops the current bitmap down to the specified box.""" 3305d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) cur_box = self._crop_box or (0, 0, self._width, self._height) 3315d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) cur_left, cur_top, cur_width, cur_height = cur_box 332a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 333a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) if (left < 0 or top < 0 or 3345d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) (left + width) > cur_width or 3355d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) (top + height) > cur_height): 336a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) raise ValueError('Invalid dimensions') 337a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 3385d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) self._crop_box = cur_left + left, cur_top + top, width, height 3395d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) return self 340a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 3415d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) def ColorHistogram(self, ignore_color=None, tolerance=0): 3425d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) """Computes a histogram of the pixel colors in this Bitmap. 3435d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) Args: 3445d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) ignore_color: An RgbaColor to exclude from the bucket counts. 3455d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) tolerance: A tolerance for the ignore_color. 346a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 3475d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) Returns: 348a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) A ColorHistogram namedtuple with 256 integers in each field: r, g, and b. 3495d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) """ 3505d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) return self._PrepareTools().Histogram(ignore_color, tolerance) 351