image-in-browser
Version:
Package for encoding / decoding images, transforming images, applying filters, drawing primitives on images on the client side (no need for server Node.js)
423 lines • 15.7 kB
JavaScript
import { ColorRgb8 } from '../color/color-rgb8.js';
import { ColorRgba8 } from '../color/color-rgba8.js';
import { MathUtils } from '../common/math-utils.js';
import { MemoryImage } from './image.js';
import { PaletteUint32 } from './palette-uint32.js';
import { PaletteUint8 } from './palette-uint8.js';
export class NeuralQuantizer {
get palette() {
return this._palette;
}
get numColors() {
return this._netSize;
}
constructor(image, numberOfColors = 256, samplingFactor = 10) {
this._netIndex = new Int32Array(256);
this._netSize = 16;
this._specials = 3;
this._bgColor = 0;
this._cutNetSize = 0;
this._maxNetPos = 0;
this._initRadius = 0;
this._initBiasRadius = 0;
this._samplingFactor = samplingFactor;
this.initialize(numberOfColors);
this.addImage(image);
}
initialize(numberOfColors) {
this._netSize = Math.max(numberOfColors, 4);
this._cutNetSize = this._netSize - this._specials;
this._maxNetPos = this._netSize - 1;
this._initRadius = Math.floor(this._netSize / 8);
this._initBiasRadius = this._initRadius * NeuralQuantizer._radiusBias;
this._paletteInternal = new PaletteUint32(256, 4);
this._palette = new PaletteUint8(256, 3);
this._specials = 3;
this._bgColor = this._specials - 1;
this._radiusPower = new Int32Array(this._netSize >>> 3);
this._network = new Array(this._netSize * 3).fill(0);
this._bias = new Array(this._netSize).fill(0);
this._freq = new Array(this._netSize).fill(0);
this._network[0] = 0.0;
this._network[1] = 0.0;
this._network[2] = 0.0;
this._network[3] = 255.0;
this._network[4] = 255.0;
this._network[5] = 255.0;
const f = 1 / this._netSize;
for (let i = 0; i < this._specials; ++i) {
this._freq[i] = f;
this._bias[i] = 0.0;
}
for (let i = this._specials, p = this._specials * 3; i < this._netSize; ++i) {
this._network[p++] = (255 * (i - this._specials)) / this._cutNetSize;
this._network[p++] = (255 * (i - this._specials)) / this._cutNetSize;
this._network[p++] = (255 * (i - this._specials)) / this._cutNetSize;
this._freq[i] = f;
this._bias[i] = 0.0;
}
}
updateRadiusPower(rad, alpha) {
for (let i = 0; i < rad; i++) {
this._radiusPower[i] = Math.trunc(alpha *
(((rad * rad - i * i) * NeuralQuantizer._radiusBias) / (rad * rad)));
}
}
specialFind(b, g, r) {
for (let i = 0, p = 0; i < this._specials; i++) {
if (this._network[p++] === b &&
this._network[p++] === g &&
this._network[p++] === r) {
return i;
}
}
return -1;
}
contest(b, g, r) {
let bestD = 1.0e30;
let bestBiasDist = bestD;
let bestPos = -1;
let bestBiasPos = bestPos;
for (let i = this._specials, p = this._specials * 3; i < this._netSize; i++) {
let dist = this._network[p++] - b;
if (dist < 0) {
dist = -dist;
}
let a = this._network[p++] - g;
if (a < 0) {
a = -a;
}
dist += a;
a = this._network[p++] - r;
if (a < 0) {
a = -a;
}
dist += a;
if (dist < bestD) {
bestD = dist;
bestPos = i;
}
const biasDist = dist - this._bias[i];
if (biasDist < bestBiasDist) {
bestBiasDist = biasDist;
bestBiasPos = i;
}
this._freq[i] -= NeuralQuantizer._beta * this._freq[i];
this._bias[i] += NeuralQuantizer._betaGamma * this._freq[i];
}
this._freq[bestPos] += NeuralQuantizer._beta;
this._bias[bestPos] -= NeuralQuantizer._betaGamma;
return bestBiasPos;
}
alterSingle(alpha, i, b, g, r) {
const p = i * 3;
this._network[p] -= alpha * (this._network[p] - b);
this._network[p + 1] -= alpha * (this._network[p + 1] - g);
this._network[p + 2] -= alpha * (this._network[p + 2] - r);
}
alterNeighbors(_, rad, i, b, g, r) {
let lo = i - rad;
if (lo < this._specials - 1) {
lo = this._specials - 1;
}
let hi = i + rad;
if (hi > this._netSize) {
hi = this._netSize;
}
let j = i + 1;
let k = i - 1;
let m = 1;
while (j < hi || k > lo) {
const a = this._radiusPower[m++];
if (j < hi) {
const p = j * 3;
this._network[p] -=
(a * (this._network[p] - b)) / NeuralQuantizer.alphaRadiusBias;
this._network[p + 1] -=
(a * (this._network[p + 1] - g)) / NeuralQuantizer.alphaRadiusBias;
this._network[p + 2] -=
(a * (this._network[p + 2] - r)) / NeuralQuantizer.alphaRadiusBias;
j++;
}
if (k > lo) {
const p = k * 3;
this._network[p] -=
(a * (this._network[p] - b)) / NeuralQuantizer.alphaRadiusBias;
this._network[p + 1] -=
(a * (this._network[p + 1] - g)) / NeuralQuantizer.alphaRadiusBias;
this._network[p + 2] -=
(a * (this._network[p + 2] - r)) / NeuralQuantizer.alphaRadiusBias;
k--;
}
}
}
learn(image) {
let biasRadius = this._initBiasRadius;
const alphaDec = 30 + Math.floor((this._samplingFactor - 1) / 3);
const lengthCount = image.width * image.height;
const samplePixels = Math.floor(lengthCount / this._samplingFactor);
let delta = Math.max(Math.floor(samplePixels / NeuralQuantizer._numCycles), 1);
let alpha = NeuralQuantizer._initAlpha;
if (delta === 0) {
delta = 1;
}
let rad = biasRadius >>> NeuralQuantizer._radiusBiasShift;
if (rad <= 1) {
rad = 0;
}
this.updateRadiusPower(rad, alpha);
let step = 0;
let pos = 0;
if (lengthCount < NeuralQuantizer._smallImageBytes) {
this._samplingFactor = 1;
step = 1;
}
else if (lengthCount % NeuralQuantizer._prime1 !== 0) {
step = NeuralQuantizer._prime1;
}
else {
if (lengthCount % NeuralQuantizer._prime2 !== 0) {
step = NeuralQuantizer._prime2;
}
else {
if (lengthCount % NeuralQuantizer._prime3 !== 0) {
step = NeuralQuantizer._prime3;
}
else {
step = NeuralQuantizer._prime4;
}
}
}
const w = image.width;
const h = image.height;
let x = 0;
let y = 0;
let i = 0;
while (i < samplePixels) {
const p = image.getPixel(x, y);
const red = p.r;
const green = p.g;
const blue = p.b;
if (i === 0) {
this._network[this._bgColor * 3] = blue;
this._network[this._bgColor * 3 + 1] = green;
this._network[this._bgColor * 3 + 2] = red;
}
let j = this.specialFind(blue, green, red);
j = j < 0 ? this.contest(blue, green, red) : j;
if (j >= this._specials) {
const a = Number(alpha) / NeuralQuantizer._initAlpha;
this.alterSingle(a, j, blue, green, red);
if (rad > 0) {
this.alterNeighbors(a, rad, j, blue, green, red);
}
}
pos += step;
x += step;
while (x > w) {
x -= w;
y++;
}
while (pos >= lengthCount) {
pos -= lengthCount;
y -= h;
}
i++;
if (i % delta === 0) {
alpha -= Math.floor(alpha / alphaDec);
biasRadius -= Math.floor(biasRadius / NeuralQuantizer._radiusDec);
rad = biasRadius >>> NeuralQuantizer._radiusBiasShift;
if (rad <= 1) {
rad = 0;
}
this.updateRadiusPower(rad, alpha);
}
}
}
fix() {
for (let i = 0, p = 0; i < this._netSize; i++) {
for (let j = 0; j < 3; ++j, ++p) {
const x = MathUtils.clampInt255(Math.trunc(0.5 + this._network[p]));
this._paletteInternal.set(i, j, x);
}
this._paletteInternal.set(i, 3, i);
}
}
inxBuild() {
let previousColor = 0;
let startPos = 0;
for (let i = 0; i < this._netSize; i++) {
let smallPos = i;
let smallVal = this._paletteInternal.get(i, 1);
for (let j = i + 1; j < this._netSize; j++) {
if (this._paletteInternal.get(j, 1) < smallVal) {
smallPos = j;
smallVal = this._paletteInternal.get(j, 1);
}
}
const p = i;
const q = smallPos;
if (i !== smallPos) {
let j = this._paletteInternal.get(q, 0);
this._paletteInternal.set(q, 0, this._paletteInternal.get(p, 0));
this._paletteInternal.set(p, 0, j);
j = this._paletteInternal.get(q, 1);
this._paletteInternal.set(q, 1, this._paletteInternal.get(p, 1));
this._paletteInternal.set(p, 1, j);
j = this._paletteInternal.get(q, 2);
this._paletteInternal.set(q, 2, this._paletteInternal.get(p, 2));
this._paletteInternal.set(p, 2, j);
j = this._paletteInternal.get(q, 3);
this._paletteInternal.set(q, 3, this._paletteInternal.get(p, 3));
this._paletteInternal.set(p, 3, j);
}
if (smallVal !== previousColor) {
this._netIndex[previousColor] = (startPos + i) >>> 1;
for (let j = previousColor + 1; j < smallVal; j++) {
this._netIndex[j] = i;
}
previousColor = Math.trunc(smallVal);
startPos = i;
}
}
this._netIndex[previousColor] = (startPos + this._maxNetPos) >>> 1;
for (let j = previousColor + 1; j < 256; j++) {
this._netIndex[j] = this._maxNetPos;
}
}
copyPalette() {
for (let i = 0; i < this._netSize; ++i) {
this._palette.setRgb(i, Math.abs(this._paletteInternal.get(i, 2)), Math.abs(this._paletteInternal.get(i, 1)), Math.abs(this._paletteInternal.get(i, 0)));
}
}
inxSearch(b, g, r) {
let bestD = 1000;
let best = -1;
let i = this._netIndex[g];
let j = i - 1;
while (i < this._netSize || j >= 0) {
if (i < this._netSize) {
let dist = this._paletteInternal.get(i, 1) - g;
if (dist >= bestD) {
i = this._netSize;
}
else {
if (dist < 0) {
dist = -dist;
}
let a = this._paletteInternal.get(i, 0) - b;
if (a < 0) {
a = -a;
}
dist += a;
if (dist < bestD) {
a = this._paletteInternal.get(i, 2) - r;
if (a < 0) {
a = -a;
}
dist += a;
if (dist < bestD) {
bestD = Math.trunc(dist);
best = i;
}
}
i++;
}
}
if (j >= 0) {
const p = j * 4;
let dist = g - this._paletteInternal.get(j, 1);
if (dist >= bestD) {
j = -1;
}
else {
if (dist < 0) {
dist = -dist;
}
let a = this._paletteInternal.get(j, 0) - b;
if (a < 0) {
a = -a;
}
dist += a;
if (dist < bestD) {
a = this._paletteInternal.get(j, 2) - r;
if (a < 0) {
a = -a;
}
dist += a;
if (dist < bestD) {
bestD = Math.trunc(dist);
best = j;
}
}
j--;
}
}
}
return best;
}
getColorIndex(c) {
const r = Math.trunc(c.r);
const g = Math.trunc(c.g);
const b = Math.trunc(c.b);
return this.inxSearch(b, g, r);
}
getColorIndexRgb(r, g, b) {
return this.inxSearch(b, g, r);
}
getQuantizedColor(c) {
const i = this.getColorIndex(c);
const out = c.length === 4 ? new ColorRgba8(0, 0, 0, 255) : new ColorRgb8(0, 0, 0);
out.r = this.palette.get(i, 0);
out.g = this.palette.get(i, 1);
out.b = this.palette.get(i, 2);
if (c.length === 4) {
out.a = c.a;
}
return out;
}
getIndexImage(image) {
const target = new MemoryImage({
width: image.width,
height: image.height,
numChannels: 1,
palette: this.palette,
});
target.frameIndex = image.frameIndex;
target.frameType = image.frameType;
target.frameDuration = image.frameDuration;
const imageIt = image[Symbol.iterator]();
const targetIt = target[Symbol.iterator]();
let imageItRes = undefined;
let targetItRes = undefined;
while ((((imageItRes = imageIt.next()), (targetItRes = targetIt.next())),
!imageItRes.done && !targetItRes.done)) {
const t = targetItRes.value;
t.setChannel(0, this.getColorIndex(imageItRes.value));
}
return target;
}
addImage(image) {
this.learn(image);
this.fix();
this.inxBuild();
this.copyPalette();
}
}
NeuralQuantizer._numCycles = 100;
NeuralQuantizer._alphaBiasShift = 10;
NeuralQuantizer._initAlpha = 1 << NeuralQuantizer._alphaBiasShift;
NeuralQuantizer._radiusBiasShift = 8;
NeuralQuantizer._radiusBias = 1 << NeuralQuantizer._radiusBiasShift;
NeuralQuantizer._alphaRadiusBiasShift = NeuralQuantizer._alphaBiasShift + NeuralQuantizer._radiusBiasShift;
NeuralQuantizer.alphaRadiusBias = 1 << NeuralQuantizer._alphaRadiusBiasShift;
NeuralQuantizer._radiusDec = 30;
NeuralQuantizer._gamma = 1024;
NeuralQuantizer._beta = 1 / 1024;
NeuralQuantizer._betaGamma = NeuralQuantizer._beta * NeuralQuantizer._gamma;
NeuralQuantizer._prime1 = 499;
NeuralQuantizer._prime2 = 491;
NeuralQuantizer._prime3 = 487;
NeuralQuantizer._prime4 = 503;
NeuralQuantizer._smallImageBytes = 3 * NeuralQuantizer._prime4;
//# sourceMappingURL=neural-quantizer.js.map