On Fri, 18 Jul 2025 at 11:57, Niklas Haas <ffm...@haasn.xyz> wrote:
>
> From: Niklas Haas <g...@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}.
>
> +@section colordetect
> +Analyze the video frames to determine the effective value range and alpha
> +mode.
> +
> +The filter accepts the following options:
> +
> +@table @option
> +@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:
> +
> +@table @samp
> +@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.
> +@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.
> +@item all
> +Enable detection of all of the above properties. This is the default.
> +@end table
> +
> +@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@ffmpeg.org
> https://ffmpeg.org/mailman/listinfo/ffmpeg-devel
>
> To unsubscribe, visit link above, or email
> ffmpeg-devel-requ...@ffmpeg.org with subject "unsubscribe".
_______________________________________________
ffmpeg-devel mailing list
ffmpeg-devel@ffmpeg.org
https://ffmpeg.org/mailman/listinfo/ffmpeg-devel

To unsubscribe, visit link above, or email
ffmpeg-devel-requ...@ffmpeg.org with subject "unsubscribe".

Reply via email to