19

How can I present object Color in CIE Lab color model.

Color c = ...
float[] lab = {0,0,0};
...
c.getColorComponents(ColorSpace.getInstance(???), lab);

But I wasn't able to force this work with CIE Lab (despite of the fact that TYPE_Lab is presented in ColorSpace class)

Thx for attention.

user562527
  • 193
  • 1
  • 1
  • 4

6 Answers6

21

Here's my implementation:

import java.awt.color.ColorSpace;

public class CIELab extends ColorSpace {

    public static CIELab getInstance() {
        return Holder.INSTANCE;
    }

    @Override
    public float[] fromCIEXYZ(float[] colorvalue) {
        double l = f(colorvalue[1]);
        double L = 116.0 * l - 16.0;
        double a = 500.0 * (f(colorvalue[0]) - l);
        double b = 200.0 * (l - f(colorvalue[2]));
        return new float[] {(float) L, (float) a, (float) b};
    }

    @Override
    public float[] fromRGB(float[] rgbvalue) {
        float[] xyz = CIEXYZ.fromRGB(rgbvalue);
        return fromCIEXYZ(xyz);
    }

    @Override
    public float getMaxValue(int component) {
        return 128f;
    }

    @Override
    public float getMinValue(int component) {
        return (component == 0)? 0f: -128f;
    }    

    @Override
    public String getName(int idx) {
        return String.valueOf("Lab".charAt(idx));
    }

    @Override
    public float[] toCIEXYZ(float[] colorvalue) {
        double i = (colorvalue[0] + 16.0) * (1.0 / 116.0);
        double X = fInv(i + colorvalue[1] * (1.0 / 500.0));
        double Y = fInv(i);
        double Z = fInv(i - colorvalue[2] * (1.0 / 200.0));
        return new float[] {(float) X, (float) Y, (float) Z};
    }

    @Override
    public float[] toRGB(float[] colorvalue) {
        float[] xyz = toCIEXYZ(colorvalue);
        return CIEXYZ.toRGB(xyz);
    }

    CIELab() {
        super(ColorSpace.TYPE_Lab, 3);
    }

    private static double f(double x) {
        if (x > 216.0 / 24389.0) {
            return Math.cbrt(x);
        } else {
            return (841.0 / 108.0) * x + N;
        }
    }

    private static double fInv(double x) {
        if (x > 6.0 / 29.0) {
            return x*x*x;
        } else {
            return (108.0 / 841.0) * (x - N);
        }
    }

    private Object readResolve() {
        return getInstance();
    }

    private static class Holder {
        static final CIELab INSTANCE = new CIELab();
    }

    private static final long serialVersionUID = 5027741380892134289L;

    private static final ColorSpace CIEXYZ =
        ColorSpace.getInstance(ColorSpace.CS_CIEXYZ);

