def _run_skpmaker(self, output_path, red=0, green=0, blue=0, width=640, height=400): """Runs the skpmaker binary to generate SKP with known characteristics. Args: output_path: Filepath to write the SKP into. red: Value of red color channel in image, 0-255. green: Value of green color channel in image, 0-255. blue: Value of blue color channel in image, 0-255. width: Width of canvas to create. height: Height of canvas to create. """ binary = find_run_binary.find_path_to_program('skpmaker') return find_run_binary.run_command([ binary, '--red', str(red), '--green', str(green), '--blue', str(blue), '--width', str(width), '--height', str(height), '--writePath', str(output_path), ])
def run_command(self, args): """Runs a program from the command line and returns stdout. Args: args: Command line to run, as a list of string parameters. args[0] is the binary to run. Returns: stdout from the program, as a single string. Raises: Exception: the program exited with a nonzero return code. """ return find_run_binary.run_command(args)
def __init__(self, gs, storage_root, expected_image_url, expected_image_locator, actual_image_url, actual_image_locator, expected_images_subdir=DEFAULT_IMAGES_SUBDIR, actual_images_subdir=DEFAULT_IMAGES_SUBDIR, image_suffix=DEFAULT_IMAGE_SUFFIX): """Download this pair of images (unless we already have them on local disk), and prepare a DiffRecord for them. Args: gs: instance of GSUtils object we can use to download images storage_root: root directory on local disk within which we store all images expected_image_url: file, GS, or HTTP url from which we will download the expected image expected_image_locator: a unique ID string under which we will store the expected image within storage_root (probably including a checksum to guarantee uniqueness) actual_image_url: file, GS, or HTTP url from which we will download the actual image actual_image_locator: a unique ID string under which we will store the actual image within storage_root (probably including a checksum to guarantee uniqueness) expected_images_subdir: the subdirectory expected images are stored in. actual_images_subdir: the subdirectory actual images are stored in. image_suffix: the suffix of images. """ expected_image_locator = _sanitize_locator(expected_image_locator) actual_image_locator = _sanitize_locator(actual_image_locator) # Download the expected/actual images, if we don't have them already. expected_image_file = os.path.join( storage_root, expected_images_subdir, str(expected_image_locator) + image_suffix) actual_image_file = os.path.join( storage_root, actual_images_subdir, str(actual_image_locator) + image_suffix) for image_file, image_url in [ (expected_image_file, expected_image_url), (actual_image_file, actual_image_url)]: if image_file and image_url: try: _download_file(gs, image_file, image_url) except Exception: logging.exception('unable to download image_url %s to file %s' % (image_url, image_file)) raise # Return early if we do not need to generate diffs. if (expected_image_url == actual_image_url or not expected_image_url or not actual_image_url): return # Get all diff images and values using the skpdiff binary. skpdiff_output_dir = tempfile.mkdtemp() try: skpdiff_summary_file = os.path.join(skpdiff_output_dir, 'skpdiff-output.json') skpdiff_rgbdiff_dir = os.path.join(storage_root, RGBDIFFS_SUBDIR) skpdiff_whitediff_dir = os.path.join(storage_root, WHITEDIFFS_SUBDIR) _mkdir_unless_exists(skpdiff_rgbdiff_dir) _mkdir_unless_exists(skpdiff_rgbdiff_dir) # TODO(epoger): Consider calling skpdiff ONCE for all image pairs, # instead of calling it separately for each image pair. # Pro: we'll incur less overhead from making repeated system calls, # spinning up the skpdiff binary, etc. # Con: we would have to wait until all image pairs were loaded before # generating any of the diffs? # Note(stephana): '--longnames' was added to allow for this # case (multiple files at once) versus specifying output diffs # directly. find_run_binary.run_command( [SKPDIFF_BINARY, '-p', expected_image_file, actual_image_file, '--jsonp', 'false', '--longnames', 'true', '--output', skpdiff_summary_file, '--differs', 'perceptual', 'different_pixels', '--rgbDiffDir', skpdiff_rgbdiff_dir, '--whiteDiffDir', skpdiff_whitediff_dir, ]) # Get information out of the skpdiff_summary_file. with contextlib.closing(open(skpdiff_summary_file)) as fp: data = json.load(fp) # For now, we can assume there is only one record in the output summary, # since we passed skpdiff only one pair of images. record = data['records'][0] self._width = record['width'] self._height = record['height'] self._diffUrl = os.path.split(record['rgbDiffPath'])[1] self._whiteDiffUrl = os.path.split(record['whiteDiffPath'])[1] # TODO: make max_diff_per_channel a tuple instead of a list, because the # structure is meaningful (first element is red, second is green, etc.) # See http://stackoverflow.com/a/626871 self._max_diff_per_channel = [ record['maxRedDiff'], record['maxGreenDiff'], record['maxBlueDiff']] per_differ_stats = record['diffs'] for stats in per_differ_stats: differ_name = stats['differName'] if differ_name == 'different_pixels': self._num_pixels_differing = stats['pointsOfInterest'] elif differ_name == 'perceptual': perceptual_similarity = stats['result'] # skpdiff returns the perceptual similarity; convert it to get the # perceptual difference percentage. # skpdiff outputs -1 if the images are different sizes. Treat any # output that does not lie in [0, 1] as having 0% perceptual # similarity. if not 0 <= perceptual_similarity <= 1: perceptual_similarity = 0 self._perceptual_difference = 100 - (perceptual_similarity * 100) finally: shutil.rmtree(skpdiff_output_dir)
def __init__(self, gs, storage_root, expected_image_url, expected_image_locator, actual_image_url, actual_image_locator, expected_images_subdir=DEFAULT_IMAGES_SUBDIR, actual_images_subdir=DEFAULT_IMAGES_SUBDIR, image_suffix=DEFAULT_IMAGE_SUFFIX): """Download this pair of images (unless we already have them on local disk), and prepare a DiffRecord for them. Args: gs: instance of GSUtils object we can use to download images storage_root: root directory on local disk within which we store all images expected_image_url: file, GS, or HTTP url from which we will download the expected image expected_image_locator: a unique ID string under which we will store the expected image within storage_root (probably including a checksum to guarantee uniqueness) actual_image_url: file, GS, or HTTP url from which we will download the actual image actual_image_locator: a unique ID string under which we will store the actual image within storage_root (probably including a checksum to guarantee uniqueness) expected_images_subdir: the subdirectory expected images are stored in. actual_images_subdir: the subdirectory actual images are stored in. image_suffix: the suffix of images. """ expected_image_locator = _sanitize_locator(expected_image_locator) actual_image_locator = _sanitize_locator(actual_image_locator) # Download the expected/actual images, if we don't have them already. expected_image_file = os.path.join( storage_root, expected_images_subdir, str(expected_image_locator) + image_suffix) actual_image_file = os.path.join( storage_root, actual_images_subdir, str(actual_image_locator) + image_suffix) for image_file, image_url in [ (expected_image_file, expected_image_url), (actual_image_file, actual_image_url)]: if image_file and image_url: try: _download_file(gs, image_file, image_url) except Exception: logging.exception('unable to download image_url %s to file %s' % (image_url, image_file)) raise # Return early if we do not need to generate diffs. if (expected_image_url == actual_image_url or not expected_image_url or not actual_image_url): return # Get all diff images and values using the skpdiff binary. skpdiff_output_dir = tempfile.mkdtemp() try: skpdiff_summary_file = os.path.join(skpdiff_output_dir, 'skpdiff-output.json') skpdiff_rgbdiff_dir = os.path.join(storage_root, RGBDIFFS_SUBDIR) skpdiff_whitediff_dir = os.path.join(storage_root, WHITEDIFFS_SUBDIR) _mkdir_unless_exists(skpdiff_rgbdiff_dir) _mkdir_unless_exists(skpdiff_rgbdiff_dir) # TODO(epoger): Consider calling skpdiff ONCE for all image pairs, # instead of calling it separately for each image pair. # Pro: we'll incur less overhead from making repeated system calls, # spinning up the skpdiff binary, etc. # Con: we would have to wait until all image pairs were loaded before # generating any of the diffs? # Note(stephana): '--longnames' was added to allow for this # case (multiple files at once) versus specifying output diffs # directly. find_run_binary.run_command( [SKPDIFF_BINARY, '-p', expected_image_file, actual_image_file, '--jsonp', 'false', '--longnames', 'true', '--output', skpdiff_summary_file, '--differs', 'perceptual', 'different_pixels', '--rgbDiffDir', skpdiff_rgbdiff_dir, '--whiteDiffDir', skpdiff_whitediff_dir, ]) # Get information out of the skpdiff_summary_file. with contextlib.closing(open(skpdiff_summary_file)) as fp: data = json.load(fp) # For now, we can assume there is only one record in the output summary, # since we passed skpdiff only one pair of images. record = data['records'][0] self._width = record['width'] self._height = record['height'] # TODO: make max_diff_per_channel a tuple instead of a list, because the # structure is meaningful (first element is red, second is green, etc.) # See http://stackoverflow.com/a/626871 self._max_diff_per_channel = [ record['maxRedDiff'], record['maxGreenDiff'], record['maxBlueDiff']] per_differ_stats = record['diffs'] for stats in per_differ_stats: differ_name = stats['differName'] if differ_name == 'different_pixels': self._num_pixels_differing = stats['pointsOfInterest'] elif differ_name == 'perceptual': perceptual_similarity = stats['result'] # skpdiff returns the perceptual similarity; convert it to get the # perceptual difference percentage. # skpdiff outputs -1 if the images are different sizes. Treat any # output that does not lie in [0, 1] as having 0% perceptual # similarity. if not 0 <= perceptual_similarity <= 1: perceptual_similarity = 0 self._perceptual_difference = 100 - (perceptual_similarity * 100) finally: shutil.rmtree(skpdiff_output_dir)
def __init__(self, storage_root, expected_image_url, expected_image_locator, actual_image_url, actual_image_locator, expected_images_subdir=DEFAULT_IMAGES_SUBDIR, actual_images_subdir=DEFAULT_IMAGES_SUBDIR, image_suffix=DEFAULT_IMAGE_SUFFIX): """Download this pair of images (unless we already have them on local disk), and prepare a DiffRecord for them. TODO(epoger): Make this asynchronously download images, rather than blocking until the images have been downloaded and processed. Args: storage_root: root directory on local disk within which we store all images expected_image_url: file or HTTP url from which we will download the expected image expected_image_locator: a unique ID string under which we will store the expected image within storage_root (probably including a checksum to guarantee uniqueness) actual_image_url: file or HTTP url from which we will download the actual image actual_image_locator: a unique ID string under which we will store the actual image within storage_root (probably including a checksum to guarantee uniqueness) expected_images_subdir: the subdirectory expected images are stored in. actual_images_subdir: the subdirectory actual images are stored in. image_suffix: the suffix of images. """ expected_image_locator = _sanitize_locator(expected_image_locator) actual_image_locator = _sanitize_locator(actual_image_locator) # Download the expected/actual images, if we don't have them already. # TODO(rmistry): Add a parameter that makes _download_and_open_image raise # an exception if images are not found locally (instead of trying to # download them). expected_image_file = os.path.join( storage_root, expected_images_subdir, str(expected_image_locator) + image_suffix) actual_image_file = os.path.join( storage_root, actual_images_subdir, str(actual_image_locator) + image_suffix) try: expected_image = _download_and_open_image( expected_image_file, expected_image_url) except Exception: logging.exception('unable to download expected_image_url %s to file %s' % (expected_image_url, expected_image_file)) raise try: actual_image = _download_and_open_image( actual_image_file, actual_image_url) except Exception: logging.exception('unable to download actual_image_url %s to file %s' % (actual_image_url, actual_image_file)) raise # Generate the diff image (absolute diff at each pixel) and # max_diff_per_channel. diff_image = _generate_image_diff(actual_image, expected_image) diff_histogram = diff_image.histogram() (diff_width, diff_height) = diff_image.size self._max_diff_per_channel = _max_per_band(diff_histogram) # Generate the whitediff image (any differing pixels show as white). # This is tricky, because when you convert color images to grayscale or # black & white in PIL, it has its own ideas about thresholds. # We have to force it: if a pixel has any color at all, it's a '1'. bands = diff_image.split() graydiff_image = ImageChops.lighter(ImageChops.lighter( bands[0], bands[1]), bands[2]) whitediff_image = (graydiff_image.point(lambda p: p > 0 and VALUES_PER_BAND) .convert('1', dither=Image.NONE)) # Calculate the perceptual difference percentage. skpdiff_csv_dir = tempfile.mkdtemp() try: skpdiff_csv_output = os.path.join(skpdiff_csv_dir, 'skpdiff-output.csv') expected_img = os.path.join(storage_root, expected_images_subdir, str(expected_image_locator) + image_suffix) actual_img = os.path.join(storage_root, actual_images_subdir, str(actual_image_locator) + image_suffix) find_run_binary.run_command( [SKPDIFF_BINARY, '-p', expected_img, actual_img, '--csv', skpdiff_csv_output, '-d', 'perceptual']) with contextlib.closing(open(skpdiff_csv_output)) as csv_file: for row in csv.DictReader(csv_file): perceptual_similarity = float(row[' perceptual'].strip()) if not 0 <= perceptual_similarity <= 1: # skpdiff outputs -1 if the images are different sizes. Treat any # output that does not lie in [0, 1] as having 0% perceptual # similarity. perceptual_similarity = 0 # skpdiff returns the perceptual similarity, convert it to get the # perceptual difference percentage. self._perceptual_difference = 100 - (perceptual_similarity * 100) finally: shutil.rmtree(skpdiff_csv_dir) # Final touches on diff_image: use whitediff_image as an alpha mask. # Unchanged pixels are transparent; differing pixels are opaque. diff_image.putalpha(whitediff_image) # Store the diff and whitediff images generated above. diff_image_locator = _get_difference_locator( expected_image_locator=expected_image_locator, actual_image_locator=actual_image_locator) basename = str(diff_image_locator) + image_suffix _save_image(diff_image, os.path.join( storage_root, DIFFS_SUBDIR, basename)) _save_image(whitediff_image, os.path.join( storage_root, WHITEDIFFS_SUBDIR, basename)) # Calculate difference metrics. (self._width, self._height) = diff_image.size self._num_pixels_differing = ( whitediff_image.histogram()[VALUES_PER_BAND - 1])
def _run_render_pictures(self, args): binary = find_run_binary.find_path_to_program('render_pictures') return find_run_binary.run_command( [binary, '--config', '8888'] + args)
def __init__(self, storage_root, expected_image_url, expected_image_locator, actual_image_url, actual_image_locator, expected_images_subdir=DEFAULT_IMAGES_SUBDIR, actual_images_subdir=DEFAULT_IMAGES_SUBDIR, image_suffix=DEFAULT_IMAGE_SUFFIX): """Download this pair of images (unless we already have them on local disk), and prepare a DiffRecord for them. TODO(epoger): Make this asynchronously download images, rather than blocking until the images have been downloaded and processed. Args: storage_root: root directory on local disk within which we store all images expected_image_url: file or HTTP url from which we will download the expected image expected_image_locator: a unique ID string under which we will store the expected image within storage_root (probably including a checksum to guarantee uniqueness) actual_image_url: file or HTTP url from which we will download the actual image actual_image_locator: a unique ID string under which we will store the actual image within storage_root (probably including a checksum to guarantee uniqueness) expected_images_subdir: the subdirectory expected images are stored in. actual_images_subdir: the subdirectory actual images are stored in. image_suffix: the suffix of images. """ expected_image_locator = _sanitize_locator(expected_image_locator) actual_image_locator = _sanitize_locator(actual_image_locator) # Download the expected/actual images, if we don't have them already. # TODO(rmistry): Add a parameter that makes _download_and_open_image raise # an exception if images are not found locally (instead of trying to # download them). expected_image_file = os.path.join( storage_root, expected_images_subdir, str(expected_image_locator) + image_suffix) actual_image_file = os.path.join( storage_root, actual_images_subdir, str(actual_image_locator) + image_suffix) try: expected_image = _download_and_open_image(expected_image_file, expected_image_url) except Exception: logging.exception( 'unable to download expected_image_url %s to file %s' % (expected_image_url, expected_image_file)) raise try: actual_image = _download_and_open_image(actual_image_file, actual_image_url) except Exception: logging.exception( 'unable to download actual_image_url %s to file %s' % (actual_image_url, actual_image_file)) raise # Generate the diff image (absolute diff at each pixel) and # max_diff_per_channel. diff_image = _generate_image_diff(actual_image, expected_image) diff_histogram = diff_image.histogram() (diff_width, diff_height) = diff_image.size self._weighted_diff_measure = _calculate_weighted_diff_metric( diff_histogram, diff_width * diff_height) self._max_diff_per_channel = _max_per_band(diff_histogram) # Generate the whitediff image (any differing pixels show as white). # This is tricky, because when you convert color images to grayscale or # black & white in PIL, it has its own ideas about thresholds. # We have to force it: if a pixel has any color at all, it's a '1'. bands = diff_image.split() graydiff_image = ImageChops.lighter( ImageChops.lighter(bands[0], bands[1]), bands[2]) whitediff_image = ( graydiff_image.point(lambda p: p > 0 and VALUES_PER_BAND).convert( '1', dither=Image.NONE)) # Calculate the perceptual difference percentage. skpdiff_csv_dir = tempfile.mkdtemp() try: skpdiff_csv_output = os.path.join(skpdiff_csv_dir, 'skpdiff-output.csv') expected_img = os.path.join( storage_root, expected_images_subdir, str(expected_image_locator) + image_suffix) actual_img = os.path.join(storage_root, actual_images_subdir, str(actual_image_locator) + image_suffix) find_run_binary.run_command([ SKPDIFF_BINARY, '-p', expected_img, actual_img, '--csv', skpdiff_csv_output, '-d', 'perceptual' ]) with contextlib.closing(open(skpdiff_csv_output)) as csv_file: for row in csv.DictReader(csv_file): perceptual_similarity = float(row[' perceptual'].strip()) if not 0 <= perceptual_similarity <= 1: # skpdiff outputs -1 if the images are different sizes. Treat any # output that does not lie in [0, 1] as having 0% perceptual # similarity. perceptual_similarity = 0 # skpdiff returns the perceptual similarity, convert it to get the # perceptual difference percentage. self._perceptual_difference = 100 - ( perceptual_similarity * 100) finally: shutil.rmtree(skpdiff_csv_dir) # Final touches on diff_image: use whitediff_image as an alpha mask. # Unchanged pixels are transparent; differing pixels are opaque. diff_image.putalpha(whitediff_image) # Store the diff and whitediff images generated above. diff_image_locator = _get_difference_locator( expected_image_locator=expected_image_locator, actual_image_locator=actual_image_locator) basename = str(diff_image_locator) + image_suffix _save_image(diff_image, os.path.join(storage_root, DIFFS_SUBDIR, basename)) _save_image(whitediff_image, os.path.join(storage_root, WHITEDIFFS_SUBDIR, basename)) # Calculate difference metrics. (self._width, self._height) = diff_image.size self._num_pixels_differing = ( whitediff_image.histogram()[VALUES_PER_BAND - 1])
def __init__(self, storage_root, expected_image_url, expected_image_locator, actual_image_url, actual_image_locator, expected_images_subdir=DEFAULT_IMAGES_SUBDIR, actual_images_subdir=DEFAULT_IMAGES_SUBDIR, image_suffix=DEFAULT_IMAGE_SUFFIX): """Download this pair of images (unless we already have them on local disk), and prepare a DiffRecord for them. TODO(epoger): Make this asynchronously download images, rather than blocking until the images have been downloaded and processed. Args: storage_root: root directory on local disk within which we store all images expected_image_url: file or HTTP url from which we will download the expected image expected_image_locator: a unique ID string under which we will store the expected image within storage_root (probably including a checksum to guarantee uniqueness) actual_image_url: file or HTTP url from which we will download the actual image actual_image_locator: a unique ID string under which we will store the actual image within storage_root (probably including a checksum to guarantee uniqueness) expected_images_subdir: the subdirectory expected images are stored in. actual_images_subdir: the subdirectory actual images are stored in. image_suffix: the suffix of images. """ expected_image_locator = _sanitize_locator(expected_image_locator) actual_image_locator = _sanitize_locator(actual_image_locator) # Download the expected/actual images, if we don't have them already. # TODO(rmistry): Add a parameter that just tries to use already-present # image files rather than downloading them. expected_image_file = os.path.join( storage_root, expected_images_subdir, str(expected_image_locator) + image_suffix) actual_image_file = os.path.join( storage_root, actual_images_subdir, str(actual_image_locator) + image_suffix) try: _download_file(expected_image_file, expected_image_url) except Exception: logging.exception('unable to download expected_image_url %s to file %s' % (expected_image_url, expected_image_file)) raise try: _download_file(actual_image_file, actual_image_url) except Exception: logging.exception('unable to download actual_image_url %s to file %s' % (actual_image_url, actual_image_file)) raise # Get all diff images and values from skpdiff binary. skpdiff_output_dir = tempfile.mkdtemp() try: skpdiff_summary_file = os.path.join(skpdiff_output_dir, 'skpdiff-output.json') skpdiff_rgbdiff_dir = os.path.join(skpdiff_output_dir, 'rgbDiff') skpdiff_whitediff_dir = os.path.join(skpdiff_output_dir, 'whiteDiff') expected_img = os.path.join(storage_root, expected_images_subdir, str(expected_image_locator) + image_suffix) actual_img = os.path.join(storage_root, actual_images_subdir, str(actual_image_locator) + image_suffix) # TODO: Call skpdiff ONCE for all image pairs, instead of calling it # repeatedly. This will allow us to parallelize a lot more work. find_run_binary.run_command( [SKPDIFF_BINARY, '-p', expected_img, actual_img, '--jsonp', 'false', '--output', skpdiff_summary_file, '--differs', 'perceptual', 'different_pixels', '--rgbDiffDir', skpdiff_rgbdiff_dir, '--whiteDiffDir', skpdiff_whitediff_dir, ]) # Get information out of the skpdiff_summary_file. with contextlib.closing(open(skpdiff_summary_file)) as fp: data = json.load(fp) # For now, we can assume there is only one record in the output summary, # since we passed skpdiff only one pair of images. record = data['records'][0] self._width = record['width'] self._height = record['height'] # TODO: make max_diff_per_channel a tuple instead of a list, because the # structure is meaningful (first element is red, second is green, etc.) # See http://stackoverflow.com/a/626871 self._max_diff_per_channel = [ record['maxRedDiff'], record['maxGreenDiff'], record['maxBlueDiff']] rgb_diff_path = record['rgbDiffPath'] white_diff_path = record['whiteDiffPath'] per_differ_stats = record['diffs'] for stats in per_differ_stats: differ_name = stats['differName'] if differ_name == 'different_pixels': self._num_pixels_differing = stats['pointsOfInterest'] elif differ_name == 'perceptual': perceptual_similarity = stats['result'] # skpdiff returns the perceptual similarity; convert it to get the # perceptual difference percentage. # skpdiff outputs -1 if the images are different sizes. Treat any # output that does not lie in [0, 1] as having 0% perceptual # similarity. if not 0 <= perceptual_similarity <= 1: perceptual_similarity = 0 self._perceptual_difference = 100 - (perceptual_similarity * 100) # Store the rgbdiff and whitediff images generated above. diff_image_locator = _get_difference_locator( expected_image_locator=expected_image_locator, actual_image_locator=actual_image_locator) basename = str(diff_image_locator) + image_suffix _mkdir_unless_exists(os.path.join(storage_root, RGBDIFFS_SUBDIR)) _mkdir_unless_exists(os.path.join(storage_root, WHITEDIFFS_SUBDIR)) # TODO: Modify skpdiff's behavior so we can tell it exactly where to # write the image files into, rather than having to move them around # after skpdiff writes them out. shutil.copyfile(rgb_diff_path, os.path.join(storage_root, RGBDIFFS_SUBDIR, basename)) shutil.copyfile(white_diff_path, os.path.join(storage_root, WHITEDIFFS_SUBDIR, basename)) finally: shutil.rmtree(skpdiff_output_dir)