]> git.cworth.org Git - apitrace/blob - scripts/snapdiff.py
snapdiff: Add option to consider alpha when comparing images.
[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         '-a', '--alpha',
162         action="store_true", dest="alpha", default=False,
163         help="take alpha channel in consideration")
164     optparser.add_option(
165         '--overwrite',
166         action="store_true", dest="overwrite", default=False,
167         help="overwrite images")
168
169     (options, args) = optparser.parse_args(sys.argv[1:])
170
171     if len(args) != 2:
172         optparser.error('incorrect number of arguments')
173
174     ref_prefix = args[0]
175     src_prefix = args[1]
176
177     ref_images = find_images(ref_prefix)
178     src_images = find_images(src_prefix)
179     images = list(set(ref_images).intersection(set(src_images)))
180     images.sort()
181
182     if options.output:
183         html = open(options.output, 'wt')
184     else:
185         html = sys.stdout
186     html.write('<html>\n')
187     html.write('  <body>\n')
188     html.write('    <table border="1">\n')
189     html.write('      <tr><th>%s</th><th>%s</th><th>&Delta;</th></tr>\n' % (ref_prefix, src_prefix))
190     for image in images:
191         ref_image = ref_prefix + image
192         src_image = src_prefix + image
193         root, ext = os.path.splitext(src_image)
194         delta_image = "%s.diff.png" % (root, )
195         if os.path.exists(ref_image) and os.path.exists(src_image):
196             if options.overwrite \
197                or not os.path.exists(delta_image) \
198                or (os.path.getmtime(delta_image) < os.path.getmtime(ref_image) \
199                    and os.path.getmtime(delta_image) < os.path.getmtime(src_image)):
200
201                 comparer = Comparer(ref_image, src_image, options.alpha)
202                 comparer.write_diff(delta_image, fuzz=options.fuzz)
203
204             html.write('      <tr>\n')
205             surface(html, ref_image)
206             surface(html, src_image)
207             surface(html, delta_image)
208             html.write('      </tr>\n')
209             html.flush()
210     html.write('    </table>\n')
211     html.write('  </body>\n')
212     html.write('</html>\n')
213
214
215 if __name__ == '__main__':
216     main()