LAB, RGB, XYZ Color Conversion Incorrect Vice versa

2.3k Views Asked by At

I'm making a custom color-picker for a project, it's in photoshop style, i got all the other conversions to work as expected but i can't get RGBToLAB and LABToRGB to work correctly.

The problem is not just that the colors are not represented correctly but that the conversion isn't perfect too.

Sample :

  • LAB _ 58:0:0
  • XYZ _ 0.25960986510312:0.25960986510312:0.25960986510312
  • RGB _ {R:10 G:8 B:7 A:255}
  • XYZ _ 0.250358161840588:5.51162077338675:66.3836625496266
  • LAB _ 85.3739502460609:0:0

The initial LAB is not the same as the last LAB, this shows that the conversion is flawed. Not only am i getting the wrong colors but there's a change in values, especially when LAB.L is suppose to be constant(in this example, because that's what the slider currently is controlling)

The LAB->RGB->LAB conversion above is flawed but so is the XYZ->RGB->XYZ conversion too.

Obviously i'm not interested in converting LABToLAB but the above does point out a flaw in the conversion.

Things i've tried :

  1. This formula on wikipedia

  2. EasyRGB's code

  3. This javascript code on github

  4. This cginc code intended for unity, which is where i'm at now

       Private Function LABToXYZ(LAB As LAB) As XYZ
        Dim X, Y, Z As New Double
    
        Y = ((LAB.L + 16.0) / 116.0)
        X = ((LAB.A / 500.0) + Y)
        Z = (Y - (LAB.B / 200.0))
    
        Dim Less = 0.206897
    
        If (X > Less) Then
            X = Math.Pow(X, 3)
        Else
            X = ((X - 16.0 / 116.0) / 7.787)
        End If
        If (Y > Less) Then
            Y = Math.Pow(Y, 3)
        Else
            Y = ((Y - 16.0 / 116.0) / 7.787)
        End If
        If (Z > Less) Then
            Z = Math.Pow(Z, 3)
        Else
            Z = ((Z - 16.0 / 116.0) / 7.787)
        End If
    
        Return New XYZ(X, Y, Z)
    End Function
    
    Private Function XYZToRGB(XYZ As XYZ) As Color
        Dim R, G, B As New Double
        Dim X, Y, Z As New Double
    
        X = (XYZ.X / 100)
        Y = (XYZ.Y / 100)
        Z = (XYZ.Z / 100)
    
        R = ((X * 3.2406) + (Y * -1.5372) + (Z * -0.4986))
        G = ((X * -0.9689) + (Y * 1.8758) + (Z * 0.0415))
        B = ((X * 0.0557) + (Y * -0.204) + (Z * 1.057))
    
        Dim Less As Double = 0.0031308
    
        If (R > Less) Then
            X = ((1.055 * Math.Pow(R, (1.0 / 2.4))) - 0.055)
        Else
            X = (R * 12.92)
        End If
        If (G > Less) Then
            Y = ((1.055 * Math.Pow(G, (1.0 / 2.4))) - 0.055)
        Else
            Y = (G * 12.92)
        End If
        If (B > Less) Then
            Z = ((1.055 * Math.Pow(B, (1.0 / 2.4))) - 0.055)
        Else
            Z = (B * 12.92)
        End If
    
        Return New Color(CSng(X), CSng(Y), CSng(Z))
    End Function
    
    Private Function RGBToXYZ(Color As Color) As XYZ
        Dim RGB = ColorToRGB(Color)
        Dim X, Y, Z As New Double
        Dim Less As Double = 0.04045
    
        If (RGB.R > Less) Then
            X = Math.Pow(((RGB.R + 0.055) / 1.055), 2.4)
        Else
            X = (RGB.R / 12.92)
        End If
        If (RGB.G > Less) Then
            Y = Math.Pow(((RGB.G + 0.055) / 1.055), 2.4)
        Else
            Y = (RGB.G / 12.92)
        End If
        If (RGB.B > Less) Then
            Z = Math.Pow(((RGB.B + 0.055) / 1.055), 2.4)
        Else
            Z = (RGB.B / 12.92)
        End If
    
        X = (((X * 0.4124) + (Y * 0.3576) + (Z * 0.1805)) * 100.0)
        Y = (((X * 0.2126) + (Y * 0.7152) + (Z * 0.0722)) * 100.0)
        Z = (((X * 0.0193) + (Y * 0.1192) + (Z * 0.9505)) * 100.0)
    
        Return New XYZ(X, Y, Z)
    End Function
    
    Private Function XYZToLAB(XYZ As XYZ) As LAB
        Dim X, Y, Z As New Double
        Dim L, A, B As New Double
        Dim Less As Double = 0.008856
    
        X = ((XYZ.X / 95.047) + (XYZ.Y / 100) + (XYZ.Z / 108.883))
        Y = ((XYZ.X / 95.047) + (XYZ.Y / 100) + (XYZ.Z / 108.883))
        Z = ((XYZ.X / 95.047) + (XYZ.Y / 100) + (XYZ.Z / 108.883))
    
        If (X > Less) Then
            X = Math.Pow(X, (1.0 / 3.0))
        Else
            X = ((7.787 * X) + (16.0 / 116.0))
        End If
        If (Y > Less) Then
            Y = Math.Pow(Y, (1.0 / 3.0))
        Else
            Y = ((7.787 * Y) + (16.0 / 116.0))
        End If
        If (Z > Less) Then
            Z = Math.Pow(Z, (1.0 / 3.0))
        Else
            Z = ((7.787 * Z) + (16.0 / 116.0))
        End If
    
        L = ((116.0 * Y) - 16.0)
        A = (500.0 * (X - Y))
        B = (200.0 * (Y - Z))
    
        Return New LAB(L, A, B)
    End Function
    
    Function ColorToRGB(Color As Color) As RGB
        Return New RGB((Convert.ToInt32(Color.R) / 255), (Convert.ToInt32(Color.G) / 255), (Convert.ToInt32(Color.B) / 255))
    End Function
    Public Class RGB
    Public ReadOnly Min As Double = 0
    Public ReadOnly Max As Double = 1
    
    Public Sub New()
    End Sub
    
    Public Sub New(R As Double, G As Double, B As Double)
        Me.R = R
        Me.G = G
        Me.B = B
    End Sub
    
    Public Sub New(Color As Color)
        Me.R = (Convert.ToInt32(Color.R) / 255)
        Me.G = (Convert.ToInt32(Color.G) / 255)
        Me.B = (Convert.ToInt32(Color.B) / 255)
    End Sub
    
    Private _R As New Double
    Private _G As New Double
    Private _B As New Double
    
    Public Property R As Double
        Get
            Return _R
        End Get
        Set
            _R = LimitInRange(Value, Min, Max)
        End Set
    End Property
    
    Public Property G As Double
        Get
            Return _G
        End Get
        Set
            _G = LimitInRange(Value, Min, Max)
        End Set
    End Property
    
    Public Property B As Double
        Get
            Return _B
        End Get
        Set
            _B = LimitInRange(Value, Min, Max)
        End Set
    End Property
    
    Overrides Function ToString() As String
        Return (_R.ToString & ":"c & _G.ToString & ":"c & _B.ToString)
    End Function
    End Class
    
    Public Class XYZ
    Public ReadOnly Min As Double = 0
    Public ReadOnly Max As Double = 100
    
    Public Sub New()
    End Sub
    
    Public Sub New(X As Double, Y As Double, Z As Double)
        Me.X = X
        Me.Y = Y
        Me.Z = Z
    End Sub
    
    Private _X As New Double
    Private _Y As New Double
    Private _Z As New Double
    
    Public Property X As Double
        Get
            Return _X
        End Get
        Set
            _X = LimitInRange(Value, Min, Max)
        End Set
    End Property
    
    Public Property Y As Double
        Get
            Return _Y
        End Get
        Set
            _Y = LimitInRange(Value, Min, Max)
        End Set
    End Property
    
    Public Property Z As Double
        Get
            Return _Z
        End Get
        Set
            _Z = LimitInRange(Value, Min, Max)
        End Set
    End Property
    
    Overrides Function ToString() As String
        Return (_X.ToString & ":"c & _Y.ToString & ":"c & _Z.ToString)
    End Function
    End Class
    
    Public Class LAB
    Public ReadOnly Min As Double = -128
    Public ReadOnly Max As Double = 127
    
    Sub New()
    End Sub
    
    Sub New(L As Double, A As Double, B As Double)
        Me.L = L
        Me.A = A
        Me.B = B
    End Sub
    
    Private _L As New Double
    Private _A As New Double
    Private _B As New Double
    
    Property L As Double
        Get
            Return _L
        End Get
        Set
            _L = LimitInRange(Value, 0, 100)
        End Set
    End Property
    
    Property A As Double
        Get
            Return _A
        End Get
        Set
            _A = LimitInRange(Value, Min, Max)
        End Set
    End Property
    
    Property B As Double
        Get
            Return _B
        End Get
        Set
            _B = LimitInRange(Value, Min, Max)
        End Set
    End Property
    
    Overrides Function ToString() As String
        Return (_L.ToString & ":"c & _A.ToString & ":"c & _B.ToString)
    End Function
    End Class
    
    Function LimitInRange(Value As Double, Min As Double, Max As Double) As Double
        Select Case Value
            Case <= Min
                Return Min
            Case >= Max
                Return Max
            Case Else
                Return Value
        End Select
    End Function
    

