]> git.cworth.org Git - apitrace/blob - scripts/snapdiff.py
aa65f51c036b3b871279ea8774b8f2d0d69a8584
[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 from PIL import ImageFilter
43
44
45 thumbSize = 320
46
47 gaussian_kernel = ImageFilter.Kernel((3, 3), [1, 2, 1, 2, 4, 2, 1, 2, 1], 16)
48
49 class Comparer:
50     '''Image comparer.'''
51
52     def __init__(self, ref_image, src_image, alpha = False):
53         if isinstance(ref_image, basestring):
54             self.ref_im = Image.open(ref_image)
55         else:
56             self.ref_im = ref_image
57
58         if isinstance(src_image, basestring):
59             self.src_im = Image.open(src_image)
60         else:
61             self.src_im = src_image
62
63         # Ignore
64         if not alpha:
65             self.ref_im = self.ref_im.convert('RGB')
66             self.src_im = self.src_im.convert('RGB')
67
68         self.diff = ImageChops.difference(self.src_im, self.ref_im)
69
70     def size_mismatch(self):
71         return self.ref_im.size != self.src_im.size
72
73     def write_diff(self, diff_image, fuzz = 0.05):
74         # make a difference image similar to ImageMagick's compare utility
75         mask = ImageEnhance.Brightness(self.diff).enhance(1.0/fuzz)
76         mask = mask.convert('L')
77
78         lowlight = Image.new('RGB', self.src_im.size, (0xff, 0xff, 0xff))
79         highlight = Image.new('RGB', self.src_im.size, (0xf1, 0x00, 0x1e))
80         diff_im = Image.composite(highlight, lowlight, mask)
81
82         diff_im = Image.blend(self.src_im, diff_im, 0xcc/255.0)
83         diff_im.save(diff_image)
84
85     def precision(self, filter=False):
86         if self.size_mismatch():
87             return 0.0
88
89         diff = self.diff
90         if filter:
91             diff = diff.filter(gaussian_kernel)
92
93         # See also http://effbot.org/zone/pil-comparing-images.htm
94         h = diff.histogram()
95         square_error = 0
96         for i in range(1, 256):
97             square_error += sum(h[i : 3*256: 256])*i*i
98         rel_error = float(square_error*2 + 1) / float(self.diff.size[0]*self.diff.size[1]*3*255*255*2)
99         bits = -math.log(rel_error)/math.log(2.0)
100         return bits
101
102     def ae(self, fuzz = 0.05):
103         # Compute absolute error
104
105         if self.size_mismatch():
106             return sys.maxint
107
108         # TODO: this is approximate due to the grayscale conversion
109         h = self.diff.convert('L').histogram()
110         ae = sum(h[int(255 * fuzz) + 1 : 256])
111         return ae
112
113
114 def surface(html, image):
115     if True:
116         name, ext = os.path.splitext(image)
117         thumb = name + '.thumb' + ext
118         if os.path.exists(image) \
119            and (not os.path.exists(thumb) \
120                 or os.path.getmtime(thumb) < os.path.getmtime(image)):
121             im = Image.open(image)
122             imageWidth, imageHeight = im.size
123             if imageWidth <= thumbSize and imageHeight <= thumbSize:
124                 if imageWidth >= imageHeight:
125                     imageHeight = imageHeight*thumbSize/imageWidth
126                     imageWidth = thumbSize
127                 else:
128                     imageWidth = imageWidth*thumbSize/imageHeight
129                     imageHeight = thumbSize
130                 html.write('        <td><img src="%s" width="%u" height="%u"/></td>\n' % (image, imageWidth, imageHeight))
131                 return
132
133             im.thumbnail((thumbSize, thumbSize))
134             im.save(thumb)
135     else:
136         thumb = image
137     html.write('        <td><a href="%s"><img src="%s"/></a></td>\n' % (image, thumb))
138
139
140 def is_image(path):
141     name = os.path.basename(path)
142     name, ext1 = os.path.splitext(name)
143     name, ext2 = os.path.splitext(name)
144     return ext1 in ('.png', '.bmp') and ext2 not in ('.diff', '.thumb')
145
146
147 def find_images(prefix):
148     if os.path.isdir(prefix):
149         prefix_dir = prefix
150     else:
151         prefix_dir = os.path.dirname(prefix)
152
153     images = []
154     for dirname, dirnames, filenames in os.walk(prefix_dir, followlinks=True):
155         for filename in filenames:
156             filepath = os.path.join(dirname, filename)
157             if filepath.startswith(prefix) and is_image(filepath):
158                 images.append(filepath[len(prefix):])
159
160     return images
161
162
163 def main():
164     global options
165
166     optparser = optparse.OptionParser(
167         usage="\n\t%prog [options] <ref_prefix> <src_prefix>")
168     optparser.add_option(
169         '-v', '--verbose',
170         action="store_true", dest="verbose", default=False,
171         help="verbose output")
172     optparser.add_option(
173         '-o', '--output', metavar='FILE',
174         type="string", dest="output", default='index.html',
175         help="output filename [default: %default]")
176     optparser.add_option(
177         '-f', '--fuzz',
178         type="float", dest="fuzz", default=0.05,
179         help="fuzz ratio [default: %default]")
180     optparser.add_option(
181         '-a', '--alpha',
182         action="store_true", dest="alpha", default=False,
183         help="take alpha channel in consideration")
184     optparser.add_option(
185         '--overwrite',
186         action="store_true", dest="overwrite", default=False,
187         help="overwrite images")
188     optparser.add_option(
189         '--show-all',
190         action="store_true", dest="show_all", default=False,
191         help="show all images, including similar ones")
192
193     (options, args) = optparser.parse_args(sys.argv[1:])
194
195     if len(args) != 2:
196         optparser.error('incorrect number of arguments')
197
198     ref_prefix = args[0]
199     src_prefix = args[1]
200
201     ref_images = find_images(ref_prefix)
202     src_images = find_images(src_prefix)
203     images = list(set(ref_images).intersection(set(src_images)))
204     images.sort()
205
206     if options.output:
207         html = open(options.output, 'wt')
208     else:
209         html = sys.stdout
210     html.write('<html>\n')
211     html.write('  <body>\n')
212     html.write('    <table border="1">\n')
213     html.write('      <tr><th>File</th><th>%s</th><th>%s</th><th>&Delta;</th></tr>\n' % (ref_prefix, src_prefix))
214     failures = 0
215     for image in images:
216         ref_image = ref_prefix + image
217         src_image = src_prefix + image
218         root, ext = os.path.splitext(src_image)
219         delta_image = "%s.diff.png" % (root, )
220         if os.path.exists(ref_image) and os.path.exists(src_image):
221             if options.verbose:
222                 sys.stdout.write('Comparing %s and %s ...' % (ref_image, src_image))
223             comparer = Comparer(ref_image, src_image, options.alpha)
224             match = comparer.ae(fuzz=options.fuzz) == 0
225             if match:
226                 result = 'MATCH'
227                 bgcolor = '#20ff20'
228             else:
229                 result = 'MISMATCH'
230                 failures += 1
231                 bgcolor = '#ff2020'
232             if options.verbose:
233                 sys.stdout.write(' %s\n' % (result,))
234             html.write('      <tr>\n')
235             html.write('        <td bgcolor="%s"><a href="%s">%s<a/></td>\n' % (bgcolor, ref_image, image))
236             if not match or options.show_all:
237                 if options.overwrite \
238                    or not os.path.exists(delta_image) \
239                    or (os.path.getmtime(delta_image) < os.path.getmtime(ref_image) \
240                        and os.path.getmtime(delta_image) < os.path.getmtime(src_image)):
241                     comparer.write_diff(delta_image, fuzz=options.fuzz)
242                 surface(html, ref_image)
243                 surface(html, src_image)
244                 surface(html, delta_image)
245             html.write('      </tr>\n')
246             html.flush()
247     html.write('    </table>\n')
248     html.write('  </body>\n')
249     html.write('</html>\n')
250
251     if failures:
252         sys.exit(1)
253
254 if __name__ == '__main__':
255     main()