File indexing completed on 2024-05-19 05:31:34

0001 /*
0002     SPDX-FileCopyrightText: 2023 Xaver Hugl <xaver.hugl@gmail.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 #include "colorspace.h"
0007 
0008 #include <qassert.h>
0009 
0010 namespace KWin
0011 {
0012 
0013 static QMatrix4x4 matrixFromColumns(const QVector3D &first, const QVector3D &second, const QVector3D &third)
0014 {
0015     QMatrix4x4 ret;
0016     ret(0, 0) = first.x();
0017     ret(1, 0) = first.y();
0018     ret(2, 0) = first.z();
0019     ret(0, 1) = second.x();
0020     ret(1, 1) = second.y();
0021     ret(2, 1) = second.z();
0022     ret(0, 2) = third.x();
0023     ret(1, 2) = third.y();
0024     ret(2, 2) = third.z();
0025     return ret;
0026 }
0027 
0028 QVector3D Colorimetry::xyToXYZ(QVector2D xy)
0029 {
0030     return QVector3D(xy.x() / xy.y(), 1, (1 - xy.x() - xy.y()) / xy.y());
0031 }
0032 
0033 QVector2D Colorimetry::xyzToXY(QVector3D xyz)
0034 {
0035     xyz /= xyz.y();
0036     return QVector2D(xyz.x() / (xyz.x() + xyz.y() + xyz.z()), xyz.y() / (xyz.x() + xyz.y() + xyz.z()));
0037 }
0038 
0039 QMatrix4x4 Colorimetry::chromaticAdaptationMatrix(QVector2D sourceWhitepoint, QVector2D destinationWhitepoint)
0040 {
0041     static const QMatrix4x4 bradford = []() {
0042         QMatrix4x4 ret;
0043         ret(0, 0) = 0.8951;
0044         ret(0, 1) = 0.2664;
0045         ret(0, 2) = -0.1614;
0046         ret(1, 0) = -0.7502;
0047         ret(1, 1) = 1.7135;
0048         ret(1, 2) = 0.0367;
0049         ret(2, 0) = 0.0389;
0050         ret(2, 1) = -0.0685;
0051         ret(2, 2) = 1.0296;
0052         return ret;
0053     }();
0054     static const QMatrix4x4 inverseBradford = []() {
0055         QMatrix4x4 ret;
0056         ret(0, 0) = 0.9869929;
0057         ret(0, 1) = -0.1470543;
0058         ret(0, 2) = 0.1599627;
0059         ret(1, 0) = 0.4323053;
0060         ret(1, 1) = 0.5183603;
0061         ret(1, 2) = 0.0492912;
0062         ret(2, 0) = -0.0085287;
0063         ret(2, 1) = 0.0400428;
0064         ret(2, 2) = 0.9684867;
0065         return ret;
0066     }();
0067     if (sourceWhitepoint == destinationWhitepoint) {
0068         return QMatrix4x4{};
0069     }
0070     const QVector3D factors = (bradford * xyToXYZ(destinationWhitepoint)) / (bradford * xyToXYZ(sourceWhitepoint));
0071     QMatrix4x4 adaptation{};
0072     adaptation(0, 0) = factors.x();
0073     adaptation(1, 1) = factors.y();
0074     adaptation(2, 2) = factors.z();
0075     return inverseBradford * adaptation * bradford;
0076 }
0077 
0078 QMatrix4x4 Colorimetry::calculateToXYZMatrix(QVector3D red, QVector3D green, QVector3D blue, QVector3D white)
0079 {
0080     const auto component_scale = (matrixFromColumns(red, green, blue)).inverted() * white;
0081     return matrixFromColumns(red * component_scale.x(), green * component_scale.y(), blue * component_scale.z());
0082 }
0083 
0084 Colorimetry Colorimetry::interpolateGamutTo(const Colorimetry &one, double factor) const
0085 {
0086     return Colorimetry{
0087         m_red * (1 - factor) + one.red() * factor,
0088         m_green * (1 - factor) + one.green() * factor,
0089         m_blue * (1 - factor) + one.blue() * factor,
0090         m_white, // whitepoint should stay the same
0091     };
0092 }
0093 
0094 Colorimetry::Colorimetry(QVector2D red, QVector2D green, QVector2D blue, QVector2D white)
0095     : m_red(red)
0096     , m_green(green)
0097     , m_blue(blue)
0098     , m_white(white)
0099     , m_toXYZ(calculateToXYZMatrix(xyToXYZ(red), xyToXYZ(green), xyToXYZ(blue), xyToXYZ(white)))
0100     , m_fromXYZ(m_toXYZ.inverted())
0101 {
0102 }
0103 
0104 Colorimetry::Colorimetry(QVector3D red, QVector3D green, QVector3D blue, QVector3D white)
0105     : m_red(xyzToXY(red))
0106     , m_green(xyzToXY(green))
0107     , m_blue(xyzToXY(blue))
0108     , m_white(xyzToXY(white))
0109     , m_toXYZ(calculateToXYZMatrix(red, green, blue, white))
0110     , m_fromXYZ(m_toXYZ.inverted())
0111 {
0112 }
0113 
0114 const QMatrix4x4 &Colorimetry::toXYZ() const
0115 {
0116     return m_toXYZ;
0117 }
0118 
0119 const QMatrix4x4 &Colorimetry::fromXYZ() const
0120 {
0121     return m_fromXYZ;
0122 }
0123 
0124 QMatrix4x4 Colorimetry::toOther(const Colorimetry &other) const
0125 {
0126     // rendering intent is relative colorimetric, so adapt to the different whitepoint
0127     return other.fromXYZ() * chromaticAdaptationMatrix(this->white(), other.white()) * toXYZ();
0128 }
0129 
0130 Colorimetry Colorimetry::adaptedTo(QVector2D newWhitepoint) const
0131 {
0132     const auto mat = chromaticAdaptationMatrix(this->white(), newWhitepoint);
0133     return Colorimetry{
0134         xyzToXY(mat * xyToXYZ(red())),
0135         xyzToXY(mat * xyToXYZ(green())),
0136         xyzToXY(mat * xyToXYZ(blue())),
0137         newWhitepoint,
0138     };
0139 }
0140 
0141 bool Colorimetry::operator==(const Colorimetry &other) const
0142 {
0143     return red() == other.red() && green() == other.green() && blue() == other.blue() && white() == other.white();
0144 }
0145 
0146 bool Colorimetry::operator==(NamedColorimetry name) const
0147 {
0148     return *this == fromName(name);
0149 }
0150 
0151 const QVector2D &Colorimetry::red() const
0152 {
0153     return m_red;
0154 }
0155 
0156 const QVector2D &Colorimetry::green() const
0157 {
0158     return m_green;
0159 }
0160 
0161 const QVector2D &Colorimetry::blue() const
0162 {
0163     return m_blue;
0164 }
0165 
0166 const QVector2D &Colorimetry::white() const
0167 {
0168     return m_white;
0169 }
0170 
0171 static const Colorimetry BT709 = Colorimetry{
0172     QVector2D{0.64, 0.33},
0173     QVector2D{0.30, 0.60},
0174     QVector2D{0.15, 0.06},
0175     QVector2D{0.3127, 0.3290},
0176 };
0177 
0178 static const Colorimetry BT2020 = Colorimetry{
0179     QVector2D{0.708, 0.292},
0180     QVector2D{0.170, 0.797},
0181     QVector2D{0.131, 0.046},
0182     QVector2D{0.3127, 0.3290},
0183 };
0184 
0185 const Colorimetry &Colorimetry::fromName(NamedColorimetry name)
0186 {
0187     switch (name) {
0188     case NamedColorimetry::BT709:
0189         return BT709;
0190     case NamedColorimetry::BT2020:
0191         return BT2020;
0192     }
0193     Q_UNREACHABLE();
0194 }
0195 
0196 const ColorDescription ColorDescription::sRGB = ColorDescription(NamedColorimetry::BT709, NamedTransferFunction::gamma22, 100, 0, 100, 100);
0197 
0198 ColorDescription::ColorDescription(const Colorimetry &colorimety, NamedTransferFunction tf, double sdrBrightness, double minHdrBrightness, double maxFrameAverageBrightness, double maxHdrHighlightBrightness, const Colorimetry &sdrColorimetry)
0199     : m_colorimetry(colorimety)
0200     , m_transferFunction(tf)
0201     , m_sdrColorimetry(sdrColorimetry)
0202     , m_sdrBrightness(sdrBrightness)
0203     , m_minHdrBrightness(minHdrBrightness)
0204     , m_maxFrameAverageBrightness(maxFrameAverageBrightness)
0205     , m_maxHdrHighlightBrightness(maxHdrHighlightBrightness)
0206 {
0207 }
0208 
0209 ColorDescription::ColorDescription(NamedColorimetry colorimetry, NamedTransferFunction tf, double sdrBrightness, double minHdrBrightness, double maxFrameAverageBrightness, double maxHdrHighlightBrightness, const Colorimetry &sdrColorimetry)
0210     : m_colorimetry(Colorimetry::fromName(colorimetry))
0211     , m_transferFunction(tf)
0212     , m_sdrColorimetry(sdrColorimetry)
0213     , m_sdrBrightness(sdrBrightness)
0214     , m_minHdrBrightness(minHdrBrightness)
0215     , m_maxFrameAverageBrightness(maxFrameAverageBrightness)
0216     , m_maxHdrHighlightBrightness(maxHdrHighlightBrightness)
0217 {
0218 }
0219 
0220 const Colorimetry &ColorDescription::colorimetry() const
0221 {
0222     return m_colorimetry;
0223 }
0224 
0225 const Colorimetry &ColorDescription::sdrColorimetry() const
0226 {
0227     return m_sdrColorimetry;
0228 }
0229 
0230 NamedTransferFunction ColorDescription::transferFunction() const
0231 {
0232     return m_transferFunction;
0233 }
0234 
0235 double ColorDescription::sdrBrightness() const
0236 {
0237     return m_sdrBrightness;
0238 }
0239 
0240 double ColorDescription::minHdrBrightness() const
0241 {
0242     return m_minHdrBrightness;
0243 }
0244 
0245 double ColorDescription::maxFrameAverageBrightness() const
0246 {
0247     return m_maxFrameAverageBrightness;
0248 }
0249 
0250 double ColorDescription::maxHdrHighlightBrightness() const
0251 {
0252     return m_maxHdrHighlightBrightness;
0253 }
0254 
0255 bool ColorDescription::operator==(const ColorDescription &other) const
0256 {
0257     return m_colorimetry == other.m_colorimetry
0258         && m_transferFunction == other.m_transferFunction
0259         && m_sdrGamutWideness == other.m_sdrGamutWideness
0260         && m_sdrBrightness == other.m_sdrBrightness
0261         && m_minHdrBrightness == other.m_minHdrBrightness
0262         && m_maxFrameAverageBrightness == other.m_maxFrameAverageBrightness
0263         && m_maxHdrHighlightBrightness == other.m_maxHdrHighlightBrightness;
0264 }
0265 
0266 static float srgbToLinear(float sRGB)
0267 {
0268     if (sRGB < 0.04045) {
0269         return std::max(sRGB / 12.92, 0.0);
0270     } else {
0271         return std::clamp(std::pow((sRGB + 0.055) / 1.055, 12.0 / 5.0), 0.0, 1.0);
0272     }
0273 }
0274 
0275 static float linearToSRGB(float linear)
0276 {
0277     if (linear < 0.0031308) {
0278         return std::max(linear / 12.92, 0.0);
0279     } else {
0280         return std::clamp(std::pow(linear, 5.0 / 12.0) * 1.055 - 0.055, 0.0, 1.0);
0281     }
0282 }
0283 
0284 static float nitsToPQ(float nits)
0285 {
0286     const float normalized = std::clamp(nits / 10000.0f, 0.0f, 1.0f);
0287     const float c1 = 0.8359375;
0288     const float c2 = 18.8515625;
0289     const float c3 = 18.6875;
0290     const float m1 = 0.1593017578125;
0291     const float m2 = 78.84375;
0292     const float powed = std::pow(normalized, m1);
0293     const float num = c1 + c2 * powed;
0294     const float denum = 1 + c3 * powed;
0295     return std::pow(num / denum, m2);
0296 }
0297 
0298 static float pqToNits(float pq)
0299 {
0300     const float c1 = 0.8359375;
0301     const float c2 = 18.8515625;
0302     const float c3 = 18.6875;
0303     const float m1_inv = 1.0 / 0.1593017578125;
0304     const float m2_inv = 1.0 / 78.84375;
0305     const float powed = std::pow(pq, m2_inv);
0306     const float num = std::max(powed - c1, 0.0f);
0307     const float den = c2 - c3 * powed;
0308     return 10000.0f * std::pow(num / den, m1_inv);
0309 }
0310 
0311 static QVector3D clamp(const QVector3D &vect, float min = 0, float max = 1)
0312 {
0313     return QVector3D(std::clamp(vect.x(), min, max), std::clamp(vect.y(), min, max), std::clamp(vect.z(), min, max));
0314 }
0315 
0316 QVector3D ColorDescription::encodedToNits(const QVector3D &nits, NamedTransferFunction tf, double sdrBrightness)
0317 {
0318     switch (tf) {
0319     case NamedTransferFunction::sRGB:
0320         return sdrBrightness * QVector3D(srgbToLinear(nits.x()), srgbToLinear(nits.y()), srgbToLinear(nits.z()));
0321     case NamedTransferFunction::gamma22:
0322         return sdrBrightness * QVector3D(std::pow(nits.x(), 2.2), std::pow(nits.y(), 2.2), std::pow(nits.z(), 2.2));
0323     case NamedTransferFunction::linear:
0324         return nits;
0325     case NamedTransferFunction::scRGB:
0326         return nits * 80.0f;
0327     case NamedTransferFunction::PerceptualQuantizer:
0328         return QVector3D(pqToNits(nits.x()), pqToNits(nits.y()), pqToNits(nits.z()));
0329     }
0330     Q_UNREACHABLE();
0331 }
0332 
0333 QVector3D ColorDescription::nitsToEncoded(const QVector3D &rgb, NamedTransferFunction tf, double sdrBrightness)
0334 {
0335     switch (tf) {
0336     case NamedTransferFunction::sRGB: {
0337         const auto clamped = clamp(rgb / sdrBrightness);
0338         return QVector3D(linearToSRGB(clamped.x()), linearToSRGB(clamped.y()), linearToSRGB(clamped.z()));
0339     }
0340     case NamedTransferFunction::gamma22: {
0341         const auto clamped = clamp(rgb / sdrBrightness);
0342         return QVector3D(std::pow(clamped.x(), 1 / 2.2), std::pow(clamped.y(), 1 / 2.2), std::pow(clamped.z(), 1 / 2.2));
0343     }
0344     case NamedTransferFunction::linear:
0345         return rgb;
0346     case NamedTransferFunction::scRGB:
0347         return rgb / 80.0f;
0348     case NamedTransferFunction::PerceptualQuantizer:
0349         return QVector3D(nitsToPQ(rgb.x()), nitsToPQ(rgb.y()), nitsToPQ(rgb.z()));
0350     }
0351     Q_UNREACHABLE();
0352 }
0353 
0354 QVector3D ColorDescription::mapTo(QVector3D rgb, const ColorDescription &dst) const
0355 {
0356     rgb = encodedToNits(rgb, m_transferFunction, m_sdrBrightness);
0357     rgb = m_colorimetry.toOther(dst.colorimetry()) * rgb;
0358     return nitsToEncoded(rgb, dst.transferFunction(), dst.sdrBrightness());
0359 }
0360 }