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]
