rebaseline_server: use just skpdiff, not Python Image Library

BUG=skia:2414
R=djsollen@google.com, borenet@google.com

Author: epoger@google.com

Review URL: https://codereview.chromium.org/325413003
This commit is contained in:
epoger 2014-07-02 07:43:04 -07:00 committed by Commit bot
parent a887026421
commit 54f1ad8bb5
14 changed files with 351 additions and 228 deletions

View File

@ -10,7 +10,7 @@ Calulate differences between image pairs, and store them in a database.
"""
import contextlib
import csv
import json
import logging
import os
import re
@ -18,11 +18,6 @@ import shutil
import sys
import tempfile
import urllib
try:
from PIL import Image, ImageChops
except ImportError:
raise ImportError('Requires PIL to be installed; see '
+ 'http://www.pythonware.com/products/pil/')
# Set the PYTHONPATH to include the tools directory.
sys.path.append(
@ -38,11 +33,9 @@ DEFAULT_IMAGES_SUBDIR = 'images'
DISALLOWED_FILEPATH_CHAR_REGEX = re.compile('[^\w\-]')
DIFFS_SUBDIR = 'diffs'
RGBDIFFS_SUBDIR = 'diffs'
WHITEDIFFS_SUBDIR = 'whitediffs'
VALUES_PER_BAND = 256
# Keys used within DiffRecord dictionary representations.
# NOTE: Keep these in sync with static/constants.js
KEY__DIFFERENCES__MAX_DIFF_PER_CHANNEL = 'maxDiffPerChannel'
@ -87,9 +80,8 @@ class DiffRecord(object):
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).
# 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)
@ -97,80 +89,91 @@ class DiffRecord(object):
storage_root, actual_images_subdir,
str(actual_image_locator) + image_suffix)
try:
expected_image = _download_and_open_image(
expected_image_file, expected_image_url)
_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:
actual_image = _download_and_open_image(
actual_image_file, actual_image_url)
_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
# 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()
# Get all diff images and values from skpdiff binary.
skpdiff_output_dir = tempfile.mkdtemp()
try:
skpdiff_csv_output = os.path.join(skpdiff_csv_dir, 'skpdiff-output.csv')
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,
'--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)
'--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_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])
shutil.rmtree(skpdiff_output_dir)
def get_num_pixels_differing(self):
"""Returns the absolute number of pixels that differ."""
@ -278,102 +281,18 @@ class ImageDiffDB(object):
# Utility functions
def _max_per_band(histogram):
"""Given the histogram of an image, return the maximum value of each band
(a.k.a. "color channel", such as R/G/B) across the entire image.
Args:
histogram: PIL histogram
Returns the maximum value of each band within the image histogram, as a list.
"""
max_per_band = []
assert(len(histogram) % VALUES_PER_BAND == 0)
num_bands = len(histogram) / VALUES_PER_BAND
for band in xrange(num_bands):
# Assuming that VALUES_PER_BAND is 256...
# the 'R' band makes up indices 0-255 in the histogram,
# the 'G' band makes up indices 256-511 in the histogram,
# etc.
min_index = band * VALUES_PER_BAND
index = min_index + VALUES_PER_BAND
while index > min_index:
index -= 1
if histogram[index] > 0:
max_per_band.append(index - min_index)
break
return max_per_band
def _generate_image_diff(image1, image2):
"""Wrapper for ImageChops.difference(image1, image2) that will handle some
errors automatically, or at least yield more useful error messages.
TODO(epoger): Currently, some of the images generated by the bots are RGBA
and others are RGB. I'm not sure why that is. For now, to avoid confusion
within the UI, convert all to RGB when diffing.
Args:
image1: a PIL image object
image2: a PIL image object
Returns: per-pixel diffs between image1 and image2, as a PIL image object
"""
try:
return ImageChops.difference(image1.convert('RGB'), image2.convert('RGB'))
except ValueError:
logging.error('Error diffing image1 [%s] and image2 [%s].' % (
repr(image1), repr(image2)))
raise
def _download_and_open_image(local_filepath, url):
"""Open the image at local_filepath; if there is no file at that path,
download it from url to that path and then open it.
def _download_file(local_filepath, url):
"""Download a file from url to local_filepath, unless it is already there.
Args:
local_filepath: path on local disk where the image should be stored
url: URL from which we can download the image if we don't have it yet
Returns: a PIL image object
"""
if not os.path.exists(local_filepath):
_mkdir_unless_exists(os.path.dirname(local_filepath))
with contextlib.closing(urllib.urlopen(url)) as url_handle:
with open(local_filepath, 'wb') as file_handle:
shutil.copyfileobj(fsrc=url_handle, fdst=file_handle)
return _open_image(local_filepath)
def _open_image(filepath):
"""Wrapper for Image.open(filepath) that yields more useful error messages.
Args:
filepath: path on local disk to load image from
Returns: a PIL image object
"""
try:
return Image.open(filepath)
except IOError:
# If we are unable to load an image from the file, delete it from disk
# and we will try to fetch it again next time. Fixes http://skbug.com/2247
logging.error('IOError loading image file %s ; deleting it.' % filepath)
os.remove(filepath)
raise
def _save_image(image, filepath, format='PNG'):
"""Write an image to disk, creating any intermediate directories as needed.
Args:
image: a PIL image object
filepath: path on local disk to write image to
format: one of the PIL image formats, listed at
http://effbot.org/imagingbook/formats.htm
"""
_mkdir_unless_exists(os.path.dirname(filepath))
image.save(filepath, format)
def _mkdir_unless_exists(path):

