Color Conversion to CIE Color Spaces

I know how to convert from the default BGR color space into others using the cvtColor function. However, I’m very familiar with CIELAB and other color spaces defined by the International Commission on Illumination. The values provided by OpenCV seem to be scaled into 0-256 ranges and not correlate well with any of these tools.

For instance:
RGB = [0,0,0] converted into LAB yields [0, 128, 128] when in CIELAB it should be [0,0,0]
RGB = [255,255,255] converted into LAB yields [255, 128, 128] when in CIELAB it should be [100,0,0]
RGB = [0,255,0] converted into LAB yields [224.0, 42.0, 211.0] when in CIELAB the A scale should be extremely negative. I’ve also never seen an L scale or B scale anywhere near that high.

While I fully expect getting truly accurate color values of an object from an image of an object to be impossible, is there an established (preferably opensource or standard) way to approximate any CIE colorspace from the values provided by OpenCV? I’m sure I can develop an approximation myself by correlating a large number of known comparisons but I would rather not reinvent the wheel. This website: http://colormine.org/ seems to do this and claims to use open source code but the github seems to have been taken down.

Code used:

test = np.zeros((1,1,3), np.uint8)
test[0,0,0]=0
test[0,0,1]=255
test[0,0,2]=0
print(test)
print(cv2.cvtColor(test, cv2.COLOR_RGB2LAB))

that’s right. not just scaled, also translated. can’t represent negative numbers in a uint8 type. that is why 0 is shifted to being 128.

I don’t know the exact (sub-integer) behavior. the docs should state this. if they don’t, you might need to dig into the source code.

https://docs.opencv.org/3.4/de/d25/imgproc_color_conversions.html

Here is that documentation. The subtracting 128 helps obviously (I figured that much out). I will investigate this exact version of XYZ and see if that fits with expected values. They still seemed very high but I will double check.

Ok, converting to a np.float32 instead of uint8 seems to provide closer numbers for CIELAB* which fits with this documentation.

The XYZ values do not seem to be correct. I can recreate the values by multiplying by the matrix given in OpenCV: Color conversions. The equations on that sheet to generate CIELab from XYZ seem to be reasonable. But the values for XYZ placed through those calculations yield absurd results. For instance, one of my pixels has a 93 Y value and a 39.6 L value. However, the calculations on that sheet state that the L value should be about 93^(1/3)*116-16 which about 510. A realistic Y value should be able 0.11 for this color.

I would appreciate if someone could point me to the source code.

Y is assumed to be around -1…+1 or something equally “normal”, not valued anywhere near 93

that is why the 116 is there (and the +16), to scale it from -1…+1 to 0…255

When you do cvtColor with RGB2XYZ, it gives Y values between 0 and 255. That is where the 93 came from. If you multiply RGB values by the matrix in the documentation I showed above, you get those exact values. Typically, I would expect a Y value of about 11 or 0.11 for that color depending on how it is reported. Y should be 0-1 or 0-100. L should be 0-100. I’ll try again I guess when i have more time. Dividing by 255 didn’t help (gave L of 66 or so when 40 is expected) but I think I know what to try next.

I’m not really sure how to get this to work properly and I’m not sure there is a point in continuing. The code below shows how to get XYZ and Lab* from a given RGB value in two ways: cvtColor and the documentation I’ve linked before about how cvtColor is intended to work.

My code gives the exact same values for XYZ as cvtColor. However, these values are significantly different from expected values. The Y value is 93 if I don’t divide by 255 first and 0.36 if I do. The expected value should be about 0.12. (In all honesty, i’m used to XYZ values being about 100 times these ones but that is just scaling I think.)

Calculating Lab* from the XYZ values (which is not a option using cvtColor) yields an L value of about 67 when 41 is expected.

My conclusion is that the matrix in the documentation is the one to determine XYZ but it isn’t the one used as an intermediate to calculate Lab*. Alternatively, there is an additional processing step I missed.

import cv2                                                                      #This is importing OpenCV
import numpy as np                                                              #This is importing numpy which I do automatically but allows matrix like manipulation of data

StartingRGB = np.zeros((1,1,3),np.uint8)
StartingRGB[0,0,0] = 50
StartingRGB[0,0,1] = 100
StartingRGB[0,0,2] = 150

