├── .gitignore ├── spec ├── images │ ├── matches │ │ ├── blur.jpg │ │ ├── crop.jpg │ │ ├── mix.jpg │ │ ├── scale.jpg │ │ ├── wrong.jpg │ │ ├── source.jpg │ │ ├── rotation.jpg │ │ ├── compression.jpg │ │ └── desaturation.jpg │ └── colorful-picture.png ├── component.json ├── simi.js └── index.html ├── .travis.yml ├── package.json ├── Gruntfile.js ├── README.md └── simi.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | components/ 3 | .idea 4 | .DS_Store 5 | .idea/ 6 | -------------------------------------------------------------------------------- /spec/images/matches/blur.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlyfied/js-image-similarity/HEAD/spec/images/matches/blur.jpg -------------------------------------------------------------------------------- /spec/images/matches/crop.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlyfied/js-image-similarity/HEAD/spec/images/matches/crop.jpg -------------------------------------------------------------------------------- /spec/images/matches/mix.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlyfied/js-image-similarity/HEAD/spec/images/matches/mix.jpg -------------------------------------------------------------------------------- /spec/images/matches/scale.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlyfied/js-image-similarity/HEAD/spec/images/matches/scale.jpg -------------------------------------------------------------------------------- /spec/images/matches/wrong.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlyfied/js-image-similarity/HEAD/spec/images/matches/wrong.jpg -------------------------------------------------------------------------------- /spec/images/matches/source.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlyfied/js-image-similarity/HEAD/spec/images/matches/source.jpg -------------------------------------------------------------------------------- /spec/images/colorful-picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlyfied/js-image-similarity/HEAD/spec/images/colorful-picture.png -------------------------------------------------------------------------------- /spec/images/matches/rotation.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlyfied/js-image-similarity/HEAD/spec/images/matches/rotation.jpg -------------------------------------------------------------------------------- /spec/images/matches/compression.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlyfied/js-image-similarity/HEAD/spec/images/matches/compression.jpg -------------------------------------------------------------------------------- /spec/images/matches/desaturation.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlyfied/js-image-similarity/HEAD/spec/images/matches/desaturation.jpg -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.8 4 | before_script: 5 | - npm cache clear 6 | - npm install grunt-cli -g 7 | script: grunt --verbose 8 | -------------------------------------------------------------------------------- /spec/component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-image-similarity", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "jquery": ">= 1.7", 6 | "underscore": "*", 7 | "mocha": ">= 1.6", 8 | "chai": ">= 1.0" 9 | } 10 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-image-similarity", 3 | "version": "0.0.1", 4 | "description": "Tiny image comparison lib", 5 | "main": "simi.js", 6 | "dependencies": { 7 | "grunt": "~0.4.1", 8 | "grunt-mocha": "~0.3.0", 9 | "grunt-contrib-jshint": "~0.3.0", 10 | "phantomjs": "~1.8.2-2", 11 | "grunt-lib-phantomjs": "~0.2.0" 12 | }, 13 | "devDependencies": {}, 14 | "scripts": { 15 | "test": "grunt" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git://github.com/bitlyfied/js-image-similarity.git" 20 | }, 21 | "author": "", 22 | "license": "BSD", 23 | "readmeFilename": "README.md", 24 | "engines" : { "node" : ">=0.9.0" } 25 | } -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*global module:false*/ 2 | module.exports = function (grunt) { 3 | 4 | // Project configuration. 5 | grunt.initConfig({ 6 | // Task configuration. 7 | mocha: { 8 | all: [ 'spec/index.html' ] 9 | }, 10 | jshint: { 11 | options: { 12 | curly: true, 13 | eqeqeq: true, 14 | immed: true, 15 | latedef: true, 16 | newcap: true, 17 | noarg: true, 18 | sub: true, 19 | undef: true, 20 | unused: true, 21 | boss: true, 22 | eqnull: true, 23 | browser: true, 24 | globals: { 25 | jQuery: true 26 | } 27 | }, 28 | gruntfile: { 29 | src: 'Gruntfile.js' 30 | }, 31 | lib_test: { 32 | src: ['simi.js'] 33 | } 34 | } 35 | }); 36 | 37 | grunt.loadNpmTasks('grunt-contrib-jshint'); 38 | grunt.loadNpmTasks('grunt-mocha'); 39 | 40 | // Default task. 41 | grunt.registerTask('default', ['jshint', 'mocha']); 42 | 43 | }; 44 | -------------------------------------------------------------------------------- /spec/simi.js: -------------------------------------------------------------------------------- 1 | describe("simi", function () { 2 | 3 | describe("utils", function () { 4 | 5 | describe("mapToBits", function () { 6 | it("generates a bit mask out of a callback run on each element of an array", function () { 7 | var bitMask = simi.utils.mapToBits([1, 2, 3, 4], function (item) { 8 | return item % 2 == 0; 9 | }); 10 | 11 | assert.equal('1010', bitMask.toString(2)); 12 | }); 13 | }); 14 | 15 | describe("thresholdMap", function () { 16 | it("returns the correct bits", function () { 17 | var input = new Array(64); 18 | input[0] = 10; 19 | input[1] = 20; 20 | input[2] = 30; 21 | 22 | var threshold = 20; 23 | 24 | var expectedOutput = '110'; 25 | 26 | var bitMask = simi.utils.thresholdMap(input, threshold); 27 | 28 | assert.equal(expectedOutput, bitMask.toString(2)); 29 | }); 30 | }); 31 | 32 | describe("hammingDistance", function () { 33 | it("returns the correct distance", function () { 34 | var from = parseInt('11111', 2), 35 | to = parseInt('10110', 2); 36 | 37 | var distance = simi.utils.hammingDistance(from, to); 38 | 39 | assert.equal(2, distance); 40 | }); 41 | }); 42 | 43 | 44 | }); 45 | 46 | }); 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | JavaScript Image Similarity Comparison 2 | ======================= 3 | 4 | [![Build Status](https://travis-ci.org/bitlyfied/js-image-similarity.png)](https://travis-ci.org/bitlyfied/js-image-similarity) 5 | 6 | This is a first draft of a basic image comparison algorithm using average hashes. 7 | The algorithm used is the one described here: 8 | 9 | http://www.hackerfactor.com/blog/index.php?/archives/432-Looks-Like-It.html 10 | 11 | The library is only supposed to work in latest WebKit. 12 | Supports for Canvas and Array.forEach is required. 13 | 14 | Usage 15 | ----- 16 | 17 | var img1 = $('#image1'), 18 | img2 = $('#imge2'); 19 | 20 | // returns a difference rate (from 0 to 1) 21 | simi.compare(img1, img2); 22 | 23 | // returns true or false (same as compare with a default threshold) 24 | simi.same(img1, img2); 25 | 26 | // returns a hash of the image 27 | // simi.hash could be invoked to cache the hash 28 | var hash = simi.hash(img1); 29 | simi.compare(hash, img2); // only img2 will be hashed 30 | 31 | 32 | Dependencies 33 | ----- 34 | 35 | The library doesn't have any external dependencies, but you need bower, jquery, underscore, mocha and chai to run tests 36 | 37 | 38 | How to run tests 39 | ---- 40 | 41 | Tests are now run automatically using Travis CI. 42 | If you need to run tests manually you just need to use Grunt and run the default task. 43 | 44 | If you want to run tests manually from a browser you can follow this process: 45 | 46 | # run tests inside the browser 47 | cd .. 48 | python -m SimpleHTTPServer 49 | open http://localhost:8000/spec/index.html 50 | 51 | Always remember to download spec depdencies using bower before doing anything: 52 | 53 | # install tests dependencies using bower 54 | cd spec 55 | bower install 56 | -------------------------------------------------------------------------------- /spec/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Mocha Tests for Simi 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 21 | 22 | 91 | 92 | 93 | 94 | 102 |
103 | 104 | 105 | -------------------------------------------------------------------------------- /simi.js: -------------------------------------------------------------------------------- 1 | (function(global){ 2 | 3 | var SIMILARITY_THRESHOLD = 0.15; 4 | 5 | function hash(image){ 6 | var pixelsMap = utils.pixelsMap(image,8,8), 7 | bytesMap = utils.desaturate(pixelsMap), 8 | average = utils.average(bytesMap), 9 | thresholdMap = utils.thresholdMap(bytesMap, average); 10 | 11 | return thresholdMap; 12 | } 13 | 14 | function compare(first, second){ 15 | first = (first instanceof HTMLImageElement) ? hash(first) : first; 16 | second = (second instanceof HTMLImageElement) ? hash(second) : second; 17 | 18 | var distance = utils.hammingDistance(first, second); 19 | 20 | return (distance / 64).toFixed(3); 21 | } 22 | 23 | function same(first, second){ 24 | return compare(first, second) <= SIMILARITY_THRESHOLD; 25 | } 26 | 27 | var utils = { 28 | /** 29 | * Returns an array with averaged colors (shades of gray) 30 | * @param data 31 | */ 32 | desaturate: function(data){ 33 | var grays = new Array(data.length / 4); 34 | for(var i = 0; i < grays.length; i++){ 35 | var j = i * 4; 36 | grays[i] = Math.round((data[j] + data[j+1] + data[j+2]) / 3); 37 | } 38 | return grays; 39 | }, 40 | 41 | /** 42 | * Returns the average of an array of numbers 43 | * @param data 44 | * @return Number 45 | */ 46 | average: function(data){ 47 | var total = 0; 48 | for(var i = 0; i < data.length; i++){ 49 | total += data[i]; 50 | } 51 | return Math.round(total / data.length); 52 | }, 53 | 54 | /** 55 | * Generates a long bitmask (64bit) from an array of 64 bytes. 56 | * Each byte is converted in a positive bit if the byte is greater 57 | * or equal than the specified threshold 58 | * 59 | * @param data 60 | * @param threshold 61 | * @return Number bitmap 62 | */ 63 | thresholdMap: function(data, threshold){ 64 | return utils.mapToBits(data, function(byteData){ 65 | return byteData >= threshold; 66 | }); 67 | }, 68 | 69 | /** 70 | * Generates a bit mask by invoking the callback on each element 71 | * of the provided array and computing the result as a series of bit. 72 | * The callback must return a boolean. 73 | * 74 | * @param data 75 | * @param callback 76 | * @return Number bitMask 77 | */ 78 | mapToBits: function(data, callback){ 79 | var result = 0, bit = 0; 80 | data.forEach(function(element){ 81 | result |= callback(element) << bit++; 82 | }); 83 | return result; 84 | }, 85 | 86 | /** 87 | * Scale down the image to the specified width ad height and returns 88 | * an array of the resulting pixels 89 | * 90 | * @param image 91 | * @param width 92 | * @param height 93 | * @return CanvasPixelArray 94 | */ 95 | pixelsMap: function(image,width,height){ 96 | var canvas = document.createElement("canvas"); 97 | canvas.width = width; canvas.height = height; 98 | 99 | var context = canvas.getContext('2d'); 100 | context.drawImage(image, 0, 0, width, height); 101 | 102 | return context.getImageData(0, 0, width, height).data; 103 | }, 104 | 105 | /** 106 | * Returning the Hamming distance between two series of bits 107 | * 108 | * @param bitsA 109 | * @param bitsB 110 | * @return Number distance 111 | */ 112 | hammingDistance: function(bitsA, bitsB){ 113 | var diffMask = (bitsA ^ bitsB).toString(2); 114 | return (diffMask.match(/1/g)||[]).length; 115 | } 116 | }; 117 | 118 | global.simi = { 119 | hash: hash, 120 | compare: compare, 121 | same: same, 122 | utils: utils 123 | }; 124 | })(window); 125 | --------------------------------------------------------------------------------