View File

@ -19,7 +19,8 @@ import imagediffdb
import imagepair
IMG_URL_BASE = 'http://chromium-skia-gm.commondatastorage.googleapis.com/gm/bitmap-64bitMD5/'
IMG_URL_BASE = ('http://chromium-skia-gm.commondatastorage.googleapis.com/'
'gm/bitmap-64bitMD5/')
class ImagePairTest(unittest.TestCase):
@ -87,7 +88,7 @@ class ImagePairTest(unittest.TestCase):
'maxDiffPerChannel': [255, 255, 247],
'numDifferingPixels': 662,
'percentDifferingPixels': 0.0662,
'perceptualDifference': 0.06620000000000914,
'perceptualDifference': 0.06620300000000157,
},
'imageAUrl': 'arcofzorro/16206093933823793653.png',
'imageBUrl': 'arcofzorro/13786535001616823825.png',

View File

@ -14,7 +14,7 @@ import posixpath
# Local imports
import column
import imagepair
import imagediffdb
# Keys used within dictionary representation of ImagePairSet.
# NOTE: Keep these in sync with static/constants.js
@ -157,12 +157,12 @@ class ImagePairSet(object):
KEY__IMAGESETS__SET__DIFFS: {
key_description: 'color difference per channel',
key_base_url: posixpath.join(
self._diff_base_url, 'diffs'),
self._diff_base_url, imagediffdb.RGBDIFFS_SUBDIR),
},
KEY__IMAGESETS__SET__WHITEDIFFS: {
key_description: 'differing pixels in white',
key_base_url: posixpath.join(
self._diff_base_url, 'whitediffs'),
self._diff_base_url, imagediffdb.WHITEDIFFS_SUBDIR),
},
},
}

View File

