John Ho Claude Fable 5 commited on
Commit ·
be948dc
1
Parent(s): 00ea497
Expose text_overlay controls on the Annotate Video tab
Browse filesThe optional caption from ffmpret.extract_frames is now reachable from
the app: annotate_video_interface gains a Text overlay textbox (default
empty = off) plus font-size slider and top/middle/bottom position
dropdown, passed through app.py -> vidbox.annotate_video ->
ffmpret.extract_frames. The caption is burned in at extraction time, so
it renders beneath the box/mask annotations. The annotate-video-file
CLI gains the matching --text-overlay/--text-font-size/
--text-y-position options, and the dropdown choices come from a new
ffmpret.TEXT_Y_POSITIONS constant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- CLAUDE.md +15 -6
- app.py +24 -1
- ffmpret.py +5 -2
- tests/test_vidbox.py +18 -0
- vidbox.py +29 -3
CLAUDE.md
CHANGED
|
@@ -75,7 +75,10 @@ commit the JSON.
|
|
| 75 |
four `boxer.BBOX_FORMATS` (default `coco_normalized`), a comma-separated `coord_keys`
|
| 76 |
textbox (default `"x,y,w,h"`), then `label_key` / `color_key` / `mask_key` (default
|
| 77 |
`"mask_b64"`, matching the SAM2 example data) / `mask_alpha` / `width` / `font_size`
|
| 78 |
-
mirroring the image Annotate tab
|
|
|
|
|
|
|
|
|
|
| 79 |
resolution/fps. Each distinct `color_key` value keeps one stable color across all frames.
|
| 80 |
- **Crop** tab: `crop_image(...)` → `pilbox.crop` (`api_name="crop"`). Inputs are an image
|
| 81 |
plus manual `gr.Number` fields `x0/y0/x1/y1` (pascal_voc box); output is the cropped
|
|
@@ -193,13 +196,15 @@ video and encoding frames back into one, via `ffmpeg-python`. Depends on `ffmpeg
|
|
| 193 |
Raises (does not return `None`) when there's no video stream, so failures are legible to
|
| 194 |
the callers that subscript the result.
|
| 195 |
- `extract_frames(input_path, fps=8, max_short_edge=1080, write_timestamp=True,
|
| 196 |
-
write_frame_num=True, output_dir=None, out_vid_path=None,
|
| 197 |
-
text_y_position="bottom") -> list[PIL.Image]` decodes frames by piping
|
|
|
|
| 198 |
to stdout. Requested `fps` is capped to source fps; **`fps=None` skips resampling and
|
| 199 |
extracts every native frame** (so output index i == source frame i — what `vidbox` needs
|
| 200 |
for frame-accurate annotation). Frames may be scaled down so the short edge ≤
|
| 201 |
`max_short_edge`. An optional `drawtext` overlay stamps timestamp/frame-number
|
| 202 |
-
(`text_y_position` ∈ {top, middle, bottom}); it uses
|
|
|
|
| 203 |
`text_overlay` string (default `None`) is prefixed to that overlay line, and is drawn even
|
| 204 |
when `write_timestamp=False`, so it works standalone. The overlay text is built by
|
| 205 |
`_build_drawtext_text` and passed to drawtext via a temp **`textfile=`** (removed in a
|
|
@@ -242,8 +247,12 @@ the three existing modules). Keeps `pilbox` lite by living in its own module.
|
|
| 242 |
|
| 243 |
- `annotate_video(video_path, detections, out_path, *, bbox_format="coco_normalized",
|
| 244 |
coord_keys=("x","y","w","h"), frame_key="frame", label_key="track_id",
|
| 245 |
-
color_key="track_id", mask_key="mask_b64", mask_alpha=0.5, width=3, font_size=20
|
| 246 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
`frame_key`, box values under `coord_keys` read positionally per `bbox_format`, optional
|
| 248 |
`label_key`/`color_key`/`mask_key`). It: probes metadata → extracts **every native frame**
|
| 249 |
(`ffmpret.extract_frames(fps=None)`) → groups detections by frame → converts each box via
|
|
|
|
| 75 |
four `boxer.BBOX_FORMATS` (default `coco_normalized`), a comma-separated `coord_keys`
|
| 76 |
textbox (default `"x,y,w,h"`), then `label_key` / `color_key` / `mask_key` (default
|
| 77 |
`"mask_b64"`, matching the SAM2 example data) / `mask_alpha` / `width` / `font_size`
|
| 78 |
+
mirroring the image Annotate tab, plus an optional `text_overlay` caption (a `gr.Textbox`,
|
| 79 |
+
default empty = off) with its own `text_font_size` slider and `text_y_position` dropdown
|
| 80 |
+
(`ffmpret.TEXT_Y_POSITIONS`, default `bottom`) — burned into every frame beneath the
|
| 81 |
+
annotations. Output is a silent annotated video at source
|
| 82 |
resolution/fps. Each distinct `color_key` value keeps one stable color across all frames.
|
| 83 |
- **Crop** tab: `crop_image(...)` → `pilbox.crop` (`api_name="crop"`). Inputs are an image
|
| 84 |
plus manual `gr.Number` fields `x0/y0/x1/y1` (pascal_voc box); output is the cropped
|
|
|
|
| 196 |
Raises (does not return `None`) when there's no video stream, so failures are legible to
|
| 197 |
the callers that subscript the result.
|
| 198 |
- `extract_frames(input_path, fps=8, max_short_edge=1080, write_timestamp=True,
|
| 199 |
+
write_frame_num=True, output_dir=None, out_vid_path=None, text_overlay=None,
|
| 200 |
+
text_font_size=20, text_y_position="bottom") -> list[PIL.Image]` decodes frames by piping
|
| 201 |
+
ffmpeg `rawvideo`
|
| 202 |
to stdout. Requested `fps` is capped to source fps; **`fps=None` skips resampling and
|
| 203 |
extracts every native frame** (so output index i == source frame i — what `vidbox` needs
|
| 204 |
for frame-accurate annotation). Frames may be scaled down so the short edge ≤
|
| 205 |
`max_short_edge`. An optional `drawtext` overlay stamps timestamp/frame-number
|
| 206 |
+
(`text_y_position` ∈ module constant `TEXT_Y_POSITIONS` = {top, middle, bottom}); it uses
|
| 207 |
+
ffmpeg's default font. A
|
| 208 |
`text_overlay` string (default `None`) is prefixed to that overlay line, and is drawn even
|
| 209 |
when `write_timestamp=False`, so it works standalone. The overlay text is built by
|
| 210 |
`_build_drawtext_text` and passed to drawtext via a temp **`textfile=`** (removed in a
|
|
|
|
| 247 |
|
| 248 |
- `annotate_video(video_path, detections, out_path, *, bbox_format="coco_normalized",
|
| 249 |
coord_keys=("x","y","w","h"), frame_key="frame", label_key="track_id",
|
| 250 |
+
color_key="track_id", mask_key="mask_b64", mask_alpha=0.5, width=3, font_size=20,
|
| 251 |
+
text_overlay=None, text_font_size=20, text_y_position="bottom") ->
|
| 252 |
+
out_path`. The three `text_*` params pass through to `ffmpret.extract_frames` — an
|
| 253 |
+
optional caption burned into every frame at extraction time, i.e. beneath the box/mask
|
| 254 |
+
annotations (`font_size` is the pilbox label font; `text_font_size` the ffmpeg overlay
|
| 255 |
+
font). `detections` is a **flat** list of per-frame dicts (frame index under
|
| 256 |
`frame_key`, box values under `coord_keys` read positionally per `bbox_format`, optional
|
| 257 |
`label_key`/`color_key`/`mask_key`). It: probes metadata → extracts **every native frame**
|
| 258 |
(`ffmpret.extract_frames(fps=None)`) → groups detections by frame → converts each box via
|
app.py
CHANGED
|
@@ -13,6 +13,7 @@ from loguru import logger
|
|
| 13 |
from PIL import ImageColor
|
| 14 |
|
| 15 |
import boxer
|
|
|
|
| 16 |
import pilbox
|
| 17 |
import vidbox
|
| 18 |
|
|
@@ -261,6 +262,9 @@ def annotate_video(
|
|
| 261 |
mask_alpha,
|
| 262 |
width,
|
| 263 |
font_size,
|
|
|
|
|
|
|
|
|
|
| 264 |
) -> str:
|
| 265 |
"""Draw per-frame bounding boxes (and optional masks) onto every frame of a video and return the annotated video.
|
| 266 |
|
|
@@ -271,7 +275,10 @@ def annotate_video(
|
|
| 271 |
drawn on its frame; each distinct color_key value keeps ONE stable color across the whole video (so
|
| 272 |
a track id is one consistent color), and any mask is drawn as a translucent overlay beneath the box
|
| 273 |
in that same color. The output is a new silent video at the source resolution and frame rate with
|
| 274 |
-
all boxes, labels, and masks burned in.
|
|
|
|
|
|
|
|
|
|
| 275 |
"pascal_voc" = [x0, y0, x1, y1] absolute pixels; "albumentations" = [x0, y0, x1, y1] normalized 0-1;
|
| 276 |
"coco" = [x0, y0, width, height] absolute pixels; "coco_normalized" = [x0, y0, width, height]
|
| 277 |
normalized 0-1 — as documented at
|
|
@@ -288,6 +295,9 @@ def annotate_video(
|
|
| 288 |
mask_alpha: Mask overlay opacity from 0.0 (invisible) to 1.0 (solid color).
|
| 289 |
width: Box outline width in pixels.
|
| 290 |
font_size: Label font size in points.
|
|
|
|
|
|
|
|
|
|
| 291 |
"""
|
| 292 |
if video_path is None:
|
| 293 |
raise gr.Error("Please provide an input video.")
|
|
@@ -313,6 +323,9 @@ def annotate_video(
|
|
| 313 |
mask_alpha=float(mask_alpha),
|
| 314 |
width=int(width),
|
| 315 |
font_size=int(font_size),
|
|
|
|
|
|
|
|
|
|
| 316 |
)
|
| 317 |
except (ValueError, KeyError) as e:
|
| 318 |
raise gr.Error(str(e))
|
|
@@ -480,6 +493,13 @@ annotate_video_interface = gr.Interface(
|
|
| 480 |
gr.Slider(0, 1, value=0.5, step=0.05, label="Mask opacity"),
|
| 481 |
gr.Slider(1, 10, value=3, step=1, label="Box width"),
|
| 482 |
gr.Slider(8, 60, value=20, step=1, label="Font size"),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
],
|
| 484 |
outputs=gr.Video(label="Annotated Video"),
|
| 485 |
examples=(
|
|
@@ -495,6 +515,9 @@ annotate_video_interface = gr.Interface(
|
|
| 495 |
0.5,
|
| 496 |
3,
|
| 497 |
20,
|
|
|
|
|
|
|
|
|
|
| 498 |
]
|
| 499 |
]
|
| 500 |
if EXAMPLE_VIDEO and EXAMPLE_VIDEO_JSON
|
|
|
|
| 13 |
from PIL import ImageColor
|
| 14 |
|
| 15 |
import boxer
|
| 16 |
+
import ffmpret
|
| 17 |
import pilbox
|
| 18 |
import vidbox
|
| 19 |
|
|
|
|
| 262 |
mask_alpha,
|
| 263 |
width,
|
| 264 |
font_size,
|
| 265 |
+
text_overlay=None,
|
| 266 |
+
text_font_size=20,
|
| 267 |
+
text_y_position="bottom",
|
| 268 |
) -> str:
|
| 269 |
"""Draw per-frame bounding boxes (and optional masks) onto every frame of a video and return the annotated video.
|
| 270 |
|
|
|
|
| 275 |
drawn on its frame; each distinct color_key value keeps ONE stable color across the whole video (so
|
| 276 |
a track id is one consistent color), and any mask is drawn as a translucent overlay beneath the box
|
| 277 |
in that same color. The output is a new silent video at the source resolution and frame rate with
|
| 278 |
+
all boxes, labels, and masks burned in. An optional text_overlay caption (e.g. a clip name or
|
| 279 |
+
camera id) can additionally be burned into every frame, horizontally centered at a chosen
|
| 280 |
+
vertical position and font size, drawn beneath the boxes/masks. The box coordinates are
|
| 281 |
+
interpreted per bbox_format, one of:
|
| 282 |
"pascal_voc" = [x0, y0, x1, y1] absolute pixels; "albumentations" = [x0, y0, x1, y1] normalized 0-1;
|
| 283 |
"coco" = [x0, y0, width, height] absolute pixels; "coco_normalized" = [x0, y0, width, height]
|
| 284 |
normalized 0-1 — as documented at
|
|
|
|
| 295 |
mask_alpha: Mask overlay opacity from 0.0 (invisible) to 1.0 (solid color).
|
| 296 |
width: Box outline width in pixels.
|
| 297 |
font_size: Label font size in points.
|
| 298 |
+
text_overlay: Optional literal caption text burned into every frame of the output video; leave empty for no caption.
|
| 299 |
+
text_font_size: Font size of the text_overlay caption in points (independent of the box-label font_size).
|
| 300 |
+
text_y_position: Vertical placement of the text_overlay caption: "top", "middle", or "bottom".
|
| 301 |
"""
|
| 302 |
if video_path is None:
|
| 303 |
raise gr.Error("Please provide an input video.")
|
|
|
|
| 323 |
mask_alpha=float(mask_alpha),
|
| 324 |
width=int(width),
|
| 325 |
font_size=int(font_size),
|
| 326 |
+
text_overlay=(text_overlay or "").strip() or None,
|
| 327 |
+
text_font_size=int(text_font_size),
|
| 328 |
+
text_y_position=text_y_position,
|
| 329 |
)
|
| 330 |
except (ValueError, KeyError) as e:
|
| 331 |
raise gr.Error(str(e))
|
|
|
|
| 493 |
gr.Slider(0, 1, value=0.5, step=0.05, label="Mask opacity"),
|
| 494 |
gr.Slider(1, 10, value=3, step=1, label="Box width"),
|
| 495 |
gr.Slider(8, 60, value=20, step=1, label="Font size"),
|
| 496 |
+
gr.Textbox(value=None, label="Text overlay (optional)"),
|
| 497 |
+
gr.Slider(8, 60, value=20, step=1, label="Text overlay font size"),
|
| 498 |
+
gr.Dropdown(
|
| 499 |
+
choices=list(ffmpret.TEXT_Y_POSITIONS),
|
| 500 |
+
value="bottom",
|
| 501 |
+
label="Text overlay position",
|
| 502 |
+
),
|
| 503 |
],
|
| 504 |
outputs=gr.Video(label="Annotated Video"),
|
| 505 |
examples=(
|
|
|
|
| 515 |
0.5,
|
| 516 |
3,
|
| 517 |
20,
|
| 518 |
+
None,
|
| 519 |
+
20,
|
| 520 |
+
"bottom",
|
| 521 |
]
|
| 522 |
]
|
| 523 |
if EXAMPLE_VIDEO and EXAMPLE_VIDEO_JSON
|
ffmpret.py
CHANGED
|
@@ -17,6 +17,9 @@ logger.add(
|
|
| 17 |
)
|
| 18 |
app = typer.Typer(pretty_exceptions_show_locals=False)
|
| 19 |
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
def parse_frame_name(fname: str):
|
| 22 |
"""return a tuple of frame_type and frame_index
|
|
@@ -229,8 +232,8 @@ def extract_frames(
|
|
| 229 |
"bottom": "h-(2*lh)",
|
| 230 |
}
|
| 231 |
assert (
|
| 232 |
-
text_y_position in
|
| 233 |
-
), f"text_y_position must be one of {list(
|
| 234 |
text_y_expr = y_position_map[text_y_position]
|
| 235 |
|
| 236 |
if output_dir:
|
|
|
|
| 17 |
)
|
| 18 |
app = typer.Typer(pretty_exceptions_show_locals=False)
|
| 19 |
|
| 20 |
+
# Vertical placements accepted by extract_frames' text_y_position.
|
| 21 |
+
TEXT_Y_POSITIONS = ("top", "middle", "bottom")
|
| 22 |
+
|
| 23 |
|
| 24 |
def parse_frame_name(fname: str):
|
| 25 |
"""return a tuple of frame_type and frame_index
|
|
|
|
| 232 |
"bottom": "h-(2*lh)",
|
| 233 |
}
|
| 234 |
assert (
|
| 235 |
+
text_y_position in TEXT_Y_POSITIONS
|
| 236 |
+
), f"text_y_position must be one of {list(TEXT_Y_POSITIONS)}, got {text_y_position!r}"
|
| 237 |
text_y_expr = y_position_map[text_y_position]
|
| 238 |
|
| 239 |
if output_dir:
|
tests/test_vidbox.py
CHANGED
|
@@ -88,6 +88,24 @@ def test_annotate_video_synthetic_coco_normalized(tiny_video, tmp_path):
|
|
| 88 |
assert 8 <= int(meta["nb_frames"]) <= 12
|
| 89 |
|
| 90 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
def test_crop_video_rejects_conflicting_boxes():
|
| 92 |
# same frame, two DIFFERENT boxes -> conflict error (needs no ffmpeg: raises
|
| 93 |
# during grouping, before extraction).
|
|
|
|
| 88 |
assert 8 <= int(meta["nb_frames"]) <= 12
|
| 89 |
|
| 90 |
|
| 91 |
+
def test_annotate_video_with_text_overlay(tiny_video, tmp_path):
|
| 92 |
+
# the overlay params pass through to ffmpret.extract_frames (drawtext via
|
| 93 |
+
# textfile); output must still encode at source dims/frames.
|
| 94 |
+
detections = [
|
| 95 |
+
{"frame": 0, "track_id": 0, "x": 0.1, "y": 0.1, "w": 0.3, "h": 0.3},
|
| 96 |
+
]
|
| 97 |
+
out = tmp_path / "overlaid.mp4"
|
| 98 |
+
result = vidbox.annotate_video(
|
| 99 |
+
tiny_video, detections, str(out), bbox_format="coco_normalized",
|
| 100 |
+
mask_key="",
|
| 101 |
+
text_overlay="cam-1", text_font_size=12, text_y_position="top",
|
| 102 |
+
)
|
| 103 |
+
assert Path(result).is_file()
|
| 104 |
+
meta = _probe(result)
|
| 105 |
+
assert meta["width"] == "64" and meta["height"] == "48"
|
| 106 |
+
assert 8 <= int(meta["nb_frames"]) <= 12
|
| 107 |
+
|
| 108 |
+
|
| 109 |
def test_crop_video_rejects_conflicting_boxes():
|
| 110 |
# same frame, two DIFFERENT boxes -> conflict error (needs no ffmpeg: raises
|
| 111 |
# during grouping, before extraction).
|
vidbox.py
CHANGED
|
@@ -26,7 +26,7 @@ from collections import defaultdict
|
|
| 26 |
import typer
|
| 27 |
from loguru import logger
|
| 28 |
from PIL import Image, ImageColor
|
| 29 |
-
from typing import Literal
|
| 30 |
|
| 31 |
import boxer
|
| 32 |
import ffmpret
|
|
@@ -63,12 +63,17 @@ def annotate_video(
|
|
| 63 |
mask_alpha: float = 0.5,
|
| 64 |
width: int = 3,
|
| 65 |
font_size: int = 20,
|
|
|
|
|
|
|
|
|
|
| 66 |
) -> str:
|
| 67 |
"""Draw per-frame boxes and masks over a video and write the annotated result.
|
| 68 |
|
| 69 |
Frames are extracted at native fps/resolution (so frame index ``i`` matches a
|
| 70 |
detection's ``frame_key`` value, and full-frame masks line up), annotated with
|
| 71 |
:func:`pilbox.annotate`, then re-encoded to a silent video at the source fps.
|
|
|
|
|
|
|
| 72 |
|
| 73 |
Args:
|
| 74 |
video_path: Path to the input video.
|
|
@@ -86,7 +91,15 @@ def annotate_video(
|
|
| 86 |
pass ``""`` to disable masks.
|
| 87 |
mask_alpha: Mask overlay opacity in ``[0, 1]``.
|
| 88 |
width: Box outline width in pixels.
|
| 89 |
-
font_size: Label font size in points.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
Returns:
|
| 92 |
``out_path``.
|
|
@@ -103,7 +116,14 @@ def annotate_video(
|
|
| 103 |
org_w, org_h, fps = vmeta["width"], vmeta["height"], vmeta["fps"]
|
| 104 |
|
| 105 |
# Extract every native frame so frame indices align with the detections.
|
| 106 |
-
frames = ffmpret.extract_frames(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
if not frames:
|
| 108 |
raise ValueError(f"no frames decoded from {video_path}")
|
| 109 |
|
|
@@ -437,6 +457,9 @@ def annotate_video_file(
|
|
| 437 |
mask_alpha: float = 0.5,
|
| 438 |
width: int = 3,
|
| 439 |
font_size: int = 20,
|
|
|
|
|
|
|
|
|
|
| 440 |
) -> str:
|
| 441 |
"""Annotate ``video_path`` using detections from a JSON file; save to ``out_path``.
|
| 442 |
|
|
@@ -458,6 +481,9 @@ def annotate_video_file(
|
|
| 458 |
mask_alpha=mask_alpha,
|
| 459 |
width=width,
|
| 460 |
font_size=font_size,
|
|
|
|
|
|
|
|
|
|
| 461 |
)
|
| 462 |
|
| 463 |
|
|
|
|
| 26 |
import typer
|
| 27 |
from loguru import logger
|
| 28 |
from PIL import Image, ImageColor
|
| 29 |
+
from typing import Literal, Optional
|
| 30 |
|
| 31 |
import boxer
|
| 32 |
import ffmpret
|
|
|
|
| 63 |
mask_alpha: float = 0.5,
|
| 64 |
width: int = 3,
|
| 65 |
font_size: int = 20,
|
| 66 |
+
text_overlay: Optional[str] = None,
|
| 67 |
+
text_font_size: int = 20,
|
| 68 |
+
text_y_position: str = "bottom",
|
| 69 |
) -> str:
|
| 70 |
"""Draw per-frame boxes and masks over a video and write the annotated result.
|
| 71 |
|
| 72 |
Frames are extracted at native fps/resolution (so frame index ``i`` matches a
|
| 73 |
detection's ``frame_key`` value, and full-frame masks line up), annotated with
|
| 74 |
:func:`pilbox.annotate`, then re-encoded to a silent video at the source fps.
|
| 75 |
+
An optional ``text_overlay`` label is burned in at extraction time, so it
|
| 76 |
+
renders beneath the box/mask annotations.
|
| 77 |
|
| 78 |
Args:
|
| 79 |
video_path: Path to the input video.
|
|
|
|
| 91 |
pass ``""`` to disable masks.
|
| 92 |
mask_alpha: Mask overlay opacity in ``[0, 1]``.
|
| 93 |
width: Box outline width in pixels.
|
| 94 |
+
font_size: Label font size in points (the pilbox box-label font).
|
| 95 |
+
text_overlay: Optional literal text burned into every frame (beneath
|
| 96 |
+
the annotations); ``None``/empty disables it.
|
| 97 |
+
text_font_size: Font size of the ``text_overlay`` (the ffmpeg drawtext
|
| 98 |
+
font, distinct from ``font_size``). Only used when ``text_overlay``
|
| 99 |
+
is set.
|
| 100 |
+
text_y_position: Vertical placement of the ``text_overlay``; one of
|
| 101 |
+
:data:`ffmpret.TEXT_Y_POSITIONS` (``top``/``middle``/``bottom``).
|
| 102 |
+
Only used when ``text_overlay`` is set.
|
| 103 |
|
| 104 |
Returns:
|
| 105 |
``out_path``.
|
|
|
|
| 116 |
org_w, org_h, fps = vmeta["width"], vmeta["height"], vmeta["fps"]
|
| 117 |
|
| 118 |
# Extract every native frame so frame indices align with the detections.
|
| 119 |
+
frames = ffmpret.extract_frames(
|
| 120 |
+
video_path,
|
| 121 |
+
fps=None,
|
| 122 |
+
write_timestamp=False,
|
| 123 |
+
text_overlay=text_overlay,
|
| 124 |
+
text_font_size=text_font_size,
|
| 125 |
+
text_y_position=text_y_position,
|
| 126 |
+
)
|
| 127 |
if not frames:
|
| 128 |
raise ValueError(f"no frames decoded from {video_path}")
|
| 129 |
|
|
|
|
| 457 |
mask_alpha: float = 0.5,
|
| 458 |
width: int = 3,
|
| 459 |
font_size: int = 20,
|
| 460 |
+
text_overlay: Optional[str] = None,
|
| 461 |
+
text_font_size: int = 20,
|
| 462 |
+
text_y_position: str = "bottom",
|
| 463 |
) -> str:
|
| 464 |
"""Annotate ``video_path`` using detections from a JSON file; save to ``out_path``.
|
| 465 |
|
|
|
|
| 481 |
mask_alpha=mask_alpha,
|
| 482 |
width=width,
|
| 483 |
font_size=font_size,
|
| 484 |
+
text_overlay=text_overlay,
|
| 485 |
+
text_font_size=text_font_size,
|
| 486 |
+
text_y_position=text_y_position,
|
| 487 |
)
|
| 488 |
|
| 489 |
|