001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2003-2006, Geotools Project Managment Committee (PMC)
005: * (C) 2001, Institut de Recherche pour le Développement
006: *
007: * This library is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU Lesser General Public
009: * License as published by the Free Software Foundation; either
010: * version 2.1 of the License, or (at your option) any later version.
011: *
012: * This library is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: */
017: package org.geotools.resources.image;
018:
019: // J2SE dependencies
020: import java.awt.Color;
021: import java.awt.color.ColorSpace;
022: import java.awt.image.ColorModel;
023: import java.awt.image.DataBuffer;
024: import java.awt.image.IndexColorModel;
025: import java.math.BigInteger;
026: import java.util.Arrays;
027:
028: // Geotools dependencies
029: import org.geotools.resources.XMath;
030:
031: /**
032: * A set of static methods for handling of colors informations. Some of those methods
033: * are useful, but not really rigorous. This is why they do not appear in any "official"
034: * package, but instead in this private one.
035: *
036: * <strong>Do not rely on this API!</strong>
037: *
038: * It may change in incompatible way in any future version.
039: *
040: * @since 2.0
041: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/coverage/src/main/java/org/geotools/resources/image/ColorUtilities.java $
042: * @version $Id: ColorUtilities.java 26799 2007-08-31 21:31:21Z desruisseaux $
043: * @author Martin Desruisseaux
044: * @author Simone Giannecchini
045: */
046: public final class ColorUtilities {
047: /**
048: * Small number for rounding errors.
049: */
050: private static final double EPS = 1E-6;
051:
052: /**
053: * Do not allow creation of instances of this class.
054: */
055: private ColorUtilities() {
056: }
057:
058: /**
059: * Creates an sRGB color with the specified red, green, blue, and alpha
060: * values in the range (0 - 255).
061: *
062: * @param r the red component
063: * @param g the green component
064: * @param b the blue component
065: * @param a the alpha component
066: * @throws IllegalArgumentException if {@coder}, {@code g}, {@code b}
067: * or {@code a} are outside of the range 0 to 255, inclusive.
068: */
069: public static int getIntFromColor(int r, int g, int b, int a) {
070: return ((a & 0xFF) << 24) | ((r & 0xFF) << 16)
071: | ((g & 0xFF) << 8) | ((b & 0xFF) << 0);
072: }
073:
074: /**
075: * Returns a subarray of the specified color array. The {@code lower} and
076: * {@code upper} index will be clamb into the {@code palette} range.
077: * If they are completly out of range, or if they would result in an empty array,
078: * then {@code null} is returned.
079: *
080: * This method is used by {@link org.geotools.cv.SampleDimension} as an heuristic
081: * approach for distributing palette colors into a list of categories.
082: *
083: * @param palette The color array (may be {@code null}).
084: * @param lower The lower index, inclusive.
085: * @param upper The upper index, inclusive.
086: * @return The subarray (may be {@code palette} if the original array already fit),
087: * or {@code null} if the {@code lower} and {@code upper} index
088: * are out of {@code palette} bounds.
089: */
090: public static Color[] subarray(final Color[] palette, int lower,
091: int upper) {
092: if (palette != null) {
093: lower = Math.max(lower, 0);
094: upper = Math.min(upper, palette.length);
095: if (lower >= upper) {
096: return null;
097: }
098: if (lower != 0 || upper != palette.length) {
099: final Color[] sub = new Color[upper - lower];
100: System.arraycopy(palette, lower, sub, 0, sub.length);
101: return sub;
102: }
103: }
104: return palette;
105: }
106:
107: /**
108: * Copy {@code colors} into array {@code ARGB} from index {@code lower}
109: * inclusive to index {@code upper} exclusive. If {@code upper-lower} is not
110: * equals to the length of {@code colors} array, then colors will be interpolated.
111: *
112: * @param colors Colors to copy into the {@code ARGB} array.
113: * @param ARGB Array of integer to write ARGB values to.
114: * @param lower Index (inclusive) of the first element of {@code ARGB} to change.
115: * @param upper Index (exclusive) of the last element of {@code ARGB} to change.
116: */
117: public static void expand(final Color[] colors, final int[] ARGB,
118: final int lower, final int upper) {
119: switch (colors.length) {
120: case 1:
121: Arrays.fill(ARGB, lower, upper, colors[0].getRGB()); // fall through
122: case 0:
123: return; // Note: getRGB() is really getARGB()
124: }
125: switch (upper - lower) {
126: case 1:
127: ARGB[lower] = colors[0].getRGB(); // fall through
128: case 0:
129: return; // Note: getRGB() is really getARGB()
130: }
131: final int maxBase = colors.length - 2;
132: final double scale = (double) (colors.length - 1)
133: / (double) (upper - 1 - lower);
134: for (int i = lower; i < upper; i++) {
135: final double index = (i - lower) * scale;
136: final int base = Math.min(maxBase, (int) (index + EPS)); // Round toward 0, which is really what we want.
137: final double delta = index - base;
138: final Color C0 = colors[base + 0];
139: final Color C1 = colors[base + 1];
140: int A = C0.getAlpha();
141: int R = C0.getRed();
142: int G = C0.getGreen();
143: int B = C0.getBlue();
144: ARGB[i] = (roundByte(A + delta * (C1.getAlpha() - A)) << 24)
145: | (roundByte(R + delta * (C1.getRed() - R)) << 16)
146: | (roundByte(G + delta * (C1.getGreen() - G)) << 8)
147: | (roundByte(B + delta * (C1.getBlue() - B)) << 0);
148: }
149: }
150:
151: /**
152: * Round a float value and clamp the
153: * result between 0 and 255 inclusive.
154: */
155: public static int roundByte(final double value) {
156: return Math.min(Math.max((int) Math.round(value), 0), 255);
157: }
158:
159: /**
160: * Returns an index color model for specified ARGB codes. If the specified
161: * array has not transparent color (i.e. all alpha values are 255), then the
162: * returned color model will be opaque. Otherwise, if the specified array has
163: * one and only one color with alpha value of 0, the returned color model will
164: * have only this transparent color. Otherwise, the returned color model will
165: * be translucent.
166: *
167: * @param ARGB An array of ARGB values.
168: * @return An index color model for the specified array.
169: */
170: public static IndexColorModel getIndexColorModel(final int[] ARGB) {
171: return getIndexColorModel(ARGB, 1, 0);
172: }
173:
174: /**
175: * Returns a tolerant index color model for the specified ARGB code. This color model accept
176: * image with the specified number of bands.
177: *
178: * @param ARGB An array of ARGB values.
179: * @param numBands The number of bands.
180: * @param visibleBand The band to display.
181: * @return An index color model for the specified array.
182: *
183: * @todo Considerer caching previously created instances using weak references. Index color
184: * model may be big (up to 256 kb), so it may be worth to cache big instances. NOTE:
185: * IndexColorModel inherits a equals(Object) implementation from ColorModel, but do
186: * not override it, so the definition is incomplete.
187: */
188: public static IndexColorModel getIndexColorModel(final int[] ARGB,
189: final int numBands, final int visibleBand) {
190: boolean hasAlpha = false;
191: int transparent = -1;
192: final int length = ARGB.length;
193: for (int i = 0; i < length; i++) {
194: final int alpha = (ARGB[i] & 0xFF000000);
195: if (alpha != 0xFF000000) {
196: if (alpha == 0x00000000 && transparent < 0) {
197: transparent = i;
198: continue;
199: }
200: hasAlpha = true;
201: break;
202: }
203: }
204: final int bits = getBitCount(length);
205: final int type = getTransferType(length);
206: if (numBands == 1) {
207: return new IndexColorModel(bits, length, ARGB, 0, hasAlpha,
208: transparent, type);
209: } else {
210: return new MultiBandsIndexColorModel(bits, length, ARGB, 0,
211: hasAlpha, transparent, type, numBands, visibleBand);
212: }
213: }
214:
215: /**
216: * Returns a bit count for an {@link IndexColorModel} mapping {@code mapSize} colors.
217: * It is guaranteed that the following relation is hold:
218: *
219: * <center><pre>(1 << getBitCount(mapSize)) >= mapSize</pre></center>
220: */
221: public static int getBitCount(final int mapSize) {
222: int max = mapSize - 1;
223: if (max <= 1) {
224: return 1;
225: }
226: int count = 0;
227: do {
228: count++;
229: max >>= 1;
230: } while (max != 0);
231: assert (1 << count) >= mapSize : mapSize;
232: assert (1 << (count - 1)) < mapSize : mapSize;
233: return count;
234: }
235:
236: /**
237: * Returns a suggered type for an {@link IndexColorModel}
238: * of {@code mapSize} colors. This method returns
239: * {@link DataBuffer#TYPE_BYTE} or {@link DataBuffer#TYPE_USHORT}.
240: */
241: public static int getTransferType(final int mapSize) {
242: return (mapSize <= 256) ? DataBuffer.TYPE_BYTE
243: : DataBuffer.TYPE_USHORT;
244: }
245:
246: /**
247: * Transforms a color from XYZ color space to LAB. The color are transformed
248: * in place. This method returns {@code color} for convenience.
249: * Reference: http://www.brucelindbloom.com/index.html?ColorDifferenceCalc.html
250: */
251: public static float[] XYZtoLAB(final float[] color) {
252: color[0] /= 0.9642; // Other refeference: 0.95047;
253: color[1] /= 1.0000; // 1.00000;
254: color[2] /= 0.8249; // 1.08883;
255: for (int i = 0; i < 3; i++) {
256: final float c = color[i];
257: color[i] = (float) ((c > 216 / 24389f) ? Math.pow(c,
258: 1.0 / 3) : ((24389 / 27.0) * c + 16) / 116);
259: }
260: final float L = 116 * color[1] - 16;
261: final float a = 500 * (color[0] - color[1]);
262: final float b = 200 * (color[1] - color[2]);
263: assert !Float.isNaN(L) && !Float.isNaN(a) && !Float.isNaN(b);
264: color[0] = L;
265: color[1] = a;
266: color[2] = b;
267: return color;
268: }
269:
270: /**
271: * Computes the distance E (CIE 1994) between two colors in LAB color space.
272: * Reference: http://www.brucelindbloom.com/index.html?ColorDifferenceCalc.html
273: */
274: public static float colorDistance(final float[] lab1,
275: final float[] lab2) {
276: if (false) {
277: // Compute distance using CIE94 formula.
278: // NOTE: this formula sometime fails because of negative
279: // value in the first Math.sqrt(...) expression.
280: final double dL = (double) lab1[0] - lab2[0];
281: final double da = (double) lab1[1] - lab2[1];
282: final double db = (double) lab1[2] - lab2[2];
283: final double C1 = XMath.hypot(lab1[1], lab1[2]);
284: final double C2 = XMath.hypot(lab2[1], lab2[2]);
285: final double dC = C1 - C2;
286: final double dH = Math.sqrt(da * da + db * db - dC * dC);
287: final double sL = dL / 2;
288: final double sC = dC / (1 + 0.048 * C1);
289: final double sH = dH / (1 + 0.014 * C1);
290: return (float) Math.sqrt(sL * sL + sC * sC + sH * sH);
291: } else {
292: // Compute distance using delta E formula.
293: double sum = 0;
294: for (int i = Math.min(lab1.length, lab2.length); --i >= 0;) {
295: final double delta = lab1[i] - lab2[i];
296: sum += delta * delta;
297: }
298: return (float) Math.sqrt(sum);
299: }
300: }
301:
302: /**
303: * Returns the most transparent pixel in the specified color model. If many colors has
304: * the same alpha value, than the darkest one is returned. This method never returns
305: * a negative value (0 is returned if the color model has no colors).
306: *
307: * @param colors The color model in which to look for a transparent color.
308: * @return The index of a transparent color, or 0.
309: */
310: public static int getTransparentPixel(final IndexColorModel colors) {
311: int index = colors.getTransparentPixel();
312: if (index < 0) {
313: index = 0;
314: int alpha = Integer.MAX_VALUE;
315: float delta = Float.POSITIVE_INFINITY;
316: final ColorSpace space = colors.getColorSpace();
317: final float[] RGB = new float[3];
318: final float[] BLACK = XYZtoLAB(space.toCIEXYZ(RGB)); // Black in Lab color space.
319: assert BLACK != RGB;
320: for (int i = colors.getMapSize(); --i >= 0;) {
321: final int a = colors.getAlpha(i);
322: if (a <= alpha) {
323: RGB[0] = colors.getRed(i) / 255f;
324: RGB[1] = colors.getGreen(i) / 255f;
325: RGB[2] = colors.getBlue(i) / 255f;
326: final float d = colorDistance(XYZtoLAB(space
327: .toCIEXYZ(RGB)), BLACK);
328: assert d >= 0 : i; // Check mostly for NaN value
329: if (a < alpha || d < delta) {
330: alpha = a;
331: delta = d;
332: index = i;
333: }
334: }
335: }
336: }
337: return index;
338: }
339:
340: /**
341: * Returns the index of the specified color, excluding the specified one. If the color
342: * is not explicitly found, a close color is returned. This method never returns a negative
343: * value (0 is returned if the color model has no colors).
344: *
345: * @param colors The color model in which to look for a color index.
346: * @param color The color to search for.
347: * @param exclude An index to exclude from the search (usually the background or the
348: * {@linkplain #getTransparentPixel transparent} pixel), or -1 if none.
349: * @return The index of the color, or 0.
350: */
351: public static int getColorIndex(final IndexColorModel colors,
352: final Color color, final int exclude) {
353: final ColorSpace space = colors.getColorSpace();
354: final float[] RGB = { color.getRed() / 255f,
355: color.getGreen() / 255f, color.getBlue() / 255f };
356: final float[] REF = XYZtoLAB(space.toCIEXYZ(RGB));
357: float delta = Float.POSITIVE_INFINITY;
358: int index = 0;
359: assert REF != RGB;
360: for (int i = colors.getMapSize(); --i >= 0;) {
361: if (i != exclude) {
362: RGB[0] = colors.getRed(i) / 255f;
363: RGB[1] = colors.getGreen(i) / 255f;
364: RGB[2] = colors.getBlue(i) / 255f;
365: final float d = colorDistance(XYZtoLAB(space
366: .toCIEXYZ(RGB)), REF);
367: assert d >= 0 : i; // Check mostly for NaN value
368: if (d <= delta) {
369: delta = d;
370: index = i;
371: }
372: }
373: }
374: return index;
375: }
376:
377: /**
378: * Tries to guess the number of bands from the specified color model. The recommanded approach
379: * is to invoke {@link java.awt.image.SampleModel#getNumBands}. This method should be used only
380: * as a fallback when the sample model is not available. This method uses some heuristic rules
381: * for guessing the number of bands, so the return value may not be exact in all cases.
382: */
383: public static int getNumBands(final ColorModel model) {
384: if (model instanceof IndexColorModel) {
385: if (model instanceof MultiBandsIndexColorModel) {
386: return ((MultiBandsIndexColorModel) model).numBands;
387: }
388: return 1;
389: }
390: return model.getNumComponents();
391: }
392:
393: /**
394: * Tells us if a specific {@link IndexColorModel} contains only gray color
395: * or not, ignoring alpha information.
396: *
397: * @param icm {@link IndexColorModel} to be inspected.
398: * @param ignoreTransparents {@code true} if the RGB values of fully transparent pixels
399: * (the ones with an {@linkplain IndexColorModel#getAlpha(int) alpha} value of 0)
400: * should not be taken in account during the check for gray color.
401: * @return {@code true} if the palette is grayscale, {@code false} otherwise.
402: */
403: public static boolean isGrayPalette(final IndexColorModel icm,
404: boolean ignoreTransparents) {
405: if (!icm.hasAlpha()) {
406: // We will not check transparent pixels if there is none in the color model.
407: ignoreTransparents = false;
408: }
409: final int mapSize = icm.getMapSize();
410: for (int i = 0; i < mapSize; i++) {
411: if (ignoreTransparents) {
412: // If this entry is transparent and we were asked
413: // to check transparents pixels, let's leave.
414: final int a = icm.getAlpha(i);
415: if (a == 0) {
416: continue;
417: }
418: }
419: // get the color for this pixel including the
420: // alpha information only if it is requested.
421: final int r = icm.getRed(i);
422: final int g = icm.getGreen(i);
423: final int b = icm.getBlue(i);
424: // If gray, all components are the same.
425: if (r != g || g != b) {
426: return false;
427: }
428: }
429: return true;
430: }
431: }
|