-
-
Save trogau/2a187dddb78c6857da771c6e4ecc2024 to your computer and use it in GitHub Desktop.
JavaScript ES6 Classes for perceptual image hashing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Colour from './Colour'; | |
import Resample from './Resample'; | |
/** | |
* Average hash for images | |
* | |
* @author John Noel <[email protected]> | |
* @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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Colour transformation functions | |
* | |
* @author John Noel <[email protected]> | |
*/ | |
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); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Discrete Cosine Transforms in JavaScript | |
* | |
* Not my code, mostly translated from other language implementations | |
* | |
* @author John Noel <[email protected]> | |
*/ | |
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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Colour from './Colour'; | |
import Resample from './Resample'; | |
/** | |
* Difference hash for images | |
* | |
* @author John Noel <[email protected]> | |
* @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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import DCT from './DCT'; | |
import Colour from './Colour'; | |
import Resample from './Resample'; | |
/** | |
* Perceptual hash for images | |
* | |
* @author John Noel <[email protected]> | |
* @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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Image resampling algorithms | |
* | |
* @author John Noel <[email protected]> | |
*/ | |
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 | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment