├── LICENSE ├── README.md ├── colorgraph.js └── rgbquant.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jack Qiao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ColorGraph.js 2 | 3 | A color graph is a color palette arranged in an undirected, weighted graph. 4 | see more info on this concept here: http://brandmark.io/color-wheel/discussion/ 5 | 6 | Mondrian painting to ->>> Mondrian color graph 7 | 8 | 9 | colorgraph.js takes an <img> and extracts its color graph. The result is a color palette and an adjacency matrix 10 | 11 | this library is meant for use on browsers, and requires the color quantization library [RgbQuant](https://github.com/leeoniya/RgbQuant.js/) 12 | (can work on node.js as well, but will require the node-canvas library) 13 | 14 | usage: 15 | 16 | 1: quantize an image 17 | ``` 18 | // img: html element 19 | // num: number of colors to quantize into 20 | // use_mode: Boolean, smooth the image using the mode of a 3x3 bucket (removes spurious 1px adjacency noise caused by down-scale sharpening) 21 | 22 | quantized = colorgraph.quantize(img,num,use_mode) 23 | 24 | { 25 | palette: [...], // quantized RGB palette 26 | out: {...}, // quantized datastructure from RgbQuant 27 | canvas: {...}, // quantized canvas for display 28 | sizes: [...] // normalized area of each color in palette 29 | } 30 | 31 | ``` 32 | 33 | 2: get color adjacency matrix (this is the node connectivity matrix that defines the graph) 34 | ``` 35 | // palette: input RGB palette 36 | // canvas: canvas containing the image 37 | // normalize: Boolean, normalize the adjacency matrix with respect to image perimeter size (for comparing the adjacency matrix of different sized images) 38 | 39 | adjacency = colorgraph.get_connectivity_matrix(quantized.palette, quantized.canvas, normalize) 40 | 41 | // row-major adjacency matrix (ith row = ith palette color) 42 | [ 43 | [...] 44 | [...] 45 | [...] 46 | ] 47 | ``` 48 | 49 | 3: ColorWheel API (optional) 50 | feel free to use my colorization API, free for non-commercial projects 51 | ``` 52 | // model: 'dribbble', 'nes', 'soviet', 'anime', 'animation', 'pixelart', 'poster', 'painting' 53 | // invert: if false, returned palette will be sorted by luminance 54 | // num: number of palettes to return 55 | 56 | var data = { 57 | model: 'dribbble', 58 | quantized: { sizes: quantized.sizes, palette: quantized.palette, adjacency: adjacency }, 59 | invert: true, 60 | num: 1 61 | }; 62 | 63 | // 0 <= hue <= 1 64 | if(hue >= 0){ 65 | data.hue = hue; 66 | } 67 | 68 | // 0 <= saturation <= 1 69 | if(saturation >= 0){ 70 | data.saturation = saturation; 71 | } 72 | 73 | $.ajax({ 74 | type: "POST", 75 | url: 'http://brandmark.io/color-wheel/api/', 76 | data: {input: JSON.stringify(data)}, 77 | success: function(results){ 78 | colorgraph.colorize(img, target_canvas, quantized, results[0]); 79 | }, 80 | dataType: 'json' 81 | }); 82 | ``` 83 | 84 | 4: Colorize image 85 | 86 | apply new colors to <canvas> for display purposes 87 | ``` 88 | // img: source 89 | // canvas: to draw to 90 | // quantized: quantized object from quantize() 91 | // palette: new RGB palette to draw 92 | 93 | colorgraph.colorize(img, canvas, quantized, palette); 94 | ``` -------------------------------------------------------------------------------- /colorgraph.js: -------------------------------------------------------------------------------- 1 | /* 2 | * colorgraph.js - Copyright 2018 Jack Qiao 3 | * (MIT License) 4 | */ 5 | 6 | var colorgraph = (function(document, RgbQuant, max_width, max_height) { 7 | function pixel_at(pixels, width, x, y){ 8 | var index = 4*(y*width+x); 9 | return [pixels[index],pixels[index+1],pixels[index+2]]; 10 | } 11 | 12 | function pequal(p1, p2){ 13 | if(p1[0] == p2[0] && p1[1] == p2[1] && p1[2] == p2[2]){ 14 | return true; 15 | } 16 | 17 | return false; 18 | } 19 | 20 | function pindex(palette, p){ 21 | for(var i=0; i max_width && max_width/canvas.width < scale){ 83 | scale = max_width/canvas.width; 84 | } 85 | if(canvas.height > max_height && max_height/canvas.height < scale){ 86 | scale = max_height/canvas.height; 87 | } 88 | 89 | var width = Math.floor(canvas.width*scale); 90 | var height = Math.floor(canvas.height*scale); 91 | var scalingfactor = 2*(width+height); 92 | 93 | for(var i=0; i max_width && max_width/img.naturalWidth < scale){ 108 | scale = max_width/img.naturalWidth; 109 | } 110 | if(img.naturalheight > max_height && max_height/img.naturalHeight < scale){ 111 | scale = max_height/img.naturalHeight; 112 | } 113 | 114 | var width = Math.floor(img.naturalWidth*scale); 115 | var height = Math.floor(img.naturalHeight*scale); 116 | 117 | can.width = width; 118 | can.height = height; 119 | 120 | var ctx = can.getContext('2d'); 121 | ctx.drawImage(img, 0, 0, width, height); 122 | 123 | var opts = { 124 | colors: numcolors, 125 | method: 2, 126 | boxSize: [64,64], 127 | boxPxls: 2, 128 | initColors: 4096, 129 | minHueCols: 256, 130 | dithKern: null, 131 | dithDelta: 0, 132 | dithSerp: false, 133 | palette: [], 134 | reIndex: false, 135 | useCache: true, 136 | cacheFreq: 10, 137 | colorDist: "euclidean" 138 | }; 139 | 140 | var q = new RgbQuant(opts); 141 | q.sample(can); 142 | var pal = q.palette(true, true); 143 | var out = q.reduce(can); 144 | 145 | var can_out = document.createElement('canvas'); 146 | can_out.width = width; 147 | can_out.height = height; 148 | var ctx_out = can_out.getContext('2d'); 149 | var out_data = ctx_out.getImageData(0,0,width,height); 150 | out_data.data.set(out); 151 | ctx_out.putImageData(out_data,0,0); 152 | 153 | // mode filter 154 | if(use_mode){ 155 | filter_mode(can_out); 156 | out_data = ctx_out.getImageData(0,0,width,height); 157 | } 158 | 159 | var imageData = ctx.getImageData(0, 0, width, height); 160 | var pixels = imageData.data; 161 | var pixelCount = pixels.length; 162 | 163 | var pixels_out = out_data.data; 164 | 165 | var sizes = []; // percent of image 166 | for(var i=0; i= 125) { 174 | r = pixels[offset + 0]; 175 | g = pixels[offset + 1]; 176 | b = pixels[offset + 2]; 177 | 178 | r_out = pixels_out[offset + 0]; 179 | g_out = pixels_out[offset + 1]; 180 | b_out = pixels_out[offset + 2]; 181 | 182 | for(var j=0; j= canvas.width){ 215 | continue; 216 | } 217 | if(y+n < 0 || y+n >= canvas.height){ 218 | continue; 219 | } 220 | w.push(pixel_at(pixels,canvas.width,x+m,y+n)); 221 | } 222 | } 223 | 224 | var mv = mode(w); 225 | var i = y*canvas.width + x; 226 | i *= 4; 227 | 228 | newPixels[i] = mv[0]; 229 | newPixels[i+1] = mv[1]; 230 | newPixels[i+2] = mv[2]; 231 | newPixels[i+3] = 255; 232 | } 233 | } 234 | newData.data.set(newPixels); 235 | ctx.putImageData(newData,0,0); 236 | } 237 | 238 | // mode = most frequent color 239 | function mode(arr) { 240 | var numMapping = {}; 241 | var greatestFreq = 0; 242 | var mode; 243 | arr.forEach(function(p) { 244 | var key = Math.round(p[0]+1000*p[1]+1000000*p[2]); 245 | numMapping[key] = (numMapping[key] || 0) + 1; 246 | 247 | if (greatestFreq < numMapping[key]) { 248 | greatestFreq = numMapping[key]; 249 | mode = p; 250 | } 251 | }); 252 | return mode; 253 | } 254 | 255 | function colorize(img, can_out, quant, newpalette){ 256 | var pal = quant.palette; 257 | 258 | var scale = 1; 259 | if(img.naturalWidth > max_width && max_width/img.naturalWidth < scale){ 260 | scale = max_width/img.naturalWidth; 261 | } 262 | if(img.naturalheight > max_height && max_height/img.naturalHeight < scale){ 263 | scale = max_height/img.naturalHeight; 264 | } 265 | 266 | var width = Math.floor(img.naturalWidth*scale); 267 | var height = Math.floor(img.naturalHeight*scale); 268 | 269 | can_out.width = width; 270 | can_out.height = height; 271 | 272 | var ctx_out = can_out.getContext('2d'); 273 | var out_data = ctx_out.getImageData(0,0,width,height); 274 | if(quant.out){ 275 | out_data.data.set(quant.out); 276 | } 277 | else{ 278 | ctx_out.drawImage(img, 0, 0, width, height); 279 | out_data = ctx_out.getImageData(0,0,width,height); 280 | } 281 | ctx_out.putImageData(out_data,0,0); 282 | 283 | var ctx = quant.canvas.getContext('2d'); 284 | var imageData = ctx.getImageData(0, 0, width, height); 285 | var pixels = imageData.data; 286 | var pixelCount = pixels.length; 287 | 288 | var pixels_out = out_data.data; 289 | 290 | for (var i = 0, offset, r, g, b, a; i < pixelCount; i = i + 4) { 291 | offset = i; 292 | var r_out = pixels_out[offset + 0]; 293 | var g_out = pixels_out[offset + 1]; 294 | var b_out = pixels_out[offset + 2]; 295 | 296 | for(var j=0; j 0, enables hues stats and min-color retention per group 27 | this.minHueCols = opts.minHueCols || 0; 28 | // HueStats instance 29 | this.hueStats = this.minHueCols ? new HueStats(this.hueGroups, this.minHueCols) : null; 30 | 31 | // subregion partitioning box size 32 | this.boxSize = opts.boxSize || [64,64]; 33 | // number of same pixels required within box for histogram inclusion 34 | this.boxPxls = opts.boxPxls || 2; 35 | // palette locked indicator 36 | this.palLocked = false; 37 | // palette sort order 38 | // this.sortPal = ['hue-','lum-','sat-']; 39 | 40 | // dithering/error diffusion kernel name 41 | this.dithKern = opts.dithKern || null; 42 | // dither serpentine pattern 43 | this.dithSerp = opts.dithSerp || false; 44 | // minimum color difference (0-1) needed to dither 45 | this.dithDelta = opts.dithDelta || 0; 46 | 47 | // accumulated histogram 48 | this.histogram = {}; 49 | // palette - rgb triplets 50 | this.idxrgb = opts.palette ? opts.palette.slice(0) : []; 51 | // palette - int32 vals 52 | this.idxi32 = []; 53 | // reverse lookup {i32:idx} 54 | this.i32idx = {}; 55 | // {i32:rgb} 56 | this.i32rgb = {}; 57 | // enable color caching (also incurs overhead of cache misses and cache building) 58 | this.useCache = opts.useCache !== false; 59 | // min color occurance count needed to qualify for caching 60 | this.cacheFreq = opts.cacheFreq || 10; 61 | // allows pre-defined palettes to be re-indexed (enabling palette compacting and sorting) 62 | this.reIndex = opts.reIndex || this.idxrgb.length == 0; 63 | // selection of color-distance equation 64 | this.colorDist = opts.colorDist == "manhattan" ? distManhattan : distEuclidean; 65 | 66 | // if pre-defined palette, build lookups 67 | if (this.idxrgb.length > 0) { 68 | var self = this; 69 | this.idxrgb.forEach(function(rgb, i) { 70 | var i32 = ( 71 | (255 << 24) | // alpha 72 | (rgb[2] << 16) | // blue 73 | (rgb[1] << 8) | // green 74 | rgb[0] // red 75 | ) >>> 0; 76 | 77 | self.idxi32[i] = i32; 78 | self.i32idx[i32] = i; 79 | self.i32rgb[i32] = rgb; 80 | }); 81 | } 82 | } 83 | 84 | // gathers histogram info 85 | RgbQuant.prototype.sample = function sample(img, width) { 86 | if (this.palLocked) 87 | throw "Cannot sample additional images, palette already assembled."; 88 | 89 | var data = getImageData(img, width); 90 | 91 | switch (this.method) { 92 | case 1: this.colorStats1D(data.buf32); break; 93 | case 2: this.colorStats2D(data.buf32, data.width); break; 94 | } 95 | }; 96 | 97 | // image quantizer 98 | // todo: memoize colors here also 99 | // @retType: 1 - Uint8Array (default), 2 - Indexed array, 3 - Match @img type (unimplemented, todo) 100 | RgbQuant.prototype.reduce = function reduce(img, retType, dithKern, dithSerp) { 101 | if (!this.palLocked) 102 | this.buildPal(); 103 | 104 | dithKern = dithKern || this.dithKern; 105 | dithSerp = typeof dithSerp != "undefined" ? dithSerp : this.dithSerp; 106 | 107 | retType = retType || 1; 108 | 109 | // reduce w/dither 110 | if (dithKern) 111 | var out32 = this.dither(img, dithKern, dithSerp); 112 | else { 113 | var data = getImageData(img), 114 | buf32 = data.buf32, 115 | len = buf32.length, 116 | out32 = new Uint32Array(len); 117 | 118 | for (var i = 0; i < len; i++) { 119 | var i32 = buf32[i]; 120 | out32[i] = this.nearestColor(i32); 121 | } 122 | } 123 | 124 | if (retType == 1) 125 | return new Uint8Array(out32.buffer); 126 | 127 | if (retType == 2) { 128 | var out = [], 129 | len = out32.length; 130 | 131 | for (var i = 0; i < len; i++) { 132 | var i32 = out32[i]; 133 | out[i] = this.i32idx[i32]; 134 | } 135 | 136 | return out; 137 | } 138 | }; 139 | 140 | // adapted from http://jsbin.com/iXofIji/2/edit by PAEz 141 | RgbQuant.prototype.dither = function(img, kernel, serpentine) { 142 | // http://www.tannerhelland.com/4660/dithering-eleven-algorithms-source-code/ 143 | var kernels = { 144 | FloydSteinberg: [ 145 | [7 / 16, 1, 0], 146 | [3 / 16, -1, 1], 147 | [5 / 16, 0, 1], 148 | [1 / 16, 1, 1] 149 | ], 150 | FalseFloydSteinberg: [ 151 | [3 / 8, 1, 0], 152 | [3 / 8, 0, 1], 153 | [2 / 8, 1, 1] 154 | ], 155 | Stucki: [ 156 | [8 / 42, 1, 0], 157 | [4 / 42, 2, 0], 158 | [2 / 42, -2, 1], 159 | [4 / 42, -1, 1], 160 | [8 / 42, 0, 1], 161 | [4 / 42, 1, 1], 162 | [2 / 42, 2, 1], 163 | [1 / 42, -2, 2], 164 | [2 / 42, -1, 2], 165 | [4 / 42, 0, 2], 166 | [2 / 42, 1, 2], 167 | [1 / 42, 2, 2] 168 | ], 169 | Atkinson: [ 170 | [1 / 8, 1, 0], 171 | [1 / 8, 2, 0], 172 | [1 / 8, -1, 1], 173 | [1 / 8, 0, 1], 174 | [1 / 8, 1, 1], 175 | [1 / 8, 0, 2] 176 | ], 177 | Jarvis: [ // Jarvis, Judice, and Ninke / JJN? 178 | [7 / 48, 1, 0], 179 | [5 / 48, 2, 0], 180 | [3 / 48, -2, 1], 181 | [5 / 48, -1, 1], 182 | [7 / 48, 0, 1], 183 | [5 / 48, 1, 1], 184 | [3 / 48, 2, 1], 185 | [1 / 48, -2, 2], 186 | [3 / 48, -1, 2], 187 | [5 / 48, 0, 2], 188 | [3 / 48, 1, 2], 189 | [1 / 48, 2, 2] 190 | ], 191 | Burkes: [ 192 | [8 / 32, 1, 0], 193 | [4 / 32, 2, 0], 194 | [2 / 32, -2, 1], 195 | [4 / 32, -1, 1], 196 | [8 / 32, 0, 1], 197 | [4 / 32, 1, 1], 198 | [2 / 32, 2, 1], 199 | ], 200 | Sierra: [ 201 | [5 / 32, 1, 0], 202 | [3 / 32, 2, 0], 203 | [2 / 32, -2, 1], 204 | [4 / 32, -1, 1], 205 | [5 / 32, 0, 1], 206 | [4 / 32, 1, 1], 207 | [2 / 32, 2, 1], 208 | [2 / 32, -1, 2], 209 | [3 / 32, 0, 2], 210 | [2 / 32, 1, 2], 211 | ], 212 | TwoSierra: [ 213 | [4 / 16, 1, 0], 214 | [3 / 16, 2, 0], 215 | [1 / 16, -2, 1], 216 | [2 / 16, -1, 1], 217 | [3 / 16, 0, 1], 218 | [2 / 16, 1, 1], 219 | [1 / 16, 2, 1], 220 | ], 221 | SierraLite: [ 222 | [2 / 4, 1, 0], 223 | [1 / 4, -1, 1], 224 | [1 / 4, 0, 1], 225 | ], 226 | }; 227 | 228 | if (!kernel || !kernels[kernel]) { 229 | throw 'Unknown dithering kernel: ' + kernel; 230 | } 231 | 232 | var ds = kernels[kernel]; 233 | 234 | var data = getImageData(img), 235 | // buf8 = data.buf8, 236 | buf32 = data.buf32, 237 | width = data.width, 238 | height = data.height, 239 | len = buf32.length; 240 | 241 | var dir = serpentine ? -1 : 1; 242 | 243 | for (var y = 0; y < height; y++) { 244 | if (serpentine) 245 | dir = dir * -1; 246 | 247 | var lni = y * width; 248 | 249 | for (var x = (dir == 1 ? 0 : width - 1), xend = (dir == 1 ? width : 0); x !== xend; x += dir) { 250 | // Image pixel 251 | var idx = lni + x, 252 | i32 = buf32[idx], 253 | r1 = (i32 & 0xff), 254 | g1 = (i32 & 0xff00) >> 8, 255 | b1 = (i32 & 0xff0000) >> 16; 256 | 257 | // Reduced pixel 258 | var i32x = this.nearestColor(i32), 259 | r2 = (i32x & 0xff), 260 | g2 = (i32x & 0xff00) >> 8, 261 | b2 = (i32x & 0xff0000) >> 16; 262 | 263 | buf32[idx] = 264 | (255 << 24) | // alpha 265 | (b2 << 16) | // blue 266 | (g2 << 8) | // green 267 | r2; 268 | 269 | // dithering strength 270 | if (this.dithDelta) { 271 | var dist = this.colorDist([r1, g1, b1], [r2, g2, b2]); 272 | if (dist < this.dithDelta) 273 | continue; 274 | } 275 | 276 | // Component distance 277 | var er = r1 - r2, 278 | eg = g1 - g2, 279 | eb = b1 - b2; 280 | 281 | for (var i = (dir == 1 ? 0 : ds.length - 1), end = (dir == 1 ? ds.length : 0); i !== end; i += dir) { 282 | var x1 = ds[i][1] * dir, 283 | y1 = ds[i][2]; 284 | 285 | var lni2 = y1 * width; 286 | 287 | if (x1 + x >= 0 && x1 + x < width && y1 + y >= 0 && y1 + y < height) { 288 | var d = ds[i][0]; 289 | var idx2 = idx + (lni2 + x1); 290 | 291 | var r3 = (buf32[idx2] & 0xff), 292 | g3 = (buf32[idx2] & 0xff00) >> 8, 293 | b3 = (buf32[idx2] & 0xff0000) >> 16; 294 | 295 | var r4 = Math.max(0, Math.min(255, r3 + er * d)), 296 | g4 = Math.max(0, Math.min(255, g3 + eg * d)), 297 | b4 = Math.max(0, Math.min(255, b3 + eb * d)); 298 | 299 | buf32[idx2] = 300 | (255 << 24) | // alpha 301 | (b4 << 16) | // blue 302 | (g4 << 8) | // green 303 | r4; // red 304 | } 305 | } 306 | } 307 | } 308 | 309 | return buf32; 310 | }; 311 | 312 | // reduces histogram to palette, remaps & memoizes reduced colors 313 | RgbQuant.prototype.buildPal = function buildPal(noSort) { 314 | if (this.palLocked || this.idxrgb.length > 0 && this.idxrgb.length <= this.colors) return; 315 | 316 | var histG = this.histogram, 317 | sorted = sortedHashKeys(histG, true); 318 | 319 | if (sorted.length == 0) 320 | throw "Nothing has been sampled, palette cannot be built."; 321 | 322 | switch (this.method) { 323 | case 1: 324 | var cols = this.initColors, 325 | last = sorted[cols - 1], 326 | freq = histG[last]; 327 | 328 | var idxi32 = sorted.slice(0, cols); 329 | 330 | // add any cut off colors with same freq as last 331 | var pos = cols, len = sorted.length; 332 | while (pos < len && histG[sorted[pos]] == freq) 333 | idxi32.push(sorted[pos++]); 334 | 335 | // inject min huegroup colors 336 | if (this.hueStats) 337 | this.hueStats.inject(idxi32); 338 | 339 | break; 340 | case 2: 341 | var idxi32 = sorted; 342 | break; 343 | } 344 | 345 | // int32-ify values 346 | idxi32 = idxi32.map(function(v){return +v;}); 347 | 348 | this.reducePal(idxi32); 349 | 350 | if (!noSort && this.reIndex) 351 | this.sortPal(); 352 | 353 | // build cache of top histogram colors 354 | if (this.useCache) 355 | this.cacheHistogram(idxi32); 356 | 357 | this.palLocked = true; 358 | }; 359 | 360 | RgbQuant.prototype.palette = function palette(tuples, noSort) { 361 | this.buildPal(noSort); 362 | return tuples ? this.idxrgb : new Uint8Array((new Uint32Array(this.idxi32)).buffer); 363 | }; 364 | 365 | RgbQuant.prototype.prunePal = function prunePal(keep) { 366 | var i32; 367 | 368 | for (var j = 0; j < this.idxrgb.length; j++) { 369 | if (!keep[j]) { 370 | i32 = this.idxi32[j]; 371 | this.idxrgb[j] = null; 372 | this.idxi32[j] = null; 373 | delete this.i32idx[i32]; 374 | } 375 | } 376 | 377 | // compact 378 | if (this.reIndex) { 379 | var idxrgb = [], 380 | idxi32 = [], 381 | i32idx = {}; 382 | 383 | for (var j = 0, i = 0; j < this.idxrgb.length; j++) { 384 | if (this.idxrgb[j]) { 385 | i32 = this.idxi32[j]; 386 | idxrgb[i] = this.idxrgb[j]; 387 | i32idx[i32] = i; 388 | idxi32[i] = i32; 389 | i++; 390 | } 391 | } 392 | 393 | this.idxrgb = idxrgb; 394 | this.idxi32 = idxi32; 395 | this.i32idx = i32idx; 396 | } 397 | }; 398 | 399 | // reduces similar colors from an importance-sorted Uint32 rgba array 400 | RgbQuant.prototype.reducePal = function reducePal(idxi32) { 401 | // if pre-defined palette's length exceeds target 402 | if (this.idxrgb.length > this.colors) { 403 | // quantize histogram to existing palette 404 | var len = idxi32.length, keep = {}, uniques = 0, idx, pruned = false; 405 | 406 | for (var i = 0; i < len; i++) { 407 | // palette length reached, unset all remaining colors (sparse palette) 408 | if (uniques == this.colors && !pruned) { 409 | this.prunePal(keep); 410 | pruned = true; 411 | } 412 | 413 | idx = this.nearestIndex(idxi32[i]); 414 | 415 | if (uniques < this.colors && !keep[idx]) { 416 | keep[idx] = true; 417 | uniques++; 418 | } 419 | } 420 | 421 | if (!pruned) { 422 | this.prunePal(keep); 423 | pruned = true; 424 | } 425 | } 426 | // reduce histogram to create initial palette 427 | else { 428 | // build full rgb palette 429 | var idxrgb = idxi32.map(function(i32) { 430 | return [ 431 | (i32 & 0xff), 432 | (i32 & 0xff00) >> 8, 433 | (i32 & 0xff0000) >> 16, 434 | ]; 435 | }); 436 | 437 | var len = idxrgb.length, 438 | palLen = len, 439 | thold = this.initDist; 440 | 441 | // palette already at or below desired length 442 | if (palLen > this.colors) { 443 | while (palLen > this.colors) { 444 | var memDist = []; 445 | 446 | // iterate palette 447 | for (var i = 0; i < len; i++) { 448 | var pxi = idxrgb[i], i32i = idxi32[i]; 449 | if (!pxi) continue; 450 | 451 | for (var j = i + 1; j < len; j++) { 452 | var pxj = idxrgb[j], i32j = idxi32[j]; 453 | if (!pxj) continue; 454 | 455 | var dist = this.colorDist(pxi, pxj); 456 | 457 | if (dist < thold) { 458 | // store index,rgb,dist 459 | memDist.push([j, pxj, i32j, dist]); 460 | 461 | // kill squashed value 462 | delete(idxrgb[j]); 463 | palLen--; 464 | } 465 | } 466 | } 467 | 468 | // palette reduction pass 469 | // console.log("palette length: " + palLen); 470 | 471 | // if palette is still much larger than target, increment by larger initDist 472 | thold += (palLen > this.colors * 3) ? this.initDist : this.distIncr; 473 | } 474 | 475 | // if palette is over-reduced, re-add removed colors with largest distances from last round 476 | if (palLen < this.colors) { 477 | // sort descending 478 | sort.call(memDist, function(a,b) { 479 | return b[3] - a[3]; 480 | }); 481 | 482 | var k = 0; 483 | while (palLen < this.colors) { 484 | // re-inject rgb into final palette 485 | idxrgb[memDist[k][0]] = memDist[k][1]; 486 | 487 | palLen++; 488 | k++; 489 | } 490 | } 491 | } 492 | 493 | var len = idxrgb.length; 494 | for (var i = 0; i < len; i++) { 495 | if (!idxrgb[i]) continue; 496 | 497 | this.idxrgb.push(idxrgb[i]); 498 | this.idxi32.push(idxi32[i]); 499 | 500 | this.i32idx[idxi32[i]] = this.idxi32.length - 1; 501 | this.i32rgb[idxi32[i]] = idxrgb[i]; 502 | } 503 | } 504 | }; 505 | 506 | // global top-population 507 | RgbQuant.prototype.colorStats1D = function colorStats1D(buf32) { 508 | var histG = this.histogram, 509 | num = 0, col, 510 | len = buf32.length; 511 | 512 | for (var i = 0; i < len; i++) { 513 | col = buf32[i]; 514 | 515 | // skip transparent 516 | if ((col & 0xff000000) >> 24 == 0) continue; 517 | 518 | // collect hue stats 519 | if (this.hueStats) 520 | this.hueStats.check(col); 521 | 522 | if (col in histG) 523 | histG[col]++; 524 | else 525 | histG[col] = 1; 526 | } 527 | }; 528 | 529 | // population threshold within subregions 530 | // FIXME: this can over-reduce (few/no colors same?), need a way to keep 531 | // important colors that dont ever reach local thresholds (gradients?) 532 | RgbQuant.prototype.colorStats2D = function colorStats2D(buf32, width) { 533 | var boxW = this.boxSize[0], 534 | boxH = this.boxSize[1], 535 | area = boxW * boxH, 536 | boxes = makeBoxes(width, buf32.length / width, boxW, boxH), 537 | histG = this.histogram, 538 | self = this; 539 | 540 | boxes.forEach(function(box) { 541 | var effc = Math.max(Math.round((box.w * box.h) / area) * self.boxPxls, 2), 542 | histL = {}, col; 543 | 544 | iterBox(box, width, function(i) { 545 | col = buf32[i]; 546 | 547 | // skip transparent 548 | if ((col & 0xff000000) >> 24 == 0) return; 549 | 550 | // collect hue stats 551 | if (self.hueStats) 552 | self.hueStats.check(col); 553 | 554 | if (col in histG) 555 | histG[col]++; 556 | else if (col in histL) { 557 | if (++histL[col] >= effc) 558 | histG[col] = histL[col]; 559 | } 560 | else 561 | histL[col] = 1; 562 | }); 563 | }); 564 | 565 | if (this.hueStats) 566 | this.hueStats.inject(histG); 567 | }; 568 | 569 | // TODO: group very low lum and very high lum colors 570 | // TODO: pass custom sort order 571 | RgbQuant.prototype.sortPal = function sortPal() { 572 | var self = this; 573 | 574 | this.idxi32.sort(function(a,b) { 575 | var idxA = self.i32idx[a], 576 | idxB = self.i32idx[b], 577 | rgbA = self.idxrgb[idxA], 578 | rgbB = self.idxrgb[idxB]; 579 | 580 | var hslA = rgb2hsl(rgbA[0],rgbA[1],rgbA[2]), 581 | hslB = rgb2hsl(rgbB[0],rgbB[1],rgbB[2]); 582 | 583 | // sort all grays + whites together 584 | var hueA = (rgbA[0] == rgbA[1] && rgbA[1] == rgbA[2]) ? -1 : hueGroup(hslA.h, self.hueGroups); 585 | var hueB = (rgbB[0] == rgbB[1] && rgbB[1] == rgbB[2]) ? -1 : hueGroup(hslB.h, self.hueGroups); 586 | 587 | var hueDiff = hueB - hueA; 588 | if (hueDiff) return -hueDiff; 589 | 590 | var lumDiff = lumGroup(+hslB.l.toFixed(2)) - lumGroup(+hslA.l.toFixed(2)); 591 | if (lumDiff) return -lumDiff; 592 | 593 | var satDiff = satGroup(+hslB.s.toFixed(2)) - satGroup(+hslA.s.toFixed(2)); 594 | if (satDiff) return -satDiff; 595 | }); 596 | 597 | // sync idxrgb & i32idx 598 | this.idxi32.forEach(function(i32, i) { 599 | self.idxrgb[i] = self.i32rgb[i32]; 600 | self.i32idx[i32] = i; 601 | }); 602 | }; 603 | 604 | // TOTRY: use HUSL - http://boronine.com/husl/ 605 | RgbQuant.prototype.nearestColor = function nearestColor(i32) { 606 | var idx = this.nearestIndex(i32); 607 | return idx === null ? 0 : this.idxi32[idx]; 608 | }; 609 | 610 | // TOTRY: use HUSL - http://boronine.com/husl/ 611 | RgbQuant.prototype.nearestIndex = function nearestIndex(i32) { 612 | // alpha 0 returns null index 613 | if ((i32 & 0xff000000) >> 24 == 0) 614 | return null; 615 | 616 | if (this.useCache && (""+i32) in this.i32idx) 617 | return this.i32idx[i32]; 618 | 619 | var min = 1000, 620 | idx, 621 | rgb = [ 622 | (i32 & 0xff), 623 | (i32 & 0xff00) >> 8, 624 | (i32 & 0xff0000) >> 16, 625 | ], 626 | len = this.idxrgb.length; 627 | 628 | for (var i = 0; i < len; i++) { 629 | if (!this.idxrgb[i]) continue; // sparse palettes 630 | 631 | var dist = this.colorDist(rgb, this.idxrgb[i]); 632 | 633 | if (dist < min) { 634 | min = dist; 635 | idx = i; 636 | } 637 | } 638 | 639 | return idx; 640 | }; 641 | 642 | RgbQuant.prototype.cacheHistogram = function cacheHistogram(idxi32) { 643 | for (var i = 0, i32 = idxi32[i]; i < idxi32.length && this.histogram[i32] >= this.cacheFreq; i32 = idxi32[i++]) 644 | this.i32idx[i32] = this.nearestIndex(i32); 645 | }; 646 | 647 | function HueStats(numGroups, minCols) { 648 | this.numGroups = numGroups; 649 | this.minCols = minCols; 650 | this.stats = {}; 651 | 652 | for (var i = -1; i < numGroups; i++) 653 | this.stats[i] = {num: 0, cols: []}; 654 | 655 | this.groupsFull = 0; 656 | } 657 | 658 | HueStats.prototype.check = function checkHue(i32) { 659 | if (this.groupsFull == this.numGroups + 1) 660 | this.check = function() {return;}; 661 | 662 | var r = (i32 & 0xff), 663 | g = (i32 & 0xff00) >> 8, 664 | b = (i32 & 0xff0000) >> 16, 665 | hg = (r == g && g == b) ? -1 : hueGroup(rgb2hsl(r,g,b).h, this.numGroups), 666 | gr = this.stats[hg], 667 | min = this.minCols; 668 | 669 | gr.num++; 670 | 671 | if (gr.num > min) 672 | return; 673 | if (gr.num == min) 674 | this.groupsFull++; 675 | 676 | if (gr.num <= min) 677 | this.stats[hg].cols.push(i32); 678 | }; 679 | 680 | HueStats.prototype.inject = function injectHues(histG) { 681 | for (var i = -1; i < this.numGroups; i++) { 682 | if (this.stats[i].num <= this.minCols) { 683 | switch (typeOf(histG)) { 684 | case "Array": 685 | this.stats[i].cols.forEach(function(col){ 686 | if (histG.indexOf(col) == -1) 687 | histG.push(col); 688 | }); 689 | break; 690 | case "Object": 691 | this.stats[i].cols.forEach(function(col){ 692 | if (!histG[col]) 693 | histG[col] = 1; 694 | else 695 | histG[col]++; 696 | }); 697 | break; 698 | } 699 | } 700 | } 701 | }; 702 | 703 | // Rec. 709 (sRGB) luma coef 704 | var Pr = .2126, 705 | Pg = .7152, 706 | Pb = .0722; 707 | 708 | // http://alienryderflex.com/hsp.html 709 | function rgb2lum(r,g,b) { 710 | return Math.sqrt( 711 | Pr * r*r + 712 | Pg * g*g + 713 | Pb * b*b 714 | ); 715 | } 716 | 717 | var rd = 255, 718 | gd = 255, 719 | bd = 255; 720 | 721 | var euclMax = Math.sqrt(Pr*rd*rd + Pg*gd*gd + Pb*bd*bd); 722 | // perceptual Euclidean color distance 723 | function distEuclidean(rgb0, rgb1) { 724 | var rd = rgb1[0]-rgb0[0], 725 | gd = rgb1[1]-rgb0[1], 726 | bd = rgb1[2]-rgb0[2]; 727 | 728 | return Math.sqrt(Pr*rd*rd + Pg*gd*gd + Pb*bd*bd) / euclMax; 729 | } 730 | 731 | var manhMax = Pr*rd + Pg*gd + Pb*bd; 732 | // perceptual Manhattan color distance 733 | function distManhattan(rgb0, rgb1) { 734 | var rd = Math.abs(rgb1[0]-rgb0[0]), 735 | gd = Math.abs(rgb1[1]-rgb0[1]), 736 | bd = Math.abs(rgb1[2]-rgb0[2]); 737 | 738 | return (Pr*rd + Pg*gd + Pb*bd) / manhMax; 739 | } 740 | 741 | // http://rgb2hsl.nichabi.com/javascript-function.php 742 | function rgb2hsl(r, g, b) { 743 | var max, min, h, s, l, d; 744 | r /= 255; 745 | g /= 255; 746 | b /= 255; 747 | max = Math.max(r, g, b); 748 | min = Math.min(r, g, b); 749 | l = (max + min) / 2; 750 | if (max == min) { 751 | h = s = 0; 752 | } else { 753 | d = max - min; 754 | s = l > 0.5 ? d / (2 - max - min) : d / (max + min); 755 | switch (max) { 756 | case r: h = (g - b) / d + (g < b ? 6 : 0); break; 757 | case g: h = (b - r) / d + 2; break; 758 | case b: h = (r - g) / d + 4; break 759 | } 760 | h /= 6; 761 | } 762 | // h = Math.floor(h * 360) 763 | // s = Math.floor(s * 100) 764 | // l = Math.floor(l * 100) 765 | return { 766 | h: h, 767 | s: s, 768 | l: rgb2lum(r,g,b), 769 | }; 770 | } 771 | 772 | function hueGroup(hue, segs) { 773 | var seg = 1/segs, 774 | haf = seg/2; 775 | 776 | if (hue >= 1 - haf || hue <= haf) 777 | return 0; 778 | 779 | for (var i = 1; i < segs; i++) { 780 | var mid = i*seg; 781 | if (hue >= mid - haf && hue <= mid + haf) 782 | return i; 783 | } 784 | } 785 | 786 | function satGroup(sat) { 787 | return sat; 788 | } 789 | 790 | function lumGroup(lum) { 791 | return lum; 792 | } 793 | 794 | function typeOf(val) { 795 | return Object.prototype.toString.call(val).slice(8,-1); 796 | } 797 | 798 | var sort = isArrSortStable() ? Array.prototype.sort : stableSort; 799 | 800 | // must be used via stableSort.call(arr, fn) 801 | function stableSort(fn) { 802 | var type = typeOf(this[0]); 803 | 804 | if (type == "Number" || type == "String") { 805 | var ord = {}, len = this.length, val; 806 | 807 | for (var i = 0; i < len; i++) { 808 | val = this[i]; 809 | if (ord[val] || ord[val] === 0) continue; 810 | ord[val] = i; 811 | } 812 | 813 | return this.sort(function(a,b) { 814 | return fn(a,b) || ord[a] - ord[b]; 815 | }); 816 | } 817 | else { 818 | var ord = this.map(function(v){return v}); 819 | 820 | return this.sort(function(a,b) { 821 | return fn(a,b) || ord.indexOf(a) - ord.indexOf(b); 822 | }); 823 | } 824 | } 825 | 826 | // test if js engine's Array#sort implementation is stable 827 | function isArrSortStable() { 828 | var str = "abcdefghijklmnopqrstuvwxyz"; 829 | 830 | return "xyzvwtursopqmnklhijfgdeabc" == str.split("").sort(function(a,b) { 831 | return ~~(str.indexOf(b)/2.3) - ~~(str.indexOf(a)/2.3); 832 | }).join(""); 833 | } 834 | 835 | // returns uniform pixel data from various img 836 | // TODO?: if array is passed, createimagedata, createlement canvas? take a pxlen? 837 | function getImageData(img, width) { 838 | var can, ctx, imgd, buf8, buf32, height; 839 | 840 | switch (typeOf(img)) { 841 | case "HTMLImageElement": 842 | can = document.createElement("canvas"); 843 | can.width = img.naturalWidth; 844 | can.height = img.naturalHeight; 845 | ctx = can.getContext("2d"); 846 | ctx.drawImage(img,0,0); 847 | case "Canvas": 848 | case "HTMLCanvasElement": 849 | can = can || img; 850 | ctx = ctx || can.getContext("2d"); 851 | case "CanvasRenderingContext2D": 852 | ctx = ctx || img; 853 | can = can || ctx.canvas; 854 | imgd = ctx.getImageData(0, 0, can.width, can.height); 855 | case "ImageData": 856 | imgd = imgd || img; 857 | width = imgd.width; 858 | if (typeOf(imgd.data) == "CanvasPixelArray") 859 | buf8 = new Uint8Array(imgd.data); 860 | else 861 | buf8 = imgd.data; 862 | case "Array": 863 | case "CanvasPixelArray": 864 | buf8 = buf8 || new Uint8Array(img); 865 | case "Uint8Array": 866 | case "Uint8ClampedArray": 867 | buf8 = buf8 || img; 868 | buf32 = new Uint32Array(buf8.buffer); 869 | case "Uint32Array": 870 | buf32 = buf32 || img; 871 | buf8 = buf8 || new Uint8Array(buf32.buffer); 872 | width = width || buf32.length; 873 | height = buf32.length / width; 874 | } 875 | 876 | return { 877 | can: can, 878 | ctx: ctx, 879 | imgd: imgd, 880 | buf8: buf8, 881 | buf32: buf32, 882 | width: width, 883 | height: height, 884 | }; 885 | } 886 | 887 | // partitions a rect of wid x hgt into 888 | // array of bboxes of w0 x h0 (or less) 889 | function makeBoxes(wid, hgt, w0, h0) { 890 | var wnum = ~~(wid/w0), wrem = wid%w0, 891 | hnum = ~~(hgt/h0), hrem = hgt%h0, 892 | xend = wid-wrem, yend = hgt-hrem; 893 | 894 | var bxs = []; 895 | for (var y = 0; y < hgt; y += h0) 896 | for (var x = 0; x < wid; x += w0) 897 | bxs.push({x:x, y:y, w:(x==xend?wrem:w0), h:(y==yend?hrem:h0)}); 898 | 899 | return bxs; 900 | } 901 | 902 | // iterates @bbox within a parent rect of width @wid; calls @fn, passing index within parent 903 | function iterBox(bbox, wid, fn) { 904 | var b = bbox, 905 | i0 = b.y * wid + b.x, 906 | i1 = (b.y + b.h - 1) * wid + (b.x + b.w - 1), 907 | cnt = 0, incr = wid - b.w + 1, i = i0; 908 | 909 | do { 910 | fn.call(this, i); 911 | i += (++cnt % b.w == 0) ? incr : 1; 912 | } while (i <= i1); 913 | } 914 | 915 | // returns array of hash keys sorted by their values 916 | function sortedHashKeys(obj, desc) { 917 | var keys = []; 918 | 919 | for (var key in obj) 920 | keys.push(key); 921 | 922 | return sort.call(keys, function(a,b) { 923 | return desc ? obj[b] - obj[a] : obj[a] - obj[b]; 924 | }); 925 | } 926 | 927 | // expose 928 | this.RgbQuant = RgbQuant; 929 | 930 | // expose to commonJS 931 | if (typeof module !== 'undefined' && module.exports) { 932 | module.exports = RgbQuant; 933 | } 934 | 935 | }).call(this); --------------------------------------------------------------------------------