diff mbox series

[FFmpeg-devel,2/2,v2] lavfi/vf_colorspace: Add SMPTE ST 2084 support

Message ID 4f54a34f6b77734f83b812bc95e0124016b0236a.camel@haerdin.se
State New
Headers show
Series None | expand

Commit Message

Tomas Härdin Feb. 6, 2023, 2:53 p.m. UTC
Here's a version of this patch with options for specifying input and
output nits. 32-bit LUTs would make this situation better if
vf_colorspace tracked nits internally. 32-bit would only be necessary
when dealing with HDR. That way SDR colorspace conversion can still use
the 16-bit SIMD.

/Tomas
diff mbox series

Patch

From c63beb4581bcd3140e122b7a7c5b4a53ae35ec29 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tomas=20H=C3=A4rdin?= <git@haerdin.se>
Date: Fri, 3 Feb 2023 14:00:38 +0100
Subject: [PATCH 2/2] lavfi/vf_colorspace: Add SMPTE ST 2084 support
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Also adds inits and onits options for specifying what 28672
means in terms of nits (cd/m²).
---
 doc/filters.texi            | 30 ++++++++++++++++++++++++++++
 libavfilter/vf_colorspace.c | 39 +++++++++++++++++++++++++++++++++++--
 2 files changed, 67 insertions(+), 2 deletions(-)

diff --git a/doc/filters.texi b/doc/filters.texi
index 3a54c68f3e..26d9dfe54b 100644
--- a/doc/filters.texi
+++ b/doc/filters.texi
@@ -9839,6 +9839,10 @@  BT.2020 for 10-bits content
 @item bt2020-12
 BT.2020 for 12-bits content
 
+@anchor{smpte2084}
+@item smpte2084
+SMPTE ST 2084 (perceptual quantizer) used by BT.2100. See @ref{inits} and @ref{onits}.
+
 @end table
 
 @anchor{primaries}
@@ -9980,6 +9984,32 @@  Override input transfer characteristics. Same accepted values as @ref{trc}.
 @item irange
 Override input color range. Same accepted values as @ref{range}.
 
+@anchor{inits}
+@item inits
+Required when input trc is @ref{smpte2084}.
+Specifies what the peak intermediate level should correspond to in nits (cd/m²).
+Perceptually dequantized values get scaled and clipped so that the range [0,inits] fits in the filter's internal psuedo-restricted 15-bit format.
+For inits < 10000 this corresponds to clipping of values outside the [0,inits]³ RGB cube.
+For inits = 10000 values are not clipped, but values in the lower range will get truncated to zero due to the internal format's limited range.
+
+With BT.2100 input basic usage is:
+@example
+colorspace=bt709:inits=100
+@end example
+which clips HDR to SDR in BT.709 colorspace.
+
+A more advanced use case is as follows:
+@example
+colorspace=bt709:inits=10000:trc=linear:format=yuv444p16,tonemap=hable:peak=0.01
+@end example
+which with BT.2100 input performs very basic tonemapping.
+
+@anchor{onits}
+@item onits
+Used in combination with @ref{smpte2084}, defaults to 10000.
+Similar to @ref{inits} but in reverse.
+Specifies how to treat the peak intermediate level in nits (cd/m²) prior to perceptual quantization (PQ) as per SMPTE ST 2084.
+
 @end table
 
 The filter converts the transfer characteristics, color space and color
