Convolutional networks, section 5 of 5, 4h

Detection and segmentation in practice

By the end: You can fine-tune a pretrained detector, compute IoU and mean average precision, and budget for labelling.

  • Boxes, masks and annotation formats
  • IoU
  • Mean average precision
  • Fine-tuning a pretrained detector
  • Labelling cost and licences

New to convolutional networks? A convolutional network is a neural network built for images. Rather than relating every pixel to every other pixel, it slides a small window across the picture hunting for one local pattern — an edge, then a corner, then a wheel — and hunts for that same pattern everywhere in the frame. Stack those layers and edges become shapes and shapes become objects. Why it matters

Classification tells you what is in an image. Detection and segmentation tell you where, as boxes or as pixel masks. This section is about doing that with a pretrained model rather than from scratch: the annotation formats you will be handed, how the overlap between a predicted box and a true one is scored, what mean average precision is actually averaging, and how to fine-tune a detector on your own data. It also costs out the labelling, which is usually the real constraint.

The annotation formats you will be handed#

Three formats account for nearly everything, and they disagree about what a box is, which is the source of most wasted afternoons in this work.

COCO     JSON. Boxes are [x, y, width, height] in pixels, top-left origin.
         Classes are integer ids with a separate categories list.

Pascal VOC  One XML file per image. Boxes are xmin, ymin, xmax, ymax in pixels.

YOLO     One text file per image, one line per object:
           class_id  x_centre  y_centre  width  height
         All four numbers are fractions of the image size, not pixels.

torchvision detection models want xmin, ymin, xmax, ymax in pixels.
The two ways to lose a day: reading COCO width and height as if they were xmax and ymax, which produces boxes that grow with distance from the origin; and forgetting that YOLO numbers are normalised, which produces boxes a few pixels wide in the top-left corner. Both look like a broken model rather than a broken loader.

Intersection over union#

def iou(a, b):                        # boxes as (x1, y1, x2, y2)
    ix1, iy1 = max(a[0], b[0]), max(a[1], b[1])
    ix2, iy2 = min(a[2], b[2]), min(a[3], b[3])
    inter = max(0, ix2 - ix1) * max(0, iy2 - iy1)
    area = lambda r: (r[2] - r[0]) * (r[3] - r[1])
    return inter / (area(a) + area(b) - inter)

truth = (10, 10, 50, 50)              # a 40 x 40 box
for pred in [(10, 10, 50, 50), (12, 12, 52, 52), (20, 20, 60, 60),
             (30, 30, 70, 70), (10, 10, 90, 90)]:
    print(pred, round(iou(truth, pred), 3))

# (10, 10, 50, 50) 1.0     exact
# (12, 12, 52, 52) 0.822   off by 2 pixels in each direction
# (20, 20, 60, 60) 0.391   off by 10, a quarter of the box
# (30, 30, 70, 70) 0.143   off by 30
# (10, 10, 90, 90) 0.25    correct corner, four times the area
Calibrate on the second row. A box two pixels out on a 40-pixel object already scores 0.822, and ten pixels out scores 0.391 — below the usual 0.5 threshold, so it counts as a complete miss and as a false alarm at the same time. IoU punishes small errors on small objects very hard, which is the whole reason small-object detection reads as a separate problem.

A predicted box counts as correct if its IoU with a true box of the same class exceeds a threshold, often 0.5. Note the last row: a box with the right corner and four times the area scores 0.25, so a detector that consistently predicts boxes too large is penalised as severely as one that misses objects entirely.

Mean average precision, worked#

Sort all predictions for a class by confidence, walk down the list, and trace precision against recall. The area under that curve is the average precision for the class. Mean average precision averages it over classes. The COCO convention also averages over IoU thresholds from 0.5 to 0.95, which rewards tight boxes. That is the definition; here it is on three true objects and five predictions.

three objects, five predictions, matched at IoU 0.5, walking down by confidence:

  conf   best IoU   verdict   running precision   running recall
  0.95     0.822      TP           1/1 = 1.000       1/3 = 0.333
  0.90     0.620      TP           2/2 = 1.000       2/3 = 0.667
  0.80     0.000      FP           2/3 = 0.667       2/3 = 0.667
  0.70     0.747      TP           3/4 = 0.750       3/3 = 1.000
  0.60     0.681      FP           3/5 = 0.600       3/3 = 1.000

  AP at IoU 0.50 = 0.917
  AP at IoU 0.75 = 0.333
  AP at IoU 0.90 = 0.000
  AP averaged over 0.50 to 0.95 in steps of 0.05 (COCO mAP) = 0.442
Two things this table teaches that the definition does not. The 0.60 prediction is a false positive despite an IoU of 0.681, because the object it overlaps was already claimed by the 0.95 prediction — duplicate detections are punished, which is what non-maximum suppression exists to prevent. And the same detector scores 0.917 or 0.442 depending only on which convention you report, so a number without its threshold is meaningless.

