The architecture, in four parts
Mask R-CNN inherits almost all from Faster R-CNN and adds one branch.
- A backbone with a feature pyramid. Often ResNet-50 or -101 with FPN — making feature maps at strides 4, 8, 16 and 32.
- A region proposal network sliding over those maps, emitting a few thousand class-agnostic "a thing is here" boxes, cut to around 1,000 by NMS.
- RoIAlign, which crops a fixed-size feature patch for each proposal.
- Three heads on that patch: classification, box refinement, and the new one, a mask.
The heads are where the param budget sits, and the split is unintuitive:
| Head | Input | Structure | Output | Parameters |
|---|
| classification | 7×7×256 | fc 12544→1024, fc 1024→1024 | 81 scores | ~13.9 M |
| box | shares those two fc layers | | 4 × 80 offsets | ~0.33 M |
| mask | 14×14×256 | 4 × conv3×3(256), deconv, conv1×1 | 80 × 28 × 28 | ~2.64 M |
The mask head is the *cheapest* of the three despite making 62,720 numbers, because it's fully convolutional and the box head isn't.
The bug that mattered
RoIPool, inherited from Fast R-CNN, converts an any region of a feature map into a fixed 7×7 grid. It does so with two roundings:
- The proposal's floating-point coords are snapped to whole feature cells.
- The 7×7 bin boundaries inside that region are snapped to whole cells too.
Drag the proposal in the explorer and watch the dashed red box — what RoIPool really pools — jump around the solid orange one, which is what the RPN proposed. The statistic underneath translates the gap into input pixels: at stride 16, being half a feature cell out is 8 pixels in the first image. At stride 32 it's 16.
For classification this truly doesn't matter. The question "is this a cat" has the same answer whether your crop is eight pixels off, and the pooled features are a summary either way. For a mask it matters enormously, because the output is a per-pixel map that gets pasted back onto the image at that location. An eight-pixel systematic offset is visible in each single guess.
RoIAlign
RoIAlign drops both roundings. The region keeps its floating-point coords, the bins are cut just, and inside each bin four sample points are read by bilinear interpolation from the four nearest feature cells — then averaged.
The dots in the explorer are those sample points. Watch them slide smoothly as you drag, where the red box jumps in discrete steps.
The smoothness is the technical point, not just an aesthetic one. Bilinear interpolation is differentiable for the sampling location, so grads flow back to the box coords. Rounding is a step function whose deriv is zero all over it's defined. So RoIPool silently cut that path.
The two pooled maps are drawn side by side with their mean abs difference. Nudge the proposal width by a tenth of a cell: the RoIAlign map barely moves, the RoIPool map can change a lot. The paper reports that this change alone raises mask AP by around 10 points scaled on COCO, and by up to 50% scaled under the strict IoU=0.75 criterion — where being eight pixels out is just what decides a match.
Decoupling mask from class
The second design choice is easy to miss. The mask head predicts K separate masks, one per class, and the loss is only applied to the channel of the ground-truth class.
The option — one mask with a per-pixel softmax over classes — is what FCN-style semantic segmentation does, and it forces the classes to compete pixel by pixel. Mask R-CNN doesn't need that contest, because the classification head has already decided what the object is. Letting the mask branch answer "which pixels belong to *this* object" on its own of "what class is it" is worth a few points of AP. It's why the mask branch never learns to suppress one class in favour of another.
At inference you take the classification head's argmax and read only that channel's 28×28 mask, resize it to the predicted box, and threshold at 0.5. The resize is why instance masks from this family have a characteristically soft, slightly blobby boundary: the real detail of the answer is 28×28 inside the box, but large the box is.
Which pyramid level a proposal is read from
One detail sits between the RPN and RoIAlign and it's the reason the stride control on this page has three settings.
With a feature pyramid there are four pick maps to crop from, at strides 4, 8, 16 and 32, and a proposal has to be assigned to one. Cropping a small object from the stride-32 map would give a region a couple of cells across, which holds almost nothing. Cropping a large one from stride 4 wastes work and gives features with too little context.
FPN assigns by size: a proposal of area *A* goes to level k = floor(4 + log2(sqrt(A) / 224)), clamped to the available range. A 224×224 proposal — the ImageNet size, chosen on purpose — lands on level 4, at stride 16. any of it smaller drops to a finer level and any of it larger rises to a coarser one.
Switch the stride control in the explorer and watch the misalignment figure scale with it. The same half-cell rounding error is 2 pixels at stride 4 and 16 pixels at stride 32. This Is why the coarse levels are where RoIPool hurt most, and why the effect was largest on just the large objects that a finder or else finds easy.
The loss, and the multi-task balance
L = L_cls + L_box + L_mask
Three terms, equally weighted, plus the RPN's own two. L_mask is an mean binary cross-entropy over the 28×28 grid of the correct class's channel only. It's worth noticing that nothing here is tuned: the paper doesn't weight the mask loss up or down. This Is unusual for a multi-task network and suggests the three tasks are truly compatible rather than competing for room.
import torchvision
from torchvision.models.detection import maskrcnn_resnet50_fpn
model = maskrcnn_resnet50_fpn(weights="DEFAULT").eval()
out = model([image_tensor])[0]
keep = out["scores"] > 0.5
boxes = out["boxes"][keep] # [N, 4]
masks = out["masks"][keep] # [N, 1, H, W], already pasted, soft
labels = out["labels"][keep]
binary = masks[:, 0] > 0.5 # the 0.5 threshold, applied explicitly
Two things in that snippet catch people. masks comes back already resized and pasted to full image detail, and it's *soft* — chances, not booleans — so a threshold has to be applied, and 0.5 is a choice rather than a law. And the masks share: this is instance segmentation, so two instances can both claim a pixel, and resolving that into a single label per pixel is panoptic segmentation. This Is a different task with a different metric.
What it left behind
Mask R-CNN was state of the art in 2017 and is now the baseline that faster methods are measured against. YOLACT and SOLO give instance masks in one stage. Mask2Former and the DETR family replace the whole propose-and-crop structure with set guess and focus, and have no RoIAlign, no NMS and no anchors at all.
But RoIAlign itself outlived the architecture. Any time you need to read a feature map at a location that isn't on the grid — deformable convolutions, spatial transformers, keypoint heads, the 3-D finders that project points into image features. The answer is bilinear sampling for just the reasons here: it's accurate and it's differentiable in the coord. The lesson generalises past spotting: if a location is smooth, don't round it.
Questions people ask
Why did rounding hurt masks but not classification? Because the two answers have different shapes. "Is this a cat" lives a crop that is eight pixels off. The pooled features are a summary either way. A mask is a per-pixel map pasted back at that location. A systematic eight-pixel offset is visible in each single guess. Same error, very different consequence.
Why does the grad arg matter as much as the fit one? Bilinear interpolation is differentiable for the sampling location. Grads reach the box coords. Rounding is a step function whose deriv is zero wherever it's defined. So RoIPool silently severed that path. RoIAlign didn't only reduce an error, it restored a training signal.
Why are the masks 28×28? That is the mask head's output grid, resized to the predicted box at inference. So the real detail of the answer is 28×28 inside the box but large the box is. This Is just why instance masks from this family have soft, slightly blobby boundaries. And why later work raised the grid or refined boundaries apart, as PointRend does.
Why one mask per class instead of a per-pixel softmax? Because the classification head has already decided what the object is. The mask branch only has to answer "which pixels belong to this object". A per-pixel softmax makes the classes compete pixel by pixel. What FCN-style semantic segmentation does. And it costs a few points of AP here.
Why do my masks share? Because this is instance segmentation. Each instance gets its own free mask and two can both claim a pixel. Forcing one label per pixel is panoptic segmentation, a different task with a different metric. The returned masks are also soft chances. The 0.5 threshold is a choice rather than a law.
Which pyramid level does a proposal get cropped from? By area: k = floor(4 + log2(sqrt(A) / 224)), clamped to the available levels. A 224×224 proposal lands on stride 16. The ImageNet size, chosen on purpose. And smaller objects drop to finer levels. That is why the half-cell rounding error scaled from 2 pixels at stride 4 to 16 at stride 32. Why the damage was worst on the large objects a finder or else finds easy.
Recap in one screen
- Faster R-CNN plus one branch: an FPN backbone, RPN proposals, an RoIAlign crop, then classification, box and mask heads.
- The mask head is the cheapest of the three despite emitting 62,720 numbers. It's fully convolutional and the box head isn't.
- RoIPool rounded twice — the region to whole feature cells, then the bin boundaries. At stride 16 half a cell is 8 input pixels.
- RoIAlign keeps the floating-point coords and samples by bilinear interpolation. About 10 points of scaled mask AP. Up to 50% at the strict IoU=0.75.
- The masks are per-class, free, 28×28 inside the box, soft, and allowed to share.
- If a location is smooth, don't round it. The idea that outlived the architecture.