PR #22800 opened by Christopher Shaw (ctshaw) URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/22800 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/22800.patch
Adds a new corner_radius option (alias r) that replaces the four sharp corners of the drawn box with quarter-circle arcs of the given radius. In each corner zone the pixel membership test uses a squared-distance circle check against two precomputed values (rc_outer_r2, rc_inner_r2) stored on the context, avoiding per-pixel multiplications in the hot path. The corner radius is clamped to min(w,h)/2 in config_input to prevent adjacent arcs from overlapping on small boxes. The option is runtime-modifiable via the process_command path. Works correctly in both border and fill modes (large thickness values). Signed-off-by: Christopher Shaw <[email protected]> >From 70bfd98a171aa8b0b93b39394829d2b7a21cc29c Mon Sep 17 00:00:00 2001 From: Chris <[email protected]> Date: Sat, 11 Apr 2026 16:22:29 -0400 Subject: [PATCH] avfilter/drawbox: add corner_radius option for rounded corners Adds a new corner_radius option (alias r) that replaces the four sharp corners of the drawn box with quarter-circle arcs of the given radius. In each corner zone the pixel membership test uses a squared-distance circle check against two precomputed values (rc_outer_r2, rc_inner_r2) stored on the context, avoiding per-pixel multiplications in the hot path. The corner radius is clamped to min(w,h)/2 in config_input to prevent adjacent arcs from overlapping on small boxes. The option is runtime-modifiable via the process_command path. Works correctly in both border and fill modes (large thickness values). Signed-off-by: Christopher Shaw <[email protected]> --- Changelog | 1 + doc/filters.texi | 17 +++++++++ libavfilter/vf_drawbox.c | 48 +++++++++++++++++++++++ tests/fate/filter-video.mak | 3 ++ tests/ref/fate/filter-drawbox-rounded | 55 +++++++++++++++++++++++++++ 5 files changed, 124 insertions(+) create mode 100644 tests/ref/fate/filter-drawbox-rounded diff --git a/Changelog b/Changelog index d399c0a8c1..a9471aeebd 100644 --- a/Changelog +++ b/Changelog @@ -6,6 +6,7 @@ version <next>: - LCEVC track muxing support in MP4 muxer - Playdate video encoder and muxer - Add v360_vulkan filter +- drawbox filter corner_radius option for rounded corners version 8.1: diff --git a/doc/filters.texi b/doc/filters.texi index 88d67cc70f..cdaa8d797e 100644 --- a/doc/filters.texi +++ b/doc/filters.texi @@ -12295,6 +12295,11 @@ A value of @code{fill} will create a filled box. Default value is @code{3}. See below for the list of accepted constants. +@item round_corners, r +Set the corner radius in pixels to draw the box with rounded corners. The radius is +clamped to @code{min(w,h)/2} to prevent the four arcs from overlapping. Default value +is @code{0}, which produces sharp (square) corners. + @item replace Applicable if the input has alpha. With value @code{1}, the pixels of the painted box will overwrite the video's color and alpha pixels. @@ -12375,6 +12380,18 @@ Draw a 2-pixel red 2.40:1 mask: @example drawbox=x=-t:y=0.5*(ih-iw/2.4)-t:w=iw+t*2:h=iw/2.4+t*2:t=2:c=red @end example + +@item +Draw a blue box with a 4-pixel border and 20-pixel rounded corners: +@example +drawbox=x=100:y=50:w=200:h=120:color=blue:t=4:r=20 +@end example + +@item +Draw a filled rounded rectangle: +@example +drawbox=x=100:y=50:w=200:h=120:[email protected]:t=fill:r=30 +@end example @end itemize @subsection Commands diff --git a/libavfilter/vf_drawbox.c b/libavfilter/vf_drawbox.c index 571988416c..b2ccb282cf 100644 --- a/libavfilter/vf_drawbox.c +++ b/libavfilter/vf_drawbox.c @@ -93,6 +93,8 @@ typedef struct DrawBoxContext { int have_alpha; int replace; int step; + int corner_radius; ///< radius of rounded corners (0 = sharp corners) + int rc_outer_r2, rc_inner_r2; ///< corner_radius thresholds, squared and cached enum AVFrameSideDataType box_source; void (*draw_region)(AVFrame *frame, struct DrawBoxContext *ctx, int left, int top, int right, int down, @@ -350,6 +352,21 @@ static int config_input(AVFilterLink *inlink) return AVERROR(EINVAL); } + /* clamp corner radius so the four arcs never overlap */ + if (s->corner_radius > 0) { + int max_r = FFMIN(s->w, s->h) / 2; + int inner_r; + if (s->corner_radius > max_r) { + av_log(ctx, AV_LOG_WARNING, + "corner_radius %d clamped to %d (min(w,h)/2)\n", + s->corner_radius, max_r); + s->corner_radius = max_r; + } + inner_r = s->corner_radius - s->thickness; + s->rc_outer_r2 = s->corner_radius * s->corner_radius; + s->rc_inner_r2 = inner_r > 0 ? inner_r * inner_r : 0; + } + av_log(ctx, AV_LOG_VERBOSE, "x:%d y:%d w:%d h:%d color:0x%02X%02X%02X%02X\n", s->x, s->y, s->w, s->h, s->yuv_color[Y], s->yuv_color[U], s->yuv_color[V], s->yuv_color[A]); @@ -365,6 +382,29 @@ fail: static av_pure av_always_inline int pixel_belongs_to_box(DrawBoxContext *s, int x, int y) { + int r = s->corner_radius; + + if (r > 0) { + int left_dist = x - s->x; + int right_dist = s->x + s->w - 1 - x; + int top_dist = y - s->y; + int bottom_dist = s->y + s->h - 1 - y; + + if ((left_dist < r || right_dist < r) && + (top_dist < r || bottom_dist < r)) { + int dx = x - ((left_dist < r) ? (s->x + r - 1) : (s->x + s->w - r)); + int dy = y - ((top_dist < r) ? (s->y + r - 1) : (s->y + s->h - r)); + int dist2 = dx * dx + dy * dy; + + if (dist2 > s->rc_outer_r2) + return 0; + if (s->rc_inner_r2 > 0 && dist2 < s->rc_inner_r2) + return 0; + + return 1; + } + } + return (y - s->y < s->thickness) || (s->y + s->h - 1 - y < s->thickness) || (x - s->x < s->thickness) || (s->x + s->w - 1 - x < s->thickness); } @@ -415,6 +455,9 @@ static int process_command(AVFilterContext *ctx, const char *cmd, const char *ar int old_h = s->h; int old_t = s->thickness; int old_r = s->replace; + int old_rc = s->corner_radius; + int old_rc_outer = s->rc_outer_r2; + int old_rc_inner = s->rc_inner_r2; int ret; ret = ff_filter_process_command(ctx, cmd, args, res, res_len, flags); @@ -433,6 +476,9 @@ end: s->h = old_h; s->thickness = old_t; s->replace = old_r; + s->corner_radius = old_rc; + s->rc_outer_r2 = old_rc_outer; + s->rc_inner_r2 = old_rc_inner; } return ret; @@ -454,6 +500,8 @@ static const AVOption drawbox_options[] = { { "c", "set color of the box", OFFSET(color_str), AV_OPT_TYPE_STRING, { .str = "black" }, 0, 0, FLAGS }, { "thickness", "set the box thickness", OFFSET(t_expr), AV_OPT_TYPE_STRING, { .str="3" }, 0, 0, FLAGS }, { "t", "set the box thickness", OFFSET(t_expr), AV_OPT_TYPE_STRING, { .str="3" }, 0, 0, FLAGS }, + { "corner_radius", "set the box corner radius for rounded corners", OFFSET(corner_radius), AV_OPT_TYPE_INT, { .i64=0 }, 0, INT_MAX/2, FLAGS }, + { "r", "set the box corner radius for rounded corners", OFFSET(corner_radius), AV_OPT_TYPE_INT, { .i64=0 }, 0, INT_MAX/2, FLAGS }, { "replace", "replace color & alpha", OFFSET(replace), AV_OPT_TYPE_BOOL, { .i64=0 }, 0, 1, FLAGS }, { "box_source","use data from bounding box in side data", OFFSET(box_source_string), AV_OPT_TYPE_STRING, { .str=NULL }, 0, 1, FLAGS }, { NULL } diff --git a/tests/fate/filter-video.mak b/tests/fate/filter-video.mak index 5016ea9d4d..f48f6d6a16 100644 --- a/tests/fate/filter-video.mak +++ b/tests/fate/filter-video.mak @@ -199,6 +199,9 @@ fate-filter-colorchannelmixer: CMD = framecrc -c:v pgmyuv -i $(SRC) -vf scale,fo FATE_FILTER_VSYNTH_PGMYUV-$(CONFIG_DRAWBOX_FILTER) += fate-filter-drawbox fate-filter-drawbox: CMD = framecrc -c:v pgmyuv -i $(SRC) -vf drawbox=224:24:88:72:[email protected] +FATE_FILTER_VSYNTH_PGMYUV-$(CONFIG_DRAWBOX_FILTER) += fate-filter-drawbox-rounded +fate-filter-drawbox-rounded: CMD = framecrc -c:v pgmyuv -i $(SRC) -vf drawbox=x=100:y=50:w=200:h=120:color=blue:t=4:r=20 + FATE_FILTER_VSYNTH_PGMYUV-$(CONFIG_FADE_FILTER) += fate-filter-fade fate-filter-fade: CMD = framecrc -c:v pgmyuv -i $(SRC) -vf fade=in:5:15,fade=out:30:15 diff --git a/tests/ref/fate/filter-drawbox-rounded b/tests/ref/fate/filter-drawbox-rounded new file mode 100644 index 0000000000..822c02d7ae --- /dev/null +++ b/tests/ref/fate/filter-drawbox-rounded @@ -0,0 +1,55 @@ +#tb 0: 1/25 +#media_type 0: video +#codec_id 0: rawvideo +#dimensions 0: 352x288 +#sar 0: 0/1 +0, 0, 0, 1, 152064, 0xd463eb6c +0, 1, 1, 1, 152064, 0x00a0e43f +0, 2, 2, 1, 152064, 0xa01fd0c4 +0, 3, 3, 1, 152064, 0xf56130e0 +0, 4, 4, 1, 152064, 0xc2d119e1 +0, 5, 5, 1, 152064, 0x4729d2a9 +0, 6, 6, 1, 152064, 0xc24be74c +0, 7, 7, 1, 152064, 0x7912167d +0, 8, 8, 1, 152064, 0xae4c72f8 +0, 9, 9, 1, 152064, 0x704c8753 +0, 10, 10, 1, 152064, 0xbab9bb80 +0, 11, 11, 1, 152064, 0x320b2e0d +0, 12, 12, 1, 152064, 0xbab44eeb +0, 13, 13, 1, 152064, 0x91de138a +0, 14, 14, 1, 152064, 0x0f99d35d +0, 15, 15, 1, 152064, 0x045962bd +0, 16, 16, 1, 152064, 0x657ebeba +0, 17, 17, 1, 152064, 0x91aec480 +0, 18, 18, 1, 152064, 0x8870e527 +0, 19, 19, 1, 152064, 0x078b5da5 +0, 20, 20, 1, 152064, 0xd6e03ec4 +0, 21, 21, 1, 152064, 0x77298332 +0, 22, 22, 1, 152064, 0xbc6e7156 +0, 23, 23, 1, 152064, 0x7360e37a +0, 24, 24, 1, 152064, 0xdf9c6550 +0, 25, 25, 1, 152064, 0xcc37e375 +0, 26, 26, 1, 152064, 0xa857c68b +0, 27, 27, 1, 152064, 0x3ee04c8d +0, 28, 28, 1, 152064, 0xe5a3748b +0, 29, 29, 1, 152064, 0x8cd696d4 +0, 30, 30, 1, 152064, 0x5d1dc393 +0, 31, 31, 1, 152064, 0x07470560 +0, 32, 32, 1, 152064, 0x556d1117 +0, 33, 33, 1, 152064, 0x168245a3 +0, 34, 34, 1, 152064, 0x00efc74e +0, 35, 35, 1, 152064, 0x42318871 +0, 36, 36, 1, 152064, 0x6fd219cd +0, 37, 37, 1, 152064, 0x4b97b860 +0, 38, 38, 1, 152064, 0x9b6edb39 +0, 39, 39, 1, 152064, 0x83c96626 +0, 40, 40, 1, 152064, 0x0749015d +0, 41, 41, 1, 152064, 0xf9b91e1e +0, 42, 42, 1, 152064, 0x4c1913d8 +0, 43, 43, 1, 152064, 0xf06f4bbe +0, 44, 44, 1, 152064, 0xd6e529a7 +0, 45, 45, 1, 152064, 0xffd8ac10 +0, 46, 46, 1, 152064, 0x968f8030 +0, 47, 47, 1, 152064, 0xfd5af46a +0, 48, 48, 1, 152064, 0x34dae717 +0, 49, 49, 1, 152064, 0x2f6d2029 -- 2.52.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
