lib/pixelComparator.js
// Copyright 2015 Yahoo! Inc.
// Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms.
var Base = require('preceptor-core').Base;
var Rect = require('./configuration/atoms/rect');
/**
* @class PixelComparator
* @extends Base
* @module Compare
*
* @property {PNGImage} _imageA
* @property {PNGImage} _imageB
* @property {Config} _config
*/
var PixelComparator = Base.extend(
/**
* Constructor for the pixel comparator
*
* @constructor
* @param {PNGImage} imageA
* @param {PNGImage} imageB
* @param {Config} config
*/
function (imageA, imageB, config) {
this._imageA = imageA;
this._imageB = imageB;
this._config = config;
},
{
/**
* Gets image A
*
* @method getImageA
* @return {PNGImage}
*/
getImageA: function () {
return this._imageA;
},
/**
* Gets image B
*
* @method getImageB
* @return {PNGImage}
*/
getImageB: function () {
return this._imageB;
},
/**
* Gets the configuration
*
* @method getConfig
* @return {Config}
*/
getConfig: function () {
return this._config;
},
/**
* Calculates the distance of colors in the 4 dimensional color space
*
* Note: The distance is squared for faster calculation.
*
* @method _colorDelta
* @param {int} offsetA Offset in first image
* @param {int} offsetB Offset in second image
* @return {number} Distance
* @private
*/
_colorDelta: function (offsetA, offsetB) {
var imageA = this._imageA.getData(),
imageB = this._imageB.getData();
return Math.pow(imageA[offsetA] - imageB[offsetB], 2) +
Math.pow(imageA[offsetA + 1] - imageB[offsetB + 1], 2) +
Math.pow(imageA[offsetA + 2] - imageB[offsetB + 2], 2) +
Math.pow(imageA[offsetA + 3] - imageB[offsetB + 3], 2);
},
/**
* Determines the areas that needs to be compared
*
* @method _getAreas
* @param {PixelComparison} comparison
* @return {Rect[]}
* @private
*/
_getAreas: function (comparison) {
var areaA, areaB,
areas = [],
imageArea = new Rect({
x: 0,
y: 0,
width: this._imageA.getWidth(),
height: this._imageA.getHeight()
});
if (!comparison.getAreaImageA() || !comparison.getAreaImageB()) {
areas.push(imageArea);
areas.push(imageArea);
} else {
areaA = comparison.getAreaImageA().clone().limitCoordinates(imageArea);
areaB = comparison.getAreaImageB().clone().limitCoordinates(imageArea);
areaA.setWidth(Math.min(areaA.getWidth(), areaB.getWidth()));
areaA.setHeight(Math.min(areaA.getHeight(), areaB.getHeight()));
areaB.setWidth(areaA.getWidth());
areaB.setHeight(areaA.getHeight());
areas.push(areaA);
areas.push(areaB);
}
return areas;
},
/**
* Compares two images and sets a flags in flag-field
*
* @method compare
* @param {PixelComparison} comparison
* @param {Buffer} flagField
*/
compare: function (comparison, flagField) {
var flagFieldIndexA, dataIndexA,
flagFieldIndexB, dataIndexB,
x, y, delta,
areas = this._getAreas(comparison),
areaA = areas[0].getCoordinates(),
areaB = areas[1].getCoordinates(),
width = areaA.width,
height = areaA.height,
deltaThreshold = comparison.getColorDeltaSquared(),
useImageB = this.getConfig().getOutput().shouldCopyImageB();
for (x = 0; x < width; x++) {
for (y = 0; y < height; y++) {
flagFieldIndexA = (x + areaA.x) + ((y + areaA.y) * width);
dataIndexA = 4 * flagFieldIndexA;
flagFieldIndexB = (x + areaB.x) + ((y + areaB.y) * width);
dataIndexB = 4 * flagFieldIndexB;
delta = this._colorDelta(dataIndexA, dataIndexB);
if (delta > deltaThreshold) {
if (this._shiftCompare(x, y, dataIndexA, comparison, this._imageA, areaA, this._imageB, areaB) &&
this._shiftCompare(x, y, dataIndexB, comparison, this._imageB, areaB, this._imageA, areaA)) {
flagField[useImageB ? flagFieldIndexB : flagFieldIndexA] |= 2;
} else {
flagField[useImageB ? flagFieldIndexB : flagFieldIndexA] |= 1;
}
}
}
}
},
/**
* Comparison, covering sub-pixel shifts
*
* @method _shiftCompare
* @param {int} x X-offset for current comparison coordinate
* @param {int} y Y-offset for current comparison coordinate
* @param {int} colorIndex Index of color in data
* @param {PixelComparison} comparison Comparison configuration
* @param {PNGImage} imageSrc Source image
* @param {object} areaSrc Area that is compared in source image
* @param {PNGImage} imageDst Destination image
* @param {object} areaDst Area that is compared in destination image
* @return {boolean} Within limits?
* @private
*/
_shiftCompare: function (x, y, colorIndex, comparison, imageSrc, areaSrc, imageDst, areaDst) {
var xOffset, xLow, xHigh,
yOffset, yLow, yHigh,
delta, localDeltaThreshold,
dataIndexSrc, dataIndexDst,
deltaThreshold = comparison.getColorDeltaSquared(),
shift = comparison.getShift(),
hShift = shift.getHorizontal(),
vShift = shift.getVertical(),
width = areaSrc.width,
height = areaSrc.height;
if ((hShift > 0) || (vShift > 0)) {
xLow = this._calculateLowerLimit(x, 0, hShift);
xHigh = this._calculateUpperLimit(x, width - 1, hShift);
yLow = this._calculateLowerLimit(y, 0, vShift);
yHigh = this._calculateUpperLimit(y, height - 1, vShift);
for (xOffset = xLow; xOffset <= xHigh; xOffset++) {
for (yOffset = yLow; yOffset <= yHigh; yOffset++) {
if ((xOffset != 0) || (yOffset != 0)) {
dataIndexSrc = ((x + areaSrc.x + xOffset) + ((y + areaSrc.y + yOffset) * width)) * 4;
localDeltaThreshold = this._colorDelta(colorIndex, dataIndexSrc);
dataIndexDst = ((x + areaDst.x + xOffset) + ((y + areaDst.y + yOffset) * width)) * 4;
delta = this._colorDelta(colorIndex, dataIndexDst);
if ((Math.abs(delta - localDeltaThreshold) < deltaThreshold) &&
(localDeltaThreshold > deltaThreshold)) {
return true;
}
}
}
}
}
return false;
},
/**
* Calculates the lower limit
*
* @method _calculateLowerLimit
* @param {int} value
* @param {int} min
* @param {int} shift
* @return {int}
* @private
*/
_calculateLowerLimit: function (value, min, shift) {
return (value - shift) < min ? -(shift + (value - shift)) : -shift;
},
/**
* Calculates the upper limit
*
* @method _calculateUpperLimit
* @param {int} value
* @param {int} max
* @param {int} shift
* @return {int}
* @private
*/
_calculateUpperLimit: function (value, max, shift) {
return (value + shift) > max ? (max - value) : shift;
}
}
);
module.exports = PixelComparator;