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]

Reply via email to