quantize.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. /*
  2. * quantize.js Copyright 2008 Nick Rabinowitz
  3. * Ported to node.js by Olivier Lesnicki
  4. * Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
  5. */
  6. // fill out a couple protovis dependencies
  7. /*
  8. * Block below copied from Protovis: http://mbostock.github.com/protovis/
  9. * Copyright 2010 Stanford Visualization Group
  10. * Licensed under the BSD License: http://www.opensource.org/licenses/bsd-license.php
  11. */
  12. if (!pv) {
  13. var pv = {
  14. map: function(array, f) {
  15. var o = {};
  16. return f ? array.map(function(d, i) {
  17. o.index = i;
  18. return f.call(o, d);
  19. }) : array.slice();
  20. },
  21. naturalOrder: function(a, b) {
  22. return (a < b) ? -1 : ((a > b) ? 1 : 0);
  23. },
  24. sum: function(array, f) {
  25. var o = {};
  26. return array.reduce(f ? function(p, d, i) {
  27. o.index = i;
  28. return p + f.call(o, d);
  29. } : function(p, d) {
  30. return p + d;
  31. }, 0);
  32. },
  33. max: function(array, f) {
  34. return Math.max.apply(null, f ? pv.map(array, f) : array);
  35. }
  36. }
  37. }
  38. /**
  39. * Basic Javascript port of the MMCQ (modified median cut quantization)
  40. * algorithm from the Leptonica library (http://www.leptonica.com/).
  41. * Returns a color map you can use to map original pixels to the reduced
  42. * palette. Still a work in progress.
  43. *
  44. * @author Nick Rabinowitz
  45. * @example
  46. // array of pixels as [R,G,B] arrays
  47. var myPixels = [[190,197,190], [202,204,200], [207,214,210], [211,214,211], [205,207,207]
  48. // etc
  49. ];
  50. var maxColors = 4;
  51. var cmap = MMCQ.quantize(myPixels, maxColors);
  52. var newPalette = cmap.palette();
  53. var newPixels = myPixels.map(function(p) {
  54. return cmap.map(p);
  55. });
  56. */
  57. var MMCQ = (function() {
  58. // private constants
  59. var sigbits = 5,
  60. rshift = 8 - sigbits,
  61. maxIterations = 1000,
  62. fractByPopulations = 0.75;
  63. // get reduced-space color index for a pixel
  64. function getColorIndex(r, g, b) {
  65. return (r << (2 * sigbits)) + (g << sigbits) + b;
  66. }
  67. // Simple priority queue
  68. function PQueue(comparator) {
  69. var contents = [],
  70. sorted = false;
  71. function sort() {
  72. contents.sort(comparator);
  73. sorted = true;
  74. }
  75. return {
  76. push: function(o) {
  77. contents.push(o);
  78. sorted = false;
  79. },
  80. peek: function(index) {
  81. if (!sorted) sort();
  82. if (index === undefined) index = contents.length - 1;
  83. return contents[index];
  84. },
  85. pop: function() {
  86. if (!sorted) sort();
  87. return contents.pop();
  88. },
  89. size: function() {
  90. return contents.length;
  91. },
  92. map: function(f) {
  93. return contents.map(f);
  94. },
  95. debug: function() {
  96. if (!sorted) sort();
  97. return contents;
  98. }
  99. };
  100. }
  101. // 3d color space box
  102. function VBox(r1, r2, g1, g2, b1, b2, histo) {
  103. var vbox = this;
  104. vbox.r1 = r1;
  105. vbox.r2 = r2;
  106. vbox.g1 = g1;
  107. vbox.g2 = g2;
  108. vbox.b1 = b1;
  109. vbox.b2 = b2;
  110. vbox.histo = histo;
  111. }
  112. VBox.prototype = {
  113. volume: function(force) {
  114. var vbox = this;
  115. if (!vbox._volume || force) {
  116. vbox._volume = ((vbox.r2 - vbox.r1 + 1) * (vbox.g2 - vbox.g1 + 1) * (vbox.b2 - vbox.b1 + 1));
  117. }
  118. return vbox._volume;
  119. },
  120. count: function(force) {
  121. var vbox = this,
  122. histo = vbox.histo;
  123. if (!vbox._count_set || force) {
  124. var npix = 0,
  125. i, j, k, index;
  126. for (i = vbox.r1; i <= vbox.r2; i++) {
  127. for (j = vbox.g1; j <= vbox.g2; j++) {
  128. for (k = vbox.b1; k <= vbox.b2; k++) {
  129. index = getColorIndex(i, j, k);
  130. npix += (histo[index] || 0);
  131. }
  132. }
  133. }
  134. vbox._count = npix;
  135. vbox._count_set = true;
  136. }
  137. return vbox._count;
  138. },
  139. copy: function() {
  140. var vbox = this;
  141. return new VBox(vbox.r1, vbox.r2, vbox.g1, vbox.g2, vbox.b1, vbox.b2, vbox.histo);
  142. },
  143. avg: function(force) {
  144. var vbox = this,
  145. histo = vbox.histo;
  146. if (!vbox._avg || force) {
  147. var ntot = 0,
  148. mult = 1 << (8 - sigbits),
  149. rsum = 0,
  150. gsum = 0,
  151. bsum = 0,
  152. hval,
  153. i, j, k, histoindex;
  154. for (i = vbox.r1; i <= vbox.r2; i++) {
  155. for (j = vbox.g1; j <= vbox.g2; j++) {
  156. for (k = vbox.b1; k <= vbox.b2; k++) {
  157. histoindex = getColorIndex(i, j, k);
  158. hval = histo[histoindex] || 0;
  159. ntot += hval;
  160. rsum += (hval * (i + 0.5) * mult);
  161. gsum += (hval * (j + 0.5) * mult);
  162. bsum += (hval * (k + 0.5) * mult);
  163. }
  164. }
  165. }
  166. if (ntot) {
  167. vbox._avg = [~~(rsum / ntot), ~~ (gsum / ntot), ~~ (bsum / ntot)];
  168. } else {
  169. //console.log('empty box');
  170. vbox._avg = [~~(mult * (vbox.r1 + vbox.r2 + 1) / 2), ~~ (mult * (vbox.g1 + vbox.g2 + 1) / 2), ~~ (mult * (vbox.b1 + vbox.b2 + 1) / 2)];
  171. }
  172. }
  173. return vbox._avg;
  174. },
  175. contains: function(pixel) {
  176. var vbox = this,
  177. rval = pixel[0] >> rshift;
  178. gval = pixel[1] >> rshift;
  179. bval = pixel[2] >> rshift;
  180. return (rval >= vbox.r1 && rval <= vbox.r2 &&
  181. gval >= vbox.g1 && gval <= vbox.g2 &&
  182. bval >= vbox.b1 && bval <= vbox.b2);
  183. }
  184. };
  185. // Color map
  186. function CMap() {
  187. this.vboxes = new PQueue(function(a, b) {
  188. return pv.naturalOrder(
  189. a.vbox.count() * a.vbox.volume(),
  190. b.vbox.count() * b.vbox.volume()
  191. )
  192. });;
  193. }
  194. CMap.prototype = {
  195. push: function(vbox) {
  196. this.vboxes.push({
  197. vbox: vbox,
  198. color: vbox.avg()
  199. });
  200. },
  201. palette: function() {
  202. return this.vboxes.map(function(vb) {
  203. return vb.color
  204. });
  205. },
  206. size: function() {
  207. return this.vboxes.size();
  208. },
  209. map: function(color) {
  210. var vboxes = this.vboxes;
  211. for (var i = 0; i < vboxes.size(); i++) {
  212. if (vboxes.peek(i).vbox.contains(color)) {
  213. return vboxes.peek(i).color;
  214. }
  215. }
  216. return this.nearest(color);
  217. },
  218. nearest: function(color) {
  219. var vboxes = this.vboxes,
  220. d1, d2, pColor;
  221. for (var i = 0; i < vboxes.size(); i++) {
  222. d2 = Math.sqrt(
  223. Math.pow(color[0] - vboxes.peek(i).color[0], 2) +
  224. Math.pow(color[1] - vboxes.peek(i).color[1], 2) +
  225. Math.pow(color[2] - vboxes.peek(i).color[2], 2)
  226. );
  227. if (d2 < d1 || d1 === undefined) {
  228. d1 = d2;
  229. pColor = vboxes.peek(i).color;
  230. }
  231. }
  232. return pColor;
  233. },
  234. forcebw: function() {
  235. // XXX: won't work yet
  236. var vboxes = this.vboxes;
  237. vboxes.sort(function(a, b) {
  238. return pv.naturalOrder(pv.sum(a.color), pv.sum(b.color))
  239. });
  240. // force darkest color to black if everything < 5
  241. var lowest = vboxes[0].color;
  242. if (lowest[0] < 5 && lowest[1] < 5 && lowest[2] < 5)
  243. vboxes[0].color = [0, 0, 0];
  244. // force lightest color to white if everything > 251
  245. var idx = vboxes.length - 1,
  246. highest = vboxes[idx].color;
  247. if (highest[0] > 251 && highest[1] > 251 && highest[2] > 251)
  248. vboxes[idx].color = [255, 255, 255];
  249. }
  250. };
  251. // histo (1-d array, giving the number of pixels in
  252. // each quantized region of color space), or null on error
  253. function getHisto(pixels) {
  254. var histosize = 1 << (3 * sigbits),
  255. histo = new Array(histosize),
  256. index, rval, gval, bval;
  257. pixels.forEach(function(pixel) {
  258. rval = pixel[0] >> rshift;
  259. gval = pixel[1] >> rshift;
  260. bval = pixel[2] >> rshift;
  261. index = getColorIndex(rval, gval, bval);
  262. histo[index] = (histo[index] || 0) + 1;
  263. });
  264. return histo;
  265. }
  266. function vboxFromPixels(pixels, histo) {
  267. var rmin = 1000000,
  268. rmax = 0,
  269. gmin = 1000000,
  270. gmax = 0,
  271. bmin = 1000000,
  272. bmax = 0,
  273. rval, gval, bval;
  274. // find min/max
  275. pixels.forEach(function(pixel) {
  276. rval = pixel[0] >> rshift;
  277. gval = pixel[1] >> rshift;
  278. bval = pixel[2] >> rshift;
  279. if (rval < rmin) rmin = rval;
  280. else if (rval > rmax) rmax = rval;
  281. if (gval < gmin) gmin = gval;
  282. else if (gval > gmax) gmax = gval;
  283. if (bval < bmin) bmin = bval;
  284. else if (bval > bmax) bmax = bval;
  285. });
  286. return new VBox(rmin, rmax, gmin, gmax, bmin, bmax, histo);
  287. }
  288. function medianCutApply(histo, vbox) {
  289. if (!vbox.count()) return;
  290. var rw = vbox.r2 - vbox.r1 + 1,
  291. gw = vbox.g2 - vbox.g1 + 1,
  292. bw = vbox.b2 - vbox.b1 + 1,
  293. maxw = pv.max([rw, gw, bw]);
  294. // only one pixel, no split
  295. if (vbox.count() == 1) {
  296. return [vbox.copy()]
  297. }
  298. /* Find the partial sum arrays along the selected axis. */
  299. var total = 0,
  300. partialsum = [],
  301. lookaheadsum = [],
  302. i, j, k, sum, index;
  303. if (maxw == rw) {
  304. for (i = vbox.r1; i <= vbox.r2; i++) {
  305. sum = 0;
  306. for (j = vbox.g1; j <= vbox.g2; j++) {
  307. for (k = vbox.b1; k <= vbox.b2; k++) {
  308. index = getColorIndex(i, j, k);
  309. sum += (histo[index] || 0);
  310. }
  311. }
  312. total += sum;
  313. partialsum[i] = total;
  314. }
  315. } else if (maxw == gw) {
  316. for (i = vbox.g1; i <= vbox.g2; i++) {
  317. sum = 0;
  318. for (j = vbox.r1; j <= vbox.r2; j++) {
  319. for (k = vbox.b1; k <= vbox.b2; k++) {
  320. index = getColorIndex(j, i, k);
  321. sum += (histo[index] || 0);
  322. }
  323. }
  324. total += sum;
  325. partialsum[i] = total;
  326. }
  327. } else { /* maxw == bw */
  328. for (i = vbox.b1; i <= vbox.b2; i++) {
  329. sum = 0;
  330. for (j = vbox.r1; j <= vbox.r2; j++) {
  331. for (k = vbox.g1; k <= vbox.g2; k++) {
  332. index = getColorIndex(j, k, i);
  333. sum += (histo[index] || 0);
  334. }
  335. }
  336. total += sum;
  337. partialsum[i] = total;
  338. }
  339. }
  340. partialsum.forEach(function(d, i) {
  341. lookaheadsum[i] = total - d
  342. });
  343. function doCut(color) {
  344. var dim1 = color + '1',
  345. dim2 = color + '2',
  346. left, right, vbox1, vbox2, d2, count2 = 0;
  347. for (i = vbox[dim1]; i <= vbox[dim2]; i++) {
  348. if (partialsum[i] > total / 2) {
  349. vbox1 = vbox.copy();
  350. vbox2 = vbox.copy();
  351. left = i - vbox[dim1];
  352. right = vbox[dim2] - i;
  353. if (left <= right)
  354. d2 = Math.min(vbox[dim2] - 1, ~~ (i + right / 2));
  355. else d2 = Math.max(vbox[dim1], ~~ (i - 1 - left / 2));
  356. // avoid 0-count boxes
  357. while (!partialsum[d2]) d2++;
  358. count2 = lookaheadsum[d2];
  359. while (!count2 && partialsum[d2 - 1]) count2 = lookaheadsum[--d2];
  360. // set dimensions
  361. vbox1[dim2] = d2;
  362. vbox2[dim1] = vbox1[dim2] + 1;
  363. // console.log('vbox counts:', vbox.count(), vbox1.count(), vbox2.count());
  364. return [vbox1, vbox2];
  365. }
  366. }
  367. }
  368. // determine the cut planes
  369. return maxw == rw ? doCut('r') :
  370. maxw == gw ? doCut('g') :
  371. doCut('b');
  372. }
  373. function quantize(pixels, maxcolors) {
  374. // short-circuit
  375. if (!pixels.length || maxcolors < 2 || maxcolors > 256) {
  376. // console.log('wrong number of maxcolors');
  377. return false;
  378. }
  379. // XXX: check color content and convert to grayscale if insufficient
  380. var histo = getHisto(pixels),
  381. histosize = 1 << (3 * sigbits);
  382. // check that we aren't below maxcolors already
  383. var nColors = 0;
  384. histo.forEach(function() {
  385. nColors++
  386. });
  387. if (nColors <= maxcolors) {
  388. // XXX: generate the new colors from the histo and return
  389. }
  390. // get the beginning vbox from the colors
  391. var vbox = vboxFromPixels(pixels, histo),
  392. pq = new PQueue(function(a, b) {
  393. return pv.naturalOrder(a.count(), b.count())
  394. });
  395. pq.push(vbox);
  396. // inner function to do the iteration
  397. function iter(lh, target) {
  398. var ncolors = 1,
  399. niters = 0,
  400. vbox;
  401. while (niters < maxIterations) {
  402. vbox = lh.pop();
  403. if (!vbox.count()) { /* just put it back */
  404. lh.push(vbox);
  405. niters++;
  406. continue;
  407. }
  408. // do the cut
  409. var vboxes = medianCutApply(histo, vbox),
  410. vbox1 = vboxes[0],
  411. vbox2 = vboxes[1];
  412. if (!vbox1) {
  413. // console.log("vbox1 not defined; shouldn't happen!");
  414. return;
  415. }
  416. lh.push(vbox1);
  417. if (vbox2) { /* vbox2 can be null */
  418. lh.push(vbox2);
  419. ncolors++;
  420. }
  421. if (ncolors >= target) return;
  422. if (niters++ > maxIterations) {
  423. // console.log("infinite loop; perhaps too few pixels!");
  424. return;
  425. }
  426. }
  427. }
  428. // first set of colors, sorted by population
  429. iter(pq, fractByPopulations * maxcolors);
  430. // console.log(pq.size(), pq.debug().length, pq.debug().slice());
  431. // Re-sort by the product of pixel occupancy times the size in color space.
  432. var pq2 = new PQueue(function(a, b) {
  433. return pv.naturalOrder(a.count() * a.volume(), b.count() * b.volume())
  434. });
  435. while (pq.size()) {
  436. pq2.push(pq.pop());
  437. }
  438. // next set - generate the median cuts using the (npix * vol) sorting.
  439. iter(pq2, maxcolors - pq2.size());
  440. // calculate the actual colors
  441. var cmap = new CMap();
  442. while (pq2.size()) {
  443. cmap.push(pq2.pop());
  444. }
  445. return cmap;
  446. }
  447. return {
  448. quantize: quantize
  449. }
  450. })();
  451. module.exports = MMCQ.quantize