Bounding-box detection¶
Train a whole-frame object detector that predicts one axis-aligned box (and a
class) per animal, and write the predictions to .slp as
sio.PredictedBoundingBox on LabeledFrame.bboxes. Two detection styles share one
data pipeline, one output type and one COCO-style evaluation:
bbox_centernet (CenterNet-style) |
bbox_ssd (SSD-style) |
|
|---|---|---|
| Idea | anchor-free: a box = a center peak + its regressed size | anchor-based: classify + refine a dense set of prior boxes |
| Heads | center heatmap (1 channel per class) + size (w, h) + sub-cell offset, all at one stride | one multi-scale head over pyramid levels (default strides 8/16/32): per anchor, class logits (incl. background) + 4 box deltas |
| Targets | Gaussian peak at each box center (CenterNet's size-adaptive radius), size/offset at the center cell | anchors matched by IoU (each box's best anchor always positive), SSD (cx, cy, w, h) encoding |
| Loss | penalty-reduced focal loss (normalized by #objects) + masked L1 | softmax CE with 3:1 hard-negative mining + smooth L1 |
| Decoding | 3x3 max-pool peak NMS, top-k, threshold (IoU-NMS optional) | softmax, threshold, top-k, class-wise IoU-NMS (0.45) |
| Typical use | one size range, crowded scenes, simplest knobs | wide size ranges, many aspect ratios |
Sample configs:
config_bbox_centernet_unet.yaml,
config_bbox_ssd_unet.yaml.
Training data: boxes, or poses / masks to derive them from¶
Ground-truth boxes are sio.UserBoundingBox objects on LabeledFrame.bboxes. You
do not need to annotate boxes if you already have poses or masks:
data_config.bbox_source derives them in memory through sleap-io at load time.
data_config.bbox_source |
Boxes come from |
|---|---|
auto (default) |
per labels file: user boxes if there are any, else user pose instances, else user masks |
bboxes |
LabeledFrame.bboxes only |
instances |
a tight box around each user instance's visible points (sio.Instance.to_bbox) |
masks |
each user mask's extent (sio.SegmentationMask.to_bbox) |
data_config.bbox_padding (pixels, default 0) inflates derived boxes on every
side — keypoints sit inside the animal's outline, so a tight keypoint box is a bit
smaller than the animal. Existing user boxes are never padded.
To make a box-only dataset from poses yourself (what the benchmark below does):
import sleap_io as sio
labels = sio.load_slp("poses.pkg.slp")
for lf in labels.user_labeled_frames:
lf.bboxes = [inst.to_bbox(mode="tight") for inst in lf.user_instances]
lf.instances = []
sio.save_slp(labels, "boxes.pkg.slp", embed="user")
Classes. Each box's category (a sio.Category) is its class. The head's
categories list is filled in at training setup from the training boxes (first
seen first) and saved with the model; boxes without a category form the single
class "object", which is also what predictions carry (category=None).
Negative frames. With data_config.use_negative_frames: true, frames the user
marked as empty are added as all-background samples.
Configuration¶
The quickest start is the config generator, which writes a detector config for any pose project (boxes are derived from the poses):
sleap-nn config labels.slp --auto --pipeline bbox_ssd -o detector.yaml # or bbox_centernet
sleap-nn config labels.slp # interactive: pick "Bounding Boxes (CenterNet / SSD)"
It uses preprocessing.scale: 0.5, output_stride 4 (CenterNet) / 8 (SSD, with
max_stride >= 32), turns rotation augmentation off and evaluation on (so the best
checkpoint is picked on box mAP, see below). The sample configs
docs/sample_configs/config_bbox_{centernet,ssd}_unet.yaml spell out every knob:
model_config:
backbone_config:
unet: {filters: 32, filters_rate: 1.5, max_stride: 32, output_stride: 4}
head_configs:
bbox_centernet:
center: {output_stride: 4} # categories, sigma, min_overlap, focal_alpha/beta
size: {output_stride: 4, loss_weight: 0.1}
offset: {output_stride: 4, loss_weight: 1.0}
model_config:
backbone_config:
unet: {filters: 32, filters_rate: 1.5, max_stride: 32, output_stride: 8}
head_configs:
bbox_ssd:
ssd:
output_stride: 8 # finest pyramid level
num_levels: 3 # levels at strides 8, 16, 32
anchor_sizes: null # auto: geometric between the boxes' p5..p95 sqrt(area)
anchor_scales: [1.0, 1.26, 1.587]
aspect_ratios: [0.5, 1.0, 2.0]
pos_iou_threshold: 0.5
neg_iou_threshold: 0.4
neg_pos_ratio: 3.0
Notes:
- CenterNet heads must share one
output_stride.sigma: null(default) uses CenterNet's size-adaptive Gaussian radius; a number fixes sigma in input pixels. - SSD binds to the backbone feature maps at
output_stride * 2**i: decoder outputs for the finer levels and the bottleneck for the deepest one. The deepest level may not exceedmax_stride(it is raised to fit). Leaveanchor_sizes: nullto size the anchors from your training boxes (in model-input pixels, i.e. afterpreprocessing.scale); the resolved sizes are written into the savedtraining_config.yamlso inference uses identical anchors. - Augmentation: boxes are carried through the geometric augmentation as their
four corners and re-boxed afterwards; boxes pushed mostly out of frame are dropped.
Prefer little or no rotation: the axis-aligned box of rotated corners is larger
than the rotated animal, so rotation inflates the targets. Scale, translation and
flip_pare exact. - Tiling is not supported for detectors yet (a clear error is raised).
Training and monitoring¶
sleap-nn train --config config_bbox_centernet_unet.yaml \
data_config.train_labels_path=[train.pkg.slp] data_config.val_labels_path=[val.pkg.slp]
Logged per epoch: train|val/center_loss, size_loss, offset_loss (CenterNet) or
train|val/cls_loss, box_loss (SSD). With trainer_config.eval.enabled: true, every
eval.frequency epochs the validation set is decoded with the same decoder as
inference and scored with COCO-style metrics, logged to the CSV log, wandb and
callback_metrics as eval/val/bbox_mAP, bbox_AP50, bbox_AP75, bbox_AR,
bbox_precision, bbox_recall, bbox_f1, bbox_f1_threshold, bbox_mean_iou,
bbox_n_tp/fp/fn (wandb also keeps best/ summaries). Training visualizations
draw ground-truth boxes in green and predictions in red (CenterNet also overlays its
center heatmap).
Checkpoint selection. With evaluation on, best.ckpt and early stopping follow
eval/val/bbox_mAP (checked on evaluation epochs, so early-stopping patience counts
evaluations), not the validation loss: the loss tracks box quality only loosely, and
in the benchmark several runs were still improving their mAP when the loss stopped
them. Choose another metric with trainer_config.eval.select_metric: bbox_mAP,
bbox_AP50, bbox_AP75, bbox_AR, bbox_f1, or val_loss for the old behavior.
With evaluation off, detectors select on val/loss.
From Python, sleap_nn.train.train(..., head_configs="bbox_ssd") builds the same
config; it takes bbox_source= / bbox_padding= and, like sleap-nn train, defaults
geometry_aug to rotation, so pass e.g. geometry_aug="scale" for a detector.
After training, every split (and data_config.test_file_path) is predicted and
evaluated automatically; metrics are saved as metrics.<split>.npz/.json and
logged to the wandb summary as eval/<split>/bbox_* (plus predict_fps).
Inference¶
sleap-nn predict --data_path video.mp4 --model_paths models/my_detector \
--peak_threshold 0.3 --max_instances 10
A detector can also be the first stage of a top-down pose model, in place of a
centroid model: pass it with a centered-instance model directory (--model_paths
<detector> --model_paths <centered_instance>). See
Top-down pose with a box detector.
sleap-nn predict detects the model type from its directory. For detectors,
--peak_threshold is the detection score threshold, --max_instances caps
detections per frame (default 100) and --bbox_nms_iou sets the class-wise IoU-NMS
threshold (default 0.45 for SSD, off for CenterNet; 1.0 disables it). Output
frames carry sio.PredictedBoundingBox(x1, y1, x2, y2, score, category) in original
image pixels on LabeledFrame.bboxes. From Python:
from sleap_nn.inference.predictor import Predictor
predictor = Predictor.from_model_paths(["models/my_detector"], peak_threshold=0.3)
labels = predictor.predict("video.mp4")
boxes = labels[0].bboxes
Tracking is not supported for detector output (the tracker works on poses and
masks). The legacy sleap-nn track / run_inference pipeline refuses detectors and
points to sleap-nn predict.
Export (ONNX / TensorRT)¶
sleap-nn export models/my_detector -o models/my_detector/exported --format onnx
sleap-nn predict -m models/my_detector/exported -i video.mp4 -o boxes.slp
A detector exports its network only: the graph returns the decoder-ready head
outputs (CenterNet's center probabilities + size/offset maps; SSD's per-anchor
logits + deltas), and the exported-model adapter decodes them with the same
BBoxDecoder as checkpoint inference, rebuilt from the training_config.yaml
copied into the export directory. Anchors, NMS and thresholds therefore live in one
place, exported and native boxes match, and the graph's spatial axes stay dynamic.
The input_scale resize and the max-stride padding also stay out of the graph (a
resize traced into it would be frozen to the trace input's size): the adapter applies
them on the host with the checkpoint path's own preprocessing (including the size
matcher, max_height / max_width, so mixed-resolution videos work as they do for
the checkpoint), then clips boxes to the frame.
Evaluation¶
sleap-nn eval --ground_truth_path test.pkg.slp --predicted_path preds.slp \
--match_method bbox --save_metrics metrics.npz
--match_method auto picks bbox when the predictions hold boxes only. Ground
truth without boxes (e.g. the pose file a detector was trained from) has them
derived first, like training does; pass --bbox_source / --bbox_padding to match
the model's data_config.bbox_source / bbox_padding (post-training eval does this
itself). Classes are matched by category name, except that when the ground truth and
the predictions each carry at most one name the comparison is class-agnostic: a
single-class detector maps every box to its one class whatever it is called.
Metrics follow the COCO protocol: per class and IoU threshold, each frame's top-100
detections are greedily matched (in descending score order) to the highest-IoU
unmatched ground-truth box; AP is the 101-point interpolated area under the
precision-recall curve over all frames. bbox_mAP averages IoU 0.50:0.05:0.95 and
classes; bbox_AR is the matching max recall. The operating point
(bbox_precision/recall/f1, bbox_mean_iou of the matches) is reported at IoU 0.5
and the score threshold that maximizes F1 (bbox_f1_threshold), so models are
compared at their own best threshold rather than an arbitrary fixed one.
sleap-nn info models/my_detector shows the detector's head and these metrics for a
trained model directory.
How it fits together¶
sleap_nn/data/bboxes.py— GT derivation (add_bboxes_to_labels), corner geometry,CenterNetTargetEncoder,SSDTargetEncoder(anchors, matching, box coding).sleap_nn/architectures/heads.py—BBoxCenterHead,BBoxSizeHead,BBoxOffsetHead, andSSDHead, aMultiScaleHeadthatModelbinds to several backbone strides.sleap_nn/training/lightning_modules.py—BBoxCenterNetLightningModule,BBoxSSDLightningModule(shared_BBoxLightningModelbase).sleap_nn/inference/bbox.py—decode_centernet,decode_ssd,BBoxDecoder(used by both training-time eval and inference);inference/layers/bbox.py—BBoxLayer.sleap_nn/evaluation.py—compute_bbox_metrics,run_evaluation(match_method="bbox");training/callbacks.py—BBoxEvaluationCallback.