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