Krita Source Code Documentation
Loading...
Searching...
No Matches
KisColorimetryUtils.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2023 Xaver Hugl <xaver.hugl@gmail.com>
3 * SPDX-FileCopyrightText: 2025 Dmitry Kazakov <dimula73@gmail.com>
4 *
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 */
7
9
10#include <QDebug>
11
13{
14
15QMatrix4x4 matrixFromColumns(const QVector3D &first, const QVector3D &second, const QVector3D &third)
16{
17 QMatrix4x4 ret;
18 ret(0, 0) = first.x();
19 ret(1, 0) = first.y();
20 ret(2, 0) = first.z();
21 ret(0, 1) = second.x();
22 ret(1, 1) = second.y();
23 ret(2, 1) = second.z();
24 ret(0, 2) = third.x();
25 ret(1, 2) = third.y();
26 ret(2, 2) = third.z();
27 return ret;
28}
29
31{
32 if (qFuzzyIsNull(y)) {
33 return XYZ{0, 0, 0};
34 }
35 return XYZ{
36 .X = x / y,
37 .Y = 1.0,
38 .Z = (1 - x - y) / y,
39 };
40}
41
42QVector2D xy::asVector() const
43{
44 return QVector2D(x, y);
45}
46
47bool xy::operator==(const xy &other) const
48{
49 return qFuzzyCompare(x, other.x) && qFuzzyCompare(y, other.y);
50}
51
53{
54 if (qFuzzyIsNull(y)) {
55 return XYZ{0, 0, 0};
56 }
57 return XYZ{
58 .X = Y * x / y,
59 .Y = Y,
60 .Z = Y * (1 - x - y) / y,
61 };
62}
63
64bool xyY::operator==(const xyY &other) const
65{
66 return qFuzzyCompare(x, other.x) && qFuzzyCompare(y, other.y) && qFuzzyCompare(Y, other.Y);
67}
68
70{
71 const double sum = X + Y + Z;
72 if (qFuzzyIsNull(sum)) {
73 // this is nonsense, but at least won't crash
74 return xyY{
75 .x = 0,
76 .y = 0,
77 .Y = 1,
78 };
79 }
80 return xyY{
81 .x = X / sum,
82 .y = Y / sum,
83 .Y = Y,
84 };
85}
86
88{
89 const double sum = X + Y + Z;
90 if (qFuzzyIsNull(sum)) {
91 // this is nonsense, but at least won't crash
92 return xy{
93 .x = 0,
94 .y = 0,
95 };
96 }
97 return xy{
98 .x = X / sum,
99 .y = Y / sum,
100 };
101}
102
103XYZ XYZ::operator*(double factor) const
104{
105 return XYZ{
106 .X = X * factor,
107 .Y = Y * factor,
108 .Z = Z * factor,
109 };
110}
111
112XYZ XYZ::operator/(double divisor) const
113{
114 return XYZ{
115 .X = X / divisor,
116 .Y = Y / divisor,
117 .Z = Z / divisor,
118 };
119}
120
121XYZ XYZ::operator+(const XYZ &other) const
122{
123 return XYZ{
124 .X = X + other.X,
125 .Y = Y + other.Y,
126 .Z = Z + other.Z,
127 };
128}
129
130QVector3D XYZ::asVector() const
131{
132 return QVector3D(X, Y, Z);
133}
134
135XYZ XYZ::fromVector(const QVector3D &vector)
136{
137 return XYZ{
138 .X = vector.x(),
139 .Y = vector.y(),
140 .Z = vector.z(),
141 };
142}
143
144bool XYZ::operator==(const XYZ &other) const
145{
146 return qFuzzyCompare(X, other.X) && qFuzzyCompare(Y, other.Y) && qFuzzyCompare(Z, other.Z);
147}
148
149QMatrix4x4 Colorimetry::chromaticAdaptationMatrix(XYZ sourceWhitepoint, XYZ destinationWhitepoint)
150{
151 static const QMatrix4x4 bradford = []() {
152 QMatrix4x4 ret;
153 ret(0, 0) = 0.8951;
154 ret(0, 1) = 0.2664;
155 ret(0, 2) = -0.1614;
156 ret(1, 0) = -0.7502;
157 ret(1, 1) = 1.7135;
158 ret(1, 2) = 0.0367;
159 ret(2, 0) = 0.0389;
160 ret(2, 1) = -0.0685;
161 ret(2, 2) = 1.0296;
162 return ret;
163 }();
164 static const QMatrix4x4 inverseBradford = []() {
165 QMatrix4x4 ret;
166 ret(0, 0) = 0.9869929;
167 ret(0, 1) = -0.1470543;
168 ret(0, 2) = 0.1599627;
169 ret(1, 0) = 0.4323053;
170 ret(1, 1) = 0.5183603;
171 ret(1, 2) = 0.0492912;
172 ret(2, 0) = -0.0085287;
173 ret(2, 1) = 0.0400428;
174 ret(2, 2) = 0.9684867;
175 return ret;
176 }();
177 if (sourceWhitepoint == destinationWhitepoint) {
178 return QMatrix4x4{};
179 }
180 const QVector3D factors = (bradford.map(destinationWhitepoint.asVector())) / (bradford.map(sourceWhitepoint.asVector()));
181 QMatrix4x4 adaptation{};
182 adaptation(0, 0) = factors.x();
183 adaptation(1, 1) = factors.y();
184 adaptation(2, 2) = factors.z();
185 return inverseBradford * adaptation * bradford;
186}
187
188QMatrix4x4 Colorimetry::calculateToXYZMatrix(XYZ red, XYZ green, XYZ blue, XYZ white)
189{
190 const QVector3D r = red.asVector();
191 const QVector3D g = green.asVector();
192 const QVector3D b = blue.asVector();
193 const auto component_scale = (matrixFromColumns(r, g, b)).inverted().map(white.asVector());
194 return matrixFromColumns(r * component_scale.x(), g * component_scale.y(), b * component_scale.z());
195}
196
198{
199 return Colorimetry{
200 m_red * (1 - factor) + one.red() * factor,
201 m_green * (1 - factor) + one.green() * factor,
202 m_blue * (1 - factor) + one.blue() * factor,
203 m_white, // whitepoint should stay the same
204 };
205}
206
207static double triangleArea(QVector2D p1, QVector2D p2, QVector2D p3)
208{
209 return std::abs(0.5 * (p1.x() * (p2.y() - p3.y()) + p2.x() * (p3.y() - p1.y()) + p3.x() * (p1.y() - p2.y())));
210}
211
212bool Colorimetry::isValid(xy red, xy green, xy blue, xy white)
213{
214 // this is more of a heuristic than a hard rule
215 // but if the gamut is too small, it's not really usable
216 const double gamutArea = triangleArea(red.asVector(), green.asVector(), blue.asVector());
217 if (gamutArea < 0.02) {
218 return false;
219 }
220 // if the white point is inside the gamut triangle,
221 // the three triangles made up between the primaries and the whitepoint
222 // must have the same area as the gamut triangle
223 const double area1 = triangleArea(white.asVector(), green.asVector(), blue.asVector());
224 const double area2 = triangleArea(red.asVector(), white.asVector(), blue.asVector());
225 const double area3 = triangleArea(red.asVector(), green.asVector(), white.asVector());
226 if (std::abs(area1 + area2 + area3 - gamutArea) > 0.001) {
227 // this would cause terrible glitches
228 return false;
229 }
230 return true;
231}
232
233bool Colorimetry::isReal(xy red, xy green, xy blue, xy white)
234{
235 if (!isValid(red, green, blue, white)) {
236 return false;
237 }
238 // outside of XYZ definitely can't be shown on a display
239 // TODO maybe calculate if all values are within the human-visible gamut too?
240 if (red.x < 0 || red.x > 1 || red.y < 0 || red.y > 1 || green.x < 0 || green.x > 1 || green.y < 0 || green.y > 1 || blue.x < 0 || blue.x > 1 || blue.y < 0
241 || blue.y > 1 || white.x < 0 || white.x > 1 || white.y < 0 || white.y > 1) {
242 return false;
243 }
244 return true;
245}
246
247Colorimetry::Colorimetry(XYZ red, XYZ green, XYZ blue, XYZ white)
248 : m_red(red)
249 , m_green(green)
250 , m_blue(blue)
251 , m_white(white)
252 , m_toXYZ(calculateToXYZMatrix(red, green, blue, white))
253 , m_fromXYZ(m_toXYZ.inverted())
254{
255}
256
257Colorimetry::Colorimetry(xyY red, xyY green, xyY blue, xyY white)
258 : Colorimetry(red.toXYZ(), green.toXYZ(), blue.toXYZ(), white.toXYZ())
259{
260}
261
262Colorimetry::Colorimetry(xy red, xy green, xy blue, xy white)
263 : m_white(xyY{white.x, white.y, 1.0}.toXYZ())
264{
265 const auto brightness = (matrixFromColumns(xyY{red.x, red.y, 1.0}.toXYZ().asVector(),
266 xyY{green.x, green.y, 1.0}.toXYZ().asVector(),
267 xyY{blue.x, blue.y, 1.0}.toXYZ().asVector()))
268 .inverted().map(
269 xyY{white.x, white.y, 1.0}.toXYZ().asVector());
270 m_red = xyY{red.x, red.y, brightness.x()}.toXYZ();
271 m_green = xyY{green.x, green.y, brightness.y()}.toXYZ();
272 m_blue = xyY{blue.x, blue.y, brightness.z()}.toXYZ();
274 m_fromXYZ = m_toXYZ.inverted();
275}
276
277const QMatrix4x4 &Colorimetry::toXYZ() const
278{
279 return m_toXYZ;
280}
281
282const QMatrix4x4 &Colorimetry::fromXYZ() const
283{
284 return m_fromXYZ;
285}
286
287// converts from XYZ to LMS suitable for ICtCp
288static const QMatrix4x4 s_xyzToDolbyLMS = []() {
289 QMatrix4x4 ret;
290 ret(0, 0) = 0.3593;
291 ret(0, 1) = 0.6976;
292 ret(0, 2) = -0.0359;
293 ret(1, 0) = -0.1921;
294 ret(1, 1) = 1.1005;
295 ret(1, 2) = 0.0754;
296 ret(2, 0) = 0.0071;
297 ret(2, 1) = 0.0748;
298 ret(2, 2) = 0.8433;
299 return ret;
300}();
301static const QMatrix4x4 s_inverseDolbyLMS = s_xyzToDolbyLMS.inverted();
302
303QMatrix4x4 Colorimetry::toLMS() const
304{
305 return s_xyzToDolbyLMS * m_toXYZ;
306}
307
308QMatrix4x4 Colorimetry::fromLMS() const
309{
311}
312
314{
315 const auto mat = chromaticAdaptationMatrix(this->white(), newWhitepoint.toXYZ());
316 return Colorimetry{
317 XYZ::fromVector(mat.map(red().asVector())),
318 XYZ::fromVector(mat.map(green().asVector())),
319 XYZ::fromVector(mat.map(blue().asVector())),
320 newWhitepoint.toXYZ(),
321 };
322}
323
325{
326 newWhitePoint.Y = 1;
327 return Colorimetry{
328 m_red,
329 m_green,
330 m_blue,
331 newWhitePoint.toXYZ(),
332 };
333}
334
336{
337 return other.fromXYZ() * chromaticAdaptationMatrix(white(), other.white()) * toXYZ();
338}
339
341{
342 return other.fromXYZ() * toXYZ();
343}
344
345bool Colorimetry::operator==(const Colorimetry &other) const
346{
347 return red() == other.red() && green() == other.green() && blue() == other.blue() && white() == other.white();
348}
349
350const XYZ &Colorimetry::red() const
351{
352 return m_red;
353}
354
356{
357 return m_green;
358}
359
360const XYZ &Colorimetry::blue() const
361{
362 return m_blue;
363}
364
366{
367 return m_white;
368}
369
371 xy{0.64, 0.33},
372 xy{0.30, 0.60},
373 xy{0.15, 0.06},
374 xy{0.3127, 0.3290},
375};
376const Colorimetry Colorimetry::PAL_M = Colorimetry{
377 xy{0.67, 0.33},
378 xy{0.21, 0.71},
379 xy{0.14, 0.08},
380 xy{0.310, 0.316},
381};
382const Colorimetry Colorimetry::PAL = Colorimetry{
383 xy{0.640, 0.330},
384 xy{0.290, 0.600},
385 xy{0.150, 0.060},
386 xy{0.3127, 0.3290},
387};
388const Colorimetry Colorimetry::NTSC = Colorimetry{
389 xy{0.630, 0.340},
390 xy{0.310, 0.595},
391 xy{0.155, 0.070},
392 xy{0.3127, 0.3290},
393};
394const Colorimetry Colorimetry::GenericFilm = Colorimetry{
395 xy{0.681, 0.319},
396 xy{0.243, 0.692},
397 xy{0.145, 0.049},
398 xy{0.310, 0.316},
399};
400const Colorimetry Colorimetry::BT2020 = Colorimetry{
401 xy{0.708, 0.292},
402 xy{0.170, 0.797},
403 xy{0.131, 0.046},
404 xy{0.3127, 0.3290},
405};
406const Colorimetry Colorimetry::CIEXYZ = Colorimetry{
407 XYZ{1.0, 0.0, 0.0},
408 XYZ{0.0, 1.0, 0.0},
409 XYZ{0.0, 0.0, 1.0},
410 xy{1.0 / 3.0, 1.0 / 3.0}.toXYZ(),
411};
412const Colorimetry Colorimetry::DCIP3 = Colorimetry{
413 xy{0.680, 0.320},
414 xy{0.265, 0.690},
415 xy{0.150, 0.060},
416 xy{0.314, 0.351},
417};
418const Colorimetry Colorimetry::DisplayP3 = Colorimetry{
419 xy{0.680, 0.320},
420 xy{0.265, 0.690},
421 xy{0.150, 0.060},
422 xy{0.3127, 0.3290},
423};
424const Colorimetry Colorimetry::AdobeRGB = Colorimetry{
425 xy{0.6400, 0.3300},
426 xy{0.2100, 0.7100},
427 xy{0.1500, 0.0600},
428 xy{0.3127, 0.3290},
429};
430
431QDebug operator<<(QDebug debug, const xy &value) {
432 QDebugStateSaver saver(debug);
433 debug.nospace() << "xy(x: " << value.x << ", y: " << value.y << ")";
434 return debug;
435}
436
437QDebug operator<<(QDebug debug, const xyY &value) {
438 QDebugStateSaver saver(debug);
439 debug.nospace() << "xyY(x: " << value.x << ", y: " << value.y << ", Y: " << value.Y << ")";
440 return debug;
441}
442
443QDebug operator<<(QDebug debug, const XYZ &value) {
444 QDebugStateSaver saver(debug);
445 debug.nospace() << "XYZ(X: " << value.X << ", Y: " << value.Y << ", Z: " << value.Z << ")";
446 return debug;
447}
448
449QDebug operator<<(QDebug debug, const Colorimetry &value) {
450 QDebugStateSaver saver(debug);
451 debug.nospace() << "Colorimetry(Red: " << value.red().toxy() << ", Green: " << value.green().toxy() << ", Blue: " << value.blue().toxy() << ", White: " << value.white().toxy() << ")";
452 return debug;
453}
454
455} // namespace KisColorimetryUtils
float value(const T *src, size_t ch)
QPointF p2
QPointF p3
QPointF p1
QMatrix4x4 relativeColorimetricTo(const Colorimetry &other) const
static const Colorimetry GenericFilm
Colorimetry interpolateGamutTo(const Colorimetry &one, double factor) const
Colorimetry(XYZ red, XYZ green, XYZ blue, XYZ white)
static QMatrix4x4 chromaticAdaptationMatrix(XYZ sourceWhitepoint, XYZ destinationWhitepoint)
static QMatrix4x4 calculateToXYZMatrix(XYZ red, XYZ green, XYZ blue, XYZ white)
static const Colorimetry AdobeRGB
QMatrix4x4 absoluteColorimetricTo(const Colorimetry &other) const
Colorimetry withWhitepoint(xyY newWhitePoint) const
static bool isReal(xy red, xy green, xy blue, xy white)
Colorimetry adaptedTo(xyY newWhitepoint) const
bool operator==(const Colorimetry &other) const
static const Colorimetry DisplayP3
static bool isValid(xy red, xy green, xy blue, xy white)
static bool qFuzzyCompare(half p1, half p2)
static bool qFuzzyIsNull(half h)
QMatrix4x4 matrixFromColumns(const QVector3D &first, const QVector3D &second, const QVector3D &third)
static const QMatrix4x4 s_xyzToDolbyLMS
QDebug operator<<(QDebug debug, const xy &value)
static const QMatrix4x4 s_inverseDolbyLMS
static double triangleArea(QVector2D p1, QVector2D p2, QVector2D p3)
static XYZ fromVector(const QVector3D &vector)
bool operator==(const XYZ &other) const
XYZ operator*(double factor) const
XYZ operator/(double factor) const
XYZ operator+(const XYZ &other) const
bool operator==(const xyY &other) const
bool operator==(const xy &other) const