static const double rec601_625_x_R = 0.640, rec601_625_x_G = 0.290, rec601_625_x_B = 0.150;
static const double rec601_625_y_R = 0.330, rec601_625_y_G = 0.600, rec601_625_y_B = 0.060;
+// Color coordinates from Rec. 2020.
+static const double rec2020_x_R = 0.708, rec2020_x_G = 0.170, rec2020_x_B = 0.131;
+static const double rec2020_y_R = 0.292, rec2020_y_G = 0.797, rec2020_y_B = 0.046;
+
ColorspaceConversionEffect::ColorspaceConversionEffect()
: source_space(COLORSPACE_sRGB),
destination_space(COLORSPACE_sRGB)
x_R = rec601_625_x_R; x_G = rec601_625_x_G; x_B = rec601_625_x_B;
y_R = rec601_625_y_R; y_G = rec601_625_y_G; y_B = rec601_625_y_B;
break;
+ case COLORSPACE_REC_2020:
+ x_R = rec2020_x_R; x_G = rec2020_x_G; x_B = rec2020_x_B;
+ y_R = rec2020_y_R; y_G = rec2020_y_G; y_B = rec2020_y_B;
+ break;
default:
assert(false);
}
EXPECT_FLOAT_EQ(1.0f, out_data[4 * 4 + 3]);
}
+TEST(ColorspaceConversionEffectTest, Rec2020_Primaries) {
+ float data[] = {
+ 0.0f, 0.0f, 0.0f, 1.0f,
+ 1.0f, 1.0f, 1.0f, 1.0f,
+ 1.0f, 0.0f, 0.0f, 1.0f,
+ 0.0f, 1.0f, 0.0f, 1.0f,
+ 0.0f, 0.0f, 1.0f, 1.0f,
+ };
+ float out_data[4 * 5];
+
+ EffectChainTester tester(data, 1, 5, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_REC_2020, GAMMA_LINEAR);
+ tester.run(out_data, GL_RGBA, COLORSPACE_XYZ, GAMMA_LINEAR);
+
+ // Black should stay black.
+ EXPECT_FLOAT_EQ(0.0f, out_data[0 * 4 + 0]);
+ EXPECT_FLOAT_EQ(0.0f, out_data[0 * 4 + 1]);
+ EXPECT_FLOAT_EQ(0.0f, out_data[0 * 4 + 2]);
+ EXPECT_FLOAT_EQ(1.0f, out_data[0 * 4 + 3]);
+
+ // Convert the primaries from XYZ to xyz, and compare to the references
+ // given by Rec. 2020.
+ float white_xyz_sum = out_data[1 * 4 + 0] + out_data[1 * 4 + 1] + out_data[1 * 4 + 2];
+ float white_x = out_data[1 * 4 + 0] / white_xyz_sum;
+ float white_y = out_data[1 * 4 + 1] / white_xyz_sum;
+ EXPECT_NEAR(0.3127, white_x, 1e-3);
+ EXPECT_NEAR(0.3290, white_y, 1e-3);
+ EXPECT_FLOAT_EQ(1.0f, out_data[1 * 4 + 3]);
+
+ float red_xyz_sum = out_data[2 * 4 + 0] + out_data[2 * 4 + 1] + out_data[2 * 4 + 2];
+ float red_x = out_data[2 * 4 + 0] / red_xyz_sum;
+ float red_y = out_data[2 * 4 + 1] / red_xyz_sum;
+ EXPECT_NEAR(0.708, red_x, 1e-3);
+ EXPECT_NEAR(0.292, red_y, 1e-3);
+ EXPECT_FLOAT_EQ(1.0f, out_data[2 * 4 + 3]);
+
+ float green_xyz_sum = out_data[3 * 4 + 0] + out_data[3 * 4 + 1] + out_data[3 * 4 + 2];
+ float green_x = out_data[3 * 4 + 0] / green_xyz_sum;
+ float green_y = out_data[3 * 4 + 1] / green_xyz_sum;
+ EXPECT_NEAR(0.170, green_x, 1e-3);
+ EXPECT_NEAR(0.797, green_y, 1e-3);
+ EXPECT_FLOAT_EQ(1.0f, out_data[3 * 4 + 3]);
+
+ float blue_xyz_sum = out_data[4 * 4 + 0] + out_data[4 * 4 + 1] + out_data[4 * 4 + 2];
+ float blue_x = out_data[4 * 4 + 0] / blue_xyz_sum;
+ float blue_y = out_data[4 * 4 + 1] / blue_xyz_sum;
+ EXPECT_NEAR(0.131, blue_x, 1e-3);
+ EXPECT_NEAR(0.046, blue_y, 1e-3);
+ EXPECT_FLOAT_EQ(1.0f, out_data[4 * 4 + 3]);
+}
+
TEST(ColorspaceConversionEffectTest, sRGBToRec601_525) {
float data[] = {
0.0f, 0.0f, 0.0f, 1.0f,
invalidate_1d_texture("compression_curve_tex");
return read_file("gamma_compression_effect.frag");
}
- if (destination_curve == GAMMA_REC_709) { // And Rec. 601.
+ if (destination_curve == GAMMA_REC_709 || // And Rec. 601, and 10-bit Rec. 2020.
+ destination_curve == GAMMA_REC_2020_12_BIT) {
+ // Rec. 2020, page 3.
+ float alpha, beta;
+ if (destination_curve == GAMMA_REC_2020_12_BIT) {
+ alpha = 1.0993f;
+ beta = 0.0181f;
+ } else {
+ alpha = 1.099f;
+ beta = 0.018f;
+ }
for (unsigned i = 0; i < COMPRESSION_CURVE_SIZE; ++i) {
float x = i / (float)(COMPRESSION_CURVE_SIZE - 1);
- if (x < 0.018f) {
+ if (x < beta) {
compression_curve[i] = 4.5f * x;
} else {
- compression_curve[i] = 1.099f * pow(x, 0.45f) - 0.099;
+ compression_curve[i] = alpha * pow(x, 0.45f) - (alpha - 1.0f);
}
}
invalidate_1d_texture("compression_curve_tex");
<< "No increase between " << i-1 << " and " << i;
}
}
+
+TEST(GammaCompressionEffectTest, Rec2020_12BitIsVeryCloseToRec709) {
+ float data[256];
+ for (unsigned i = 0; i < 256; ++i) {
+ data[i] = i / 255.0f;
+ }
+ float out_data_709[256];
+ float out_data_2020[256];
+
+ EffectChainTester tester(data, 256, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
+ tester.run(out_data_709, GL_RED, COLORSPACE_sRGB, GAMMA_REC_709);
+ EffectChainTester tester2(data, 256, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
+ tester2.run(out_data_2020, GL_RED, COLORSPACE_sRGB, GAMMA_REC_2020_12_BIT);
+
+ double sqdiff = 0.0;
+ for (unsigned i = 0; i < 256; ++i) {
+ EXPECT_NEAR(out_data_709[i], out_data_2020[i], 1e-3);
+ sqdiff += (out_data_709[i] - out_data_2020[i]) * (out_data_709[i] - out_data_2020[i]);
+ }
+ EXPECT_GT(sqdiff, 1e-6);
+}
invalidate_1d_texture("expansion_curve_tex");
return read_file("gamma_expansion_effect.frag");
}
- if (source_curve == GAMMA_REC_709) { // And Rec. 601.
+ if (source_curve == GAMMA_REC_709 || // Also includes Rec. 601, and 10-bit Rec. 2020.
+ source_curve == GAMMA_REC_2020_12_BIT) {
+ // Rec. 2020, page 3.
+ float alpha, beta;
+ if (source_curve == GAMMA_REC_2020_12_BIT) {
+ alpha = 1.0993f;
+ beta = 0.0181f;
+ } else {
+ alpha = 1.099f;
+ beta = 0.018f;
+ }
for (unsigned i = 0; i < EXPANSION_CURVE_SIZE; ++i) {
float x = i / (float)(EXPANSION_CURVE_SIZE - 1);
- if (x < 0.081f) {
+ if (x < beta * 4.5f) {
expansion_curve[i] = (1.0/4.5f) * x;
} else {
- expansion_curve[i] = pow((x + 0.099) * (1.0/1.099f), 1.0f/0.45f);
+ expansion_curve[i] = pow((x + (alpha - 1.0f)) / alpha, 1.0f/0.45f);
}
}
invalidate_1d_texture("expansion_curve_tex");
expect_equal(data, out_data, 5, 1);
}
+
+TEST(GammaExpansionEffectTest, Rec2020_12BitIsVeryCloseToRec709) {
+ float data[256];
+ for (unsigned i = 0; i < 256; ++i) {
+ data[i] = i / 255.0f;
+ }
+ float out_data_709[256];
+ float out_data_2020[256];
+
+ EffectChainTester tester(data, 256, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_REC_709);
+ tester.run(out_data_709, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
+ EffectChainTester tester2(data, 256, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_REC_2020_12_BIT);
+ tester2.run(out_data_2020, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
+
+ double sqdiff = 0.0;
+ for (unsigned i = 0; i < 256; ++i) {
+ EXPECT_NEAR(out_data_709[i], out_data_2020[i], 1e-3);
+ sqdiff += (out_data_709[i] - out_data_2020[i]) * (out_data_709[i] - out_data_2020[i]);
+ }
+ EXPECT_GT(sqdiff, 1e-6);
+}
#ifndef _MOVIT_IMAGE_FORMAT_H
#define _MOVIT_IMAGE_FORMAT_H 1
+// Note: Input depths above 8 bits have not been tested, so Rec. 2020
+// support should be regarded as somewhat untested (it assumes 10-
+// or 12-bit input). We also only support “conventional non-constant
+// luminance” for Rec. 2020, where Y' is derived from R'G'B' instead of
+// RGB, since this is the same system as used in Rec. 601 and 709.
+
enum MovitPixelFormat {
FORMAT_RGB,
FORMAT_RGBA_PREMULTIPLIED_ALPHA,
COLORSPACE_REC_601_525 = 1,
COLORSPACE_REC_601_625 = 2,
COLORSPACE_XYZ = 3, // Mostly useful for testing and debugging.
+ COLORSPACE_REC_2020 = 4,
};
enum GammaCurve {
GAMMA_sRGB = 1,
GAMMA_REC_601 = 2,
GAMMA_REC_709 = 2, // Same as Rec. 601.
+ GAMMA_REC_2020_10_BIT = 2, // Same as Rec. 601.
+ GAMMA_REC_2020_12_BIT = 3,
};
enum YCbCrLumaCoefficients {
YCBCR_REC_601 = 0,
YCBCR_REC_709 = 1,
+ YCBCR_REC_2020 = 2,
};
struct ImageFormat {
coeff[1] = 0.7152;
coeff[2] = 0.0722;
break;
+
+ case YCBCR_REC_2020:
+ // Rec. 2020, page 4.
+ coeff[0] = 0.2627;
+ coeff[1] = 0.6780;
+ coeff[2] = 0.0593;
+ break;
+
default:
assert(false);
}
scale[1] = 1.0;
scale[2] = 1.0;
} else {
- // Rec. 601, page 4; Rec. 709, page 19.
+ // Rec. 601, page 4; Rec. 709, page 19; Rec. 2020, page 4.
offset[0] = 16.0 / 255.0;
offset[1] = 128.0 / 255.0;
offset[2] = 128.0 / 255.0;
expect_equal(expected_data, out_data, 4 * width, height, 0.025, 0.002);
}
+TEST(YCbCrInput, Rec2020) {
+ const int width = 1;
+ const int height = 5;
+
+ // Pure-color test inputs, calculated with the formulas in Rec. 2020
+ // page 4, tables 4 and 5 (for conventional non-constant luminance).
+ // Note that we still use 8-bit inputs, even though Rec. 2020 is only
+ // defined for 10- and 12-bit.
+ unsigned char y[width * height] = {
+ 16, 235, 74, 164, 29,
+ };
+ unsigned char cb[width * height] = {
+ 128, 128, 97, 47, 240,
+ };
+ unsigned char cr[width * height] = {
+ 128, 128, 240, 25, 119,
+ };
+ float expected_data[4 * width * height] = {
+ 0.0, 0.0, 0.0, 1.0,
+ 1.0, 1.0, 1.0, 1.0,
+ 1.0, 0.0, 0.0, 1.0,
+ 0.0, 1.0, 0.0, 1.0,
+ 0.0, 0.0, 1.0, 1.0,
+ };
+ float out_data[4 * width * height];
+
+ EffectChainTester tester(NULL, width, height);
+
+ ImageFormat format;
+ format.color_space = COLORSPACE_sRGB;
+ format.gamma_curve = GAMMA_sRGB;
+
+ YCbCrFormat ycbcr_format;
+ ycbcr_format.luma_coefficients = YCBCR_REC_2020;
+ ycbcr_format.full_range = false;
+ ycbcr_format.chroma_subsampling_x = 1;
+ ycbcr_format.chroma_subsampling_y = 1;
+ ycbcr_format.cb_x_position = 0.5f;
+ ycbcr_format.cb_y_position = 0.5f;
+ ycbcr_format.cr_x_position = 0.5f;
+ ycbcr_format.cr_y_position = 0.5f;
+
+ YCbCrInput *input = new YCbCrInput(format, ycbcr_format, width, height);
+ input->set_pixel_data(0, y);
+ input->set_pixel_data(1, cb);
+ input->set_pixel_data(2, cr);
+ tester.get_chain()->add_input(input);
+
+ tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_sRGB);
+
+ // Y'CbCr isn't 100% accurate (the input values are rounded),
+ // so we need some leeway.
+ expect_equal(expected_data, out_data, 4 * width, height, 0.025, 0.002);
+}
+
TEST(YCbCrInput, Subsampling420) {
const int width = 4;
const int height = 4;