From 98476dfacffe5b64889ecef2ff0ca18753b4fccc Mon Sep 17 00:00:00 2001 From: AnonymDevOSS Date: Tue, 28 Oct 2025 14:05:37 +0100 Subject: [PATCH 1/7] feat: add threaded I/O pipeline for video processing Implements pipeline with bounded queues to overlap decode, compute and encode. Reduces I/O stalls. --- supervision/utils/video.py | 126 +++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/supervision/utils/video.py b/supervision/utils/video.py index 3b281b4e2..5fb2c4bf9 100644 --- a/supervision/utils/video.py +++ b/supervision/utils/video.py @@ -1,9 +1,11 @@ from __future__ import annotations +import threading import time from collections import deque from collections.abc import Callable, Generator from dataclasses import dataclass +from queue import Queue import cv2 import numpy as np @@ -255,6 +257,130 @@ def callback(scene: np.ndarray, index: int) -> np.ndarray: sink.write_frame(frame=result_frame) +def process_video_threads( + source_path: str, + target_path: str, + callback: Callable[[np.ndarray, int], np.ndarray], + *, + max_frames: int | None = None, + prefetch: int = 32, + writer_buffer: int = 32, + show_progress: bool = False, + progress_message: str = "Processing video (with threads)", +) -> None: + """ + Process a video using a threaded pipeline that asynchronously + reads frames, applies a callback to each, and writes the results + to an output file. + + Overview: + This function implements a three-stage pipeline designed to maximize + frame throughput. + + │ Reader │ >> │ Processor │ >> │ Writer │ + (thread) (main) (thread) + + - Reader thread: reads frames from disk into a bounded queue ('read_q') + until full, then blocks. This ensures we never load more than 'prefetch' + frames into memory at once. + + - Main thread: dequeues frames, applies the 'callback(frame, idx)', + and enqueues the processed result into 'write_q'. + This is the compute stage. It's important to note that it's not threaded, + so you can safely use any detectors, trackers, or other stateful objects + without synchronization issues. + + - Writer thread: dequeues frames and writes them to disk. + + Both queues are bounded to enforce back-pressure: + - The reader cannot outpace processing (avoids unbounded RAM usage). + - The processor cannot outpace writing (avoids output buffer bloat). + + Summary: + - It's thread-safe: because the callback runs only in the main thread, + using a single stateful detector/tracker inside callback does not require + synchronization with the reader/writer threads. + + - While the main thread processes frame N, the reader is already decoding frame N+1, + and the writer is encoding frame N-1. They operate concurrently without blocking + each other. + + - When is it fastest? + - When there's heavy computation in the callback function that releases + the Python GIL (for example, OpenCV filters, resizes, color conversions, ...) + - When using CUDA or GPU-accelerated inference. + + - When is it better not to use it? + - When the callback function is Python-heavy and GIL-bound. In that case, + using a process-based approach is more effective. + + Args: + source_path (str): The path to the source video file. + target_path (str): The path to the target video file. + callback (Callable[[np.ndarray, int], np.ndarray]): A function that takes in + a numpy ndarray representation of a video frame and an + int index of the frame and returns a processed numpy ndarray + representation of the frame. + max_frames (Optional[int]): The maximum number of frames to process. + prefetch (int): The maximum number of frames buffered by the reader thread. + writer_buffer (int): The maximum number of frames buffered before writing. + show_progress (bool): Whether to show a progress bar. + progress_message (str): The message to display in the progress bar. + """ + + source_video_info = VideoInfo.from_video_path(video_path=source_path) + total_frames = ( + min(source_video_info.total_frames, max_frames) + if max_frames is not None + else source_video_info.total_frames + ) + + # Each queue includes frames + sentinel + read_q: Queue[tuple[int, np.ndarray] | None] = Queue(maxsize=prefetch) + write_q: Queue[np.ndarray | None] = Queue(maxsize=writer_buffer) + + def reader_thread(): + gen = get_video_frames_generator(source_path=source_path, end=max_frames) + for idx, frame in enumerate(gen): + read_q.put((idx, frame)) + read_q.put(None) # sentinel + + def writer_thread(video_sink: VideoSink): + while True: + frame = write_q.get() + if frame is None: + break + video_sink.write_frame(frame=frame) + + # Heads up! We set 'daemon=True' so this thread won't block program exit + # if the main thread finishes first. + t_reader = threading.Thread(target=reader_thread, daemon=True) + with VideoSink(target_path=target_path, video_info=source_video_info) as sink: + t_writer = threading.Thread(target=writer_thread, args=(sink,), daemon=True) + t_reader.start() + t_writer.start() + + process_bar = tqdm( + total=total_frames, disable=not show_progress, desc=progress_message + ) + + # Main thread: we take a frame, apply function and update process bar. + while True: + item = read_q.get() + if item is None: + break + idx, frame = item + out = callback(frame, idx) + write_q.put(out) + if total_frames is not None: + process_bar.update(1) + + write_q.put(None) + t_reader.join() + t_writer.join() + process_bar.close() + + class FPSMonitor: """ A class for monitoring frames per second (FPS) to benchmark latency. From 03f623920a045fb4a352b341ce84174e344bae3a Mon Sep 17 00:00:00 2001 From: AnonymDevOSS Date: Tue, 28 Oct 2025 14:05:52 +0100 Subject: [PATCH 2/7] feat: tests for add threaded I/O pipeline for video processing --- test/utils/test_process_video.py | 95 ++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 test/utils/test_process_video.py diff --git a/test/utils/test_process_video.py b/test/utils/test_process_video.py new file mode 100644 index 000000000..2c3c68a9b --- /dev/null +++ b/test/utils/test_process_video.py @@ -0,0 +1,95 @@ +from pathlib import Path + +import cv2 +import numpy as np +import pytest + +import supervision as sv + + +def make_video( + path: Path, w: int = 160, h: int = 96, fps: int = 20, frames: int = 24 +) -> None: + """Create a small synthetic test video with predictable frame-colors.""" + fourcc = cv2.VideoWriter_fourcc(*"mp4v") + writer = cv2.VideoWriter(str(path), fourcc, fps, (w, h)) + assert writer.isOpened(), "Failed to open VideoWriter" + for i in range(frames): + v = (i * 11) % 250 + frame = np.full((h, w, 3), (v, 255 - v, (2 * v) % 255), np.uint8) + writer.write(frame) + writer.release() + + +def read_frames(path: Path) -> list[np.ndarray]: + """Read all frames from a video into memory.""" + cap = cv2.VideoCapture(str(path)) + assert cap.isOpened(), f"Cannot open video: {path}" + out = [] + while True: + ok, frame = cap.read() + if not ok: + break + out.append(frame) + cap.release() + return out + + +def frames_equal(a: np.ndarray, b: np.ndarray, max_abs_tol: int = 0) -> bool: + """Return True if frames are the same within acertain tolerance.""" + if a.shape != b.shape: + return False + diff = np.abs(a.astype(np.int16) - b.astype(np.int16)) + return diff.max() <= max_abs_tol + + +def callback_noop(frame: np.ndarray, idx: int) -> np.ndarray: + """No-op callback: validates pure pipeline correctness.""" + return frame + + +def callbackb_opencv(frame: np.ndarray, idx: int) -> np.ndarray: + """ + Simulations some cv2 task... + """ + g = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + return cv2.cvtColor(g, cv2.COLOR_GRAY2BGR) + + +@pytest.mark.parametrize( + "callback", [callback_noop, callbackb_opencv], ids=["identity", "opencv"] +) +def test_process_video_vs_threads_same_output(callback, tmp_path: Path): + """ + Ensure that process_video() and process_video_threads() produce identical + results for the same synthetic source video and callback. + """ + name = callback.__name__ + src = tmp_path / f"src_{name}.mp4" + dst_single = tmp_path / f"out_single_{name}.mp4" + dst_threads = tmp_path / f"out_threads_{name}.mp4" + + make_video(src, frames=24) + + sv.utils.video.process_video( + source_path=str(src), + target_path=str(dst_single), + callback=callback, + show_progress=False, + ) + sv.utils.video.process_video_threads( + source_path=str(src), + target_path=str(dst_threads), + callback=callback, + prefetch=4, + writer_buffer=4, + show_progress=False, + ) + + frames_single = read_frames(dst_single) + frames_threads = read_frames(dst_threads) + + assert len(frames_single) == len(frames_threads) != 0, "Frame count mismatch." + + for i, (fs, ft) in enumerate(zip(frames_single, frames_threads)): + assert frames_equal(fs, ft), f"Frame {i} is different." From 15364ac9a3958a529ec2ad0db60fa43043d2f500 Mon Sep 17 00:00:00 2001 From: AnonymDevOSS Date: Fri, 14 Nov 2025 21:45:06 +0100 Subject: [PATCH 3/7] Rename process_video_threads to process_video and remove legacy implementation and redundant tests --- supervision/utils/video.py | 65 +--------------------- test/utils/test_process_video.py | 95 -------------------------------- 2 files changed, 1 insertion(+), 159 deletions(-) delete mode 100644 test/utils/test_process_video.py diff --git a/supervision/utils/video.py b/supervision/utils/video.py index 5fb2c4bf9..dbf63a985 100644 --- a/supervision/utils/video.py +++ b/supervision/utils/video.py @@ -195,69 +195,6 @@ def get_video_frames_generator( def process_video( - source_path: str, - target_path: str, - callback: Callable[[np.ndarray, int], np.ndarray], - max_frames: int | None = None, - show_progress: bool = False, - progress_message: str = "Processing video", -) -> None: - """ - Process a video file by applying a callback function on each frame - and saving the result to a target video file. - - Args: - source_path (str): The path to the source video file. - target_path (str): The path to the target video file. - callback (Callable[[np.ndarray, int], np.ndarray]): A function that takes in - a numpy ndarray representation of a video frame and an - int index of the frame and returns a processed numpy ndarray - representation of the frame. - max_frames (Optional[int]): The maximum number of frames to process. - show_progress (bool): Whether to show a progress bar. - progress_message (str): The message to display in the progress bar. - - Examples: - ```python - import supervision as sv - - def callback(scene: np.ndarray, index: int) -> np.ndarray: - ... - - process_video( - source_path=, - target_path=, - callback=callback - ) - ``` - """ - source_video_info = VideoInfo.from_video_path(video_path=source_path) - video_frames_generator = get_video_frames_generator( - source_path=source_path, end=max_frames - ) - with VideoSink(target_path=target_path, video_info=source_video_info) as sink: - total_frames = ( - min(source_video_info.total_frames, max_frames) - if max_frames is not None - else source_video_info.total_frames - ) - for index, frame in enumerate( - tqdm( - video_frames_generator, - total=total_frames, - disable=not show_progress, - desc=progress_message, - ) - ): - result_frame = callback(frame, index) - sink.write_frame(frame=result_frame) - else: - for index, frame in enumerate(video_frames_generator): - result_frame = callback(frame, index) - sink.write_frame(frame=result_frame) - - -def process_video_threads( source_path: str, target_path: str, callback: Callable[[np.ndarray, int], np.ndarray], @@ -266,7 +203,7 @@ def process_video_threads( prefetch: int = 32, writer_buffer: int = 32, show_progress: bool = False, - progress_message: str = "Processing video (with threads)", + progress_message: str = "Processing video", ) -> None: """ Process a video using a threaded pipeline that asynchronously diff --git a/test/utils/test_process_video.py b/test/utils/test_process_video.py deleted file mode 100644 index 2c3c68a9b..000000000 --- a/test/utils/test_process_video.py +++ /dev/null @@ -1,95 +0,0 @@ -from pathlib import Path - -import cv2 -import numpy as np -import pytest - -import supervision as sv - - -def make_video( - path: Path, w: int = 160, h: int = 96, fps: int = 20, frames: int = 24 -) -> None: - """Create a small synthetic test video with predictable frame-colors.""" - fourcc = cv2.VideoWriter_fourcc(*"mp4v") - writer = cv2.VideoWriter(str(path), fourcc, fps, (w, h)) - assert writer.isOpened(), "Failed to open VideoWriter" - for i in range(frames): - v = (i * 11) % 250 - frame = np.full((h, w, 3), (v, 255 - v, (2 * v) % 255), np.uint8) - writer.write(frame) - writer.release() - - -def read_frames(path: Path) -> list[np.ndarray]: - """Read all frames from a video into memory.""" - cap = cv2.VideoCapture(str(path)) - assert cap.isOpened(), f"Cannot open video: {path}" - out = [] - while True: - ok, frame = cap.read() - if not ok: - break - out.append(frame) - cap.release() - return out - - -def frames_equal(a: np.ndarray, b: np.ndarray, max_abs_tol: int = 0) -> bool: - """Return True if frames are the same within acertain tolerance.""" - if a.shape != b.shape: - return False - diff = np.abs(a.astype(np.int16) - b.astype(np.int16)) - return diff.max() <= max_abs_tol - - -def callback_noop(frame: np.ndarray, idx: int) -> np.ndarray: - """No-op callback: validates pure pipeline correctness.""" - return frame - - -def callbackb_opencv(frame: np.ndarray, idx: int) -> np.ndarray: - """ - Simulations some cv2 task... - """ - g = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) - return cv2.cvtColor(g, cv2.COLOR_GRAY2BGR) - - -@pytest.mark.parametrize( - "callback", [callback_noop, callbackb_opencv], ids=["identity", "opencv"] -) -def test_process_video_vs_threads_same_output(callback, tmp_path: Path): - """ - Ensure that process_video() and process_video_threads() produce identical - results for the same synthetic source video and callback. - """ - name = callback.__name__ - src = tmp_path / f"src_{name}.mp4" - dst_single = tmp_path / f"out_single_{name}.mp4" - dst_threads = tmp_path / f"out_threads_{name}.mp4" - - make_video(src, frames=24) - - sv.utils.video.process_video( - source_path=str(src), - target_path=str(dst_single), - callback=callback, - show_progress=False, - ) - sv.utils.video.process_video_threads( - source_path=str(src), - target_path=str(dst_threads), - callback=callback, - prefetch=4, - writer_buffer=4, - show_progress=False, - ) - - frames_single = read_frames(dst_single) - frames_threads = read_frames(dst_threads) - - assert len(frames_single) == len(frames_threads) != 0, "Frame count mismatch." - - for i, (fs, ft) in enumerate(zip(frames_single, frames_threads)): - assert frames_equal(fs, ft), f"Frame {i} is different." From fc0e133ea9f1dce65ffe7a6cfd4798ba44d6b058 Mon Sep 17 00:00:00 2001 From: AnonymDevOSS Date: Fri, 14 Nov 2025 21:53:14 +0100 Subject: [PATCH 4/7] added example as it was delited by mistake --- supervision/utils/video.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/supervision/utils/video.py b/supervision/utils/video.py index dbf63a985..7227b92b0 100644 --- a/supervision/utils/video.py +++ b/supervision/utils/video.py @@ -251,6 +251,18 @@ def process_video( - When the callback function is Python-heavy and GIL-bound. In that case, using a process-based approach is more effective. + Examples: + ```python + import supervision as sv + def callback(scene: np.ndarray, index: int) -> np.ndarray: + ... + process_video( + source_path=, + target_path=, + callback=callback + ) + ``` + Args: source_path (str): The path to the source video file. target_path (str): The path to the target video file. From 80d76e712a372105becf2a493e617f06cd5c7069 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Sat, 15 Nov 2025 02:10:04 +0100 Subject: [PATCH 5/7] style and docstring improvements --- supervision/__init__.py | 2 + supervision/detection/utils/boxes.py | 51 ++++++++ supervision/utils/video.py | 186 +++++++++++++-------------- 3 files changed, 141 insertions(+), 98 deletions(-) diff --git a/supervision/__init__.py b/supervision/__init__.py index 04d3fb254..aa23963bb 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -57,6 +57,7 @@ move_boxes, pad_boxes, scale_boxes, + box_aspect_ratio ) from supervision.detection.utils.converters import ( mask_to_polygons, @@ -201,6 +202,7 @@ "box_iou_batch_with_jaccard", "box_non_max_merge", "box_non_max_suppression", + "box_aspect_ratio", "calculate_masks_centroids", "calculate_optimal_line_thickness", "calculate_optimal_text_scale", diff --git a/supervision/detection/utils/boxes.py b/supervision/detection/utils/boxes.py index 3b01fcb68..904739899 100644 --- a/supervision/detection/utils/boxes.py +++ b/supervision/detection/utils/boxes.py @@ -6,6 +6,57 @@ from supervision.detection.utils.iou_and_nms import box_iou_batch +def box_aspect_ratio(xyxy: np.ndarray) -> np.ndarray: + """ + Calculate aspect ratios of bounding boxes given in xyxy format. + + Computes the width divided by height for each bounding box. Returns NaN + for boxes with zero height to avoid division errors. + + Args: + xyxy (`numpy.ndarray`): Array of bounding boxes in `(x_min, y_min, x_max, y_max)` + format with shape `(N, 4)`. + + Returns: + `numpy.ndarray`: Array of aspect ratios with shape `(N,)`, where each element is + the width divided by height of a box. Elements are NaN if height is zero. + + Examples: + ```python + import numpy as np + import supervision as sv + + xyxy = np.array([ + [10, 20, 30, 50], + [0, 0, 40, 10], + ]) + + sv.box_aspect_ratio(xyxy) + # array([0.66666667, 4. ]) + + xyxy = np.array([ + [10, 10, 30, 10], + [5, 5, 25, 25], + ]) + + sv.box_aspect_ratio(xyxy) + # array([ nan, 1. ]) + ``` + """ + widths = xyxy[:, 2] - xyxy[:, 0] + heights = xyxy[:, 3] - xyxy[:, 1] + + aspect_ratios = np.full_like(widths, np.nan, dtype=np.float64) + np.divide( + widths, + heights, + out=aspect_ratios, + where=heights != 0, + ) + + return aspect_ratios + + def clip_boxes(xyxy: np.ndarray, resolution_wh: tuple[int, int]) -> np.ndarray: """ Clips bounding boxes coordinates to fit within the frame resolution. diff --git a/supervision/utils/video.py b/supervision/utils/video.py index 7227b92b0..0ece0916d 100644 --- a/supervision/utils/video.py +++ b/supervision/utils/video.py @@ -206,128 +206,118 @@ def process_video( progress_message: str = "Processing video", ) -> None: """ - Process a video using a threaded pipeline that asynchronously - reads frames, applies a callback to each, and writes the results - to an output file. + Process video frames asynchronously using a threaded pipeline. + + This function orchestrates a three-stage pipeline to optimize video processing + throughput: + + 1. Reader thread: Continuously reads frames from the source video file and + enqueues them into a bounded queue (`frame_read_queue`). The queue size is + limited by the `prefetch` parameter to control memory usage. + 2. Main thread (Processor): Dequeues frames from `frame_read_queue`, applies the + user-defined `callback` function to process each frame, then enqueues the + processed frames into another bounded queue (`frame_write_queue`) for writing. + The processing happens in the main thread, simplifying use of stateful objects + without synchronization. + 3. Writer thread: Dequeues processed frames from `frame_write_queue` and writes + them sequentially to the output video file. - Overview: - This function implements a three-stage pipeline designed to maximize - frame throughput. - - │ Reader │ >> │ Processor │ >> │ Writer │ - (thread) (main) (thread) - - - Reader thread: reads frames from disk into a bounded queue ('read_q') - until full, then blocks. This ensures we never load more than 'prefetch' - frames into memory at once. - - - Main thread: dequeues frames, applies the 'callback(frame, idx)', - and enqueues the processed result into 'write_q'. - This is the compute stage. It's important to note that it's not threaded, - so you can safely use any detectors, trackers, or other stateful objects - without synchronization issues. - - - Writer thread: dequeues frames and writes them to disk. - - Both queues are bounded to enforce back-pressure: - - The reader cannot outpace processing (avoids unbounded RAM usage). - - The processor cannot outpace writing (avoids output buffer bloat). + Args: + source_path (str): Path to the input video file. + target_path (str): Path where the processed video will be saved. + callback (Callable[[numpy.ndarray, int], numpy.ndarray]): Function called for + each frame, accepting the frame as a numpy array and its zero-based index, + returning the processed frame. + max_frames (int | None): Optional maximum number of frames to process. + If None, the entire video is processed (default). + prefetch (int): Maximum number of frames buffered by the reader thread. + Controls memory use; default is 32. + writer_buffer (int): Maximum number of frames buffered before writing. + Controls output buffer size; default is 32. + show_progress (bool): Whether to display a tqdm progress bar during processing. + Default is False. + progress_message (str): Description shown in the progress bar. - Summary: - - It's thread-safe: because the callback runs only in the main thread, - using a single stateful detector/tracker inside callback does not require - synchronization with the reader/writer threads. + Returns: + None - - While the main thread processes frame N, the reader is already decoding frame N+1, - and the writer is encoding frame N-1. They operate concurrently without blocking - each other. + Example: + ```python + import cv2 + import supervision as sv + from rfdetr import RFDETRMedium - - When is it fastest? - - When there's heavy computation in the callback function that releases - the Python GIL (for example, OpenCV filters, resizes, color conversions, ...) - - When using CUDA or GPU-accelerated inference. + model = RFDETRMedium() - - When is it better not to use it? - - When the callback function is Python-heavy and GIL-bound. In that case, - using a process-based approach is more effective. + def callback(frame, frame_index): + return model.predict(frame) - Examples: - ```python - import supervision as sv - def callback(scene: np.ndarray, index: int) -> np.ndarray: - ... process_video( - source_path=, - target_path=, - callback=callback + source_path="source.mp4", + target_path="target.mp4", + callback=frame_callback, ) ``` - - Args: - source_path (str): The path to the source video file. - target_path (str): The path to the target video file. - callback (Callable[[np.ndarray, int], np.ndarray]): A function that takes in - a numpy ndarray representation of a video frame and an - int index of the frame and returns a processed numpy ndarray - representation of the frame. - max_frames (Optional[int]): The maximum number of frames to process. - prefetch (int): The maximum number of frames buffered by the reader thread. - writer_buffer (int): The maximum number of frames buffered before writing. - show_progress (bool): Whether to show a progress bar. - progress_message (str): The message to display in the progress bar. """ - - source_video_info = VideoInfo.from_video_path(video_path=source_path) + video_info = VideoInfo.from_video_path(video_path=source_path) total_frames = ( - min(source_video_info.total_frames, max_frames) + min(video_info.total_frames, max_frames) if max_frames is not None - else source_video_info.total_frames + else video_info.total_frames ) - # Each queue includes frames + sentinel - read_q: Queue[tuple[int, np.ndarray] | None] = Queue(maxsize=prefetch) - write_q: Queue[np.ndarray | None] = Queue(maxsize=writer_buffer) + frame_read_queue: Queue[tuple[int, np.ndarray] | None] = Queue(maxsize=prefetch) + frame_write_queue: Queue[np.ndarray | None] = Queue(maxsize=writer_buffer) - def reader_thread(): - gen = get_video_frames_generator(source_path=source_path, end=max_frames) - for idx, frame in enumerate(gen): - read_q.put((idx, frame)) - read_q.put(None) # sentinel + def reader_thread() -> None: + frame_generator = get_video_frames_generator( + source_path=source_path, + end=max_frames, + ) + for frame_index, frame in enumerate(frame_generator): + frame_read_queue.put((frame_index, frame)) + frame_read_queue.put(None) - def writer_thread(video_sink: VideoSink): + def writer_thread(video_sink: VideoSink) -> None: while True: - frame = write_q.get() + frame = frame_write_queue.get() if frame is None: break video_sink.write_frame(frame=frame) - # Heads up! We set 'daemon=True' so this thread won't block program exit - # if the main thread finishes first. - t_reader = threading.Thread(target=reader_thread, daemon=True) - with VideoSink(target_path=target_path, video_info=source_video_info) as sink: - t_writer = threading.Thread(target=writer_thread, args=(sink,), daemon=True) - t_reader.start() - t_writer.start() + reader_worker = threading.Thread(target=reader_thread, daemon=True) + with VideoSink(target_path=target_path, video_info=video_info) as video_sink: + writer_worker = threading.Thread( + target=writer_thread, + args=(video_sink,), + daemon=True, + ) + + reader_worker.start() + writer_worker.start() - process_bar = tqdm( - total=total_frames, disable=not show_progress, desc=progress_message + progress_bar = tqdm( + total=total_frames, + disable=not show_progress, + desc=progress_message, ) - # Main thread: we take a frame, apply function and update process bar. - while True: - item = read_q.get() - if item is None: - break - idx, frame = item - out = callback(frame, idx) - write_q.put(out) - if total_frames is not None: - process_bar.update(1) - - write_q.put(None) - t_reader.join() - t_writer.join() - process_bar.close() + try: + while True: + read_item = frame_read_queue.get() + if read_item is None: + break + + frame_index, frame = read_item + processed_frame = callback(frame, frame_index) + + frame_write_queue.put(processed_frame) + progress_bar.update(1) + finally: + frame_write_queue.put(None) + reader_worker.join() + writer_worker.join() + progress_bar.close() class FPSMonitor: From 38f89125b1d6d4b5d7791c01dd0ecdc7f8984681 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 15 Nov 2025 01:10:43 +0000 Subject: [PATCH 6/7] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20auto=20?= =?UTF-8?q?format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/supervision/__init__.py b/supervision/__init__.py index aa23963bb..43d63cad5 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -52,12 +52,12 @@ from supervision.detection.tools.polygon_zone import PolygonZone, PolygonZoneAnnotator from supervision.detection.tools.smoother import DetectionsSmoother from supervision.detection.utils.boxes import ( + box_aspect_ratio, clip_boxes, denormalize_boxes, move_boxes, pad_boxes, scale_boxes, - box_aspect_ratio ) from supervision.detection.utils.converters import ( mask_to_polygons, @@ -197,12 +197,12 @@ "VideoInfo", "VideoSink", "approximate_polygon", + "box_aspect_ratio", "box_iou", "box_iou_batch", "box_iou_batch_with_jaccard", "box_non_max_merge", "box_non_max_suppression", - "box_aspect_ratio", "calculate_masks_centroids", "calculate_optimal_line_thickness", "calculate_optimal_text_scale", From 316cb6271d53028758b3bf5b92b8f1b59f2bafbf Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Sat, 15 Nov 2025 02:33:11 +0100 Subject: [PATCH 7/7] make `ruff` happy --- supervision/detection/utils/boxes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/supervision/detection/utils/boxes.py b/supervision/detection/utils/boxes.py index 904739899..1b1d9d2d3 100644 --- a/supervision/detection/utils/boxes.py +++ b/supervision/detection/utils/boxes.py @@ -14,8 +14,8 @@ def box_aspect_ratio(xyxy: np.ndarray) -> np.ndarray: for boxes with zero height to avoid division errors. Args: - xyxy (`numpy.ndarray`): Array of bounding boxes in `(x_min, y_min, x_max, y_max)` - format with shape `(N, 4)`. + xyxy (`numpy.ndarray`): Array of bounding boxes in + `(x_min, y_min, x_max, y_max)` format with shape `(N, 4)`. Returns: `numpy.ndarray`: Array of aspect ratios with shape `(N,)`, where each element is