Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
3580d25
feat(utils): add TkImageWindow and switch to opencv-python-headless
Borda Jun 14, 2026
e624ea3
Potential fix for pull request finding
Borda Jun 15, 2026
33000e8
Potential fix for pull request finding
Borda Jun 15, 2026
335caa0
Potential fix for pull request finding
Borda Jun 15, 2026
937ec58
Potential fix for pull request finding
Borda Jun 15, 2026
e17a4c0
Fix Tk image window event handling
Borda Jun 15, 2026
a70e11e
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] Jun 15, 2026
b817aee
Merge branch 'drop/cv2-gui' of https://github.com/roboflow/supervisio…
Borda Jun 15, 2026
bf1305b
Merge branch 'drop/cv2-gui' of https://github.com/roboflow/supervisio…
Borda Jun 15, 2026
8987daa
Merge branch 'develop' into drop/cv2-gui
Borda Jun 15, 2026
b3bb85e
fix(tests): patch PIL.ImageTk via sys.modules to fix Python 3.12+ CI …
Borda Jun 15, 2026
703862d
fix(examples): revert 3 stream examples from TkImageWindow to cv2.imshow
Borda Jun 15, 2026
edec485
docs(image_window): clarify API differences from cv2 and correct exce…
Borda Jun 15, 2026
e1b59da
docs(video): add webcam ownership pattern to get_video_frames_generator
Borda Jun 15, 2026
f8b6bd4
docs: add TkImageWindow to mkdocs navigation
Borda Jun 15, 2026
cbf1a1a
Merge branch 'drop/cv2-gui' of https://github.com/roboflow/supervisio…
Borda Jun 15, 2026
0262595
Merge branch 'develop' into drop/cv2-gui
Borda Jun 16, 2026
440839e
fix(detection): add type args to _merge_obb_corners parameter
Borda Jun 16, 2026
64382e9
fix(utils): correct TkImageWindow docstring, key-queue, ghost-window,…
Borda Jun 16, 2026
c58b9c4
docs(changelog): add TkImageWindow entry, fix pip co-install wording
Borda Jun 16, 2026
37d0ab1
test(utils): add edge-case tests, parametrize TkImageWindow error paths
Borda Jun 16, 2026
2c08819
Merge branch 'develop' into drop/cv2-gui
Borda Jun 22, 2026
e6536f8
Merge branch 'develop' into drop/cv2-gui
Borda Jun 24, 2026
d5745f1
feat(utils): resize TkImageWindow scales image, not crops
Borda Jun 26, 2026
e40f1ff
Merge branch 'develop' into drop/cv2-gui
Borda Jun 26, 2026
3c9ec36
Merge branch 'develop' into drop/cv2-gui
Borda Jun 28, 2026
589b2af
Merge branch 'develop' into drop/cv2-gui
Borda Jun 29, 2026
a50e180
Merge branch 'develop' into drop/cv2-gui
Borda Jul 8, 2026
8de90fd
Merge branch 'develop' into drop/cv2-gui
Borda Jul 9, 2026
5f5f82e
Merge branch 'develop' into drop/cv2-gui
Borda Jul 9, 2026
4227ad9
revert accidental delete
Borda Jul 9, 2026
67f7cef
Merge remote-tracking branch 'origin/develop' into drop/cv2-gui
Borda Jul 9, 2026
022f722
refactor(rename): TkImageWindow -> ImageWindow across examples/docs/e…
Borda Jul 9, 2026
97fd91b
fix(image_window): correct mouse coords after resize; reuse cv2_to_pi…
Borda Jul 9, 2026
d196a21
docs(changelog): rename mention, consolidate duplicate text, use warn…
Borda Jul 9, 2026
17ce271
lint: auto-fix violations after resolve cycle
Borda Jul 9, 2026
527779c
fix(changelog): move ImageWindow entry out of already-published 0.29.…
Borda Jul 9, 2026
c654b25
fix(utils): add ImageWindow.is_open and update examples
Borda Jul 10, 2026
c9ed0c2
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] Jul 10, 2026
9cd8c60
Potential fix for pull request finding
Borda Jul 10, 2026
bff69b4
Potential fix for pull request finding
Borda Jul 10, 2026
cfd1f69
Apply suggestions from code review
Borda Jul 14, 2026
e6fe172
Merge branch 'develop' into drop/cv2-gui
Borda Jul 14, 2026
d6996fb
docs: correct premature opencv-headless claims
Borda Jul 15, 2026
317900f
docs(changelog): drop premature OpenCV-dependency-change note
Borda Jul 15, 2026
51f665d
docs: fix wrong opencv-python-only claim for cv2.VideoCapture in FAQ
Borda Jul 15, 2026
e903d68
replace opencv-python-headless with opencv-python as dependency in uv…
Borda Jul 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ date_modified: 2026-07-08
- Fixed: dataset IO/export edge cases now avoid mutating caller-owned `Detections` during `DetectionDataset` construction, reject non-integer and out-of-range class ids with a clear `ValueError`, load COCO annotations that omit optional `iscrowd`/`area` fields, expose `DetectionDataset.from_coco(use_iscrowd=...)` without changing the existing positional `show_progress` argument, export mask pixel area to COCO when no stored area is present, ignore folder-structure root clutter and non-image files inside class folders, and accept PIL-readable YOLO images such as RGBA or palette PNGs.

