Permalink
Browse files

Initial import

  • Loading branch information...
0 parents commit 1be32984e111799dfde77b45322af80735db89d5 @josip committed Mar 21, 2011
Showing with 242 additions and 0 deletions.
  1. +16 −0 Makefile
  2. +85 −0 README.md
  3. +3 −0 index.js
  4. +21 −0 package.json
  5. +117 −0 src/colour-extractor.coffee
@@ -0,0 +1,16 @@
+generate-js:
+ coffee -c -o lib src
+
+remove-js:
+ @rm -rf lib/
+
+publish: generate-js
+ npm publish
+ @remove-js
+
+install: generate-js
+ npm install
+ @remove-js
+
+.PHONY: all
+
@@ -0,0 +1,85 @@
+# colour-extractor
+
+Extract colour palettes from photos using Node.js.
+
+## Installation
+
+Is as simple as with any other Node.js module:
+
+ $ npm install colour-extractor
+
+NOTE: `colour-extractor` depends on [gm](http://aheckmann.github.com/gm/) module, which in turn depends on [GraphicsMagick](http://www.graphicsmagick.org).
+
+## Usage
+
+`colour-extractor` exports two functions:
+
+ ce = require('colour-extractor')
+ ce.extractColours('Photos/Cats/01.jpg', true, function (colours) {
+ console.log(colours);
+ });
+
+`extractColours` function takes three arguments:
+
+ * path to your photo,
+ * `true` if you'd like the resulting array to be sorted by frequency,
+ `false` if you'd like to get colours sorted as they appear in the photo (top-to-bottom),
+ * a callback function.
+
+Callback function will be passed an `Array` with RGB triplet of each colour and its frequency:
+
+ [
+ [1, [46, 70, 118]],
+ [0.3, [0, 0, 2]],
+ [0.2, [12, 44, 11]]
+ ]
+
+The second function, `colourKey`, returns an array with nine colours, where each one can be mapped to a 3x3 box, ie. super-pixelised representation of the photo.
+
+ ce.colourKey('Photos/Cats/999999.jpg', function (colours) {
+ database.store('colour-keys', photoId, colours);
+ res.send(colours);
+ // render colours to user while they wait for the photo to load.
+ // (or something equally brilliant)
+ });
+
+
+### Utilities
+
+`colour-extractor` exports two more utility functions:
+
+ > cd.rgb2hex(100, 10, 12);
+
+ cd.rgb2hex([44, 44, 44]);
+ cd.hex2rgb('#fff');
+
+## How it works?
+
+That's what I'd like to know as well! Anyhow, `colour-extractor` parses GraphicMagick's histogram, tries to detect similar colours and remove ones which appear less frequently than others.
+
+If you happen to know an actual algorithm that deals with this sort of stuff, don't hesitate to contact me!
+
+## Licence
+
+Copyright (C) 2011 by Josip Lisec <Josip Of JLX.cc>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+ * The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+ * The Software shall not be used in preparation, baking and consumption of chocolate cupcakes.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
@@ -0,0 +1,3 @@
+module.exports = require('./lib/colour-extractor.js');
+
+
@@ -0,0 +1,21 @@
+{
+ "name": "colour-extractor",
+ "description": "Extract colour palettes from images",
+ "version": "0.1.0",
+ "homepage": "http://github.com/josip/node-colour-extractor",
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/josip/node-colour-extractor.git"
+ },
+ "author": "Josip Lisec <josip@jlx.cc> (http://jlx.cc)",
+ "main": "lib/colour-extractor.js",
+ "directories": {
+ "lib": "lib"
+ },
+ "engines": {
+ "node": "0.4.x"
+ },
+ "dependencies": {
+ "gm": ">=0.4.0"
+ }
+}
@@ -0,0 +1,117 @@
+gm = require('gm')
+fs = require('fs')
+temp = require('temp')
+
+MAX_W = 14
+MIFF_START = 'comment={'
+MIF_END = '\x0A}\x0A\x0C\x0A'
+
+exports.topColours = (sourceFilename, sorted, cb) ->
+ img = gm(sourceFilename)
+ tmpFilename = temp.path({suffix: '.miff'})
+ img.size((err, wh) ->
+ ratio = wh.width/MAX_W
+ w2 = wh.width/2
+ h2 = wh.height/2
+ img.bitdepth(8) # Initial colour reduction, prob. smarter than our 'algorithm'
+ .crop(w2, h2, w2/2, w2/2) # Center should be the most interesting
+ .scale(Math.ceil(wh.height/ratio), MAX_W) # Scales the image, histogram generation can take some time
+ .write('histogram:' + tmpFilename, (err) ->
+ histogram = ''
+ miffRS = fs.createReadStream(tmpFilename, {encoding: 'utf8'})
+
+ miffRS.addListener('data', (chunk) ->
+ endDelimPos = chunk.indexOf(MIFF_END)
+
+ if endDelimPos != -1
+ histogram += chunk.slice(0, endDelimPos + MIFF_END.length)
+ miffRS.destroy()
+ else
+ histogram += chunk
+ )
+
+ miffRS.addListener('close', ->
+ fs.unlink(tmpFilename)
+
+ colours = reduceSimilar(histogram.slice(histogram.indexOf(MIFF_START) + MIFF_START.length)
+ .split('\n')
+ .slice(1, -3)
+ .map(parseHistogramLine))
+ colours = colours.sort(sortByFrequency) if sorted
+ cb(colours)
+ )
+ )
+ )
+
+exports.colourKey = (path, cb) ->
+ exports.topColours(path, false, (xs) ->
+ M = xs.length
+ m = Math.ceil(M/2)
+
+ cb([
+ xs[0], xs[1], xs[2],
+ xs[m-1], xs[m], xs[m+1],
+ xs[M-3], xs[M-2], xs[M-1]
+ ])
+ )
+
+exports.rgb2hex = (r, g, b) ->
+ rgb = if arguments.length is 1 then r else [r, g, b]
+ '#' + rgb.map((x) -> x.toString(16)).join('')
+
+exports.hex2rgb = (xs) ->
+ xs = xs.slice(1) if xs[0] is '#'
+ [xs.slice(0, 2), xs.slice(2, -2), xs.slice(-2)].map((x) -> parseInt(x, 16))
+
+
+# PRIVATE FUNCTIONS
+include = (x, xs) ->
+ xs.push(x) if xs.indexOf(x) is -1
+ xs
+
+sortByFrequency = ([a, _], [b, _]) ->
+ return -1 if a > b
+ return 1 if a < b
+ return 0
+
+distance = ([r1, g1, b1], [r2, g2, b2]) ->
+ Math.sqrt(Math.pow(r1 - r2, 2) + Math.pow(g1 - g2, 2) + Math.pow(b1 - b2, 2))
+
+###
+Example line:
+ f: (rrr, ggg, bbb) #rrggbb\n
+ \ \ \_____________ Hex code / "black" / "white"
+ \ \______________________________ RGB triplet
+ \_________________________________ Frequency at which colour appears
+###
+parseHistogramLine = (xs) ->
+ xs = xs.trim().split(':')
+ [+xs[0], xs[1].split('(')[1].split(')')[0].split(',').map((x) -> +x.trim())]
+
+# Magic
+reduceSimilar = (xs, r) ->
+ minD = Infinity
+ maxD = 0
+ maxF = 0
+
+ n = 0
+ N = xs.length - 1
+ tds = for x in xs
+ break if n is N
+ d = distance(x[1], xs[++n][1])
+ minD = d if d < minD
+ maxD = d if d > maxD
+ d
+
+ # Geometric mean helps us detecting similar colours appearing at lower frequencies
+ avgD = Math.sqrt(minD * maxD)
+ n = 0
+ rs = []
+ for d in tds
+ if d > avgD
+ include(xs[n], rs)
+ maxF = xs[n][0] if xs[n][0] > maxF
+ n++
+
+ # Normalise values, [0, maxF] => [0, 1]
+ rs.map(([f, c]) -> [f/maxF, c])

0 comments on commit 1be3298

Please sign in to comment.