Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compare different sized images? #25

Open
amyleew opened this issue Mar 14, 2017 · 8 comments
Open

Compare different sized images? #25

amyleew opened this issue Mar 14, 2017 · 8 comments

Comments

@amyleew
Copy link

amyleew commented Mar 14, 2017

Is there any option for comparing differently sized images? For example, a way to compare the sizes and add to the canvas size of the smaller size or would that still cause distorted or inaccurate diff results?

/cc @mourner

@mourner
Copy link
Member

mourner commented Mar 15, 2017

Currently no easy way to compare differently sized images, but it's possible to prepare for it manually if you slice out the relevant pixels from a bigger image before feeding to pixelmatch. Something like this:

var croppedImage = new Uint8Array(4 * smallerHeight * smallerWidth);

for (var y = 0; y < smallerHeight; y++) {
    for (var x = 0; x < smallerWidth; x++) {
        var i = (y * biggerWidth + x) * 4;
        var j = (y * smallerWidth + x) * 4;
        croppedImage[j + 0] = biggerImage[i + 0];
        croppedImage[j + 1] = biggerImage[i + 1];
        croppedImage[j + 2] = biggerImage[i + 2];
        croppedImage[j + 3] = biggerImage[i + 3];
    }    
}

ReDrUm pushed a commit to ReDrUm/pixelmatch that referenced this issue Sep 20, 2019
…add-link-to-blog-post to master

