PR #24181 opened by Niklas Haas (haasn)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24181
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24181.patch

Enabled by default for 10-bit content (one point of deviation), disabled for 
8-bit.


>From 91b14f9a80848baa3c319d78114aaf7f2c165fff Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Mon, 17 Aug 2026 12:29:59 +0200
Subject: [PATCH 1/4] avfilter/vf_colordetect: add support for full range alpha
 offset

This allows us to add a threshold for alpha comparisons to the full range
path as well.

The AArch64 changes were LLM-assisted, since I'm not familiar with that
architecture. However, the logic checks out to me.

Also adjusts the checkasm test to make sure there are no rounding differences
between the C and assembly implementations. Tested correct across 100k seeds
on x86.

Sponsored-by: nxtedition AB
Signed-off-by: Niklas Haas <[email protected]>
---
 libavfilter/aarch64/vf_colordetect_neon.S | 12 +++++++++++
 libavfilter/vf_colordetectdsp.h           |  4 ++--
 libavfilter/x86/vf_colordetect.asm        |  2 ++
 tests/checkasm/vf_colordetect.c           | 25 +++++++++++++++++------
 4 files changed, 35 insertions(+), 8 deletions(-)

diff --git a/libavfilter/aarch64/vf_colordetect_neon.S 
b/libavfilter/aarch64/vf_colordetect_neon.S
index f3cca16fed..ada21d4ba4 100644
--- a/libavfilter/aarch64/vf_colordetect_neon.S
+++ b/libavfilter/aarch64/vf_colordetect_neon.S
@@ -177,13 +177,17 @@ endfunc
  * x4: ptrdiff_t width,
  * x5: ptrdiff_t height,
  * w6: int alpha_max,
+ * w7: int mpeg_range (unused)
+ * [sp]: int offset
  */
 function ff_detect_alpha_full_neon, export=1
+        ldr             w13, [sp]
         ands            x7, x4, #15             // width % 16
         bic             x8, x4, #15             // width / 16 * 16
         movi            v0.16b, #0
         movi            v1.16b, #255
         dup             v2.16b, w6              // alpha_max
+        dup             v18.16b, w13            // offset
         sub             x1, x1, x8              // color_stride - aligned_width
         sub             x3, x3, x8              // alpha_stride - aligned_width
         b.eq            1f
@@ -197,6 +201,7 @@ function ff_detect_alpha_full_neon, export=1
         ldr             q5, [x0], #16
         ldr             q6, [x2], #16
         subs            x12, x12, #16
+        uqsub           v5.16b, v5.16b, v18.16b // color - offset
         cmhi            v7.16b, v5.16b, v6.16b
         cmeq            v16.16b, v6.16b, v2.16b
         orr             v0.16b, v0.16b, v7.16b
@@ -207,6 +212,7 @@ function ff_detect_alpha_full_neon, export=1
         // handle loop tail
         ldr             q5, [x0]
         ldr             q6, [x2]
+        uqsub           v5.16b, v5.16b, v18.16b // color - offset
         cmhi            v7.16b, v5.16b, v6.16b
         cmeq            v16.16b, v6.16b, v2.16b
         and             v7.16b, v7.16b, v3.16b
@@ -242,13 +248,17 @@ endfunc
  * x4: ptrdiff_t width,
  * x5: ptrdiff_t height,
  * w6: int alpha_max,
+ * w7: int mpeg_range (unused)
+ * [sp]: int offset
  */
 function ff_detect_alpha16_full_neon, export=1
+        ldr             w13, [sp]
         ands            x7, x4, #7              // width % 8
         bic             x8, x4, #7              // width / 8 * 8
         movi            v0.8h, #0
         movi            v1.16b, #255
         dup             v2.8h, w6               // alpha_max
+        dup             v18.8h, w13             // offset
         sub             x1, x1, x8, lsl #1      // color_stride - 
(aligned_width * 2)
         sub             x3, x3, x8, lsl #1      // alpha_stride - 
(aligned_width * 2)
         b.eq            1f
@@ -262,6 +272,7 @@ function ff_detect_alpha16_full_neon, export=1
         ldr             q5, [x0], #16
         ldr             q6, [x2], #16
         subs            x12, x12, #8
+        uqsub           v5.8h, v5.8h, v18.8h    // color - offset
         cmhi            v7.8h, v5.8h, v6.8h
         cmeq            v16.8h, v6.8h, v2.8h
         orr             v0.16b, v0.16b, v7.16b
