Skip to content

Commit

Permalink
Merge pull request #92 from gemini-testing/HERMIONE-1080.pw_api
Browse files Browse the repository at this point in the history
feat: calc equality and build diff simultaneously
  • Loading branch information
KuznetsovRoman authored Aug 2, 2023
2 parents b9399bc + abbe9ed commit fe57276
Show file tree
Hide file tree
Showing 9 changed files with 181 additions and 36 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,26 @@ const buffer = await looksSame.createDiff({
});
```

## Comparing images and creating diff image simultaneously

If you need both co compare images and create diff image, you can pass option `createDiffImage: true`,
it would work faster than two separate function calls:

```javascript
const {
equal,
diffImage,
differentPixels,
totalPixels,
diffBounds,
diffClusters
} = await looksSame('image1.png', 'image2.png', {createDiffImage: true});

if (!equal) {
await diffImage.save('diffImage.png');
}
```

## Comparing colors

If you just need to compare two colors you can use `colors` function:
Expand Down
42 changes: 26 additions & 16 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use strict';

const _ = require('lodash');
const parseColor = require('parse-color');
const colorDiff = require('color-diff');
const img = require('./lib/image');
const areColorsSame = require('./lib/same-colors');
Expand All @@ -22,6 +21,9 @@ const makeNoCaretColorComparator = (comparator, pixelRatio) => {
};

function makeCIEDE2000Comparator(tolerance) {
const upperBound = tolerance * 6.2; // cie76 <= 6.2 * ciede2000
const lowerBound = tolerance * 0.695; // cie76 >= 0.695 * ciede2000

return function doColorsLookSame(data) {
if (areColorsSame(data)) {
return true;
Expand All @@ -30,6 +32,20 @@ function makeCIEDE2000Comparator(tolerance) {
const lab1 = colorDiff.rgb_to_lab(data.color1);
const lab2 = colorDiff.rgb_to_lab(data.color2);

const cie76 = Math.sqrt(
(lab1.L - lab2.L) * (lab1.L - lab2.L) +
(lab1.a - lab2.a) * (lab1.a - lab2.a) +
(lab1.b - lab2.b) * (lab1.b - lab2.b)
);

if (cie76 >= upperBound) {
return false;
}

if (cie76 <= lowerBound) {
return true;
}

return colorDiff.diff(lab1, lab2) < tolerance;
};
}
Expand Down Expand Up @@ -95,7 +111,7 @@ const buildDiffImage = async (img1, img2, options) => {
const color1 = img1.getPixel(x, y);
const color2 = img2.getPixel(x, y);

if (!options.comparator({color1, color2, img1, img2, x, y, width, height})) {
if (!options.comparator({color1, color2, img1, img2, x, y, width, height, minWidth, minHeight})) {
setPixel(resultBuffer, x, y, highlightColor);
} else {
setPixel(resultBuffer, x, y, color1);
Expand All @@ -105,16 +121,6 @@ const buildDiffImage = async (img1, img2, options) => {
return img.fromBuffer(resultBuffer, {raw: {width, height, channels: 3}});
};

const parseColorString = (str) => {
const parsed = parseColor(str || '#ff00ff');

return {
R: parsed.rgb[0],
G: parsed.rgb[1],
B: parsed.rgb[2]
};
};

const getToleranceFromOpts = (opts) => {
if (!_.hasIn(opts, 'tolerance')) {
return JND;
Expand Down Expand Up @@ -162,10 +168,10 @@ module.exports = exports = async function looksSame(image1, image2, opts = {}) {
if (areBuffersEqual) {
const diffBounds = (new DiffArea()).area;

return {equal: true, metaInfo, diffBounds, diffClusters: [diffBounds]};
return {equal: true, metaInfo, diffBounds, diffClusters: [diffBounds], diffImage: null};
}

if (first.width !== second.width || first.height !== second.height) {
if (!opts.createDiffImage && (first.width !== second.width || first.height !== second.height)) {
const diffBounds = getMaxDiffBounds(first, second);

return {equal: false, metaInfo, diffBounds, diffClusters: [diffBounds]};
Expand All @@ -178,7 +184,11 @@ module.exports = exports = async function looksSame(image1, image2, opts = {}) {
);

const comparator = createComparator(img1, img2, opts);
const {stopOnFirstFail, shouldCluster, clustersSize} = opts;
const {stopOnFirstFail, shouldCluster, clustersSize, createDiffImage, highlightColor} = opts;

if (createDiffImage) {
return utils.calcDiffImage(img1, img2, comparator, {highlightColor, shouldCluster, clustersSize});
}

const {diffArea, diffClusters} = await utils.getDiffPixelsCoords(img1, img2, comparator, {stopOnFirstFail, shouldCluster, clustersSize});
const diffBounds = diffArea.area;
Expand Down Expand Up @@ -215,7 +225,7 @@ exports.createDiff = async function saveDiff(opts) {
const [image1, image2] = utils.formatImages(opts.reference, opts.current);
const {first, second} = await utils.readPair(image1, image2);
const diffImage = await buildDiffImage(first, second, {
highlightColor: parseColorString(opts.highlightColor),
highlightColor: utils.parseColorString(opts.highlightColor),
comparator: createComparator(first, second, opts)
});

Expand Down
3 changes: 2 additions & 1 deletion lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ module.exports = {
JND: 2.3, // Just noticeable difference if ciede2000 >= JND then colors difference is noticeable by human eye
REQUIRED_IMAGE_FIELDS: ['source', 'boundingBox'],
REQUIRED_BOUNDING_BOX_FIELDS: ['left', 'top', 'right', 'bottom'],
CLUSTERS_SIZE: 10
CLUSTERS_SIZE: 10,
DIFF_IMAGE_CHANNELS: 3
};
4 changes: 1 addition & 3 deletions lib/ignore-caret-comparator/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
'use strict';

const _ = require('lodash');

const STATES = {
InitState: require('./states/init'),
CaretDetectedState: require('./states/caret-detected')
Expand Down Expand Up @@ -31,7 +29,7 @@ module.exports = class IgnoreCaretComparator {
}

_checkIsCaret(data) {
return this._state.validate(_.pick(data, ['x', 'y']), _.pick(data, ['img1', 'img2']));
return this._state.validate(data);
}

switchState(stateName) {
Expand Down
27 changes: 13 additions & 14 deletions lib/ignore-caret-comparator/states/init.js
Original file line number Diff line number Diff line change
@@ -1,46 +1,45 @@
'use strict';

const _ = require('lodash');
const State = require('./state');
const areColorsSame = require('../../same-colors');

module.exports = class InitState extends State {
validate(firstCaretPoint, imgs) {
const lastCaretPoint = this._getLastCaretPoint(firstCaretPoint, imgs);
validate(data) {
const lastCaretPoint = this._getLastCaretPoint(data);

if (!this._looksLikeCaret(firstCaretPoint, lastCaretPoint)) {
if (!this._looksLikeCaret(data, lastCaretPoint)) {
return false;
}

this.caretTopLeft = firstCaretPoint;
this.caretTopLeft = data;
this.caretBottomRight = lastCaretPoint;

this.switchState('CaretDetectedState');

return true;
}

_getLastCaretPoint(firstCaretPoint, imgs) {
let currPoint = firstCaretPoint;
_getLastCaretPoint(data) {
let currPoint = data;

/* eslint-disable-next-line no-constant-condition */
while (true) {
const nextPoint = this._getNextCaretPoint(firstCaretPoint, currPoint);
const nextPoint = this._getNextCaretPoint(data, currPoint);

if (this._isPointOutsideImages(nextPoint, imgs) || this._areColorsSame(nextPoint, imgs)) {
if (this._isPointOutsideImages(nextPoint, data) || this._areColorsSame(nextPoint, data)) {
return currPoint;
}
currPoint = nextPoint;
}
}

_isPointOutsideImages(point, imgs) {
return _.some(imgs, (img) => point.x >= img.width || point.y >= img.height);
_isPointOutsideImages(point, data) {
return point.x >= data.minWidth || point.y >= data.minHeight;
}

_areColorsSame(point, imgs) {
const color1 = imgs.img1.getPixel(point.x, point.y);
const color2 = imgs.img2.getPixel(point.x, point.y);
_areColorsSame(point, data) {
const color1 = data.img1.getPixel(point.x, point.y);
const color2 = data.img2.getPixel(point.x, point.y);

return areColorsSame({color1, color2});
}
Expand Down
8 changes: 8 additions & 0 deletions lib/image/image.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ module.exports = class Image extends ImageBase {
this._channels = info.channels;
}

async initMeta() {
const {width, height, channels} = await this._img.metadata();

this._width = width;
this._height = height;
this._channels = channels;
}

getPixel(x, y) {
const idx = this._getIdx(x, y);
return {
Expand Down
100 changes: 100 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
'use strict';

const _ = require('lodash');
const parseColor = require('parse-color');
const img = require('./image');
const buffer = require('./img-buffer');
const DiffArea = require('./diff-area');
const DiffClusters = require('./diff-clusters');
const validators = require('./validators');
const areColorsSame = require('./same-colors');
const {DIFF_IMAGE_CHANNELS} = require('./constants');

exports.readImgCb = async ({source, ...opts}) => {
const readFunc = Buffer.isBuffer(source) ? img.fromBuffer : img.fromFile;
Expand Down Expand Up @@ -96,3 +99,100 @@ exports.areBuffersEqual = (img1, img2) => {

return img1.buffer.equals(img2.buffer);
};

exports.parseColorString = (str) => {
const parsed = parseColor(str || '#ff00ff');

return {
R: parsed.rgb[0],
G: parsed.rgb[1],
B: parsed.rgb[2]
};
};

exports.calcDiffImage = async (img1, img2, comparator, {highlightColor, shouldCluster, clustersSize}) => {
const diffColor = exports.parseColorString(highlightColor);

const minHeight = Math.min(img1.height, img2.height);
const minWidth = Math.min(img1.width, img2.width);

const maxHeight = Math.max(img1.height, img2.height);
const maxWidth = Math.max(img1.width, img2.width);

const totalPixels = maxHeight * maxWidth;
const metaInfo = {refImg: {size: {width: img1.width, height: img1.height}}};

const diffBuffer = Buffer.alloc(maxHeight * maxWidth * DIFF_IMAGE_CHANNELS);
const diffArea = new DiffArea();
const diffClusters = new DiffClusters(clustersSize);

let differentPixels = 0;
let diffBufferPos = 0;

const markDiff = (x, y) => {
diffBuffer[diffBufferPos++] = diffColor.R;
diffBuffer[diffBufferPos++] = diffColor.G;
diffBuffer[diffBufferPos++] = diffColor.B;
differentPixels++;

diffArea.update(x, y);
if (shouldCluster) {
diffClusters.update(x, y);
}
};

for (let y = 0; y < maxHeight; y++) {
for (let x = 0; x < maxWidth; x++) {
if (y > minHeight || x > minWidth) {
markDiff(x, y); // Out of bounds pixels considered as diff
continue;
}

const color1 = img1.getPixel(x, y);
const color2 = img2.getPixel(x, y);

const areSame = areColorsSame({color1, color2}) || comparator({
img1,
img2,
x,
y,
color1,
color2,
width: maxWidth,
height: maxHeight,
minWidth,
minHeight
});

if (areSame) {
diffBuffer[diffBufferPos++] = color2.R;
diffBuffer[diffBufferPos++] = color2.G;
diffBuffer[diffBufferPos++] = color2.B;
} else {
markDiff(x, y);
}
}

// eslint-disable-next-line no-bitwise
if (!(y & 0xff)) { // Release event queue every 256 rows
await new Promise(setImmediate);
}
}

let diffImage = null;

if (differentPixels) {
diffImage = await img.fromBuffer(diffBuffer, {raw: {width: maxWidth, height: maxHeight, channels: DIFF_IMAGE_CHANNELS}});
await diffImage.initMeta();
}

return {
equal: !differentPixels,
metaInfo,
diffImage,
differentPixels,
totalPixels,
diffBounds: diffArea.area,
diffClusters: diffClusters.clusters
};
};
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion test/ignore-caret-comparator.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,16 @@ describe('IgnoreCaretComparator', () => {

for (let y = 0; y < pixels.length; ++y) {
for (let x = 0; x < pixels[y].length; ++x) {
res = comparator({color1: img1.data[y][x], color2: img2.data[y][x], x, y, img1, img2});
res = comparator({
color1: img1.data[y][x],
color2: img2.data[y][x],
x,
y,
img1,
img2,
minWidth: width,
minHeight: height
});
if (!res) {
break;
}
Expand Down

0 comments on commit fe57276

Please sign in to comment.