Skip to content

Instantly share code, notes, and snippets.

@johnnoel
Created February 18, 2017 11:58
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save johnnoel/b6c80ef49de4e2fbf4c616956a289e23 to your computer and use it in GitHub Desktop.
Save johnnoel/b6c80ef49de4e2fbf4c616956a289e23 to your computer and use it in GitHub Desktop.
JavaScript ES6 Classes for perceptual image hashing
import Colour from './Colour';
import Resample from './Resample';
/**
* Average hash for images
*
* @author John Noel <john.noel@chaostangent.com>
* @see http://www.hackerfactor.com/blog/index.php?/archives/432-Looks-Like-It.html
* @see https://github.com/jenssegers/imagehash/blob/master/src/Implementations/AverageHash.php
*/
class Average
{
/**
* Hash the given image data
*
* @param ImageData imageData An ImageData object, usually from a <canvas>
* @return integer
*/
static hash(imageData) {
let size = Average.size,
pixelCount = (size * size);
if (imageData.width != size || imageData.size != size) {
imageData = Resample.nearestNeighbour(imageData, size, size);
}
imageData = Colour.grayscale(imageData);
let sum = 0;
for (let i = 0; i < pixelCount; i++) {
// already grayscale so just take the first channel
sum += imageData.data[4 * i];
}
let average = Math.floor(sum / pixelCount),
hash = 0,
one = 1;
for (let i = 0; i < pixelCount; i++) {
if (imageData.data[4 * i] > average) {
hash |= one;
}
one = one << 1;
}
return hash;
}
}
Average.size = 8;
export default Average;
/**
* Colour transformation functions
*
* @author John Noel <john.noel@chaostangent.com>
*/
export default class Colour
{
/**
* Convert the image data into grayscale
*
* This just takes the average of all three colour values (RGB) and sets
* each channel to that value. Alpha information is preserved
*
* @param ImageData imageData An ImageData object, usually from a <canvas>
* @return ImageData Returns a new ImageData object
* @see http://www.johndcook.com/blog/2009/08/24/algorithms-convert-color-grayscale/
*/
static grayscale(imageData) {
let pixelCount = imageData.width * imageData.height,
converted = new Uint8ClampedArray(pixelCount * 4);
for (let i = 0; i < pixelCount; i++) {
let offset = i * 4,
gray = Math.floor(imageData.data[offset] + imageData.data[offset + 1] + imageData.data[offset + 2]) / 3;
for (let j = 0; j < 3; j++) {
converted[offset + j] = gray;
}
// retain alpha channel
converted[offset + 3] = imageData[offset + 3];
}
return new ImageData(converted, imageData.width, imageData.height);
}
/**
* Convert the image data into YCbCr and extract the luminosity (Y) part
*
* This uses ITU-R BT.601 conversion method and will retain any alpha
* information on a pixel
*
* @param ImageData imageData An ImageData object, usually from a <canvas>
* @return ImageData Returns a converted ImageData object
* @see https://en.wikipedia.org/wiki/YCbCr#ITU-R_BT.601_conversion
*/
static luminosity(imageData) {
let pixels = imageData.width * imageData.height;
let converted = new Uint8ClampedArray(pixels * 4);
for (let i = 0; i < pixels; i++) {
let offset = i * 4;
converted[offset] = 0.299 * imageData.data[offset];
converted[offset + 1] = 0.587 * imageData.data[offset + 1];
converted[offset + 2] = 0.114 * imageData.data[offset + 2];
converted[offset + 3] = imageData.data[offset + 3];
}
return new ImageData(converted, imageData.width, imageData.height);
}
}
/**
* Discrete Cosine Transforms in JavaScript
*
* Not my code, mostly translated from other language implementations
*
* @author John Noel <john.noel@chaostangent.com>
*/
export default class DCT
{
/**
* 1 dimensional DCT
*
* @param integer N size
* @param array f A 1 dimensional array of values
* @return Float64Array
* @see https://github.com/jenssegers/imagehash/blob/master/src/Implementations/PerceptualHash.php
*/
static DCT1D(N, f) {
let F = new Float64Array(N);
for (let i = 0; i < N; i++) {
let sum = 0;
for (let j = 0; j < N; j++) {
sum += f[j] * Math.cos(i * Math.PI * (j + 0.5) / N);
}
sum *= Math.sqrt(2 / N);
if (i == 0) {
sum *= 1 / Math.sqrt(2);
}
F[i] = sum;
}
return F;
}
/**
* 2 dimensional DCT
*
* @param integer N size
* @param array f A 1 dimensional array of values
* @return Float64Array
* @see https://github.com/naptha/phash.js/blob/master/phash.js
*/
static DCT2D(N, f) {
let c = new Float64Array(N);
for (let i = 1; i < N; i++) {
c[i] = 1;
}
c[0] = 1 / Math.sqrt(2);
let F = new Float64Array(N * N);
// precompute cosine lookup table
let entries = (2 * N) * (N - 1);
let COS = new Float64Array(entries);
for (let i = 0; i < entries; i++) {
COS[i] = Math.cos(i / (2 * N) * Math.PI);
}
for (let u = 0; u < N; u++) {
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 += COS[(2 * i + 1) * u]
* COS[(2 * j + 1) * v]
* f[N * i + j];
}
}
sum *= ((c[u] * c[v]) / 4);
F[N * u + v] = sum;
}
}
return F;
}
}
import Colour from './Colour';
import Resample from './Resample';
/**
* Difference hash for images
*
* @author John Noel <john.noel@chaostangent.com>
* @see http://www.hackerfactor.com/blog/?/archives/529-Kind-of-Like-That.html
* @see https://github.com/jenssegers/imagehash/blob/master/src/Implementations/DifferenceHash.php
*/
class Difference
{
/**
* Hash the given image data
*
* @param ImageData imageData An ImageData object, usually from a <canvas>
* @return integer
*/
static hash(imageData) {
let size = Difference.size,
w = size,
h = size + 1;
if (imageData.width != w || imageData.height != h) {
imageData = Resample.nearestNeighbour(imageData, w, h);
}
imageData = Colour.grayscale(imageData);
let hash = 0,
one = 1;
for (let y = 0; y < h; y++) {
// ignore other channels as already converted to grayscale
let left = imageData.data[4 * w * y];
for (let x = 1; x < w; x++) {
let right = imageData.data[4 * (w * x + y)];
if (left > right) {
hash |= one;
}
left = right;
one = one << 1;
}
}
return hash;
}
}
Difference.size = 8;
export default Difference;
import DCT from './DCT';
import Colour from './Colour';
import Resample from './Resample';
/**
* Perceptual hash for images
*
* @author John Noel <john.noel@chaostangent.com>
* @see http://www.phash.org/
* @see https://github.com/jenssegers/imagehash
*/
class Perceptual
{
/**
* Hash the given image data
*
* @param ImageData imageData An ImageData object, usually from a <canvas>
* @return integer
*/
static hash(imageData) {
let size = Perceptual.size;
if (imageData.width != size || imageData.height != size) {
imageData = Resample.nearestNeighbour(imageData, size, size);
}
imageData = Colour.luminosity(imageData);
// take a 1D DCT of each row
let rows = [];
for (let y = 0; y < size; y++) {
let row = new Float64Array(size);
for (let x = 0; x < size; x++) {
let base = 4 * (size * x + y);
row[x] = imageData.data[base]
+ imageData.data[base + 1]
+ imageData.data[base + 2];
}
rows[y] = DCT.DCT1D(size, row);
}
// then take a 1D DCT of each column
let matrix = [];
for (let x = 0; x < size; x++) {
let col = new Float64Array(size);
for (let y = 0; y < size; y++) {
col[y] = rows[y][x];
}
matrix[x] = DCT.DCT1D(size, col);
}
// grab the top 8x8 pixels
let top8 = [];
for (let y = 0; y < 8; y++) {
for (let x = 0; x < 8; x++) {
top8.push(matrix[y][x]);
}
}
// calculate the median
let median = top8.slice(0).sort((a, b) => {
return a - b;
})[31];
let hash = 0;
let one = 1;
// calculate the hash
for (let i = 0; i < 64; i++) {
if (top8[i] > median) {
hash |= one;
}
one = one << 1;
}
return hash;
}
}
Perceptual.size = 32;
export default Perceptual;
/**
* Image resampling algorithms
*
* @author John Noel <john.noel@chaostangent.com>
*/
export default class Resample
{
/**
* Resample an image using nearest neighbour
*
* @param ImageData imageData
* @param integer targetWidth
* @param integer targetHeight
* @return ImageData
* @see http://tech-algorithm.com/articles/nearest-neighbor-image-scaling/
*/
static nearestNeighbour(imageData, targetWidth, targetHeight) {
let w = imageData.width,
h = imageData.height,
xr = w / targetWidth,
yr = h / targetHeight,
ret = new Uint8ClampedArray((targetWidth * targetHeight) * 4);
for (let i = 0; i < targetHeight; i++) {
for (let j = 0; j < targetWidth; j++) {
let px = Math.floor(j * xr),
py = Math.floor(i * yr);
let rt = 4 * ((i * targetWidth) + j),
rs = 4 * ((py * w) + px);
for (let k = 0; k < 4; k++) {
ret[rt+k] = imageData.data[rs+k];
}
}
}
return new ImageData(ret, targetWidth, targetHeight);
}
/**
* Resample an image using bilinear
*
* @param ImageData imageData
* @param integer targetSize
* @return ImageData
* @see http://tech-algorithm.com/articles/bilinear-image-scaling/
*/
static bilinear(imageData, targetSize) {
let w = imageData.width,
h = imageData.height,
xr = w / targetSize,
yr = h / targetSize,
ret = new Uint8ClampedArray((targetSize * targetSize) * 4),
offset = 0;
for (let i = 0; i < targetSize; i++) {
for (let j = 0; j < targetSize; j++) {
let x = Math.round(xr * j),
y = Math.round(yr * i),
xd = (xr * j) - x,
yd = (yr * i) - y,
idx = (y * w + x) * 4;
for (let k = 0; k < 4; k++) {
let a = imageData.data[idx + k],
b = imageData.data[idx + k + 4],
c = imageData.data[idx + k + w],
d = imageData.data[idx + k + w + 4];
ret[offset + k] = a * (1 - xd) * (1 - yd) + b * xd * (1 - yd) +
c * yd * (1 - xd) + d * xd * yd;
}
offset += 4;
}
}
return new ImageData(ret, targetSize, targetSize);
}
/**
* Resample an image using bicubic
*
* @param ImageData imageData
* @param integer targetSize
* @return ImageData
* @see http://techslides.com/javascript-image-resizer-and-scaling-algorithms
* @see http://jsfiddle.net/HZewg/1/
*/
static bicubic(imageData, targetSize) {
// todo
}
}
@johnnoel
Copy link
Author

Would you mind elaborating @freearhey? This is 3.5 years old at this point so it's more than likely there's been a lot of drift in the code.

@freearhey
Copy link

@johnnoel of course. This code produces a hash completely different from the current version of jenssegers/imagehash and since you have not specified anywhere that this code is outdated for me personally it was a surprise. Also just to make it work, I had to spend a lot of time, so I decided to warn the others.

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