Add partial Rec. 2020 support.
authorSteinar H. Gunderson <sgunderson@bigfoot.com>
Mon, 30 Dec 2013 01:31:23 +0000 (02:31 +0100)
committerSteinar H. Gunderson <sgunderson@bigfoot.com>
Mon, 30 Dec 2013 01:31:23 +0000 (02:31 +0100)
colorspace_conversion_effect.cpp
colorspace_conversion_effect_test.cpp
gamma_compression_effect.cpp
gamma_compression_effect_test.cpp
gamma_expansion_effect.cpp
gamma_expansion_effect_test.cpp
image_format.h
ycbcr_input.cpp
ycbcr_input_test.cpp

index 0e24255..bb23afc 100644 (file)
@@ -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);
        }
index 76bcd49..18f66ef 100644 (file)
@@ -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,
index 0f0a2e4..5581fca 100644 (file)
@@ -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");
index c0b4ccf..e65acee 100644 (file)
@@ -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);
+}
index 4574567..5b5f44d 100644 (file)
@@ -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");
index 6fc0252..dc50f91 100644 (file)
@@ -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);
+}
index 3766235..df6f487 100644 (file)
@@ -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 {
index 4bc519c..d1d849a 100644 (file)
@@ -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;
index 15d7639..4ff263a 100644 (file)
@@ -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;