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

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: 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..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
« 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