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

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: 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 """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
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