warden-vision
Reusable computer vision building blocks for detection, tracking, and
annotation — detection/keypoint/classification data structures (including
oriented bounding boxes), geometry and counting zones, three multi-object
trackers, ~20 annotators, evaluation metrics, dataset I/O, and video/image
utilities — in the spirit of Roboflow's Python
supervision library,
reimplemented as a dependency-light, feature-gated Rust crate.
This is an independent project, not affiliated with or endorsed by Roboflow.
Why
Python supervision is the de facto toolkit for gluing together an object
detector's output, a tracker, and on-screen annotation. warden-vision
covers the same ground natively in Rust — no Python runtime, no PyTorch —
so it fits into Rust inference pipelines (e.g. using
ort, tract,
or candle to run the model
itself) without a foreign-language boundary between inference and
post-processing.
Features at a glance
- Core data model —
Detections/Detection(boxes, masks, oriented bounding boxes viaDetection::from_obb, confidence, class, tracker id),KeyPoints(pose/landmarks),Classifications(whole-image classification). - Detector-output parsing —
Detections::from_ultralytics_onnxandDetections::from_transformers_onnxparse the raw tensor layouts YOLOv8+ and DETR-family models produce, independent of which Rust inference runtime you used to run them. - Geometry —
Point,Rect, anchorPositions, polygon utilities, rotated-rectangle IoU (oriented_box_iou),LineZone/PolygonZonecrossing/occupancy counters. - Tracking —
SortTracker(Kalman + IoU),ByteTracker(two-stage high/low-confidence matching), andDeepSortTracker(motion + caller- supplied appearance embeddings, for when paths cross), plusDetectionsSmootherfor jitter reduction andInferenceSlicerfor SAHI-style tiled inference (with arayon-parallelrun_parallelbehind theparallelfeature). - Annotation — box, mask, polygon, label (bitmap font or, via
ab_glyph, real TTF/OTF), circle, dot, triangle, ellipse, round-box, box-corner, trace, heat map, halo, percentage bar, icon overlay, background dim, blur/pixelate redaction, and keypoint skeleton annotators — all working against a pure-Rustimage::RgbaImagebackend and (optionally) an OpenCVMatbackend, sharing one configuration type. - Metrics — Precision, Recall, F1, ConfusionMatrix, MeanAveragePrecision, MeanAverageRecall.
- Dataset I/O — COCO, YOLO, and Pascal VOC detection formats, plus an image-folder classification format.
- Video/image utilities —
FPSMonitor, letterbox/resize/crop/overlay,ImageSink, and (via OpenCVvideoio)VideoInfo/VideoSink/frame iteration.
Installation
[]
= { = "0.1", = ["annotate-image"] }
Feature flags
| Feature | Adds | Native deps required |
|---|---|---|
core (default) |
Detections, geometry, trackers, metrics — pure Rust, always on | none |
annotate-image |
Pure-Rust annotators drawing on image::RgbaImage, RichLabelAnnotator, image utilities |
none |
annotate-opencv |
The same annotators drawing on opencv::core::Mat, plus video I/O |
system OpenCV install |
datasets |
COCO/YOLO/Pascal VOC/classification dataset loaders and exporters | none |
parallel |
rayon-parallel Detections::non_max_suppression_parallel, InferenceSlicer::run_parallel |
none |
core has no dependencies beyond ndarray/nalgebra/lapjv/thiserror
and builds anywhere Rust does. annotate-opencv needs a system OpenCV
install (imgproc + videoio modules) and a working bindgen/libclang
toolchain to build its native bindings. parallel is worth it once a frame
has hundreds to thousands of candidate boxes/tiles (e.g. merging
InferenceSlicer output) — see the benchmark below for why the sequential
versions stay the better default at typical detector output sizes.
Quick example
use ;
use ;
use SortTracker;
// Detections from your detector of choice for this frame (xyxy pixel
// boxes). See `Detections::from_ultralytics_onnx` /
// `from_transformers_onnx` if you're running a model directly.
let mut detections = new;
// Assign stable identities across frames.
let mut tracker = default;
tracker.update;
// Draw boxes + labels onto an RGBA frame.
let mut frame = new;
default.annotate.unwrap;
Runnable version: cargo run --example quickstart --features annotate-image
(see examples/quickstart.rs).
More examples in examples/:
| Example | What it shows | Run with |
|---|---|---|
quickstart |
Detections → tracker → box annotator | cargo run --example quickstart --features annotate-image |
tracking_and_counting |
ByteTracker across multiple frames, LineZone in/out counting, trace + line-zone annotators |
cargo run --example tracking_and_counting --features annotate-image |
onnx_postprocessing |
Parsing a raw YOLOv8-style ONNX tensor into Detections, NMS, rescaling to the original image size |
cargo run --example onnx_postprocessing |
metrics_evaluation |
Precision/Recall/F1/mAP accumulated across a small predictions-vs-targets set | cargo run --example metrics_evaluation |
dataset_io |
Building a DetectionDataset and round-tripping it through YOLO format |
cargo run --example dataset_io --features datasets |
Module overview
| Module | Contents |
|---|---|
core |
Detection/Detections (incl. from_obb), KeyPoints, Classifications, InferenceSlicer, ONNX-tensor adapters |
geometry |
Point, Rect, Position, polygon utilities, oriented_box_iou, LineZone/PolygonZone |
tracker |
SortTracker, ByteTracker, DeepSortTracker, DetectionsSmoother |
annotators |
The Annotator trait and every drawing config, pure-Rust + OpenCV backends |
metrics |
Precision, Recall, F1Score, ConfusionMatrix, MeanAveragePrecision, MeanAverageRecall |
utils |
FPSMonitor, image resize/letterbox/crop/overlay, ImageSink |
dataset (feature datasets) |
COCO/YOLO/Pascal VOC/classification loaders and exporters |
video (feature annotate-opencv) |
VideoInfo, VideoSink, frame iteration |
Full API docs: cargo doc --open --features annotate-image,datasets.
Relationship to Python supervision
This crate mirrors supervision's API shape and covers its core
detection/tracking/annotation/metrics/dataset surface, but is not a
line-for-line port:
- Detector adapters parse raw ONNX output tensors
(
from_ultralytics_onnx,from_transformers_onnx) instead of binding toultralytics/transformersPython objects, which have no Rust equivalent — see thecore::adaptersmodule docs for the reasoning. - ByteTrack is a two-stage (high/low-confidence) implementation of the core idea, not a byte-for-byte port of the reference codebase.
DeepSortTrackeris a single-pass, motion-gated approximation of DeepSORT's matching-cascade idea (combined IoU + nearest-neighbor cosine distance over a per-track embedding gallery), not a port of the reference implementation either. It also doesn't run the embedding model — you supply one feature vector per detection each frame, same division of responsibility as the ONNX adapters not owning inference.- Oriented bounding boxes live on
Detection::obb/Detection::from_obbrather than a separateOrientedBoxestype;from_obbalso populatesmaskwith the same four corners so the existingPolygonAnnotator/MaskAnnotatorrender them, instead of shipping a duplicateOrientedBoxAnnotatorthat would be identical code. - Not (yet) implemented:
RichLabelAnnotator's OpenCV backend (OpenCV has no built-in TTF rendering without FreeType), and a handful ofDetections.from_*adapters for detector libraries beyond Ultralytics/transformers.
Benchmark vs Python supervision
examples/bench_vs_supervision.rs and scripts/bench_vs_supervision.py run
the same three workloads — NMS, ByteTrack, box+label annotation — over
identical synthetic data (a shared xorshift PRNG keeps both sides bit-for-bit
comparable; NMS keeps the same 9,394/10,000 boxes on both sides, confirming
matching semantics). One data point, single-threaded, no SIMD tuning either
side (Intel i9-9880H @ 2.30GHz, supervision 0.29.1):
| Scenario | warden-vision (release) | Python supervision |
Speedup |
|---|---|---|---|
| NMS, 10,000 boxes | 187 ms/call | 4,162 ms/call | ~22x |
NMS, 10,000 boxes, parallel feature (16 threads) |
54 ms/call | 4,162 ms/call | ~77x |
| ByteTrack, 500 frames x 30 dets | 7,818 FPS | 352 FPS | ~22x |
| Box+label annotate, 500 frames x 50 boxes, 1920x1080 | 2,706 FPS | 413 FPS | ~6.6x |
Reproduce:
&&
Caveats: this is one machine, one run each, no warmup loop, and the NMS case in particular is a worst-case stress test (10k boxes is far more than a single detector head produces) rather than a typical-frame workload — treat the numbers as directional, not a formal benchmark suite.
The NMS row also doubles as a lesson in how not to parallelize greedy NMS:
the first non_max_suppression_parallel attempt dispatched a rayon job
per surviving candidate at every step of the sequential suppression loop
and measured slower than plain sequential NMS — thousands of small,
shrinking parallel dispatches lose to their own overhead. Splitting it into
one large parallel pass that precomputes the full pairwise IoU relation,
followed by a cheap sequential bookkeeping pass over the precomputed
booleans, is what actually gets the ~3.5x from 16 threads shown above. See
Detections::non_max_suppression_parallel's doc comment in
src/core/detections.rs for the full writeup.
Contributing
Issues and PRs welcome. Run cargo fmt, cargo clippy --all-targets --features annotate-image,datasets,parallel, and cargo test --features annotate-image,datasets,parallel before submitting — CI runs the same
checks, plus an annotate-opencv job that's allowed to fail (it needs a
system OpenCV install and isn't well-covered yet). If you touch that
backend, check its CI result and mention in your PR how you tested it.
License
MIT — see LICENSE.