File indexing completed on 2024-05-12 04:44:28
0001 // SPDX-FileCopyrightText: Lukas Sommer <sommerluk@gmail.com> 0002 // SPDX-License-Identifier: BSD-2-Clause OR MIT 0003 0004 // Own header 0005 #include "absolutecolor.h" 0006 0007 #include "helpermath.h" 0008 #include "helperposixmath.h" 0009 #include <cmath> 0010 #include <lcms2.h> 0011 #include <optional> 0012 #include <qgenericmatrix.h> 0013 #include <qglobal.h> 0014 #include <qmath.h> 0015 0016 #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) 0017 #include <qhashfunctions.h> 0018 #include <type_traits> 0019 #endif 0020 0021 namespace PerceptualColor 0022 { 0023 0024 // Doxygen doesn’t handle correctly the Q_GLOBAL_STATIC_WITH_ARGS macro, so 0025 // we instruct Doxygen with the @cond command to ignore this part of the code. 0026 /// @cond 0027 0028 // clang-format off 0029 0030 // https://bottosson.github.io/posts/oklab/#converting-from-xyz-to-oklab 0031 Q_GLOBAL_STATIC_WITH_ARGS( 0032 const SquareMatrix3, 0033 m1, 0034 (std::array<double, 9>{{ 0035 +0.8189330101, +0.3618667424, -0.1288597137, 0036 +0.0329845436, +0.9293118715, +0.0361456387, 0037 +0.0482003018, +0.2643662691, +0.6338517070}}.data())) 0038 0039 // https://bottosson.github.io/posts/oklab/#converting-from-xyz-to-oklab 0040 Q_GLOBAL_STATIC_WITH_ARGS( 0041 const SquareMatrix3, 0042 m2, 0043 (std::array<double, 9>{{ 0044 +0.2104542553, +0.7936177850, -0.0040720468, 0045 +1.9779984951, -2.4285922050, +0.4505937099, 0046 +0.0259040371, +0.7827717662, -0.8086757660}}.data())) 0047 0048 // https://fujiwaratko.sakura.ne.jp/infosci/colorspace/bradford_e.html 0049 Q_GLOBAL_STATIC_WITH_ARGS( 0050 const SquareMatrix3, 0051 xyzD65ToXyzD50, 0052 (std::array<double, 9>{{ 0053 +1.047886, +0.022919, -0.050216, 0054 +0.029582, +0.990484, -0.017079, 0055 -0.009252, +0.015073, +0.751678}}.data())) 0056 0057 // clang-format on 0058 0059 Q_GLOBAL_STATIC_WITH_ARGS( // 0060 const SquareMatrix3, 0061 m1inverse, 0062 (inverseMatrix(*m1).value_or(SquareMatrix3()))) 0063 0064 Q_GLOBAL_STATIC_WITH_ARGS( // 0065 const SquareMatrix3, 0066 m2inverse, 0067 (inverseMatrix(*m2).value_or(SquareMatrix3()))) 0068 0069 Q_GLOBAL_STATIC_WITH_ARGS( // 0070 const SquareMatrix3, 0071 xyzD50ToXyzD65, 0072 (inverseMatrix(*xyzD65ToXyzD50).value_or(SquareMatrix3()))) 0073 0074 /// @endcond 0075 0076 /** @brief List of all available conversions from this color model. 0077 * 0078 * @param model The color model from which to convert. 0079 * 0080 * @returns List of all available conversions from this color model. */ 0081 QList<AbsoluteColor::Conversion> AbsoluteColor::conversionsFrom(const ColorModel model) 0082 { 0083 QList<AbsoluteColor::Conversion> result; 0084 for (const auto &item : conversionList) { 0085 if (item.from == model) { 0086 result.append(item); 0087 } 0088 } 0089 return result; 0090 } 0091 0092 /** @brief Adds some @ref GenericColor to an existing hash table. 0093 * 0094 * @param values A hash table with color values. 0095 * @param model The color model from which to perform conversions. 0096 * 0097 * @pre <em>values</em> contains the key <em>model</em>. 0098 * 0099 * @post For all available direct conversions from <em>model</em>, it is 0100 * checked whether a value for the destination color model is already 0101 * available in <em>values</em>. If not, this value is calculated and added 0102 * to <em>values</em>, and this function is called recursively again for this 0103 * destination color model. */ 0104 void AbsoluteColor::addDirectConversionsRecursivly(QHash<ColorModel, GenericColor> *values, ColorModel model) 0105 { 0106 const auto availableConversions = conversionsFrom(model); 0107 const auto currentValue = values->value(model); 0108 for (const auto &conversion : availableConversions) { 0109 if (!values->contains(conversion.to)) { 0110 values->insert(conversion.to, conversion.conversionFunction(currentValue)); 0111 addDirectConversionsRecursivly(values, conversion.to); 0112 } 0113 } 0114 } 0115 0116 /** @brief Calculate conversions to all color models. 0117 * 0118 * @param model The original color model 0119 * @param value The original color value 0120 * 0121 * @returns A list containing the original value and containing conversions 0122 * to all other @ref ColorModel. */ 0123 QHash<ColorModel, GenericColor> AbsoluteColor::allConversions(const ColorModel model, const GenericColor &value) 0124 { 0125 QHash<ColorModel, GenericColor> result; 0126 result.insert(model, value); 0127 addDirectConversionsRecursivly(&result, model); 0128 return result; 0129 } 0130 0131 /** @internal 0132 * 0133 * @brief Conversion from <a href="https://bottosson.github.io/posts/oklab/"> 0134 * Oklab color space</a> to 0135 * <a href="https://en.wikipedia.org/wiki/CIE_1931_color_space#Definition_of_the_CIE_XYZ_color_space"> 0136 * CIE 1931 XYZ color space</a>. 0137 * 0138 * @param value The value to be converted 0139 * 0140 * @note <a href="https://bottosson.github.io/posts/oklab/"> 0141 * Oklab</a> does not specify which 0142 * <a href="https://en.wikipedia.org/wiki/CIE_1931_color_space#CIE_standard_observer"> 0143 * observer</a> the D65 whitepoint should use. But it states that 0144 * <em>“Oklab uses a D65 whitepoint, since this is what sRGB and other 0145 * common color spaces use.”</em>. As 0146 * <a href="https://en.wikipedia.org/wiki/SRGB">sRGB</a> 0147 * uses the <em>CIE 1931 2° Standard Observer</em>, this 0148 * might be a good choice. 0149 * 0150 * @returns the same color in 0151 * <a href="https://en.wikipedia.org/wiki/CIE_1931_color_space#Definition_of_the_CIE_XYZ_color_space"> 0152 * CIE 1931 XYZ color space</a>. The XYZ value has 0153 * <a href="https://bottosson.github.io/posts/oklab/#converting-from-xyz-to-oklab"> 0154 * “a D65 whitepoint and white as Y=1”</a>. */ 0155 GenericColor AbsoluteColor::fromOklabToXyzD65(const GenericColor &value) 0156 { 0157 // The following algorithm is as described in 0158 // https://bottosson.github.io/posts/oklab/#converting-from-xyz-to-oklab 0159 // 0160 // Oklab: “The inverse operation, going from Oklab to XYZ is done with 0161 // the following steps:” 0162 0163 auto lms = (*m2inverse) * value.toTrio(); // NOTE Entries might be negative. 0164 // LMS (long, medium, short) is the response of the three types of 0165 // cones of the human eye. 0166 0167 lms(/*row*/ 0, /*column*/ 0) = std::pow(lms(/*row*/ 0, /*column*/ 0), 3); 0168 lms(/*row*/ 1, /*column*/ 0) = std::pow(lms(/*row*/ 1, /*column*/ 0), 3); 0169 lms(/*row*/ 2, /*column*/ 0) = std::pow(lms(/*row*/ 2, /*column*/ 0), 3); 0170 0171 return GenericColor((*m1inverse) * lms); 0172 } 0173 0174 /** @internal 0175 * 0176 * @brief Conversion from 0177 * <a href="https://en.wikipedia.org/wiki/CIE_1931_color_space#Definition_of_the_CIE_XYZ_color_space"> 0178 * CIE 1931 XYZ color space</a> to 0179 * <a href="https://bottosson.github.io/posts/oklab/"> 0180 * Oklab color space</a>. 0181 * 0182 * @param value The value to be converted 0183 * 0184 * @pre The XYZ value has 0185 * <a href="https://bottosson.github.io/posts/oklab/#converting-from-xyz-to-oklab"> 0186 * “a D65 whitepoint and white as Y=1”</a>. 0187 * 0188 * @note <a href="https://bottosson.github.io/posts/oklab/"> 0189 * Oklab</a> does not specify which 0190 * <a href="https://en.wikipedia.org/wiki/CIE_1931_color_space#CIE_standard_observer"> 0191 * observer</a> the D65 whitepoint should use. But it states that 0192 * <em>“Oklab uses a D65 whitepoint, since this is what sRGB and other 0193 * common color spaces use.”</em>. As 0194 * <a href="https://en.wikipedia.org/wiki/SRGB">sRGB</a> 0195 * uses the <em>CIE 1931 2° Standard Observer</em>, this 0196 * might be a good choice. 0197 * 0198 * @returns the same color in 0199 * <a href="https://bottosson.github.io/posts/oklab/"> 0200 * Oklab color space</a>. */ 0201 GenericColor AbsoluteColor::fromXyzD65ToOklab(const GenericColor &value) 0202 { 0203 // The following algorithm is as described in 0204 // https://bottosson.github.io/posts/oklab/#converting-from-xyz-to-oklab 0205 // 0206 // Oklab: “First the XYZ coordinates are converted to an approximate 0207 // cone responses:” 0208 auto lms = (*m1) * value.toTrio(); // NOTE Entries might be negative. 0209 // LMS (long, medium, short) is the response of the three types of 0210 // cones of the human eye. 0211 0212 // Oklab: “A non-linearity is applied:” 0213 // NOTE The original paper of Björn Ottosson, available at 0214 // https://bottosson.github.io/posts/oklab/#converting-from-xyz-to-oklab 0215 // proposes to calculate this: “x raised to the power of ⅓”. However, 0216 // x might be negative. The original paper does not explicitly explain 0217 // what the expected behaviour is, as “x raised to the power of ⅓” 0218 // is not universally defined for negative x values. Also, 0219 // std::pow(x, 1.0/3) would return “nan” for negative x. The 0220 // original paper does not provide a reference implementation for 0221 // the conversion between XYZ and Oklab. But it provides a reference 0222 // implementation for a direct (shortcut) conversion between sRGB 0223 // and Oklab, and this reference implementation uses std::cbrtf() 0224 // instead of std::pow(x, 1.0/3). And std::cbrtf() seems to allow 0225 // a negative radicand. This makes round-trip conversations possible, 0226 // because it gives unique results for each x value. Therefore, here 0227 // we do the same, but using std::cbrt() instead of std::cbrtf() to 0228 // allow double precision instead of float precision. 0229 lms(/*row*/ 0, /*column*/ 0) = std::cbrt(lms(/*row*/ 0, /*column*/ 0)); 0230 lms(/*row*/ 1, /*column*/ 0) = std::cbrt(lms(/*row*/ 1, /*column*/ 0)); 0231 lms(/*row*/ 2, /*column*/ 0) = std::cbrt(lms(/*row*/ 2, /*column*/ 0)); 0232 0233 // Oklab: “Finally, this is transformed into the Lab-coordinates:” 0234 return GenericColor((*m2) * lms); 0235 } 0236 0237 /** @internal 0238 * 0239 * @brief Color conversion. 0240 * 0241 * @param value Color to be converted. 0242 * 0243 * @returns the converted color */ 0244 GenericColor AbsoluteColor::fromXyzD65ToXyzD50(const GenericColor &value) 0245 { 0246 return GenericColor((*xyzD65ToXyzD50) * value.toTrio()); 0247 } 0248 0249 /** @internal 0250 * 0251 * @brief Color conversion. 0252 * 0253 * @param value Color to be converted. 0254 * 0255 * @returns the converted color */ 0256 GenericColor AbsoluteColor::fromXyzD50ToXyzD65(const GenericColor &value) 0257 { 0258 return GenericColor((*xyzD50ToXyzD65) * value.toTrio()); 0259 } 0260 0261 /** @internal 0262 * 0263 * @brief Color conversion. 0264 * 0265 * @param value Color to be converted. 0266 * 0267 * @returns the converted color */ 0268 GenericColor AbsoluteColor::fromXyzD50ToCielabD50(const GenericColor &value) 0269 { 0270 const cmsCIEXYZ cmsXyzD50 = value.reinterpretAsXyzToCmsciexyz(); 0271 cmsCIELab result; 0272 cmsXYZ2Lab(cmsD50_XYZ(), // white point (for both, XYZ and also Cielab) 0273 &result, // output 0274 &cmsXyzD50); // input 0275 return GenericColor(result); 0276 } 0277 0278 /** @internal 0279 * 0280 * @brief Color conversion. 0281 * 0282 * @param value Color to be converted. 0283 * 0284 * @returns the converted color */ 0285 GenericColor AbsoluteColor::fromCielabD50ToXyzD50(const GenericColor &value) 0286 { 0287 const auto temp = value.reinterpretAsLabToCmscielab(); 0288 cmsCIEXYZ xyzD50; 0289 cmsLab2XYZ(cmsD50_XYZ(), // white point (for both, XYZ and also Lab) 0290 &xyzD50, // output 0291 &temp); // input 0292 return GenericColor(xyzD50); 0293 } 0294 0295 /** @internal 0296 * 0297 * @brief Color conversion. 0298 * 0299 * @param value Color to be converted. 0300 * 0301 * @returns the converted color 0302 * 0303 * This is a generic function converting between polar coordinates 0304 * (format: ignored, radius, angleDegree, ignored) and Cartesian coordinates 0305 * (format: ignored, x, y, ignored). */ 0306 GenericColor AbsoluteColor::fromCartesianToPolar(const GenericColor &value) 0307 { 0308 GenericColor result = value; 0309 const auto &x = value.second; 0310 const auto &y = value.third; 0311 const auto radius = sqrt(pow(x, 2) + pow(y, 2)); 0312 result.second = radius; 0313 if (radius == 0) { 0314 result.third = 0; 0315 return result; 0316 } 0317 if (y >= 0) { 0318 result.third = qRadiansToDegrees(acos(x / radius)); 0319 } else { 0320 result.third = qRadiansToDegrees(2 * pi - acos(x / radius)); 0321 } 0322 return result; 0323 } 0324 0325 /** @internal 0326 * 0327 * @brief Color conversion. 0328 * 0329 * @param value Color to be converted. 0330 * 0331 * @returns the converted color 0332 * 0333 * This is a generic function converting between polar coordinates 0334 * (format: ignored, radius, angleDegree, ignored) and Cartesian coordinates 0335 * (format: ignored, x, y, ignored). */ 0336 GenericColor AbsoluteColor::fromPolarToCartesian(const GenericColor &value) 0337 { 0338 const auto &radius = value.second; 0339 const auto &angleDegree = value.third; 0340 return GenericColor(value.first, // 0341 radius * cos(qDegreesToRadians(angleDegree)), 0342 radius * sin(qDegreesToRadians(angleDegree)), 0343 value.fourth); 0344 } 0345 0346 /** @brief Convert a color from one color model to another. 0347 * 0348 * @param from The color model from which the conversion is made. 0349 * @param value The value being converted. 0350 * @param to The color model to which the conversion is made. 0351 * 0352 * @returns The value converted into the new color model. 0353 * 0354 * @note This function is <em>not</em> speed-optimized. */ 0355 std::optional<GenericColor> AbsoluteColor::convert(const ColorModel from, const GenericColor &value, const ColorModel to) 0356 { 0357 const auto temp = allConversions(from, value); 0358 if (temp.contains(to)) { 0359 return temp.value(to); 0360 } 0361 return std::nullopt; 0362 } 0363 0364 } // namespace PerceptualColor