Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert color conversion internals to matrices stored as 1D array #1932

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions include/avif/internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,18 +100,18 @@ avifTransferFunction avifTransferCharacteristicsGetLinearToGammaFunction(avifTra
// Computes the RGB->YUV conversion coefficients kr, kg, kb, such that Y=kr*R+kg*G+kb*B.
void avifColorPrimariesComputeYCoeffs(avifColorPrimaries colorPrimaries, float coeffs[3]);

// Computes a conversion matrix from RGB to XYZ with a D50 white point.
AVIF_NODISCARD avifBool avifColorPrimariesComputeRGBToXYZD50Matrix(avifColorPrimaries colorPrimaries, double coeffs[3][3]);
// Computes a conversion matrix from XYZ with a D50 white point to RGB.
AVIF_NODISCARD avifBool avifColorPrimariesComputeXYZD50ToRGBMatrix(avifColorPrimaries colorPrimaries, double coeffs[3][3]);
// Computes the RGB->RGB conversion matrix to convert from one set of RGB primaries to another.
// Computes a row-major conversion matrix (stored as an array) from RGB to XYZ with a D50 white point.
AVIF_NODISCARD avifBool avifColorPrimariesComputeRGBToXYZD50Matrix(avifColorPrimaries colorPrimaries, double coeffs[9]);
// Computes a row-major conversion matrix (stored as an array) from XYZ with a D50 white point to RGB.
AVIF_NODISCARD avifBool avifColorPrimariesComputeXYZD50ToRGBMatrix(avifColorPrimaries colorPrimaries, double coeffs[9]);
wantehchang marked this conversation as resolved.
Show resolved Hide resolved
// Computes the RGB->RGB conversion matrix (stored as an array) to convert from one set of RGB primaries to another.
AVIF_NODISCARD avifBool avifColorPrimariesComputeRGBToRGBMatrix(avifColorPrimaries srcColorPrimaries,
avifColorPrimaries dstColorPrimaries,
double coeffs[3][3]);
double coeffs[9]);
// Converts the given linear RGB pixel from one color space to another using the provided coefficients.
// The coefficients can be obtained with avifColorPrimariesComputeRGBToRGBMatrix().
// The output values are not clamped and may be < 0 or > 1.
void avifLinearRGBConvertColorSpace(float rgb[4], const double coeffs[3][3]);
void avifLinearRGBConvertColorSpace(float rgb[4], const double coeffs[9]);

#define AVIF_ARRAY_DECLARE(TYPENAME, ITEMSTYPE, ITEMSNAME) \
typedef struct TYPENAME \
Expand Down
63 changes: 38 additions & 25 deletions src/colrconvert.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,17 @@ static avifBool avifXyToXYZ(const float xy[2], double XYZ[3])
}

// Computes I = M^-1. Returns false if M seems to be singular.
static avifBool avifMatInv(const double M[3][3], double I[3][3])
static avifBool avifMatInv(const double Mmat[9], double Imat[9])
{
const double * const M[3] = { Mmat + 0, Mmat + 3, Mmat + 6 };
double det = M[0][0] * (M[1][1] * M[2][2] - M[2][1] * M[1][2]) - M[0][1] * (M[1][0] * M[2][2] - M[1][2] * M[2][0]) +
M[0][2] * (M[1][0] * M[2][1] - M[1][1] * M[2][0]);
if (fabs(det) < epsilon) {
return AVIF_FALSE;
}
det = 1.0 / det;

double * const I[3] = { Imat + 0, Imat + 3, Imat + 6 };
I[0][0] = (M[1][1] * M[2][2] - M[2][1] * M[1][2]) * det;
I[0][1] = (M[0][2] * M[2][1] - M[0][1] * M[2][2]) * det;
I[0][2] = (M[0][1] * M[1][2] - M[0][2] * M[1][1]) * det;
Expand All @@ -45,8 +47,11 @@ static avifBool avifMatInv(const double M[3][3], double I[3][3])
}

