Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/python | 1 #!/usr/bin/python |
| 2 | 2 |
| 3 """ | 3 """ |
|
epoger
2013/12/20 19:46:37
Have you confirmed that its self-test still passes
rmistry
2013/12/20 20:00:04
Yes I ran it before and after my changes.
| |
| 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 images_subdir=DEFAULT_IMAGES_SUBDIR, | |
| 39 actual_images_subdir=DEFAULT_IMAGES_SUBDIR, | |
| 40 use_diff_image_locator=True, | |
| 41 image_suffix=DEFAULT_IMAGE_SUFFIX): | |
| 38 """Download this pair of images (unless we already have them on local disk), | 42 """Download this pair of images (unless we already have them on local disk), |
| 39 and prepare a DiffRecord for them. | 43 and prepare a DiffRecord for them. |
| 40 | 44 |
| 41 TODO(epoger): Make this asynchronously download images, rather than blocking | 45 TODO(epoger): Make this asynchronously download images, rather than blocking |
| 42 until the images have been downloaded and processed. | 46 until the images have been downloaded and processed. |
| 43 | 47 |
| 44 Args: | 48 Args: |
| 45 storage_root: root directory on local disk within which we store all | 49 storage_root: root directory on local disk within which we store all |
| 46 images | 50 images |
| 47 expected_image_url: file or HTTP url from which we will download the | 51 expected_image_url: file or HTTP url from which we will download the |
| 48 expected image | 52 expected image |
| 49 expected_image_locator: a unique ID string under which we will store the | 53 expected_image_locator: a unique ID string under which we will store the |
| 50 expected image within storage_root (probably including a checksum to | 54 expected image within storage_root (probably including a checksum to |
| 51 guarantee uniqueness) | 55 guarantee uniqueness) |
| 52 actual_image_url: file or HTTP url from which we will download the | 56 actual_image_url: file or HTTP url from which we will download the |
| 53 actual image | 57 actual image |
| 54 actual_image_locator: a unique ID string under which we will store the | 58 actual_image_locator: a unique ID string under which we will store the |
| 55 actual image within storage_root (probably including a checksum to | 59 actual image within storage_root (probably including a checksum to |
| 56 guarantee uniqueness) | 60 guarantee uniqueness) |
| 61 images_subdir: the subdirectory images are stored in. Optional parameter, | |
|
epoger
2013/12/20 19:46:37
what are "images" as opposed to "actual images"?
rmistry
2013/12/20 20:00:04
Ah right. Done.
| |
| 62 the default value is in DEFAULT_IMAGES_SUBDIR. | |
|
epoger
2013/12/20 19:46:37
Thank you for being descriptive, but I think we're
rmistry
2013/12/20 20:00:04
Done.
| |
| 63 actual_images_subdir: the subdirectory actual images are stored in. | |
| 64 Optional parameter, the default value is in DEFAULT_IMAGES_SUBDIR. | |
| 65 use_diff_image_locator: If True outputs differences as | |
| 66 'expected_image_locator-vs-actual_image_locator' else outputs | |
| 67 differences using the expected_image_locator if expected_image_locator | |
|
epoger
2013/12/20 19:46:37
Confusing description... so, if this is False, but
rmistry
2013/12/20 20:00:04
Agreed. Done.
| |
| 68 is equal to the actual_image_locator. Optional parameter, the default | |
| 69 value is True. | |
| 70 image_suffix: the suffix of images. Optional parameter, the default | |
| 71 value is in DEFAULT_IMAGE_SUFFIX. | |
| 57 """ | 72 """ |
| 58 # Download the expected/actual images, if we don't have them already. | 73 # Download the expected/actual images, if we don't have them already. |
| 59 expected_image = _download_and_open_image( | 74 expected_image = _download_and_open_image( |
| 60 os.path.join(storage_root, IMAGES_SUBDIR, | 75 os.path.join(storage_root, images_subdir, |
| 61 str(expected_image_locator) + IMAGE_SUFFIX), | 76 str(expected_image_locator) + image_suffix), |
|
epoger
2013/12/20 19:46:37
Warning: if you are not using checksums to generat
rmistry
2013/12/20 20:00:04
I tested this through the python interpreter and i
epoger
2013/12/20 20:05:08
Sounds like a good idea. More than anything, I wa
rmistry
2013/12/20 20:53:27
Added a TODO.
| |
| 62 expected_image_url) | 77 expected_image_url) |
| 63 actual_image = _download_and_open_image( | 78 actual_image = _download_and_open_image( |
| 64 os.path.join(storage_root, IMAGES_SUBDIR, | 79 os.path.join(storage_root, actual_images_subdir, |
| 65 str(actual_image_locator) + IMAGE_SUFFIX), | 80 str(actual_image_locator) + image_suffix), |
| 66 actual_image_url) | 81 actual_image_url) |
| 67 | 82 |
| 68 # Generate the diff image (absolute diff at each pixel) and | 83 # Generate the diff image (absolute diff at each pixel) and |
| 69 # max_diff_per_channel. | 84 # max_diff_per_channel. |
| 70 diff_image = _generate_image_diff(actual_image, expected_image) | 85 diff_image = _generate_image_diff(actual_image, expected_image) |
| 71 diff_histogram = diff_image.histogram() | 86 diff_histogram = diff_image.histogram() |
| 72 (diff_width, diff_height) = diff_image.size | 87 (diff_width, diff_height) = diff_image.size |
| 73 self._weighted_diff_measure = _calculate_weighted_diff_metric( | 88 self._weighted_diff_measure = _calculate_weighted_diff_metric( |
| 74 diff_histogram, diff_width * diff_height) | 89 diff_histogram, diff_width * diff_height) |
| 75 self._max_diff_per_channel = _max_per_band(diff_histogram) | 90 self._max_diff_per_channel = _max_per_band(diff_histogram) |
| 76 | 91 |
| 77 # Generate the whitediff image (any differing pixels show as white). | 92 # Generate the whitediff image (any differing pixels show as white). |
| 78 # This is tricky, because when you convert color images to grayscale or | 93 # This is tricky, because when you convert color images to grayscale or |
| 79 # black & white in PIL, it has its own ideas about thresholds. | 94 # 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'. | 95 # We have to force it: if a pixel has any color at all, it's a '1'. |
| 81 bands = diff_image.split() | 96 bands = diff_image.split() |
| 82 graydiff_image = ImageChops.lighter(ImageChops.lighter( | 97 graydiff_image = ImageChops.lighter(ImageChops.lighter( |
| 83 bands[0], bands[1]), bands[2]) | 98 bands[0], bands[1]), bands[2]) |
| 84 whitediff_image = (graydiff_image.point(lambda p: p > 0 and VALUES_PER_BAND) | 99 whitediff_image = (graydiff_image.point(lambda p: p > 0 and VALUES_PER_BAND) |
| 85 .convert('1', dither=Image.NONE)) | 100 .convert('1', dither=Image.NONE)) |
| 86 | 101 |
| 87 # Final touches on diff_image: use whitediff_image as an alpha mask. | 102 # Final touches on diff_image: use whitediff_image as an alpha mask. |
| 88 # Unchanged pixels are transparent; differing pixels are opaque. | 103 # Unchanged pixels are transparent; differing pixels are opaque. |
| 89 diff_image.putalpha(whitediff_image) | 104 diff_image.putalpha(whitediff_image) |
| 90 | 105 |
| 91 # Store the diff and whitediff images generated above. | 106 # Store the diff and whitediff images generated above. |
| 92 diff_image_locator = _get_difference_locator( | 107 if not use_diff_image_locator and ( |
| 93 expected_image_locator=expected_image_locator, | 108 expected_image_locator == actual_image_locator): |
| 94 actual_image_locator=actual_image_locator) | 109 basename = expected_image_locator + image_suffix |
| 95 basename = str(diff_image_locator) + IMAGE_SUFFIX | 110 else: |
| 111 diff_image_locator = _get_difference_locator( | |
| 112 expected_image_locator=expected_image_locator, | |
| 113 actual_image_locator=actual_image_locator) | |
| 114 basename = str(diff_image_locator) + image_suffix | |
| 96 _save_image(diff_image, os.path.join( | 115 _save_image(diff_image, os.path.join( |
| 97 storage_root, DIFFS_SUBDIR, basename)) | 116 storage_root, DIFFS_SUBDIR, basename)) |
| 98 _save_image(whitediff_image, os.path.join( | 117 _save_image(whitediff_image, os.path.join( |
| 99 storage_root, WHITEDIFFS_SUBDIR, basename)) | 118 storage_root, WHITEDIFFS_SUBDIR, basename)) |
| 100 | 119 |
| 101 # Calculate difference metrics. | 120 # Calculate difference metrics. |
| 102 (self._width, self._height) = diff_image.size | 121 (self._width, self._height) = diff_image.size |
| 103 self._num_pixels_differing = ( | 122 self._num_pixels_differing = ( |
| 104 whitediff_image.histogram()[VALUES_PER_BAND - 1]) | 123 whitediff_image.histogram()[VALUES_PER_BAND - 1]) |
| 105 | 124 |
| (...skipping 212 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 318 and actual_image. | 337 and actual_image. |
| 319 | 338 |
| 320 Args: | 339 Args: |
| 321 expected_image_locator: locator string pointing at expected image | 340 expected_image_locator: locator string pointing at expected image |
| 322 actual_image_locator: locator string pointing at actual image | 341 actual_image_locator: locator string pointing at actual image |
| 323 | 342 |
| 324 Returns: locator where the diffs between expected and actual images can be | 343 Returns: locator where the diffs between expected and actual images can be |
| 325 found | 344 found |
| 326 """ | 345 """ |
| 327 return "%s-vs-%s" % (expected_image_locator, actual_image_locator) | 346 return "%s-vs-%s" % (expected_image_locator, actual_image_locator) |
| OLD | NEW |