/*
* Copyright 2010-2015 Institut Pasteur.
*
* This file is part of Icy.
*
* Icy is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Icy is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Icy. If not, see <http://www.gnu.org/licenses/>.
*/
package icy.util;
import icy.math.MathUtil;
import java.awt.Color;
import java.awt.color.ColorSpace;
/**
* Color utilities class.
*
* @author Stephane
*/
public class ColorUtil
{
/**
* RGB colorSpace
*/
public final static ColorSpace sRGB = ColorSpace.getInstance(ColorSpace.CS_sRGB);
/**
* Basic rainbow colors
*/
private final static Color[] colors = generateRainbow(32, true, true, true);
/**
* Returns a random color.
*/
public static Color getRandomColor()
{
return colors[Random.nextInt(20)];
}
/**
* Generates a rainbow color table (HSV ramp) of the specified size.
*
* @param saturation
* saturation factor (from 0 to 1).
* @param brightness
* brightness factor (from 0 to 1).
* @param size
* the size of rainbow color table.
* @param black
* if true the table will also contains a black color entry.
* @param white
* if true the table will also contains a white color entry.
* @param gray
* if true the table will also contains a gray color entry.
*/
public static Color[] generateRainbow(float saturation, float brightness, int size, boolean black, boolean white,
boolean gray)
{
final Color[] result = new Color[size];
int start = 0;
if (black)
result[start++] = Color.black;
if (white)
result[start++] = Color.white;
if (gray)
result[start++] = Color.gray;
for (int i = start; i < result.length; i++)
result[i] = Color.getHSBColor((float) (i - start) / (float) (size - start), saturation, brightness);
return result;
}
/**
* Generates a rainbow color table (HSV ramp) of the specified size.
*
* @param size
* the size of the rainbow color table.
* @param black
* if true the table will also contains a black color entry.
* @param white
* if true the table will also contains a white color entry.
* @param gray
* if true the table will also contains a gray color entry.
*/
public static Color[] generateRainbow(int size, boolean black, boolean white, boolean gray)
{
return generateRainbow(1f, 1f, size, black, white, gray);
}
/**
* Generates a rainbow color table (HSV ramp) of the specified size.
*
* @param size
* the size of the HSV color table.
*/
public static Color[] generateRainbow(int size)
{
return generateRainbow(size, false, false, false);
}
/**
* Get String representation of the specified color.<br>
* <br>
* Default representation is "A:R:G:B" where :<br>
* A = alpha level in hexadecimal (0x00-0xFF)<br>
* R = red level in hexadecimal (0x00-0xFF)<br>
* G = green level in hexadecimal (0x00-0xFF)<br>
* B = blue level in hexadecimal (0x00-0xFF)<br>
*
* @param color
*/
public static String toString(Color color)
{
return toString(color, true, ":");
}
/**
* Get String representation of the specified rgb value.<br>
* <br>
* Default representation is "A:R:G:B" where :<br>
* A = alpha level in hexadecimal (00-FF)<br>
* R = red level in hexadecimal (00-FF)<br>
* G = green level in hexadecimal (00-FF)<br>
* B = blue level in hexadecimal (00-FF)<br>
*
* @param rgb
*/
public static String toString(int rgb)
{
return toString(rgb, true, ":");
}
/**
* Get String representation of the specified Color value.<br>
* <br>
* Default representation is "A:R:G:B" where :<br>
* A = alpha level<br>
* R = red level<br>
* G = green level<br>
* B = blue level<br>
*
* @param color
* @param hexa
* component level are represented in hexadecimal (2 digits)
*/
public static String toString(Color color, boolean hexa)
{
return toString(color, hexa, ":");
}
/**
* Get String representation of the specified rgb value.<br>
* <br>
* Default representation is "A:R:G:B" where :<br>
* A = alpha level<br>
* R = red level<br>
* G = green level<br>
* B = blue level<br>
*
* @param rgb
* @param hexa
* component level are represented in hexadecimal (2 digits)
*/
public static String toString(int rgb, boolean hexa)
{
return toString(rgb, hexa, ":");
}
/**
* Get String representation of the specified color.<br>
* <br>
* Default representation is "AsepRsepGsepB" where :<br>
* A = alpha level in hexadecimal (0x00-0xFF)<br>
* R = red level in hexadecimal (0x00-0xFF)<br>
* G = green level in hexadecimal (0x00-0xFF)<br>
* B = blue level in hexadecimal (0x00-0xFF)<br>
* sep = the specified separator<br>
* <br>
* Ex : toString(Color.red, true, ":") --> "FF:FF:00:00"
*
* @param color
* @param hexa
* component level are represented in hexadecimal (2 digits)
*/
public static String toString(Color color, boolean hexa, String sep)
{
if (color == null)
return "-";
return toString(color.getRGB(), hexa, sep);
}
/**
* Get String representation of the specified rgb value.<br>
* <br>
* Default representation is "AsepRsepGsepB" where :<br>
* A = alpha level in hexadecimal (0x00-0xFF)<br>
* R = red level in hexadecimal (0x00-0xFF)<br>
* G = green level in hexadecimal (0x00-0xFF)<br>
* B = blue level in hexadecimal (0x00-0xFF)<br>
* sep = the specified separator<br>
* <br>
* Ex : toString(0xFF00FF00, true, ":") --> "FF:00:FF:00"
*
* @param rgb
* @param hexa
* component level are represented in hexadecimal (2 digits)
*/
public static String toString(int rgb, boolean hexa, String sep)
{
final int a = (rgb >> 24) & 0xFF;
final int r = (rgb >> 16) & 0xFF;
final int g = (rgb >> 8) & 0xFF;
final int b = (rgb >> 0) & 0xFF;
if (hexa)
return (StringUtil.toHexaString(a, 2) + sep + StringUtil.toHexaString(r, 2) + sep
+ StringUtil.toHexaString(g, 2) + sep + StringUtil.toHexaString(b, 2)).toUpperCase();
return StringUtil.toString(a) + sep + StringUtil.toString(r) + sep + StringUtil.toString(g) + sep
+ StringUtil.toString(b);
}
/**
* Returns <code>true</code> if the specified color is pure black (alpha is not verified)
*/
public static boolean isBlack(Color color)
{
return (color.getRGB() & 0x00FFFFFF) == 0;
}
/**
* Mix 2 colors with priority color
*/
public static Color mixOver(Color backColor, Color frontColor)
{
final int r, g, b, a;
final float frontAlpha = frontColor.getAlpha() / 255f;
final float invAlpha = 1f - frontAlpha;
r = (int) ((backColor.getRed() * invAlpha) + (frontColor.getRed() * frontAlpha));
g = (int) ((backColor.getGreen() * invAlpha) + (frontColor.getGreen() * frontAlpha));
b = (int) ((backColor.getBlue() * invAlpha) + (frontColor.getBlue() * frontAlpha));
a = Math.max(backColor.getAlpha(), frontColor.getAlpha());
return new Color(r, g, b, a);
}
/**
* Mix 2 colors using the following ratio for mixing:<br/>
* 0f means 100% of color 1 and 0% of color 2<br/>
* 0.5f means 50% of color 1 and 50% of color 2<br/>
* 1f means 0% of color 1 and 100% of color 2
*/
public static Color mix(Color c1, Color c2, float ratio)
{
final int r, g, b;
final float r2 = Math.min(1f, Math.max(0f, ratio));
final float r1 = 1f - r2;
r = (int) ((c1.getRed() * r1) + (c2.getRed() * r2));
g = (int) ((c1.getGreen() * r1) + (c2.getGreen() * r2));
b = (int) ((c1.getBlue() * r1) + (c2.getBlue() * r2));
return new Color(r, g, b);
}
/**
* Mix 2 colors without "priority" color
*/
public static Color mix(Color c1, Color c2, boolean useAlpha)
{
final int r, g, b, a;
if (useAlpha)
{
final float a1 = c1.getAlpha() / 255f;
final float a2 = c2.getAlpha() / 255f;
final float af = a1 + a2;
r = (int) (((c1.getRed() * a1) + (c2.getRed() * a2)) / af);
g = (int) (((c1.getGreen() * a1) + (c2.getGreen() * a2)) / af);
b = (int) (((c1.getBlue() * a1) + (c2.getBlue() * a2)) / af);
a = Math.max(c1.getAlpha(), c2.getAlpha());
}
else
{
r = (c1.getRed() + c2.getRed()) / 2;
g = (c1.getGreen() + c2.getGreen()) / 2;
b = (c1.getBlue() + c2.getBlue()) / 2;
a = 255;
}
return new Color(r, g, b, a);
}
/**
* Mix 2 colors (no alpha)
*/
public static Color mix(Color c1, Color c2)
{
return mix(c1, c2, false);
}
/**
* Add 2 colors
*/
public static Color add(Color c1, Color c2, boolean useAlpha)
{
final int r, g, b, a;
r = Math.min(c1.getRed() + c2.getRed(), 255);
g = Math.min(c1.getGreen() + c2.getGreen(), 255);
b = Math.min(c1.getBlue() + c2.getBlue(), 255);
if (useAlpha)
a = Math.max(c1.getAlpha(), c2.getAlpha());
else
a = 255;
return new Color(r, g, b, a);
}
/**
* Add 2 colors
*/
public static Color add(Color c1, Color c2)
{
return add(c1, c2, false);
}
/**
* Sub 2 colors
*/
public static Color sub(Color c1, Color c2, boolean useAlpha)
{
final int r, g, b, a;
r = Math.max(c1.getRed() - c2.getRed(), 0);
g = Math.max(c1.getGreen() - c2.getGreen(), 0);
b = Math.max(c1.getBlue() - c2.getBlue(), 0);
if (useAlpha)
a = Math.max(c1.getAlpha(), c2.getAlpha());
else
a = 255;
return new Color(r, g, b, a);
}
/**
* Subtract 2 colors
*/
public static Color sub(Color c1, Color c2)
{
return sub(c1, c2, false);
}
/**
* Get opposite (XORed) color
*/
public static Color xor(Color c)
{
return new Color(c.getRed() ^ 0xFF, c.getGreen() ^ 0xFF, c.getBlue() ^ 0xFF, c.getAlpha());
}
/**
* get to gray level (simple RGB mix)
*/
public static int getGrayMix(Color c)
{
return getGrayMix(c.getRGB());
}
/**
* get to gray level (simple RGB mix)
*/
public static int getGrayMix(int rgb)
{
return (((rgb >> 16) & 0xFF) + ((rgb >> 8) & 0xFF) + ((rgb >> 0) & 0xFF)) / 3;
}
/**
* Convert to gray level color (simple RGB mix)
*/
public static Color getGrayColorMix(Color c)
{
final int gray = getGrayMix(c);
return new Color(gray, gray, gray);
}
/**
* Convert to gray level color (from luminance calculation)
*/
public static Color getGrayColorLum(Color c)
{
final int gray = getLuminance(c);
return new Color(gray, gray, gray);
}
/**
* Return luminance (in [0..255] range)
*/
public static int getLuminance(Color c)
{
return (int) ((c.getRed() * 0.299) + (c.getGreen() * 0.587) + (c.getBlue() * 0.114));
}
/**
* Convert the specified color to HSV color.
*/
public static float[] toHSV(Color c)
{
return toHSV(c.getRGBColorComponents(null));
}
/**
* Convert the specified RGB color to HSV color.
*/
public static float[] toHSV(float[] rgb)
{
float r = rgb[0];
float g = rgb[1];
float b = rgb[2];
float min, max, delta;
float h, s, v;
min = Math.min(r, Math.min(g, b));
max = Math.max(r, Math.max(g, b));
// black
if (max == 0f)
return new float[] {0, 0, 0};
v = max;
delta = max - min;
s = delta / max;
// graylevel
if (delta == 0f)
return new float[] {0, s, v};
if (r == max)
// between yellow & magenta
h = (g - b) / delta;
else if (g == max)
// between cyan & yellow
h = 2 + (b - r) / delta;
else
// between magenta & cyan
h = 4 + (r - g) / delta;
// want positif hue
if (h < 0)
h += 6f;
return new float[] {h / 6f, s, v};
}
/**
* Convert the specified HSV color to RGB color.
*/
public static float[] fromHSV(float[] hsv)
{
float h = hsv[0];
float s = hsv[0];
float v = hsv[0];
float f, p, q, t;
float r, g, b;
int i;
// no color
if (s == 0f)
return new float[] {v, v, v};
// sector 0 to 5
h *= 6f;
i = (int) Math.floor(h);
// factorial part of h
f = h - i;
p = v * (1f - s);
q = v * (1f - (s * f));
t = v * (1f - (s * (1 - f)));
switch (i)
{
case 0:
r = v;
g = t;
b = p;
break;
case 1:
r = q;
g = v;
b = p;
break;
case 2:
r = p;
g = v;
b = t;
break;
case 3:
r = p;
g = q;
b = v;
break;
case 4:
r = t;
g = p;
b = v;
break;
default:
r = v;
g = p;
b = q;
break;
}
return new float[] {r, g, b};
}
/**
* Convert the specified XYZ color to RGB color.
*/
public static float[] fromXYZ(float[] xyz)
{
return sRGB.fromCIEXYZ(xyz);
}
/**
* Convert the specified color to XYZ color.
*/
public static float[] toXYZ(Color c)
{
return toXYZ(c.getRGBColorComponents(null));
}
/**
* Convert the specified RGB color to XYZ color.
*/
public static float[] toXYZ(float[] rgb)
{
return sRGB.toCIEXYZ(rgb);
}
/**
* Convert the specified color to LAB color.
*/
public static float[] toLAB(Color c)
{
return toLAB(c.getRGBColorComponents(null));
}
/**
* Convert the specified RGB color to LAB color.
*/
public static float[] toLAB(float[] rgb)
{
return XYZtoLAB(toXYZ(rgb));
}
private static float pivotXYZ(float value)
{
return (value > 0.008856f) ? (float) MathUtil.cubicRoot(value) : (7.787f * value) + 0.1379f;
}
/**
* Convert the specified XYZ color to LAB color.
*/
public static float[] XYZtoLAB(float[] xyz)
{
float x = pivotXYZ(xyz[0] / 95.047f);
float y = pivotXYZ(xyz[1] / 100f);
float z = pivotXYZ(xyz[2] / 108.883f);
float l = Math.max(0, (116f * y) - 16f);
float a = 500f * (x - y);
float b = 200f * (y - z);
return new float[] {l, a, b};
}
/**
* Compute and returns the distance between the 2 colors.<br>
* The HSV distance returns a value between 0 and 1 where 1 is maximum distance.<br>
* The LAB distance returns a positive value where > 2.3 value is considered a
* significant distance.
*
* @param c1
* first color
* @param c2
* second color
* @param hsv
* If set to true we use the HSV color space to compute the color distance otherwise we
* use the LAB color space.
*/
public static double getDistance(Color c1, Color c2, boolean hsv)
{
if (hsv)
{
// use HSV color space
final float[] hsv1 = toHSV(c1);
final float[] hsv2 = toHSV(c2);
return getDistance(hsv1, hsv2, true);
}
// use LAB color space
final float[] lab1 = toLAB(c1);
final float[] lab2 = toLAB(c2);
return getDistance(lab1, lab2, true);
}
/**
* Returns the distance between 2 colors from same color space.
*/
static double getDistance(float[] c1, float[] c2, boolean compareThirdComponent)
{
float result = (float) (Math.pow(c1[0] - c2[0], 2d) + Math.pow(c1[1] - c2[1], 2d));
if (compareThirdComponent)
result += Math.pow(c1[2] - c2[2], 2d);
return result;
}
/**
* Returns the dominant color from the specified color array.<br>
* The dominant color is calculated by computing the color histogram from a rainbow gradient and
* returning the highest bin number.
*/
public static Color getDominantColor(Color colors[])
{
return getDominantColor(colors, 33);
}
/**
* Returns the dominant color from the specified color array.<br>
* The dominant color is calculated by computing the color histogram from a rainbow gradient and
* returning the color corresponding to the highest bin.
*
* @param colors
* Color array we want to retrieve the dominant color from.
* @param binNumber
* the number of bin to construct the rainbow gradient.
*/
public static Color getDominantColor(Color colors[], int binNumber)
{
final Color[] baseColors = generateRainbow(1f, 1f, binNumber, false, false, true);
final float[][] colorsHSV = new float[colors.length][];
final float[][] baseColorsHSV = new float[binNumber][];
// convert colors to HSV float component
for (int i = 0; i < colors.length; i++)
colorsHSV[i] = toHSV(colors[i]);
for (int i = 0; i < baseColors.length; i++)
baseColorsHSV[i] = toHSV(baseColors[i]);
final int[] bins = new int[binNumber];
for (float[] colorHsv : colorsHSV)
{
double minDist = getDistance(colorHsv, baseColorsHSV[0], true);
int minInd = 0;
for (int ind = 1; ind < baseColorsHSV.length; ind++)
{
final double dist = getDistance(colorHsv, baseColorsHSV[ind], true);
if (dist < minDist)
{
minDist = dist;
minInd = ind;
}
}
bins[minInd]++;
}
int max = bins[0];
int maxInd = 0;
for (int i = 1; i < bins.length; i++)
{
final int v = bins[i];
if (v > max)
{
max = v;
maxInd = i;
}
}
return baseColors[maxInd];
}
/**
* Converts a wavelength into a {@link Color} object.<br/>
* Taken from Earl F. Glynn's web page:
* <a href="http://www.efg2.com/Lab/ScienceAndEngineering/Spectra.htm">Spectra Lab Report</a>
*
* @param wavelength
* the wavelength to convert (in nanometers)
* @return a {@link Color} object representing the specified wavelength
*/
public static Color getColorFromWavelength(double wavelength)
{
double factor;
double r, g, b;
if ((wavelength >= 380) && (wavelength < 440))
{
r = -(wavelength - 440) / (440 - 380);
g = 0.0;
b = 1.0;
}
else if ((wavelength >= 440) && (wavelength < 490))
{
r = 0.0;
g = (wavelength - 440) / (490 - 440);
b = 1.0;
}
else if ((wavelength >= 490) && (wavelength < 510))
{
r = 0.0;
g = 1.0;
b = -(wavelength - 510) / (510 - 490);
}
else if ((wavelength >= 510) && (wavelength < 580))
{
r = (wavelength - 510) / (580 - 510);
g = 1.0;
b = 0.0;
}
else if ((wavelength >= 580) && (wavelength < 645))
{
r = 1.0;
g = -(wavelength - 645) / (645 - 580);
b = 0.0;
}
else if ((wavelength >= 645) && (wavelength < 781))
{
r = 1.0;
g = 0.0;
b = 0.0;
}
else
{
r = 0.0;
g = 0.0;
b = 0.0;
}
// Let the intensity fall off near the vision limits
if ((wavelength >= 380) && (wavelength < 420))
factor = 0.3 + 0.7 * (wavelength - 380) / (420 - 380);
else if ((wavelength >= 420) && (wavelength < 701))
factor = 1.0;
else if ((wavelength >= 701) && (wavelength < 781))
factor = 0.3 + 0.7 * (780 - wavelength) / (780 - 700);
else
factor = 0.0;
int[] rgb = new int[3];
rgb[0] = r == 0.0 ? 0 : (int) Math.round(255 * r * factor);
rgb[1] = g == 0.0 ? 0 : (int) Math.round(255 * g * factor);
rgb[2] = b == 0.0 ? 0 : (int) Math.round(255 * b * factor);
return new Color(rgb[0], rgb[1], rgb[2]);
}
}