Img2Num C++ (Internal Developer Docs) dev
API Documentation
Loading...
Searching...
No Matches
cielab_impl.h
1#ifndef CIELAB_IMPL_H
2#define CIELAB_IMPL_H
3
4#ifndef CIELAB_H
5#error "cielab_impl.h should not be included directly; include cielab.h instead"
6#endif
7
8#include <algorithm>
9#include <cmath>
10
11// ====== Used in xyz_to_lab =======
12constexpr double DELTA{6.0 / 29.0}; // 0.2068966
13constexpr double DELTA_CUBED{DELTA * DELTA * DELTA}; // 0.008856
14constexpr double KAPPA{1.0 / (3.0 * DELTA * DELTA)}; // 7.787
15constexpr double EPSILON{16.0 / 116.0}; // 0.137931
16
17// ====== Used in srgb_to_linear ======
18constexpr double SRGB_LINEAR_THRESHOLD{0.04045}; // linear segment boundary
19constexpr double SRGB_LINEAR_FACTOR{12.92}; // scale factor for linear segment
20constexpr double SRGB_GAMMA_OFFSET{0.055}; // offset for nonlinear segment
21constexpr double SRGB_GAMMA{2.4}; // gamma exponent for nonlinear segment
22constexpr double SRGB_GAMMA_INV{1.0 / SRGB_GAMMA}; // gamma exponent for nonlinear segment
23
24/*
25 * ====== Used in rgb_to_lab ======
26 *
27 * sRGB Khronos/W3C Transformation Matrices (D65 illuminant)
28 *
29 * sRGB → CIE XYZ:
30 * ┌ ┐ ┌ ┐ ┌ ┐
31 * │ X │ │ 0.4124564 0.3575761 0.1804375 │ │ R │
32 * │ Y │ = │ 0.2126729 0.7151522 0.0721750 │ │ G │
33 * │ Z │ │ 0.0193339 0.1191920 0.9503041 │ │ B │
34 * └ ┘ └ ┘ └ ┘
35 *
36 * Reference: ITU-R BT.709 / sRGB standard (IEC 61966-2-1:1999)
37 */
38constexpr double SRGB_R_TO_X{0.4124564};
39constexpr double SRGB_G_TO_X{0.3575761};
40constexpr double SRGB_B_TO_X{0.1804375};
41constexpr double SRGB_R_TO_Y{0.2126729};
42constexpr double SRGB_G_TO_Y{0.7151522};
43constexpr double SRGB_B_TO_Y{0.0721750};
44constexpr double SRGB_R_TO_Z{0.0193339};
45constexpr double SRGB_G_TO_Z{0.1191920};
46constexpr double SRGB_B_TO_Z{0.9503041};
47
48/*
49 * ====== Used in lab_to_rgb ======
50 *
51 * sRGB Khronos/W3C Transformation Matrices (D65 illuminant)
52 * Inverse transformation (XYZ → sRGB) per Khronos/W3C, D65 white, slightly
53 * different from original BT.709 inverse.
54 *
55 * CIE XYZ → sRGB (inverse):
56 * ┌ ┐ ┌ ┐ ┌ ┐
57 * │ R │ │ 3.240970 -1.537383 -0.498611 │ │ X │
58 * │ G │ = │ -0.969244 1.875968 0.041555 │ │ Y │
59 * │ B │ │ 0.055630 -0.203977 1.056972 │ │ Z │
60 * └ ┘ └ ┘ └ ┘
61 *
62 * Reference: ITU-R BT.709 / sRGB standard (IEC 61966-2-1:1999)
63 */
64constexpr double SRGB_X_TO_R{3.240970};
65constexpr double SRGB_Y_TO_R{-1.537383};
66constexpr double SRGB_Z_TO_R{-0.498611};
67constexpr double SRGB_X_TO_G{-0.969244};
68constexpr double SRGB_Y_TO_G{1.875968};
69constexpr double SRGB_Z_TO_G{0.041555};
70constexpr double SRGB_X_TO_B{0.055630};
71constexpr double SRGB_Y_TO_B{-0.203977};
72constexpr double SRGB_Z_TO_B{1.056972};
73
74// Reference white point for D65 illuminant
75constexpr double D65_Xn{0.95047};
76constexpr double D65_Yn{1.0};
77constexpr double D65_Zn{1.08883};
78
79constexpr double LAB_L_FACTOR{116.0};
80constexpr double LAB_L_OFFSET{16.0};
81constexpr double LAB_A_FACTOR{500.0};
82constexpr double LAB_B_FACTOR{200.0};
83
84// Function for the non-linear XYZ to Lab transformation
85inline double xyz_to_lab(const double t) {
86 // prevent negative due to tiny floating errors
87 const double safe_t{std::max(0.0, t)};
88 return (safe_t > DELTA_CUBED) ? std::cbrt(safe_t) : (KAPPA * safe_t) + EPSILON;
89}
90
91// Function for the non-linear sRGB to linear RGB transformation (inverse gamma
92// correction)
93inline double srgb_to_linear(const double c) {
94 const double safe_c{std::clamp(c, 0.0, 1.0)};
95 return (safe_c <= SRGB_LINEAR_THRESHOLD)
96 ? safe_c / SRGB_LINEAR_FACTOR
97 : std::pow((safe_c + SRGB_GAMMA_OFFSET) / (1.0 + SRGB_GAMMA_OFFSET), SRGB_GAMMA);
98}
99
100template <typename Tin, typename Tout>
101void rgb_to_lab(const Tin r_u8, const Tin g_u8, const Tin b_u8, Tout &out_l, Tout &out_a,
102 Tout &out_b) {
103 // 1. Convert 8-bit RGB [0, 255] to linear RGB [0.0, 1.0]
104
105 double _r = static_cast<double>(r_u8);
106 double _g = static_cast<double>(g_u8);
107 double _b = static_cast<double>(b_u8);
108
109 double r{srgb_to_linear(_r / 255.0)};
110 double g{srgb_to_linear(_g / 255.0)};
111 double b{srgb_to_linear(_b / 255.0)};
112
113 // 2. Convert linear RGB to CIE XYZ (using D65 white point reference)
114 // The matrix below is for sRGB to XYZ (D65)
115 const double x{SRGB_R_TO_X * r + SRGB_G_TO_X * g + SRGB_B_TO_X * b};
116 const double y{SRGB_R_TO_Y * r + SRGB_G_TO_Y * g + SRGB_B_TO_Y * b};
117 const double z{SRGB_R_TO_Z * r + SRGB_G_TO_Z * g + SRGB_B_TO_Z * b};
118
119 // Normalize XYZ values by the white point
120 const double Xr{x / D65_Xn};
121 const double Yr{y / D65_Yn};
122 const double Zr{z / D65_Zn};
123
124 // 3. Convert CIE XYZ to CIE L*a*b*
125 const double fx{xyz_to_lab(Xr)};
126 const double fy{xyz_to_lab(Yr)};
127 const double fz{xyz_to_lab(Zr)};
128
129 // 4. Output values
130 out_l = static_cast<Tout>(LAB_L_FACTOR * fy - LAB_L_OFFSET);
131 out_a = static_cast<Tout>(LAB_A_FACTOR * (fx - fy));
132 out_b = static_cast<Tout>(LAB_B_FACTOR * (fy - fz));
133
134 out_l = std::clamp(out_l, static_cast<Tout>(0.0), static_cast<Tout>(100.0));
135}
136
137constexpr double inverse_xyz_to_lab(double t) {
138 return (t > DELTA) ? (t * t * t) : (3 * DELTA * DELTA * (t - EPSILON));
139}
140
141inline double gamma_encode(double u) {
142 // Guard against negative values from out-of-gamut colors
143 u = std::max(0.0, u);
144 return (u <= SRGB_LINEAR_THRESHOLD / SRGB_LINEAR_FACTOR)
145 ? SRGB_LINEAR_FACTOR * u
146 : (1.0 + SRGB_GAMMA_OFFSET) * std::pow(u, SRGB_GAMMA_INV) - SRGB_GAMMA_OFFSET;
147}
148
149template <typename Tin, typename Tout>
150void lab_to_rgb(const Tin L, const Tin A, const Tin B, Tout &out_r_u8, Tout &out_g_u8,
151 Tout &out_b_u8) {
152 const double _L = static_cast<double>(L);
153 const double _A = static_cast<double>(A);
154 const double _B = static_cast<double>(B);
155
156 // --- Lab → XYZ (D65 white point)
157 const double fy{(_L + LAB_L_OFFSET) / LAB_L_FACTOR};
158 const double fx{fy + _A / LAB_A_FACTOR};
159 const double fz{fy - _B / LAB_B_FACTOR};
160
161 const double X{D65_Xn * inverse_xyz_to_lab(fx)};
162 const double Y{D65_Yn * inverse_xyz_to_lab(fy)};
163 const double Z{D65_Zn * inverse_xyz_to_lab(fz)};
164
165 // --- XYZ → linear RGB (sRGB)
166 double r{SRGB_X_TO_R * X + SRGB_Y_TO_R * Y + SRGB_Z_TO_R * Z};
167 double g{SRGB_X_TO_G * X + SRGB_Y_TO_G * Y + SRGB_Z_TO_G * Z};
168 double b{SRGB_X_TO_B * X + SRGB_Y_TO_B * Y + SRGB_Z_TO_B * Z};
169
170 // --- linear RGB → sRGB (gamma correction)
171 r = gamma_encode(std::clamp(r, 0.0, 1.0));
172 g = gamma_encode(std::clamp(g, 0.0, 1.0));
173 b = gamma_encode(std::clamp(b, 0.0, 1.0));
174
175 // --- Clamp and convert to 8-bit
176 out_r_u8 = static_cast<Tout>(std::round(255.0 * std::clamp(r, 0.0, 1.0)));
177 out_g_u8 = static_cast<Tout>(std::round(255.0 * std::clamp(g, 0.0, 1.0)));
178 out_b_u8 = static_cast<Tout>(std::round(255.0 * std::clamp(b, 0.0, 1.0)));
179}
180
181template <typename Tin, typename Tout>
182void rgb_to_lab(const ImageLib::RGBAPixel<Tin> &rgba, ImageLib::LABAPixel<Tout> &laba) {
183 Tout l{laba.l};
184 Tout a{laba.a};
185 Tout b{laba.b};
186
187 rgb_to_lab(rgba.red, rgba.green, rgba.blue, l, a, b);
188
189 laba.l = l;
190 laba.a = a;
191 laba.b = b;
192
193 laba.alpha = static_cast<Tout>(rgba.alpha);
194}
195
196template <typename Tin, typename Tout>
197void rgb_to_lab(const ImageLib::RGBPixel<Tin> &rgb, ImageLib::LABPixel<Tout> &lab) {
198 rgb_to_lab<Tin, Tout>(rgb.red, rgb.green, rgb.blue, lab.l, lab.a, lab.b);
199}
200
201template <typename Tin, typename Tout>
202void lab_to_rgb(const ImageLib::LABAPixel<Tin> &laba, ImageLib::RGBAPixel<Tout> &rgba) {
203 Tout r{rgba.red};
204 Tout g{rgba.green};
205 Tout b{rgba.blue};
206
207 lab_to_rgb<Tin, Tout>(laba.l, laba.a, laba.b, r, g, b);
208
209 rgba.red = r;
210 rgba.green = g;
211 rgba.blue = b;
212
213 rgba.alpha = static_cast<Tout>(laba.alpha);
214}
215
216template <typename Tin, typename Tout>
217void lab_to_rgb(const ImageLib::LABPixel<Tin> &lab, ImageLib::RGBPixel<Tout> &rgb) {
218 lab_to_rgb<Tin, Tout>(lab.l, lab.a, lab.b, rgb.red, rgb.green, rgb.blue);
219}
220
221#endif