torch_em.model.unetr

   1from functools import partial
   2from collections import OrderedDict
   3from typing import Optional, Tuple, Union, Literal
   4
   5import torch
   6import torch.nn as nn
   7import torch.nn.functional as F
   8
   9from .vit import get_vision_transformer
  10from .unet import Decoder, ConvBlock2d, ConvBlock3d, Upsampler2d, Upsampler3d, _update_conv_kwargs
  11
  12try:
  13    from micro_sam.util import get_sam_model
  14except ImportError:
  15    get_sam_model = None
  16
  17try:
  18    from micro_sam.v2.util import get_sam2_model
  19except ImportError:
  20    get_sam2_model = None
  21
  22try:
  23    from micro_sam3.util import get_sam3_model
  24except ImportError:
  25    get_sam3_model = None
  26
  27
  28#
  29# UNETR IMPLEMENTATION [Vision Transformer (ViT from SAM / CellposeSAM / SAM2 / SAM3 / DINOv2 / DINOv3 / MAE / ScaleMAE) + UNet Decoder from `torch_em`]  # noqa
  30#
  31
  32
  33def _check_input_normalization_range(
  34    x: torch.Tensor,
  35    expected_range: Optional[Tuple[float, float]],
  36    unit_scale_max: Optional[float] = None,
  37) -> None:
  38    """Check whether raw inputs match the value range expected by the model normalizer.
  39
  40    Args:
  41        x: The input tensor to validate.
  42        expected_range: The (min, max) value range the input must lie within. If None, all checks are skipped.
  43        unit_scale_max: If set, raises an error when the input's maximum value is at or below this threshold,
  44            catching inputs that are likely in the wrong scale (e.g. [0, 1] instead of [0, 255] for SAM1).
  45    """
  46    if expected_range is None:
  47        return
  48
  49    if not torch.all(torch.isfinite(x)):
  50        raise ValueError("The input contains NaN or infinite values before normalization.")
  51
  52    min_value, max_value = expected_range
  53    if torch.any((x < min_value) | (x > max_value)):
  54        actual_min, actual_max = torch.aminmax(x.detach())
  55        raise ValueError(
  56            "The input is outside the expected scale before normalization: "
  57            f"expected values in [{min_value}, {max_value}], got [{actual_min.item()}, {actual_max.item()}]. "
  58            "Please check whether the raw inputs should be scaled to [0, 1] or kept in [0, 255] "
  59            "before applying the pretrained normalization statistics."
  60        )
  61
  62    if unit_scale_max is not None:
  63        actual_max = x.detach().max().item()
  64        if actual_max <= unit_scale_max:
  65            raise ValueError(
  66                f"The input maximum value ({actual_max:.4f}) suggests the input is in the wrong scale: "
  67                f"expected inputs with values in [{min_value}, {max_value}], "
  68                f"but the maximum is only {actual_max:.4f}. "
  69                "Please check whether the raw inputs should be scaled to [0, 255] instead of [0, 1]."
  70            )
  71
  72
  73def _as_stats(mean, std, device, dtype, is_3d: bool):
  74    view_shape = (1, -1, 1, 1, 1) if is_3d else (1, -1, 1, 1)
  75    pixel_mean = torch.tensor(mean, device=device, dtype=dtype).view(*view_shape)
  76    pixel_std = torch.tensor(std, device=device, dtype=dtype).view(*view_shape)
  77    return pixel_mean, pixel_std
  78
  79
  80class UNETRBase(nn.Module):
  81    """Base class for implementing a UNETR.
  82
  83    Args:
  84        img_size: The size of the input for the image encoder. Input images will be resized to match this size.
  85        backbone: The name of the vision transformer implementation.
  86            One of "sam", "sam2", "sam3", "cellpose_sam", "mae", "scalemae", "dinov2", "dinov3"
  87            (see all combinations below)
  88        encoder: The vision transformer. Can either be a name, such as "vit_b"
  89            (see all combinations for this below) or a torch module.
  90        decoder: The convolutional decoder.
  91        out_channels: The number of output channels of the UNETR.
  92        use_sam_stats: Whether to normalize the input data with the statistics of the
  93            pretrained SAM / SAM2 / SAM3 model.
  94        use_dino_stats: Whether to normalize the input data with the statistics of the
  95            pretrained DINOv2 / DINOv3 model.
  96        use_mae_stats: Whether to normalize the input data with the statistics of the pretrained MAE model.
  97        resize_input: Whether to resize the input images to match `img_size`.
  98            By default, it resizes the inputs to match the `img_size`.
  99        encoder_checkpoint: Checkpoint for initializing the vision transformer.
 100            Can either be a filepath or an already loaded checkpoint.
 101        final_activation: The activation to apply to the UNETR output.
 102        use_skip_connection: Whether to use skip connections. By default, it uses skip connections.
 103        embed_dim: The embedding dimensionality, corresponding to the output dimension of the vision transformer.
 104        use_conv_transpose: Whether to use transposed convolutions instead of resampling for upsampling.
 105            By default, it uses resampling for upsampling.
 106        perform_range_checks: Whether to validate the input value range before normalization on each forward pass.
 107            You can disable the checks to avoid GPU sync overhead during training when inputs are known to be correct.
 108        initial_features: The number of features of the finest decoder level. The features per level are
 109            'initial_features * gain ** i', so this scales the decoder parameters quadratically.
 110
 111        NOTE: The currently supported combinations of 'backbone' x 'encoder' are the following:
 112
 113        SAM_family_models:
 114            - 'sam' x 'vit_b'
 115            - 'sam' x 'vit_l'
 116            - 'sam' x 'vit_h'
 117            - 'sam2' x 'hvit_t'
 118            - 'sam2' x 'hvit_s'
 119            - 'sam2' x 'hvit_b'
 120            - 'sam2' x 'hvit_l'
 121            - 'sam3' x 'vit_pe'
 122            - 'cellpose_sam' x 'vit_l'
 123
 124        DINO_family_models:
 125            - 'dinov2' x 'vit_s'
 126            - 'dinov2' x 'vit_b'
 127            - 'dinov2' x 'vit_l'
 128            - 'dinov2' x 'vit_g'
 129            - 'dinov2' x 'vit_s_reg4'
 130            - 'dinov2' x 'vit_b_reg4'
 131            - 'dinov2' x 'vit_l_reg4'
 132            - 'dinov2' x 'vit_g_reg4'
 133            - 'dinov3' x 'vit_s'
 134            - 'dinov3' x 'vit_s+'
 135            - 'dinov3' x 'vit_b'
 136            - 'dinov3' x 'vit_l'
 137            - 'dinov3' x 'vit_l+'
 138            - 'dinov3' x 'vit_h+'
 139            - 'dinov3' x 'vit_7b'
 140
 141        MAE_family_models:
 142            - 'mae' x 'vit_b'
 143            - 'mae' x 'vit_l'
 144            - 'mae' x 'vit_h'
 145            - 'scalemae' x 'vit_b'
 146            - 'scalemae' x 'vit_l'
 147            - 'scalemae' x 'vit_h'
 148    """
 149    def __init__(
 150        self,
 151        img_size: int = 1024,
 152        backbone: Literal["sam", "sam2", "sam3", "cellpose_sam", "mae", "scalemae", "dinov2", "dinov3"] = "sam",
 153        encoder: Optional[Union[nn.Module, str]] = "vit_b",
 154        decoder: Optional[nn.Module] = None,
 155        out_channels: int = 1,
 156        use_sam_stats: bool = False,
 157        use_mae_stats: bool = False,
 158        use_dino_stats: bool = False,
 159        resize_input: bool = True,
 160        encoder_checkpoint: Optional[Union[str, OrderedDict]] = None,
 161        final_activation: Optional[Union[str, nn.Module]] = None,
 162        use_skip_connection: bool = True,
 163        embed_dim: Optional[int] = None,
 164        use_conv_transpose: bool = False,
 165        perform_range_checks: bool = True,
 166        initial_features: int = 64,
 167        **kwargs
 168    ) -> None:
 169        super().__init__()
 170
 171        self.img_size = img_size
 172        self.use_sam_stats = use_sam_stats
 173        self.use_mae_stats = use_mae_stats
 174        self.use_dino_stats = use_dino_stats
 175        self.use_skip_connection = use_skip_connection
 176        self.resize_input = resize_input
 177        self.perform_range_checks = perform_range_checks
 178        self.use_conv_transpose = use_conv_transpose
 179        self.initial_features = initial_features
 180        self.backbone = backbone
 181
 182        if isinstance(encoder, str):  # e.g. "vit_b" / "hvit_b" / "vit_pe"
 183            print(f"Using {encoder} from {backbone.upper()}")
 184            self.encoder = get_vision_transformer(img_size=img_size, backbone=backbone, model=encoder, **kwargs)
 185
 186            if encoder_checkpoint is not None:
 187                self._load_encoder_from_checkpoint(backbone=backbone, encoder=encoder, checkpoint=encoder_checkpoint)
 188
 189            if embed_dim is None:
 190                embed_dim = self.encoder.embed_dim
 191
 192            # For SAM1 encoder, if 'apply_neck' is applied, the embedding dimension must change.
 193            if hasattr(self.encoder, "apply_neck") and self.encoder.apply_neck:
 194                embed_dim = self.encoder.neck[2].out_channels  # the value is 256
 195
 196        else:  # `nn.Module` ViT backbone
 197            self.encoder = encoder
 198
 199            have_neck = False
 200            for name, _ in self.encoder.named_parameters():
 201                if name.startswith("neck"):
 202                    have_neck = True
 203
 204            if embed_dim is None:
 205                if have_neck:
 206                    embed_dim = self.encoder.neck[2].out_channels  # the value is 256
 207                else:
 208                    embed_dim = self.encoder.patch_embed.proj.out_channels
 209
 210        self.embed_dim = embed_dim
 211        self.final_activation = self._get_activation(final_activation)
 212
 213    def _load_encoder_from_checkpoint(self, backbone, encoder, checkpoint):
 214        """Function to load pretrained weights to the image encoder.
 215        """
 216        if isinstance(checkpoint, str):
 217            if backbone == "sam" and isinstance(encoder, str):
 218                # If we have a SAM encoder, then we first try to load the full SAM Model
 219                # (using micro_sam) and otherwise fall back on directly loading the encoder state
 220                # from the checkpoint
 221                try:
 222                    _, model = get_sam_model(model_type=encoder, checkpoint_path=checkpoint, return_sam=True)
 223                    encoder_state = model.image_encoder.state_dict()
 224                except Exception:
 225                    # Try loading the encoder state directly from a checkpoint.
 226                    encoder_state = torch.load(checkpoint, weights_only=False)
 227
 228            elif backbone == "cellpose_sam" and isinstance(encoder, str):
 229                # The architecture matches CellposeSAM exactly (same rel_pos sizes),
 230                # so weights load directly without any interpolation.
 231                encoder_state = torch.load(checkpoint, map_location="cpu", weights_only=False)
 232                # Handle DataParallel/DistributedDataParallel prefix.
 233                if any(k.startswith("module.") for k in encoder_state.keys()):
 234                    encoder_state = OrderedDict(
 235                        {k[len("module."):]: v for k, v in encoder_state.items()}
 236                    )
 237                # Extract encoder weights from CellposeSAM checkpoint format (strip 'encoder.' prefix).
 238                if any(k.startswith("encoder.") for k in encoder_state.keys()):
 239                    encoder_state = OrderedDict(
 240                        {k[len("encoder."):]: v for k, v in encoder_state.items() if k.startswith("encoder.")}
 241                    )
 242
 243            elif backbone == "sam2" and isinstance(encoder, str):
 244                # If we have a SAM2 encoder, then we first try to load the full SAM2 Model.
 245                # (using micro_sam2) and otherwise fall back on directly loading the encoder state
 246                # from the checkpoint
 247                try:
 248                    model = get_sam2_model(model_type=encoder, checkpoint_path=checkpoint)
 249                    encoder_state = model.image_encoder.state_dict()
 250                except Exception:
 251                    # Try loading the encoder state directly from a checkpoint.
 252                    encoder_state = torch.load(checkpoint, weights_only=False)
 253
 254            elif backbone == "sam3" and isinstance(encoder, str):
 255                # If we have a SAM3 encoder, then we first try to load the full SAM3 Model.
 256                # (using micro_sam3) and otherwise fall back on directly loading the encoder state
 257                # from the checkpoint
 258                try:
 259                    model = get_sam3_model(checkpoint_path=checkpoint)
 260                    encoder_state = model.backbone.vision_backbone.state_dict()
 261                    # Let's align loading the encoder weights with expected parameter names
 262                    encoder_state = {
 263                        k[len("trunk."):] if k.startswith("trunk.") else k: v for k, v in encoder_state.items()
 264                    }
 265                    # And drop the 'convs' and 'sam2_convs' - these seem like some upsampling blocks.
 266                    encoder_state = {
 267                        k: v for k, v in encoder_state.items()
 268                        if not (k.startswith("convs.") or k.startswith("sam2_convs."))
 269                    }
 270                except Exception:
 271                    # Try loading the encoder state directly from a checkpoint.
 272                    encoder_state = torch.load(checkpoint, weights_only=False)
 273
 274            elif backbone == "mae":
 275                # vit initialization hints from:
 276                #     - https://github.com/facebookresearch/mae/blob/main/main_finetune.py#L233-L242
 277                encoder_state = torch.load(checkpoint, weights_only=False)["model"]
 278                encoder_state = OrderedDict({
 279                    k: v for k, v in encoder_state.items() if (k != "mask_token" and not k.startswith("decoder"))
 280                })
 281                # Let's remove the `head` from our current encoder (as the MAE pretrained don't expect it)
 282                current_encoder_state = self.encoder.state_dict()
 283                if ("head.weight" in current_encoder_state) and ("head.bias" in current_encoder_state):
 284                    del self.encoder.head
 285
 286            elif backbone == "scalemae":
 287                # Load the encoder state directly from a checkpoint.
 288                encoder_state = torch.load(checkpoint)["model"]
 289                encoder_state = OrderedDict({
 290                    k: v for k, v in encoder_state.items()
 291                    if not k.startswith(("mask_token", "decoder", "fcn", "fpn", "pos_embed"))
 292                })
 293
 294                # Let's remove the `head` from our current encoder (as the MAE pretrained don't expect it)
 295                current_encoder_state = self.encoder.state_dict()
 296                if ("head.weight" in current_encoder_state) and ("head.bias" in current_encoder_state):
 297                    del self.encoder.head
 298
 299                if "pos_embed" in current_encoder_state:  # NOTE: ScaleMAE uses 'pos. embeddings' in a diff. format.
 300                    del self.encoder.pos_embed
 301
 302            elif backbone in ["dinov2", "dinov3"]:  # Load the encoder state directly from a checkpoint.
 303                encoder_state = torch.load(checkpoint)
 304
 305            else:
 306                raise ValueError(
 307                    f"We don't support either the '{backbone}' backbone or the '{encoder}' model combination (or both)."
 308                )
 309
 310        else:
 311            encoder_state = checkpoint
 312
 313        self.encoder.load_state_dict(encoder_state)
 314
 315    def _get_activation(self, activation):
 316        return_activation = None
 317        if activation is None:
 318            return None
 319        if isinstance(activation, nn.Module):
 320            return activation
 321        if isinstance(activation, str):
 322            return_activation = getattr(nn, activation, None)
 323        if return_activation is None:
 324            raise ValueError(f"Invalid activation: {activation}")
 325
 326        return return_activation()
 327
 328    @staticmethod
 329    def get_preprocess_shape(oldh: int, oldw: int, long_side_length: int) -> Tuple[int, int]:
 330        """Compute the output size given input size and target long side length.
 331
 332        Args:
 333            oldh: The input image height.
 334            oldw: The input image width.
 335            long_side_length: The longest side length for resizing.
 336
 337        Returns:
 338            The new image height.
 339            The new image width.
 340        """
 341        scale = long_side_length * 1.0 / max(oldh, oldw)
 342        newh, neww = oldh * scale, oldw * scale
 343        neww = int(neww + 0.5)
 344        newh = int(newh + 0.5)
 345        return (newh, neww)
 346
 347    def resize_longest_side(self, image: torch.Tensor) -> torch.Tensor:
 348        """Resize the image so that the longest side has the correct length.
 349
 350        Expects batched images with shape BxCxHxW OR BxCxDxHxW and float format.
 351
 352        Args:
 353            image: The input image.
 354
 355        Returns:
 356            The resized image.
 357        """
 358        if image.ndim == 4:  # i.e. 2d image
 359            target_size = self.get_preprocess_shape(image.shape[2], image.shape[3], self.encoder.img_size)
 360            return F.interpolate(image, target_size, mode="bilinear", align_corners=False, antialias=True)
 361        elif image.ndim == 5:  # i.e. 3d volume
 362            B, C, Z, H, W = image.shape
 363            target_size = self.get_preprocess_shape(H, W, self.img_size)
 364            return F.interpolate(image, (Z, *target_size), mode="trilinear", align_corners=False)
 365        else:
 366            raise ValueError("Expected 4d or 5d inputs, got", image.shape)
 367
 368    def _as_stats(self, mean, std, device, dtype, is_3d: bool):
 369        """@private
 370        """
 371        return _as_stats(mean, std, device, dtype, is_3d)
 372
 373    def _check_input_normalization_range(self, x: torch.Tensor, expected_range: Optional[Tuple[float, float]]) -> None:
 374        """@private
 375        """
 376        _check_input_normalization_range(x, expected_range)
 377
 378    def preprocess(self, x: torch.Tensor) -> torch.Tensor:
 379        """@private
 380        """
 381        return preprocess_vit_inputs(
 382            x,
 383            use_sam_stats=self.use_sam_stats,
 384            backbone=self.backbone,
 385            use_mae_stats=self.use_mae_stats,
 386            use_dino_stats=self.use_dino_stats,
 387            resize_input=self.resize_input,
 388            img_size=self.img_size,
 389            encoder_img_size=self.encoder.img_size,
 390            perform_range_checks=self.perform_range_checks,
 391        )
 392
 393    def postprocess_masks(
 394        self, masks: torch.Tensor, input_size: Tuple[int, ...], original_size: Tuple[int, ...],
 395    ) -> torch.Tensor:
 396        """@private
 397        """
 398        if masks.ndim == 4:  # i.e. 2d labels
 399            masks = F.interpolate(
 400                masks,
 401                (self.encoder.img_size, self.encoder.img_size),
 402                mode="bilinear",
 403                align_corners=False,
 404            )
 405            masks = masks[..., : input_size[0], : input_size[1]]
 406            masks = F.interpolate(masks, original_size, mode="bilinear", align_corners=False)
 407
 408        elif masks.ndim == 5:  # i.e. 3d volumetric labels
 409            masks = F.interpolate(
 410                masks,
 411                (input_size[0], self.img_size, self.img_size),
 412                mode="trilinear",
 413                align_corners=False,
 414            )
 415            masks = masks[..., :input_size[0], :input_size[1], :input_size[2]]
 416            masks = F.interpolate(masks, original_size, mode="trilinear", align_corners=False)
 417
 418        else:
 419            raise ValueError("Expected 4d or 5d labels, got", masks.shape)
 420
 421        return masks
 422
 423
 424def preprocess_vit_inputs(
 425    x: torch.Tensor,
 426    use_sam_stats: bool = False,
 427    backbone: str = "sam",
 428    use_mae_stats: bool = False,
 429    use_dino_stats: bool = False,
 430    resize_input: bool = True,
 431    img_size: int = 1024,
 432    encoder_img_size: int = 1024,
 433    perform_range_checks: bool = True,
 434) -> Tuple[torch.Tensor, Tuple]:
 435    """Preprocess inputs for ViT-backbones in UNETR models.
 436
 437    Handles normalization stat selection, input range validation, optional resizing to the longest side,
 438    and padding to `encoder_img_size`. Can be used as a standalone function without a model instance.
 439
 440    Args:
 441        x: Input tensor of shape (B, C, H, W) for 2D or (B, C, Z, H, W) for 3D.
 442        use_sam_stats: Whether to normalize with SAM/SAM2/SAM3 backbone statistics.
 443        backbone: The backbone name - controls which SAM stats are used when `use_sam_stats=True`.
 444        use_mae_stats: Whether to normalize with MAE statistics.
 445        use_dino_stats: Whether to normalize with DINOv2/DINOv3 statistics.
 446        resize_input: Whether to resize the input to the longest side before padding.
 447        img_size: The model image size, used for 3D resize.
 448        encoder_img_size: The encoder image size, used for 2D resize and padding.
 449        perform_range_checks: Whether to validate the expected input value range before normalization.
 450            You can disable the checks to avoid GPU sync overhead during training when inputs are known to be correct.
 451
 452    Returns:
 453        The preprocessed tensor and the spatial shape after resizing (before padding).
 454    """
 455    is_3d = (x.ndim == 5)
 456    device, dtype = x.device, x.dtype
 457    mean, std = (0.0, 0.0, 0.0), (1.0, 1.0, 1.0)
 458    expected_range = None
 459    unit_scale_max = None
 460
 461    if use_sam_stats:
 462        if backbone == "sam2":
 463            mean, std = (0.485, 0.456, 0.406), (0.229, 0.224, 0.225)
 464            expected_range = (0.0, 1.0)
 465        elif backbone == "sam3":
 466            mean, std = (0.5, 0.5, 0.5), (0.5, 0.5, 0.5)
 467            expected_range = (0.0, 1.0)
 468        else:  # sam1 / default
 469            mean, std = (123.675, 116.28, 103.53), (58.395, 57.12, 57.375)
 470            expected_range = (0.0, 255.0)
 471            unit_scale_max = 1.0
 472    elif use_mae_stats:  # TODO: add mean std from mae / scalemae experiments (or open up arguments for this)
 473        raise NotImplementedError
 474    elif use_dino_stats:
 475        mean, std = (0.485, 0.456, 0.406), (0.229, 0.224, 0.225)
 476        expected_range = (0.0, 1.0)
 477    else:
 478        mean, std = (0.0, 0.0, 0.0), (1.0, 1.0, 1.0)
 479        expected_range = None
 480
 481    if perform_range_checks:
 482        _check_input_normalization_range(x, expected_range, unit_scale_max)
 483    pixel_mean, pixel_std = _as_stats(mean, std, device=device, dtype=dtype, is_3d=is_3d)
 484
 485    if resize_input:
 486        if x.ndim == 4:
 487            target_size = UNETRBase.get_preprocess_shape(x.shape[2], x.shape[3], encoder_img_size)
 488            x = F.interpolate(x, target_size, mode="bilinear", align_corners=False, antialias=True)
 489        elif x.ndim == 5:
 490            B, C, Z, H, W = x.shape
 491            target_size = UNETRBase.get_preprocess_shape(H, W, img_size)
 492            x = F.interpolate(x, (Z, *target_size), mode="trilinear", align_corners=False)
 493
 494    input_shape = x.shape[-3:] if is_3d else x.shape[-2:]
 495
 496    x = (x - pixel_mean) / pixel_std
 497    h, w = x.shape[-2:]
 498    padh = encoder_img_size - h
 499    padw = encoder_img_size - w
 500
 501    if is_3d:
 502        x = F.pad(x, (0, padw, 0, padh, 0, 0))
 503    else:
 504        x = F.pad(x, (0, padw, 0, padh))
 505
 506    return x, input_shape
 507
 508
 509class UNETR(UNETRBase):
 510    """A (2d-only) UNet Transformer using a vision transformer as encoder and a convolutional decoder.
 511    """
 512    def __init__(
 513        self,
 514        img_size: int = 1024,
 515        backbone: Literal["sam", "sam2", "sam3", "cellpose_sam", "mae", "scalemae", "dinov2", "dinov3"] = "sam",
 516        encoder: Optional[Union[nn.Module, str]] = "vit_b",
 517        decoder: Optional[nn.Module] = None,
 518        out_channels: int = 1,
 519        use_sam_stats: bool = False,
 520        use_mae_stats: bool = False,
 521        use_dino_stats: bool = False,
 522        resize_input: bool = True,
 523        encoder_checkpoint: Optional[Union[str, OrderedDict]] = None,
 524        final_activation: Optional[Union[str, nn.Module]] = None,
 525        use_skip_connection: bool = True,
 526        embed_dim: Optional[int] = None,
 527        use_conv_transpose: bool = False,
 528        perform_range_checks: bool = True,
 529        **kwargs
 530    ) -> None:
 531
 532        super().__init__(
 533            img_size=img_size,
 534            backbone=backbone,
 535            encoder=encoder,
 536            decoder=decoder,
 537            out_channels=out_channels,
 538            use_sam_stats=use_sam_stats,
 539            use_mae_stats=use_mae_stats,
 540            use_dino_stats=use_dino_stats,
 541            resize_input=resize_input,
 542            encoder_checkpoint=encoder_checkpoint,
 543            final_activation=final_activation,
 544            use_skip_connection=use_skip_connection,
 545            embed_dim=embed_dim,
 546            use_conv_transpose=use_conv_transpose,
 547            perform_range_checks=perform_range_checks,
 548            **kwargs,
 549        )
 550
 551        encoder = self.encoder
 552
 553        if backbone == "sam2" and hasattr(encoder, "trunk"):
 554            in_chans = encoder.trunk.patch_embed.proj.in_channels
 555        elif hasattr(encoder, "in_chans"):
 556            in_chans = encoder.in_chans
 557        else:  # `nn.Module` ViT backbone.
 558            try:
 559                in_chans = encoder.patch_embed.proj.in_channels
 560            except AttributeError:  # for getting the input channels while using 'vit_t' from MobileSam
 561                in_chans = encoder.patch_embed.seq[0].c.in_channels
 562
 563        # parameters for the decoder network
 564        depth = 3
 565        gain = 2
 566        features_decoder = [self.initial_features * gain ** i for i in range(depth + 1)][::-1]
 567        scale_factors = depth * [2]
 568        self.out_channels = out_channels
 569
 570        # choice of upsampler - to use (bilinear interpolation + conv) or conv transpose
 571        _upsampler = SingleDeconv2DBlock if use_conv_transpose else Upsampler2d
 572
 573        self.decoder = decoder or Decoder(
 574            features=features_decoder,
 575            scale_factors=scale_factors[::-1],
 576            conv_block_impl=ConvBlock2d,
 577            sampler_impl=_upsampler,
 578        )
 579
 580        if use_skip_connection:
 581            self.deconv1 = Deconv2DBlock(
 582                in_channels=self.embed_dim,
 583                out_channels=features_decoder[0],
 584                use_conv_transpose=use_conv_transpose,
 585            )
 586            self.deconv2 = nn.Sequential(
 587                Deconv2DBlock(
 588                    in_channels=self.embed_dim,
 589                    out_channels=features_decoder[0],
 590                    use_conv_transpose=use_conv_transpose,
 591                ),
 592                Deconv2DBlock(
 593                    in_channels=features_decoder[0],
 594                    out_channels=features_decoder[1],
 595                    use_conv_transpose=use_conv_transpose,
 596                )
 597            )
 598            self.deconv3 = nn.Sequential(
 599                Deconv2DBlock(
 600                    in_channels=self.embed_dim,
 601                    out_channels=features_decoder[0],
 602                    use_conv_transpose=use_conv_transpose,
 603                ),
 604                Deconv2DBlock(
 605                    in_channels=features_decoder[0],
 606                    out_channels=features_decoder[1],
 607                    use_conv_transpose=use_conv_transpose,
 608                ),
 609                Deconv2DBlock(
 610                    in_channels=features_decoder[1],
 611                    out_channels=features_decoder[2],
 612                    use_conv_transpose=use_conv_transpose,
 613                )
 614            )
 615            self.deconv4 = ConvBlock2d(in_chans, features_decoder[-1])
 616        else:
 617            self.deconv1 = Deconv2DBlock(
 618                in_channels=self.embed_dim,
 619                out_channels=features_decoder[0],
 620                use_conv_transpose=use_conv_transpose,
 621            )
 622            self.deconv2 = Deconv2DBlock(
 623                in_channels=features_decoder[0],
 624                out_channels=features_decoder[1],
 625                use_conv_transpose=use_conv_transpose,
 626            )
 627            self.deconv3 = Deconv2DBlock(
 628                in_channels=features_decoder[1],
 629                out_channels=features_decoder[2],
 630                use_conv_transpose=use_conv_transpose,
 631            )
 632            self.deconv4 = Deconv2DBlock(
 633                in_channels=features_decoder[2],
 634                out_channels=features_decoder[3],
 635                use_conv_transpose=use_conv_transpose,
 636            )
 637
 638        self.base = ConvBlock2d(self.embed_dim, features_decoder[0])
 639        self.out_conv = nn.Conv2d(features_decoder[-1], out_channels, 1)
 640        self.deconv_out = _upsampler(
 641            scale_factor=2, in_channels=features_decoder[-1], out_channels=features_decoder[-1]
 642        )
 643        self.decoder_head = ConvBlock2d(2 * features_decoder[-1], features_decoder[-1])
 644
 645    def forward(self, x: torch.Tensor) -> torch.Tensor:
 646        """Apply the UNETR to the input data.
 647
 648        Args:
 649            x: The input tensor.
 650
 651        Returns:
 652            The UNETR output.
 653        """
 654        original_shape = x.shape[-2:]
 655
 656        # Reshape the inputs to the shape expected by the encoder
 657        # and normalize the inputs if normalization is part of the model.
 658        x, input_shape = self.preprocess(x)
 659
 660        encoder_outputs = self.encoder(x)
 661
 662        if isinstance(encoder_outputs[-1], list):
 663            # `encoder_outputs` can be arranged in only two forms:
 664            #   - either we only return the image embeddings
 665            #   - or, we return the image embeddings and the "list" of global attention layers
 666            z12, from_encoder = encoder_outputs
 667        else:
 668            z12 = encoder_outputs
 669
 670        if self.use_skip_connection:
 671            from_encoder = from_encoder[::-1]
 672            z9 = self.deconv1(from_encoder[0])
 673            z6 = self.deconv2(from_encoder[1])
 674            z3 = self.deconv3(from_encoder[2])
 675            z0 = self.deconv4(x)
 676
 677        else:
 678            z9 = self.deconv1(z12)
 679            z6 = self.deconv2(z9)
 680            z3 = self.deconv3(z6)
 681            z0 = self.deconv4(z3)
 682
 683        updated_from_encoder = [z9, z6, z3]
 684
 685        x = self.base(z12)
 686        x = self.decoder(x, encoder_inputs=updated_from_encoder)
 687        x = self.deconv_out(x)
 688
 689        x = torch.cat([x, z0], dim=1)
 690        x = self.decoder_head(x)
 691
 692        x = self.out_conv(x)
 693        if self.final_activation is not None:
 694            x = self.final_activation(x)
 695
 696        x = self.postprocess_masks(x, input_shape, original_shape)
 697        return x
 698
 699
 700class UNETR2D(UNETR):
 701    """A two-dimensional UNet Transformer using a vision transformer as encoder and a convolutional decoder.
 702    """
 703    pass
 704
 705
 706class UNETR3D(UNETRBase):
 707    """A three dimensional UNet Transformer using a vision transformer as encoder and a convolutional decoder.
 708    """
 709    def __init__(
 710        self,
 711        img_size: int = 1024,
 712        backbone: Literal["sam", "sam2", "sam3", "cellpose_sam", "mae", "scalemae", "dinov2", "dinov3"] = "sam",
 713        encoder: Optional[Union[nn.Module, str]] = "hvit_b",
 714        decoder: Optional[nn.Module] = None,
 715        out_channels: int = 1,
 716        use_sam_stats: bool = False,
 717        use_mae_stats: bool = False,
 718        use_dino_stats: bool = False,
 719        resize_input: bool = True,
 720        encoder_checkpoint: Optional[Union[str, OrderedDict]] = None,
 721        final_activation: Optional[Union[str, nn.Module]] = None,
 722        use_skip_connection: bool = False,
 723        embed_dim: Optional[int] = None,
 724        use_conv_transpose: bool = False,
 725        use_strip_pooling: bool = True,
 726        perform_range_checks: bool = True,
 727        **kwargs
 728    ):
 729        if use_skip_connection:
 730            raise NotImplementedError("The framework cannot handle skip connections atm.")
 731        if use_conv_transpose:
 732            raise NotImplementedError("It's not enabled to switch between interpolation and transposed convolutions.")
 733
 734        # Sort the `embed_dim` out
 735        embed_dim = 256 if embed_dim is None else embed_dim
 736
 737        super().__init__(
 738            img_size=img_size,
 739            backbone=backbone,
 740            encoder=encoder,
 741            decoder=decoder,
 742            out_channels=out_channels,
 743            use_sam_stats=use_sam_stats,
 744            use_mae_stats=use_mae_stats,
 745            use_dino_stats=use_dino_stats,
 746            resize_input=resize_input,
 747            encoder_checkpoint=encoder_checkpoint,
 748            final_activation=final_activation,
 749            use_skip_connection=use_skip_connection,
 750            embed_dim=embed_dim,
 751            use_conv_transpose=use_conv_transpose,
 752            perform_range_checks=perform_range_checks,
 753            **kwargs,
 754        )
 755
 756        # The 3d convolutional decoder.
 757        # First, get the important parameters for the decoder.
 758        depth = 3
 759        gain = 2
 760        features_decoder = [self.initial_features * gain ** i for i in range(depth + 1)][::-1]
 761        scale_factors = [1, 2, 2]
 762        self.out_channels = out_channels
 763
 764        # The mapping blocks.
 765        self.deconv1 = Deconv3DBlock(
 766            in_channels=embed_dim,
 767            out_channels=features_decoder[0],
 768            scale_factor=scale_factors,
 769            use_strip_pooling=use_strip_pooling,
 770        )
 771        self.deconv2 = Deconv3DBlock(
 772            in_channels=features_decoder[0],
 773            out_channels=features_decoder[1],
 774            scale_factor=scale_factors,
 775            use_strip_pooling=use_strip_pooling,
 776        )
 777        self.deconv3 = Deconv3DBlock(
 778            in_channels=features_decoder[1],
 779            out_channels=features_decoder[2],
 780            scale_factor=scale_factors,
 781            use_strip_pooling=use_strip_pooling,
 782        )
 783        self.deconv4 = Deconv3DBlock(
 784            in_channels=features_decoder[2],
 785            out_channels=features_decoder[3],
 786            scale_factor=scale_factors,
 787            use_strip_pooling=use_strip_pooling,
 788        )
 789
 790        # The core decoder block.
 791        self.decoder = decoder or Decoder(
 792            features=features_decoder,
 793            scale_factors=[scale_factors] * depth,
 794            conv_block_impl=partial(ConvBlock3dWithStrip, use_strip_pooling=use_strip_pooling),
 795            sampler_impl=Upsampler3d,
 796        )
 797
 798        # And the final upsampler to match the expected dimensions.
 799        self.deconv_out = Deconv3DBlock(  # NOTE: changed `end_up` to `deconv_out`
 800            in_channels=features_decoder[-1],
 801            out_channels=features_decoder[-1],
 802            scale_factor=scale_factors,
 803            use_strip_pooling=use_strip_pooling,
 804        )
 805
 806        # Additional conjunction blocks.
 807        self.base = ConvBlock3dWithStrip(
 808            in_channels=embed_dim,
 809            out_channels=features_decoder[0],
 810            use_strip_pooling=use_strip_pooling,
 811        )
 812
 813        # And the output layers.
 814        self.decoder_head = ConvBlock3dWithStrip(
 815            in_channels=2 * features_decoder[-1],
 816            out_channels=features_decoder[-1],
 817            use_strip_pooling=use_strip_pooling,
 818        )
 819        self.out_conv = nn.Conv3d(features_decoder[-1], out_channels, 1)
 820
 821    def forward(self, x: torch.Tensor):
 822        """Forward pass of the UNETR-3D model.
 823
 824        Args:
 825            x: Inputs of expected shape (B, C, Z, Y, X), where Z considers flexible inputs.
 826
 827        Returns:
 828            The UNETR output.
 829        """
 830        B, C, Z, H, W = x.shape
 831        original_shape = (Z, H, W)
 832
 833        # Preprocessing step
 834        x, input_shape = self.preprocess(x)
 835
 836        # Run the image encoder.
 837        curr_features = torch.stack([self.encoder(x[:, :, i])[0] for i in range(Z)], dim=2)
 838
 839        # Prepare the counterparts for the decoder.
 840        # NOTE: The section below is sequential, there's no skip connections atm.
 841        z9 = self.deconv1(curr_features)
 842        z6 = self.deconv2(z9)
 843        z3 = self.deconv3(z6)
 844        z0 = self.deconv4(z3)
 845
 846        updated_from_encoder = [z9, z6, z3]
 847
 848        # Align the features through the base block.
 849        x = self.base(curr_features)
 850        # Run the decoder
 851        x = self.decoder(x, encoder_inputs=updated_from_encoder)
 852        x = self.deconv_out(x)  # NOTE before `end_up`
 853
 854        # And the final output head.
 855        x = torch.cat([x, z0], dim=1)
 856        x = self.decoder_head(x)
 857        x = self.out_conv(x)
 858        if self.final_activation is not None:
 859            x = self.final_activation(x)
 860
 861        # Postprocess the output back to original size.
 862        x = self.postprocess_masks(x, input_shape, original_shape)
 863        return x
 864
 865#
 866#  ADDITIONAL FUNCTIONALITIES
 867#
 868
 869
 870def _strip_pooling_layers(enabled, channels) -> nn.Module:
 871    return DepthStripPooling(channels) if enabled else nn.Identity()
 872
 873
 874class DepthStripPooling(nn.Module):
 875    """@private
 876    """
 877    def __init__(self, channels: int, reduction: int = 4):
 878        """Block for strip pooling along the depth dimension (only).
 879
 880        eg. for 3D (Z > 1) - it aggregates global context across depth by adaptive avg pooling
 881        to Z=1, and then passes through a small 1x1x1 MLP, then broadcasts it back to Z to
 882        modulate the original features (using a gated residual).
 883
 884        For 2D (Z == 1): returns input unchanged (no-op).
 885
 886        Args:
 887            channels: The output channels.
 888            reduction: The reduction of the hidden layers.
 889        """
 890        super().__init__()
 891        hidden = max(1, channels // reduction)
 892        self.conv1 = nn.Conv3d(channels, hidden, kernel_size=1)
 893        self.bn1 = nn.BatchNorm3d(hidden)
 894        self.relu = nn.ReLU(inplace=True)
 895        self.conv2 = nn.Conv3d(hidden, channels, kernel_size=1)
 896
 897    def forward(self, x: torch.Tensor) -> torch.Tensor:
 898        if x.dim() != 5:
 899            raise ValueError(f"DepthStripPooling expects 5D tensors as input, got '{x.shape}'.")
 900
 901        B, C, Z, H, W = x.shape
 902        if Z == 1:  # i.e. always the case of all 2d.
 903            return x  # We simply do nothing there.
 904
 905        # We pool only along the depth dimension: i.e. target shape (B, C, 1, H, W)
 906        feat = F.adaptive_avg_pool3d(x, output_size=(1, H, W))
 907        feat = self.conv1(feat)
 908        feat = self.bn1(feat)
 909        feat = self.relu(feat)
 910        feat = self.conv2(feat)
 911        gate = torch.sigmoid(feat).expand(B, C, Z, H, W)  # Broadcast the collapsed depth context back to all slices
 912
 913        # Gated residual fusion
 914        return x * gate + x
 915
 916
 917class Deconv3DBlock(nn.Module):
 918    """@private
 919    """
 920    def __init__(
 921        self,
 922        scale_factor,
 923        in_channels,
 924        out_channels,
 925        kernel_size=3,
 926        anisotropic_kernel=True,
 927        use_strip_pooling=True,
 928    ):
 929        super().__init__()
 930        conv_block_kwargs = {
 931            "in_channels": out_channels,
 932            "out_channels": out_channels,
 933            "kernel_size": kernel_size,
 934            "padding": ((kernel_size - 1) // 2),
 935        }
 936        if anisotropic_kernel:
 937            conv_block_kwargs = _update_conv_kwargs(conv_block_kwargs, scale_factor)
 938
 939        self.block = nn.Sequential(
 940            Upsampler3d(scale_factor, in_channels, out_channels),
 941            nn.Conv3d(**conv_block_kwargs),
 942            nn.BatchNorm3d(out_channels),
 943            nn.ReLU(True),
 944            _strip_pooling_layers(enabled=use_strip_pooling, channels=out_channels),
 945        )
 946
 947    def forward(self, x):
 948        return self.block(x)
 949
 950
 951class ConvBlock3dWithStrip(nn.Module):
 952    """@private
 953    """
 954    def __init__(
 955        self, in_channels: int, out_channels: int, use_strip_pooling: bool = True, **kwargs
 956    ):
 957        super().__init__()
 958        self.block = nn.Sequential(
 959            ConvBlock3d(in_channels, out_channels, **kwargs),
 960            _strip_pooling_layers(enabled=use_strip_pooling, channels=out_channels),
 961        )
 962
 963    def forward(self, x):
 964        return self.block(x)
 965
 966
 967class SingleDeconv2DBlock(nn.Module):
 968    """@private
 969    """
 970    def __init__(self, scale_factor, in_channels, out_channels):
 971        super().__init__()
 972        self.block = nn.ConvTranspose2d(in_channels, out_channels, kernel_size=2, stride=2, padding=0, output_padding=0)
 973
 974    def forward(self, x):
 975        return self.block(x)
 976
 977
 978class SingleConv2DBlock(nn.Module):
 979    """@private
 980    """
 981    def __init__(self, in_channels, out_channels, kernel_size):
 982        super().__init__()
 983        self.block = nn.Conv2d(
 984            in_channels, out_channels, kernel_size=kernel_size, stride=1, padding=((kernel_size - 1) // 2)
 985        )
 986
 987    def forward(self, x):
 988        return self.block(x)
 989
 990
 991class Conv2DBlock(nn.Module):
 992    """@private
 993    """
 994    def __init__(self, in_channels, out_channels, kernel_size=3):
 995        super().__init__()
 996        self.block = nn.Sequential(
 997            SingleConv2DBlock(in_channels, out_channels, kernel_size),
 998            nn.BatchNorm2d(out_channels),
 999            nn.ReLU(True)
1000        )
1001
1002    def forward(self, x):
1003        return self.block(x)
1004
1005
1006class Deconv2DBlock(nn.Module):
1007    """@private
1008    """
1009    def __init__(self, in_channels, out_channels, kernel_size=3, use_conv_transpose=True):
1010        super().__init__()
1011        _upsampler = SingleDeconv2DBlock if use_conv_transpose else Upsampler2d
1012        self.block = nn.Sequential(
1013            _upsampler(scale_factor=2, in_channels=in_channels, out_channels=out_channels),
1014            SingleConv2DBlock(out_channels, out_channels, kernel_size),
1015            nn.BatchNorm2d(out_channels),
1016            nn.ReLU(True)
1017        )
1018
1019    def forward(self, x):
1020        return self.block(x)
class UNETRBase(torch.nn.modules.module.Module):
 81class UNETRBase(nn.Module):
 82    """Base class for implementing a UNETR.
 83
 84    Args:
 85        img_size: The size of the input for the image encoder. Input images will be resized to match this size.
 86        backbone: The name of the vision transformer implementation.
 87            One of "sam", "sam2", "sam3", "cellpose_sam", "mae", "scalemae", "dinov2", "dinov3"
 88            (see all combinations below)
 89        encoder: The vision transformer. Can either be a name, such as "vit_b"
 90            (see all combinations for this below) or a torch module.
 91        decoder: The convolutional decoder.
 92        out_channels: The number of output channels of the UNETR.
 93        use_sam_stats: Whether to normalize the input data with the statistics of the
 94            pretrained SAM / SAM2 / SAM3 model.
 95        use_dino_stats: Whether to normalize the input data with the statistics of the
 96            pretrained DINOv2 / DINOv3 model.
 97        use_mae_stats: Whether to normalize the input data with the statistics of the pretrained MAE model.
 98        resize_input: Whether to resize the input images to match `img_size`.
 99            By default, it resizes the inputs to match the `img_size`.
100        encoder_checkpoint: Checkpoint for initializing the vision transformer.
101            Can either be a filepath or an already loaded checkpoint.
102        final_activation: The activation to apply to the UNETR output.
103        use_skip_connection: Whether to use skip connections. By default, it uses skip connections.
104        embed_dim: The embedding dimensionality, corresponding to the output dimension of the vision transformer.
105        use_conv_transpose: Whether to use transposed convolutions instead of resampling for upsampling.
106            By default, it uses resampling for upsampling.
107        perform_range_checks: Whether to validate the input value range before normalization on each forward pass.
108            You can disable the checks to avoid GPU sync overhead during training when inputs are known to be correct.
109        initial_features: The number of features of the finest decoder level. The features per level are
110            'initial_features * gain ** i', so this scales the decoder parameters quadratically.
111
112        NOTE: The currently supported combinations of 'backbone' x 'encoder' are the following:
113
114        SAM_family_models:
115            - 'sam' x 'vit_b'
116            - 'sam' x 'vit_l'
117            - 'sam' x 'vit_h'
118            - 'sam2' x 'hvit_t'
119            - 'sam2' x 'hvit_s'
120            - 'sam2' x 'hvit_b'
121            - 'sam2' x 'hvit_l'
122            - 'sam3' x 'vit_pe'
123            - 'cellpose_sam' x 'vit_l'
124
125        DINO_family_models:
126            - 'dinov2' x 'vit_s'
127            - 'dinov2' x 'vit_b'
128            - 'dinov2' x 'vit_l'
129            - 'dinov2' x 'vit_g'
130            - 'dinov2' x 'vit_s_reg4'
131            - 'dinov2' x 'vit_b_reg4'
132            - 'dinov2' x 'vit_l_reg4'
133            - 'dinov2' x 'vit_g_reg4'
134            - 'dinov3' x 'vit_s'
135            - 'dinov3' x 'vit_s+'
136            - 'dinov3' x 'vit_b'
137            - 'dinov3' x 'vit_l'
138            - 'dinov3' x 'vit_l+'
139            - 'dinov3' x 'vit_h+'
140            - 'dinov3' x 'vit_7b'
141
142        MAE_family_models:
143            - 'mae' x 'vit_b'
144            - 'mae' x 'vit_l'
145            - 'mae' x 'vit_h'
146            - 'scalemae' x 'vit_b'
147            - 'scalemae' x 'vit_l'
148            - 'scalemae' x 'vit_h'
149    """
150    def __init__(
151        self,
152        img_size: int = 1024,
153        backbone: Literal["sam", "sam2", "sam3", "cellpose_sam", "mae", "scalemae", "dinov2", "dinov3"] = "sam",
154        encoder: Optional[Union[nn.Module, str]] = "vit_b",
155        decoder: Optional[nn.Module] = None,
156        out_channels: int = 1,
157        use_sam_stats: bool = False,
158        use_mae_stats: bool = False,
159        use_dino_stats: bool = False,
160        resize_input: bool = True,
161        encoder_checkpoint: Optional[Union[str, OrderedDict]] = None,
162        final_activation: Optional[Union[str, nn.Module]] = None,
163        use_skip_connection: bool = True,
164        embed_dim: Optional[int] = None,
165        use_conv_transpose: bool = False,
166        perform_range_checks: bool = True,
167        initial_features: int = 64,
168        **kwargs
169    ) -> None:
170        super().__init__()
171
172        self.img_size = img_size
173        self.use_sam_stats = use_sam_stats
174        self.use_mae_stats = use_mae_stats
175        self.use_dino_stats = use_dino_stats
176        self.use_skip_connection = use_skip_connection
177        self.resize_input = resize_input
178        self.perform_range_checks = perform_range_checks
179        self.use_conv_transpose = use_conv_transpose
180        self.initial_features = initial_features
181        self.backbone = backbone
182
183        if isinstance(encoder, str):  # e.g. "vit_b" / "hvit_b" / "vit_pe"
184            print(f"Using {encoder} from {backbone.upper()}")
185            self.encoder = get_vision_transformer(img_size=img_size, backbone=backbone, model=encoder, **kwargs)
186
187            if encoder_checkpoint is not None:
188                self._load_encoder_from_checkpoint(backbone=backbone, encoder=encoder, checkpoint=encoder_checkpoint)
189
190            if embed_dim is None:
191                embed_dim = self.encoder.embed_dim
192
193            # For SAM1 encoder, if 'apply_neck' is applied, the embedding dimension must change.
194            if hasattr(self.encoder, "apply_neck") and self.encoder.apply_neck:
195                embed_dim = self.encoder.neck[2].out_channels  # the value is 256
196
197        else:  # `nn.Module` ViT backbone
198            self.encoder = encoder
199
200            have_neck = False
201            for name, _ in self.encoder.named_parameters():
202                if name.startswith("neck"):
203                    have_neck = True
204
205            if embed_dim is None:
206                if have_neck:
207                    embed_dim = self.encoder.neck[2].out_channels  # the value is 256
208                else:
209                    embed_dim = self.encoder.patch_embed.proj.out_channels
210
211        self.embed_dim = embed_dim
212        self.final_activation = self._get_activation(final_activation)
213
214    def _load_encoder_from_checkpoint(self, backbone, encoder, checkpoint):
215        """Function to load pretrained weights to the image encoder.
216        """
217        if isinstance(checkpoint, str):
218            if backbone == "sam" and isinstance(encoder, str):
219                # If we have a SAM encoder, then we first try to load the full SAM Model
220                # (using micro_sam) and otherwise fall back on directly loading the encoder state
221                # from the checkpoint
222                try:
223                    _, model = get_sam_model(model_type=encoder, checkpoint_path=checkpoint, return_sam=True)
224                    encoder_state = model.image_encoder.state_dict()
225                except Exception:
226                    # Try loading the encoder state directly from a checkpoint.
227                    encoder_state = torch.load(checkpoint, weights_only=False)
228
229            elif backbone == "cellpose_sam" and isinstance(encoder, str):
230                # The architecture matches CellposeSAM exactly (same rel_pos sizes),
231                # so weights load directly without any interpolation.
232                encoder_state = torch.load(checkpoint, map_location="cpu", weights_only=False)
233                # Handle DataParallel/DistributedDataParallel prefix.
234                if any(k.startswith("module.") for k in encoder_state.keys()):
235                    encoder_state = OrderedDict(
236                        {k[len("module."):]: v for k, v in encoder_state.items()}
237                    )
238                # Extract encoder weights from CellposeSAM checkpoint format (strip 'encoder.' prefix).
239                if any(k.startswith("encoder.") for k in encoder_state.keys()):
240                    encoder_state = OrderedDict(
241                        {k[len("encoder."):]: v for k, v in encoder_state.items() if k.startswith("encoder.")}
242                    )
243
244            elif backbone == "sam2" and isinstance(encoder, str):
245                # If we have a SAM2 encoder, then we first try to load the full SAM2 Model.
246                # (using micro_sam2) and otherwise fall back on directly loading the encoder state
247                # from the checkpoint
248                try:
249                    model = get_sam2_model(model_type=encoder, checkpoint_path=checkpoint)
250                    encoder_state = model.image_encoder.state_dict()
251                except Exception:
252                    # Try loading the encoder state directly from a checkpoint.
253                    encoder_state = torch.load(checkpoint, weights_only=False)
254
255            elif backbone == "sam3" and isinstance(encoder, str):
256                # If we have a SAM3 encoder, then we first try to load the full SAM3 Model.
257                # (using micro_sam3) and otherwise fall back on directly loading the encoder state
258                # from the checkpoint
259                try:
260                    model = get_sam3_model(checkpoint_path=checkpoint)
261                    encoder_state = model.backbone.vision_backbone.state_dict()
262                    # Let's align loading the encoder weights with expected parameter names
263                    encoder_state = {
264                        k[len("trunk."):] if k.startswith("trunk.") else k: v for k, v in encoder_state.items()
265                    }
266                    # And drop the 'convs' and 'sam2_convs' - these seem like some upsampling blocks.
267                    encoder_state = {
268                        k: v for k, v in encoder_state.items()
269                        if not (k.startswith("convs.") or k.startswith("sam2_convs."))
270                    }
271                except Exception:
272                    # Try loading the encoder state directly from a checkpoint.
273                    encoder_state = torch.load(checkpoint, weights_only=False)
274
275            elif backbone == "mae":
276                # vit initialization hints from:
277                #     - https://github.com/facebookresearch/mae/blob/main/main_finetune.py#L233-L242
278                encoder_state = torch.load(checkpoint, weights_only=False)["model"]
279                encoder_state = OrderedDict({
280                    k: v for k, v in encoder_state.items() if (k != "mask_token" and not k.startswith("decoder"))
281                })
282                # Let's remove the `head` from our current encoder (as the MAE pretrained don't expect it)
283                current_encoder_state = self.encoder.state_dict()
284                if ("head.weight" in current_encoder_state) and ("head.bias" in current_encoder_state):
285                    del self.encoder.head
286
287            elif backbone == "scalemae":
288                # Load the encoder state directly from a checkpoint.
289                encoder_state = torch.load(checkpoint)["model"]
290                encoder_state = OrderedDict({
291                    k: v for k, v in encoder_state.items()
292                    if not k.startswith(("mask_token", "decoder", "fcn", "fpn", "pos_embed"))
293                })
294
295                # Let's remove the `head` from our current encoder (as the MAE pretrained don't expect it)
296                current_encoder_state = self.encoder.state_dict()
297                if ("head.weight" in current_encoder_state) and ("head.bias" in current_encoder_state):
298                    del self.encoder.head
299
300                if "pos_embed" in current_encoder_state:  # NOTE: ScaleMAE uses 'pos. embeddings' in a diff. format.
301                    del self.encoder.pos_embed
302
303            elif backbone in ["dinov2", "dinov3"]:  # Load the encoder state directly from a checkpoint.
304                encoder_state = torch.load(checkpoint)
305
306            else:
307                raise ValueError(
308                    f"We don't support either the '{backbone}' backbone or the '{encoder}' model combination (or both)."
309                )
310
311        else:
312            encoder_state = checkpoint
313
314        self.encoder.load_state_dict(encoder_state)
315
316    def _get_activation(self, activation):
317        return_activation = None
318        if activation is None:
319            return None
320        if isinstance(activation, nn.Module):
321            return activation
322        if isinstance(activation, str):
323            return_activation = getattr(nn, activation, None)
324        if return_activation is None:
325            raise ValueError(f"Invalid activation: {activation}")
326
327        return return_activation()
328
329    @staticmethod
330    def get_preprocess_shape(oldh: int, oldw: int, long_side_length: int) -> Tuple[int, int]:
331        """Compute the output size given input size and target long side length.
332
333        Args:
334            oldh: The input image height.
335            oldw: The input image width.
336            long_side_length: The longest side length for resizing.
337
338        Returns:
339            The new image height.
340            The new image width.
341        """
342        scale = long_side_length * 1.0 / max(oldh, oldw)
343        newh, neww = oldh * scale, oldw * scale
344        neww = int(neww + 0.5)
345        newh = int(newh + 0.5)
346        return (newh, neww)
347
348    def resize_longest_side(self, image: torch.Tensor) -> torch.Tensor:
349        """Resize the image so that the longest side has the correct length.
350
351        Expects batched images with shape BxCxHxW OR BxCxDxHxW and float format.
352
353        Args:
354            image: The input image.
355
356        Returns:
357            The resized image.
358        """
359        if image.ndim == 4:  # i.e. 2d image
360            target_size = self.get_preprocess_shape(image.shape[2], image.shape[3], self.encoder.img_size)
361            return F.interpolate(image, target_size, mode="bilinear", align_corners=False, antialias=True)
362        elif image.ndim == 5:  # i.e. 3d volume
363            B, C, Z, H, W = image.shape
364            target_size = self.get_preprocess_shape(H, W, self.img_size)
365            return F.interpolate(image, (Z, *target_size), mode="trilinear", align_corners=False)
366        else:
367            raise ValueError("Expected 4d or 5d inputs, got", image.shape)
368
369    def _as_stats(self, mean, std, device, dtype, is_3d: bool):
370        """@private
371        """
372        return _as_stats(mean, std, device, dtype, is_3d)
373
374    def _check_input_normalization_range(self, x: torch.Tensor, expected_range: Optional[Tuple[float, float]]) -> None:
375        """@private
376        """
377        _check_input_normalization_range(x, expected_range)
378
379    def preprocess(self, x: torch.Tensor) -> torch.Tensor:
380        """@private
381        """
382        return preprocess_vit_inputs(
383            x,
384            use_sam_stats=self.use_sam_stats,
385            backbone=self.backbone,
386            use_mae_stats=self.use_mae_stats,
387            use_dino_stats=self.use_dino_stats,
388            resize_input=self.resize_input,
389            img_size=self.img_size,
390            encoder_img_size=self.encoder.img_size,
391            perform_range_checks=self.perform_range_checks,
392        )
393
394    def postprocess_masks(
395        self, masks: torch.Tensor, input_size: Tuple[int, ...], original_size: Tuple[int, ...],
396    ) -> torch.Tensor:
397        """@private
398        """
399        if masks.ndim == 4:  # i.e. 2d labels
400            masks = F.interpolate(
401                masks,
402                (self.encoder.img_size, self.encoder.img_size),
403                mode="bilinear",
404                align_corners=False,
405            )
406            masks = masks[..., : input_size[0], : input_size[1]]
407            masks = F.interpolate(masks, original_size, mode="bilinear", align_corners=False)
408
409        elif masks.ndim == 5:  # i.e. 3d volumetric labels
410            masks = F.interpolate(
411                masks,
412                (input_size[0], self.img_size, self.img_size),
413                mode="trilinear",
414                align_corners=False,
415            )
416            masks = masks[..., :input_size[0], :input_size[1], :input_size[2]]
417            masks = F.interpolate(masks, original_size, mode="trilinear", align_corners=False)
418
419        else:
420            raise ValueError("Expected 4d or 5d labels, got", masks.shape)
421
422        return masks

Base class for implementing a UNETR.

Arguments:
  • img_size: The size of the input for the image encoder. Input images will be resized to match this size.
  • backbone: The name of the vision transformer implementation. One of "sam", "sam2", "sam3", "cellpose_sam", "mae", "scalemae", "dinov2", "dinov3" (see all combinations below)
  • encoder: The vision transformer. Can either be a name, such as "vit_b" (see all combinations for this below) or a torch module.
  • decoder: The convolutional decoder.
  • out_channels: The number of output channels of the UNETR.
  • use_sam_stats: Whether to normalize the input data with the statistics of the pretrained SAM / SAM2 / SAM3 model.
  • use_dino_stats: Whether to normalize the input data with the statistics of the pretrained DINOv2 / DINOv3 model.
  • use_mae_stats: Whether to normalize the input data with the statistics of the pretrained MAE model.
  • resize_input: Whether to resize the input images to match img_size. By default, it resizes the inputs to match the img_size.
  • encoder_checkpoint: Checkpoint for initializing the vision transformer. Can either be a filepath or an already loaded checkpoint.
  • final_activation: The activation to apply to the UNETR output.
  • use_skip_connection: Whether to use skip connections. By default, it uses skip connections.
  • embed_dim: The embedding dimensionality, corresponding to the output dimension of the vision transformer.
  • use_conv_transpose: Whether to use transposed convolutions instead of resampling for upsampling. By default, it uses resampling for upsampling.
  • perform_range_checks: Whether to validate the input value range before normalization on each forward pass. You can disable the checks to avoid GPU sync overhead during training when inputs are known to be correct.
  • initial_features: The number of features of the finest decoder level. The features per level are 'initial_features * gain ** i', so this scales the decoder parameters quadratically.
  • NOTE: The currently supported combinations of 'backbone' x 'encoder' are the following:
  • SAM_family_models: - 'sam' x 'vit_b'
    • 'sam' x 'vit_l'
    • 'sam' x 'vit_h'
    • 'sam2' x 'hvit_t'
    • 'sam2' x 'hvit_s'
    • 'sam2' x 'hvit_b'
    • 'sam2' x 'hvit_l'
    • 'sam3' x 'vit_pe'
    • 'cellpose_sam' x 'vit_l'
  • DINO_family_models: - 'dinov2' x 'vit_s'
    • 'dinov2' x 'vit_b'
    • 'dinov2' x 'vit_l'
    • 'dinov2' x 'vit_g'
    • 'dinov2' x 'vit_s_reg4'
    • 'dinov2' x 'vit_b_reg4'
    • 'dinov2' x 'vit_l_reg4'
    • 'dinov2' x 'vit_g_reg4'
    • 'dinov3' x 'vit_s'
    • 'dinov3' x 'vit_s+'
    • 'dinov3' x 'vit_b'
    • 'dinov3' x 'vit_l'
    • 'dinov3' x 'vit_l+'
    • 'dinov3' x 'vit_h+'
    • 'dinov3' x 'vit_7b'
  • MAE_family_models: - 'mae' x 'vit_b'
    • 'mae' x 'vit_l'
    • 'mae' x 'vit_h'
    • 'scalemae' x 'vit_b'
    • 'scalemae' x 'vit_l'
    • 'scalemae' x 'vit_h'
UNETRBase( img_size: int = 1024, backbone: Literal['sam', 'sam2', 'sam3', 'cellpose_sam', 'mae', 'scalemae', 'dinov2', 'dinov3'] = 'sam', encoder: Union[torch.nn.modules.module.Module, str, NoneType] = 'vit_b', decoder: Optional[torch.nn.modules.module.Module] = None, out_channels: int = 1, use_sam_stats: bool = False, use_mae_stats: bool = False, use_dino_stats: bool = False, resize_input: bool = True, encoder_checkpoint: Union[str, collections.OrderedDict, NoneType] = None, final_activation: Union[torch.nn.modules.module.Module, str, NoneType] = None, use_skip_connection: bool = True, embed_dim: Optional[int] = None, use_conv_transpose: bool = False, perform_range_checks: bool = True, initial_features: int = 64, **kwargs)
150    def __init__(
151        self,
152        img_size: int = 1024,
153        backbone: Literal["sam", "sam2", "sam3", "cellpose_sam", "mae", "scalemae", "dinov2", "dinov3"] = "sam",
154        encoder: Optional[Union[nn.Module, str]] = "vit_b",
155        decoder: Optional[nn.Module] = None,
156        out_channels: int = 1,
157        use_sam_stats: bool = False,
158        use_mae_stats: bool = False,
159        use_dino_stats: bool = False,
160        resize_input: bool = True,
161        encoder_checkpoint: Optional[Union[str, OrderedDict]] = None,
162        final_activation: Optional[Union[str, nn.Module]] = None,
163        use_skip_connection: bool = True,
164        embed_dim: Optional[int] = None,
165        use_conv_transpose: bool = False,
166        perform_range_checks: bool = True,
167        initial_features: int = 64,
168        **kwargs
169    ) -> None:
170        super().__init__()
171
172        self.img_size = img_size
173        self.use_sam_stats = use_sam_stats
174        self.use_mae_stats = use_mae_stats
175        self.use_dino_stats = use_dino_stats
176        self.use_skip_connection = use_skip_connection
177        self.resize_input = resize_input
178        self.perform_range_checks = perform_range_checks
179        self.use_conv_transpose = use_conv_transpose
180        self.initial_features = initial_features
181        self.backbone = backbone
182
183        if isinstance(encoder, str):  # e.g. "vit_b" / "hvit_b" / "vit_pe"
184            print(f"Using {encoder} from {backbone.upper()}")
185            self.encoder = get_vision_transformer(img_size=img_size, backbone=backbone, model=encoder, **kwargs)
186
187            if encoder_checkpoint is not None:
188                self._load_encoder_from_checkpoint(backbone=backbone, encoder=encoder, checkpoint=encoder_checkpoint)
189
190            if embed_dim is None:
191                embed_dim = self.encoder.embed_dim
192
193            # For SAM1 encoder, if 'apply_neck' is applied, the embedding dimension must change.
194            if hasattr(self.encoder, "apply_neck") and self.encoder.apply_neck:
195                embed_dim = self.encoder.neck[2].out_channels  # the value is 256
196
197        else:  # `nn.Module` ViT backbone
198            self.encoder = encoder
199
200            have_neck = False
201            for name, _ in self.encoder.named_parameters():
202                if name.startswith("neck"):
203                    have_neck = True
204
205            if embed_dim is None:
206                if have_neck:
207                    embed_dim = self.encoder.neck[2].out_channels  # the value is 256
208                else:
209                    embed_dim = self.encoder.patch_embed.proj.out_channels
210
211        self.embed_dim = embed_dim
212        self.final_activation = self._get_activation(final_activation)

Initialize internal Module state, shared by both nn.Module and ScriptModule.

img_size
use_sam_stats
use_mae_stats
use_dino_stats
use_skip_connection
resize_input
perform_range_checks
use_conv_transpose
initial_features
backbone
embed_dim
final_activation
@staticmethod
def get_preprocess_shape(oldh: int, oldw: int, long_side_length: int) -> Tuple[int, int]:
329    @staticmethod
330    def get_preprocess_shape(oldh: int, oldw: int, long_side_length: int) -> Tuple[int, int]:
331        """Compute the output size given input size and target long side length.
332
333        Args:
334            oldh: The input image height.
335            oldw: The input image width.
336            long_side_length: The longest side length for resizing.
337
338        Returns:
339            The new image height.
340            The new image width.
341        """
342        scale = long_side_length * 1.0 / max(oldh, oldw)
343        newh, neww = oldh * scale, oldw * scale
344        neww = int(neww + 0.5)
345        newh = int(newh + 0.5)
346        return (newh, neww)

Compute the output size given input size and target long side length.

Arguments:
  • oldh: The input image height.
  • oldw: The input image width.
  • long_side_length: The longest side length for resizing.
Returns:

The new image height. The new image width.

def resize_longest_side(self, image: torch.Tensor) -> torch.Tensor:
348    def resize_longest_side(self, image: torch.Tensor) -> torch.Tensor:
349        """Resize the image so that the longest side has the correct length.
350
351        Expects batched images with shape BxCxHxW OR BxCxDxHxW and float format.
352
353        Args:
354            image: The input image.
355
356        Returns:
357            The resized image.
358        """
359        if image.ndim == 4:  # i.e. 2d image
360            target_size = self.get_preprocess_shape(image.shape[2], image.shape[3], self.encoder.img_size)
361            return F.interpolate(image, target_size, mode="bilinear", align_corners=False, antialias=True)
362        elif image.ndim == 5:  # i.e. 3d volume
363            B, C, Z, H, W = image.shape
364            target_size = self.get_preprocess_shape(H, W, self.img_size)
365            return F.interpolate(image, (Z, *target_size), mode="trilinear", align_corners=False)
366        else:
367            raise ValueError("Expected 4d or 5d inputs, got", image.shape)

Resize the image so that the longest side has the correct length.

Expects batched images with shape BxCxHxW OR BxCxDxHxW and float format.

Arguments:
  • image: The input image.
Returns:

The resized image.

def preprocess_vit_inputs( x: torch.Tensor, use_sam_stats: bool = False, backbone: str = 'sam', use_mae_stats: bool = False, use_dino_stats: bool = False, resize_input: bool = True, img_size: int = 1024, encoder_img_size: int = 1024, perform_range_checks: bool = True) -> Tuple[torch.Tensor, Tuple]:
425def preprocess_vit_inputs(
426    x: torch.Tensor,
427    use_sam_stats: bool = False,
428    backbone: str = "sam",
429    use_mae_stats: bool = False,
430    use_dino_stats: bool = False,
431    resize_input: bool = True,
432    img_size: int = 1024,
433    encoder_img_size: int = 1024,
434    perform_range_checks: bool = True,
435) -> Tuple[torch.Tensor, Tuple]:
436    """Preprocess inputs for ViT-backbones in UNETR models.
437
438    Handles normalization stat selection, input range validation, optional resizing to the longest side,
439    and padding to `encoder_img_size`. Can be used as a standalone function without a model instance.
440
441    Args:
442        x: Input tensor of shape (B, C, H, W) for 2D or (B, C, Z, H, W) for 3D.
443        use_sam_stats: Whether to normalize with SAM/SAM2/SAM3 backbone statistics.
444        backbone: The backbone name - controls which SAM stats are used when `use_sam_stats=True`.
445        use_mae_stats: Whether to normalize with MAE statistics.
446        use_dino_stats: Whether to normalize with DINOv2/DINOv3 statistics.
447        resize_input: Whether to resize the input to the longest side before padding.
448        img_size: The model image size, used for 3D resize.
449        encoder_img_size: The encoder image size, used for 2D resize and padding.
450        perform_range_checks: Whether to validate the expected input value range before normalization.
451            You can disable the checks to avoid GPU sync overhead during training when inputs are known to be correct.
452
453    Returns:
454        The preprocessed tensor and the spatial shape after resizing (before padding).
455    """
456    is_3d = (x.ndim == 5)
457    device, dtype = x.device, x.dtype
458    mean, std = (0.0, 0.0, 0.0), (1.0, 1.0, 1.0)
459    expected_range = None
460    unit_scale_max = None
461
462    if use_sam_stats:
463        if backbone == "sam2":
464            mean, std = (0.485, 0.456, 0.406), (0.229, 0.224, 0.225)
465            expected_range = (0.0, 1.0)
466        elif backbone == "sam3":
467            mean, std = (0.5, 0.5, 0.5), (0.5, 0.5, 0.5)
468            expected_range = (0.0, 1.0)
469        else:  # sam1 / default
470            mean, std = (123.675, 116.28, 103.53), (58.395, 57.12, 57.375)
471            expected_range = (0.0, 255.0)
472            unit_scale_max = 1.0
473    elif use_mae_stats:  # TODO: add mean std from mae / scalemae experiments (or open up arguments for this)
474        raise NotImplementedError
475    elif use_dino_stats:
476        mean, std = (0.485, 0.456, 0.406), (0.229, 0.224, 0.225)
477        expected_range = (0.0, 1.0)
478    else:
479        mean, std = (0.0, 0.0, 0.0), (1.0, 1.0, 1.0)
480        expected_range = None
481
482    if perform_range_checks:
483        _check_input_normalization_range(x, expected_range, unit_scale_max)
484    pixel_mean, pixel_std = _as_stats(mean, std, device=device, dtype=dtype, is_3d=is_3d)
485
486    if resize_input:
487        if x.ndim == 4:
488            target_size = UNETRBase.get_preprocess_shape(x.shape[2], x.shape[3], encoder_img_size)
489            x = F.interpolate(x, target_size, mode="bilinear", align_corners=False, antialias=True)
490        elif x.ndim == 5:
491            B, C, Z, H, W = x.shape
492            target_size = UNETRBase.get_preprocess_shape(H, W, img_size)
493            x = F.interpolate(x, (Z, *target_size), mode="trilinear", align_corners=False)
494
495    input_shape = x.shape[-3:] if is_3d else x.shape[-2:]
496
497    x = (x - pixel_mean) / pixel_std
498    h, w = x.shape[-2:]
499    padh = encoder_img_size - h
500    padw = encoder_img_size - w
501
502    if is_3d:
503        x = F.pad(x, (0, padw, 0, padh, 0, 0))
504    else:
505        x = F.pad(x, (0, padw, 0, padh))
506
507    return x, input_shape

Preprocess inputs for ViT-backbones in UNETR models.

Handles normalization stat selection, input range validation, optional resizing to the longest side, and padding to encoder_img_size. Can be used as a standalone function without a model instance.

Arguments:
  • x: Input tensor of shape (B, C, H, W) for 2D or (B, C, Z, H, W) for 3D.
  • use_sam_stats: Whether to normalize with SAM/SAM2/SAM3 backbone statistics.
  • backbone: The backbone name - controls which SAM stats are used when use_sam_stats=True.
  • use_mae_stats: Whether to normalize with MAE statistics.
  • use_dino_stats: Whether to normalize with DINOv2/DINOv3 statistics.
  • resize_input: Whether to resize the input to the longest side before padding.
  • img_size: The model image size, used for 3D resize.
  • encoder_img_size: The encoder image size, used for 2D resize and padding.
  • perform_range_checks: Whether to validate the expected input value range before normalization. You can disable the checks to avoid GPU sync overhead during training when inputs are known to be correct.
Returns:

The preprocessed tensor and the spatial shape after resizing (before padding).

class UNETR(UNETRBase):
510class UNETR(UNETRBase):
511    """A (2d-only) UNet Transformer using a vision transformer as encoder and a convolutional decoder.
512    """
513    def __init__(
514        self,
515        img_size: int = 1024,
516        backbone: Literal["sam", "sam2", "sam3", "cellpose_sam", "mae", "scalemae", "dinov2", "dinov3"] = "sam",
517        encoder: Optional[Union[nn.Module, str]] = "vit_b",
518        decoder: Optional[nn.Module] = None,
519        out_channels: int = 1,
520        use_sam_stats: bool = False,
521        use_mae_stats: bool = False,
522        use_dino_stats: bool = False,
523        resize_input: bool = True,
524        encoder_checkpoint: Optional[Union[str, OrderedDict]] = None,
525        final_activation: Optional[Union[str, nn.Module]] = None,
526        use_skip_connection: bool = True,
527        embed_dim: Optional[int] = None,
528        use_conv_transpose: bool = False,
529        perform_range_checks: bool = True,
530        **kwargs
531    ) -> None:
532
533        super().__init__(
534            img_size=img_size,
535            backbone=backbone,
536            encoder=encoder,
537            decoder=decoder,
538            out_channels=out_channels,
539            use_sam_stats=use_sam_stats,
540            use_mae_stats=use_mae_stats,
541            use_dino_stats=use_dino_stats,
542            resize_input=resize_input,
543            encoder_checkpoint=encoder_checkpoint,
544            final_activation=final_activation,
545            use_skip_connection=use_skip_connection,
546            embed_dim=embed_dim,
547            use_conv_transpose=use_conv_transpose,
548            perform_range_checks=perform_range_checks,
549            **kwargs,
550        )
551
552        encoder = self.encoder
553
554        if backbone == "sam2" and hasattr(encoder, "trunk"):
555            in_chans = encoder.trunk.patch_embed.proj.in_channels
556        elif hasattr(encoder, "in_chans"):
557            in_chans = encoder.in_chans
558        else:  # `nn.Module` ViT backbone.
559            try:
560                in_chans = encoder.patch_embed.proj.in_channels
561            except AttributeError:  # for getting the input channels while using 'vit_t' from MobileSam
562                in_chans = encoder.patch_embed.seq[0].c.in_channels
563
564        # parameters for the decoder network
565        depth = 3
566        gain = 2
567        features_decoder = [self.initial_features * gain ** i for i in range(depth + 1)][::-1]
568        scale_factors = depth * [2]
569        self.out_channels = out_channels
570
571        # choice of upsampler - to use (bilinear interpolation + conv) or conv transpose
572        _upsampler = SingleDeconv2DBlock if use_conv_transpose else Upsampler2d
573
574        self.decoder = decoder or Decoder(
575            features=features_decoder,
576            scale_factors=scale_factors[::-1],
577            conv_block_impl=ConvBlock2d,
578            sampler_impl=_upsampler,
579        )
580
581        if use_skip_connection:
582            self.deconv1 = Deconv2DBlock(
583                in_channels=self.embed_dim,
584                out_channels=features_decoder[0],
585                use_conv_transpose=use_conv_transpose,
586            )
587            self.deconv2 = nn.Sequential(
588                Deconv2DBlock(
589                    in_channels=self.embed_dim,
590                    out_channels=features_decoder[0],
591                    use_conv_transpose=use_conv_transpose,
592                ),
593                Deconv2DBlock(
594                    in_channels=features_decoder[0],
595                    out_channels=features_decoder[1],
596                    use_conv_transpose=use_conv_transpose,
597                )
598            )
599            self.deconv3 = nn.Sequential(
600                Deconv2DBlock(
601                    in_channels=self.embed_dim,
602                    out_channels=features_decoder[0],
603                    use_conv_transpose=use_conv_transpose,
604                ),
605                Deconv2DBlock(
606                    in_channels=features_decoder[0],
607                    out_channels=features_decoder[1],
608                    use_conv_transpose=use_conv_transpose,
609                ),
610                Deconv2DBlock(
611                    in_channels=features_decoder[1],
612                    out_channels=features_decoder[2],
613                    use_conv_transpose=use_conv_transpose,
614                )
615            )
616            self.deconv4 = ConvBlock2d(in_chans, features_decoder[-1])
617        else:
618            self.deconv1 = Deconv2DBlock(
619                in_channels=self.embed_dim,
620                out_channels=features_decoder[0],
621                use_conv_transpose=use_conv_transpose,
622            )
623            self.deconv2 = Deconv2DBlock(
624                in_channels=features_decoder[0],
625                out_channels=features_decoder[1],
626                use_conv_transpose=use_conv_transpose,
627            )
628            self.deconv3 = Deconv2DBlock(
629                in_channels=features_decoder[1],
630                out_channels=features_decoder[2],
631                use_conv_transpose=use_conv_transpose,
632            )
633            self.deconv4 = Deconv2DBlock(
634                in_channels=features_decoder[2],
635                out_channels=features_decoder[3],
636                use_conv_transpose=use_conv_transpose,
637            )
638
639        self.base = ConvBlock2d(self.embed_dim, features_decoder[0])
640        self.out_conv = nn.Conv2d(features_decoder[-1], out_channels, 1)
641        self.deconv_out = _upsampler(
642            scale_factor=2, in_channels=features_decoder[-1], out_channels=features_decoder[-1]
643        )
644        self.decoder_head = ConvBlock2d(2 * features_decoder[-1], features_decoder[-1])
645
646    def forward(self, x: torch.Tensor) -> torch.Tensor:
647        """Apply the UNETR to the input data.
648
649        Args:
650            x: The input tensor.
651
652        Returns:
653            The UNETR output.
654        """
655        original_shape = x.shape[-2:]
656
657        # Reshape the inputs to the shape expected by the encoder
658        # and normalize the inputs if normalization is part of the model.
659        x, input_shape = self.preprocess(x)
660
661        encoder_outputs = self.encoder(x)
662
663        if isinstance(encoder_outputs[-1], list):
664            # `encoder_outputs` can be arranged in only two forms:
665            #   - either we only return the image embeddings
666            #   - or, we return the image embeddings and the "list" of global attention layers
667            z12, from_encoder = encoder_outputs
668        else:
669            z12 = encoder_outputs
670
671        if self.use_skip_connection:
672            from_encoder = from_encoder[::-1]
673            z9 = self.deconv1(from_encoder[0])
674            z6 = self.deconv2(from_encoder[1])
675            z3 = self.deconv3(from_encoder[2])
676            z0 = self.deconv4(x)
677
678        else:
679            z9 = self.deconv1(z12)
680            z6 = self.deconv2(z9)
681            z3 = self.deconv3(z6)
682            z0 = self.deconv4(z3)
683
684        updated_from_encoder = [z9, z6, z3]
685
686        x = self.base(z12)
687        x = self.decoder(x, encoder_inputs=updated_from_encoder)
688        x = self.deconv_out(x)
689
690        x = torch.cat([x, z0], dim=1)
691        x = self.decoder_head(x)
692
693        x = self.out_conv(x)
694        if self.final_activation is not None:
695            x = self.final_activation(x)
696
697        x = self.postprocess_masks(x, input_shape, original_shape)
698        return x

A (2d-only) UNet Transformer using a vision transformer as encoder and a convolutional decoder.

UNETR( img_size: int = 1024, backbone: Literal['sam', 'sam2', 'sam3', 'cellpose_sam', 'mae', 'scalemae', 'dinov2', 'dinov3'] = 'sam', encoder: Union[torch.nn.modules.module.Module, str, NoneType] = 'vit_b', decoder: Optional[torch.nn.modules.module.Module] = None, out_channels: int = 1, use_sam_stats: bool = False, use_mae_stats: bool = False, use_dino_stats: bool = False, resize_input: bool = True, encoder_checkpoint: Union[str, collections.OrderedDict, NoneType] = None, final_activation: Union[torch.nn.modules.module.Module, str, NoneType] = None, use_skip_connection: bool = True, embed_dim: Optional[int] = None, use_conv_transpose: bool = False, perform_range_checks: bool = True, **kwargs)
513    def __init__(
514        self,
515        img_size: int = 1024,
516        backbone: Literal["sam", "sam2", "sam3", "cellpose_sam", "mae", "scalemae", "dinov2", "dinov3"] = "sam",
517        encoder: Optional[Union[nn.Module, str]] = "vit_b",
518        decoder: Optional[nn.Module] = None,
519        out_channels: int = 1,
520        use_sam_stats: bool = False,
521        use_mae_stats: bool = False,
522        use_dino_stats: bool = False,
523        resize_input: bool = True,
524        encoder_checkpoint: Optional[Union[str, OrderedDict]] = None,
525        final_activation: Optional[Union[str, nn.Module]] = None,
526        use_skip_connection: bool = True,
527        embed_dim: Optional[int] = None,
528        use_conv_transpose: bool = False,
529        perform_range_checks: bool = True,
530        **kwargs
531    ) -> None:
532
533        super().__init__(
534            img_size=img_size,
535            backbone=backbone,
536            encoder=encoder,
537            decoder=decoder,
538            out_channels=out_channels,
539            use_sam_stats=use_sam_stats,
540            use_mae_stats=use_mae_stats,
541            use_dino_stats=use_dino_stats,
542            resize_input=resize_input,
543            encoder_checkpoint=encoder_checkpoint,
544            final_activation=final_activation,
545            use_skip_connection=use_skip_connection,
546            embed_dim=embed_dim,
547            use_conv_transpose=use_conv_transpose,
548            perform_range_checks=perform_range_checks,
549            **kwargs,
550        )
551
552        encoder = self.encoder
553
554        if backbone == "sam2" and hasattr(encoder, "trunk"):
555            in_chans = encoder.trunk.patch_embed.proj.in_channels
556        elif hasattr(encoder, "in_chans"):
557            in_chans = encoder.in_chans
558        else:  # `nn.Module` ViT backbone.
559            try:
560                in_chans = encoder.patch_embed.proj.in_channels
561            except AttributeError:  # for getting the input channels while using 'vit_t' from MobileSam
562                in_chans = encoder.patch_embed.seq[0].c.in_channels
563
564        # parameters for the decoder network
565        depth = 3
566        gain = 2
567        features_decoder = [self.initial_features * gain ** i for i in range(depth + 1)][::-1]
568        scale_factors = depth * [2]
569        self.out_channels = out_channels
570
571        # choice of upsampler - to use (bilinear interpolation + conv) or conv transpose
572        _upsampler = SingleDeconv2DBlock if use_conv_transpose else Upsampler2d
573
574        self.decoder = decoder or Decoder(
575            features=features_decoder,
576            scale_factors=scale_factors[::-1],
577            conv_block_impl=ConvBlock2d,
578            sampler_impl=_upsampler,
579        )
580
581        if use_skip_connection:
582            self.deconv1 = Deconv2DBlock(
583                in_channels=self.embed_dim,
584                out_channels=features_decoder[0],
585                use_conv_transpose=use_conv_transpose,
586            )
587            self.deconv2 = nn.Sequential(
588                Deconv2DBlock(
589                    in_channels=self.embed_dim,
590                    out_channels=features_decoder[0],
591                    use_conv_transpose=use_conv_transpose,
592                ),
593                Deconv2DBlock(
594                    in_channels=features_decoder[0],
595                    out_channels=features_decoder[1],
596                    use_conv_transpose=use_conv_transpose,
597                )
598            )
599            self.deconv3 = nn.Sequential(
600                Deconv2DBlock(
601                    in_channels=self.embed_dim,
602                    out_channels=features_decoder[0],
603                    use_conv_transpose=use_conv_transpose,
604                ),
605                Deconv2DBlock(
606                    in_channels=features_decoder[0],
607                    out_channels=features_decoder[1],
608                    use_conv_transpose=use_conv_transpose,
609                ),
610                Deconv2DBlock(
611                    in_channels=features_decoder[1],
612                    out_channels=features_decoder[2],
613                    use_conv_transpose=use_conv_transpose,
614                )
615            )
616            self.deconv4 = ConvBlock2d(in_chans, features_decoder[-1])
617        else:
618            self.deconv1 = Deconv2DBlock(
619                in_channels=self.embed_dim,
620                out_channels=features_decoder[0],
621                use_conv_transpose=use_conv_transpose,
622            )
623            self.deconv2 = Deconv2DBlock(
624                in_channels=features_decoder[0],
625                out_channels=features_decoder[1],
626                use_conv_transpose=use_conv_transpose,
627            )
628            self.deconv3 = Deconv2DBlock(
629                in_channels=features_decoder[1],
630                out_channels=features_decoder[2],
631                use_conv_transpose=use_conv_transpose,
632            )
633            self.deconv4 = Deconv2DBlock(
634                in_channels=features_decoder[2],
635                out_channels=features_decoder[3],
636                use_conv_transpose=use_conv_transpose,
637            )
638
639        self.base = ConvBlock2d(self.embed_dim, features_decoder[0])
640        self.out_conv = nn.Conv2d(features_decoder[-1], out_channels, 1)
641        self.deconv_out = _upsampler(
642            scale_factor=2, in_channels=features_decoder[-1], out_channels=features_decoder[-1]
643        )
644        self.decoder_head = ConvBlock2d(2 * features_decoder[-1], features_decoder[-1])

Initialize internal Module state, shared by both nn.Module and ScriptModule.

out_channels
decoder
base
out_conv
deconv_out
decoder_head
def forward(self, x: torch.Tensor) -> torch.Tensor:
646    def forward(self, x: torch.Tensor) -> torch.Tensor:
647        """Apply the UNETR to the input data.
648
649        Args:
650            x: The input tensor.
651
652        Returns:
653            The UNETR output.
654        """
655        original_shape = x.shape[-2:]
656
657        # Reshape the inputs to the shape expected by the encoder
658        # and normalize the inputs if normalization is part of the model.
659        x, input_shape = self.preprocess(x)
660
661        encoder_outputs = self.encoder(x)
662
663        if isinstance(encoder_outputs[-1], list):
664            # `encoder_outputs` can be arranged in only two forms:
665            #   - either we only return the image embeddings
666            #   - or, we return the image embeddings and the "list" of global attention layers
667            z12, from_encoder = encoder_outputs
668        else:
669            z12 = encoder_outputs
670
671        if self.use_skip_connection:
672            from_encoder = from_encoder[::-1]
673            z9 = self.deconv1(from_encoder[0])
674            z6 = self.deconv2(from_encoder[1])
675            z3 = self.deconv3(from_encoder[2])
676            z0 = self.deconv4(x)
677
678        else:
679            z9 = self.deconv1(z12)
680            z6 = self.deconv2(z9)
681            z3 = self.deconv3(z6)
682            z0 = self.deconv4(z3)
683
684        updated_from_encoder = [z9, z6, z3]
685
686        x = self.base(z12)
687        x = self.decoder(x, encoder_inputs=updated_from_encoder)
688        x = self.deconv_out(x)
689
690        x = torch.cat([x, z0], dim=1)
691        x = self.decoder_head(x)
692
693        x = self.out_conv(x)
694        if self.final_activation is not None:
695            x = self.final_activation(x)
696
697        x = self.postprocess_masks(x, input_shape, original_shape)
698        return x

Apply the UNETR to the input data.

Arguments:
  • x: The input tensor.
Returns:

The UNETR output.

class UNETR2D(UNETR):
701class UNETR2D(UNETR):
702    """A two-dimensional UNet Transformer using a vision transformer as encoder and a convolutional decoder.
703    """
704    pass

A two-dimensional UNet Transformer using a vision transformer as encoder and a convolutional decoder.

class UNETR3D(UNETRBase):
707class UNETR3D(UNETRBase):
708    """A three dimensional UNet Transformer using a vision transformer as encoder and a convolutional decoder.
709    """
710    def __init__(
711        self,
712        img_size: int = 1024,
713        backbone: Literal["sam", "sam2", "sam3", "cellpose_sam", "mae", "scalemae", "dinov2", "dinov3"] = "sam",
714        encoder: Optional[Union[nn.Module, str]] = "hvit_b",
715        decoder: Optional[nn.Module] = None,
716        out_channels: int = 1,
717        use_sam_stats: bool = False,
718        use_mae_stats: bool = False,
719        use_dino_stats: bool = False,
720        resize_input: bool = True,
721        encoder_checkpoint: Optional[Union[str, OrderedDict]] = None,
722        final_activation: Optional[Union[str, nn.Module]] = None,
723        use_skip_connection: bool = False,
724        embed_dim: Optional[int] = None,
725        use_conv_transpose: bool = False,
726        use_strip_pooling: bool = True,
727        perform_range_checks: bool = True,
728        **kwargs
729    ):
730        if use_skip_connection:
731            raise NotImplementedError("The framework cannot handle skip connections atm.")
732        if use_conv_transpose:
733            raise NotImplementedError("It's not enabled to switch between interpolation and transposed convolutions.")
734
735        # Sort the `embed_dim` out
736        embed_dim = 256 if embed_dim is None else embed_dim
737
738        super().__init__(
739            img_size=img_size,
740            backbone=backbone,
741            encoder=encoder,
742            decoder=decoder,
743            out_channels=out_channels,
744            use_sam_stats=use_sam_stats,
745            use_mae_stats=use_mae_stats,
746            use_dino_stats=use_dino_stats,
747            resize_input=resize_input,
748            encoder_checkpoint=encoder_checkpoint,
749            final_activation=final_activation,
750            use_skip_connection=use_skip_connection,
751            embed_dim=embed_dim,
752            use_conv_transpose=use_conv_transpose,
753            perform_range_checks=perform_range_checks,
754            **kwargs,
755        )
756
757        # The 3d convolutional decoder.
758        # First, get the important parameters for the decoder.
759        depth = 3
760        gain = 2
761        features_decoder = [self.initial_features * gain ** i for i in range(depth + 1)][::-1]
762        scale_factors = [1, 2, 2]
763        self.out_channels = out_channels
764
765        # The mapping blocks.
766        self.deconv1 = Deconv3DBlock(
767            in_channels=embed_dim,
768            out_channels=features_decoder[0],
769            scale_factor=scale_factors,
770            use_strip_pooling=use_strip_pooling,
771        )
772        self.deconv2 = Deconv3DBlock(
773            in_channels=features_decoder[0],
774            out_channels=features_decoder[1],
775            scale_factor=scale_factors,
776            use_strip_pooling=use_strip_pooling,
777        )
778        self.deconv3 = Deconv3DBlock(
779            in_channels=features_decoder[1],
780            out_channels=features_decoder[2],
781            scale_factor=scale_factors,
782            use_strip_pooling=use_strip_pooling,
783        )
784        self.deconv4 = Deconv3DBlock(
785            in_channels=features_decoder[2],
786            out_channels=features_decoder[3],
787            scale_factor=scale_factors,
788            use_strip_pooling=use_strip_pooling,
789        )
790
791        # The core decoder block.
792        self.decoder = decoder or Decoder(
793            features=features_decoder,
794            scale_factors=[scale_factors] * depth,
795            conv_block_impl=partial(ConvBlock3dWithStrip, use_strip_pooling=use_strip_pooling),
796            sampler_impl=Upsampler3d,
797        )
798
799        # And the final upsampler to match the expected dimensions.
800        self.deconv_out = Deconv3DBlock(  # NOTE: changed `end_up` to `deconv_out`
801            in_channels=features_decoder[-1],
802            out_channels=features_decoder[-1],
803            scale_factor=scale_factors,
804            use_strip_pooling=use_strip_pooling,
805        )
806
807        # Additional conjunction blocks.
808        self.base = ConvBlock3dWithStrip(
809            in_channels=embed_dim,
810            out_channels=features_decoder[0],
811            use_strip_pooling=use_strip_pooling,
812        )
813
814        # And the output layers.
815        self.decoder_head = ConvBlock3dWithStrip(
816            in_channels=2 * features_decoder[-1],
817            out_channels=features_decoder[-1],
818            use_strip_pooling=use_strip_pooling,
819        )
820        self.out_conv = nn.Conv3d(features_decoder[-1], out_channels, 1)
821
822    def forward(self, x: torch.Tensor):
823        """Forward pass of the UNETR-3D model.
824
825        Args:
826            x: Inputs of expected shape (B, C, Z, Y, X), where Z considers flexible inputs.
827
828        Returns:
829            The UNETR output.
830        """
831        B, C, Z, H, W = x.shape
832        original_shape = (Z, H, W)
833
834        # Preprocessing step
835        x, input_shape = self.preprocess(x)
836
837        # Run the image encoder.
838        curr_features = torch.stack([self.encoder(x[:, :, i])[0] for i in range(Z)], dim=2)
839
840        # Prepare the counterparts for the decoder.
841        # NOTE: The section below is sequential, there's no skip connections atm.
842        z9 = self.deconv1(curr_features)
843        z6 = self.deconv2(z9)
844        z3 = self.deconv3(z6)
845        z0 = self.deconv4(z3)
846
847        updated_from_encoder = [z9, z6, z3]
848
849        # Align the features through the base block.
850        x = self.base(curr_features)
851        # Run the decoder
852        x = self.decoder(x, encoder_inputs=updated_from_encoder)
853        x = self.deconv_out(x)  # NOTE before `end_up`
854
855        # And the final output head.
856        x = torch.cat([x, z0], dim=1)
857        x = self.decoder_head(x)
858        x = self.out_conv(x)
859        if self.final_activation is not None:
860            x = self.final_activation(x)
861
862        # Postprocess the output back to original size.
863        x = self.postprocess_masks(x, input_shape, original_shape)
864        return x

A three dimensional UNet Transformer using a vision transformer as encoder and a convolutional decoder.

UNETR3D( img_size: int = 1024, backbone: Literal['sam', 'sam2', 'sam3', 'cellpose_sam', 'mae', 'scalemae', 'dinov2', 'dinov3'] = 'sam', encoder: Union[torch.nn.modules.module.Module, str, NoneType] = 'hvit_b', decoder: Optional[torch.nn.modules.module.Module] = None, out_channels: int = 1, use_sam_stats: bool = False, use_mae_stats: bool = False, use_dino_stats: bool = False, resize_input: bool = True, encoder_checkpoint: Union[str, collections.OrderedDict, NoneType] = None, final_activation: Union[torch.nn.modules.module.Module, str, NoneType] = None, use_skip_connection: bool = False, embed_dim: Optional[int] = None, use_conv_transpose: bool = False, use_strip_pooling: bool = True, perform_range_checks: bool = True, **kwargs)
710    def __init__(
711        self,
712        img_size: int = 1024,
713        backbone: Literal["sam", "sam2", "sam3", "cellpose_sam", "mae", "scalemae", "dinov2", "dinov3"] = "sam",
714        encoder: Optional[Union[nn.Module, str]] = "hvit_b",
715        decoder: Optional[nn.Module] = None,
716        out_channels: int = 1,
717        use_sam_stats: bool = False,
718        use_mae_stats: bool = False,
719        use_dino_stats: bool = False,
720        resize_input: bool = True,
721        encoder_checkpoint: Optional[Union[str, OrderedDict]] = None,
722        final_activation: Optional[Union[str, nn.Module]] = None,
723        use_skip_connection: bool = False,
724        embed_dim: Optional[int] = None,
725        use_conv_transpose: bool = False,
726        use_strip_pooling: bool = True,
727        perform_range_checks: bool = True,
728        **kwargs
729    ):
730        if use_skip_connection:
731            raise NotImplementedError("The framework cannot handle skip connections atm.")
732        if use_conv_transpose:
733            raise NotImplementedError("It's not enabled to switch between interpolation and transposed convolutions.")
734
735        # Sort the `embed_dim` out
736        embed_dim = 256 if embed_dim is None else embed_dim
737
738        super().__init__(
739            img_size=img_size,
740            backbone=backbone,
741            encoder=encoder,
742            decoder=decoder,
743            out_channels=out_channels,
744            use_sam_stats=use_sam_stats,
745            use_mae_stats=use_mae_stats,
746            use_dino_stats=use_dino_stats,
747            resize_input=resize_input,
748            encoder_checkpoint=encoder_checkpoint,
749            final_activation=final_activation,
750            use_skip_connection=use_skip_connection,
751            embed_dim=embed_dim,
752            use_conv_transpose=use_conv_transpose,
753            perform_range_checks=perform_range_checks,
754            **kwargs,
755        )
756
757        # The 3d convolutional decoder.
758        # First, get the important parameters for the decoder.
759        depth = 3
760        gain = 2
761        features_decoder = [self.initial_features * gain ** i for i in range(depth + 1)][::-1]
762        scale_factors = [1, 2, 2]
763        self.out_channels = out_channels
764
765        # The mapping blocks.
766        self.deconv1 = Deconv3DBlock(
767            in_channels=embed_dim,
768            out_channels=features_decoder[0],
769            scale_factor=scale_factors,
770            use_strip_pooling=use_strip_pooling,
771        )
772        self.deconv2 = Deconv3DBlock(
773            in_channels=features_decoder[0],
774            out_channels=features_decoder[1],
775            scale_factor=scale_factors,
776            use_strip_pooling=use_strip_pooling,
777        )
778        self.deconv3 = Deconv3DBlock(
779            in_channels=features_decoder[1],
780            out_channels=features_decoder[2],
781            scale_factor=scale_factors,
782            use_strip_pooling=use_strip_pooling,
783        )
784        self.deconv4 = Deconv3DBlock(
785            in_channels=features_decoder[2],
786            out_channels=features_decoder[3],
787            scale_factor=scale_factors,
788            use_strip_pooling=use_strip_pooling,
789        )
790
791        # The core decoder block.
792        self.decoder = decoder or Decoder(
793            features=features_decoder,
794            scale_factors=[scale_factors] * depth,
795            conv_block_impl=partial(ConvBlock3dWithStrip, use_strip_pooling=use_strip_pooling),
796            sampler_impl=Upsampler3d,
797        )
798
799        # And the final upsampler to match the expected dimensions.
800        self.deconv_out = Deconv3DBlock(  # NOTE: changed `end_up` to `deconv_out`
801            in_channels=features_decoder[-1],
802            out_channels=features_decoder[-1],
803            scale_factor=scale_factors,
804            use_strip_pooling=use_strip_pooling,
805        )
806
807        # Additional conjunction blocks.
808        self.base = ConvBlock3dWithStrip(
809            in_channels=embed_dim,
810            out_channels=features_decoder[0],
811            use_strip_pooling=use_strip_pooling,
812        )
813
814        # And the output layers.
815        self.decoder_head = ConvBlock3dWithStrip(
816            in_channels=2 * features_decoder[-1],
817            out_channels=features_decoder[-1],
818            use_strip_pooling=use_strip_pooling,
819        )
820        self.out_conv = nn.Conv3d(features_decoder[-1], out_channels, 1)

Initialize internal Module state, shared by both nn.Module and ScriptModule.

out_channels
deconv1
deconv2
deconv3
deconv4
decoder
deconv_out
base
decoder_head
out_conv
def forward(self, x: torch.Tensor):
822    def forward(self, x: torch.Tensor):
823        """Forward pass of the UNETR-3D model.
824
825        Args:
826            x: Inputs of expected shape (B, C, Z, Y, X), where Z considers flexible inputs.
827
828        Returns:
829            The UNETR output.
830        """
831        B, C, Z, H, W = x.shape
832        original_shape = (Z, H, W)
833
834        # Preprocessing step
835        x, input_shape = self.preprocess(x)
836
837        # Run the image encoder.
838        curr_features = torch.stack([self.encoder(x[:, :, i])[0] for i in range(Z)], dim=2)
839
840        # Prepare the counterparts for the decoder.
841        # NOTE: The section below is sequential, there's no skip connections atm.
842        z9 = self.deconv1(curr_features)
843        z6 = self.deconv2(z9)
844        z3 = self.deconv3(z6)
845        z0 = self.deconv4(z3)
846
847        updated_from_encoder = [z9, z6, z3]
848
849        # Align the features through the base block.
850        x = self.base(curr_features)
851        # Run the decoder
852        x = self.decoder(x, encoder_inputs=updated_from_encoder)
853        x = self.deconv_out(x)  # NOTE before `end_up`
854
855        # And the final output head.
856        x = torch.cat([x, z0], dim=1)
857        x = self.decoder_head(x)
858        x = self.out_conv(x)
859        if self.final_activation is not None:
860            x = self.final_activation(x)
861
862        # Postprocess the output back to original size.
863        x = self.postprocess_masks(x, input_shape, original_shape)
864        return x

Forward pass of the UNETR-3D model.

Arguments:
  • x: Inputs of expected shape (B, C, Z, Y, X), where Z considers flexible inputs.
Returns:

The UNETR output.