RgbQuant.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
  1. /*
  2. * Copyright (c) 2015, Leon Sorokin
  3. * All rights reserved. (MIT Licensed)
  4. *
  5. * RgbQuant.js - an image quantization lib
  6. */
  7. /**
  8. * @namespace RgbQuant
  9. */
  10. (function() {
  11. /**
  12. * @typedef {Object} Palette
  13. * @property {Array} colors - Array of colors, each represented as an array of [R, G, B, A] values.
  14. * @property {Array} aInd - Array of indices mapping original colors to the palette.
  15. */
  16. /**
  17. * @typedef {Object} Options
  18. * @property {number} [colors=256] - Desired number of colors, up to 256.
  19. * @property {string} [method=2] - 1 = NeuQuant (older, faster), 2 = WuQuant (newer, slower, better quality).
  20. * @property {number} [boxSize=[64,64]] - Bounding box size for mapping image pixels to palette.
  21. * @property {number} [boxPxls=2] - Num of pointers stored per box.
  22. * @property {number} [initColors=4096] - Number of colors used for initial palette selection.
  23. * @property {Function} [progress] - Callback function for progress updates.
  24. * @property {Array} [palette] - Pre-defined palette to use.
  25. */
  26. /**
  27. * @constructor
  28. * @alias RgbQuant
  29. * @param {Options} [opts] - Options for quantization.
  30. */
  31. function RgbQuant(opts) {
  32. opts = opts || {};
  33. // desired final palette size
  34. this.colors = opts.colors || 256;
  35. // # of highest-frequency colors to start with for palette reduction
  36. this.initColors = opts.initColors || 4096;
  37. // color-distance threshold for initial reduction pass
  38. this.initDist = opts.initDist || 0.01;
  39. // subsequent passes threshold
  40. this.distIncr = opts.distIncr || 0.005;
  41. // method: 1 = NeuQuant, 2 = WuQuant
  42. this.method = opts.method || 2;
  43. // if > 0, enables handling of alpha transparency
  44. this.hasAlpha = opts.hasAlpha || false;
  45. // how many passes to run in collecting histogram
  46. this.pass = opts.pass === false ? 0 : 1;
  47. // values that define bounding box for pixels used for palette building
  48. // if null, uses full image
  49. this.boxSize = opts.boxSize || [64, 64];
  50. this.boxPxls = opts.boxPxls || 2;
  51. // if provided, use this palette (and skip quant)
  52. this.palette = opts.palette;
  53. this.pruned = this.palette ? true : false;
  54. // final reduced palette
  55. this.pal = null;
  56. // palette index for referring pixels
  57. this.pali = null;
  58. // sorted palette by hue
  59. this.pals = null;
  60. // for WuQuant
  61. this.histogram = {};
  62. this.vboxes = [];
  63. this.progress = opts.progress || function() {};
  64. this.history = {};
  65. this.setDefault(opts);
  66. }
  67. RgbQuant.prototype.setDefault = function(opts) {
  68. this.pass = (opts.pass === false || this.pruned) ? 0 : 1;
  69. this.hasAlpha = opts.hasAlpha || false;
  70. if (this.hasAlpha) {
  71. this.colors = Math.min(this.colors, 255);
  72. }
  73. }
  74. /**
  75. * @param {Image|Array} img - Image element or pixel array.
  76. */
  77. RgbQuant.prototype.sample = function(img) {
  78. if (this.pass) {
  79. if (Array.isArray(img) || typeof img.BYTES_PER_ELEMENT === 'number')
  80. var pixels = img;
  81. else {
  82. var c = document.createElement("canvas");
  83. c.width = img.width;
  84. c.height = img.height;
  85. var ctx = c.getContext("2d");
  86. ctx.drawImage(img, 0, 0);
  87. var pixels = ctx.getImageData(0, 0, img.width, img.height).data;
  88. }
  89. this.collect(pixels);
  90. }
  91. };
  92. RgbQuant.prototype.collect = function(pixels) {
  93. this.vboxes = [];
  94. this.histogram = {};
  95. var vbox = new VBox(0, 0, 0, 0, 0, 0);
  96. vbox.r1 = vbox.g1 = vbox.b1 = 255;
  97. var i = 0;
  98. if (this.boxSize) {
  99. var width = Math.ceil(Math.sqrt(pixels.length / 4));
  100. var height = Math.round(pixels.length / 4 / width);
  101. var bWidth = Math.floor(width / this.boxSize[0]);
  102. var bHeight = Math.floor(height / this.boxSize[1]);
  103. var nPix = this.boxPxls;
  104. var x, y;
  105. for (var j = 0; j < bWidth; j++) {
  106. for (var k = 0; k < bHeight; k++) {
  107. for (var l = 0; l < nPix; l++) {
  108. x = Math.floor(j * this.boxSize[0] + Math.random() * this.boxSize[0]);
  109. y = Math.floor(k * this.boxSize[1] + Math.random() * this.boxSize[1]);
  110. i = (y * width + x) * 4;
  111. this.addPixel(i, pixels, vbox);
  112. }
  113. }
  114. }
  115. }
  116. else {
  117. for (var len = pixels.length; i < len; i += 4)
  118. this.addPixel(i, pixels, vbox);
  119. }
  120. this.vboxes.push(vbox);
  121. };
  122. RgbQuant.prototype.addPixel = function(i, pixels, vbox) {
  123. var r = pixels[i],
  124. g = pixels[i+1],
  125. b = pixels[i+2],
  126. a = pixels[i+3];
  127. vbox.r2 = Math.max(vbox.r2, r);
  128. vbox.g2 = Math.max(vbox.g2, g);
  129. vbox.b2 = Math.max(vbox.b2, b);
  130. vbox.r1 = Math.min(vbox.r1, r);
  131. vbox.g1 = Math.min(vbox.g1, g);
  132. vbox.b1 = Math.min(vbox.b1, b);
  133. if (this.method == 1) {
  134. if (!(r > 250 && g > 250 && b > 250)) {
  135. this.init_add(
  136. ( (r << 8) + g << 8) + b
  137. );
  138. }
  139. }
  140. else {
  141. var ind = getColorIndex(r, g, b);
  142. if (this.histogram[ind])
  143. this.histogram[ind]++;
  144. else
  145. this.histogram[ind] = 1;
  146. }
  147. }
  148. RgbQuant.prototype.medianCut = function(vboxes) {
  149. var vbox, cut;
  150. var i = 0;
  151. while (i < this.colors - 1) {
  152. vbox = vboxes.shift();
  153. cut = vbox.cut(this.histogram);
  154. vboxes.push.apply(vboxes, cut);
  155. i++;
  156. }
  157. }
  158. /**
  159. * @returns {Palette}
  160. */
  161. RgbQuant.prototype.quantize = function() {
  162. // short-circuit
  163. if (this.palette)
  164. this.pal = this.palette.slice(0);
  165. else {
  166. if (this.method == 1) {
  167. this.prune();
  168. this.reduce();
  169. }
  170. // WuQuant
  171. else {
  172. this.medianCut(this.vboxes);
  173. var pal = [];
  174. for (var i = 0; i < this.vboxes.length; i++) {
  175. pal.push(this.vboxes[i].color(this.histogram));
  176. }
  177. this.pal = pal;
  178. }
  179. }
  180. if (this.hasAlpha) {
  181. this.pal.push([0,0,0,0]);
  182. }
  183. return this.pal;
  184. };
  185. RgbQuant.prototype.reduce = function() {
  186. this.prune();
  187. var i = 0;
  188. var len = this.pal.length;
  189. var rd = [], gd = [], bd = [];
  190. while (len > this.colors) {
  191. var min_d = 1e10, min_i;
  192. for (i = 0; i < len; i++) {
  193. for (var j = i + 1; j < len; j++) {
  194. var d = dist(this.pal[i], this.pal[j]);
  195. if (d < min_d) {
  196. min_d = d;
  197. min_i = [i, j];
  198. }
  199. }
  200. }
  201. var p1 = this.pal[min_i[0]];
  202. var p2 = this.pal[min_i[1]];
  203. var n = this.pali[min_i[0]] + this.pali[min_i[1]];
  204. var r = (p1[0] * this.pali[min_i[0]] + p2[0] * this.pali[min_i[1]])/n;
  205. var g = (p1[1] * this.pali[min_i[0]] + p2[1] * this.pali[min_i[1]])/n;
  206. var b = (p1[2] * this.pali[min_i[0]] + p2[2] * this.pali[min_i[1]])/n;
  207. this.pal[min_i[0]] = [r,g,b];
  208. this.pali[min_i[0]] = n;
  209. this.pal.splice(min_i[1], 1);
  210. this.pali.splice(min_i[1], 1);
  211. len--;
  212. }
  213. return this.pal;
  214. };
  215. RgbQuant.prototype.prune = function() {
  216. var dist = this.initDist;
  217. for (var p = 0; p < 5; p++) {
  218. for (i = 0; i < this.pal.length; i++) {
  219. for (var j = i + 1; j < this.pal.length; j++) {
  220. if (dist(this.pal[i], this.pal[j]) < dist) {
  221. this.pal.splice(j, 1);
  222. this.pali.splice(j, 1);
  223. }
  224. }
  225. }
  226. dist += this.distIncr;
  227. }
  228. }
  229. RgbQuant.prototype.init_add = function(color) {
  230. var pali = this.pali,
  231. pal = this.pal;
  232. if (pali[color])
  233. pali[color]++;
  234. else {
  235. pal.push(
  236. [
  237. (color >> 16) & 0xff,
  238. (color >> 8) & 0xff,
  239. (color) & 0xff
  240. ]
  241. );
  242. pali[color] = 1;
  243. }
  244. };
  245. RgbQuant.prototype.init = function(pixels) {
  246. this.pal = [];
  247. this.pali = {};
  248. if (this.boxSize) {
  249. var width = Math.ceil(Math.sqrt(pixels.length / 4));
  250. var height = Math.round(pixels.length / 4 / width);
  251. var bWidth = Math.floor(width / this.boxSize[0]);
  252. var bHeight = Math.floor(height / this.boxSize[1]);
  253. var nPix = this.boxPxls;
  254. var x, y;
  255. for (var j = 0; j < bWidth; j++) {
  256. for (var k = 0; k < bHeight; k++) {
  257. for (var l = 0; l < nPix; l++) {
  258. x = Math.floor(j * this.boxSize[0] + Math.random() * this.boxSize[0]);
  259. y = Math.floor(k * this.boxSize[1] + Math.random() * this.boxSize[1]);
  260. i = (y * width + x) * 4;
  261. var color = ( (pixels[i] << 8) + pixels[i+1] << 8) + pixels[i+2];
  262. this.init_add(color);
  263. }
  264. }
  265. }
  266. }
  267. else {
  268. for (var i = 0, len = pixels.length; i < len; i+=4) {
  269. var color = ( (pixels[i] << 8) + pixels[i+1] << 8) + pixels[i+2];
  270. this.init_add(color);
  271. }
  272. }
  273. var pal = this.pal,
  274. pali = this.pali,
  275. len = pal.length,
  276. newpal = [],
  277. newpali = [];
  278. var hist = [];
  279. for (var i = 0; i < len; i++) {
  280. hist[i] = [pali[ ((pal[i][0] << 8) + pal[i][1] << 8) + pal[i][2] ], pal[i]];
  281. }
  282. hist.sort(function(a,b) { return b[0] - a[0]; });
  283. var newlen = Math.min(len, this.initColors);
  284. this.pal = []; this.pali = [];
  285. for (var i = 0; i < newlen; i++) {
  286. this.pal[i] = hist[i][1];
  287. this.pali[i] = hist[i][0];
  288. }
  289. };
  290. RgbQuant.prototype.map = function(img) {
  291. if (img instanceof Array)
  292. var pixels = img;
  293. else {
  294. var c = document.createElement("canvas");
  295. c.width = img.width;
  296. c.height = img.height;
  297. var ctx = c.getContext("2d");
  298. ctx.drawImage(img, 0, 0);
  299. var pixels = ctx.getImageData(0, 0, img.width, img.height).data;
  300. }
  301. var out = new Uint8Array(pixels.length);
  302. var pal = this.pal;
  303. for (var i = 0, len = pixels.length; i < len; i+=4) {
  304. var pi = this.nearest(
  305. [
  306. pixels[i],
  307. pixels[i+1],
  308. pixels[i+2],
  309. pixels[i+3],
  310. ]
  311. );
  312. out[i] = pal[pi][0];
  313. out[i+1] = pal[pi][1];
  314. out[i+2] = pal[pi][2];
  315. out[i+3] = pal[pi][3];
  316. }
  317. return out;
  318. }
  319. RgbQuant.prototype.nearest = function(color) {
  320. var min_d = 1e10, min_i;
  321. for (var i = 0; i < this.pal.length; i++) {
  322. var d = dist(color, this.pal[i]);
  323. if (d < min_d) {
  324. min_d = d;
  325. min_i = i;
  326. }
  327. }
  328. return min_i;
  329. };
  330. RgbQuant.prototype.getHex = function() {
  331. var hex = [];
  332. for (var i = 0; i < this.pal.length; i++) {
  333. hex.push(
  334. "#" + ("000000" + ( (this.pal[i][0] << 16) | (this.pal[i][1] << 8) | this.pal[i][2] ).toString(16)).slice(-6)
  335. );
  336. }
  337. return hex;
  338. };
  339. // Rec. 709 (sRGB) luma coeff
  340. var Pr = .2126, Pl = .0722, Pg = .7152;
  341. function dist(a, b) {
  342. var dr = a[0]-b[0],
  343. dg = a[1]-b[1],
  344. db = a[2]-b[2];
  345. return Pr * dr * dr + Pg * dg * dg + Pl * db * db;
  346. }
  347. function getColorIndex(r, g, b) {
  348. return (r << 16) + (g << 8) + b;
  349. }
  350. var VBox = (function() {
  351. var dim = 3,
  352. shift = 5,
  353. size = 1 << shift,
  354. mask = size - 1;
  355. function VBox(r1, g1, b1, r2, g2, b2) {
  356. this.r1 = r1; this.g1 = g1; this.b1 = b1;
  357. this.r2 = r2; this.g2 = g2; this.b2 = b2;
  358. this._avg = this._cnt = 0;
  359. }
  360. VBox.prototype.contains = function(r, g, b) {
  361. return r >= this.r1 && r <= this.r2 &&
  362. g >= this.g1 && g <= this.g2 &&
  363. b >= this.b1 && b <= this.b2;
  364. }
  365. VBox.prototype.volume = function() {
  366. return (this.r2 - this.r1) * (this.g2 - this.g1) * (this.b2 - this.b1);
  367. }
  368. VBox.prototype.size = function(hist) {
  369. if (!this._size) {
  370. var npix = 0,
  371. r, g, b, ind;
  372. for (r = this.r1; r <= this.r2; r++) {
  373. for (g = this.g1; g <= this.g2; g++) {
  374. for (b = this.b1; b <= this.b2; b++) {
  375. ind = getColorIndex(r, g, b);
  376. if (hist[ind])
  377. npix += hist[ind];
  378. }
  379. }
  380. }
  381. this._size = npix;
  382. }
  383. return this._size;
  384. }
  385. VBox.prototype.color = function(hist) {
  386. if (!this._avg) {
  387. var mult = 1 << (8 - shift),
  388. sum = [0,0,0],
  389. npix = 0,
  390. r, g, b, ind;
  391. for (r = this.r1; r <= this.r2; r++) {
  392. for (g = this.g1; g <= this.g2; g++) {
  393. for (b = this.b1; b <= this.b2; b++) {
  394. ind = getColorIndex(r, g, b);
  395. if (hist[ind]) {
  396. var h = hist[ind];
  397. npix += h;
  398. sum[0] += h * (r + 0.5) * mult;
  399. sum[1] += h * (g + 0.5) * mult;
  400. sum[2] += h * (b + 0.5) * mult;
  401. }
  402. }
  403. }
  404. }
  405. if (npix) {
  406. this._avg = [
  407. ~~(sum[0] / npix),
  408. ~~(sum[1] / npix),
  409. ~~(sum[2] / npix)
  410. ];
  411. }
  412. else {
  413. // empty box
  414. this._avg = [
  415. ~~(mult * (this.r1 + this.r2 + 1) / 2),
  416. ~~(mult * (this.g1 + this.g2 + 1) / 2),
  417. ~~(mult * (this.b1 + this.b2 + 1) / 2)
  418. ];
  419. }
  420. }
  421. return this._avg;
  422. }
  423. VBox.prototype.cut = function(hist) {
  424. var self = this;
  425. function doCut(dim) {
  426. var d1 = self[dim + '1'],
  427. d2 = self[dim + '2'],
  428. count = 0,
  429. total = 0,
  430. sum = 0,
  431. vbox, cut,
  432. min, max, i, j, k, ind;
  433. var left, right;
  434. if (dim == 'r') { left = 'g'; right = 'b'; }
  435. else if (dim == 'g') { left = 'r'; right = 'b'; }
  436. else { left = 'r'; right = 'g'; }
  437. var partialsum = [], lookaheadsum = [];
  438. for (i = d1; i <= d2; i++) {
  439. sum = 0;
  440. for (j = self[left+'1']; j <= self[left+'2']; j++) {
  441. for (k = self[right+'1']; k <= self[right+'2']; k++) {
  442. ind = getColorIndex(
  443. dim == 'r' ? i : (left == 'r' ? j : k),
  444. dim == 'g' ? i : (left == 'g' ? j : k),
  445. dim == 'b' ? i : (left == 'b' ? j : k)
  446. );
  447. if (hist[ind])
  448. sum += hist[ind];
  449. }
  450. }
  451. total += sum;
  452. partialsum[i] = total;
  453. }
  454. for (i = d1; i <= d2; i++) {
  455. if (partialsum[i] > total / 2) {
  456. vbox = self.clone();
  457. cut = self.clone();
  458. var d2_1 = i - d1, d2_2 = d2 - i;
  459. if (d2_1 >= d2_2)
  460. var d2_ = Math.min(d2 - 1, ~~(i + d2_2 / 2));
  461. else
  462. var d2_ = Math.max(d1 + 1, ~~(i - d2_1 / 2));
  463. while (!partialsum[d2_]) d2_++;
  464. var c = partialsum[d2_ - 1] || 0;
  465. while (c == partialsum[d2_] && d2_ < d2) d2_++;
  466. vbox[dim+'2'] = d2_;
  467. cut[dim+'1'] = d2_ + 1;
  468. return [vbox, cut];
  469. }
  470. }
  471. }
  472. var lr = this.r2 - this.r1,
  473. lg = this.g2 - this.g1,
  474. lb = this.b2 - this.b1;
  475. if (lr >= lg && lr >= lb)
  476. return doCut('r');
  477. if (lg >= lr && lg >= lb)
  478. return doCut('g');
  479. if (lb >= lr && lb >= lg)
  480. return doCut('b');
  481. }
  482. VBox.prototype.clone = function() {
  483. return new VBox(this.r1, this.g1, this.b1, this.r2, this.g2, this.b2);
  484. }
  485. return VBox;
  486. })();
  487. // global functions
  488. this.RgbQuant = RgbQuant;
  489. this.dist = dist;
  490. }).call(this);
  491. //
  492. // NeuQuant
  493. //
  494. // NeuQuant Neural-Net Quantization Algorithm
  495. // ------------------------------------------
  496. //
  497. // Copyright (c) 1994 Anthony Dekker
  498. //
  499. // E-mail: dekker@ACM.org
  500. //
  501. // Web: http://www.scientificcia.com
  502. //
  503. // With my permission you are free to use this code for non-commercial purposes.
  504. // For commercial implementations, please contact me to obtain a licensing agreement.
  505. //
  506. //
  507. // DESCRIPTION:
  508. //
  509. // NeuQuant is a color quantization algorithm that reduces the number of colors
  510. // in a true-color image. It is especially suited for reducing images to 256
  511. // colors and less.
  512. //
  513. // The algorithm is based on a neural network that is trained on the input
  514. // image. The network is a 1D self-organizing map (SOM) that learns the
  515. // distribution of colors in the image. The neurons in the map represent the
  516. // colors in the output palette.
  517. //
  518. // The algorithm proceeds as follows:
  519. //
  520. // 1. The network is initialized with a set of random colors.
  521. // 2. For each pixel in the input image, the neuron that is closest to the
  522. // pixel's color is found.
  523. // 3. This neuron's color is updated to be closer to the pixel's color.
  524. // 4. The neuron's neighbors are also updated, but by a smaller amount.
  525. // 5. The learning rate and neighborhood size are decreased over time.
  526. // 6. After the network has been trained, the neurons' colors form the
  527. // output palette.
  528. //
  529. // 7. The input image is then mapped to the output palette by finding the
  530. // closest color in the palette for each pixel.
  531. //
  532. // This implementation of the algorithm is based on the original C code by
  533. // Anthony Dekker. It has been adapted for JavaScript and modified to
  534. if (typeof(module) !== 'undefined') {
  535. module.exports.RgbQuant = this.RgbQuant;
  536. }