From 9b95345e8e1dde29991638ed69def0cf187e28de Mon Sep 17 00:00:00 2001 From: "Steinar H. Gunderson" Date: Mon, 30 Dec 2013 02:31:23 +0100 Subject: [PATCH] Add partial Rec. 2020 support. --- colorspace_conversion_effect.cpp | 8 ++++ colorspace_conversion_effect_test.cpp | 50 ++++++++++++++++++++++++ gamma_compression_effect.cpp | 16 ++++++-- gamma_compression_effect_test.cpp | 21 ++++++++++ gamma_expansion_effect.cpp | 16 ++++++-- gamma_expansion_effect_test.cpp | 21 ++++++++++ image_format.h | 10 +++++ ycbcr_input.cpp | 10 ++++- ycbcr_input_test.cpp | 55 +++++++++++++++++++++++++++ 9 files changed, 200 insertions(+), 7 deletions(-) diff --git a/colorspace_conversion_effect.cpp b/colorspace_conversion_effect.cpp index 0e24255..bb23afc 100644 --- a/colorspace_conversion_effect.cpp +++ b/colorspace_conversion_effect.cpp @@ -18,6 +18,10 @@ static const double rec601_525_y_R = 0.340, rec601_525_y_G = 0.595, rec601_525_y 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) @@ -48,6 +52,10 @@ Matrix3d get_xyz_matrix(Colorspace space) 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); } diff --git a/colorspace_conversion_effect_test.cpp b/colorspace_conversion_effect_test.cpp index 76bcd49..18f66ef 100644 --- a/colorspace_conversion_effect_test.cpp +++ b/colorspace_conversion_effect_test.cpp @@ -187,6 +187,56 @@ TEST(ColorspaceConversionEffectTest, Rec601_625_Primaries) { 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, diff --git a/gamma_compression_effect.cpp b/gamma_compression_effect.cpp index 0f0a2e4..5581fca 100644 --- a/gamma_compression_effect.cpp +++ b/gamma_compression_effect.cpp @@ -30,13 +30,23 @@ std::string GammaCompressionEffect::output_fragment_shader() 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"); diff --git a/gamma_compression_effect_test.cpp b/gamma_compression_effect_test.cpp index c0b4ccf..e65acee 100644 --- a/gamma_compression_effect_test.cpp +++ b/gamma_compression_effect_test.cpp @@ -67,3 +67,24 @@ TEST(GammaCompressionEffectTest, Rec709_RampAlwaysIncreases) { << "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); +} diff --git a/gamma_expansion_effect.cpp b/gamma_expansion_effect.cpp index 4574567..5b5f44d 100644 --- a/gamma_expansion_effect.cpp +++ b/gamma_expansion_effect.cpp @@ -29,13 +29,23 @@ std::string GammaExpansionEffect::output_fragment_shader() 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"); diff --git a/gamma_expansion_effect_test.cpp b/gamma_expansion_effect_test.cpp index 6fc0252..dc50f91 100644 --- a/gamma_expansion_effect_test.cpp +++ b/gamma_expansion_effect_test.cpp @@ -95,3 +95,24 @@ TEST(GammaExpansionEffectTest, Rec709_AlphaIsUnchanged) { 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); +} diff --git a/image_format.h b/image_format.h index 3766235..df6f487 100644 --- a/image_format.h +++ b/image_format.h @@ -1,6 +1,12 @@ #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, @@ -18,6 +24,7 @@ enum Colorspace { 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 { @@ -26,11 +33,14 @@ 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 { diff --git a/ycbcr_input.cpp b/ycbcr_input.cpp index 4bc519c..d1d849a 100644 --- a/ycbcr_input.cpp +++ b/ycbcr_input.cpp @@ -202,6 +202,14 @@ std::string YCbCrInput::output_fragment_shader() 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); } @@ -215,7 +223,7 @@ std::string YCbCrInput::output_fragment_shader() 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; diff --git a/ycbcr_input_test.cpp b/ycbcr_input_test.cpp index 15d7639..4ff263a 100644 --- a/ycbcr_input_test.cpp +++ b/ycbcr_input_test.cpp @@ -168,6 +168,61 @@ TEST(YCbCrInput, Rec709) { 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; -- 2.39.2