1c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch# Copyright 2013 The Chromium Authors. All rights reserved.
2c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch# Use of this source code is governed by a BSD-style license that can be
3c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch# found in the LICENSE file.
4c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
5c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch"""Utilities for performing pixel-by-pixel image comparision."""
6c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
7c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdochimport itertools
8c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdochimport StringIO
9c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdochfrom PIL import Image
10c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
11c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
12c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdochdef _AreTheSameSize(images):
13c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  """Returns whether a set of images are the size size.
14c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
15c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  Args:
16c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    images: a list of images to compare.
17c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
18c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  Returns:
19c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    boolean.
20c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
21c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  Raises:
22c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    Exception: One image or fewer is passed in.
23c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  """
24c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  if len(images) > 1:
25c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    return all(images[0].size == img.size for img in images[1:])
26c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  else:
27c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    raise Exception('No images passed in.')
28c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
29c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
30c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdochdef _GetDifferenceWithMask(image1, image2, mask=None,
31c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch                           masked_color=(225, 225, 225, 255),
32c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch                           same_color=(255, 255, 255, 255),
33c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch                           different_color=(210, 0, 0, 255)):
34c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  """Returns an image representing the difference between the two images.
35c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
36c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  This function computes the difference between two images taking into
37c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  account a mask if it is provided. The final three arguments represent
38c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  the coloration of the generated image.
39c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
40c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  Args:
41c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    image1: the first image to compare.
42c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    image2: the second image to compare.
43c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    mask: an optional mask image consisting of only black and white pixels
44c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch      where white pixels indicate the portion of the image to be masked out.
45c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    masked_color: the color of a masked section in the resulting image.
46c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    same_color: the color of an unmasked section that is the same.
47c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch      between images 1 and 2 in the resulting image.
48c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    different_color: the color of an unmasked section that is different
49c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch      between images 1 and 2 in the resulting image.
50c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
51c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  Returns:
52c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    A 2-tuple with an image representing the unmasked difference between the
53c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    two input images and the number of different pixels.
54c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
55c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  Raises:
56c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    Exception: if image1, image2, and mask are not the same size.
57c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  """
58c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  image_mask = mask
59c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  if not mask:
60c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    image_mask = Image.new('RGBA', image1.size, (0, 0, 0, 255))
61c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  if not _AreTheSameSize([image1, image2, image_mask]):
62c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    raise Exception('images and mask must be the same size.')
63c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  image_diff = Image.new('RGBA', image1.size, (0, 0, 0, 255))
64c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  data = []
65c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  diff_pixels = 0
66c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  for m, px1, px2 in itertools.izip(image_mask.getdata(),
67c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch                                    image1.getdata(),
68c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch                                    image2.getdata()):
69c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    if m == (255, 255, 255, 255):
70c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch      data.append(masked_color)
71c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    elif px1 == px2:
72c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch      data.append(same_color)
73c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    else:
74c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch      data.append(different_color)
75c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch      diff_pixels += 1
76c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
77c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  image_diff.putdata(data)
78c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  return (image_diff, diff_pixels)
79c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
80c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
81c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdochdef CreateMask(images):
82c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  """Computes a mask for a set of images.
83c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
84c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  Returns a difference mask that is computed from the images
85c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  which are passed in. The mask will have a white pixel
86c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  anywhere that the input images differ and a black pixel
87c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  everywhere else.
88c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
89c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  Args:
90c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    images: list of images to compute the mask from.
91c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
92c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  Returns:
93c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    an image of only black and white pixels where white pixels represent
94c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch      areas in the input images that have differences.
95c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
96c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  Raises:
97c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    Exception: if the images passed in are not of the same size.
98c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    Exception: if fewer than one image is passed in.
99c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  """
100c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  if not images:
101c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    raise Exception('mask must be created from one or more images.')
102c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  mask = Image.new('RGBA', images[0].size, (0, 0, 0, 255))
103c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  image = images[0]
104c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  for other_image in images[1:]:
105c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    mask = _GetDifferenceWithMask(
106c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch        image,
107c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch        other_image,
108c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch        mask,
109c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch        masked_color=(255, 255, 255, 255),
110c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch        same_color=(0, 0, 0, 255),
111c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch        different_color=(255, 255, 255, 255))[0]
112c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  return mask
113c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
114c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
115c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdochdef AddMasks(masks):
116c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  """Combines a list of mask images into one mask image.
117c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
118c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  Args:
119c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    masks: a list of mask-images.
120c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
121c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  Returns:
122c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    a new mask that represents the sum of the masked
123c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch      regions of the passed in list of mask-images.
124c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
125c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  Raises:
126c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    Exception: if masks is an empty list, or if masks are not the same size.
127c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  """
128c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  if not masks:
129c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    raise Exception('masks must be a list containing at least one image.')
130c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  if len(masks) > 1 and not _AreTheSameSize(masks):
131c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    raise Exception('masks in list must be of the same size.')
132c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  white = (255, 255, 255, 255)
133c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  black = (0, 0, 0, 255)
134c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  masks_data = [mask.getdata() for mask in masks]
135c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  image = Image.new('RGBA', masks[0].size, black)
136c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  image.putdata([white if white in px_set else black
137c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch                 for px_set in itertools.izip(*masks_data)])
138c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  return image
139c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
140c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
141c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdochdef ConvertDiffToMask(diff):
142c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  """Converts a Diff image into a Mask image.
143c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
144c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  Args:
145c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    diff: the diff image to convert.
146c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
147c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  Returns:
148c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    a new mask image where everything that was masked or different in the diff
149c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    is now masked.
150c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  """
151c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  white = (255, 255, 255, 255)
152c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  black = (0, 0, 0, 255)
153c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  diff_data = diff.getdata()
154c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  image = Image.new('RGBA', diff.size, black)
155c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  image.putdata([black if px == white else white for px in diff_data])
156c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  return image
157c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
158c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
159c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdochdef VisualizeImageDifferences(image1, image2, mask=None):
160c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  """Returns an image repesenting the unmasked differences between two images.
161c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
162c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  Iterates through the pixel values of two images and an optional
163c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  mask. If the pixel values are the same, or the pixel is masked,
164c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  (0,0,0) is stored for that pixel. Otherwise, (255,255,255) is stored.
165c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  This ultimately produces an image where unmasked differences between
166c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  the two images are white pixels, and everything else is black.
167c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
168c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  Args:
169c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    image1: an RGB image
170c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    image2: another RGB image of the same size as image1.
171c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    mask: an optional RGB image consisting of only white and black pixels
172c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch      where the white pixels represent the parts of the images to be masked
173c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch      out.
174c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
175c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  Returns:
176c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    A 2-tuple with an image representing the unmasked difference between the
177c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    two input images and the number of different pixels.
178c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
179c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  Raises:
180c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    Exception: if the two images and optional mask are different sizes.
181c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  """
182c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  return _GetDifferenceWithMask(image1, image2, mask)
183c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
184c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
185c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdochdef InflateMask(image, passes):
186c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  """A function that adds layers of pixels around the white edges of a mask.
187c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
188c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  This function evaluates a 'frontier' of valid pixels indices. Initially,
189c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    this frontier contains all indices in the image. However, with each pass
190c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    only the pixels' indices which were added to the mask by inflation
191c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    are added to the next pass's frontier. This gives the algorithm a
192c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    large upfront cost that scales negligably when the number of passes
193c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    is increased.
194c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
195c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  Args:
196c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    image: the RGBA PIL.Image mask to inflate.
197c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    passes: the number of passes to inflate the image by.
198c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
199c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  Returns:
200c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    A RGBA PIL.Image.
201c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  """
202c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  inflated = Image.new('RGBA', image.size)
203c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  new_dataset = list(image.getdata())
204c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  old_dataset = list(image.getdata())
205c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
206c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  frontier = set(range(len(old_dataset)))
207c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  new_frontier = set()
208c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
209c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  l = [-1, 1]
210c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
211c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  def _ShadeHorizontal(index, px):
212c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    col = index % image.size[0]
213c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    if px == (255, 255, 255, 255):
214c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch      for x in l:
215c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch        if 0 <= col + x < image.size[0]:
216c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch          if old_dataset[index + x] != (255, 255, 255, 255):
217c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch            new_frontier.add(index + x)
218c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch          new_dataset[index + x] = (255, 255, 255, 255)
219c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
220c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  def _ShadeVertical(index, px):
221c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    row = index / image.size[0]
222c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    if px == (255, 255, 255, 255):
223c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch      for x in l:
224c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch        if 0 <= row + x < image.size[1]:
225c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch          if old_dataset[index + image.size[0] * x] != (255, 255, 255, 255):
226c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch            new_frontier.add(index + image.size[0] * x)
227c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch          new_dataset[index + image.size[0] * x] = (255, 255, 255, 255)
228c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
229c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  for _ in range(passes):
230c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    for index in frontier:
231c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch      _ShadeHorizontal(index, old_dataset[index])
232c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch      _ShadeVertical(index, old_dataset[index])
233c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    old_dataset, new_dataset = new_dataset, new_dataset
234c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    frontier, new_frontier = new_frontier, set()
235c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  inflated.putdata(new_dataset)
236c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  return inflated
237c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
238c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
239c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdochdef TotalDifferentPixels(image1, image2, mask=None):
240c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  """Computes the number of different pixels between two images.
241c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
242c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  Args:
243c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    image1: the first RGB image to be compared.
244c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    image2: the second RGB image to be compared.
245c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    mask: an optional RGB image of only black and white pixels
246c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch      where white pixels indicate the parts of the image to be masked out.
247c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
248c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  Returns:
249c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    the number of differing pixels between the images.
250c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
251c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  Raises:
252c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    Exception: if the images to be compared and the mask are not the same size.
253c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  """
254c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  image_mask = mask
255c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  if not mask:
256c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    image_mask = Image.new('RGBA', image1.size, (0, 0, 0, 255))
257c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  if _AreTheSameSize([image1, image2, image_mask]):
258c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    total_diff = 0
259c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    for px1, px2, m in itertools.izip(image1.getdata(),
260c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch                                      image2.getdata(),
261c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch                                      image_mask.getdata()):
262c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch      if m == (255, 255, 255, 255):
263c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch        continue
264c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch      elif px1 != px2:
265c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch        total_diff += 1
266c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch      else:
267c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch        continue
268c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    return total_diff
269c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  else:
270c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    raise Exception('images and mask must be the same size')
271c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
272c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
273c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdochdef SameImage(image1, image2, mask=None):
274c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  """Returns a boolean representing whether the images are the same.
275c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
276c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  Returns a boolean indicating whether two images are similar
277c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  enough to be considered the same. Essentially wraps the
278c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  TotalDifferentPixels function.
279c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
280c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
281c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  Args:
282c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    image1: an RGB image to compare.
283c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    image2: an RGB image to compare.
284c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    mask: an optional image of only black and white pixels
285c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    where white pixels are masked out
286c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
287c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  Returns:
288c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    True if the images are similar, False otherwise.
289c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
290c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  Raises:
291c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    Exception: if the images (and mask) are different sizes.
292c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  """
293c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  different_pixels = TotalDifferentPixels(image1, image2, mask)
294c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  return different_pixels == 0
295c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
296c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
297c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdochdef EncodePNG(image):
298c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  """Returns the PNG file-contents of the image.
299c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
300c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  Args:
301c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    image: an RGB image to be encoded.
302c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
303c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  Returns:
304c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    a base64 encoded string representing the image.
305c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  """
306c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  f = StringIO.StringIO()
307c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  image.save(f, 'PNG')
308c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  encoded_image = f.getvalue()
309c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  f.close()
310c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  return encoded_image
311c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
312c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
313c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdochdef DecodePNG(png):
314c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  """Returns a RGB image from PNG file-contents.
315c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
316c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  Args:
317c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    encoded_image: PNG file-contents of an RGB image.
318c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch
319c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  Returns:
320c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch    an RGB image
321c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  """
322c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch  return Image.open(StringIO.StringIO(png))
323