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

Side by Side Diff: gm/rebaseline_server/imagediffdb.py

Issue 178253010: rebaseline_server: use new intermediate JSON format (Closed) Base URL: https://skia.googlesource.com/skia.git@master
Patch Set: incorporate Ravi's suggestions Created 6 years, 10 months 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 unified diff | Download patch
« no previous file with comments | « gm/rebaseline_server/column.py ('k') | gm/rebaseline_server/imagepair.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/python 1 #!/usr/bin/python
2 2
3 """ 3 """
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 """
(...skipping 25 matching lines...) Expand all
36 DEFAULT_IMAGE_SUFFIX = '.png' 36 DEFAULT_IMAGE_SUFFIX = '.png'
37 DEFAULT_IMAGES_SUBDIR = 'images' 37 DEFAULT_IMAGES_SUBDIR = 'images'
38 38
39 DISALLOWED_FILEPATH_CHAR_REGEX = re.compile('[^\w\-]') 39 DISALLOWED_FILEPATH_CHAR_REGEX = re.compile('[^\w\-]')
40 40
41 DIFFS_SUBDIR = 'diffs' 41 DIFFS_SUBDIR = 'diffs'
42 WHITEDIFFS_SUBDIR = 'whitediffs' 42 WHITEDIFFS_SUBDIR = 'whitediffs'
43 43
44 VALUES_PER_BAND = 256 44 VALUES_PER_BAND = 256
45 45
46 # Keys used within DiffRecord dictionary representations.
47 # NOTE: Keep these in sync with static/constants.js
48 KEY__DIFFERENCE_DATA__MAX_DIFF_PER_CHANNEL = 'maxDiffPerChannel'
49 KEY__DIFFERENCE_DATA__NUM_DIFF_PIXELS = 'numDifferingPixels'
50 KEY__DIFFERENCE_DATA__PERCENT_DIFF_PIXELS = 'percentDifferingPixels'
51 KEY__DIFFERENCE_DATA__PERCEPTUAL_DIFF = 'perceptualDifference'
52 KEY__DIFFERENCE_DATA__WEIGHTED_DIFF = 'weightedDiffMeasure'
53
46 54
47 class DiffRecord(object): 55 class DiffRecord(object):
48 """ Record of differences between two images. """ 56 """ Record of differences between two images. """
49 57
50 def __init__(self, storage_root, 58 def __init__(self, storage_root,
51 expected_image_url, expected_image_locator, 59 expected_image_url, expected_image_locator,
52 actual_image_url, actual_image_locator, 60 actual_image_url, actual_image_locator,
53 expected_images_subdir=DEFAULT_IMAGES_SUBDIR, 61 expected_images_subdir=DEFAULT_IMAGES_SUBDIR,
54 actual_images_subdir=DEFAULT_IMAGES_SUBDIR, 62 actual_images_subdir=DEFAULT_IMAGES_SUBDIR,
55 image_suffix=DEFAULT_IMAGE_SUFFIX): 63 image_suffix=DEFAULT_IMAGE_SUFFIX):
(...skipping 123 matching lines...) Expand 10 before | Expand all | Expand 10 after
179 187
180 def get_max_diff_per_channel(self): 188 def get_max_diff_per_channel(self):
181 """Returns the maximum difference between the expected and actual images 189 """Returns the maximum difference between the expected and actual images
182 for each R/G/B channel, as a list.""" 190 for each R/G/B channel, as a list."""
183 return self._max_diff_per_channel 191 return self._max_diff_per_channel
184 192
185 def as_dict(self): 193 def as_dict(self):
186 """Returns a dictionary representation of this DiffRecord, as needed when 194 """Returns a dictionary representation of this DiffRecord, as needed when
187 constructing the JSON representation.""" 195 constructing the JSON representation."""
188 return { 196 return {
189 'numDifferingPixels': self._num_pixels_differing, 197 KEY__DIFFERENCE_DATA__NUM_DIFF_PIXELS: self._num_pixels_differing,
190 'percentDifferingPixels': self.get_percent_pixels_differing(), 198 KEY__DIFFERENCE_DATA__PERCENT_DIFF_PIXELS:
191 'weightedDiffMeasure': self.get_weighted_diff_measure(), 199 self.get_percent_pixels_differing(),
192 'maxDiffPerChannel': self._max_diff_per_channel, 200 KEY__DIFFERENCE_DATA__WEIGHTED_DIFF: self.get_weighted_diff_measure(),
201 KEY__DIFFERENCE_DATA__MAX_DIFF_PER_CHANNEL: self._max_diff_per_channel,
202 KEY__DIFFERENCE_DATA__PERCEPTUAL_DIFF: self._perceptual_difference,
193 } 203 }
194 204
195 205
196 class ImageDiffDB(object): 206 class ImageDiffDB(object):
197 """ Calculates differences between image pairs, maintaining a database of 207 """ Calculates differences between image pairs, maintaining a database of
198 them for download.""" 208 them for download."""
199 209
200 def __init__(self, storage_root): 210 def __init__(self, storage_root):
201 """ 211 """
202 Args: 212 Args:
(...skipping 76 matching lines...) Expand 10 before | Expand all | Expand 10 after
279 # different pixel by the square of its delta value. (The more different 289 # different pixel by the square of its delta value. (The more different
280 # a pixel is from its expectation, the more we care about it.) 290 # a pixel is from its expectation, the more we care about it.)
281 assert(len(histogram) % VALUES_PER_BAND == 0) 291 assert(len(histogram) % VALUES_PER_BAND == 0)
282 num_bands = len(histogram) / VALUES_PER_BAND 292 num_bands = len(histogram) / VALUES_PER_BAND
283 max_diff = num_pixels * num_bands * (VALUES_PER_BAND - 1)**2 293 max_diff = num_pixels * num_bands * (VALUES_PER_BAND - 1)**2
284 total_diff = 0 294 total_diff = 0
285 for index in xrange(len(histogram)): 295 for index in xrange(len(histogram)):
286 total_diff += histogram[index] * (index % VALUES_PER_BAND)**2 296 total_diff += histogram[index] * (index % VALUES_PER_BAND)**2
287 return float(100 * total_diff) / max_diff 297 return float(100 * total_diff) / max_diff
288 298
299
289 def _max_per_band(histogram): 300 def _max_per_band(histogram):
290 """Given the histogram of an image, return the maximum value of each band 301 """Given the histogram of an image, return the maximum value of each band
291 (a.k.a. "color channel", such as R/G/B) across the entire image. 302 (a.k.a. "color channel", such as R/G/B) across the entire image.
292 303
293 Args: 304 Args:
294 histogram: PIL histogram 305 histogram: PIL histogram
295 306
296 Returns the maximum value of each band within the image histogram, as a list. 307 Returns the maximum value of each band within the image histogram, as a list.
297 """ 308 """
298 max_per_band = [] 309 max_per_band = []
299 assert(len(histogram) % VALUES_PER_BAND == 0) 310 assert(len(histogram) % VALUES_PER_BAND == 0)
300 num_bands = len(histogram) / VALUES_PER_BAND 311 num_bands = len(histogram) / VALUES_PER_BAND
301 for band in xrange(num_bands): 312 for band in xrange(num_bands):
302 # Assuming that VALUES_PER_BAND is 256... 313 # Assuming that VALUES_PER_BAND is 256...
303 # the 'R' band makes up indices 0-255 in the histogram, 314 # the 'R' band makes up indices 0-255 in the histogram,
304 # the 'G' band makes up indices 256-511 in the histogram, 315 # the 'G' band makes up indices 256-511 in the histogram,
305 # etc. 316 # etc.
306 min_index = band * VALUES_PER_BAND 317 min_index = band * VALUES_PER_BAND
307 index = min_index + VALUES_PER_BAND 318 index = min_index + VALUES_PER_BAND
308 while index > min_index: 319 while index > min_index:
309 index -= 1 320 index -= 1
310 if histogram[index] > 0: 321 if histogram[index] > 0:
311 max_per_band.append(index - min_index) 322 max_per_band.append(index - min_index)
312 break 323 break
313 return max_per_band 324 return max_per_band
314 325
326
315 def _generate_image_diff(image1, image2): 327 def _generate_image_diff(image1, image2):
316 """Wrapper for ImageChops.difference(image1, image2) that will handle some 328 """Wrapper for ImageChops.difference(image1, image2) that will handle some
317 errors automatically, or at least yield more useful error messages. 329 errors automatically, or at least yield more useful error messages.
318 330
319 TODO(epoger): Currently, some of the images generated by the bots are RGBA 331 TODO(epoger): Currently, some of the images generated by the bots are RGBA
320 and others are RGB. I'm not sure why that is. For now, to avoid confusion 332 and others are RGB. I'm not sure why that is. For now, to avoid confusion
321 within the UI, convert all to RGB when diffing. 333 within the UI, convert all to RGB when diffing.
322 334
323 Args: 335 Args:
324 image1: a PIL image object 336 image1: a PIL image object
325 image2: a PIL image object 337 image2: a PIL image object
326 338
327 Returns: per-pixel diffs between image1 and image2, as a PIL image object 339 Returns: per-pixel diffs between image1 and image2, as a PIL image object
328 """ 340 """
329 try: 341 try:
330 return ImageChops.difference(image1.convert('RGB'), image2.convert('RGB')) 342 return ImageChops.difference(image1.convert('RGB'), image2.convert('RGB'))
331 except ValueError: 343 except ValueError:
332 logging.error('Error diffing image1 [%s] and image2 [%s].' % ( 344 logging.error('Error diffing image1 [%s] and image2 [%s].' % (
333 repr(image1), repr(image2))) 345 repr(image1), repr(image2)))
334 raise 346 raise
335 347
348
336 def _download_and_open_image(local_filepath, url): 349 def _download_and_open_image(local_filepath, url):
337 """Open the image at local_filepath; if there is no file at that path, 350 """Open the image at local_filepath; if there is no file at that path,
338 download it from url to that path and then open it. 351 download it from url to that path and then open it.
339 352
340 Args: 353 Args:
341 local_filepath: path on local disk where the image should be stored 354 local_filepath: path on local disk where the image should be stored
342 url: URL from which we can download the image if we don't have it yet 355 url: URL from which we can download the image if we don't have it yet
343 356
344 Returns: a PIL image object 357 Returns: a PIL image object
345 """ 358 """
346 if not os.path.exists(local_filepath): 359 if not os.path.exists(local_filepath):
347 _mkdir_unless_exists(os.path.dirname(local_filepath)) 360 _mkdir_unless_exists(os.path.dirname(local_filepath))
348 with contextlib.closing(urllib.urlopen(url)) as url_handle: 361 with contextlib.closing(urllib.urlopen(url)) as url_handle:
349 with open(local_filepath, 'wb') as file_handle: 362 with open(local_filepath, 'wb') as file_handle:
350 shutil.copyfileobj(fsrc=url_handle, fdst=file_handle) 363 shutil.copyfileobj(fsrc=url_handle, fdst=file_handle)
351 return _open_image(local_filepath) 364 return _open_image(local_filepath)
352 365
366
353 def _open_image(filepath): 367 def _open_image(filepath):
354 """Wrapper for Image.open(filepath) that yields more useful error messages. 368 """Wrapper for Image.open(filepath) that yields more useful error messages.
355 369
356 Args: 370 Args:
357 filepath: path on local disk to load image from 371 filepath: path on local disk to load image from
358 372
359 Returns: a PIL image object 373 Returns: a PIL image object
360 """ 374 """
361 try: 375 try:
362 return Image.open(filepath) 376 return Image.open(filepath)
363 except IOError: 377 except IOError:
364 logging.error('IOError loading image file %s' % filepath) 378 logging.error('IOError loading image file %s' % filepath)
365 raise 379 raise
366 380
381
367 def _save_image(image, filepath, format='PNG'): 382 def _save_image(image, filepath, format='PNG'):
368 """Write an image to disk, creating any intermediate directories as needed. 383 """Write an image to disk, creating any intermediate directories as needed.
369 384
370 Args: 385 Args:
371 image: a PIL image object 386 image: a PIL image object
372 filepath: path on local disk to write image to 387 filepath: path on local disk to write image to
373 format: one of the PIL image formats, listed at 388 format: one of the PIL image formats, listed at
374 http://effbot.org/imagingbook/formats.htm 389 http://effbot.org/imagingbook/formats.htm
375 """ 390 """
376 _mkdir_unless_exists(os.path.dirname(filepath)) 391 _mkdir_unless_exists(os.path.dirname(filepath))
377 image.save(filepath, format) 392 image.save(filepath, format)
378 393
394
379 def _mkdir_unless_exists(path): 395 def _mkdir_unless_exists(path):
380 """Unless path refers to an already-existing directory, create it. 396 """Unless path refers to an already-existing directory, create it.
381 397
382 Args: 398 Args:
383 path: path on local disk 399 path: path on local disk
384 """ 400 """
385 if not os.path.isdir(path): 401 if not os.path.isdir(path):
386 os.makedirs(path) 402 os.makedirs(path)
387 403
404
388 def _sanitize_locator(locator): 405 def _sanitize_locator(locator):
389 """Returns a sanitized version of a locator (one in which we know none of the 406 """Returns a sanitized version of a locator (one in which we know none of the
390 characters will have special meaning in filenames). 407 characters will have special meaning in filenames).
391 408
392 Args: 409 Args:
393 locator: string, or something that can be represented as a string 410 locator: string, or something that can be represented as a string
394 """ 411 """
395 return DISALLOWED_FILEPATH_CHAR_REGEX.sub('_', str(locator)) 412 return DISALLOWED_FILEPATH_CHAR_REGEX.sub('_', str(locator))
396 413
414
397 def _get_difference_locator(expected_image_locator, actual_image_locator): 415 def _get_difference_locator(expected_image_locator, actual_image_locator):
398 """Returns the locator string used to look up the diffs between expected_image 416 """Returns the locator string used to look up the diffs between expected_image
399 and actual_image. 417 and actual_image.
400 418
419 We must keep this function in sync with getImageDiffRelativeUrl() in
420 static/loader.js
421
401 Args: 422 Args:
402 expected_image_locator: locator string pointing at expected image 423 expected_image_locator: locator string pointing at expected image
403 actual_image_locator: locator string pointing at actual image 424 actual_image_locator: locator string pointing at actual image
404 425
405 Returns: already-sanitized locator where the diffs between expected and 426 Returns: already-sanitized locator where the diffs between expected and
406 actual images can be found 427 actual images can be found
407 """ 428 """
408 return "%s-vs-%s" % (_sanitize_locator(expected_image_locator), 429 return "%s-vs-%s" % (_sanitize_locator(expected_image_locator),
409 _sanitize_locator(actual_image_locator)) 430 _sanitize_locator(actual_image_locator))
OLDNEW
« no previous file with comments | « gm/rebaseline_server/column.py ('k') | gm/rebaseline_server/imagepair.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698