xref: /aosp_15_r20/external/autotest/client/cros/chameleon/screen_comparison.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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