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..e3f7232ae2bfb73a65bbc99a28158c4fa62c39ea |
--- /dev/null |
+++ b/chrome/test/functional/ispy/image_tools.py |
@@ -0,0 +1,294 @@ |
+# 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 whether a set of images are the same size. |
+ |
+ Args: |
+ images: a list of images to compare |
+ |
+ 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) |
+ |
+ |
+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 ( |
+ _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. |
+ """ |
+ 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) |
+ 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 whether the images are the same. |
+ |
+ Returns a boolean indicating whether two images |
+ are similar enough to be considered the same. Essentially |
+ wraps the Similarity function and adds a max_different_pixels |
+ cutoff for Sameness. |
frankf
2013/06/17 21:48:31
I don't thinkg Sameness has defined meaning. "Simi
cgrimm
2013/06/17 23:41:18
Done.
|
+ |
+ Args: |
+ image1: an Image to compare |
frankf
2013/06/17 21:48:31
Document how this is encoded
cgrimm
2013/06/17 23:41:18
Done.
|
+ image2: an Image to compare |
+ max_different_pixels: a number that is the cutoff for image sameness |
frankf
2013/06/17 21:48:31
Once you add this threshold, then this brute force
cgrimm
2013/06/17 23:41:18
resolved
|
+ mask: an optional Image that occludes parts of the images from |
+ same-ness calculation |
+ |
+ Returns: |
+ a boolean representing if the images are the same. |
frankf
2013/06/17 21:48:31
"True if the images are similar, false otherwise"
cgrimm
2013/06/17 23:41:18
Done.
|
+ |
+ Raises: |
+ Error: if the images (and mask) are different sizes. |
frankf
2013/06/17 21:48:31
Is Error a type of exception?
cgrimm
2013/06/17 23:41:18
This is a remnant from when I had a subclass of Ex
|
+ """ |
+ |
+ different_pixels = TotalDifferentPixels(image1, image2, mask) |
+ return different_pixels <= max_different_pixels |