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

Unified Diff: chrome/test/functional/ispy/image_tools.py

Issue 16855010: Python Tools for Pixel-by-Pixel Image Comparison (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: minor changes: math.ceiling to prevent rounding errors, [0:3] subsections of lists. Created 7 years, 6 months 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: 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
« no previous file with comments | « no previous file | chrome/test/functional/ispy/image_tools_test.py » ('j') | chrome/test/functional/ispy/image_tools_test.py » ('J')

Powered by Google App Engine
This is Rietveld 408576698