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