Created
May 31, 2017 19:44
-
-
Save krhoyt/cf9de4eed5f406de044fd1851fb0f5dc to your computer and use it in GitHub Desktop.
Image Hashing in the Browser
This file contains 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
// 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; | |
} | |
} |
This file contains 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
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; | |
} |
This file contains 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
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(); |
This file contains 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
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; |
This file contains 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
<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