Replaced Quantize by ColorQuantizer, which was freshly ported from the latest ImageMagick sources and updated with the Alpha Channel support that I added earlier to Qantize. This fixes a nasty bug when quantizing images with few colors (e.g. 32 -> 16 which often resultd in only 4 colors) and increases the general quality of color reduction a lot.
This commit is contained in:
parent
b5f128a341
commit
1f0fdb922d
3 changed files with 636 additions and 2 deletions
634
src/helma/image/ColorQuantizer.java
Normal file
634
src/helma/image/ColorQuantizer.java
Normal file
|
@ -0,0 +1,634 @@
|
||||||
|
/*
|
||||||
|
* Helma License Notice
|
||||||
|
*
|
||||||
|
* The contents of this file are subject to the Helma License
|
||||||
|
* Version 2.0 (the "License"). You may not use this file except in
|
||||||
|
* compliance with the License. A copy of the License is available at
|
||||||
|
* http://adele.helma.org/download/helma/license.txt
|
||||||
|
*
|
||||||
|
* Copyright 1998-2003 Helma Software. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* $RCSfile$
|
||||||
|
* $Author$
|
||||||
|
* $Revision$
|
||||||
|
* $Date$
|
||||||
|
*/
|
||||||
|
|
||||||
|
package helma.image;
|
||||||
|
|
||||||
|
import java.awt.AlphaComposite;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.awt.image.DataBufferByte;
|
||||||
|
import java.awt.image.DataBufferInt;
|
||||||
|
import java.awt.image.IndexColorModel;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Modifications by Juerg Lehni:
|
||||||
|
*
|
||||||
|
* - Support for alpha-channels.
|
||||||
|
* - Returns a BufferedImage of TYPE_BYTE_INDEXED with a IndexColorModel.
|
||||||
|
* - Dithering of images through helma.image.DiffusionFilterOp by setting
|
||||||
|
* the dither parameter to true.
|
||||||
|
* - Support for a transparent color, which is correctly rendered by GIFEncoder.
|
||||||
|
* All pixels with alpha < 0x80 are converted to this color when the parameter
|
||||||
|
* alphaToBitmask is set to true.
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
% %
|
||||||
|
% %
|
||||||
|
% %
|
||||||
|
% 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 %
|
||||||
|
% %
|
||||||
|
% %
|
||||||
|
% Methods to Reduce the Number of Unique Colors in an Image %
|
||||||
|
% %
|
||||||
|
% %
|
||||||
|
% Software Design %
|
||||||
|
% John Cristy %
|
||||||
|
% July 1992 %
|
||||||
|
% %
|
||||||
|
% %
|
||||||
|
% Copyright (C) 2003 ImageMagick Studio, a non-profit organization dedicated %
|
||||||
|
% to making software imaging solutions freely available. %
|
||||||
|
% %
|
||||||
|
% 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 %
|
||||||
|
% ImageMagick Studio 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 ImageMagick Studio %
|
||||||
|
% shall not be used in advertising or otherwise to promote the sale, use or %
|
||||||
|
% other dealings in ImageMagick without prior written authorization from the %
|
||||||
|
% ImageMagick Studio. %
|
||||||
|
% %
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
%
|
||||||
|
% 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 methods 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.
|
||||||
|
%
|
||||||
|
% QuantizeImage() 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 restorage_class 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 storage_class 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, storage_class 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.
|
||||||
|
%
|
||||||
|
% This method is based on a similar algorithm written by Paul Raveling.
|
||||||
|
%
|
||||||
|
%
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class ColorQuantizer {
|
||||||
|
public static final int MAX_NODES = 266817;
|
||||||
|
public static final int MAX_TREE_DEPTH = 8;
|
||||||
|
public static final int MAX_CHILDREN = 16;
|
||||||
|
public static final int MAX_RGB = 255;
|
||||||
|
|
||||||
|
static class ClosestColor {
|
||||||
|
int distance;
|
||||||
|
int colorIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Node {
|
||||||
|
Cube cube;
|
||||||
|
Node parent;
|
||||||
|
Node children[];
|
||||||
|
int numChildren;
|
||||||
|
|
||||||
|
int id;
|
||||||
|
int level;
|
||||||
|
|
||||||
|
int uniqueCount;
|
||||||
|
|
||||||
|
int totalRed;
|
||||||
|
int totalGreen;
|
||||||
|
int totalBlue;
|
||||||
|
int totalAlpha;
|
||||||
|
long quantizeError;
|
||||||
|
|
||||||
|
int colorIndex;
|
||||||
|
|
||||||
|
Node(Cube cube) {
|
||||||
|
this(cube, 0, 0, null);
|
||||||
|
this.parent = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
Node(Cube cube, int id, int level, Node parent) {
|
||||||
|
this.cube = cube;
|
||||||
|
this.parent = parent;
|
||||||
|
this.id = id;
|
||||||
|
this.level = level;
|
||||||
|
this.children = new Node[MAX_CHILDREN];
|
||||||
|
this.numChildren = 0;
|
||||||
|
if (parent != null) {
|
||||||
|
parent.children[id] = this;
|
||||||
|
parent.numChildren++;
|
||||||
|
}
|
||||||
|
cube.numNodes++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pruneLevel() {
|
||||||
|
// Traverse any children.
|
||||||
|
if (numChildren > 0)
|
||||||
|
for (int id = 0; id < MAX_CHILDREN; id++)
|
||||||
|
if (children[id] != null)
|
||||||
|
children[id].pruneLevel();
|
||||||
|
if (level == cube.depth)
|
||||||
|
prune();
|
||||||
|
}
|
||||||
|
|
||||||
|
void pruneToCubeDepth() {
|
||||||
|
// Traverse any children.
|
||||||
|
if (numChildren > 0)
|
||||||
|
for (int id = 0; id < MAX_CHILDREN; id++)
|
||||||
|
if (children[id] != null)
|
||||||
|
children[id].pruneToCubeDepth();
|
||||||
|
if (level > cube.depth)
|
||||||
|
prune();
|
||||||
|
}
|
||||||
|
|
||||||
|
void prune() {
|
||||||
|
// Traverse any children.
|
||||||
|
if (numChildren > 0)
|
||||||
|
for (int id = 0; id < MAX_CHILDREN; id++)
|
||||||
|
if (children[id] != null)
|
||||||
|
children[id].prune();
|
||||||
|
// Merge color statistics into parent.
|
||||||
|
parent.uniqueCount += uniqueCount;
|
||||||
|
parent.totalRed += totalRed;
|
||||||
|
parent.totalGreen += totalGreen;
|
||||||
|
parent.totalBlue += totalBlue;
|
||||||
|
parent.totalAlpha += totalAlpha;
|
||||||
|
parent.children[id] = null;
|
||||||
|
parent.numChildren--;
|
||||||
|
cube.numNodes--;
|
||||||
|
}
|
||||||
|
|
||||||
|
void reduce(long pruningThreshold) {
|
||||||
|
// Traverse any children.
|
||||||
|
if (numChildren > 0)
|
||||||
|
for (int id = 0; id < MAX_CHILDREN; id++)
|
||||||
|
if (children[id] != null)
|
||||||
|
children[id].reduce(pruningThreshold);
|
||||||
|
if (quantizeError <= pruningThreshold)
|
||||||
|
prune();
|
||||||
|
else {
|
||||||
|
// Find minimum pruning threshold.
|
||||||
|
if (uniqueCount > 0)
|
||||||
|
cube.numColors++;
|
||||||
|
if (quantizeError < cube.nextThreshold)
|
||||||
|
cube.nextThreshold = quantizeError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void findClosestColor(int red, int green, int blue, int alpha, ClosestColor closest) {
|
||||||
|
// Traverse any children.
|
||||||
|
if (numChildren > 0)
|
||||||
|
for (int id = 0; id < MAX_CHILDREN; id++)
|
||||||
|
if (children[id] != null)
|
||||||
|
children[id].findClosestColor(red, green, blue, alpha, closest);
|
||||||
|
if (uniqueCount != 0) {
|
||||||
|
// Determine if this color is "closest".
|
||||||
|
int r = (cube.colorMap[0][colorIndex] & 0xff) - red;
|
||||||
|
int distance = r * r;
|
||||||
|
if (distance < closest.distance) {
|
||||||
|
int g = (cube.colorMap[1][colorIndex] & 0xff) - green;
|
||||||
|
distance += g * g;
|
||||||
|
if (distance < closest.distance) {
|
||||||
|
int b = (cube.colorMap[2][colorIndex] & 0xff) - blue;
|
||||||
|
distance += b * b;
|
||||||
|
if (distance < closest.distance) {
|
||||||
|
int a = (cube.colorMap[3][colorIndex] & 0xff) - alpha;
|
||||||
|
distance += a * a;
|
||||||
|
if (distance < closest.distance) {
|
||||||
|
closest.distance = distance;
|
||||||
|
closest.colorIndex = colorIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int fillColorMap(byte colorMap[][], int index) {
|
||||||
|
// Traverse any children.
|
||||||
|
if (numChildren > 0)
|
||||||
|
for (int id = 0; id < MAX_CHILDREN; id++)
|
||||||
|
if (children[id] != null)
|
||||||
|
index = children[id].fillColorMap(colorMap, index);
|
||||||
|
if (uniqueCount != 0) {
|
||||||
|
// Colormap entry is defined by the mean color in this cube.
|
||||||
|
colorMap[0][index] = (byte) (totalRed / uniqueCount + 0.5);
|
||||||
|
colorMap[1][index] = (byte) (totalGreen / uniqueCount + 0.5);
|
||||||
|
colorMap[2][index] = (byte) (totalBlue / uniqueCount + 0.5);
|
||||||
|
colorMap[3][index] = (byte) (totalAlpha / uniqueCount + 0.5);
|
||||||
|
colorIndex = index++;
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Cube {
|
||||||
|
Node root;
|
||||||
|
|
||||||
|
int numColors;
|
||||||
|
boolean addTransparency;
|
||||||
|
// firstColor is set to 1 when when addTransparency is true!
|
||||||
|
int firstColor;
|
||||||
|
byte colorMap[][];
|
||||||
|
|
||||||
|
long nextThreshold;
|
||||||
|
|
||||||
|
int numNodes;
|
||||||
|
int depth;
|
||||||
|
|
||||||
|
Cube(int maxColors) {
|
||||||
|
this.depth = getDepth(maxColors);
|
||||||
|
this.numColors = 0;
|
||||||
|
root = new Node(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
int getDepth(int numColors) {
|
||||||
|
// Depth of color tree is: Log4(colormap size)+2.
|
||||||
|
int depth;
|
||||||
|
for (depth = 1; numColors != 0; depth++)
|
||||||
|
numColors >>= 2;
|
||||||
|
if (depth > MAX_TREE_DEPTH)
|
||||||
|
depth = MAX_TREE_DEPTH;
|
||||||
|
if (depth < 2)
|
||||||
|
depth = 2;
|
||||||
|
return depth;
|
||||||
|
}
|
||||||
|
|
||||||
|
void classifyImageColors(BufferedImage image, boolean alphaToBitmask) {
|
||||||
|
addTransparency = false;
|
||||||
|
firstColor = 0;
|
||||||
|
|
||||||
|
Node node, child;
|
||||||
|
int x, px, y, index, level, id, count;
|
||||||
|
int pixel, red, green, blue, alpha;
|
||||||
|
int bisect, midRed, midGreen, midBlue, midAlpha;
|
||||||
|
|
||||||
|
int width = image.getWidth();
|
||||||
|
int height = image.getHeight();
|
||||||
|
|
||||||
|
// Classify the first 256 colors to a tree depth of MAX_TREE_DEPTH.
|
||||||
|
int levelThreshold = MAX_TREE_DEPTH;
|
||||||
|
// create a BufferedImage of only 1 pixel height for fetching the
|
||||||
|
// rows
|
||||||
|
// of the image in the correct format (ARGB)
|
||||||
|
// This speeds up things by more than factor 2, compared to the
|
||||||
|
// standard
|
||||||
|
// BufferedImage.getRGB solution
|
||||||
|
BufferedImage row = new BufferedImage(width, 1, BufferedImage.TYPE_INT_ARGB);
|
||||||
|
Graphics2D g2d = row.createGraphics();
|
||||||
|
int pixels[] = ((DataBufferInt) row.getRaster().getDataBuffer()).getData();
|
||||||
|
// make sure alpha values do not add up for each row:
|
||||||
|
g2d.setComposite(AlphaComposite.Src);
|
||||||
|
// calculate scanline by scanline in order to safe memory.
|
||||||
|
// It also seems to run faster like that
|
||||||
|
for (y = 0; y < height; y++) {
|
||||||
|
g2d.drawImage(image, null, 0, -y);
|
||||||
|
// now pixels contains the rgb values of the row y!
|
||||||
|
if (numNodes > MAX_NODES) {
|
||||||
|
// Prune one level if the color tree is too large.
|
||||||
|
root.pruneLevel();
|
||||||
|
depth--;
|
||||||
|
}
|
||||||
|
for (x = 0; x < width;) {
|
||||||
|
pixel = pixels[x];
|
||||||
|
red = (pixel >> 16) & 0xff;
|
||||||
|
green = (pixel >> 8) & 0xff;
|
||||||
|
blue = (pixel >> 0) & 0xff;
|
||||||
|
alpha = (pixel >> 24) & 0xff;
|
||||||
|
if (alphaToBitmask)
|
||||||
|
alpha = alpha < 0x80 ? 0 : 0xff;
|
||||||
|
|
||||||
|
// skip same pixels, but count them
|
||||||
|
px = x;
|
||||||
|
for (++x; x < width; x++)
|
||||||
|
if (pixels[x] != pixel)
|
||||||
|
break;
|
||||||
|
count = x - px;
|
||||||
|
|
||||||
|
// Start at the root and descend the color cube tree.
|
||||||
|
if (alpha > 0) {
|
||||||
|
index = MAX_TREE_DEPTH - 1;
|
||||||
|
bisect = (MAX_RGB + 1) >> 1;
|
||||||
|
midRed = bisect;
|
||||||
|
midGreen = bisect;
|
||||||
|
midBlue = bisect;
|
||||||
|
midAlpha = bisect;
|
||||||
|
node = root;
|
||||||
|
for (level = 1; level <= levelThreshold; level++) {
|
||||||
|
id = (((red >> index) & 0x01) << 3 |
|
||||||
|
((green >> index) & 0x01) << 2 |
|
||||||
|
((blue >> index) & 0x01) << 1 |
|
||||||
|
((alpha >> index) & 0x01));
|
||||||
|
bisect >>= 1;
|
||||||
|
midRed += (id & 8) != 0 ? bisect : -bisect;
|
||||||
|
midGreen += (id & 4) != 0 ? bisect : -bisect;
|
||||||
|
midBlue += (id & 2) != 0 ? bisect : -bisect;
|
||||||
|
midAlpha += (id & 1) != 0 ? bisect : -bisect;
|
||||||
|
child = node.children[id];
|
||||||
|
if (child == null) {
|
||||||
|
// Set colors of new node to contain pixel.
|
||||||
|
child = new Node(this, id, level, node);
|
||||||
|
if (level == levelThreshold) {
|
||||||
|
numColors++;
|
||||||
|
if (numColors == 256) {
|
||||||
|
// More than 256 colors; classify to the
|
||||||
|
// cube_info.depth tree depth.
|
||||||
|
levelThreshold = depth;
|
||||||
|
root.pruneToCubeDepth();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Approximate the quantization error represented by
|
||||||
|
// this node.
|
||||||
|
node = child;
|
||||||
|
int r = red - midRed;
|
||||||
|
int g = green - midGreen;
|
||||||
|
int b = blue - midBlue;
|
||||||
|
int a = alpha - midAlpha;
|
||||||
|
node.quantizeError += count * (r * r + g * g + b * b + a * a);
|
||||||
|
root.quantizeError += node.quantizeError;
|
||||||
|
index--;
|
||||||
|
}
|
||||||
|
// Sum RGB for this leaf for later derivation of the mean
|
||||||
|
// cube color.
|
||||||
|
node.uniqueCount += count;
|
||||||
|
node.totalRed += count * red;
|
||||||
|
node.totalGreen += count * green;
|
||||||
|
node.totalBlue += count * blue;
|
||||||
|
node.totalAlpha += count * alpha;
|
||||||
|
} else if (!addTransparency) {
|
||||||
|
addTransparency = true;
|
||||||
|
numColors++;
|
||||||
|
firstColor = 1; // start at 1 as 0 will be the transparent color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void reduceImageColors(int maxColors) {
|
||||||
|
nextThreshold = 0;
|
||||||
|
while (numColors > maxColors) {
|
||||||
|
long pruningThreshold = nextThreshold;
|
||||||
|
nextThreshold = root.quantizeError - 1;
|
||||||
|
numColors = firstColor;
|
||||||
|
root.reduce(pruningThreshold);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BufferedImage assignImageColors(BufferedImage image, boolean dither, boolean alphaToBitmask) {
|
||||||
|
// Allocate image colormap.
|
||||||
|
colorMap = new byte[4][numColors];
|
||||||
|
root.fillColorMap(colorMap, firstColor);
|
||||||
|
// create the right color model, depending on transparency settings:
|
||||||
|
IndexColorModel icm;
|
||||||
|
|
||||||
|
int colorDepth = getDepth(numColors);
|
||||||
|
int width = image.getWidth();
|
||||||
|
int height = image.getHeight();
|
||||||
|
|
||||||
|
if (alphaToBitmask) {
|
||||||
|
if (addTransparency) {
|
||||||
|
icm = new IndexColorModel(depth, numColors, colorMap[0], colorMap[1], colorMap[2], 0);
|
||||||
|
} else {
|
||||||
|
icm = new IndexColorModel(depth, numColors, colorMap[0], colorMap[1], colorMap[2]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
icm = new IndexColorModel(depth, numColors, colorMap[0], colorMap[1], colorMap[2], colorMap[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the indexed BufferedImage:
|
||||||
|
BufferedImage dest = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, icm);
|
||||||
|
|
||||||
|
boolean firstOut = true;
|
||||||
|
if (dither)
|
||||||
|
new DiffusionFilterOp().filter(image, dest);
|
||||||
|
else {
|
||||||
|
ClosestColor closest = new ClosestColor();
|
||||||
|
// convert to indexed color
|
||||||
|
byte[] dst = ((DataBufferByte) dest.getRaster().getDataBuffer()).getData();
|
||||||
|
|
||||||
|
// create a BufferedImage of only 1 pixel height for fetching
|
||||||
|
// the rows of the image in the correct format (ARGB)
|
||||||
|
// This speeds up things by more than factor 2, compared to the
|
||||||
|
// standard BufferedImage.getRGB solution
|
||||||
|
BufferedImage row = new BufferedImage(width, 1, BufferedImage.TYPE_INT_ARGB);
|
||||||
|
Graphics2D g2d = row.createGraphics();
|
||||||
|
int pixels[] = ((DataBufferInt) row.getRaster().getDataBuffer()).getData();
|
||||||
|
// make sure alpha values do not add up for each row:
|
||||||
|
g2d.setComposite(AlphaComposite.Src);
|
||||||
|
// calculate scanline by scanline in order to safe memory.
|
||||||
|
// It also seems to run faster like that
|
||||||
|
Node node;
|
||||||
|
int x, y, i, id;
|
||||||
|
int pixel, red, green, blue, alpha;
|
||||||
|
int pos = 0;
|
||||||
|
for (y = 0; y < height; y++) {
|
||||||
|
g2d.drawImage(image, null, 0, -y);
|
||||||
|
// now pixels contains the rgb values of the row y!
|
||||||
|
// filter this row now:
|
||||||
|
for (x = 0; x < width;) {
|
||||||
|
pixel = pixels[x];
|
||||||
|
red = (pixel >> 16) & 0xff;
|
||||||
|
green = (pixel >> 8) & 0xff;
|
||||||
|
blue = (pixel >> 0) & 0xff;
|
||||||
|
alpha = (pixel >> 24) & 0xff;
|
||||||
|
|
||||||
|
if (alphaToBitmask)
|
||||||
|
alpha = alpha < 128 ? 0 : 0xff;
|
||||||
|
|
||||||
|
byte col;
|
||||||
|
if (alpha == 0 && addTransparency) {
|
||||||
|
col = 0; // transparency color is at position 0 of color map
|
||||||
|
} else {
|
||||||
|
// walk the tree to find the cube containing that
|
||||||
|
// color
|
||||||
|
node = root;
|
||||||
|
for (i = MAX_TREE_DEPTH - 1; i > 0; i--) {
|
||||||
|
id = (((red >> i) & 0x01) << 3 |
|
||||||
|
((green >> i) & 0x01) << 2 |
|
||||||
|
((blue >> i) & 0x01) << 1 |
|
||||||
|
((alpha >> i) & 0x01));
|
||||||
|
if (node.children[id] == null)
|
||||||
|
break;
|
||||||
|
node = node.children[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the closest color.
|
||||||
|
closest.distance = Integer.MAX_VALUE;
|
||||||
|
node.parent.findClosestColor(red, green, blue, alpha, closest);
|
||||||
|
col = (byte) closest.colorIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// first color
|
||||||
|
dst[pos++] = col;
|
||||||
|
|
||||||
|
// next colors the same?
|
||||||
|
for (++x; x < width; x++) {
|
||||||
|
if (pixels[x] != pixel)
|
||||||
|
break;
|
||||||
|
dst[pos++] = col;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static BufferedImage quantizeImage(BufferedImage image, int maxColors, boolean dither, boolean alphaToBitmask) {
|
||||||
|
Cube cube = new Cube(maxColors);
|
||||||
|
cube.classifyImageColors(image, alphaToBitmask);
|
||||||
|
cube.reduceImageColors(maxColors);
|
||||||
|
return cube.assignImageColors(image, dither, alphaToBitmask);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -99,7 +99,7 @@ public class GIFEncoder {
|
||||||
|
|
||||||
// make sure it's index colors:
|
// make sure it's index colors:
|
||||||
if (bi.getType() != BufferedImage.TYPE_BYTE_INDEXED)
|
if (bi.getType() != BufferedImage.TYPE_BYTE_INDEXED)
|
||||||
bi = Quantize.process(bi, 256, false, true);
|
bi = ColorQuantizer.quantizeImage(bi, 256, false, true);
|
||||||
|
|
||||||
raster = bi.getRaster();
|
raster = bi.getRaster();
|
||||||
|
|
||||||
|
|
|
@ -540,7 +540,7 @@ public class ImageWrapper {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public void reduceColors(int colors, boolean dither, boolean alphaToBitmask) {
|
public void reduceColors(int colors, boolean dither, boolean alphaToBitmask) {
|
||||||
setImage(Quantize.process(getBufferedImage(), colors, dither,
|
setImage(ColorQuantizer.quantizeImage(getBufferedImage(), colors, dither,
|
||||||
alphaToBitmask));
|
alphaToBitmask));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue