From 78a7e54f8f5c4ca6ce1ee6b0ac82c42b21738ac5 Mon Sep 17 00:00:00 2001 From: Allan Sandfeld Jensen Date: Mon, 3 Dec 2018 16:03:17 +0100 Subject: Custom color-space based on chromaticities Change-Id: I7fa6efa8993aa2b79ea60b6a21bf57c4f67a120f Reviewed-by: Eirik Aavitsland --- src/gui/painting/qcolormatrix_p.h | 45 +++++- src/gui/painting/qcolorspace.cpp | 155 ++++++++++++++++++--- src/gui/painting/qcolorspace.h | 3 + src/gui/painting/qcolorspace_p.h | 26 ++++ .../gui/painting/qcolorspace/tst_qcolorspace.cpp | 68 +++++++++ 5 files changed, 269 insertions(+), 28 deletions(-) diff --git a/src/gui/painting/qcolormatrix_p.h b/src/gui/painting/qcolormatrix_p.h index 3d1dca6222..66db95df7e 100644 --- a/src/gui/painting/qcolormatrix_p.h +++ b/src/gui/painting/qcolormatrix_p.h @@ -52,6 +52,7 @@ // #include +#include #include QT_BEGIN_NAMESPACE @@ -61,7 +62,13 @@ class QColorVector { public: QColorVector() = default; - constexpr QColorVector(float x, float y, float z) : x(x), y(y), z(z), _unused(0.0f) { } + Q_DECL_CONSTEXPR QColorVector(float x, float y, float z) : x(x), y(y), z(z), _unused(0.0f) { } + explicit Q_DECL_CONSTEXPR QColorVector(const QPointF &chr) // from XY chromaticity + : x(chr.x() / chr.y()) + , y(1.0f) + , z((1.0 - chr.x() - chr.y()) / chr.y()) + , _unused(0.0f) + { } float x; // X, x or red float y; // Y, y or green float z; // Z, Y or blue @@ -69,11 +76,28 @@ public: friend inline bool operator==(const QColorVector &v1, const QColorVector &v2); friend inline bool operator!=(const QColorVector &v1, const QColorVector &v2); + bool isNull() const + { + return !x && !y && !z; + } + + static Q_DECL_CONSTEXPR QColorVector null() { return QColorVector(0.0f, 0.0f, 0.0f); } + static bool isValidChromaticity(const QPointF &chr) + { + if (chr.x() < qreal(0.0) || chr.x() > qreal(1.0)) + return false; + if (chr.y() <= qreal(0.0) || chr.y() > qreal(1.0)) + return false; + if (chr.x() + chr.y() > qreal(1.0)) + return false; + return true; + } - static constexpr QColorVector null() { return QColorVector(0.0f, 0.0f, 0.0f); } - // Common whitepoints on normalized XYZ form: - static constexpr QColorVector D50() { return QColorVector(0.96421f, 1.0f, 0.82519f); } - static constexpr QColorVector D65() { return QColorVector(0.95043f, 1.0f, 1.08890f); } + // Common whitepoints: + static Q_DECL_CONSTEXPR QPointF D50Chromaticity() { return QPointF(0.34567, 0.35850); } + static Q_DECL_CONSTEXPR QPointF D65Chromaticity() { return QPointF(0.31271, 0.32902); } + static Q_DECL_CONSTEXPR QColorVector D50() { return QColorVector(D50Chromaticity()); } + static Q_DECL_CONSTEXPR QColorVector D65() { return QColorVector(D65Chromaticity()); } }; inline bool operator==(const QColorVector &v1, const QColorVector &v2) @@ -102,6 +126,10 @@ public: friend inline bool operator==(const QColorMatrix &m1, const QColorMatrix &m2); friend inline bool operator!=(const QColorMatrix &m1, const QColorMatrix &m2); + bool isNull() const + { + return r.isNull() && g.isNull() && b.isNull(); + } bool isValid() const { // A color matrix must be invertible @@ -167,6 +195,13 @@ public: { return { { 1.0f, 0.0f, 0.0f }, { 0.0f, 1.0f, 0.0f }, { 0.0f, 0.0f, 1.0f } }; } + static QColorMatrix fromScale(QColorVector v) + { + return QColorMatrix { { v.x, 0.0f, 0.0f }, + { 0.0f, v.y, 0.0f }, + { 0.0f, 0.0f, v.z } }; + } + // These are used to recognize matrices from ICC profiles: static QColorMatrix toXyzFromSRgb() { return QColorMatrix { { 0.4360217452f, 0.2224751115f, 0.0139281144f }, diff --git a/src/gui/painting/qcolorspace.cpp b/src/gui/painting/qcolorspace.cpp index 24785f7b61..c98c31fe05 100644 --- a/src/gui/painting/qcolorspace.cpp +++ b/src/gui/painting/qcolorspace.cpp @@ -53,6 +53,102 @@ QT_BEGIN_NAMESPACE +QColorSpacePrimaries::QColorSpacePrimaries(QColorSpace::Gamut gamut) +{ + switch (gamut) { + case QColorSpace::Gamut::SRgb: + redPoint = QPointF(0.640, 0.330); + greenPoint = QPointF(0.300, 0.600); + bluePoint = QPointF(0.150, 0.060); + whitePoint = QColorVector::D65Chromaticity(); + break; + case QColorSpace::Gamut::DciP3D65: + redPoint = QPointF(0.680, 0.320); + greenPoint = QPointF(0.265, 0.690); + bluePoint = QPointF(0.150, 0.060); + whitePoint = QColorVector::D65Chromaticity(); + break; + case QColorSpace::Gamut::Bt2020: + redPoint = QPointF(0.708, 0.292); + greenPoint = QPointF(0.190, 0.797); + bluePoint = QPointF(0.131, 0.046); + whitePoint = QColorVector::D65Chromaticity(); + break; + case QColorSpace::Gamut::AdobeRgb: + redPoint = QPointF(0.640, 0.330); + greenPoint = QPointF(0.210, 0.710); + bluePoint = QPointF(0.150, 0.060); + whitePoint = QColorVector::D65Chromaticity(); + break; + case QColorSpace::Gamut::ProPhotoRgb: + redPoint = QPointF(0.7347, 0.2653); + greenPoint = QPointF(0.1596, 0.8404); + bluePoint = QPointF(0.0366, 0.0001); + whitePoint = QColorVector::D50Chromaticity(); + break; + default: + Q_UNREACHABLE(); + } +} + +bool QColorSpacePrimaries::areValid() const +{ + if (!QColorVector::isValidChromaticity(redPoint)) + return false; + if (!QColorVector::isValidChromaticity(greenPoint)) + return false; + if (!QColorVector::isValidChromaticity(bluePoint)) + return false; + if (!QColorVector::isValidChromaticity(whitePoint)) + return false; + return true; +} + +QColorMatrix QColorSpacePrimaries::toXyzMatrix() const +{ + // This converts to XYZ in some undefined scale. + QColorMatrix toXyz = { QColorVector(redPoint), + QColorVector(greenPoint), + QColorVector(bluePoint) }; + + // Since the white point should be (1.0, 1.0, 1.0) in the + // input, we can figure out the scale by using the + // inverse conversion on the white point. + QColorVector wXyz(whitePoint); + QColorVector whiteScale = toXyz.inverted().map(wXyz); + + // Now we have scaled conversion to XYZ relative to the given whitepoint + toXyz = toXyz * QColorMatrix::fromScale(whiteScale); + + // But we want a conversion to XYZ relative to D50 + QColorVector wXyzD50 = QColorVector::D50(); + + if (wXyz != wXyzD50) { + // Do chromatic adaptation to map our white point to XYZ D50. + + // The Bradford method chromatic adaptation matrix: + QColorMatrix abrad = { { 0.8951f, -0.7502f, 0.0389f }, + { 0.2664f, 1.7135f, -0.0685f }, + { -0.1614f, 0.0367f, 1.0296f } }; + QColorMatrix abradinv = { { 0.9869929f, 0.4323053f, -0.0085287f }, + { -0.1470543f, 0.5183603f, 0.0400428f }, + { 0.1599627f, 0.0492912f, 0.9684867f } }; + + QColorVector srcCone = abrad.map(wXyz); + QColorVector dstCone = abrad.map(wXyzD50); + + QColorMatrix wToD50 = { { dstCone.x / srcCone.x, 0, 0 }, + { 0, dstCone.y / srcCone.y, 0 }, + { 0, 0, dstCone.z / srcCone.z } }; + + + QColorMatrix chromaticAdaptation = abradinv * (wToD50 * abrad); + toXyz = chromaticAdaptation * toXyz; + } + + return toXyz; +} + QColorSpacePrivate::QColorSpacePrivate() : id(QColorSpace::Unknown) , gamut(QColorSpace::Gamut::Custom) @@ -128,6 +224,21 @@ QColorSpacePrivate::QColorSpacePrivate(QColorSpace::Gamut gamut, QColorSpace::Tr initialize(); } +QColorSpacePrivate::QColorSpacePrivate(const QColorSpacePrimaries &primaries, + QColorSpace::TransferFunction fun, + float gamma) + : gamut(QColorSpace::Gamut::Custom) + , transferFunction(fun) + , gamma(gamma) +{ + Q_ASSERT(primaries.areValid()); + toXyz = primaries.toXyzMatrix(); + whitePoint = QColorVector(primaries.whitePoint); + if (!identifyColorSpace()) + id = QColorSpace::Unknown; + setTransferFunction(); +} + bool QColorSpacePrivate::identifyColorSpace() { switch (gamut) { @@ -195,33 +306,14 @@ void QColorSpacePrivate::initialize() void QColorSpacePrivate::setToXyzMatrix() { - switch (gamut) { - case QColorSpace::Gamut::SRgb: - toXyz = QColorMatrix::toXyzFromSRgb(); - whitePoint = QColorVector::D65(); - return; - case QColorSpace::Gamut::AdobeRgb: - toXyz = QColorMatrix::toXyzFromAdobeRgb(); - whitePoint = QColorVector::D65(); - return; - case QColorSpace::Gamut::DciP3D65: - toXyz = QColorMatrix::toXyzFromDciP3D65(); - whitePoint = QColorVector::D65(); - return; - case QColorSpace::Gamut::ProPhotoRgb: - toXyz = QColorMatrix::toXyzFromProPhotoRgb(); - whitePoint = QColorVector::D50(); - return; - case QColorSpace::Gamut::Bt2020: - toXyz = QColorMatrix::toXyzFromBt2020(); - whitePoint = QColorVector::D65(); - return; - case QColorSpace::Gamut::Custom: + if (gamut == QColorSpace::Gamut::Custom) { toXyz = QColorMatrix::null(); whitePoint = QColorVector::D50(); return; } - Q_UNREACHABLE(); + QColorSpacePrimaries primaries(gamut); + toXyz = primaries.toXyzMatrix(); + whitePoint = QColorVector(primaries.whitePoint); } void QColorSpacePrivate::setTransferFunction() @@ -386,6 +478,23 @@ QColorSpace::QColorSpace(QColorSpace::Gamut gamut, float gamma) { } +/*! + Creates a custom colorspace with a gamut based on the chromaticities of the primary colors \a whitePoint, + \a redPoint, \a greenPoint and \a bluePoint, and using the transfer function \a fun and optionally \a gamma. + */ +QColorSpace::QColorSpace(const QPointF &whitePoint, const QPointF &redPoint, + const QPointF &greenPoint, const QPointF &bluePoint, + QColorSpace::TransferFunction fun, float gamma) +{ + QColorSpacePrimaries primaries(whitePoint, redPoint, greenPoint, bluePoint); + if (!primaries.areValid()) { + qWarning() << "QColorSpace attempted constructed from invalid primaries:" << whitePoint << redPoint << greenPoint << bluePoint; + d_ptr = QColorSpace(QColorSpace::Undefined).d_ptr; + return; + } + d_ptr = new QColorSpacePrivate(primaries, fun, gamma); +} + QColorSpace::~QColorSpace() { } diff --git a/src/gui/painting/qcolorspace.h b/src/gui/painting/qcolorspace.h index 923546ec6f..56676826a9 100644 --- a/src/gui/painting/qcolorspace.h +++ b/src/gui/painting/qcolorspace.h @@ -85,6 +85,9 @@ public: QColorSpace(ColorSpaceId colorSpaceId = Undefined); QColorSpace(Gamut gamut, TransferFunction fun, float gamma = 0.0f); QColorSpace(Gamut gamut, float gamma); + QColorSpace(const QPointF &whitePoint, const QPointF &redPoint, + const QPointF &greenPoint, const QPointF &bluePoint, + TransferFunction fun, float gamma = 0.0f); ~QColorSpace(); QColorSpace(QColorSpace &&colorSpace); diff --git a/src/gui/painting/qcolorspace_p.h b/src/gui/painting/qcolorspace_p.h index 91107a9a89..21260a281d 100644 --- a/src/gui/painting/qcolorspace_p.h +++ b/src/gui/painting/qcolorspace_p.h @@ -57,15 +57,41 @@ #include "qcolortrclut_p.h" #include +#include QT_BEGIN_NAMESPACE +class Q_GUI_EXPORT QColorSpacePrimaries +{ +public: + QColorSpacePrimaries() = default; + QColorSpacePrimaries(QColorSpace::Gamut gamut); + QColorSpacePrimaries(QPointF whitePoint, + QPointF redPoint, + QPointF greenPoint, + QPointF bluePoint) + : whitePoint(whitePoint) + , redPoint(redPoint) + , greenPoint(greenPoint) + , bluePoint(bluePoint) + { } + + QColorMatrix toXyzMatrix() const; + bool areValid() const; + + QPointF whitePoint; + QPointF redPoint; + QPointF greenPoint; + QPointF bluePoint; +}; + class QColorSpacePrivate : public QSharedData { public: QColorSpacePrivate(); QColorSpacePrivate(QColorSpace::ColorSpaceId colorSpaceId); QColorSpacePrivate(QColorSpace::Gamut gamut, QColorSpace::TransferFunction fun, float gamma); + QColorSpacePrivate(const QColorSpacePrimaries &primaries, QColorSpace::TransferFunction fun, float gamma); QColorSpacePrivate(const QColorSpacePrivate &other) = default; QColorSpacePrivate &operator=(const QColorSpacePrivate &other) = default; diff --git a/tests/auto/gui/painting/qcolorspace/tst_qcolorspace.cpp b/tests/auto/gui/painting/qcolorspace/tst_qcolorspace.cpp index 35bca58854..7a88eb18b2 100644 --- a/tests/auto/gui/painting/qcolorspace/tst_qcolorspace.cpp +++ b/tests/auto/gui/painting/qcolorspace/tst_qcolorspace.cpp @@ -33,6 +33,8 @@ #include #include +#include + Q_DECLARE_METATYPE(QColorSpace::ColorSpaceId) Q_DECLARE_METATYPE(QColorSpace::Gamut) Q_DECLARE_METATYPE(QColorSpace::TransferFunction) @@ -59,6 +61,10 @@ private slots: void loadImage(); void gamut(); + void primariesXyz(); + void primaries2_data(); + void primaries2(); + void invalidPrimaries(); }; tst_QColorSpace::tst_QColorSpace() @@ -289,5 +295,67 @@ void tst_QColorSpace::gamut() QVERIFY(tgreen.blueF() > 0.2); } +void tst_QColorSpace::primariesXyz() +{ + QColorSpace sRgb = QColorSpace::SRgb; + QColorSpace adobeRgb = QColorSpace::AdobeRgb; + QColorSpace displayP3 = QColorSpace::DisplayP3; + QColorSpace proPhotoRgb = QColorSpace::ProPhotoRgb; + QColorSpace bt2020 = QColorSpace::Bt2020; + + // Check if our calculated matrices, match the precalculated ones. + QCOMPARE(sRgb.d_func()->toXyz, QColorMatrix::toXyzFromSRgb()); + QCOMPARE(adobeRgb.d_func()->toXyz, QColorMatrix::toXyzFromAdobeRgb()); + QCOMPARE(displayP3.d_func()->toXyz, QColorMatrix::toXyzFromDciP3D65()); + QCOMPARE(proPhotoRgb.d_func()->toXyz, QColorMatrix::toXyzFromProPhotoRgb()); + QCOMPARE(bt2020.d_func()->toXyz, QColorMatrix::toXyzFromBt2020()); +} + +void tst_QColorSpace::primaries2_data() +{ + QTest::addColumn("gamut"); + + QTest::newRow("sRGB") << QColorSpace::Gamut::SRgb; + QTest::newRow("DCI-P3 (D65)") << QColorSpace::Gamut::DciP3D65; + QTest::newRow("Adobe RGB (1998)") << QColorSpace::Gamut::AdobeRgb; + QTest::newRow("ProPhoto RGB") << QColorSpace::Gamut::ProPhotoRgb; + QTest::newRow("BT.2020") << QColorSpace::Gamut::Bt2020; +} + +void tst_QColorSpace::primaries2() +{ + QFETCH(QColorSpace::Gamut, gamut); + QColorSpacePrimaries primaries(gamut); + + QColorSpace original(gamut, QColorSpace::TransferFunction::Linear); + QColorSpace custom1(primaries.whitePoint, primaries.redPoint, + primaries.greenPoint, primaries.bluePoint, QColorSpace::TransferFunction::Linear); + QCOMPARE(original, custom1); + + // A custom color swizzled color-space: + QColorSpace custom2(primaries.whitePoint, primaries.bluePoint, + primaries.greenPoint, primaries.redPoint, QColorSpace::TransferFunction::Linear); + + QVERIFY(custom1 != custom2); + QColor color1(255, 127, 63); + QColor color2 = custom1.transformationToColorSpace(custom2).map(color1); + QCOMPARE(color2.red(), color1.blue()); + QCOMPARE(color2.green(), color1.green()); + QCOMPARE(color2.blue(), color1.red()); + QCOMPARE(color2.alpha(), color1.alpha()); + QColor color3 = custom2.transformationToColorSpace(custom1).map(color2); + QCOMPARE(color3.red(), color1.red()); + QCOMPARE(color3.green(), color1.green()); + QCOMPARE(color3.blue(), color1.blue()); + QCOMPARE(color3.alpha(), color1.alpha()); +} + +void tst_QColorSpace::invalidPrimaries() +{ + QColorSpace custom(QPointF(), QPointF(), QPointF(), QPointF(), QColorSpace::TransferFunction::Linear); + QVERIFY(!custom.isValid()); + QCOMPARE(custom.colorSpaceId(), QColorSpace::Undefined); +} + QTEST_MAIN(tst_QColorSpace) #include "tst_qcolorspace.moc" -- cgit v1.2.3