def yb_compare_images(expected, actual, tol): """ Compares a baseline image and test generated actual image using a matplotlib's built-in imagine comparison function expected : string, imagepath The image filepath to the baseline image actual : string, imagepath The image filepath to the actual test generated image tol : float The tolerance (a color value difference, where 255 is the maximal difference). The test fails if the average pixel difference is greater than this value. """ __tracebackhide__ = True if not os.path.exists(expected): raise ImageComparisonFailure('image does not exist: %s' % expected) # method from matplotlib.testing.compare err = compare_images(expected, actual, tol, in_decorator=True) if err: raise ImageComparisonFailure( 'images not close (RMS %(rms).3f):\n\t%(actual)s\n\t%(expected)s ' % err)
def do_test(): figure = plt.figure(fignum) if self._remove_text: self.remove_text(figure) figure.savefig(actual_fname, **self._savefig_kwarg) plt.close(figure) err = compare_images(expected_fname, actual_fname, self._tol, in_decorator=True) try: if not os.path.exists(expected_fname): raise ImageComparisonFailure( 'image does not exist: %s' % expected_fname) if err: raise ImageComparisonFailure( 'images not close: %(actual)s vs. %(expected)s' ' (RMS %(rms).3f)' % err) except ImageComparisonFailure: if not check_freetype_version(self._freetype_version): raise KnownFailure( "Mismatched version of freetype. Test " "requires '%s', you have '%s'" % (self._freetype_version, ft2font.__freetype_version__)) raise
def _compare_animation(anim, expected, format_, nframes, tol): # generate images from the animation base_dir, filename = split(join('tests', 'baseline_images', expected)) out_dir = split(join('tests', 'output_images', expected))[0] if not os.path.exists(out_dir): os.makedirs(out_dir) anim.save(os.path.join(out_dir, (filename + format_)), writer=BunchOFiles()) for i in range(nframes): image_name = '%s%d%s' % (filename, i, format_) expected_name = os.path.join(base_dir, image_name) actual_name = os.path.join(out_dir, image_name) err = compare_images(expected_name, actual_name, tol, in_decorator=True) if not os.path.exists(expected_name): raise ImageComparisonFailure('image does not exist: %s' % expected_name) if err: for key in ["actual", "expected"]: err[key] = os.path.relpath(err[key]) raise ImageComparisonFailure( 'images not close (RMS %(rms).3f):\n\t%(actual)s\n\t%(expected)s ' % err)
def save_diff_image(expected, actual, output): expectedImage = _png.read_png(expected) actualImage = _png.read_png(actual) actualImage, expectedImage = crop_to_same( actual, actualImage, expected, expectedImage) expectedImage = np.array(expectedImage).astype(float) actualImage = np.array(actualImage).astype(float) if expectedImage.shape != actualImage.shape: raise ImageComparisonFailure( "Image sizes do not match expected size: {0} " "actual size {1}".format(expectedImage.shape, actualImage.shape)) absDiffImage = np.abs(expectedImage - actualImage) # expand differences in luminance domain absDiffImage *= 255 * 10 save_image_np = np.clip(absDiffImage, 0, 255).astype(np.uint8) height, width, depth = save_image_np.shape # The PDF renderer doesn't produce an alpha channel, but the # matplotlib PNG writer requires one, so expand the array if depth == 3: with_alpha = np.empty((height, width, 4), dtype=np.uint8) with_alpha[:, :, 0:3] = save_image_np save_image_np = with_alpha # Hard-code the alpha channel to fully solid save_image_np[:, :, 3] = 255 _png.write_png(save_image_np, output)
def __call__(self, orig, dest): if not self._proc: self._stdout = TemporaryFile() self._proc = subprocess.Popen( [ mpl._get_executable_info("gs").executable, "-dNOPAUSE", "-sDEVICE=png16m" ], # As far as I can see, ghostscript never outputs to stderr. stdin=subprocess.PIPE, stdout=subprocess.PIPE) try: self._read_until(b"\nGS") except _ConverterError: raise OSError("Failed to start Ghostscript") def encode_and_escape(name): return (os.fsencode(name).replace(b"\\", b"\\\\").replace( b"(", br"\(").replace(b")", br"\)")) self._proc.stdin.write(b"<< /OutputFile (" + encode_and_escape(dest) + b") >> setpagedevice (" + encode_and_escape(orig) + b") run flush\n") self._proc.stdin.flush() # GS> if nothing left on the stack; GS<n> if n items left on the stack. err = self._read_until(b"GS") stack = self._read_until(b">") if stack or not os.path.exists(dest): stack_size = int(stack[1:]) if stack else 0 self._proc.stdin.write(b"pop\n" * stack_size) # Using the systemencoding should at least get the filenames right. raise ImageComparisonFailure((err + b"GS" + stack + b">").decode( sys.getfilesystemencoding(), "replace"))
def __call__(self, orig, dest): if (not self._proc # First run. or self._proc.poll() is not None): # Inkscape terminated. env = os.environ.copy() # If one passes e.g. a png file to Inkscape, it will try to # query the user for conversion options via a GUI (even with # `--without-gui`). Unsetting `DISPLAY` prevents this (and causes # GTK to crash and Inkscape to terminate, but that'll just be # reported as a regular exception below). env.pop("DISPLAY", None) # May already be unset. # Do not load any user options. env["INKSCAPE_PROFILE_DIR"] = os.devnull # Old versions of Inkscape (0.48.3.1, used on Travis as of now) # seem to sometimes deadlock when stderr is redirected to a pipe, # so we redirect it to a temporary file instead. This is not # necessary anymore as of Inkscape 0.92.1. stderr = TemporaryFile() self._proc = subprocess.Popen( ["inkscape", "--without-gui", "--shell"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=stderr, env=env) # Slight abuse, but makes shutdown handling easier. self._proc.stderr = stderr try: self._read_until(b"\n>") except _ConverterError as err: raise OSError("Failed to start Inkscape in interactive " "mode") from err # Inkscape uses glib's `g_shell_parse_argv`, which has a consistent # behavior across platforms, so we can just use `shlex.quote`. orig_b, dest_b = map(_shlex_quote_bytes, map(os.fsencode, [orig, dest])) if b"\n" in orig_b or b"\n" in dest_b: # Who knows whether the current folder name has a newline, or if # our encoding is even ASCII compatible... Just fall back on the # slow solution (Inkscape uses `fgets` so it will always stop at a # newline). cbook.warn_deprecated( "3.3", message="Support for converting files from paths " "containing a newline is deprecated since %(since)s and " "support will be removed %(removal)s") return make_external_conversion_command( lambda old, new: ['inkscape', '-z', old, '--export-png', new])( orig, dest) self._proc.stdin.write(orig_b + b" --export-png=" + dest_b + b"\n") self._proc.stdin.flush() try: self._read_until(b"\n>") except _ConverterError as err: # Inkscape's output is not localized but gtk's is, so the output # stream probably has a mixed encoding. Using the filesystem # encoding should at least get the filenames right... self._proc.stderr.seek(0) raise ImageComparisonFailure(self._proc.stderr.read().decode( sys.getfilesystemencoding(), "replace")) from err
def calculate_rms(expectedImage, actualImage): "Calculate the per-pixel errors, then compute the root mean square error." if expectedImage.shape != actualImage.shape: raise ImageComparisonFailure( "Image sizes do not match expected size: {0} " "actual size {1}".format(expectedImage.shape, actualImage.shape)) # Convert to float to avoid overflowing finite integer types. return np.sqrt(((expectedImage - actualImage).astype(float) ** 2).mean())
def __call__(self, orig, dest): if (not self._proc # First run. or self._proc.poll() is not None): # Inkscape terminated. env = os.environ.copy() # If one passes e.g. a png file to Inkscape, it will try to # query the user for conversion options via a GUI (even with # `--without-gui`). Unsetting `DISPLAY` prevents this (and causes # GTK to crash and Inkscape to terminate, but that'll just be # reported as a regular exception below). env.pop("DISPLAY", None) # May already be unset. # Do not load any user options. # `os.environ` needs native strings on Py2+Windows. env[str("INKSCAPE_PROFILE_DIR")] = os.devnull # Old versions of Inkscape (0.48.3.1, used on Travis as of now) # seem to sometimes deadlock when stderr is redirected to a pipe, # so we redirect it to a temporary file instead. This is not # necessary anymore as of Inkscape 0.92.1. self._stderr = TemporaryFile() self._proc = subprocess.Popen( [str("inkscape"), "--without-gui", "--shell"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=self._stderr, env=env) if not self._read_to_prompt(): raise OSError("Failed to start Inkscape") try: fsencode = os.fsencode except AttributeError: # Py2. def fsencode(s): return s.encode(sys.getfilesystemencoding()) # Inkscape uses glib's `g_shell_parse_argv`, which has a consistent # behavior across platforms, so we can just use `shlex.quote`. orig_b, dest_b = map(_shlex_quote_bytes, map(fsencode, [orig, dest])) if b"\n" in orig_b or b"\n" in dest_b: # Who knows whether the current folder name has a newline, or if # our encoding is even ASCII compatible... Just fall back on the # slow solution (Inkscape uses `fgets` so it will always stop at a # newline). return make_external_conversion_command( lambda old, new: [str('inkscape'), '-z', old, '--export-png', new])(orig, dest) self._proc.stdin.write(orig_b + b" --export-png=" + dest_b + b"\n") self._proc.stdin.flush() if not self._read_to_prompt(): # Inkscape's output is not localized but gtk's is, so the # output stream probably has a mixed encoding. Using # `getfilesystemencoding` should at least get the filenames # right... self._stderr.seek(0) raise ImageComparisonFailure(self._stderr.read().decode( sys.getfilesystemencoding(), "replace"))
def calculate_rms(expectedImage, actualImage): "Calculate the per-pixel errors, then compute the root mean square error." if expectedImage.shape != actualImage.shape: raise ImageComparisonFailure( "Image sizes do not match expected size: {0} " "actual size {1}".format(expectedImage.shape, actualImage.shape)) num_values = expectedImage.size abs_diff_image = abs(expectedImage - actualImage) histogram = np.bincount(abs_diff_image.ravel(), minlength=256) sum_of_squares = np.sum(histogram * np.arange(len(histogram))**2) rms = np.sqrt(float(sum_of_squares) / num_values) return rms
def save_diff_image(expected, actual, output): ''' Parameters ---------- expected : str File path of expected image. actual : str File path of actual image. output : str File path to save difference image to. ''' # Drop alpha channels, similarly to compare_images. from matplotlib import _png with open(expected, "rb") as expected_file: expected_image = _png.read_png(expected_file)[..., :3] with open(actual, "rb") as actual_file: actual_image = _png.read_png(actual_file)[..., :3] actual_image, expected_image = crop_to_same( actual, actual_image, expected, expected_image) expected_image = np.array(expected_image).astype(float) actual_image = np.array(actual_image).astype(float) if expected_image.shape != actual_image.shape: raise ImageComparisonFailure( "Image sizes do not match expected size: {} " "actual size {}".format(expected_image.shape, actual_image.shape)) abs_diff_image = np.abs(expected_image - actual_image) # expand differences in luminance domain abs_diff_image *= 255 * 10 save_image_np = np.clip(abs_diff_image, 0, 255).astype(np.uint8) height, width, depth = save_image_np.shape # The PDF renderer doesn't produce an alpha channel, but the # matplotlib PNG writer requires one, so expand the array if depth == 3: with_alpha = np.empty((height, width, 4), dtype=np.uint8) with_alpha[:, :, 0:3] = save_image_np save_image_np = with_alpha # Hard-code the alpha channel to fully solid save_image_np[:, :, 3] = 255 with open(output, "wb") as output_file: _png.write_png(save_image_np, output_file)
def convert(filename, cache): """ Convert the named file into a png file. Returns the name of the created file. If *cache* is True, the result of the conversion is cached in `matplotlib._get_cachedir() + '/test_cache/'`. The caching is based on a hash of the exact contents of the input file. The is no limit on the size of the cache, so it may need to be manually cleared periodically. """ base, extension = filename.rsplit('.', 1) if extension not in converter: raise ImageComparisonFailure( "Don't know how to convert %s files to png" % extension) newname = base + '_' + extension + '.png' if not os.path.exists(filename): raise IOError("'%s' does not exist" % filename) # Only convert the file if the destination doesn't already exist or # is out of date. if (not os.path.exists(newname) or os.stat(newname).st_mtime < os.stat(filename).st_mtime): if cache: cache_dir = get_cache_dir() else: cache_dir = None if cache_dir is not None: hash_value = get_file_hash(filename) new_ext = os.path.splitext(newname)[1] cached_file = os.path.join(cache_dir, hash_value + new_ext) if os.path.exists(cached_file): shutil.copyfile(cached_file, newname) return newname converter[extension](filename, newname) if cache_dir is not None: shutil.copyfile(newname, cached_file) return newname
def save_diff_image(expected, actual, output): """ Parameters ---------- expected : str File path of expected image. actual : str File path of actual image. output : str File path to save difference image to. """ # Drop alpha channels, similarly to compare_images. expected_image = np.asarray(Image.open(expected).convert("RGB")) actual_image = np.asarray(Image.open(actual).convert("RGB")) actual_image, expected_image = crop_to_same(actual, actual_image, expected, expected_image) expected_image = np.array(expected_image).astype(float) actual_image = np.array(actual_image).astype(float) if expected_image.shape != actual_image.shape: raise ImageComparisonFailure( "Image sizes do not match expected size: {} " "actual size {}".format(expected_image.shape, actual_image.shape)) abs_diff_image = np.abs(expected_image - actual_image) # expand differences in luminance domain abs_diff_image *= 255 * 10 save_image_np = np.clip(abs_diff_image, 0, 255).astype(np.uint8) height, width, depth = save_image_np.shape # The PDF renderer doesn't produce an alpha channel, but the # matplotlib PNG writer requires one, so expand the array if depth == 3: with_alpha = np.empty((height, width, 4), dtype=np.uint8) with_alpha[:, :, 0:3] = save_image_np save_image_np = with_alpha # Hard-code the alpha channel to fully solid save_image_np[:, :, 3] = 255 Image.fromarray(save_image_np).save(output, format="png")
def calculate_rms(expectedImage, actualImage): "Calculate the per-pixel errors, then compute the root mean square error." if expectedImage.shape != actualImage.shape: raise ImageComparisonFailure( "image sizes do not match expected size: {0} " "actual size {1}".format(expectedImage.shape, actualImage.shape)) num_values = np.prod(expectedImage.shape) abs_diff_image = abs(expectedImage - actualImage) # On Numpy 1.6, we can use bincount with minlength, which is much # faster than using histogram expected_version = version.LooseVersion("1.6") found_version = version.LooseVersion(np.__version__) if found_version >= expected_version: histogram = np.bincount(abs_diff_image.ravel(), minlength=256) else: histogram = np.histogram(abs_diff_image, bins=np.arange(257))[0] sum_of_squares = np.sum(histogram * np.arange(len(histogram))**2) rms = np.sqrt(float(sum_of_squares) / num_values) return rms
def __call__(self, orig, dest): old_inkscape = mpl._get_executable_info("inkscape").version < "1" terminator = b"\n>" if old_inkscape else b"> " if not hasattr(self, "_tmpdir"): self._tmpdir = TemporaryDirectory() if (not self._proc # First run. or self._proc.poll() is not None): # Inkscape terminated. env = { **os.environ, # If one passes e.g. a png file to Inkscape, it will try to # query the user for conversion options via a GUI (even with # `--without-gui`). Unsetting `DISPLAY` prevents this (and # causes GTK to crash and Inkscape to terminate, but that'll # just be reported as a regular exception below). "DISPLAY": "", # Do not load any user options. "INKSCAPE_PROFILE_DIR": os.devnull, } # Old versions of Inkscape (e.g. 0.48.3.1) seem to sometimes # deadlock when stderr is redirected to a pipe, so we redirect it # to a temporary file instead. This is not necessary anymore as of # Inkscape 0.92.1. stderr = TemporaryFile() self._proc = subprocess.Popen( ["inkscape", "--without-gui", "--shell"] if old_inkscape else ["inkscape", "--shell"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=stderr, env=env, cwd=self._tmpdir.name) # Slight abuse, but makes shutdown handling easier. self._proc.stderr = stderr try: self._read_until(terminator) except _ConverterError as err: raise OSError("Failed to start Inkscape in interactive " "mode") from err # Inkscape's shell mode does not support escaping metacharacters in the # filename ("\n", and ":;" for inkscape>=1). Avoid any problems by # running from a temporary directory and using fixed filenames. inkscape_orig = Path(self._tmpdir.name, os.fsdecode(b"f.svg")) inkscape_dest = Path(self._tmpdir.name, os.fsdecode(b"f.png")) try: inkscape_orig.symlink_to(Path(orig).resolve()) except OSError: shutil.copyfile(orig, inkscape_orig) self._proc.stdin.write( b"f.svg --export-png=f.png\n" if old_inkscape else b"file-open:f.svg;export-filename:f.png;export-do;file-close\n") self._proc.stdin.flush() try: self._read_until(terminator) except _ConverterError as err: # Inkscape's output is not localized but gtk's is, so the output # stream probably has a mixed encoding. Using the filesystem # encoding should at least get the filenames right... self._proc.stderr.seek(0) raise ImageComparisonFailure(self._proc.stderr.read().decode( sys.getfilesystemencoding(), "replace")) from err os.remove(inkscape_orig) shutil.move(inkscape_dest, dest)