Unit 08.01: IoU, and why 0.5 is a choice
IoU turns two boxes into one number, and the threshold that calls it a hit is a choice.
0.5 is a convention
Four predictions against one ground-truth box.
The code computes IoU for each and applies a 0.5 threshold.
def iou(a, b):
ax1, ay1, ax2, ay2 = a
bx1, by1, bx2, by2 = b
ix1, iy1 = max(ax1, bx1), max(ay1, by1)
ix2, iy2 = min(ax2, bx2), min(ay2, by2)
inter = max(0, ix2 - ix1) * max(0, iy2 - iy1)
union = ((ax2 - ax1) * (ay2 - ay1) + (bx2 - bx1) * (by2 - by1) - inter)
return inter / union
truth = (10, 10, 50, 50)
for name, pred in [("exact", (10, 10, 50, 50)),
("off by 5px", (15, 15, 55, 55)),
("half overlap", (30, 10, 70, 50)),
("twice the size", (0, 0, 60, 60))]:
score = iou(truth, pred)
print(f"{name:16} IoU {score:.2f} counts as a hit at 0.5: {score >= 0.5}")
print("\n0.5 is a convention. At 0.75 the 'off by 5px' box becomes a miss")
# The IoU threshold decides what "detected" means, and moving it changes every
# reported number without changing the model. Report the threshold with the
# metric, always.
The box off by five pixels scores 0.68 - a hit at 0.5 and a miss at 0.75. Moving the threshold changes every reported number without changing the model at all.
Which threshold is right depends on what the box is for. Cropping a region for a downstream classifier tolerates loose boxes; measuring an object's dimensions does not.
The mistake this prevents
The mistake is reporting detection metrics without the IoU threshold. mAP at 0.5 and at 0.5:0.95 are different numbers for the same model, and comparing across papers or across teams without it is meaningless.
Takeaway
Report the IoU threshold with every detection metric. It is a choice that moves every number, and it should follow from what the box is used for.
