1- #=======================================================================
2-
3- """ A set of utilities for comparing results.
41"""
5- #=======================================================================
2+ Provides a collection of utilities for comparing (image) results.
63
4+ """
75from __future__ import (absolute_import , division , print_function ,
86 unicode_literals )
97
108import six
11- from six .moves import xrange
9+
10+ import hashlib
11+ import os
12+ import shutil
13+
14+ import numpy as np
1215
1316import matplotlib
1417from matplotlib .compat import subprocess
1518from matplotlib .testing .noseclasses import ImageComparisonFailure
16- from matplotlib .testing import image_util
1719from matplotlib import _png
1820from matplotlib import _get_cachedir
1921from matplotlib import cbook
2022from 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- #=======================================================================
3023
31- __all__ = [
32- 'compare_float' ,
33- 'compare_images' ,
34- 'comparable_formats' ,
35- ]
36-
37- #-----------------------------------------------------------------------
24+ __all__ = ['compare_float' , 'compare_images' , 'comparable_formats' ]
3825
3926
4027def make_test_filename (fname , purpose ):
@@ -47,32 +34,29 @@ def make_test_filename(fname, purpose):
4734
4835
4936def 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.
5240
5341 You can specify a relative tolerance, absolute tolerance, or both.
42+
5443 """
5544 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." )
6148 msg = ""
6249
6350 if absTol is not None :
6451 absDiff = abs (expected - actual )
6552 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}' ]
7058
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 ])
7660
7761 if relTol is not None :
7862 # 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):
8266 relDiff = relDiff / abs (expected )
8367
8468 if relTol < relDiff :
85-
8669 # 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}' ]
9275
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 ])
9877
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
10879
10980
11081def get_cache_dir ():
@@ -122,7 +93,7 @@ def get_cache_dir():
12293 return cache_dir
12394
12495
125- def get_file_hash (path , block_size = 2 ** 20 ):
96+ def get_file_hash (path , block_size = 2 ** 20 ):
12697 md5 = hashlib .md5 ()
12798 with open (path , 'rb' ) as fd :
12899 while True :
@@ -132,8 +103,6 @@ def get_file_hash(path, block_size=2 ** 20):
132103 md5 .update (data )
133104 return md5 .hexdigest ()
134105
135- converter = {}
136-
137106
138107def make_external_conversion_command (cmd ):
139108 def convert (old , new ):
@@ -152,36 +121,51 @@ def convert(old, new):
152121
153122 return convert
154123
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 )
162124
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 ()
167146
168147
169148def 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+ """
172154 return ['png' ] + list (six .iterkeys (converter ))
173155
174156
175157def convert (filename , cache ):
176- '''
158+ """
177159 Convert the named file into a png file. Returns the name of the
178160 created file.
179161
180162 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+ """
185169 base , extension = filename .rsplit ('.' , 1 )
186170 if extension not in converter :
187171 raise ImageComparisonFailure (
@@ -200,9 +184,9 @@ def convert(filename, cache):
200184 cache_dir = None
201185
202186 if cache_dir is not None :
203- hash = get_file_hash (filename )
187+ hash_value = get_file_hash (filename )
204188 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 )
206190 if os .path .exists (cached_file ):
207191 shutil .copyfile (cached_file , newname )
208192 return newname
@@ -214,13 +198,20 @@ def convert(filename, cache):
214198
215199 return newname
216200
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.
217205verifiers = {}
218206
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+
219212
220213def verify (filename ):
221- """
222- Verify the file through some sort of verification tool.
223- """
214+ """Verify the file through some sort of verification tool."""
224215 if not os .path .exists (filename ):
225216 raise IOError ("'%s' does not exist" % filename )
226217 base , extension = filename .rsplit ('.' , 1 )
@@ -239,11 +230,6 @@ def verify(filename):
239230 msg += "Standard error:\n %s\n " % stderr
240231 raise IOError (msg )
241232
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-
247233
248234def crop_to_same (actual_path , actual_image , expected_path , expected_image ):
249235 # 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):
257243
258244
259245def 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."
261247 num_values = np .prod (expectedImage .shape )
262248 abs_diff_image = abs (expectedImage - actualImage )
263249
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
266252 expected_version = version .LooseVersion ("1.6" )
267253 found_version = version .LooseVersion (np .__version__ )
268254 if found_version >= expected_version :
@@ -277,24 +263,34 @@ def calculate_rms(expectedImage, actualImage):
277263
278264
279265def 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+ """
298294 if not os .path .exists (actual ):
299295 msg = "Output image %s does not exist." % actual
300296 raise Exception (msg )
@@ -329,10 +325,6 @@ def compare_images(expected, actual, tol, in_decorator=False):
329325 expectedImage = expectedImage .astype (np .int16 )
330326 actualImage = actualImage .astype (np .int16 )
331327
332- # compare the resulting image histogram functions
333- expected_version = version .LooseVersion ("1.6" )
334- found_version = version .LooseVersion (np .__version__ )
335-
336328 rms = calculate_rms (expectedImage , actualImage )
337329
338330 diff_image = make_test_filename (actual , 'failed-diff' )
@@ -344,23 +336,19 @@ def compare_images(expected, actual, tol, in_decorator=False):
344336
345337 save_diff_image (expected , actual , diff_image )
346338
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
364352
365353
366354def save_diff_image (expected , actual , output ):
0 commit comments