WIP median cut
This commit is contained in:
parent
e5226f0df1
commit
f437114be0
1 changed files with 73 additions and 14 deletions
|
@ -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
|
||||||
|
@ -653,6 +656,62 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue