OLD | NEW |
1 #!/usr/bin/python | 1 #!/usr/bin/python |
2 | 2 |
3 """ | 3 """ |
4 Copyright 2013 Google Inc. | 4 Copyright 2013 Google Inc. |
5 | 5 |
6 Use of this source code is governed by a BSD-style license that can be | 6 Use of this source code is governed by a BSD-style license that can be |
7 found in the LICENSE file. | 7 found in the LICENSE file. |
8 | 8 |
9 Calulate differences between image pairs, and store them in a database. | 9 Calulate differences between image pairs, and store them in a database. |
10 """ | 10 """ |
11 | 11 |
12 import contextlib | 12 import contextlib |
13 import logging | 13 import logging |
14 import os | 14 import os |
| 15 import re |
15 import shutil | 16 import shutil |
16 import urllib | 17 import urllib |
17 try: | 18 try: |
18 from PIL import Image, ImageChops | 19 from PIL import Image, ImageChops |
19 except ImportError: | 20 except ImportError: |
20 raise ImportError('Requires PIL to be installed; see ' | 21 raise ImportError('Requires PIL to be installed; see ' |
21 + 'http://www.pythonware.com/products/pil/') | 22 + 'http://www.pythonware.com/products/pil/') |
22 | 23 |
23 DEFAULT_IMAGE_SUFFIX = '.png' | 24 DEFAULT_IMAGE_SUFFIX = '.png' |
24 DEFAULT_IMAGES_SUBDIR = 'images' | 25 DEFAULT_IMAGES_SUBDIR = 'images' |
25 | 26 |
| 27 DISALLOWED_FILEPATH_CHAR_REGEX = re.compile('[^\w\-]') |
| 28 |
26 DIFFS_SUBDIR = 'diffs' | 29 DIFFS_SUBDIR = 'diffs' |
27 WHITEDIFFS_SUBDIR = 'whitediffs' | 30 WHITEDIFFS_SUBDIR = 'whitediffs' |
28 | 31 |
29 VALUES_PER_BAND = 256 | 32 VALUES_PER_BAND = 256 |
30 | 33 |
31 | 34 |
32 class DiffRecord(object): | 35 class DiffRecord(object): |
33 """ Record of differences between two images. """ | 36 """ Record of differences between two images. """ |
34 | 37 |
35 def __init__(self, storage_root, | 38 def __init__(self, storage_root, |
(...skipping 18 matching lines...) Expand all Loading... |
54 guarantee uniqueness) | 57 guarantee uniqueness) |
55 actual_image_url: file or HTTP url from which we will download the | 58 actual_image_url: file or HTTP url from which we will download the |
56 actual image | 59 actual image |
57 actual_image_locator: a unique ID string under which we will store the | 60 actual_image_locator: a unique ID string under which we will store the |
58 actual image within storage_root (probably including a checksum to | 61 actual image within storage_root (probably including a checksum to |
59 guarantee uniqueness) | 62 guarantee uniqueness) |
60 expected_images_subdir: the subdirectory expected images are stored in. | 63 expected_images_subdir: the subdirectory expected images are stored in. |
61 actual_images_subdir: the subdirectory actual images are stored in. | 64 actual_images_subdir: the subdirectory actual images are stored in. |
62 image_suffix: the suffix of images. | 65 image_suffix: the suffix of images. |
63 """ | 66 """ |
| 67 expected_image_locator = _sanitize_locator(expected_image_locator) |
| 68 actual_image_locator = _sanitize_locator(actual_image_locator) |
| 69 |
64 # Download the expected/actual images, if we don't have them already. | 70 # Download the expected/actual images, if we don't have them already. |
65 # TODO(rmistry): Add a parameter that makes _download_and_open_image raise | 71 # TODO(rmistry): Add a parameter that makes _download_and_open_image raise |
66 # an exception if images are not found locally (instead of trying to | 72 # an exception if images are not found locally (instead of trying to |
67 # download them). | 73 # download them). |
68 expected_image = _download_and_open_image( | 74 expected_image = _download_and_open_image( |
69 os.path.join(storage_root, expected_images_subdir, | 75 os.path.join(storage_root, expected_images_subdir, |
70 str(expected_image_locator) + image_suffix), | 76 str(expected_image_locator) + image_suffix), |
71 expected_image_url) | 77 expected_image_url) |
72 actual_image = _download_and_open_image( | 78 actual_image = _download_and_open_image( |
73 os.path.join(storage_root, actual_images_subdir, | 79 os.path.join(storage_root, actual_images_subdir, |
(...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
125 def get_weighted_diff_measure(self): | 131 def get_weighted_diff_measure(self): |
126 """Returns a weighted measure of image diffs, as a float between 0 and 100 | 132 """Returns a weighted measure of image diffs, as a float between 0 and 100 |
127 (inclusive).""" | 133 (inclusive).""" |
128 return self._weighted_diff_measure | 134 return self._weighted_diff_measure |
129 | 135 |
130 def get_max_diff_per_channel(self): | 136 def get_max_diff_per_channel(self): |
131 """Returns the maximum difference between the expected and actual images | 137 """Returns the maximum difference between the expected and actual images |
132 for each R/G/B channel, as a list.""" | 138 for each R/G/B channel, as a list.""" |
133 return self._max_diff_per_channel | 139 return self._max_diff_per_channel |
134 | 140 |
| 141 def as_dict(self): |
| 142 """Returns a dictionary representation of this DiffRecord, as needed when |
| 143 constructing the JSON representation.""" |
| 144 return { |
| 145 'numDifferingPixels': self._num_pixels_differing, |
| 146 'percentDifferingPixels': self.get_percent_pixels_differing(), |
| 147 'weightedDiffMeasure': self.get_weighted_diff_measure(), |
| 148 'maxDiffPerChannel': self._max_diff_per_channel, |
| 149 } |
| 150 |
135 | 151 |
136 class ImageDiffDB(object): | 152 class ImageDiffDB(object): |
137 """ Calculates differences between image pairs, maintaining a database of | 153 """ Calculates differences between image pairs, maintaining a database of |
138 them for download.""" | 154 them for download.""" |
139 | 155 |
140 def __init__(self, storage_root): | 156 def __init__(self, storage_root): |
141 """ | 157 """ |
142 Args: | 158 Args: |
143 storage_root: string; root path within the DB will store all of its stuff | 159 storage_root: string; root path within the DB will store all of its stuff |
144 """ | 160 """ |
(...skipping 22 matching lines...) Expand all Loading... |
167 expected image | 183 expected image |
168 expected_image_locator: a unique ID string under which we will store the | 184 expected_image_locator: a unique ID string under which we will store the |
169 expected image within storage_root (probably including a checksum to | 185 expected image within storage_root (probably including a checksum to |
170 guarantee uniqueness) | 186 guarantee uniqueness) |
171 actual_image_url: file or HTTP url from which we will download the | 187 actual_image_url: file or HTTP url from which we will download the |
172 actual image | 188 actual image |
173 actual_image_locator: a unique ID string under which we will store the | 189 actual_image_locator: a unique ID string under which we will store the |
174 actual image within storage_root (probably including a checksum to | 190 actual image within storage_root (probably including a checksum to |
175 guarantee uniqueness) | 191 guarantee uniqueness) |
176 """ | 192 """ |
| 193 expected_image_locator = _sanitize_locator(expected_image_locator) |
| 194 actual_image_locator = _sanitize_locator(actual_image_locator) |
177 key = (expected_image_locator, actual_image_locator) | 195 key = (expected_image_locator, actual_image_locator) |
178 if not key in self._diff_dict: | 196 if not key in self._diff_dict: |
179 try: | 197 try: |
180 new_diff_record = DiffRecord( | 198 new_diff_record = DiffRecord( |
181 self._storage_root, | 199 self._storage_root, |
182 expected_image_url=expected_image_url, | 200 expected_image_url=expected_image_url, |
183 expected_image_locator=expected_image_locator, | 201 expected_image_locator=expected_image_locator, |
184 actual_image_url=actual_image_url, | 202 actual_image_url=actual_image_url, |
185 actual_image_locator=actual_image_locator) | 203 actual_image_locator=actual_image_locator) |
186 except Exception: | 204 except Exception: |
187 logging.exception('got exception while creating new DiffRecord') | 205 logging.exception('got exception while creating new DiffRecord') |
188 return | 206 return |
189 self._diff_dict[key] = new_diff_record | 207 self._diff_dict[key] = new_diff_record |
190 | 208 |
191 def get_diff_record(self, expected_image_locator, actual_image_locator): | 209 def get_diff_record(self, expected_image_locator, actual_image_locator): |
192 """Returns the DiffRecord for this image pair. | 210 """Returns the DiffRecord for this image pair. |
193 | 211 |
194 Raises a KeyError if we don't have a DiffRecord for this image pair. | 212 Raises a KeyError if we don't have a DiffRecord for this image pair. |
195 """ | 213 """ |
196 key = (expected_image_locator, actual_image_locator) | 214 key = (_sanitize_locator(expected_image_locator), |
| 215 _sanitize_locator(actual_image_locator)) |
197 return self._diff_dict[key] | 216 return self._diff_dict[key] |
198 | 217 |
199 | 218 |
200 # Utility functions | 219 # Utility functions |
201 | 220 |
202 def _calculate_weighted_diff_metric(histogram, num_pixels): | 221 def _calculate_weighted_diff_metric(histogram, num_pixels): |
203 """Given the histogram of a diff image (per-channel diff at each | 222 """Given the histogram of a diff image (per-channel diff at each |
204 pixel between two images), calculate the weighted diff metric (a | 223 pixel between two images), calculate the weighted diff metric (a |
205 stab at how different the two images really are). | 224 stab at how different the two images really are). |
206 | 225 |
(...skipping 108 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
315 | 334 |
316 def _mkdir_unless_exists(path): | 335 def _mkdir_unless_exists(path): |
317 """Unless path refers to an already-existing directory, create it. | 336 """Unless path refers to an already-existing directory, create it. |
318 | 337 |
319 Args: | 338 Args: |
320 path: path on local disk | 339 path: path on local disk |
321 """ | 340 """ |
322 if not os.path.isdir(path): | 341 if not os.path.isdir(path): |
323 os.makedirs(path) | 342 os.makedirs(path) |
324 | 343 |
| 344 def _sanitize_locator(locator): |
| 345 """Returns a sanitized version of a locator (one in which we know none of the |
| 346 characters will have special meaning in filenames). |
| 347 |
| 348 Args: |
| 349 locator: string, or something that can be represented as a string |
| 350 """ |
| 351 return DISALLOWED_FILEPATH_CHAR_REGEX.sub('_', str(locator)) |
| 352 |
325 def _get_difference_locator(expected_image_locator, actual_image_locator): | 353 def _get_difference_locator(expected_image_locator, actual_image_locator): |
326 """Returns the locator string used to look up the diffs between expected_image | 354 """Returns the locator string used to look up the diffs between expected_image |
327 and actual_image. | 355 and actual_image. |
328 | 356 |
329 Args: | 357 Args: |
330 expected_image_locator: locator string pointing at expected image | 358 expected_image_locator: locator string pointing at expected image |
331 actual_image_locator: locator string pointing at actual image | 359 actual_image_locator: locator string pointing at actual image |
332 | 360 |
333 Returns: locator where the diffs between expected and actual images can be | 361 Returns: already-sanitized locator where the diffs between expected and |
334 found | 362 actual images can be found |
335 """ | 363 """ |
336 return "%s-vs-%s" % (expected_image_locator, actual_image_locator) | 364 return "%s-vs-%s" % (_sanitize_locator(expected_image_locator), |
| 365 _sanitize_locator(actual_image_locator)) |
OLD | NEW |