A trained detector does not hand you clean boxes. It hands you thousands of overlapping candidates per frame, most of them junk, and two small pieces of post-processing decide what actually reaches the screen: the confidence threshold and non-maximum suppression. In our SUAS aerial pipeline these two settings changed field behaviour more than most of our training decisions did, and they cost nothing to change.
Figure: rendered by me on synthetic data.
mAP will not pick your threshold
Our detector reports mAP50 = 0.904. That number is computed by sweeping across confidence values, so it tells you the model ranks its detections well. It tells you nothing about which single threshold to run in the field, because in the field you do not get to sweep. You pick one operating point and you live with it.
The right point depends on what sits downstream. In an aerial search task a missed target costs you the mission, a false positive costs an operator two seconds of attention. So you bias toward recall and run low. Feed a tracker instead and the logic flips again: ByteTrack explicitly wants the low-confidence boxes, it uses them to bridge occlusion, so gating hard before the tracker deletes exactly the information it needs. I wrote about that in Object tracking and ByteTrack, and it is why our sports pipeline runs the detector more open than the aerial one.
Pick the threshold from the cost of each error type. Not from what makes the demo screenshot look tidy.
One IoU knob, two failure modes
NMS itself is a simple loop: take the highest-scoring box, delete everything that overlaps it beyond an IoU threshold, repeat. The knob fails in both directions. Set it too aggressive and two distinct objects standing close together merge into one box: two people become one person, and your count is wrong. Set it too permissive and duplicates survive, and every surviving duplicate becomes a phantom object, or later, a phantom track.
Small objects make the knob crueler. IoU between small boxes collapses fast: a few pixels of offset on a 20-pixel box is a large relative shift, so two honest detections of the same small target can overlap less than you expect and both survive suppression. Aerial detection lives exactly at those sizes. Tune the IoU threshold on your real object scale, not on COCO intuition.
Duplicates are born on tile seams
We tile 4K frames with 40 % overlap, so any target near a seam is detected twice, once per tile, in two different local coordinate systems. If you run NMS per tile and then paste the results together, those seam duplicates survive by construction: NMS never saw them in the same frame.
The fix is ordering. Map every box back to global frame coordinates first, then run a single NMS pass over the whole frame. It is one coordinate transform and one extra call, and it removed a whole class of double detections that no amount of threshold tuning had touched.
Post-processing is part of the latency budget
On the Jetson we deploy with TensorRT, and where NMS runs matters: inside the engine as part of the exported graph, or on the CPU after inference. The answer shifts with batch size and candidate count, and it belongs in the same budget discussion as the model itself. I covered the deployment side in TensorRT and FP16.
The general point stands without numbers: detection does not end at the model. The last centimetre of the pipeline is two thresholds, and they deserve the same scrutiny as any layer you trained.
References
- Ultralytics, YOLO performance metrics: https://docs.ultralytics.com/guides/yolo-performance-metrics/
- Ultralytics, predict mode and inference arguments: https://docs.ultralytics.com/modes/predict/
- OpenCV dnn module reference, including NMSBoxes: https://docs.opencv.org/4.x/d6/d0f/group__dnn.html
- Wikipedia, Jaccard index (IoU): https://en.wikipedia.org/wiki/Jaccard_index