blob: cb662e007f9218a229b257aa843d9129af98013e [file] [log] [blame]
import 'dart:math';
import 'dart:typed_data';
import '../color.dart';
import '../image.dart';
import '../image_exception.dart';
import 'quantizer.dart';
/* 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.
*
* Dart port by Brendan Duncan.
*/
/// Compute a color map with a given number of colors that best represents
/// the given image.
class NeuralQuantizer extends Quantizer {
Uint8List colorMap;
NeuralQuantizer(Image image, {int numberOfColors = 256}) {
if (image.width * image.height < MAX_PRIME) {
throw new ImageException('Image is too small');
}
_initialize(numberOfColors);
_setupArrays();
addImage(image);
}
/// Add an image to the quantized color table.
void addImage(Image image) {
_learn(image);
_fix();
_inxBuild();
_copyColorMap();
}
/// How many colors are in the [colorMap]?
int get numColors => NET_SIZE;
/// Get a color from the [colorMap].
int color(int index) => getColor(
colorMap[index * 3], colorMap[index * 3 + 1], colorMap[index * 3 + 2]);
/// Find the index of the closest color to [c] in the [colorMap].
int lookup(int c) {
int r = getRed(c);
int g = getGreen(c);
int b = getBlue(c);
return _inxSearch(b, g, r);
}
/// Find the index of the closest color to [r],[g],[b] in the [colorMap].
int lookupRGB(int r, int g, int b) {
return _inxSearch(b, g, r);
}
/// Find the color closest to [c] in the [colorMap].
int getQuantizedColor(int c) {
int r = getRed(c);
int g = getGreen(c);
int b = getBlue(c);
int a = getAlpha(c);
int i = _inxSearch(b, g, r);
i *= 3;
return getColor(colorMap[i], colorMap[i + 1], colorMap[i + 2], a);
}
/// Convert the [image] to an index map, mapping to this [colorMap].
Uint8List getIndexMap(Image image) {
Uint8List map = Uint8List(image.width * image.height);
for (int i = 0, len = image.length; i < len; ++i) {
map[i] = lookup(image[i]);
}
return map;
}
void _initialize(int numberOfColors) {
NET_SIZE = max(numberOfColors, 4); // number of colours used
CUT_NET_SIZE = NET_SIZE - SPECIALS;
MAX_NET_POS = NET_SIZE - 1;
INIT_RAD = NET_SIZE ~/ 8; // for 256 cols, radius starts at 32
INIT_BIAS_RADIUS = INIT_RAD * RADIUS_BIAS;
_network = List<double>(NET_SIZE * 3);
_colorMap = Int32List(NET_SIZE * 4);
_bias = List<double>(NET_SIZE);
_freq = List<double>(NET_SIZE);
colorMap = Uint8List(NET_SIZE * 3);
SPECIALS = 3; // number of reserved colours used
BG_COLOR = SPECIALS - 1;
}
void _copyColorMap() {
for (int i = 0, p = 0, q = 0; i < NET_SIZE; ++i) {
colorMap[p++] = _colorMap[q + 2].abs() & 0xff;
colorMap[p++] = _colorMap[q + 1].abs() & 0xff;
colorMap[p++] = _colorMap[q].abs() & 0xff;
q += 4;
}
}
int _inxSearch(int b, int g, int r) {
// Search for BGR values 0..255 and return colour index
int bestd = 1000; // biggest possible dist is 256*3
int best = -1;
int i = _netIndex[g]; // index on g
int j = i - 1; // start at netindex[g] and work outwards
while ((i < NET_SIZE) || (j >= 0)) {
if (i < NET_SIZE) {
int p = i * 4;
int dist = _colorMap[p + 1] - g; // inx key
if (dist >= bestd) {
i = NET_SIZE; // stop iter
} else {
if (dist < 0) {
dist = -dist;
}
int a = _colorMap[p] - b;
if (a < 0) {
a = -a;
}
dist += a;
if (dist < bestd) {
a = _colorMap[p + 2] - r;
if (a < 0) {
a = -a;
}
dist += a;
if (dist < bestd) {
bestd = dist;
best = i;
}
}
i++;
}
}
if (j >= 0) {
int p = j * 4;
int dist = g - _colorMap[p + 1]; // inx key - reverse dif
if (dist >= bestd) {
j = -1; // stop iter
} else {
if (dist < 0) {
dist = -dist;
}
int a = _colorMap[p] - b;
if (a < 0) {
a = -a;
}
dist += a;
if (dist < bestd) {
a = _colorMap[p + 2] - r;
if (a < 0) {
a = -a;
}
dist += a;
if (dist < bestd) {
bestd = dist;
best = j;
}
}
j--;
}
}
}
return best;
}
void _fix() {
for (int i = 0, p = 0, q = 0; i < NET_SIZE; i++, q += 4) {
for (int j = 0; j < 3; ++j, ++p) {
int x = (0.5 + _network[p]).toInt();
if (x < 0) {
x = 0;
}
if (x > 255) {
x = 255;
}
_colorMap[q + j] = x;
}
_colorMap[q + 3] = i;
}
}
/// Insertion sort of network and building of netindex[0..255]
void _inxBuild() {
int previouscol = 0;
int startpos = 0;
for (int i = 0, p = 0; i < NET_SIZE; i++, p += 4) {
int smallpos = i;
int smallval = _colorMap[p + 1]; // index on g
// find smallest in i..netsize-1
for (int j = i + 1, q = p + 4; j < NET_SIZE; j++, q += 4) {
if (_colorMap[q + 1] < smallval) {
// index on g
smallpos = j;
smallval = _colorMap[q + 1]; // index on g
}
}
int q = smallpos * 4;
// swap p (i) and q (smallpos) entries
if (i != smallpos) {
int j = _colorMap[q];
_colorMap[q] = _colorMap[p];
_colorMap[p] = j;
j = _colorMap[q + 1];
_colorMap[q + 1] = _colorMap[p + 1];
_colorMap[p + 1] = j;
j = _colorMap[q + 2];
_colorMap[q + 2] = _colorMap[p + 2];
_colorMap[p + 2] = j;
j = _colorMap[q + 3];
_colorMap[q + 3] = _colorMap[p + 3];
_colorMap[p + 3] = j;
}
// smallval entry is now in position i
if (smallval != previouscol) {
_netIndex[previouscol] = (startpos + i) >> 1;
for (int j = previouscol + 1; j < smallval; j++) {
_netIndex[j] = i;
}
previouscol = smallval;
startpos = i;
}
}
_netIndex[previouscol] = (startpos + MAX_NET_POS) >> 1;
for (int j = previouscol + 1; j < 256; j++) {
_netIndex[j] = MAX_NET_POS; // really 256
}
}
void _learn(Image image) {
int biasRadius = INIT_BIAS_RADIUS;
int alphadec = 30 + ((_sampleFac - 1) ~/ 3);
int lengthCount = image.length;
int samplePixels = lengthCount ~/ _sampleFac;
int delta = samplePixels ~/ NUM_CYCLES;
int alpha = INIT_ALPHA;
int rad = biasRadius >> RADIUS_BIAS_SHIFT;
if (rad <= 1) {
rad = 0;
}
int step = 0;
int pos = 0;
if ((lengthCount % PRIME1) != 0) {
step = PRIME1;
} else {
if ((lengthCount % PRIME2) != 0) {
step = PRIME2;
} else {
if ((lengthCount % PRIME3) != 0) {
step = PRIME3;
} else {
step = PRIME4;
}
}
}
int i = 0;
while (i < samplePixels) {
int p = image[pos];
int red = getRed(p);
int green = getGreen(p);
int blue = getBlue(p);
double b = blue.toDouble();
double g = green.toDouble();
double r = red.toDouble();
if (i == 0) {
// remember background colour
_network[BG_COLOR * 3] = b;
_network[BG_COLOR * 3 + 1] = g;
_network[BG_COLOR * 3 + 2] = r;
}
int j = _specialFind(b, g, r);
j = j < 0 ? _contest(b, g, r) : j;
if (j >= SPECIALS) {
// don't learn for specials
double a = (1.0 * alpha) / INIT_ALPHA;
_alterSingle(a, j, b, g, r);
if (rad > 0) {
_alterNeighbors(a, rad, j, b, g, r); // alter neighbours
}
}
pos += step;
while (pos >= lengthCount) {
pos -= lengthCount;
}
i++;
if (i % delta == 0) {
alpha -= alpha ~/ alphadec;
biasRadius -= biasRadius ~/ RADIUS_DEC;
rad = biasRadius >> RADIUS_BIAS_SHIFT;
if (rad <= 1) {
rad = 0;
}
}
}
}
void _alterSingle(double alpha, int i, double b, double g, double r) {
// Move neuron i towards biased (b,g,r) by factor alpha
int p = i * 3;
_network[p] -= (alpha * (_network[p] - b));
_network[p + 1] -= (alpha * (_network[p + 1] - g));
_network[p + 2] -= (alpha * (_network[p + 2] - r));
}
void _alterNeighbors(
double alpha, int rad, int i, double b, double g, double r) {
int lo = i - rad;
if (lo < SPECIALS - 1) {
lo = SPECIALS - 1;
}
int hi = i + rad;
if (hi > NET_SIZE) {
hi = NET_SIZE;
}
int j = i + 1;
int k = i - 1;
int q = 0;
while ((j < hi) || (k > lo)) {
double a = (alpha * (rad * rad - q * q)) / (rad * rad);
q++;
if (j < hi) {
int p = j * 3;
_network[p] -= (a * (_network[p] - b));
_network[1] -= (a * (_network[p + 1] - g));
_network[2] -= (a * (_network[p + 2] - r));
j++;
}
if (k > lo) {
int p = k * 3;
_network[p] -= (a * (_network[p] - b));
_network[p + 1] -= (a * (_network[p + 1] - g));
_network[p + 2] -= (a * (_network[p + 2] - r));
k--;
}
}
}
// Search for biased BGR values
int _contest(double b, double g, double r) {
// finds closest neuron (min dist) and updates freq
// finds best neuron (min dist-bias) and returns position
// for frequently chosen neurons, freq[i] is high and bias[i] is negative
// bias[i] = gamma*((1/netsize)-freq[i])
double bestd = 1.0e30;
double bestbiasd = bestd;
int bestpos = -1;
int bestbiaspos = bestpos;
for (int i = SPECIALS, p = SPECIALS * 3; i < NET_SIZE; i++) {
double dist = _network[p++] - b;
if (dist < 0) {
dist = -dist;
}
double a = _network[p++] - g;
if (a < 0) {
a = -a;
}
dist += a;
a = _network[p++] - r;
if (a < 0) {
a = -a;
}
dist += a;
if (dist < bestd) {
bestd = dist;
bestpos = i;
}
double biasdist = dist - _bias[i];
if (biasdist < bestbiasd) {
bestbiasd = biasdist;
bestbiaspos = i;
}
_freq[i] -= BETA * _freq[i];
_bias[i] += BETA_GAMMA * _freq[i];
}
_freq[bestpos] += BETA;
_bias[bestpos] -= BETA_GAMMA;
return bestbiaspos;
}
int _specialFind(double b, double g, double r) {
for (int i = 0, p = 0; i < SPECIALS; i++) {
if (_network[p++] == b && _network[p++] == g && _network[p++] == r) {
return i;
}
}
return -1;
}
void _setupArrays() {
_network[0] = 0.0; // black
_network[1] = 0.0;
_network[2] = 0.0;
_network[3] = 255.0; // white
_network[4] = 255.0;
_network[5] = 255.0;
// RESERVED bgColour // background
final double f = 1.0 / NET_SIZE;
for (int i = 0; i < SPECIALS; ++i) {
_freq[i] = f;
_bias[i] = 0.0;
}
for (int i = SPECIALS, p = SPECIALS * 3; i < NET_SIZE; ++i) {
_network[p++] = (255.0 * (i - SPECIALS)) / CUT_NET_SIZE;
_network[p++] = (255.0 * (i - SPECIALS)) / CUT_NET_SIZE;
_network[p++] = (255.0 * (i - SPECIALS)) / CUT_NET_SIZE;
_freq[i] = f;
_bias[i] = 0.0;
}
}
static const int NUM_CYCLES = 100; // no. of learning cycles
int NET_SIZE = 16; // number of colours used
int SPECIALS = 3; // number of reserved colours used
int BG_COLOR; // reserved background colour
int CUT_NET_SIZE;
int MAX_NET_POS;
int INIT_RAD; // for 256 cols, radius starts at 32
static const int RADIUS_BIAS_SHIFT = 6;
static const int RADIUS_BIAS = 1 << RADIUS_BIAS_SHIFT;
int INIT_BIAS_RADIUS;
static const int RADIUS_DEC = 30; // factor of 1/30 each cycle
static const int ALPHA_BIAS_SHIFT = 10; // alpha starts at 1
static const int INIT_ALPHA = 1 << ALPHA_BIAS_SHIFT; // biased by 10 bits
static const double GAMMA = 1024.0;
static const double BETA = 1.0 / 1024.0;
static const double BETA_GAMMA = BETA * GAMMA;
/// the network itself
List<double> _network;
Int32List _colorMap;
Int32List _netIndex = Int32List(256);
// bias and freq arrays for learning
List<double> _bias;
List<double> _freq;
// four primes near 500 - assume no image has a length so large
// that it is divisible by all four primes
static const int PRIME1 = 499;
static const int PRIME2 = 491;
static const int PRIME3 = 487;
static const int PRIME4 = 503;
static const int MAX_PRIME = PRIME4;
int _sampleFac = 1;
}