]> git.cworth.org Git - apitrace/blob - scripts/snapdiff.py
snapdiff: Compare .bmp images too.
[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     name = os.path.basename(path)
125     name, ext1 = os.path.splitext(name)
126     name, ext2 = os.path.splitext(name)
127     print name, ext1, ext2
128     return ext1 in ('.png', '.bmp') and ext2 not in ('.diff', '.thumb')
129
130
131 def find_images(prefix):
132     prefix = os.path.abspath(prefix)
133     if os.path.isdir(prefix):
134         prefix_dir = prefix
135     else:
136         prefix_dir = os.path.dirname(prefix)
137
138     images = []
139     for dirname, dirnames, filenames in os.walk(prefix_dir, followlinks=True):
140         for filename in filenames:
141             filepath = os.path.join(dirname, filename)
142             if filepath.startswith(prefix) and is_image(filepath):
143                 images.append(filepath[len(prefix):])
144
145     return images
146
147
148 def main():
149     global options
150
151     optparser = optparse.OptionParser(
152         usage="\n\t%prog [options] <ref_prefix> <src_prefix>")
153     optparser.add_option(
154         '-o', '--output', metavar='FILE',
155         type="string", dest="output", default='index.html',
156         help="output filename [default: %default]")
157     optparser.add_option(
158         '-f', '--fuzz',
159         type="float", dest="fuzz", default=0.05,
160         help="fuzz ratio [default: %default]")
161     optparser.add_option(
162         '-a', '--alpha',
163         action="store_true", dest="alpha", default=False,
164         help="take alpha channel in consideration")
165     optparser.add_option(
166         '--overwrite',
167         action="store_true", dest="overwrite", default=False,
168         help="overwrite images")
169
170     (options, args) = optparser.parse_args(sys.argv[1:])
171
172     if len(args) != 2:
173         optparser.error('incorrect number of arguments')
174
175     ref_prefix = args[0]
176     src_prefix = args[1]
177
178     ref_images = find_images(ref_prefix)
179     src_images = find_images(src_prefix)
180     images = list(set(ref_images).intersection(set(src_images)))
181     images.sort()
182
183     if options.output:
184         html = open(options.output, 'wt')
185     else:
186         html = sys.stdout
187     html.write('<html>\n')
188     html.write('  <body>\n')
189     html.write('    <table border="1">\n')
190     html.write('      <tr><th>%s</th><th>%s</th><th>&Delta;</th></tr>\n' % (ref_prefix, src_prefix))
191     for image in images:
192         ref_image = ref_prefix + image
193         src_image = src_prefix + image
194         root, ext = os.path.splitext(src_image)
195         delta_image = "%s.diff.png" % (root, )
196         if os.path.exists(ref_image) and os.path.exists(src_image):
197             if options.overwrite \
198                or not os.path.exists(delta_image) \
199                or (os.path.getmtime(delta_image) < os.path.getmtime(ref_image) \
200                    and os.path.getmtime(delta_image) < os.path.getmtime(src_image)):
201
202                 comparer = Comparer(ref_image, src_image, options.alpha)
203                 comparer.write_diff(delta_image, fuzz=options.fuzz)
204
205             html.write('      <tr>\n')
206             surface(html, ref_image)
207             surface(html, src_image)
208             surface(html, delta_image)
209             html.write('      </tr>\n')
210             html.flush()
211     html.write('    </table>\n')
212     html.write('  </body>\n')
213     html.write('</html>\n')
214
215
216 if __name__ == '__main__':
217     main()