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 |