FFmpeg coverage


Directory: ../../../ffmpeg/
File: src/libavfilter/vf_gainmap.c
Date: 2026-10-01 14:18:39
Exec Total Coverage
Lines: 297 354 83.9%
Functions: 21 22 95.5%
Branches: 127 181 70.2%

Line Branch Exec Source
1 /*
2 * This file is part of FFmpeg.
3 *
4 * FFmpeg is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2.1 of the License, or (at your option) any later version.
8 *
9 * FFmpeg is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with FFmpeg; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 */
18
19 /**
20 * @file
21 * Filter to compute an HDR gain map from a base and alternate rendition.
22 */
23
24 #include <float.h>
25 #include <math.h>
26
27 #include "libavutil/csp.h"
28 #include "libavutil/gain_map.h"
29 #include "libavutil/internal.h"
30 #include "libavutil/mastering_display_metadata.h"
31 #include "libavutil/mem.h"
32 #include "libavutil/opt.h"
33 #include "libavutil/pixdesc.h"
34
35 #include "avfilter.h"
36 #include "colorspace.h"
37 #include "filters.h"
38 #include "formats.h"
39 #include "framesync.h"
40 #include "video.h"
41
42 #define SDR_DIFFUSE_WHITE 203.0
43
44 enum GainMapColorspace {
45 GAINMAP_CSP_BASE, ///< apply the map in the base rendition's space
46 GAINMAP_CSP_ALT, ///< apply it in the alternate rendition's space
47 GAINMAP_CSP_NB,
48 };
49
50 enum GainMapMode {
51 GAINMAP_RGB, ///< one channel per component
52 GAINMAP_LUMA, ///< single channel, from the luminance
53 GAINMAP_MAXRGB, ///< single channel, from max(R,G,B)
54 GAINMAP_MODE_NB,
55 };
56
57 enum GainMapMeasure {
58 MEASURE_MIN = 1 << 0,
59 MEASURE_MAX = 1 << 1,
60 MEASURE_GAMMA = 1 << 2,
61 MEASURE_RANGE = MEASURE_MIN | MEASURE_MAX,
62 MEASURE_ALL = MEASURE_RANGE | MEASURE_GAMMA,
63 };
64
65 typedef struct GainMapEOTF {
66 enum AVColorTransferCharacteristic trc;
67 av_csp_eotf_function eotf; /* or NULL */
68 double Lw;
69
70 #define GAINMAP_LUT_SIZE 1025
71 float lut[GAINMAP_LUT_SIZE + 1]; /* extra padding entry */
72 } GainMapEOTF;
73
74 typedef struct GainMapContext {
75 const AVClass *class;
76 FFFrameSync fs;
77
78 /* Filter options */
79 int mode;
80 int colorspace;
81 AVRational gain_min;
82 AVRational gain_max;
83 AVRational gamma;
84 AVRational base_offset;
85 AVRational alt_offset;
86 AVRational base_nits;
87 AVRational alt_nits;
88
89 int warned_noop;
90 int nb_channels;
91 int nb_threads; /* for slice threading */
92 int convert; /* colorspace conversion needed */
93 int sign; /* sign(alt_peak - base_peak) */
94
95 /* Colorspace parameters */
96 GainMapEOTF base_eotf;
97 GainMapEOTF alt_eotf;
98 float rgb2y[3];
99 float rgb2rgb[3][3];
100
101 /* Quantization parameters, either static or recomputed dynamically */
102 float quant_scale[3];
103 float quant_offset[3];
104 float quant_gamma[3];
105
106 /* Measured frame statistics */
107 float (*slice_min)[3]; /* for MEASURE_MIN */
108 float (*slice_max)[3]; /* for MEASURE_MAX */
109 double (*slice_sum)[3]; /* for MEASURE_GAMMA */
110 enum GainMapMeasure measure;
111
112 /* Generated gain map parameters (recomputed per frame) */
113 AVGainMapParams params;
114 } GainMapContext;
115
116 typedef struct ThreadData {
117 AVFrame *out;
118 const AVFrame *base, *alt;
119 } ThreadData;
120
121 #define OFFSET(x) offsetof(GainMapContext, x)
122 #define FLAGS AV_OPT_FLAG_FILTERING_PARAM | AV_OPT_FLAG_VIDEO_PARAM
123
124 static const AVOption gainmap_options[] = {
125 { "mode", "quantity the gain is computed over", OFFSET(mode), AV_OPT_TYPE_INT, { .i64 = GAINMAP_RGB }, 0, GAINMAP_MODE_NB - 1, FLAGS, .unit = "mode" },
126 { "rgb", "one gain channel per component", 0, AV_OPT_TYPE_CONST, { .i64 = GAINMAP_RGB }, 0, 0, FLAGS, .unit = "mode" },
127 { "luma", "single gain channel, from luminance", 0, AV_OPT_TYPE_CONST, { .i64 = GAINMAP_LUMA }, 0, 0, FLAGS, .unit = "mode" },
128 { "maxrgb", "single gain channel, from max(R,G,B)", 0, AV_OPT_TYPE_CONST, { .i64 = GAINMAP_MAXRGB }, 0, 0, FLAGS, .unit = "mode" },
129 { "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" },
130 { "base", "the base rendition's colour space", 0, AV_OPT_TYPE_CONST, { .i64 = GAINMAP_CSP_BASE }, 0, 0, FLAGS, .unit = "colorspace" },
131 { "alternate", "the alternate rendition's colour space", 0, AV_OPT_TYPE_CONST, { .i64 = GAINMAP_CSP_ALT }, 0, 0, FLAGS, .unit = "colorspace" },
132 { "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 },
133 { "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 },
134 { "gamma", "override encoding gamma of the stored map (default: measured)", OFFSET(gamma), AV_OPT_TYPE_RATIONAL, { .dbl = NAN }, 0.0001, 100.0, FLAGS },
135 { "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 },
136 { "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 },
137 { "base_nits", "override input luminance of the base rendition", OFFSET(base_nits), AV_OPT_TYPE_RATIONAL, { .dbl = NAN }, 1.0, 10000.0, FLAGS },
138 { "alt_nits", "override input luminance of the alternate rendition", OFFSET(alt_nits), AV_OPT_TYPE_RATIONAL, { .dbl = NAN }, 1.0, 10000.0, FLAGS },
139 { NULL }
140 };
141
142
2/2
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 16 times.
84 FRAMESYNC_DEFINE_CLASS(gainmap, GainMapContext, fs);
143
144 /* Quantize to a fixed point representation with the correct rounding mode */
145 28 static AVRational quantq(double x, enum AVRounding rnd)
146 {
147 28 const int scale = 1 << 22;
148
149
3/4
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 6 times.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
28 switch (rnd) {
150 6 case AV_ROUND_DOWN: x = floor(x * scale); break;
151 6 case AV_ROUND_UP: x = ceil(x * scale); break;
152 16 case AV_ROUND_NEAR_INF: x = round(x * scale); break;
153 ✗ default: av_unreachable("not used / implemented");
154 }
155
156 AVRational q;
157 28 av_reduce(&q.num, &q.den, x, scale, INT_MAX);
158 28 return q;
159 }
160
161 12 static void setup_range(AVFilterContext *ctx, int ch, AVRational min, AVRational max)
162 {
163 12 GainMapContext *s = ctx->priv;
164
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
12 if (av_cmp_q(max, min) <= 0) {
165 /* Nothing to quantize; flat gain map */
166 ✗ s->quant_scale[ch] = s->quant_offset[ch] = 0.0f;
167 ✗ max = min; /* sanity */
168 } else {
169 12 const float minf = av_q2d(min), maxf = av_q2d(max);
170 12 s->quant_scale[ch] = 1.0f / (maxf - minf);
171 12 s->quant_offset[ch] = -minf * s->quant_scale[ch];
172 }
173
174 12 s->params.channels[ch].gain_map_min = min;
175 12 s->params.channels[ch].gain_map_max = max;
176 12 av_log(ctx, AV_LOG_TRACE, "channel %d: measured min=%g max=%g\n",
177 ch, av_q2d(min), av_q2d(max));
178 12 }
179
180 12 static void setup_gamma(AVFilterContext *ctx, int ch, AVRational gamma)
181 {
182 12 GainMapContext *s = ctx->priv;
183 12 s->quant_gamma[ch] = av_q2d(gamma);
184 12 s->params.channels[ch].gamma = gamma;
185 12 av_log(ctx, AV_LOG_TRACE, "channel %d: measured gamma=%g\n",
186 12 ch, s->quant_gamma[ch]);
187 12 }
188
189 10 static av_cold int init(AVFilterContext *ctx)
190 {
191 10 GainMapContext *s = ctx->priv;
192
193
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
10 if (av_cmp_q(s->gain_min, s->gain_max) >= 0) {
194 ✗ av_log(ctx, AV_LOG_ERROR, "min (%g) must be below max (%g)\n",
195 av_q2d(s->gain_min), av_q2d(s->gain_max));
196 ✗ return AVERROR(EINVAL);
197 }
198
199
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 6 times.
10 s->nb_channels = s->mode == GAINMAP_RGB ? 3 : 1;
200
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 2 times.
10 if (!s->gain_min.den)
201 8 s->measure |= MEASURE_MIN;
202
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 2 times.
10 if (!s->gain_max.den)
203 8 s->measure |= MEASURE_MAX;
204
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 2 times.
10 if (!s->gamma.den)
205 8 s->measure |= MEASURE_GAMMA;
206
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 2 times.
10 if (s->measure) {
207 8 av_log(ctx, AV_LOG_VERBOSE, "Using two passes to measure gain map "
208 "parameters from the input frame.\n");
209 }
210
211 10 s->params = (AVGainMapParams) {
212 .version = 0,
213 10 .nb_channels = s->nb_channels,
214 10 .use_base_color_space = s->colorspace == GAINMAP_CSP_BASE,
215 /* payload metadata recomputed per frame */
216 };
217
218
2/2
✓ Branch 0 taken 18 times.
✓ Branch 1 taken 10 times.
28 for (int c = 0; c < s->nb_channels; c++) {
219 18 struct AVGainMapChannel *ch = &s->params.channels[c];
220 18 ch->base_offset = s->base_offset;
221 18 ch->alternate_offset = s->alt_offset;
222
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 12 times.
18 if (!(s->measure & MEASURE_RANGE))
223 6 setup_range(ctx, c, s->gain_min, s->gain_max);
224
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 12 times.
18 if (!(s->measure & MEASURE_GAMMA))
225 6 setup_gamma(ctx, c, s->gamma);
226 }
227
228 10 return 0;
229 }
230
231 5 static int query_formats(const AVFilterContext *ctx,
232 AVFilterFormatsConfig **cfg_in,
233 AVFilterFormatsConfig **cfg_out)
234 {
235 5 const GainMapContext *s = ctx->priv;
236 enum AVPixelFormat in_fmt, out_fmt;
237 int ret;
238
239 5 in_fmt = AV_PIX_FMT_GBRPF32;
240
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 2 times.
5 out_fmt = s->nb_channels == 1 ? AV_PIX_FMT_GRAYF32 : AV_PIX_FMT_GBRPF32;
241
242 5 ret = ff_formats_ref(ff_make_formats_list_singleton(out_fmt), &cfg_out[0]->formats);
243
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 if (ret < 0)
244 ✗ return ret;
245
246 5 return ff_set_common_formats2(ctx, cfg_in, cfg_out,
247 ff_make_formats_list_singleton(in_fmt));
248 }
249
250 8 static double frame_luminance(const AVFrame *frame)
251 {
252 const AVFrameSideData *sd;
253 8 sd = av_frame_get_side_data(frame, AV_FRAME_DATA_MASTERING_DISPLAY_METADATA);
254
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (sd) {
255 const AVMasteringDisplayMetadata *mdm;
256 ✗ mdm = (const AVMasteringDisplayMetadata *) sd->data;
257 ✗ if (mdm->has_luminance && mdm->max_luminance.num > 0)
258 ✗ return av_q2d(mdm->max_luminance);
259 }
260
261
2/3
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
8 switch (frame->color_trc) {
262 4 case AVCOL_TRC_SMPTE2084: return 10000.0;
263 ✗ case AVCOL_TRC_ARIB_STD_B67: return 1000.0;
264 4 default: return SDR_DIFFUSE_WHITE;
265 }
266 }
267
268 10 static void update_eotf(GainMapEOTF *tf, enum AVColorTransferCharacteristic trc,
269 av_csp_eotf_function eotf, double Lw)
270 {
271
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 10 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
10 if (trc == tf->trc && Lw == tf->Lw)
272 ✗ return; /* no change */
273 10 tf->trc = trc;
274 10 tf->Lw = Lw;
275
276
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 switch (trc) {
277 /* EOTFs that are safe to collapse into a 1D LUT */
278 10 case AVCOL_TRC_BT709:
279 case AVCOL_TRC_GAMMA22:
280 case AVCOL_TRC_GAMMA28:
281 case AVCOL_TRC_SMPTE170M:
282 case AVCOL_TRC_SMPTE240M:
283 case AVCOL_TRC_LINEAR:
284 case AVCOL_TRC_IEC61966_2_4:
285 case AVCOL_TRC_BT1361_ECG:
286 case AVCOL_TRC_IEC61966_2_1:
287 case AVCOL_TRC_BT2020_10:
288 case AVCOL_TRC_BT2020_12:
289 case AVCOL_TRC_SMPTE2084: {
290
2/2
✓ Branch 0 taken 10250 times.
✓ Branch 1 taken 10 times.
10260 for (int i = 0; i < GAINMAP_LUT_SIZE; i++) {
291 10250 double x[3] = { i / (GAINMAP_LUT_SIZE - 1.0) };
292 10250 eotf(Lw, 0.0, x);
293 10250 tf->lut[i] = x[0] / SDR_DIFFUSE_WHITE; /* normalize */
294 }
295
296 10 tf->lut[GAINMAP_LUT_SIZE] = tf->lut[GAINMAP_LUT_SIZE - 1];
297 10 tf->eotf = NULL;
298 10 break;
299 }
300 /* EOTFs that use the av_csp_eotf_function fallback */
301 ✗ case AVCOL_TRC_ARIB_STD_B67: /* nontrivial OOTF */
302 case AVCOL_TRC_SMPTE428: /* different normalization per channel */
303 default:
304 ✗ tf->eotf = eotf;
305 ✗ break;
306 }
307 }
308
309 ✗ static const char *unknown_if_null(const char *s)
310 {
311 ✗ return s ? s : "unknown";
312 }
313
314 /* Run per frame, since trc/primaries are not (yet) link-level properties */
315 5 static int setup_colorspace(AVFilterContext *ctx, const AVFrame *base, const AVFrame *alt)
316 {
317 5 GainMapContext *const s = ctx->priv;
318 5 av_csp_eotf_function base_eotf = av_csp_itu_eotf(base->color_trc);
319 5 av_csp_eotf_function alt_eotf = av_csp_itu_eotf(alt->color_trc);
320
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
5 if (!base_eotf || !alt_eotf) {
321 ✗ av_log(ctx, AV_LOG_ERROR, "Unknown transfer: base=%s, alternate=%s\n",
322 ✗ unknown_if_null(av_color_transfer_name(base->color_trc)),
323 ✗ unknown_if_null(av_color_transfer_name(alt->color_trc)));
324 ✗ return AVERROR(EINVAL);
325 }
326
327 const AVColorPrimariesDesc *base_desc, *alt_desc;
328 5 base_desc = av_csp_primaries_desc_from_id(base->color_primaries);
329 5 alt_desc = av_csp_primaries_desc_from_id(alt->color_primaries);
330
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
5 if (!base_desc || !alt_desc) {
331 ✗ av_log(ctx, AV_LOG_ERROR, "Unknown primaries: base=%s, alternate=%s\n",
332 ✗ unknown_if_null(av_color_primaries_name(base->color_primaries)),
333 ✗ unknown_if_null(av_color_primaries_name(alt->color_primaries)));
334 ✗ return AVERROR(EINVAL);
335 }
336
337
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 4 times.
5 double base_lw = s->base_nits.den ? av_q2d(s->base_nits) : frame_luminance(base);
338
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 4 times.
5 double alt_lw = s->alt_nits.den ? av_q2d(s->alt_nits) : frame_luminance(alt);
339 5 double base_headroom = fmax(log2(base_lw / SDR_DIFFUSE_WHITE), 0.0);
340 5 double alt_headroom = fmax(log2(alt_lw / SDR_DIFFUSE_WHITE), 0.0);
341 5 s->params.base_hdr_headroom = quantq(base_headroom, AV_ROUND_NEAR_INF);
342 5 s->params.alternate_hdr_headroom = quantq(alt_headroom, AV_ROUND_NEAR_INF);
343 5 s->sign = FFDIFFSIGN(alt_headroom, base_headroom);
344
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 if (!s->sign) {
345 /* No-op, set up empty gain map */
346 ✗ for (int i = 0; i < s->nb_channels; i++) {
347 ✗ setup_range(ctx, i, (AVRational) { 0, 1 }, (AVRational) { 0, 1 });
348 ✗ setup_gamma(ctx, i, (AVRational) { 1, 1 });
349 }
350
351 ✗ av_log_once(ctx, AV_LOG_WARNING, AV_LOG_VERBOSE, &s->warned_noop,
352 "Base and alternate renditions have the same peak "
353 "luminance (%g nits), gain map will be empty\n", base_lw);
354 ✗ return 0;
355 } else {
356 5 av_log(ctx, AV_LOG_DEBUG, "Base peak: %g nits, alternate peak: %g nits\n",
357 base_lw, alt_lw);
358 }
359
360 5 update_eotf(&s->base_eotf, base->color_trc, base_eotf, base_lw);
361 5 update_eotf(&s->alt_eotf, alt->color_trc, alt_eotf, alt_lw);
362
363 /* Assume conversion from alt to base */
364
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 if (s->colorspace == GAINMAP_CSP_ALT)
365 ✗ FFSWAP(const AVColorPrimariesDesc *, base_desc, alt_desc);
366
367 double rgb2xyz[3][3];
368 5 ff_fill_rgb2xyz_table(&base_desc->prim, &base_desc->wp, rgb2xyz);
369
2/2
✓ Branch 0 taken 15 times.
✓ Branch 1 taken 5 times.
20 for (int i = 0; i < 3; i++)
370 15 s->rgb2y[i] = rgb2xyz[1][i] / av_q2d(base_desc->wp.y);
371
372 5 s->convert = base->color_primaries != alt->color_primaries;
373
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 if (s->convert) {
374 /* Note: Ignores whitepoint differences (chromatic adaptation) */
375 double xyz2rgb[3][3], rgb2rgb[3][3];
376 5 ff_fill_rgb2xyz_table(&base_desc->prim, &base_desc->wp, rgb2xyz);
377 5 ff_matrix_invert_3x3(rgb2xyz, xyz2rgb);
378 5 ff_fill_rgb2xyz_table(&alt_desc->prim, &alt_desc->wp, rgb2xyz);
379 5 ff_matrix_mul_3x3(rgb2rgb, rgb2xyz, xyz2rgb);
380
2/2
✓ Branch 0 taken 15 times.
✓ Branch 1 taken 5 times.
20 for (int i = 0; i < 3; i++)
381
2/2
✓ Branch 0 taken 45 times.
✓ Branch 1 taken 15 times.
60 for (int j = 0; j < 3; j++)
382 45 s->rgb2rgb[i][j] = (float) rgb2rgb[i][j];
383 } else {
384 ✗ memset(s->rgb2rgb, 0, sizeof(s->rgb2rgb));
385 ✗ for (int i = 0; i < 3; i++)
386 ✗ s->rgb2rgb[i][i] = 1.0f;
387 }
388
389 5 return 0;
390 }
391
392 27648 static av_always_inline float quantize(float gain, float scale, float offset, float gamma)
393 {
394 27648 const float norm = av_clipf(scale * gain + offset, 0.0f, 1.0f);
395
2/2
✓ Branch 0 taken 18432 times.
✓ Branch 1 taken 9216 times.
27648 return gamma == 1.0f ? norm : powf(norm, gamma);
396 }
397
398 static av_always_inline void
399 30720 get_pixel(const GainMapContext *s, float dst[3],
400 const float *restrict const src[3], int x, int alt)
401 {
402
2/2
✓ Branch 0 taken 15360 times.
✓ Branch 1 taken 15360 times.
30720 const GainMapEOTF *const tf = alt ? &s->alt_eotf : &s->base_eotf;
403 float rgb[3];
404
405 /* Linearize to normalized RGB */
406
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 30720 times.
30720 if (tf->eotf) {
407 ✗ double rgbd[3] = { src[0][x], src[1][x], src[2][x] };
408 ✗ tf->eotf(tf->Lw, 0.0, rgbd);
409 ✗ for (int i = 0; i < 3; i++)
410 ✗ rgb[i] = rgbd[i] / SDR_DIFFUSE_WHITE;
411 } else {
412
2/2
✓ Branch 0 taken 92160 times.
✓ Branch 1 taken 30720 times.
122880 for (int i = 0; i < 3; i++) {
413 92160 const float fx = av_clipf(src[i][x], 0.0f, 1.0f) * (GAINMAP_LUT_SIZE - 1.0);
414 92160 const int ix = (int) fx;
415 92160 const float lo = tf->lut[ix];
416 92160 const float hi = tf->lut[ix + 1];
417 92160 rgb[i] = lo + (fx - ix) * (hi - lo);
418 }
419 }
420
421 /* Convert to correct colorspace if needed */
422
3/4
✓ Branch 0 taken 30720 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15360 times.
✓ Branch 3 taken 15360 times.
46080 if (s->convert && (s->colorspace == GAINMAP_CSP_ALT) != alt) {
423 15360 const float r = rgb[0], g = rgb[1], b = rgb[2];
424
2/2
✓ Branch 0 taken 46080 times.
✓ Branch 1 taken 15360 times.
61440 for (int i = 0; i < 3; i++) {
425 46080 dst[i] = fmaxf(r * s->rgb2rgb[i][0] +
426 46080 g * s->rgb2rgb[i][1] +
427 46080 b * s->rgb2rgb[i][2], 0.0f);
428 }
429 } else {
430
2/2
✓ Branch 0 taken 46080 times.
✓ Branch 1 taken 15360 times.
61440 for (int i = 0; i < 3; i++)
431 46080 dst[i] = fmaxf(rgb[i], 0.0f);
432 }
433 30720 }
434
435 /* GBRP plane order */
436 static const int gbr_order[3] = { 2, 0, 1 };
437
438 static av_always_inline int
439 45 slice_internal(AVFilterContext *ctx, void *arg, int jobnr, int nb_jobs, int quant)
440 {
441 45 GainMapContext *s = ctx->priv;
442 45 const ThreadData *td = arg;
443 45 const AVFrame *base = td->base, *alt = td->alt, *out = td->out;
444
445 45 const float *restrict scale = s->quant_scale;
446 45 const float *restrict offset = s->quant_offset;
447 45 const float *restrict gamma = s->quant_gamma;
448
449 /* Normalize both to the base colorspace */
450 45 const float base_off = av_q2d(s->base_offset);
451 45 const float alt_off = av_q2d(s->alt_offset);
452 45 const float sign = s->sign;
453
454 45 const int y_start = ff_slice_pos(out->height, jobnr, nb_jobs);
455 45 const int y_end = ff_slice_pos(out->height, jobnr + 1, nb_jobs);
456 45 const int width = out->width;
457 45 const int mode = s->mode;
458 45 const int nb_ch = s->nb_channels;
459
460 45 float min[3] = { FLT_MAX, FLT_MAX, FLT_MAX };
461 45 float max[3] = { -FLT_MAX, -FLT_MAX, -FLT_MAX };
462 45 double sum[3] = { 0.0, 0.0, 0.0 };
463
464
2/2
✓ Branch 0 taken 240 times.
✓ Branch 1 taken 45 times.
285 for (int y = y_start; y < y_end; y++) {
465 const float *restrict b_row[3], *restrict a_row[3];
466
2/2
✓ Branch 0 taken 720 times.
✓ Branch 1 taken 240 times.
960 for (int i = 0; i < 3; i++) {
467 720 const int p = gbr_order[i];
468 720 b_row[i] = (const float *) (base->data[p] + y * base->linesize[p]);
469 720 a_row[i] = (const float *) ( alt->data[p] + y * alt->linesize[p]);
470 }
471
472 float *restrict out_row[3];
473
2/2
✓ Branch 0 taken 432 times.
✓ Branch 1 taken 240 times.
672 for (int i = 0; i < nb_ch; i++) {
474
2/2
✓ Branch 0 taken 288 times.
✓ Branch 1 taken 144 times.
432 const int p = nb_ch == 1 ? 0 : gbr_order[i];
475 432 out_row[i] = (float *) (out->data[p] + y * out->linesize[p]);
476 }
477
478
2/2
✓ Branch 0 taken 15360 times.
✓ Branch 1 taken 240 times.
15600 for (int x = 0; x < width; x++) {
479 float b[3], a[3], gain[3];
480 15360 get_pixel(s, b, b_row, x, 0);
481 15360 get_pixel(s, a, a_row, x, 1);
482
483 #define GAIN(a, b) (sign * log2f(((a) + alt_off) / ((b) + base_off)))
484
3/4
✓ Branch 0 taken 6144 times.
✓ Branch 1 taken 3072 times.
✓ Branch 2 taken 6144 times.
✗ Branch 3 not taken.
15360 switch (mode) {
485 6144 case GAINMAP_MAXRGB: {
486 6144 const float max_a = fmaxf(fmaxf(a[0], a[1]), a[2]);
487 6144 const float max_b = fmaxf(fmaxf(b[0], b[1]), b[2]);
488 6144 gain[0] = GAIN(max_a, max_b);
489 6144 break;
490 }
491 3072 case GAINMAP_LUMA: {
492 3072 const float y_a = s->rgb2y[0] * a[0] + s->rgb2y[1] * a[1] + s->rgb2y[2] * a[2];
493 3072 const float y_b = s->rgb2y[0] * b[0] + s->rgb2y[1] * b[1] + s->rgb2y[2] * b[2];
494 3072 gain[0] = GAIN(y_a, y_b);
495 3072 break;
496 }
497 6144 case GAINMAP_RGB:
498 6144 gain[0] = GAIN(a[0], b[0]);
499 6144 gain[1] = GAIN(a[1], b[1]);
500 6144 gain[2] = GAIN(a[2], b[2]);
501 6144 break;
502 }
503 #undef GAIN
504
505
2/2
✓ Branch 0 taken 3072 times.
✓ Branch 1 taken 12288 times.
15360 if (quant) {
506
2/2
✓ Branch 0 taken 9216 times.
✓ Branch 1 taken 3072 times.
12288 for (int i = 0; i < nb_ch; i++)
507 9216 out_row[i][x] = quantize(gain[i], scale[i], offset[i], gamma[i]);
508 } else {
509
2/2
✓ Branch 0 taken 18432 times.
✓ Branch 1 taken 12288 times.
30720 for (int i = 0; i < nb_ch; i++) {
510 18432 out_row[i][x] = gain[i];
511 18432 min[i] = fminf(min[i], gain[i]);
512 18432 max[i] = fmaxf(max[i], gain[i]);
513 18432 sum[i] += gain[i];
514 }
515 }
516
517 }
518 }
519
520
4/4
✓ Branch 0 taken 144 times.
✓ Branch 1 taken 9 times.
✓ Branch 2 taken 108 times.
✓ Branch 3 taken 36 times.
153 for (int i = 0; !quant && i < 3; i++) {
521 108 s->slice_min[jobnr][i] = min[i];
522 108 s->slice_max[jobnr][i] = max[i];
523 108 s->slice_sum[jobnr][i] = sum[i];
524 }
525
526 45 return 0;
527 }
528
529 36 static int slice_gain(AVFilterContext *ctx, void *arg, int jobnr, int nb_jobs)
530 {
531 36 return slice_internal(ctx, arg, jobnr, nb_jobs, 0);
532 }
533
534 9 static int slice_gain_quant(AVFilterContext *ctx, void *arg, int jobnr, int nb_jobs)
535 {
536 9 return slice_internal(ctx, arg, jobnr, nb_jobs, 1);
537 }
538
539 36 static int slice_quant(AVFilterContext *ctx, void *arg, int jobnr, int nb_jobs)
540 {
541 36 const ThreadData *td = arg;
542 36 const GainMapContext *s = ctx->priv;
543 36 const AVFrame *out = td->out;
544
545 36 const float *restrict scale = s->quant_scale;
546 36 const float *restrict offset = s->quant_offset;
547 36 const float *restrict gamma = s->quant_gamma;
548
549 36 const int y_start = ff_slice_pos(out->height, jobnr, nb_jobs);
550 36 const int y_end = ff_slice_pos(out->height, jobnr + 1, nb_jobs);
551 36 const int width = out->width;
552 36 const int nb_ch = s->nb_channels;
553
554
2/2
✓ Branch 0 taken 192 times.
✓ Branch 1 taken 36 times.
228 for (int y = y_start; y < y_end; y++) {
555 float *restrict out_row[3];
556
2/2
✓ Branch 0 taken 288 times.
✓ Branch 1 taken 192 times.
480 for (int i = 0; i < nb_ch; i++) {
557
2/2
✓ Branch 0 taken 144 times.
✓ Branch 1 taken 144 times.
288 const int p = nb_ch == 1 ? 0 : gbr_order[i];
558 288 out_row[i] = (float *) (out->data[p] + y * out->linesize[p]);
559 }
560
561
2/2
✓ Branch 0 taken 12288 times.
✓ Branch 1 taken 192 times.
12480 for (int x = 0; x < width; x++) {
562
2/2
✓ Branch 0 taken 18432 times.
✓ Branch 1 taken 12288 times.
30720 for (int i = 0; i < nb_ch; i++)
563 18432 out_row[i][x] = quantize(out_row[i][x], scale[i], offset[i], gamma[i]);
564 }
565 }
566
567 36 return 0;
568 }
569
570 4 static void choose_quant_params(AVFilterContext *ctx, const AVFrame *out, int nb_jobs)
571 {
572 4 GainMapContext *s = ctx->priv;
573 4 float frame_min[3] = { FLT_MAX, FLT_MAX, FLT_MAX };
574 4 float frame_max[3] = { -FLT_MAX, -FLT_MAX, -FLT_MAX };
575 4 double frame_sum[3] = { 0.0, 0.0, 0.0 };
576
577
2/2
✓ Branch 0 taken 36 times.
✓ Branch 1 taken 4 times.
40 for (int j = 0; j < nb_jobs; j++) {
578
2/2
✓ Branch 0 taken 54 times.
✓ Branch 1 taken 36 times.
90 for (int i = 0; i < s->nb_channels; i++) {
579 54 frame_min[i] = fminf(frame_min[i], s->slice_min[j][i]);
580 54 frame_max[i] = fmaxf(frame_max[i], s->slice_max[j][i]);
581 54 frame_sum[i] += s->slice_sum[j][i];
582 }
583 }
584
585
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 4 times.
10 for (int i = 0; i < s->nb_channels; i++) {
586 6 AVRational min = s->gain_min, max = s->gain_max;
587
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 if (s->measure & MEASURE_MIN)
588 6 min = quantq(frame_min[i], AV_ROUND_DOWN);
589
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 if (s->measure & MEASURE_MAX)
590 6 max = quantq(frame_max[i], AV_ROUND_UP);
591
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 if (s->measure & MEASURE_RANGE)
592 6 setup_range(ctx, i, min, max);
593
594
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (!(s->measure & MEASURE_GAMMA))
595 ✗ continue;
596
597 6 const int64_t nb_pixels = (int64_t) out->width * out->height;
598 6 const double mean = frame_sum[i] / nb_pixels;
599 6 const double mean_quant = s->quant_scale[i] * mean + s->quant_offset[i];
600
601 /**
602 * Choose gamma such that mean_quant ^ gamma = 0.5; clamp to a sane
603 * value range of [1/8, 8] to prevent degenerate encodings. A gamma
604 * of >8 would collapse half the encoding space into a single 8-bit
605 * value, at which point we're losing more then gaining, and a value
606 * of <1/8 limits the amount by which a single high outlier pixel
607 * can determine the overall encoding gamma.
608 *
609 * We also clamp the mean to [0.01, 0.99] to prevent numerical explosion
610 * in the case that the mean is very close to 0 or 1. This is a looser
611 * bound than the final gamma clamp, so the exact values chosen do not
612 * matter as much.
613 */
614 6 double gamma = log(0.5) / log(av_clipd(mean_quant, 0.01, 0.99));
615 6 setup_gamma(ctx, i, quantq(av_clipd(gamma, 1/8.0, 8.0), AV_ROUND_NEAR_INF));
616 }
617 4 }
618
619 5 static int process_frame(FFFrameSync *fs)
620 {
621 5 AVFilterContext *ctx = fs->parent;
622 5 GainMapContext *s = ctx->priv;
623 5 AVFilterLink *outlink = ctx->outputs[0];
624 AVFrame *base, *alt, *out;
625
626 5 int ret = ff_framesync_dualinput_get(fs, &base, &alt);
627
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 if (ret < 0)
628 ✗ return ret;
629
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
5 if (!base || !alt)
630 ✗ return AVERROR_BUG;
631
632 5 ret = setup_colorspace(ctx, base, alt);
633
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 if (ret < 0)
634 ✗ return ret;
635
636 5 out = ff_get_video_buffer(outlink, outlink->w, outlink->h);
637
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 if (!out)
638 ✗ return AVERROR(ENOMEM);
639 5 av_frame_copy_props(out, base);
640 5 av_frame_side_data_remove_by_props(&out->side_data, &out->nb_side_data,
641 AV_SIDE_DATA_PROP_COLOR_DEPENDENT);
642
643 5 out->color_trc = AVCOL_TRC_UNSPECIFIED;
644 5 out->color_primaries = AVCOL_PRI_UNSPECIFIED;
645 5 out->colorspace = AVCOL_SPC_UNSPECIFIED;
646 5 out->color_range = AVCOL_RANGE_JPEG;
647
648 5 ThreadData td = {
649 .out = out,
650 .base = base,
651 .alt = alt,
652 };
653
654
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 if (!s->sign) { /* no-op */
655 ✗ for (int i = 0; i < s->nb_channels; i++)
656 ✗ memset(out->data[i], 0, out->height * out->linesize[i]);
657 ✗ goto skip;
658 }
659
660 5 const int nb_jobs = FFMIN(outlink->h, s->nb_threads);
661
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 1 times.
5 ret = ff_filter_execute(ctx, s->measure ? slice_gain : slice_gain_quant,
662 &td, NULL, nb_jobs);
663
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 if (ret < 0)
664 ✗ goto fail;
665
666
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 1 times.
5 if (s->measure) {
667 4 choose_quant_params(ctx, out, nb_jobs);
668 4 ret = ff_filter_execute(ctx, slice_quant, &td, NULL, nb_jobs);
669
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 if (ret < 0)
670 ✗ goto fail;
671 }
672
673 5 skip:;
674 AVGainMapParams *params;
675 5 params = av_gain_map_params_create_side_data(&out->side_data, &out->nb_side_data);
676
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 if (!params) {
677 ✗ ret = AVERROR(ENOMEM);
678 ✗ goto fail;
679 }
680
681 5 *params = s->params;
682
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
5 if (av_gain_map_params_validate(params) < 0)
683 ✗ return AVERROR_BUG; /* should never happen */
684
685 5 return ff_filter_frame(outlink, out);
686
687 ✗ fail:
688 ✗ av_frame_free(&out);
689 ✗ return ret;
690 }
691
692 5 static int config_output(AVFilterLink *outlink)
693 {
694 5 AVFilterContext *ctx = outlink->src;
695 5 GainMapContext *s = ctx->priv;
696 5 AVFilterLink *base = ctx->inputs[0];
697 5 AVFilterLink *alt = ctx->inputs[1];
698 int ret;
699
700
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
5 if (base->w != alt->w || base->h != alt->h) {
701 ✗ av_log(ctx, AV_LOG_ERROR,
702 "Input dimensions must match (%dx%d != %dx%d)\n",
703 base->w, base->h, alt->w, alt->h);
704 ✗ return AVERROR(EINVAL);
705 }
706
707 5 outlink->w = base->w;
708 5 outlink->h = base->h;
709 5 outlink->colorspace = AVCOL_SPC_UNSPECIFIED;
710 5 outlink->color_range = AVCOL_RANGE_JPEG;
711
712 5 s->nb_threads = ff_filter_get_nb_threads(ctx);
713 5 s->slice_min = av_calloc(s->nb_threads, sizeof(*s->slice_min));
714 5 s->slice_max = av_calloc(s->nb_threads, sizeof(*s->slice_max));
715 5 s->slice_sum = av_calloc(s->nb_threads, sizeof(*s->slice_sum));
716
3/6
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 5 times.
5 if (!s->slice_min || !s->slice_max || !s->slice_sum)
717 ✗ return AVERROR(ENOMEM);
718
719 5 ret = ff_framesync_init_dualinput(&s->fs, ctx);
720
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 if (ret < 0)
721 ✗ return ret;
722
723 5 s->fs.on_event = process_frame;
724 5 return ff_framesync_configure(&s->fs);
725 }
726
727 18 static int activate(AVFilterContext *ctx)
728 {
729 18 GainMapContext *s = ctx->priv;
730 18 return ff_framesync_activate(&s->fs);
731 }
732
733 10 static av_cold void uninit(AVFilterContext *ctx)
734 {
735 10 GainMapContext *s = ctx->priv;
736 10 ff_framesync_uninit(&s->fs);
737 10 av_freep(&s->slice_min);
738 10 av_freep(&s->slice_max);
739 10 av_freep(&s->slice_sum);
740 10 }
741
742 static const AVFilterPad gainmap_inputs[] = {
743 {
744 .name = "base",
745 .type = AVMEDIA_TYPE_VIDEO,
746 },
747 {
748 .name = "alternate",
749 .type = AVMEDIA_TYPE_VIDEO,
750 },
751 };
752
753 static const AVFilterPad gainmap_outputs[] = {
754 {
755 .name = "default",
756 .type = AVMEDIA_TYPE_VIDEO,
757 .config_props = config_output,
758 },
759 };
760
761 const FFFilter ff_vf_gainmap = {
762 .p.name = "gainmap",
763 .p.description = NULL_IF_CONFIG_SMALL("Generate a gain map from a base/alternate pair."),
764 .p.priv_class = &gainmap_class,
765 .p.flags = AVFILTER_FLAG_SLICE_THREADS,
766 .preinit = gainmap_framesync_preinit,
767 .priv_size = sizeof(GainMapContext),
768 .init = init,
769 .uninit = uninit,
770 .activate = activate,
771 FILTER_INPUTS(gainmap_inputs),
772 FILTER_OUTPUTS(gainmap_outputs),
773 FILTER_QUERY_FUNC2(query_formats),
774 };
775