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
  202
  203
  204
  205
  206
  207
  208
  209
  210
  211
  212
  213
  214
  215
  216
  217
  218
  219
  220
  221
  222
  223
  224
  225
  226
  227
  228
  229
  230
  231
  232
  233
  234
  235
  236
  237

content / test / gpu / gold_inexact_matching / local_minima_parameter_optimizer.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.

from __future__ import print_function

import collections
import itertools
import logging
import sys
from typing import Dict

import gold_inexact_matching.iterative_parameter_optimizer\
    as iterative_optimizer
from gold_inexact_matching import common_typing as ct
from gold_inexact_matching import parameter_set


Sparse2DIntArray = Dict[int, Dict[int, int]]


class LocalMinimaParameterOptimizer(
    iterative_optimizer.IterativeParameterOptimizer):
  """A ParameterOptimizer to find local minima.

  Works on any number of variable parameters and is faster than brute
  forcing, but not guaranteed to find all interesting parameter combinations.
  """
  MIN_EDGE_THRESHOLD_WEIGHT = 0
  MIN_MAX_DIFF_WEIGHT = MIN_DELTA_THRESHOLD_WEIGHT = 0

  def __init__(self, args: ct.ParsedCmdArgs, test_name: str):
    super().__init__(args, test_name)
    # These are (or will be) maps of ints to maps of ints to ints, i.e. a 2D
    # array containing ints, just using maps instead of lists. They hold the
    # most permissive value visited so far that resulted in a comparison failure
    # for a particular parameter given the other two parameters. These are used
    # to prune combinations we don't care about, similar to skipping
    # combinations that produce a higher weight than our smallest.
    # Delta -> Edge -> Max Diff
    self._permissive_max_diff_map: Sparse2DIntArray = {}
    # Max Diff -> Edge -> Delta
    self._permissive_delta_map: Sparse2DIntArray = {}
    # Max Diff -> Delta -> Edge
    self._permissive_edge_map: Sparse2DIntArray = {}

  @classmethod
  def AddArguments(cls, parser: ct.CmdArgParser) -> ct.ArgumentGroupTuple:
    common_group, sobel_group, fuzzy_group = super(
        LocalMinimaParameterOptimizer, cls).AddArguments(parser)

    common_group.add_argument(
        '--use-bfs',
        action='store_true',
        default=False,
        help='Use a breadth-first search instead of a depth-first search. This '
        'will likely be significantly slower, but is more likely to find '
        'multiple local minima with the same weight.')

    sobel_group.add_argument(
        '--edge-threshold-weight',
        default=1,
        type=int,
        help='The weight associated with the edge threshold. Higher values '
        'will penalize a more permissive parameter value more harshly.')

    fuzzy_group.add_argument(
        '--max-diff-weight',
        default=3,
        type=int,
        help='The weight associated with the maximum number of different '
        'pixels. Higher values will penalize a more permissive parameter value '
        'more harshly.')
    fuzzy_group.add_argument(
        '--delta-threshold-weight',
        default=10,
        type=int,
        help='The weight associated with the per-channel delta sum. Higher '
        'values will penalize a more permissive parameter value more harshly.')

    return common_group, sobel_group, fuzzy_group

  def _VerifyArgs(self) -> None:
    super()._VerifyArgs()

    assert self._args.edge_threshold_weight >= self.MIN_EDGE_THRESHOLD_WEIGHT

    assert self._args.max_diff_weight >= self.MIN_MAX_DIFF_WEIGHT
    assert self._args.delta_threshold_weight >= self.MIN_DELTA_THRESHOLD_WEIGHT

  def _RunOptimizationImpl(self) -> None:
    visited_parameters = set()
    to_visit = collections.deque()
    smallest_weight = sys.maxsize
    smallest_parameters = []

    to_visit.append(self._GetMostPermissiveParameters())
    # Do a search, only considering adjacent parameters if:
    # 1. Their weight is less than or equal to the smallest found weight.
    # 2. They haven't been visited already.
    # 3. They are not guaranteed to fail based on previously tested parameters.
    # 4. The current parameters result in a successful comparison.
    while to_visit:
      current_parameters = None
      if self._args.use_bfs:
        current_parameters = to_visit.popleft()
      else:
        current_parameters = to_visit.pop()
      weight = self._GetWeight(current_parameters)
      if weight > smallest_weight:
        continue
      if current_parameters in visited_parameters:
        continue
      if self._ParametersAreGuaranteedToFail(current_parameters):
        visited_parameters.add(current_parameters)
        continue
      visited_parameters.add(current_parameters)
      success, _, _ = self._RunComparisonForParameters(current_parameters)
      if success:
        for adjacent in self._AdjacentParameters(current_parameters):
          to_visit.append(adjacent)
        if smallest_weight == weight:
          logging.info('Found additional smallest parameter %s',
                       current_parameters)
          smallest_parameters.append(current_parameters)
        else:
          logging.info('Found new smallest parameter with weight %d: %s',
                       weight, current_parameters)
          smallest_weight = weight
          smallest_parameters = [current_parameters]
      else:
        self._UpdateMostPermissiveFailedParameters(current_parameters)
    print('Found %d parameter(s) with the smallest weight:' %
          len(smallest_parameters))
    for p in smallest_parameters:
      print(p)

  def _ParametersAreGuaranteedToFail(self,
                                     parameters: parameter_set.ParameterSet
                                     ) -> bool:
    """Checks whether the given ParameterSet is guaranteed to fail.

    A ParameterSet is guaranteed to fail if we have already tried and failed
    with a similar ParameterSet that was more permissive. Specifically, if we
    have tried and failed with a ParameterSet with all but one parameters
    matching, and the non-matching parameter was more permissive than the
    current one.

    Args:
      parameters: The ParameterSet instance to check.

    Returns:
      True if |parameters| is guaranteed to fail based on previously tried
      parameters, otherwise False.
    """
    permissive_max_diff = self._permissive_max_diff_map.get(
        parameters.delta_threshold, {}).get(parameters.edge_threshold, -1)
    if parameters.max_diff < permissive_max_diff:
      return True

    permissive_delta = self._permissive_delta_map.get(
        parameters.max_diff, {}).get(parameters.edge_threshold, -1)
    if parameters.delta_threshold < permissive_delta:
      return True

    permissive_edge = self._permissive_edge_map.get(
        parameters.max_diff, {}).get(parameters.delta_threshold, sys.maxsize)
    if parameters.edge_threshold > permissive_edge:
      return True

    return False

  def _UpdateMostPermissiveFailedParameters(
      self, parameters: parameter_set.ParameterSet) -> None:
    """Updates the array of most permissive failed parameters.

    This is used in conjunction with _ParametersAreGuaranteedToFail to prune
    ParameterSets without having to actually test them. Values are updated if
    |parameters| shares two parameters with a a previously failed ParameterSet,
    but |parameters|' third parameter is more permissive.

    Args:
      parameters: A ParameterSet to pull updated values from.
    """
    permissive_max_diff = self._permissive_max_diff_map.setdefault(
        parameters.delta_threshold, {}).get(parameters.edge_threshold, -1)
    permissive_max_diff = max(permissive_max_diff, parameters.max_diff)
    self._permissive_max_diff_map[parameters.delta_threshold][
        parameters.edge_threshold] = permissive_max_diff

    permissive_delta = self._permissive_delta_map.setdefault(
        parameters.max_diff, {}).get(parameters.edge_threshold, -1)
    permissive_delta = max(permissive_delta, parameters.delta_threshold)
    self._permissive_delta_map[parameters.max_diff][
        parameters.edge_threshold] = permissive_delta

    permissive_edge = self._permissive_edge_map.setdefault(
        parameters.max_diff, {}).get(parameters.delta_threshold, sys.maxsize)
    permissive_edge = min(permissive_edge, parameters.edge_threshold)
    self._permissive_edge_map[parameters.max_diff][
        parameters.delta_threshold] = permissive_edge

  def _AdjacentParameters(self, starting_parameters):
    max_diff = starting_parameters.max_diff
    delta_threshold = starting_parameters.delta_threshold
    edge_threshold = starting_parameters.edge_threshold

    max_diff_step = self._args.max_diff_step
    delta_threshold_step = self._args.delta_threshold_step
    edge_threshold_step = self._args.edge_threshold_step

    max_diffs = [
        max(self._args.min_max_diff, max_diff - max_diff_step), max_diff,
        min(self._args.max_max_diff, max_diff + max_diff_step)
    ]
    delta_thresholds = [
        max(self._args.min_delta_threshold,
            delta_threshold - delta_threshold_step), delta_threshold,
        min(self._args.max_delta_threshold,
            delta_threshold + delta_threshold_step)
    ]
    edge_thresholds = [
        max(self._args.min_edge_threshold,
            edge_threshold - edge_threshold_step), edge_threshold,
        min(self._args.max_edge_threshold, edge_threshold + edge_threshold_step)
    ]
    for combo in itertools.product(max_diffs, delta_thresholds,
                                   edge_thresholds):
      adjacent = parameter_set.ParameterSet(combo[0], combo[1], combo[2])
      if adjacent != starting_parameters:
        yield adjacent

  def _GetWeight(self, parameters: parameter_set.ParameterSet) -> int:
    return (parameters.max_diff * self._args.max_diff_weight +
            parameters.delta_threshold * self._args.delta_threshold_weight +
            (self.MAX_EDGE_THRESHOLD - parameters.edge_threshold) *
            self._args.edge_threshold_weight)