PR #25423 opened by Kacper Michajłow (kasper93)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25423
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25423.patch


From 492b44eb3f6a5075ba85be17597fd9248afb2a7b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]>
Date: Sat, 24 Oct 2026 04:06:38 +0200
Subject: [PATCH 1/3] avfilter/colorspace: derive the RGB to XYZ matrix without
 dividing by y
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The matrix columns were the primaries scaled to Y = 1, x/y and
(1 - x - y)/y, and were then rescaled to fit the white point, so the
division by y cancels out. Use the xyz chromaticity vectors directly,
which gives the same matrix and also works for a primary with y = 0,
such as the Z axis of the CIE 1931 XYZ.

Signed-off-by: Kacper Michajłow <[email protected]>
---
 libavfilter/colorspace.c | 16 +++++++++-------
 1 file changed, 9 insertions(+), 7 deletions(-)

diff --git a/libavfilter/colorspace.c b/libavfilter/colorspace.c
index f0bd14be18a..2655d96363d 100644
--- a/libavfilter/colorspace.c
+++ b/libavfilter/colorspace.c
@@ -86,13 +86,15 @@ void ff_fill_rgb2xyz_table(const AVPrimaryCoefficients 
*coeffs,
     double xb = av_q2d(coeffs->b.x), yb = av_q2d(coeffs->b.y);
     double xw = av_q2d(wp->x), yw = av_q2d(wp->y);
 
-    rgb2xyz[0][0] = xr / yr;
-    rgb2xyz[0][1] = xg / yg;
-    rgb2xyz[0][2] = xb / yb;
-    rgb2xyz[1][0] = rgb2xyz[1][1] = rgb2xyz[1][2] = 1.0;
-    rgb2xyz[2][0] = (1.0 - xr - yr) / yr;
-    rgb2xyz[2][1] = (1.0 - xg - yg) / yg;
-    rgb2xyz[2][2] = (1.0 - xb - yb) / yb;
+    rgb2xyz[0][0] = xr;
+    rgb2xyz[0][1] = xg;
+    rgb2xyz[0][2] = xb;
+    rgb2xyz[1][0] = yr;
+    rgb2xyz[1][1] = yg;
+    rgb2xyz[1][2] = yb;
+    rgb2xyz[2][0] = 1.0 - xr - yr;
+    rgb2xyz[2][1] = 1.0 - xg - yg;
+    rgb2xyz[2][2] = 1.0 - xb - yb;
     ff_matrix_invert_3x3(rgb2xyz, i);
     zw = 1.0 - xw - yw;
     sr = i[0][0] * xw + i[0][1] * yw + i[0][2] * zw;
-- 
2.54.0


From 334e3d69e62e37b8c9ca09ddabba2a28402e244a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]>
Date: Sat, 24 Oct 2026 04:06:39 +0200
Subject: [PATCH 2/3] swscale/csputils: derive the RGB to XYZ matrix without
 dividing by y
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

ff_sws_rgb2xyz built its columns from X/Y and Z/Y of each primary and
then solved for the scale that fits the white point, so the division by
y cancels out, while av_div_q by y = 0 turned the matrix into infinities
and NaNs. Use the xyz chromaticity vectors directly, which gives the
same matrix and also works for a primary with y = 0, such as the Z axis
of the CIE 1931 XYZ.

Signed-off-by: Kacper Michajłow <[email protected]>
---
 libswscale/csputils.c | 25 ++++++++++++++-----------
 1 file changed, 14 insertions(+), 11 deletions(-)

diff --git a/libswscale/csputils.c b/libswscale/csputils.c
index 6ab6ac3f7a8..1c2c96da277 100644
--- a/libswscale/csputils.c
+++ b/libswscale/csputils.c
@@ -92,23 +92,26 @@ static inline AVRational cie_Z(AVCIExy xy)
 SwsMatrix3x3 ff_sws_rgb2xyz(const AVColorPrimariesDesc *desc)
 {
     SwsMatrix3x3 out = {{{0}}};
-    float S[3], X[3], Z[3], Xw, Zw;
+    float S[3], X[3], Y[3], Z[3], Xw, Zw;
 
-    X[0] = av_q2d(cie_X(desc->prim.r));
-    X[1] = av_q2d(cie_X(desc->prim.g));
-    X[2] = av_q2d(cie_X(desc->prim.b));
+    X[0] = av_q2d(desc->prim.r.x);
+    X[1] = av_q2d(desc->prim.g.x);
+    X[2] = av_q2d(desc->prim.b.x);
 
-    Z[0] = av_q2d(cie_Z(desc->prim.r));
-    Z[1] = av_q2d(cie_Z(desc->prim.g));
-    Z[2] = av_q2d(cie_Z(desc->prim.b));
+    Y[0] = av_q2d(desc->prim.r.y);
+    Y[1] = av_q2d(desc->prim.g.y);
+    Y[2] = av_q2d(desc->prim.b.y);
+
+    for (int i = 0; i < 3; i++)
+        Z[i] = 1.0f - X[i] - Y[i];
 
     Xw = av_q2d(cie_X(desc->wp));
     Zw = av_q2d(cie_Z(desc->wp));
 
-    /* S = XYZ^-1 * W */
+    /* S = xyz^-1 * W */
     for (int i = 0; i < 3; i++) {
         out.m[0][i] = X[i];
-        out.m[1][i] = 1.0f;
+        out.m[1][i] = Y[i];
         out.m[2][i] = Z[i];
     }
 
@@ -117,10 +120,10 @@ SwsMatrix3x3 ff_sws_rgb2xyz(const AVColorPrimariesDesc 
*desc)
     for (int i = 0; i < 3; i++)
         S[i] = out.m[i][0] * Xw + out.m[i][1] + out.m[i][2] * Zw;
 
-    /* M = [Sc * XYZc] */
+    /* M = [Sc * xyzc] */
     for (int i = 0; i < 3; i++) {
         out.m[0][i] = S[i] * X[i];
-        out.m[1][i] = S[i];
+        out.m[1][i] = S[i] * Y[i];
         out.m[2][i] = S[i] * Z[i];
     }
 
-- 
2.54.0


From ef97881c33f083b646ff83e17172e661b4f8b3b8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]>
Date: Sat, 24 Oct 2026 05:05:34 +0200
Subject: [PATCH 3/3] avutil/csp: describe SMPTE ST 428-1 primaries as CIE 1931
 XYZ
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