// Computes C = A*B
static void avifMatMul(const double A[3][3], const double B[3][3], double C[3][3])
static void avifMatMul(const double Amat[9], const double Bmat[9], double Cmat[9])
{
const double * const A[3] = { Amat + 0, Amat + 3, Amat + 6 };
const double * const B[3] = { Bmat + 0, Bmat + 3, Bmat + 6 };
double * const C[3] = { Cmat + 0, Cmat + 3, Cmat + 6 };
C[0][0] = A[0][0] * B[0][0] + A[0][1] * B[1][0] + A[0][2] * B[2][0];
C[0][1] = A[0][0] * B[0][1] + A[0][1] * B[1][1] + A[0][2] * B[2][1];
C[0][2] = A[0][0] * B[0][2] + A[0][1] * B[1][2] + A[0][2] * B[2][2];
Expand All @@ -59,8 +64,9 @@ static void avifMatMul(const double A[3][3], const double B[3][3], double C[3][3
}

// Set M to have values of d on the leading diagonal, and zero elsewhere.
static void avifMatDiag(const double d[3], double M[3][3])
static void avifMatDiag(const double d[3], double Mmat[9])
{
double * const M[3] = { Mmat + 0, Mmat + 3, Mmat + 6 };
M[0][0] = d[0];
M[0][1] = 0;
M[0][2] = 0;
Expand All @@ -73,48 +79,55 @@ static void avifMatDiag(const double d[3], double M[3][3])
}

// Computes y = M.x
static void avifVecMul(const double M[3][3], const double x[3], double y[3])
static void avifVecMul(const double Mmat[9], const double x[3], double y[3])
{
const double * const M[3] = { Mmat + 0, Mmat + 3, Mmat + 6 };
y[0] = M[0][0] * x[0] + M[0][1] * x[1] + M[0][2] * x[2];
y[1] = M[1][0] * x[0] + M[1][1] * x[1] + M[1][2] * x[2];
y[2] = M[2][0] * x[0] + M[2][1] * x[1] + M[2][2] * x[2];
}

// Bradford chromatic adaptation matrix
// from https://www.researchgate.net/publication/253799640_A_uniform_colour_space_based_upon_CIECAM97s
static const double avifBradford[3][3] = {
{ 0.8951, 0.2664, -0.1614 },
{ -0.7502, 1.7135, 0.0367 },
{ 0.0389, -0.0685, 1.0296 },
static const double avifBradford[9] = {
0.8951, 0.2664, -0.1614, // row 0
-0.7502, 1.7135, 0.0367, // row 1
0.0389, -0.0685, 1.0296 // row 2
};

// LMS values for D50 whitepoint
static const double avifLmsD50[3] = { 0.996284, 1.02043, 0.818644 };

avifBool avifColorPrimariesComputeRGBToXYZD50Matrix(avifColorPrimaries colorPrimaries, double coeffs[3][3])
avifBool avifColorPrimariesComputeRGBToXYZD50Matrix(avifColorPrimaries colorPrimaries, double coeffs[9])
{
float primaries[8];
avifColorPrimariesGetValues(colorPrimaries, primaries);

double whitePointXYZ[3];
AVIF_CHECK(avifXyToXYZ(&primaries[6], whitePointXYZ));

const double rgbPrimaries[3][3] = {
{ primaries[0], primaries[2], primaries[4] },
{ primaries[1], primaries[3], primaries[5] },
{ 1.0 - primaries[0] - primaries[1], 1.0 - primaries[2] - primaries[3], 1.0 - primaries[4] - primaries[5] }
const double rgbPrimaries[9] = {
primaries[0],
primaries[2],
primaries[4], // row 0
primaries[1],
primaries[3],
primaries[5], // row 1
1.0 - primaries[0] - primaries[1],
1.0 - primaries[2] - primaries[3],
1.0 - primaries[4] - primaries[5] // row 2
};

double rgbPrimariesInv[3][3];
double rgbPrimariesInv[9];
AVIF_CHECK(avifMatInv(rgbPrimaries, rgbPrimariesInv));

double rgbCoefficients[3];
avifVecMul(rgbPrimariesInv, whitePointXYZ, rgbCoefficients);

double rgbCoefficientsMat[3][3];
double rgbCoefficientsMat[9];
avifMatDiag(rgbCoefficients, rgbCoefficientsMat);

double rgbXYZ[3][3];
double rgbXYZ[9];
avifMatMul(rgbPrimaries, rgbCoefficientsMat, rgbXYZ);

// ICC stores primaries XYZ under PCS.
Expand All @@ -129,13 +142,13 @@ avifBool avifColorPrimariesComputeRGBToXYZD50Matrix(avifColorPrimaries colorPrim
lms[i] = avifLmsD50[i] / lms[i];
}

double adaptation[3][3];
double adaptation[9];
avifMatDiag(lms, adaptation);

double tmp[3][3];
double tmp[9];
avifMatMul(adaptation, avifBradford, tmp);

double bradfordInv[3][3];
double bradfordInv[9];
if (!avifMatInv(avifBradford, bradfordInv)) {
return AVIF_FALSE;
}
Expand All @@ -146,23 +159,23 @@ avifBool avifColorPrimariesComputeRGBToXYZD50Matrix(avifColorPrimaries colorPrim
return AVIF_TRUE;
}

avifBool avifColorPrimariesComputeXYZD50ToRGBMatrix(avifColorPrimaries colorPrimaries, double coeffs[3][3])
avifBool avifColorPrimariesComputeXYZD50ToRGBMatrix(avifColorPrimaries colorPrimaries, double coeffs[9])
{
double rgbToXyz[3][3];
double rgbToXyz[9];
AVIF_CHECK(avifColorPrimariesComputeRGBToXYZD50Matrix(colorPrimaries, rgbToXyz));
AVIF_CHECK(avifMatInv(rgbToXyz, coeffs));
return AVIF_TRUE;
}

avifBool avifColorPrimariesComputeRGBToRGBMatrix(avifColorPrimaries srcColorPrimaries,
avifColorPrimaries dstColorPrimaries,
double coeffs[3][3])
double coeffs[9])
{
// Note: no special casing for srcColorPrimaries == dstColorPrimaries to allow
// testing that the computation actually produces the identity matrix.
double srcRGBToXYZ[3][3];
double srcRGBToXYZ[9];
AVIF_CHECK(avifColorPrimariesComputeRGBToXYZD50Matrix(srcColorPrimaries, srcRGBToXYZ));
double xyzToDstRGB[3][3];
double xyzToDstRGB[9];
AVIF_CHECK(avifColorPrimariesComputeXYZD50ToRGBMatrix(dstColorPrimaries, xyzToDstRGB));
// coeffs = xyzToDstRGB * srcRGBToXYZ
// i.e. srcRGB -> XYZ -> dstRGB
Expand All @@ -175,7 +188,7 @@ avifBool avifColorPrimariesComputeRGBToRGBMatrix(avifColorPrimaries srcColorPrim
// better to clamp the output to [0, 1]. Linear values don't need clamping because values
// > 1.0 are valid for HDR transfer curves, and the gamma compression function will do the
// clamping as necessary.
void avifLinearRGBConvertColorSpace(float rgb[4], const double coeffs[3][3])
void avifLinearRGBConvertColorSpace(float rgb[4], const double coeffs[9])
{
const double rgbDouble[3] = { rgb[0], rgb[1], rgb[2] };
double converted[3];
Expand Down
12 changes: 6 additions & 6 deletions src/gainmap.c
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ avifResult avifRGBImageApplyGainMap(const avifRGBImage * baseImage,
// Early exit if the gain map does not need to be applied.
if (weight == 0.0f) {
const avifBool primariesDiffer = (baseColorPrimaries != outputColorPrimaries);
double conversionCoeffs[3][3];
double conversionCoeffs[9];
if (primariesDiffer && !avifColorPrimariesComputeRGBToRGBMatrix(baseColorPrimaries, outputColorPrimaries, conversionCoeffs)) {
avifDiagnosticsPrintf(diag, "Unsupported RGB color space conversion");
res = AVIF_RESULT_NOT_IMPLEMENTED;
Expand Down Expand Up @@ -207,8 +207,8 @@ avifResult avifRGBImageApplyGainMap(const avifRGBImage * baseImage,
goto cleanup;
}

double inputConversionCoeffs[3][3];
double outputConversionCoeffs[3][3];
double inputConversionCoeffs[9];
double outputConversionCoeffs[9];
if (needsInputColorConversion &&
!avifColorPrimariesComputeRGBToRGBMatrix(baseColorPrimaries, gainMapMathPrimaries, inputConversionCoeffs)) {
avifDiagnosticsPrintf(diag, "Unsupported RGB color space conversion");
Expand Down Expand Up @@ -465,8 +465,8 @@ static avifResult avifChooseColorSpaceForGainMapMath(avifColorPrimaries basePrim
}
// Color convert pure red, pure green and pure blue in turn and see if they result in negative values.
float rgba[4] = { 0 };
double baseToAltCoeffs[3][3];
double altToBaseCoeffs[3][3];
double baseToAltCoeffs[9];
double altToBaseCoeffs[9];
if (!avifColorPrimariesComputeRGBToRGBMatrix(basePrimaries, altPrimaries, baseToAltCoeffs) ||
!avifColorPrimariesComputeRGBToRGBMatrix(altPrimaries, basePrimaries, altToBaseCoeffs)) {
return AVIF_RESULT_NOT_IMPLEMENTED;
Expand Down Expand Up @@ -555,7 +555,7 @@ avifResult avifRGBImageComputeGainMap(const avifRGBImage * baseRgbImage,
float yCoeffs[3];
avifColorPrimariesComputeYCoeffs(gainMapMathPrimaries, yCoeffs);

double rgbConversionCoeffs[3][3];
double rgbConversionCoeffs[9];
if (colorSpacesDiffer) {
if (useBaseColorSpace) {
if (!avifColorPrimariesComputeRGBToRGBMatrix(altColorPrimaries, baseColorPrimaries, rgbConversionCoeffs)) {
Expand Down
68 changes: 40 additions & 28 deletions tests/gtest/avifcolrconverttest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -15,36 +15,38 @@ namespace {
// Used to pass the data folder path to the GoogleTest suites.
const char* data_path = nullptr;

void ExpectMatrixNear(const double actual[3][3],
void ExpectMatrixNear(const double actual[9],
const std::array<std::array<double, 3>, 3>& expected,
const double epsilon) {
EXPECT_NEAR(actual[0][0], expected[0][0], epsilon);
EXPECT_NEAR(actual[0][1], expected[0][1], epsilon);
EXPECT_NEAR(actual[0][2], expected[0][2], epsilon);
EXPECT_NEAR(actual[1][0], expected[1][0], epsilon);
EXPECT_NEAR(actual[1][1], expected[1][1], epsilon);
EXPECT_NEAR(actual[1][2], expected[1][2], epsilon);
EXPECT_NEAR(actual[2][0], expected[2][0], epsilon);
EXPECT_NEAR(actual[2][1], expected[2][1], epsilon);
EXPECT_NEAR(actual[2][2], expected[2][2], epsilon);
EXPECT_NEAR(actual[0], expected[0][0], epsilon);
EXPECT_NEAR(actual[1], expected[0][1], epsilon);
EXPECT_NEAR(actual[2], expected[0][2], epsilon);
EXPECT_NEAR(actual[3], expected[1][0], epsilon);
EXPECT_NEAR(actual[4], expected[1][1], epsilon);
EXPECT_NEAR(actual[5], expected[1][2], epsilon);
EXPECT_NEAR(actual[6], expected[2][0], epsilon);
EXPECT_NEAR(actual[7], expected[2][1], epsilon);
EXPECT_NEAR(actual[8], expected[2][2], epsilon);
}

TEST(RgbToXyzD50Matrix, GoldenValues) {
double coeffs[3][3];
double coeffs[9];
ASSERT_TRUE(avifColorPrimariesComputeRGBToXYZD50Matrix(
AVIF_COLOR_PRIMARIES_BT709, coeffs));
// Golden values from
// http://brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
const double kEpsilon = 0.00015;
ExpectMatrixNear(coeffs,
{{{0.4360747, 0.3850649, 0.1430804},
{0.2225045, 0.7168786, 0.0606169},
{0.0139322, 0.0971045, 0.7141733}}},
{{
0.4360747, 0.3850649, 0.1430804, // row 0
0.2225045, 0.7168786, 0.0606169, // row 1
0.0139322, 0.0971045, 0.7141733 // row 2
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the original still works, the original code seems to be a better initializer for std::array<std::array<double, 3>, 3> (see line 19).

This comment also applies to the second arguments for the ExpectMatrixNear() calls below.

}},
kEpsilon);
}

TEST(XyzD50ToRgbMatrix, GoldenValues) {
double coeffs[3][3];
double coeffs[9];
ASSERT_TRUE(avifColorPrimariesComputeXYZD50ToRGBMatrix(
AVIF_COLOR_PRIMARIES_BT709, coeffs));
// Golden values from
Expand All @@ -64,41 +66,51 @@ TEST(RgbToRgbConversion, Identity) {
const double kEpsilon = 0.000001;
for (int primaries = AVIF_COLOR_PRIMARIES_UNKNOWN;
primaries <= AVIF_COLOR_PRIMARIES_SMPTE432; ++primaries) {
double coeffs[3][3];
double coeffs[9];
ASSERT_TRUE(avifColorPrimariesComputeRGBToRGBMatrix(
(avifColorPrimaries)primaries, (avifColorPrimaries)primaries, coeffs));
ExpectMatrixNear(coeffs,
{{{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}}},
{{
1.0, 0.0, 0.0, // row 0
0.0, 1.0, 0.0, // row 1
0.0, 0.0, 1.0 // row 2
}},
kEpsilon);
}
}

TEST(ColorPrimariesComputeRGBToRGBMatrix, GoldenValues) {
// Golden values from http://color.support/colorspacecalculator.html
double coeffs[3][3];
double coeffs[9];
ASSERT_TRUE(avifColorPrimariesComputeRGBToRGBMatrix(
AVIF_COLOR_PRIMARIES_BT709, AVIF_COLOR_PRIMARIES_BT2020, coeffs));
const double kEpsilon = 0.0001;
ExpectMatrixNear(coeffs,
{{{0.627404, 0.329283, 0.043313},
{0.069097, 0.919540, 0.011362},
{0.016391, 0.088013, 0.895595}}},
{{
0.627404, 0.329283, 0.043313, // row 0
0.069097, 0.919540, 0.011362, // row 1
0.016391, 0.088013, 0.895595 // row 2
}},
kEpsilon);

ASSERT_TRUE(avifColorPrimariesComputeRGBToRGBMatrix(
AVIF_COLOR_PRIMARIES_BT2020, AVIF_COLOR_PRIMARIES_BT709, coeffs));
ExpectMatrixNear(coeffs,
{{{1.660491, -0.587641, -0.072850},
{-0.124550, 1.132900, -0.008349},
{-0.018151, -0.100579, 1.118730}}},
{{
1.660491, -0.587641, -0.072850, // row 0
-0.124550, 1.132900, -0.008349, // row 1
-0.018151, -0.100579, 1.118730 // row 2
}},
kEpsilon);

ASSERT_TRUE(avifColorPrimariesComputeRGBToRGBMatrix(
AVIF_COLOR_PRIMARIES_BT709, AVIF_COLOR_PRIMARIES_XYZ, coeffs));
ExpectMatrixNear(coeffs,
{{{0.438449, 0.392176, 0.169375},
{0.222828, 0.708691, 0.068481},
{0.017314, 0.110445, 0.872241}}},
{{
0.438449, 0.392176, 0.169375, // row 0
0.222828, 0.708691, 0.068481, // row 1
0.017314, 0.110445, 0.872241 // row 2
}},
kEpsilon);
}

Expand Down Expand Up @@ -167,7 +179,7 @@ TEST_P(ConvertImageColorspaceTest, ConvertImage) {
avifTransferCharacteristicsGetLinearToGammaFunction(
reference_image->transferCharacteristics);

double coeffs[3][3];
double coeffs[9];
ASSERT_TRUE(avifColorPrimariesComputeRGBToRGBMatrix(
src_image->colorPrimaries, reference_image->colorPrimaries, coeffs));

Expand Down
Loading