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