Transforms
As seen in the Color Space page, it's important to understand the difference between Absolute (or device-independent) Color Space (.e.g. CIEXYZ, CIExyY, CIELAB) and device-dependent colors spaces (e.g. RGB, HSL, HSB, HSV).
For example, it makes sense to convert from a device-dependent RGB space to a HSL space since, even though they are both device-dependent, they are defined in the same "dependent space".
Also, it makes sense to convert from 2 device-independent spaces like CIEXYZ and CIELAB.
Most importantly, the conversions between device-dependent color spaces and device-independent color spaces must always be accompanied by a Color Profile that appropriately describes the dependence to the device.
Device-Dependent Color Space Conversions
Here, we will list the different conversions between device-dependent color spaces.
RGB / HSL
(Source: http://www.easyrgb.com)
RGB → HSL
- Input: RGB in [0,1]
- Output: HSL in [0,1]
var_Min = min( R, G, B ) // Min. value of RGB var_Max = max( R, G, B ) // Max. value of RGB del_Max = var_Max - var_Min // Delta RGB value L = ( var_Max + var_Min ) / 2 if ( del_Max == 0 ) // This is a gray, no chroma... { H = 0 // HSL results from 0 to 1 S = 0 } else // Chromatic data... { if ( L < 0.5 ) S = del_Max / ( var_Max + var_Min ) else S = del_Max / ( 2 - var_Max - var_Min ) del_R = ( ( ( var_Max - var_R ) / 6 ) + ( del_Max / 2 ) ) / del_Max del_G = ( ( ( var_Max - var_G ) / 6 ) + ( del_Max / 2 ) ) / del_Max del_B = ( ( ( var_Max - var_B ) / 6 ) + ( del_Max / 2 ) ) / del_Max if ( var_R == var_Max ) H = del_B - del_G else if ( var_G == var_Max ) H = ( 1 / 3 ) + del_R - del_B else if ( var_B == var_Max ) H = ( 2 / 3 ) + del_G - del_R if ( H < 0 ) H += 1 if ( H > 1 ) H -= 1 }
HSL → RGB
- Input: HSL in [0,1]
- Output: RGB in [0,1]
if ( S == 0 ) { (R,G,B) = L; } else { if ( L < 0.5 ) var_2 = L * ( 1 + S ) else var_2 = ( L + S ) - ( S * L ) var_1 = 2 * L - var_2 R = Hue_2_RGB( var_1, var_2, H + ( 1 / 3 ) ) G = Hue_2_RGB( var_1, var_2, H ) B = Hue_2_RGB( var_1, var_2, H - ( 1 / 3 ) ) } Hue_2_RGB( v1, v2, vH ) { if ( vH < 0 ) vH += 1 if ( vH > 1 ) vH -= 1 if ( ( 6 * vH ) < 1 ) return ( v1 + ( v2 - v1 ) * 6 * vH ) if ( ( 2 * vH ) < 1 ) return ( v2 ) if ( ( 3 * vH ) < 2 ) return ( v1 + ( v2 - v1 ) * ( ( 2 / 3 ) - vH ) * 6 ) return ( v1 ) }
RGB / HSV
(Source: http://www.easyrgb.com)
RGB → HSV
- Input: RGB in [0,1]
- Output: HSV in [0,1]
var_Min = min( R, G, B ) // Min. value of RGB var_Max = max( R, G, B ) // Max. value of RGB del_Max = var_Max - var_Min // Delta RGB value V = var_Max if ( del_Max == 0 ) // This is a gray, no chroma... { H = 0 // HSV results from 0 to 1 S = 0 } else // Chromatic data... { S = del_Max / var_Max del_R = ( ( ( var_Max - var_R ) / 6 ) + ( del_Max / 2 ) ) / del_Max del_G = ( ( ( var_Max - var_G ) / 6 ) + ( del_Max / 2 ) ) / del_Max del_B = ( ( ( var_Max - var_B ) / 6 ) + ( del_Max / 2 ) ) / del_Max if ( var_R == var_Max ) H = del_B - del_G else if ( var_G == var_Max ) H = ( 1 / 3 ) + del_R - del_B else if ( var_B == var_Max ) H = ( 2 / 3 ) + del_G - del_R if ( H < 0 ) H += 1 if ( H > 1 ) H -= 1 }
HSV → RGB
- Input: HSV in [0,1]
- Output: RGB in [0,1]
if ( S == 0 ) // HSV from 0 to 1 { (R,G,B) = V } else { var_h = H * 6 if ( var_h == 6 ) var_h = 0 // H must be < 1 var_i = int( var_h ) // Or ... var_i = floor( var_h ) var_1 = V * ( 1 - S ) var_2 = V * ( 1 - S * ( var_h - var_i ) ) var_3 = V * ( 1 - S * ( 1 - ( var_h - var_i ) ) ) if ( var_i == 0 ) { R = V ; G = var_3 ; B = var_1 } else if ( var_i == 1 ) { R = var_2 ; G = V ; B = var_1 } else if ( var_i == 2 ) { R = var_1 ; G = V ; B = var_3 } else if ( var_i == 3 ) { R = var_1 ; G = var_2 ; B = V } else if ( var_i == 4 ) { R = var_3 ; G = var_1 ; B = V } else { R = V ; G = var_1 ; B = var_2 } }
Device-Independent Color Space Conversions
Here, we will list the different conversions between device-independent color spaces.
XYZ / xyY
(Source: http://www.easyrgb.com)
XYZ → xyY
- Input: (Observer. = 2°, Illuminant = D65)
- X in [0, 0.95047]
- Y in [0, 1.00000]
- Z in [0, 1.08883]
- Output: xyY in [0,1]
xyY → XYZ
- Input: xyY in [0,1]
- Output: (Observer. = 2°, Illuminant = D65)
- X in [0, 0.95047]
- Y in [0, 1.00000]
- Z in [0, 1.08883]
XYZ / Lab
(Source: http://www.easyrgb.com)
Remember that CIE L*a*b* is device-independent but needs a white point reference nevertheless.
Here, the D65 illuminant is used.
XYZ → L*a*b*
- Input: (Observer. = 2°, Illuminant = D65)
- X in [0, 0.95047]
- Y in [0, 1.00000]
- Z in [0, 1.08883]
- Output:
- L* in [0,100]
- a*, b* in [-1,1]
var_X = X / ref_X // ref_X = 0.95047 Observer= 2°, Illuminant= D65 var_Y = Y / ref_Y // ref_Y = 1.000 var_Z = Z / ref_Z // ref_Z = 1.08883 if ( var_X > 0.008856 ) var_X = var_X ^ ( 1/3 ) else var_X = ( 7.787 * var_X ) + ( 16 / 116 ) if ( var_Y > 0.008856 ) var_Y = var_Y ^ ( 1/3 ) else var_Y = ( 7.787 * var_Y ) + ( 16 / 116 ) if ( var_Z > 0.008856 ) var_Z = var_Z ^ ( 1/3 ) else var_Z = ( 7.787 * var_Z ) + ( 16 / 116 ) CIE-L\* = ( 116 * var_Y ) - 16 CIE-a\* = 500 * ( var_X - var_Y ) CIE-b\* = 200 * ( var_Y - var_Z )
L*a*b* → XYZ
- Input:
- L* in [0,100]
- a*, b* in [-1,1]
- Output: (Observer. = 2°, Illuminant = D65)
- X in [0, 0.95047]
- Y in [0, 1.00000]
- Z in [0, 1.08883]
var_Y = ( CIE-L\* + 16 ) / 116 var_X = CIE-a\* / 500 + var_Y var_Z = var_Y - CIE-b\* / 200 if ( var_Y^3 > 0.008856 ) var_Y = var_Y^3 else var_Y = ( var_Y - 16 / 116 ) / 7.787 if ( var_X^3 > 0.008856 ) var_X = var_X^3 else var_X = ( var_X - 16 / 116 ) / 7.787 if ( var_Z^3 > 0.008856 ) var_Z = var_Z^3 else var_Z = ( var_Z - 16 / 116 ) / 7.787 X = ref_X * var_X // ref_X = 0.95047 Observer= 2°, Illuminant= D65 Y = ref_Y * var_Y // ref_Y = 1.00000 Z = ref_Z * var_Z // ref_Z = 1.08883
Device-dependent / Device-independent Color Space Conversions
RGB (in sRGB) / XYZ
(Source: http://www.easyrgb.com)
Please refer to the sRGB color profile specification to understand the pseudo-gamma correction in the following routines.
RGB → XYZ
- Input: RGB in [0,1] with sRGB gamma profile
- Output: (Observer. = 2°, Illuminant = D65)
- X in [0, 0.95047]
- Y in [0, 1.00000]
- Z in [0, 1.08883]
// Apply gamma correction (*i.e.* conversion to linear-space) if ( R > 0.04045 ) R = ( ( R + 0.055 ) / 1.055 ) ^ 2.4 else R = R / 12.92 if ( G > 0.04045 ) G = ( ( G + 0.055 ) / 1.055 ) ^ 2.4 else G = G / 12.92 if ( B > 0.04045 ) B = ( ( B + 0.055 ) / 1.055 ) ^ 2.4 else B = B / 12.92 // Observer. = 2°, Illuminant = D65 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
XYZ → RGB
- Input: (Observer. = 2°, Illuminant = D65)
- X in [0, 0.95047]
- Y in [0, 1.00000]
- Z in [0, 1.08883]
- Output: RGB in [0,1] with sRGB gamma profile
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.2040 + Z * 1.0570 if ( R > 0.0031308 ) R = 1.055 * ( R ^ ( 1 / 2.4 ) ) - 0.055 else R = 12.92 * R if ( G > 0.0031308 ) G = 1.055 * ( G ^ ( 1 / 2.4 ) ) - 0.055 else G = 12.92 * G if ( B > 0.0031308 ) B = 1.055 * ( B ^ ( 1 / 2.4 ) ) - 0.055 else B = 12.92 * B
RGB (in Adobe RGB) / XYZ
(Source: http://www.adobe.com/digitalimag/pdfs/AdobeRGB1998.pdf)
RGB → XYZ
- Input: RGB in [0,1] with Adobe RGB gamma profile
- Output: (Observer. = 2°, Illuminant = D65)
- X in [0, 0.95047]
- Y in [0, 1.00000]
- Z in [0, 1.08883]
// Gamma correction of ~2.2 R = R ^ 2.19921875 G = G ^ 2.19921875 B = B ^ 2.19921875 // Observer. = 2°, Illuminant = D65 X = 0.57667 * R + 0.18556 * G + 0.18823 * B Y = 0.29734 * R + 0.62736 * G + 0.07529 * B Z = 0.02703 * R + 0.07069 * G + 0.99134 * B
XYZ → RGB
- Input: (Observer. = 2°, Illuminant = D65)
- X in [0, 0.95047]
- Y in [0, 1.00000]
- Z in [0, 1.08883]
- Output: RGB in [0,1] with Adobe RGB gamma profile
R = 2.04159 * X - 0.56501 * Y - 0.34473 * Z G = -0.96924 * X + 1.87597 * Y + 0.04156 * Z B = 0.01344 * X - 0.11836 * Y + 1.01517 * Z // Gamma correction R = R ^ (1.0 / 2.19921875) G = G ^ (1.0 / 2.19921875) B = B ^ (1.0 / 2.19921875)
RGB (in Adobe RGB ICC Profile v2.4) / XYZ
(Source: http://www.adobe.com/digitalimag/pdfs/AdobeRGB1998.pdf)
RGB → XYZ
- Input: RGB in [0,1] with Adobe RGB gamma profile
- Output: (Observer. = 2°, Illuminant = D50)
- X in [0, 0.9642]
- Y in [0, 1.0000]
- Z in [0, 0.8249]
// Gamma correction of ~2.2 R = R ^ 2.19921875 G = G ^ 2.19921875 B = B ^ 2.19921875 // Observer. = 2°, Illuminant = D50 X = 0.60974 * R + 0.20528 * G + 0.14919 * B Y = 0.31111 * R + 0.62567 * G + 0.06322 * B Z = 0.01947 * R + 0.06087 * G + 0.74457 * B
XYZ → RGB
- Input: (Observer. = 2°, Illuminant = D50)
- X in [0, 0.9642]
- Y in [0, 1.0000]
- Z in [0, 0.8249]
- Output: RGB in [0,1] with Adobe RGB gamma profile
R = 1.96253 * X - 0.61068 * Y - 0.34137 * Z G = -0.97876 * X + 1.91615 * Y + 0.03342 * Z B = 0.02869 * X - 0.14067 * Y + 1.34926 * Z // Gamma correction R = R ^ (1.0 / 2.19921875) G = G ^ (1.0 / 2.19921875) B = B ^ (1.0 / 2.19921875)
RGB (in ProPhoto RGB) / XYZ
(Source: http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.202.294)
RGB → XYZ
- Input: RGB in [0,1] with ProPhoto RGB gamma profile
- Output: (Observer. = 2°, Illuminant = D50)
- X in [0, 0.96421]
- Y in [0, 1.00000]
- Z in [0, 0.82519]
// Gamma correction if ( R > 16 * 0.001953 ) R = R ^ 1.8 else R = R / 16 if ( G > 16 * 0.001953 ) G = G ^ 1.8 else G = G / 16 if ( B > 16 * 0.001953 ) B = B ^ 1.8 else B = B / 16 // Observer. = 2°, Illuminant = D50 X = 0.7977 * R + 0.1352 * G + 0.0313 * B Y = 0.2880 * R + 0.7119 * G + 0.0001 * B Z = 0.0000 * R + 0.0000 * G + 0.8249 * B
XYZ → RGB
- Input: (Observer. = 2°, Illuminant = D50)
- X in [0, 0.96421]
- Y in [0, 1.00000]
- Z in [0, 0.82519]
- Output: RGB in [0,1] with ProPhoto RGB gamma profile
R = 1.3460 * X - 0.2556 * Y - 0.0511 * Z G = -0.5446 * X + 1.5082 * Y + 0.0205 * Z B = 0.0000 * X + 0.0000 * Y + 1.2123 * Z // Gamma correction if ( R > 0.001953 ) R = R ^ ( 1 / 1.8 ) else R = 16.0 * R if ( G > 0.001953 ) G = G ^ ( 1 / 1.8 ) else G = 16.0 * G if ( B > 0.001953 ) B = B ^ ( 1 / 1.8 ) else B = 16.0 * B
RGB (in Radiance RGB) / XYZ
Radiance RGB is the default profile for Radiance files (*.HDR) and the chromaticities found in the source code are:
color | x | y |
---|---|---|
Red | 0.6400 | 0.3300 |
Green | 0.2900 | 0.6000 |
Blue | 0.1500 | 0.0600 |
White | 0.3333 | 0.3333 |
RGB → XYZ
- Input: RGB in [0,1]
- Output:
- X in [0, 0.96421]
- Y in [0, 1.00000]
- Z in [0, 0.82519]
XYZ → RGB
- Input:
- X in [0, 0.96421]
- Y in [0, 1.00000]
- Z in [0, 0.82519]
- Output: RGB in [0,1]
Dealing with Generic Color Profiles
XYZ Matrices
When dealing with standard profiles like sRGB, Adobe RGB or ProPhoto RGB you are given the chromaticities of Red, Green, Blue and the one for the White Point.
Also, when opening PNG file you can encounter the cHRM chunk that describes the same chromaticities. You then need to transform these 4 2D values into a 3x3 matrix to convert the RGB value to and from the XYZ master space.
First of all, remembering our basic CIEXYZ and CIExyY conversions, let's enumerate what we know:
- From the chromaticities for Red
$$ xyz_R = \frac{XYZ_R}{X_R+Y_R+Z_R} = \frac{XYZ_R}{\Sigma_R} $$ - From the chromaticities for Green
$$ xyz_G = \frac{XYZ_G}{X_G+Y_G+Z_G} = \frac{XYZ_G}{\Sigma_G} $$ -
From the chromaticities for Blue
$$ xyz_B = \frac{XYZ_B}{X_B+Y_B+Z_B} = \frac{XYZ_B}{\Sigma_B} $$ -
From the chromaticities for White
$$ xyz_W $$ - The standard luminance for White
$$ Y_W = 1 $$ - The complete expression for the White Point (since we have a completely defined xyY_W, we can thus easily convert to XYZ_W, see CIE XYZ color space)
$$ XYZ_W $$
Note that we don't know the XYZ_R, XYZ_G and XYZ_B vectors.
So we are looking for M_{XYZ} so that: $$ XYZ = RGB . M_{XYZ} = RGB . \begin{bmatrix} XYZ_R \\ XYZ_G \\ XYZ_B \end{bmatrix} = RGB . \begin{bmatrix} \Sigma_R.xyz_R \\ \Sigma_G.xyz_G \\ \Sigma_B.xyz_B \end{bmatrix} $$
Using RGB_W = [1,1,1] we can write: $$ XYZ_W = [1,1,1] . M_{XYZ} = \Sigma_R.xyz_R + \Sigma_G.xyz_G + \Sigma_B.xyz_B $$
Or, in matrix form again: $$ XYZ_W = \Sigma_{RGB} . \begin{bmatrix} xyz_R \\ xyz_G \\ xyz_B \end{bmatrix} = \Sigma_{RGB} . M_{xyz} $$
Solving by right composing with M_{xyz}^{-1}: $$ \Sigma_{RGB} = XYZ_W . M_{xyz}^{-1} $$
we thus obtain \Sigma_{RGB} and since: $$ \begin{bmatrix} \Sigma_R . xyz_R \\ \Sigma_G . xyz_G \\ \Sigma_B . xyz_B \end{bmatrix} = \begin{bmatrix} XYZ_R \\ XYZ_G \\ XYZ_B \end{bmatrix} = M_{XYZ} $$
We finally obtain M_{XYZ} that converts a RGB color into an XYZ color (obviously, you need to use the inverse M_{XYZ}^{-1} to convert from XYZ back into RGB).
To sum up:
- Invert the 3x3 M_{xyz} matrix constructed from the 3 R,G,B chromaticities
- Multiply with XYZ_W to obtain \Sigma_{RGB}
- Finish by using \Sigma_{RGB} and M_{xyz} to obtain M_{XYZ}
You can find the code to build the M_{XYZ} matrix here: ColorProfile.cpp
Custom ICC Profiles
Decoding true ICC profiles is a little over the top for our purpose. I never truly had the need for it so I won't be discussing full custom profiles here although you can read the ICC Profile specs if you like.
Also, if you really need a complete CMS, you should download the excellent Little CMS by Marti Maria.