TAI BUI
Back to curriculum
BuildPython

Object Detection — YOLO from Scratch

Detection is classification plus regression, applied at every position in a feature map and cleaned up afterward with non-maximum suppression.

TIME: ~75 minutesPREREQUISITES: Phase 4 Lesson 03 (CNNs), Phase 4 Lesson 04 (Image Classification), Phase 4 Lesson 05 (Transfer Learning)

Learning Objectives

  • Explain the grid-and-anchor design that turns detection into a dense prediction problem and state what every number in the output tensor means
  • Compute Intersection-over-Union between boxes and implement non-maximum suppression from scratch
  • Build a minimal YOLO-style head on top of a pretrained backbone, including the classification, objectness, and box-regression losses
  • Read a detection metric row ([email protected], recall, [email protected], [email protected]:0.95) and pick which knob to turn next

The Problem

Classification says "this image is a dog." Detection says "there is a dog at pixels (112, 40, 280, 210), there is a cat at (400, 180, 560, 310), and nothing else in the frame." That one structural change (predicting a variable number of labelled boxes instead of a single label per image) is what every autonomous system, every surveillance product, every document layout parser, and every factory vision line depends on.

Detection is also where every engineering trade-off in vision shows up at once: boxes need to be accurate (the regression head's job), each box needs the right class (classification head), the model needs to know when there's nothing there (objectness score), and every real object should get exactly one prediction, not five (non-maximum suppression). Miss any one of these and the pipeline misses objects, hallucinates boxes that aren't there, or reports the same object fifteen times in slightly different positions.

YOLO (You Only Look Once, Redmon et al. 2016) made all of this run in real time with a single forward pass through a conv net. The same structural decisions still underlie most modern detectors: YOLOv8, YOLOv9, and YOLO-NAS all keep the grid-anchor-NMS core intact. Even RT-DETR, which replaces anchors and NMS with a transformer decoder, is still answering the same three questions: where, what, and how confident. Learn the core once and every variant becomes a rearrangement, or a deliberate removal, of the same parts.

Warm-up0/3
1. You're asked to point out exactly where a specific object is in a photograph, not just say what's in it. Compared to labeling the whole image with one tag, what extra kind of output do you need to produce?Warm-up
A photo with one object circled loosely, next to a classifier output box reading label surfboard, and a dashed question mark box asking what else is needed beyond the label.
Illustrative single-object photo.
2. Three people each circle what looks like the same parked car in a photo, drawing three overlapping boxes around it. If your goal is one clean answer of 'here's the car,' what's the sensible thing to do with the extra circles?Warm-up
A parked car with three overlapping rectangles drawn around it by three different people, each box in a slightly different place, with a color legend naming each person.
Illustrative; the three boxes are drawn by hand, not computed.
3. A model produces an unbounded raw score, and you want to squash it into a value strictly between 0 and 1. Which function does that?Warm-up
A raw score of negative 3.2 flowing through an unlabeled function box into a result that must stay strictly between 0 and 1.
Illustrative single score; the squashing function is left blank.

The Concept

Detection as dense prediction

A classifier outputs C numbers per image. A YOLO-style detector outputs (S x S x B x (5 + C)) numbers per image, where S is the spatial grid size and B is the number of boxes (anchors) per cell.

Mermaid diagram

Each of the S * S grid cells predicts B boxes. For each box:

  • 4 numbers describe geometry: tx, ty, tw, th.
  • 1 number is the objectness score: "is there an object centred in this cell?"
  • C numbers are class probabilities.

Total per cell: B * (5 + C). For VOC (the PASCAL VOC detection dataset) with S=13, B=2, C=20, that is 50 numbers per cell.

Why grids and anchors

Plain regression would predict (x, y, w, h) for every object as an absolute coordinate. That is hard for a conv network because translating the image should not translate every prediction by the same amount: each object is anchored to a specific position in the frame. The grid answers this by assigning each ground-truth box to the grid cell its centre falls in; only that cell is responsible for that object.

Anchors address a second problem. A 3x3 conv cannot easily regress a 500-pixel-wide box out of a 16-pixel receptive field feature cell. Instead, we pre-define B prior box shapes (anchors) per cell and predict small deltas from each anchor. The model learns to pick the right anchor and nudge it rather than regress from nothing.

Anchor box priors (example for 416x416 input):

  small:   (30,  60)
  medium:  (75,  170)
  large:   (200, 380)

At each grid cell, every anchor emits (tx, ty, tw, th, obj, c_1, ..., c_C).

Modern detectors often use an FPN (Feature Pyramid Network: a stack of feature maps at different resolutions) with different anchor sets per resolution: small anchors on shallow, high-resolution maps, large anchors on deep, low-resolution ones. Same idea, more scales.

Decoding predictions

The raw tx, ty, tw, th are not box coordinates; they are regression targets to be transformed before plotting:

centre x  = (sigmoid(tx) + cell_x) * stride
centre y  = (sigmoid(ty) + cell_y) * stride
width     = anchor_w * exp(tw)
height    = anchor_h * exp(th)

sigmoid keeps centre offsets inside the cell. exp lets the width scale freely from the anchor without a sign flip. stride scales the grid coordinates back to pixels. This decode step is the same in every YOLO version since v2.

IoU

Detection's universal similarity metric between two boxes:

IoU(A, B) = area(A intersect B) / area(A union B)

IoU = 1 means identical; IoU = 0 means no overlap. IoU between the prediction and the ground-truth box is what decides whether a prediction counts as a true positive (typically IoU >= 0.5). IoU between two predictions is what NMS uses to deduplicate.

Non-maximum suppression

A conv network trained on adjacent anchors will often predict overlapping boxes for the same object. NMS keeps the highest-confidence prediction and deletes any other prediction with IoU above a threshold.

NMS(boxes, scores, iou_threshold):
    sort boxes by score descending
    keep = []
    while boxes not empty:
        pick the top-scoring box, add to keep
        remove every box with IoU > iou_threshold to the picked box
    return keep

Typical threshold: 0.45 for object detection. Recent detectors replace standard NMS with soft-NMS, DIoU-NMS, or learn the suppression directly (RT-DETR), but the structural purpose is the same.

The loss

YOLO loss is three losses added with weights:

L = lambda_coord * L_box(pred, target, where obj=1)
  + lambda_obj   * L_obj(pred, 1,     where obj=1)
  + lambda_noobj * L_obj(pred, 0,     where obj=0)
  + lambda_cls   * L_cls(pred, target, where obj=1)

Only cells that contain an object contribute to the box-regression and classification losses. Cells without objects contribute only to the objectness loss (teaching the model to stay silent). lambda_noobj is usually small (~0.5) because the vast majority of cells are empty and would otherwise dominate the total loss.

Modern variants swap MSE box loss for CIoU / DIoU (which optimise IoU directly), use focal loss for class imbalance, and balance objectness with quality focal loss. The three-component structure is unchanged.

Detection metrics

Accuracy does not transfer to detection. Four numbers that do:

  • Precision@IoU=0.5: of the predictions counted as positives, how many are actually correct.
  • Recall@IoU=0.5: of the real objects, how many did we find.
  • [email protected]: precision-recall curve area at IoU threshold 0.5; one number per class.
  • [email protected]:0.95: average of AP over IoU thresholds 0.5, 0.55, ..., 0.95. The COCO metric; strictest and most informative.

Report all four. A detector that's strong on [email protected] but weak on [email protected]:0.95 is localising roughly but not tightly; fix with a better box-regression loss. A detector with high precision and low recall is too conservative; lower the confidence threshold or increase the objectness weight.

Check: The Concept0/27
1. The lesson calls IoU "detection's universal similarity metric." How is it defined for two boxes A and B?Define
Two overlapping rectangles A and B over the same background, with the region where they overlap shaded, and no area numbers or formula shown.
Illustrative pair of boxes; coordinates are given, no ratio is computed on screen.
2. What is an anchor in a YOLO-style detector?Define
Three rectangles of different sizes, 30 by 60, 75 by 170 and 200 by 380, all centred on the same point representing one grid cell, with a color legend naming each as small, medium or large.
Drawn to scale from the lesson's own anchor example for 416x416 input.
3. What is non-maximum suppression (NMS)?Define
One object with four overlapping predicted boxes around it, each a different color with its own confidence score of 0.95, 0.88, 0.62 or 0.54, none of them removed yet.
Illustrative scores on four overlapping predictions.
4. What does the objectness score predict for an anchor in a given grid cell?Define
An object's box drawn across nine cells of a small grid with a dot at the box centre, and an unknown objectness value marked in every cell of the patch.
Illustrative patch of grid cells.
5. What is the grid stride of a detection head?Define
A 416 by 416 image subdivided into a 13 by 13 grid of equal cells with one cell highlighted, beside a card repeating the input size and grid size and a dashed box asking for the grid stride.
Grid overlay at the lesson's own 416 and 13 example sizes.
6. What does mAP average?Define
A small table of one detector's reported metrics, precision at 0.5, recall at 0.5 and mAP at 0.5, with the mAP row highlighted beside a dashed box asking what that number averages.
Illustrative metric values, typed not captured; the metric names come from the lesson's own list.
7. What is [email protected]?Define
A table of four predictions with their IoU against the ground truth and their confidence scores, next to a dashed box asking what AP at 0.5 is.
IoU values computed with the lesson's own box_iou on four box pairs; the confidence scores are illustrative.
8. What is [email protected]:0.95?Define
A card showing one evaluation summary line that reports an average precision at IoU 0.50 to 0.95 of 0.521, next to a dashed box asking what that line reports.
Summary line typed, not captured from a run; the value stands in for a real one.
9. The lesson lists [email protected] and [email protected] as two of the four detection metrics to report. In plain terms, what does [email protected] measure?Define
One frame holding five dashed ground-truth boxes and four solid predicted boxes, three of them sitting on a real object and one on empty background, next to a dashed box asking what recall at IoU 0.5 equals.
Illustrative frame of results; the boxes are placed by hand, not captured from a detector.
10. What does [email protected] measure?Define
One frame holding four dashed ground-truth boxes and six solid predicted boxes, four of them sitting on a real object and two on empty background, next to a dashed box asking what precision at IoU 0.5 equals.
Illustrative frame of results; the boxes are placed by hand, not captured from a detector.
11. You're debugging a custom YOLO head and print its output shape: (N, 13, 13, 3, 25). A teammate insists the last dimension should be 20, just the class count. What's missing from their count?Explain
A card for a YOLOHead with 3 anchors and 20 classes showing its real printed output shape (4, 13, 13, 3, 25), a teammate's claim that the last number should be 20, and a dashed box asking what accounts for the difference.
Shape mirrored with numpy reshape and transpose on the head's own view-then-permute operation, since torch is not installed here; a batch size of 4 stands for the stem's N.
12. Per the lesson, why do YOLO-style detectors use pre-defined anchor box shapes instead of regressing absolute box coordinates directly?Explain
A tiny 16 pixel square representing one feature cell's receptive field, next to a much wider 500 pixel rectangle representing the object, both drawn to one shared scale.
Pixel sizes quoted from the lesson's own receptive-field example.
13. The decode step computes width = anchor_w * exp(tw) and height = anchor_h * exp(th). Why exp() instead of a plain linear scale?Explain
A curve of width equals 75 times exp of tw, plotted over tw from -1.5 to 1.5, rising from left to right.
Computed from the lesson's own decode formula with the medium anchor width of 75.
14. Why does the grid design assign each ground-truth box to only one cell?Explain
A box drawn across nine cells of a grid with a dot at its centre, every cell the box touches outlined with dashes, and a dashed box asking which cell is responsible for the object.
Illustrative grid and object box; no cell is marked as the answer.
15. Why is lambda_noobj usually small, around 0.5, in the YOLO loss?Explain
A 7 by 7 grid of cells with 3 cells highlighted as containing an object and 46 left plain as empty.
Illustrative small grid; real detector grids are larger but keep the same lopsided ratio.
16. Which loss terms does a grid cell that contains no object contribute to?Explain
A card showing one grid cell with has_obj equal to false, next to three question-mark boxes for the box loss, the objectness loss and the classification loss.
Typed from the lesson's own loss term names, not captured from a run.
17. Why does a detector need NMS after the head produces its predictions?Explain
A package in a frame with four overlapping predicted boxes around it in different colors, none of them removed yet.
Illustrative set of raw predictions on one package.
18. In an FPN-style detector, why are small anchors placed on shallow high-resolution maps and large anchors on deep low-resolution maps?Explain
Two grids side by side at different resolutions: a small anchor box on the finer high-resolution grid and a large anchor box on the coarser low-resolution grid.
Illustrative pyramid resolutions; anchor placement follows the lesson's own FPN description.
19. IoU is used in two different places in detection. How do the two uses differ?Explain
Two box pairs with their own computed IoU values, 0.70 and 0.57, a dashed box asking what each pair's comparison is used for.
Computed with the lesson's own box_iou on two fresh box pairs.
20. Why is [email protected]:0.95 considered stricter than [email protected]?Explain
A ruler from 0 to 1 marked at 0.5 and 0.95, with a dot at 0.56 for a loosely fitted box, well past 0.5 but far from 0.95.
Computed with the lesson's own box_iou on a deliberately loose box pair.
21. A detector with grid stride 32 receives a 640 x 640 input. What are the grid dimensions?Apply
A card stating a 640 by 640 input and a grid stride of 32 pixels per cell, next to a dashed box asking for the grid size.
Input size and stride quoted directly from the question.
22. A YOLOHead is built with num_anchors=3 and num_classes=4. How many output channels does its 1x1 conv produce for each grid cell?Apply
A card naming a YOLOHead with 3 anchors and 4 classes and its 1 by 1 conv with the output channel count left blank, next to a dashed box asking for that count.
Config quoted directly from the question; the conv line comes from the lesson's own head.
23. A teammate refactors the box-decoding code and accidentally drops the sigmoid on tx and ty. During training, predicted box centers start drifting into neighboring cells. What did removing the sigmoid break?Analyze
The lesson's decode formula with sigmoid on tx, next to the teammate's edited version with sigmoid removed, and a predicted centre dot drifting from its own cell toward a neighbor with an arrow.
Formulas quoted from the lesson; the drifting symptom is the one the question describes.
24. Your model has [email protected] = 0.75 but [email protected]:0.95 = 0.32. What is the most accurate interpretation?Analyze
A bar chart with mAP@0.5 at 0.75 and mAP@0.5:0.95 at 0.32, a large gap between the two bars, next to a question about what that gap means.
Values quoted directly from the question.
25. A detector reports the same forklift six times in one frame, each box slightly shifted. Which stage should you check first?Analyze
One forklift in a frame with six nearly identical overlapping boxes drawn around it in different colors, each from a separate prediction.
Illustrative six overlapping predictions on one forklift, matching the question's own count.
26. A detector scores [email protected] = 0.92 and [email protected] = 0.35. Which next step does the lesson support?Analyze
A bar chart with precision@0.5 at 0.92 and recall@0.5 at 0.35, next to a question about what to do next.
Values quoted directly from the question.
27. Your dataset has both tiny objects and objects filling half the frame. Which design from the lesson fits best?Design
One square frame with a large object covering about half of it and a tiny object near the bottom right corner, next to a dashed box asking which detector design handles both sizes.
Illustrative object sizes inside one 640 by 640 frame.

Build It

Step 1: IoU

The workhorse of the whole lesson. Works on two arrays of boxes in (x1, y1, x2, y2) format.

python
import numpy as np
 
def box_iou(boxes_a, boxes_b):
    ax1, ay1, ax2, ay2 = boxes_a[:, 0], boxes_a[:, 1], boxes_a[:, 2], boxes_a[:, 3]
    bx1, by1, bx2, by2 = boxes_b[:, 0], boxes_b[:, 1], boxes_b[:, 2], boxes_b[:, 3]
 
    inter_x1 = np.maximum(ax1[:, None], bx1[None, :])
    inter_y1 = np.maximum(ay1[:, None], by1[None, :])
    inter_x2 = np.minimum(ax2[:, None], bx2[None, :])
    inter_y2 = np.minimum(ay2[:, None], by2[None, :])
 
    inter_w = np.clip(inter_x2 - inter_x1, 0, None)
    inter_h = np.clip(inter_y2 - inter_y1, 0, None)
    inter = inter_w * inter_h
 
    area_a = (ax2 - ax1) * (ay2 - ay1)
    area_b = (bx2 - bx1) * (by2 - by1)
    union = area_a[:, None] + area_b[None, :] - inter
    return inter / np.clip(union, 1e-8, None)

Returns an (N_a, N_b) matrix of pairwise IoUs. Use it against a single ground-truth box by making one of the arrays shape (1, 4).

Step 2: Non-max suppression

python
def nms(boxes, scores, iou_threshold=0.45):
    order = np.argsort(-scores)
    keep = []
    while len(order) > 0:
        i = order[0]
        keep.append(i)
        if len(order) == 1:
            break
        rest = order[1:]
        ious = box_iou(boxes[[i]], boxes[rest])[0]
        order = rest[ious <= iou_threshold]
    return np.array(keep, dtype=np.int64)

Deterministic, O(N log N) from the sort, and matches the behaviour of torchvision.ops.nms on identical inputs.

Step 3: Box encoding and decoding

Convert between pixel coordinates and the (tx, ty, tw, th) targets that the network actually regresses.

python
def encode(box_xyxy, cell_x, cell_y, stride, anchor_wh):
    x1, y1, x2, y2 = box_xyxy
    cx = 0.5 * (x1 + x2)
    cy = 0.5 * (y1 + y2)
    w = x2 - x1
    h = y2 - y1
    tx = cx / stride - cell_x
    ty = cy / stride - cell_y
    tw = np.log(w / anchor_wh[0] + 1e-8)
    th = np.log(h / anchor_wh[1] + 1e-8)
    return np.array([tx, ty, tw, th])
 
 
def decode(tx_ty_tw_th, cell_x, cell_y, stride, anchor_wh):
    tx, ty, tw, th = tx_ty_tw_th
    cx = (sigmoid(tx) + cell_x) * stride
    cy = (sigmoid(ty) + cell_y) * stride
    w = anchor_wh[0] * np.exp(tw)
    h = anchor_wh[1] * np.exp(th)
    return np.array([cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2])
 
 
def sigmoid(x):
    return 1.0 / (1.0 + np.exp(-x))

Test it: encode a box, then decode it. You should get back something very close to the original box, up to the sigmoid inverse not being perfectly invertible when tx falls outside the post-sigmoid range.

Step 4: A minimal YOLO head

One 1x1 conv on a feature map, reshaping to (B, S, S, num_anchors, 5 + C).

python
import torch
import torch.nn as nn
 
class YOLOHead(nn.Module):
    def __init__(self, in_c, num_anchors, num_classes):
        super().__init__()
        self.num_anchors = num_anchors
        self.num_classes = num_classes
        self.conv = nn.Conv2d(in_c, num_anchors * (5 + num_classes), kernel_size=1)
 
    def forward(self, x):
        n, _, h, w = x.shape
        y = self.conv(x)
        y = y.view(n, self.num_anchors, 5 + self.num_classes, h, w)
        y = y.permute(0, 3, 4, 1, 2).contiguous()
        return y

Output shape: (N, H, W, num_anchors, 5 + C). The last dimension holds [tx, ty, tw, th, obj, cls_0, ..., cls_{C-1}].

Step 5: Ground-truth assignment

For every ground-truth box, decide which (cell, anchor) is responsible.

python
def assign_targets(boxes_xyxy, classes, anchors, stride, grid_size, num_classes):
    num_anchors = len(anchors)
    target = np.zeros((grid_size, grid_size, num_anchors, 5 + num_classes), dtype=np.float32)
    has_obj = np.zeros((grid_size, grid_size, num_anchors), dtype=bool)
 
    for box, cls in zip(boxes_xyxy, classes):
        x1, y1, x2, y2 = box
        cx, cy = 0.5 * (x1 + x2), 0.5 * (y1 + y2)
        gx, gy = int(cx / stride), int(cy / stride)
        bw, bh = x2 - x1, y2 - y1
 
        ious = np.array([
            (min(bw, aw) * min(bh, ah)) / (bw * bh + aw * ah - min(bw, aw) * min(bh, ah))
            for aw, ah in anchors
        ])
        best = int(np.argmax(ious))
        aw, ah = anchors[best]
 
        target[gy, gx, best, 0] = cx / stride - gx
        target[gy, gx, best, 1] = cy / stride - gy
        target[gy, gx, best, 2] = np.log(bw / aw + 1e-8)
        target[gy, gx, best, 3] = np.log(bh / ah + 1e-8)
        target[gy, gx, best, 4] = 1.0
        target[gy, gx, best, 5 + cls] = 1.0
        has_obj[gy, gx, best] = True
    return target, has_obj

Anchor selection here is just best-shape IoU with the ground truth, a cheap proxy that matches the YOLOv2/v3 assignment. v5 and later use more sophisticated strategies (task-aligned matching, dynamic k) that refine the same idea.

Step 6: The three losses

python
def yolo_loss(pred, target, has_obj, lambda_coord=5.0, lambda_obj=1.0, lambda_noobj=0.5, lambda_cls=1.0):
    has_obj_t = torch.from_numpy(has_obj).bool()
    target_t = torch.from_numpy(target).float()
 
    # box-regression loss: only on cells with objects
    box_pred = pred[..., :4][has_obj_t]
    box_true = target_t[..., :4][has_obj_t]
    loss_box = torch.nn.functional.mse_loss(box_pred, box_true, reduction="sum")
 
    # objectness loss
    obj_pred = pred[..., 4]
    obj_true = target_t[..., 4]
    loss_obj_pos = torch.nn.functional.binary_cross_entropy_with_logits(
        obj_pred[has_obj_t], obj_true[has_obj_t], reduction="sum")
    loss_obj_neg = torch.nn.functional.binary_cross_entropy_with_logits(
        obj_pred[~has_obj_t], obj_true[~has_obj_t], reduction="sum")
 
    # classification loss on cells with objects
    cls_pred = pred[..., 5:][has_obj_t]
    cls_true = target_t[..., 5:][has_obj_t]
    loss_cls = torch.nn.functional.binary_cross_entropy_with_logits(
        cls_pred, cls_true, reduction="sum")
 
    total = (lambda_coord * loss_box
             + lambda_obj * loss_obj_pos
             + lambda_noobj * loss_obj_neg
             + lambda_cls * loss_cls)
    return total, {"box": loss_box.item(), "obj_pos": loss_obj_pos.item(),
                   "obj_neg": loss_obj_neg.item(), "cls": loss_cls.item()}

Five hyper-parameters that every YOLO tutorial either hardcodes or sweeps. The ratios matter: lambda_coord=5, lambda_noobj=0.5 mirrors the original YOLOv1 paper and still works as a reasonable default.

Step 7: Inference pipeline

Decode the raw head output, apply sigmoid/exp, threshold on objectness, and NMS.

python
def postprocess(pred_tensor, anchors, stride, img_size, conf_threshold=0.25, iou_threshold=0.45):
    pred = pred_tensor.detach().cpu().numpy()
    grid_h, grid_w = pred.shape[1], pred.shape[2]
    num_anchors = len(anchors)
 
    boxes, scores, classes = [], [], []
    for gy in range(grid_h):
        for gx in range(grid_w):
            for a in range(num_anchors):
                tx, ty, tw, th, obj, *cls = pred[0, gy, gx, a]
                score = sigmoid(obj) * sigmoid(np.array(cls)).max()
                if score < conf_threshold:
                    continue
                cls_idx = int(np.argmax(cls))
                cx = (sigmoid(tx) + gx) * stride
                cy = (sigmoid(ty) + gy) * stride
                w = anchors[a][0] * np.exp(tw)
                h = anchors[a][1] * np.exp(th)
                boxes.append([cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2])
                scores.append(float(score))
                classes.append(cls_idx)
 
    if not boxes:
        return np.zeros((0, 4)), np.zeros((0,)), np.zeros((0,), dtype=int)
    boxes = np.array(boxes)
    scores = np.array(scores)
    classes = np.array(classes)
    keep = nms(boxes, scores, iou_threshold)
    return boxes[keep], scores[keep], classes[keep]

That is the complete eval path: head -> decode -> threshold -> NMS.

Check: Build It0/8
1. In the lesson's ground-truth assignment step (assign_targets), how is an object's responsible anchor chosen among the anchors at its grid cell?Explain
A tall ground-truth box with three anchor priors, small, medium and large, overlaid at its corner, and a dashed box asking which anchor becomes responsible.
Drawn to one shared scale from a fresh ground-truth box and the lesson's own anchor sizes.
2. box_iou is called with one box A = (0, 0, 10, 10) and one box B = (5, 0, 15, 10), each in an array of shape (1, 4). What does the returned (1, 1) matrix contain?Apply
Two 10 by 10 boxes A and B overlapping by half their width, labeled with their exact coordinates, next to a dashed box asking what the returned IoU matrix holds.
Boxes drawn to scale from the coordinates given in the question; no overlap or union number is shown.
3. Three boxes A, B, C have scores 0.9, 0.8, 0.7. IoU(A,B)=0.6, IoU(A,C)=0.1, IoU(B,C)=0.5, and the NMS threshold is 0.45. Which boxes does the lesson's nms return?Apply
A table of three boxes A, B and C with their scores and pairwise IoUs, and a threshold of 0.45, next to a dashed box asking which boxes NMS keeps.
Table reproduces the scores and IoUs given directly in the question.
4. decode is called with tx=ty=tw=th=0, cell_x=3, cell_y=2, stride=32, and anchor (30, 60). Which (x1, y1, x2, y2) box does it return?Apply
A card with the question's exact decode inputs, a pixel grid with cell 3, 2 highlighted and pixel positions marked along the bottom, and the anchor drawn at the same scale beside a dashed box asking for the resulting box coordinates.
Inputs quoted from the question; the grid and anchor are drawn to one shared scale.
5. With stride 32 and anchors (30, 60), (75, 170), (200, 380), assign_targets receives a ground-truth box (100, 100, 164, 196). Which cell and anchor become responsible?Apply
A pixel grid with its origin at zero, 32 pixels per cell, with one ground-truth box drawn on it to scale, the pixel positions marked along both axes, and a dashed box asking which cell and which anchor assign_targets would mark responsible.
Grid and box drawn to scale from the question's own coordinates; no cell or anchor is highlighted.
6. YOLOHead(in_c=256, num_anchors=3, num_classes=20) receives a feature map of shape (2, 256, 13, 13). What shape does forward return?Apply
A card naming a YOLOHead with 256 input channels, 3 anchors and 20 classes, and a feature map of shape (2, 256, 13, 13), next to a dashed box asking for the output shape.
Config and feature map shape quoted directly from the question.
7. In postprocess, one anchor has sigmoid(obj) = 0.6 and its best class has sigmoid probability 0.5. With conf_threshold=0.25, what happens to it?Apply
A card listing an objectness probability of 0.6 and a best class probability of 0.5 for one anchor, a confidence threshold of 0.25, and a dashed box asking for the score and whether it is kept.
Probabilities quoted directly from the question.
8. A detector trains to a suspiciously flat loss curve on images where two objects often fall in the same grid cell with the same best-matching anchor. What is the likely cause?Analyze
Two object centres inside the same highlighted grid cell, both matched to the same anchor, next to a dashed box asking what the single target slot for that cell and anchor ends up holding.
Illustrative pair of objects sharing one cell and anchor.

Use It

torchvision.models.detection ships production detectors with the same conceptual structure. Loading a pretrained model takes three lines.

python
import torch
from torchvision.models.detection import fasterrcnn_resnet50_fpn_v2
 
model = fasterrcnn_resnet50_fpn_v2(weights="DEFAULT")
model.eval()
with torch.no_grad():
    predictions = model([torch.randn(3, 400, 600)])
print(predictions[0].keys())
print(f"boxes:  {predictions[0]['boxes'].shape}")
print(f"scores: {predictions[0]['scores'].shape}")
print(f"labels: {predictions[0]['labels'].shape}")

For real-time inference pipelines, ultralytics (YOLOv8/v9) is the standard: from ultralytics import YOLO; model = YOLO('yolov8n.pt'); model(img). The model handles decoding and NMS internally and returns the same boxes / scores / labels triple you built above.

Check: Use It0/1
1. You need real-time detection in a few lines of code and do not want to hand-write head decoding and NMS. What does the lesson point to?Design
A four step flow of head, decode, threshold and NMS labelled as hand-written code, next to a dashed box asking for the fastest route to real-time detection with the least code.
The four steps are quoted from the lesson's own eval path.

Ship It

This lesson produces:

  • outputs/prompt-detection-metric-reader.md, a prompt that turns a precision, recall, AP, [email protected]:0.95 row into a one-line diagnosis and the single most useful next experiment.
  • outputs/skill-anchor-designer.md, a skill that, given a dataset of ground-truth boxes, runs k-means on (w, h) and returns anchor sets per FPN level plus the coverage statistics you need to pick the right number of anchors.
Check: Ship It0/2
1. You train on your own factory images using anchors borrowed from a different dataset, and the model keeps missing objects with unusual shapes. What should you check first?Analyze
Three dashed borrowed anchor shapes beside two solid factory part outlines, all drawn to one shared scale on a shared baseline with their widths and heights labelled.
Part sizes are illustrative; the borrowed anchors are the lesson's own three anchor examples.
2. You start a detector on a new dataset and must choose anchor sets per FPN level and how many anchors to use. Which approach fits the lesson?Design
A scatter plot of width and height values for a new dataset's ground-truth boxes, forming three loose groups, next to a dashed box asking how to choose anchors for it.
Illustrative synthetic dataset of box widths and heights.
Author's Note

The bug that costs the most time here almost never lives in the loss function or the head. It lives in ground-truth assignment. I've watched a detector train to a suspiciously flat loss curve because two objects landed in the same grid cell and only one survived assignment, or because someone borrowed an anchor set from a completely different dataset and never checked whether it matched their own box shapes. skill-anchor-designer above exists because running k-means on your own box widths and heights takes five minutes and will tell you immediately whether the anchors you inherited actually fit your objects.

NMS looks like a footnote until you're staring at a detector that reports the same forklift six times in one frame. Once you've implemented it by hand, every "why are there duplicate boxes" bug report turns from a mystery into a five-minute fix. That's the real argument for building this from scratch even though ultralytics will do all of it for you in three lines: you want the mental model in place before the library's defaults ever surprise you.

Exercises

  1. (Easy) Implement box_iou and run it against torchvision.ops.box_iou on 1,000 random box pairs. Verify max absolute difference is below 1e-6.
  2. (Medium) Port yolo_loss to a version that uses CIoU box loss instead of MSE. Show on a 100-image synthetic dataset that CIoU converges to a better final [email protected]:0.95 than MSE in the same number of epochs.
  3. (Hard) Implement multi-scale inference: feed the same image at three resolutions through the model, union the box predictions, and run a single NMS at the end. Measure the mAP lift vs single-scale inference on a held-out set.

Key Terms

TermWhat people sayWhat it actually means
Anchor"Box prior"A pre-defined box shape at each grid cell from which the network predicts deltas instead of absolute coordinates
IoU"Overlap"Intersection-over-union of two boxes; the universal similarity measure in detection
NMS"Deduplicate"Greedy algorithm that keeps highest-score predictions and removes overlapping ones above a threshold
Objectness"Is there something here"Per-anchor, per-cell scalar predicting whether an object is centred in that cell
Grid stride"Downsample factor"Pixels per grid cell; a 416-px input with a 13-grid head has stride 32
mAP"Mean average precision"Average of the area under the precision-recall curve, averaged over classes and (for COCO) IoU thresholds
[email protected]"PASCAL VOC AP"Average precision with IoU threshold 0.5; the lenient version of the metric
[email protected]:0.95"COCO AP"Average over IoU thresholds 0.5..0.95 step 0.05; the strict version and current community standard

Further Reading