/**
* Copyright (c) 2003-2009, Xith3D Project Group all rights reserved.
*
* Portions based on the Java3D interface, Copyright by Sun Microsystems.
* Many thanks to the developers of Java3D and Sun Microsystems for their
* innovation and design.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of the 'Xith3D Project Group' nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) A
* RISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE
*/
package org.xith3d.utility.image;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.awt.image.PixelGrabber;
import java.io.IOException;
/**
* @author David Yazel
*/
public class Quantize
{
private final static boolean QUICK = true;
private final static int MAX_RGB = 255;
private final static int MAX_NODES = 266817;
private final static int MAX_TREE_DEPTH = 8;
// these are precomputed in advance
private static int[] SQUARES;
private 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 );
}
}
/**
* Snag the pixels from an image.
*/
static int[][] getPixels( Image image ) throws IOException
{
int w = image.getWidth( null );
int h = image.getHeight( null );
int[] pix = new int[ w * h ];
PixelGrabber grabber = new PixelGrabber( image, 0, 0, w, h, pix, 0, w );
try
{
if ( grabber.grabPixels() != true )
{
throw new IOException( "Grabber returned false: " + grabber.status() );
}
}
catch ( InterruptedException e )
{
e.printStackTrace();
}
int[][] pixels = new int[ w ][ h ];
for ( int x = w; x-- > 0; )
{
for ( int y = h; y-- > 0; )
{
pixels[ x ][ y ] = pix[ ( y * w ) + x ];
}
}
return pixels;
}
/**
* Takes a buffered image and reduces the colors to the
* specified number
*/
public static BufferedImage quantize( BufferedImage source, int max_colors ) throws IOException
{
int[][] pixels = getPixels( source );
int[] palette = quantizeImage( pixels, max_colors );
int w = source.getWidth();
int h = source.getHeight();
BufferedImage dest = new BufferedImage( w, h, BufferedImage.TYPE_INT_RGB );
int[] rgbArray = new int[ w * h ];
for ( int y = h - 1; y >= 0; y-- )
for ( int x = 0; x < w; x++ )
rgbArray[ ( y * w ) + x ] = palette[ pixels[ x ][ y ] ];
dest.setRGB( 0, 0, w, h, rgbArray, 0, w );
return dest;
}
/**
* 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 );
}
}
/**
* 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;
}
}
}
}
/**
* The result of a closest color search.
*/
static class Search
{
int distance;
int 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;
++cube.nodes;
if ( level == cube.depth )
{
++cube.colors;
}
++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 ] );
}
@Override
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 );
}
}
}
}