FFmpeg coverage


Directory: ../../../ffmpeg/
File: src/libavfilter/vf_colordetect.c
Date: 2026-09-28 04:46:15
Exec Total Coverage
Lines: 0 110 0.0%
Functions: 0 7 0.0%
Branches: 0 50 0.0%

Line Branch Exec Source
1 /*
2 * Copyright (c) 2025 Niklas Haas
3 *
4 * This file is part of FFmpeg.
5 *
6 * FFmpeg is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 2.1 of the License, or (at your option) any later version.
10 *
11 * FFmpeg is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with FFmpeg; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19 */
20
21 /**
22 * @file
23 * Video color space detector, tries to auto-detect YUV range and alpha mode.
24 */
25
26 #include <stdbool.h>
27 #include <stdatomic.h>
28
29 #include "config.h"
30
31 #include "libavutil/mem.h"
32 #include "libavutil/opt.h"
33 #include "libavutil/pixdesc.h"
34
35 #include "avfilter.h"
36 #include "filters.h"
37 #include "formats.h"
38 #include "video.h"
39
40 #include "vf_colordetectdsp.h"
41
42 enum ColorDetectMode {
43 COLOR_DETECT_COLOR_RANGE = 1 << 0,
44 COLOR_DETECT_ALPHA_MODE = 1 << 1,
45 };
46
47 typedef struct ColorDetectContext {
48 const AVClass *class;
49 FFColorDetectDSPContext dsp;
50 unsigned mode;
51 float threshold;
52
53 const AVPixFmtDescriptor *desc;
54 int nb_threads;
55 int depth;
56 int range;
57 int idx_a;
58
59 /* for color range detection only */
60 int mpeg_min;
61 int mpeg_max;
62
63 /* for alpha detection only */
64 int mpeg_range;
65 int offset;
66
67 atomic_int detected_range; // enum AVColorRange
68 atomic_int detected_alpha; // enum FFAlphaDetect
69 } ColorDetectContext;
70
71 #define OFFSET(x) offsetof(ColorDetectContext, x)
72 #define FLAGS AV_OPT_FLAG_VIDEO_PARAM|AV_OPT_FLAG_FILTERING_PARAM
73
74 static const AVOption colordetect_options[] = {
75 { "mode", "Image properties to detect", OFFSET(mode), AV_OPT_TYPE_FLAGS, {.i64 = -1}, 0, UINT_MAX, FLAGS, .unit = "mode" },
76 { "color_range", "Detect (YUV) color range", 0, AV_OPT_TYPE_CONST, {.i64 = COLOR_DETECT_COLOR_RANGE}, 0, 0, FLAGS, .unit = "mode" },
77 { "alpha_mode", "Detect alpha mode", 0, AV_OPT_TYPE_CONST, {.i64 = COLOR_DETECT_ALPHA_MODE }, 0, 0, FLAGS, .unit = "mode" },
78 { "all", "Detect all supported properties", 0, AV_OPT_TYPE_CONST, {.i64 = -1}, 0, 0, FLAGS, .unit = "mode" },
79
80 /* Note: threshold should not be increased past ~0.4 as it overflows 8-bit SIMD otherwise */
81 { "threshold", "Detection threshold, as a fraction of the full range", OFFSET(threshold), AV_OPT_TYPE_FLOAT, {.dbl = 0.001}, 0.0, 0.05, FLAGS },
82 { NULL }
83 };
84
85 AVFILTER_DEFINE_CLASS(colordetect);
86
87 ✗ static int query_format(const AVFilterContext *ctx,
88 AVFilterFormatsConfig **cfg_in,
89 AVFilterFormatsConfig **cfg_out)
90 {
91 ✗ int want_flags = AV_PIX_FMT_FLAG_PLANAR;
92 ✗ int reject_flags = AV_PIX_FMT_FLAG_PAL | AV_PIX_FMT_FLAG_HWACCEL |
93 AV_PIX_FMT_FLAG_BITSTREAM | AV_PIX_FMT_FLAG_FLOAT |
94 AV_PIX_FMT_FLAG_BAYER | AV_PIX_FMT_FLAG_XYZ;
95
96 if (HAVE_BIGENDIAN) {
97 want_flags |= AV_PIX_FMT_FLAG_BE;
98 } else {
99 ✗ reject_flags |= AV_PIX_FMT_FLAG_BE;
100 }
101
102 ✗ AVFilterFormats *formats = ff_formats_pixdesc_filter(want_flags, reject_flags);
103 ✗ return ff_set_common_formats2(ctx, cfg_in, cfg_out, formats);
104 }
105
106 ✗ static int config_input(AVFilterLink *inlink)
107 {
108 ✗ AVFilterContext *ctx = inlink->dst;
109 ✗ ColorDetectContext *s = ctx->priv;
110 ✗ const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(inlink->format);
111 ✗ const int depth = desc->comp[0].depth;
112 ✗ const int range = (1 << depth) - 1;
113 ✗ const int threshold = lrintf(s->threshold * range);
114 ✗ const int mpeg_min = 16 << (depth - 8);
115 ✗ const int mpeg_max = 235 << (depth - 8);
116 ✗ if (depth > 16) /* not currently possible; prevent future bugs */
117 ✗ return AVERROR(ENOTSUP);
118
119 ✗ s->desc = desc;
120 ✗ s->depth = depth;
121 ✗ s->range = range;
122 ✗ s->nb_threads = ff_filter_get_nb_threads(ctx);
123
124 /* Color range detection: */
125 ✗ s->mpeg_min = av_clip_uintp2(mpeg_min - threshold, depth);
126 ✗ s->mpeg_max = av_clip_uintp2(mpeg_max + threshold, depth);
127
128 /**
129 * Alpha mode detection:
130 *
131 * To check if a value is out of range, we need to compare the color value
132 * against the maximum possible color for a given alpha value.
133 * x > ((mpeg_max - mpeg_min) / pixel_max) * a + mpeg_min + threshold
134 *
135 * This simplifies to:
136 * (x - mpeg_min - threshold) * pixel_max > (mpeg_max - mpeg_min) * a
137 * = range * x - offset > mpeg_range * a in the below formula.
138 *
139 * We subtract an additional offset of (1 << (depth - 1)) to account for
140 * rounding errors in the value of `x`.
141 *
142 * For full range input this degenerates to `x > a + threshold`, so the
143 * threshold is passed through directly, without the `range` scaling.
144 */
145 ✗ if (inlink->color_range == AVCOL_RANGE_JPEG) {
146 ✗ s->mpeg_range = range;
147 ✗ s->offset = threshold;
148 } else {
149 ✗ s->mpeg_range = mpeg_max - mpeg_min;
150 ✗ s->offset = range * (mpeg_min + threshold) + (1 << (depth - 1));
151 }
152
153 ✗ if (desc->flags & AV_PIX_FMT_FLAG_RGB) {
154 ✗ atomic_init(&s->detected_range, AVCOL_RANGE_JPEG);
155 } else {
156 ✗ atomic_init(&s->detected_range, AVCOL_RANGE_UNSPECIFIED);
157 }
158
159 ✗ if (desc->flags & AV_PIX_FMT_FLAG_ALPHA) {
160 ✗ s->idx_a = desc->comp[desc->nb_components - 1].plane;
161 ✗ atomic_init(&s->detected_alpha, FF_ALPHA_UNDETERMINED);
162 } else {
163 ✗ atomic_init(&s->detected_alpha, FF_ALPHA_NONE);
164 }
165
166 ✗ ff_color_detect_dsp_init(&s->dsp, depth, s->offset, inlink->color_range);
167 ✗ return 0;
168 }
169
170 ✗ static int detect_range(AVFilterContext *ctx, void *arg,
171 int jobnr, int nb_jobs)
172 {
173 ✗ ColorDetectContext *s = ctx->priv;
174 ✗ const AVFrame *in = arg;
175 ✗ const ptrdiff_t stride = in->linesize[0];
176 ✗ const int y_start = ff_slice_pos(in->height, jobnr, nb_jobs);
177 ✗ const int y_end = ff_slice_pos(in->height, jobnr + 1, nb_jobs);
178 ✗ const int h_slice = y_end - y_start;
179
180 ✗ if (s->dsp.detect_range(in->data[0] + y_start * stride, stride,
181 ✗ in->width, h_slice, s->mpeg_min, s->mpeg_max))
182 ✗ atomic_store_explicit(&s->detected_range, AVCOL_RANGE_JPEG,
183 memory_order_relaxed);
184
185 ✗ return 0;
186 }
187
188 ✗ static int detect_alpha(AVFilterContext *ctx, void *arg,
189 int jobnr, int nb_jobs)
190 {
191 ✗ ColorDetectContext *s = ctx->priv;
192 ✗ const AVFrame *in = arg;
193 ✗ const int w = in->width;
194 ✗ const int h = in->height;
195 ✗ const int y_start = ff_slice_pos(h, jobnr, nb_jobs);
196 ✗ const int y_end = ff_slice_pos(h, jobnr + 1, nb_jobs);
197 ✗ const int h_slice = y_end - y_start;
198
199 ✗ const int nb_planes = (s->desc->flags & AV_PIX_FMT_FLAG_RGB) ? 3 : 1;
200 ✗ const ptrdiff_t alpha_stride = in->linesize[s->idx_a];
201 ✗ const uint8_t *alpha = in->data[s->idx_a] + y_start * alpha_stride;
202
203 ✗ int ret = 0;
204 ✗ for (int i = 0; i < nb_planes; i++) {
205 ✗ const ptrdiff_t stride = in->linesize[i];
206 ✗ ret = s->dsp.detect_alpha(in->data[i] + y_start * stride, stride,
207 alpha, alpha_stride, w, h_slice, s->range,
208 s->mpeg_range, s->offset);
209 ✗ ret |= atomic_fetch_or_explicit(&s->detected_alpha, ret, memory_order_relaxed);
210 ✗ if (ret == FF_ALPHA_STRAIGHT)
211 ✗ break;
212 }
213
214 ✗ return 0;
215 }
216
217 ✗ static int filter_frame(AVFilterLink *inlink, AVFrame *in)
218 {
219 ✗ AVFilterContext *ctx = inlink->dst;
220 ✗ ColorDetectContext *s = ctx->priv;
221 ✗ const int nb_threads = FFMIN(inlink->h, s->nb_threads);
222
223 ✗ enum AVColorRange detected_range = atomic_load_explicit(&s->detected_range, memory_order_relaxed);
224 ✗ if (s->mode & COLOR_DETECT_COLOR_RANGE && detected_range == AVCOL_RANGE_UNSPECIFIED)
225 ✗ ff_filter_execute(ctx, detect_range, in, NULL, nb_threads);
226
227 ✗ enum FFAlphaDetect detected_alpha = atomic_load_explicit(&s->detected_alpha, memory_order_relaxed);
228 ✗ if (s->mode & COLOR_DETECT_ALPHA_MODE && detected_alpha != FF_ALPHA_NONE &&
229 detected_alpha != FF_ALPHA_STRAIGHT)
230 ✗ ff_filter_execute(ctx, detect_alpha, in, NULL, nb_threads);
231
232 ✗ return ff_filter_frame(inlink->dst->outputs[0], in);
233 }
234
235 ✗ static av_cold void report_detected_props(AVFilterContext *ctx)
236 {
237 ✗ ColorDetectContext *s = ctx->priv;
238 ✗ if (!s->mode)
239 ✗ return;
240
241 ✗ av_log(ctx, AV_LOG_INFO, "Detected color properties:\n");
242 ✗ if (s->mode & COLOR_DETECT_COLOR_RANGE) {
243 ✗ enum AVColorRange detected_range = atomic_load_explicit(&s->detected_range,
244 memory_order_relaxed);
245 ✗ av_log(ctx, AV_LOG_INFO, " Color range: %s\n",
246 detected_range == AVCOL_RANGE_JPEG ? "JPEG / full range"
247 : "undetermined");
248 }
249
250 ✗ if (s->mode & COLOR_DETECT_ALPHA_MODE) {
251 ✗ enum FFAlphaDetect detected_alpha = atomic_load_explicit(&s->detected_alpha,
252 memory_order_relaxed);
253 ✗ av_log(ctx, AV_LOG_INFO, " Alpha mode: %s\n",
254 detected_alpha == FF_ALPHA_NONE ? "none" :
255 ✗ detected_alpha == FF_ALPHA_STRAIGHT ? "straight" :
256 detected_alpha == FF_ALPHA_TRANSPARENT ? "undetermined"
257 ✗ : "opaque");
258 }
259 }
260
261 ✗ static int activate(AVFilterContext *ctx)
262 {
263 ✗ AVFilterLink *inlink = ctx->inputs[0];
264 ✗ AVFilterLink *outlink = ctx->outputs[0];
265 AVFrame *frame;
266 int64_t pts;
267 int ret;
268
269 ✗ ret = ff_outlink_get_status(outlink);
270 ✗ if (ret) {
271 ✗ ff_inlink_set_status(inlink, ret);
272 ✗ report_detected_props(ctx);
273 ✗ return 0;
274 }
275
276 ✗ ret = ff_inlink_consume_frame(inlink, &frame);
277 ✗ if (ret < 0) {
278 ✗ return ret;
279 ✗ } else if (ret) {
280 ✗ return filter_frame(inlink, frame);
281 }
282
283 ✗ if (ff_inlink_acknowledge_status(inlink, &ret, &pts)) {
284 ✗ ff_outlink_set_status(outlink, ret, pts);
285 ✗ report_detected_props(ctx);
286 ✗ return 0;
287 }
288
289 ✗ FF_FILTER_FORWARD_WANTED(outlink, inlink);
290 ✗ return FFERROR_NOT_READY;
291 }
292
293 static const AVFilterPad colordetect_inputs[] = {
294 {
295 .name = "default",
296 .type = AVMEDIA_TYPE_VIDEO,
297 .config_props = config_input,
298 },
299 };
300
301 const FFFilter ff_vf_colordetect = {
302 .p.name = "colordetect",
303 .p.description = NULL_IF_CONFIG_SMALL("Detect video color properties."),
304 .p.priv_class = &colordetect_class,
305 .p.flags = AVFILTER_FLAG_SLICE_THREADS | AVFILTER_FLAG_METADATA_ONLY,
306 .priv_size = sizeof(ColorDetectContext),
307 FILTER_INPUTS(colordetect_inputs),
308 FILTER_OUTPUTS(ff_video_default_filterpad),
309 FILTER_QUERY_FUNC2(query_format),
310 .activate = activate,
311 };
312