yahoo/blink-diff

View on GitHub
lib/pixelComparator.js

Summary

Maintainability
C
1 day
Test Coverage
// 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;