Skip to content

Medical AI · Deep learning & imaging · Lesson 3 of 5

Image segmentation with U-Net

Reviewed by Dr. Ram Kumar G, MBBS, MD (Internal Medicine), MPH (USA), on · Last updated

  • Advanced
  • Technical focus
  • About 35 minutes
  • Includes exercise

Before this lesson: CNNs for chest X-ray and retinal images

In short

Segmentation assigns a label to every pixel, outlining a lesion, organ or wound rather than just saying whether it is present. The U-Net architecture does this with an encoder that captures context, a decoder that restores detail, and skip connections that carry fine detail across. Results are usually scored with the Dice coefficient, which measures overlap between the predicted and true outlines. Report the spread of scores, not only the average, because a model with a good mean Dice can still fail badly on some images.

You will learn to

  • Distinguish classification, detection and segmentation
  • Explain the encoder, decoder and skip connections of a U-Net
  • Train a small U-Net in PyTorch and score it with the Dice coefficient
  • Choose segmentation metrics and report them responsibly

Classification, detection and segmentation

Segmentation supports measurement: wound area, tumour volume, organ size, plaque burden. It is also a step in many larger systems, such as finding the retina's layers before grading disease.

TaskOutputClinical example
ClassificationOne label per imageReferable diabetic retinopathy: yes or no
DetectionBoxes around objectsLocating lung nodules on CT
SegmentationA label for every pixelOutlining a foot ulcer to measure its area over time

The U-Net idea

Ronneberger and colleagues introduced U-Net in 2015 for biomedical images. nnU-Net, which configures U-Nets automatically for each dataset, became a strong baseline across many segmentation challenges.

  • Encoder: convolution and pooling layers shrink the image, capturing context ('this is the edge of a lesion').
  • Decoder: upsampling layers restore full resolution so every pixel gets a prediction.
  • Skip connections: feature maps from the encoder are passed directly to the decoder at the same resolution, so fine boundaries are not lost.

A tiny U-Net in PyTorch

The synthetic 32 × 32 scans below contain one or two irregular 'lesions' with known pixel masks. The U-Net has one downsampling step and one skip connection, the smallest version of the idea.

python
import numpy as np
import torch
from torch import nn

def make_scans(n, seed, size=32):
    """Synthetic scans with one or two irregular 'lesions', and the true pixel mask for each."""
    rng = np.random.default_rng(seed)
    yy, xx = np.mgrid[:size, :size]
    images, masks = rng.normal(0, 1, (n, size, size)), np.zeros((n, size, size))
    for i in range(n):
        for _ in range(rng.integers(1, 3)):
            cy, cx = rng.integers(6, size - 6, 2)
            ry, rx = rng.uniform(2, 5, 2)
            masks[i] = np.maximum(masks[i], ((yy - cy) / ry) ** 2 + ((xx - cx) / rx) ** 2 < 1)
        images[i] += 1.5 * masks[i]
    to_t = lambda a: torch.tensor(a, dtype=torch.float32).unsqueeze(1)
    return to_t(images), to_t(masks)

def block(c_in, c_out):
    return nn.Sequential(nn.Conv2d(c_in, c_out, 3, padding=1), nn.ReLU(),
                         nn.Conv2d(c_out, c_out, 3, padding=1), nn.ReLU())

class TinyUNet(nn.Module):
    """Encoder-decoder with a skip connection: the U-Net idea at the smallest useful size."""
    def __init__(self):
        super().__init__()
        self.enc1, self.enc2 = block(1, 8), block(8, 16)
        self.pool, self.up = nn.MaxPool2d(2), nn.ConvTranspose2d(16, 8, 2, stride=2)
        self.dec1 = block(16, 8)                       # 8 upsampled + 8 from the skip connection
        self.out = nn.Conv2d(8, 1, 1)
    def forward(self, x):
        e1 = self.enc1(x)                              # full resolution: detail
        e2 = self.enc2(self.pool(e1))                  # half resolution: context
        d1 = self.dec1(torch.cat([self.up(e2), e1], dim=1))
        return self.out(d1)                            # one logit per pixel

def dice(pred_mask, true_mask, eps=1e-6):
    inter = (pred_mask * true_mask).sum(dim=(1, 2, 3))
    return ((2 * inter + eps) / (pred_mask.sum(dim=(1, 2, 3)) + true_mask.sum(dim=(1, 2, 3)) + eps))

X_train, M_train = make_scans(1500, seed=1)
X_test, M_test = make_scans(300, seed=2)
torch.manual_seed(0)
unet = TinyUNet()
opt = torch.optim.Adam(unet.parameters(), lr=2e-3)
for epoch in range(1, 9):
    for idx in torch.randperm(len(X_train)).split(32):
        opt.zero_grad()
        nn.functional.binary_cross_entropy_with_logits(unet(X_train[idx]), M_train[idx]).backward()
        opt.step()
    if epoch in (1, 4, 8):
        with torch.no_grad():
            d = dice((unet(X_test) > 0).float(), M_test)
        print(f"epoch {epoch}: mean Dice on test scans {d.mean():.3f}")