I need the code in VB.Net, that's why i'm working on converting and adapting the unity code for my project, however i am stuck and need some help.

If anybody knows what i'm doing wrong, i'll be glad to listen.

UPDATE 1: I've tried to correct the conversion more by mismatching the two conversion methods, i'm getting closer to a perfect conversion, however i'm afraid that i might have gotten tunnel vision from working on this issue for so long.

Sample :

  • LAB _ 0:0:0
  • XYZ _ 0.262413383082537:0.262413383082537:0.262413383082537
  • RGB _ {R:10 G:8 B:7 A:255}
  • XYZ _ 0.250358161840588:0.253536089358344:0.236754082437929
  • LAB _ 2.29017121228677:-0.12373260790384:0.261362975778545

As you see the problem is less than before but it's still there.

    Private Function LABToXYZ(LAB As LAB) As XYZ
        Dim X, Y, Z As New Double

        Y = ((LAB.L + 16.0) / 116.0)
        X = ((LAB.A / 500.0) + Y)
        Z = (Y - (LAB.B / 200.0))

        Dim Less = 0.008856

        If (X > Less) Then
            X = Math.Pow(X, 3)
        Else
            X = ((X - 16.0 / 116.0) / 7.787)
        End If
        If (Y > Less) Then
            Y = Math.Pow(Y, 3)
        Else
            Y = ((Y - 16.0 / 116.0) / 7.787)
        End If
        If (Z > Less) Then
            Z = Math.Pow(Z, 3)
        Else
            Z = ((Z - 16.0 / 116.0) / 7.787)
        End If

        Return New XYZ(X * 100, Y * 100, Z * 100)
    End Function

    Private Function XYZToRGB(XYZ As XYZ) As Color
        Dim R, G, B As New Double
        Dim X, Y, Z As New Double

        X = (XYZ.X / 100)
        Y = (XYZ.Y / 100)
        Z = (XYZ.Z / 100)

        R = ((X * 3.2406) + (Y * -1.5372) + (Z * -0.4986))
        G = ((X * -0.9689) + (Y * 1.8758) + (Z * 0.0415))
        B = ((X * 0.0557) + (Y * -0.204) + (Z * 1.057))

        Dim Less As Double = 0.0031308

        If (R > Less) Then
            R = ((1.055 * Math.Pow(R, (1.0 / 2.4))) - 0.055)
        Else
            R = (R * 12.92)
        End If
        If (G > Less) Then
            G = ((1.055 * Math.Pow(G, (1.0 / 2.4))) - 0.055)
        Else
            G = (G * 12.92)
        End If
        If (B > Less) Then
            B = ((1.055 * Math.Pow(B, (1.0 / 2.4))) - 0.055)
        Else
            B = (B * 12.92)
        End If

        Return New Color(CSng(R), CSng(G), CSng(B))
    End Function

    Private Function RGBToXYZ(Color As Color) As XYZ
        Dim RGB = ColorToRGB(Color)
        Dim X, Y, Z As New Double
        Dim R, G, B As New Double
        Dim Less As Double = 0.04045

        If (RGB.R > Less) Then
            r = Math.Pow(((RGB.R + 0.055) / 1.055), 2.4)
        Else
            R = (RGB.R / 12.92)
        End If
        If (RGB.G > Less) Then
            G = Math.Pow(((RGB.G + 0.055) / 1.055), 2.4)
        Else
            G = (RGB.G / 12.92)
        End If
        If (RGB.B > Less) Then
            B = Math.Pow(((RGB.B + 0.055) / 1.055), 2.4)
        Else
            B = (RGB.B / 12.92)
        End If

        R *= 100
        G *= 100
        B *= 100

        X = ((R * 0.4124) + (G * 0.3576) + (B * 0.1805))
        Y = ((R * 0.2126) + (G * 0.7152) + (B * 0.0722))
        Z = ((R * 0.0193) + (G * 0.1192) + (B * 0.9505))

        Return New XYZ(X, Y, Z)
    End Function

    Private Function XYZToLAB(XYZ As XYZ) As LAB
        Dim X, Y, Z As New Double
        Dim L, A, B As New Double
        Dim Less As Double = 0.008856

        X = XYZ.X / 100
        Y = XYZ.Y / 100
        Z = XYZ.Z / 100

        If (X > Less) Then
            X = Math.Pow(X, (1.0 / 3.0))
        Else
            X = ((7.787 * X) + (16.0 / 116.0))
        End If
        If (Y > Less) Then
            Y = Math.Pow(Y, (1.0 / 3.0))
        Else
            Y = ((7.787 * Y) + (16.0 / 116.0))
        End If
        If (Z > Less) Then
            Z = Math.Pow(Z, (1.0 / 3.0))
        Else
            Z = ((7.787 * Z) + (16.0 / 116.0))
        End If

        L = ((116.0 * Y) - 16.0)
        A = (500.0 * (X - Y))
        B = (200.0 * (Y - Z))

        Return New LAB(L, A, B)
    End Function

