Custom color-space based on chromaticities

Change-Id: I7fa6efa8993aa2b79ea60b6a21bf57c4f67a120f
Reviewed-by: Eirik Aavitsland <eirik.aavitsland@qt.io>
This commit is contained in:
Allan Sandfeld Jensen 2018-12-03 16:03:17 +01:00
parent fe57936d8c
commit 78a7e54f8f
5 changed files with 269 additions and 28 deletions

View File

@ -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 },

View File

@ -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()
{
}

View File

@ -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);

View File

@ -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;

View File

@ -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"