Detect objects: IoU and NMS
A detector proposes 81 boxes for a desk with six things on it. Write the IoU and the non-maximum suppression under every YOLO and keep the ones that count.
DETECTIONS.taller and DETECTIONS.calle hold the raw boxes YOLOS-tiny proposed for two photos of ours: { label, score, box }, with box = [x1, y1, x2, y2] between 0 and 1. Many repeat. Write four functions:
iou(a, b): intersection over union of two boxes. 0 when they do not touch.
filterByScore(boxes, min): the boxes with score >= min, in the same order.
nms(boxes, limit): sort by score, highest first, and walk through: a box stays if no already accepted box **with the same label** overlaps it with iou > limit. Return the accepted ones in that order.
countByLabel(boxes): { label: how many } with the keys in alphabetical order. The preview draws the boxes over the photo and lets you move both thresholds.
Challenges 0/6
- IoU: half overlapping, identical and apart
- The confidence threshold keeps the order
- NMS: three boxes of one person and a dog on top become two
- The workshop desk at confidence 0.5 and IoU 0.5
- The street: two people, a dog, a car, a bike
- With the model's boxes (your photo) or the workshop desk: in order and without duplicates
function iou(a, b) {
// intersection area / (areaA + areaB - intersection); 0 when they do not touch
return 0;
}
function filterByScore(boxes, min) {
// score >= min, same order
return boxes;
}
function nms(boxes, limit) {
// highest score first; keep a box unless an accepted box with the same label overlaps it with iou > limit
return boxes;
}
function countByLabel(boxes) {
// { label: count }, keys sorted
return {};
}
console.log(countByLabel(nms(filterByScore(DETECTIONS.taller.boxes, 0.5), 0.5)));The idea
A detector does not return "a laptop". It returns hundreds of proposals, each with a label, a confidence and a box, and many point at the same object. Two steps sort that out: a confidence threshold, which drops the doubtful ones, and non-maximum suppression (NMS), which keeps the best of each group of overlapping boxes. IoU (intersection over union) is the rule for "overlapping".
What we measured: quantised YOLOS-tiny (9.7 MB) took 711 ms per 1024 × 768 photo on the DGX Spark CPU on 2026-10-18, with transformers.js 3.8.1. On the workshop desk it proposed 81 boxes with confidence ≥ 0.05; at threshold 0.5 and IoU 0.5 what remains is what the preview shows. It also gets things wrong: it calls the spanners on the wall "knife", counts ten "book" among notebooks and folders, and sees four "cup" where there is one mug, a pen holder and a speaker. We generated the photos with FLUX.1-schnell.
You can run it yourself on this page: the "A model in your browser" box loads YOLOS-tiny with ONNX Runtime Web (24 MB, all from this site) and in the browser on the DGX Spark it took about 730 ms per photo on 2026-10-19. Its raw boxes, for a sample or for your own photo, enter the exercise as a third scene.
The IoU threshold is a business decision, not a magic number. At 0.3 you lose objects that touch (two chairs side by side); at 0.7 you keep duplicates. Look at the preview with each value before you fix it.
All of this runs with no GPU and no cloud. A 10 MB detector in the browser or on a mini PC is enough to count parts on a shelf or spot a gap on a line, and the image never leaves the place where it was taken.
Run it on your machine
With Python and NumPy, on this exercise's raw boxes (CC BY 4.0):
curl -O https://vorluxai.com/data/labs/detecciones-taller.json
python3 -c "
import json, numpy as np
boxes = [b for b in json.load(open('detecciones-taller.json'))['boxes'] if b['score'] >= 0.5]
def iou(a, b):
ix = max(0, min(a[2], b[2]) - max(a[0], b[0])); iy = max(0, min(a[3], b[3]) - max(a[1], b[1]))
inter = ix * iy; return inter / ((a[2]-a[0])*(a[3]-a[1]) + (b[2]-b[0])*(b[3]-b[1]) - inter)
kept = []
for b in sorted(boxes, key=lambda b: -b['score']):
if not any(k['label'] == b['label'] and iou(k['box'], b['box']) > 0.5 for k in kept): kept.append(b)
print(len(boxes), 'cajas ->', len(kept), 'tras NMS:', sorted(k['label'] for k in kept))
" Go deeper: the Hugging Face reference →
This in production, with your data? Let's talk for 15 minutes →