Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(382)

Unified Diff: gm/rebaseline_server/imagediffdb.py

Issue 82823005: rebaseline_server: improve pixel diff reporting (Closed) Base URL: http://skia.googlecode.com/svn/trunk/
Patch Set: partially_deobfuscate Created 7 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | gm/rebaseline_server/imagediffdb_test.py » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: gm/rebaseline_server/imagediffdb.py
===================================================================
--- gm/rebaseline_server/imagediffdb.py (revision 12361)
+++ gm/rebaseline_server/imagediffdb.py (working copy)
@@ -21,14 +21,14 @@
+ 'http://www.pythonware.com/products/pil/')
IMAGE_SUFFIX = '.png'
-IMAGE_FORMAT = 'PNG' # must match one of the PIL image formats, listed at
- # http://effbot.org/imagingbook/formats.htm
IMAGES_SUBDIR = 'images'
DIFFS_SUBDIR = 'diffs'
WHITEDIFFS_SUBDIR = 'whitediffs'
+VALUES_PER_BAND = 256
+
class DiffRecord(object):
""" Record of differences between two images. """
@@ -65,33 +65,43 @@
str(actual_image_locator) + IMAGE_SUFFIX),
actual_image_url)
- # Store the diff image (absolute diff at each pixel).
+ # Generate the diff image (absolute diff at each pixel) and
+ # max_diff_per_channel.
diff_image = _generate_image_diff(actual_image, expected_image)
- self._weighted_diff_measure = _calculate_weighted_diff_metric(diff_image)
+ diff_histogram = diff_image.histogram()
+ (diff_width, diff_height) = diff_image.size
+ self._weighted_diff_measure = _calculate_weighted_diff_metric(
+ diff_histogram, diff_width * diff_height)
+ self._max_diff_per_channel = _max_per_band(diff_histogram)
+
+ # Generate the whitediff image (any differing pixels show as white).
+ # This is tricky, because when you convert color images to grayscale or
+ # black & white in PIL, it has its own ideas about thresholds.
+ # We have to force it: if a pixel has any color at all, it's a '1'.
+ bands = diff_image.split()
+ graydiff_image = ImageChops.lighter(ImageChops.lighter(
+ bands[0], bands[1]), bands[2])
+ whitediff_image = (graydiff_image.point(lambda p: p > 0 and VALUES_PER_BAND)
+ .convert('1', dither=Image.NONE))
+
+ # Final touches on diff_image: use whitediff_image as an alpha mask.
+ # Unchanged pixels are transparent; differing pixels are opaque.
+ diff_image.putalpha(whitediff_image)
+
+ # Store the diff and whitediff images generated above.
diff_image_locator = _get_difference_locator(
expected_image_locator=expected_image_locator,
actual_image_locator=actual_image_locator)
- diff_image_filepath = os.path.join(
- storage_root, DIFFS_SUBDIR, str(diff_image_locator) + IMAGE_SUFFIX)
- _mkdir_unless_exists(os.path.join(storage_root, DIFFS_SUBDIR))
- diff_image.save(diff_image_filepath, IMAGE_FORMAT)
+ basename = str(diff_image_locator) + IMAGE_SUFFIX
+ _save_image(diff_image, os.path.join(
+ storage_root, DIFFS_SUBDIR, basename))
+ _save_image(whitediff_image, os.path.join(
+ storage_root, WHITEDIFFS_SUBDIR, basename))
- # Store the whitediff image (any differing pixels show as white).
- #
- # TODO(epoger): From http://effbot.org/imagingbook/image.htm , it seems
- # like we should be able to use im.point(function, mode) to perform both
- # the point() and convert('1') operations simultaneously, but I couldn't
- # get it to work.
- whitediff_image = (diff_image.point(lambda p: (0, 256)[p!=0])
- .convert('1'))
- whitediff_image_filepath = os.path.join(
- storage_root, WHITEDIFFS_SUBDIR, str(diff_image_locator) + IMAGE_SUFFIX)
- _mkdir_unless_exists(os.path.join(storage_root, WHITEDIFFS_SUBDIR))
- whitediff_image.save(whitediff_image_filepath, IMAGE_FORMAT)
-
# Calculate difference metrics.
(self._width, self._height) = diff_image.size
- self._num_pixels_differing = whitediff_image.histogram()[255]
+ self._num_pixels_differing = (
+ whitediff_image.histogram()[VALUES_PER_BAND - 1])
def get_num_pixels_differing(self):
"""Returns the absolute number of pixels that differ."""
@@ -108,7 +118,12 @@
(inclusive)."""
return self._weighted_diff_measure
+ def get_max_diff_per_channel(self):
+ """Returns the maximum difference between the expected and actual images
+ for each R/G/B channel, as a list."""
+ return self._max_diff_per_channel
+
class ImageDiffDB(object):
""" Calculates differences between image pairs, maintaining a database of
them for download."""
@@ -175,27 +190,56 @@
# Utility functions
-def _calculate_weighted_diff_metric(image):
- """Given a diff image (per-channel diff at each pixel between two images),
- calculate the weighted diff metric (a stab at how different the two images
- really are).
+def _calculate_weighted_diff_metric(histogram, num_pixels):
+ """Given the histogram of a diff image (per-channel diff at each
+ pixel between two images), calculate the weighted diff metric (a
+ stab at how different the two images really are).
Args:
- image: PIL image; a per-channel diff between two images
+ histogram: PIL histogram of a per-channel diff between two images
+ num_pixels: integer; the total number of pixels in the diff image
Returns: a weighted diff metric, as a float between 0 and 100 (inclusive).
"""
- # TODO(epoger): This is just a wild guess at an appropriate metric.
+ # TODO(epoger): As a wild guess at an appropriate metric, weight each
+ # different pixel by the square of its delta value. (The more different
+ # a pixel is from its expectation, the more we care about it.)
# In the long term, we will probably use some metric generated by
# skpdiff anyway.
- (width, height) = image.size
- maxdiff = 3 * (width * height) * 255**2
- h = image.histogram()
- assert(len(h) % 256 == 0)
- totaldiff = sum(map(lambda index,value: value * (index%256)**2,
- range(len(h)), h))
- return float(100 * totaldiff) / maxdiff
+ assert(len(histogram) % VALUES_PER_BAND == 0)
+ num_bands = len(histogram) / VALUES_PER_BAND
+ max_diff = num_pixels * num_bands * (VALUES_PER_BAND - 1)**2
+ total_diff = 0
+ for index in xrange(len(histogram)):
+ total_diff += histogram[index] * (index % VALUES_PER_BAND)**2
+ return float(100 * total_diff) / max_diff
+def _max_per_band(histogram):
+ """Given the histogram of an image, return the maximum value of each band
+ (a.k.a. "color channel", such as R/G/B) across the entire image.
+
+ Args:
+ histogram: PIL histogram
+
+ Returns the maximum value of each band within the image histogram, as a list.
+ """
+ max_per_band = []
+ assert(len(histogram) % VALUES_PER_BAND == 0)
+ num_bands = len(histogram) / VALUES_PER_BAND
+ for band in xrange(num_bands):
+ # Assuming that VALUES_PER_BAND is 256...
+ # the 'R' band makes up indices 0-255 in the histogram,
+ # the 'G' band makes up indices 256-511 in the histogram,
+ # etc.
+ min_index = band * VALUES_PER_BAND
+ index = min_index + VALUES_PER_BAND
+ while index > min_index:
+ index -= 1
+ if histogram[index] > 0:
+ max_per_band.append(index - min_index)
+ break
+ return max_per_band
+
def _generate_image_diff(image1, image2):
"""Wrapper for ImageChops.difference(image1, image2) that will handle some
errors automatically, or at least yield more useful error messages.
@@ -248,6 +292,18 @@
logging.error('IOError loading image file %s' % filepath)
raise
+def _save_image(image, filepath, format='PNG'):
+ """Write an image to disk, creating any intermediate directories as needed.
+
+ Args:
+ image: a PIL image object
+ filepath: path on local disk to write image to
+ format: one of the PIL image formats, listed at
+ http://effbot.org/imagingbook/formats.htm
+ """
+ _mkdir_unless_exists(os.path.dirname(filepath))
+ image.save(filepath, format)
+
def _mkdir_unless_exists(path):
"""Unless path refers to an already-existing directory, create it.
« no previous file with comments | « no previous file | gm/rebaseline_server/imagediffdb_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698