diff --git a/src/helma/image/GIFEncoder.java b/src/helma/image/GIFEncoder.java
new file mode 100644
index 00000000..57d9b710
--- /dev/null
+++ b/src/helma/image/GIFEncoder.java
@@ -0,0 +1,478 @@
+/*
+ * @(#)GIFEncoder.java 0.90 4/21/96 Adam Doppelt
+ */
+
+package helma.image;
+
+import java.io.*;
+import java.awt.*;
+import java.awt.image.*;
+
+/**
+ * GIFEncoder is a class which takes an image and saves it to a stream
+ * using the GIF file format (Graphics Interchange
+ * Format). A GIFEncoder
+ * is constructed with either an AWT Image (which must be fully
+ * loaded) or a set of RGB arrays. The image can be written out with a
+ * call to Write
.
+ * + * Three caveats: + *
+ * + *
+ * + *
+ * java.awt.AWTException: Grabber returned false: 192
+ *
+ * + * GIFEncoder is based upon gifsave.c, which was written and released + * by:
+ *
+ *
+ * Phone: +47 2 230539
+ * sverrehu@ifi.uio.no
+ *
+ * + * @param image The image to encode. The image must be + * completely loaded. + * @exception AWTException Will be thrown if the pixel grab fails. This + * can happen if Java runs out of memory. It may also indicate that the image + * contains more than 256 colors. + * */ + public GIFEncoder(Image image) throws AWTException { + width_ = (short)image.getWidth(null); + height_ = (short)image.getHeight(null); + + int values[] = new int[width_ * height_]; + PixelGrabber grabber = new PixelGrabber( + image, 0, 0, width_, height_, values, 0, width_); + + try { + if(grabber.grabPixels() != true) + throw new AWTException("Grabber returned false: " + + grabber.status()); + } + catch (InterruptedException e) { ; } + + byte r[][] = new byte[width_][height_]; + byte g[][] = new byte[width_][height_]; + byte b[][] = new byte[width_][height_]; + int index = 0; + for (int y = 0; y < height_; ++y) + for (int x = 0; x < width_; ++x) { + r[x][y] = (byte)((values[index] >> 16) & 0xFF); + g[x][y] = (byte)((values[index] >> 8) & 0xFF); + b[x][y] = (byte)((values[index]) & 0xFF); + ++index; + } + ToIndexedColor(r, g, b); + } + +/** + * Construct a GIFEncoder. The constructor will convert the image to + * an indexed color array. This may take some time.
+ * + * Each array stores intensity values for the image. In other words, + * r[x][y] refers to the red intensity of the pixel at column x, row + * y.
+ * + * @param r An array containing the red intensity values. + * @param g An array containing the green intensity values. + * @param b An array containing the blue intensity values. + * + * @exception AWTException Will be thrown if the image contains more than + * 256 colors. + * */ + public GIFEncoder(byte r[][], byte g[][], byte b[][]) throws AWTException { + width_ = (short)(r.length); + height_ = (short)(r[0].length); + + ToIndexedColor(r, g, b); + } + +/** + * Writes the image out to a stream in the GIF file format. This will + * be a single GIF87a image, non-interlaced, with no background color. + * This may take some time.
+ * + * @param output The stream to output to. This should probably be a + * buffered stream. + * + * @exception IOException Will be thrown if a write operation fails. + * */ + public void Write(OutputStream output) throws IOException { + BitUtils.WriteString(output, "GIF87a"); + + ScreenDescriptor sd = new ScreenDescriptor(width_, height_, + numColors_); + sd.Write(output); + + output.write(colors_, 0, colors_.length); + + ImageDescriptor id = new ImageDescriptor(width_, height_, ','); + id.Write(output); + + byte codesize = BitUtils.BitsNeeded(numColors_); + if (codesize == 1) + ++codesize; + output.write(codesize); + + LZWCompressor.LZWCompress(output, codesize, pixels_); + output.write(0); + + id = new ImageDescriptor((byte)0, (byte)0, ';'); + id.Write(output); + output.flush(); + } + + void ToIndexedColor(byte r[][], byte g[][], + byte b[][]) throws AWTException { + pixels_ = new byte[width_ * height_]; + colors_ = new byte[256 * 3]; + int colornum = 0; + for (int x = 0; x < width_; ++x) { + for (int y = 0; y < height_; ++y) { + int search; + for (search = 0; search < colornum; ++search) + if (colors_[search * 3] == r[x][y] && + colors_[search * 3 + 1] == g[x][y] && + colors_[search * 3 + 2] == b[x][y]) + break; + + if (search > 255) + throw new AWTException("Too many colors."); + + pixels_[y * width_ + x] = (byte)search; + + if (search == colornum) { + colors_[search * 3] = r[x][y]; + colors_[search * 3 + 1] = g[x][y]; + colors_[search * 3 + 2] = b[x][y]; + ++colornum; + } + } + } + numColors_ = 1 << BitUtils.BitsNeeded(colornum); + byte copy[] = new byte[numColors_ * 3]; + System.arraycopy(colors_, 0, copy, 0, numColors_ * 3); + colors_ = copy; + } + +} + +class BitFile { + OutputStream output_; + byte buffer_[]; + int index_, bitsLeft_; + + public BitFile(OutputStream output) { + output_ = output; + buffer_ = new byte[256]; + index_ = 0; + bitsLeft_ = 8; + } + + public void Flush() throws IOException { + int numBytes = index_ + (bitsLeft_ == 8 ? 0 : 1); + if (numBytes > 0) { + output_.write(numBytes); + output_.write(buffer_, 0, numBytes); + buffer_[0] = 0; + index_ = 0; + bitsLeft_ = 8; + } + } + + public void WriteBits(int bits, int numbits) throws IOException { + int bitsWritten = 0; + int numBytes = 255; + do { + if ((index_ == 254 && bitsLeft_ == 0) || index_ > 254) { + output_.write(numBytes); + output_.write(buffer_, 0, numBytes); + + buffer_[0] = 0; + index_ = 0; + bitsLeft_ = 8; + } + + if (numbits <= bitsLeft_) { + buffer_[index_] |= (bits & ((1 << numbits) - 1)) << + (8 - bitsLeft_); + bitsWritten += numbits; + bitsLeft_ -= numbits; + numbits = 0; + } + else { + buffer_[index_] |= (bits & ((1 << bitsLeft_) - 1)) << + (8 - bitsLeft_); + bitsWritten += bitsLeft_; + bits >>= bitsLeft_; + numbits -= bitsLeft_; + buffer_[++index_] = 0; + bitsLeft_ = 8; + } + } while (numbits != 0); + } +} + +class LZWStringTable { + private final static int RES_CODES = 2; + private final static short HASH_FREE = (short)0xFFFF; + private final static short NEXT_FIRST = (short)0xFFFF; + private final static int MAXBITS = 12; + private final static int MAXSTR = (1 << MAXBITS); + private final static short HASHSIZE = 9973; + private final static short HASHSTEP = 2039; + + byte strChr_[]; + short strNxt_[]; + short strHsh_[]; + short numStrings_; + + public LZWStringTable() { + strChr_ = new byte[MAXSTR]; + strNxt_ = new short[MAXSTR]; + strHsh_ = new short[HASHSIZE]; + } + + public int AddCharString(short index, byte b) { + int hshidx; + + if (numStrings_ >= MAXSTR) + return 0xFFFF; + + hshidx = Hash(index, b); + while (strHsh_[hshidx] != HASH_FREE) + hshidx = (hshidx + HASHSTEP) % HASHSIZE; + + strHsh_[hshidx] = numStrings_; + strChr_[numStrings_] = b; + strNxt_[numStrings_] = (index != HASH_FREE) ? index : NEXT_FIRST; + + return numStrings_++; + } + + public short FindCharString(short index, byte b) { + int hshidx, nxtidx; + + if (index == HASH_FREE) + return b; + + hshidx = Hash(index, b); + while ((nxtidx = strHsh_[hshidx]) != HASH_FREE) { + if (strNxt_[nxtidx] == index && strChr_[nxtidx] == b) + return (short)nxtidx; + hshidx = (hshidx + HASHSTEP) % HASHSIZE; + } + + return (short)0xFFFF; + } + + public void ClearTable(int codesize) { + numStrings_ = 0; + + for (int q = 0; q < HASHSIZE; q++) { + strHsh_[q] = HASH_FREE; + } + + int w = (1 << codesize) + RES_CODES; + for (int q = 0; q < w; q++) + AddCharString((short)0xFFFF, (byte)q); + } + + static public int Hash(short index, byte lastbyte) { + return ((int)((short)(lastbyte << 8) ^ index) & 0xFFFF) % HASHSIZE; + } +} + +class LZWCompressor { + + public static void LZWCompress(OutputStream output, int codesize, + byte toCompress[]) throws IOException { + byte c; + short index; + int clearcode, endofinfo, numbits, limit, errcode; + short prefix = (short)0xFFFF; + + BitFile bitFile = new BitFile(output); + LZWStringTable strings = new LZWStringTable(); + + clearcode = 1 << codesize; + endofinfo = clearcode + 1; + + numbits = codesize + 1; + limit = (1 << numbits) - 1; + + strings.ClearTable(codesize); + bitFile.WriteBits(clearcode, numbits); + + for (int loop = 0; loop < toCompress.length; ++loop) { + c = toCompress[loop]; + if ((index = strings.FindCharString(prefix, c)) != -1) + prefix = index; + else { + bitFile.WriteBits(prefix, numbits); + if (strings.AddCharString(prefix, c) > limit) { + if (++numbits > 12) { + bitFile.WriteBits(clearcode, numbits - 1); + strings.ClearTable(codesize); + numbits = codesize + 1; + } + limit = (1 << numbits) - 1; + } + + prefix = (short)((short)c & 0xFF); + } + } + + if (prefix != -1) + bitFile.WriteBits(prefix, numbits); + + bitFile.WriteBits(endofinfo, numbits); + bitFile.Flush(); + } +} + +class ScreenDescriptor { + public short localScreenWidth_, localScreenHeight_; + private byte byte_; + public byte backgroundColorIndex_, pixelAspectRatio_; + + public ScreenDescriptor(short width, short height, int numColors) { + localScreenWidth_ = width; + localScreenHeight_ = height; + SetGlobalColorTableSize((byte)(BitUtils.BitsNeeded(numColors) - 1)); + SetGlobalColorTableFlag((byte)1); + SetSortFlag((byte)0); + SetColorResolution((byte)7); + backgroundColorIndex_ = 0; + pixelAspectRatio_ = 0; + } + + public void Write(OutputStream output) throws IOException { + BitUtils.WriteWord(output, localScreenWidth_); + BitUtils.WriteWord(output, localScreenHeight_); + output.write(byte_); + output.write(backgroundColorIndex_); + output.write(pixelAspectRatio_); + } + + public void SetGlobalColorTableSize(byte num) { + byte_ |= (num & 7); + } + + public void SetSortFlag(byte num) { + byte_ |= (num & 1) << 3; + } + + public void SetColorResolution(byte num) { + byte_ |= (num & 7) << 4; + } + + public void SetGlobalColorTableFlag(byte num) { + byte_ |= (num & 1) << 7; + } +} + +class ImageDescriptor { + public byte separator_; + public short leftPosition_, topPosition_, width_, height_; + private byte byte_; + + public ImageDescriptor(short width, short height, char separator) { + separator_ = (byte)separator; + leftPosition_ = 0; + topPosition_ = 0; + width_ = width; + height_ = height; + SetLocalColorTableSize((byte)0); + SetReserved((byte)0); + SetSortFlag((byte)0); + SetInterlaceFlag((byte)0); + SetLocalColorTableFlag((byte)0); + } + + public void Write(OutputStream output) throws IOException { + output.write(separator_); + BitUtils.WriteWord(output, leftPosition_); + BitUtils.WriteWord(output, topPosition_); + BitUtils.WriteWord(output, width_); + BitUtils.WriteWord(output, height_); + output.write(byte_); + } + + public void SetLocalColorTableSize(byte num) { + byte_ |= (num & 7); + } + + public void SetReserved(byte num) { + byte_ |= (num & 3) << 3; + } + + public void SetSortFlag(byte num) { + byte_ |= (num & 1) << 5; + } + + public void SetInterlaceFlag(byte num) { + byte_ |= (num & 1) << 6; + } + + public void SetLocalColorTableFlag(byte num) { + byte_ |= (num & 1) << 7; + } +} + +class BitUtils { + public static byte BitsNeeded(int n) { + byte ret = 1; + + if (n-- == 0) + return 0; + + while ((n >>= 1) != 0) + ++ret; + + return ret; + } + + public static void WriteWord(OutputStream output, + short w) throws IOException { + output.write(w & 0xFF); + output.write((w >> 8) & 0xFF); + } + + static void WriteString(OutputStream output, + String string) throws IOException { + for (int loop = 0; loop < string.length(); ++loop) + output.write((byte)(string.charAt(loop))); + } +} diff --git a/src/helma/image/Quantize.java b/src/helma/image/Quantize.java new file mode 100644 index 00000000..3666e620 --- /dev/null +++ b/src/helma/image/Quantize.java @@ -0,0 +1,701 @@ +/* + * @(#)Quantize.java 0.90 9/19/00 Adam Doppelt + */ +package helma.image; + +/** + * An efficient color quantization algorithm, adapted from the C++ + * implementation quantize.c in ImageMagick. The pixels for + * an image are placed into an oct tree. The oct tree is reduced in + * size, and the pixels from the original image are reassigned to the + * nodes in the reduced tree.
+ * + * Here is the copyright notice from ImageMagick: + * + *
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Permission is hereby granted, free of charge, to any person obtaining a % +% copy of this software and associated documentation files ("ImageMagick"), % +% to deal in ImageMagick without restriction, including without limitation % +% the rights to use, copy, modify, merge, publish, distribute, sublicense, % +% and/or sell copies of ImageMagick, and to permit persons to whom the % +% ImageMagick is furnished to do so, subject to the following conditions: % +% % +% The above copyright notice and this permission notice shall be included in % +% all copies or substantial portions of ImageMagick. % +% % +% The software is provided "as is", without warranty of any kind, express or % +% implied, including but not limited to the warranties of merchantability, % +% fitness for a particular purpose and noninfringement. In no event shall % +% E. I. du Pont de Nemours and Company be liable for any claim, damages or % +% other liability, whether in an action of contract, tort or otherwise, % +% arising from, out of or in connection with ImageMagick or the use or other % +% dealings in ImageMagick. % +% % +% Except as contained in this notice, the name of the E. I. du Pont de % +% Nemours and Company shall not be used in advertising or otherwise to % +% promote the sale, use or other dealings in ImageMagick without prior % +% written authorization from the E. I. du Pont de Nemours and Company. % +% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++ * + * + * @version 0.90 19 Sep 2000 + * @author Adam Doppelt + */ +public class Quantize { + +/* +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% % +% % +% % +% QQQ U U AAA N N TTTTT IIIII ZZZZZ EEEEE % +% Q Q U U A A NN N T I ZZ E % +% Q Q U U AAAAA N N N T I ZZZ EEEEE % +% Q QQ U U A A N NN T I ZZ E % +% QQQQ UUU A A N N T IIIII ZZZZZ EEEEE % +% % +% % +% Reduce the Number of Unique Colors in an Image % +% % +% % +% Software Design % +% John Cristy % +% July 1992 % +% % +% % +% Copyright 1998 E. I. du Pont de Nemours and Company % +% % +% Permission is hereby granted, free of charge, to any person obtaining a % +% copy of this software and associated documentation files ("ImageMagick"), % +% to deal in ImageMagick without restriction, including without limitation % +% the rights to use, copy, modify, merge, publish, distribute, sublicense, % +% and/or sell copies of ImageMagick, and to permit persons to whom the % +% ImageMagick is furnished to do so, subject to the following conditions: % +% % +% The above copyright notice and this permission notice shall be included in % +% all copies or substantial portions of ImageMagick. % +% % +% The software is provided "as is", without warranty of any kind, express or % +% implied, including but not limited to the warranties of merchantability, % +% fitness for a particular purpose and noninfringement. In no event shall % +% E. I. du Pont de Nemours and Company be liable for any claim, damages or % +% other liability, whether in an action of contract, tort or otherwise, % +% arising from, out of or in connection with ImageMagick or the use or other % +% dealings in ImageMagick. % +% % +% Except as contained in this notice, the name of the E. I. du Pont de % +% Nemours and Company shall not be used in advertising or otherwise to % +% promote the sale, use or other dealings in ImageMagick without prior % +% written authorization from the E. I. du Pont de Nemours and Company. % +% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Realism in computer graphics typically requires using 24 bits/pixel to +% generate an image. Yet many graphic display devices do not contain +% the amount of memory necessary to match the spatial and color +% resolution of the human eye. The QUANTIZE program takes a 24 bit +% image and reduces the number of colors so it can be displayed on +% raster device with less bits per pixel. In most instances, the +% quantized image closely resembles the original reference image. +% +% A reduction of colors in an image is also desirable for image +% transmission and real-time animation. +% +% Function Quantize takes a standard RGB or monochrome images and quantizes +% them down to some fixed number of colors. +% +% For purposes of color allocation, an image is a set of n pixels, where +% each pixel is a point in RGB space. RGB space is a 3-dimensional +% vector space, and each pixel, pi, is defined by an ordered triple of +% red, green, and blue coordinates, (ri, gi, bi). +% +% Each primary color component (red, green, or blue) represents an +% intensity which varies linearly from 0 to a maximum value, cmax, which +% corresponds to full saturation of that color. Color allocation is +% defined over a domain consisting of the cube in RGB space with +% opposite vertices at (0,0,0) and (cmax,cmax,cmax). QUANTIZE requires +% cmax = 255. +% +% The algorithm maps this domain onto a tree in which each node +% represents a cube within that domain. In the following discussion +% these cubes are defined by the coordinate of two opposite vertices: +% The vertex nearest the origin in RGB space and the vertex farthest +% from the origin. +% +% The tree's root node represents the the entire domain, (0,0,0) through +% (cmax,cmax,cmax). Each lower level in the tree is generated by +% subdividing one node's cube into eight smaller cubes of equal size. +% This corresponds to bisecting the parent cube with planes passing +% through the midpoints of each edge. +% +% The basic algorithm operates in three phases: Classification, +% Reduction, and Assignment. Classification builds a color +% description tree for the image. Reduction collapses the tree until +% the number it represents, at most, the number of colors desired in the +% output image. Assignment defines the output image's color map and +% sets each pixel's color by reclassification in the reduced tree. +% Our goal is to minimize the numerical discrepancies between the original +% colors and quantized colors (quantization error). +% +% Classification begins by initializing a color description tree of +% sufficient depth to represent each possible input color in a leaf. +% However, it is impractical to generate a fully-formed color +% description tree in the classification phase for realistic values of +% cmax. If colors components in the input image are quantized to k-bit +% precision, so that cmax= 2k-1, the tree would need k levels below the +% root node to allow representing each possible input color in a leaf. +% This becomes prohibitive because the tree's total number of nodes is +% 1 + sum(i=1,k,8k). +% +% A complete tree would require 19,173,961 nodes for k = 8, cmax = 255. +% Therefore, to avoid building a fully populated tree, QUANTIZE: (1) +% Initializes data structures for nodes only as they are needed; (2) +% Chooses a maximum depth for the tree as a function of the desired +% number of colors in the output image (currently log2(colormap size)). +% +% For each pixel in the input image, classification scans downward from +% the root of the color description tree. At each level of the tree it +% identifies the single node which represents a cube in RGB space +% containing the pixel's color. It updates the following data for each +% such node: +% +% n1: Number of pixels whose color is contained in the RGB cube +% which this node represents; +% +% n2: Number of pixels whose color is not represented in a node at +% lower depth in the tree; initially, n2 = 0 for all nodes except +% leaves of the tree. +% +% Sr, Sg, Sb: Sums of the red, green, and blue component values for +% all pixels not classified at a lower depth. The combination of +% these sums and n2 will ultimately characterize the mean color of a +% set of pixels represented by this node. +% +% E: The distance squared in RGB space between each pixel contained +% within a node and the nodes' center. This represents the quantization +% error for a node. +% +% Reduction repeatedly prunes the tree until the number of nodes with +% n2 > 0 is less than or equal to the maximum number of colors allowed +% in the output image. On any given iteration over the tree, it selects +% those nodes whose E count is minimal for pruning and merges their +% color statistics upward. It uses a pruning threshold, Ep, to govern +% node selection as follows: +% +% Ep = 0 +% while number of nodes with (n2 > 0) > required maximum number of colors +% prune all nodes such that E <= Ep +% Set Ep to minimum E in remaining nodes +% +% This has the effect of minimizing any quantization error when merging +% two nodes together. +% +% When a node to be pruned has offspring, the pruning procedure invokes +% itself recursively in order to prune the tree from the leaves upward. +% n2, Sr, Sg, and Sb in a node being pruned are always added to the +% corresponding data in that node's parent. This retains the pruned +% node's color characteristics for later averaging. +% +% For each node, n2 pixels exist for which that node represents the +% smallest volume in RGB space containing those pixel's colors. When n2 +% > 0 the node will uniquely define a color in the output image. At the +% beginning of reduction, n2 = 0 for all nodes except a the leaves of +% the tree which represent colors present in the input image. +% +% The other pixel count, n1, indicates the total number of colors +% within the cubic volume which the node represents. This includes n1 - +% n2 pixels whose colors should be defined by nodes at a lower level in +% the tree. +% +% Assignment generates the output image from the pruned tree. The +% output image consists of two parts: (1) A color map, which is an +% array of color descriptions (RGB triples) for each color present in +% the output image; (2) A pixel array, which represents each pixel as +% an index into the color map array. +% +% First, the assignment phase makes one pass over the pruned color +% description tree to establish the image's color map. For each node +% with n2 > 0, it divides Sr, Sg, and Sb by n2 . This produces the +% mean color of all pixels that classify no lower than this node. Each +% of these colors becomes an entry in the color map. +% +% Finally, the assignment phase reclassifies each pixel in the pruned +% tree to identify the deepest node containing the pixel's color. The +% pixel's value in the pixel array becomes the index of this node's mean +% color in the color map. +% +% With the permission of USC Information Sciences Institute, 4676 Admiralty +% Way, Marina del Rey, California 90292, this code was adapted from module +% ALCOLS written by Paul Raveling. +% +% The names of ISI and USC are not used in advertising or publicity +% pertaining to distribution of the software without prior specific +% written permission from ISI. +% +*/ + + final static boolean QUICK = true; + + final static int MAX_RGB = 255; + final static int MAX_NODES = 266817; + final static int MAX_TREE_DEPTH = 8; + + // these are precomputed in advance + static int SQUARES[]; + static int SHIFT[]; + + static { + SQUARES = new int[MAX_RGB + MAX_RGB + 1]; + for (int i= -MAX_RGB; i <= MAX_RGB; i++) { + SQUARES[i + MAX_RGB] = i * i; + } + + SHIFT = new int[MAX_TREE_DEPTH + 1]; + for (int i = 0; i < MAX_TREE_DEPTH + 1; ++i) { + SHIFT[i] = 1 << (15 - i); + } + } + + /** + * Reduce the image to the given number of colors. The pixels are + * reduced in place. + * @return The new color palette. + */ + public static int[] quantizeImage(int pixels[][], int max_colors) { + Cube cube = new Cube(pixels, max_colors); + cube.classification(); + cube.reduction(); + cube.assignment(); + return cube.colormap; + } + + static class Cube { + int pixels[][]; + int max_colors; + int colormap[]; + + Node root; + int depth; + + // counter for the number of colors in the cube. this gets + // recalculated often. + int colors; + + // counter for the number of nodes in the tree + int nodes; + + Cube(int pixels[][], int max_colors) { + this.pixels = pixels; + this.max_colors = max_colors; + + int i = max_colors; + // tree_depth = log max_colors + // 4 + for (depth = 1; i != 0; depth++) { + i /= 4; + } + if (depth > 1) { + --depth; + } + if (depth > MAX_TREE_DEPTH) { + depth = MAX_TREE_DEPTH; + } else if (depth < 2) { + depth = 2; + } + + root = new Node(this); + } + + /* + * Procedure Classification begins by initializing a color + * description tree of sufficient depth to represent each + * possible input color in a leaf. However, it is impractical + * to generate a fully-formed color description tree in the + * classification phase for realistic values of cmax. If + * colors components in the input image are quantized to k-bit + * precision, so that cmax= 2k-1, the tree would need k levels + * below the root node to allow representing each possible + * input color in a leaf. This becomes prohibitive because the + * tree's total number of nodes is 1 + sum(i=1,k,8k). + * + * A complete tree would require 19,173,961 nodes for k = 8, + * cmax = 255. Therefore, to avoid building a fully populated + * tree, QUANTIZE: (1) Initializes data structures for nodes + * only as they are needed; (2) Chooses a maximum depth for + * the tree as a function of the desired number of colors in + * the output image (currently log2(colormap size)). + * + * For each pixel in the input image, classification scans + * downward from the root of the color description tree. At + * each level of the tree it identifies the single node which + * represents a cube in RGB space containing It updates the + * following data for each such node: + * + * number_pixels : Number of pixels whose color is contained + * in the RGB cube which this node represents; + * + * unique : Number of pixels whose color is not represented + * in a node at lower depth in the tree; initially, n2 = 0 + * for all nodes except leaves of the tree. + * + * total_red/green/blue : Sums of the red, green, and blue + * component values for all pixels not classified at a lower + * depth. The combination of these sums and n2 will + * ultimately characterize the mean color of a set of pixels + * represented by this node. + */ + void classification() { + int pixels[][] = this.pixels; + + int width = pixels.length; + int height = pixels[0].length; + + // convert to indexed color + for (int x = width; x-- > 0; ) { + for (int y = height; y-- > 0; ) { + int pixel = pixels[x][y]; + int red = (pixel >> 16) & 0xFF; + int green = (pixel >> 8) & 0xFF; + int blue = (pixel >> 0) & 0xFF; + + // a hard limit on the number of nodes in the tree + if (nodes > MAX_NODES) { + System.out.println("pruning"); + root.pruneLevel(); + --depth; + } + + // walk the tree to depth, increasing the + // number_pixels count for each node + Node node = root; + for (int level = 1; level <= depth; ++level) { + int id = (((red > node.mid_red ? 1 : 0) << 0) | + ((green > node.mid_green ? 1 : 0) << 1) | + ((blue > node.mid_blue ? 1 : 0) << 2)); + if (node.child[id] == null) { + new Node(node, id, level); + } + node = node.child[id]; + node.number_pixels += SHIFT[level]; + } + + ++node.unique; + node.total_red += red; + node.total_green += green; + node.total_blue += blue; + } + } + } + + /* + * reduction repeatedly prunes the tree until the number of + * nodes with unique > 0 is less than or equal to the maximum + * number of colors allowed in the output image. + * + * When a node to be pruned has offspring, the pruning + * procedure invokes itself recursively in order to prune the + * tree from the leaves upward. The statistics of the node + * being pruned are always added to the corresponding data in + * that node's parent. This retains the pruned node's color + * characteristics for later averaging. + */ + void reduction() { + int threshold = 1; + while (colors > max_colors) { + colors = 0; + threshold = root.reduce(threshold, Integer.MAX_VALUE); + } + } + + /** + * The result of a closest color search. + */ + static class Search { + int distance; + int color_number; + } + + /* + * Procedure assignment generates the output image from the + * pruned tree. The output image consists of two parts: (1) A + * color map, which is an array of color descriptions (RGB + * triples) for each color present in the output image; (2) A + * pixel array, which represents each pixel as an index into + * the color map array. + * + * First, the assignment phase makes one pass over the pruned + * color description tree to establish the image's color map. + * For each node with n2 > 0, it divides Sr, Sg, and Sb by n2. + * This produces the mean color of all pixels that classify no + * lower than this node. Each of these colors becomes an entry + * in the color map. + * + * Finally, the assignment phase reclassifies each pixel in + * the pruned tree to identify the deepest node containing the + * pixel's color. The pixel's value in the pixel array becomes + * the index of this node's mean color in the color map. + */ + void assignment() { + colormap = new int[colors]; + + colors = 0; + root.colormap(); + + int pixels[][] = this.pixels; + + int width = pixels.length; + int height = pixels[0].length; + + Search search = new Search(); + + // convert to indexed color + for (int x = width; x-- > 0; ) { + for (int y = height; y-- > 0; ) { + int pixel = pixels[x][y]; + int red = (pixel >> 16) & 0xFF; + int green = (pixel >> 8) & 0xFF; + int blue = (pixel >> 0) & 0xFF; + + // walk the tree to find the cube containing that color + Node node = root; + for ( ; ; ) { + int id = (((red > node.mid_red ? 1 : 0) << 0) | + ((green > node.mid_green ? 1 : 0) << 1) | + ((blue > node.mid_blue ? 1 : 0) << 2) ); + if (node.child[id] == null) { + break; + } + node = node.child[id]; + } + + if (QUICK) { + // if QUICK is set, just use that + // node. Strictly speaking, this isn't + // necessarily best match. + pixels[x][y] = node.color_number; + } else { + // Find the closest color. + search.distance = Integer.MAX_VALUE; + node.parent.closestColor(red, green, blue, search); + pixels[x][y] = search.color_number; + } + } + } + } + + /** + * A single Node in the tree. + */ + static class Node { + Cube cube; + + // parent node + Node parent; + + // child nodes + Node child[]; + int nchild; + + // our index within our parent + int id; + // our level within the tree + int level; + // our color midpoint + int mid_red; + int mid_green; + int mid_blue; + + // the pixel count for this node and all children + int number_pixels; + + // the pixel count for this node + int unique; + // the sum of all pixels contained in this node + int total_red; + int total_green; + int total_blue; + + // used to build the colormap + int color_number; + + Node(Cube cube) { + this.cube = cube; + this.parent = this; + this.child = new Node[8]; + this.id = 0; + this.level = 0; + + this.number_pixels = Integer.MAX_VALUE; + + this.mid_red = (MAX_RGB + 1) >> 1; + this.mid_green = (MAX_RGB + 1) >> 1; + this.mid_blue = (MAX_RGB + 1) >> 1; + } + + Node(Node parent, int id, int level) { + this.cube = parent.cube; + this.parent = parent; + this.child = new Node[8]; + this.id = id; + this.level = level; + + // add to the cube + ++cube.nodes; + if (level == cube.depth) { + ++cube.colors; + } + + // add to the parent + ++parent.nchild; + parent.child[id] = this; + + // figure out our midpoint + int bi = (1 << (MAX_TREE_DEPTH - level)) >> 1; + mid_red = parent.mid_red + ((id & 1) > 0 ? bi : -bi); + mid_green = parent.mid_green + ((id & 2) > 0 ? bi : -bi); + mid_blue = parent.mid_blue + ((id & 4) > 0 ? bi : -bi); + } + + /** + * Remove this child node, and make sure our parent + * absorbs our pixel statistics. + */ + void pruneChild() { + --parent.nchild; + parent.unique += unique; + parent.total_red += total_red; + parent.total_green += total_green; + parent.total_blue += total_blue; + parent.child[id] = null; + --cube.nodes; + cube = null; + parent = null; + } + + /** + * Prune the lowest layer of the tree. + */ + void pruneLevel() { + if (nchild != 0) { + for (int id = 0; id < 8; id++) { + if (child[id] != null) { + child[id].pruneLevel(); + } + } + } + if (level == cube.depth) { + pruneChild(); + } + } + + /** + * Remove any nodes that have fewer than threshold + * pixels. Also, as long as we're walking the tree: + * + * - figure out the color with the fewest pixels + * - recalculate the total number of colors in the tree + */ + int reduce(int threshold, int next_threshold) { + if (nchild != 0) { + for (int id = 0; id < 8; id++) { + if (child[id] != null) { + next_threshold = child[id].reduce(threshold, next_threshold); + } + } + } + if (number_pixels <= threshold) { + pruneChild(); + } else { + if (unique != 0) { + cube.colors++; + } + if (number_pixels < next_threshold) { + next_threshold = number_pixels; + } + } + return next_threshold; + } + + /* + * colormap traverses the color cube tree and notes each + * colormap entry. A colormap entry is any node in the + * color cube tree where the number of unique colors is + * not zero. + */ + void colormap() { + if (nchild != 0) { + for (int id = 0; id < 8; id++) { + if (child[id] != null) { + child[id].colormap(); + } + } + } + if (unique != 0) { + int r = ((total_red + (unique >> 1)) / unique); + int g = ((total_green + (unique >> 1)) / unique); + int b = ((total_blue + (unique >> 1)) / unique); + cube.colormap[cube.colors] = ((( 0xFF) << 24) | + ((r & 0xFF) << 16) | + ((g & 0xFF) << 8) | + ((b & 0xFF) << 0)); + color_number = cube.colors++; + } + } + + /* ClosestColor traverses the color cube tree at a + * particular node and determines which colormap entry + * best represents the input color. + */ + void closestColor(int red, int green, int blue, Search search) { + if (nchild != 0) { + for (int id = 0; id < 8; id++) { + if (child[id] != null) { + child[id].closestColor(red, green, blue, search); + } + } + } + + if (unique != 0) { + int color = cube.colormap[color_number]; + int distance = distance(color, red, green, blue); + if (distance < search.distance) { + search.distance = distance; + search.color_number = color_number; + } + } + } + + /** + * Figure out the distance between this node and som color. + */ + final static int distance(int color, int r, int g, int b) { + return (SQUARES[((color >> 16) & 0xFF) - r + MAX_RGB] + + SQUARES[((color >> 8) & 0xFF) - g + MAX_RGB] + + SQUARES[((color >> 0) & 0xFF) - b + MAX_RGB]); + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + if (parent == this) { + buf.append("root"); + } else { + buf.append("node"); + } + buf.append(' '); + buf.append(level); + buf.append(" ["); + buf.append(mid_red); + buf.append(','); + buf.append(mid_green); + buf.append(','); + buf.append(mid_blue); + buf.append(']'); + return new String(buf); + } + } + } +}