1 # Copyright 2014 The Chromium OS 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 """Classes to do screen comparison.""" 6 7 import logging 8 import os 9 import time 10 11 from PIL import ImageChops 12 13 14 class ScreenComparer(object): 15 """A class to compare two screens. 16 17 Calling its member method compare() does the comparison. 18 19 """ 20 21 def __init__(self, capturer1, capturer2, output_dir, pixel_diff_margin, 22 wrong_pixels_margin, skip_if_diff_sizes=False): 23 """Initializes the ScreenComparer objects. 24 25 @param capture1: The screen capturer object. 26 @param capture2: The screen capturer object. 27 @param output_dir: The directory for output images. 28 @param pixel_diff_margin: The margin for comparing a pixel. Only 29 if a pixel difference exceeds this margin, will treat as a wrong 30 pixel. Sets None means using default value by detecting 31 connector type. 32 @param wrong_pixels_margin: The percentage of margin for wrong pixels. 33 The value is in a closed interval [0.0, 1.0]. If the total 34 number of wrong pixels exceeds this margin, the check fails. 35 @param skip_if_diff_sizes: Skip the comparison if the image sizes are 36 different. Used in mirrored test as the internal and external 37 screens have different resolutions. 38 """ 39 # TODO(waihong): Support multiple capturers. 40 self._capturer1 = capturer1 41 self._capturer2 = capturer2 42 self._output_dir = output_dir 43 self._pixel_diff_margin = pixel_diff_margin 44 assert 0.0 <= wrong_pixels_margin <= 1.0 45 self._wrong_pixels_margin = wrong_pixels_margin 46 self._skip_if_diff_sizes = skip_if_diff_sizes 47 48 49 def compare(self): 50 """Compares the screens. 51 52 @return: None if the check passes; otherwise, a string of error message. 53 """ 54 tags = [self._capturer1.TAG, self._capturer2.TAG] 55 images = [self._capturer1.capture(), self._capturer2.capture()] 56 57 if None in images: 58 message = ('Failed to capture the screen of %s.' % 59 tags[images.index(None)]) 60 logging.error(message) 61 return message 62 63 # Sometimes the format of images got from X is not RGB, 64 # which may lead to ValueError raised by ImageChops.difference(). 65 # So here we check the format before comparing them. 66 for i, image in enumerate(images): 67 if image.mode != 'RGB': 68 images[i] = image.convert('RGB') 69 70 message = 'Unexpected exception' 71 time_str = time.strftime('%H%M%S') 72 try: 73 # The size property is the resolution of the image. 74 if images[0].size != images[1].size: 75 message = ('Sizes of images %s and %s do not match: ' 76 '%dx%d != %dx%d' % 77 (tuple(tags) + images[0].size + images[1].size)) 78 if self._skip_if_diff_sizes: 79 logging.info(message) 80 return None 81 else: 82 logging.error(message) 83 return message 84 85 size = images[0].size[0] * images[0].size[1] 86 max_acceptable_wrong_pixels = int(self._wrong_pixels_margin * size) 87 88 logging.info('Comparing the images between %s and %s...', *tags) 89 diff_image = ImageChops.difference(*images) 90 histogram = diff_image.convert('L').histogram() 91 92 num_wrong_pixels = sum(histogram[self._pixel_diff_margin + 1:]) 93 max_diff_value = max(filter( 94 lambda x: histogram[x], xrange(len(histogram)))) 95 if num_wrong_pixels > 0: 96 logging.debug('Histogram of difference: %r', histogram) 97 prefix_str = '%s-%dx%d' % ((time_str,) + images[0].size) 98 message = ('Result of %s: total %d wrong pixels ' 99 '(diff up to %d)' % ( 100 prefix_str, num_wrong_pixels, max_diff_value)) 101 if num_wrong_pixels > max_acceptable_wrong_pixels: 102 logging.error(message) 103 return message 104 105 message += (', within the acceptable range %d' % 106 max_acceptable_wrong_pixels) 107 logging.warning(message) 108 else: 109 logging.info('Result: all pixels match (within +/- %d)', 110 max_diff_value) 111 message = None 112 return None 113 finally: 114 if message is not None: 115 for i in (0, 1): 116 # Use time and image size as the filename prefix. 117 prefix_str = '%s-%dx%d' % ((time_str,) + images[i].size) 118 # TODO(waihong): Save to a better lossless format. 119 file_path = os.path.join( 120 self._output_dir, 121 '%s-%s.png' % (prefix_str, tags[i])) 122 logging.info('Output the image %d to %s', i, file_path) 123 images[i].save(file_path) 124 125 file_path = os.path.join( 126 self._output_dir, '%s-diff.png' % prefix_str) 127 logging.info('Output the diff image to %s', file_path) 128 diff_image = ImageChops.difference(*images) 129 gray_image = diff_image.convert('L') 130 bw_image = gray_image.point( 131 lambda x: 0 if x <= self._pixel_diff_margin else 255, 132 '1') 133 bw_image.save(file_path) 134