Initial commit
This commit is contained in:
commit
e49726988c
5445 files changed
+1089914
No files matched your search
+75
@@ -0,0 +1,75 @@
|
||||
import { expect, test, describe } from "vitest";
|
||||
import { getTestImagePath } from "@jimp/test-utils";
|
||||
import { createJimp } from "@jimp/core";
|
||||
import bmp from "@jimp/js-bmp";
|
||||
import png from "@jimp/js-png";
|
||||
import jpeg from "@jimp/js-jpeg";
|
||||
import tiff from "@jimp/js-tiff";
|
||||
|
||||
import { methods, compareHashes, distance } from "./index.js";
|
||||
|
||||
const Jimp = createJimp({
|
||||
formats: [bmp, png, jpeg, tiff],
|
||||
plugins: [methods],
|
||||
});
|
||||
|
||||
describe("hash", () => {
|
||||
test("base 2", async () => {
|
||||
const image = await Jimp.read(getTestImagePath("dice.png"));
|
||||
|
||||
expect(image.hash(2)).toBe(
|
||||
"1100010000011111011010111110000000010101001011010101101000010010",
|
||||
);
|
||||
});
|
||||
|
||||
test("base 10 (decimal)", async () => {
|
||||
const image = await Jimp.read(getTestImagePath("cops.jpg"));
|
||||
|
||||
expect(image.hash(10)).toBe("13442314021806033441");
|
||||
});
|
||||
|
||||
test("base 16 (hex)", async () => {
|
||||
const image = await Jimp.read(getTestImagePath("rgb.tiff"));
|
||||
|
||||
expect(image.hash(16)).toBe("949800481007044c");
|
||||
});
|
||||
|
||||
test("base 64", async () => {
|
||||
const image = await Jimp.read(getTestImagePath("windows95.bmp"));
|
||||
|
||||
expect(image.hash(64)).toBe("fb0xj0Mw400");
|
||||
});
|
||||
|
||||
test("base 23", async function () {
|
||||
const image = await Jimp.read(getTestImagePath("panoramic.jpg"));
|
||||
|
||||
expect(image.hash(23)).toBe("0m1m2id7l7cl4fb");
|
||||
});
|
||||
|
||||
test("base 17", async () => {
|
||||
const image = await Jimp.read(getTestImagePath("dice.png"));
|
||||
|
||||
expect(image.hash(17)).toBe("4ffe16bf68eee351");
|
||||
});
|
||||
});
|
||||
|
||||
describe("pHash", () => {
|
||||
test("should calculate the distance using distanceFromHash", async function () {
|
||||
const image1 = await Jimp.read(getTestImagePath("dice.png"));
|
||||
const image2 = await Jimp.read(getTestImagePath("mask.png"));
|
||||
|
||||
const hash = image1.pHash();
|
||||
|
||||
expect(image2.distanceFromHash(hash)).toBe(distance(image1, image2));
|
||||
});
|
||||
|
||||
test("should calculate the distance using compareHashes.", async function () {
|
||||
const image1 = await Jimp.read(getTestImagePath("dice.png"));
|
||||
const image2 = await Jimp.read(getTestImagePath("mask.png"));
|
||||
|
||||
const hash1 = image1.pHash();
|
||||
const hash2 = image2.pHash();
|
||||
|
||||
expect(compareHashes(hash1, hash2)).toBe(distance(image1, image2));
|
||||
});
|
||||
});
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
import { JimpClass } from "@jimp/types";
|
||||
import anyBase from "any-base";
|
||||
|
||||
import ImagePHash from "./phash.js";
|
||||
|
||||
// an array storing the maximum string length of hashes at various bases
|
||||
// 0 and 1 do not exist as possible hash lengths
|
||||
// prettier-ignore
|
||||
const alphabet =
|
||||
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_";
|
||||
|
||||
// an array storing the maximum string length of hashes at various bases
|
||||
// 0 and 1 do not exist as possible hash lengths
|
||||
const maxHashLength = [NaN, NaN];
|
||||
|
||||
for (let i = 2; i < 65; i++) {
|
||||
const maxHash = anyBase(
|
||||
anyBase.BIN,
|
||||
alphabet.slice(0, i),
|
||||
)(new Array(64 + 1).join("1"));
|
||||
maxHashLength.push(maxHash.length);
|
||||
}
|
||||
|
||||
export const methods = {
|
||||
/**
|
||||
* Calculates the perceptual hash
|
||||
* @returns the perceptual hash
|
||||
* @example
|
||||
* ```ts
|
||||
* import { Jimp } from "jimp";
|
||||
*
|
||||
* const image = await Jimp.read("test/image.png");
|
||||
*
|
||||
* image.hash();
|
||||
* ```
|
||||
*/
|
||||
pHash<I extends JimpClass>(image: I) {
|
||||
const pHash = new ImagePHash();
|
||||
return pHash.getHash(image);
|
||||
},
|
||||
|
||||
/**
|
||||
* Generates a perceptual hash of the image <https://en.wikipedia.org/wiki/Perceptual_hashing>. And pads the string. Can configure base.
|
||||
* @param base A number between 2 and 64 representing the base for the hash (e.g. 2 is binary, 10 is decimal, 16 is hex, 64 is base 64). Defaults to 64.
|
||||
* @example
|
||||
* ```ts
|
||||
* import { Jimp } from "jimp";
|
||||
*
|
||||
* const image = await Jimp.read("test/image.png");
|
||||
*
|
||||
* image.hash(2); // binary
|
||||
* image.hash(64); // base 64
|
||||
* ```
|
||||
*/
|
||||
hash<I extends JimpClass>(image: I, base = 64) {
|
||||
if (base < 2 || base > 64) {
|
||||
throw new Error("base must be a number between 2 and 64");
|
||||
}
|
||||
|
||||
const subAlphabet = alphabet.slice(0, base);
|
||||
const pHash = this.pHash(image);
|
||||
const maxLength = maxHashLength[base]!;
|
||||
|
||||
return anyBase(anyBase.BIN, subAlphabet)(pHash).padStart(maxLength, "0");
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculates the hamming distance of the current image and a hash based on their perceptual hash
|
||||
* @param compareHash hash to compare to
|
||||
* @returns a number ranging from 0 to 1, 0 means they are believed to be identical
|
||||
* @example
|
||||
* ```ts
|
||||
* import { Jimp } from "jimp";
|
||||
*
|
||||
* const image = await Jimp.read("test/image.png");
|
||||
*
|
||||
* image.distanceFromHash(image.pHash());
|
||||
* ```
|
||||
*/
|
||||
distanceFromHash<I extends JimpClass>(image: I, compareHash: string) {
|
||||
const pHash = new ImagePHash();
|
||||
const currentHash = pHash.getHash(image);
|
||||
|
||||
return pHash.distance(currentHash, compareHash);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the hamming distance of two images based on their perceptual hash
|
||||
* @param img1 A Jimp image to compare
|
||||
* @param img2 A Jimp image to compare
|
||||
* @returns A number ranging from 0 to 1, 0 means they are believed to be identical
|
||||
* @example
|
||||
* ```ts
|
||||
* import { Jimp, distance } from "jimp";
|
||||
*
|
||||
* const image1 = await Jimp.read("test/image.png");
|
||||
* const image2 = await Jimp.read("test/image.png");
|
||||
*
|
||||
* distance(image1, image2); // 0.5
|
||||
* ```
|
||||
*/
|
||||
export function distance<I extends JimpClass>(img1: I, img2: I) {
|
||||
const phash = new ImagePHash();
|
||||
const hash1 = phash.getHash(img1);
|
||||
const hash2 = phash.getHash(img2);
|
||||
|
||||
return phash.distance(hash1, hash2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the hamming distance of two images based on their perceptual hash
|
||||
* @param hash1 A pHash
|
||||
* @param hash2 A pHash
|
||||
* @returns A number ranging from 0 to 1, 0 means they are believed to be identical
|
||||
* @example
|
||||
* ```ts
|
||||
* import { Jimp, compareHashes } from "jimp";
|
||||
*
|
||||
* const image1 = await Jimp.read("test/image.png");
|
||||
* const image2 = await Jimp.read("test/image.png");
|
||||
*
|
||||
* compareHashes(image1.pHash(), image2.pHash()); // 0.5
|
||||
* ```
|
||||
*/
|
||||
export function compareHashes(hash1: string, hash2: string) {
|
||||
const phash = new ImagePHash();
|
||||
return phash.distance(hash1, hash2);
|
||||
}
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
Copyright (c) 2011 Elliot Shepherd
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { methods as color } from "@jimp/plugin-color";
|
||||
import { methods } from "@jimp/plugin-resize";
|
||||
import { JimpClass } from "@jimp/types";
|
||||
import { clone } from "@jimp/utils";
|
||||
|
||||
// https://code.google.com/p/ironchef-team21/source/browse/ironchef_team21/src/ImagePHash.java
|
||||
|
||||
/*
|
||||
* pHash-like image hash.
|
||||
* Author: Elliot Shepherd (elliot@jarofworms.com
|
||||
* Based On: http://www.hackerfactor.com/blog/index.php?/archives/432-Looks-Like-It.html
|
||||
*/
|
||||
|
||||
class ImagePHash {
|
||||
size: number;
|
||||
smallerSize: number;
|
||||
|
||||
constructor(size?: number, smallerSize?: number) {
|
||||
this.size = size || 32;
|
||||
this.smallerSize = smallerSize || 8;
|
||||
initCoefficients(this.size);
|
||||
}
|
||||
|
||||
distance(s1: string, s2: string) {
|
||||
let counter = 0;
|
||||
|
||||
for (let k = 0; k < s1.length; k++) {
|
||||
if (s1[k] !== s2[k]) {
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
|
||||
return counter / s1.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a 'binary string' (like. 001010111011100010) which is easy to do a hamming distance on.
|
||||
*/
|
||||
getHash(img: JimpClass) {
|
||||
/* 1. Reduce size.
|
||||
* Like Average Hash, pHash starts with a small image.
|
||||
* However, the image is larger than 8x8; 32x32 is a good size.
|
||||
* This is really done to simplify the DCT computation and not
|
||||
* because it is needed to reduce the high frequencies.
|
||||
*/
|
||||
img = methods.resize(clone(img), { w: this.size, h: this.size });
|
||||
|
||||
/* 2. Reduce color.
|
||||
* The image is reduced to a grayscale just to further simplify
|
||||
* the number of computations.
|
||||
*/
|
||||
img = color.greyscale(img);
|
||||
|
||||
const vals = [];
|
||||
|
||||
for (let x = 0; x < img.bitmap.width; x++) {
|
||||
const row = [];
|
||||
for (let y = 0; y < img.bitmap.height; y++) {
|
||||
row[y] = intToRGBA(img.getPixelColor(x, y)).b;
|
||||
}
|
||||
vals[x] = row;
|
||||
}
|
||||
|
||||
/* 3. Compute the DCT.
|
||||
* The DCT separates the image into a collection of frequencies
|
||||
* and scalars. While JPEG uses an 8x8 DCT, this algorithm uses
|
||||
* a 32x32 DCT.
|
||||
*/
|
||||
const dctVals = applyDCT(vals, this.size);
|
||||
|
||||
/* 4. Reduce the DCT.
|
||||
* This is the magic step. While the DCT is 32x32, just keep the
|
||||
* top-left 8x8. Those represent the lowest frequencies in the
|
||||
* picture.
|
||||
*/
|
||||
/* 5. Compute the average value.
|
||||
* Like the Average Hash, compute the mean DCT value (using only
|
||||
* the 8x8 DCT low-frequency values and excluding the first term
|
||||
* since the DC coefficient can be significantly different from
|
||||
* the other values and will throw off the average).
|
||||
*/
|
||||
let total = 0;
|
||||
|
||||
for (let x = 0; x < this.smallerSize; x++) {
|
||||
for (let y = 0; y < this.smallerSize; y++) {
|
||||
total += dctVals[x]![y]!;
|
||||
}
|
||||
}
|
||||
|
||||
const avg = total / (this.smallerSize * this.smallerSize);
|
||||
|
||||
/* 6. Further reduce the DCT.
|
||||
* This is the magic step. Set the 64 hash bits to 0 or 1
|
||||
* depending on whether each of the 64 DCT values is above or
|
||||
* below the average value. The result doesn't tell us the
|
||||
* actual low frequencies; it just tells us the very-rough
|
||||
* relative scale of the frequencies to the mean. The result
|
||||
* will not vary as long as the overall structure of the image
|
||||
* remains the same; this can survive gamma and color histogram
|
||||
* adjustments without a problem.
|
||||
*/
|
||||
let hash = "";
|
||||
|
||||
for (let x = 0; x < this.smallerSize; x++) {
|
||||
for (let y = 0; y < this.smallerSize; y++) {
|
||||
hash += dctVals[x]![y]! > avg ? "1" : "0";
|
||||
}
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
|
||||
// DCT function stolen from http://stackoverflow.com/questions/4240490/problems-with-dct-and-idct-algorithm-in-java
|
||||
|
||||
/**
|
||||
Convert a 32-bit integer color value to an RGBA object.
|
||||
*/
|
||||
function intToRGBA(i: number) {
|
||||
const a = i & 0xff;
|
||||
i >>>= 8;
|
||||
const b = i & 0xff;
|
||||
i >>>= 8;
|
||||
const g = i & 0xff;
|
||||
i >>>= 8;
|
||||
const r = i & 0xff;
|
||||
|
||||
return { r, g, b, a };
|
||||
}
|
||||
|
||||
const c: number[] = [];
|
||||
|
||||
function initCoefficients(size: number) {
|
||||
for (let i = 1; i < size; i++) {
|
||||
c[i] = 1;
|
||||
}
|
||||
|
||||
c[0] = 1 / Math.sqrt(2.0);
|
||||
}
|
||||
|
||||
function applyDCT(f: number[][], size: number) {
|
||||
const N = size;
|
||||
const F: number[][] = [];
|
||||
|
||||
for (let u = 0; u < N; u++) {
|
||||
const row = [];
|
||||
for (let v = 0; v < N; v++) {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < N; i++) {
|
||||
for (let j = 0; j < N; j++) {
|
||||
sum +=
|
||||
Math.cos(((2 * i + 1) / (2.0 * N)) * u * Math.PI) *
|
||||
Math.cos(((2 * j + 1) / (2.0 * N)) * v * Math.PI) *
|
||||
f[i]![j]!;
|
||||
}
|
||||
}
|
||||
|
||||
sum *= (c[u]! * c[v]!) / 4;
|
||||
row[v] = sum;
|
||||
F[u] = row;
|
||||
}
|
||||
}
|
||||
|
||||
return F;
|
||||
}
|
||||
|
||||
export default ImagePHash;
|
||||
Reference in new issue
Block a user