Custom color-space based on chromaticities
Change-Id: I7fa6efa8993aa2b79ea60b6a21bf57c4f67a120f Reviewed-by: Eirik Aavitsland <eirik.aavitsland@qt.io>
This commit is contained in:
parent
fe57936d8c
commit
78a7e54f8f
@ -52,6 +52,7 @@
|
||||
//
|
||||
|
||||
#include <QtGui/qtguiglobal.h>
|
||||
#include <QtCore/qpoint.h>
|
||||
#include <cmath>
|
||||
|
||||
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 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); }
|
||||
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;
|
||||
}
|
||||
|
||||
// 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 },
|
||||
|
@ -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()
|
||||
{
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -57,15 +57,41 @@
|
||||
#include "qcolortrclut_p.h"
|
||||
|
||||
#include <QtCore/qshareddata.h>
|
||||
#include <QtCore/qpoint.h>
|
||||
|
||||
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;
|
||||
|
||||
|
@ -33,6 +33,8 @@
|
||||
#include <qimage.h>
|
||||
#include <qimagereader.h>
|
||||
|
||||
#include <private/qcolorspace_p.h>
|
||||
|
||||
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<QColorSpace::Gamut>("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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user