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