UNPKG

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
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