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..dd04427e1cc0a86036916738c7ba0df21d47be32 |
| --- /dev/null |
| +++ b/chrome/test/functional/ispy/image_tools.py |
| @@ -0,0 +1,277 @@ |
| +# 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. |
| + |
| +"""Utilities for performing pixel-by-pixel image comparison.""" |
| + |
| + |
| +import itertools |
| +import math |
| +import PIL |
| + |
| + |
| +def _AreTheSameSize(images): |
| + """Returns if a set of images are the same size. |
|
craigdh
2013/06/14 19:08:40
It returns either way :)
Returns *whether* a set.
cgrimm
2013/06/17 16:24:58
Done.
|
| + |
| + Args: |
| + images: a list of images to compare |
|
craigdh
2013/06/14 19:08:40
Put blank line between Args/Returns/Raises.
cgrimm
2013/06/17 16:24:58
Done.
|
| + Returns: |
| + boolean |
| + Raises: |
| + Exception: One image or fewer is passed in. |
| + """ |
| + if len(images) > 1: |
| + return not any(images[0].size != img.size for img in images[1:]) |
| + else: |
| + raise Exception('No images passed in') |
| + |
| + |
| +def _GetColorDist(px1, px2): |
| + """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 |
| + """ |
| + return sum( |
| + (ch1-ch2)**2 |
| + for ch1, ch2 in itertools.izip(px1[0:3], px2[0:3]) |
| + ) / 195075. # 3*255**2 |
| + |
| + |
| +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(math.ceil(255*n)), |
| + int(math.ceil(255*n)), |
| + int(math.ceil(255*n))) |
| + |
| + |
| +def _Brightness(px): |
| + """Gets the brightness of a pixel. |
| + |
| + Returns the sum of a given pixel's color channels. |
| + |
| + Args: |
| + px: a 3-tuple (R,G,B) |
| + Returns: |
| + a number between 0 and 3*255 |
| + """ |
| + return sum(px[0:3]) |
|
craigdh
2013/06/14 19:08:40
Skip the [0:3], makes an unnecessary copy of the l
cgrimm
2013/06/17 16:24: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) |
| + """ |
| + if _Brightness(px1) < _Brightness(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) |
| + """ |
| + if _Brightness(px1) < _Brightness(px2): |
| + return px2 |
| + else: |
| + return px1 |
| + |
| + |
| +def CreateMask(images, threshold=True, cutoff=0): |
| + """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: |
| + images: the images to compute the mask from |
| + threshold: boolean, whether or not to threshold the mask |
| + cutoff: number, the value to threshold the mask at |
| + Returns: |
| + an image that is a mask of the passed in images. |
| + Raises: |
| + Exception: if the images passed in are not of the same size |
| + """ |
| + if not _AreTheSameSize(images): |
| + raise Exception('All images must be the same size') |
| + mask = PIL.Image.new('RGB', images[0].size) |
| + mask_data = mask.getdata() |
| + image_data = images[0].getdata() |
| + for other_image in images[1:]: |
| + mask_data = _ComputeLargestDifference(mask_data, image_data, |
| + other_image.getdata()) |
| + |
| + if threshold: |
| + mask.putdata([ |
| + (255, 255, 255) if _Brightness(px) > cutoff |
| + else (0, 0, 0) |
| + for px in mask_data |
| + ]) |
| + else: |
| + mask.putdata(list(mask_data)) |
| + return mask |
| + |
| + |
| +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 iter( |
|
craigdh
2013/06/14 19:08:40
You shouldn't need the iter
cgrimm
2013/06/17 16:24:58
Done.
|
| + _MaxPixel(m, _GetColorDistAsColor(i1, i2)) |
| + for m, i1, i2 in itertools.izip(mask_data, image_data1, image_data2) |
| + ) |
| + |
| + |
| +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. |
|
craigdh
2013/06/14 19:08:40
why not integers?
cgrimm
2013/06/17 16:24:58
Done.
|
| + """ |
| + 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: |
| + Exception: if the pixel is not (0,0,0) or (255,255,255) |
| + """ |
| + if px[0:3] == (0, 0, 0): |
| + return 1. |
| + if px[0:3] == (255, 255, 255): |
| + return 0. |
| + else: |
| + raise Exception('Mask may only contain black or white pixels') |
| + |
| + |
| +def TotalDifferentPixels(image1, image2, mask=None): |
| + """Computes the number of different pixels between two images. |
| + |
| + 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: |
| + the number of differing pixels between the images. |
| + Raises: |
| + Exception: if the images to be compared and mask are not the same size. |
| + """ |
| + if mask: |
| + if _AreTheSameSize([image1, image2, mask]): |
| + return sum( |
| + _MaskToValue(m)*_ThresholdColorDiff(px1, px2, 0) |
|
craigdh
2013/06/14 19:08:40
space before and after operators
cgrimm
2013/06/17 16:24:58
Done.
|
| + for m, px1, px2 in itertools.izip(mask.getdata(), |
| + image1.getdata(), |
| + image2.getdata()) |
| + ) |
| + else: |
| + raise Exception('images and mask must be the same size') |
| + else: |
| + if _AreTheSameSize([image1, image2]): |
| + return sum( |
| + _ThresholdColorDiff(px1, px2, 0) |
| + for px1, px2 in itertools.izip( |
| + image1.getdata(), |
| + image2.getdata() |
| + ) |
| + ) |
| + else: |
| + raise Exception('images and mask must be the same size') |
| + |
| + |
| +def SameImage(image1, image2, max_different_pixels=0, mask=None): |
| + """Returns a boolean representing if the images are the Same. |
|
craigdh
2013/06/14 19:08:40
if -> whether
Same -> same
cgrimm
2013/06/17 16:24:58
Done.
|
| + |
| + Returns a boolean relating to whether or not two images |
|
craigdh
2013/06/14 19:08:40
relating to -> indicating
cgrimm
2013/06/17 16:24:58
Done.
|
| + are similar enough to be considered the same. Essentially |
| + wraps the Similarity function and adds a max_different_pixels |
| + cutoff for Sameness. |
| + |
| + Args: |
| + image1: an Image to compare |
| + image2: an Image to compare |
| + max_different_pixels: 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. |
| + """ |
| + |
| + different_pixels = TotalDifferentPixels(image1, image2, mask) |
| + return different_pixels <= max_different_pixels |