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