package spike.palette.paul.com.palettespike;
/**
* This sample code is made available as part of the book "Digital Image
* Processing - An Algorithmic Introduction using Java" by Wilhelm Burger
* and Mark J. Burge, Copyright (C) 2005-2008 Springer-Verlag Berlin,
* Heidelberg, New York.
* Note that this code comes with absolutely no warranty of any kind.
* See http://www.imagingbook.com for details and licensing conditions.
*
* Modified by Chris Banes.
*/
import android.graphics.Color;
import android.util.Log;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
/*
* This is an implementation of Heckbert's median-cut color quantization algorithm
* (Heckbert P., "Color Image Quantization for Frame Buffer Display", ACM Transactions
* on Computer Graphics (SIGGRAPH), pp. 297-307, 1982).
* Unlike in the original algorithm, no initial uniform (scalar) quantization is used to
* for reducing the number of image colors. Instead, all colors contained in the original
* image are considered in the quantization process. After the set of representative
* colors has been found, each image color is mapped to the closest representative
* in RGB color space using the Euclidean distance.
* The quantization process has two steps: first a ColorQuantizer object is created from
* a given image using one of the constructor methods provided. Then this ColorQuantizer
* can be used to quantize the original image or any other image using the same set of
* representative colors (color table).
*/
public class MedianCutQuantizer {
private static final String LOG_TAG = MedianCutQuantizer.class.getSimpleName();
private ColorNode[] imageColors = null; // original (unique) image colors
private ColorNode[] quantColors = null; // quantized colors
public MedianCutQuantizer(int[] pixels, int Kmax) {
quantColors = findRepresentativeColors(pixels, Kmax);
}
public int countQuantizedColors() {
return quantColors.length;
}
public ColorNode[] getQuantizedColors() {
return quantColors;
}
ColorNode[] findRepresentativeColors(int[] pixels, int Kmax) {
ColorHistogram colorHist = new ColorHistogram(pixels);
int K = colorHist.getNumberOfColors();
ColorNode[] rCols = null;
imageColors = new ColorNode[K];
for (int i = 0; i < K; i++) {
int rgb = colorHist.getColor(i);
int cnt = colorHist.getCount(i);
imageColors[i] = new ColorNode(rgb, cnt);
}
if (K <= Kmax) {
// image has fewer colors than Kmax
rCols = imageColors;
} else {
ColorBox initialBox = new ColorBox(0, K - 1, 0);
List<ColorBox> colorSet = new ArrayList<ColorBox>();
colorSet.add(initialBox);
int k = 1;
boolean done = false;
while (k < Kmax && !done) {
ColorBox nextBox = findBoxToSplit(colorSet);
if (nextBox != null) {
ColorBox newBox = nextBox.splitBox();
colorSet.add(newBox);
k = k + 1;
} else {
done = true;
}
}
rCols = averageColors(colorSet);
}
return rCols;
}
public void quantizeImage(int[] pixels) {
for (int i = 0; i < pixels.length; i++) {
ColorNode color = findClosestColor(pixels[i]);
pixels[i] = Color.rgb(color.red, color.grn, color.blu);
}
}
ColorNode findClosestColor(int rgb) {
int idx = findClosestColorIndex(rgb);
return quantColors[idx];
}
int findClosestColorIndex(int rgb) {
int red = Color.red(rgb);
int grn = Color.green(rgb);
int blu = Color.blue(rgb);
int minIdx = 0;
int minDistance = Integer.MAX_VALUE;
for (int i = 0; i < quantColors.length; i++) {
ColorNode color = quantColors[i];
int d2 = color.distance2(red, grn, blu);
if (d2 < minDistance) {
minDistance = d2;
minIdx = i;
}
}
return minIdx;
}
private ColorBox findBoxToSplit(List<ColorBox> colorBoxes) {
ColorBox boxToSplit = null;
// from the set of splitable color boxes
// select the one with the minimum level
int minLevel = Integer.MAX_VALUE;
for (ColorBox box : colorBoxes) {
if (box.colorCount() >= 2) { // box can be split
if (box.level < minLevel) {
boxToSplit = box;
minLevel = box.level;
}
}
}
return boxToSplit;
}
private ColorNode[] averageColors(List<ColorBox> colorBoxes) {
int n = colorBoxes.size();
ColorNode[] avgColors = new ColorNode[n];
int i = 0;
for (ColorBox box : colorBoxes) {
avgColors[i] = box.getAverageColor();
i = i + 1;
}
return avgColors;
}
// -------------- class ColorNode -------------------------------------------
public static class ColorNode {
private final int red, grn, blu;
private final int cnt;
private float[] hsv;
ColorNode(int rgb, int cnt) {
this.red = Color.red(rgb);
this.grn = Color.green(rgb);
this.blu = Color.blue(rgb);
this.cnt = cnt;
}
ColorNode(int red, int grn, int blu, int cnt) {
this.red = red;
this.grn = grn;
this.blu = blu;
this.cnt = cnt;
}
public int getRgb() {
return Color.rgb(red, grn, blu);
}
public float[] getHsv() {
if (hsv == null) {
hsv = new float[3];
Color.RGBToHSV(red, grn, blu, hsv);
}
return hsv;
}
public int getCount() {
return cnt;
}
int distance2(int red, int grn, int blu) {
// returns the squared distance between (red, grn, blu)
// and this this color
int dr = this.red - red;
int dg = this.grn - grn;
int db = this.blu - blu;
return dr * dr + dg * dg + db * db;
}
public String toString() {
return new StringBuilder(getClass().getSimpleName())
.append(" #").append(Integer.toHexString(getRgb()))
.append(". count: ").append(cnt).toString();
}
}
// -------------- class ColorBox -------------------------------------------
class ColorBox {
int lower = 0; // lower index into 'imageColors'
int upper = -1; // upper index into 'imageColors'
int level; // split level o this color box
int count = 0; // number of pixels represented by thos color box
int rmin, rmax; // range of contained colors in red dimension
int gmin, gmax; // range of contained colors in green dimension
int bmin, bmax; // range of contained colors in blue dimension
ColorBox(int lower, int upper, int level) {
this.lower = lower;
this.upper = upper;
this.level = level;
this.trim();
}
int colorCount() {
return upper - lower;
}
void trim() {
// recompute the boundaries of this color box
rmin = 255;
rmax = 0;
gmin = 255;
gmax = 0;
bmin = 255;
bmax = 0;
count = 0;
for (int i = lower; i <= upper; i++) {
ColorNode color = imageColors[i];
count = count + color.cnt;
int r = color.red;
int g = color.grn;
int b = color.blu;
if (r > rmax) {
rmax = r;
}
if (r < rmin) {
rmin = r;
}
if (g > gmax) {
gmax = g;
}
if (g < gmin) {
gmin = g;
}
if (b > bmax) {
bmax = b;
}
if (b < bmin) {
bmin = b;
}
}
}
// Split this color box at the median point along its
// longest color dimension
ColorBox splitBox() {
if (this.colorCount() < 2) // this box cannot be split
{
return null;
} else {
// find longest dimension of this box:
ColorDimension dim = getLongestColorDimension();
// find median along dim
int med = findMedian(dim);
// now split this box at the median return the resulting new
// box.
int nextLevel = level + 1;
ColorBox newBox = new ColorBox(med + 1, upper, nextLevel);
this.upper = med;
this.level = nextLevel;
this.trim();
return newBox;
}
}
// Find longest dimension of this color box (RED, GREEN, or BLUE)
ColorDimension getLongestColorDimension() {
int rLength = rmax - rmin;
int gLength = gmax - gmin;
int bLength = bmax - bmin;
if (bLength >= rLength && bLength >= gLength) {
return ColorDimension.BLUE;
} else if (gLength >= rLength && gLength >= bLength) {
return ColorDimension.GREEN;
} else {
return ColorDimension.RED;
}
}
// Find the position of the median in RGB space along
// the red, green or blue dimension, respectively.
int findMedian(ColorDimension dim) {
// sort color in this box along dimension dim:
Arrays.sort(imageColors, lower, upper + 1, dim.comparator);
// find the median point:
int half = count / 2;
int nPixels, median;
for (median = lower, nPixels = 0; median < upper; median++) {
nPixels = nPixels + imageColors[median].cnt;
if (nPixels >= half) {
break;
}
}
return median;
}
ColorNode getAverageColor() {
int rSum = 0;
int gSum = 0;
int bSum = 0;
int n = 0;
for (int i = lower; i <= upper; i++) {
ColorNode ci = imageColors[i];
int cnt = ci.cnt;
rSum = rSum + cnt * ci.red;
gSum = gSum + cnt * ci.grn;
bSum = bSum + cnt * ci.blu;
n = n + cnt;
}
double nd = n;
int avgRed = (int) (0.5 + rSum / nd);
int avgGrn = (int) (0.5 + gSum / nd);
int avgBlu = (int) (0.5 + bSum / nd);
return new ColorNode(avgRed, avgGrn, avgBlu, n);
}
public String toString() {
String s = this.getClass().getSimpleName();
s = s + " lower=" + lower + " upper=" + upper;
s = s + " count=" + count + " level=" + level;
s = s + " rmin=" + rmin + " rmax=" + rmax;
s = s + " gmin=" + gmin + " gmax=" + gmax;
s = s + " bmin=" + bmin + " bmax=" + bmax;
s = s + " bmin=" + bmin + " bmax=" + bmax;
return s;
}
}
// --- color dimensions ------------------------
// The main purpose of this enumeration class is associate
// the color dimensions with the corresponding comparators.
enum ColorDimension {
RED(new redComparator()),
GREEN(new grnComparator()),
BLUE(new bluComparator());
public final Comparator<ColorNode> comparator;
ColorDimension(Comparator<ColorNode> cmp) {
this.comparator = cmp;
}
}
// --- color comparators used for sorting colors along different dimensions ---
static class redComparator implements Comparator<ColorNode> {
public int compare(ColorNode colA, ColorNode colB) {
return colA.red - colB.red;
}
}
static class grnComparator implements Comparator<ColorNode> {
public int compare(ColorNode colA, ColorNode colB) {
return colA.grn - colB.grn;
}
}
static class bluComparator implements Comparator<ColorNode> {
public int compare(ColorNode colA, ColorNode colB) {
return colA.blu - colB.blu;
}
}
//-------- utility methods -----------
void listColorNodes(ColorNode[] nodes) {
int i = 0;
for (ColorNode color : nodes) {
Log.d(LOG_TAG, "Color Node #" + i + " " + color.toString());
i++;
}
}
static class ColorHistogram {
int colorArray[] = null;
int countArray[] = null;
ColorHistogram(int[] color, int[] count) {
this.countArray = count;
this.colorArray = color;
}
ColorHistogram(int[] pixelsOrig) {
int N = pixelsOrig.length;
int[] pixelsCpy = new int[N];
for (int i = 0; i < N; i++) {
// remove possible alpha components
pixelsCpy[i] = 0xFFFFFF & pixelsOrig[i];
}
Arrays.sort(pixelsCpy);
// count unique colors:
int k = -1; // current color index
int curColor = -1;
for (int i = 0; i < pixelsCpy.length; i++) {
if (pixelsCpy[i] != curColor) {
k++;
curColor = pixelsCpy[i];
}
}
int nColors = k + 1;
// tabulate and count unique colors:
colorArray = new int[nColors];
countArray = new int[nColors];
k = -1; // current color index
curColor = -1;
for (int i = 0; i < pixelsCpy.length; i++) {
if (pixelsCpy[i] != curColor) { // new color
k++;
curColor = pixelsCpy[i];
colorArray[k] = curColor;
countArray[k] = 1;
} else {
countArray[k]++;
}
}
}
public int[] getColorArray() {
return colorArray;
}
public int[] getCountArray() {
return countArray;
}
public int getNumberOfColors() {
if (colorArray == null) {
return 0;
} else {
return colorArray.length;
}
}
public int getColor(int index) {
return this.colorArray[index];
}
public int getCount(int index) {
return this.countArray[index];
}
}
} //class MedianCut