UPDATE 2: Further testing shows an exceptionally undesired behavior in XNA.Framework.Color, resulting in any fraction being interpreted as a %. Meaning that 200.10 would be over 200% of the max color value(255), which would cap it at the max value(255), so unless you specify integers you could end up getting a very wrong output.

I'm trying to mismatch the code from this example as well. I feel that i'm progressing, even if i had to go away from using the XNA.Framework.Color class in the conversions.

I'll update with a final solution if i find one.

UPDATE 3: Online testing here (source code here) and here shows that my LABToXYZ is incorrect.

My results :

  • Lab _ 100:0:0
  • XYZ _ 95.047:100:100

Their results :

  • Lab _ 100:0:0
  • XYZ _ 95.05:100:108.88

    Public Function LABtoXYZ(LAB As LAB) As XYZ
        Dim X, Y, Z As Double
        Y = ((LAB.L + 16.0) / 116.0)
        X = ((LAB.A / 500.0) + Y)
        Z = (Y - (LAB.B / 200.0))
    
        Dim Pow_X = Math.Pow(X, 3.0)
        Dim Pow_Y = Math.Pow(Y, 3.0)
        Dim Pow_Z = Math.Pow(Z, 3.0)
    
        Dim Less = 216 / 24389
    
        If (Pow_X > Less) Then
            X = Pow_X
        Else
            X = ((X - (16.0 / 116.0)) / 7.787)
        End If
        If (Pow_Y > Less) Then
            Y = Pow_Y
        Else
            Y = ((Y - (16.0 / 116.0)) / 7.787)
        End If
        If (Pow_Z > Less) Then
            Z = Pow_Z
        Else
            Z = ((Z - (16.0 / 116.0)) / 7.787)
        End If
    
        Return New XYZ((X * 95.047), (Y * 100.0), (Z * 108.883))
    End Function
    

But doing LAB with all 0s result in a XYZ with all 0s, which is correct behavior, i can't tell what's wrong, it's Z that's incorrect but where is the error in my code?

Further examples here seems to suggest that my code is correct but i'm still getting an incorrect Z.

UPDATE 4: Further refinement and re-redoing all the code, i've found that a conversion and an adaption of the examples found here, gave me the results i wanted, even tho there were some errors in that examples, notable the ^2.2 when it should have been ^2.4.

I also found some problems with precision that had to turn doubles into integers for the conversion to be perfect, but this might be the final update, unless i experience any issues, i'll leave this question open for awhile as i continue to test the code in practice. I will come back and mark it as answered when i'm confident that the code isn't flawed.

Sample : Test 1

  • LAB _ 1:0:0
  • XYZ _ 0.105222895807779:0.110706172533356:0.120540201839494
  • RGB _ 4:4:4:255
  • XYZ _ 0.115400959145268:0.121410793419535:0.132216354033874
  • LAB _ 1:0:0

Test 2

  • LAB _ 10:0:0
  • XYZ _ 1.07024816003116:1.12601992701628:1.22604427713313
  • RGB _ 27:27:27:255
  • XYZ _ 1.04175693531671:1.09600940064882:1.19355423730657
  • LAB _ 10:0:0

Test 3

  • LAB _ 100:0:0
  • XYZ _ 95.047:100:108.883
  • RGB _ 255:255:255:255
  • XYZ _ 95.05:100:108.9
  • LAB _ 100:0:0

Test 4

  • LAB _ 11:0:0
  • XYZ _ 1.19854884694432:1.26100649883144:1.37302170612264
  • RGB _ 29:29:29:255
  • XYZ _ 1.16783071832485:1.22864883569159:1.33799858206814
  • LAB _ 11:0:0

As seen above, there's a tiny variation that if not rounded, would cause an imperfect conversion.

The Classes

  Public Class RGB
        Public ReadOnly Min As Double = 0.0
        Public ReadOnly Max As Double = 255.0

        Public Sub New()
        End Sub

        Public Sub New(R As Integer, G As Integer, B As Integer)
            Me.R = R
            Me.G = G
            Me.B = B
        End Sub

        Public Sub New(R As Integer, G As Integer, B As Integer, A As Integer)
            Me.R = R
            Me.G = G
            Me.B = B
            Me.A = A
        End Sub
        Public Sub New(R As Double, G As Double, B As Double, A As Double)
            Me.R = Convert.ToInt32(R)
            Me.G = Convert.ToInt32(G)
            Me.B = Convert.ToInt32(B)
            Me.A = Convert.ToInt32(A)
        End Sub
        Public Sub New(R As Double, G As Double, B As Double)
            Me.R = Convert.ToInt32(R * 255)
            Me.G = Convert.ToInt32(G * 255)
            Me.B = Convert.ToInt32(B * 255)
        End Sub
        Public Sub New(Color As Color)
            Me.R = Convert.ToInt32(Color.R)
            Me.G = Convert.ToInt32(Color.G)
            Me.B = Convert.ToInt32(Color.B)
            Me.A = Convert.ToInt32(Color.A)
        End Sub

        Private _R As New Double
        Private _G As New Double
        Private _B As New Double
        Private _A As Double = 255

        Public Property R As Double
            Get
                Return _R
            End Get
            Set
                _R = LimitInRange(Value, Min, Max)
            End Set
        End Property

        Public Property G As Double
            Get
                Return _G
            End Get
            Set
                _G = LimitInRange(Value, Min, Max)
            End Set
        End Property

        Public Property B As Double
            Get
                Return _B
            End Get
            Set
                _B = LimitInRange(Value, Min, Max)
            End Set
        End Property

        Public Property A As Double
            Get
                Return _A
            End Get
            Set
                _A = LimitInRange(Value, Min, Max)
            End Set
        End Property

        Overrides Function ToString() As String
            Return (_R.ToString & ":"c & _G.ToString & ":"c & _B.ToString & ":"c & _A.ToString)
        End Function

        Public Shared Operator =(Left As RGB, Right As RGB) As Boolean
            If ((Left.R = Right.R) AndAlso (Left.G = Right.G) AndAlso (Left.B = Right.B) AndAlso (Left.A = Right.A)) Then
                Return True
            Else
                Return False
            End If
        End Operator

        Public Shared Operator <>(Left As RGB, Right As RGB) As Boolean
            Return (Not (Left = Right))
        End Operator

    End Class

    Public Class XYZ
        Public ReadOnly Min As Double = 0

        Public Sub New()
        End Sub

        Public Sub New(X As Double, Y As Double, Z As Double)
            Me.X = X
            Me.Y = Y
            Me.Z = Z
        End Sub

        Private _X As New Double
        Private _Y As New Double
        Private _Z As New Double

        Public Property X As Double
            Get
                Return _X
            End Get
            Set
                _X = LimitInRange(Value, Min, 95.05)
            End Set
        End Property

        Public Property Y As Double
            Get
                Return _Y
            End Get
            Set
                _Y = LimitInRange(Value, Min, 100)
            End Set
        End Property

        Public Property Z As Double
            Get
                Return _Z
            End Get
            Set
                _Z = LimitInRange(Value, Min, 108.9)
            End Set
        End Property

        Overrides Function ToString() As String
            Return (_X.ToString & ":"c & _Y.ToString & ":"c & _Z.ToString)
        End Function

    End Class

    Public Class LAB
        Public ReadOnly Min As Double = -128
        Public ReadOnly Max As Double = 127

        Sub New()
        End Sub

        Sub New(L As Double, A As Double, B As Double)
            Me.L = L
            Me.A = A
            Me.B = B
        End Sub

        Private _L As New Double
        Private _A As New Double
        Private _B As New Double

        Property L As Double
            Get
                Return _L
            End Get
            Set
                _L = LimitInRange(Value, 0, 100)
            End Set
        End Property

        Property A As Double
            Get
                Return _A
            End Get
            Set
                _A = LimitInRange(Value, Min, Max)
            End Set
        End Property

        Property B As Double
            Get
                Return _B
            End Get
            Set
                _B = LimitInRange(Value, Min, Max)
            End Set
        End Property

        Overrides Function ToString() As String
            Return (_L.ToString & ":"c & _A.ToString & ":"c & _B.ToString)
        End Function

    End Class

