]> git.cworth.org Git - apitrace/blob - scripts/snapdiff.py
Fail image comparison when there is a size mismatch.
[apitrace] / scripts / snapdiff.py
1 #!/usr/bin/env python
2 ##########################################################################
3 #
4 # Copyright 2011 Jose Fonseca
5 # Copyright 2008-2009 VMware, Inc.
6 # All Rights Reserved.
7 #
8 # Permission is hereby granted, free of charge, to any person obtaining a copy
9 # of this software and associated documentation files (the "Software"), to deal
10 # in the Software without restriction, including without limitation the rights
11 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 # copies of the Software, and to permit persons to whom the Software is
13 # furnished to do so, subject to the following conditions:
14 #
15 # The above copyright notice and this permission notice shall be included in
16 # all copies or substantial portions of the Software.
17 #
18 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 # THE SOFTWARE.
25 #
26 ##########################################################################/
27
28
29 '''Snapshot (image) comparison script.
30 '''
31
32
33 import sys
34 import os.path
35 import optparse
36 import math
37 import operator
38
39 from PIL import Image
40 from PIL import ImageChops
41 from PIL import ImageEnhance
42
43
44 thumb_size = 320, 320
45
46
47 class Comparer:
48     '''Image comparer.'''
49
50     def __init__(self, ref_image, src_image, alpha = False):
51         if isinstance(ref_image, basestring):
52             self.ref_im = Image.open(ref_image)
53         else:
54             self.ref_im = ref_image
55
56         if isinstance(src_image, basestring):
57             self.src_im = Image.open(src_image)
58         else:
59             self.src_im = src_image
60
61         # Ignore
62         if not alpha:
63             self.ref_im = self.ref_im.convert('RGB')
64             self.src_im = self.src_im.convert('RGB')
65
66         self.diff = ImageChops.difference(self.src_im, self.ref_im)
67
68     def size_mismatch(self):
69         return self.ref_im.size != self.src_im.size
70
71     def write_diff(self, diff_image, fuzz = 0.05):
72         # make a difference image similar to ImageMagick's compare utility
73         mask = ImageEnhance.Brightness(self.diff).enhance(1.0/fuzz)
74         mask = mask.convert('L')
75
76         lowlight = Image.new('RGB', self.src_im.size, (0xff, 0xff, 0xff))
77         highlight = Image.new('RGB', self.src_im.size, (0xf1, 0x00, 0x1e))
78         diff_im = Image.composite(highlight, lowlight, mask)
79
80         diff_im = Image.blend(self.src_im, diff_im, 0xcc/255.0)
81         diff_im.save(diff_image)
82
83     def precision(self):
84         if self.size_mismatch():
85             return 0.0
86
87         # See also http://effbot.org/zone/pil-comparing-images.htm
88         h = self.diff.histogram()
89         square_error = 0
90         for i in range(1, 256):
91             square_error += sum(h[i : 3*256: 256])*i*i
92         rel_error = float(square_error*2 + 1) / float(self.diff.size[0]*self.diff.size[1]*3*255*255*2)
93         bits = -math.log(rel_error)/math.log(2.0)
94         return bits
95
96     def ae(self, fuzz = 0.05):
97         # Compute absolute error
98
99         if self.size_mismatch():
100             return sys.maxint
101
102         # TODO: this is approximate due to the grayscale conversion
103         h = self.diff.convert('L').histogram()
104         ae = sum(h[int(255 * fuzz) + 1 : 256])
105         return ae
106
107
108 def surface(html, image):
109     if True:
110         name, ext = os.path.splitext(image)
111         thumb = name + '.thumb' + ext
112         if os.path.exists(image) \
113            and (not os.path.exists(thumb) \
114                 or os.path.getmtime(thumb) < os.path.getmtime(image)):
115             im = Image.open(image)
116             im.thumbnail(thumb_size)
117             im.save(thumb)
118     else:
119         thumb = image
120     html.write('        <td><a href="%s"><img src="%s"/></a></td>\n' % (image, thumb))
121
122
123 def is_image(path):
124     return \
125         path.endswith('.png') \
126         and not path.endswith('.diff.png') \
127         and not path.endswith('.thumb.png')
128
129
130 def find_images(prefix):
131     prefix = os.path.abspath(prefix)
132     if os.path.isdir(prefix):
133         prefix_dir = prefix
134     else:
135         prefix_dir = os.path.dirname(prefix)
136
137     images = []
138     for dirname, dirnames, filenames in os.walk(prefix_dir, followlinks=True):
139         for filename in filenames:
140             filepath = os.path.join(dirname, filename)
141             if filepath.startswith(prefix) and is_image(filepath):
142                 images.append(filepath[len(prefix):])
143
144     return images
145
146
147 def main():
148     global options
149
150     optparser = optparse.OptionParser(
151         usage="\n\t%prog [options] <ref_prefix> <src_prefix>")
152     optparser.add_option(
153         '-o', '--output', metavar='FILE',
154         type="string", dest="output", default='index.html',
155         help="output filename [default: %default]")
156     optparser.add_option(
157         '-f', '--fuzz',
158         type="float", dest="fuzz", default=0.05,
159         help="fuzz ratio [default: %default]")
160     optparser.add_option(
161         '--overwrite',
162         action="store_true", dest="overwrite", default=False,
163         help="overwrite")
164
165     (options, args) = optparser.parse_args(sys.argv[1:])
166
167     if len(args) != 2:
168         optparser.error('incorrect number of arguments')
169
170     ref_prefix = args[0]
171     src_prefix = args[1]
172
173     ref_images = find_images(ref_prefix)
174     src_images = find_images(src_prefix)
175     images = list(set(ref_images).intersection(set(src_images)))
176     images.sort()
177
178     if options.output:
179         html = open(options.output, 'wt')
180     else:
181         html = sys.stdout
182     html.write('<html>\n')
183     html.write('  <body>\n')
184     html.write('    <table border="1">\n')
185     html.write('      <tr><th>%s</th><th>%s</th><th>&Delta;</th></tr>\n' % (ref_prefix, src_prefix))
186     for image in images:
187         ref_image = ref_prefix + image
188         src_image = src_prefix + image
189         root, ext = os.path.splitext(src_image)
190         delta_image = "%s.diff.png" % (root, )
191         if os.path.exists(ref_image) and os.path.exists(src_image):
192             if options.overwrite \
193                or not os.path.exists(delta_image) \
194                or (os.path.getmtime(delta_image) < os.path.getmtime(ref_image) \
195                    and os.path.getmtime(delta_image) < os.path.getmtime(src_image)):
196
197                 comparer = Comparer(ref_image, src_image)
198                 comparer.write_diff(delta_image, fuzz=options.fuzz)
199
200             html.write('      <tr>\n')
201             surface(html, ref_image)
202             surface(html, src_image)
203             surface(html, delta_image)
204             html.write('      </tr>\n')
205             html.flush()
206     html.write('    </table>\n')
207     html.write('  </body>\n')
208     html.write('</html>\n')
209
210
211 if __name__ == '__main__':
212     main()