1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
   13
   14
   15
   16
   17
   18
   19
   20
   21
   22
   23
   24
   25
   26
   27
   28
   29
   30
   31
   32
   33
   34
   35
   36
   37
   38
   39
   40
   41
   42
   43
   44
   45
   46
   47
   48
   49
   50
   51
   52
   53
   54
   55
   56
   57
   58
   59
   60
   61
   62
   63
   64
   65
   66
   67
   68
   69
   70
   71
   72
   73
   74
   75
   76
   77
   78
   79
   80
   81
   82
   83
   84
   85
   86
   87
   88
   89
   90
   91
   92
   93
   94
   95
   96
   97
   98
   99
  100
  101
  102
  103
  104
  105
  106
  107
  108
  109
  110
  111
  112
  113
  114
  115
  116
  117
  118
  119
  120
  121
  122
  123
  124
  125
  126
  127
  128
  129
  130
  131
  132
  133
  134
  135
  136
  137
  138
  139
  140
  141
  142
  143
  144
  145
  146
  147
  148
  149
  150
  151
  152
  153
  154
  155
  156
  157
  158
  159
  160
  161
  162
  163
  164
  165
  166
  167
  168
  169
  170
  171
  172
  173
  174
  175
  176
  177
  178
  179
  180
  181
  182
  183
  184
  185
  186
  187
  188
  189
  190
  191
  192
  193
  194
  195
  196
  197
  198
  199
  200
  201

content / test / gpu / gpu_tests / skia_gold_matching_algorithms.py [blame]

# Copyright 2020 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Classes related to the possible matching algorithms for Skia Gold."""

import math
from typing import List, Optional, Union


class Parameters():
  """Constants for Skia Gold algorithm parameters.

  These correspond to the constants defined in goldctl's
  imgmatching/constants.go.
  """
  # The max number of pixels in an image that can differ and still allow the
  # fuzzy comparison to pass.
  MAX_DIFFERENT_PIXELS = 'fuzzy_max_different_pixels'
  # The max RGBA sum difference between two pixels that is still considered
  # valid. For example, if a pixel differs by (1, 2, 3, 0), then the threshold
  # would need to be 6 or higher in order for the fuzzy comparison to pass.
  # Mutually exclusive with PIXEL_PER_CHANNEL_DELTA_THRESHOLD.
  PIXEL_DELTA_THRESHOLD = 'fuzzy_pixel_delta_threshold'
  # The max per-channel RGBA difference between two pixels that is still
  # considered valid. For example, if a pixel differs by (1, 2, 3, 0), then the
  # threshold would need to be 3 or higher in order for the fuzzy comparison to
  # pass. Mutually exclusive with PIXEL_DELTA_THRESHOLD.
  PIXEL_PER_CHANNEL_DELTA_THRESHOLD = 'fuzzy_pixel_per_channel_delta_threshold'
  # How many pixels along the border of the image to ignore. 0 is typical for
  # most tests. 1 is useful for tests that have edges that go all the way to the
  # borders of the image, as Sobel filters do not get applied to pixels that are
  # on the image border. Larger values are supported, but don't have much of a
  # use case.
  IGNORED_BORDER_THICKNESS = 'fuzzy_ignored_border_thickness'
  # A number in the range [0, 255] specifying how much of an image should be
  # blacked out when using a Sobel filter. 0 results in the most pixels being
  # blacked out, while 255 results in no pixels being blacked out, i.e. no
  # Sobel filter functionality.
  EDGE_THRESHOLD = 'sobel_edge_threshold'
  # A number in the range [1, sqrt(MAX_INT32)] specifying how large the sample
  # area should be for the sample_area algorithm.
  SAMPLE_AREA_WIDTH = 'sample_area_width'
  # A number in the range [0, SAMPLE_AREA_WIDTH^2] specifying how many pixels
  # are allowed to differ in the sample area and not cause the matching to fail
  # in the sample_area algorithm.
  SAMPLE_AREA_MAX_DIFFERENT_PIXELS_PER_AREA = (
      'sample_area_max_different_pixels_per_area')
  # An optional number in the range [0, 255] specifying how much a pair of
  # pixels between the two images can differ on a single channel and still be
  # considered identical when using the sample_area algorithm.
  SAMPLE_AREA_CHANNEL_DELTA_THRESHOLD = 'sample_area_channel_delta_threshold'


class SkiaGoldMatchingAlgorithm():
  ALGORITHM_KEY = 'image_matching_algorithm'
  """Abstract base class for all algorithms."""

  def GetCmdline(self) -> List[str]:
    """Gets command line parameters for the algorithm.

    Returns:
      A list of strings representing the algorithm's parameters. The returned
      list is suitable for extending an existing goldctl imgtest add
      commandline, which will cause goldctl to use the specified algorithm
      instead of the default.
    """
    return _GenerateOptionalKey(SkiaGoldMatchingAlgorithm.ALGORITHM_KEY,
                                self.Name())

  def Name(self) -> str:
    """Returns a string representation of the algorithm."""
    raise NotImplementedError()