@ -94,7 +94,7 @@
}
},
"header": {
"dataHash": "5496105477154010366",
"dataHash": "-5829724510169924592",
"isEditable": false,
"isExported": true,
"schemaVersion": 3,
@ -189,7 +189,7 @@
],
"numDifferingPixels": 6081,
"percentDifferingPixels": 2.4324,
"perceptualDifference": 1.917199999999994
"perceptualDifference": 1.9172010000000057
},
"extraColumns": {
"builder": "Test-Builder-We-Have-No-Expectations-File-For",
@ -210,7 +210,7 @@
],
"numDifferingPixels": 50097,
"percentDifferingPixels": 30.5767822265625,
"perceptualDifference": 3.3917
"perceptualDifference": 3.391725000000008
},
"extraColumns": {
"builder": "Test-Builder-We-Have-No-Expectations-File-For",
@ -253,7 +253,7 @@
],
"numDifferingPixels": 6081,
"percentDifferingPixels": 2.4324,
"perceptualDifference": 1.917199999999994
"perceptualDifference": 1.9172010000000057
},
"extraColumns": {
"builder": "Test-Mac10.7-MacMini4.1-GeForce320M-x86_64-Debug",
@ -274,7 +274,7 @@
],
"numDifferingPixels": 50097,
"percentDifferingPixels": 30.5767822265625,
"perceptualDifference": 3.3917
"perceptualDifference": 3.391725000000008
},
"extraColumns": {
"builder": "Test-Mac10.7-MacMini4.1-GeForce320M-x86_64-Debug",

View File

@ -110,7 +110,7 @@
}
},
"header": {
"dataHash": "7849962375815855931",
"dataHash": "-7804718549064096650",
"isEditable": false,
"isExported": true,
"schemaVersion": 3,
@ -128,7 +128,7 @@
],
"numDifferingPixels": 120000,
"percentDifferingPixels": 75.0,
"perceptualDifference": 50.122499999999995
"perceptualDifference": 50.122499
},
"expectations": {
"bugs": null,
@ -154,7 +154,7 @@
],
"numDifferingPixels": 765891,
"percentDifferingPixels": 97.38807678222656,
"perceptualDifference": 25.25699999999999
"perceptualDifference": 25.256985
},
"expectations": {
"bugs": [
@ -182,7 +182,7 @@
],
"numDifferingPixels": 422432,
"percentDifferingPixels": 53.715006510416664,
"perceptualDifference": 25.120500000000007
"perceptualDifference": 25.120543999999995
},
"expectations": {
"bugs": [
@ -202,16 +202,6 @@
"isDifferent": true
},
{
"differenceData": {
"maxDiffPerChannel": [
222,
223,
222
],
"numDifferingPixels": 53150,
"percentDifferingPixels": 12.035778985507246,
"perceptualDifference": 100
},
"expectations": {
"bugs": [
1578
@ -230,16 +220,6 @@
"isDifferent": true
},
{
"differenceData": {
"maxDiffPerChannel": [
221,
221,
221
],
"numDifferingPixels": 53773,
"percentDifferingPixels": 12.17685688405797,
"perceptualDifference": 100
},
"expectations": {
"bugs": [
1578

View File

@ -9,11 +9,13 @@
#include "SkImageDecoder.h"
#include "SkOSFile.h"
#include "SkRunnable.h"
#include "SkSize.h"
#include "SkStream.h"
#include "SkTDict.h"
#include "SkThreadPool.h"
#include "SkDiffContext.h"
#include "SkImageDiffer.h"
#include "skpdiff_util.h"
SkDiffContext::SkDiffContext() {
@ -28,9 +30,21 @@ SkDiffContext::~SkDiffContext() {
}
}
void SkDiffContext::setDifferenceDir(const SkString& path) {
void SkDiffContext::setAlphaMaskDir(const SkString& path) {
if (!path.isEmpty() && sk_mkdir(path.c_str())) {
fDifferenceDir = path;
fAlphaMaskDir = path;
}
}
void SkDiffContext::setRgbDiffDir(const SkString& path) {
if (!path.isEmpty() && sk_mkdir(path.c_str())) {
fRgbDiffDir = path;
}
}
void SkDiffContext::setWhiteDiffDir(const SkString& path) {
if (!path.isEmpty() && sk_mkdir(path.c_str())) {
fWhiteDiffDir = path;
}
}
@ -90,13 +104,13 @@ void SkDiffContext::addDiff(const char* baselinePath, const char* testPath) {
newRecord->fBaselinePath = baselinePath;
newRecord->fTestPath = testPath;
newRecord->fSize = SkISize::Make(baselineBitmap.width(), baselineBitmap.height());
bool alphaMaskPending = false;
// only enable alpha masks if a difference dir has been provided
if (!fDifferenceDir.isEmpty()) {
alphaMaskPending = true;
}
// only generate diff images if we have a place to store them
SkImageDiffer::BitmapsToCreate bitmapsToCreate;
bitmapsToCreate.alphaMask = !fAlphaMaskDir.isEmpty();
bitmapsToCreate.rgbDiff = !fRgbDiffDir.isEmpty();
bitmapsToCreate.whiteDiff = !fWhiteDiffDir.isEmpty();
// Perform each diff
for (int differIndex = 0; differIndex < fDifferCount; differIndex++) {
@ -106,30 +120,69 @@ void SkDiffContext::addDiff(const char* baselinePath, const char* testPath) {
DiffData& diffData = newRecord->fDiffs.push_back();
diffData.fDiffName = differ->getName();
if (!differ->diff(&baselineBitmap, &testBitmap, alphaMaskPending, &diffData.fResult)) {
// if the diff failed record -1 as the result
if (!differ->diff(&baselineBitmap, &testBitmap, bitmapsToCreate, &diffData.fResult)) {
// if the diff failed, record -1 as the result
// TODO(djsollen): Record more detailed information about exactly what failed.
// (Image dimension mismatch? etc.) See http://skbug.com/2710 ('make skpdiff
// report more detail when it fails to compare two images')
diffData.fResult.result = -1;
continue;
}
if (alphaMaskPending
if (bitmapsToCreate.alphaMask
&& SkImageDiffer::RESULT_CORRECT != diffData.fResult.result
&& !diffData.fResult.poiAlphaMask.empty()
&& !newRecord->fCommonName.isEmpty()) {
newRecord->fDifferencePath = SkOSPath::SkPathJoin(fDifferenceDir.c_str(),
newRecord->fCommonName.c_str());
newRecord->fAlphaMaskPath = SkOSPath::SkPathJoin(fAlphaMaskDir.c_str(),
newRecord->fCommonName.c_str());
// compute the image diff and output it
SkBitmap copy;
diffData.fResult.poiAlphaMask.copyTo(&copy, kN32_SkColorType);
SkImageEncoder::EncodeFile(newRecord->fDifferencePath.c_str(), copy,
SkImageEncoder::EncodeFile(newRecord->fAlphaMaskPath.c_str(), copy,
SkImageEncoder::kPNG_Type, 100);
// cleanup the existing bitmap to free up resources;
diffData.fResult.poiAlphaMask.reset();
alphaMaskPending = false;
bitmapsToCreate.alphaMask = false;
}
if (bitmapsToCreate.rgbDiff
&& SkImageDiffer::RESULT_CORRECT != diffData.fResult.result
&& !diffData.fResult.rgbDiffBitmap.empty()
&& !newRecord->fCommonName.isEmpty()) {
// TODO(djsollen): Rather than taking the max r/g/b diffs that come back from
// a particular differ and storing them as toplevel fields within
// newRecord, we should extend outputRecords() to report optional
// fields for each differ (not just "result" and "pointsOfInterest").
// See http://skbug.com/2712 ('allow skpdiff to report different sets
// of result fields for different comparison algorithms')
newRecord->fMaxRedDiff = diffData.fResult.maxRedDiff;
newRecord->fMaxGreenDiff = diffData.fResult.maxGreenDiff;
newRecord->fMaxBlueDiff = diffData.fResult.maxBlueDiff;
newRecord->fRgbDiffPath = SkOSPath::SkPathJoin(fRgbDiffDir.c_str(),
newRecord->fCommonName.c_str());
SkImageEncoder::EncodeFile(newRecord->fRgbDiffPath.c_str(),
diffData.fResult.rgbDiffBitmap,
SkImageEncoder::kPNG_Type, 100);
diffData.fResult.rgbDiffBitmap.reset();
bitmapsToCreate.rgbDiff = false;
}
if (bitmapsToCreate.whiteDiff
&& SkImageDiffer::RESULT_CORRECT != diffData.fResult.result
&& !diffData.fResult.whiteDiffBitmap.empty()
&& !newRecord->fCommonName.isEmpty()) {
newRecord->fWhiteDiffPath = SkOSPath::SkPathJoin(fWhiteDiffDir.c_str(),
newRecord->fCommonName.c_str());
SkImageEncoder::EncodeFile(newRecord->fWhiteDiffPath.c_str(),
diffData.fResult.whiteDiffBitmap,
SkImageEncoder::kPNG_Type, 100);
diffData.fResult.whiteDiffBitmap.reset();
bitmapsToCreate.whiteDiff = false;
}
}
}
@ -229,11 +282,15 @@ void SkDiffContext::outputRecords(SkWStream& stream, bool useJSONP) {
} else {
stream.writeText("{\n");
}
// TODO(djsollen): Would it be better to use the jsoncpp library to write out the JSON?
// This manual approach is probably more efficient, but it sure is ugly.
// See http://skbug.com/2713 ('make skpdiff use jsoncpp library to write out
// JSON output, instead of manual writeText() calls?')
stream.writeText(" \"records\": [\n");
while (NULL != currentRecord) {
stream.writeText(" {\n");
SkString differenceAbsPath = get_absolute_path(currentRecord->fDifferencePath);
SkString baselineAbsPath = get_absolute_path(currentRecord->fBaselinePath);
SkString testAbsPath = get_absolute_path(currentRecord->fTestPath);
@ -242,7 +299,15 @@ void SkDiffContext::outputRecords(SkWStream& stream, bool useJSONP) {
stream.writeText("\",\n");
stream.writeText(" \"differencePath\": \"");
stream.writeText(differenceAbsPath.c_str());
stream.writeText(get_absolute_path(currentRecord->fAlphaMaskPath).c_str());
stream.writeText("\",\n");
stream.writeText(" \"rgbDiffPath\": \"");
stream.writeText(get_absolute_path(currentRecord->fRgbDiffPath).c_str());
stream.writeText("\",\n");
stream.writeText(" \"whiteDiffPath\": \"");
stream.writeText(get_absolute_path(currentRecord->fWhiteDiffPath).c_str());
stream.writeText("\",\n");
stream.writeText(" \"baselinePath\": \"");
@ -253,6 +318,23 @@ void SkDiffContext::outputRecords(SkWStream& stream, bool useJSONP) {
stream.writeText(testAbsPath.c_str());
stream.writeText("\",\n");
stream.writeText(" \"width\": ");
stream.writeDecAsText(currentRecord->fSize.width());
stream.writeText(",\n");
stream.writeText(" \"height\": ");
stream.writeDecAsText(currentRecord->fSize.height());
stream.writeText(",\n");
stream.writeText(" \"maxRedDiff\": ");
stream.writeDecAsText(currentRecord->fMaxRedDiff);
stream.writeText(",\n");
stream.writeText(" \"maxGreenDiff\": ");
stream.writeDecAsText(currentRecord->fMaxGreenDiff);
stream.writeText(",\n");
stream.writeText(" \"maxBlueDiff\": ");
stream.writeDecAsText(currentRecord->fMaxBlueDiff);
stream.writeText(",\n");
stream.writeText(" \"diffs\": [\n");
for (int diffIndex = 0; diffIndex < currentRecord->fDiffs.count(); diffIndex++) {
DiffData& data = currentRecord->fDiffs[diffIndex];

View File

@ -28,10 +28,28 @@ public:
void setThreadCount(int threadCount) { fThreadCount = threadCount; }
/**
* Creates the directory if it does not exist and uses it to store differences
* between images.
* Sets the directory within which to store alphaMasks (images that
* are transparent for each pixel that differs between baseline and test).
*
* If the directory does not exist yet, it will be created.
*/
void setDifferenceDir(const SkString& directory);
void setAlphaMaskDir(const SkString& directory);
/**
* Sets the directory within which to store rgbDiffs (images showing the
* per-channel difference between baseline and test at each pixel).
*
* If the directory does not exist yet, it will be created.
*/
void setRgbDiffDir(const SkString& directory);
/**
* Sets the directory within which to store whiteDiffs (images showing white
* for each pixel that differs between baseline and test).
*
* If the directory does not exist yet, it will be created.
*/
void setWhiteDiffDir(const SkString& directory);
/**
* Sets the differs to be used in each diff. Already started diffs will not retroactively use
@ -74,6 +92,14 @@ public:
* "differencePath" : (optional) string containing the path to an alpha
* mask of the pixel difference between the baseline
* and test images
* TODO(epoger): consider renaming this "alphaMaskPath"
* to distinguish from other difference types?
* "rgbDiffPath" : (optional) string containing the path to a bitmap
* showing per-channel differences between the
* baseline and test images at each pixel
* "whiteDiffPath" : (optional) string containing the path to a bitmap
* showing every pixel that differs between the
* baseline and test images as white
*
* They also have an array named "diffs" with each element being one diff record for the two
* images indicated in the above field.
@ -117,10 +143,21 @@ private:
};
struct DiffRecord {
// TODO(djsollen): Some of these fields are required, while others are optional
// (e.g., fRgbDiffPath is only filled in if SkDifferentPixelsMetric
// was run). Figure out a way to note that. See http://skbug.com/2712
// ('allow skpdiff to report different sets of result fields for
// different comparison algorithms')
SkString fCommonName;
SkString fDifferencePath;
SkString fAlphaMaskPath;
SkString fRgbDiffPath;
SkString fWhiteDiffPath;
SkString fBaselinePath;
SkString fTestPath;
SkISize fSize;
int fMaxRedDiff;
int fMaxGreenDiff;
int fMaxBlueDiff;
SkTArray<DiffData> fDiffs;
};
@ -137,7 +174,9 @@ private:
int fDifferCount;
int fThreadCount;
SkString fDifferenceDir;
SkString fAlphaMaskDir;
SkString fRgbDiffDir;
SkString fWhiteDiffDir;
};
#endif

View File

@ -28,7 +28,8 @@ class SkDifferentPixelsMetric :
#endif
public:
virtual const char* getName() const SK_OVERRIDE;
virtual bool diff(SkBitmap* baseline, SkBitmap* test, bool computeMask,
virtual bool diff(SkBitmap* baseline, SkBitmap* test,
const BitmapsToCreate& bitmapsToCreate,
Result* result) const SK_OVERRIDE;
protected:

View File

@ -14,7 +14,8 @@ const char* SkDifferentPixelsMetric::getName() const {
return "different_pixels";
}
bool SkDifferentPixelsMetric::diff(SkBitmap* baseline, SkBitmap* test, bool computeMask,
bool SkDifferentPixelsMetric::diff(SkBitmap* baseline, SkBitmap* test,
const BitmapsToCreate& bitmapsToCreate,
Result* result) const {
double startTime = get_seconds();
@ -22,17 +23,34 @@ bool SkDifferentPixelsMetric::diff(SkBitmap* baseline, SkBitmap* test, bool comp
if (baseline->width() != test->width() || baseline->height() != test->height() ||
baseline->width() <= 0 || baseline->height() <= 0 ||
baseline->colorType() != test->colorType()) {
SkASSERT(baseline->width() == test->width());
SkASSERT(baseline->height() == test->height());
SkASSERT(baseline->width() > 0);
SkASSERT(baseline->height() > 0);
SkASSERT(baseline->colorType() == test->colorType());
return false;
}
int width = baseline->width();
int height = baseline->height();
int maxRedDiff = 0;
int maxGreenDiff = 0;
int maxBlueDiff = 0;
// Prepare the POI alpha mask if needed
if (computeMask) {
// Prepare any bitmaps we will be filling in
if (bitmapsToCreate.alphaMask) {
result->poiAlphaMask.allocPixels(SkImageInfo::MakeA8(width, height));
result->poiAlphaMask.eraseARGB(SK_AlphaOPAQUE, 0, 0, 0);
}
if (bitmapsToCreate.rgbDiff) {
result->rgbDiffBitmap.allocPixels(SkImageInfo::Make(width, height, baseline->colorType(),
kPremul_SkAlphaType));
result->rgbDiffBitmap.eraseARGB(SK_AlphaTRANSPARENT, 0, 0, 0);
}
if (bitmapsToCreate.whiteDiff) {
result->whiteDiffBitmap.allocPixels(SkImageInfo::MakeN32Premul(width, height));
result->whiteDiffBitmap.eraseARGB(SK_AlphaOPAQUE, 0, 0, 0);
}
// Prepare the pixels for comparison
result->poiCount = 0;
@ -40,24 +58,60 @@ bool SkDifferentPixelsMetric::diff(SkBitmap* baseline, SkBitmap* test, bool comp
test->lockPixels();
for (int y = 0; y < height; y++) {
// Grab a row from each image for easy comparison
unsigned char* baselineRow = (unsigned char*)baseline->getAddr(0, y);
unsigned char* testRow = (unsigned char*)test->getAddr(0, y);
// TODO(epoger): The code below already assumes 4 bytes per pixel, so I think
// we could just call getAddr32() to save a little time.
// OR, if we want to play it safe, call ComputeBytesPerPixel instead
// of assuming 4 bytes per pixel.
uint32_t* baselineRow = static_cast<uint32_t *>(baseline->getAddr(0, y));
uint32_t* testRow = static_cast<uint32_t *>(test->getAddr(0, y));
for (int x = 0; x < width; x++) {
// Compare one pixel at a time so each differing pixel can be noted
if (memcmp(&baselineRow[x * 4], &testRow[x * 4], 4) != 0) {
// TODO(epoger): This loop looks like a good place to work on performance,
// but we should run the code through a profiler to be sure.
uint32_t baselinePixel = baselineRow[x];
uint32_t testPixel = testRow[x];
if (baselinePixel != testPixel) {
result->poiCount++;
if (computeMask) {
int redDiff = abs(static_cast<int>(SkColorGetR(baselinePixel) -
SkColorGetR(testPixel)));
if (redDiff > maxRedDiff) {maxRedDiff = redDiff;}
int greenDiff = abs(static_cast<int>(SkColorGetG(baselinePixel) -
SkColorGetG(testPixel)));
if (greenDiff > maxGreenDiff) {maxGreenDiff = greenDiff;}
int blueDiff = abs(static_cast<int>(SkColorGetB(baselinePixel) -
SkColorGetB(testPixel)));
if (blueDiff > maxBlueDiff) {maxBlueDiff = blueDiff;}
if (bitmapsToCreate.alphaMask) {
*result->poiAlphaMask.getAddr8(x,y) = SK_AlphaTRANSPARENT;
}
if (bitmapsToCreate.rgbDiff) {
*result->rgbDiffBitmap.getAddr32(x,y) =
SkColorSetRGB(redDiff, greenDiff, blueDiff);
}
if (bitmapsToCreate.whiteDiff) {
*result->whiteDiffBitmap.getAddr32(x,y) = SK_ColorWHITE;
}
}
}
}
test->unlockPixels();
baseline->unlockPixels();
if (computeMask) {
result->maxRedDiff = maxRedDiff;
result->maxGreenDiff = maxGreenDiff;
result->maxBlueDiff = maxBlueDiff;
if (bitmapsToCreate.alphaMask) {
result->poiAlphaMask.unlockPixels();
}
if (bitmapsToCreate.rgbDiff) {
result->rgbDiffBitmap.unlockPixels();
}
if (bitmapsToCreate.whiteDiff) {
result->whiteDiffBitmap.unlockPixels();
}
// Calculates the percentage of identical pixels
result->result = 1.0 - ((double)result->poiCount / (width * height));

View File

@ -36,7 +36,8 @@ const char* SkDifferentPixelsMetric::getName() const {
return "different_pixels";
}
bool SkDifferentPixelsMetric::diff(SkBitmap* baseline, SkBitmap* test, bool computeMask,
bool SkDifferentPixelsMetric::diff(SkBitmap* baseline, SkBitmap* test, bool computeAlphaMask,
bool computeRgbDiff, bool computeWhiteDiff,
Result* result) const {
double startTime = get_seconds();

View File

@ -24,10 +24,30 @@ public:
struct Result {
double result;
int poiCount;
// TODO(djsollen): Figure out a way that the differ can report which of the
// optional fields it has filled in. See http://skbug.com/2712 ('allow
// skpdiff to report different sets of result fields for different comparison algorithms')
SkBitmap poiAlphaMask; // optional
SkBitmap rgbDiffBitmap; // optional
SkBitmap whiteDiffBitmap; // optional
int maxRedDiff; // optional
int maxGreenDiff; // optional
int maxBlueDiff; // optional
double timeElapsed; // optional
};
// A bitfield indicating which bitmap types we want a differ to create.
//
// TODO(epoger): Remove whiteDiffBitmap, because alphaMask can provide
// the same functionality and more.
// It will be a little bit tricky, because the rebaseline_server client
// and server side code will both need to change to use the alphaMask.
struct BitmapsToCreate {
bool alphaMask;
bool rgbDiff;
bool whiteDiff;
};
/**
* Gets a unique and descriptive name of this differ
* @return A statically allocated null terminated string that is the name of this differ
@ -43,10 +63,10 @@ public:
* diff on a pair of bitmaps.
* @param baseline The correct bitmap
* @param test The bitmap whose difference is being tested
* @param computeMask true if the differ is to attempt to create poiAlphaMask
* @param bitmapsToCreate Which bitmaps the differ should attempt to create
* @return true on success, and false in the case of failure
*/
virtual bool diff(SkBitmap* baseline, SkBitmap* test, bool computeMask,
virtual bool diff(SkBitmap* baseline, SkBitmap* test, const BitmapsToCreate& bitmapsToCreate,
Result* result) const = 0;
};

View File

@ -442,7 +442,8 @@ static double pmetric(const ImageLAB* baselineLAB, const ImageLAB* testLAB, int*
return 1.0 - (double)(*poiCount) / (width * height);
}
bool SkPMetric::diff(SkBitmap* baseline, SkBitmap* test, bool computeMask, Result* result) const {
bool SkPMetric::diff(SkBitmap* baseline, SkBitmap* test, const BitmapsToCreate& bitmapsToCreate,
Result* result) const {
double startTime = get_seconds();
// Ensure the images are comparable

View File

@ -19,7 +19,7 @@
class SkPMetric : public SkImageDiffer {
public:
virtual const char* getName() const SK_OVERRIDE { return "perceptual"; }
virtual bool diff(SkBitmap* baseline, SkBitmap* test, bool computeMask,
virtual bool diff(SkBitmap* baseline, SkBitmap* test, const BitmapsToCreate& bitmapsToCreate,
Result* result) const SK_OVERRIDE;
private:

View File

@ -5,6 +5,11 @@
* found in the LICENSE file.
*/
// TODO(djsollen): Rename this whole package (perhaps to "SkMultiDiffer").
// It's not just for "pdiff" (perceptual diffs)--it's a harness that allows
// the execution of an arbitrary set of difference algorithms.
// See http://skbug.com/2711 ('rename skpdiff')
#if SK_SUPPORT_OPENCL
#define __NO_STD_VECTOR // Uses cl::vectpr instead of std::vectpr
@ -37,10 +42,12 @@ DEFINE_bool2(list, l, false, "List out available differs");
DEFINE_string2(differs, d, "", "The names of the differs to use or all of them by default");
DEFINE_string2(folders, f, "", "Compare two folders with identical subfile names: <baseline folder> <test folder>");
DEFINE_string2(patterns, p, "", "Use two patterns to compare images: <baseline> <test>");
DEFINE_string2(output, o, "", "Writes the output of these diffs to output: <output>");
DEFINE_string(alphaDir, "", "Writes the alpha mask of these diffs to output: <output>");
DEFINE_string2(output, o, "", "Writes a JSON summary of these diffs to file: <filepath>");
DEFINE_string(alphaDir, "", "If the differ can generate an alpha mask, write it into directory: <dirpath>");
DEFINE_string(rgbDiffDir, "", "If the differ can generate an image showing the RGB diff at each pixel, write it into directory: <dirpath>");
DEFINE_string(whiteDiffDir, "", "If the differ can generate an image showing every changed pixel in white, write it into directory: <dirpath>");
DEFINE_bool(jsonp, true, "Output JSON with padding");
DEFINE_string(csv, "", "Writes the output of these diffs to a csv file");
DEFINE_string(csv, "", "Writes the output of these diffs to a csv file: <filepath>");
DEFINE_int32(threads, -1, "run N threads in parallel [default is derived from CPUs available]");
#if SK_SUPPORT_OPENCL
@ -193,12 +200,30 @@ int tool_main(int argc, char * argv[]) {
return 1;
}
}
if (!FLAGS_rgbDiffDir.isEmpty()) {
if (1 != FLAGS_rgbDiffDir.count()) {
SkDebugf("rgbDiffDir flag expects one argument: <directory>\n");
return 1;
}
}
if (!FLAGS_whiteDiffDir.isEmpty()) {
if (1 != FLAGS_whiteDiffDir.count()) {
SkDebugf("whiteDiffDir flag expects one argument: <directory>\n");
return 1;
}
}
SkDiffContext ctx;
ctx.setDiffers(chosenDiffers);
if (!FLAGS_alphaDir.isEmpty()) {
ctx.setDifferenceDir(SkString(FLAGS_alphaDir[0]));
ctx.setAlphaMaskDir(SkString(FLAGS_alphaDir[0]));
}
if (!FLAGS_rgbDiffDir.isEmpty()) {
ctx.setRgbDiffDir(SkString(FLAGS_rgbDiffDir[0]));
}
if (!FLAGS_whiteDiffDir.isEmpty()) {
ctx.setWhiteDiffDir(SkString(FLAGS_whiteDiffDir[0]));
}
if (FLAGS_threads >= 0) {