    private static final double N = 4.0 / 29.0;

}
finnw
  • 47,861
  • 24
  • 143
  • 221
  • @Peter Perháč, the [Creative Commons Attribution Share Alike](http://creativecommons.org/licenses/by-sa/2.5/) license applies. – finnw Sep 12 '11 at 11:15
  • 1
    Excellent - I used this without any problems, however I did make a small change. In my application I converted an RGB image to CIELab color space, performed some processing on the L channel, then converted back to RGB, and noticed ugly artifacts. The solution I used was to clamp the X,Y,Z values calculated in the above `toCIEXYZ` method to the range [0,1]. Not sure if this is the 'right' way to do things, but it got rid of the artifacts. – James Dec 11 '11 at 11:43
  • @James on behalf of [yasir-gakhar](https://stackoverflow.com/users/9446678/yasir-gakhar): Hi James .I have calculated R,G and B values of an image and do some calculations to get L,a and b values for LAB Colorspace. Now how can I transfer my RGB image into LAB image using these L,a and b Values in Android studio (Except OpenCV's Builtin function because I want to first convert RGB into XYZ and then finally XYZ to LAB color space)? – jps Mar 09 '18 at 13:24
  • 1
    @jps @yasir-gakhar The implementation in this answer does exactly that - see that the `fromRGB` method calls `fromCIEXYZ`. – James Mar 10 '18 at 01:05
10

I had some problems using the code in @finw's answer. I believe they were mostly due to the fact that to do a CIELab conversion you should specify an illuminant:

http://en.wikipedia.org/wiki/Standard_illuminant

One of the popular standards is D50, which is basically just a standard daylight. Because @finw's code doesn't have the correction for illumination, the colors that are supposed to be neutral gray come out slightly tinted. One way of checking this is to try:

 float[] g = { 50.0f, 0f, 0f };
 CIELab.getInstance().toRGB(g); 
 for (float f : g) System.out.println(f);

You should get roughly the same number on all three channels, but you end up with an RGB profile that's noticeably (albeit slightly) blue. I'm sure it is possible to correct this in @finw's code, but after a bit of playing with it and searching around, I found some excellent conversion code here:

http://www.f4.fhtw-berlin.de/~barthel/ImageJ/ColorInspector//HTMLHelp/farbraumJava.htm

For completeness, here it is.

public void rgb2lab(int R, int G, int B, int[] lab) {
    //http://www.brucelindbloom.com

    float r, g, b, X, Y, Z, fx, fy, fz, xr, yr, zr;
    float Ls, as, bs;
    float eps = 216.f/24389.f;
    float k = 24389.f/27.f;

    float Xr = 0.964221f;  // reference white D50
    float Yr = 1.0f;
    float Zr = 0.825211f;

    // RGB to XYZ
    r = R/255.f; //R 0..1
    g = G/255.f; //G 0..1
    b = B/255.f; //B 0..1

    // assuming sRGB (D65)
    if (r <= 0.04045)
        r = r/12;
    else
        r = (float) Math.pow((r+0.055)/1.055,2.4);

    if (g <= 0.04045)
        g = g/12;
    else
        g = (float) Math.pow((g+0.055)/1.055,2.4);

    if (b <= 0.04045)
        b = b/12;
    else
        b = (float) Math.pow((b+0.055)/1.055,2.4);


    X =  0.436052025f*r     + 0.385081593f*g + 0.143087414f *b;
    Y =  0.222491598f*r     + 0.71688606f *g + 0.060621486f *b;
    Z =  0.013929122f*r     + 0.097097002f*g + 0.71418547f  *b;

    // XYZ to Lab
    xr = X/Xr;
    yr = Y/Yr;
    zr = Z/Zr;

    if ( xr > eps )
        fx =  (float) Math.pow(xr, 1/3.);
    else
        fx = (float) ((k * xr + 16.) / 116.);

    if ( yr > eps )
        fy =  (float) Math.pow(yr, 1/3.);
    else
    fy = (float) ((k * yr + 16.) / 116.);

    if ( zr > eps )
        fz =  (float) Math.pow(zr, 1/3.);
    else
        fz = (float) ((k * zr + 16.) / 116);

    Ls = ( 116 * fy ) - 16;
    as = 500*(fx-fy);
    bs = 200*(fy-fz);

    lab[0] = (int) (2.55*Ls + .5);
    lab[1] = (int) (as + .5); 
    lab[2] = (int) (bs + .5);       
} 

In my tests, it produces gray values that are appropriately chroma-free, and it is much speedier to boot.

charleslparker
  • 1,904
  • 1
  • 21
  • 31
  • 1
    Are you sure this is the correct implementation? I tried to covert white (255, 255, 255) and the result was (L=255, a=0, b=0) instead of (L=100, a=0, b=0). I also check http://colormine.org/convert/rgb-to-lab for comparison with other color, such as red, yellow, and green. – fikr4n Jan 26 '15 at 10:26
  • 1
    It seems `Ls` is scale up to fill [0, 255] range. Returning just `Ls` is sufficient. Also I've omittied `+ .5`s. They seem redundant as they will cause the values to overflow. I can just round the value, which makes more sense. – akinuri Jan 01 '18 at 13:02
  • Re standard illuminant, D50 "Simulates warm daylight at sunrise or sunset with correlated color temperature of 5003 K. Also known as horizon light." May be warmer than expected. D65 is "noon" daylight. XYZ ref values: https://www.mathworks.com/help/images/ref/whitepoint.html – Oskar Austegard Apr 21 '22 at 21:34
8

I used this code and it worked:

public double[] rgbToLab(int R, int G, int B) {

    double r, g, b, X, Y, Z, xr, yr, zr;

    // D65/2°
    double Xr = 95.047;  
    double Yr = 100.0;
    double Zr = 108.883;


    // --------- RGB to XYZ ---------//

    r = R/255.0;
    g = G/255.0;
    b = B/255.0;

    if (r > 0.04045)
        r = Math.pow((r+0.055)/1.055,2.4);
    else
        r = r/12.92;

    if (g > 0.04045)
        g = Math.pow((g+0.055)/1.055,2.4);
    else
        g = g/12.92;

    if (b > 0.04045)
        b = Math.pow((b+0.055)/1.055,2.4);
    else
        b = b/12.92 ;

    r*=100;
    g*=100;
    b*=100;

    X =  0.4124*r + 0.3576*g + 0.1805*b;
    Y =  0.2126*r + 0.7152*g + 0.0722*b;
    Z =  0.0193*r + 0.1192*g + 0.9505*b;


    // --------- XYZ to Lab --------- //

    xr = X/Xr;
    yr = Y/Yr;
    zr = Z/Zr;

    if ( xr > 0.008856 )
        xr =  (float) Math.pow(xr, 1/3.);
    else
        xr = (float) ((7.787 * xr) + 16 / 116.0);

    if ( yr > 0.008856 )
        yr =  (float) Math.pow(yr, 1/3.);
    else
        yr = (float) ((7.787 * yr) + 16 / 116.0);

    if ( zr > 0.008856 )
        zr =  (float) Math.pow(zr, 1/3.);
    else
        zr = (float) ((7.787 * zr) + 16 / 116.0);


    double[] lab = new double[3];

    lab[0] = (116*yr)-16;
    lab[1] = 500*(xr-yr); 
    lab[2] = 200*(yr-zr); 

    return lab;

} 

For the code above I used the formulas provided here in order to convert from rgb to XYZ and then from XYZ to CIELab. The results I get are the same with this online converter.

Thanasis1101
  • 1,614
  • 4
  • 17
  • 28
2

There is a TYPE_Lab, but no corresponding CS_Lab. You will need to extend ColorSpace and override the abstract methods to convert between XYZ, RGB, and Lab. The required conversions can be found at Lab color space (Wikipedia).

OrangeDog
  • 36,653
  • 12
  • 122
  • 207
2

Sorry to bump an old thread but any new ones would likely get marked as duplicate - I feel the top-rated answers are complex or over-engineered and others are not complete or just lacking in information.

public static float[] rgbToLab(int r, int g, int b) {
    return ColorSpace.getInstance(ColorSpace.CS_CIEXYZ).fromRGB(new float[]{r / 255f, g / 255f, b / 255f});
}

Easy 1 liner using awt.color.ColorSpace - working very well in my practice. You can calculate the distance like so

// Euclidean Distance
public static double distance(Color target, Color control) {
    float[] a = rgbToLab(target), b = rgbToLab(control);
    double L = a[0] - b[0], A = a[1] - b[1], B = a[2] - b[2];

    return Math.sqrt((L * L) + (A * A) + (B * B));
}

public static float[] rgbToLab(Color color) {
    return rgbToLab(color.getRed(), color.getGreen(), color.getBlue());
}

This yields results as so;

// Control color = #D9C967
#213B1E | DISTANCE: 2.5532837723818224E-4
#19301C | DISTANCE: 2.74658203125E-4
#1E2D10 | DISTANCE: 2.74658203125E-4
#DDC669 | DISTANCE: 0.0
#DDC56B | DISTANCE: 0.0
#DAC761 | DISTANCE: 0.0
Jens Bannmann
  • 4,845
  • 5
  • 49
  • 76
  • According to [this](http://colormine.org/convert/rgb-to-lab) calculator your solution does not work. My unit tests fails with `Lab(0.43585205078125,0.222381591796875,0.013916015625) was not equal to Lab(53.23,80.1,67.22)` for a pure red i.e., `255,0,0` RGB value. The values are sufficiently different that this is not an issue of comparing doubles. – Sim Nov 02 '22 at 14:43
1

CIELAB seems to be supported only by name in the current Java library - if you look at the source of java.awt.color.Colorspace, you'll see that only a handful of the named color spaces are supported.

andrewmu
  • 14,276
  • 4
  • 39
  • 37