That collapse from 0.917 to 0.333 between IoU 0.5 and 0.75 is the honest state of most detectors: they find the objects and their boxes are loose. Whether that matters is a question about your application, not about the model. If the box drives a robot arm, tightness is the product. If it crops a thumbnail, 0.5 is plenty and reporting COCO mAP will make a perfectly adequate system look broken.

Do not write the matching logic yourself for a real evaluation

The version above is deliberately simple so you can see what it does. Real implementations handle crowd regions, ignore areas, per-class matching and the 101-point interpolation, and they disagree with a naive version by several points. Write it once to understand it, then evaluate with pycocotools or torchmetrics and compare your number with theirs.

Fine-tune, do not train from scratch#

Start from a detector pretrained on a large dataset such as COCO and fine-tune it on your images. A few hundred well-labelled images per class often go a long way. torchvision ships detection and segmentation models with pretrained weights; several popular libraries make the whole loop a few lines. The change you make is the same one as for classification — replace the head with one sized for your classes — it is just buried a little deeper.

import torch
import torchvision
from torchvision.models.detection.faster_rcnn import FastRCNNPredictor

num_classes = 4                      # your classes PLUS one for background
model = torchvision.models.detection.fasterrcnn_resnet50_fpn(weights="DEFAULT")
in_features = model.roi_heads.box_predictor.cls_score.in_features
model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes)

# In training mode it takes targets and returns a dict of losses, not predictions.
model.train()
images = [torch.rand(3, 240, 320), torch.rand(3, 200, 300)]   # any sizes, unbatched
targets = [{"boxes": torch.tensor([[10., 10., 90., 90.]]), "labels": torch.tensor([1])},
           {"boxes": torch.tensor([[20., 30., 80., 120.]]), "labels": torch.tensor([2])}]
losses = model(images, targets)
print(sorted(losses))
loss = sum(losses.values())          # you add them up yourself

model.eval()
with torch.no_grad():
    out = model([torch.rand(3, 240, 320)])
print(sorted(out[0]), tuple(out[0]["boxes"].shape))

# ['loss_box_reg', 'loss_classifier', 'loss_objectness', 'loss_rpn_box_reg']
# ['boxes', 'labels', 'scores'] (100, 4)
Three things that catch people. Class 0 is background, so four classes of your own means num_classes of five. The model returns losses in train mode and predictions in eval mode, from the same call, and you sum the four losses yourself. And it returns 100 boxes for an image that contains one — they are sorted by score and you threshold them, which is your decision, not the model's.

Labels cost the most#

Cost this out before anything else, because it usually decides the project. Drawing a box takes on the order of ten seconds once a labeller is warmed up; a careful polygon mask takes a minute or more. Suppose you want 500 images per class across 4 classes, at an average of 3 objects per image.

2,000 images x 3 objects = 6,000 annotations

  boxes at 10 s each      16.7 hours
  masks at 70 s each     116.7 hours

Add a review pass at half the original rate, and a second labeller on a
10% sample to measure agreement:

  boxes     16.7 + 8.3 + 1.7  =  26.7 hours
  masks    116.7 + 58.3 + 11.7 = 186.7 hours
One person-week against five. The interesting part is what the gap buys: if the business question is how many of a thing are in the frame, the masks buy nothing at all.
  • Boxes take seconds each; pixel masks take much longer. Choose the cheapest label that answers the business question.
  • Write labelling guidelines with examples of edge cases, such as occluded or partly visible objects, and measure agreement.
  • Use the model to pre-label and have people correct, once it is decent. Correcting a box is roughly three times faster than drawing one, so this pays for itself well before the model is good.
  • Label your validation set first and label it more carefully than the training set. Noise in training data costs you a little accuracy; noise in the validation set means you cannot tell which of two models is better, which costs you every decision afterwards.

Check licences

Some widely used detection libraries are released under strong copyleft licences with obligations even for software offered as a network service. Check before building a product on one.

Where people go wrong

Reporting one overall mAP when the objects that matter are a small slice of the data.

If this comes up in an interview

Explain mean average precision for object detection.

For each class, rank predictions by confidence, match them to ground truth boxes by IoU above a threshold, and compute the area under the precision-recall curve: that is average precision. Mean over classes gives mAP. COCO-style mAP also averages over IoU thresholds from 0.5 to 0.95.

Then they push further

Your detector has good mAP but misses small objects the business cares about. What do you do?

Measure mAP by object size to confirm, then try higher input resolution, tiling large images, anchor or architecture settings suited to small objects, and more labelled small examples.

What gets marked up. Marked up for IoU matching, the precision-recall curve, and breaking the metric down by slice.

Build this: Label 200 images of something around you with boxes, fine-tune a pretrained torchvision detector, and report mAP overall and by object size, with your own IoU and AP implementation checked against a library's.

This is one section of Convolutional networks, 21 hours across 5 sections. Read it in full to get the whole topic in order.