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 |