# epoch 1: mean Dice on test scans 0.000
# epoch 4: mean Dice on test scans 0.864
# epoch 8: mean Dice on test scans 0.891
Synthetic scans with masks, a tiny U-Net, the Dice score and training. About ten seconds on a CPU.
  • After one epoch the Dice is 0: the network predicts 'no lesion' everywhere. Because lesion pixels are a small fraction of each image, that is a cheap way to lower the per-pixel loss early on. Losses based on Dice, or weighting lesion pixels more, help with this imbalance.
  • By epoch 8 the mean Dice is 0.891.

About the numbers

  • The code uses fixed random seeds, PyTorch 2.8 and a laptop CPU. Other versions or hardware can give slightly different numbers; the pattern of results is what matters.
  • Images in this track are synthetic, so they run anywhere in seconds. The exercises point to MedMNIST, an openly licensed collection of small medical-image datasets, for real images.

Comparing with a baseline, and looking at the worst cases

python
with torch.no_grad():
    d = dice((unet(X_test) > 0).float(), M_test)
    threshold_only = dice((X_test > 0.75).float(), M_test)       # naive: bright pixels = lesion
print(f"Simple brightness threshold: mean Dice {threshold_only.mean():.3f}")
print(f"U-Net: mean Dice {d.mean():.3f}; worst 5% of scans below {torch.quantile(d, 0.05):.3f}")

# Simple brightness threshold: mean Dice 0.270
# U-Net: mean Dice 0.891; worst 5% of scans below 0.769
Continuing from the previous block: a brightness-threshold baseline and the worst 5% of U-Net scores.
  • A brightness threshold, which marks noise as lesion, scores 0.270; the U-Net uses shape and context to reach 0.891.
  • One scan in twenty scores below 0.769. In real use those worst cases, often small or unusual lesions, are where errors matter most. Report the distribution of scores and inspect the failures.

Choosing and reporting segmentation metrics

The 'Metrics Reloaded' recommendations (Maier-Hein et al., 2024) guide the choice of metric by the clinical question. Also report agreement between human annotators: if experts disagree on boundaries, a model cannot be expected to agree with any one of them perfectly.

MetricMeasuresCaution
Dice coefficientOverlap between predicted and true regions (0 to 1)Unstable for very small structures; one missed pixel matters a lot
Intersection over union (IoU)Overlap divided by combined areaClosely related to Dice; do not report both as independent evidence
Hausdorff distance (95th percentile)How far apart the boundaries areSensitive to outliers; the 95th percentile version is more robust
Volume or area errorDifference in measured sizeDirectly relevant when the output is used for measurement
Per-lesion detectionWhether each lesion was found at allMean Dice can hide missed small lesions

Exercise

Train and evaluate a U-Net, improve it for small lesions, and report results responsibly.

Data: Synthetic scans and masks in the notebook; optional open segmentation dataset under its licence (Generated in the notebook)

  1. Reproduce the tiny U-Net and plot predicted masks for the five worst-scoring test scans.
  2. Replace the loss with a combination of cross-entropy and Dice loss and compare early epochs and final Dice.
  3. Make some lesions much smaller and report per-lesion detection rate alongside mean Dice.
  4. Add a second downsampling level and compare results and training time.
  5. Write a results paragraph that reports the mean, the spread and the failure cases.

Open the notebook in ColabView on GitHubWorked solution coming soon

Key takeaways

  • Segmentation labels every pixel, enabling measurement of lesions and organs.
  • U-Net combines an encoder, a decoder and skip connections to keep fine detail.
  • Dice measures overlap; small lesions and imbalance need special care.
  • Report score distributions and failure cases, not only the mean.

Terms used: Convolutional neural network (CNN), Dice coefficient, Image segmentation

Related: CNNs for chest X-ray and retinal images, Diabetic foot ulcers, Applied case studies: foot-ulcer image classification

FAQ

What is image segmentation?

Assigning a label to every pixel of an image, for example marking which pixels belong to a lesion, so its outline, area or volume can be measured.

What is a U-Net?

A neural network for segmentation with an encoder that captures context, a decoder that restores resolution, and skip connections that pass fine detail from encoder to decoder.

What is a good Dice score?

It depends on the structure and task. Large organs often reach above 0.9, while small or poorly defined lesions score lower. Compare with agreement between human experts on the same images.

Why did the model predict nothing after the first epoch?

Lesion pixels are a small fraction of each image, so predicting background everywhere quickly reduces the loss. More training, Dice-based losses or weighting lesion pixels overcomes this.

Can segmentation measure diabetic foot ulcers?

Research systems segment wounds in photographs to track area over time. Lighting, camera angle and skin tone affect accuracy, so such tools need validation in the populations where they will be used.

Educational content only. This material teaches methods; it is not clinical advice. Code, models and examples here are not validated medical devices and must not be used to make decisions about real patients without proper clinical validation, governance and any required regulatory approval. Examples use synthetic or openly licensed de-identified data only. Never paste identifiable patient information into public AI tools. Mentions of models, tools or vendors are for identification, not endorsement.