[FFmpeg-devel] [PATCH v3 1/3] avfilter/vf_colordetect: add new color range detection filter
Kacper Michajlow
kasper93 at gmail.com
Fri Jul 18 15:38:04 EEST 2025
On Fri, 18 Jul 2025 at 11:57, Niklas Haas <ffmpeg at haasn.xyz> wrote:
>
> From: Niklas Haas <git at haasn.dev>
>
> This filter can detect various properties about the image, including
> whether or not there are out-of-range values, or whether the input appears
> to use straight or premultiplied alpha.
>
> Of course, these can only be heuristics, with "undetermined" as the base
> case. While we can definitely prove the existence of full range or
> straight alpha colors, we can never infer the opposite.
> ---
> doc/filters.texi | 27 ++++
> libavfilter/Makefile | 1 +
> libavfilter/allfilters.c | 1 +
> libavfilter/vf_colordetect.c | 252 +++++++++++++++++++++++++++++++++++
> libavfilter/vf_colordetect.h | 149 +++++++++++++++++++++
> 5 files changed, 430 insertions(+)
> create mode 100644 libavfilter/vf_colordetect.c
> create mode 100644 libavfilter/vf_colordetect.h
>
> diff --git a/doc/filters.texi b/doc/filters.texi
> index ed2956fe75..74e9e71559 100644
> --- a/doc/filters.texi
> +++ b/doc/filters.texi
> @@ -9753,6 +9753,33 @@ colorchannelmixer=.393:.769:.189:0:.349:.686:.168:0:.272:.534:.131
>
> This filter supports the all above options as @ref{commands}.
>
> + at section colordetect
> +Analyze the video frames to determine the effective value range and alpha
> +mode.
> +
> +The filter accepts the following options:
> +
> + at table @option
> + at item mode
> +Set of properties to detect. Unavailable properties, such as alpha mode for
> +an input image without an alpha channel, will be ignored automatically.
> +
> +Accepts a combination of the following flags:
> +
> + at table @samp
> + at item color_range
> +Detect if the source countains luma pixels outside the limited (MPEG) range,
> +which indicates that this is a full range YUV source.
> + at item alpha_mode
> +Detect if the source contains color values above the alpha channel, which
> +indicates that the alpha channel is independent (straight), rather than
> +premultiplied.
> + at item all
> +Enable detection of all of the above properties. This is the default.
> + at end table
> +
> + at end table
> +
> @section colorize
> Overlay a solid color on the video stream.
>
> diff --git a/libavfilter/Makefile b/libavfilter/Makefile
> index 9e9153f5b0..e19f67a3a7 100644
> --- a/libavfilter/Makefile
> +++ b/libavfilter/Makefile
> @@ -237,6 +237,7 @@ OBJS-$(CONFIG_COLORBALANCE_FILTER) += vf_colorbalance.o
> OBJS-$(CONFIG_COLORCHANNELMIXER_FILTER) += vf_colorchannelmixer.o
> OBJS-$(CONFIG_COLORCONTRAST_FILTER) += vf_colorcontrast.o
> OBJS-$(CONFIG_COLORCORRECT_FILTER) += vf_colorcorrect.o
> +OBJS-$(CONFIG_COLORDETECT_FILTER) += vf_colordetect.o
> OBJS-$(CONFIG_COLORIZE_FILTER) += vf_colorize.o
> OBJS-$(CONFIG_COLORKEY_FILTER) += vf_colorkey.o
> OBJS-$(CONFIG_COLORKEY_OPENCL_FILTER) += vf_colorkey_opencl.o opencl.o \
> diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c
> index 409099bf1f..f3c2092b15 100644
> --- a/libavfilter/allfilters.c
> +++ b/libavfilter/allfilters.c
> @@ -218,6 +218,7 @@ extern const FFFilter ff_vf_colorbalance;
> extern const FFFilter ff_vf_colorchannelmixer;
> extern const FFFilter ff_vf_colorcontrast;
> extern const FFFilter ff_vf_colorcorrect;
> +extern const FFFilter ff_vf_colordetect;
> extern const FFFilter ff_vf_colorize;
> extern const FFFilter ff_vf_colorkey;
> extern const FFFilter ff_vf_colorkey_opencl;
> diff --git a/libavfilter/vf_colordetect.c b/libavfilter/vf_colordetect.c
> new file mode 100644
> index 0000000000..0fb892634f
> --- /dev/null
> +++ b/libavfilter/vf_colordetect.c
> @@ -0,0 +1,252 @@
> +/*
> + * Copyright (c) 2025 Niklas Haas
> + *
> + * 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
> + * Video color space detector, tries to auto-detect YUV range and alpha mode.
> + */
> +
> +#include <stdbool.h>
> +#include <stdatomic.h>
> +
> +#include "config.h"
> +
> +#include "libavutil/mem.h"
> +#include "libavutil/opt.h"
> +#include "libavutil/pixdesc.h"
> +
> +#include "avfilter.h"
> +#include "filters.h"
> +#include "formats.h"
> +#include "video.h"
> +
> +#include "vf_colordetect.h"
> +
> +enum AlphaMode {
> + ALPHA_NONE = -1,
> + ALPHA_UNDETERMINED = 0,
> + ALPHA_STRAIGHT,
> + /* No way to positively identify premultiplied alpha */
> +};
> +
> +enum ColorDetectMode {
> + COLOR_DETECT_COLOR_RANGE = 1 << 0,
> + COLOR_DETECT_ALPHA_MODE = 1 << 1,
> +};
> +
> +typedef struct ColorDetectContext {
> + const AVClass *class;
> + FFColorDetectDSPContext dsp;
> + unsigned mode;
> +
> + const AVPixFmtDescriptor *desc;
> + int nb_threads;
> + int depth;
> + int idx_a;
> + int mpeg_min;
> + int mpeg_max;
> +
> + atomic_int detected_range; // enum AVColorRange
> + atomic_int detected_alpha; // enum AlphaMode
> +} ColorDetectContext;
> +
> +#define OFFSET(x) offsetof(ColorDetectContext, x)
> +#define FLAGS AV_OPT_FLAG_VIDEO_PARAM|AV_OPT_FLAG_FILTERING_PARAM
> +
> +static const AVOption colordetect_options[] = {
> + { "mode", "Image properties to detect", OFFSET(mode), AV_OPT_TYPE_FLAGS, {.i64 = -1}, 0, UINT_MAX, FLAGS, .unit = "mode" },
> + { "color_range", "Detect (YUV) color range", 0, AV_OPT_TYPE_CONST, {.i64 = COLOR_DETECT_COLOR_RANGE}, 0, 0, FLAGS, .unit = "mode" },
> + { "alpha_mode", "Detect alpha mode", 0, AV_OPT_TYPE_CONST, {.i64 = COLOR_DETECT_ALPHA_MODE }, 0, 0, FLAGS, .unit = "mode" },
> + { "all", "Detect all supported properties", 0, AV_OPT_TYPE_CONST, {.i64 = -1}, 0, 0, FLAGS, .unit = "mode" },
> + { NULL }
> +};
> +
> +AVFILTER_DEFINE_CLASS(colordetect);
> +
> +static int query_format(const AVFilterContext *ctx,
> + AVFilterFormatsConfig **cfg_in,
> + AVFilterFormatsConfig **cfg_out)
> +{
> + int want_flags = AV_PIX_FMT_FLAG_PLANAR;
> + int reject_flags = AV_PIX_FMT_FLAG_PAL | AV_PIX_FMT_FLAG_HWACCEL |
> + AV_PIX_FMT_FLAG_BITSTREAM | AV_PIX_FMT_FLAG_FLOAT |
> + AV_PIX_FMT_FLAG_BAYER | AV_PIX_FMT_FLAG_XYZ;
> +
> + if (HAVE_BIGENDIAN) {
> + want_flags |= AV_PIX_FMT_FLAG_BE;
> + } else {
> + reject_flags |= AV_PIX_FMT_FLAG_BE;
> + }
> +
> + AVFilterFormats *formats = ff_formats_pixdesc_filter(want_flags, reject_flags);
> + return ff_set_common_formats2(ctx, cfg_in, cfg_out, formats);
> +}
> +
> +static int config_input(AVFilterLink *inlink)
> +{
> + AVFilterContext *ctx = inlink->dst;
> + ColorDetectContext *s = ctx->priv;
> + const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(inlink->format);
> + const int depth = desc->comp[0].depth;
> + const int mpeg_min = 16 << (depth - 8);
> + const int mpeg_max = 235 << (depth - 8);
> + if (depth > 16) /* not currently possible; prevent future bugs */
> + return AVERROR(ENOTSUP);
> +
> + s->desc = desc;
> + s->depth = depth;
> + s->mpeg_min = mpeg_min;
> + s->mpeg_max = mpeg_max;
> + s->nb_threads = ff_filter_get_nb_threads(ctx);
> +
> + if (desc->flags & AV_PIX_FMT_FLAG_RGB) {
> + atomic_init(&s->detected_range, AVCOL_RANGE_JPEG);
> + } else {
> + atomic_init(&s->detected_range, AVCOL_RANGE_UNSPECIFIED);
> + }
> +
> + if (desc->flags & AV_PIX_FMT_FLAG_ALPHA) {
> + s->idx_a = desc->comp[desc->nb_components - 1].plane;
> + atomic_init(&s->detected_alpha, ALPHA_UNDETERMINED);
> + } else {
> + atomic_init(&s->detected_alpha, ALPHA_NONE);
> + }
> +
> + ff_color_detect_dsp_init(&s->dsp, depth, inlink->color_range);
> + return 0;
> +}
> +
> +static int detect_range(AVFilterContext *ctx, void *arg,
> + int jobnr, int nb_jobs)
> +{
> + ColorDetectContext *s = ctx->priv;
> + const AVFrame *in = arg;
> + const ptrdiff_t stride = in->linesize[0];
> + const int y_start = (in->height * jobnr) / nb_jobs;
> + const int y_end = (in->height * (jobnr + 1)) / nb_jobs;
> + const int h_slice = y_end - y_start;
> +
> + if (s->dsp.detect_range(in->data[0] + y_start * stride, stride,
> + in->width, h_slice, s->mpeg_min, s->mpeg_max))
> + atomic_store(&s->detected_range, AVCOL_RANGE_JPEG);
> +
> + return 0;
> +}
> +
> +static int detect_alpha(AVFilterContext *ctx, void *arg,
> + int jobnr, int nb_jobs)
> +{
> + ColorDetectContext *s = ctx->priv;
> + const AVFrame *in = arg;
> + const int w = in->width;
> + const int h = in->height;
> + const int y_start = (h * jobnr) / nb_jobs;
> + const int y_end = (h * (jobnr + 1)) / nb_jobs;
> + const int h_slice = y_end - y_start;
> +
> + const int nb_planes = (s->desc->flags & AV_PIX_FMT_FLAG_RGB) ? 3 : 1;
> + const ptrdiff_t alpha_stride = in->linesize[s->idx_a];
> + const uint8_t *alpha = in->data[s->idx_a] + y_start * alpha_stride;
> +
> + const int p = (1 << s->depth) - 1;
> + const int q = s->mpeg_max - s->mpeg_min;
> + const int k = s->mpeg_min * p + 128;
> +
> + for (int i = 0; i < nb_planes; i++) {
> + const ptrdiff_t stride = in->linesize[i];
> + if (s->dsp.detect_alpha(in->data[i] + y_start * stride, stride,
> + alpha, alpha_stride, w, h_slice, p, q, k)) {
> + atomic_store(&s->detected_alpha, ALPHA_STRAIGHT);
> + return 0;
> + }
> + }
> +
> + return 0;
> +}
> +
> +static int filter_frame(AVFilterLink *inlink, AVFrame *in)
> +{
> + AVFilterContext *ctx = inlink->dst;
> + ColorDetectContext *s = ctx->priv;
> + const int nb_threads = FFMIN(inlink->h, s->nb_threads);
> +
> + if (s->mode & COLOR_DETECT_COLOR_RANGE && s->detected_range == AVCOL_RANGE_UNSPECIFIED)
> + ff_filter_execute(ctx, detect_range, in, NULL, nb_threads);
> + if (s->mode & COLOR_DETECT_ALPHA_MODE && s->detected_alpha == ALPHA_UNDETERMINED)
> + ff_filter_execute(ctx, detect_alpha, in, NULL, nb_threads);
> +
> + return ff_filter_frame(inlink->dst->outputs[0], in);
> +}
> +
> +static av_cold void uninit(AVFilterContext *ctx)
> +{
> + ColorDetectContext *s = ctx->priv;
> + if (!s->mode)
> + return;
> +
> + av_log(ctx, AV_LOG_INFO, "Detected color properties:\n");
> + if (s->mode & COLOR_DETECT_COLOR_RANGE) {
> + av_log(ctx, AV_LOG_INFO, " Color range: %s\n",
> + s->detected_range == AVCOL_RANGE_JPEG ? "JPEG / full range"
> + : "undetermined");
> + }
> +
> + if (s->mode & COLOR_DETECT_ALPHA_MODE) {
> + av_log(ctx, AV_LOG_INFO, " Alpha mode: %s\n",
> + s->detected_alpha == ALPHA_NONE ? "none" :
> + s->detected_alpha == ALPHA_STRAIGHT ? "straight / independent"
> + : "undetermined");
> + }
> +}
> +
> +av_cold void ff_color_detect_dsp_init(FFColorDetectDSPContext *dsp, int depth,
> + enum AVColorRange color_range)
> +{
> + if (!dsp->detect_range)
> + dsp->detect_range = depth > 8 ? ff_detect_range16_c : ff_detect_range_c;
> + if (!dsp->detect_alpha) {
> + if (color_range == AVCOL_RANGE_JPEG) {
> + dsp->detect_alpha = depth > 8 ? ff_detect_alpha16_full_c : ff_detect_alpha_full_c;
> + } else {
> + dsp->detect_alpha = depth > 8 ? ff_detect_alpha16_limited_c : ff_detect_alpha_limited_c;
> + }
> + }
> +}
> +
> +static const AVFilterPad colordetect_inputs[] = {
> + {
> + .name = "default",
> + .type = AVMEDIA_TYPE_VIDEO,
> + .config_props = config_input,
> + .filter_frame = filter_frame,
> + },
> +};
> +
> +const FFFilter ff_vf_colordetect = {
> + .p.name = "colordetect",
> + .p.description = NULL_IF_CONFIG_SMALL("Detect video color properties."),
> + .p.priv_class = &colordetect_class,
> + .p.flags = AVFILTER_FLAG_SLICE_THREADS | AVFILTER_FLAG_METADATA_ONLY,
> + .priv_size = sizeof(ColorDetectContext),
> + FILTER_INPUTS(colordetect_inputs),
> + FILTER_OUTPUTS(ff_video_default_filterpad),
> + FILTER_QUERY_FUNC2(query_format),
> + .uninit = uninit,
> +};
> diff --git a/libavfilter/vf_colordetect.h b/libavfilter/vf_colordetect.h
> new file mode 100644
> index 0000000000..8998ed83d4
> --- /dev/null
> +++ b/libavfilter/vf_colordetect.h
> @@ -0,0 +1,149 @@
> +/*
> + * 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
> + */
> +
> +#ifndef AVFILTER_VF_COLORDETECT_H
> +#define AVFILTER_VF_COLORDETECT_H
> +
> +#include <stddef.h>
> +#include <stdint.h>
> +
> +#include <libavutil/macros.h>
> +#include <libavutil/pixfmt.h>
> +
> +typedef struct FFColorDetectDSPContext {
> + /* Returns 1 if an out-of-range value was detected, 0 otherwise */
> + int (*detect_range)(const uint8_t *data, ptrdiff_t stride,
> + ptrdiff_t width, ptrdiff_t height,
> + int mpeg_min, int mpeg_max);
> +
> + /* Returns 1 if the color value exceeds the alpha value, 0 otherwise */
> + int (*detect_alpha)(const uint8_t *color, ptrdiff_t color_stride,
> + const uint8_t *alpha, ptrdiff_t alpha_stride,
> + ptrdiff_t width, ptrdiff_t height,
> + int p, int q, int k);
> +} FFColorDetectDSPContext;
> +
> +void ff_color_detect_dsp_init(FFColorDetectDSPContext *dsp, int depth,
> + enum AVColorRange color_range);
> +
> +static inline int ff_detect_range_c(const uint8_t *data, ptrdiff_t stride,
> + ptrdiff_t width, ptrdiff_t height,
> + int mpeg_min, int mpeg_max)
> +{
> + while (height--) {
> + for (int x = 0; x < width; x++) {
> + const uint8_t val = data[x];
> + if (val < mpeg_min || val > mpeg_max)
> + return 1;
> + }
> + data += stride;
> + }
> +
> + return 0;
> +}
You could process width as a whole to allow better vectorization.
Assuming you don't process 10000x1 images, it will be faster on average.
Before (clang --march=znver4):
detect_range_8_c: 5264.6 ( 1.00x)
detect_range_8_avx2: 124.5 (42.30x)
detect_range_8_avx512: 121.6 (43.31x)
After (clang --march=znver4):
detect_range_8_c: 211.5 ( 1.00x)
detect_range_8_avx2: 136.4 ( 1.55x)
detect_range_8_avx512: 95.4 ( 2.22x)
static inline int ff_detect_range_c(const uint8_t *data, ptrdiff_t stride,
ptrdiff_t width, ptrdiff_t height,
- int mpeg_min, int mpeg_max)
+ uint8_t mpeg_min, uint8_t mpeg_max)
{
while (height--) {
+ bool out_of_range = false;
for (int x = 0; x < width; x++) {
const uint8_t val = data[x];
- if (val < mpeg_min || val > mpeg_max)
- return 1;
+ out_of_range |= val < mpeg_min || val > mpeg_max;
}
+ if (out_of_range)
+ return 1;
data += stride;
}
- Kacper
> +
> +static inline int ff_detect_range16_c(const uint8_t *data, ptrdiff_t stride,
> + ptrdiff_t width, ptrdiff_t height,
> + int mpeg_min, int mpeg_max)
> +{
> + while (height--) {
> + const uint16_t *data16 = (const uint16_t *) data;
> + for (int x = 0; x < width; x++) {
> + const uint16_t val = data16[x];
> + if (val < mpeg_min || val > mpeg_max)
> + return 1;
> + }
> + data += stride;
> + }
> +
> + return 0;
> +}
> +
> +static inline int
> +ff_detect_alpha_full_c(const uint8_t *color, ptrdiff_t color_stride,
> + const uint8_t *alpha, ptrdiff_t alpha_stride,
> + ptrdiff_t width, ptrdiff_t height,
> + int p, int q, int k)
> +{
> + while (height--) {
> + for (int x = 0; x < width; x++) {
> + if (color[x] > alpha[x])
> + return 1;
> + }
> + color += color_stride;
> + alpha += alpha_stride;
> + }
> + return 0;
> +}
> +
> +static inline int
> +ff_detect_alpha_limited_c(const uint8_t *color, ptrdiff_t color_stride,
> + const uint8_t *alpha, ptrdiff_t alpha_stride,
> + ptrdiff_t width, ptrdiff_t height,
> + int p, int q, int k)
> +{
> + while (height--) {
> + for (int x = 0; x < width; x++) {
> + if (p * color[x] - k > q * alpha[x])
> + return 1;
> + }
> + color += color_stride;
> + alpha += alpha_stride;
> + }
> + return 0;
> +}
> +
> +static inline int
> +ff_detect_alpha16_full_c(const uint8_t *color, ptrdiff_t color_stride,
> + const uint8_t *alpha, ptrdiff_t alpha_stride,
> + ptrdiff_t width, ptrdiff_t height,
> + int p, int q, int k)
> +{
> + while (height--) {
> + const uint16_t *color16 = (const uint16_t *) color;
> + const uint16_t *alpha16 = (const uint16_t *) alpha;
> + for (int x = 0; x < width; x++) {
> + if (color16[x] > alpha16[x])
> + return 1;
> + }
> + color += color_stride;
> + alpha += alpha_stride;
> + }
> + return 0;
> +}
> +
> +static inline int
> +ff_detect_alpha16_limited_c(const uint8_t *color, ptrdiff_t color_stride,
> + const uint8_t *alpha, ptrdiff_t alpha_stride,
> + ptrdiff_t width, ptrdiff_t height,
> + int p, int q, int k)
> +{
> + while (height--) {
> + const uint16_t *color16 = (const uint16_t *) color;
> + const uint16_t *alpha16 = (const uint16_t *) alpha;
> + for (int x = 0; x < width; x++) {
> + if ((int64_t) p * color16[x] - k > (int64_t) q * alpha16[x])
> + return 1;
> + }
> + color += color_stride;
> + alpha += alpha_stride;
> + }
> + return 0;
> +}
> +
> +#endif /* AVFILTER_VF_COLORDETECT_H */
> --
> 2.50.1
>
> _______________________________________________
> ffmpeg-devel mailing list
> ffmpeg-devel at ffmpeg.org
> https://ffmpeg.org/mailman/listinfo/ffmpeg-devel
>
> To unsubscribe, visit link above, or email
> ffmpeg-devel-request at ffmpeg.org with subject "unsubscribe".
More information about the ffmpeg-devel
mailing list