WIP median cut

This commit is contained in:
Brooke Vibber 2023-03-20 19:30:07 -07:00
parent e5226f0df1
commit f437114be0

View file

@ -4,6 +4,15 @@ import {
import Jimp from 'Jimp'; import Jimp from 'Jimp';
function zeroes(n) {
let arr = [];
for (let i = 0; i < n; i++) {
arr.push(0);
}
return arr;
}
function toLinear(val) { function toLinear(val) {
// use a 2.4 gamma approximation // use a 2.4 gamma approximation
// this is BT.1886 compatible // this is BT.1886 compatible
@ -392,7 +401,7 @@ let atariRGB = [
* @param {number} n - target color count * @param {number} n - target color count
* @param {RGB[]} inputError * @param {RGB[]} inputError
* @param {number} y * @param {number} y
* @returns {{output: Uint8Array, palette: number[], error: RGB[]}} * @returns {{output: number[], palette: number[], error: RGB[]}}
*/ */
function decimate(input, palette, n, inputError, y) { function decimate(input, palette, n, inputError, y) {
// to brute-force, the possible palettes are: // 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 // Apply dithering with given palette and collect color usage stats
let dither = (palette) => { let dither = (palette) => {
let fitness = new Float64Array(width); let fitness = zeroes(width);
let error = { let error = {
cur: [], cur: [],
next: [], next: [],
@ -435,8 +444,8 @@ function decimate(input, palette, n, inputError, y) {
error.next[i] = new RGB(0, 0, 0); error.next[i] = new RGB(0, 0, 0);
} }
let output = new Uint8Array(width); let output = zeroes(width);
let popularity = new Int32Array(palette.length); let popularity = zeroes(palette.length);
let distance2 = 0; let distance2 = 0;
let nextError = new RGB(0, 0, 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) { for (let i of reserved) {
keepers[i & 0xfe] = 1; // drop that 0 luminance bit! 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 // this takes the top hues, and uses the brightest of each hue
// needs tuning // needs tuning
/* /*
@ -567,6 +568,7 @@ function decimate(input, palette, n, inputError, y) {
// popularity? not really working right // popularity? not really working right
// first, dither to the total atari palette // first, dither to the total atari palette
/*
while (decimated.length > n) { while (decimated.length > n) {
//console.log(y); //console.log(y);
@ -605,6 +607,7 @@ function decimate(input, palette, n, inputError, y) {
//decimated = a.slice(0, decimated.length - 1); //decimated = a.slice(0, decimated.length - 1);
//console.log(decimated); //console.log(decimated);
} }
*/
//console.log('end', decimated); //console.log('end', decimated);
// old algo // 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 // Palette fits
return dither(decimated); return dither(decimated);
} }