Image segmentation
Segmentation labels every single dot of a picture, so instead of a rectangle around an object you get its exact outline.
- 23 min read
- 3 reading levels
- Updated
Read these first
On this page 7
One lesson, three depths. Pick the one that fits you today — you can switch any time.
Beginner — No maths. Plain English.
Segmentation gives every dot in the picture its own label, so you get exact outlines instead of rectangles.
Think of cutting your face out of a photograph to stick on a birthday card. You did not cut a rectangle around your head. You followed the curve of your hair, around your ear, down along your chin.
A rectangle would have brought half the wall and somebody's elbow with it. The scissors followed the actual edge.
That is the whole difference between the last two lessons and this one.
What it adds to detection
Object detection draws a box. A box is fast, cheap and often enough.
But a box around a person standing with their arms out is mostly not the person. A box around a winding road is mostly not the road. When the shape matters, a rectangle is the wrong answer.
DETECTION SEGMENTATION
+-----------+ ####
| ### | ########
| ####### | ##########
| #########| ##########
+-----------+ #######
"a person, in here" "these exact dots are the person"What a mask actually is
The output of segmentation is called a mask. A mask is a second picture, the same height and width as the first. Each dot in it holds a class number instead of a brightness.
Here is a tiny one. Dots marked with a full stop are sky, commas are grass, and hashes are a ball.
............
............
............
....####....
....####....
,,,,####,,,,
,,,,,,,,,,,,
,,,,,,,,,,,,Every single dot got an answer. That is the defining property, and it is also why this task is expensive.
The three kinds, and why the difference matters
You will meet three words, and mixing them up will cost you a week of building the wrong thing.
Semantic segmentation labels every dot with what it is. Three cars parked in a row all come back as one blob marked "car". It cannot tell you there were three.
Instance segmentation keeps them separate. Car one, car two, car three, each with its own outline. It usually ignores things that have no clear separate identity, like sky or road.
Panoptic segmentation does both at once. Countable things get separate identities; background stuff gets a single label each.
semantic -> "car" "car" "car" one blob, one label
instance -> "car 1" "car 2" "car 3" three outlines
panoptic -> "car 1" "car 2" "car 3" + "road" "sky" "building"Ask yourself one question to choose: do I need to count them? If yes, you need instance or panoptic. If you only need to know where the road is, semantic is cheaper and simpler.
Where you have already seen it
- Background blur on a video call. Every dot is being labelled "person" or "not person", thirty times a second.
- Portrait mode on a phone. The blur follows your hair, which a rectangle never could.
- "Remove background" in a photo app. That is a mask, cut out and saved.
- Medical scans. Outlining a tumour so its volume can be measured across months.
- Satellite images. Mapping which land is water, forest, crop or building.
- Self-driving displays. The drivable road painted in one colour and pavements in another.
What is honestly hard about this
This is the most expensive task in the section. Learn the reasons before you promise anybody a delivery date.
Labelling is brutal. Detection asks a person to drag a rectangle. Segmentation asks them to trace an outline, by hand, for every object, in every image. The team behind the Cityscapes street dataset measured it. One carefully labelled image took over an hour and a half. Not the model, not the compute — one person, one image, ninety minutes.
Edges are where it goes wrong. The middle of a person is easy. Hair, fingers, bicycle spokes, chair legs. Anything thin or wispy is genuinely hard. It is also the first thing you notice when you look at the result.
The headline score hides the failures. Segmentation is scored with an average that treats every class equally. A model can look respectable while completely failing on the smallest and most important class. The Developer section shows this happening with real numbers, and it is the single most useful thing on this page.
Remember this
- Segmentation gives every dot a label. The result is a mask, the same size as the picture.
- Semantic says what. Instance also says which one. Panoptic does both.
- Labelling is the expensive part, by a very wide margin.
- The average score can look fine while a class you care about is at zero.
What to learn next
- Vision transformers — the backbone behind most current segmentation models.
- Object detection — boxes, IoU and NMS, which instance segmentation builds on.
- Convolutional neural networks — the encoder underneath the small models used here.
Developer — Code and libraries.
Three steps. Build a mask by hand, learn to score one honestly, then run a real model that fits in 12.5 MB.
Everything runs on a CPU. Nothing needs CUDA.
Setup
pip install torch torchvision matplotlibDownload sizes, stated up front, because they are not small:
| What | Size | When |
|---|---|---|
| PyTorch, CPU build | a few hundred MB | once, at install |
| LR-ASPP MobileNetV3 weights | 12.5 MB | first run, cached after |
| Mask R-CNN weights (optional block) | 170 MB | only if you run the last example |
matplotlib is here only because it ships a photograph inside the package, so no image is downloaded.
A mask, built by hand
import numpy as np
# A mask is an image where every pixel holds a class number instead of a brightness.
NAMES = {0: "sky", 1: "grass", 2: "ball"}
SYMBOL = {0: ".", 1: ",", 2: "#"}
mask = np.zeros((8, 12), dtype=np.uint8)
mask[5:, :] = 1 # the bottom three rows are grass
mask[3:6, 4:8] = 2 # a ball, overlapping the horizon
print("the mask as numbers:")
print(mask)
print()
print("the same mask, drawn:")
for row in mask:
print("".join(SYMBOL[int(v)] for v in row))
print()
print("shape:", mask.shape, " same height and width as the photo it describes")
for value, name in NAMES.items():
count = int((mask == value).sum())
print(f" {name:6s}: {count:3d} pixels ({100 * count / mask.size:.1f}%)")the mask as numbers: [[0 0 0 0 0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0 0 0 0 0] [0 0 0 0 2 2 2 2 0 0 0 0] [0 0 0 0 2 2 2 2 0 0 0 0] [1 1 1 1 2 2 2 2 1 1 1 1] [1 1 1 1 1 1 1 1 1 1 1 1] [1 1 1 1 1 1 1 1 1 1 1 1]] the same mask, drawn: ............ ............ ............ ....####.... ....####.... ,,,,####,,,, ,,,,,,,,,,,, ,,,,,,,,,,,, shape: (8, 12) same height and width as the photo it describes sky : 52 pixels (54.2%) grass : 32 pixels (33.3%) ball : 12 pixels (12.5%)
A mask is an image. It has a height and a width and it lines up dot for dot with the photograph. The only difference is what the numbers mean.
Note the dtype=np.uint8. Class numbers are small integers, and every segmentation library stores masks this way. Never store a mask as a float, and never resize one with a smoothing method. Averaging class 1 and class 3 gives class 2, which is a different object entirely. Resize masks with nearest-neighbour, always.
Scoring a mask honestly
This block is the reason this lesson exists. Take a model that learned sky and grass perfectly and never learned the ball at all.
import numpy as np
NAMES = {0: "sky", 1: "grass", 2: "ball"}
truth = np.zeros((8, 12), dtype=np.uint8)
truth[5:, :] = 1
truth[3:6, 4:8] = 2
# A model that learned sky and grass perfectly and never learned "ball".
pred = np.zeros((8, 12), dtype=np.uint8)
pred[5:, :] = 1
print("pixel accuracy:", f"{(pred == truth).mean():.3f}")
print()
ious = []
for value, name in NAMES.items():
intersection = int(((pred == value) & (truth == value)).sum())
union = int(((pred == value) | (truth == value)).sum())
iou = intersection / union if union else float("nan")
ious.append(iou)
print(f" {name:6s} IoU {iou:.3f} (shared {intersection:3d} of {union:3d} pixels)")
print()
print("mean IoU:", f"{np.mean(ious):.3f}")pixel accuracy: 0.875 sky IoU 0.867 (shared 52 of 60 pixels) grass IoU 0.889 (shared 32 of 36 pixels) ball IoU 0.000 (shared 0 of 12 pixels) mean IoU: 0.585
Look at the first line and the last line together.
Pixel accuracy says 0.875. Nearly seven-eighths of the dots were labelled correctly. On a dashboard that reads as a working model.
The ball IoU is exactly zero. The model has never once found the object the whole system exists to find.
This is not a contrived example. It happens in every real segmentation problem. The thing you care about is nearly always the smallest region in the frame: the tumour, the crack, the defect, the pothole. Background dominates the pixel count, so background dominates pixel accuracy.
Mean IoU is the standard metric precisely because it averages per class rather than per pixel. Each class contributes equally regardless of size, so a class at zero drags the average down hard. Report mean IoU, and then report per-class IoU underneath it, every time.
The union guard matters too. If a class appears in neither the truth nor the prediction, the union is zero and the IoU is undefined. Averaging a class that is not present as if it scored zero quietly ruins your number.
A real model, 12.5 MB
import torch
import matplotlib.cbook as cbook
from torchvision.io import decode_image
from torchvision.models.segmentation import (
lraspp_mobilenet_v3_large,
LRASPP_MobileNet_V3_Large_Weights,
)
image = decode_image(cbook.get_sample_data("grace_hopper.jpg").name)
weights = LRASPP_MobileNet_V3_Large_Weights.DEFAULT
print("weights file:", weights.meta["_file_size"], "MB")
print("classes :", len(weights.meta["categories"]))
model = lraspp_mobilenet_v3_large(weights=weights).eval()
with torch.no_grad():
scores = model(weights.transforms()(image).unsqueeze(0))["out"][0]
print("input :", tuple(image.shape))
print("output :", tuple(scores.shape), "<- one score per class, per pixel")
mask = scores.argmax(0) # pick the winning class at every pixel
names = weights.meta["categories"]
print()
total = mask.numel()
for value in mask.unique():
count = int((mask == value).sum())
print(f" {names[value]:14s} {count:7d} px {100 * count / total:5.1f}%")
print()
print("the person mask, shrunk to fit a terminal:")
person = (mask == names.index("person")).float().unsqueeze(0).unsqueeze(0)
small = torch.nn.functional.interpolate(person, size=(24, 40), mode="area")[0, 0]
for row in small:
print("".join("#" if v > 0.5 else "." for v in row))weights file: 12.49 MB classes : 21 input : (3, 600, 512) output : (21, 609, 520) <- one score per class, per pixel __background__ 130931 px 41.3% person 185749 px 58.7% the person mask, shrunk to fit a terminal: ........................................ ................########................ ..............#############............. .............###############............ ............#################........... .............################........... ............#################........... ............#################........... ............#################........... ............################............ #...........################............ #.............#############............. #..............###########.............. #..........##################........... #...##############################...... ##.###################################.. #######################################. ######################################## ######################################## ######################################## ######################################## ######################################## ######################################## .........##.#..##.#.....................
That is a head, two shoulders, and a torso filling the bottom of the frame. Drawn in hash characters, from a 12.5 MB model, on a laptop processor, in a fraction of a second.
Read the output carefully, twice
Segmentation is classification, run once per pixel. The output shape is (21, 609, 520): twenty-one class scores at every one of 316,680 positions. argmax(0) picks the winner at each. If you understood image classification, you already understand this. It is the same softmax over classes, repeated across a grid.
The output is not the size of the input. In went 600 x 512. Out came 609 x 520. The transform resized the short side to 520 pixels, and the mask comes back in that space. Before you overlay a mask on the original photograph, resize it back with mode="nearest". Skipping this produces a mask that is subtly, maddeningly misaligned.
Look at the left edge. Several rows carry a lone # in column zero, well away from her shoulder. Crop the original photograph there and you find the American flag hanging behind her. A strip of that flag has been labelled "person", all the way down the frame. Nothing in the class percentages would have told you. Only looking did.
Twenty-one classes is all it knows. This checkpoint was trained on the twenty Pascal VOC categories plus background. It has no label for "flag", so every pixel of that flag has to go somewhere. The closed-set problem from image classification applies here at every single pixel.
Choosing a model, with honest numbers
These come straight out of torchvision's own metadata, measured on COCO with VOC labels.
| Model | Weights | Parameters | mean IoU |
|---|---|---|---|
| LR-ASPP MobileNetV3 | 12.5 MB | 3.2M | 57.9 |
| DeepLabV3 MobileNetV3 | 42.3 MB | 11.0M | 60.3 |
| FCN ResNet-50 | 135 MB | 35.3M | 60.5 |
| DeepLabV3 ResNet-50 | 160.5 MB | 42.0M | 66.4 |
Read the first and last rows against each other. Thirteen times the parameters and thirteen times the download buys about eight and a half points of mean IoU. Whether that trade is worth it is a question about your product, not about machine learning.
Instance segmentation, if you have the bandwidth
This one downloads 170 MB. Skip it on mobile data. Everything above already works without it.
import torch
import matplotlib.cbook as cbook
from torchvision.io import decode_image
from torchvision.models.detection import (
maskrcnn_resnet50_fpn,
MaskRCNN_ResNet50_FPN_Weights,
)
image = decode_image(cbook.get_sample_data("grace_hopper.jpg").name)
weights = MaskRCNN_ResNet50_FPN_Weights.DEFAULT
print("weights file:", weights.meta["_file_size"], "MB")
model = maskrcnn_resnet50_fpn(weights=weights).eval()
with torch.no_grad():
result = model([weights.transforms()(image)])[0]
names = weights.meta["categories"]
keep = result["scores"] > 0.5
print("instances above 0.5:", int(keep.sum()))
for label, score, mask in zip(result["labels"][keep], result["scores"][keep],
result["masks"][keep]):
pixels = int((mask[0] > 0.5).sum())
print(f" {names[label]:8s} {score:.2f} {pixels:6d} pixels in its mask")weights file: 169.84 MB instances above 0.5: 3 person 1.00 168769 pixels in its mask tie 0.96 3536 pixels in its mask bird 0.70 815 pixels in its mask
Person, correct. Tie, correct — she is wearing one. Bird, at 0.70 confidence.
There is no bird. Crop the region the model outlined and you find a white star on the American flag behind her. Five points, pale against dark blue, roughly the size and shape of a small bird with its wings out.
Sit with that for a moment. A 44 million parameter model, 170 MB of weights, published metrics in good standing. And it is 70 percent sure that a flag star is a bird. It is not broken. It is doing exactly what it was trained to do on data that never contained this situation.
This is why one habit repeats in every lesson of this section: look at the pictures your model got wrong. No number on this page would have told you about the bird.
Notice also what instance segmentation gives you that the semantic model did not. Each object arrives as its own mask with its own score, so counting is possible. result["masks"] has shape (N, 1, H, W) — one channel per instance, holding soft values you threshold yourself, usually at 0.5.
What about Segment Anything?
SAM (Meta, 2023) segments objects from a click or a box, without being told what they are. It is genuinely impressive.
It is also not a laptop tool. The smallest checkpoint is around 375 MB and the largest is roughly 2.4 GB. The image encoder is a large vision transformer, and on a CPU one image takes tens of seconds and can reach minutes. On a GPU the encoder runs once per image and each later click is close to instant. That is what the demos show you.
With no GPU, use it through a hosted demo to see what it does. Then build your pipeline on something like the 12.5 MB model above. Distilled variants such as MobileSAM and FastSAM exist for this reason. Look at those before concluding it is out of reach.
Common mistakes
Resizing a mask with interpolation. Averaging class 1 and class 3 gives class 2. Always mode="nearest" for masks, and any smoothing method for images.
Forgetting to map the mask back to the original size. The model works at its own resolution. Overlay without resizing back and everything is a few pixels off, in a way that looks like a bad model.
Reporting pixel accuracy. Demonstrated above. It is dominated by background and it will flatter a model that has failed completely at the task.
Averaging IoU over classes that are absent. A class with an empty union has an undefined IoU, not a zero one. Exclude it or your mean is meaningless.
Ignoring class imbalance in the loss. The target region is usually a tiny fraction of the pixels, so plain cross-entropy is dominated by background. Dice loss, or cross-entropy plus Dice, is the standard fix in medical imaging and works well anywhere the target is small.
Trusting a mask you have never looked at. Save an overlay. Open it. Every time.
Try it yourself
In score_a_mask.py, make the model find the ball, but place it one row too low. The shapes then overlap by two rows out of three. Recompute. Pixel accuracy barely moves; ball IoU jumps from 0.000 to something real. You have measured the difference between "found it" and "found it exactly", which is the difference segmentation exists to capture.
Then run segment.py on a photograph of your own — a street, a room, a person, anything. Print the class percentages, then look at the picture and name three things the model has no label for.
Every one of those things still got labelled as something. Finding out what is the most useful hour you will spend on this topic.
What to learn next
- Vision transformers — the backbone behind most current segmentation models.
- Object detection — boxes, IoU and NMS, which instance segmentation builds on.
- Convolutional neural networks — the encoder underneath the small models used here.
Researcher — Mathematics and papers.
The task
Semantic segmentation is dense classification:
f_theta : R^(H x W x 3) -> R^(H x W x K), prediction y_hat(i,j) = argmax_k f_theta(x)_{i,j,k}H,W— spatial dimensions.K— number of classes, including a background or void class.
Instance segmentation additionally partitions pixels into object identities, and panoptic segmentation (Kirillov et al., 2019) unifies both by assigning every pixel a (class, instance_id) pair, with instance_id ignored for "stuff" classes.
The structural tension in every architecture here is resolution against context. Classification backbones downsample aggressively — a stride of 32 is standard — because context requires a large receptive field. Dense prediction needs the output back at full resolution. Every design below is a different answer to that.
Architectural lineage
FCN (Long, Shelhamer and Darrell, 2015) replaced the classifier's fully connected layers with convolutions and upsampled with learned transposed convolutions, fusing skip connections from earlier, higher-resolution stages. This is the paper that created the field.
U-Net (Ronneberger, Fischer and Brox, 2015) made the encoder-decoder symmetric, concatenating full-resolution encoder features into the decoder at every scale. It was designed for biomedical images with a few dozen training examples, and it remains the default in medical imaging a decade later. Its combination of heavy augmentation and skip connections is unusually data-efficient.
DeepLab (Chen et al., 2017, 2018) attacked resolution loss with atrous — dilated — convolution:
y(i) = sum over k of x(i + r * k) * w(k)r— dilation rate.r = 1is a standard convolution.
Dilation enlarges the receptive field without downsampling and without adding parameters. Atrous Spatial Pyramid Pooling runs several rates in parallel and concatenates, capturing objects at multiple scales in one layer. DeepLabv3+ adds a light decoder for sharper boundaries.
PSPNet (Zhao et al., 2017) uses pyramid pooling over four grid scales for global context.
Mask R-CNN (He et al., 2017) extends Faster R-CNN with a per-RoI mask branch. Its key technical contribution is RoIAlign: RoIPool quantised region boundaries to the feature grid, misaligning masks by several pixels; RoIAlign uses bilinear sampling at exact locations instead. That single change contributes most of the mask AP gain reported in the paper.
Transformer-based unification. MaskFormer and Mask2Former (Cheng et al., 2021, 2022) recast all three tasks as mask classification. Predict a set of binary masks, each carrying one class label, using a transformer decoder with learned queries. One architecture reaches state of the art on semantic, instance and panoptic benchmarks. This is now the dominant framing.
SAM (Kirillov et al., 2023) is promptable and class-agnostic, trained on SA-1B — 1.1 billion masks over 11 million images. The design decision that makes it usable is asymmetric cost: a heavy ViT image encoder runs once per image, and a lightweight decoder answers each prompt in milliseconds. That asymmetry is also why it is impractical on CPU, where the encoder is the bottleneck.
Losses
Cross-entropy, averaged over pixels, is the baseline and is dominated by the majority class.
Dice loss optimises overlap directly:
L_dice = 1 - (2 * sum_i (p_i * g_i) + eps) / (sum_i p_i + sum_i g_i + eps)p_i— predicted probability for the foreground at pixeli.g_i— ground-truth indicator, 0 or 1.eps— smoothing constant, preventing division by zero on empty masks.
Dice is scale-invariant with respect to target size, which is exactly what a small-lesion problem needs. It is also noisier to optimise than cross-entropy, which is why the standard practice in medical segmentation is a weighted sum of both.
Lovász-Softmax (Berman et al., 2018) is a convex surrogate for IoU itself, derived from the Lovász extension of a submodular set function. It optimises the evaluation metric more directly than Dice.
Boundary-aware losses — boundary loss, Hausdorff-distance surrogates — target the failure mode that region-overlap losses are least sensitive to. Worth reaching for when the application cares about the edge rather than the area.
Metrics, and what they conceal
IoU_k = TP_k / (TP_k + FP_k + FN_k)
mIoU = (1 / K) * sum over k of IoU_kTwo properties are frequently misunderstood.
mIoU is unweighted by class frequency. That is deliberate and it is the reason to prefer it to pixel accuracy. It also means one rare class scoring badly moves the headline number as much as one dominant class, which cuts both ways.
Classes absent from both prediction and ground truth have an undefined IoU. Different codebases handle this differently — some average over present classes only, some substitute zero, some substitute one. This alone produces published numbers that are not comparable. Check the evaluation code, not the paper's table.
For panoptic segmentation, PQ (Kirillov et al., 2019) decomposes usefully:
PQ = ( sum over matched (p,g) of IoU(p,g) ) / ( |TP| + 0.5|FP| + 0.5|FN| )
= SQ * RQSQ— segmentation quality, the mean IoU over matched segments.RQ— recognition quality, the F1 score over segment matching.
The factorisation is the useful part: it separates "did you find the right objects" from "did you outline them well".
Annotation cost, quantified
This dominates project planning and is under-discussed.
Cordts et al. (2016) reported that Cityscapes fine annotation and quality control required more than 1.5 hours per image on average, for 5,000 images. Coarse polygon annotation for a further 20,000 images was budgeted at roughly 7 minutes each and is measurably lower quality.
The standard responses, in rough order of return on effort:
- Weak supervision from image-level labels, scribbles, points or boxes. Box-supervised instance segmentation (BoxInst, DiscoBox) reaches a large fraction of full-supervision quality.
- Model-assisted annotation. Predict, then have a human correct. SAM is extremely effective here, and this is currently its most common production use.
- Synthetic data. GTA5 and SYNTHIA for driving; domain adaptation closes part of the resulting gap.
- Self-supervised pre-training. DINOv2 features support strong linear segmentation heads with far fewer labels.
Cost
For an encoder-decoder at input H x W, the decoder operates at high resolution and its cost is often comparable to the encoder's, even with far fewer parameters. Dilated backbones at output stride 8 rather than 32 raise activation memory roughly sixteen-fold at the deepest stage, which is why DeepLab implementations trade output stride against batch size.
At inference, memory scales as O(H * W * K) for the logits alone. At 1024 x 2048 with 19 Cityscapes classes in float32, the logit tensor is about 159 MB per image before anything else. Compute the argmax in chunks, or in float16, when this matters.
Papers
- Long, J., Shelhamer, E. & Darrell, T. (2015). Fully Convolutional Networks for Semantic Segmentation. arXiv:1411.4038
- Ronneberger, O., Fischer, P. & Brox, T. (2015). U-Net. arXiv:1505.04597
- Cordts, M. et al. (2016). The Cityscapes Dataset for Semantic Urban Scene Understanding. arXiv:1604.01685
- He, K. et al. (2017). Mask R-CNN. arXiv:1703.06870
- Zhao, H. et al. (2017). Pyramid Scene Parsing Network. arXiv:1612.01105
- Chen, L.-C. et al. (2017). Rethinking Atrous Convolution for Semantic Image Segmentation. arXiv:1706.05587
- Chen, L.-C. et al. (2018). Encoder-Decoder with Atrous Separable Convolution (DeepLabv3+). arXiv:1802.02611
- Berman, M. et al. (2018). The Lovász-Softmax Loss. arXiv:1705.08790
- Kirillov, A. et al. (2019). Panoptic Segmentation. arXiv:1801.00868
- Howard, A. et al. (2019). Searching for MobileNetV3. arXiv:1905.02244 — the LR-ASPP head used above.
- Cheng, B. et al. (2022). Masked-attention Mask Transformer for Universal Image Segmentation (Mask2Former). arXiv:2112.01527
- Kirillov, A. et al. (2023). Segment Anything. arXiv:2304.02643
- Isensee, F. et al. (2021). nnU-Net: a self-configuring method for deep learning-based biomedical image segmentation. Nature Methods 18.
Open problems
Boundaries. Region-overlap metrics are insensitive to a few pixels of edge error, and human observers are extremely sensitive to it. Boundary-F and trimap evaluation exist and are rarely reported.
Thin and transparent structures. Hair, wires, mesh, glass. Matting — predicting a continuous alpha rather than a hard label — is the right formulation and is a substantially harder learning problem.
Domain shift. A segmentation model trained on one city, one scanner or one camera degrades sharply on another. Test-time adaptation and domain generalisation both help and neither closes the gap.
Open-vocabulary dense prediction. SAM segments anything and names nothing; CLIP names things and localises poorly. Combining them well — open-vocabulary panoptic segmentation — is an active area, and current systems trail closed-set specialists by a wide margin on any fixed taxonomy.
Evaluation under label ambiguity. For medical images, inter-rater IoU between expert annotators is frequently in the 0.7 to 0.9 range. A model reported at 0.85 may be within human disagreement, and a single ground-truth mask cannot express that. nnU-Net's authors make this point repeatedly and it is still routinely ignored.
What to learn next
- Vision transformers — the backbone behind most current segmentation models.
- Object detection — boxes, IoU and NMS, which instance segmentation builds on.
- Convolutional neural networks — the encoder underneath the small models used here.