From f437114be0b141b242d63db81d3ffef13072cec1 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 20 Mar 2023 19:30:07 -0700 Subject: [PATCH] WIP median cut --- dither-image.js | 87 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 73 insertions(+), 14 deletions(-) diff --git a/dither-image.js b/dither-image.js index 2a9d5f0..e2927dd 100644 --- a/dither-image.js +++ b/dither-image.js @@ -4,6 +4,15 @@ import { import Jimp from 'Jimp'; + +function zeroes(n) { + let arr = []; + for (let i = 0; i < n; i++) { + arr.push(0); + } + return arr; +} + function toLinear(val) { // use a 2.4 gamma approximation // this is BT.1886 compatible @@ -392,7 +401,7 @@ let atariRGB = [ * @param {number} n - target color count * @param {RGB[]} inputError * @param {number} y - * @returns {{output: Uint8Array, palette: number[], error: RGB[]}} + * @returns {{output: number[], palette: number[], error: RGB[]}} */ function decimate(input, palette, n, inputError, y) { // to brute-force, the possible palettes are: @@ -425,7 +434,7 @@ function decimate(input, palette, n, inputError, y) { // Apply dithering with given palette and collect color usage stats let dither = (palette) => { - let fitness = new Float64Array(width); + let fitness = zeroes(width); let error = { cur: [], next: [], @@ -435,8 +444,8 @@ function decimate(input, palette, n, inputError, y) { error.next[i] = new RGB(0, 0, 0); } - let output = new Uint8Array(width); - let popularity = new Int32Array(palette.length); + let output = zeroes(width); + let popularity = zeroes(palette.length); let distance2 = 0; let nextError = new RGB(0, 0, 0); @@ -519,19 +528,11 @@ function decimate(input, palette, n, inputError, y) { } */ - let keepers = new Uint8Array(256); + let keepers = zeroes(256); for (let i of reserved) { keepers[i & 0xfe] = 1; // drop that 0 luminance bit! } - let zeros = (n) => { - let arr = []; - for (let i = 0; i < n; i++) { - arr.push(0); - } - return arr; - }; - // this takes the top hues, and uses the brightest of each hue // needs tuning /* @@ -567,6 +568,7 @@ function decimate(input, palette, n, inputError, y) { // popularity? not really working right // first, dither to the total atari palette + /* while (decimated.length > n) { //console.log(y); @@ -605,6 +607,7 @@ function decimate(input, palette, n, inputError, y) { //decimated = a.slice(0, decimated.length - 1); //console.log(decimated); } + */ //console.log('end', decimated); // old algo @@ -652,7 +655,63 @@ function decimate(input, palette, n, inputError, y) { } } */ - + + // Median cut! + // https://en.wikipedia.org/wiki/Median_cut + //let buckets = [input.slice()]; + let initial = dither(palette); + let buckets = [initial.output.map((i) => atariRGB[palette[i]])]; + let medianCut = (bucket, range) => { + // Sort by the channel with the greatest range, + // then cut the bucket in two at the median. + if (range.g >= range.r && range.g >= range.b) { + bucket.sort((a, b) => b.g - a.g); + } else if (range.r >= range.g && range.r >= range.b) { + bucket.sort((a, b) => b.r - a.r); + } else if (range.b >= range.g && range.b >= range.r) { + bucket.sort((a, b) => b.b - a.b); + } + let half = bucket.length >> 1; + return [bucket.slice(0, half), bucket.slice(half)]; + }; + while (buckets.length < n) { + // Find the bucket with the greatest range in any channel + let ranges = buckets.map((bucket) => { + let red = bucket.map((rgb) => rgb.r); + let green = bucket.map((rgb) => rgb.g); + let blue = bucket.map((rgb) => rgb.b); + return new RGB( + Math.max(...red) - Math.min(...red), + Math.max(...green) - Math.min(...green), + Math.max(...blue) - Math.min(...blue) + ); + }); + let topRanges = ranges.map((rgb) => Math.max(rgb.r, rgb.g, rgb.b)); + let greatest = Math.max(...topRanges); + let index = topRanges.indexOf(greatest); + let [lo, hi] = medianCut(buckets[index], ranges[index]); + buckets.splice(index, 1, lo, hi); + } + decimated = buckets.map((bucket) => { + // Average the RGB colors in this chunk + let rgb = bucket + .reduce((acc, rgb) => acc.inc(rgb), new RGB(0, 0, 0)) + .divide(bucket.length); + + // Take the brightest color in the bucket + //let rgb = bucket[bucket.length - 1]; + + // And map into the Atari palette + let dists = palette.map(( i) => rgb.difference(atariRGB[i]).magnitude()); + let closest = Math.min(...dists); + let index = dists.indexOf(closest); + return palette[index]; + }); + // hack + decimated.sort((a, b) => a - b); + console.log(decimated); + decimated[0] = 0; + // Palette fits return dither(decimated); }