Search code examples
c#interoprgbcolor-managementlab-color-space

LAB to RGB using Lcms2 gives odd result


I am doing a project where I need to do lab->rgb. I am using a color management library called lcms2, link: https://github.com/mm2/Little-CMS

My project is in c#, I use interop to communicate with the dll. The project will have to run on linux Ubuntu Core 16, which isn't supported by most c# colour management libraries, which is why I'm going with lcms2.

When trying to convert the LAB L:99, A:-122, B:-66 I get really strange results, an RGB with values:

R: -3769.14044380188 G: 304.88466560840607 B: 378.99470329284668

This is the (simplified) data classes, and the extension method used in the test, as well as the test of course:

[Test()]
public void LabThatGaveWeirdResultDoesntDoItAnymore()
{
    var lab = new LabColor(99, -112, -66);
    var rgb = lab.ToRgb();

    Assert.IsTrue(new[] { rgb.R, rgb.G, rgb.B }.All(v => v >= 0 && v <= 255));
}

public class LabColor
{
    public double L { get; }
    public double A { get; }
    public double B { get; }

    public LabColor(int l, int a, int b)
    {
        L = l;
        A = a;
        B = b;
    }
}

public class RgbColor
{
    public double R { get; }
    public double G { get; }
    public double B { get; }

    public RgbColor(double r, double g, double b)
    {
        R = r;
        G = g;
        B = b;
    }
}
public static RgbColor ToRgb(this LabColor lab, Intent intent = Intent.Perceptual)
{
    var labProfile = Lcms2.GetLabProfile();
    var hRgbProfile = Lcms2.GetRgbProfile();
    var transform = Lcms2.GetTransform(labProfile, hRgbProfile, intent);

    var rgbBuffer = new double[3];
    var labBuffer = new[]
    {
        lab.L,
        lab.A,
        lab.B
    };

    Lcms2.Transform(transform, labBuffer, ref rgbBuffer, 1);

    Lcms2.FreeMemory(labProfile);
    Lcms2.FreeMemory(hRgbProfile);
    Lcms2.FreeMemory(transform);

    return new RgbColor(rgbBuffer[0] * 255, rgbBuffer[1] * 255, rgbBuffer[2] * 255);
}

The lcms2 wrapper code:

internal enum ProfileFormat
{
    Rgb = 4456472,
    Cmyk = 4587552,
    Lab = 4849688
}

public enum Intent
{
    Perceptual = 0,
    RelativeColorimetric = 1,
    Saturation = 2,
    AbsoluteColorimetric = 3
}

internal sealed class Profile
{
    public Profile(IntPtr pointer, ProfileFormat format)
    {
        Pointer = pointer;
        Format = format;
    }

    internal ProfileFormat Format { get; }

    internal IntPtr Pointer { get; }
}

internal sealed class Transform
{
    public Transform(IntPtr pointer)
    {
        Pointer = pointer;
    }

    internal IntPtr Pointer { get; }
}

internal sealed class Lcms2
{
    [DllImport("lcms2.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "cmsCreate_sRGBProfile")]
    static extern IntPtr cmsCreate_sRGBProfile();

    public static Profile GetRgbProfile()
    {
        return new Profile(cmsCreate_sRGBProfile(), ProfileFormat.Rgb);
    }

    [DllImport("lcms2.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "cmsCreateLab4Profile")]
    static extern IntPtr cmsCreateLab4Profile();

    public static Profile GetLabProfile()
    {
        return new Profile(cmsCreateLab4Profile(), ProfileFormat.Lab);
    }

    [DllImport("lcms2.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "cmsCreateTransform")]
    static extern IntPtr cmsCreateTransform(IntPtr inProfilePtr, uint inputFormat, IntPtr outPutProfilePtr, uint outputFormat, uint intent, uint dword);

    public static Transform GetTransform(Profile inProfile, Profile outProfile, Intent intent)
    {
        return new Transform(cmsCreateTransform(inProfile.Pointer, (uint)inProfile.Format, outProfile.Pointer, (uint)outProfile.Format, (uint)intent, 0));
    }

    [DllImport("lcms2.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "cmsDoTransform")]
    static extern void cmsDoTransform(IntPtr transformPtr, double[] inputBuffer, double[] outputBuffer, uint size);

    public static void Transform(Transform transform, double[] inputBuffer, ref double[] outputBuffer, uint size)
    {
        cmsDoTransform(transform.Pointer, inputBuffer, outputBuffer, size);
    }

    [DllImport("lcms2.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "cmsDeleteTransform")]
    static extern void cmsDeleteTransform(IntPtr transformPtr);

    public static void FreeMemory(Transform transform)
    {
        cmsDeleteTransform(transform.Pointer);
    }

    [DllImport("lcms2.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "cmsCloseProfile")]
    static extern void cmsCloseProfile(IntPtr profilePtr);
    public static void FreeMemory(Profile profile)
    {
        cmsCloseProfile(profile.Pointer);
    }

I calculated the profileformat integers by recreating the code in the c++ lcms2 project. They were defined as

#define TYPE_RGB_DBL          (FLOAT_SH(1)|COLORSPACE_SH(PT_RGB)|CHANNELS_SH(3)|BYTES_SH(0))
#define TYPE_Lab_DBL          (FLOAT_SH(1)|COLORSPACE_SH(PT_Lab)|CHANNELS_SH(3)|BYTES_SH(0))

I mimicked the code, creating:

        uint FLOAT_SH(uint i) => (i) << 22;
        uint COLORSPACE_SH(uint i) => (i) << 16;
        uint PT_Lab = 10;
        uint PT_RGB = 4;
        uint CHANNELS_SH(uint i) => (i) << 3;
        uint BYTES_SH(uint i) => (i);

        var TYPE_RGB_DBL = (FLOAT_SH(1) | COLORSPACE_SH(PT_RGB) | CHANNELS_SH(3) | BYTES_SH(0)); //4456472
        var TYPE_Lab_DBL = (FLOAT_SH(1) | COLORSPACE_SH(PT_Lab) | CHANNELS_SH(3) | BYTES_SH(0)); //4849688

Solution

  • It just happens that Lab (99, -122, -66) is out of sRGB gamut. You cannot represent this color in sRGB. If you clip the values it is RGB=(0, 255, 255), but this is not the color you asked for.

    Otherwise the unbounded values for sRGB are:

    C:\github\Little-CMS\bin>transicc -t1 -i*Lab LittleCMS ColorSpace conversion calculator - 4.3 [LittleCMS 2.08]

    Enter values, 'q' to quit L*? 99 a*? -122 b*? -66

    R=-4111.8278 G=307.7362 B=378.8372