Chromium Code Reviews| Index: chrome/test/functional/ispy/image_tools.py |
| diff --git a/chrome/test/functional/ispy/image_tools.py b/chrome/test/functional/ispy/image_tools.py |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..571cad5d6f9fecd734441b33059ec53ac78a3aad |
| --- /dev/null |
| +++ b/chrome/test/functional/ispy/image_tools.py |
| @@ -0,0 +1,295 @@ |
| +# Copyright (c) 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. |
| + |
| +"""A Module containing useful image tools for use by I Spy.""" |
|
craigdh
2013/06/13 19:28:31
"""Utilities for performing pixel-by-pixel image c
cgrimm
2013/06/14 18:43:58
Done.
|
| + |
| +__author__ = 'cgrimm@google.com (Chris Grimm)' |
|
craigdh
2013/06/13 19:28:31
We don't do this in chromium.
cgrimm
2013/06/14 18:43:58
Done.
|
| + |
| +from itertools import izip |
|
craigdh
2013/06/13 19:28:31
Prefer importing the module and then calling modul
cgrimm
2013/06/14 18:43:58
Done.
|
| +from PIL import Image |
|
craigdh
2013/06/13 19:28:31
ditto
cgrimm
2013/06/14 18:43:58
Done.
|
| + |
| + |
| +class Error(Exception): |
|
craigdh
2013/06/13 19:28:31
This adds little additional meaning. Just use Exce
cgrimm
2013/06/14 18:43:58
Done.
|
| + pass |
| + |
| + |
| +def AreTheSameSize(*images): |
|
craigdh
2013/06/13 19:28:31
Just pass the list by value. The contents of the l
cgrimm
2013/06/14 18:43:58
Done.
|
| + """Returns if a set of images are the same size. |
| + |
| + Args: |
| + *images: a list of images to compare |
| + Returns: |
| + boolean |
| + Raises: |
| + Error: One image or fewer is passed in. |
| + """ |
| + if len(images) > 1: |
| + i1 = images[0] |
| + for i2 in images: |
| + if i1.size == i2.size: |
|
craigdh
2013/06/13 19:28:31
No need for the pass, just do:
if i1.size != i2.s
cgrimm
2013/06/14 18:43:58
Done.
|
| + pass |
| + else: |
| + return False |
| + return True |
|
craigdh
2013/06/13 19:28:31
Actually, you can probably do this whole function
cgrimm
2013/06/14 18:43:58
Done.
|
| + else: |
| + raise Error('No images passed in') |
| + |
| + |
| +def GetColorDist(px1, px2): |
|
craigdh
2013/06/13 19:28:31
Functions local to the module should be prefixed w
cgrimm
2013/06/14 18:43:58
Done.
|
| + """Returns the normalized color distance between pixels. |
| + |
| + This function gets the color distance between two pixels and |
| + returns a number between 0 and 1 where 0 is lowest distance |
| + and 1 is the greatest distance. |
| + |
| + Args: |
| + px1: a 3-tuple (R,G,B) |
| + px2: a 3-tuple (R,G,B) |
| + Returns: |
| + a number between 0 and 1 |
| + """ |
| + dist = 0. |
| + for i in range(3): |
| + dist += (px1[i]-px2[i])**2 |
|
craigdh
2013/06/13 19:28:31
Use sum() for efficiency as this is in the critica
cgrimm
2013/06/14 18:43:58
Done.
|
| + return dist/(3*255**2) |
|
craigdh
2013/06/13 19:28:31
Does python calculate this constant every time? I'
cgrimm
2013/06/14 18:43:58
Done.
|
| + |
| + |
| +def GetColorDistAsColor(px1, px2): |
| + """Computes Color Distance as a greyscale pixel. |
| + |
| + This function gets the color distance between two pixels |
| + represented as a pixel ranging in color from (0,0,0) to |
| + (255,255,255). |
| + |
| + Args: |
| + px1: a 3-tuple (R,G,B) |
| + px2: a 3-tuple (R,G,B) |
| + Returns: |
| + a 3-tuple between (0,0,0) and (255,255,255) |
| + """ |
| + n = GetColorDist(px1, px2) |
| + return (int(255*n), int(255*n), int(255*n)) |
| + |
| + |
| +def Brightness(px): |
| + """gets the brightness of a pixel. |
|
craigdh
2013/06/13 19:28:31
g -> G
cgrimm
2013/06/14 18:43:58
Done.
|
| + |
| + returns the sum of a given pixel's color channels. |
|
craigdh
2013/06/13 19:28:31
r -> R
cgrimm
2013/06/14 18:43:58
Done.
|
| + |
| + Args: |
| + px: a 3-tuple (R,G,B) |
| + Returns: |
| + a number between 0 and 3*255 |
| + """ |
| + return px[0]+px[1]+px[2] |
|
craigdh
2013/06/13 19:28:31
Just sum(px)
cgrimm
2013/06/14 18:43:58
Done.
|
| + |
| + |
| +def MinPixel(px1, px2): |
| + """gets the pixel with the lower brightness. |
| + |
| + Calculates the brightness of each pixel and returns |
| + the pixel with the lower brightness. |
| + |
| + Args: |
| + px1: a 3-tuple (R,G,B) |
| + px2: a 3-tuple (R,G,B) |
| + Returns: |
| + a 3-tuple (R,G,B) |
| + """ |
| + average_px1 = Brightness(px1) |
|
craigdh
2013/06/13 19:28:31
no need for the intermediate variables here.
cgrimm
2013/06/14 18:43:58
Done.
|
| + average_px2 = Brightness(px2) |
| + if average_px1 < average_px2: |
| + return px1 |
| + else: |
| + return px2 |
| + |
| + |
| +def MaxPixel(px1, px2): |
| + """gets the pixel with the greater brightness. |
| + |
| + Calculates the brightness of each pixel and returns |
| + the pixel with the greater brightness. |
| + |
| + Args: |
| + px1: a 3-tuple (R,G,B) |
| + px2: a 3-tuple (R,G,B) |
| + Returns: |
| + a 3-tuple (R,G,B) |
| + """ |
| + average_px1 = Brightness(px1) |
|
craigdh
2013/06/13 19:28:31
ditto
cgrimm
2013/06/14 18:43:58
Done.
|
| + average_px2 = Brightness(px2) |
| + if average_px1 < average_px2: |
| + return px2 |
| + else: |
| + return px1 |
| + |
| + |
| +def CreateMask(is_hard, cutoff, *images): |
|
craigdh
2013/06/13 19:28:31
No need for the *, as mentioned earlier.
cgrimm
2013/06/14 18:43:58
Done.
|
| + """Computes a mask for a set of images. |
| + |
| + Returns an image that is computed from the images |
| + passed in. The mask's values are computed by calculating |
| + total differences between the image, and storing them |
| + such that (255,255,255) represents great difference, |
| + and (0,0,0) represents no difference. If is_hard is |
| + True, the resulting mask is put through a |
| + thresholding pass where pixel values below the |
| + cutoff become (0,0,0) and ones above become (255,255,255). |
| + |
| + Args: |
| + is_hard: boolean, whether or not to threshold the mask |
|
craigdh
2013/06/13 19:28:31
is_hard -> threshold
Also, I would pass images fi
cgrimm
2013/06/14 18:43:58
Done.
|
| + cutoff: number, the value to threshold the mask at |
| + *images: the images to compute the mask from |
| + Returns: |
| + an image that is a mask of the passed in images. |
| + Raises: |
| + Error: if the images passed in are not of the same size |
| + """ |
| + if AreTheSameSize(*images): |
|
craigdh
2013/06/13 19:28:31
Instead of indenting the whole function, just do t
cgrimm
2013/06/14 18:43:58
Done.
|
| + mask = Image.new('RGB', images[0].size) |
| + mask_data = list(mask.getdata()) |
|
craigdh
2013/06/13 19:28:31
The data structure returned is already iterable, i
cgrimm
2013/06/14 18:43:58
Done.
|
| + data_set = [list(x.getdata()) for x in images] |
|
craigdh
2013/06/13 19:28:31
data_set is unnecessary. Just pass images[0].getda
cgrimm
2013/06/14 18:43:58
Done.
|
| + i1 = data_set[0] |
|
craigdh
2013/06/13 19:28:31
more descriptive variable names than i1, i2
cgrimm
2013/06/14 18:43:58
Done.
|
| + for i2 in data_set[1:]: |
| + mask_data = ComputeLargestDifference(mask_data, i1, i2) |
| + |
| + if is_hard: |
| + mask_data = [ |
| + (255, 255, 255) if Brightness(x) > cutoff |
| + else (0, 0, 0) |
| + for x in mask_data |
| + ] |
|
craigdh
2013/06/13 19:28:31
Instead of constructing a list, just create an ite
cgrimm
2013/06/14 18:43:58
putdata requires a sequence not an iterator.
|
| + |
| + mask.putdata(mask_data) |
| + return mask |
| + else: |
| + raise Error('All Images must be the same size') |
| + |
| + |
| +def ComputeLargestDifference(mask_data, image_data1, image_data2): |
| + """Modifies a mask based upon a comparision of two images. |
| + |
| + A helper function used to generate masks. The mask, as |
| + it exists when the function is called, is compared to two |
| + images on a pixel-by-pixel basis. For each pixel, the |
| + ColorDistance between the two images is computed as a Color, |
| + and compared against the color of the mask at the same |
| + pixel. The brightest of these pixels is chosen for pixel |
| + in the images/mask, and a new set of pixels is returned. |
| + |
| + Args: |
| + mask_data: a list of pixels of the mask |
| + image_data1: a list of pixels of an image |
| + image_data2: a list of pixels of another image |
| + Returns: |
| + a list of pixels representing a modified version of the mask. |
| + """ |
| + return [ |
|
craigdh
2013/06/13 19:28:31
Should be able to create an iterator instead of a
cgrimm
2013/06/14 18:43:58
Done.
|
| + MaxPixel(m, GetColorDistAsColor(i1, i2)) |
| + for m, i1, i2 in izip(mask_data, image_data1, image_data2) |
| + ] |
|
craigdh
2013/06/13 19:28:31
Also, this indentation doesn't look like chromium
cgrimm
2013/06/14 18:43:58
Done.
|
| + |
| + |
| +def ThresholdColorDiff(px1, px2, cutoff): |
| + """Thresholds the color distance of two pixels. |
| + |
| + Computes a Threshold of the color distance of two pixels, |
| + if the normalized color distance of px1 and px2 is greater than |
| + cutoff, returns 1. otherwise returns 0. |
| + |
| + Args: |
| + px1: a 3-tuple (R,G,B) |
| + px2: a 3-tuple (R,G,B) |
| + cutoff: a number used as the threshold limit |
| + Returns: |
| + either 1. or 0. depending on the distance of px1 and px2. |
| + """ |
| + diff = GetColorDist(px1, px2) |
| + if diff > cutoff: |
| + return 1. |
| + else: |
| + return 0. |
| + |
| + |
| +def MaskToValue(px): |
| + """Converts a black or white pixel to 1. or 0. |
| + |
| + Args: |
| + px: a 3-tuple (R,G,B) |
| + Returns: |
| + 1. if the pixel is (0,0,0), 0. if the pixel is (255,255,255) |
| + Raises: |
| + Error: if the pixel is not (0,0,0) or (255,255,255) |
| + """ |
| + if px == (0, 0, 0): |
| + return 1. |
| + if px == (255, 255, 255): |
| + return 0. |
| + else: |
| + raise Error('Mask may only contain black or white pixels') |
| + |
| + |
| +def Similarity(image1, image2, mask=None): |
| + """computes the similarity of two images. |
| + |
| + Computes the similarity of two images taking into account |
| + an optional mask. the similarity is returned as a number |
| + between 0 and 1, where 1 is the same image, and 0 is a |
|
craigdh
2013/06/13 19:28:31
We're doing pixel-by-pixel comparisons, so this wo
cgrimm
2013/06/14 18:43:58
Done.
|
| + dramatically different image. |
| + |
| + Args: |
| + image1: the first Image to be compared |
| + image2: the second Image to be compared |
| + mask: an optional mask to occlude parts of the images |
| + from calculation |
| + Returns: |
| + a number representing the similarity of the images. |
| + Raises: |
| + Error: if the images to be compared and mask are not the same size. |
| + """ |
| + if mask: |
| + if AreTheSameSize(image1, image2, mask): |
| + return 1. - sum([ |
| + MaskToValue(m)*ThresholdColorDiff(px1, px2, 0.001) |
| + for m, px1, px2 in izip(mask.getdata(), |
| + image1.getdata(), |
| + image2.getdata()) |
| + ]) / (image1.size[0]*image1.size[1]) |
| + else: |
| + raise Error('images and mask must be the same size') |
| + else: |
| + if AreTheSameSize(image1, image2): |
| + return 1. - sum([ |
| + ThresholdColorDiff(px1, px2, 0.001) |
| + for px1, px2 in izip(image1.getdata(), image2.getdata()) |
| + ]) / (image1.size[0]*image1.size[1]) |
| + else: |
| + raise Error('images must be the same size') |
| + |
| + |
| +def SameImage(image1, image2, certainty, mask=None): |
| + """Returns a boolean representing if the images are the Same. |
| + |
| + Returns a boolean relating to whether or not two images |
| + are similar enough to be considered the same. Essentially |
| + wraps the Similarity function and adds a certainty |
| + parameter: a number between 0 and 1 that represents the |
|
craigdh
2013/06/13 19:28:31
As above, take a difference in pixels for the cuto
cgrimm
2013/06/14 18:43:58
Done.
|
| + cutoff in Similarity for Sameness. |
| + |
| + Args: |
| + image1: an Image to compare |
| + image2: an Image to compare |
| + certainty: a number that is the cutoff for image sameness |
| + mask: an optional Image that occludes parts of the images from |
| + same-ness calculation |
| + Returns: |
| + a boolean representing if the images are the same. |
| + Raises: |
| + Error: if the images (and mask) are different sizes. |
| + """ |
| + similar = Similarity(image1, image2, mask) |
| + if similar >= certainty: |
|
craigdh
2013/06/13 19:28:31
just:
return similar >= certainty
cgrimm
2013/06/14 18:43:58
Done.
|
| + return True |
| + else: |
| + return False |