### Added
- Added: [`sv.ImageWindow`](utils/image_window.md/#supervision.utils.image_window.ImageWindow) — tkinter + Pillow desktop window that replaces `cv2.imshow` / `cv2.waitKey`, usable regardless of which OpenCV wheel (or none) is installed. Key differences from cv2:
- `wait_key()` returns a tkinter keysym `str` (e.g. `"q"`, `"Escape"`) or `None`, not an `int` — update `key == ord("q")` to `key == "q"`.
- Mouse callback signature is `(x: int, y: int, event_type: str)` where `event_type` is `"down"`, `"up"`, or `"move"` — incompatible with cv2's `(event, x, y, flags, param)`.
- Only left-button events are captured; scroll, right-button, and modifier flags have no equivalent.
- Requires `python3-tk` (not pip-installable): `sudo apt-get install python3-tk` on Debian/Ubuntu, `brew install tcl-tk` on macOS with Homebrew/pyenv.
- `KeyPoints.merge` — combine a list of `KeyPoints` objects into one, mirroring `Detections.merge`. Empty inputs are ignored; all non-empty inputs must share the same number of keypoints per skeleton. Completes the merge-then-suppress workflow introduced by `KeyPoints.with_nms` ([#2412](https://github.com/roboflow/supervision/pull/2412))
- `BaseAnnotator.requires_mask` — class-level `bool` flag on all annotators; `True` for `MaskAnnotator`, `PolygonAnnotator`, and `HaloAnnotator`; `False` for all others. Integrations can inspect this before materializing expensive mask payloads ([#2370](https://github.com/roboflow/supervision/pull/2370))
- `CompactMask.from_coco_rle` — efficient COCO RLE ingestion into crop-scoped compact mask format without materializing dense `(N, H, W)` arrays ([#2367](https://github.com/roboflow/supervision/pull/2367))
Expand Down
21 changes: 21 additions & 0 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,27 @@ Install `supervision[metrics]`, then use `supervision.metrics.mean_average_preci

Yes. Supervision is free and open source under the MIT license.

## How do I process frames from a webcam with supervision?

Supervision does not support live camera capture. Manage the capture device yourself with `cv2.VideoCapture`, which works regardless of which OpenCV wheel (`opencv-python` or `opencv-python-headless`) is installed, and pass individual frames to supervision annotators:

```python
import cv2 # requires: pip install opencv-python (or opencv-python-headless)
import supervision as sv

cap = cv2.VideoCapture(0)
annotator = sv.BoxAnnotator()

while True:
ret, frame = cap.read()
if not ret:
break
# run your detector, then annotate:
# annotated = annotator.annotate(frame, detections)

cap.release()
```
Comment thread
Borda marked this conversation as resolved.

## Where is the source code?

The source code is available at [github.com/roboflow/supervision](https://github.com/roboflow/supervision).
12 changes: 12 additions & 0 deletions docs/utils/image_window.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
comments: true
status: new
---

# Image Window

<div class="md-typeset">
<h2><a href="#supervision.utils.image_window.ImageWindow">ImageWindow</a></h2>
</div>

:::supervision.utils.image_window.ImageWindow
11 changes: 6 additions & 5 deletions examples/count_people_in_zone/inference_example.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import json
import os

import cv2
import numpy as np
from inference.core.models.roboflow import RoboflowInferenceModel
from inference.models.utils import get_roboflow_model
Expand Down Expand Up @@ -117,7 +116,7 @@ def annotate(
"""
annotated_frame = frame.copy()
for zone, zone_annotator, box_annotator in zip(
zones, zone_annotators, box_annotators
zones, zone_annotators, box_annotators, strict=True
):
detections_in_zone = detections[zone.trigger(detections=detections)]
annotated_frame = zone_annotator.annotate(scene=annotated_frame)
Expand Down Expand Up @@ -179,6 +178,7 @@ def main(
)
sink.write_frame(annotated_frame)
else:
window = sv.ImageWindow("Processed Video")
for frame in tqdm(frames_generator, total=video_info.total_frames):
detections = detect(frame, model, confidence_threshold, iou_threshold)
annotated_frame = annotate(
Expand All @@ -188,11 +188,12 @@ def main(
box_annotators=box_annotators,
detections=detections,
)
cv2.imshow("Processed Video", annotated_frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
window.show(annotated_frame)
key = window.wait_key(1)
if not window.is_open or key == "q":
break

cv2.destroyAllWindows()
window.close()


if __name__ == "__main__":
Expand Down
11 changes: 6 additions & 5 deletions examples/count_people_in_zone/ultralytics_example.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import json

import cv2
import numpy as np
from tqdm import tqdm
from ultralytics import YOLO
Expand Down Expand Up @@ -116,7 +115,7 @@ def annotate(
"""
annotated_frame = frame.copy()
for zone, zone_annotator, box_annotator in zip(
zones, zone_annotators, box_annotators
zones, zone_annotators, box_annotators, strict=True
):
detections_in_zone = detections[zone.trigger(detections=detections)]
annotated_frame = zone_annotator.annotate(scene=annotated_frame)
Expand Down Expand Up @@ -167,6 +166,7 @@ def main(
)
sink.write_frame(annotated_frame)
else:
window = sv.ImageWindow("Processed Video")
for frame in tqdm(frames_generator, total=video_info.total_frames):
detections = detect(frame, model, confidence_threshold, iou_threshold)
annotated_frame = annotate(
Expand All @@ -176,11 +176,12 @@ def main(
box_annotators=box_annotators,
detections=detections,
)
cv2.imshow("Processed Video", annotated_frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
window.show(annotated_frame)
key = window.wait_key(1)
if not window.is_open or key == "q":
break

cv2.destroyAllWindows()
window.close()


if __name__ == "__main__":
Expand Down
10 changes: 6 additions & 4 deletions examples/speed_estimation/inference_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ def main(
coordinates = defaultdict(lambda: deque(maxlen=int(video_info.fps)))

with sv.VideoSink(target_video_path, video_info) as sink:
window = sv.ImageWindow("frame")
for frame in frame_generator:
results = model.infer(
frame, confidence=confidence_threshold, iou=iou_threshold
Expand All @@ -109,7 +110,7 @@ def main(
)
points = view_transformer.transform_points(points=points).astype(int)

for tracker_id, [_, y] in zip(detections.tracker_id, points):
for tracker_id, [_, y] in zip(detections.tracker_id, points, strict=True):
coordinates[tracker_id].append(y)

labels = []
Expand All @@ -136,10 +137,11 @@ def main(
)

sink.write_frame(annotated_frame)
cv2.imshow("frame", annotated_frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
window.show(annotated_frame)
key = window.wait_key(1)
if not window.is_open or key == "q":
break
cv2.destroyAllWindows()
window.close()


if __name__ == "__main__":
Expand Down
10 changes: 6 additions & 4 deletions examples/speed_estimation/ultralytics_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ def main(
coordinates = defaultdict(lambda: deque(maxlen=int(video_info.fps)))

with sv.VideoSink(target_video_path, video_info) as sink:
window = sv.ImageWindow("frame")
for frame in frame_generator:
result = model(frame, conf=confidence_threshold, iou=iou_threshold)[0]
detections = sv.Detections.from_ultralytics(result)
Expand All @@ -93,7 +94,7 @@ def main(
)
points = view_transformer.transform_points(points=points).astype(int)

for tracker_id, [_, y] in zip(detections.tracker_id, points):
for tracker_id, [_, y] in zip(detections.tracker_id, points, strict=True):
coordinates[tracker_id].append(y)

labels = []
Expand All @@ -120,10 +121,11 @@ def main(
)

sink.write_frame(annotated_frame)
cv2.imshow("frame", annotated_frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
window.show(annotated_frame)
key = window.wait_key(1)
if not window.is_open or key == "q":
break
cv2.destroyAllWindows()
window.close()


if __name__ == "__main__":
Expand Down
8 changes: 5 additions & 3 deletions examples/speed_estimation/yolo_nas_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ def main(
coordinates = defaultdict(lambda: deque(maxlen=int(video_info.fps)))

with sv.VideoSink(target_video_path, video_info) as sink:
window = sv.ImageWindow("frame")
for frame in frame_generator:
result = model.predict(frame, conf=confidence_threshold, iou=iou_threshold)[
0
Expand Down Expand Up @@ -123,10 +124,11 @@ def main(
)

sink.write_frame(annotated_frame)
cv2.imshow("frame", annotated_frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
window.show(annotated_frame)
key = window.wait_key(1)
if not window.is_open or key == "q":
break
cv2.destroyAllWindows()
window.close()


if __name__ == "__main__":
Expand Down
2 changes: 2 additions & 0 deletions examples/time_in_zone/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ https://github.com/roboflow/supervision/assets/26109316/d051cc8a-dd15-41d4-aa36-
uv pip install -r requirements.txt
```

The three RTSP `*_stream_example.py` scripts display frames from an `InferencePipeline` callback running on a worker thread, so they use OpenCV HighGUI instead of `sv.ImageWindow`. Install `opencv-python` and keep only one OpenCV wheel installed to run those scripts. The file and naive-stream examples use `sv.ImageWindow`, which works regardless of which OpenCV wheel (or none) is installed.

## 🛠 scripts

### `download_from_youtube`
Expand Down
9 changes: 5 additions & 4 deletions examples/time_in_zone/inference_file_example.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import cv2
import numpy as np
from inference import get_model
from utils.general import find_in_list, load_zones_config
Expand Down Expand Up @@ -49,6 +48,7 @@ def main(
]
timers = [FPSBasedTimer(video_info.fps) for _ in zones]

window = sv.ImageWindow("Processed Video")
for frame in frames_generator:
results = model.infer(
frame, confidence=confidence_threshold, iou_threshold=iou_threshold
Expand Down Expand Up @@ -84,10 +84,11 @@ def main(
custom_color_lookup=custom_color_lookup,
)

cv2.imshow("Processed Video", annotated_frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
window.show(annotated_frame)
key = window.wait_key(1)
if not window.is_open or key == "q":
break
cv2.destroyAllWindows()
window.close()


if __name__ == "__main__":
Expand Down
9 changes: 5 additions & 4 deletions examples/time_in_zone/inference_naive_stream_example.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import cv2
import numpy as np
from inference import get_model
from utils.general import find_in_list, get_stream_frames_generator, load_zones_config
Expand Down Expand Up @@ -49,6 +48,7 @@ def main(
]
timers = [ClockBasedTimer() for _ in zones]

window = sv.ImageWindow("Processed Video")
for frame in frames_generator:
fps_monitor.tick()
fps = fps_monitor.fps
Expand Down Expand Up @@ -94,10 +94,11 @@ def main(
custom_color_lookup=custom_color_lookup,
)

cv2.imshow("Processed Video", annotated_frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
window.show(annotated_frame)
key = window.wait_key(1)
if not window.is_open or key == "q":
break
cv2.destroyAllWindows()
window.close()


if __name__ == "__main__":
Expand Down
9 changes: 5 additions & 4 deletions examples/time_in_zone/rfdetr_file_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from enum import Enum

import cv2
import numpy as np
from rfdetr import RFDETRBase, RFDETRLarge, RFDETRMedium, RFDETRNano, RFDETRSmall
from utils.general import find_in_list, load_zones_config
Expand Down Expand Up @@ -128,6 +127,7 @@ def main(
]
timers = [FPSBasedTimer(video_info.fps) for _ in zones]

window = sv.ImageWindow("Processed Video")
for frame in frames_generator:
detections = model.predict(frame, threshold=confidence_threshold)
detections = detections[find_in_list(detections.class_id, classes)]
Expand Down Expand Up @@ -161,10 +161,11 @@ def main(
custom_color_lookup=custom_color_lookup,
)

cv2.imshow("Processed Video", annotated_frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
window.show(annotated_frame)
key = window.wait_key(1)
if not window.is_open or key == "q":
break
cv2.destroyAllWindows()
window.close()


if __name__ == "__main__":
Expand Down
9 changes: 5 additions & 4 deletions examples/time_in_zone/rfdetr_naive_stream_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from enum import Enum

import cv2
import numpy as np
from rfdetr import RFDETRBase, RFDETRLarge, RFDETRMedium, RFDETRNano, RFDETRSmall
from utils.general import find_in_list, get_stream_frames_generator, load_zones_config
Expand Down Expand Up @@ -128,6 +127,7 @@ def main(
]
timers = [ClockBasedTimer() for _ in zones]

window = sv.ImageWindow("Processed Video")
for frame in frames_generator:
fps_monitor.tick()
fps = fps_monitor.fps
Expand Down Expand Up @@ -171,11 +171,12 @@ def main(
custom_color_lookup=custom_color_lookup,
)

cv2.imshow("Processed Video", annotated_frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
window.show(annotated_frame)
key = window.wait_key(1)
if not window.is_open or key == "q":
break

cv2.destroyAllWindows()
window.close()


if __name__ == "__main__":
Expand Down
Loading
Loading