diff options
| author | Yuval Adam <yuval@y3xz.com> | 2015-06-13 15:46:40 +0300 |
|---|---|---|
| committer | Yuval Adam <yuval@y3xz.com> | 2015-06-13 15:46:40 +0300 |
| commit | 21eb0e3b59966a469cd2b9f09722642bf1b8c661 (patch) | |
| tree | e969cc5368e16febfdca79a3c8ef139522662fd8 /client | |
| parent | 9ae25440f70d97ce33dc2b139b381a098f761349 (diff) | |
Switch to rgbquant
Diffstat (limited to 'client')
| -rw-r--r-- | client/button.js | 7 | ||||
| -rw-r--r-- | client/effects.js | 78 | ||||
| -rw-r--r-- | client/quantize.js | 471 | ||||
| -rw-r--r-- | client/rgbquant.js | 935 | ||||
| -rw-r--r-- | client/templates/photos.html | 1 |
5 files changed, 1002 insertions, 490 deletions
diff --git a/client/button.js b/client/button.js index 2fabf31..786091d 100644 --- a/client/button.js +++ b/client/button.js @@ -47,6 +47,13 @@ Template.photos.events({ Effects.pixelate(x.id); $('#canvas-'+x.id).show() }) + }, + 'click button.color': function (event) { + _.each(Session.get('photos'), function(x) { + $('#img-'+x.id).hide() + Effects.colorize(x.id); + $('#canvas-'+x.id).show() + }) } }) diff --git a/client/effects.js b/client/effects.js index 3743b9e..9e095da 100644 --- a/client/effects.js +++ b/client/effects.js @@ -14,11 +14,12 @@ Effects = (function() { for (var y = 0; y < raster.height; y++) { for(var x = 0; x < raster.width; x++) { var color = raster.getPixel(x, y); - colors.push([ + colors.push( Math.round(256 * color.red), Math.round(256 * color.green), - Math.round(256 * color.blue) - ]); + Math.round(256 * color.blue), + 1 // alpha + ); var rec = new paper.Path.Rectangle({ point: [x * gridSize, y * gridSize], size: [gridSize, gridSize], @@ -29,33 +30,72 @@ Effects = (function() { } paper.view.draw(); - return; - var quant = MMCQ.quantize(colors, 2); - var palette = quant.palette(); + // quantize now + var quant = new RgbQuant({ + colors: 2, + minHueCols: 2 + }); + quant.sample(colors); + var palette = quant.palette(true); + var newcolors = quant.reduce(colors); + + var newcolors2 = []; + for (var x=0; x < gridDim * gridDim; x++) { + newcolors2.push([newcolors[(x*4)], newcolors[(x*4)+1], newcolors[(x*4)+2]]); + // skip dummy alpha value (x*4)+3 + } + // two primary colors var pc1 = new paper.Color(palette[0][0] / 256, palette[0][1] / 256, palette[0][2] / 256); var pc2 = new paper.Color(palette[1][0] / 256, palette[1][1] / 256, palette[1][2] / 256); - var c1 = new paper.Path.Rectangle({ - point: [0, gridDim * gridSize], - size: [gridSize * gridDim / 2, 50], - strokeColor: pc1, - fillColor: pc1 - }); + // save for later + Session.set(id + ':pc1', pc1); + Session.set(id + ':pc2', pc2); + Session.set(id + ':colors', colors); + Session.set(id + ':newcolors', newcolors2); + console.log(newcolors2); + } - var c2 = new paper.Path.Rectangle({ - point: [gridSize * gridDim / 2, gridDim * gridSize], - size: [gridSize * gridDim / 2, 50], - strokeColor: pc2, - fillColor: pc2 - }); + function colorize(id) { + var canvas = document.getElementById('canvas-' + id); + paper.setup(canvas); + + // params + var gridSize = 6; + var gridDim = 9; + + // fetch the new colors + var pc1 = new paper.Color(Session.get(id + ':pc1')); + var pc2 = new paper.Color(Session.get(id + ':pc2')); + var colors = Session.get(id + ':newcolors'); + + console.log(colors); + + // sample each color, diff and color new pixels + for (var y = 0; y < gridDim; y++) { + for(var x = 0; x < gridDim; x++) { + var c = colors[(y * gridDim) + x]; + var nc = new paper.Color(c[0] / 256, c[1] / 256, c[2] / 256); + + // console.log(x * gridSize, y * gridSize, nc); + + var rec = new paper.Path.Rectangle({ + point: [x * gridSize, y * gridSize], + size: [gridSize, gridSize], + strokeColor: nc, + fillColor: nc + }); + } + } paper.view.draw(); } return { - pixelate: pixelate + pixelate: pixelate, + colorize: colorize } })(); diff --git a/client/quantize.js b/client/quantize.js deleted file mode 100644 index 5a99945..0000000 --- a/client/quantize.js +++ /dev/null @@ -1,471 +0,0 @@ -/*! - * quantize.js Copyright 2008 Nick Rabinowitz. - * Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php - */ - -// fill out a couple protovis dependencies -/*! - * Block below copied from Protovis: http://mbostock.github.com/protovis/ - * Copyright 2010 Stanford Visualization Group - * Licensed under the BSD License: http://www.opensource.org/licenses/bsd-license.php - */ -if (!pv) { - var pv = { - map: function(array, f) { - var o = {}; - return f - ? array.map(function(d, i) { o.index = i; return f.call(o, d); }) - : array.slice(); - }, - naturalOrder: function(a, b) { - return (a < b) ? -1 : ((a > b) ? 1 : 0); - }, - sum: function(array, f) { - var o = {}; - return array.reduce(f - ? function(p, d, i) { o.index = i; return p + f.call(o, d); } - : function(p, d) { return p + d; }, 0); - }, - max: function(array, f) { - return Math.max.apply(null, f ? pv.map(array, f) : array); - } - } -} - -/** - * Basic Javascript port of the MMCQ (modified median cut quantization) - * algorithm from the Leptonica library (http://www.leptonica.com/). - * Returns a color map you can use to map original pixels to the reduced - * palette. Still a work in progress. - * - * @author Nick Rabinowitz - * @example - -// array of pixels as [R,G,B] arrays -var myPixels = [[190,197,190], [202,204,200], [207,214,210], [211,214,211], [205,207,207] - // etc - ]; -var maxColors = 4; - -var cmap = MMCQ.quantize(myPixels, maxColors); -var newPalette = cmap.palette(); -var newPixels = myPixels.map(function(p) { - return cmap.map(p); -}); - - */ -MMCQ = (function() { - // private constants - var sigbits = 5, - rshift = 8 - sigbits, - maxIterations = 1000, - fractByPopulations = 0.75; - - // get reduced-space color index for a pixel - function getColorIndex(r, g, b) { - return (r << (2 * sigbits)) + (g << sigbits) + b; - } - - // Simple priority queue - function PQueue(comparator) { - var contents = [], - sorted = false; - - function sort() { - contents.sort(comparator); - sorted = true; - } - - return { - push: function(o) { - contents.push(o); - sorted = false; - }, - peek: function(index) { - if (!sorted) sort(); - if (index===undefined) index = contents.length - 1; - return contents[index]; - }, - pop: function() { - if (!sorted) sort(); - return contents.pop(); - }, - size: function() { - return contents.length; - }, - map: function(f) { - return contents.map(f); - }, - debug: function() { - if (!sorted) sort(); - return contents; - } - }; - } - - // 3d color space box - function VBox(r1, r2, g1, g2, b1, b2, histo) { - var vbox = this; - vbox.r1 = r1; - vbox.r2 = r2; - vbox.g1 = g1; - vbox.g2 = g2; - vbox.b1 = b1; - vbox.b2 = b2; - vbox.histo = histo; - } - VBox.prototype = { - volume: function(force) { - var vbox = this; - if (!vbox._volume || force) { - vbox._volume = ((vbox.r2 - vbox.r1 + 1) * (vbox.g2 - vbox.g1 + 1) * (vbox.b2 - vbox.b1 + 1)); - } - return vbox._volume; - }, - count: function(force) { - var vbox = this, - histo = vbox.histo; - if (!vbox._count_set || force) { - var npix = 0, - i, j, k; - for (i = vbox.r1; i <= vbox.r2; i++) { - for (j = vbox.g1; j <= vbox.g2; j++) { - for (k = vbox.b1; k <= vbox.b2; k++) { - index = getColorIndex(i,j,k); - npix += (histo[index] || 0); - } - } - } - vbox._count = npix; - vbox._count_set = true; - } - return vbox._count; - }, - copy: function() { - var vbox = this; - return new VBox(vbox.r1, vbox.r2, vbox.g1, vbox.g2, vbox.b1, vbox.b2, vbox.histo); - }, - avg: function(force) { - var vbox = this, - histo = vbox.histo; - if (!vbox._avg || force) { - var ntot = 0, - mult = 1 << (8 - sigbits), - rsum = 0, - gsum = 0, - bsum = 0, - hval, - i, j, k, histoindex; - for (i = vbox.r1; i <= vbox.r2; i++) { - for (j = vbox.g1; j <= vbox.g2; j++) { - for (k = vbox.b1; k <= vbox.b2; k++) { - histoindex = getColorIndex(i,j,k); - hval = histo[histoindex] || 0; - ntot += hval; - rsum += (hval * (i + 0.5) * mult); - gsum += (hval * (j + 0.5) * mult); - bsum += (hval * (k + 0.5) * mult); - } - } - } - if (ntot) { - vbox._avg = [~~(rsum/ntot), ~~(gsum/ntot), ~~(bsum/ntot)]; - } else { - console.log('empty box'); - vbox._avg = [ - ~~(mult * (vbox.r1 + vbox.r2 + 1) / 2), - ~~(mult * (vbox.g1 + vbox.g2 + 1) / 2), - ~~(mult * (vbox.b1 + vbox.b2 + 1) / 2) - ]; - } - } - return vbox._avg; - }, - contains: function(pixel) { - var vbox = this, - rval = pixel[0] >> rshift; - gval = pixel[1] >> rshift; - bval = pixel[2] >> rshift; - return (rval >= vbox.r1 && rval <= vbox.r2 && - gval >= vbox.g1 && rval <= vbox.g2 && - bval >= vbox.b1 && rval <= vbox.b2); - } - }; - - // Color map - function CMap() { - this.vboxes = new PQueue(function(a,b) { - return pv.naturalOrder( - a.vbox.count()*a.vbox.volume(), - b.vbox.count()*b.vbox.volume() - ) - });; - } - CMap.prototype = { - push: function(vbox) { - this.vboxes.push({ - vbox: vbox, - color: vbox.avg() - }); - }, - palette: function() { - return this.vboxes.map(function(vb) { return vb.color }); - }, - size: function() { - return this.vboxes.size(); - }, - map: function(color) { - var vboxes = this.vboxes; - for (var i=0; i<vboxes.size(); i++) { - if (vboxes.peek(i).vbox.contains(color)) { - return vboxes.peek(i).color; - } - } - return this.nearest(color); - }, - nearest: function(color) { - var vboxes = this.vboxes, - d1, d2, pColor; - for (var i=0; i<vboxes.size(); i++) { - d2 = Math.sqrt( - Math.pow(color[0] - vboxes.peek(i).color[0], 2) + - Math.pow(color[1] - vboxes.peek(i).color[1], 2) + - Math.pow(color[1] - vboxes.peek(i).color[1], 2) - ); - if (d2 < d1 || d1 === undefined) { - d1 = d2; - pColor = vboxes.peek(i).color; - } - } - return pColor; - }, - forcebw: function() { - // XXX: won't work yet - var vboxes = this.vboxes; - vboxes.sort(function(a,b) { return pv.naturalOrder(pv.sum(a.color), pv.sum(b.color) )}); - - // force darkest color to black if everything < 5 - var lowest = vboxes[0].color; - if (lowest[0] < 5 && lowest[1] < 5 && lowest[2] < 5) - vboxes[0].color = [0,0,0]; - - // force lightest color to white if everything > 251 - var idx = vboxes.length-1, - highest = vboxes[idx].color; - if (highest[0] > 251 && highest[1] > 251 && highest[2] > 251) - vboxes[idx].color = [255,255,255]; - } - }; - - // histo (1-d array, giving the number of pixels in - // each quantized region of color space), or null on error - function getHisto(pixels) { - var histosize = 1 << (3 * sigbits), - histo = new Array(histosize), - index, rval, gval, bval; - pixels.forEach(function(pixel) { - rval = pixel[0] >> rshift; - gval = pixel[1] >> rshift; - bval = pixel[2] >> rshift; - index = getColorIndex(rval, gval, bval); - histo[index] = (histo[index] || 0) + 1; - }); - return histo; - } - - function vboxFromPixels(pixels, histo) { - var rmin=1000000, rmax=0, - gmin=1000000, gmax=0, - bmin=1000000, bmax=0, - rval, gval, bval; - // find min/max - pixels.forEach(function(pixel) { - rval = pixel[0] >> rshift; - gval = pixel[1] >> rshift; - bval = pixel[2] >> rshift; - if (rval < rmin) rmin = rval; - else if (rval > rmax) rmax = rval; - if (gval < gmin) gmin = gval; - else if (gval > gmax) gmax = gval; - if (bval < bmin) bmin = bval; - else if (bval > bmax) bmax = bval; - }); - return new VBox(rmin, rmax, gmin, gmax, bmin, bmax, histo); - } - - function medianCutApply(histo, vbox) { - if (!vbox.count()) return; - - var rw = vbox.r2 - vbox.r1 + 1, - gw = vbox.g2 - vbox.g1 + 1, - bw = vbox.b2 - vbox.b1 + 1, - maxw = pv.max([rw, gw, bw]); - // only one pixel, no split - if (vbox.count() == 1) { - return [vbox.copy()] - } - /* Find the partial sum arrays along the selected axis. */ - var total = 0, - partialsum = [], - lookaheadsum = [], - i, j, k, sum, index; - if (maxw == rw) { - for (i = vbox.r1; i <= vbox.r2; i++) { - sum = 0; - for (j = vbox.g1; j <= vbox.g2; j++) { - for (k = vbox.b1; k <= vbox.b2; k++) { - index = getColorIndex(i,j,k); - sum += (histo[index] || 0); - } - } - total += sum; - partialsum[i] = total; - } - } - else if (maxw == gw) { - for (i = vbox.g1; i <= vbox.g2; i++) { - sum = 0; - for (j = vbox.r1; j <= vbox.r2; j++) { - for (k = vbox.b1; k <= vbox.b2; k++) { - index = getColorIndex(j,i,k); - sum += (histo[index] || 0); - } - } - total += sum; - partialsum[i] = total; - } - } - else { /* maxw == bw */ - for (i = vbox.b1; i <= vbox.b2; i++) { - sum = 0; - for (j = vbox.r1; j <= vbox.r2; j++) { - for (k = vbox.g1; k <= vbox.g2; k++) { - index = getColorIndex(j,k,i); - sum += (histo[index] || 0); - } - } - total += sum; - partialsum[i] = total; - } - } - partialsum.forEach(function(d,i) { - lookaheadsum[i] = total-d - }); - function doCut(color) { - var dim1 = color + '1', - dim2 = color + '2', - left, right, vbox1, vbox2, d2, count2=0; - for (i = vbox[dim1]; i <= vbox[dim2]; i++) { - if (partialsum[i] > total / 2) { - vbox1 = vbox.copy(); - vbox2 = vbox.copy(); - left = i - vbox[dim1]; - right = vbox[dim2] - i; - if (left <= right) - d2 = Math.min(vbox[dim2] - 1, ~~(i + right / 2)); - else d2 = Math.max(vbox[dim1], ~~(i - 1 - left / 2)); - // avoid 0-count boxes - while (!partialsum[d2]) d2++; - count2 = lookaheadsum[d2]; - while (!count2 && partialsum[d2-1]) count2 = lookaheadsum[--d2]; - // set dimensions - vbox1[dim2] = d2; - vbox2[dim1] = vbox1[dim2] + 1; - console.log('vbox counts:', vbox.count(), vbox1.count(), vbox2.count()); - return [vbox1, vbox2]; - } - } - - } - // determine the cut planes - return maxw == rw ? doCut('r') : - maxw == gw ? doCut('g') : - doCut('b'); - } - - function quantize(pixels, maxcolors) { - // short-circuit - if (!pixels.length || maxcolors < 2 || maxcolors > 256) { - console.log('wrong number of maxcolors'); - return false; - } - - // XXX: check color content and convert to grayscale if insufficient - - var histo = getHisto(pixels), - histosize = 1 << (3 * sigbits); - - // check that we aren't below maxcolors already - var nColors = 0; - histo.forEach(function() { nColors++ }); - if (nColors <= maxcolors) { - // XXX: generate the new colors from the histo and return - } - - // get the beginning vbox from the colors - var vbox = vboxFromPixels(pixels, histo), - pq = new PQueue(function(a,b) { return pv.naturalOrder(a.count(), b.count()) }); - pq.push(vbox); - - // inner function to do the iteration - function iter(lh, target) { - var ncolors = 1, - niters = 0, - vbox; - while (niters < maxIterations) { - vbox = lh.pop(); - if (!vbox.count()) { /* just put it back */ - lh.push(vbox); - niters++; - continue; - } - // do the cut - var vboxes = medianCutApply(histo, vbox), - vbox1 = vboxes[0], - vbox2 = vboxes[1]; - - if (!vbox1) { - console.log("vbox1 not defined; shouldn't happen!"); - return; - } - lh.push(vbox1); - if (vbox2) { /* vbox2 can be null */ - lh.push(vbox2); - ncolors++; - } - if (ncolors >= target) return; - if (niters++ > maxIterations) { - console.log("infinite loop; perhaps too few pixels!"); - return; - } - } - } - - // first set of colors, sorted by population - iter(pq, fractByPopulations * maxcolors); - // console.log(pq.size(), pq.debug().length, pq.debug().slice()); - - // Re-sort by the product of pixel occupancy times the size in color space. - var pq2 = new PQueue(function(a,b) { - return pv.naturalOrder(a.count()*a.volume(), b.count()*b.volume()) - }); - while (pq.size()) { - pq2.push(pq.pop()); - } - - // next set - generate the median cuts using the (npix * vol) sorting. - iter(pq2, maxcolors - pq2.size()); - - // calculate the actual colors - var cmap = new CMap(); - while (pq2.size()) { - cmap.push(pq2.pop()); - } - - return cmap; - } - - return { - quantize: quantize - } -})(); diff --git a/client/rgbquant.js b/client/rgbquant.js new file mode 100644 index 0000000..953e6f4 --- /dev/null +++ b/client/rgbquant.js @@ -0,0 +1,935 @@ +/* +* Copyright (c) 2015, Leon Sorokin +* All rights reserved. (MIT Licensed) +* +* RgbQuant.js - an image quantization lib +*/ + +(function(){ + function RgbQuant(opts) { + opts = opts || {}; + + // 1 = by global population, 2 = subregion population threshold + this.method = opts.method || 2; + // desired final palette size + this.colors = opts.colors || 256; + // # of highest-frequency colors to start with for palette reduction + this.initColors = opts.initColors || 4096; + // color-distance threshold for initial reduction pass + this.initDist = opts.initDist || 0.01; + // subsequent passes threshold + this.distIncr = opts.distIncr || 0.005; + // palette grouping + this.hueGroups = opts.hueGroups || 10; + this.satGroups = opts.satGroups || 10; + this.lumGroups = opts.lumGroups || 10; + // if > 0, enables hues stats and min-color retention per group + this.minHueCols = opts.minHueCols || 0; + // HueStats instance + this.hueStats = this.minHueCols ? new HueStats(this.hueGroups, this.minHueCols) : null; + + // subregion partitioning box size + this.boxSize = opts.boxSize || [64,64]; + // number of same pixels required within box for histogram inclusion + this.boxPxls = opts.boxPxls || 2; + // palette locked indicator + this.palLocked = false; + // palette sort order +// this.sortPal = ['hue-','lum-','sat-']; + + // dithering/error diffusion kernel name + this.dithKern = opts.dithKern || null; + // dither serpentine pattern + this.dithSerp = opts.dithSerp || false; + // minimum color difference (0-1) needed to dither + this.dithDelta = opts.dithDelta || 0; + + // accumulated histogram + this.histogram = {}; + // palette - rgb triplets + this.idxrgb = opts.palette ? opts.palette.slice(0) : []; + // palette - int32 vals + this.idxi32 = []; + // reverse lookup {i32:idx} + this.i32idx = {}; + // {i32:rgb} + this.i32rgb = {}; + // enable color caching (also incurs overhead of cache misses and cache building) + this.useCache = opts.useCache !== false; + // min color occurance count needed to qualify for caching + this.cacheFreq = opts.cacheFreq || 10; + // allows pre-defined palettes to be re-indexed (enabling palette compacting and sorting) + this.reIndex = opts.reIndex || this.idxrgb.length == 0; + // selection of color-distance equation + this.colorDist = opts.colorDist == "manhattan" ? distManhattan : distEuclidean; + + // if pre-defined palette, build lookups + if (this.idxrgb.length > 0) { + var self = this; + this.idxrgb.forEach(function(rgb, i) { + var i32 = ( + (255 << 24) | // alpha + (rgb[2] << 16) | // blue + (rgb[1] << 8) | // green + rgb[0] // red + ) >>> 0; + + self.idxi32[i] = i32; + self.i32idx[i32] = i; + self.i32rgb[i32] = rgb; + }); + } + } + + // gathers histogram info + RgbQuant.prototype.sample = function sample(img, width) { + if (this.palLocked) + throw "Cannot sample additional images, palette already assembled."; + + var data = getImageData(img, width); + + switch (this.method) { + case 1: this.colorStats1D(data.buf32); break; + case 2: this.colorStats2D(data.buf32, data.width); break; + } + }; + + // image quantizer + // todo: memoize colors here also + // @retType: 1 - Uint8Array (default), 2 - Indexed array, 3 - Match @img type (unimplemented, todo) + RgbQuant.prototype.reduce = function reduce(img, retType, dithKern, dithSerp) { + if (!this.palLocked) + this.buildPal(); + + dithKern = dithKern || this.dithKern; + dithSerp = typeof dithSerp != "undefined" ? dithSerp : this.dithSerp; + + retType = retType || 1; + + // reduce w/dither + if (dithKern) + var out32 = this.dither(img, dithKern, dithSerp); + else { + var data = getImageData(img), + buf32 = data.buf32, + len = buf32.length, + out32 = new Uint32Array(len); + + for (var i = 0; i < len; i++) { + var i32 = buf32[i]; + out32[i] = this.nearestColor(i32); + } + } + + if (retType == 1) + return new Uint8Array(out32.buffer); + + if (retType == 2) { + var out = [], + len = out32.length; + + for (var i = 0; i < len; i++) { + var i32 = out32[i]; + out[i] = this.i32idx[i32]; + } + + return out; + } + }; + + // adapted from http://jsbin.com/iXofIji/2/edit by PAEz + RgbQuant.prototype.dither = function(img, kernel, serpentine) { + // http://www.tannerhelland.com/4660/dithering-eleven-algorithms-source-code/ + var kernels = { + FloydSteinberg: [ + [7 / 16, 1, 0], + [3 / 16, -1, 1], + [5 / 16, 0, 1], + [1 / 16, 1, 1] + ], + FalseFloydSteinberg: [ + [3 / 8, 1, 0], + [3 / 8, 0, 1], + [2 / 8, 1, 1] + ], + Stucki: [ + [8 / 42, 1, 0], + [4 / 42, 2, 0], + [2 / 42, -2, 1], + [4 / 42, -1, 1], + [8 / 42, 0, 1], + [4 / 42, 1, 1], + [2 / 42, 2, 1], + [1 / 42, -2, 2], + [2 / 42, -1, 2], + [4 / 42, 0, 2], + [2 / 42, 1, 2], + [1 / 42, 2, 2] + ], + Atkinson: [ + [1 / 8, 1, 0], + [1 / 8, 2, 0], + [1 / 8, -1, 1], + [1 / 8, 0, 1], + [1 / 8, 1, 1], + [1 / 8, 0, 2] + ], + Jarvis: [ // Jarvis, Judice, and Ninke / JJN? + [7 / 48, 1, 0], + [5 / 48, 2, 0], + [3 / 48, -2, 1], + [5 / 48, -1, 1], + [7 / 48, 0, 1], + [5 / 48, 1, 1], + [3 / 48, 2, 1], + [1 / 48, -2, 2], + [3 / 48, -1, 2], + [5 / 48, 0, 2], + [3 / 48, 1, 2], + [1 / 48, 2, 2] + ], + Burkes: [ + [8 / 32, 1, 0], + [4 / 32, 2, 0], + [2 / 32, -2, 1], + [4 / 32, -1, 1], + [8 / 32, 0, 1], + [4 / 32, 1, 1], + [2 / 32, 2, 1], + ], + Sierra: [ + [5 / 32, 1, 0], + [3 / 32, 2, 0], + [2 / 32, -2, 1], + [4 / 32, -1, 1], + [5 / 32, 0, 1], + [4 / 32, 1, 1], + [2 / 32, 2, 1], + [2 / 32, -1, 2], + [3 / 32, 0, 2], + [2 / 32, 1, 2], + ], + TwoSierra: [ + [4 / 16, 1, 0], + [3 / 16, 2, 0], + [1 / 16, -2, 1], + [2 / 16, -1, 1], + [3 / 16, 0, 1], + [2 / 16, 1, 1], + [1 / 16, 2, 1], + ], + SierraLite: [ + [2 / 4, 1, 0], + [1 / 4, -1, 1], + [1 / 4, 0, 1], + ], + }; + + if (!kernel || !kernels[kernel]) { + throw 'Unknown dithering kernel: ' + kernel; + } + + var ds = kernels[kernel]; + + var data = getImageData(img), +// buf8 = data.buf8, + buf32 = data.buf32, + width = data.width, + height = data.height, + len = buf32.length; + + var dir = serpentine ? -1 : 1; + + for (var y = 0; y < height; y++) { + if (serpentine) + dir = dir * -1; + + var lni = y * width; + + for (var x = (dir == 1 ? 0 : width - 1), xend = (dir == 1 ? width : 0); x !== xend; x += dir) { + // Image pixel + var idx = lni + x, + i32 = buf32[idx], + r1 = (i32 & 0xff), + g1 = (i32 & 0xff00) >> 8, + b1 = (i32 & 0xff0000) >> 16; + + // Reduced pixel + var i32x = this.nearestColor(i32), + r2 = (i32x & 0xff), + g2 = (i32x & 0xff00) >> 8, + b2 = (i32x & 0xff0000) >> 16; + + buf32[idx] = + (255 << 24) | // alpha + (b2 << 16) | // blue + (g2 << 8) | // green + r2; + + // dithering strength + if (this.dithDelta) { + var dist = this.colorDist([r1, g1, b1], [r2, g2, b2]); + if (dist < this.dithDelta) + continue; + } + + // Component distance + var er = r1 - r2, + eg = g1 - g2, + eb = b1 - b2; + + for (var i = (dir == 1 ? 0 : ds.length - 1), end = (dir == 1 ? ds.length : 0); i !== end; i += dir) { + var x1 = ds[i][1] * dir, + y1 = ds[i][2]; + + var lni2 = y1 * width; + + if (x1 + x >= 0 && x1 + x < width && y1 + y >= 0 && y1 + y < height) { + var d = ds[i][0]; + var idx2 = idx + (lni2 + x1); + + var r3 = (buf32[idx2] & 0xff), + g3 = (buf32[idx2] & 0xff00) >> 8, + b3 = (buf32[idx2] & 0xff0000) >> 16; + + var r4 = Math.max(0, Math.min(255, r3 + er * d)), + g4 = Math.max(0, Math.min(255, g3 + eg * d)), + b4 = Math.max(0, Math.min(255, b3 + eb * d)); + + buf32[idx2] = + (255 << 24) | // alpha + (b4 << 16) | // blue + (g4 << 8) | // green + r4; // red + } + } + } + } + + return buf32; + }; + + // reduces histogram to palette, remaps & memoizes reduced colors + RgbQuant.prototype.buildPal = function buildPal(noSort) { + if (this.palLocked || this.idxrgb.length > 0 && this.idxrgb.length <= this.colors) return; + + var histG = this.histogram, + sorted = sortedHashKeys(histG, true); + + if (sorted.length == 0) + throw "Nothing has been sampled, palette cannot be built."; + + switch (this.method) { + case 1: + var cols = this.initColors, + last = sorted[cols - 1], + freq = histG[last]; + + var idxi32 = sorted.slice(0, cols); + + // add any cut off colors with same freq as last + var pos = cols, len = sorted.length; + while (pos < len && histG[sorted[pos]] == freq) + idxi32.push(sorted[pos++]); + + // inject min huegroup colors + if (this.hueStats) + this.hueStats.inject(idxi32); + + break; + case 2: + var idxi32 = sorted; + break; + } + + // int32-ify values + idxi32 = idxi32.map(function(v){return +v;}); + + this.reducePal(idxi32); + + if (!noSort && this.reIndex) + this.sortPal(); + + // build cache of top histogram colors + if (this.useCache) + this.cacheHistogram(idxi32); + + this.palLocked = true; + }; + + RgbQuant.prototype.palette = function palette(tuples, noSort) { + this.buildPal(noSort); + return tuples ? this.idxrgb : new Uint8Array((new Uint32Array(this.idxi32)).buffer); + }; + + RgbQuant.prototype.prunePal = function prunePal(keep) { + var i32; + + for (var j = 0; j < this.idxrgb.length; j++) { + if (!keep[j]) { + i32 = this.idxi32[j]; + this.idxrgb[j] = null; + this.idxi32[j] = null; + delete this.i32idx[i32]; + } + } + + // compact + if (this.reIndex) { + var idxrgb = [], + idxi32 = [], + i32idx = {}; + + for (var j = 0, i = 0; j < this.idxrgb.length; j++) { + if (this.idxrgb[j]) { + i32 = this.idxi32[j]; + idxrgb[i] = this.idxrgb[j]; + i32idx[i32] = i; + idxi32[i] = i32; + i++; + } + } + + this.idxrgb = idxrgb; + this.idxi32 = idxi32; + this.i32idx = i32idx; + } + }; + + // reduces similar colors from an importance-sorted Uint32 rgba array + RgbQuant.prototype.reducePal = function reducePal(idxi32) { + // if pre-defined palette's length exceeds target + if (this.idxrgb.length > this.colors) { + // quantize histogram to existing palette + var len = idxi32.length, keep = {}, uniques = 0, idx, pruned = false; + + for (var i = 0; i < len; i++) { + // palette length reached, unset all remaining colors (sparse palette) + if (uniques == this.colors && !pruned) { + this.prunePal(keep); + pruned = true; + } + + idx = this.nearestIndex(idxi32[i]); + + if (uniques < this.colors && !keep[idx]) { + keep[idx] = true; + uniques++; + } + } + + if (!pruned) { + this.prunePal(keep); + pruned = true; + } + } + // reduce histogram to create initial palette + else { + // build full rgb palette + var idxrgb = idxi32.map(function(i32) { + return [ + (i32 & 0xff), + (i32 & 0xff00) >> 8, + (i32 & 0xff0000) >> 16, + ]; + }); + + var len = idxrgb.length, + palLen = len, + thold = this.initDist; + + // palette already at or below desired length + if (palLen > this.colors) { + while (palLen > this.colors) { + var memDist = []; + + // iterate palette + for (var i = 0; i < len; i++) { + var pxi = idxrgb[i], i32i = idxi32[i]; + if (!pxi) continue; + + for (var j = i + 1; j < len; j++) { + var pxj = idxrgb[j], i32j = idxi32[j]; + if (!pxj) continue; + + var dist = this.colorDist(pxi, pxj); + + if (dist < thold) { + // store index,rgb,dist + memDist.push([j, pxj, i32j, dist]); + + // kill squashed value + delete(idxrgb[j]); + palLen--; + } + } + } + + // palette reduction pass + // console.log("palette length: " + palLen); + + // if palette is still much larger than target, increment by larger initDist + thold += (palLen > this.colors * 3) ? this.initDist : this.distIncr; + } + + // if palette is over-reduced, re-add removed colors with largest distances from last round + if (palLen < this.colors) { + // sort descending + sort.call(memDist, function(a,b) { + return b[3] - a[3]; + }); + + var k = 0; + while (palLen < this.colors) { + // re-inject rgb into final palette + idxrgb[memDist[k][0]] = memDist[k][1]; + + palLen++; + k++; + } + } + } + + var len = idxrgb.length; + for (var i = 0; i < len; i++) { + if (!idxrgb[i]) continue; + + this.idxrgb.push(idxrgb[i]); + this.idxi32.push(idxi32[i]); + + this.i32idx[idxi32[i]] = this.idxi32.length - 1; + this.i32rgb[idxi32[i]] = idxrgb[i]; + } + } + }; + + // global top-population + RgbQuant.prototype.colorStats1D = function colorStats1D(buf32) { + var histG = this.histogram, + num = 0, col, + len = buf32.length; + + for (var i = 0; i < len; i++) { + col = buf32[i]; + + // skip transparent + if ((col & 0xff000000) >> 24 == 0) continue; + + // collect hue stats + if (this.hueStats) + this.hueStats.check(col); + + if (col in histG) + histG[col]++; + else + histG[col] = 1; + } + }; + + // population threshold within subregions + // FIXME: this can over-reduce (few/no colors same?), need a way to keep + // important colors that dont ever reach local thresholds (gradients?) + RgbQuant.prototype.colorStats2D = function colorStats2D(buf32, width) { + var boxW = this.boxSize[0], + boxH = this.boxSize[1], + area = boxW * boxH, + boxes = makeBoxes(width, buf32.length / width, boxW, boxH), + histG = this.histogram, + self = this; + + boxes.forEach(function(box) { + var effc = Math.max(Math.round((box.w * box.h) / area) * self.boxPxls, 2), + histL = {}, col; + + iterBox(box, width, function(i) { + col = buf32[i]; + + // skip transparent + if ((col & 0xff000000) >> 24 == 0) return; + + // collect hue stats + if (self.hueStats) + self.hueStats.check(col); + + if (col in histG) + histG[col]++; + else if (col in histL) { + if (++histL[col] >= effc) + histG[col] = histL[col]; + } + else + histL[col] = 1; + }); + }); + + if (this.hueStats) + this.hueStats.inject(histG); + }; + + // TODO: group very low lum and very high lum colors + // TODO: pass custom sort order + RgbQuant.prototype.sortPal = function sortPal() { + var self = this; + + this.idxi32.sort(function(a,b) { + var idxA = self.i32idx[a], + idxB = self.i32idx[b], + rgbA = self.idxrgb[idxA], + rgbB = self.idxrgb[idxB]; + + var hslA = rgb2hsl(rgbA[0],rgbA[1],rgbA[2]), + hslB = rgb2hsl(rgbB[0],rgbB[1],rgbB[2]); + + // sort all grays + whites together + var hueA = (rgbA[0] == rgbA[1] && rgbA[1] == rgbA[2]) ? -1 : hueGroup(hslA.h, self.hueGroups); + var hueB = (rgbB[0] == rgbB[1] && rgbB[1] == rgbB[2]) ? -1 : hueGroup(hslB.h, self.hueGroups); + + var hueDiff = hueB - hueA; + if (hueDiff) return -hueDiff; + + var lumDiff = lumGroup(+hslB.l.toFixed(2)) - lumGroup(+hslA.l.toFixed(2)); + if (lumDiff) return -lumDiff; + + var satDiff = satGroup(+hslB.s.toFixed(2)) - satGroup(+hslA.s.toFixed(2)); + if (satDiff) return -satDiff; + }); + + // sync idxrgb & i32idx + this.idxi32.forEach(function(i32, i) { + self.idxrgb[i] = self.i32rgb[i32]; + self.i32idx[i32] = i; + }); + }; + + // TOTRY: use HUSL - http://boronine.com/husl/ + RgbQuant.prototype.nearestColor = function nearestColor(i32) { + var idx = this.nearestIndex(i32); + return idx === null ? 0 : this.idxi32[idx]; + }; + + // TOTRY: use HUSL - http://boronine.com/husl/ + RgbQuant.prototype.nearestIndex = function nearestIndex(i32) { + // alpha 0 returns null index + if ((i32 & 0xff000000) >> 24 == 0) + return null; + + if (this.useCache && (""+i32) in this.i32idx) + return this.i32idx[i32]; + + var min = 1000, + idx, + rgb = [ + (i32 & 0xff), + (i32 & 0xff00) >> 8, + (i32 & 0xff0000) >> 16, + ], + len = this.idxrgb.length; + + for (var i = 0; i < len; i++) { + if (!this.idxrgb[i]) continue; // sparse palettes + + var dist = this.colorDist(rgb, this.idxrgb[i]); + + if (dist < min) { + min = dist; + idx = i; + } + } + + return idx; + }; + + RgbQuant.prototype.cacheHistogram = function cacheHistogram(idxi32) { + for (var i = 0, i32 = idxi32[i]; i < idxi32.length && this.histogram[i32] >= this.cacheFreq; i32 = idxi32[i++]) + this.i32idx[i32] = this.nearestIndex(i32); + }; + + function HueStats(numGroups, minCols) { + this.numGroups = numGroups; + this.minCols = minCols; + this.stats = {}; + + for (var i = -1; i < numGroups; i++) + this.stats[i] = {num: 0, cols: []}; + + this.groupsFull = 0; + } + + HueStats.prototype.check = function checkHue(i32) { + if (this.groupsFull == this.numGroups + 1) + this.check = function() {return;}; + + var r = (i32 & 0xff), + g = (i32 & 0xff00) >> 8, + b = (i32 & 0xff0000) >> 16, + hg = (r == g && g == b) ? -1 : hueGroup(rgb2hsl(r,g,b).h, this.numGroups), + gr = this.stats[hg], + min = this.minCols; + + gr.num++; + + if (gr.num > min) + return; + if (gr.num == min) + this.groupsFull++; + + if (gr.num <= min) + this.stats[hg].cols.push(i32); + }; + + HueStats.prototype.inject = function injectHues(histG) { + for (var i = -1; i < this.numGroups; i++) { + if (this.stats[i].num <= this.minCols) { + switch (typeOf(histG)) { + case "Array": + this.stats[i].cols.forEach(function(col){ + if (histG.indexOf(col) == -1) + histG.push(col); + }); + break; + case "Object": + this.stats[i].cols.forEach(function(col){ + if (!histG[col]) + histG[col] = 1; + else + histG[col]++; + }); + break; + } + } + } + }; + + // Rec. 709 (sRGB) luma coef + var Pr = .2126, + Pg = .7152, + Pb = .0722; + + // http://alienryderflex.com/hsp.html + function rgb2lum(r,g,b) { + return Math.sqrt( + Pr * r*r + + Pg * g*g + + Pb * b*b + ); + } + + var rd = 255, + gd = 255, + bd = 255; + + var euclMax = Math.sqrt(Pr*rd*rd + Pg*gd*gd + Pb*bd*bd); + // perceptual Euclidean color distance + function distEuclidean(rgb0, rgb1) { + var rd = rgb1[0]-rgb0[0], + gd = rgb1[1]-rgb0[1], + bd = rgb1[2]-rgb0[2]; + + return Math.sqrt(Pr*rd*rd + Pg*gd*gd + Pb*bd*bd) / euclMax; + } + + var manhMax = Pr*rd + Pg*gd + Pb*bd; + // perceptual Manhattan color distance + function distManhattan(rgb0, rgb1) { + var rd = Math.abs(rgb1[0]-rgb0[0]), + gd = Math.abs(rgb1[1]-rgb0[1]), + bd = Math.abs(rgb1[2]-rgb0[2]); + + return (Pr*rd + Pg*gd + Pb*bd) / manhMax; + } + + // http://rgb2hsl.nichabi.com/javascript-function.php + function rgb2hsl(r, g, b) { + var max, min, h, s, l, d; + r /= 255; + g /= 255; + b /= 255; + max = Math.max(r, g, b); + min = Math.min(r, g, b); + l = (max + min) / 2; + if (max == min) { + h = s = 0; + } else { + d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch (max) { + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break + } + h /= 6; + } +// h = Math.floor(h * 360) +// s = Math.floor(s * 100) +// l = Math.floor(l * 100) + return { + h: h, + s: s, + l: rgb2lum(r,g,b), + }; + } + + function hueGroup(hue, segs) { + var seg = 1/segs, + haf = seg/2; + + if (hue >= 1 - haf || hue <= haf) + return 0; + + for (var i = 1; i < segs; i++) { + var mid = i*seg; + if (hue >= mid - haf && hue <= mid + haf) + return i; + } + } + + function satGroup(sat) { + return sat; + } + + function lumGroup(lum) { + return lum; + } + + function typeOf(val) { + return Object.prototype.toString.call(val).slice(8,-1); + } + + var sort = isArrSortStable() ? Array.prototype.sort : stableSort; + + // must be used via stableSort.call(arr, fn) + function stableSort(fn) { + var type = typeOf(this[0]); + + if (type == "Number" || type == "String") { + var ord = {}, len = this.length, val; + + for (var i = 0; i < len; i++) { + val = this[i]; + if (ord[val] || ord[val] === 0) continue; + ord[val] = i; + } + + return this.sort(function(a,b) { + return fn(a,b) || ord[a] - ord[b]; + }); + } + else { + var ord = this.map(function(v){return v}); + + return this.sort(function(a,b) { + return fn(a,b) || ord.indexOf(a) - ord.indexOf(b); + }); + } + } + + // test if js engine's Array#sort implementation is stable + function isArrSortStable() { + var str = "abcdefghijklmnopqrstuvwxyz"; + + return "xyzvwtursopqmnklhijfgdeabc" == str.split("").sort(function(a,b) { + return ~~(str.indexOf(b)/2.3) - ~~(str.indexOf(a)/2.3); + }).join(""); + } + + // returns uniform pixel data from various img + // TODO?: if array is passed, createimagedata, createlement canvas? take a pxlen? + function getImageData(img, width) { + var can, ctx, imgd, buf8, buf32, height; + + switch (typeOf(img)) { + case "HTMLImageElement": + can = document.createElement("canvas"); + can.width = img.naturalWidth; + can.height = img.naturalHeight; + ctx = can.getContext("2d"); + ctx.drawImage(img,0,0); + case "Canvas": + case "HTMLCanvasElement": + can = can || img; + ctx = ctx || can.getContext("2d"); + case "CanvasRenderingContext2D": + ctx = ctx || img; + can = can || ctx.canvas; + imgd = ctx.getImageData(0, 0, can.width, can.height); + case "ImageData": + imgd = imgd || img; + width = imgd.width; + if (typeOf(imgd.data) == "CanvasPixelArray") + buf8 = new Uint8Array(imgd.data); + else + buf8 = imgd.data; + case "Array": + case "CanvasPixelArray": + buf8 = buf8 || new Uint8Array(img); + case "Uint8Array": + case "Uint8ClampedArray": + buf8 = buf8 || img; + buf32 = new Uint32Array(buf8.buffer); + case "Uint32Array": + buf32 = buf32 || img; + buf8 = buf8 || new Uint8Array(buf32.buffer); + width = width || buf32.length; + height = buf32.length / width; + } + + return { + can: can, + ctx: ctx, + imgd: imgd, + buf8: buf8, + buf32: buf32, + width: width, + height: height, + }; + } + + // partitions a rect of wid x hgt into + // array of bboxes of w0 x h0 (or less) + function makeBoxes(wid, hgt, w0, h0) { + var wnum = ~~(wid/w0), wrem = wid%w0, + hnum = ~~(hgt/h0), hrem = hgt%h0, + xend = wid-wrem, yend = hgt-hrem; + + var bxs = []; + for (var y = 0; y < hgt; y += h0) + for (var x = 0; x < wid; x += w0) + bxs.push({x:x, y:y, w:(x==xend?wrem:w0), h:(y==yend?hrem:h0)}); + + return bxs; + } + + // iterates @bbox within a parent rect of width @wid; calls @fn, passing index within parent + function iterBox(bbox, wid, fn) { + var b = bbox, + i0 = b.y * wid + b.x, + i1 = (b.y + b.h - 1) * wid + (b.x + b.w - 1), + cnt = 0, incr = wid - b.w + 1, i = i0; + + do { + fn.call(this, i); + i += (++cnt % b.w == 0) ? incr : 1; + } while (i <= i1); + } + + // returns array of hash keys sorted by their values + function sortedHashKeys(obj, desc) { + var keys = []; + + for (var key in obj) + keys.push(key); + + return sort.call(keys, function(a,b) { + return desc ? obj[b] - obj[a] : obj[a] - obj[b]; + }); + } + + // expose + this.RgbQuant = RgbQuant; + + // expose to commonJS + if (typeof module !== 'undefined' && module.exports) { + module.exports = RgbQuant; + } + +}).call(this); diff --git a/client/templates/photos.html b/client/templates/photos.html index 41873bc..7a8f8c5 100644 --- a/client/templates/photos.html +++ b/client/templates/photos.html @@ -8,6 +8,7 @@ {{/each}} </div> <button class="pixelate btn btn-default">Pixelate</button> + <button class="color btn btn-default">Color</button> </div> </template> |
