Source code for tiatoolbox.models.architecture.segformer
"""SegFormer semantic segmentation via Hugging Face Transformers (Apache-2.0).
This module wraps ``transformers.SegformerForSemanticSegmentation`` and keeps the
TIAToolbox ``ModelABC`` inference API. SMP-format checkpoints are auto-remapped on
``load_state_dict``.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
import cv2
import dask.array as da
import numpy as np
import torch
from torch import nn
from transformers import SegformerConfig, SegformerForSemanticSegmentation
from tiatoolbox.models.architecture.segformer_hf import (
MIT_ENCODER_CONFIGS,
is_smp_segformer_state_dict,
remap_smp_segformer_state_dict,
)
from tiatoolbox.models.models_abc import ModelABC
if TYPE_CHECKING: # pragma: no cover
from collections.abc import Mapping
from torch.nn.modules.module import _IncompatibleKeys
[docs]
class Segformer(ModelABC):
"""SegFormer semantic segmentation model (Hugging Face Transformers).
Args:
encoder_name:
Mix Transformer backbone name: ``mit_b0`` ... ``mit_b5``.
decoder_segmentation_channels:
Channel width of the all-MLP decoder (HF ``decoder_hidden_size``).
in_channels:
Number of input image channels (default RGB = 3).
classes:
Number of output segmentation classes (``num_labels``).
activation:
Optional activation applied after the classifier / upsample.
upsampling:
Spatial upsample factor applied to HF logits (HF outputs at 1/4
resolution; default ``4`` restores input size).
.. _SegFormer:
https://arxiv.org/abs/2105.15203
"""
def __init__(
self,
encoder_name: str = "mit_b5",
decoder_segmentation_channels: int = 256,
in_channels: int = 3,
classes: int = 1,
activation: nn.Module | None = None,
upsampling: int = 4,
) -> None:
"""Initializes the SegFormer model."""
super().__init__()
self.requires_divisible_input_shape = True
if encoder_name not in MIT_ENCODER_CONFIGS:
supported = ", ".join(sorted(MIT_ENCODER_CONFIGS))
msg = f"Unknown encoder_name={encoder_name!r}. Supported: {supported}."
raise ValueError(msg)
config = SegformerConfig(
num_labels=classes,
num_channels=in_channels,
decoder_hidden_size=decoder_segmentation_channels,
reshape_last_stage=True,
**MIT_ENCODER_CONFIGS[encoder_name],
)
self.model = SegformerForSemanticSegmentation(config)
self.upsampling = (
nn.UpsamplingBilinear2d(scale_factor=upsampling)
if upsampling > 1
else nn.Identity()
)
self.activation = activation if activation is not None else nn.Identity()
self.name = f"segformer-{encoder_name}"
[docs]
def forward(
self: Segformer,
x: torch.Tensor,
*args: tuple[Any, ...], # noqa: ARG002
**kwargs: dict, # noqa: ARG002
) -> torch.Tensor:
"""Run encoder-decoder and upsample logits to the input resolution."""
logits = self.model(pixel_values=x).logits
return self.activation(self.upsampling(logits))
[docs]
def load_state_dict(
self,
state_dict: Mapping[str, torch.Tensor],
strict: bool = True, # noqa: FBT001, FBT002
assign: bool = False, # noqa: FBT001, FBT002
) -> _IncompatibleKeys:
"""Load HF or SMP SegFormer weights.
SMP checkpoints (keys like ``encoder.patch_embed1``) are remapped to Hugging
Face names. Classifier tensors keep their shapes, so checkpoints that only
differ by output class count load when ``classes`` matches.
"""
mapped: dict[str, torch.Tensor] = dict(state_dict)
if is_smp_segformer_state_dict(mapped):
mapped = remap_smp_segformer_state_dict(mapped)
# Remapped / native HF keys are ``segformer.*`` / ``decode_head.*``.
# This wrapper stores the HF module under ``self.model``.
needs_model_prefix = (
bool(mapped)
and not any(key.startswith("model.") for key in mapped)
and any(key.startswith(("segformer.", "decode_head.")) for key in mapped)
)
if needs_model_prefix:
mapped = {f"model.{key}": value for key, value in mapped.items()}
return super().load_state_dict(mapped, strict=strict, assign=assign)
[docs]
@staticmethod
def preproc(image: np.ndarray) -> np.ndarray:
"""Preprocess input image for inference.
Applies ImageNet normalization to the input image.
Args:
image (np.ndarray):
Input image as a NumPy array of shape (H, W, C) in uint8 format.
Returns:
np.ndarray:
Preprocessed image normalized to ImageNet statistics.
Example:
>>> img = np.random.randint(0, 255, (448, 448, 3), dtype=np.uint8)
>>> processed = Segformer.preproc(img)
>>> processed.shape
(448, 448, 3)
"""
mean = np.array([0.485, 0.456, 0.406])
std = np.array([0.229, 0.224, 0.225])
return (image / 255.0 - mean) / std
[docs]
def postproc( # skipcq: PYL-W0221
self: Segformer, image: np.ndarray
) -> np.ndarray:
"""Postprocess model output to generate a class mask.
Applies argmax and morphological operations to classify pixels.
Args:
image (np.ndarray):
Input probability map as a NumPy array of shape (H, W, C).
Returns:
np.ndarray:
Tissue mask
Example:
>>> model = Segformer(classes=2)
>>> mask = model.postproc(probs)
>>> mask.shape
(448, 448)
"""
if isinstance(image, da.Array):
image = image.compute()
prediction_mask = np.argmax(image, axis=-1).astype(np.uint8)
kernel_diameter = 3
kernel = cv2.getStructuringElement(
cv2.MORPH_ELLIPSE, (kernel_diameter, kernel_diameter)
)
prediction_mask = cv2.morphologyEx(prediction_mask, cv2.MORPH_CLOSE, kernel)
return cv2.morphologyEx(prediction_mask, cv2.MORPH_OPEN, kernel)
[docs]
@staticmethod
def infer_batch(
model: Segformer,
batch_data: torch.Tensor,
*,
device: str,
) -> np.ndarray:
"""Run inference on a batch of images.
Transfers the model and input batch to the specified device, performs
forward pass, and returns softmax probabilities.
Args:
model (Segformer):
Segformer model instance.
batch_data (torch.Tensor):
Batch of input images in NHWC format.
device (str):
Device for inference (e.g., "cpu" or "cuda").
Returns:
np.ndarray:
Inference results as a NumPy array of shape (N, H, W, C).
Example:
>>> batch = torch.randn(4, 448, 448, 3)
>>> probs = Segformer.infer_batch(
... model, batch, device="cpu"
... )
>>> probs.shape
(4, 448, 448, 1)
"""
model = model.to(device)
model.eval()
imgs = batch_data
imgs = imgs.to(device=device, dtype=torch.float32)
imgs = imgs.permute(0, 3, 1, 2) # to NCHW
with torch.inference_mode():
logits = model(imgs)
probs = torch.nn.functional.softmax(logits, dim=1)
probs = probs.permute(0, 2, 3, 1) # to NHWC
return probs.cpu().numpy()