Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@krhoyt
Created May 31, 2017 19:44
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save krhoyt/cf9de4eed5f406de044fd1851fb0f5dc to your computer and use it in GitHub Desktop.
Save krhoyt/cf9de4eed5f406de044fd1851fb0f5dc to your computer and use it in GitHub Desktop.
Image Hashing in the Browser
// Original source:
// https://github.com/commonsmachinery/blockhash-js
// Update for local file reading
// Update (loosely) for ES6
// Removes dependencies from origina project
class Blockhash {
constructor() {
this.canvas = document.createElement( 'canvas' );
this.canvas.style.visibility = 'hidden';
this.canvas.style.position = 'absolute';
document.body.appendChild( this.canvas );
this.image = document.createElement( 'img' );
this.image.style.visibility = 'hidden';
this.image.style.position = 'absolute';
this.image.addEventListener( 'load', evt => this.doLoad( evt ) );
document.body.appendChild( this.image );
this.reader = new FileReader();
this.reader.addEventListener( 'load', evt => this.doRead( evt ) );
this.bits = null;
this.method = null;
this.callback = null;
}
blockhash( file, bits, method, callback ) {
this.bits = bits;
this.method = method;
this.callback = callback;
this.reader.readAsDataURL( file );
}
doLoad( evt ) {
this.canvas.width = this.image.width;
this.canvas.height = this.image.height;
let context = this.canvas.getContext( '2d' );
context.drawImage( this.image, 0, 0, this.image.width, this.image.height );
let data = context.getImageData( 0, 0, this.image.width, this.image.height );
let hash = this.blockhashData( data, this.bits, this.method );
this.callback( hash );
}
doRead( evt ) {
this.image.src = evt.target.result;
}
/*
* From original
*/
hammingDistance( hash1, hash2 ) {
let d = 0;
let i;
for( i = 0; i < hash1.length; i++ ) {
let n1 = parseInt( hash1[i], 16 );
let n2 = parseInt( hash2[i], 16 );
d += one_bits[n1 ^ n2];
}
return d;
}
median( data ) {
let mdarr = data.slice( 0 );
mdarr.sort( function( a, b ) {
return a - b;
} );
if( mdarr.length % 2 === 0 ) {
return ( mdarr[mdarr.length / 2] + mdarr[mdarr.length / 2 + 1] ) / 2.0;
}
return mdarr[Math.floor( mdarr.length / 2 )];
}
translate_blocks_to_bits( blocks, pixels_per_block ) {
let half_block_value = pixels_per_block * 256 * 3 / 2;
let bandsize = blocks.length / 4;
// Compare medians across four horizontal bands
for( let i = 0; i < 4; i++ ) {
let m = this.median( blocks.slice( i * bandsize, ( i + 1 ) * bandsize ) );
for( let j = i * bandsize; j < ( i + 1 ) * bandsize; j++ ) {
let v = blocks[j];
// Output a 1 if the block is brighter than the median.
// With images dominated by black or white, the median may
// end up being 0 or the max value, and thus having a lot
// of blocks of value equal to the median. To avoid
// generating hashes of all zeros or ones, in that case output
// 0 if the median is in the lower value space, 1 otherwise
blocks[j] = Number( v > m || ( Math.abs( v - m ) < 1 && m > half_block_value ) );
}
}
}
bits_to_hexhash( bits ) {
let hex = [];
for( let i = 0; i < bits.length; i += 4 ) {
let nibble = bits.slice( i, i + 4 );
hex.push( parseInt( nibble.join( '' ), 2 ).toString( 16 ) );
}
return hex.join( '' );
}
bmvbhash_even( data, bits ) {
let blocksize_x = Math.floor( data.width / bits );
let blocksize_y = Math.floor( data.height / bits );
let result = [];
for( let y = 0; y < bits; y++ ) {
for( let x = 0; x < bits; x++ ) {
let total = 0;
for( let iy = 0; iy < blocksize_y; iy++ ) {
for( let ix = 0; ix < blocksize_x; ix++ ) {
let cx = x * blocksize_x + ix;
let cy = y * blocksize_y + iy;
let ii = ( cy * data.width + cx ) * 4;
let alpha = data.data[ii+3];
if( alpha === 0 ) {
total += 765;
} else {
total += data.data[ii] + data.data[ii+1] + data.data[ii+2];
}
}
}
result.push( total );
}
}
this.translate_blocks_to_bits( result, blocksize_x * blocksize_y );
return this.bits_to_hexhash( result );
}
bmvbhash( data, bits ) {
let result = [];
let i, j, x, y;
let block_width, block_height;
let weight_top, weight_bottom, weight_left, weight_right;
let block_top, block_bottom, block_left, block_right;
let y_mod, y_frac, y_int;
let x_mod, x_frac, x_int;
let blocks = [];
let even_x = data.width % bits === 0;
let even_y = data.height % bits === 0;
if( even_x && even_y ) {
return this.bmvbhash_even( data, bits );
}
// Initialize blocks array with 0s
for( i = 0; i < bits; i++ ) {
blocks.push( [] );
for( j = 0; j < bits; j++ ) {
blocks[i].push( 0 );
}
}
block_width = data.width / bits;
block_height = data.height / bits;
for( y = 0; y < data.height; y++ ) {
if( even_y ) {
// don't bother dividing y, if the size evenly divides by bits
block_top = block_bottom = Math.floor( y / block_height );
weight_top = 1;
weight_bottom = 0;
} else {
y_mod = ( y + 1 ) % block_height;
y_frac = y_mod - Math.floor( y_mod );
y_int = y_mod - y_frac;
weight_top = ( 1 - y_frac );
weight_bottom = ( y_frac );
// y_int will be 0 on bottom/right borders and on block boundaries
if( y_int > 0 || ( y + 1 ) === data.height ) {
block_top = block_bottom = Math.floor( y / block_height );
} else {
block_top = Math.floor( y / block_height );
block_bottom = Math.ceil( y / block_height );
}
}
for( x = 0; x < data.width; x++ ) {
let ii = ( y * data.width + x ) * 4;
let avgvalue, alpha = data.data[ii+3];
if( alpha === 0 ) {
avgvalue = 765;
} else {
avgvalue = data.data[ii] + data.data[ii+1] + data.data[ii+2];
}
if( even_x ) {
block_left = block_right = Math.floor( x / block_width );
weight_left = 1;
weight_right = 0;
} else {
x_mod = ( x + 1 ) % block_width;
x_frac = x_mod - Math.floor(x_mod);
x_int = x_mod - x_frac;
weight_left = ( 1 - x_frac );
weight_right = x_frac;
// x_int will be 0 on bottom/right borders and on block boundaries
if( x_int > 0 || ( x + 1 ) === data.width ) {
block_left = block_right = Math.floor( x / block_width );
} else {
block_left = Math.floor( x / block_width );
block_right = Math.ceil( x / block_width );
}
}
// add weighted pixel value to relevant blocks
blocks[block_top][block_left] += avgvalue * weight_top * weight_left;
blocks[block_top][block_right] += avgvalue * weight_top * weight_right;
blocks[block_bottom][block_left] += avgvalue * weight_bottom * weight_left;
blocks[block_bottom][block_right] += avgvalue * weight_bottom * weight_right;
}
}
for( i = 0; i < bits; i++ ) {
for( j = 0; j < bits; j++ ) {
result.push( blocks[i][j] );
}
}
this.translate_blocks_to_bits( result, block_width * block_height );
return this.bits_to_hexhash( result );
}
blockhashData( data, bits, method ) {
var hash;
if( method === 1 ) {
hash = this.bmvbhash_even( data, bits );
} else if( method === 2 ) {
hash = this.bmvbhash( data, bits );
} else {
throw new Error( 'Bad hashing method.' );
}
return hash;
}
}
body {
color: rgba( 0, 0, 0, 0.87 );
font-family: 'Roboto', sans-serif;
margin: 0;
overflow: hidden;
padding: 0;
}
.holder {
align-items: center;
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
}
.full {
position: absolute;
visibility: hidden;
}
.render {
box-shadow: 0 2px 5px rgba( 0, 0, 0, 0.26 );
display: none;
}
class Hashing {
constructor() {
this.help = document.querySelector( '.help' );
// Drag and drop
this.holder = document.querySelector( '.holder' );
this.holder.addEventListener( 'dragover', evt => this.doDragOver( evt ) );
this.holder.addEventListener( 'drop', evt => this.doDragDrop( evt ) );
// Image element
// Used for measuring original image
this.full = document.querySelector( '.full' );
this.full.addEventListener( 'load', evt => this.doImageLoad( evt ) );
// Canvas element
// Surface to display the image
this.render = document.querySelector( '.render' );
// Multiple handlers for XHR
// Need references for removal
this.doPreflightLoad = this.doPreflightLoad.bind( this );
this.doUpload = this.doUpload.bind( this );
// Hashing routines
// File reference
this.blockhash = new Blockhash();
this.file = null;
this.xhr = new XMLHttpRequest();
}
// Load image element
// Triggers sizing and display
show( path ) {
this.full.src = path;
}
// File dropped
// Perform hash
doDragDrop( evt ) {
evt.preventDefault();
console.log( 'File drop.' );
this.file = evt.dataTransfer.files[0];
this.blockhash.blockhash( this.file, 8, 2, hash => this.doHash( hash ) );
}
// Enable drag and drop
doDragOver( evt ) {
evt.preventDefault();
evt.dataTransfer.dropEffect = 'copy';
}
// Image hashed
// See if it exists on the server
doHash( hash ) {
console.log( 'Hash: ' + hash );
this.xhr.addEventListener( 'load', this.doPreflightLoad );
this.xhr.open( 'POST', '/api/image/preflight', true );
this.xhr.setRequestHeader( 'Content-Type', 'application/json' );
this.xhr.send( JSON.stringify( {
key: Hashing.API_KEY,
hash: hash
} ) );
}
// Image element loaded
// Size and display on canvas
doImageLoad( evt ) {
// Hide instructions
this.help.style.display = 'none';
// Landscape orientation
if( this.full.width > this.full.height ) {
this.render.width = Math.round( window.innerWidth * 0.75 );
this.render.height = Math.round( this.render.width / ( this.full.width / this.full.height ) );
} else {
// Portrait orientation
this.render.height = Math.round( window.innerHeight * 0.75 );
this.render.width = Math.round( this.render.height * ( this.full.width / this.full.height ) );
}
// Show canvas where image will go
this.render.style.display = 'block';
// Put image into canvas
let context = this.render.getContext( '2d' );
context.drawImage( this.full, 0, 0, this.render.width, this.render.height );
}
// File check completed
// Populate if exists
// Upload file if it does not
doPreflightLoad( evt ) {
var data = JSON.parse( this.xhr.responseText );
console.log( data );
// Clean up
this.xhr.removeEventListener( 'load', this.doPreflightLoad );
// Exists
// Populate image element
if( data.exists ) {
console.log( 'Found.' );
this.show( '/api/image/original/' + data.hash + '.jpg?key=' + Hashing.API_KEY );
} else {
// Does not exist
// Upload file to server
console.log( 'Not found.' );
// Form
let data = new FormData();
data.append( 'image', this.file );
data.append( 'key', Hashing.API_KEY );
// Upload
this.xhr.addEventListener( 'load', this.doUpload );
this.xhr.open( 'POST', '/api/image/original', true );
this.xhr.send( data );
}
}
// Upload completed
doUpload( evt ) {
let data = JSON.parse( this.xhr.responseText );
console.log( 'Upload complete.' );
console.log( data );
// Load image element
this.show( '/api/image/original/' + data.name + '.jpg?key=' + Hashing.API_KEY );
// Clean up
this.xhr.removeEventListener( 'load', this.doUpload );
}
}
Hashing.API_KEY = '159b191cdc1d7456e36af7a13fe764e5746989a8';
let app = new Hashing();
var express = require( 'express' );
var fs = require( 'fs' );
var imghash = require( 'imghash' );
var multer = require( 'multer' );
var mv = require( 'mv' );
var path = require( 'path' );
var randomstring = require( 'randomstring' );
var request = require( 'request' );
// Router
var router = express.Router();
// Upload storage options
// Unique name with extension
var storage = multer.diskStorage( {
destination: 'uploads',
filename: function( req, file, cb ) {
cb( null, randomstring.generate() + '.jpg' );
}
} );
// Upload handler
var upload = multer( {
storage: storage
} );
// Get specific image
router.get( '/original/:path', function( req, res ) {
res.sendFile( path.resolve( __dirname + '/../originals/' + req.query.key + '/' + req.params.path ) );
} );
// Image upload
router.post( '/original', upload.single( 'image' ), function( req, res ) {
var destination = path.resolve( __dirname + '/../originals/' + req.body.key );
// Does user have directory
fs.access( destination, function( err ) {
// Make directory if neeeded
if( err ) {
fs.mkdirSync( destination )
}
// Perceptual image hash
imghash.hash( req.file.path ).then( ( hash ) => {
// Does file already exist
fs.access( path.resolve( destination + '/' + hash + '.jpg' ), function( err ) {
if( err ) {
// File not previously uploaded
// Move upload to user directory
// Use hash as destination name
mv( req.file.path, destination + '/' + hash + '.jpg', function( err ) {
// Tell user where to find file
res.json( {
name: hash
} );
} );
} else {
// File already uploaded
// Tell user where to find file
res.json( {
name: hash
} );
}
} );
} );
} );
} );
// Check exists before uploading
router.post( '/preflight', function( req, res ) {
var destination = path.resolve( __dirname + '/../originals/' + req.body.key );
fs.access( path.resolve( destination + '/' + req.body.hash + '.jpg' ), function( err ) {
var found = true;
if( err ) {
found = false;
}
res.json( {
exists: found,
hash: req.body.hash
} );
} );
} );
// Test
router.get( '/test', function( req, res ) {
res.json( {facial: 'Image management.'} );
} );
// Export
module.exports = router;
<html>
<head>
<title>Hashing</title>
<!-- Google Fonts -->
<!-- Roboto -->
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
<!-- Styles -->
<link href="hashing.css" rel="stylesheet">
</head>
<body>
<!-- Center content -->
<div class="holder">
<!-- Instructions -->
<p class="help">Drag and drop file here.</p>
<!-- Display image -->
<canvas class="render"></canvas>
<!-- Measure image -->
<img class="full">
</div>
<!-- Blockhash -->
<!-- http://blockhash.io -->
<script src="blockhash.js"></script>
<!-- Application -->
<script src="hashing.js"></script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment