Home | History | Annotate | Download | only in chameleon
      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