From fbd35caef830a1d0a25f1ee6e3b5d71453c98a59 Mon Sep 17 00:00:00 2001 From: Niklas Haas Date: Tue, 1 Apr 2014 00:17:07 +0200 Subject: [PATCH] video: Refactor rendering intent for DCP XYZ and :srgb Notably, we now conform to SMPTE 428-1-2006 when decoding XYZ12 input, and we can support rendering intents other than colorimetric when converting between BT.709 and BT.2020, like with :srgb or :icc-profile. --- DOCS/man/vo.rst | 4 ++ video/csputils.c | 130 +++++++++++++++++++++++++------------------ video/csputils.h | 16 +++++- video/out/gl_video.c | 11 +++- 4 files changed, 102 insertions(+), 59 deletions(-) diff --git a/DOCS/man/vo.rst b/DOCS/man/vo.rst index dc4f49097c..587cdd2afd 100644 --- a/DOCS/man/vo.rst +++ b/DOCS/man/vo.rst @@ -505,6 +505,10 @@ Available video output drivers are: Its size depends on the ``3dlut-size``, and can be very big. ``icc-intent=`` + Specifies the ICC Intent used for transformations between colorspaces. + This affects the rendering when using ``icc-profile`` or ``srgb`` and + also affects the way DCP XYZ content gets converted to RGB. + 0 perceptual 1 diff --git a/video/csputils.c b/video/csputils.c index bd56a1e29f..202585187a 100644 --- a/video/csputils.c +++ b/video/csputils.c @@ -342,60 +342,81 @@ void mp_get_rgb2xyz_matrix(struct mp_csp_primaries space, float m[3][3]) } } +// M := M * XYZd<-XYZs +void mp_apply_chromatic_adaptation(struct mp_csp_col_xy src, struct mp_csp_col_xy dest, float m[3][3]) +{ + // If the white points are nearly identical, this is a wasteful identity + // operation. + if (fabs(src.x - dest.x) < 1e-6 && fabs(src.y - dest.y) < 1e-6) + return; + + // XYZd<-XYZs = Ma^-1 * (I*[Cd/Cs]) * Ma + // http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html + float C[3][2], tmp[3][3] = {{0}}; + + // Ma = Bradford matrix, arguably most popular method in use today. + // This is derived experimentally and thus hard-coded. + float bradford[3][3] = { + { 0.8951, 0.2664, -0.1614 }, + { -0.7502, 1.7135, 0.0367 }, + { 0.0389, -0.0685, 1.0296 }, + }; + + for (int i = 0; i < 3; i++) { + // source cone + C[i][0] = bradford[i][0] * src.x / src.y + + bradford[i][1] * 1 + + bradford[i][2] * (1 - src.x - src.y) / src.y; + + // dest cone + C[i][1] = bradford[i][0] * dest.x / dest.y + + bradford[i][1] * 1 + + bradford[i][2] * (1 - dest.x - dest.y) / dest.y; + } + + // tmp := I * [Cd/Cs] * Ma + for (int i = 0; i < 3; i++) + tmp[i][i] = C[i][1] / C[i][0]; + + mp_mul_matrix3x3(tmp, bradford); + + // M := M * Ma^-1 * tmp + mp_invert_matrix3x3(bradford); + mp_mul_matrix3x3(m, bradford); + mp_mul_matrix3x3(m, tmp); +} + /** * \brief get the coefficients of the source -> bt2020 cms matrix * \param src primaries of the source gamut * \param dest primaries of the destination gamut + * \param intent rendering intent for the transformation * \param m array to store coefficients into */ -void mp_get_cms_matrix(struct mp_csp_primaries src, struct mp_csp_primaries dest, float m[3][3]) +void mp_get_cms_matrix(struct mp_csp_primaries src, struct mp_csp_primaries dest, enum mp_render_intent intent, float m[3][3]) { + float tmp[3][3]; + + // In saturation mapping, we don't care about accuracy and just want + // primaries to map to primaries, making this an identity transformation. + if (intent == MP_INTENT_SATURATION) { + for (int i = 0; i < 3; i++) + m[i][i] = 1; + return; + } + // RGBd<-RGBs = RGBd<-XYZd * XYZd<-XYZs * XYZs<-RGBs // Equations from: http://www.brucelindbloom.com/index.html?Math.html - float tmp[3][3] = {{0}}; + // Note: Perceptual is treated like relative colorimetric. There's no + // definition for perceptual other than "make it look good". // RGBd<-XYZd, inverted from XYZd<-RGBd mp_get_rgb2xyz_matrix(dest, m); mp_invert_matrix3x3(m); - // Chromatic adaptation, only needed if the white point differs - if (fabs(src.white.x - dest.white.x) > 1e-6 || - fabs(src.white.y - dest.white.y) > 1e-6) { - // XYZd<-XYZs = Ma^-1 * (I*[Cd/Cs]) * Ma - // http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html - float C[3][2]; - - // Ma = Bradford matrix, arguably most popular method in use today. - // This is derived experimentally and thus hard-coded. - float bradford[3][3] = { - { 0.8951, 0.2664, -0.1614 }, - { -0.7502, 1.7135, 0.0367 }, - { 0.0389, -0.0685, 1.0296 }, - }; - - for (int i = 0; i < 3; i++) { - // source cone - C[i][0] = bradford[i][0] * src.white.x / src.white.y - + bradford[i][1] * 1 - + bradford[i][2] * (1 - src.white.x - src.white.y) / src.white.y; - - // dest cone - C[i][1] = bradford[i][0] * dest.white.x / dest.white.y - + bradford[i][1] * 1 - + bradford[i][2] * (1 - dest.white.x - dest.white.y) / dest.white.y; - } - - // tmp := I * [Cd/Cs] * Ma - for (int i = 0; i < 3; i++) - tmp[i][i] = C[i][1] / C[i][0]; - - mp_mul_matrix3x3(tmp, bradford); - - // M := M * Ma^-1 * tmp - mp_invert_matrix3x3(bradford); - mp_mul_matrix3x3(m, bradford); - mp_mul_matrix3x3(m, tmp); - } + // Chromatic adaptation, except in absolute colorimetric intent + if (intent != MP_INTENT_ABSOLUTE_COLORIMETRIC) + mp_apply_chromatic_adaptation(src.white, dest.white, m); // XYZs<-RGBs mp_get_rgb2xyz_matrix(src, tmp); @@ -436,17 +457,25 @@ static void luma_coeffs(float m[3][4], float lr, float lg, float lb) } /** - * \brief get the coefficients of an xyz -> rgb conversion matrix + * \brief get the coefficients of an SMPTE 428-1 xyz -> rgb conversion matrix * \param params parameters for the conversion, only brightness is used * \param prim primaries of the RGB space to transform to + * \param intent the rendering intent used to convert to the target primaries * \param m array to store the coefficients into */ -void mp_get_xyz2rgb_coeffs(struct mp_csp_params *params, struct mp_csp_primaries prim, float m[3][4]) +void mp_get_xyz2rgb_coeffs(struct mp_csp_params *params, struct mp_csp_primaries prim, enum mp_render_intent intent, float m[3][4]) { float tmp[3][3], brightness = params->brightness; mp_get_rgb2xyz_matrix(prim, tmp); mp_invert_matrix3x3(tmp); + // All non-absolute mappings want to map source white to target white + if (intent != MP_INTENT_ABSOLUTE_COLORIMETRIC) { + // SMPTE 428-1 defines the calibration white point as CIE xy (0.314, 0.351) + static const struct mp_csp_col_xy smpte428 = {0.314, 0.351}; + mp_apply_chromatic_adaptation(smpte428, prim.white, tmp); + } + // Since this outputs linear RGB rather than companded RGB, we // want to linearize any brightness additions. 2 is a reasonable // approximation for any sort of gamma function that could be in use. @@ -502,19 +531,10 @@ void mp_get_yuv2rgb_coeffs(struct mp_csp_params *params, float m[3][4]) } case MP_CSP_XYZ: { // The vo should probably not be using a matrix generated by this - // function for XYZ sources, but if it does, let's just assume we - // want BT.709. - float xyz_to_rgb[3][3]; - mp_get_rgb2xyz_matrix(mp_get_csp_primaries(MP_CSP_PRIM_BT_709), xyz_to_rgb); - mp_invert_matrix3x3(xyz_to_rgb); - - for (int i = 0; i < 3; i++) { - for (int j = 0; j < 3; j++) - m[i][j] = xyz_to_rgb[i][j]; - - m[i][3] = 0; - } - + // function for XYZ sources, but if it does, let's just assume it + // wants BT.709 with D65 white point (virtually all other content). + mp_get_xyz2rgb_coeffs(params, mp_get_csp_primaries(MP_CSP_PRIM_BT_709), + MP_INTENT_RELATIVE_COLORIMETRIC, m); levels_in = -1; break; } diff --git a/video/csputils.h b/video/csputils.h index f2e96beb3d..9257102df5 100644 --- a/video/csputils.h +++ b/video/csputils.h @@ -70,6 +70,15 @@ enum mp_csp_prim { // Any enum mp_csp_prim value is a valid index (except MP_CSP_PRIM_COUNT) extern const char *const mp_csp_prim_names[MP_CSP_PRIM_COUNT]; +// These constants are based on the ICC specification (Table 23) and match +// up with the API of LittleCMS, which treats them as integers. +enum mp_render_intent { + MP_INTENT_PERCEPTUAL = 0, + MP_INTENT_RELATIVE_COLORIMETRIC = 1, + MP_INTENT_SATURATION = 2, + MP_INTENT_ABSOLUTE_COLORIMETRIC = 3 +}; + struct mp_csp_details { enum mp_csp format; enum mp_csp_levels levels_in; // encoded video @@ -187,10 +196,13 @@ void mp_gen_gamma_map(unsigned char *map, int size, float gamma); #define COL_C 3 struct mp_csp_primaries mp_get_csp_primaries(enum mp_csp_prim csp); -void mp_get_cms_matrix(struct mp_csp_primaries src, struct mp_csp_primaries dest, float cms_matrix[3][3]); +void mp_apply_chromatic_adaptation(struct mp_csp_col_xy src, struct mp_csp_col_xy dest, float m[3][3]); +void mp_get_cms_matrix(struct mp_csp_primaries src, struct mp_csp_primaries dest, + enum mp_render_intent intent, float cms_matrix[3][3]); void mp_get_rgb2xyz_matrix(struct mp_csp_primaries space, float m[3][3]); -void mp_get_xyz2rgb_coeffs(struct mp_csp_params *params, struct mp_csp_primaries prim, float xyz2rgb[3][4]); +void mp_get_xyz2rgb_coeffs(struct mp_csp_params *params, struct mp_csp_primaries prim, + enum mp_render_intent intent, float xyz2rgb[3][4]); void mp_get_yuv2rgb_coeffs(struct mp_csp_params *params, float yuv2rgb[3][4]); void mp_gen_yuv2rgb_map(struct mp_csp_params *params, uint8_t *map, int size); diff --git a/video/out/gl_video.c b/video/out/gl_video.c index 989c1792db..52ad1a66f4 100644 --- a/video/out/gl_video.c +++ b/video/out/gl_video.c @@ -577,7 +577,11 @@ static void update_uniforms(struct gl_video *p, GLuint program) if (loc >= 0) { float m[3][4] = {{0}}; if (p->image_desc.flags & MP_IMGFLAG_XYZ) { - mp_get_xyz2rgb_coeffs(&cparams, p->csp_src, m); + // Hard-coded as relative colorimetric for now, since this transforms + // from the source file's D55 material to whatever color space our + // projector/display lives in, which should be D55 for a proper + // home cinema setup either way. + mp_get_xyz2rgb_coeffs(&cparams, p->csp_src, MP_INTENT_RELATIVE_COLORIMETRIC, m); } else { mp_get_yuv2rgb_coeffs(&cparams, m); } @@ -649,7 +653,10 @@ static void update_uniforms(struct gl_video *p, GLuint program) loc = gl->GetUniformLocation(program, "cms_matrix"); if (loc >= 0) { float cms_matrix[3][3] = {{0}}; - mp_get_cms_matrix(p->csp_src, p->csp_dest, cms_matrix); + // Hard-coded to relative colorimetric - for a BT.2020 3DLUT we expect + // the input to be actual BT.2020 and not something red- or blueshifted, + // and for sRGB monitors we most likely want relative scaling either way. + mp_get_cms_matrix(p->csp_src, p->csp_dest, MP_INTENT_RELATIVE_COLORIMETRIC, cms_matrix); gl->UniformMatrix3fv(loc, 1, GL_TRUE, &cms_matrix[0][0]); }