1
- #=======================================================================
2
-
3
- """ A set of utilities for comparing results.
4
1
"""
5
- #=======================================================================
2
+ Provides a collection of utilities for comparing (image) results.
6
3
4
+ """
7
5
from __future__ import (absolute_import , division , print_function ,
8
6
unicode_literals )
9
7
10
8
import six
11
- from six .moves import xrange
9
+
10
+ import hashlib
11
+ import os
12
+ import shutil
13
+
14
+ import numpy as np
12
15
13
16
import matplotlib
14
17
from matplotlib .compat import subprocess
15
18
from matplotlib .testing .noseclasses import ImageComparisonFailure
16
- from matplotlib .testing import image_util
17
19
from matplotlib import _png
18
20
from matplotlib import _get_cachedir
19
21
from matplotlib import cbook
20
22
from distutils import version
21
- import hashlib
22
- import math
23
- import os
24
- import numpy as np
25
- import shutil
26
- import sys
27
- from functools import reduce
28
-
29
- #=======================================================================
30
23
31
- __all__ = [
32
- 'compare_float' ,
33
- 'compare_images' ,
34
- 'comparable_formats' ,
35
- ]
36
-
37
- #-----------------------------------------------------------------------
24
+ __all__ = ['compare_float' , 'compare_images' , 'comparable_formats' ]
38
25
39
26
40
27
def make_test_filename (fname , purpose ):
@@ -47,32 +34,29 @@ def make_test_filename(fname, purpose):
47
34
48
35
49
36
def compare_float (expected , actual , relTol = None , absTol = None ):
50
- """Fail if the floating point values are not close enough, with
51
- the givem message.
37
+ """
38
+ Fail if the floating point values are not close enough, with
39
+ the given message.
52
40
53
41
You can specify a relative tolerance, absolute tolerance, or both.
42
+
54
43
"""
55
44
if relTol is None and absTol is None :
56
- exMsg = "You haven't specified a 'relTol' relative tolerance "
57
- exMsg += "or a 'absTol' absolute tolerance function argument. "
58
- exMsg += "You must specify one."
59
- raise ValueError (exMsg )
60
-
45
+ raise ValueError ("You haven't specified a 'relTol' relative "
46
+ "tolerance or a 'absTol' absolute tolerance "
47
+ "function argument. You must specify one." )
61
48
msg = ""
62
49
63
50
if absTol is not None :
64
51
absDiff = abs (expected - actual )
65
52
if absTol < absDiff :
66
- expectedStr = str (expected )
67
- actualStr = str (actual )
68
- absDiffStr = str (absDiff )
69
- absTolStr = str (absTol )
53
+ template = ['' ,
54
+ 'Expected: {expected}' ,
55
+ 'Actual: {actual}' ,
56
+ 'Abs diff: {absDiff}' ,
57
+ 'Abs tol: {absTol}' ]
70
58
71
- msg += "\n "
72
- msg += " Expected: " + expectedStr + "\n "
73
- msg += " Actual: " + actualStr + "\n "
74
- msg += " Abs Diff: " + absDiffStr + "\n "
75
- msg += " Abs Tol: " + absTolStr + "\n "
59
+ msg += '\n ' .join ([line .format (locals ()) for line in template ])
76
60
77
61
if relTol is not None :
78
62
# The relative difference of the two values. If the expected value is
@@ -82,29 +66,16 @@ def compare_float(expected, actual, relTol=None, absTol=None):
82
66
relDiff = relDiff / abs (expected )
83
67
84
68
if relTol < relDiff :
85
-
86
69
# The relative difference is a ratio, so it's always unitless.
87
- relDiffStr = str ( relDiff )
88
- relTolStr = str ( relTol )
89
-
90
- expectedStr = str ( expected )
91
- actualStr = str ( actual )
70
+ template = [ '' ,
71
+ 'Expected: {expected}' ,
72
+ 'Actual: {actual}' ,
73
+ 'Rel diff: {relDiff}' ,
74
+ 'Rel tol: {relTol}' ]
92
75
93
- msg += "\n "
94
- msg += " Expected: " + expectedStr + "\n "
95
- msg += " Actual: " + actualStr + "\n "
96
- msg += " Rel Diff: " + relDiffStr + "\n "
97
- msg += " Rel Tol: " + relTolStr + "\n "
76
+ msg += '\n ' .join ([line .format (locals ()) for line in template ])
98
77
99
- if msg :
100
- return msg
101
- else :
102
- return None
103
-
104
- #-----------------------------------------------------------------------
105
- # A dictionary that maps filename extensions to functions that map
106
- # parameters old and new to a list that can be passed to Popen to
107
- # convert files with that extension to png format.
78
+ return msg or None
108
79
109
80
110
81
def get_cache_dir ():
@@ -122,7 +93,7 @@ def get_cache_dir():
122
93
return cache_dir
123
94
124
95
125
- def get_file_hash (path , block_size = 2 ** 20 ):
96
+ def get_file_hash (path , block_size = 2 ** 20 ):
126
97
md5 = hashlib .md5 ()
127
98
with open (path , 'rb' ) as fd :
128
99
while True :
@@ -132,8 +103,6 @@ def get_file_hash(path, block_size=2 ** 20):
132
103
md5 .update (data )
133
104
return md5 .hexdigest ()
134
105
135
- converter = {}
136
-
137
106
138
107
def make_external_conversion_command (cmd ):
139
108
def convert (old , new ):
@@ -152,36 +121,51 @@ def convert(old, new):
152
121
153
122
return convert
154
123
155
- gs , gs_v = matplotlib .checkdep_ghostscript ()
156
- if gs_v is not None :
157
- cmd = lambda old , new : \
158
- [gs , '-q' , '-sDEVICE=png16m' , '-dNOPAUSE' , '-dBATCH' ,
159
- '-sOutputFile=' + new , old ]
160
- converter ['pdf' ] = make_external_conversion_command (cmd )
161
- converter ['eps' ] = make_external_conversion_command (cmd )
162
124
163
- if matplotlib .checkdep_inkscape () is not None :
164
- cmd = lambda old , new : \
165
- ['inkscape' , '-z' , old , '--export-png' , new ]
166
- converter ['svg' ] = make_external_conversion_command (cmd )
125
+ def _update_converter ():
126
+ gs , gs_v = matplotlib .checkdep_ghostscript ()
127
+ if gs_v is not None :
128
+ cmd = lambda old , new : \
129
+ [gs , '-q' , '-sDEVICE=png16m' , '-dNOPAUSE' , '-dBATCH' ,
130
+ '-sOutputFile=' + new , old ]
131
+ converter ['pdf' ] = make_external_conversion_command (cmd )
132
+ converter ['eps' ] = make_external_conversion_command (cmd )
133
+
134
+ if matplotlib .checkdep_inkscape () is not None :
135
+ cmd = lambda old , new : \
136
+ ['inkscape' , '-z' , old , '--export-png' , new ]
137
+ converter ['svg' ] = make_external_conversion_command (cmd )
138
+
139
+
140
+ #: A dictionary that maps filename extensions to functions which
141
+ #: themselves map arguments `old` and `new` (filenames) to a list of strings.
142
+ #: The list can then be passed to Popen to convert files with that
143
+ #: extension to png format.
144
+ converter = {}
145
+ _update_converter ()
167
146
168
147
169
148
def comparable_formats ():
170
- '''Returns the list of file formats that compare_images can compare
171
- on this system.'''
149
+ """
150
+ Returns the list of file formats that compare_images can compare
151
+ on this system.
152
+
153
+ """
172
154
return ['png' ] + list (six .iterkeys (converter ))
173
155
174
156
175
157
def convert (filename , cache ):
176
- '''
158
+ """
177
159
Convert the named file into a png file. Returns the name of the
178
160
created file.
179
161
180
162
If *cache* is True, the result of the conversion is cached in
181
- `~/.matplotlib/test_cache/`. The caching is based on a hash of the
182
- exact contents of the input file. The is no limit on the size of
183
- the cache, so it may need to be manually cleared periodically.
184
- '''
163
+ `matplotlib._get_cachedir() + '/test_cache/'`. The caching is based
164
+ on a hash of the exact contents of the input file. The is no limit
165
+ on the size of the cache, so it may need to be manually cleared
166
+ periodically.
167
+
168
+ """
185
169
base , extension = filename .rsplit ('.' , 1 )
186
170
if extension not in converter :
187
171
raise ImageComparisonFailure (
@@ -200,9 +184,9 @@ def convert(filename, cache):
200
184
cache_dir = None
201
185
202
186
if cache_dir is not None :
203
- hash = get_file_hash (filename )
187
+ hash_value = get_file_hash (filename )
204
188
new_ext = os .path .splitext (newname )[1 ]
205
- cached_file = os .path .join (cache_dir , hash + new_ext )
189
+ cached_file = os .path .join (cache_dir , hash_value + new_ext )
206
190
if os .path .exists (cached_file ):
207
191
shutil .copyfile (cached_file , newname )
208
192
return newname
@@ -214,13 +198,20 @@ def convert(filename, cache):
214
198
215
199
return newname
216
200
201
+ #: Maps file extensions to a function which takes a filename as its
202
+ #: only argument to return a list suitable for execution with Popen.
203
+ #: The purpose of this is so that the result file (with the given
204
+ #: extension) can be verified with tools such as xmllint for svg.
217
205
verifiers = {}
218
206
207
+ # Turning this off, because it seems to cause multiprocessing issues
208
+ if matplotlib .checkdep_xmllint () and False :
209
+ verifiers ['svg' ] = lambda filename : [
210
+ 'xmllint' , '--valid' , '--nowarning' , '--noout' , filename ]
211
+
219
212
220
213
def verify (filename ):
221
- """
222
- Verify the file through some sort of verification tool.
223
- """
214
+ """Verify the file through some sort of verification tool."""
224
215
if not os .path .exists (filename ):
225
216
raise IOError ("'%s' does not exist" % filename )
226
217
base , extension = filename .rsplit ('.' , 1 )
@@ -239,11 +230,6 @@ def verify(filename):
239
230
msg += "Standard error:\n %s\n " % stderr
240
231
raise IOError (msg )
241
232
242
- # Turning this off, because it seems to cause multiprocessing issues
243
- if matplotlib .checkdep_xmllint () and False :
244
- verifiers ['svg' ] = lambda filename : [
245
- 'xmllint' , '--valid' , '--nowarning' , '--noout' , filename ]
246
-
247
233
248
234
def crop_to_same (actual_path , actual_image , expected_path , expected_image ):
249
235
# clip the images to the same size -- this is useful only when
@@ -257,12 +243,12 @@ def crop_to_same(actual_path, actual_image, expected_path, expected_image):
257
243
258
244
259
245
def calculate_rms (expectedImage , actualImage ):
260
- # calculate the per-pixel errors, then compute the root mean square error
246
+ "Calculate the per-pixel errors, then compute the root mean square error."
261
247
num_values = np .prod (expectedImage .shape )
262
248
abs_diff_image = abs (expectedImage - actualImage )
263
249
264
- # On Numpy 1.6, we can use bincount with minlength, which is much faster than
265
- # using histogram
250
+ # On Numpy 1.6, we can use bincount with minlength, which is much
251
+ # faster than using histogram
266
252
expected_version = version .LooseVersion ("1.6" )
267
253
found_version = version .LooseVersion (np .__version__ )
268
254
if found_version >= expected_version :
@@ -277,24 +263,34 @@ def calculate_rms(expectedImage, actualImage):
277
263
278
264
279
265
def compare_images (expected , actual , tol , in_decorator = False ):
280
- '''Compare two image files - not the greatest, but fast and good enough.
281
-
282
- = EXAMPLE
283
-
284
- # img1 = "./baseline/plot.png"
285
- # img2 = "./output/plot.png"
286
- #
287
- # compare_images( img1, img2, 0.001 ):
288
-
289
- = INPUT VARIABLES
290
- - expected The filename of the expected image.
291
- - actual The filename of the actual image.
292
- - tol The tolerance (a color value difference, where 255 is the
293
- maximal difference). The test fails if the average pixel
294
- difference is greater than this value.
295
- - in_decorator If called from image_comparison decorator, this should be
296
- True. (default=False)
297
- '''
266
+ """
267
+ Compare two "image" files checking differences within a tolerance.
268
+
269
+ The two given filenames may point to files which are convertible to
270
+ PNG via the `.converter` dictionary. The underlying RMS is calculated
271
+ with the `.calculate_rms` function.
272
+
273
+ Parameters
274
+ ----------
275
+ expected : str
276
+ The filename of the expected image.
277
+ actual :str
278
+ The filename of the actual image.
279
+ tol : float
280
+ The tolerance (a color value difference, where 255 is the
281
+ maximal difference). The test fails if the average pixel
282
+ difference is greater than this value.
283
+ in_decorator : bool
284
+ If called from image_comparison decorator, this should be
285
+ True. (default=False)
286
+
287
+ Example
288
+ -------
289
+ img1 = "./baseline/plot.png"
290
+ img2 = "./output/plot.png"
291
+ compare_images( img1, img2, 0.001 ):
292
+
293
+ """
298
294
if not os .path .exists (actual ):
299
295
msg = "Output image %s does not exist." % actual
300
296
raise Exception (msg )
@@ -329,10 +325,6 @@ def compare_images(expected, actual, tol, in_decorator=False):
329
325
expectedImage = expectedImage .astype (np .int16 )
330
326
actualImage = actualImage .astype (np .int16 )
331
327
332
- # compare the resulting image histogram functions
333
- expected_version = version .LooseVersion ("1.6" )
334
- found_version = version .LooseVersion (np .__version__ )
335
-
336
328
rms = calculate_rms (expectedImage , actualImage )
337
329
338
330
diff_image = make_test_filename (actual , 'failed-diff' )
@@ -344,23 +336,19 @@ def compare_images(expected, actual, tol, in_decorator=False):
344
336
345
337
save_diff_image (expected , actual , diff_image )
346
338
347
- if in_decorator :
348
- results = dict (
349
- rms = rms ,
350
- expected = str (expected ),
351
- actual = str (actual ),
352
- diff = str (diff_image ),
353
- )
354
- return results
355
- else :
356
- # old-style call from mplTest directory
357
- msg = " Error: Image files did not match.\n " \
358
- " RMS Value: " + str (rms ) + "\n " \
359
- " Expected:\n " + str (expected ) + "\n " \
360
- " Actual:\n " + str (actual ) + "\n " \
361
- " Difference:\n " + str (diff_image ) + "\n " \
362
- " Tolerance: " + str (tol ) + "\n "
363
- return msg
339
+ results = dict (rms = rms , expected = str (expected ),
340
+ actual = str (actual ), diff = str (diff_image ))
341
+
342
+ if not in_decorator :
343
+ # Then the results should be a string suitable for stdout.
344
+ template = ['Error: Image files did not match.' ,
345
+ 'RMS Value: {rms}' ,
346
+ 'Expected: \n {expected}' ,
347
+ 'Actual: \n {actual}' ,
348
+ 'Difference:\n {diff}' ,
349
+ 'Tolerance: \n {tol}' ,]
350
+ results = '\n ' .join ([line .format (** results ) for line in template ])
351
+ return results
364
352
365
353
366
354
def save_diff_image (expected , actual , output ):
0 commit comments