diff --git a/libavfilter/vf_colorspace.c b/libavfilter/vf_colorspace.c
index 1e1ab5fb34..996c5f4e25 100644
--- a/libavfilter/vf_colorspace.c
+++ b/libavfilter/vf_colorspace.c
@@ -118,6 +118,7 @@  typedef struct ColorSpaceContext {
     enum AVColorTransferCharacteristic in_trc, out_trc, user_trc, user_itrc;
     enum AVColorPrimaries in_prm, out_prm, user_prm, user_iprm;
     enum AVPixelFormat in_format, user_format;
+    double user_inits, user_onits;
     int fast_mode;
     enum DitherMode dither;
     enum WhitepointAdaptation wp_adapt;
@@ -173,6 +174,7 @@  static const struct TransferCharacteristics transfer_characteristics[AVCOL_TRC_N
     [AVCOL_TRC_IEC61966_2_4] = { 1.099, 0.018, 0.45, 4.5 },
     [AVCOL_TRC_BT2020_10] = { 1.099,  0.018,  0.45, 4.5 },
     [AVCOL_TRC_BT2020_12] = { 1.0993, 0.0181, 0.45, 4.5 },
+    [AVCOL_TRC_SMPTE2084] = { 1.0,    0,      0,    0 }, // fake entry, actual TRC uses entirely separate formula
 };
 
 static const struct TransferCharacteristics *
@@ -197,6 +199,9 @@  static int fill_gamma_table(ColorSpaceContext *s)
     double in_ialpha = 1.0 / in_alpha, in_igamma = 1.0 / in_gamma, in_idelta = 1.0 / in_delta;
     double out_alpha = s->out_txchr->alpha, out_beta = s->out_txchr->beta;
     double out_gamma = s->out_txchr->gamma, out_delta = s->out_txchr->delta;
+    double m1 = 1305.0/8192, m2 = 2523.0/32, c2 = 2413.0/128, c3 = 2392.0/128, c1 = c3 - c2 + 1;
+    double im1 = 1.0 / m1, im2 = 1.0 / m2;
+    double iscale = 10000.0 / s->user_inits, ioscale = s->user_onits / 10000.0;
 
     s->lin_lut = av_malloc(sizeof(*s->lin_lut) * 32768 * 2);
     if (!s->lin_lut)
@@ -206,7 +211,15 @@  static int fill_gamma_table(ColorSpaceContext *s)
         double v = (n - 2048.0) / 28672.0, d, l;
 
         // delinearize
-        if (v <= -out_beta) {
+        if (s->out_trc == AVCOL_TRC_SMPTE2084) {
+            // see BT.2100-2
+            if (v >= 0) {
+                double vm1 = pow(v * ioscale, m1);
+                d = pow((c1 + c2 * vm1)/(1 + c3 * vm1), m2);
+            } else {
+                d = 0;
+            }
+        } else if (v <= -out_beta) {
             d = -out_alpha * pow(-v, out_gamma) + (out_alpha - 1.0);
         } else if (v < out_beta) {
             d = out_delta * v;
@@ -216,7 +229,19 @@  static int fill_gamma_table(ColorSpaceContext *s)
         s->delin_lut[n] = av_clip_int16(lrint(d * 28672.0));
 
         // linearize
-        if (v <= -in_beta * in_delta) {
+        if (s->in_trc == AVCOL_TRC_SMPTE2084) {
+            // see BT.2100-2
+            if (v >= 0) {
+                double vim2 = pow(v, im2);
+                // for inits < 10000 this will tonemap by clipping in RGB
+                // for inits = 10000 this makes unclipped linear values accessible when trc=linear
+                // note that precision will be lost in the lower end of PQ values, for example with
+                // 12-bit PQ and 16-bit output the first 422 values all get truncated to zero
+                l = iscale * pow((vim2 - c1 > 0 ? vim2 - c1 : 0) / (c2 - c3 * vim2), im1);
+            } else {
+                l = 0;
+            }
+        } else if (v <= -in_beta * in_delta) {
             l = -pow((1.0 - in_alpha - v) * in_ialpha, in_igamma);
         } else if (v < in_beta * in_delta) {
             l = v * in_idelta;
@@ -514,6 +539,11 @@  static int create_filtergraph(AVFilterContext *ctx,
         }
     }
 
+    if (s->in_trc == AVCOL_TRC_SMPTE2084 && s->user_inits <= 0) {
+        av_log(ctx, AV_LOG_ERROR, "smpte2084 requires inits\n");
+        return AVERROR(EINVAL);
+    }
+
     if (!s->out_txchr) {
         av_freep(&s->lin_lut);
         s->out_trc = out->color_trc;
@@ -956,6 +986,7 @@  static const AVOption colorspace_options[] = {
     ENUM("iec61966-2-4", AVCOL_TRC_IEC61966_2_4, "trc"),
     ENUM("bt2020-10",    AVCOL_TRC_BT2020_10,    "trc"),
     ENUM("bt2020-12",    AVCOL_TRC_BT2020_12,    "trc"),
+    ENUM("smpte2084",    AVCOL_TRC_SMPTE2084,    "trc"),
 
     { "format",   "Output pixel format",
       OFFSET(user_format), AV_OPT_TYPE_INT,  { .i64 = AV_PIX_FMT_NONE },
@@ -1008,6 +1039,10 @@  static const AVOption colorspace_options[] = {
     { "itrc",       "Input transfer characteristics",
       OFFSET(user_itrc),  AV_OPT_TYPE_INT, { .i64 = AVCOL_TRC_UNSPECIFIED },
       AVCOL_TRC_RESERVED0, AVCOL_TRC_NB - 1, FLAGS, "trc" },
+    { "inits",      "Input nits. Specifies how PQ (SMPTE ST 2084) values are to be mapped to intermediate/output linear RGB",
+      OFFSET(user_inits), AV_OPT_TYPE_DOUBLE, {.dbl = 0}, 0, 10000, FLAGS },
+    { "onits",      "Output nits. Specifies how input/intermediate linear RGB is scaled for output PQ (SMPTE ST 2084) values",
+      OFFSET(user_onits), AV_OPT_TYPE_DOUBLE, {.dbl = 10000}, 1, 10000, FLAGS },
 
     { NULL }
 };
-- 
2.30.2