H.273 value 10, SMPTE ST 428-1 (CIE 1931 XYZ), lists red (X) at (1, 0),
green (Y) at (0, 1), blue (Z) at (0, 0) and the centre white at
(1/3, 1/3).

The table had the monochromatic primaries of the CIE 1931 RGB space
instead.

Note that ST 428-1 only defines the encoding of the XYZ tristimulus
values and the transfer function. XYZ is absolute, so nothing in it
names the white the content was mastered to, and real DCDM content does
not have an equal-energy white. The standard reserves its headroom for
white points such as D55, D61 and D65, and the reference projector of RP
431-2 has DCI white. However, H.273 describes both the primaries (10)
(they shouldn't have) and the transfer (17) of such content, so follow
it here. For decoding, the mastering primaries can be given in place of
value 10 (e.g. AVCOL_PRI_SMPTE431 for a DCDM) with the ST 428-1 transfer
kept.

Signed-off-by: Kacper Michajłow <[email protected]>
---
 libavutil/csp.c    | 2 +-
 tests/ref/fate/csp | 6 +++---
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/libavutil/csp.c b/libavutil/csp.c
index 8ff53cff5fa..7ebbcc58560 100644
--- a/libavutil/csp.c
+++ b/libavutil/csp.c
@@ -79,7 +79,7 @@ static const AVColorPrimariesDesc 
color_primaries[AVCOL_PRI_NB] = {
     [AVCOL_PRI_BT470BG]   = { WP_D65, { { AVR(0.640), AVR(0.330) }, { 
AVR(0.290), AVR(0.600) }, { AVR(0.150), AVR(0.060) } } },
     [AVCOL_PRI_SMPTE170M] = { WP_D65, { { AVR(0.630), AVR(0.340) }, { 
AVR(0.310), AVR(0.595) }, { AVR(0.155), AVR(0.070) } } },
     [AVCOL_PRI_SMPTE240M] = { WP_D65, { { AVR(0.630), AVR(0.340) }, { 
AVR(0.310), AVR(0.595) }, { AVR(0.155), AVR(0.070) } } },
-    [AVCOL_PRI_SMPTE428]  = { WP_E,   { { AVR(0.735), AVR(0.265) }, { 
AVR(0.274), AVR(0.718) }, { AVR(0.167), AVR(0.009) } } },
+    [AVCOL_PRI_SMPTE428]  = { WP_E,   { { AVR(1.000), AVR(0.000) }, { 
AVR(0.000), AVR(1.000) }, { AVR(0.000), AVR(0.000) } } },
     [AVCOL_PRI_SMPTE431]  = { WP_DCI, { { AVR(0.680), AVR(0.320) }, { 
AVR(0.265), AVR(0.690) }, { AVR(0.150), AVR(0.060) } } },
     [AVCOL_PRI_SMPTE432]  = { WP_D65, { { AVR(0.680), AVR(0.320) }, { 
AVR(0.265), AVR(0.690) }, { AVR(0.150), AVR(0.060) } } },
     [AVCOL_PRI_FILM]      = { WP_C,   { { AVR(0.681), AVR(0.319) }, { 
AVR(0.243), AVR(0.692) }, { AVR(0.145), AVR(0.049) } } },
diff --git a/tests/ref/fate/csp b/tests/ref/fate/csp
index bc36bc66bac..ff04a5f8492 100644
--- a/tests/ref/fate/csp
+++ b/tests/ref/fate/csp
@@ -67,9 +67,9 @@ prm=bt2020           ->
   round-trip id=bt2020           desc=OK
 prm=smpte428         ->
   wp =(1/3, 1/3)
-  r  =(73500/100000, 26500/100000)
-  g  =(27400/100000, 71800/100000)
-  b  =(16700/100000, 900/100000)
+  r  =(100000/100000, 0/100000)
+  g  =(0/100000, 100000/100000)
+  b  =(0/100000, 0/100000)
   round-trip id=smpte428         desc=OK
 prm=smpte431         ->
   wp =(31400/100000, 35100/100000)
-- 
2.54.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to