* commit '753c24cd7d6e4d387a62e25b2a46799aa44dfe2d':
  docs(readme): add link to tech blog post
  fix(package): support jest v23 (mapbox#81)
@cqtangsong
Copy link

Excuse me,Does it support the comparison of two images of different sizes, Now? if so how to set it

@zinen
Copy link

zinen commented Sep 15, 2021

I have had luck with subimage-match. It allows different sizes of images and is basically developed around this repo's method and efficiency.

Not to steal credits though... just for the sake of helping on this issue.

@ansonliao
Copy link

Hi @zinen , look like subimage-match can't handle different sizes of images by some scenarios, for example, my case is the capture the image from different iPhone model iOS simulator, subimage-match can't handle this case, for example, capture the same image from iPhone 11 pro iOS simulator and iPhone 14 Pro Max iOS simulator.

@Abdull
Copy link

Abdull commented Nov 28, 2022

Here is my solution to compare (possibly) differently-dimensioned images: first place each image on an empty canvas having the maximum width and height among both images. Then compare these identically sized versions. I use sharp for the image dimension harmonization.

#!/usr/bin/env node
// filename: pixelmatch-different-size.mjs
'use strict';

import fs from 'fs';
import { PNG } from 'pngjs'; // npm install pngjs ... https://www.npmjs.com/package/pngjs/v/6.0.0
import pixelmatch from 'pixelmatch'; // npm install pixelmatch ... https://www.npmjs.com/package/pixelmatch/v/5.3.0
import sharp from 'sharp'; // npm install sharp ... https://www.npmjs.com/package/sharp/v/0.31.1

const buffer1 = fs.readFileSync('img1.png'); // https://nodejs.org/api/fs.html#fsreadfilesyncpath-options
const sharp1 = sharp(buffer1); // https://sharp.pixelplumbing.com/api-constructor
const sharp1Metadata = await sharp1.metadata(); // https://sharp.pixelplumbing.com/api-input#metadata

const buffer2 = fs.readFileSync('img2.png');
const sharp2 = sharp(buffer2);
const sharp2Metadata = await sharp2.metadata();

// choose maximum width and height among both images, providing a bounding box canvas which can contain either image
const maximumWidth = Math.max(sharp1Metadata.width, sharp2Metadata.width);
const maximumHeight = Math.max(sharp1Metadata.height, sharp2Metadata.height);
    
// derive a new version "boxedBuffer1" of "img1.png", conceptually placing "img1.png" in the upper-left corner of a bounding box canvas
const boxedBuffer1 = await sharp1
  .resize({ // https://sharp.pixelplumbing.com/api-resize#resize
    width: maximumWidth,
    height: maximumHeight,
    fit: 'contain', // instead of cropping or stretching, use "(letter/pillar/window)-boxing" (black space) to fill any excess space
    position: 'left top' // arrange the original image in the upper-left corner
  })
  .raw()
  .toBuffer();

const boxedBuffer2 = await sharp2
  .resize({
    width: maximumWidth,
    height: maximumHeight,
    fit: 'contain',
    position: 'left top'
  })
  .raw()
  .toBuffer();

// boxedBuffer1 and boxedBuffer2 will have the same dimensions

const diff = new PNG({ // create a new PNG for holding the pixelmatch difference
  width: maximumWidth,
  height: maximumHeight
});

const numDiffPixels = pixelmatch(
  boxedBuffer1,
  boxedBuffer2,
  diff.data,
  maximumWidth,
  maximumHeight,
  {threshold: 0.05} // my favorite threshold to also consider lighter grays as differences
);

fs.writeFileSync('diff-boxed.png', PNG.sync.write(diff));
await sharp1.png().toFile('img1-boxed.png');
await sharp2.png().toFile('img2-boxed.png');

Example

img1.png: img1

img2.png: img2

$ node pixelmatch-different-size.mjs    
numDiffPixels:  69302

img1-boxed.png: img1-boxed

img2-boxed.png: img2-boxed

diff-boxed.png: diff-boxed

@htho
Copy link

htho commented Mar 1, 2024

here is my minimal-dependency solution to the problem:

/** Creates a diff of the given input images, which may have different sizes. 
 * @param {PNG} img1
 * @param {PNG} img2
 * @param {pixelmatch.PixelmatchOptions} options
 * @returns {{diff: PNG, numOfDiffPixels: number}}
 */
function createDiff(img1, img2, options) {
	const diffDimensions = {
		width: Math.max(img1.width, img2.width),
		height: Math.max(img1.height, img2.height),
	};

	const resizedImg1 = createResized(img1, diffDimensions);
	const resizedImg2 = createResized(img2, diffDimensions);

	const diff = new PNG(diffDimensions);

	const numOfDiffPixels = pixelmatch(
		resizedImg1.data,
		resizedImg2.data,
		diff.data,
		diffDimensions.width,
		diffDimensions.height,
		options,
	);

	return {diff, numOfDiffPixels};
}

/** Cretes a copy of {@link img}, with the {@link dimensions}.
 * @param {PNG} img 
 * @param {{width: number, height: number}} dimensions 
 * @returns {PNG} 
 */
function createResized(img, dimensions) {
	if(img.width > dimensions.width || img.height > dimensions.height) {
		throw new Error(`New dimensions expected to be greater than or equal to the original dimensions!`);
	}
	const resized = new PNG(dimensions);
	PNG.bitblt(img, resized, 0, 0, img.width, img.height);

	return resized;
}

@htho
Copy link

htho commented Mar 1, 2024

Looking at the arguments of Pixelmatch, it is very unlikely to ever support different image sizes out-of-the box.

Pixelmatch operates on Buffers. It does not care where these buffers come from, it does not create new ones.

Pixelmatch receives two Buffers of input-image data.
In these Buffers, 4 bytes of data make a pixel. Buffers are 1-Dimensional. There is no information about the width and the height of the images stored in these buffers. Width and height are meta-data.

We provide a Buffer for the output diff.

We need to provide the dimensions Pixelmatch uses to interpret these Buffers.

Pixelmatch does one thing. I like that.

@bronius
Copy link

bronius commented Mar 15, 2024

here is my minimal-dependency solution to the problem:

This works really well, @htho and @Abdull -- thank you for posting your implementations. Setting an automatically sized, common-dimensions canvas as a starting point and comparing from 0,0 is a great solution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants