]> git.cworth.org Git - apitrace/blob - scripts/snapdiff.py
Merge remote-tracking branch 'origin/master' into on-demand-loading
[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 write_diff(self, diff_image, fuzz = 0.05):
69         # make a difference image similar to ImageMagick's compare utility
70         mask = ImageEnhance.Brightness(self.diff).enhance(1.0/fuzz)
71         mask = mask.convert('L')
72
73         lowlight = Image.new('RGB', self.src_im.size, (0xff, 0xff, 0xff))
74         highlight = Image.new('RGB', self.src_im.size, (0xf1, 0x00, 0x1e))
75         diff_im = Image.composite(highlight, lowlight, mask)
76
77         diff_im = Image.blend(self.src_im, diff_im, 0xcc/255.0)
78         diff_im.save(diff_image)
79
80     def precision(self):
81         # See also http://effbot.org/zone/pil-comparing-images.htm
82         h = self.diff.histogram()
83         square_error = 0
84         for i in range(1, 256):
85             square_error += sum(h[i : 3*256: 256])*i*i
86         rel_error = float(square_error*2 + 1) / float(self.diff.size[0]*self.diff.size[1]*3*255*255*2)
87         bits = -math.log(rel_error)/math.log(2.0)
88         return bits
89
90     def ae(self):
91         # Compute absolute error
92         # TODO: this is approximate due to the grayscale conversion
93         h = self.diff.convert('L').histogram()
94         ae = sum(h[int(255 * fuzz) + 1 : 256])
95         return ae
96
97
98 def surface(html, image):
99     if True:
100         name, ext = os.path.splitext(image)
101         thumb = name + '.thumb' + ext
102         if os.path.exists(image) \
103            and (not os.path.exists(thumb) \
104                 or os.path.getmtime(thumb) < os.path.getmtime(image)):
105             im = Image.open(image)
106             im.thumbnail(thumb_size)
107             im.save(thumb)
108     else:
109         thumb = image
110     html.write('        <td><a href="%s"><img src="%s"/></a></td>\n' % (image, thumb))
111
112
113 def is_image(path):
114     return \
115         path.endswith('.png') \
116         and not path.endswith('.diff.png') \
117         and not path.endswith('.thumb.png')
118
119
120 def find_images(prefix):
121     prefix = os.path.abspath(prefix)
122     if os.path.isdir(prefix):
123         prefix_dir = prefix
124     else:
125         prefix_dir = os.path.dirname(prefix)
126
127     images = []
128     for dirname, dirnames, filenames in os.walk(prefix_dir, followlinks=True):
129         for filename in filenames:
130             filepath = os.path.join(dirname, filename)
131             if filepath.startswith(prefix) and is_image(filepath):
132                 images.append(filepath[len(prefix):])
133
134     return images
135
136
137 def main():
138     global options
139
140     optparser = optparse.OptionParser(
141         usage="\n\t%prog [options] <ref_prefix> <src_prefix>",
142         version="%%prog")
143     optparser.add_option(
144         '-o', '--output', metavar='FILE',
145         type="string", dest="output", default='index.html',
146         help="output filename [default: %default]")
147     optparser.add_option(
148         '-f', '--fuzz',
149         type="float", dest="fuzz", default=0.05,
150         help="fuzz ratio [default: %default]")
151     optparser.add_option(
152         '--overwrite',
153         action="store_true", dest="overwrite", default=False,
154         help="overwrite")
155
156     (options, args) = optparser.parse_args(sys.argv[1:])
157
158     if len(args) != 2:
159         optparser.error('incorrect number of arguments')
160
161     ref_prefix = args[0]
162     src_prefix = args[1]
163
164     ref_images = find_images(ref_prefix)
165     src_images = find_images(src_prefix)
166     images = list(set(ref_images).intersection(set(src_images)))
167     images.sort()
168
169     if options.output:
170         html = open(options.output, 'wt')
171     else:
172         html = sys.stdout
173     html.write('<html>\n')
174     html.write('  <body>\n')
175     html.write('    <table border="1">\n')
176     html.write('      <tr><th>%s</th><th>%s</th><th>&Delta;</th></tr>\n' % (ref_prefix, src_prefix))
177     for image in images:
178         ref_image = ref_prefix + image
179         src_image = src_prefix + image
180         root, ext = os.path.splitext(src_image)
181         delta_image = "%s.diff.png" % (root, )
182         if os.path.exists(ref_image) and os.path.exists(src_image):
183             if options.overwrite \
184                or not os.path.exists(delta_image) \
185                or (os.path.getmtime(delta_image) < os.path.getmtime(ref_image) \
186                    and os.path.getmtime(delta_image) < os.path.getmtime(src_image)):
187
188                 comparer = Comparer(ref_image, src_image)
189                 comparer.write_diff(delta_image, fuzz=options.fuzz)
190
191             html.write('      <tr>\n')
192             surface(html, ref_image)
193             surface(html, src_image)
194             surface(html, delta_image)
195             html.write('      </tr>\n')
196             html.flush()
197     html.write('    </table>\n')
198     html.write('  </body>\n')
199     html.write('</html>\n')
200
201
202 if __name__ == '__main__':
203     main()