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