| 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 """ |
| (...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 42 WHITEDIFFS_SUBDIR = 'whitediffs' | 42 WHITEDIFFS_SUBDIR = 'whitediffs' |
| 43 | 43 |
| 44 VALUES_PER_BAND = 256 | 44 VALUES_PER_BAND = 256 |
| 45 | 45 |
| 46 # Keys used within DiffRecord dictionary representations. | 46 # Keys used within DiffRecord dictionary representations. |
| 47 # NOTE: Keep these in sync with static/constants.js | 47 # NOTE: Keep these in sync with static/constants.js |
| 48 KEY__DIFFERENCES__MAX_DIFF_PER_CHANNEL = 'maxDiffPerChannel' | 48 KEY__DIFFERENCES__MAX_DIFF_PER_CHANNEL = 'maxDiffPerChannel' |
| 49 KEY__DIFFERENCES__NUM_DIFF_PIXELS = 'numDifferingPixels' | 49 KEY__DIFFERENCES__NUM_DIFF_PIXELS = 'numDifferingPixels' |
| 50 KEY__DIFFERENCES__PERCENT_DIFF_PIXELS = 'percentDifferingPixels' | 50 KEY__DIFFERENCES__PERCENT_DIFF_PIXELS = 'percentDifferingPixels' |
| 51 KEY__DIFFERENCES__PERCEPTUAL_DIFF = 'perceptualDifference' | 51 KEY__DIFFERENCES__PERCEPTUAL_DIFF = 'perceptualDifference' |
| 52 KEY__DIFFERENCES__WEIGHTED_DIFF = 'weightedDiffMeasure' | |
| 53 | 52 |
| 54 | 53 |
| 55 class DiffRecord(object): | 54 class DiffRecord(object): |
| 56 """ Record of differences between two images. """ | 55 """ Record of differences between two images. """ |
| 57 | 56 |
| 58 def __init__(self, storage_root, | 57 def __init__(self, storage_root, |
| 59 expected_image_url, expected_image_locator, | 58 expected_image_url, expected_image_locator, |
| 60 actual_image_url, actual_image_locator, | 59 actual_image_url, actual_image_locator, |
| 61 expected_images_subdir=DEFAULT_IMAGES_SUBDIR, | 60 expected_images_subdir=DEFAULT_IMAGES_SUBDIR, |
| 62 actual_images_subdir=DEFAULT_IMAGES_SUBDIR, | 61 actual_images_subdir=DEFAULT_IMAGES_SUBDIR, |
| (...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 110 except Exception: | 109 except Exception: |
| 111 logging.exception('unable to download actual_image_url %s to file %s' % | 110 logging.exception('unable to download actual_image_url %s to file %s' % |
| 112 (actual_image_url, actual_image_file)) | 111 (actual_image_url, actual_image_file)) |
| 113 raise | 112 raise |
| 114 | 113 |
| 115 # Generate the diff image (absolute diff at each pixel) and | 114 # Generate the diff image (absolute diff at each pixel) and |
| 116 # max_diff_per_channel. | 115 # max_diff_per_channel. |
| 117 diff_image = _generate_image_diff(actual_image, expected_image) | 116 diff_image = _generate_image_diff(actual_image, expected_image) |
| 118 diff_histogram = diff_image.histogram() | 117 diff_histogram = diff_image.histogram() |
| 119 (diff_width, diff_height) = diff_image.size | 118 (diff_width, diff_height) = diff_image.size |
| 120 self._weighted_diff_measure = _calculate_weighted_diff_metric( | |
| 121 diff_histogram, diff_width * diff_height) | |
| 122 self._max_diff_per_channel = _max_per_band(diff_histogram) | 119 self._max_diff_per_channel = _max_per_band(diff_histogram) |
| 123 | 120 |
| 124 # Generate the whitediff image (any differing pixels show as white). | 121 # Generate the whitediff image (any differing pixels show as white). |
| 125 # This is tricky, because when you convert color images to grayscale or | 122 # This is tricky, because when you convert color images to grayscale or |
| 126 # black & white in PIL, it has its own ideas about thresholds. | 123 # black & white in PIL, it has its own ideas about thresholds. |
| 127 # We have to force it: if a pixel has any color at all, it's a '1'. | 124 # We have to force it: if a pixel has any color at all, it's a '1'. |
| 128 bands = diff_image.split() | 125 bands = diff_image.split() |
| 129 graydiff_image = ImageChops.lighter(ImageChops.lighter( | 126 graydiff_image = ImageChops.lighter(ImageChops.lighter( |
| 130 bands[0], bands[1]), bands[2]) | 127 bands[0], bands[1]), bands[2]) |
| 131 whitediff_image = (graydiff_image.point(lambda p: p > 0 and VALUES_PER_BAND) | 128 whitediff_image = (graydiff_image.point(lambda p: p > 0 and VALUES_PER_BAND) |
| (...skipping 50 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 182 def get_percent_pixels_differing(self): | 179 def get_percent_pixels_differing(self): |
| 183 """Returns the percentage of pixels that differ, as a float between | 180 """Returns the percentage of pixels that differ, as a float between |
| 184 0 and 100 (inclusive).""" | 181 0 and 100 (inclusive).""" |
| 185 return ((float(self._num_pixels_differing) * 100) / | 182 return ((float(self._num_pixels_differing) * 100) / |
| 186 (self._width * self._height)) | 183 (self._width * self._height)) |
| 187 | 184 |
| 188 def get_perceptual_difference(self): | 185 def get_perceptual_difference(self): |
| 189 """Returns the perceptual difference percentage.""" | 186 """Returns the perceptual difference percentage.""" |
| 190 return self._perceptual_difference | 187 return self._perceptual_difference |
| 191 | 188 |
| 192 def get_weighted_diff_measure(self): | |
| 193 """Returns a weighted measure of image diffs, as a float between 0 and 100 | |
| 194 (inclusive). | |
| 195 | |
| 196 TODO(epoger): Delete this function, now that we have perceptual diff? | |
| 197 """ | |
| 198 return self._weighted_diff_measure | |
| 199 | |
| 200 def get_max_diff_per_channel(self): | 189 def get_max_diff_per_channel(self): |
| 201 """Returns the maximum difference between the expected and actual images | 190 """Returns the maximum difference between the expected and actual images |
| 202 for each R/G/B channel, as a list.""" | 191 for each R/G/B channel, as a list.""" |
| 203 return self._max_diff_per_channel | 192 return self._max_diff_per_channel |
| 204 | 193 |
| 205 def as_dict(self): | 194 def as_dict(self): |
| 206 """Returns a dictionary representation of this DiffRecord, as needed when | 195 """Returns a dictionary representation of this DiffRecord, as needed when |
| 207 constructing the JSON representation.""" | 196 constructing the JSON representation.""" |
| 208 return { | 197 return { |
| 209 KEY__DIFFERENCES__NUM_DIFF_PIXELS: self._num_pixels_differing, | 198 KEY__DIFFERENCES__NUM_DIFF_PIXELS: self._num_pixels_differing, |
| 210 KEY__DIFFERENCES__PERCENT_DIFF_PIXELS: | 199 KEY__DIFFERENCES__PERCENT_DIFF_PIXELS: |
| 211 self.get_percent_pixels_differing(), | 200 self.get_percent_pixels_differing(), |
| 212 KEY__DIFFERENCES__WEIGHTED_DIFF: self.get_weighted_diff_measure(), | |
| 213 KEY__DIFFERENCES__MAX_DIFF_PER_CHANNEL: self._max_diff_per_channel, | 201 KEY__DIFFERENCES__MAX_DIFF_PER_CHANNEL: self._max_diff_per_channel, |
| 214 KEY__DIFFERENCES__PERCEPTUAL_DIFF: self._perceptual_difference, | 202 KEY__DIFFERENCES__PERCEPTUAL_DIFF: self._perceptual_difference, |
| 215 } | 203 } |
| 216 | 204 |
| 217 | 205 |
| 218 class ImageDiffDB(object): | 206 class ImageDiffDB(object): |
| 219 """ Calculates differences between image pairs, maintaining a database of | 207 """ Calculates differences between image pairs, maintaining a database of |
| 220 them for download.""" | 208 them for download.""" |
| 221 | 209 |
| 222 def __init__(self, storage_root): | 210 def __init__(self, storage_root): |
| (...skipping 60 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 283 | 271 |
| 284 Raises a KeyError if we don't have a DiffRecord for this image pair. | 272 Raises a KeyError if we don't have a DiffRecord for this image pair. |
| 285 """ | 273 """ |
| 286 key = (_sanitize_locator(expected_image_locator), | 274 key = (_sanitize_locator(expected_image_locator), |
| 287 _sanitize_locator(actual_image_locator)) | 275 _sanitize_locator(actual_image_locator)) |
| 288 return self._diff_dict[key] | 276 return self._diff_dict[key] |
| 289 | 277 |
| 290 | 278 |
| 291 # Utility functions | 279 # Utility functions |
| 292 | 280 |
| 293 def _calculate_weighted_diff_metric(histogram, num_pixels): | |
| 294 """Given the histogram of a diff image (per-channel diff at each | |
| 295 pixel between two images), calculate the weighted diff metric (a | |
| 296 stab at how different the two images really are). | |
| 297 | |
| 298 TODO(epoger): Delete this function, now that we have perceptual diff? | |
| 299 | |
| 300 Args: | |
| 301 histogram: PIL histogram of a per-channel diff between two images | |
| 302 num_pixels: integer; the total number of pixels in the diff image | |
| 303 | |
| 304 Returns: a weighted diff metric, as a float between 0 and 100 (inclusive). | |
| 305 """ | |
| 306 # TODO(epoger): As a wild guess at an appropriate metric, weight each | |
| 307 # different pixel by the square of its delta value. (The more different | |
| 308 # a pixel is from its expectation, the more we care about it.) | |
| 309 assert(len(histogram) % VALUES_PER_BAND == 0) | |
| 310 num_bands = len(histogram) / VALUES_PER_BAND | |
| 311 max_diff = num_pixels * num_bands * (VALUES_PER_BAND - 1)**2 | |
| 312 total_diff = 0 | |
| 313 for index in xrange(len(histogram)): | |
| 314 total_diff += histogram[index] * (index % VALUES_PER_BAND)**2 | |
| 315 return float(100 * total_diff) / max_diff | |
| 316 | |
| 317 | |
| 318 def _max_per_band(histogram): | 281 def _max_per_band(histogram): |
| 319 """Given the histogram of an image, return the maximum value of each band | 282 """Given the histogram of an image, return the maximum value of each band |
| 320 (a.k.a. "color channel", such as R/G/B) across the entire image. | 283 (a.k.a. "color channel", such as R/G/B) across the entire image. |
| 321 | 284 |
| 322 Args: | 285 Args: |
| 323 histogram: PIL histogram | 286 histogram: PIL histogram |
| 324 | 287 |
| 325 Returns the maximum value of each band within the image histogram, as a list. | 288 Returns the maximum value of each band within the image histogram, as a list. |
| 326 """ | 289 """ |
| 327 max_per_band = [] | 290 max_per_band = [] |
| (...skipping 114 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 442 | 405 |
| 443 Args: | 406 Args: |
| 444 expected_image_locator: locator string pointing at expected image | 407 expected_image_locator: locator string pointing at expected image |
| 445 actual_image_locator: locator string pointing at actual image | 408 actual_image_locator: locator string pointing at actual image |
| 446 | 409 |
| 447 Returns: already-sanitized locator where the diffs between expected and | 410 Returns: already-sanitized locator where the diffs between expected and |
| 448 actual images can be found | 411 actual images can be found |
| 449 """ | 412 """ |
| 450 return "%s-vs-%s" % (_sanitize_locator(expected_image_locator), | 413 return "%s-vs-%s" % (_sanitize_locator(expected_image_locator), |
| 451 _sanitize_locator(actual_image_locator)) | 414 _sanitize_locator(actual_image_locator)) |
| OLD | NEW |