001/* 002 * Copyright 2010-2015 Institut Pasteur. 003 * 004 * This file is part of Icy. 005 * 006 * Icy is free software: you can redistribute it and/or modify 007 * it under the terms of the GNU General Public License as published by 008 * the Free Software Foundation, either version 3 of the License, or 009 * (at your option) any later version. 010 * 011 * Icy is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 014 * GNU General Public License for more details. 015 * 016 * You should have received a copy of the GNU General Public License 017 * along with Icy. If not, see <http://www.gnu.org/licenses/>. 018 */ 019package icy.util; 020 021import icy.math.MathUtil; 022 023import java.awt.Color; 024import java.awt.color.ColorSpace; 025 026/** 027 * Color utilities class. 028 * 029 * @author Stephane 030 */ 031public class ColorUtil 032{ 033 /** 034 * RGB colorSpace 035 */ 036 public final static ColorSpace sRGB = ColorSpace.getInstance(ColorSpace.CS_sRGB); 037 038 /** 039 * Basic rainbow colors 040 */ 041 private final static Color[] colors = generateRainbow(32, true, true, true); 042 043 /** 044 * Returns a random color. 045 */ 046 public static Color getRandomColor() 047 { 048 return colors[Random.nextInt(20)]; 049 } 050 051 /** 052 * Generates a rainbow color table (HSV ramp) of the specified size. 053 * 054 * @param saturation 055 * saturation factor (from 0 to 1). 056 * @param brightness 057 * brightness factor (from 0 to 1). 058 * @param size 059 * the size of rainbow color table. 060 * @param black 061 * if true the table will also contains a black color entry. 062 * @param white 063 * if true the table will also contains a white color entry. 064 * @param gray 065 * if true the table will also contains a gray color entry. 066 */ 067 public static Color[] generateRainbow(float saturation, float brightness, int size, boolean black, boolean white, 068 boolean gray) 069 { 070 final Color[] result = new Color[size]; 071 072 int start = 0; 073 if (black) 074 result[start++] = Color.black; 075 if (white) 076 result[start++] = Color.white; 077 if (gray) 078 result[start++] = Color.gray; 079 080 for (int i = start; i < result.length; i++) 081 result[i] = Color.getHSBColor((float) (i - start) / (float) (size - start), saturation, brightness); 082 083 return result; 084 } 085 086 /** 087 * Generates a rainbow color table (HSV ramp) of the specified size. 088 * 089 * @param size 090 * the size of the rainbow color table. 091 * @param black 092 * if true the table will also contains a black color entry. 093 * @param white 094 * if true the table will also contains a white color entry. 095 * @param gray 096 * if true the table will also contains a gray color entry. 097 */ 098 public static Color[] generateRainbow(int size, boolean black, boolean white, boolean gray) 099 { 100 return generateRainbow(1f, 1f, size, black, white, gray); 101 } 102 103 /** 104 * Generates a rainbow color table (HSV ramp) of the specified size. 105 * 106 * @param size 107 * the size of the HSV color table. 108 */ 109 public static Color[] generateRainbow(int size) 110 { 111 return generateRainbow(size, false, false, false); 112 } 113 114 /** 115 * Get String representation of the specified color.<br> 116 * <br> 117 * Default representation is "A:R:G:B" where :<br> 118 * A = alpha level in hexadecimal (0x00-0xFF)<br> 119 * R = red level in hexadecimal (0x00-0xFF)<br> 120 * G = green level in hexadecimal (0x00-0xFF)<br> 121 * B = blue level in hexadecimal (0x00-0xFF)<br> 122 * 123 * @param color 124 */ 125 public static String toString(Color color) 126 { 127 return toString(color, true, ":"); 128 } 129 130 /** 131 * Get String representation of the specified rgb value.<br> 132 * <br> 133 * Default representation is "A:R:G:B" where :<br> 134 * A = alpha level in hexadecimal (00-FF)<br> 135 * R = red level in hexadecimal (00-FF)<br> 136 * G = green level in hexadecimal (00-FF)<br> 137 * B = blue level in hexadecimal (00-FF)<br> 138 * 139 * @param rgb 140 */ 141 public static String toString(int rgb) 142 { 143 return toString(rgb, true, ":"); 144 } 145 146 /** 147 * Get String representation of the specified Color value.<br> 148 * <br> 149 * Default representation is "A:R:G:B" where :<br> 150 * A = alpha level<br> 151 * R = red level<br> 152 * G = green level<br> 153 * B = blue level<br> 154 * 155 * @param color 156 * @param hexa 157 * component level are represented in hexadecimal (2 digits) 158 */ 159 public static String toString(Color color, boolean hexa) 160 { 161 return toString(color, hexa, ":"); 162 } 163 164 /** 165 * Get String representation of the specified rgb value.<br> 166 * <br> 167 * Default representation is "A:R:G:B" where :<br> 168 * A = alpha level<br> 169 * R = red level<br> 170 * G = green level<br> 171 * B = blue level<br> 172 * 173 * @param rgb 174 * @param hexa 175 * component level are represented in hexadecimal (2 digits) 176 */ 177 public static String toString(int rgb, boolean hexa) 178 { 179 return toString(rgb, hexa, ":"); 180 } 181 182 /** 183 * Get String representation of the specified color.<br> 184 * <br> 185 * Default representation is "AsepRsepGsepB" where :<br> 186 * A = alpha level in hexadecimal (0x00-0xFF)<br> 187 * R = red level in hexadecimal (0x00-0xFF)<br> 188 * G = green level in hexadecimal (0x00-0xFF)<br> 189 * B = blue level in hexadecimal (0x00-0xFF)<br> 190 * sep = the specified separator<br> 191 * <br> 192 * Ex : toString(Color.red, true, ":") --> "FF:FF:00:00" 193 * 194 * @param color 195 * @param hexa 196 * component level are represented in hexadecimal (2 digits) 197 */ 198 public static String toString(Color color, boolean hexa, String sep) 199 { 200 if (color == null) 201 return "-"; 202 203 return toString(color.getRGB(), hexa, sep); 204 } 205 206 /** 207 * Get String representation of the specified rgb value.<br> 208 * <br> 209 * Default representation is "AsepRsepGsepB" where :<br> 210 * A = alpha level in hexadecimal (0x00-0xFF)<br> 211 * R = red level in hexadecimal (0x00-0xFF)<br> 212 * G = green level in hexadecimal (0x00-0xFF)<br> 213 * B = blue level in hexadecimal (0x00-0xFF)<br> 214 * sep = the specified separator<br> 215 * <br> 216 * Ex : toString(0xFF00FF00, true, ":") --> "FF:00:FF:00" 217 * 218 * @param rgb 219 * @param hexa 220 * component level are represented in hexadecimal (2 digits) 221 */ 222 public static String toString(int rgb, boolean hexa, String sep) 223 { 224 final int a = (rgb >> 24) & 0xFF; 225 final int r = (rgb >> 16) & 0xFF; 226 final int g = (rgb >> 8) & 0xFF; 227 final int b = (rgb >> 0) & 0xFF; 228 229 if (hexa) 230 return (StringUtil.toHexaString(a, 2) + sep + StringUtil.toHexaString(r, 2) + sep 231 + StringUtil.toHexaString(g, 2) + sep + StringUtil.toHexaString(b, 2)).toUpperCase(); 232 233 return StringUtil.toString(a) + sep + StringUtil.toString(r) + sep + StringUtil.toString(g) + sep 234 + StringUtil.toString(b); 235 } 236 237 /** 238 * Returns <code>true</code> if the specified color is pure black (alpha is not verified) 239 */ 240 public static boolean isBlack(Color color) 241 { 242 return (color.getRGB() & 0x00FFFFFF) == 0; 243 } 244 245 /** 246 * Mix 2 colors with priority color 247 */ 248 public static Color mixOver(Color backColor, Color frontColor) 249 { 250 final int r, g, b, a; 251 252 final float frontAlpha = frontColor.getAlpha() / 255f; 253 final float invAlpha = 1f - frontAlpha; 254 255 r = (int) ((backColor.getRed() * invAlpha) + (frontColor.getRed() * frontAlpha)); 256 g = (int) ((backColor.getGreen() * invAlpha) + (frontColor.getGreen() * frontAlpha)); 257 b = (int) ((backColor.getBlue() * invAlpha) + (frontColor.getBlue() * frontAlpha)); 258 a = Math.max(backColor.getAlpha(), frontColor.getAlpha()); 259 260 return new Color(r, g, b, a); 261 } 262 263 /** 264 * Mix 2 colors using the following ratio for mixing:<br/> 265 * 0f means 100% of color 1 and 0% of color 2<br/> 266 * 0.5f means 50% of color 1 and 50% of color 2<br/> 267 * 1f means 0% of color 1 and 100% of color 2 268 */ 269 public static Color mix(Color c1, Color c2, float ratio) 270 { 271 final int r, g, b; 272 final float r2 = Math.min(1f, Math.max(0f, ratio)); 273 final float r1 = 1f - r2; 274 275 r = (int) ((c1.getRed() * r1) + (c2.getRed() * r2)); 276 g = (int) ((c1.getGreen() * r1) + (c2.getGreen() * r2)); 277 b = (int) ((c1.getBlue() * r1) + (c2.getBlue() * r2)); 278 279 return new Color(r, g, b); 280 } 281 282 /** 283 * Mix 2 colors without "priority" color 284 */ 285 public static Color mix(Color c1, Color c2, boolean useAlpha) 286 { 287 final int r, g, b, a; 288 289 if (useAlpha) 290 { 291 final float a1 = c1.getAlpha() / 255f; 292 final float a2 = c2.getAlpha() / 255f; 293 final float af = a1 + a2; 294 295 r = (int) (((c1.getRed() * a1) + (c2.getRed() * a2)) / af); 296 g = (int) (((c1.getGreen() * a1) + (c2.getGreen() * a2)) / af); 297 b = (int) (((c1.getBlue() * a1) + (c2.getBlue() * a2)) / af); 298 a = Math.max(c1.getAlpha(), c2.getAlpha()); 299 } 300 else 301 { 302 r = (c1.getRed() + c2.getRed()) / 2; 303 g = (c1.getGreen() + c2.getGreen()) / 2; 304 b = (c1.getBlue() + c2.getBlue()) / 2; 305 a = 255; 306 } 307 308 return new Color(r, g, b, a); 309 } 310 311 /** 312 * Mix 2 colors (no alpha) 313 */ 314 public static Color mix(Color c1, Color c2) 315 { 316 return mix(c1, c2, false); 317 } 318 319 /** 320 * Add 2 colors 321 */ 322 public static Color add(Color c1, Color c2, boolean useAlpha) 323 { 324 final int r, g, b, a; 325 326 r = Math.min(c1.getRed() + c2.getRed(), 255); 327 g = Math.min(c1.getGreen() + c2.getGreen(), 255); 328 b = Math.min(c1.getBlue() + c2.getBlue(), 255); 329 330 if (useAlpha) 331 a = Math.max(c1.getAlpha(), c2.getAlpha()); 332 else 333 a = 255; 334 335 return new Color(r, g, b, a); 336 } 337 338 /** 339 * Add 2 colors 340 */ 341 public static Color add(Color c1, Color c2) 342 { 343 return add(c1, c2, false); 344 } 345 346 /** 347 * Sub 2 colors 348 */ 349 public static Color sub(Color c1, Color c2, boolean useAlpha) 350 { 351 final int r, g, b, a; 352 353 r = Math.max(c1.getRed() - c2.getRed(), 0); 354 g = Math.max(c1.getGreen() - c2.getGreen(), 0); 355 b = Math.max(c1.getBlue() - c2.getBlue(), 0); 356 357 if (useAlpha) 358 a = Math.max(c1.getAlpha(), c2.getAlpha()); 359 else 360 a = 255; 361 362 return new Color(r, g, b, a); 363 } 364 365 /** 366 * Subtract 2 colors 367 */ 368 public static Color sub(Color c1, Color c2) 369 { 370 return sub(c1, c2, false); 371 } 372 373 /** 374 * Get opposite (XORed) color 375 */ 376 public static Color xor(Color c) 377 { 378 return new Color(c.getRed() ^ 0xFF, c.getGreen() ^ 0xFF, c.getBlue() ^ 0xFF, c.getAlpha()); 379 } 380 381 /** 382 * get to gray level (simple RGB mix) 383 */ 384 public static int getGrayMix(Color c) 385 { 386 return getGrayMix(c.getRGB()); 387 } 388 389 /** 390 * get to gray level (simple RGB mix) 391 */ 392 public static int getGrayMix(int rgb) 393 { 394 return (((rgb >> 16) & 0xFF) + ((rgb >> 8) & 0xFF) + ((rgb >> 0) & 0xFF)) / 3; 395 } 396 397 /** 398 * Convert to gray level color (simple RGB mix) 399 */ 400 public static Color getGrayColorMix(Color c) 401 { 402 final int gray = getGrayMix(c); 403 return new Color(gray, gray, gray); 404 } 405 406 /** 407 * Convert to gray level color (from luminance calculation) 408 */ 409 public static Color getGrayColorLum(Color c) 410 { 411 final int gray = getLuminance(c); 412 return new Color(gray, gray, gray); 413 } 414 415 /** 416 * Return luminance (in [0..255] range) 417 */ 418 public static int getLuminance(Color c) 419 { 420 return (int) ((c.getRed() * 0.299) + (c.getGreen() * 0.587) + (c.getBlue() * 0.114)); 421 } 422 423 /** 424 * Convert the specified color to HSV color. 425 */ 426 public static float[] toHSV(Color c) 427 { 428 return toHSV(c.getRGBColorComponents(null)); 429 } 430 431 /** 432 * Convert the specified RGB color to HSV color. 433 */ 434 public static float[] toHSV(float[] rgb) 435 { 436 float r = rgb[0]; 437 float g = rgb[1]; 438 float b = rgb[2]; 439 float min, max, delta; 440 float h, s, v; 441 442 min = Math.min(r, Math.min(g, b)); 443 max = Math.max(r, Math.max(g, b)); 444 445 // black 446 if (max == 0f) 447 return new float[] {0, 0, 0}; 448 449 v = max; 450 delta = max - min; 451 s = delta / max; 452 453 // graylevel 454 if (delta == 0f) 455 return new float[] {0, s, v}; 456 457 if (r == max) 458 // between yellow & magenta 459 h = (g - b) / delta; 460 else if (g == max) 461 // between cyan & yellow 462 h = 2 + (b - r) / delta; 463 else 464 // between magenta & cyan 465 h = 4 + (r - g) / delta; 466 467 // want positif hue 468 if (h < 0) 469 h += 6f; 470 471 return new float[] {h / 6f, s, v}; 472 } 473 474 /** 475 * Convert the specified HSV color to RGB color. 476 */ 477 public static float[] fromHSV(float[] hsv) 478 { 479 float h = hsv[0]; 480 float s = hsv[0]; 481 float v = hsv[0]; 482 float f, p, q, t; 483 float r, g, b; 484 int i; 485 486 // no color 487 if (s == 0f) 488 return new float[] {v, v, v}; 489 490 // sector 0 to 5 491 h *= 6f; 492 i = (int) Math.floor(h); 493 // factorial part of h 494 f = h - i; 495 p = v * (1f - s); 496 q = v * (1f - (s * f)); 497 t = v * (1f - (s * (1 - f))); 498 499 switch (i) 500 { 501 case 0: 502 r = v; 503 g = t; 504 b = p; 505 break; 506 case 1: 507 r = q; 508 g = v; 509 b = p; 510 break; 511 case 2: 512 r = p; 513 g = v; 514 b = t; 515 break; 516 case 3: 517 r = p; 518 g = q; 519 b = v; 520 break; 521 case 4: 522 r = t; 523 g = p; 524 b = v; 525 break; 526 default: 527 r = v; 528 g = p; 529 b = q; 530 break; 531 } 532 533 return new float[] {r, g, b}; 534 } 535 536 /** 537 * Convert the specified XYZ color to RGB color. 538 */ 539 public static float[] fromXYZ(float[] xyz) 540 { 541 return sRGB.fromCIEXYZ(xyz); 542 } 543 544 /** 545 * Convert the specified color to XYZ color. 546 */ 547 public static float[] toXYZ(Color c) 548 { 549 return toXYZ(c.getRGBColorComponents(null)); 550 } 551 552 /** 553 * Convert the specified RGB color to XYZ color. 554 */ 555 public static float[] toXYZ(float[] rgb) 556 { 557 return sRGB.toCIEXYZ(rgb); 558 } 559 560 /** 561 * Convert the specified color to LAB color. 562 */ 563 public static float[] toLAB(Color c) 564 { 565 return toLAB(c.getRGBColorComponents(null)); 566 } 567 568 /** 569 * Convert the specified RGB color to LAB color. 570 */ 571 public static float[] toLAB(float[] rgb) 572 { 573 return XYZtoLAB(toXYZ(rgb)); 574 } 575 576 private static float pivotXYZ(float value) 577 { 578 return (value > 0.008856f) ? (float) MathUtil.cubicRoot(value) : (7.787f * value) + 0.1379f; 579 } 580 581 /** 582 * Convert the specified XYZ color to LAB color. 583 */ 584 public static float[] XYZtoLAB(float[] xyz) 585 { 586 float x = pivotXYZ(xyz[0] / 95.047f); 587 float y = pivotXYZ(xyz[1] / 100f); 588 float z = pivotXYZ(xyz[2] / 108.883f); 589 590 float l = Math.max(0, (116f * y) - 16f); 591 float a = 500f * (x - y); 592 float b = 200f * (y - z); 593 594 return new float[] {l, a, b}; 595 } 596 597 /** 598 * Compute and returns the distance between the 2 colors.<br> 599 * The HSV distance returns a value between 0 and 1 where 1 is maximum distance.<br> 600 * The LAB distance returns a positive value where > 2.3 value is considered a 601 * significant distance. 602 * 603 * @param c1 604 * first color 605 * @param c2 606 * second color 607 * @param hsv 608 * If set to true we use the HSV color space to compute the color distance otherwise we 609 * use the LAB color space. 610 */ 611 public static double getDistance(Color c1, Color c2, boolean hsv) 612 { 613 if (hsv) 614 { 615 // use HSV color space 616 final float[] hsv1 = toHSV(c1); 617 final float[] hsv2 = toHSV(c2); 618 619 return getDistance(hsv1, hsv2, true); 620 } 621 622 // use LAB color space 623 final float[] lab1 = toLAB(c1); 624 final float[] lab2 = toLAB(c2); 625 626 return getDistance(lab1, lab2, true); 627 } 628 629 /** 630 * Returns the distance between 2 colors from same color space. 631 */ 632 static double getDistance(float[] c1, float[] c2, boolean compareThirdComponent) 633 { 634 float result = (float) (Math.pow(c1[0] - c2[0], 2d) + Math.pow(c1[1] - c2[1], 2d)); 635 636 if (compareThirdComponent) 637 result += Math.pow(c1[2] - c2[2], 2d); 638 639 return result; 640 } 641 642 /** 643 * Returns the dominant color from the specified color array.<br> 644 * The dominant color is calculated by computing the color histogram from a rainbow gradient and 645 * returning the highest bin number. 646 */ 647 public static Color getDominantColor(Color colors[]) 648 { 649 return getDominantColor(colors, 33); 650 } 651 652 /** 653 * Returns the dominant color from the specified color array.<br> 654 * The dominant color is calculated by computing the color histogram from a rainbow gradient and 655 * returning the color corresponding to the highest bin. 656 * 657 * @param colors 658 * Color array we want to retrieve the dominant color from. 659 * @param binNumber 660 * the number of bin to construct the rainbow gradient. 661 */ 662 public static Color getDominantColor(Color colors[], int binNumber) 663 { 664 final Color[] baseColors = generateRainbow(1f, 1f, binNumber, false, false, true); 665 666 final float[][] colorsHSV = new float[colors.length][]; 667 final float[][] baseColorsHSV = new float[binNumber][]; 668 669 // convert colors to HSV float component 670 for (int i = 0; i < colors.length; i++) 671 colorsHSV[i] = toHSV(colors[i]); 672 for (int i = 0; i < baseColors.length; i++) 673 baseColorsHSV[i] = toHSV(baseColors[i]); 674 675 final int[] bins = new int[binNumber]; 676 677 for (float[] colorHsv : colorsHSV) 678 { 679 double minDist = getDistance(colorHsv, baseColorsHSV[0], true); 680 int minInd = 0; 681 682 for (int ind = 1; ind < baseColorsHSV.length; ind++) 683 { 684 final double dist = getDistance(colorHsv, baseColorsHSV[ind], true); 685 686 if (dist < minDist) 687 { 688 minDist = dist; 689 minInd = ind; 690 } 691 } 692 693 bins[minInd]++; 694 } 695 696 int max = bins[0]; 697 int maxInd = 0; 698 699 for (int i = 1; i < bins.length; i++) 700 { 701 final int v = bins[i]; 702 703 if (v > max) 704 { 705 max = v; 706 maxInd = i; 707 } 708 } 709 710 return baseColors[maxInd]; 711 } 712 713 /** 714 * Converts a wavelength into a {@link Color} object.<br/> 715 * Taken from Earl F. Glynn's web page: 716 * <a href="http://www.efg2.com/Lab/ScienceAndEngineering/Spectra.htm">Spectra Lab Report</a> 717 * 718 * @param wavelength 719 * the wavelength to convert (in nanometers) 720 * @return a {@link Color} object representing the specified wavelength 721 */ 722 public static Color getColorFromWavelength(double wavelength) 723 { 724 double factor; 725 double r, g, b; 726 727 if ((wavelength >= 380) && (wavelength < 440)) 728 { 729 r = -(wavelength - 440) / (440 - 380); 730 g = 0.0; 731 b = 1.0; 732 } 733 else if ((wavelength >= 440) && (wavelength < 490)) 734 { 735 r = 0.0; 736 g = (wavelength - 440) / (490 - 440); 737 b = 1.0; 738 } 739 else if ((wavelength >= 490) && (wavelength < 510)) 740 { 741 r = 0.0; 742 g = 1.0; 743 b = -(wavelength - 510) / (510 - 490); 744 } 745 else if ((wavelength >= 510) && (wavelength < 580)) 746 { 747 r = (wavelength - 510) / (580 - 510); 748 g = 1.0; 749 b = 0.0; 750 } 751 else if ((wavelength >= 580) && (wavelength < 645)) 752 { 753 r = 1.0; 754 g = -(wavelength - 645) / (645 - 580); 755 b = 0.0; 756 } 757 else if ((wavelength >= 645) && (wavelength < 781)) 758 { 759 r = 1.0; 760 g = 0.0; 761 b = 0.0; 762 } 763 else 764 { 765 r = 0.0; 766 g = 0.0; 767 b = 0.0; 768 } 769 770 // Let the intensity fall off near the vision limits 771 if ((wavelength >= 380) && (wavelength < 420)) 772 factor = 0.3 + 0.7 * (wavelength - 380) / (420 - 380); 773 else if ((wavelength >= 420) && (wavelength < 701)) 774 factor = 1.0; 775 else if ((wavelength >= 701) && (wavelength < 781)) 776 factor = 0.3 + 0.7 * (780 - wavelength) / (780 - 700); 777 else 778 factor = 0.0; 779 780 int[] rgb = new int[3]; 781 782 rgb[0] = r == 0.0 ? 0 : (int) Math.round(255 * r * factor); 783 rgb[1] = g == 0.0 ? 0 : (int) Math.round(255 * g * factor); 784 rgb[2] = b == 0.0 ? 0 : (int) Math.round(255 * b * factor); 785 786 return new Color(rgb[0], rgb[1], rgb[2]); 787 } 788}