Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 # Copyright (c) 2013 The Chromium Authors. All rights reserved. | |
| 2 # Use of this source code is governed by a BSD-style license that can be | |
| 3 # found in the LICENSE file. | |
| 4 | |
| 5 """Utilities for performing pixel-by-pixel image comparison.""" | |
| 6 | |
| 7 | |
| 8 import itertools | |
| 9 import math | |
| 10 import PIL | |
| 11 | |
| 12 | |
| 13 def _AreTheSameSize(images): | |
| 14 """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.
| |
| 15 | |
| 16 Args: | |
| 17 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.
| |
| 18 Returns: | |
| 19 boolean | |
| 20 Raises: | |
| 21 Exception: One image or fewer is passed in. | |
| 22 """ | |
| 23 if len(images) > 1: | |
| 24 return not any(images[0].size != img.size for img in images[1:]) | |
| 25 else: | |
| 26 raise Exception('No images passed in') | |
| 27 | |
| 28 | |
| 29 def _GetColorDist(px1, px2): | |
| 30 """Returns the normalized color distance between pixels. | |
| 31 | |
| 32 This function gets the color distance between two pixels and | |
| 33 returns a number between 0 and 1 where 0 is lowest distance | |
| 34 and 1 is the greatest distance. | |
| 35 | |
| 36 Args: | |
| 37 px1: a 3-tuple (R,G,B) | |
| 38 px2: a 3-tuple (R,G,B) | |
| 39 Returns: | |
| 40 a number between 0 and 1 | |
| 41 """ | |
| 42 return sum( | |
| 43 (ch1-ch2)**2 | |
| 44 for ch1, ch2 in itertools.izip(px1[0:3], px2[0:3]) | |
| 45 ) / 195075. # 3*255**2 | |
| 46 | |
| 47 | |
| 48 def _GetColorDistAsColor(px1, px2): | |
| 49 """Computes Color Distance as a greyscale pixel. | |
| 50 | |
| 51 This function gets the color distance between two pixels | |
| 52 represented as a pixel ranging in color from (0,0,0) to | |
| 53 (255,255,255). | |
| 54 | |
| 55 Args: | |
| 56 px1: a 3-tuple (R,G,B) | |
| 57 px2: a 3-tuple (R,G,B) | |
| 58 Returns: | |
| 59 a 3-tuple between (0,0,0) and (255,255,255) | |
| 60 """ | |
| 61 n = _GetColorDist(px1, px2) | |
| 62 return (int(math.ceil(255*n)), | |
| 63 int(math.ceil(255*n)), | |
| 64 int(math.ceil(255*n))) | |
| 65 | |
| 66 | |
| 67 def _Brightness(px): | |
| 68 """Gets the brightness of a pixel. | |
| 69 | |
| 70 Returns the sum of a given pixel's color channels. | |
| 71 | |
| 72 Args: | |
| 73 px: a 3-tuple (R,G,B) | |
| 74 Returns: | |
| 75 a number between 0 and 3*255 | |
| 76 """ | |
| 77 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.
| |
| 78 | |
| 79 | |
| 80 def _MinPixel(px1, px2): | |
| 81 """Gets the pixel with the lower brightness. | |
| 82 | |
| 83 Calculates the brightness of each pixel and returns | |
| 84 the pixel with the lower brightness. | |
| 85 | |
| 86 Args: | |
| 87 px1: a 3-tuple (R,G,B) | |
| 88 px2: a 3-tuple (R,G,B) | |
| 89 Returns: | |
| 90 a 3-tuple (R,G,B) | |
| 91 """ | |
| 92 if _Brightness(px1) < _Brightness(px2): | |
| 93 return px1 | |
| 94 else: | |
| 95 return px2 | |
| 96 | |
| 97 | |
| 98 def _MaxPixel(px1, px2): | |
| 99 """gets the pixel with the greater brightness. | |
| 100 | |
| 101 Calculates the brightness of each pixel and returns | |
| 102 the pixel with the greater brightness. | |
| 103 | |
| 104 Args: | |
| 105 px1: a 3-tuple (R,G,B) | |
| 106 px2: a 3-tuple (R,G,B) | |
| 107 Returns: | |
| 108 a 3-tuple (R,G,B) | |
| 109 """ | |
| 110 if _Brightness(px1) < _Brightness(px2): | |
| 111 return px2 | |
| 112 else: | |
| 113 return px1 | |
| 114 | |
| 115 | |
| 116 def CreateMask(images, threshold=True, cutoff=0): | |
| 117 """Computes a mask for a set of images. | |
| 118 | |
| 119 Returns an image that is computed from the images | |
| 120 passed in. The mask's values are computed by calculating | |
| 121 total differences between the image, and storing them | |
| 122 such that (255,255,255) represents great difference, | |
| 123 and (0,0,0) represents no difference. If is_hard is | |
| 124 True, the resulting mask is put through a | |
| 125 thresholding pass where pixel values below the | |
| 126 cutoff become (0,0,0) and ones above become (255,255,255). | |
| 127 | |
| 128 Args: | |
| 129 images: the images to compute the mask from | |
| 130 threshold: boolean, whether or not to threshold the mask | |
| 131 cutoff: number, the value to threshold the mask at | |
| 132 Returns: | |
| 133 an image that is a mask of the passed in images. | |
| 134 Raises: | |
| 135 Exception: if the images passed in are not of the same size | |
| 136 """ | |
| 137 if not _AreTheSameSize(images): | |
| 138 raise Exception('All images must be the same size') | |
| 139 mask = PIL.Image.new('RGB', images[0].size) | |
| 140 mask_data = mask.getdata() | |
| 141 image_data = images[0].getdata() | |
| 142 for other_image in images[1:]: | |
| 143 mask_data = _ComputeLargestDifference(mask_data, image_data, | |
| 144 other_image.getdata()) | |
| 145 | |
| 146 if threshold: | |
| 147 mask.putdata([ | |
| 148 (255, 255, 255) if _Brightness(px) > cutoff | |
| 149 else (0, 0, 0) | |
| 150 for px in mask_data | |
| 151 ]) | |
| 152 else: | |
| 153 mask.putdata(list(mask_data)) | |
| 154 return mask | |
| 155 | |
| 156 | |
| 157 def _ComputeLargestDifference(mask_data, image_data1, image_data2): | |
| 158 """Modifies a mask based upon a comparision of two images. | |
| 159 | |
| 160 A helper function used to generate masks. The mask, as | |
| 161 it exists when the function is called, is compared to two | |
| 162 images on a pixel-by-pixel basis. For each pixel, the | |
| 163 ColorDistance between the two images is computed as a Color, | |
| 164 and compared against the color of the mask at the same | |
| 165 pixel. The brightest of these pixels is chosen for pixel | |
| 166 in the images/mask, and a new set of pixels is returned. | |
| 167 | |
| 168 Args: | |
| 169 mask_data: a list of pixels of the mask | |
| 170 image_data1: a list of pixels of an image | |
| 171 image_data2: a list of pixels of another image | |
| 172 Returns: | |
| 173 a list of pixels representing a modified version of the mask. | |
| 174 """ | |
| 175 return iter( | |
|
craigdh
2013/06/14 19:08:40
You shouldn't need the iter
cgrimm
2013/06/17 16:24:58
Done.
| |
| 176 _MaxPixel(m, _GetColorDistAsColor(i1, i2)) | |
| 177 for m, i1, i2 in itertools.izip(mask_data, image_data1, image_data2) | |
| 178 ) | |
| 179 | |
| 180 | |
| 181 def _ThresholdColorDiff(px1, px2, cutoff): | |
| 182 """Thresholds the color distance of two pixels. | |
| 183 | |
| 184 Computes a Threshold of the color distance of two pixels, | |
| 185 if the normalized color distance of px1 and px2 is greater than | |
| 186 cutoff, returns 1. otherwise returns 0. | |
| 187 | |
| 188 Args: | |
| 189 px1: a 3-tuple (R,G,B) | |
| 190 px2: a 3-tuple (R,G,B) | |
| 191 cutoff: a number used as the threshold limit | |
| 192 Returns: | |
| 193 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.
| |
| 194 """ | |
| 195 diff = _GetColorDist(px1, px2) | |
| 196 if diff > cutoff: | |
| 197 return 1. | |
| 198 else: | |
| 199 return 0. | |
| 200 | |
| 201 | |
| 202 def _MaskToValue(px): | |
| 203 """Converts a black or white pixel to 1. or 0. | |
| 204 | |
| 205 Args: | |
| 206 px: a 3-tuple (R,G,B) | |
| 207 Returns: | |
| 208 1. if the pixel is (0,0,0), 0. if the pixel is (255,255,255) | |
| 209 Raises: | |
| 210 Exception: if the pixel is not (0,0,0) or (255,255,255) | |
| 211 """ | |
| 212 if px[0:3] == (0, 0, 0): | |
| 213 return 1. | |
| 214 if px[0:3] == (255, 255, 255): | |
| 215 return 0. | |
| 216 else: | |
| 217 raise Exception('Mask may only contain black or white pixels') | |
| 218 | |
| 219 | |
| 220 def TotalDifferentPixels(image1, image2, mask=None): | |
| 221 """Computes the number of different pixels between two images. | |
| 222 | |
| 223 Args: | |
| 224 image1: the first Image to be compared | |
| 225 image2: the second Image to be compared | |
| 226 mask: an optional mask to occlude parts of the images | |
| 227 from calculation | |
| 228 Returns: | |
| 229 the number of differing pixels between the images. | |
| 230 Raises: | |
| 231 Exception: if the images to be compared and mask are not the same size. | |
| 232 """ | |
| 233 if mask: | |
| 234 if _AreTheSameSize([image1, image2, mask]): | |
| 235 return sum( | |
| 236 _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.
| |
| 237 for m, px1, px2 in itertools.izip(mask.getdata(), | |
| 238 image1.getdata(), | |
| 239 image2.getdata()) | |
| 240 ) | |
| 241 else: | |
| 242 raise Exception('images and mask must be the same size') | |
| 243 else: | |
| 244 if _AreTheSameSize([image1, image2]): | |
| 245 return sum( | |
| 246 _ThresholdColorDiff(px1, px2, 0) | |
| 247 for px1, px2 in itertools.izip( | |
| 248 image1.getdata(), | |
| 249 image2.getdata() | |
| 250 ) | |
| 251 ) | |
| 252 else: | |
| 253 raise Exception('images and mask must be the same size') | |
| 254 | |
| 255 | |
| 256 def SameImage(image1, image2, max_different_pixels=0, mask=None): | |
| 257 """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.
| |
| 258 | |
| 259 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.
| |
| 260 are similar enough to be considered the same. Essentially | |
| 261 wraps the Similarity function and adds a max_different_pixels | |
| 262 cutoff for Sameness. | |
| 263 | |
| 264 Args: | |
| 265 image1: an Image to compare | |
| 266 image2: an Image to compare | |
| 267 max_different_pixels: a number that is the cutoff for image sameness | |
| 268 mask: an optional Image that occludes parts of the images from | |
| 269 same-ness calculation | |
| 270 Returns: | |
| 271 a boolean representing if the images are the same. | |
| 272 Raises: | |
| 273 Error: if the images (and mask) are different sizes. | |
| 274 """ | |
| 275 | |
| 276 different_pixels = TotalDifferentPixels(image1, image2, mask) | |
| 277 return different_pixels <= max_different_pixels | |
| OLD | NEW |