class ExactMatchingAlgorithm(SkiaGoldMatchingAlgorithm):
  """Class for the default exact matching algorithm in Gold."""

  def GetCmdline(self) -> List[str]:
    return []

  def Name(self) -> str:
    return 'exact'


class FuzzyMatchingAlgorithm(SkiaGoldMatchingAlgorithm):
  """Class for the fuzzy matching algorithm in Gold."""

  def __init__(self,
               max_different_pixels: int,
               pixel_delta_threshold: int = 0,
               pixel_per_channel_delta_threshold: int = 0,
               ignored_border_thickness: int = 0):
    super().__init__()
    assert max_different_pixels >= 0
    assert pixel_delta_threshold >= 0
    assert pixel_per_channel_delta_threshold >= 0
    assert not (pixel_delta_threshold > 0
                and pixel_per_channel_delta_threshold > 0)
    assert ignored_border_thickness >= 0
    self._max_different_pixels = max_different_pixels
    self._pixel_delta_threshold = pixel_delta_threshold
    self._pixel_per_channel_delta_threshold = pixel_per_channel_delta_threshold
    self._ignored_border_thickness = ignored_border_thickness

  def GetCmdline(self) -> List[str]:
    retval = super().GetCmdline()
    retval.extend(
        _GenerateOptionalKey(Parameters.MAX_DIFFERENT_PIXELS,
                             self._max_different_pixels))
    if self._pixel_delta_threshold:
      retval.extend(
          _GenerateOptionalKey(Parameters.PIXEL_DELTA_THRESHOLD,
                               self._pixel_delta_threshold))
    if self._pixel_per_channel_delta_threshold:
      retval.extend(
          _GenerateOptionalKey(Parameters.PIXEL_PER_CHANNEL_DELTA_THRESHOLD,
                               self._pixel_per_channel_delta_threshold))
    retval.extend(
        _GenerateOptionalKey(Parameters.IGNORED_BORDER_THICKNESS,
                             self._ignored_border_thickness))
    return retval

  def Name(self) -> str:
    return 'fuzzy'


class SobelMatchingAlgorithm(FuzzyMatchingAlgorithm):
  """Class for the Sobel filter matching algorithm in Gold.

  Technically a superset of the fuzzy matching algorithm.
  """

  def __init__(self, edge_threshold: int, *args, **kwargs):
    super().__init__(*args, **kwargs)
    assert int(edge_threshold) >= 0
    assert int(edge_threshold) <= 255
    if edge_threshold == 255:
      raise RuntimeError(
          'Sobel matching with edge threshold set to 255 is the same as fuzzy '
          'matching.')
    self._edge_threshold = edge_threshold

  def GetCmdline(self) -> List[str]:
    retval = super().GetCmdline()
    retval.extend(
        _GenerateOptionalKey(Parameters.EDGE_THRESHOLD, self._edge_threshold))
    return retval

  def Name(self) -> str:
    return 'sobel'


def _GenerateOptionalKey(key: str, value: Union[int, str]) -> List[str]:
  return ['--add-test-optional-key', '%s:%s' % (key, value)]


class SampleAreaMatchingAlgorithm(SkiaGoldMatchingAlgorithm):
  """Class for the sample_area matching algorithm in Gold."""

  def __init__(self,
               sample_area_width: int,
               max_different_pixels_per_area: int,
               sample_area_channel_delta_threshold: Optional[int] = None):
    super().__init__()
    assert sample_area_width >= 1
    assert sample_area_width <= math.sqrt(2**31 - 1)
    assert max_different_pixels_per_area >= 0
    assert max_different_pixels_per_area <= sample_area_width**2
    if max_different_pixels_per_area == sample_area_width**2:
      raise RuntimeError(
          'sample_area matching with a max different pixels per area set to '
          'the sample area size is equivalent to auto-approving any image.')
    if sample_area_channel_delta_threshold is not None:
      assert sample_area_channel_delta_threshold >= 0
      assert sample_area_channel_delta_threshold <= 255
      if sample_area_channel_delta_threshold == 255:
        raise RuntimeError(
            'sample area matching with a tolerance of 255 is equivalent to '
            'auto-approving any image.')
    self._sample_area_width = sample_area_width
    self._max_different_pixels_per_area = max_different_pixels_per_area
    self._sample_area_channel_delta_threshold = (
        sample_area_channel_delta_threshold)

  def GetCmdline(self) -> List[str]:
    retval = super().GetCmdline()
    retval.extend(
        _GenerateOptionalKey(Parameters.SAMPLE_AREA_WIDTH,
                             self._sample_area_width))
    retval.extend(
        _GenerateOptionalKey(
            Parameters.SAMPLE_AREA_MAX_DIFFERENT_PIXELS_PER_AREA,
            self._max_different_pixels_per_area))
    if self._sample_area_channel_delta_threshold is not None:
      retval.extend(
          _GenerateOptionalKey(Parameters.SAMPLE_AREA_CHANNEL_DELTA_THRESHOLD,
                               self._sample_area_channel_delta_threshold))
    return retval

  def Name(self) -> str:
    return 'sample_area'