"use strict"; /** * NeuQuant Neural-Net Quantization Algorithm * ------------------------------------------ * * Copyright (c) 1994 Anthony Dekker * * NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994. * See "Kohonen neural networks for optimal colour quantization" * in "Network: Computation in Neural Systems" Vol. 5 (1994) pp 351-367. * for a discussion of the algorithm. * See also http://members.ozemail.com.au/~dekker/NEUQUANT.HTML * * Any party obtaining a copy of these files from the author, directly or * indirectly, is granted, free of charge, a full and unrestricted irrevocable, * world-wide, paid up, royalty-free, nonexclusive right and license to deal * in this software and documentation files (the "Software"), including without * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons who receive * copies from any such party to do so, with the only requirement being * that this copyright notice remain intact. * * (JavaScript port 2012 by Johan Nordberg) * (TypeScript port 2021 by Antonio Román) */ Object.defineProperty(exports, "__esModule", { value: true }); exports.NeuQuant = void 0; /* eslint-disable prefer-destructuring, no-negated-condition */ const learningCycles = 100; // Number of learning cycles. const maximumColorsSize = 256; // Number of colors used. const maximumColorsPosition = maximumColorsSize - 1; // defs for freq and bias const networkBiasShift = 4; // Bias for color values. const integerBiasShift = 16; // Bias for fractions. const integerBias = 1 << integerBiasShift; const gammaShift = 10; const betaShift = 10; const beta = integerBias >> betaShift; // `beta` = 1 / 1024 const betaGamma = integerBias << (gammaShift - betaShift); // Defaults for decreasing radius factor: // -> For 256 colors, radius starts at 32.0 biased by 6 bits and decreases by a factor of 1 / 30 each cycle. const maximumRadius = maximumColorsSize >> 3; const initialRadiusBiasShift = 6; const initialRadiusBias = 1 << initialRadiusBiasShift; const initialRadius = maximumRadius * initialRadiusBias; const initialRadiusDecrement = 30; // Defaults for decreasing alpha factor: // -> Alpha starts at 1.0 const alphaBiasShift = 10; const initialAlpha = 1 << alphaBiasShift; // Constants used for radius power calculation: const radiusBiasShift = 8; const radiusBias = 1 << radiusBiasShift; const alphaRadiusBiasShift = alphaBiasShift + radiusBiasShift; const alphaRadiusBias = 1 << alphaRadiusBiasShift; // Four primes near 500 - assume no image has a length so large that it is divisible by all four primes: const prime1 = 499; const prime2 = 491; const prime3 = 487; const prime4 = 503; const minimumPictureBytes = 3 * prime4; class NeuQuant { /** * Creates the neural quantifier instance. * @param pixels Array of pixels in RGB format, as such that it's decoded as `[r, g, b, r, g, b, r, g, b, ...]`. * @param sampleFactorial Sampling factor from `1` to `30`, where lower is better quality. */ constructor(pixels, sampleFactorial) { /** * Array of pixels in RGB format, as such that it's decoded as `[r, g, b, r, g, b, r, g, b, ...]`. */ Object.defineProperty(this, "pixels", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** * Sampling factor from `1` to `30`, where lower is better quality. */ Object.defineProperty(this, "sampleFactorial", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** * The neural networks, composed by {@link maximumColorsSize} {@link Float64Array float arrays} of size 4. */ Object.defineProperty(this, "networks", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** * Network lookup indexes, composed by 256 indexes. */ Object.defineProperty(this, "networkIndexes", { enumerable: true, configurable: true, writable: true, value: void 0 }); // bias and freq arrays for learning Object.defineProperty(this, "biases", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "frequencies", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "radiusPowers", { enumerable: true, configurable: true, writable: true, value: void 0 }); this.pixels = pixels; this.sampleFactorial = sampleFactorial; this.networks = []; this.networkIndexes = new Int32Array(256); this.biases = new Int32Array(maximumColorsSize); this.frequencies = new Int32Array(maximumColorsSize); this.radiusPowers = new Int32Array(maximumColorsSize >> 3); this.init(); this.learn(); this.unBiasNetwork(); this.buildIndexes(); } /** * Builds the networks' color map. * @returns A RGB-encoded {@link Float64Array}. */ getColorMap() { const map = new Float64Array(maximumColorsSize * 3); const index = new Float64Array(maximumColorsSize); for (let i = 0; i < maximumColorsSize; i++) { index[this.networks[i][3]] = i; } for (let l = 0, k = 0; l < maximumColorsSize; l++) { const network = this.networks[index[l]]; map[k++] = network[0]; map[k++] = network[1]; map[k++] = network[2]; } return map; } /** * Searches for BGR values 0..255 and returns a color index * @param b The blue color byte, between 0 and 255. * @param g The green color byte, between 0 and 255. * @param r The red color byte, between 0 and 255. * @returns The best color index. */ lookupRGB(b, g, r) { // Biggest possible distance is 256 * 3, so we will define the biggest as an out-of-bounds number. let bestDistance = 1000; let best = -1; const index = this.networkIndexes[g]; // Index on `g` for (let i = index; i < maximumColorsSize; ++i) { const network = this.networks[i]; // Compare the distance of the green element, break if it's too big: let distance = network[1] - g; if (distance >= bestDistance) break; // If `distance` is negative, make it positive: if (distance < 0) distance = -distance; // Compare the distance with the blue element added, continue if it's too big: distance += Math.abs(network[0] - b); if (distance >= bestDistance) continue; // Compare the distance with the red element added, continue if it's too big: distance += Math.abs(network[2] - r); if (distance >= bestDistance) continue; bestDistance = distance; best = network[3]; } // Start at networkIndex[g] and work outwards for (let j = index - 1; j >= 0; --j) { const network = this.networks[j]; // Compare the distance of the green element, break if it's too big: let distance = g - network[1]; if (distance >= bestDistance) break; // If `distance` is negative, make it positive: if (distance < 0) distance = -distance; // Compare the distance with the blue element added, continue if it's too big: distance += Math.abs(network[0] - b); if (distance >= bestDistance) continue; // Compare the distance with the red element added, continue if it's too big: distance += Math.abs(network[2] - r); if (distance >= bestDistance) continue; bestDistance = distance; best = network[3]; } return best; } /** * Initializes the state for the arrays. */ init() { for (let i = 0; i < maximumColorsSize; i++) { const v = (i << (networkBiasShift + 8)) / maximumColorsSize; this.networks[i] = new Float64Array([v, v, v, 0]); this.frequencies[i] = integerBias / maximumColorsSize; this.biases[i] = 0; } } /** * Un-biases network to give byte values 0..255 and record position i to prepare for sort. */ unBiasNetwork() { for (let i = 0; i < maximumColorsSize; i++) { const network = this.networks[i]; network[0] >>= networkBiasShift; network[1] >>= networkBiasShift; network[2] >>= networkBiasShift; network[3] = i; // record color number } } /** * Moves neuron `i` towards biased (`B`, `G`, `R`) by factor `alpha`. * @param alpha The factor at which the neuron `i` should move towards. * @param i The neuron's index. * @param b The blue color. * @param g The green color. * @param r The red color. */ alterSingle(alpha, i, b, g, r) { const network = this.networks[i]; network[0] -= (alpha * (network[0] - b)) / initialAlpha; network[1] -= (alpha * (network[1] - g)) / initialAlpha; network[2] -= (alpha * (network[2] - r)) / initialAlpha; } /** * Moves neurons in a `radius` around index `i` towards biased (`B`, `G`, `R`) by factor * {@link NeuQuant.radiusPowers `radiusPower[m]`}. * @param radius The radius around `i` to alter. * @param i The neuron's index. * @param b The blue color. * @param g The green color. * @param r The red color. */ alterNeighbors(radius, i, b, g, r) { const lo = Math.abs(i - radius); const hi = Math.min(i + radius, maximumColorsSize); let j = i + 1; let k = i - 1; let m = 1; while (j < hi || k > lo) { const alpha = this.radiusPowers[m++]; if (j < hi) { const network = this.networks[j++]; network[0] -= (alpha * (network[0] - b)) / alphaRadiusBias; network[1] -= (alpha * (network[1] - g)) / alphaRadiusBias; network[2] -= (alpha * (network[2] - r)) / alphaRadiusBias; } if (k > lo) { const network = this.networks[k--]; network[0] -= (alpha * (network[0] - b)) / alphaRadiusBias; network[1] -= (alpha * (network[1] - g)) / alphaRadiusBias; network[2] -= (alpha * (network[2] - r)) / alphaRadiusBias; } } } /** * Searches for biased BGR values. * * - Finds the closest neuron (minimum distance) and updates {@link NeuQuant.frequencies}. * - Finds the best neuron (minimum distance-bias) and returns the position. * * For frequently chosen neurons, {@link NeuQuant.frequencies `frequencies[i]`} is high and * {@link NeuQuant.biases `biases[i]`} is negative. * * The latter is determined by the multiplication of `gamma` with the subtraction of the inverse of * {@link maximumColorsSize} with {@link NeuQuant.frequencies `frequencies[i]`}: * * ```typescript * biases[i] = gamma * ((1 / maximumColorsSize) - frequencies[i]) * ``` * @param b The blue color. * @param g The green color. * @param r The red color. * @returns The best bias position. */ contest(b, g, r) { let bestDistance = ~(1 << 31); let bestBiasDistance = bestDistance; let bestPosition = -1; let bestBiasPosition = bestPosition; for (let i = 0; i < maximumColorsSize; i++) { const network = this.networks[i]; const distance = Math.abs(network[0] - b) + Math.abs(network[1] - g) + Math.abs(network[2] - r); if (distance < bestDistance) { bestDistance = distance; bestPosition = i; } const biasDistance = distance - (this.biases[i] >> (integerBiasShift - networkBiasShift)); if (biasDistance < bestBiasDistance) { bestBiasDistance = biasDistance; bestBiasPosition = i; } const betaFrequency = this.frequencies[i] >> betaShift; this.frequencies[i] -= betaFrequency; this.biases[i] += betaFrequency << gammaShift; } this.frequencies[bestPosition] += beta; this.biases[bestPosition] -= betaGamma; return bestBiasPosition; } /** * Sorts the neural network and builds {@link NeuQuant.networkIndexes `networkIndex[0..255]`}. */ buildIndexes() { let previousColor = 0; let startPosition = 0; for (let i = 0; i < maximumColorsSize; i++) { const network = this.networks[i]; let smallestPosition = i; let smallestValue = network[1]; // index on g // Find smallest in [i .. maximumColorsSize - 1] for (let j = i + 1; j < maximumColorsSize; j++) { const q = this.networks[j]; if (q[1] < smallestValue) { smallestPosition = j; smallestValue = q[1]; // index on g } } // Swap network (i) and q (smallestPosition) entries: if (i !== smallestPosition) { const q = this.networks[smallestPosition]; [q[0], network[0]] = [network[0], q[0]]; [q[1], network[1]] = [network[1], q[1]]; [q[2], network[2]] = [network[2], q[2]]; [q[3], network[3]] = [network[3], q[3]]; } // smallestValue entry is now in position i if (smallestValue !== previousColor) { this.networkIndexes[previousColor] = (startPosition + i) >> 1; for (let j = previousColor + 1; j < smallestValue; j++) { this.networkIndexes[j] = i; } previousColor = smallestValue; startPosition = i; } } this.networkIndexes[previousColor] = (startPosition + maximumColorsPosition) >> 1; for (let j = previousColor + 1; j < 256; j++) { this.networkIndexes[j] = maximumColorsPosition; } } /** * Runs the main learning loop. */ learn() { const length = this.pixels.length; const alphaDecrement = 30 + (this.sampleFactorial - 1) / 3; const samplePixels = length / (3 * this.sampleFactorial); let delta = ~~(samplePixels / learningCycles); let alpha = initialAlpha; let radius = initialRadius; let localRadius = radius >> initialRadiusBiasShift; if (localRadius <= 1) localRadius = 0; for (let i = 0; i < localRadius; i++) { this.radiusPowers[i] = alpha * (((localRadius * localRadius - i * i) * radiusBias) / (localRadius * localRadius)); } let step; if (length < minimumPictureBytes) { this.sampleFactorial = 1; step = 3; } else if (length % prime1 !== 0) { step = 3 * prime1; } else if (length % prime2 !== 0) { step = 3 * prime2; } else if (length % prime3 !== 0) { step = 3 * prime3; } else { step = 3 * prime4; } let pixelPosition = 0; let i = 0; while (i < samplePixels) { const b = (this.pixels[pixelPosition] & 0xff) << networkBiasShift; const g = (this.pixels[pixelPosition + 1] & 0xff) << networkBiasShift; const r = (this.pixels[pixelPosition + 2] & 0xff) << networkBiasShift; let j = this.contest(b, g, r); this.alterSingle(alpha, j, b, g, r); if (localRadius !== 0) this.alterNeighbors(localRadius, j, b, g, r); pixelPosition += step; if (pixelPosition >= length) pixelPosition -= length; if (delta === 0) delta = 1; ++i; if (i % delta !== 0) continue; alpha -= alpha / alphaDecrement; radius -= radius / initialRadiusDecrement; localRadius = radius >> initialRadiusBiasShift; if (localRadius <= 1) localRadius = 0; for (j = 0; j < localRadius; j++) { this.radiusPowers[j] = alpha * (((localRadius * localRadius - j * j) * radiusBias) / (localRadius * localRadius)); } } } } exports.NeuQuant = NeuQuant; //# sourceMappingURL=NeuQuant.js.map