@@ -272,6 +283,7 @@ function ff_detect_alpha16_full_neon, export=1
         // handle loop tail
         ldr             q5, [x0]
         ldr             q6, [x2]
+        uqsub           v5.8h, v5.8h, v18.8h    // color - offset
         cmhi            v7.8h, v5.8h, v6.8h
         cmeq            v16.8h, v6.8h, v2.8h
         and             v7.16b, v7.16b, v3.16b
diff --git a/libavfilter/vf_colordetectdsp.h b/libavfilter/vf_colordetectdsp.h
index ca4727b589..df3fc27906 100644
--- a/libavfilter/vf_colordetectdsp.h
+++ b/libavfilter/vf_colordetectdsp.h
@@ -119,7 +119,7 @@ ff_detect_alpha_full_c(const uint8_t *color, ptrdiff_t 
color_stride,
     while (height--) {
         uint8_t straight = 0;
         for (int x = 0; x < width; x++) {
-            straight  |= color[x] > alpha[x];
+            straight  |= color[x] > alpha[x] + offset;
             transparent |= alpha[x] != alpha_max;
         }
         if (straight)
@@ -163,7 +163,7 @@ ff_detect_alpha16_full_c(const uint8_t *color, ptrdiff_t 
color_stride,
         const uint16_t *alpha16 = (const uint16_t *) alpha;
         uint8_t straight = 0;
         for (int x = 0; x < width; x++) {
-            straight  |= color16[x] > alpha16[x];
+            straight  |= color16[x] > alpha16[x] + offset;
             transparent |= alpha16[x] != alpha_max;
         }
         if (straight)
diff --git a/libavfilter/x86/vf_colordetect.asm 
b/libavfilter/x86/vf_colordetect.asm
index 7a96b6933c..f0e4508c63 100644
--- a/libavfilter/x86/vf_colordetect.asm
+++ b/libavfilter/x86/vf_colordetect.asm
@@ -92,6 +92,7 @@ cglobal detect_alpha%1_%3, 1, 6, 7, color, ret, alpha, x, 
width, height
     vpbroadcast%2 m5, r8m ; offset
 %else
     vpbroadcast%1 m3, r6m ; alpha_max
+    vpbroadcast%1 m4, r8m ; offset
 %endif
     mova m6, m3
     xor retd, retd
@@ -102,6 +103,7 @@ cglobal detect_alpha%1_%3, 1, 6, 7, color, ret, alpha, x, 
width, height
         movu m1, [colorq + xq]
         movu m2, [alphaq + xq]
         pand m6, m2
+        psubus%1 m1, m4
         pmaxu%1 m1, m2
     %else
         pmovzx%1%2 m1, [colorq + xq]
diff --git a/tests/checkasm/vf_colordetect.c b/tests/checkasm/vf_colordetect.c
index 471f77fcc7..f5739c6567 100644
--- a/tests/checkasm/vf_colordetect.c
+++ b/tests/checkasm/vf_colordetect.c
@@ -75,9 +75,16 @@ static void check_alpha_detect(int depth, enum AVColorRange 
range)
     const int mpeg_max = 235 << (depth - 8);
     const int alpha_max = (1 << depth) - 1;
     const int mpeg_range = mpeg_max - mpeg_min;
-    const int offset = alpha_max * mpeg_min + (1 << (depth - 1));
     int res_ref, res_new;
 
+    int offset;
+    int threshold = checkasm_rand() % FFMIN(HEIGHT, mpeg_min);
+    if (range == AVCOL_RANGE_JPEG) {
+        offset = threshold;
+    } else {
+        offset = alpha_max * (mpeg_min + threshold) + (1 << (depth - 1));
+    }
+
     FFColorDetectDSPContext dsp = {0};
     ff_color_detect_dsp_init(&dsp, depth, range);
 
@@ -89,13 +96,19 @@ static void check_alpha_detect(int depth, enum AVColorRange 
range)
     memset(luma,  0x80, HEIGHT * STRIDE);
     memset(alpha, 0xF0, HEIGHT * STRIDE);
 
-    /* Try and force overflow */
+    /* Try and force overflow and edge cases */
     if (depth > 8 && range == AVCOL_RANGE_MPEG) {
-        ((uint16_t *) luma)[0] = 235 << (depth - 8);
-        ((uint16_t *) luma)[1] =  16 << (depth - 8);
+        for (int i = 0; i < threshold; i++) {
+            uint16_t *line = (uint16_t *) (luma + i * STRIDE);
+            line[0] = (235 << (depth - 8)) + i;
+            line[1] = ( 16 << (depth - 8)) - i;
+        }
     } else {
-        luma[0] = 235;
-        luma[1] = 16;
+        for (int i = 0; i < threshold; i++) {
+            uint8_t *line = luma + i * STRIDE;
+            line[0] = 235 + i;
+            line[1] = 16 - i;
+        }
     }
 
     /* Place an out-of-range value in a random position near the center */
-- 
2.52.0


>From 43f2de87f90abbce660bb34d79cdafe6448e2e4f Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Mon, 17 Aug 2026 12:38:24 +0200
Subject: [PATCH 2/4] avfilter/vf_colordetect: precompute alpha detection
 fields

In particular, we also store s->mpeg_range explicitly due to the upcoming
commit, which will allow these values to deviates between the range detection
and alpha mode detection passes.

Sponsored-by: nxtedition AB
Signed-off-by: Niklas Haas <[email protected]>
---
 libavfilter/vf_colordetect.c | 50 ++++++++++++++++++++++--------------
 1 file changed, 31 insertions(+), 19 deletions(-)

diff --git a/libavfilter/vf_colordetect.c b/libavfilter/vf_colordetect.c
index 397e278110..c8782db37e 100644
--- a/libavfilter/vf_colordetect.c
+++ b/libavfilter/vf_colordetect.c
@@ -52,10 +52,17 @@ typedef struct ColorDetectContext {
     const AVPixFmtDescriptor *desc;
     int nb_threads;
     int depth;
+    int range;
     int idx_a;
+
+    /* for color range detection only */
     int mpeg_min;
     int mpeg_max;
 
+    /* for alpha detection only */
+    int mpeg_range;
+    int offset;
+
     atomic_int detected_range; // enum AVColorRange
     atomic_int detected_alpha; // enum FFAlphaDetect
 } ColorDetectContext;
@@ -98,6 +105,7 @@ static int config_input(AVFilterLink *inlink)
     ColorDetectContext *s = ctx->priv;
     const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(inlink->format);
     const int depth = desc->comp[0].depth;
+    const int range = (1 << depth) - 1;
     const int mpeg_min =  16 << (depth - 8);
     const int mpeg_max = 235 << (depth - 8);
     if (depth > 16) /* not currently possible; prevent future bugs */
@@ -105,9 +113,29 @@ static int config_input(AVFilterLink *inlink)
 
     s->desc = desc;
     s->depth = depth;
+    s->range = range;
+    s->nb_threads = ff_filter_get_nb_threads(ctx);
+
+    /* Color range detection: */
     s->mpeg_min = mpeg_min;
     s->mpeg_max = mpeg_max;
-    s->nb_threads = ff_filter_get_nb_threads(ctx);
+
+    /**
+     * Alpha mode detection:
+     *
+     * To check if a value is out of range, we need to compare the color value
+     * against the maximum possible color for a given alpha value.
+     *   x > ((mpeg_max - mpeg_min) / pixel_max) * a + mpeg_min
+     *
+     * This simplifies to:
+     *   (x - mpeg_min) * pixel_max > (mpeg_max - mpeg_min) * a
+     *   = range * x - offset > mpeg_range * a in the below formula.
+     *
+     * We subtract an additional offset of (1 << (depth - 1)) to account for
+     * rounding errors in the value of `x`.
+     */
+    s->mpeg_range = mpeg_max - mpeg_min;
+    s->offset = range * mpeg_min + (1 << (s->depth - 1));
 
     if (desc->flags & AV_PIX_FMT_FLAG_RGB) {
         atomic_init(&s->detected_range, AVCOL_RANGE_JPEG);
@@ -159,28 +187,12 @@ static int detect_alpha(AVFilterContext *ctx, void *arg,
     const ptrdiff_t alpha_stride = in->linesize[s->idx_a];
     const uint8_t *alpha = in->data[s->idx_a] + y_start * alpha_stride;
 
-    /**
-     * To check if a value is out of range, we need to compare the color value
-     * against the maximum possible color for a given alpha value.
-     *   x > ((mpeg_max - mpeg_min) / pixel_max) * a + mpeg_min
-     *
-     * This simplifies to:
-     *   (x - mpeg_min) * pixel_max > (mpeg_max - mpeg_min) * a
-     *   = alpha_max * x - offset > mpeg_range * a in the below formula.
-     *
-     * We subtract an additional offset of (1 << (depth - 1)) to account for
-     * rounding errors in the value of `x`.
-     */
-    const int alpha_max = (1 << s->depth) - 1;
-    const int mpeg_range = s->mpeg_max - s->mpeg_min;
-    const int offset = alpha_max * s->mpeg_min + (1 << (s->depth - 1));
-
     int ret = 0;
     for (int i = 0; i < nb_planes; i++) {
         const ptrdiff_t stride = in->linesize[i];
         ret = s->dsp.detect_alpha(in->data[i] + y_start * stride, stride,
-                                  alpha, alpha_stride, w, h_slice, alpha_max,
-                                  mpeg_range, offset);
+                                  alpha, alpha_stride, w, h_slice, s->range,
+                                  s->mpeg_range, s->offset);
         ret |= atomic_fetch_or_explicit(&s->detected_alpha, ret, 
memory_order_relaxed);
         if (ret == FF_ALPHA_STRAIGHT)
             break;
-- 
2.52.0


>From 441a238d27e64d917647fad5b725eccd8bdedeb0 Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Mon, 17 Aug 2026 12:56:07 +0200
Subject: [PATCH 3/4] avfilter/vf_colordetect: add threshold option

This is useful to filter false positives due to encoding noise in lossily
compressed sources.

The reason this is limited to 0.05 (= 5%) is twofold:

1. Higher values would mostly defeat the purpose of this filter, since a
   deviation of >5% is enough to fully blur the distinction between JPEG and
   MPEG range, let alone premultiplied and straight alpha. There's no useful
   signal to extract with such a high tolerance for noise.

2. Higher values would overflow the `offset` value in the 8-bit SIMD. This
   could also be solved by clamping, but the first point is the salient one
   anyways.

Sponsored-by: nxtedition AB
Signed-off-by: Niklas Haas <[email protected]>
---
 doc/filters.texi             |  4 ++++
 libavfilter/vf_colordetect.c | 25 +++++++++++++++++++------
 2 files changed, 23 insertions(+), 6 deletions(-)

diff --git a/doc/filters.texi b/doc/filters.texi
index 51885fddde..6f05c61354 100644
--- a/doc/filters.texi
+++ b/doc/filters.texi
@@ -9941,6 +9941,10 @@ premultiplied. Also detects if the alpha plane is fully 
opaque or not.
 Enable detection of all of the above properties. This is the default.
 @end table
 
+@item threshold
+Allow pixels to exceed the expected bounds by this fraction of the full scale
+value range before treating it as evidence of (respectively) full range or
+straight alpha. Value range is from 0 to 0.05. Defaults to 0.0.
 @end table
 
 @section colorize
diff --git a/libavfilter/vf_colordetect.c b/libavfilter/vf_colordetect.c
index c8782db37e..3750b7d86a 100644
--- a/libavfilter/vf_colordetect.c
+++ b/libavfilter/vf_colordetect.c
@@ -48,6 +48,7 @@ typedef struct ColorDetectContext {
     const AVClass *class;
     FFColorDetectDSPContext dsp;
     unsigned mode;
+    float threshold;
 
     const AVPixFmtDescriptor *desc;
     int nb_threads;
@@ -75,6 +76,9 @@ static const AVOption colordetect_options[] = {
         { "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" },
+
+    /* Note: threshold should not be increased past ~0.4 as it overflows 8-bit 
SIMD otherwise */
+    { "threshold", "Detection threshold, as a fraction of the full range", 
OFFSET(threshold), AV_OPT_TYPE_FLOAT, {.dbl = 0.0}, 0.0, 0.05, FLAGS },
     { NULL }
 };
 
@@ -106,6 +110,7 @@ static int config_input(AVFilterLink *inlink)
     const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(inlink->format);
     const int depth = desc->comp[0].depth;
     const int range = (1 << depth) - 1;
+    const int threshold = lrintf(s->threshold * range);
     const int mpeg_min =  16 << (depth - 8);
     const int mpeg_max = 235 << (depth - 8);
     if (depth > 16) /* not currently possible; prevent future bugs */
@@ -117,25 +122,33 @@ static int config_input(AVFilterLink *inlink)
     s->nb_threads = ff_filter_get_nb_threads(ctx);
 
     /* Color range detection: */
-    s->mpeg_min = mpeg_min;
-    s->mpeg_max = mpeg_max;
+    s->mpeg_min = av_clip_uintp2(mpeg_min - threshold, depth);
+    s->mpeg_max = av_clip_uintp2(mpeg_max + threshold, depth);
 
     /**
      * Alpha mode detection:
      *
      * To check if a value is out of range, we need to compare the color value
      * against the maximum possible color for a given alpha value.
-     *   x > ((mpeg_max - mpeg_min) / pixel_max) * a + mpeg_min
+     *   x > ((mpeg_max - mpeg_min) / pixel_max) * a + mpeg_min + threshold
      *
      * This simplifies to:
-     *   (x - mpeg_min) * pixel_max > (mpeg_max - mpeg_min) * a
+     *   (x - mpeg_min - threshold) * pixel_max > (mpeg_max - mpeg_min) * a
      *   = range * x - offset > mpeg_range * a in the below formula.
      *
      * We subtract an additional offset of (1 << (depth - 1)) to account for
      * rounding errors in the value of `x`.
+     *
+     * For full range input this degenerates to `x > a + threshold`, so the
+     * threshold is passed through directly, without the `range` scaling.
      */
-    s->mpeg_range = mpeg_max - mpeg_min;
-    s->offset = range * mpeg_min + (1 << (s->depth - 1));
+    if (inlink->color_range == AVCOL_RANGE_JPEG) {
+        s->mpeg_range = range;
+        s->offset = threshold;
+    } else {
+        s->mpeg_range = mpeg_max - mpeg_min;
+        s->offset = range * (mpeg_min + threshold) + (1 << (depth - 1));
+    }
 
     if (desc->flags & AV_PIX_FMT_FLAG_RGB) {
         atomic_init(&s->detected_range, AVCOL_RANGE_JPEG);
-- 
2.52.0


>From 2509a0c80fc7e4c7ce0a8eb93a0d3abdc5be70bf Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Mon, 17 Aug 2026 13:08:41 +0200
Subject: [PATCH 4/4] avfilter/vf_colordetect: increase threshold default to
 0.001

I split this off from the commit adding the option to distinguish between the
change in code (which should be a no-op by default) and the change in behavior
(which is a deliberate deviation) when bisecting.

0.001 = 0.1% permits just one 10-bit code point of deviation, while
requiring tight bounds for 8-bit content.

Sponsored-by: nxtedition AB
Signed-off-by: Niklas Haas <[email protected]>
---
 doc/filters.texi             | 2 +-
 libavfilter/vf_colordetect.c | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/doc/filters.texi b/doc/filters.texi
index 6f05c61354..4a228adda3 100644
--- a/doc/filters.texi
+++ b/doc/filters.texi
@@ -9944,7 +9944,7 @@ Enable detection of all of the above properties. This is 
the default.
 @item threshold
 Allow pixels to exceed the expected bounds by this fraction of the full scale
 value range before treating it as evidence of (respectively) full range or
-straight alpha. Value range is from 0 to 0.05. Defaults to 0.0.
+straight alpha. Value range is from 0 to 0.05. Defaults to 0.001 (=0.1%).
 @end table
 
 @section colorize
diff --git a/libavfilter/vf_colordetect.c b/libavfilter/vf_colordetect.c
index 3750b7d86a..4fffaa4968 100644
--- a/libavfilter/vf_colordetect.c
+++ b/libavfilter/vf_colordetect.c
@@ -78,7 +78,7 @@ static const AVOption colordetect_options[] = {
         { "all",         "Detect all supported properties", 0, 
AV_OPT_TYPE_CONST, {.i64 = -1}, 0, 0, FLAGS, .unit = "mode" },
 
     /* Note: threshold should not be increased past ~0.4 as it overflows 8-bit 
SIMD otherwise */
-    { "threshold", "Detection threshold, as a fraction of the full range", 
OFFSET(threshold), AV_OPT_TYPE_FLOAT, {.dbl = 0.0}, 0.0, 0.05, FLAGS },
+    { "threshold", "Detection threshold, as a fraction of the full range", 
OFFSET(threshold), AV_OPT_TYPE_FLOAT, {.dbl = 0.001}, 0.0, 0.05, FLAGS },
     { NULL }
 };
 
-- 
2.52.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to