4 Object Detection: The Two-Stage Lineage
Skills ladder. Implement IoU and mAP by hand once, before ever trusting a library’s version. Set thresholds on validation data, never on the test set you intend to quote.
Classification answers what; detection answers what, where, and how many.
This third question creates a structural break from the models we’ve seen so far, which can only produce a fixed number of outputs. Figure 4.1 demonstrates this.
A classifier’s head produces a fixed-length vector, one score per class, the same length for every image. An object detector has to produce a set whose size we do not know in advance: no objects in one photograph, two in the next, forty in the one after that.
Every architecture considered in this chapter turns this variable-length set problem into a fixed-size problem, which we can address with the architectures of the last three chapters. In the two-stage lineage, models first nominate or propose candidate regions where they think objects may be present, then classify each one. The classifier’s output is fixed-length as before, and the variable length problem has shifted to the component that proposes regions.
The four designs that follow are a sequence of removed bottlenecks, and in each case, the arithmetic of the bottleneck is the argument for the next design.
4.1 Object Detection, Metrics, and NMS
A detection is a bounding box, a class, and a score. The bounding box is an axis-aligned rectangle defined by four pixel coordinates in the input image, and it has three common encodings. The corner form \((x_1, y_1, x_2, y_2)\) specifies the top-left and bottom-right corners. This is the most convenient representation to calculate the IoU metric, presented later in this section. The centre form \((x, y, w, h)\) gives the centre point of the box, with a width and a height. The box regressors from Section 4.2 onwards use this representation, because a location correction has to be measured against something: a translation (in units of the box’s own width/height) and a scaling (as a log ratio) mean the same thing at every scale, while two independently regressed corners do not. The third form, which COCO’s annotations use, is \((x_1, y_1, w, h)\): the same top-left corner as the first form, followed by a size rather than a second corner.
Detection code carries all three and often converts between them. Torchvision’s box_convert names them xyxy, cxcywh and xywh, each used where its arithmetic is most natural. The last two are the ones to watch out for. Both read as x, y, then a width and a height, and nothing in a file, a tensor, or a debugger can distinguish them. Mistaking cxcywh for xywh, or vice versa, leads to offsets of w/2 and h/2 that can be difficult to trace.
The class is one label from the closed label set that the detector was trained on. The score is the detector’s confidence in that pairing, a number between 0 and 1 (typically from a softmax). The output for an image is a list of those triples: [(box_location_0, class_0, score_0]), (box_location_1, class_1, score_1])...].
Figure 4.2 shows the output of a real detector on our image. It finds both the Ball and the Dog with scores of 0.999 and 0.996, respectively. It also returns seven other boxes. The calculations that follow later in this chapter use this set of annotations.
Faster R-CNN R50-FPN v2. Gold, dashed: the two ground-truth boxes drawn by hand for this book, the dog at \((286, 322, 611, 665)\) and the sports ball at \((605, 562, 770, 712)\). The nine numbered boxes are every detection with a class score above 0.05: blue where the class agrees and the overlap clears 0.5, grey where it does not. The orange scores an IoU of 0.976 against the ball’s ground truth, localising it better than the correct sports-ball detection does, but a match requires the class to agree. Four more are stacked on one rock, calling it a teddy bear, a bird, a sheep and a bear, and one is a bird on the rock behind it at 0.499 with a second over the same patch at 0.100.
Nothing has gone wrong. Those seven boxes are what this detector produces whenever the score threshold is low enough to keep them, and the usual practice is to discard them based on a higher threshold.
4.1.1 Intersection over union
Deciding whether a predicted box is the same object as a ground-truth box needs a metric. Detection benchmarks typically use intersection over union (IoU): the area the two boxes overlap, divided by the area of their union.
\[ \mathrm{IoU}(B_p, B_{gt}) \;=\; \frac{\lvert B_p \cap B_{gt} \rvert}{\lvert B_p \cup B_{gt} \rvert} \]
It runs from 0 for disjoint boxes to 1 for identical ones, and it is scale-free: multiply every coordinate of both boxes by three (i.e. scale the image and detections by three), and the ratio does not change, because both areas grow by a factor of nine.
What do you think a reasonable threshold is for deciding whether two boxes overlap sufficiently? Use Figure 4.3 to build some intuition.
PASCAL VOC set the threshold for a correct detection at 0.5: “the threshold of 50% was set deliberately low to account for inaccuracies in bounding boxes in the ground truth data,” since a box around a non-convex object is subjective and a person with their arms and legs spread has no single right rectangle [1]. It is a convention and may vary by image domain and application.
4.1.2 Non-maximum suppression
A single object can produce many correct boxes, each slightly offset from the others. Figure 4.4 shows every box this detector proposes for one animal in the book’s photograph, with suppression switched off.
A detector evaluates a densely packed set of overlapping candidate regions, which means that when a predicted box closely matches the true object, plenty of other candidates will almost coincide with it. As a result, detections of the same real object typically arrive as clusters of nearly identical boxes. For example, with non-maximum suppression turned off and a score threshold of 0.05, this detector outputs 38 boxes marked dog our image. Of the dog boxes, all 37 other boxes overlap the top-scoring dog detection at an IoU greater than 0.5, and 29 overlap it above 0.9. These boxes are essentially duplicates of a stronger scored detection.
Non-maximum suppression (NMS) removes them, greedily, one class at a time so that we do not return multiple detections for a single object.
Applied to the 38 candidate dog boxes of Figure 4.4 at the usual \(\tau = 0.5\), this retains exactly one.
Figure 4.5 shows a scenario with two objects of the same class. Using only one dog can illustrate the need for suppression, but not its purpose: with a single object, an algorithm that removes overlapping boxes behaves like one that simply selects the highest-scoring box. The distinction only appears when multiple objects are present and both should remain. Two generated puppies demonstrate which boxes are removed, and which must stay.
Both of those outcomes turned on a number that was chosen rather than derived. Set \(\tau\) too high and duplicates survive, which is the failure Figure 4.5 shows at 0.90. Set it too low and the algorithm starts deleting boxes that belong to different objects, which that scene cannot show at any sensible threshold, because its two puppies sit far enough apart that their correct boxes barely overlap at all. The second failure needs neighbours close enough that the right answer for one already covers part of the other. Two people standing shoulder to shoulder can be marked by two equally sized boxes offset by half their width, which overlap at an IoU of exactly \(1/3\): a threshold of 0.3 deletes one of them, and 0.4 keeps both. Nothing in the image says which side of that line to stand on, and no gradient reaches it, so the value is set by hand and checked on held-out data.
Figure 4.6 is that case. It moves the same four boxes onto two puppies sitting against each other, where the overlap between the two correct boxes is no longer negligible.
The deletion is final. NMS runs after the network, on boxes and scores alone; it has no gradient, and nothing downstream can recover a box it removed. Soft-NMS decays the scores of overlapping boxes rather than deleting them, for exactly that reason [2].
Back on our default image, running NMS per class has a visible consequence in Figure 4.2. The four spurious boxes on the rock overlap each other at an IoU above 0.96, and the orange sits on the sports ball at an IoU of 0.956. Same pixels, different labels, so nothing suppresses anything. Figure 4.7 runs the algorithm on the unsuppressed candidates, one deletion at a time.
4.1.3 Precision, recall, and the score threshold
With IoU available, we can assign detections to ground-truth boxes and count successes and failures. The procedure is greedy, working down the score ranking, and its one subtlety is that a ground-truth box can be claimed only once: a second detection of an already-claimed object is a false positive, not a bonus. The VOC benchmark handled “5 detections for one object” as “1 correct detection and 4 false detections,” and placed the responsibility for suppression on the detector rather than the evaluation protocol [1].
Given a set of detected boxes (with scores) and ground-truth boxes for an image, the standard evaluation protocol assigns detections to ground-truths as follows:
Sort detections by confidence score, from highest to lowest.
For each detection in order:
- Find the ground-truth box of the same class with the highest IoU (Intersection-over-Union) with this detection, among those still unclaimed.
- If that IoU is at least the threshold (e.g., 0.5), assign this detection as a True Positive (TP), and mark that ground-truth box as “claimed”.
- If no such ground-truth exists or IoU is below threshold, this detection is a False Positive (FP).
Any ground-truth box not matched by any detection is a False Negative (FN).
Special caution: As each ground-truth box is removed after being “claimed”, if multiple detections overlap with the same ground-truth box, only the first to claim it is a True Positive; the rest are False Positives. VOC counted “5 detections of a single object” as “1 correct detection and 4 false detections”, and said that filtering them was the detector’s job, not the benchmark’s [1].
Non-maximum suppression is what prevents that last case from inflating the count. It runs before evaluation, not after, so the 59 candidates in Figure 4.7 are reduced to two by the time they reach the matcher; the other 57 duplicates are never evaluated. Suppression alleviates the issue but does not eliminate it because it is applied per class, the orange on top of the sports ball in Figure 4.2 remains unaffected and is, rightfully, counted here as a false positive.
Precision is the fraction of detections that were right, \(\mathrm{TP}/(\mathrm{TP} + \mathrm{FP})\): of all the dogs we predicted, what proportion were correct?
Recall is the fraction of objects that were found, \(\mathrm{TP}/(\mathrm{TP} + \mathrm{FN})\): of all the actual dogs in the image, what proportion of them did we find?
The score threshold is the classification confidence below which detections are discarded, and it trades one against the other.
4.1.4 Average precision
The score threshold determines the balance between precision and recall. Raising the threshold requires the model to be more confident before signalling a detection: this cuts out weak guesses, so precision improves, but it also removes some correct but less confident detections, causing recall to fall as objects go unfound. Lowering the threshold has the opposite effect: the detector accepts more uncertain predictions, so it recovers missed objects and raises recall, but at the cost of admitting more false positives, which lowers precision. Raising the threshold cannot buy recall, and lowering it usually costs precision — but not always: when a newly admitted detection finds an unclaimed object, both rise together, which is exactly what the last row of Table 4.1 does. The trade-off is a tendency of the sweep, not a law of each step, which is why quoting performance at a single threshold says as much about the chosen threshold as about the detector itself.
Varying the threshold from 1 down to 0 traces out a curve that captures the trade-off between precision and recall. As the threshold decreases, additional detections are included, so recall can only increase, since more true objects may be found. Precision, on the other hand, generally steps downward each time a newly admitted detection turns out to be a false positive. This relationship forms the precision–recall curve, illustrated in Figure 4.8, with each point labelled by the threshold at which it is reached.
There are only five thresholds worth testing, each just below a detection score, since detections are included or excluded only as the threshold crosses these values. Between these points, nothing changes; the sweep yields five distinct operating points, which are all shown in Table 4.1.
| score threshold | kept | TP | FP | precision | recall |
|---|---|---|---|---|---|
| 0.99 | 1 | 1 | 0 | 1.000 | 0.333 |
| 0.95 | 2 | 2 | 0 | 1.000 | 0.667 |
| 0.90 | 3 | 2 | 1 | 0.667 | 0.667 |
| 0.50 | 4 | 2 | 2 | 0.500 | 0.667 |
| 0.10 | 5 | 3 | 2 | 0.600 | 1.000 |
Each threshold picks out a different point on the precision–recall curve. Here, we have three objects (all the same class) and five detections, the same ones tallied in the table above. As you drag the threshold slider, \(\tau\), through the widget, you’ll see the panels shift in sync: detections with lower scores fade from the photo, any object left unclaimed is highlighted as missed, the true and false positive counts (and the corresponding precision and recall values) update for what remains, and the active spot moves along the curve. The curve itself never changes; it is drawn once for all possible thresholds. Where you stand on it is all about your choice of threshold. That’s why average precision, represented by the shaded area under the curve, is a property of the detector as a whole, while actual precision and recall depend on the specific threshold you set.
It’s helpful to check the extremes. At \(\tau = 0\), every detection, good or bad, is included, so all three objects are found, but two detections are counted as wrong. Raise the threshold just past 0.10: now the weakest detection disappears, and with it, puppy 3 is missed, dropping recall to 2/3. From there, no amount of threshold fiddling will bring that missed object back: raising the threshold always removes detections, never adds them. Push \(\tau\) to 1 and everything is filtered out; no detections remain, so precision becomes 0/0: undefined, not zero.
The area beneath that curve is called average precision (AP). It is the average of the detector’s precision across its entire range of operating thresholds, rather than at a single point. Precision and recall alone always depend on the threshold you pick, and what “the right threshold” is will change depending on where or how the model is used. A traffic survey and a pedestrian crossing (as in exercise 7) might need very different settings, even with the same detector. Average precision sidesteps this by folding the threshold out of the equation entirely: two detectors can be compared fairly, without having to agree first on how conservative or adventurous they should be.
mAP, mean Average Precision, takes that one step further by averaging AP across all classes. The benchmark scores each class separately, matching every dog detection only to dog boxes, so that rare classes aren’t overwhelmed by common ones. It then computes the mean over all per-class AP values. This mean is unweighted, so a class that barely appears counts just as much as one that shows up thousands of times in the overall figure. That’s a deliberate choice, with trade-offs, and Section 4.1 will explore its implications later in this section.
COCO also goes beyond a single IoU threshold. Its headline AP is calculated by averaging results across ten IoU thresholds, from 0.50 to 0.95 in increments of 0.05, making detectors accountable not just for finding objects but for localising them precisely [3]. This is why AP scores on COCO tend to be lower than those reported for VOC. For example, Faster R-CNN with VGG-16 achieves 21.5% AP on the COCO test-dev split, but scores 42.1% AP on those same images when evaluated at the single VOC-style threshold of 0.5 [4]. Both results stem from identical predictions on identical data; the difference between them is entirely due to the evaluation metric. The further decline from the 73.2% AP reported on VOC 2007 reflects differences in the datasets themselves: COCO contains more objects per image, and a larger proportion of small objects.
Sweeping the threshold, step by step: the widget sorts the 59 pre-suppression candidates for the dog and the ball, plus the 7 final detections the detector reports for classes the scene does not hold by their confidence scores, and matches them up against the two ground-truth boxes. Try moving the threshold slider: every detection above that line stays visible in the left panel, while your “operating point” walks along the precision–recall curve on the right. The shaded area under the (interpolated) curve, the average precision or AP, does not shift as you adjust the threshold: it’s determined by the full set of predictions, not your settings.
Every threshold you set in detection comes down to a decision about who ends up dealing with the mistakes. If you dial up recall, a human reviewer will have to sift through extra false positives the system lets through. If you optimise for precision instead, the model quietly tosses out detections it isn’t confident about, potentially missing something important. Usually, the burden from these errors doesn’t fall on the same people. The most vulnerable spots are often those classes the model saw little of during training, and real-world difficulties or distribution shifts can make its confidence even shakier. These risks show up only after you deploy, not just in your validation numbers. mAP, for its part, hides all this nuance: by averaging over all classes (and, in the case of COCO, over multiple IoU thresholds), it can reward small, broad improvements while ignoring major losses on the very class you care about most. A benchmark score is not a justification for launching a system. Before you quote a number, ask yourself: which mistakes got lost in the average, and who would have had to deal with them?
4.2 R-CNN: detection by classification
R-CNN is, at its core, quite straightforward: it takes a set of candidate regions in an image and runs a classifier on each one [5].
These regions come from selective search. The algorithm works by first over-segmenting the image into small patches called superpixels, areas of similar colour and texture, using Felzenszwalb and Huttenlocher’s efficient graph-based segmentation. It then continually merges the neighbouring pair that are most alike, based on four handcrafted similarity scores (colour, texture, size, and how fully they “fill” their common bounding box). Every grouping created during this process contributes its own bounding box as a potential candidate [6]. In its fast mode, which R-CNN uses, the method yields about 2,134 boxes per image on the VOC 2007 test set (covering 98% of annotated objects), or 2,147 boxes at 3.79 seconds per image on train+val (where parameters were chosen). This is pure algorithmic logic: every rule is chosen by hand, not learned from data.
For each proposal, R-CNN crops and warps it to a fixed \(227 \times 227\) square, regardless of the original aspect ratio, and feeds it into a CNN that was first pretrained on ImageNet and then fine-tuned for detection. The CNN’s 4,096-dimensional feature vector is then passed through a separate linear SVM for each class (this is the “frozen features” approach from Section 3.4, but here, the backbone is fine-tuned first). A ridge regressor specific to each class also predicts four tweaks to the box: two scale-invariant shifts of the center, and two log-scale adjustments to its width and height [5]. Finally, non-maximum suppression (NMS) eliminates duplicates.
This process consists of three distinct learning steps, all run in sequence, with features written to disk in between: first, fine-tune the network with a log loss; next, extract and cache a feature vector for every proposal in the training set; finally, train the SVMs and regressors. The sheer scale was daunting: Girshick later reported the caching alone cost 2.5 GPU-days for the 5,000 VOC 2007 trainval images, and filled hundreds of gigabytes [7].
Despite this, R-CNN worked wonders. On VOC 2012, it achieved a mAP of 53.3%, over 30% better than the previous best [5]. On VOC 2007, it scored 58.5% using AlexNet as its backbone, and 66.0% with VGG-16, both with box regression added. The whole pipeline is illustrated in Figure 4.11.
R-CNN’s remaining problem was speed, and the reason was baked into its design. Every one of those (often overlapping) proposals required its own full CNN forward pass. With VGG-16, that added up to 47 seconds per image on a GPU [7]. To process the 4,952 images of the VOC 2007 test set? That’s 65 hours for one evaluation run. The convolution time, run thousands of times on heavily overlapping crops, was by far the biggest bottleneck.
4.3 Fast R-CNN: share the features
Let’s look for the unnecessary repetition here. A convolutional response depends only on the local neighbourhood of pixels, meaning that if you take two crops that both cover that area (well inside their own boundaries), you’ll compute the same feature there both times. (Chapter 2 reminds us that this “equivariance” isn’t perfect due to padding, strides, and the fact that edges behave differently, so crops of the same object can still produce slightly different outputs near the edges. Changing the crop size, or the way it’s resized, adds discrepancies.) Still, in R-CNN, running the network backbone thousands of overlapping times means almost all convolutional work is redundant.
Fast R-CNN fixes this waste: it runs the backbone CNN once, on the whole image [7]. Instead of cropping pixels, it projects each proposal onto the resulting feature map. To do this, it divides the region’s corners by the backbone’s stride (which is 16 for VGG-16’s conv5, the product of four pooling layers), landing the box on precisely the part of the feature map that corresponds to those pixels. Figure 4.12 visually compares this streamlined pipeline to the original.
SPPnet [8] had already shown it was possible to pool proposals from a single feature map calculated for the whole image, but its training could not update the backbone’s convolutional layers, limiting the accuracy of deep backbones [7]. Fast R-CNN introduced end-to-end training for the entire model.
Of course, there’s one wrinkle: even after projecting, these boxes can still have all sorts of shapes, but fully connected (dense) layers need input of a fixed size. That’s where RoI pooling comes in. It divides each projected box, regardless of its actual dimensions, into a fixed \(7 \times 7\) grid, and then max-pools each bin into one value [7]. As a result, every proposal, whether big or small, fat or skinny, gets converted into a neat \(7 \times 7 \times C\) block, suitable for the dense head.
There’s a lot of rounding involved here, and it’s worth seeing what happens in numbers. Take the dog’s bounding box from Figure 4.2, \((286, 322, 611, 665)\), running through a stride-16 map. Divide by 16 to project: \((16.863, 18.986, 36.026, 39.210)\) in feature map “cells.” The system then snaps these to whole numbers: \((17, 19, 36, 39)\). How much did the box move in the image? The edges shift by \(+2.3\), \(+0.2\), \(-0.4\), and \(-3.6\) pixels, and the worst possible shift is half a cell, 8.5 pixels. That snapped box is 19 cells wide and 20 tall, neither neatly divisible by 7. So, for the \(7 \times 7\) grid, we need a second quantisation: for each bin, its start is rounded down, its end rounded up, leading to uneven bins of 3, 4, 4, 3, 4, 4, 3 cells wide for the columns. The two directions of rounding mean neighbouring bins can overlap: for example, bins 1 and 2 both read column 19. In total, 6 of the 19 columns get double-counted, with the narrow bins covering three-quarters of what the wide bins see. The end result is a \(7 \times 7\) grid that’s close, but not quite exactly where we originally wanted it.
Here’s a fun detail: one cell isn’t a “clean” 16 pixels, but 16.96, due to preprocessing. The original \(1264 \times 848\) image is resized to \(800\) pixels tall, then padded out to \(1216 \times 800\) so that every stride divides it. The resulting map is \(76 \times 50\) at stride 16 (in padded coordinates), which actually means each “cell” is \(848 / 50 = 16.96\) pixels in the original. Be sure to project boxes in the right coordinate system! The figures in this chapter use a feature pyramid; specifically, pooling comes from the stride-16 level (P4), but the arithmetic is the same for any stride.
If all you want is a class label, these minor pixel misalignments don’t really matter. But if you need a box placement that’s just right, or per-pixel masks (see Chapter 6), those snap-induced misalignments show up in your results. If your detector’s boxes are always a couple pixels off, this may well be why. The fix is RoIAlign, which does away with both roundings and instead samples features at each bin’s exact real-value position using bilinear interpolation [9]. This improvement matters most for segmentation, but it helps precise detection too; Figure 4.13 shows both approaches side by side.
With feature maps available, there’s no longer any need for separate SVMs and regressors. Fast R-CNN replaces all three of R-CNN’s earlier learning steps with a single network, trained end-to-end using a single loss: a softmax across \(K + 1\) classes (background included), and for each class, four box adjustments in R-CNN’s format. The loss is:
\[ L \;=\; L_{\text{cls}} \;+\; \lambda\,[u \ge 1]\, L_{\text{loc}}, \]
Here, \(u\) is the true class of the proposal; the box regression term is switched off (by the bracket) for proposals labelled background, which have no “true” box to regress to.
The results? Accuracy holds, but speed soars. Training VGG-16 for detection drops from 84 hours to 9.5. Testing falls from 47 seconds per image to 0.32, a 146-fold improvement (or 213× with a further SVD trick on the dense head). And, as a bonus, VOC 2007 mAP goes up a hair (from 66.0% to 66.9%) [7]. That 65-hour evaluation? Now 26 minutes.
All these timings skip proposal generation, which now becomes the most time-consuming part. Selective search usually runs on CPU and can take significantly longer than the rest of the trained network.
4.4 Faster R-CNN: learn the proposals
Ren et al. asked why the proposals, alone in the pipeline, were still hand-coded when they could be learned. Here’s the time breakdown for VGG-16 with selective search: 146 ms in the backbone CNN, 1,510 ms for making proposals, and 174 ms for the per-region head. That’s 1,830 ms per image, with a whopping 83% spent on slow, untrained proposals, or about half a frame per second [4].
Yet the features needed to spot “good” regions are right there in the shared feature map. Proposals are boxes with scores, and a box is four numbers, exactly what a tiny convolutional network can emit. So Faster R-CNN introduces the region proposal network (RPN): a lightweight prediction head, reading from the shared feature map and trained seamlessly using ordinary backpropagation.
The RPN comprises three convolutional layers. The first is a bank of 512 filters (256 for smaller backbones), each of size \(3 \times 3\): one filter reads a \(3 \times 3\) window through every channel of the shared map and produces a single number per position, so the bank together produces a 512-dimensional vector at each position. Two branches of \(1 \times 1\) convolutions then read this vector: one has \(2k\) kernels outputting \(2k\) “objectness” scores (two neurons per reference box, a binary object-or-not classification), the other predicts \(4k\) numbers (boxes), where \(k\) is the number of reference boxes at each map position [4]. Importantly, all these convolutions are shared across the spatial map, like the convolutions in the backbone network itself, but here applied to proposing regions instead of classifying. See Figure 4.14 for a diagram.
Here, \(k\) is the number of anchors: predefined, fixed boxes centred at each position. In the classic setup, there are \(k = 9\) per position: three different areas (\(128^2\), \(256^2\), \(512^2\) pixels) and three aspect ratios (1:1, 1:2, 2:1) [4]. An anchor isn’t a prediction; it’s a “starter” shape for the network to refine. The RPN regresses each anchor to better fit objects using this familiar parameterisation:
\[ t_x = \frac{x - x_a}{w_a}, \qquad t_y = \frac{y - y_a}{h_a}, \qquad t_w = \log\frac{w}{w_a}, \qquad t_h = \log\frac{h}{h_a}, \]
Here, \((x_a, y_a, w_a, h_a)\) define the anchor’s center and size. The x/y shifts are relative to the anchor’s size, and the w/h changes are in log-space, so the same predicted values have the same meaning at any scale. Compare this to directly predicting absolute pixel coordinates every time, a much harder learning problem. Instead, each anchor only needs to learn how to “nudge” itself, from a sensible starting place, to tightly fit an actual object.
Anchors blanket the image thanks to dense feature maps. For an image of \(1000 \times 600\) pixels on a stride-16 map, the resulting grid is \(62 \times 37\): 2,294 positions, 9 anchors each, about 20,646 anchors for the whole image. (The paper reports that, typically, only about 6,000 survive after excluding those that hang off the edges [4].)
Assigning labels to anchors is based on IoU overlap with ground-truth boxes. If an anchor gets IoU \(\ge\) 0.7 against any ground-truth box, it’s positive (“foreground”); if its IoU is below 0.3 for all, it’s negative (“background”); otherwise, it doesn’t affect training. There’s a third, crucial rule: for each ground-truth box, the anchor that overlaps it most, no matter how low the IoU, is marked positive as well. Why? Because with only 9 fixed shapes per grid spot, some objects are impossible to match closely. If not for this rule, objects with no good anchor nearby would never contribute to training. For instance, an upright pole \(24 \times 420\) pixels in size only reaches 0.196 IoU with the best possible anchor, well below the positive threshold.
For training, the loss is computed over 256 anchors sampled per image, balancing positives and negatives up to 1:1. Since background examples vastly outnumber foreground, this sampling prevents overwhelming the training with negatives [4]. At test time, the RPN’s boxes are filtered by NMS at 0.7 IoU, leaving about 2,000 per image, out of which the top 300 by “objectness” are sent to the detection head for final classification. Figure 4.15 shows this process interactively.
With this shared proposal mechanism, the timing improves: 141 ms for backbone, 10 ms for proposals, 47 ms for the head, a total of 198 ms per image (about 5 fps), which is more than 9 times faster than using selective search. And accuracy? It actually improves, rising to 73.2% mAP on VOC 2007 and 70.4% on VOC 2012 (trained with extra data) [4].
Now, the only “hand-designed” part left is NMS (and some knobs: the NMS threshold, anchor shapes, and the cap of 300 proposals). The slowest part remaining is the per-region head, whose computation still scales with the number of proposals, 47 out of the 198 ms per image. Importantly, what’s still tricky for accuracy is handling scale.
4.5 Feature pyramid networks
So what about that scale problem? Using one feature map means one stride, and one resolution. On a stride-16 map, a tiny \(32 \times 32\) object is covered by a mere \(2 \times 2\) cells. Asking RoI pooling to squeeze a \(7 \times 7\) grid from that territory means most bins see repeated values: the finer details get lost. If you try to use a shallower layer for better spatial resolution, you run into a different problem: those early layers have small receptive fields and only simple patterns (edges, blobs), not high-level object features. Tiny objects actually need fine detail and deep features, but standard CNNs put those qualities at opposite ends of the network.
Researchers proposed four design options for dealing with scale, shown side-by-side in Figure 4.16:
- Featurize an image pyramid: resize the input several times and run a full backbone on each, so every scale gets deep features. (Great, but expensive: one pass through the backbone per scale!)
- Predict from a single feature map: fast and efficient, but only works well for objects at the scale matching that map’s stride.
- Predict from all feature hierarchy levels (“pyramidal feature hierarchy”): this reuses backbone work, but the finer, early levels are stuck with weak, low-level features.
- Build a feature pyramid network (FPN): combine the best of both worlds, building a pyramid where every level carries deep features.
FPN’s solution is to add a top-down path alongside the backbone, in addition to reusing all the stages of the network, but enhancing and merging information so that every level, shallow or deep, holds useful features. The backbone’s own “stages” (for ResNet: C2 to C5, at strides 4, 8, 16, and 32) provide the raw maps. The process starts from the deepest map: apply a \(1 \times 1\) convolution to make the number of channels manageable (256), which becomes P5. Then, P4 is made by upsampling P5 by 2× using nearest-neighbour interpolation, adding C4 (also sent through its own \(1 \times 1\) lateral convolution to 256 channels), and then running a \(3 \times 3\) convolution to reduce aliasing. Repeat this for P3 and P2. For the largest scales, P6 is formed by downsampling P5 by 2× (the typical implementation is a \(2\times\) max-pool). Now, every level in the pyramid, from fine to coarse, can be read by the detector head or the RPN. All maps are 256 channels deep, so one set of weights suffices for the prediction head (Figure 4.17 illustrates all this).
This design is guided by two rules:
- The RPN’s anchors are now spread across all pyramid levels: \(32^2\), \(64^2\), \(128^2\), \(256^2\), and \(512^2\) at P2 through P6, each with three aspect ratios, giving 15 anchor types instead of 9 at a single level. This lets anchor sizes fit image content at different scales.
- Each RoI (proposal) is assigned to a pyramid level according to its size:
\[ k \;=\; \left\lfloor k_0 + \log_2 \frac{\sqrt{wh}}{224} \right\rfloor, \]
with \(k_0 = 4\) [10]. Boxes of \(224 \times 224\) (ImageNet size) go to P4 (stride 16); halve the side and drop a level. The dog from Figure 4.2 goes to P4, the ball (smaller) goes to P3. So, even in a single photo, objects at different scales use different levels, which is the crucial improvement.
And the results fit expectations. On COCO, using a ResNet-50 backbone and predicting only from C4 yields 31.6 AP (minival split), but switching to FPN lifts this to 33.9, and dramatically boosts AP for small objects (from 13.2 to 17.8) [10]. With a deeper ResNet-101 backbone, FPN Faster R-CNN reaches 36.2 AP on COCO’s test-dev at 0.172 seconds per image on a single GPU.
Looking back, Section 1.5 introduced Gaussian pyramids (blurring and downsampling) and Laplacian pyramids (subtracting each level from the upsampled higher one) to let fixed operators find structure at any level. FPN borrows the way Laplacian pyramids are built, going top-down and combining and upsampling at each step, but swaps the details: instead of reconstructing differences, it adds refined features, and doesn’t aim for perfect invertibility, just for the right structure.
4.6 The two-stage template and its costs
Stepping back, a clear template emerges from these four papers: use a backbone to build shared features, propose regions somehow, and use a head to classify/refine each region. Figure 4.18 draws the three detectors as one pipeline, three times over. In R-CNN, proposals came from a hand-designed module and the backbone was run per region; Fast R-CNN moved the backbone “in front,” so features were shared; Faster R-CNN replaced the proposal generator with a learned head; FPN extended backbone features to cover different object scales. What remains in all four is the “two-stage” idea: nominate regions, then carefully adjudicate.
Second-stage processing happens per region. Whatever the head costs in FLOPs gets paid over and over, once per proposal (typically 300 per image), so computation grows with the number of proposals, not the image size itself. If you want to go faster, you can either use fewer proposals, or shrink the head, but both can cost you accuracy.
But the two-stage design has another subtle cost: it has “seams” that gradients can’t cross. RoI pooling introduces (at least) two points of quantisation. The region-selection step picks out which 300 of around 20,000 anchors the head will ever see, by sorting and thresholding, and gradients don’t reach through this. NMS, which deletes duplicate detections, is run after loss calculation, and is again not differentiable. So, a two-stage detector is trained jointly, but it isn’t one smooth, continuous function from pixels all the way to boxes.
The next chapter, Chapter 5, shows what happens if you get rid of stage two: directly predict a class and box at each anchor on the feature map in one fell swoop, and let NMS tidy up. This gives you speed, but opens up new challenges, like class imbalance, that the RPN’s careful sampling had been helping to manage.
Exercises
IoU by hand. Given a ground-truth box \((120, 80, 240, 180)\) and a prediction \((160, 80, 280, 180)\) (all in \((x_1, y_1, x_2, y_2)\) format, pixels), calculate:
- the intersection area
- each box’s area
- the union area,
- and the IoU. The answer should land exactly at the VOC threshold. Then, shift the prediction box 10 pixels to the right (to \((170, 80, 290, 180)\)) and repeat. Now multiply all eight coordinates of the original by 3 and compute again. Explain what (if anything) changed, and why.
Precision, recall, and AP. A detector is evaluated on one class across several images. The dataset contains four non-difficult ground-truth objects, G1–G4. The six detections are already ordered by decreasing score. The table gives each detection’s best same-image match; every unlisted IoU is below 0.5.
Detection Score Best match IoU D1 0.95 G1 0.82 D2 0.90 G2 0.77 D3 0.80 G2 0.61 D4 0.70 G3 0.58 D5 0.60 G3 0.44 D6 0.50 — 0.09 Use an IoU threshold of 0.5. A ground-truth object may be claimed only once.
Mark each detection as a true positive or false positive. For every true positive, name the object it claims.
Calculate the running precision and recall after each detection, then sketch the precision–recall curve.
Form the interpolated precision envelope and calculate its exact area. Express the AP as a fraction with denominator 16.
The three false positives have different causes: a duplicate detection, poor localisation, and a detection of background. Identify each one.
What is the highest recall the detector reaches? Explain why it cannot reach 1.
NMS traced by hand. You’re given five detections for one class: \(A = (100, 100, 200, 200)\) at 0.92, \(B = (120, 110, 220, 215)\) at 0.85, \(C = (300, 120, 400, 230)\) at 0.71, \(D = (310, 130, 405, 240)\) at 0.63, \(E = (125, 125, 225, 225)\) at 0.55. Calculate the four nonzero pairwise IoUs; then, trace greedy NMS for \(\tau=0.5\) and for \(\tau=0.3\), listing survivors after every step. Box \(E\) overlaps \(B\) at 0.72 yet survives \(\tau=0.5\). Why, in terms of algorithm order? If the true scene has two objects, which threshold “got it right,” and what tradeoff did the other make?
Counting anchors. For an input size of \(1216 \times 800\) and stride 16, with 9 anchors per feature-map position, how many positions and how many anchors are there? Now consider the FPN setup: five levels at strides 4, 8, 16, 32, and 64, with one scale and three aspect ratios per level (and rounding up each map’s size as needed). What’s the total number of anchors, and what share comes from the finest level? What does this fraction say about the ratio of positive to negative anchors for an anchor-based detector?
RoI projection, with the error in pixels. For a proposal at \((73, 41, 219, 188)\) (stride 16 backbone), compute:
- the projected corners before and after snapping to whole cells,
- how far each edge moves in pixels,
- and the maximum possible movement for any edge, for any proposal. The snapped box is \(9 \times 9\) cells: divide one axis into 7 bins according to real RoI pooling rules (bin starts rounded down, ends rounded up), and list the cells each bin covers and its width. Six bins should be two cells wide, one will be three, and each pair of adjacent bins shares a cell. What effect does reading a cell twice have on the head’s “view” of the object? Which rounding moves a bin boundary furthest?
Why the third assignment rule exists. In Faster R-CNN, an anchor is positive if its IoU is at least 0.7 with a ground-truth box, or if it’s the highest-overlapping anchor for that box, whatever the IoU. Take a ground-truth box \((388,190,412,610)\) (a tall, skinny pole; \(24 \times 420\) px). Use only area and aspect ratios to show that none of the usual nine anchors (\(128^2\), \(256^2\), \(512^2\); ratios 1:1, 1:2, 2:1) can hit IoU \(0.7\) here, at any stride-16 position (the best you’ll find is 0.196 after exhaustive search). What happens during training without the third rule, and what would the detector do with poles like this at test time?
A threshold, in a deployment. Suppose you’re using a two-stage detector for a traffic survey with a fixed intersection camera, counting vehicles. You control two thresholds: score and NMS IoU. What starting values would you pick for each? For each, describe what happens if you move it up or down. On a day’s new footage, what would you measure to tune them? If you had to set one threshold “wrong,” which would you pick, and would your answer change if the system were tasked to count pedestrians instead?
Lab 4
Lab 4: pretrained Faster R-CNN inference; compute IoU/mAP on a small set; visualise proposals and NMS effects.
Further reading
- Everingham et al. [1] — the VOC challenge; section 4.2 defines the metric used throughout this chapter, and section 6.2.3 discusses the impact of raising the IoU threshold to 0.5.
- Lin et al. [3] — COCO; the dataset and new metric that replaced VOC’s, and the reason detection “scores” seem to halve after 2015.
- Uijlings et al. [6] — selective search; a look at the craftsmanship and complexity behind hand-designed proposal algorithms.
- Girshick et al. [5] — R-CNN; relevant for its box parameterisation (Appendix C), still foundational today.
- Girshick [7] — Fast R-CNN; see Section 1.1’s plain-spoken summary of R-CNN’s three core weaknesses.
- He et al. [8] — SPPnet; the pivotal intermediate step. Fast R-CNN’s main contribution is making end-to-end training possible.
- Ren et al. [4] — Faster R-CNN; Section 3.1’s explanation of anchors and the experiment table together justify the classic settings.
- Lin et al. [10] — FPN; Figure 1 lays out the four “scale” designs, while their ablation table shows the payoff for each design choice.
- He et al. [9] — Mask R-CNN; read now for RoIAlign, and revisit in Chapter 6 for the full treatment.
- Bodla et al. [2] — Soft-NMS; the case where greedy NMS fails is a subtle test of your algorithm intuition.