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