Post

Image Segmentation

Image Segmentation

Project

Table of contents


Overview

This project demonstrates how to implement an image segmentation model using the U-Net architecture. The goal is to segment medical images, such as brain MRI scans, to identify regions of interest (e.g., lesions or abnormalities). The workflow includes data loading, preprocessing (including augmentation and normalization), model definition, training, evaluation, and visualization.


Details of Dataset

  • Dataset Used: Brain MRI dataset for FLAIR abnormality segmentation
  • Data Format: 2D image slices (often .tif files) and corresponding masks
  • Channels: 3 (input images)
  • Segmentation Classes: 1 (binary mask: abnormality vs. background)

Data Preparation

Preprocessing Steps:

  • Cropping to the smallest enclosing volume
  • Padding to square shape
  • Resizing to a fixed size (e.g., 256x256)
  • Normalization (channel-wise mean/std)

Augmentation:

  • Random scaling, rotation, and horizontal flipping are applied to increase data diversity and improve model robustness.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from torchvision.transforms import Compose
from skimage.transform import rescale, rotate
import numpy as np

# Example augmentation pipeline
def transforms(scale=None, angle=None, flip_prob=None):
    transform_list = []
    if scale is not None:
        transform_list.append(Scale(scale))
    if angle is not None:
        transform_list.append(Rotate(angle))
    if flip_prob is not None:
        transform_list.append(HorizontalFlip(flip_prob))
    return Compose(transform_list)

Architecture of Network

U-Net Architecture:

  • Encoder-decoder structure with skip connections
  • Each block consists of two convolutional layers, batch normalization, and ReLU activations
  • Downsampling via max pooling; upsampling via transposed convolutions
  • Final layer uses a 1x1 convolution and sigmoid activation for binary segmentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import torch
import torch.nn as nn
from collections import OrderedDict

class UNet(nn.Module):
    def __init__(self, in_channels=3, out_channels=1, init_features=32):
        super(UNet, self).__init__()
        features = init_features
        self.encoder1 = UNet._block(in_channels, features, name="enc1")
        self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)
        self.encoder2 = UNet._block(features, features * 2, name="enc2")
        self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)
        self.encoder3 = UNet._block(features * 2, features * 4, name="enc3")
        self.pool3 = nn.MaxPool2d(kernel_size=2, stride=2)
        self.encoder4 = UNet._block(features * 4, features * 8, name="enc4")
        self.pool4 = nn.MaxPool2d(kernel_size=2, stride=2)
        self.bottleneck = UNet._block(features * 8, features * 16, name="bottleneck")
        self.upconv4 = nn.ConvTranspose2d(features * 16, features * 8, kernel_size=2, stride=2)
        self.decoder4 = UNet._block((features * 8) * 2, features * 8, name="dec4")
        self.upconv3 = nn.ConvTranspose2d(features * 8, features * 4, kernel_size=2, stride=2)
        self.decoder3 = UNet._block((features * 4) * 2, features * 4, name="dec3")
        self.upconv2 = nn.ConvTranspose2d(features * 4, features * 2, kernel_size=2, stride=2)
        self.decoder2 = UNet._block((features * 2) * 2, features * 2, name="dec2")
        self.upconv1 = nn.ConvTranspose2d(features * 2, features, kernel_size=2, stride=2)
        self.decoder1 = UNet._block(features * 2, features, name="dec1")
        self.conv = nn.Conv2d(in_channels=features, out_channels=out_channels, kernel_size=1)
    def forward(self, x):
        enc1 = self.encoder1(x)
        enc2 = self.encoder2(self.pool1(enc1))
        enc3 = self.encoder3(self.pool2(enc2))
        enc4 = self.encoder4(self.pool3(enc3))
        bottleneck = self.bottleneck(self.pool4(enc4))
        dec4 = self.upconv4(bottleneck)
        dec4 = torch.cat((dec4, enc4), dim=1)
        dec4 = self.decoder4(dec4)
        dec3 = self.upconv3(dec4)
        dec3 = torch.cat((dec3, enc3), dim=1)
        dec3 = self.decoder3(dec3)
        dec2 = self.upconv2(dec3)
        dec2 = torch.cat((dec2, enc2), dim=1)
        dec2 = self.decoder2(dec2)
        dec1 = self.upconv1(dec2)
        dec1 = torch.cat((dec1, enc1), dim=1)
        dec1 = self.decoder1(dec1)
        return torch.sigmoid(self.conv(dec1))
    @staticmethod
    def _block(in_channels, features, name):
        return nn.Sequential(OrderedDict([
            (name + "conv1", nn.Conv2d(in_channels, features, 3, padding=1, bias=False)),
            (name + "norm1", nn.BatchNorm2d(features)),
            (name + "relu1", nn.ReLU(inplace=True)),
            (name + "conv2", nn.Conv2d(features, features, 3, padding=1, bias=False)),
            (name + "norm2", nn.BatchNorm2d(features)),
            (name + "relu2", nn.ReLU(inplace=True)),
        ]))

Training and Test

Loss function
Binary Cross Entropy Loss (nn.BCELoss) is used for binary segmentation tasks.

Optimizer
Adam optimizer (optim.Adam) is used for its adaptive learning rate and efficient convergence.

Settings

  • Batch size: 2
  • Learning rate: 0.001
  • Epochs: (set as needed, e.g., 20)
  • Device: Uses GPU (CUDA) if available, otherwise CPU
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import torch.optim as optim

model = UNet()
optimizer = optim.Adam(model.parameters(), lr=0.001)
criterion = nn.BCELoss()

for epoch in range(20):
    model.train()
    for images, masks in train_loader:
        images, masks = images.to(device), masks.to(device)
        outputs = model(images)
        loss = criterion(outputs, masks)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

Evaluation

  • Dice Similarity Coefficient (DSC) is used to evaluate segmentation performance.
  • Visual inspection of predicted masks is also common.
1
2
3
4
def dsc(y_pred, y_true):
    y_pred = (y_pred > 0.5).astype(int)
    y_true = y_true.astype(int)
    return np.sum(y_pred[y_true == 1]) * 2.0 / (np.sum(y_pred) + np.sum(y_true))

Additional Sections

Saving the Model:
The trained model is saved using:

1
torch.save(model.state_dict(), "unet_model.pth")

Visualization:
Visualize input images, ground truth masks, and predicted masks for qualitative assessment.


Conclusion

This project covers the essential steps for building a medical image segmentation model using U-Net:

  • Data loading, preprocessing, and augmentation
  • Model architecture design
  • Training and evaluation
  • Model saving and visualization

You can extend this project by experimenting with different architectures, datasets, and hyperparameters.


Resources

Project repository

GitHub Code: Medical Image Classification, Segmentation using U-Net, and Tabular Data Classification

This post is licensed under CC BY 4.0 by the author.