Skip to content

[Example] Add 2D Multi-Organ Segmentation with TransUNet - #2361

Open
yassienashrafwasfy wants to merge 7 commits into
keras-team:masterfrom
yassienashrafwasfy:master
Open

[Example] Add 2D Multi-Organ Segmentation with TransUNet#2361
yassienashrafwasfy wants to merge 7 commits into
keras-team:masterfrom
yassienashrafwasfy:master

Conversation

@yassienashrafwasfy

@yassienashrafwasfy yassienashrafwasfy commented May 1, 2026

Copy link
Copy Markdown

Referencing: (#2340)

This PR introduces a new Computer Vision example: 2D Multi-Organ Segmentation with TransUNet.

Overview:
This example implements TransUNet (Chen et al., 2021), a hybrid architecture that leverages both the
high-resolution spatial features of CNNs and the global context modeling of Vision Transformers (ViT). The
model is designed for medical image segmentation, specifically targeting multi-organ structures.

Key Features:

  • Architecture: Implements a ResNet-50 backbone for feature extraction, a Transformer-based bottleneck for
    global encoding, and a cascaded upsampling decoder (CUP) with U-Net skip connections.
  • Dataset: Demonstrates the pipeline using the Synapse multi-organ segmentation dataset, handling both .npz
    and .h5 file formats for training and volume-based validation.
  • Keras 3 Compatibility: Built using Keras 3 and keras.ops, ensuring it is backend-agnostic and supports
    TensorFlow, JAX, and PyTorch.
  • Advanced Loss/Metrics: Utilizes a weighted combination of Sparse Categorical Cross-Entropy and Soft Dice
    Loss, along with a custom MeanIoU wrapper for sparse labels.
  • Visualization: Includes comprehensive utilities for colorizing segmentation masks and visualizing model
    predictions against ground truth overlays.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a TransUNet implementation for 2D multi-organ segmentation on the Synapse dataset, provided as both a Jupyter notebook and a Python script. The feedback focuses on improving the model's flexibility and performance: specifically, removing the hardcoded TensorFlow backend to support Keras 3's multi-backend capabilities, and updating the architecture to support non-square input images by parameterizing the encoder and feature map calculations. Additionally, it is suggested to exclude the background class from the Dice loss calculation to better prioritize small organ segmentation.


import os

os.environ["KERAS_BACKEND"] = "tensorflow"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The PR description states that this example is Keras 3 compatible and backend-agnostic. However, hardcoding the backend to "tensorflow" prevents users from running this example with JAX or PyTorch. Since the model is built using keras.ops and tf.data is supported as a universal data loader in Keras 3, this line should be removed to allow multi-backend usage.

Comment on lines +167 to +169
def _build_resnet_encoder():
"""Returns a ResNet-50 feature pyramid as a plain keras.Model."""
inputs = keras.Input(shape=(224, 224, 3))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The input shape is hardcoded to (224, 224, 3) in the encoder builder. This ignores the image_size parameter passed to the TransUNet constructor. If a user initializes the model with a different image size, it will lead to a shape mismatch in the backbone.

Suggested change
def _build_resnet_encoder():
"""Returns a ResNet-50 feature pyramid as a plain keras.Model."""
inputs = keras.Input(shape=(224, 224, 3))
def _build_resnet_encoder(input_shape=(224, 224, 3)):
"""Returns a ResNet-50 feature pyramid as a plain keras.Model."""
inputs = keras.Input(shape=input_shape)

Comment on lines +277 to +281
feature_map_size = image_size[0] // 16
self.cnn_backbone = _build_resnet_encoder()
self.patch_embedding = PatchEmbedding(
patch_size, embedding_dim, (feature_map_size // patch_size) ** 2
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The calculation of num_patches and the initialization of the backbone assume a square input image. To support non-square images, the height and width feature map sizes should be calculated independently.

Suggested change
feature_map_size = image_size[0] // 16
self.cnn_backbone = _build_resnet_encoder()
self.patch_embedding = PatchEmbedding(
patch_size, embedding_dim, (feature_map_size // patch_size) ** 2
)
h_feat, w_feat = image_size[0] // 16, image_size[1] // 16
self.cnn_backbone = _build_resnet_encoder(image_size)
self.patch_embedding = PatchEmbedding(
patch_size, embedding_dim, (h_feat // patch_size) * (w_feat // patch_size)
)

Comment on lines +306 to +307
fmap_size = self.image_size[0] // 16 // self.patch_size
decoded = ops.reshape(tokens, (-1, fmap_size, fmap_size, self.embedding_dim))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The reshaping of tokens in the call method assumes a square feature map. This will fail if the input image is not square.

Suggested change
fmap_size = self.image_size[0] // 16 // self.patch_size
decoded = ops.reshape(tokens, (-1, fmap_size, fmap_size, self.embedding_dim))
h_fmap = self.image_size[0] // 16 // self.patch_size
w_fmap = self.image_size[1] // 16 // self.patch_size
decoded = ops.reshape(tokens, (-1, h_fmap, w_fmap, self.embedding_dim))

y_pred = ops.clip(y_pred, 1e-6, 1.0)
intersection = ops.sum(y_true_oh * y_pred, axis=[1, 2])
union = ops.sum(y_true_oh + y_pred, axis=[1, 2])
dice_loss = 1 - ops.mean((2.0 * intersection + smooth) / (union + smooth))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The combined_loss includes the background class (index 0) in the Dice loss calculation. In multi-organ segmentation, the background typically dominates the image area, which can bias the Dice score and lead the model to prioritize background accuracy over small organ segmentation. It is standard practice to exclude the background class from the Dice loss.

Suggested change
dice_loss = 1 - ops.mean((2.0 * intersection + smooth) / (union + smooth))
dice_loss = 1 - ops.mean((2.0 * intersection[:, 1:] + smooth) / (union[:, 1:] + smooth))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants