Perceptual Color

absolutecolor.cpp
1// SPDX-FileCopyrightText: Lukas Sommer <sommerluk@gmail.com>
2// SPDX-License-Identifier: BSD-2-Clause OR MIT
3
4// Own header
5#include "absolutecolor.h"
6
7#include "helpermath.h"
8#include "helperposixmath.h"
9#include <cmath>
10#include <lcms2.h>
11#include <optional>
12#include <qgenericmatrix.h>
13#include <qglobal.h>
14#include <qmath.h>
15
16#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
17#include <qhashfunctions.h>
18#include <type_traits>
19#endif
20
21namespace PerceptualColor
22{
23
24// Doxygen doesn’t handle correctly the Q_GLOBAL_STATIC_WITH_ARGS macro, so
25// we instruct Doxygen with the @cond command to ignore this part of the code.
26/// @cond
27
28// clang-format off
29
30// https://bottosson.github.io/posts/oklab/#converting-from-xyz-to-oklab
31Q_GLOBAL_STATIC_WITH_ARGS(
32 const SquareMatrix3,
33 m1,
34 (std::array<double, 9>{{
35 +0.8189330101, +0.3618667424, -0.1288597137,
36 +0.0329845436, +0.9293118715, +0.0361456387,
37 +0.0482003018, +0.2643662691, +0.6338517070}}.data()))
38
39// https://bottosson.github.io/posts/oklab/#converting-from-xyz-to-oklab
40Q_GLOBAL_STATIC_WITH_ARGS(
41 const SquareMatrix3,
42 m2,
43 (std::array<double, 9>{{
44 +0.2104542553, +0.7936177850, -0.0040720468,
45 +1.9779984951, -2.4285922050, +0.4505937099,
46 +0.0259040371, +0.7827717662, -0.8086757660}}.data()))
47
48// https://fujiwaratko.sakura.ne.jp/infosci/colorspace/bradford_e.html
49Q_GLOBAL_STATIC_WITH_ARGS(
50 const SquareMatrix3,
51 xyzD65ToXyzD50,
52 (std::array<double, 9>{{
53 +1.047886, +0.022919, -0.050216,
54 +0.029582, +0.990484, -0.017079,
55 -0.009252, +0.015073, +0.751678}}.data()))
56
57// clang-format on
58
59Q_GLOBAL_STATIC_WITH_ARGS( //
60 const SquareMatrix3,
61 m1inverse,
62 (inverseMatrix(*m1).value_or(SquareMatrix3())))
63
64Q_GLOBAL_STATIC_WITH_ARGS( //
65 const SquareMatrix3,
66 m2inverse,
67 (inverseMatrix(*m2).value_or(SquareMatrix3())))
68
69Q_GLOBAL_STATIC_WITH_ARGS( //
70 const SquareMatrix3,
71 xyzD50ToXyzD65,
72 (inverseMatrix(*xyzD65ToXyzD50).value_or(SquareMatrix3())))
73
74/// @endcond
75
76/** @brief List of all available conversions from this color model.
77 *
78 * @param model The color model from which to convert.
79 *
80 * @returns List of all available conversions from this color model. */
81QList<AbsoluteColor::Conversion> AbsoluteColor::conversionsFrom(const ColorModel model)
82{
84 for (const auto &item : conversionList) {
85 if (item.from == model) {
86 result.append(item);
87 }
88 }
89 return result;
90}
91
92/** @brief Adds some @ref GenericColor to an existing hash table.
93 *
94 * @param values A hash table with color values.
95 * @param model The color model from which to perform conversions.
96 *
97 * @pre <em>values</em> contains the key <em>model</em>.
98 *
99 * @post For all available direct conversions from <em>model</em>, it is
100 * checked whether a value for the destination color model is already
101 * available in <em>values</em>. If not, this value is calculated and added
102 * to <em>values</em>, and this function is called recursively again for this
103 * destination color model. */
104void AbsoluteColor::addDirectConversionsRecursivly(QHash<ColorModel, GenericColor> *values, ColorModel model)
105{
106 const auto availableConversions = conversionsFrom(model);
107 const auto currentValue = values->value(model);
108 for (const auto &conversion : availableConversions) {
109 if (!values->contains(conversion.to)) {
110 values->insert(conversion.to, conversion.conversionFunction(currentValue));
111 addDirectConversionsRecursivly(values, conversion.to);
112 }
113 }
114}
115
116/** @brief Calculate conversions to all color models.
117 *
118 * @param model The original color model
119 * @param value The original color value
120 *
121 * @returns A list containing the original value and containing conversions
122 * to all other @ref ColorModel. */
123QHash<ColorModel, GenericColor> AbsoluteColor::allConversions(const ColorModel model, const GenericColor &value)
124{
126 result.insert(model, value);
127 addDirectConversionsRecursivly(&result, model);
128 return result;
129}
130
131/** @internal
132 *
133 * @brief Conversion from <a href="https://bottosson.github.io/posts/oklab/">
134 * Oklab color space</a> to
135 * <a href="https://en.wikipedia.org/wiki/CIE_1931_color_space#Definition_of_the_CIE_XYZ_color_space">
136 * CIE 1931 XYZ color space</a>.
137 *
138 * @param value The value to be converted
139 *
140 * @note <a href="https://bottosson.github.io/posts/oklab/">
141 * Oklab</a> does not specify which
142 * <a href="https://en.wikipedia.org/wiki/CIE_1931_color_space#CIE_standard_observer">
143 * observer</a> the D65 whitepoint should use. But it states that
144 * <em>“Oklab uses a D65 whitepoint, since this is what sRGB and other
145 * common color spaces use.”</em>. As
146 * <a href="https://en.wikipedia.org/wiki/SRGB">sRGB</a>
147 * uses the <em>CIE 1931 2° Standard Observer</em>, this
148 * might be a good choice.
149 *
150 * @returns the same color in
151 * <a href="https://en.wikipedia.org/wiki/CIE_1931_color_space#Definition_of_the_CIE_XYZ_color_space">
152 * CIE 1931 XYZ color space</a>. The XYZ value has
153 * <a href="https://bottosson.github.io/posts/oklab/#converting-from-xyz-to-oklab">
154 * “a D65 whitepoint and white as Y=1”</a>. */
155GenericColor AbsoluteColor::fromOklabToXyzD65(const GenericColor &value)
156{
157 // The following algorithm is as described in
158 // https://bottosson.github.io/posts/oklab/#converting-from-xyz-to-oklab
159 //
160 // Oklab: “The inverse operation, going from Oklab to XYZ is done with
161 // the following steps:”
162
163 auto lms = (*m2inverse) * value.toTrio(); // NOTE Entries might be negative.
164 // LMS (long, medium, short) is the response of the three types of
165 // cones of the human eye.
166
167 lms(/*row*/ 0, /*column*/ 0) = std::pow(lms(/*row*/ 0, /*column*/ 0), 3);
168 lms(/*row*/ 1, /*column*/ 0) = std::pow(lms(/*row*/ 1, /*column*/ 0), 3);
169 lms(/*row*/ 2, /*column*/ 0) = std::pow(lms(/*row*/ 2, /*column*/ 0), 3);
170
171 return GenericColor((*m1inverse) * lms);
172}
173
174/** @internal
175 *
176 * @brief Conversion from
177 * <a href="https://en.wikipedia.org/wiki/CIE_1931_color_space#Definition_of_the_CIE_XYZ_color_space">
178 * CIE 1931 XYZ color space</a> to
179 * <a href="https://bottosson.github.io/posts/oklab/">
180 * Oklab color space</a>.
181 *
182 * @param value The value to be converted
183 *
184 * @pre The XYZ value has
185 * <a href="https://bottosson.github.io/posts/oklab/#converting-from-xyz-to-oklab">
186 * “a D65 whitepoint and white as Y=1”</a>.
187 *
188 * @note <a href="https://bottosson.github.io/posts/oklab/">
189 * Oklab</a> does not specify which
190 * <a href="https://en.wikipedia.org/wiki/CIE_1931_color_space#CIE_standard_observer">
191 * observer</a> the D65 whitepoint should use. But it states that
192 * <em>“Oklab uses a D65 whitepoint, since this is what sRGB and other
193 * common color spaces use.”</em>. As
194 * <a href="https://en.wikipedia.org/wiki/SRGB">sRGB</a>
195 * uses the <em>CIE 1931 2° Standard Observer</em>, this
196 * might be a good choice.
197 *
198 * @returns the same color in
199 * <a href="https://bottosson.github.io/posts/oklab/">
200 * Oklab color space</a>. */
201GenericColor AbsoluteColor::fromXyzD65ToOklab(const GenericColor &value)
202{
203 // The following algorithm is as described in
204 // https://bottosson.github.io/posts/oklab/#converting-from-xyz-to-oklab
205 //
206 // Oklab: “First the XYZ coordinates are converted to an approximate
207 // cone responses:”
208 auto lms = (*m1) * value.toTrio(); // NOTE Entries might be negative.
209 // LMS (long, medium, short) is the response of the three types of
210 // cones of the human eye.
211
212 // Oklab: “A non-linearity is applied:”
213 // NOTE The original paper of Björn Ottosson, available at
214 // https://bottosson.github.io/posts/oklab/#converting-from-xyz-to-oklab
215 // proposes to calculate this: “x raised to the power of ⅓”. However,
216 // x might be negative. The original paper does not explicitly explain
217 // what the expected behaviour is, as “x raised to the power of ⅓”
218 // is not universally defined for negative x values. Also,
219 // std::pow(x, 1.0/3) would return “nan” for negative x. The
220 // original paper does not provide a reference implementation for
221 // the conversion between XYZ and Oklab. But it provides a reference
222 // implementation for a direct (shortcut) conversion between sRGB
223 // and Oklab, and this reference implementation uses std::cbrtf()
224 // instead of std::pow(x, 1.0/3). And std::cbrtf() seems to allow
225 // a negative radicand. This makes round-trip conversations possible,
226 // because it gives unique results for each x value. Therefore, here
227 // we do the same, but using std::cbrt() instead of std::cbrtf() to
228 // allow double precision instead of float precision.
229 lms(/*row*/ 0, /*column*/ 0) = std::cbrt(lms(/*row*/ 0, /*column*/ 0));
230 lms(/*row*/ 1, /*column*/ 0) = std::cbrt(lms(/*row*/ 1, /*column*/ 0));
231 lms(/*row*/ 2, /*column*/ 0) = std::cbrt(lms(/*row*/ 2, /*column*/ 0));
232
233 // Oklab: “Finally, this is transformed into the Lab-coordinates:”
234 return GenericColor((*m2) * lms);
235}
236
237/** @internal
238 *
239 * @brief Color conversion.
240 *
241 * @param value Color to be converted.
242 *
243 * @returns the converted color */
244GenericColor AbsoluteColor::fromXyzD65ToXyzD50(const GenericColor &value)
245{
246 return GenericColor((*xyzD65ToXyzD50) * value.toTrio());
247}
248
249/** @internal
250 *
251 * @brief Color conversion.
252 *
253 * @param value Color to be converted.
254 *
255 * @returns the converted color */
256GenericColor AbsoluteColor::fromXyzD50ToXyzD65(const GenericColor &value)
257{
258 return GenericColor((*xyzD50ToXyzD65) * value.toTrio());
259}
260
261/** @internal
262 *
263 * @brief Color conversion.
264 *
265 * @param value Color to be converted.
266 *
267 * @returns the converted color */
268GenericColor AbsoluteColor::fromXyzD50ToCielabD50(const GenericColor &value)
269{
270 const cmsCIEXYZ cmsXyzD50 = value.reinterpretAsXyzToCmsciexyz();
271 cmsCIELab result;
272 cmsXYZ2Lab(cmsD50_XYZ(), // white point (for both, XYZ and also Cielab)
273 &result, // output
274 &cmsXyzD50); // input
275 return GenericColor(result);
276}
277
278/** @internal
279 *
280 * @brief Color conversion.
281 *
282 * @param value Color to be converted.
283 *
284 * @returns the converted color */
285GenericColor AbsoluteColor::fromCielabD50ToXyzD50(const GenericColor &value)
286{
287 const auto temp = value.reinterpretAsLabToCmscielab();
288 cmsCIEXYZ xyzD50;
289 cmsLab2XYZ(cmsD50_XYZ(), // white point (for both, XYZ and also Lab)
290 &xyzD50, // output
291 &temp); // input
292 return GenericColor(xyzD50);
293}
294
295/** @internal
296 *
297 * @brief Color conversion.
298 *
299 * @param value Color to be converted.
300 *
301 * @returns the converted color
302 *
303 * This is a generic function converting between polar coordinates
304 * (format: ignored, radius, angleDegree, ignored) and Cartesian coordinates
305 * (format: ignored, x, y, ignored). */
306GenericColor AbsoluteColor::fromCartesianToPolar(const GenericColor &value)
307{
308 GenericColor result = value;
309 const auto &x = value.second;
310 const auto &y = value.third;
311 const auto radius = sqrt(pow(x, 2) + pow(y, 2));
312 result.second = radius;
313 if (radius == 0) {
314 result.third = 0;
315 return result;
316 }
317 if (y >= 0) {
318 result.third = qRadiansToDegrees(acos(x / radius));
319 } else {
320 result.third = qRadiansToDegrees(2 * pi - acos(x / radius));
321 }
322 return result;
323}
324
325/** @internal
326 *
327 * @brief Color conversion.
328 *
329 * @param value Color to be converted.
330 *
331 * @returns the converted color
332 *
333 * This is a generic function converting between polar coordinates
334 * (format: ignored, radius, angleDegree, ignored) and Cartesian coordinates
335 * (format: ignored, x, y, ignored). */
336GenericColor AbsoluteColor::fromPolarToCartesian(const GenericColor &value)
337{
338 const auto &radius = value.second;
339 const auto &angleDegree = value.third;
340 return GenericColor(value.first, //
341 radius * cos(qDegreesToRadians(angleDegree)),
342 radius * sin(qDegreesToRadians(angleDegree)),
343 value.fourth);
344}
345
346/** @brief Convert a color from one color model to another.
347 *
348 * @param from The color model from which the conversion is made.
349 * @param value The value being converted.
350 * @param to The color model to which the conversion is made.
351 *
352 * @returns The value converted into the new color model.
353 *
354 * @note This function is <em>not</em> speed-optimized. */
355std::optional<GenericColor> AbsoluteColor::convert(const ColorModel from, const GenericColor &value, const ColorModel to)
356{
357 const auto temp = allConversions(from, value);
358 if (temp.contains(to)) {
359 return temp.value(to);
360 }
361 return std::nullopt;
362}
363
364} // namespace PerceptualColor
The namespace of this library.
bool contains(const Key &key) const const
iterator insert(const Key &key, const T &value)
T value(const Key &key) const const
void append(QList< T > &&value)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sat Dec 21 2024 16:57:17 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.