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