Converters

Public Function LABtoXYZ(LAB As LAB) As XYZ
        Dim X, Y, Z As New Double
        Y = ((LAB.L + 16.0) / 116.0)
        X = ((LAB.A / 500.0) + Y)
        Z = (Y - (LAB.B / 200.0))

        Dim Pow_X = Math.Pow(X, 3.0)
        Dim Pow_Y = Math.Pow(Y, 3.0)
        Dim Pow_Z = Math.Pow(Z, 3.0)

        Dim Less = (216 / 24389)

        If (Pow_X > Less) Then
            X = Pow_X
        Else
            X = ((X - (16.0 / 116.0)) / 7.787)
        End If
        If (Pow_Y > Less) Then
            Y = Pow_Y
        Else
            Y = ((Y - (16.0 / 116.0)) / 7.787)
        End If
        If (Pow_Z > Less) Then
            Z = Pow_Z
        Else
            Z = ((Z - (16.0 / 116.0)) / 7.787)
        End If

        Return New XYZ((X * 95.047), (Y * 100.0), (Z * 108.883))
    End Function

    Private Function XYZToRGB(XYZ As XYZ) As RGB
        Dim X, Y, Z As New Double
        Dim R, G, B As New Double
        Dim Pow As Double = (1.0 / 2.4)
        Dim Less As Double = 0.0031308

        X = (XYZ.X / 100)
        Y = (XYZ.Y / 100)
        Z = (XYZ.Z / 100)

        R = ((X * 3.24071) + (Y * -1.53726) + (Z * -0.498571))
        G = ((X * -0.969258) + (Y * 1.87599) + (Z * 0.0415557))
        B = ((X * 0.0556352) + (Y * -0.203996) + (Z * 1.05707))

        If (R > Less) Then
            R = ((1.055 * Math.Pow(R, Pow)) - 0.055)
        Else
            R *= 12.92
        End If
        If (G > Less) Then
            G = ((1.055 * Math.Pow(G, Pow)) - 0.055)
        Else
            G *= 12.92
        End If
        If (B > Less) Then
            B = ((1.055 * Math.Pow(B, Pow)) - 0.055)
        Else
            B *= 12.92
        End If

        Return New RGB(R, G, B)
    End Function

    Private Function RGBToXYZ(RGB As RGB) As XYZ
        Dim X, Y, Z As New Double
        Dim R, G, B As New Double
        Dim Less As Double = 0.04045

        R = (RGB.R / 255)
        G = (RGB.G / 255)
        B = (RGB.B / 255)

        If (R > Less) Then
            R = Math.Pow(((R + 0.055) / 1.055), 2.4)
        Else
            R = (R / 12.92)
        End If
        If (G > Less) Then
            G = Math.Pow(((G + 0.055) / 1.055), 2.4)
        Else
            G = (G / 12.92)
        End If
        If (B > Less) Then
            B = Math.Pow(((B + 0.055) / 1.055), 2.4)
        Else
            B = (B / 12.92)
        End If

        X = ((R * 0.4124) + (G * 0.3576) + (B * 0.1805))
        Y = ((R * 0.2126) + (G * 0.7152) + (B * 0.0722))
        Z = ((R * 0.0193) + (G * 0.1192) + (B * 0.9505))

        Return New XYZ(X * 100, Y * 100, Z * 100)
    End Function

    Private Function XYZToLAB(XYZ As XYZ) As LAB
        Dim X, Y, Z As New Double
        Dim L, A, B As New Double
        Dim Less As Double = 0.008856
        Dim Pow As Double = (1.0 / 3.0)

        X = ((XYZ.X / 100) / 0.9505)
        Y = (XYZ.Y / 100)
        Z = ((XYZ.Z / 100) / 1.089)

        If (X > Less) Then
            X = Math.Pow(X, Pow)
        Else
            X = ((7.787 * X) + (16.0 / 116.0))
        End If
        If (Y > Less) Then
            Y = Math.Pow(Y, Pow)
        Else
            Y = ((7.787 * Y) + (16.0 / 116.0))
        End If
        If (Z > Less) Then
            Z = Math.Pow(Z, Pow)
        Else
            Z = ((7.787 * Z) + (16.0 / 116.0))
        End If

        L = ((116.0 * Y) - 16.0)
        A = (500.0 * (X - Y))
        B = (200.0 * (Y - Z))

        'We solve the precision problem by rounding to nearest integer
        'This makes the conversion perfect.
        Return New LAB(CInt(L), CInt(A), CInt(B))
    End Function

Further testing is required before i'll mark this as solved.

UPDATE 5: Haven't had any issues so far... I don't know how to mark this as answered when there is only the question posted. The full free code and more can be found here.

1

There are 1 best solutions below

0
On

I have not parsed all your code, but an issue in your first code block, in the function RGBToXYZ

    X = (((X * 0.4124) + (Y * 0.3576) + (Z * 0.1805)) * 100.0)
    Y = (((X * 0.2126) + (Y * 0.7152) + (Z * 0.0722)) * 100.0)
    Z = (((X * 0.0193) + (Y * 0.1192) + (Z * 0.9505)) * 100.0)

    Return New XYZ(X, Y, Z)

You do the matrix for X, then use X again for the matrix for Y... but now X is at the new value! This is not a place to be skimpy on variables.

This should be instead something like this:

    Dim Xout, Yout, Zout As New Double

    Xout = ((X * 0.4124) + (Y * 0.3576) + (Z * 0.1805))
    Yout = ((X * 0.2126) + (Y * 0.7152) + (Z * 0.0722))
    Zout = ((X * 0.0193) + (Y * 0.1192) + (Z * 0.9505))

    Return New XYZ(Xout, Yout, Zout)

Also, I suggest keeping XYZ as a 0.0-1.0 range.

And for other things:

LABToXYZ is missing a needed illuminant conversion. It needs to return:

    X = (X * 0.95047)
    Z = (Z * 1.08883)

And then XYZtoLAB has:

    X = ((XYZ.X / 95.047) + (XYZ.Y / 100) + (XYZ.Z / 108.883))
    Y = ((XYZ.X / 95.047) + (XYZ.Y / 100) + (XYZ.Z / 108.883))
    Z = ((XYZ.X / 95.047) + (XYZ.Y / 100) + (XYZ.Z / 108.883))

Which is just making X Y and Z all the same...

Should be (assuming keeping XYZ as 0-1):

    X = (XYZ.X / 0.95047)
    Y = (XYZ.Y)
    Z = (XYZ.Z / 1.08883)

I just realized that you solved your own question — I'll leave this here though in case someone runs across it in search of similar answers.