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}