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

Side by Side 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: minor changes to documentation 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 unified diff | Download patch
OLDNEW
(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
OLDNEW
« 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