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 |