示例#1
0
    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),
        ])
示例#2
0
    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)
示例#3
0
    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)
示例#4
0
  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),
    ])
示例#5
0
  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)
示例#6
0
文件: imagediffdb.py 项目: elima/skia
  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)
示例#7
0
  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)
示例#9
0
    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])
示例#10
0
文件: imagediffdb.py 项目: Axure/skia
  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)