xref: /aosp_15_r20/external/autotest/client/cros/image_comparison/pdiff_image_comparer.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1# Lint as: python2, python3
2# Copyright 2015 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
6import errno
7from PIL import Image
8import logging
9import subprocess
10import tempfile
11
12from autotest_lib.client.cros.image_comparison import comparison_result
13from autotest_lib.client.cros.video import method_logger
14
15
16class PdiffImageComparer(object):
17    """
18    Compares two images using ChromeOS' perceptualdiff binary.
19
20    """
21
22    @method_logger.log
23    def compare(self, golden_img_path, test_img_path, box=None):
24        """
25        Compares a test image against the specified golden image using the
26        terminal tool 'perceptualdiff'.
27
28        @param golden_img_path: path, complete path to a golden image.
29        @param test_img_path: path, complete path to a test image.
30        @param box: int tuple, left, upper, right, lower pixel coordinates.
31                    Defines the rectangular boundary within which to compare.
32        @return: int, number of pixels that are different.
33        @raise : Whatever _pdiff_compare raises.
34
35        """
36        if not box:
37            return self._pdiff_compare(golden_img_path, test_img_path)
38
39        ext = '.png'
40        tmp_golden_img_file = tempfile.NamedTemporaryFile(suffix=ext)
41        tmp_test_img_file = tempfile.NamedTemporaryFile(suffix=ext)
42
43        with tmp_golden_img_file, tmp_test_img_file:
44            tmp_golden_img_path = tmp_golden_img_file.name
45            tmp_test_img_path = tmp_test_img_file.name
46
47            Image.open(golden_img_path).crop(box).save(tmp_golden_img_path)
48            Image.open(test_img_path).crop(box).save(tmp_test_img_path)
49
50            return self._pdiff_compare(tmp_golden_img_path, tmp_test_img_path)
51
52
53    def _pdiff_compare(self, golden_img_path, test_img_path):
54        """
55        Invokes perceptualdiff using subprocess tools.
56
57        @param golden_img_path: path, complete path to a golden image.
58        @param test_img_path: path, complete path to a test image.
59        @return: int, number of pixels that are different.
60        @raise ValueError if image dimensions are not the same.
61        @raise OSError: if file does not exist or can not be opened.
62
63        """
64
65        # TODO mussa: Could downsampling the images be good for us?
66
67        tmp_diff_file = tempfile.NamedTemporaryFile(suffix='.png', delete=False)
68        args = ['perceptualdiff', golden_img_path, test_img_path, '-output',
69                tmp_diff_file.name]
70
71        logging.debug("Start process with args : " + str(args))
72
73        p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
74
75        output = p.communicate()
76        logging.debug('output of perceptual diff command is')
77        logging.debug(output)
78
79        stdoutdata = output[0]
80
81        mismatch_error = "Image dimensions do not match"
82        diff_message = "Images are visibly different"
83        filenotfound_message = "Cannot open"
84
85        #TODO(dhaddock): Check for image not created
86        if p.returncode == 0:
87            # pdiff exited with 0, images were the same
88            return comparison_result.ComparisonResult(0, '', None)
89
90        if mismatch_error in stdoutdata:
91            raise ValueError("pdiff says: " + stdoutdata)
92
93        if diff_message in stdoutdata:
94            diff_pixels = [int(s) for s in stdoutdata.split() if s.isdigit()][0]
95            return comparison_result.ComparisonResult(int(diff_pixels), '',
96                                                      tmp_diff_file.name)
97
98        if filenotfound_message in stdoutdata:
99            raise OSError(errno.ENOENT, "pdiff says: " + stdoutdata)
100
101        raise RuntimeError("Unknown result from pdiff: "
102                           "Output : " + stdoutdata)
103
104    def __enter__(self):
105        return self
106
107    def __exit__(self, exc_type, exc_val, exc_tb):
108        pass
109