| 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 shutil | 15 import shutil |
| 16 import urllib | 16 import urllib |
| 17 try: | 17 try: |
| 18 from PIL import Image, ImageChops | 18 from PIL import Image, ImageChops |
| 19 except ImportError: | 19 except ImportError: |
| 20 raise ImportError('Requires PIL to be installed; see ' | 20 raise ImportError('Requires PIL to be installed; see ' |
| 21 + 'http://www.pythonware.com/products/pil/') | 21 + 'http://www.pythonware.com/products/pil/') |
| 22 | 22 |
| 23 IMAGE_SUFFIX = '.png' | 23 DEFAULT_IMAGE_SUFFIX = '.png' |
| 24 DEFAULT_IMAGES_SUBDIR = 'images' |
| 24 | 25 |
| 25 IMAGES_SUBDIR = 'images' | |
| 26 DIFFS_SUBDIR = 'diffs' | 26 DIFFS_SUBDIR = 'diffs' |
| 27 WHITEDIFFS_SUBDIR = 'whitediffs' | 27 WHITEDIFFS_SUBDIR = 'whitediffs' |
| 28 | 28 |
| 29 VALUES_PER_BAND = 256 | 29 VALUES_PER_BAND = 256 |
| 30 | 30 |
| 31 | 31 |
| 32 class DiffRecord(object): | 32 class DiffRecord(object): |
| 33 """ Record of differences between two images. """ | 33 """ Record of differences between two images. """ |
| 34 | 34 |
| 35 def __init__(self, storage_root, | 35 def __init__(self, storage_root, |
| 36 expected_image_url, expected_image_locator, | 36 expected_image_url, expected_image_locator, |
| 37 actual_image_url, actual_image_locator): | 37 actual_image_url, actual_image_locator, |
| 38 expected_images_subdir=DEFAULT_IMAGES_SUBDIR, |
| 39 actual_images_subdir=DEFAULT_IMAGES_SUBDIR, |
| 40 image_suffix=DEFAULT_IMAGE_SUFFIX): |
| 38 """Download this pair of images (unless we already have them on local disk), | 41 """Download this pair of images (unless we already have them on local disk), |
| 39 and prepare a DiffRecord for them. | 42 and prepare a DiffRecord for them. |
| 40 | 43 |
| 41 TODO(epoger): Make this asynchronously download images, rather than blocking | 44 TODO(epoger): Make this asynchronously download images, rather than blocking |
| 42 until the images have been downloaded and processed. | 45 until the images have been downloaded and processed. |
| 43 | 46 |
| 44 Args: | 47 Args: |
| 45 storage_root: root directory on local disk within which we store all | 48 storage_root: root directory on local disk within which we store all |
| 46 images | 49 images |
| 47 expected_image_url: file or HTTP url from which we will download the | 50 expected_image_url: file or HTTP url from which we will download the |
| 48 expected image | 51 expected image |
| 49 expected_image_locator: a unique ID string under which we will store the | 52 expected_image_locator: a unique ID string under which we will store the |
| 50 expected image within storage_root (probably including a checksum to | 53 expected image within storage_root (probably including a checksum to |
| 51 guarantee uniqueness) | 54 guarantee uniqueness) |
| 52 actual_image_url: file or HTTP url from which we will download the | 55 actual_image_url: file or HTTP url from which we will download the |
| 53 actual image | 56 actual image |
| 54 actual_image_locator: a unique ID string under which we will store the | 57 actual_image_locator: a unique ID string under which we will store the |
| 55 actual image within storage_root (probably including a checksum to | 58 actual image within storage_root (probably including a checksum to |
| 56 guarantee uniqueness) | 59 guarantee uniqueness) |
| 60 expected_images_subdir: the subdirectory expected images are stored in. |
| 61 actual_images_subdir: the subdirectory actual images are stored in. |
| 62 image_suffix: the suffix of images. |
| 57 """ | 63 """ |
| 58 # Download the expected/actual images, if we don't have them already. | 64 # 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 |
| 66 # an exception if images are not found locally (instead of trying to |
| 67 # download them). |
| 59 expected_image = _download_and_open_image( | 68 expected_image = _download_and_open_image( |
| 60 os.path.join(storage_root, IMAGES_SUBDIR, | 69 os.path.join(storage_root, expected_images_subdir, |
| 61 str(expected_image_locator) + IMAGE_SUFFIX), | 70 str(expected_image_locator) + image_suffix), |
| 62 expected_image_url) | 71 expected_image_url) |
| 63 actual_image = _download_and_open_image( | 72 actual_image = _download_and_open_image( |
| 64 os.path.join(storage_root, IMAGES_SUBDIR, | 73 os.path.join(storage_root, actual_images_subdir, |
| 65 str(actual_image_locator) + IMAGE_SUFFIX), | 74 str(actual_image_locator) + image_suffix), |
| 66 actual_image_url) | 75 actual_image_url) |
| 67 | 76 |
| 68 # Generate the diff image (absolute diff at each pixel) and | 77 # Generate the diff image (absolute diff at each pixel) and |
| 69 # max_diff_per_channel. | 78 # max_diff_per_channel. |
| 70 diff_image = _generate_image_diff(actual_image, expected_image) | 79 diff_image = _generate_image_diff(actual_image, expected_image) |
| 71 diff_histogram = diff_image.histogram() | 80 diff_histogram = diff_image.histogram() |
| 72 (diff_width, diff_height) = diff_image.size | 81 (diff_width, diff_height) = diff_image.size |
| 73 self._weighted_diff_measure = _calculate_weighted_diff_metric( | 82 self._weighted_diff_measure = _calculate_weighted_diff_metric( |
| 74 diff_histogram, diff_width * diff_height) | 83 diff_histogram, diff_width * diff_height) |
| 75 self._max_diff_per_channel = _max_per_band(diff_histogram) | 84 self._max_diff_per_channel = _max_per_band(diff_histogram) |
| 76 | 85 |
| 77 # Generate the whitediff image (any differing pixels show as white). | 86 # Generate the whitediff image (any differing pixels show as white). |
| 78 # This is tricky, because when you convert color images to grayscale or | 87 # This is tricky, because when you convert color images to grayscale or |
| 79 # black & white in PIL, it has its own ideas about thresholds. | 88 # black & white in PIL, it has its own ideas about thresholds. |
| 80 # We have to force it: if a pixel has any color at all, it's a '1'. | 89 # We have to force it: if a pixel has any color at all, it's a '1'. |
| 81 bands = diff_image.split() | 90 bands = diff_image.split() |
| 82 graydiff_image = ImageChops.lighter(ImageChops.lighter( | 91 graydiff_image = ImageChops.lighter(ImageChops.lighter( |
| 83 bands[0], bands[1]), bands[2]) | 92 bands[0], bands[1]), bands[2]) |
| 84 whitediff_image = (graydiff_image.point(lambda p: p > 0 and VALUES_PER_BAND) | 93 whitediff_image = (graydiff_image.point(lambda p: p > 0 and VALUES_PER_BAND) |
| 85 .convert('1', dither=Image.NONE)) | 94 .convert('1', dither=Image.NONE)) |
| 86 | 95 |
| 87 # Final touches on diff_image: use whitediff_image as an alpha mask. | 96 # Final touches on diff_image: use whitediff_image as an alpha mask. |
| 88 # Unchanged pixels are transparent; differing pixels are opaque. | 97 # Unchanged pixels are transparent; differing pixels are opaque. |
| 89 diff_image.putalpha(whitediff_image) | 98 diff_image.putalpha(whitediff_image) |
| 90 | 99 |
| 91 # Store the diff and whitediff images generated above. | 100 # Store the diff and whitediff images generated above. |
| 92 diff_image_locator = _get_difference_locator( | 101 diff_image_locator = _get_difference_locator( |
| 93 expected_image_locator=expected_image_locator, | 102 expected_image_locator=expected_image_locator, |
| 94 actual_image_locator=actual_image_locator) | 103 actual_image_locator=actual_image_locator) |
| 95 basename = str(diff_image_locator) + IMAGE_SUFFIX | 104 basename = str(diff_image_locator) + image_suffix |
| 96 _save_image(diff_image, os.path.join( | 105 _save_image(diff_image, os.path.join( |
| 97 storage_root, DIFFS_SUBDIR, basename)) | 106 storage_root, DIFFS_SUBDIR, basename)) |
| 98 _save_image(whitediff_image, os.path.join( | 107 _save_image(whitediff_image, os.path.join( |
| 99 storage_root, WHITEDIFFS_SUBDIR, basename)) | 108 storage_root, WHITEDIFFS_SUBDIR, basename)) |
| 100 | 109 |
| 101 # Calculate difference metrics. | 110 # Calculate difference metrics. |
| 102 (self._width, self._height) = diff_image.size | 111 (self._width, self._height) = diff_image.size |
| 103 self._num_pixels_differing = ( | 112 self._num_pixels_differing = ( |
| 104 whitediff_image.histogram()[VALUES_PER_BAND - 1]) | 113 whitediff_image.histogram()[VALUES_PER_BAND - 1]) |
| 105 | 114 |
| (...skipping 212 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 318 and actual_image. | 327 and actual_image. |
| 319 | 328 |
| 320 Args: | 329 Args: |
| 321 expected_image_locator: locator string pointing at expected image | 330 expected_image_locator: locator string pointing at expected image |
| 322 actual_image_locator: locator string pointing at actual image | 331 actual_image_locator: locator string pointing at actual image |
| 323 | 332 |
| 324 Returns: locator where the diffs between expected and actual images can be | 333 Returns: locator where the diffs between expected and actual images can be |
| 325 found | 334 found |
| 326 """ | 335 """ |
| 327 return "%s-vs-%s" % (expected_image_locator, actual_image_locator) | 336 return "%s-vs-%s" % (expected_image_locator, actual_image_locator) |
| OLD | NEW |