StartingRGB = np.multiply(StartingRGB.astype(np.float32),1/255.0)

opencvXYZ = cv2.cvtColor(StartingRGB, cv2.COLOR_RGB2XYZ)
opencvLAB = cv2.cvtColor(StartingRGB, cv2.COLOR_RGB2LAB)

XYZRGBConverter = [[0.412453,0.357580,0.180423],[0.212671,0.715160,0.072169],[0.019334,0.119193,0.950227]]

calculatedXYZ = np.reshape(np.transpose(np.dot(XYZRGBConverter,np.transpose(np.reshape(StartingRGB,(-1,3))))),(-1,1,3))
calculatedLAB = np.zeros_like(opencvXYZ)
calculatedLAB[:,0,0] = [116*np.cbrt(t)-16 if t>0.008856 else 903.3*t for t in opencvXYZ[:,0,1]]

temp = np.zeros_like(opencvXYZ)
temp[:,0,0] = [np.cbrt(t) if t>0.008856 else 7.787*t+16.0/116.0 for t in opencvXYZ[:,0,0]/0.950456]
temp[:,0,1] = [np.cbrt(t) if t>0.008856 else 7.787*t+16.0/116.0 for t in opencvXYZ[:,0,1]]
temp[:,0,2] = [np.cbrt(t) if t>0.008856 else 7.787*t+16.0/116.0 for t in opencvXYZ[:,0,2]/1.088754]
calculatedLAB[:,0,1] = 500*(temp[:,0,0]-temp[:,0,1])
calculatedLAB[:,0,2] = 200*(temp[:,0,1]-temp[:,0,2])

expectedXYZ = np.zeros_like(opencvXYZ)
expectedXYZ[0,0,0] = 0.11377584875271197
expectedXYZ[0,0,1] = 0.1199446097547431
expectedXYZ[0,0,2] = 0.30569660696966523

recalculatedLAB = np.zeros_like(expectedXYZ)
recalculatedLAB[:,0,0] = [116*np.cbrt(t)-16 if t>0.008856 else 903.3*t for t in expectedXYZ[:,0,1]]

tempy = np.zeros_like(expectedXYZ)
tempy[:,0,0] = [np.cbrt(t) if t>0.008856 else 7.787*t+16.0/116.0 for t in expectedXYZ[:,0,0]/0.950456]
tempy[:,0,1] = [np.cbrt(t) if t>0.008856 else 7.787*t+16.0/116.0 for t in expectedXYZ[:,0,1]]
tempy[:,0,2] = [np.cbrt(t) if t>0.008856 else 7.787*t+16.0/116.0 for t in expectedXYZ[:,0,2]/1.088754]

recalculatedLAB[:,0,1] = 500*(tempy[:,0,0]-tempy[:,0,1])
recalculatedLAB[:,0,2] = 200*(tempy[:,0,1]-tempy[:,0,2])


print("RGB value to start with (normallized to 0 to 1 range): ")
print(StartingRGB)
print("\nXYZ calculated with cvtColor: ")
print(opencvXYZ)
print("\nL*a*b* calculated with cvtColor: ")
print(opencvLAB)
print("\nXYZ calculated from instructions: ")
print(calculatedXYZ)
print("\nL*a*b* calculated from instructions: ")
print(calculatedLAB)
print("\nXYZ value expected from other sources: ")
print(expectedXYZ)
print("\nL*a*b* calculated from instructions and expected value: ")
print(recalculatedLAB)

RGB value to start with (normalized to 0 to 1 range):
[[[0.19607845 0.3921569 0.5882353 ]]]

XYZ calculated with cvtColor:
[[[0.3272318 0.36460748 0.60949045]]]

Lab* calculated with cvtColor:
[[[ 41.082764 0.046875 -32.46875 ]]]

XYZ calculated from instructions:
[[[0.32723179 0.36460748 0.60949042]]]

Lab* calculated from instructions:
[[[ 66.870476 -6.762356 -21.952188]]]

XYZ value expected from other sources:
[[[0.11377585 0.11994461 0.3056966 ]]]

Lab* calculated from instructions and expected value:
[[[ 41.207314 -0.16321242 -32.330204 ]]]