This is an automated email from the git hooks/post-receive script. Git pushed a commit to branch master in repository ffmpeg.
commit 931db4d37759e1914861a78667d8a47955a0a6ff Author: Niklas Haas <[email protected]> AuthorDate: Wed Aug 19 13:28:47 2026 +0200 Commit: Niklas Haas <[email protected]> CommitDate: Wed Sep 30 14:37:59 2026 +0200 avfilter/vf_gainmap: add filter to compute a gain map This is a mostly straightforward translation of the logic from ISO 21496. Implementation notes: - Slice threading is used to vastly speed up processing - We quantize the EOTF to a LUT if possible; this doesn't work for HLG becaus HLG embeds a channel-dependent OOTF. It's possible we could implement a custom HLG function here in the future, but for now, simply calling out to the EOTF function is a reasonable fallback. - Similar to libultrahdr, we use a two-pass approach when the gain range is unknown a priori, and fold the quantization step into a single pass if all parameters are specified up-front. - Unlike libultrahdr, we make an effort to also pick an optimal gamma to tune the quantized encoding. Users can always disable this by specifying `:gamma=1.0` explicitly. - This filter is bidirectional and can also compute an HDR->SDR gainmap, so the naming is left deliberately abstract internally. - We need to be careful to avoid platform-specific floating point differences. This is accomplished by quantizing the floating point values to a fixed point AVRational representation rather than using av_d2q(). Signed-off-by: Niklas Haas <[email protected]> --- doc/filters.texi | 74 ++++ libavfilter/Makefile | 1 + libavfilter/allfilters.c | 1 + libavfilter/vf_gainmap.c | 774 +++++++++++++++++++++++++++++++++++ tests/fate/filter-video.mak | 8 + tests/ref/fate/filter-gainmap | 6 + tests/ref/fate/filter-gainmap-fixed | 6 + tests/ref/fate/filter-gainmap-luma | 6 + tests/ref/fate/filter-gainmap-maxrgb | 6 + 9 files changed, 882 insertions(+) diff --git a/doc/filters.texi b/doc/filters.texi index 24d52a3a01..e8c8b23481 100644 --- a/doc/filters.texi +++ b/doc/filters.texi @@ -15230,6 +15230,80 @@ sort -n MAP_FILE ffmpeg -i INPUT -i OUTPUT -filter_complex '[0:v]fsync=file=MAP_FILE[ref];[1:v][ref]ssim' -f null - @end example +@section gainmap + +Compute an HDR gain map from two renditions of the same image, as defined by +ISO 21496-1. + +The first input is the base rendition (typically SDR), and the second input +is the alternate rendition (typically HDR). Both must have the same +dimensions. Outputs a gain map, as either @samp{gbrpf32} or @samp{grayf32} +depending on the chosen @option{mode}. + +@table @option +@item mode +Controls what quantity the gain map is computed against. + +The accepted values are: +@table @samp +@item rgb +One gain map channel per component. + +@item luma +A single gain map channel, computed from the luma. + +@item maxrgb +A single gain map channel, computed from @samp{max(R, G, B)}. +@end table + +Default is @samp{rgb}. + +@item colorspace +Chooses which colorspace to compute the gain map in. + +The accepted values are: +@table @samp +@item base +The base rendition's colorspace. + +@item alternate +The alternate rendition's colorspace. +@end table + +Default is @samp{base}. + +@item min +@item max +Bounds on the encoded gain (log2). If unspecified, these will be +measured from the frame. + +@item gamma +Encoding gamma of the resulting gain map. If unspecified, this will be +optimized for the frame in question. Setting this to @code{1.0} will produce +a linear gain map, which may be more efficient in some applications. + +@item base_offset +@item alt_offset +Offset to prevent numerical instability near zero. Both default to @code{1/64}. + +@item base_nits +@item alt_nits +Override the reference luminance of the input, in nits. If unspecified, uses +the values from the input metadata. +@end table + +The @code{gainmap} filter also supports the @ref{framesync} options. + +@subsection Examples + +@itemize +@item +Compute a gain map between an SDR and an HDR version of the same image: +@example +ffmpeg -i $sdr -i $hdr -filter_complex "[0:v][1:v]gainmap" $out +@end example +@end itemize + @section gblur Apply Gaussian blur filter. diff --git a/libavfilter/Makefile b/libavfilter/Makefile index ceea328e67..ac194bead7 100644 --- a/libavfilter/Makefile +++ b/libavfilter/Makefile @@ -333,6 +333,7 @@ OBJS-$(CONFIG_FREI0R_FILTER) += vf_frei0r.o OBJS-$(CONFIG_FRUC_VULKAN_FILTER) += vf_fruc_vulkan.o vulkan.o vulkan_filter.o OBJS-$(CONFIG_FSPP_FILTER) += vf_fspp.o vf_fsppdsp.o qp_table.o OBJS-$(CONFIG_FSYNC_FILTER) += vf_fsync.o +OBJS-$(CONFIG_GAINMAP_FILTER) += vf_gainmap.o colorspace.o framesync.o OBJS-$(CONFIG_GBLUR_FILTER) += vf_gblur.o OBJS-$(CONFIG_GBLUR_VULKAN_FILTER) += vf_gblur_vulkan.o vulkan.o vulkan_filter.o OBJS-$(CONFIG_GEQ_FILTER) += vf_geq.o diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c index 3b93fe12f6..931ac8481e 100644 --- a/libavfilter/allfilters.c +++ b/libavfilter/allfilters.c @@ -307,6 +307,7 @@ extern const FFFilter ff_vf_frei0r; extern const FFFilter ff_vf_fruc_vulkan; extern const FFFilter ff_vf_fspp; extern const FFFilter ff_vf_fsync; +extern const FFFilter ff_vf_gainmap; extern const FFFilter ff_vf_gblur; extern const FFFilter ff_vf_gblur_vulkan; extern const FFFilter ff_vf_geq; diff --git a/libavfilter/vf_gainmap.c b/libavfilter/vf_gainmap.c new file mode 100644 index 0000000000..866fef53a6 --- /dev/null +++ b/libavfilter/vf_gainmap.c @@ -0,0 +1,774 @@ +/* + * This file is part of FFmpeg. + * + * FFmpeg is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * FFmpeg is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with FFmpeg; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/** + * @file + * Filter to compute an HDR gain map from a base and alternate rendition. + */ + +#include <float.h> +#include <math.h> + +#include "libavutil/csp.h" +#include "libavutil/gain_map.h" +#include "libavutil/internal.h" +#include "libavutil/mastering_display_metadata.h" +#include "libavutil/mem.h" +#include "libavutil/opt.h" +#include "libavutil/pixdesc.h" + +#include "avfilter.h" +#include "colorspace.h" +#include "filters.h" +#include "formats.h" +#include "framesync.h" +#include "video.h" + +#define SDR_DIFFUSE_WHITE 203.0 + +enum GainMapColorspace { + GAINMAP_CSP_BASE, ///< apply the map in the base rendition's space + GAINMAP_CSP_ALT, ///< apply it in the alternate rendition's space + GAINMAP_CSP_NB, +}; + +enum GainMapMode { + GAINMAP_RGB, ///< one channel per component + GAINMAP_LUMA, ///< single channel, from the luminance + GAINMAP_MAXRGB, ///< single channel, from max(R,G,B) + GAINMAP_MODE_NB, +}; + +enum GainMapMeasure { + MEASURE_MIN = 1 << 0, + MEASURE_MAX = 1 << 1, + MEASURE_GAMMA = 1 << 2, + MEASURE_RANGE = MEASURE_MIN | MEASURE_MAX, + MEASURE_ALL = MEASURE_RANGE | MEASURE_GAMMA, +}; + +typedef struct GainMapEOTF { + enum AVColorTransferCharacteristic trc; + av_csp_eotf_function eotf; /* or NULL */ + double Lw; + +#define GAINMAP_LUT_SIZE 1025 + float lut[GAINMAP_LUT_SIZE + 1]; /* extra padding entry */ +} GainMapEOTF; + +typedef struct GainMapContext { + const AVClass *class; + FFFrameSync fs; + + /* Filter options */ + int mode; + int colorspace; + AVRational gain_min; + AVRational gain_max; + AVRational gamma; + AVRational base_offset; + AVRational alt_offset; + AVRational base_nits; + AVRational alt_nits; + + int warned_noop; + int nb_channels; + int nb_threads; /* for slice threading */ + int convert; /* colorspace conversion needed */ + int sign; /* sign(alt_peak - base_peak) */ + + /* Colorspace parameters */ + GainMapEOTF base_eotf; + GainMapEOTF alt_eotf; + float rgb2y[3]; + float rgb2rgb[3][3]; + + /* Quantization parameters, either static or recomputed dynamically */ + float quant_scale[3]; + float quant_offset[3]; + float quant_gamma[3]; + + /* Measured frame statistics */ + float (*slice_min)[3]; /* for MEASURE_MIN */ + float (*slice_max)[3]; /* for MEASURE_MAX */ + double (*slice_sum)[3]; /* for MEASURE_GAMMA */ + enum GainMapMeasure measure; + + /* Generated gain map parameters (recomputed per frame) */ + AVGainMapParams params; +} GainMapContext; + +typedef struct ThreadData { + AVFrame *out; + const AVFrame *base, *alt; +} ThreadData; + +#define OFFSET(x) offsetof(GainMapContext, x) +#define FLAGS AV_OPT_FLAG_FILTERING_PARAM | AV_OPT_FLAG_VIDEO_PARAM + +static const AVOption gainmap_options[] = { + { "mode", "quantity the gain is computed over", OFFSET(mode), AV_OPT_TYPE_INT, { .i64 = GAINMAP_RGB }, 0, GAINMAP_MODE_NB - 1, FLAGS, .unit = "mode" }, + { "rgb", "one gain channel per component", 0, AV_OPT_TYPE_CONST, { .i64 = GAINMAP_RGB }, 0, 0, FLAGS, .unit = "mode" }, + { "luma", "single gain channel, from luminance", 0, AV_OPT_TYPE_CONST, { .i64 = GAINMAP_LUMA }, 0, 0, FLAGS, .unit = "mode" }, + { "maxrgb", "single gain channel, from max(R,G,B)", 0, AV_OPT_TYPE_CONST, { .i64 = GAINMAP_MAXRGB }, 0, 0, FLAGS, .unit = "mode" }, + { "colorspace", "rendition whose colour space the map is applied in", OFFSET(colorspace), AV_OPT_TYPE_INT, { .i64 = GAINMAP_CSP_BASE }, 0, GAINMAP_CSP_NB - 1, FLAGS, .unit = "colorspace" }, + { "base", "the base rendition's colour space", 0, AV_OPT_TYPE_CONST, { .i64 = GAINMAP_CSP_BASE }, 0, 0, FLAGS, .unit = "colorspace" }, + { "alternate", "the alternate rendition's colour space", 0, AV_OPT_TYPE_CONST, { .i64 = GAINMAP_CSP_ALT }, 0, 0, FLAGS, .unit = "colorspace" }, + { "min", "override lower bound on the encoded gain, in log2 space", OFFSET(gain_min), AV_OPT_TYPE_RATIONAL, { .dbl = NAN }, -32.0, 32.0, FLAGS }, + { "max", "override upper bound on the encoded gain, in log2 space", OFFSET(gain_max), AV_OPT_TYPE_RATIONAL, { .dbl = NAN }, -32.0, 32.0, FLAGS }, + { "gamma", "override encoding gamma of the stored map (default: measured)", OFFSET(gamma), AV_OPT_TYPE_RATIONAL, { .dbl = NAN }, 0.0001, 100.0, FLAGS }, + { "base_offset", "constant added to the base rendition", OFFSET(base_offset), AV_OPT_TYPE_RATIONAL, { .dbl = 1.0 / 64.0 }, 1e-6, 1.0, FLAGS }, + { "alt_offset", "constant added to the alternate rendition", OFFSET(alt_offset), AV_OPT_TYPE_RATIONAL, { .dbl = 1.0 / 64.0 }, 1e-6, 1.0, FLAGS }, + { "base_nits", "override input luminance of the base rendition", OFFSET(base_nits), AV_OPT_TYPE_RATIONAL, { .dbl = NAN }, 1.0, 10000.0, FLAGS }, + { "alt_nits", "override input luminance of the alternate rendition", OFFSET(alt_nits), AV_OPT_TYPE_RATIONAL, { .dbl = NAN }, 1.0, 10000.0, FLAGS }, + { NULL } +}; + +FRAMESYNC_DEFINE_CLASS(gainmap, GainMapContext, fs); + +/* Quantize to a fixed point representation with the correct rounding mode */ +static AVRational quantq(double x, enum AVRounding rnd) +{ + const int scale = 1 << 22; + + switch (rnd) { + case AV_ROUND_DOWN: x = floor(x * scale); break; + case AV_ROUND_UP: x = ceil(x * scale); break; + case AV_ROUND_NEAR_INF: x = round(x * scale); break; + default: av_unreachable("not used / implemented"); + } + + AVRational q; + av_reduce(&q.num, &q.den, x, scale, INT_MAX); + return q; +} + +static void setup_range(AVFilterContext *ctx, int ch, AVRational min, AVRational max) +{ + GainMapContext *s = ctx->priv; + if (av_cmp_q(max, min) <= 0) { + /* Nothing to quantize; flat gain map */ + s->quant_scale[ch] = s->quant_offset[ch] = 0.0f; + max = min; /* sanity */ + } else { + const float minf = av_q2d(min), maxf = av_q2d(max); + s->quant_scale[ch] = 1.0f / (maxf - minf); + s->quant_offset[ch] = -minf * s->quant_scale[ch]; + } + + s->params.channels[ch].gain_map_min = min; + s->params.channels[ch].gain_map_max = max; + av_log(ctx, AV_LOG_TRACE, "channel %d: measured min=%g max=%g\n", + ch, av_q2d(min), av_q2d(max)); +} + +static void setup_gamma(AVFilterContext *ctx, int ch, AVRational gamma) +{ + GainMapContext *s = ctx->priv; + s->quant_gamma[ch] = av_q2d(gamma); + s->params.channels[ch].gamma = gamma; + av_log(ctx, AV_LOG_TRACE, "channel %d: measured gamma=%g\n", + ch, s->quant_gamma[ch]); +} + +static av_cold int init(AVFilterContext *ctx) +{ + GainMapContext *s = ctx->priv; + + if (av_cmp_q(s->gain_min, s->gain_max) >= 0) { + av_log(ctx, AV_LOG_ERROR, "min (%g) must be below max (%g)\n", + av_q2d(s->gain_min), av_q2d(s->gain_max)); + return AVERROR(EINVAL); + } + + s->nb_channels = s->mode == GAINMAP_RGB ? 3 : 1; + if (!s->gain_min.den) + s->measure |= MEASURE_MIN; + if (!s->gain_max.den) + s->measure |= MEASURE_MAX; + if (!s->gamma.den) + s->measure |= MEASURE_GAMMA; + if (s->measure) { + av_log(ctx, AV_LOG_VERBOSE, "Using two passes to measure gain map " + "parameters from the input frame.\n"); + } + + s->params = (AVGainMapParams) { + .version = 0, + .nb_channels = s->nb_channels, + .use_base_color_space = s->colorspace == GAINMAP_CSP_BASE, + /* payload metadata recomputed per frame */ + }; + + for (int c = 0; c < s->nb_channels; c++) { + struct AVGainMapChannel *ch = &s->params.channels[c]; + ch->base_offset = s->base_offset; + ch->alternate_offset = s->alt_offset; + if (!(s->measure & MEASURE_RANGE)) + setup_range(ctx, c, s->gain_min, s->gain_max); + if (!(s->measure & MEASURE_GAMMA)) + setup_gamma(ctx, c, s->gamma); + } + + return 0; +} + +static int query_formats(const AVFilterContext *ctx, + AVFilterFormatsConfig **cfg_in, + AVFilterFormatsConfig **cfg_out) +{ + const GainMapContext *s = ctx->priv; + enum AVPixelFormat in_fmt, out_fmt; + int ret; + + in_fmt = AV_PIX_FMT_GBRPF32; + out_fmt = s->nb_channels == 1 ? AV_PIX_FMT_GRAYF32 : AV_PIX_FMT_GBRPF32; + + ret = ff_formats_ref(ff_make_formats_list_singleton(out_fmt), &cfg_out[0]->formats); + if (ret < 0) + return ret; + + return ff_set_common_formats2(ctx, cfg_in, cfg_out, + ff_make_formats_list_singleton(in_fmt)); +} + +static double frame_luminance(const AVFrame *frame) +{ + const AVFrameSideData *sd; + sd = av_frame_get_side_data(frame, AV_FRAME_DATA_MASTERING_DISPLAY_METADATA); + if (sd) { + const AVMasteringDisplayMetadata *mdm; + mdm = (const AVMasteringDisplayMetadata *) sd->data; + if (mdm->has_luminance && mdm->max_luminance.num > 0) + return av_q2d(mdm->max_luminance); + } + + switch (frame->color_trc) { + case AVCOL_TRC_SMPTE2084: return 10000.0; + case AVCOL_TRC_ARIB_STD_B67: return 1000.0; + default: return SDR_DIFFUSE_WHITE; + } +} + +static void update_eotf(GainMapEOTF *tf, enum AVColorTransferCharacteristic trc, + av_csp_eotf_function eotf, double Lw) +{ + if (trc == tf->trc && Lw == tf->Lw) + return; /* no change */ + tf->trc = trc; + tf->Lw = Lw; + + switch (trc) { + /* EOTFs that are safe to collapse into a 1D LUT */ + case AVCOL_TRC_BT709: + case AVCOL_TRC_GAMMA22: + case AVCOL_TRC_GAMMA28: + case AVCOL_TRC_SMPTE170M: + case AVCOL_TRC_SMPTE240M: + case AVCOL_TRC_LINEAR: + case AVCOL_TRC_IEC61966_2_4: + case AVCOL_TRC_BT1361_ECG: + case AVCOL_TRC_IEC61966_2_1: + case AVCOL_TRC_BT2020_10: + case AVCOL_TRC_BT2020_12: + case AVCOL_TRC_SMPTE2084: { + for (int i = 0; i < GAINMAP_LUT_SIZE; i++) { + double x[3] = { i / (GAINMAP_LUT_SIZE - 1.0) }; + eotf(Lw, 0.0, x); + tf->lut[i] = x[0] / SDR_DIFFUSE_WHITE; /* normalize */ + } + + tf->lut[GAINMAP_LUT_SIZE] = tf->lut[GAINMAP_LUT_SIZE - 1]; + tf->eotf = NULL; + break; + } + /* EOTFs that use the av_csp_eotf_function fallback */ + case AVCOL_TRC_ARIB_STD_B67: /* nontrivial OOTF */ + case AVCOL_TRC_SMPTE428: /* different normalization per channel */ + default: + tf->eotf = eotf; + break; + } +} + +static const char *unknown_if_null(const char *s) +{ + return s ? s : "unknown"; +} + +/* Run per frame, since trc/primaries are not (yet) link-level properties */ +static int setup_colorspace(AVFilterContext *ctx, const AVFrame *base, const AVFrame *alt) +{ + GainMapContext *const s = ctx->priv; + av_csp_eotf_function base_eotf = av_csp_itu_eotf(base->color_trc); + av_csp_eotf_function alt_eotf = av_csp_itu_eotf(alt->color_trc); + if (!base_eotf || !alt_eotf) { + av_log(ctx, AV_LOG_ERROR, "Unknown transfer: base=%s, alternate=%s\n", + unknown_if_null(av_color_transfer_name(base->color_trc)), + unknown_if_null(av_color_transfer_name(alt->color_trc))); + return AVERROR(EINVAL); + } + + const AVColorPrimariesDesc *base_desc, *alt_desc; + base_desc = av_csp_primaries_desc_from_id(base->color_primaries); + alt_desc = av_csp_primaries_desc_from_id(alt->color_primaries); + if (!base_desc || !alt_desc) { + av_log(ctx, AV_LOG_ERROR, "Unknown primaries: base=%s, alternate=%s\n", + unknown_if_null(av_color_primaries_name(base->color_primaries)), + unknown_if_null(av_color_primaries_name(alt->color_primaries))); + return AVERROR(EINVAL); + } + + double base_lw = s->base_nits.den ? av_q2d(s->base_nits) : frame_luminance(base); + double alt_lw = s->alt_nits.den ? av_q2d(s->alt_nits) : frame_luminance(alt); + double base_headroom = fmax(log2(base_lw / SDR_DIFFUSE_WHITE), 0.0); + double alt_headroom = fmax(log2(alt_lw / SDR_DIFFUSE_WHITE), 0.0); + s->params.base_hdr_headroom = quantq(base_headroom, AV_ROUND_NEAR_INF); + s->params.alternate_hdr_headroom = quantq(alt_headroom, AV_ROUND_NEAR_INF); + s->sign = FFDIFFSIGN(alt_headroom, base_headroom); + if (!s->sign) { + /* No-op, set up empty gain map */ + for (int i = 0; i < s->nb_channels; i++) { + setup_range(ctx, i, (AVRational) { 0, 1 }, (AVRational) { 0, 1 }); + setup_gamma(ctx, i, (AVRational) { 1, 1 }); + } + + av_log_once(ctx, AV_LOG_WARNING, AV_LOG_VERBOSE, &s->warned_noop, + "Base and alternate renditions have the same peak " + "luminance (%g nits), gain map will be empty\n", base_lw); + return 0; + } else { + av_log(ctx, AV_LOG_DEBUG, "Base peak: %g nits, alternate peak: %g nits\n", + base_lw, alt_lw); + } + + update_eotf(&s->base_eotf, base->color_trc, base_eotf, base_lw); + update_eotf(&s->alt_eotf, alt->color_trc, alt_eotf, alt_lw); + + /* Assume conversion from alt to base */ + if (s->colorspace == GAINMAP_CSP_ALT) + FFSWAP(const AVColorPrimariesDesc *, base_desc, alt_desc); + + double rgb2xyz[3][3]; + ff_fill_rgb2xyz_table(&base_desc->prim, &base_desc->wp, rgb2xyz); + for (int i = 0; i < 3; i++) + s->rgb2y[i] = rgb2xyz[1][i] / av_q2d(base_desc->wp.y); + + s->convert = base->color_primaries != alt->color_primaries; + if (s->convert) { + /* Note: Ignores whitepoint differences (chromatic adaptation) */ + double xyz2rgb[3][3], rgb2rgb[3][3]; + ff_fill_rgb2xyz_table(&base_desc->prim, &base_desc->wp, rgb2xyz); + ff_matrix_invert_3x3(rgb2xyz, xyz2rgb); + ff_fill_rgb2xyz_table(&alt_desc->prim, &alt_desc->wp, rgb2xyz); + ff_matrix_mul_3x3(rgb2rgb, rgb2xyz, xyz2rgb); + for (int i = 0; i < 3; i++) + for (int j = 0; j < 3; j++) + s->rgb2rgb[i][j] = (float) rgb2rgb[i][j]; + } else { + memset(s->rgb2rgb, 0, sizeof(s->rgb2rgb)); + for (int i = 0; i < 3; i++) + s->rgb2rgb[i][i] = 1.0f; + } + + return 0; +} + +static av_always_inline float quantize(float gain, float scale, float offset, float gamma) +{ + const float norm = av_clipf(scale * gain + offset, 0.0f, 1.0f); + return gamma == 1.0f ? norm : powf(norm, gamma); +} + +static av_always_inline void +get_pixel(const GainMapContext *s, float dst[3], + const float *restrict const src[3], int x, int alt) +{ + const GainMapEOTF *const tf = alt ? &s->alt_eotf : &s->base_eotf; + float rgb[3]; + + /* Linearize to normalized RGB */ + if (tf->eotf) { + double rgbd[3] = { src[0][x], src[1][x], src[2][x] }; + tf->eotf(tf->Lw, 0.0, rgbd); + for (int i = 0; i < 3; i++) + rgb[i] = rgbd[i] / SDR_DIFFUSE_WHITE; + } else { + for (int i = 0; i < 3; i++) { + const float fx = av_clipf(src[i][x], 0.0f, 1.0f) * (GAINMAP_LUT_SIZE - 1.0); + const int ix = (int) fx; + const float lo = tf->lut[ix]; + const float hi = tf->lut[ix + 1]; + rgb[i] = lo + (fx - ix) * (hi - lo); + } + } + + /* Convert to correct colorspace if needed */ + if (s->convert && (s->colorspace == GAINMAP_CSP_ALT) != alt) { + const float r = rgb[0], g = rgb[1], b = rgb[2]; + for (int i = 0; i < 3; i++) { + dst[i] = fmaxf(r * s->rgb2rgb[i][0] + + g * s->rgb2rgb[i][1] + + b * s->rgb2rgb[i][2], 0.0f); + } + } else { + for (int i = 0; i < 3; i++) + dst[i] = fmaxf(rgb[i], 0.0f); + } +} + +/* GBRP plane order */ +static const int gbr_order[3] = { 2, 0, 1 }; + +static av_always_inline int +slice_internal(AVFilterContext *ctx, void *arg, int jobnr, int nb_jobs, int quant) +{ + GainMapContext *s = ctx->priv; + const ThreadData *td = arg; + const AVFrame *base = td->base, *alt = td->alt, *out = td->out; + + const float *restrict scale = s->quant_scale; + const float *restrict offset = s->quant_offset; + const float *restrict gamma = s->quant_gamma; + + /* Normalize both to the base colorspace */ + const float base_off = av_q2d(s->base_offset); + const float alt_off = av_q2d(s->alt_offset); + const float sign = s->sign; + + const int y_start = ff_slice_pos(out->height, jobnr, nb_jobs); + const int y_end = ff_slice_pos(out->height, jobnr + 1, nb_jobs); + const int width = out->width; + const int mode = s->mode; + const int nb_ch = s->nb_channels; + + float min[3] = { FLT_MAX, FLT_MAX, FLT_MAX }; + float max[3] = { -FLT_MAX, -FLT_MAX, -FLT_MAX }; + double sum[3] = { 0.0, 0.0, 0.0 }; + + for (int y = y_start; y < y_end; y++) { + const float *restrict b_row[3], *restrict a_row[3]; + for (int i = 0; i < 3; i++) { + const int p = gbr_order[i]; + b_row[i] = (const float *) (base->data[p] + y * base->linesize[p]); + a_row[i] = (const float *) ( alt->data[p] + y * alt->linesize[p]); + } + + float *restrict out_row[3]; + for (int i = 0; i < nb_ch; i++) { + const int p = nb_ch == 1 ? 0 : gbr_order[i]; + out_row[i] = (float *) (out->data[p] + y * out->linesize[p]); + } + + for (int x = 0; x < width; x++) { + float b[3], a[3], gain[3]; + get_pixel(s, b, b_row, x, 0); + get_pixel(s, a, a_row, x, 1); + + #define GAIN(a, b) (sign * log2f(((a) + alt_off) / ((b) + base_off))) + switch (mode) { + case GAINMAP_MAXRGB: { + const float max_a = fmaxf(fmaxf(a[0], a[1]), a[2]); + const float max_b = fmaxf(fmaxf(b[0], b[1]), b[2]); + gain[0] = GAIN(max_a, max_b); + break; + } + case GAINMAP_LUMA: { + const float y_a = s->rgb2y[0] * a[0] + s->rgb2y[1] * a[1] + s->rgb2y[2] * a[2]; + const float y_b = s->rgb2y[0] * b[0] + s->rgb2y[1] * b[1] + s->rgb2y[2] * b[2]; + gain[0] = GAIN(y_a, y_b); + break; + } + case GAINMAP_RGB: + gain[0] = GAIN(a[0], b[0]); + gain[1] = GAIN(a[1], b[1]); + gain[2] = GAIN(a[2], b[2]); + break; + } + #undef GAIN + + if (quant) { + for (int i = 0; i < nb_ch; i++) + out_row[i][x] = quantize(gain[i], scale[i], offset[i], gamma[i]); + } else { + for (int i = 0; i < nb_ch; i++) { + out_row[i][x] = gain[i]; + min[i] = fminf(min[i], gain[i]); + max[i] = fmaxf(max[i], gain[i]); + sum[i] += gain[i]; + } + } + + } + } + + for (int i = 0; !quant && i < 3; i++) { + s->slice_min[jobnr][i] = min[i]; + s->slice_max[jobnr][i] = max[i]; + s->slice_sum[jobnr][i] = sum[i]; + } + + return 0; +} + +static int slice_gain(AVFilterContext *ctx, void *arg, int jobnr, int nb_jobs) +{ + return slice_internal(ctx, arg, jobnr, nb_jobs, 0); +} + +static int slice_gain_quant(AVFilterContext *ctx, void *arg, int jobnr, int nb_jobs) +{ + return slice_internal(ctx, arg, jobnr, nb_jobs, 1); +} + +static int slice_quant(AVFilterContext *ctx, void *arg, int jobnr, int nb_jobs) +{ + const ThreadData *td = arg; + const GainMapContext *s = ctx->priv; + const AVFrame *out = td->out; + + const float *restrict scale = s->quant_scale; + const float *restrict offset = s->quant_offset; + const float *restrict gamma = s->quant_gamma; + + const int y_start = ff_slice_pos(out->height, jobnr, nb_jobs); + const int y_end = ff_slice_pos(out->height, jobnr + 1, nb_jobs); + const int width = out->width; + const int nb_ch = s->nb_channels; + + for (int y = y_start; y < y_end; y++) { + float *restrict out_row[3]; + for (int i = 0; i < nb_ch; i++) { + const int p = nb_ch == 1 ? 0 : gbr_order[i]; + out_row[i] = (float *) (out->data[p] + y * out->linesize[p]); + } + + for (int x = 0; x < width; x++) { + for (int i = 0; i < nb_ch; i++) + out_row[i][x] = quantize(out_row[i][x], scale[i], offset[i], gamma[i]); + } + } + + return 0; +} + +static void choose_quant_params(AVFilterContext *ctx, const AVFrame *out, int nb_jobs) +{ + GainMapContext *s = ctx->priv; + float frame_min[3] = { FLT_MAX, FLT_MAX, FLT_MAX }; + float frame_max[3] = { -FLT_MAX, -FLT_MAX, -FLT_MAX }; + double frame_sum[3] = { 0.0, 0.0, 0.0 }; + + for (int j = 0; j < nb_jobs; j++) { + for (int i = 0; i < s->nb_channels; i++) { + frame_min[i] = fminf(frame_min[i], s->slice_min[j][i]); + frame_max[i] = fmaxf(frame_max[i], s->slice_max[j][i]); + frame_sum[i] += s->slice_sum[j][i]; + } + } + + for (int i = 0; i < s->nb_channels; i++) { + AVRational min = s->gain_min, max = s->gain_max; + if (s->measure & MEASURE_MIN) + min = quantq(frame_min[i], AV_ROUND_DOWN); + if (s->measure & MEASURE_MAX) + max = quantq(frame_max[i], AV_ROUND_UP); + if (s->measure & MEASURE_RANGE) + setup_range(ctx, i, min, max); + + if (!(s->measure & MEASURE_GAMMA)) + continue; + + const int64_t nb_pixels = (int64_t) out->width * out->height; + const double mean = frame_sum[i] / nb_pixels; + const double mean_quant = s->quant_scale[i] * mean + s->quant_offset[i]; + + /** + * Choose gamma such that mean_quant ^ gamma = 0.5; clamp to a sane + * value range of [1/8, 8] to prevent degenerate encodings. A gamma + * of >8 would collapse half the encoding space into a single 8-bit + * value, at which point we're losing more then gaining, and a value + * of <1/8 limits the amount by which a single high outlier pixel + * can determine the overall encoding gamma. + * + * We also clamp the mean to [0.01, 0.99] to prevent numerical explosion + * in the case that the mean is very close to 0 or 1. This is a looser + * bound than the final gamma clamp, so the exact values chosen do not + * matter as much. + */ + double gamma = log(0.5) / log(av_clipd(mean_quant, 0.01, 0.99)); + setup_gamma(ctx, i, quantq(av_clipd(gamma, 1/8.0, 8.0), AV_ROUND_NEAR_INF)); + } +} + +static int process_frame(FFFrameSync *fs) +{ + AVFilterContext *ctx = fs->parent; + GainMapContext *s = ctx->priv; + AVFilterLink *outlink = ctx->outputs[0]; + AVFrame *base, *alt, *out; + + int ret = ff_framesync_dualinput_get(fs, &base, &alt); + if (ret < 0) + return ret; + if (!base || !alt) + return AVERROR_BUG; + + ret = setup_colorspace(ctx, base, alt); + if (ret < 0) + return ret; + + out = ff_get_video_buffer(outlink, outlink->w, outlink->h); + if (!out) + return AVERROR(ENOMEM); + av_frame_copy_props(out, base); + av_frame_side_data_remove_by_props(&out->side_data, &out->nb_side_data, + AV_SIDE_DATA_PROP_COLOR_DEPENDENT); + + out->color_trc = AVCOL_TRC_UNSPECIFIED; + out->color_primaries = AVCOL_PRI_UNSPECIFIED; + out->colorspace = AVCOL_SPC_UNSPECIFIED; + out->color_range = AVCOL_RANGE_JPEG; + + ThreadData td = { + .out = out, + .base = base, + .alt = alt, + }; + + if (!s->sign) { /* no-op */ + for (int i = 0; i < s->nb_channels; i++) + memset(out->data[i], 0, out->height * out->linesize[i]); + goto skip; + } + + const int nb_jobs = FFMIN(outlink->h, s->nb_threads); + ret = ff_filter_execute(ctx, s->measure ? slice_gain : slice_gain_quant, + &td, NULL, nb_jobs); + if (ret < 0) + goto fail; + + if (s->measure) { + choose_quant_params(ctx, out, nb_jobs); + ret = ff_filter_execute(ctx, slice_quant, &td, NULL, nb_jobs); + if (ret < 0) + goto fail; + } + +skip:; + AVGainMapParams *params; + params = av_gain_map_params_create_side_data(&out->side_data, &out->nb_side_data); + if (!params) { + ret = AVERROR(ENOMEM); + goto fail; + } + + *params = s->params; + if (av_gain_map_params_validate(params) < 0) + return AVERROR_BUG; /* should never happen */ + + return ff_filter_frame(outlink, out); + +fail: + av_frame_free(&out); + return ret; +} + +static int config_output(AVFilterLink *outlink) +{ + AVFilterContext *ctx = outlink->src; + GainMapContext *s = ctx->priv; + AVFilterLink *base = ctx->inputs[0]; + AVFilterLink *alt = ctx->inputs[1]; + int ret; + + if (base->w != alt->w || base->h != alt->h) { + av_log(ctx, AV_LOG_ERROR, + "Input dimensions must match (%dx%d != %dx%d)\n", + base->w, base->h, alt->w, alt->h); + return AVERROR(EINVAL); + } + + outlink->w = base->w; + outlink->h = base->h; + outlink->colorspace = AVCOL_SPC_UNSPECIFIED; + outlink->color_range = AVCOL_RANGE_JPEG; + + s->nb_threads = ff_filter_get_nb_threads(ctx); + s->slice_min = av_calloc(s->nb_threads, sizeof(*s->slice_min)); + s->slice_max = av_calloc(s->nb_threads, sizeof(*s->slice_max)); + s->slice_sum = av_calloc(s->nb_threads, sizeof(*s->slice_sum)); + if (!s->slice_min || !s->slice_max || !s->slice_sum) + return AVERROR(ENOMEM); + + ret = ff_framesync_init_dualinput(&s->fs, ctx); + if (ret < 0) + return ret; + + s->fs.on_event = process_frame; + return ff_framesync_configure(&s->fs); +} + +static int activate(AVFilterContext *ctx) +{ + GainMapContext *s = ctx->priv; + return ff_framesync_activate(&s->fs); +} + +static av_cold void uninit(AVFilterContext *ctx) +{ + GainMapContext *s = ctx->priv; + ff_framesync_uninit(&s->fs); + av_freep(&s->slice_min); + av_freep(&s->slice_max); + av_freep(&s->slice_sum); +} + +static const AVFilterPad gainmap_inputs[] = { + { + .name = "base", + .type = AVMEDIA_TYPE_VIDEO, + }, + { + .name = "alternate", + .type = AVMEDIA_TYPE_VIDEO, + }, +}; + +static const AVFilterPad gainmap_outputs[] = { + { + .name = "default", + .type = AVMEDIA_TYPE_VIDEO, + .config_props = config_output, + }, +}; + +const FFFilter ff_vf_gainmap = { + .p.name = "gainmap", + .p.description = NULL_IF_CONFIG_SMALL("Generate a gain map from a base/alternate pair."), + .p.priv_class = &gainmap_class, + .p.flags = AVFILTER_FLAG_SLICE_THREADS, + .preinit = gainmap_framesync_preinit, + .priv_size = sizeof(GainMapContext), + .init = init, + .uninit = uninit, + .activate = activate, + FILTER_INPUTS(gainmap_inputs), + FILTER_OUTPUTS(gainmap_outputs), + FILTER_QUERY_FUNC2(query_formats), +}; diff --git a/tests/fate/filter-video.mak b/tests/fate/filter-video.mak index f813d72106..5f0387b527 100644 --- a/tests/fate/filter-video.mak +++ b/tests/fate/filter-video.mak @@ -224,6 +224,14 @@ $(FATE_FILTER_FRAMEPACK): CMD = framecrc -c:v pgmyuv -i $(TARGET_PATH)/tests/vsy FATE_FILTER_VSYNTH_PGMYUV-$(CONFIG_FRAMEPACK_FILTER) += $(FATE_FILTER_FRAMEPACK) fate-filter-framepack: $(FATE_FILTER_FRAMEPACK) +FATE_FILTER_GAINMAP_SRC = testsrc2=s=64x48:r=1:d=1,scale,format=gbrpf32,split[b][a];[b]setparams=color_trc=iec61966-2-1:color_primaries=bt709[base];[a]setparams=color_trc=smpte2084:color_primaries=bt2020[alt];[base][alt] + +FATE_FILTER-$(call FILTERFRAMECRC, GAINMAP TESTSRC2 FORMAT SETPARAMS SPLIT, SCALE_FILTER) += fate-filter-gainmap fate-filter-gainmap-luma fate-filter-gainmap-maxrgb fate-filter-gainmap-fixed +fate-filter-gainmap: CMD = framecrc -lavfi "$(FATE_FILTER_GAINMAP_SRC)gainmap,scale" -pix_fmt gbrp +fate-filter-gainmap-luma: CMD = framecrc -lavfi "$(FATE_FILTER_GAINMAP_SRC)gainmap=mode=luma,scale" -pix_fmt gray +fate-filter-gainmap-maxrgb: CMD = framecrc -lavfi "$(FATE_FILTER_GAINMAP_SRC)gainmap=mode=maxrgb,scale" -pix_fmt gray +fate-filter-gainmap-fixed: CMD = framecrc -lavfi "$(FATE_FILTER_GAINMAP_SRC)gainmap=min=0:max=2.3:gamma=1:base_nits=203:alt_nits=1000,scale" -pix_fmt gbrp + FATE_FILTER_VSYNTH_PGMYUV-$(CONFIG_GRADFUN_FILTER) += fate-filter-gradfun fate-filter-gradfun: CMD = framecrc -c:v pgmyuv -i $(SRC) -vf gradfun diff --git a/tests/ref/fate/filter-gainmap b/tests/ref/fate/filter-gainmap new file mode 100644 index 0000000000..96d5904207 --- /dev/null +++ b/tests/ref/fate/filter-gainmap @@ -0,0 +1,6 @@ +#tb 0: 1/1 +#media_type 0: video +#codec_id 0: rawvideo +#dimensions 0: 64x48 +#sar 0: 1/1 +0, 0, 0, 1, 9216, 0x91dedb2f diff --git a/tests/ref/fate/filter-gainmap-fixed b/tests/ref/fate/filter-gainmap-fixed new file mode 100644 index 0000000000..93b721a20e --- /dev/null +++ b/tests/ref/fate/filter-gainmap-fixed @@ -0,0 +1,6 @@ +#tb 0: 1/1 +#media_type 0: video +#codec_id 0: rawvideo +#dimensions 0: 64x48 +#sar 0: 1/1 +0, 0, 0, 1, 9216, 0x28fde0dc diff --git a/tests/ref/fate/filter-gainmap-luma b/tests/ref/fate/filter-gainmap-luma new file mode 100644 index 0000000000..ea6d074487 --- /dev/null +++ b/tests/ref/fate/filter-gainmap-luma @@ -0,0 +1,6 @@ +#tb 0: 1/1 +#media_type 0: video +#codec_id 0: rawvideo +#dimensions 0: 64x48 +#sar 0: 1/1 +0, 0, 0, 1, 3072, 0xbfac1585 diff --git a/tests/ref/fate/filter-gainmap-maxrgb b/tests/ref/fate/filter-gainmap-maxrgb new file mode 100644 index 0000000000..1d52102004 --- /dev/null +++ b/tests/ref/fate/filter-gainmap-maxrgb @@ -0,0 +1,6 @@ +#tb 0: 1/1 +#media_type 0: video +#codec_id 0: rawvideo +#dimensions 0: 64x48 +#sar 0: 1/1 +0, 0, 0, 1, 3072, 0x2253f63c -- To stop receiving notification emails like this one, please contact [email protected]. _______________________________________________ ffmpeg-cvslog mailing list -- [email protected] To unsubscribe send an email to [email protected]
