imagepairset.py revision 68a3815401f461976f76891d0477cb1440fa0aba
1#!/usr/bin/python
2
3"""
4Copyright 2014 Google Inc.
5
6Use of this source code is governed by a BSD-style license that can be
7found in the LICENSE file.
8
9ImagePairSet class; see its docstring below.
10"""
11
12# System-level imports
13import posixpath
14
15# Local imports
16import column
17import imagepair
18
19# Keys used within dictionary representation of ImagePairSet.
20# NOTE: Keep these in sync with static/constants.js
21KEY__ROOT__EXTRACOLUMNHEADERS = 'extraColumnHeaders'
22KEY__ROOT__HEADER = 'header'
23KEY__ROOT__IMAGEPAIRS = 'imagePairs'
24KEY__ROOT__IMAGESETS = 'imageSets'
25KEY__IMAGESETS__FIELD__BASE_URL = 'baseUrl'
26KEY__IMAGESETS__FIELD__DESCRIPTION = 'description'
27KEY__IMAGESETS__SET__DIFFS = 'diffs'
28KEY__IMAGESETS__SET__IMAGE_A = 'imageA'
29KEY__IMAGESETS__SET__IMAGE_B = 'imageB'
30KEY__IMAGESETS__SET__WHITEDIFFS = 'whiteDiffs'
31
32DEFAULT_DESCRIPTIONS = ('setA', 'setB')
33
34
35class ImagePairSet(object):
36  """A collection of ImagePairs, representing two arbitrary sets of images.
37
38  These could be:
39  - images generated before and after a code patch
40  - expected and actual images for some tests
41  - or any other pairwise set of images.
42  """
43
44  def __init__(self, diff_base_url, descriptions=None):
45    """
46    Args:
47      diff_base_url: base URL indicating where diff images can be loaded from
48      descriptions: a (string, string) tuple describing the two image sets.
49          If not specified, DEFAULT_DESCRIPTIONS will be used.
50    """
51    self._column_header_factories = {}
52    self._descriptions = descriptions or DEFAULT_DESCRIPTIONS
53    self._extra_column_tallies = {}  # maps column_id -> values
54                                     #                -> instances_per_value
55    self._image_pair_dicts = []
56    self._image_base_url = None
57    self._diff_base_url = diff_base_url
58
59  def add_image_pair(self, image_pair):
60    """Adds an ImagePair; this may be repeated any number of times."""
61    # Special handling when we add the first ImagePair...
62    if not self._image_pair_dicts:
63      self._image_base_url = image_pair.base_url
64
65    if image_pair.base_url != self._image_base_url:
66      raise Exception('added ImagePair with base_url "%s" instead of "%s"' % (
67          image_pair.base_url, self._image_base_url))
68    self._image_pair_dicts.append(image_pair.as_dict())
69    extra_columns_dict = image_pair.extra_columns_dict
70    if extra_columns_dict:
71      for column_id, value in extra_columns_dict.iteritems():
72        self._add_extra_column_value_to_summary(column_id, value)
73
74  def set_column_header_factory(self, column_id, column_header_factory):
75    """Overrides the default settings for one of the extraColumn headers.
76
77    Args:
78      column_id: string; unique ID of this column (must match a key within
79          an ImagePair's extra_columns dictionary)
80      column_header_factory: a ColumnHeaderFactory object
81    """
82    self._column_header_factories[column_id] = column_header_factory
83
84  def get_column_header_factory(self, column_id):
85    """Returns the ColumnHeaderFactory object for a particular extraColumn.
86
87    Args:
88      column_id: string; unique ID of this column (must match a key within
89          an ImagePair's extra_columns dictionary)
90    """
91    column_header_factory = self._column_header_factories.get(column_id, None)
92    if not column_header_factory:
93      column_header_factory = column.ColumnHeaderFactory(header_text=column_id)
94      self._column_header_factories[column_id] = column_header_factory
95    return column_header_factory
96
97  def ensure_extra_column_values_in_summary(self, column_id, values):
98    """Ensure this column_id/value pair is part of the extraColumns summary.
99
100    Args:
101      column_id: string; unique ID of this column
102      value: string; a possible value for this column
103    """
104    for value in values:
105      self._add_extra_column_value_to_summary(
106          column_id=column_id, value=value, addend=0)
107
108  def _add_extra_column_value_to_summary(self, column_id, value, addend=1):
109    """Records one column_id/value extraColumns pair found within an ImagePair.
110
111    We use this information to generate tallies within the column header
112    (how many instances we saw of a particular value, within a particular
113    extraColumn).
114
115    Args:
116      column_id: string; unique ID of this column (must match a key within
117          an ImagePair's extra_columns dictionary)
118      value: string; a possible value for this column
119      addend: integer; how many instances to add to the tally
120    """
121    known_values_for_column = self._extra_column_tallies.get(column_id, None)
122    if not known_values_for_column:
123      known_values_for_column = {}
124      self._extra_column_tallies[column_id] = known_values_for_column
125    instances_of_this_value = known_values_for_column.get(value, 0)
126    instances_of_this_value += addend
127    known_values_for_column[value] = instances_of_this_value
128
129  def _column_headers_as_dict(self):
130    """Returns all column headers as a dictionary."""
131    asdict = {}
132    for column_id, values_for_column in self._extra_column_tallies.iteritems():
133      column_header_factory = self.get_column_header_factory(column_id)
134      asdict[column_id] = column_header_factory.create_as_dict(
135          values_for_column)
136    return asdict
137
138  def as_dict(self):
139    """Returns a dictionary describing this package of ImagePairs.
140
141    Uses the KEY__* constants as keys.
142    """
143    key_description = KEY__IMAGESETS__FIELD__DESCRIPTION
144    key_base_url = KEY__IMAGESETS__FIELD__BASE_URL
145    return {
146        KEY__ROOT__EXTRACOLUMNHEADERS: self._column_headers_as_dict(),
147        KEY__ROOT__IMAGEPAIRS: self._image_pair_dicts,
148        KEY__ROOT__IMAGESETS: {
149            KEY__IMAGESETS__SET__IMAGE_A: {
150                key_description: self._descriptions[0],
151                key_base_url: self._image_base_url,
152            },
153            KEY__IMAGESETS__SET__IMAGE_B: {
154                key_description: self._descriptions[1],
155                key_base_url: self._image_base_url,
156            },
157            KEY__IMAGESETS__SET__DIFFS: {
158                key_description: 'color difference per channel',
159                key_base_url: posixpath.join(
160                    self._diff_base_url, 'diffs'),
161            },
162            KEY__IMAGESETS__SET__WHITEDIFFS: {
163                key_description: 'differing pixels in white',
164                key_base_url: posixpath.join(
165                    self._diff_base_url, 'whitediffs'),
166            },
167        },
168    }
169