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 whether a set of images are the same size. | |
| 15 | |
| 16 Args: | |
| 17 images: a list of images to compare | |
| 18 | |
| 19 Returns: | |
| 20 boolean | |
| 21 | |
| 22 Raises: | |
| 23 Exception: One image or fewer is passed in. | |
| 24 """ | |
| 25 if len(images) > 1: | |
| 26 return not any(images[0].size != img.size for img in images[1:]) | |
|
frankf
2013/06/18 01:52:18
not any -> all
cgrimm
2013/06/18 18:53:02
Done.
| |
| 27 else: | |
| 28 raise Exception('No images passed in') | |
|
frankf
2013/06/18 01:52:18
Please choose the most specific exception, in this
cgrimm
2013/06/18 18:53:02
Done.
| |
| 29 | |
| 30 | |
| 31 def _GetColorDist(px1, px2): | |
| 32 """Returns the normalized color distance between pixels. | |
| 33 | |
| 34 This function gets the color distance between two pixels and | |
| 35 returns a number between 0 and 1 where 0 is lowest distance | |
| 36 and 1 is the greatest distance. | |
| 37 | |
| 38 Args: | |
| 39 px1: a 3-tuple (R,G,B) | |
| 40 px2: a 3-tuple (R,G,B) | |
| 41 | |
| 42 Returns: | |
| 43 a number between 0 and 1 | |
| 44 """ | |
| 45 return sum( | |
| 46 (ch1 - ch2) ** 2 | |
|
frankf
2013/06/18 01:52:18
Why squared? If you just want absolute value, use
craigdh
2013/06/18 16:25:01
Agreed, I don't think you need to do these distanc
cgrimm
2013/06/18 18:53:02
Done.
cgrimm
2013/06/18 18:53:02
Done.
| |
| 47 for ch1, ch2 in itertools.izip(px1[0:3], px2[0:3]) | |
|
frankf
2013/06/18 01:52:18
Why do you need [0:3]?
cgrimm
2013/06/18 18:53:02
Done.
| |
| 48 ) / 195075. # 3*255**2 | |
| 49 | |
| 50 | |
| 51 def _GetColorDistAsColor(px1, px2): | |
|
frankf
2013/06/18 01:52:18
Dist is ambiguous (Distribution, Dist)
-> GetColo
cgrimm
2013/06/18 18:53:02
Done.
cgrimm
2013/06/18 18:53:02
Done.
| |
| 52 """Computes Color Distance as a greyscale pixel. | |
|
frankf
2013/06/18 01:52:18
Why is there random capitalization?
cgrimm
2013/06/18 18:53:02
Done.
| |
| 53 | |
| 54 This function gets the color distance between two pixels | |
| 55 represented as a pixel ranging in color from (0,0,0) to | |
| 56 (255,255,255). | |
| 57 | |
| 58 Args: | |
| 59 px1: a 3-tuple (R,G,B) | |
| 60 px2: a 3-tuple (R,G,B) | |
| 61 | |
| 62 Returns: | |
| 63 a 3-tuple between (0,0,0) and (255,255,255) | |
| 64 """ | |
| 65 n = _GetColorDist(px1, px2) | |
| 66 return (int(math.ceil(255 * n)), | |
| 67 int(math.ceil(255 * n)), | |
| 68 int(math.ceil(255 * n))) | |
| 69 | |
| 70 | |
| 71 def _Brightness(px): | |
| 72 """Gets the brightness of a pixel. | |
| 73 | |
| 74 Returns the sum of a given pixel's color channels. | |
| 75 | |
| 76 Args: | |
| 77 px: a 3-tuple (R,G,B) | |
| 78 | |
| 79 Returns: | |
| 80 a number between 0 and 3*255 | |
| 81 """ | |
| 82 return sum(px) | |
| 83 | |
| 84 | |
| 85 def _MinPixel(px1, px2): | |
|
frankf
2013/06/18 01:52:18
It might be better to define a Pixel class and ove
craigdh
2013/06/18 16:25:01
I think in this case it's alright to use a 3-tuple
cgrimm
2013/06/18 18:53:02
Done.
| |
| 86 """Gets the pixel with the lower brightness. | |
| 87 | |
| 88 Calculates the brightness of each pixel and returns | |
| 89 the pixel with the lower brightness. | |
| 90 | |
| 91 Args: | |
| 92 px1: a 3-tuple (R,G,B) | |
| 93 px2: a 3-tuple (R,G,B) | |
| 94 | |
| 95 Returns: | |
| 96 a 3-tuple (R,G,B) | |
| 97 """ | |
| 98 if _Brightness(px1) < _Brightness(px2): | |
| 99 return px1 | |
| 100 else: | |
| 101 return px2 | |
| 102 | |
| 103 | |
| 104 def _MaxPixel(px1, px2): | |
| 105 """gets the pixel with the greater brightness. | |
| 106 | |
| 107 Calculates the brightness of each pixel and returns | |
| 108 the pixel with the greater brightness. | |
| 109 | |
| 110 Args: | |
| 111 px1: a 3-tuple (R,G,B) | |
| 112 px2: a 3-tuple (R,G,B) | |
| 113 | |
| 114 Returns: | |
| 115 a 3-tuple (R,G,B) | |
| 116 """ | |
| 117 if _Brightness(px1) < _Brightness(px2): | |
| 118 return px2 | |
| 119 else: | |
| 120 return px1 | |
| 121 | |
| 122 | |
| 123 def CreateMask(images, threshold=True, cutoff=0): | |
| 124 """Computes a mask for a set of images. | |
| 125 | |
| 126 Returns an image that is computed from the images | |
| 127 passed in. The mask's values are computed by calculating | |
| 128 total differences between the image, and storing them | |
| 129 such that (255,255,255) represents great difference, | |
|
frankf
2013/06/18 01:52:18
largest
cgrimm
2013/06/18 18:53:02
Done.
| |
| 130 and (0,0,0) represents no difference. If is_hard is | |
| 131 True, the resulting mask is put through a | |
| 132 thresholding pass where pixel values below the | |
| 133 cutoff become (0,0,0) and ones above become (255,255,255). | |
| 134 | |
| 135 Args: | |
| 136 images: the images to compute the mask from | |
| 137 threshold: boolean, whether or not to threshold the mask | |
| 138 cutoff: number, the value to threshold the mask at | |
| 139 | |
| 140 Returns: | |
| 141 an image that is a mask of the passed in images. | |
| 142 | |
| 143 Raises: | |
| 144 Exception: if the images passed in are not of the same size | |
| 145 """ | |
| 146 if not _AreTheSameSize(images): | |
| 147 raise Exception('All images must be the same size') | |
| 148 mask = PIL.Image.new('RGB', images[0].size) | |
| 149 mask_data = mask.getdata() | |
| 150 image_data = images[0].getdata() | |
| 151 for other_image in images[1:]: | |
|
frankf
2013/06/18 01:52:18
I think it's clear to use max(map...
cgrimm
2013/06/18 18:53:02
Done.
| |
| 152 mask_data = _ComputeLargestDifference(mask_data, image_data, | |
| 153 other_image.getdata()) | |
| 154 | |
| 155 if threshold: | |
| 156 mask.putdata([ | |
| 157 (255, 255, 255) if _Brightness(px) > cutoff | |
| 158 else (0, 0, 0) | |
| 159 for px in mask_data | |
| 160 ]) | |
| 161 else: | |
| 162 mask.putdata(list(mask_data)) | |
| 163 return mask | |
| 164 | |
| 165 | |
| 166 def _ComputeLargestDifference(mask_data, image_data1, image_data2): | |
| 167 """Modifies a mask based upon a comparision of two images. | |
| 168 | |
| 169 A helper function used to generate masks. The mask, as | |
|
frankf
2013/06/18 01:52:18
Aren't all these "helper functions"?
cgrimm
2013/06/18 18:53:02
Done.
| |
| 170 it exists when the function is called, is compared to two | |
| 171 images on a pixel-by-pixel basis. For each pixel, the | |
| 172 ColorDistance between the two images is computed as a Color, | |
| 173 and compared against the color of the mask at the same | |
| 174 pixel. The brightest of these pixels is chosen for pixel | |
| 175 in the images/mask, and a new set of pixels is returned. | |
| 176 | |
| 177 Args: | |
| 178 mask_data: a list of pixels of the mask | |
| 179 image_data1: a list of pixels of an image | |
| 180 image_data2: a list of pixels of another image | |
| 181 | |
| 182 Returns: | |
| 183 a list of pixels representing a modified version of the mask. | |
| 184 """ | |
| 185 return ( | |
| 186 _MaxPixel(m, _GetColorDistAsColor(i1, i2)) | |
| 187 for m, i1, i2 in itertools.izip(mask_data, image_data1, image_data2) | |
| 188 ) | |
| 189 | |
| 190 | |
| 191 def _ThresholdColorDiff(px1, px2, cutoff): | |
| 192 """Thresholds the color distance of two pixels. | |
| 193 | |
| 194 Computes a Threshold of the color distance of two pixels, | |
| 195 if the normalized color distance of px1 and px2 is greater than | |
| 196 cutoff, returns 1. otherwise returns 0. | |
| 197 | |
| 198 Args: | |
| 199 px1: a 3-tuple (R,G,B) | |
| 200 px2: a 3-tuple (R,G,B) | |
| 201 cutoff: a number used as the threshold limit | |
| 202 | |
| 203 Returns: | |
| 204 either 1. or 0. depending on the distance of px1 and px2. | |
| 205 """ | |
| 206 diff = _GetColorDist(px1, px2) | |
| 207 if diff > cutoff: | |
| 208 return 1 | |
| 209 else: | |
| 210 return 0 | |
| 211 | |
| 212 | |
| 213 def _MaskToValue(px): | |
| 214 """Converts a black or white pixel to 1. or 0. | |
| 215 | |
| 216 Args: | |
| 217 px: a 3-tuple (R,G,B) | |
| 218 | |
| 219 Returns: | |
| 220 1. if the pixel is (0,0,0), 0. if the pixel is (255,255,255) | |
| 221 | |
| 222 Raises: | |
| 223 Exception: if the pixel is not (0,0,0) or (255,255,255) | |
| 224 """ | |
| 225 if px[0:3] == (0, 0, 0): | |
|
craigdh
2013/06/18 16:25:01
No need for [0:3]. If it's it's not already a 3-tu
cgrimm
2013/06/18 18:53:02
Done.
| |
| 226 return 1. | |
| 227 if px[0:3] == (255, 255, 255): | |
| 228 return 0. | |
| 229 else: | |
| 230 raise Exception('Mask may only contain black or white pixels') | |
| 231 | |
| 232 | |
| 233 def TotalDifferentPixels(image1, image2, mask=None): | |
| 234 """Computes the number of different pixels between two images. | |
| 235 | |
| 236 Args: | |
| 237 image1: the first RGB PIL.Image to be compared | |
| 238 image2: the second RGB PIL.Image to be compared | |
| 239 mask: an optional mask to occlude parts of the images | |
| 240 from calculation | |
| 241 | |
| 242 Returns: | |
| 243 the number of differing pixels between the images. | |
| 244 | |
| 245 Raises: | |
| 246 Exception: if the images to be compared and mask are not the same size. | |
| 247 """ | |
| 248 if mask: | |
| 249 if _AreTheSameSize([image1, image2, mask]): | |
| 250 return sum( | |
| 251 _MaskToValue(m) * _ThresholdColorDiff(px1, px2, 0) | |
| 252 for m, px1, px2 in itertools.izip(mask.getdata(), | |
| 253 image1.getdata(), | |
| 254 image2.getdata()) | |
| 255 ) | |
| 256 else: | |
| 257 raise Exception('images and mask must be the same size') | |
| 258 else: | |
| 259 if _AreTheSameSize([image1, image2]): | |
| 260 return sum( | |
| 261 _ThresholdColorDiff(px1, px2, 0) | |
| 262 for px1, px2 in itertools.izip( | |
| 263 image1.getdata(), | |
| 264 image2.getdata() | |
| 265 ) | |
| 266 ) | |
| 267 else: | |
| 268 raise Exception('images and mask must be the same size') | |
| 269 | |
| 270 | |
| 271 def SameImage(image1, image2, max_different_pixels=0, mask=None): | |
| 272 """Returns a boolean representing whether the images are the same. | |
| 273 | |
| 274 Returns a boolean indicating whether two images | |
| 275 are similar enough to be considered the same. Essentially | |
| 276 wraps the TotalDifferentPixels function and adds a max_different_pixels | |
| 277 cutoff for whether or not the images are the same. | |
| 278 | |
| 279 Args: | |
| 280 image1: an RGB PIL.Image to compare | |
|
craigdh
2013/06/17 23:53:06
end all sentences with a period
cgrimm
2013/06/18 18:53:02
Done.
| |
| 281 image2: an RGB PIL.Image to compare | |
| 282 max_different_pixels: a number that is the cutoff for image sameness | |
| 283 mask: an optional Image that occludes parts of the images from | |
| 284 same-ness calculation | |
| 285 | |
| 286 Returns: | |
| 287 True if the images are similar, false otherwise. | |
| 288 | |
| 289 Raises: | |
| 290 Exception: if the images (and mask) are different sizes. | |
| 291 """ | |
| 292 | |
| 293 different_pixels = TotalDifferentPixels(image1, image2, mask) | |
| 294 return different_pixels <= max_different_pixels | |
| 295 | |
| 296 | |
| 297 def VisualizeImageDifferences(image1, image2, mask=None): | |
| 298 """Returns an image representing the unmasked differences between two images. | |
| 299 | |
| 300 Iterates through the pixel values of two images and an optional | |
| 301 mask. If the pixel values are the same, or the pixel is masked, | |
| 302 (0,0,0) is stored for that pixel. Otherwise, (255,255,255) is | |
| 303 stored. This ultimately produces an image where unmasked differences | |
| 304 between the two images are white pixels, and everything else | |
| 305 is black. | |
| 306 | |
| 307 Args: | |
| 308 image1: an Image | |
| 309 image2: another Image of the same size as image1 | |
| 310 mask: an optional Image that represents parts of the two | |
| 311 images to ignore when generating the difference image. | |
| 312 | |
| 313 Returns: | |
| 314 a black and white image representing the unmasked difference between | |
| 315 the two input images. | |
| 316 | |
| 317 Raises: | |
| 318 Exception: if the two images and optional mask are different sizes | |
| 319 """ | |
| 320 | |
| 321 if mask: | |
| 322 if not _AreTheSameSize([image1, image2, mask]): | |
| 323 raise Exception('images and mask must be the same size') | |
| 324 image_diff = PIL.Image.new('RGB', image1.size, (0, 0, 0)) | |
| 325 data = list( | |
| 326 (0, 0, 0) if m == (255, 255, 255) or px1 == px2 | |
| 327 else (255, 255, 255) | |
| 328 for m, px1, px2 in itertools.izip(mask.getdata(), | |
| 329 image1.getdata(), | |
| 330 image2.getdata()) | |
| 331 ) | |
| 332 image_diff.putdata(data) | |
| 333 return image_diff | |
| 334 else: | |
| 335 if not _AreTheSameSize([image1, image2]): | |
| 336 raise Exception('images must be the same size') | |
| 337 image_diff = PIL.Image.new('RGB', image1.size, (0, 0, 0)) | |
| 338 data = list( | |
| 339 (0, 0, 0) if px1 == px2 | |
| 340 else (255, 255, 255) | |
| 341 for px1, px2 in itertools.izip(image1.getdata(), | |
| 342 image2.getdata()) | |
| 343 ) | |
| 344 image_diff.putdata(data) | |
| 345 return image_diff | |
| 346 | |
| OLD | NEW |