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 :
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.
I have not parsed all your code, but an issue in your first code block, in the function RGBToXYZ
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:
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:
And then XYZtoLAB has:
Which is just making X Y and Z all the same...
Should be (assuming keeping XYZ as 0-1):
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.