torch_em.model.unetr
1from os import PathLike 2from functools import partial 3from collections.abc import Mapping 4from typing import Callable, Optional, Tuple, Union 5 6import torch 7import torch.nn as nn 8import torch.nn.functional as F 9 10from .util import VIT_CONFIGS 11from .vit import get_vision_transformer 12from .unet import Decoder, ConvBlock2d, ConvBlock3d, Upsampler2d, Upsampler3d, _update_conv_kwargs 13 14 15# 16# UNETR WITH PRETRAINED ENCODERS 17# 18 19 20def _convert_hf_dinov3_state(state, reference_state): 21 """Convert a HuggingFace DINOv3 state to the layout of the original DINOv3 code.""" 22 renames = [ 23 ("embeddings.patch_embeddings", "patch_embed.proj"), ("embeddings.register_tokens", "storage_tokens"), 24 ("embeddings.", ""), ("layer.", "blocks."), ("attention.o_proj", "attn.proj"), 25 ("layer_scale1.lambda1", "ls1.gamma"), ("layer_scale2.lambda1", "ls2.gamma"), 26 ("up_proj", "fc1"), ("down_proj", "fc2"), 27 ] 28 converted = {} 29 for key, value in state.items(): 30 # The model of transformers stores the blocks under 'model.layer', but the embeddings without a prefix. 31 key = key.removeprefix("model.") 32 for old, new in renames: 33 key = key.replace(old, new) 34 converted[key] = value 35 36 # HuggingFace splits the fused qkv projection and drops the bias of the k projection. 37 for q_key in [key for key in converted if ".attention.q_proj." in key]: 38 q, v = converted.pop(q_key), converted.pop(q_key.replace("q_proj", "v_proj")) 39 k = converted.pop(q_key.replace("q_proj", "k_proj"), torch.zeros_like(v)) 40 converted[q_key.replace("attention.q_proj", "attn.qkv")] = torch.cat([q, k, v]) 41 converted["mask_token"] = converted["mask_token"].flatten(0, 1) 42 43 # HuggingFace does not store the RoPE periods and the qkv bias masks. 44 converted.update({k: v for k, v in reference_state.items() if k.endswith(("rope_embed.periods", "bias_mask"))}) 45 return converted 46 47 48def get_unetr_model( 49 ndim: int, 50 backbone: str, 51 model_type: Union[str, nn.Module], 52 encoder_checkpoint: Optional[Union[str, PathLike, Mapping]] = None, 53 init_decoder: Optional[Callable[[nn.Module], nn.Module]] = None, 54 **init_kwargs, 55) -> nn.Module: 56 """Create a UNETR with a ViT encoder for 2D or 3D segmentation. 57 58 Pass `encoder_checkpoint` to load encoder weights. 59 Torchvision loads its default pretrained weights without a checkpoint. 60 Set `pretrained=False` to disable this torchvision download. 61 3D models apply the encoder to each slice and use a 3D convolutional decoder. 62 63 Args: 64 ndim: The number of spatial dimensions. Use 2 or 3. 65 backbone: The ViT family supported by `get_vision_transformer`: "sam", "sam2", "sam3", "cellpose_sam", 66 "mae", "scalemae", "dinov2", "dinov3", or "torchvision". 67 Use a custom name for an injected encoder from another family. 68 model_type: The encoder name or a module from `get_vision_transformer`. Examples: "hvit_t" for SAM2, 69 "vit_b_reg4" for DINOv2, or "vit_b_16" for torchvision. SAM weight names can include training suffixes. 70 Custom modules must expose `img_size`, `embed_dim`, and `in_chans`. 71 They return feature maps or a tuple of feature maps and intermediate features. 72 encoder_checkpoint: The encoder checkpoint, as a file path or a loaded state dictionary. 73 init_decoder: The function that initializes the decoder with pretrained weights. Use this only for 2D models. 74 init_kwargs: The keyword arguments that override the defaults for UNETR2D or UNETR3D. 75 The factory selects the image size and normalization for the backbone. 76 It disables skip connections when the encoder features do not match the decoder. 77 78 Returns: 79 The UNETR model. 80 """ 81 if ndim not in (2, 3): 82 raise ValueError(f"Invalid ndim={ndim}. Use 2 or 3.") 83 if init_decoder is not None and ndim != 2: 84 raise ValueError(f"`init_decoder` is only supported for 2D models, got ndim={ndim}.") 85 86 config = VIT_CONFIGS.get(backbone) 87 if config is None and not isinstance(model_type, nn.Module): 88 raise ValueError(f"Unsupported backbone '{backbone}'.") 89 90 # Get the model class. 91 model_class = UNETR2D if ndim == 2 else UNETR3D 92 if isinstance(model_type, str): 93 encoder = "_".join(model_type.split("_")[:2]) if config.weight_suffixes else model_type 94 img_size = config.img_size 95 encoder_name = encoder 96 elif isinstance(model_type, nn.Module): 97 encoder = model_type 98 img_size = encoder.img_size 99 encoder_name = getattr(encoder, "model_type", None) 100 else: 101 raise TypeError("The model_type must be an encoder name or a torch module.") 102 103 model_kwargs = {"img_size": img_size} 104 if config is not None and config.stats is not None: 105 model_kwargs[f"use_{config.stats}_stats"] = True 106 compatible_skips = config is not None and encoder_name in config.skip_models 107 if ndim == 2 and (not compatible_skips or getattr(encoder, "apply_neck", False)): 108 model_kwargs["use_skip_connection"] = False 109 if backbone == "torchvision" and encoder_checkpoint is not None: 110 model_kwargs["pretrained"] = False 111 model_kwargs.update(init_kwargs) 112 113 model = model_class(backbone=backbone, encoder=encoder, encoder_checkpoint=encoder_checkpoint, **model_kwargs) 114 if init_decoder is not None: 115 model = init_decoder(model) 116 117 return model 118 119 120# 121# UNETR IMPLEMENTATION [Vision Transformer (ViT from SAM / CellposeSAM / SAM2 / SAM3 / 122# DINOv2 / DINOv3 / MAE / ScaleMAE / torchvision) + UNet Decoder from `torch_em`] 123# 124 125 126def _check_input_normalization_range( 127 x: torch.Tensor, 128 expected_range: Optional[Tuple[float, float]], 129 unit_scale_max: Optional[float] = None, 130 range_tolerance: float = 1e-4, 131) -> None: 132 """Check whether raw inputs match the value range expected by the model normalizer. 133 134 Args: 135 x: The input tensor to validate. 136 expected_range: The (min, max) value range the input must lie within. If None, all checks are skipped. 137 unit_scale_max: If set, raises an error when the input's maximum value is at or below this threshold, 138 catching inputs that are likely in the wrong scale (e.g. [0, 1] instead of [0, 255] for SAM1). 139 range_tolerance: The allowed deviation outside of the expected range, as a fraction of the range width. 140 It accepts float rounding errors, for example from augmentations. 141 For float inputs, the function uses at least 4 times the machine epsilon of the input dtype. 142 """ 143 if expected_range is None: 144 return 145 146 if not torch.all(torch.isfinite(x)): 147 raise ValueError("The input contains NaN or infinite values before normalization.") 148 149 min_value, max_value = expected_range 150 if torch.is_floating_point(x): 151 range_tolerance = max(range_tolerance, 4 * torch.finfo(x.dtype).eps) 152 tolerance = range_tolerance * (max_value - min_value) 153 154 # Compare Python floats, because a half precision dtype rounds off the tolerance. 155 actual_min, actual_max = (value.item() for value in torch.aminmax(x.detach())) 156 if actual_min < min_value - tolerance or actual_max > max_value + tolerance: 157 raise ValueError( 158 f"The input values [{actual_min}, {actual_max}] are outside of the expected range " 159 f"[{min_value}, {max_value}] with a tolerance of {tolerance:.2g}. " 160 "Make sure that the scale of the raw inputs matches the pretrained normalization, [0, 1] or [0, 255]." 161 ) 162 163 if unit_scale_max is not None and actual_max <= unit_scale_max: 164 raise ValueError( 165 f"The maximum input value {actual_max:.4f} is too small for the expected range [{min_value}, {max_value}]. " 166 "Scale the raw inputs to [0, 255] instead of [0, 1]." 167 ) 168 169 170def _as_stats(mean, std, device, dtype, is_3d: bool): 171 view_shape = (1, -1, 1, 1, 1) if is_3d else (1, -1, 1, 1) 172 pixel_mean = torch.tensor(mean, device=device, dtype=dtype).view(*view_shape) 173 pixel_std = torch.tensor(std, device=device, dtype=dtype).view(*view_shape) 174 return pixel_mean, pixel_std 175 176 177class UNETRBase(nn.Module): 178 """Base class for implementing a UNETR. 179 180 Args: 181 img_size: The size of the input for the image encoder. Input images will be resized to match this size. 182 backbone: The name of the vision transformer implementation. 183 One of "sam", "sam2", "sam3", "cellpose_sam", "mae", "scalemae", "dinov2", "dinov3" 184 (see all combinations below) 185 encoder: The vision transformer. Can either be a name, such as "vit_b" 186 (see all combinations for this below) or a torch module. 187 decoder: The convolutional decoder. 188 out_channels: The number of output channels of the UNETR. 189 use_sam_stats: Whether to normalize the input data with the statistics of the 190 pretrained SAM / SAM2 / SAM3 model. 191 use_dino_stats: Whether to normalize the input data with the statistics of the 192 pretrained DINOv2 / DINOv3 model. 193 use_imagenet_stats: Whether to normalize with standard ImageNet statistics, i.e. 194 mean - (0.485, 0.456, 0.406) and std - (0.229, 0.224, 0.225), raw inputs between range [0, 1]. 195 Use this with the 'torchvision' backbone when loading pretrained weights. 196 use_mae_stats: Whether to normalize the input data with the statistics of the pretrained MAE model. 197 resize_input: Whether to resize the input images to match `img_size`. 198 By default, it resizes the inputs to match the `img_size`. 199 encoder_checkpoint: Checkpoint for initializing the vision transformer. 200 Can either be a filepath or an already loaded checkpoint. 201 final_activation: The activation to apply to the UNETR output. 202 use_skip_connection: Whether to use skip connections. By default, it uses skip connections. 203 embed_dim: The embedding dimensionality, corresponding to the output dimension of the vision transformer. 204 use_conv_transpose: Whether to use transposed convolutions instead of resampling for upsampling. 205 By default, it uses resampling for upsampling. 206 perform_range_checks: Whether to validate the input value range before normalization on each forward pass. 207 You can disable the checks to avoid GPU sync overhead during training when inputs are known to be correct. 208 initial_features: The number of features of the finest decoder level. The features per level are 209 'initial_features * gain ** i', so this scales the decoder parameters quadratically. 210 211 NOTE: The currently supported combinations of 'backbone' x 'encoder' are the following: 212 213 SAM_family_models: 214 - 'sam' x 'vit_b' 215 - 'sam' x 'vit_l' 216 - 'sam' x 'vit_h' 217 - 'sam2' x 'hvit_t' 218 - 'sam2' x 'hvit_s' 219 - 'sam2' x 'hvit_b' 220 - 'sam2' x 'hvit_l' 221 - 'sam3' x 'vit_pe' 222 - 'cellpose_sam' x 'vit_l' 223 224 DINO_family_models: 225 - 'dinov2' x 'vit_s' 226 - 'dinov2' x 'vit_b' 227 - 'dinov2' x 'vit_l' 228 - 'dinov2' x 'vit_g' 229 - 'dinov2' x 'vit_s_reg4' 230 - 'dinov2' x 'vit_b_reg4' 231 - 'dinov2' x 'vit_l_reg4' 232 - 'dinov2' x 'vit_g_reg4' 233 - 'dinov3' x 'vit_s' 234 - 'dinov3' x 'vit_s+' 235 - 'dinov3' x 'vit_b' 236 - 'dinov3' x 'vit_l' 237 - 'dinov3' x 'vit_l+' 238 - 'dinov3' x 'vit_h+' 239 - 'dinov3' x 'vit_7b' 240 241 MAE_family_models: 242 - 'mae' x 'vit_b' 243 - 'mae' x 'vit_l' 244 - 'mae' x 'vit_h' 245 - 'scalemae' x 'vit_b' 246 - 'scalemae' x 'vit_l' 247 - 'scalemae' x 'vit_h' 248 249 torchvision_models: 250 - 'torchvision' x 'vit_b_16' 251 - 'torchvision' x 'vit_b_32' 252 - 'torchvision' x 'vit_l_16' 253 - 'torchvision' x 'vit_l_32' 254 - 'torchvision' x 'vit_h_14' 255 """ 256 def __init__( 257 self, 258 img_size: int = 1024, 259 backbone: str = "sam", 260 encoder: Optional[Union[nn.Module, str]] = "vit_b", 261 decoder: Optional[nn.Module] = None, 262 out_channels: int = 1, 263 use_sam_stats: bool = False, 264 use_mae_stats: bool = False, 265 use_dino_stats: bool = False, 266 use_imagenet_stats: bool = False, 267 resize_input: bool = True, 268 encoder_checkpoint: Optional[Union[str, PathLike, Mapping]] = None, 269 final_activation: Optional[Union[str, nn.Module]] = None, 270 use_skip_connection: bool = True, 271 embed_dim: Optional[int] = None, 272 use_conv_transpose: bool = False, 273 perform_range_checks: bool = True, 274 initial_features: int = 64, 275 **kwargs 276 ) -> None: 277 super().__init__() 278 279 self.img_size = img_size 280 self.use_sam_stats = use_sam_stats 281 self.use_mae_stats = use_mae_stats 282 self.use_dino_stats = use_dino_stats 283 self.use_imagenet_stats = use_imagenet_stats 284 self.use_skip_connection = use_skip_connection 285 self.resize_input = resize_input 286 self.perform_range_checks = perform_range_checks 287 self.use_conv_transpose = use_conv_transpose 288 self.initial_features = initial_features 289 self.backbone = backbone 290 291 if isinstance(encoder, str): # e.g. "vit_b" / "hvit_b" / "vit_pe" 292 print(f"Using {encoder} from {backbone.upper()}") 293 self.encoder = get_vision_transformer(img_size=img_size, backbone=backbone, model=encoder, **kwargs) 294 else: # `nn.Module` ViT backbone 295 self.encoder = encoder 296 297 if encoder_checkpoint is not None: 298 self._load_encoder_from_checkpoint(backbone, encoder_checkpoint) 299 300 # Sort the `embed_dim` out using the encoder output width. 301 if embed_dim is None: 302 have_neck = hasattr(self.encoder, "neck") and not hasattr(self.encoder, "embed_dim") 303 # For SAM1 encoder, if 'apply_neck' is applied, the embedding dimension must change. 304 if getattr(self.encoder, "apply_neck", have_neck): 305 embed_dim = self.encoder.neck[2].out_channels # 256 for SAM1. 306 elif hasattr(self.encoder, "embed_dim"): 307 embed_dim = self.encoder.embed_dim 308 else: 309 embed_dim = self.encoder.patch_embed.proj.out_channels 310 311 self.embed_dim = embed_dim 312 self.final_activation = self._get_activation(final_activation) 313 314 def _load_encoder_from_checkpoint(self, backbone, checkpoint): 315 """Load encoder weights from an encoder state or a full model checkpoint.""" 316 if isinstance(checkpoint, (str, PathLike)): 317 # Load the checkpoint directly; extract encoder weights from full model checkpoints below. 318 encoder_state = torch.load(checkpoint, map_location="cpu", weights_only=False) 319 else: 320 encoder_state = checkpoint 321 322 for state_key in ("model_state", "model", "state_dict"): 323 if state_key in encoder_state: 324 encoder_state = encoder_state[state_key] 325 # Handle DataParallel/DistributedDataParallel prefix. 326 encoder_state = {key.removeprefix("module."): value for key, value in encoder_state.items()} 327 328 if backbone in ("sam", "sam2"): 329 # Extract the image encoder state from the full SAM or SAM2 checkpoint. 330 prefixes = ("image_encoder.", "encoder.") 331 elif backbone == "sam3": 332 # Align the trunk names and leave out the neck's 'convs' and 'sam2_convs' upsampling blocks. 333 prefixes = ( 334 "detector.backbone.vision_backbone.trunk.", "backbone.vision_backbone.trunk.", 335 "vision_backbone.trunk.", "trunk.", "encoder." 336 ) 337 elif backbone == "torchvision": 338 prefixes = ("encoder.",) if "encoder.conv_proj.weight" in encoder_state else () 339 else: 340 # CellposeSAM matches the pretrained rel_pos sizes, so its weights need no interpolation. 341 # Extract encoder weights from CellposeSAM checkpoint format (strip 'encoder.' prefix). 342 prefixes = ("encoder.",) 343 for prefix in prefixes: 344 if any(key.startswith(prefix) for key in encoder_state): 345 encoder_state = { 346 key[len(prefix):]: value for key, value in encoder_state.items() if key.startswith(prefix) 347 } 348 break 349 350 if backbone in ("mae", "scalemae"): 351 # ViT initialization hints from: 352 # https://github.com/facebookresearch/mae/blob/main/main_finetune.py#L233-L242 353 excluded = ("mask_token", "decoder", "head.") 354 if backbone == "scalemae": 355 # NOTE: ScaleMAE computes positional embeddings from the input resolution. 356 excluded += ("fcn", "fpn", "pos_embed") 357 if hasattr(self.encoder, "pos_embed"): 358 del self.encoder.pos_embed 359 encoder_state = {key: value for key, value in encoder_state.items() if not key.startswith(excluded)} 360 # Remove the classification head, which the MAE pretrained encoder weights do not need. 361 if hasattr(self.encoder, "head"): 362 del self.encoder.head 363 elif backbone == "torchvision": 364 encoder_state = {k: v for k, v in encoder_state.items() if not k.startswith("heads.")} 365 elif backbone == "dinov3" and "embeddings.patch_embeddings.weight" in encoder_state: 366 # DINOv3 from HuggingFace, e.g. from micro-sam. The original checkpoints of Meta need no conversion. 367 encoder_state = _convert_hf_dinov3_state(encoder_state, self.encoder.state_dict()) 368 elif backbone == "dinov2" and encoder_state["pos_embed"].shape != self.encoder.pos_embed.shape: 369 # DINOv2 from the original checkpoints of Meta, which micro-sam also uses. 370 # Resize the grid of the pretrained positional embeddings to the input size. 371 cls_pos, patch_pos = encoder_state["pos_embed"][:, :1], encoder_state["pos_embed"][:, 1:] 372 size = int(patch_pos.shape[1] ** 0.5) 373 scale = (self.encoder.img_size // self.encoder.patch_size + self.encoder.interpolate_offset) / size 374 patch_pos = patch_pos.reshape(1, size, size, -1).permute(0, 3, 1, 2) 375 patch_pos = F.interpolate(patch_pos, scale_factor=scale, mode="bicubic") 376 encoder_state["pos_embed"] = torch.cat([cls_pos, patch_pos.flatten(2).transpose(1, 2)], dim=1) 377 378 self.encoder.load_state_dict(encoder_state) 379 380 def _get_activation(self, activation): 381 return_activation = None 382 if activation is None: 383 return None 384 if isinstance(activation, nn.Module): 385 return activation 386 if isinstance(activation, str): 387 return_activation = getattr(nn, activation, None) 388 if return_activation is None: 389 raise ValueError(f"Invalid activation: {activation}") 390 391 return return_activation() 392 393 @staticmethod 394 def get_preprocess_shape(oldh: int, oldw: int, long_side_length: int) -> Tuple[int, int]: 395 """Compute the output size given input size and target long side length. 396 397 Args: 398 oldh: The input image height. 399 oldw: The input image width. 400 long_side_length: The longest side length for resizing. 401 402 Returns: 403 The new image height. 404 The new image width. 405 """ 406 scale = long_side_length * 1.0 / max(oldh, oldw) 407 newh, neww = oldh * scale, oldw * scale 408 neww = int(neww + 0.5) 409 newh = int(newh + 0.5) 410 return (newh, neww) 411 412 def resize_longest_side(self, image: torch.Tensor) -> torch.Tensor: 413 """Resize the image so that the longest side has the correct length. 414 415 Expects batched images with shape BxCxHxW OR BxCxDxHxW and float format. 416 417 Args: 418 image: The input image. 419 420 Returns: 421 The resized image. 422 """ 423 if image.ndim == 4: # i.e. 2d image 424 target_size = self.get_preprocess_shape(image.shape[2], image.shape[3], self.encoder.img_size) 425 return F.interpolate(image, target_size, mode="bilinear", align_corners=False, antialias=True) 426 elif image.ndim == 5: # i.e. 3d volume 427 B, C, Z, H, W = image.shape 428 target_size = self.get_preprocess_shape(H, W, self.img_size) 429 return F.interpolate(image, (Z, *target_size), mode="trilinear", align_corners=False) 430 else: 431 raise ValueError("Expected 4d or 5d inputs, got", image.shape) 432 433 def _as_stats(self, mean, std, device, dtype, is_3d: bool): 434 """@private 435 """ 436 return _as_stats(mean, std, device, dtype, is_3d) 437 438 def _check_input_normalization_range(self, x: torch.Tensor, expected_range: Optional[Tuple[float, float]]) -> None: 439 """@private 440 """ 441 _check_input_normalization_range(x, expected_range) 442 443 def encode(self, x: torch.Tensor): 444 """Preprocess the input and run the image encoder. 445 446 Args: 447 x: The input tensor. 448 449 Returns: 450 The encoder features to pass to `decode` and the spatial shape after preprocessing. 451 """ 452 raise NotImplementedError 453 454 def decode(self, features, input_shape: Tuple[int, ...], original_shape: Tuple[int, ...]) -> torch.Tensor: 455 """Run the convolutional decoder on the encoder features. 456 457 Args: 458 features: The encoder features returned by `encode`. 459 input_shape: The spatial shape after preprocessing, returned by `encode`. 460 original_shape: The spatial shape of the original input. 461 462 Returns: 463 The UNETR output, resized to `original_shape`. 464 """ 465 raise NotImplementedError 466 467 def forward(self, x: torch.Tensor) -> torch.Tensor: 468 """Apply the UNETR to the input data. 469 470 Args: 471 x: The input tensor. 472 473 Returns: 474 The UNETR output. 475 """ 476 features, input_shape = self.encode(x) 477 return self.decode(features, input_shape, tuple(x.shape[2:])) 478 479 def _encode_image(self, x): 480 # Encoders can return either: 481 # - only the image embeddings 482 # - image embeddings and intermediate attention features 483 features = self.encoder(x) 484 return (features, None) if isinstance(features, torch.Tensor) else features 485 486 def preprocess(self, x: torch.Tensor) -> torch.Tensor: 487 """@private 488 """ 489 return preprocess_vit_inputs( 490 x, 491 use_sam_stats=self.use_sam_stats, 492 backbone=self.backbone, 493 use_mae_stats=self.use_mae_stats, 494 use_dino_stats=self.use_dino_stats, 495 use_imagenet_stats=self.use_imagenet_stats, 496 resize_input=self.resize_input, 497 img_size=self.img_size, 498 encoder_img_size=self.encoder.img_size, 499 perform_range_checks=self.perform_range_checks, 500 ) 501 502 def postprocess_masks( 503 self, masks: torch.Tensor, input_size: Tuple[int, ...], original_size: Tuple[int, ...], 504 ) -> torch.Tensor: 505 """@private 506 """ 507 if masks.ndim == 4: # i.e. 2d labels 508 masks = F.interpolate( 509 masks, 510 (self.encoder.img_size, self.encoder.img_size), 511 mode="bilinear", 512 align_corners=False, 513 ) 514 masks = masks[..., : input_size[0], : input_size[1]] 515 masks = F.interpolate(masks, original_size, mode="bilinear", align_corners=False) 516 517 elif masks.ndim == 5: # i.e. 3d volumetric labels 518 masks = F.interpolate( 519 masks, 520 (input_size[0], self.img_size, self.img_size), 521 mode="trilinear", 522 align_corners=False, 523 ) 524 masks = masks[..., :input_size[0], :input_size[1], :input_size[2]] 525 masks = F.interpolate(masks, original_size, mode="trilinear", align_corners=False) 526 527 else: 528 raise ValueError("Expected 4d or 5d labels, got", masks.shape) 529 530 return masks 531 532 533def preprocess_vit_inputs( 534 x: torch.Tensor, 535 use_sam_stats: bool = False, 536 backbone: str = "sam", 537 use_mae_stats: bool = False, 538 use_dino_stats: bool = False, 539 use_imagenet_stats: bool = False, 540 resize_input: bool = True, 541 img_size: int = 1024, 542 encoder_img_size: int = 1024, 543 perform_range_checks: bool = True, 544) -> Tuple[torch.Tensor, Tuple]: 545 """Preprocess inputs for ViT-backbones in UNETR models. 546 547 Handles normalization stat selection, input range validation, optional resizing to the longest side, 548 and padding to `encoder_img_size`. Can be used as a standalone function without a model instance. 549 550 Args: 551 x: Input tensor of shape (B, C, H, W) for 2D or (B, C, Z, H, W) for 3D. 552 use_sam_stats: Whether to normalize with SAM/SAM2/SAM3 backbone statistics. 553 backbone: The backbone name - controls which SAM stats are used when `use_sam_stats=True`. 554 use_mae_stats: Whether to normalize with MAE statistics. 555 use_dino_stats: Whether to normalize with DINOv2/DINOv3 statistics. 556 use_imagenet_stats: Whether to normalize with standard ImageNet statistics 557 (mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225), range [0, 1]). 558 Use this for torchvision pretrained backbones. 559 resize_input: Whether to resize the input to the longest side before padding. 560 img_size: The model image size, used for 3D resize. 561 encoder_img_size: The encoder image size, used for 2D resize and padding. 562 perform_range_checks: Whether to validate the expected input value range before normalization. 563 You can disable the checks to avoid GPU sync overhead during training when inputs are known to be correct. 564 565 Returns: 566 The preprocessed tensor and the spatial shape after resizing (before padding). 567 """ 568 is_3d = (x.ndim == 5) 569 device, dtype = x.device, x.dtype 570 mean, std = (0.0, 0.0, 0.0), (1.0, 1.0, 1.0) 571 expected_range = None 572 unit_scale_max = None 573 574 if use_sam_stats: 575 if backbone == "sam2": 576 mean, std = (0.485, 0.456, 0.406), (0.229, 0.224, 0.225) 577 expected_range = (0.0, 1.0) 578 elif backbone == "sam3": 579 mean, std = (0.5, 0.5, 0.5), (0.5, 0.5, 0.5) 580 expected_range = (0.0, 1.0) 581 else: # sam1 / default 582 mean, std = (123.675, 116.28, 103.53), (58.395, 57.12, 57.375) 583 expected_range = (0.0, 255.0) 584 unit_scale_max = 1.0 585 elif use_mae_stats or use_dino_stats or use_imagenet_stats: 586 # MAE, ScaleMAE, and DINO use the same ImageNet mean and standard deviation. 587 mean, std = (0.485, 0.456, 0.406), (0.229, 0.224, 0.225) 588 expected_range = (0.0, 1.0) 589 else: 590 mean, std = (0.0, 0.0, 0.0), (1.0, 1.0, 1.0) 591 expected_range = None 592 593 if perform_range_checks: 594 _check_input_normalization_range(x, expected_range, unit_scale_max) 595 pixel_mean, pixel_std = _as_stats(mean, std, device=device, dtype=dtype, is_3d=is_3d) 596 597 if resize_input: 598 if x.ndim == 4: 599 target_size = UNETRBase.get_preprocess_shape(x.shape[2], x.shape[3], encoder_img_size) 600 x = F.interpolate(x, target_size, mode="bilinear", align_corners=False, antialias=True) 601 elif x.ndim == 5: 602 B, C, Z, H, W = x.shape 603 target_size = UNETRBase.get_preprocess_shape(H, W, img_size) 604 x = F.interpolate(x, (Z, *target_size), mode="trilinear", align_corners=False) 605 606 input_shape = x.shape[-3:] if is_3d else x.shape[-2:] 607 608 x = (x - pixel_mean) / pixel_std 609 h, w = x.shape[-2:] 610 padh = encoder_img_size - h 611 padw = encoder_img_size - w 612 613 if is_3d: 614 x = F.pad(x, (0, padw, 0, padh, 0, 0)) 615 else: 616 x = F.pad(x, (0, padw, 0, padh)) 617 618 return x, input_shape 619 620 621class UNETR(UNETRBase): 622 """A (2d-only) UNet Transformer using a vision transformer as encoder and a convolutional decoder. 623 """ 624 def __init__( 625 self, 626 img_size: int = 1024, 627 backbone: str = "sam", 628 encoder: Optional[Union[nn.Module, str]] = "vit_b", 629 decoder: Optional[nn.Module] = None, 630 out_channels: int = 1, 631 use_sam_stats: bool = False, 632 use_mae_stats: bool = False, 633 use_dino_stats: bool = False, 634 use_imagenet_stats: bool = False, 635 resize_input: bool = True, 636 encoder_checkpoint: Optional[Union[str, PathLike, Mapping]] = None, 637 final_activation: Optional[Union[str, nn.Module]] = None, 638 use_skip_connection: bool = True, 639 embed_dim: Optional[int] = None, 640 use_conv_transpose: bool = False, 641 perform_range_checks: bool = True, 642 **kwargs 643 ) -> None: 644 645 super().__init__( 646 img_size=img_size, 647 backbone=backbone, 648 encoder=encoder, 649 decoder=decoder, 650 out_channels=out_channels, 651 use_sam_stats=use_sam_stats, 652 use_mae_stats=use_mae_stats, 653 use_dino_stats=use_dino_stats, 654 use_imagenet_stats=use_imagenet_stats, 655 resize_input=resize_input, 656 encoder_checkpoint=encoder_checkpoint, 657 final_activation=final_activation, 658 use_skip_connection=use_skip_connection, 659 embed_dim=embed_dim, 660 use_conv_transpose=use_conv_transpose, 661 perform_range_checks=perform_range_checks, 662 **kwargs, 663 ) 664 self.init_kwargs = { 665 "img_size": img_size, "backbone": backbone, "encoder": encoder, "decoder": decoder, 666 "out_channels": out_channels, "use_sam_stats": use_sam_stats, 667 "use_mae_stats": use_mae_stats, "use_dino_stats": use_dino_stats, 668 "use_imagenet_stats": use_imagenet_stats, "resize_input": resize_input, 669 "final_activation": final_activation, 670 "use_skip_connection": use_skip_connection, "embed_dim": embed_dim, 671 "use_conv_transpose": use_conv_transpose, "perform_range_checks": perform_range_checks, 672 **kwargs, 673 } 674 675 encoder = self.encoder 676 677 if backbone == "sam2" and hasattr(encoder, "trunk"): 678 in_chans = encoder.trunk.patch_embed.proj.in_channels 679 elif hasattr(encoder, "in_chans"): 680 in_chans = encoder.in_chans 681 else: # `nn.Module` ViT backbone. 682 try: 683 in_chans = encoder.patch_embed.proj.in_channels 684 except AttributeError: # for getting the input channels while using 'vit_t' from MobileSam 685 in_chans = encoder.patch_embed.seq[0].c.in_channels 686 687 # parameters for the decoder network 688 depth = 3 689 gain = 2 690 features_decoder = [self.initial_features * gain ** i for i in range(depth + 1)][::-1] 691 scale_factors = depth * [2] 692 self.out_channels = out_channels 693 694 # choice of upsampler - to use (bilinear interpolation + conv) or conv transpose 695 _upsampler = SingleDeconv2DBlock if use_conv_transpose else Upsampler2d 696 697 self.decoder = decoder or Decoder( 698 features=features_decoder, 699 scale_factors=scale_factors[::-1], 700 conv_block_impl=ConvBlock2d, 701 sampler_impl=_upsampler, 702 ) 703 704 if use_skip_connection: 705 self.deconv1 = Deconv2DBlock( 706 in_channels=self.embed_dim, 707 out_channels=features_decoder[0], 708 use_conv_transpose=use_conv_transpose, 709 ) 710 self.deconv2 = nn.Sequential( 711 Deconv2DBlock( 712 in_channels=self.embed_dim, 713 out_channels=features_decoder[0], 714 use_conv_transpose=use_conv_transpose, 715 ), 716 Deconv2DBlock( 717 in_channels=features_decoder[0], 718 out_channels=features_decoder[1], 719 use_conv_transpose=use_conv_transpose, 720 ) 721 ) 722 self.deconv3 = nn.Sequential( 723 Deconv2DBlock( 724 in_channels=self.embed_dim, 725 out_channels=features_decoder[0], 726 use_conv_transpose=use_conv_transpose, 727 ), 728 Deconv2DBlock( 729 in_channels=features_decoder[0], 730 out_channels=features_decoder[1], 731 use_conv_transpose=use_conv_transpose, 732 ), 733 Deconv2DBlock( 734 in_channels=features_decoder[1], 735 out_channels=features_decoder[2], 736 use_conv_transpose=use_conv_transpose, 737 ) 738 ) 739 self.deconv4 = ConvBlock2d(in_chans, features_decoder[-1]) 740 else: 741 self.deconv1 = Deconv2DBlock( 742 in_channels=self.embed_dim, 743 out_channels=features_decoder[0], 744 use_conv_transpose=use_conv_transpose, 745 ) 746 self.deconv2 = Deconv2DBlock( 747 in_channels=features_decoder[0], 748 out_channels=features_decoder[1], 749 use_conv_transpose=use_conv_transpose, 750 ) 751 self.deconv3 = Deconv2DBlock( 752 in_channels=features_decoder[1], 753 out_channels=features_decoder[2], 754 use_conv_transpose=use_conv_transpose, 755 ) 756 self.deconv4 = Deconv2DBlock( 757 in_channels=features_decoder[2], 758 out_channels=features_decoder[3], 759 use_conv_transpose=use_conv_transpose, 760 ) 761 762 self.base = ConvBlock2d(self.embed_dim, features_decoder[0]) 763 self.out_conv = nn.Conv2d(features_decoder[-1], out_channels, 1) 764 self.deconv_out = _upsampler( 765 scale_factor=2, in_channels=features_decoder[-1], out_channels=features_decoder[-1] 766 ) 767 self.decoder_head = ConvBlock2d(2 * features_decoder[-1], features_decoder[-1]) 768 769 def encode(self, x: torch.Tensor): 770 """Preprocess the input and run the image encoder. 771 772 Args: 773 x: The input tensor of shape (B, C, Y, X). 774 775 Returns: 776 The features as a tuple of the image embeddings, the list of intermediate encoder outputs 777 (None if the encoder returns only the embeddings) and the preprocessed input, which the 778 skip connections consume, and the spatial shape after preprocessing. 779 """ 780 # Reshape the inputs to the shape expected by the encoder 781 # and normalize the inputs if normalization is part of the model. 782 x, input_shape = self.preprocess(x) 783 z12, from_encoder = self._encode_image(x) 784 return (z12, from_encoder, x), input_shape 785 786 def decode(self, features, input_shape: Tuple[int, ...], original_shape: Tuple[int, ...]) -> torch.Tensor: 787 """Run the convolutional decoder on the encoder features. 788 789 Args: 790 features: The tuple returned by `encode`. 791 input_shape: The spatial shape (Y, X) after preprocessing. 792 original_shape: The spatial shape (Y, X) of the original input. 793 794 Returns: 795 The UNETR output, resized to `original_shape`. 796 """ 797 z12, from_encoder, x = features 798 799 if self.use_skip_connection: 800 from_encoder = from_encoder[::-1] 801 z9 = self.deconv1(from_encoder[0]) 802 z6 = self.deconv2(from_encoder[1]) 803 z3 = self.deconv3(from_encoder[2]) 804 z0 = self.deconv4(x) 805 806 else: 807 z9 = self.deconv1(z12) 808 z6 = self.deconv2(z9) 809 z3 = self.deconv3(z6) 810 z0 = self.deconv4(z3) 811 812 updated_from_encoder = [z9, z6, z3] 813 814 x = self.base(z12) 815 x = self.decoder(x, encoder_inputs=updated_from_encoder) 816 x = self.deconv_out(x) 817 818 x = torch.cat([x, z0], dim=1) 819 x = self.decoder_head(x) 820 821 x = self.out_conv(x) 822 if self.final_activation is not None: 823 x = self.final_activation(x) 824 825 return self.postprocess_masks(x, input_shape, original_shape) 826 827 828class UNETR2D(UNETR): 829 """A two-dimensional UNet Transformer using a vision transformer as encoder and a convolutional decoder. 830 """ 831 pass 832 833 834class UNETR3D(UNETRBase): 835 """A 3D UNet Transformer with a vision transformer encoder and a convolutional decoder.""" 836 def __init__( 837 self, 838 img_size: int = 1024, 839 backbone: str = "sam", 840 encoder: Optional[Union[nn.Module, str]] = "vit_b", 841 decoder: Optional[nn.Module] = None, 842 out_channels: int = 1, 843 use_sam_stats: bool = False, 844 use_mae_stats: bool = False, 845 use_dino_stats: bool = False, 846 use_imagenet_stats: bool = False, 847 resize_input: bool = True, 848 encoder_checkpoint: Optional[Union[str, PathLike, Mapping]] = None, 849 final_activation: Optional[Union[str, nn.Module]] = None, 850 use_skip_connection: bool = False, 851 embed_dim: Optional[int] = None, 852 use_conv_transpose: bool = False, 853 use_strip_pooling: bool = True, 854 perform_range_checks: bool = True, 855 **kwargs 856 ): 857 if use_skip_connection: 858 raise NotImplementedError("The framework cannot handle skip connections atm.") 859 if use_conv_transpose: 860 raise NotImplementedError("It's not enabled to switch between interpolation and transposed convolutions.") 861 862 super().__init__( 863 img_size=img_size, 864 backbone=backbone, 865 encoder=encoder, 866 decoder=decoder, 867 out_channels=out_channels, 868 use_sam_stats=use_sam_stats, 869 use_mae_stats=use_mae_stats, 870 use_dino_stats=use_dino_stats, 871 use_imagenet_stats=use_imagenet_stats, 872 resize_input=resize_input, 873 encoder_checkpoint=encoder_checkpoint, 874 final_activation=final_activation, 875 use_skip_connection=use_skip_connection, 876 embed_dim=embed_dim, 877 use_conv_transpose=use_conv_transpose, 878 perform_range_checks=perform_range_checks, 879 **kwargs, 880 ) 881 self.init_kwargs = { 882 "img_size": img_size, "backbone": backbone, "encoder": encoder, "decoder": decoder, 883 "out_channels": out_channels, "use_sam_stats": use_sam_stats, 884 "use_mae_stats": use_mae_stats, "use_dino_stats": use_dino_stats, 885 "use_imagenet_stats": use_imagenet_stats, "resize_input": resize_input, 886 "final_activation": final_activation, 887 "use_skip_connection": use_skip_connection, "embed_dim": embed_dim, 888 "use_conv_transpose": use_conv_transpose, "use_strip_pooling": use_strip_pooling, 889 "perform_range_checks": perform_range_checks, **kwargs, 890 } 891 892 # The 3d convolutional decoder. 893 # First, get the important parameters for the decoder. 894 depth = 3 895 gain = 2 896 features_decoder = [self.initial_features * gain ** i for i in range(depth + 1)][::-1] 897 scale_factors = [1, 2, 2] 898 self.out_channels = out_channels 899 900 # The mapping blocks. 901 self.deconv1 = Deconv3DBlock( 902 in_channels=self.embed_dim, 903 out_channels=features_decoder[0], 904 scale_factor=scale_factors, 905 use_strip_pooling=use_strip_pooling, 906 ) 907 self.deconv2 = Deconv3DBlock( 908 in_channels=features_decoder[0], 909 out_channels=features_decoder[1], 910 scale_factor=scale_factors, 911 use_strip_pooling=use_strip_pooling, 912 ) 913 self.deconv3 = Deconv3DBlock( 914 in_channels=features_decoder[1], 915 out_channels=features_decoder[2], 916 scale_factor=scale_factors, 917 use_strip_pooling=use_strip_pooling, 918 ) 919 self.deconv4 = Deconv3DBlock( 920 in_channels=features_decoder[2], 921 out_channels=features_decoder[3], 922 scale_factor=scale_factors, 923 use_strip_pooling=use_strip_pooling, 924 ) 925 926 # The core decoder block. 927 self.decoder = decoder or Decoder( 928 features=features_decoder, 929 scale_factors=[scale_factors] * depth, 930 conv_block_impl=partial(ConvBlock3dWithStrip, use_strip_pooling=use_strip_pooling), 931 sampler_impl=Upsampler3d, 932 ) 933 934 # And the final upsampler to match the expected dimensions. 935 self.deconv_out = Deconv3DBlock( # NOTE: changed `end_up` to `deconv_out` 936 in_channels=features_decoder[-1], 937 out_channels=features_decoder[-1], 938 scale_factor=scale_factors, 939 use_strip_pooling=use_strip_pooling, 940 ) 941 942 # Additional conjunction blocks. 943 self.base = ConvBlock3dWithStrip( 944 in_channels=self.embed_dim, out_channels=features_decoder[0], use_strip_pooling=use_strip_pooling 945 ) 946 947 # And the output layers. 948 self.decoder_head = ConvBlock3dWithStrip( 949 in_channels=2 * features_decoder[-1], 950 out_channels=features_decoder[-1], 951 use_strip_pooling=use_strip_pooling 952 ) 953 self.out_conv = nn.Conv3d(features_decoder[-1], out_channels, 1) 954 955 def encode(self, x: torch.Tensor): 956 """Preprocess the input and run the image encoder on every z-slice. 957 958 Args: 959 x: The input tensor with shape (B, C, Z, Y, X). The depth Z can vary. 960 961 Returns: 962 The encoder features of shape (B, D, Z, Y', X') and the spatial shape after preprocessing. 963 """ 964 Z = x.shape[2] 965 x, input_shape = self.preprocess(x) 966 features = torch.stack([self._encode_image(x[:, :, i])[0] for i in range(Z)], dim=2) 967 return features, input_shape 968 969 def decode(self, features, input_shape: Tuple[int, ...], original_shape: Tuple[int, ...]) -> torch.Tensor: 970 """Run the convolutional decoder on the encoder features. 971 972 Args: 973 features: The encoder features with shape (B, D, Z, Y', X'), returned by `encode`. 974 input_shape: The spatial shape (Z, Y, X) after preprocessing. 975 original_shape: The spatial shape (Z, Y, X) of the original input. 976 977 Returns: 978 The UNETR output, resized to `original_shape`. 979 """ 980 # Prepare the counterparts for the decoder. 981 # NOTE: These mappings are sequential; they do not use intermediate encoder skip connections. 982 z9 = self.deconv1(features) 983 z6 = self.deconv2(z9) 984 z3 = self.deconv3(z6) 985 z0 = self.deconv4(z3) 986 987 updated_from_encoder = [z9, z6, z3] 988 989 # Align the features through the base block. 990 x = self.base(features) 991 # Run the decoder 992 x = self.decoder(x, encoder_inputs=updated_from_encoder) 993 x = self.deconv_out(x) # NOTE: formerly `end_up`. 994 995 # And the final output head. 996 x = torch.cat([x, z0], dim=1) 997 x = self.decoder_head(x) 998 x = self.out_conv(x) 999 if self.final_activation is not None: 1000 x = self.final_activation(x) 1001 1002 # Postprocess the output back to original size. 1003 return self.postprocess_masks(x, input_shape, original_shape) 1004 1005 1006# 1007# ADDITIONAL FUNCTIONALITIES 1008# 1009 1010 1011def _strip_pooling_layers(enabled, channels) -> nn.Module: 1012 return DepthStripPooling(channels) if enabled else nn.Identity() 1013 1014 1015class DepthStripPooling(nn.Module): 1016 """@private 1017 """ 1018 def __init__(self, channels: int, reduction: int = 4): 1019 """Block for strip pooling along the depth dimension (only). 1020 1021 eg. for 3D (Z > 1) - it aggregates global context across depth by adaptive avg pooling 1022 to Z=1, and then passes through a small 1x1x1 MLP, then broadcasts it back to Z to 1023 modulate the original features (using a gated residual). 1024 1025 For 2D (Z == 1): returns input unchanged (no-op). 1026 1027 Args: 1028 channels: The output channels. 1029 reduction: The reduction of the hidden layers. 1030 """ 1031 super().__init__() 1032 hidden = max(1, channels // reduction) 1033 self.conv1 = nn.Conv3d(channels, hidden, kernel_size=1) 1034 self.bn1 = nn.BatchNorm3d(hidden) 1035 self.relu = nn.ReLU(inplace=True) 1036 self.conv2 = nn.Conv3d(hidden, channels, kernel_size=1) 1037 1038 def forward(self, x: torch.Tensor) -> torch.Tensor: 1039 if x.dim() != 5: 1040 raise ValueError(f"DepthStripPooling expects 5D tensors as input, got '{x.shape}'.") 1041 1042 B, C, Z, H, W = x.shape 1043 if Z == 1: # i.e. always the case of all 2d. 1044 return x # We simply do nothing there. 1045 1046 # We pool only along the depth dimension: i.e. target shape (B, C, 1, H, W). 1047 # A plain mean over Z is the same operation as adaptive_avg_pool3d to (1, H, W), but its 1048 # reduction kernel is several times faster at full resolution. 1049 feat = x.mean(dim=2, keepdim=True) 1050 feat = self.conv1(feat) 1051 feat = self.bn1(feat) 1052 feat = self.relu(feat) 1053 feat = self.conv2(feat) 1054 gate = torch.sigmoid(feat).expand(B, C, Z, H, W) # Broadcast the collapsed depth context back to all slices 1055 1056 # Gated residual fusion 1057 return x * gate + x 1058 1059 1060class Deconv3DBlock(nn.Module): 1061 """@private 1062 """ 1063 def __init__( 1064 self, 1065 scale_factor, 1066 in_channels, 1067 out_channels, 1068 kernel_size=3, 1069 anisotropic_kernel=True, 1070 use_strip_pooling=True, 1071 ): 1072 super().__init__() 1073 conv_block_kwargs = { 1074 "in_channels": out_channels, 1075 "out_channels": out_channels, 1076 "kernel_size": kernel_size, 1077 "padding": ((kernel_size - 1) // 2), 1078 } 1079 if anisotropic_kernel: 1080 conv_block_kwargs = _update_conv_kwargs(conv_block_kwargs, scale_factor) 1081 1082 self.block = nn.Sequential( 1083 Upsampler3d(scale_factor, in_channels, out_channels), 1084 nn.Conv3d(**conv_block_kwargs), 1085 nn.BatchNorm3d(out_channels), 1086 nn.ReLU(True), 1087 _strip_pooling_layers(enabled=use_strip_pooling, channels=out_channels), 1088 ) 1089 1090 def forward(self, x): 1091 return self.block(x) 1092 1093 1094class ConvBlock3dWithStrip(nn.Module): 1095 """@private 1096 """ 1097 def __init__( 1098 self, in_channels: int, out_channels: int, use_strip_pooling: bool = True, **kwargs 1099 ): 1100 super().__init__() 1101 self.block = nn.Sequential( 1102 ConvBlock3d(in_channels, out_channels, **kwargs), 1103 _strip_pooling_layers(enabled=use_strip_pooling, channels=out_channels), 1104 ) 1105 1106 def forward(self, x): 1107 return self.block(x) 1108 1109 1110class SingleDeconv2DBlock(nn.Module): 1111 """@private 1112 """ 1113 def __init__(self, scale_factor, in_channels, out_channels): 1114 super().__init__() 1115 self.block = nn.ConvTranspose2d(in_channels, out_channels, kernel_size=2, stride=2, padding=0, output_padding=0) 1116 1117 def forward(self, x): 1118 return self.block(x) 1119 1120 1121class SingleConv2DBlock(nn.Module): 1122 """@private 1123 """ 1124 def __init__(self, in_channels, out_channels, kernel_size): 1125 super().__init__() 1126 self.block = nn.Conv2d( 1127 in_channels, out_channels, kernel_size=kernel_size, stride=1, padding=((kernel_size - 1) // 2) 1128 ) 1129 1130 def forward(self, x): 1131 return self.block(x) 1132 1133 1134class Conv2DBlock(nn.Module): 1135 """@private 1136 """ 1137 def __init__(self, in_channels, out_channels, kernel_size=3): 1138 super().__init__() 1139 self.block = nn.Sequential( 1140 SingleConv2DBlock(in_channels, out_channels, kernel_size), 1141 nn.BatchNorm2d(out_channels), 1142 nn.ReLU(True) 1143 ) 1144 1145 def forward(self, x): 1146 return self.block(x) 1147 1148 1149class Deconv2DBlock(nn.Module): 1150 """@private 1151 """ 1152 def __init__(self, in_channels, out_channels, kernel_size=3, use_conv_transpose=True): 1153 super().__init__() 1154 _upsampler = SingleDeconv2DBlock if use_conv_transpose else Upsampler2d 1155 self.block = nn.Sequential( 1156 _upsampler(scale_factor=2, in_channels=in_channels, out_channels=out_channels), 1157 SingleConv2DBlock(out_channels, out_channels, kernel_size), 1158 nn.BatchNorm2d(out_channels), 1159 nn.ReLU(True) 1160 ) 1161 1162 def forward(self, x): 1163 return self.block(x)
49def get_unetr_model( 50 ndim: int, 51 backbone: str, 52 model_type: Union[str, nn.Module], 53 encoder_checkpoint: Optional[Union[str, PathLike, Mapping]] = None, 54 init_decoder: Optional[Callable[[nn.Module], nn.Module]] = None, 55 **init_kwargs, 56) -> nn.Module: 57 """Create a UNETR with a ViT encoder for 2D or 3D segmentation. 58 59 Pass `encoder_checkpoint` to load encoder weights. 60 Torchvision loads its default pretrained weights without a checkpoint. 61 Set `pretrained=False` to disable this torchvision download. 62 3D models apply the encoder to each slice and use a 3D convolutional decoder. 63 64 Args: 65 ndim: The number of spatial dimensions. Use 2 or 3. 66 backbone: The ViT family supported by `get_vision_transformer`: "sam", "sam2", "sam3", "cellpose_sam", 67 "mae", "scalemae", "dinov2", "dinov3", or "torchvision". 68 Use a custom name for an injected encoder from another family. 69 model_type: The encoder name or a module from `get_vision_transformer`. Examples: "hvit_t" for SAM2, 70 "vit_b_reg4" for DINOv2, or "vit_b_16" for torchvision. SAM weight names can include training suffixes. 71 Custom modules must expose `img_size`, `embed_dim`, and `in_chans`. 72 They return feature maps or a tuple of feature maps and intermediate features. 73 encoder_checkpoint: The encoder checkpoint, as a file path or a loaded state dictionary. 74 init_decoder: The function that initializes the decoder with pretrained weights. Use this only for 2D models. 75 init_kwargs: The keyword arguments that override the defaults for UNETR2D or UNETR3D. 76 The factory selects the image size and normalization for the backbone. 77 It disables skip connections when the encoder features do not match the decoder. 78 79 Returns: 80 The UNETR model. 81 """ 82 if ndim not in (2, 3): 83 raise ValueError(f"Invalid ndim={ndim}. Use 2 or 3.") 84 if init_decoder is not None and ndim != 2: 85 raise ValueError(f"`init_decoder` is only supported for 2D models, got ndim={ndim}.") 86 87 config = VIT_CONFIGS.get(backbone) 88 if config is None and not isinstance(model_type, nn.Module): 89 raise ValueError(f"Unsupported backbone '{backbone}'.") 90 91 # Get the model class. 92 model_class = UNETR2D if ndim == 2 else UNETR3D 93 if isinstance(model_type, str): 94 encoder = "_".join(model_type.split("_")[:2]) if config.weight_suffixes else model_type 95 img_size = config.img_size 96 encoder_name = encoder 97 elif isinstance(model_type, nn.Module): 98 encoder = model_type 99 img_size = encoder.img_size 100 encoder_name = getattr(encoder, "model_type", None) 101 else: 102 raise TypeError("The model_type must be an encoder name or a torch module.") 103 104 model_kwargs = {"img_size": img_size} 105 if config is not None and config.stats is not None: 106 model_kwargs[f"use_{config.stats}_stats"] = True 107 compatible_skips = config is not None and encoder_name in config.skip_models 108 if ndim == 2 and (not compatible_skips or getattr(encoder, "apply_neck", False)): 109 model_kwargs["use_skip_connection"] = False 110 if backbone == "torchvision" and encoder_checkpoint is not None: 111 model_kwargs["pretrained"] = False 112 model_kwargs.update(init_kwargs) 113 114 model = model_class(backbone=backbone, encoder=encoder, encoder_checkpoint=encoder_checkpoint, **model_kwargs) 115 if init_decoder is not None: 116 model = init_decoder(model) 117 118 return model
Create a UNETR with a ViT encoder for 2D or 3D segmentation.
Pass encoder_checkpoint to load encoder weights.
Torchvision loads its default pretrained weights without a checkpoint.
Set pretrained=False to disable this torchvision download.
3D models apply the encoder to each slice and use a 3D convolutional decoder.
Arguments:
- ndim: The number of spatial dimensions. Use 2 or 3.
- backbone: The ViT family supported by
get_vision_transformer: "sam", "sam2", "sam3", "cellpose_sam", "mae", "scalemae", "dinov2", "dinov3", or "torchvision". Use a custom name for an injected encoder from another family. - model_type: The encoder name or a module from
get_vision_transformer. Examples: "hvit_t" for SAM2, "vit_b_reg4" for DINOv2, or "vit_b_16" for torchvision. SAM weight names can include training suffixes. Custom modules must exposeimg_size,embed_dim, andin_chans. They return feature maps or a tuple of feature maps and intermediate features. - encoder_checkpoint: The encoder checkpoint, as a file path or a loaded state dictionary.
- init_decoder: The function that initializes the decoder with pretrained weights. Use this only for 2D models.
- init_kwargs: The keyword arguments that override the defaults for UNETR2D or UNETR3D. The factory selects the image size and normalization for the backbone. It disables skip connections when the encoder features do not match the decoder.
Returns:
The UNETR model.
178class UNETRBase(nn.Module): 179 """Base class for implementing a UNETR. 180 181 Args: 182 img_size: The size of the input for the image encoder. Input images will be resized to match this size. 183 backbone: The name of the vision transformer implementation. 184 One of "sam", "sam2", "sam3", "cellpose_sam", "mae", "scalemae", "dinov2", "dinov3" 185 (see all combinations below) 186 encoder: The vision transformer. Can either be a name, such as "vit_b" 187 (see all combinations for this below) or a torch module. 188 decoder: The convolutional decoder. 189 out_channels: The number of output channels of the UNETR. 190 use_sam_stats: Whether to normalize the input data with the statistics of the 191 pretrained SAM / SAM2 / SAM3 model. 192 use_dino_stats: Whether to normalize the input data with the statistics of the 193 pretrained DINOv2 / DINOv3 model. 194 use_imagenet_stats: Whether to normalize with standard ImageNet statistics, i.e. 195 mean - (0.485, 0.456, 0.406) and std - (0.229, 0.224, 0.225), raw inputs between range [0, 1]. 196 Use this with the 'torchvision' backbone when loading pretrained weights. 197 use_mae_stats: Whether to normalize the input data with the statistics of the pretrained MAE model. 198 resize_input: Whether to resize the input images to match `img_size`. 199 By default, it resizes the inputs to match the `img_size`. 200 encoder_checkpoint: Checkpoint for initializing the vision transformer. 201 Can either be a filepath or an already loaded checkpoint. 202 final_activation: The activation to apply to the UNETR output. 203 use_skip_connection: Whether to use skip connections. By default, it uses skip connections. 204 embed_dim: The embedding dimensionality, corresponding to the output dimension of the vision transformer. 205 use_conv_transpose: Whether to use transposed convolutions instead of resampling for upsampling. 206 By default, it uses resampling for upsampling. 207 perform_range_checks: Whether to validate the input value range before normalization on each forward pass. 208 You can disable the checks to avoid GPU sync overhead during training when inputs are known to be correct. 209 initial_features: The number of features of the finest decoder level. The features per level are 210 'initial_features * gain ** i', so this scales the decoder parameters quadratically. 211 212 NOTE: The currently supported combinations of 'backbone' x 'encoder' are the following: 213 214 SAM_family_models: 215 - 'sam' x 'vit_b' 216 - 'sam' x 'vit_l' 217 - 'sam' x 'vit_h' 218 - 'sam2' x 'hvit_t' 219 - 'sam2' x 'hvit_s' 220 - 'sam2' x 'hvit_b' 221 - 'sam2' x 'hvit_l' 222 - 'sam3' x 'vit_pe' 223 - 'cellpose_sam' x 'vit_l' 224 225 DINO_family_models: 226 - 'dinov2' x 'vit_s' 227 - 'dinov2' x 'vit_b' 228 - 'dinov2' x 'vit_l' 229 - 'dinov2' x 'vit_g' 230 - 'dinov2' x 'vit_s_reg4' 231 - 'dinov2' x 'vit_b_reg4' 232 - 'dinov2' x 'vit_l_reg4' 233 - 'dinov2' x 'vit_g_reg4' 234 - 'dinov3' x 'vit_s' 235 - 'dinov3' x 'vit_s+' 236 - 'dinov3' x 'vit_b' 237 - 'dinov3' x 'vit_l' 238 - 'dinov3' x 'vit_l+' 239 - 'dinov3' x 'vit_h+' 240 - 'dinov3' x 'vit_7b' 241 242 MAE_family_models: 243 - 'mae' x 'vit_b' 244 - 'mae' x 'vit_l' 245 - 'mae' x 'vit_h' 246 - 'scalemae' x 'vit_b' 247 - 'scalemae' x 'vit_l' 248 - 'scalemae' x 'vit_h' 249 250 torchvision_models: 251 - 'torchvision' x 'vit_b_16' 252 - 'torchvision' x 'vit_b_32' 253 - 'torchvision' x 'vit_l_16' 254 - 'torchvision' x 'vit_l_32' 255 - 'torchvision' x 'vit_h_14' 256 """ 257 def __init__( 258 self, 259 img_size: int = 1024, 260 backbone: str = "sam", 261 encoder: Optional[Union[nn.Module, str]] = "vit_b", 262 decoder: Optional[nn.Module] = None, 263 out_channels: int = 1, 264 use_sam_stats: bool = False, 265 use_mae_stats: bool = False, 266 use_dino_stats: bool = False, 267 use_imagenet_stats: bool = False, 268 resize_input: bool = True, 269 encoder_checkpoint: Optional[Union[str, PathLike, Mapping]] = None, 270 final_activation: Optional[Union[str, nn.Module]] = None, 271 use_skip_connection: bool = True, 272 embed_dim: Optional[int] = None, 273 use_conv_transpose: bool = False, 274 perform_range_checks: bool = True, 275 initial_features: int = 64, 276 **kwargs 277 ) -> None: 278 super().__init__() 279 280 self.img_size = img_size 281 self.use_sam_stats = use_sam_stats 282 self.use_mae_stats = use_mae_stats 283 self.use_dino_stats = use_dino_stats 284 self.use_imagenet_stats = use_imagenet_stats 285 self.use_skip_connection = use_skip_connection 286 self.resize_input = resize_input 287 self.perform_range_checks = perform_range_checks 288 self.use_conv_transpose = use_conv_transpose 289 self.initial_features = initial_features 290 self.backbone = backbone 291 292 if isinstance(encoder, str): # e.g. "vit_b" / "hvit_b" / "vit_pe" 293 print(f"Using {encoder} from {backbone.upper()}") 294 self.encoder = get_vision_transformer(img_size=img_size, backbone=backbone, model=encoder, **kwargs) 295 else: # `nn.Module` ViT backbone 296 self.encoder = encoder 297 298 if encoder_checkpoint is not None: 299 self._load_encoder_from_checkpoint(backbone, encoder_checkpoint) 300 301 # Sort the `embed_dim` out using the encoder output width. 302 if embed_dim is None: 303 have_neck = hasattr(self.encoder, "neck") and not hasattr(self.encoder, "embed_dim") 304 # For SAM1 encoder, if 'apply_neck' is applied, the embedding dimension must change. 305 if getattr(self.encoder, "apply_neck", have_neck): 306 embed_dim = self.encoder.neck[2].out_channels # 256 for SAM1. 307 elif hasattr(self.encoder, "embed_dim"): 308 embed_dim = self.encoder.embed_dim 309 else: 310 embed_dim = self.encoder.patch_embed.proj.out_channels 311 312 self.embed_dim = embed_dim 313 self.final_activation = self._get_activation(final_activation) 314 315 def _load_encoder_from_checkpoint(self, backbone, checkpoint): 316 """Load encoder weights from an encoder state or a full model checkpoint.""" 317 if isinstance(checkpoint, (str, PathLike)): 318 # Load the checkpoint directly; extract encoder weights from full model checkpoints below. 319 encoder_state = torch.load(checkpoint, map_location="cpu", weights_only=False) 320 else: 321 encoder_state = checkpoint 322 323 for state_key in ("model_state", "model", "state_dict"): 324 if state_key in encoder_state: 325 encoder_state = encoder_state[state_key] 326 # Handle DataParallel/DistributedDataParallel prefix. 327 encoder_state = {key.removeprefix("module."): value for key, value in encoder_state.items()} 328 329 if backbone in ("sam", "sam2"): 330 # Extract the image encoder state from the full SAM or SAM2 checkpoint. 331 prefixes = ("image_encoder.", "encoder.") 332 elif backbone == "sam3": 333 # Align the trunk names and leave out the neck's 'convs' and 'sam2_convs' upsampling blocks. 334 prefixes = ( 335 "detector.backbone.vision_backbone.trunk.", "backbone.vision_backbone.trunk.", 336 "vision_backbone.trunk.", "trunk.", "encoder." 337 ) 338 elif backbone == "torchvision": 339 prefixes = ("encoder.",) if "encoder.conv_proj.weight" in encoder_state else () 340 else: 341 # CellposeSAM matches the pretrained rel_pos sizes, so its weights need no interpolation. 342 # Extract encoder weights from CellposeSAM checkpoint format (strip 'encoder.' prefix). 343 prefixes = ("encoder.",) 344 for prefix in prefixes: 345 if any(key.startswith(prefix) for key in encoder_state): 346 encoder_state = { 347 key[len(prefix):]: value for key, value in encoder_state.items() if key.startswith(prefix) 348 } 349 break 350 351 if backbone in ("mae", "scalemae"): 352 # ViT initialization hints from: 353 # https://github.com/facebookresearch/mae/blob/main/main_finetune.py#L233-L242 354 excluded = ("mask_token", "decoder", "head.") 355 if backbone == "scalemae": 356 # NOTE: ScaleMAE computes positional embeddings from the input resolution. 357 excluded += ("fcn", "fpn", "pos_embed") 358 if hasattr(self.encoder, "pos_embed"): 359 del self.encoder.pos_embed 360 encoder_state = {key: value for key, value in encoder_state.items() if not key.startswith(excluded)} 361 # Remove the classification head, which the MAE pretrained encoder weights do not need. 362 if hasattr(self.encoder, "head"): 363 del self.encoder.head 364 elif backbone == "torchvision": 365 encoder_state = {k: v for k, v in encoder_state.items() if not k.startswith("heads.")} 366 elif backbone == "dinov3" and "embeddings.patch_embeddings.weight" in encoder_state: 367 # DINOv3 from HuggingFace, e.g. from micro-sam. The original checkpoints of Meta need no conversion. 368 encoder_state = _convert_hf_dinov3_state(encoder_state, self.encoder.state_dict()) 369 elif backbone == "dinov2" and encoder_state["pos_embed"].shape != self.encoder.pos_embed.shape: 370 # DINOv2 from the original checkpoints of Meta, which micro-sam also uses. 371 # Resize the grid of the pretrained positional embeddings to the input size. 372 cls_pos, patch_pos = encoder_state["pos_embed"][:, :1], encoder_state["pos_embed"][:, 1:] 373 size = int(patch_pos.shape[1] ** 0.5) 374 scale = (self.encoder.img_size // self.encoder.patch_size + self.encoder.interpolate_offset) / size 375 patch_pos = patch_pos.reshape(1, size, size, -1).permute(0, 3, 1, 2) 376 patch_pos = F.interpolate(patch_pos, scale_factor=scale, mode="bicubic") 377 encoder_state["pos_embed"] = torch.cat([cls_pos, patch_pos.flatten(2).transpose(1, 2)], dim=1) 378 379 self.encoder.load_state_dict(encoder_state) 380 381 def _get_activation(self, activation): 382 return_activation = None 383 if activation is None: 384 return None 385 if isinstance(activation, nn.Module): 386 return activation 387 if isinstance(activation, str): 388 return_activation = getattr(nn, activation, None) 389 if return_activation is None: 390 raise ValueError(f"Invalid activation: {activation}") 391 392 return return_activation() 393 394 @staticmethod 395 def get_preprocess_shape(oldh: int, oldw: int, long_side_length: int) -> Tuple[int, int]: 396 """Compute the output size given input size and target long side length. 397 398 Args: 399 oldh: The input image height. 400 oldw: The input image width. 401 long_side_length: The longest side length for resizing. 402 403 Returns: 404 The new image height. 405 The new image width. 406 """ 407 scale = long_side_length * 1.0 / max(oldh, oldw) 408 newh, neww = oldh * scale, oldw * scale 409 neww = int(neww + 0.5) 410 newh = int(newh + 0.5) 411 return (newh, neww) 412 413 def resize_longest_side(self, image: torch.Tensor) -> torch.Tensor: 414 """Resize the image so that the longest side has the correct length. 415 416 Expects batched images with shape BxCxHxW OR BxCxDxHxW and float format. 417 418 Args: 419 image: The input image. 420 421 Returns: 422 The resized image. 423 """ 424 if image.ndim == 4: # i.e. 2d image 425 target_size = self.get_preprocess_shape(image.shape[2], image.shape[3], self.encoder.img_size) 426 return F.interpolate(image, target_size, mode="bilinear", align_corners=False, antialias=True) 427 elif image.ndim == 5: # i.e. 3d volume 428 B, C, Z, H, W = image.shape 429 target_size = self.get_preprocess_shape(H, W, self.img_size) 430 return F.interpolate(image, (Z, *target_size), mode="trilinear", align_corners=False) 431 else: 432 raise ValueError("Expected 4d or 5d inputs, got", image.shape) 433 434 def _as_stats(self, mean, std, device, dtype, is_3d: bool): 435 """@private 436 """ 437 return _as_stats(mean, std, device, dtype, is_3d) 438 439 def _check_input_normalization_range(self, x: torch.Tensor, expected_range: Optional[Tuple[float, float]]) -> None: 440 """@private 441 """ 442 _check_input_normalization_range(x, expected_range) 443 444 def encode(self, x: torch.Tensor): 445 """Preprocess the input and run the image encoder. 446 447 Args: 448 x: The input tensor. 449 450 Returns: 451 The encoder features to pass to `decode` and the spatial shape after preprocessing. 452 """ 453 raise NotImplementedError 454 455 def decode(self, features, input_shape: Tuple[int, ...], original_shape: Tuple[int, ...]) -> torch.Tensor: 456 """Run the convolutional decoder on the encoder features. 457 458 Args: 459 features: The encoder features returned by `encode`. 460 input_shape: The spatial shape after preprocessing, returned by `encode`. 461 original_shape: The spatial shape of the original input. 462 463 Returns: 464 The UNETR output, resized to `original_shape`. 465 """ 466 raise NotImplementedError 467 468 def forward(self, x: torch.Tensor) -> torch.Tensor: 469 """Apply the UNETR to the input data. 470 471 Args: 472 x: The input tensor. 473 474 Returns: 475 The UNETR output. 476 """ 477 features, input_shape = self.encode(x) 478 return self.decode(features, input_shape, tuple(x.shape[2:])) 479 480 def _encode_image(self, x): 481 # Encoders can return either: 482 # - only the image embeddings 483 # - image embeddings and intermediate attention features 484 features = self.encoder(x) 485 return (features, None) if isinstance(features, torch.Tensor) else features 486 487 def preprocess(self, x: torch.Tensor) -> torch.Tensor: 488 """@private 489 """ 490 return preprocess_vit_inputs( 491 x, 492 use_sam_stats=self.use_sam_stats, 493 backbone=self.backbone, 494 use_mae_stats=self.use_mae_stats, 495 use_dino_stats=self.use_dino_stats, 496 use_imagenet_stats=self.use_imagenet_stats, 497 resize_input=self.resize_input, 498 img_size=self.img_size, 499 encoder_img_size=self.encoder.img_size, 500 perform_range_checks=self.perform_range_checks, 501 ) 502 503 def postprocess_masks( 504 self, masks: torch.Tensor, input_size: Tuple[int, ...], original_size: Tuple[int, ...], 505 ) -> torch.Tensor: 506 """@private 507 """ 508 if masks.ndim == 4: # i.e. 2d labels 509 masks = F.interpolate( 510 masks, 511 (self.encoder.img_size, self.encoder.img_size), 512 mode="bilinear", 513 align_corners=False, 514 ) 515 masks = masks[..., : input_size[0], : input_size[1]] 516 masks = F.interpolate(masks, original_size, mode="bilinear", align_corners=False) 517 518 elif masks.ndim == 5: # i.e. 3d volumetric labels 519 masks = F.interpolate( 520 masks, 521 (input_size[0], self.img_size, self.img_size), 522 mode="trilinear", 523 align_corners=False, 524 ) 525 masks = masks[..., :input_size[0], :input_size[1], :input_size[2]] 526 masks = F.interpolate(masks, original_size, mode="trilinear", align_corners=False) 527 528 else: 529 raise ValueError("Expected 4d or 5d labels, got", masks.shape) 530 531 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_imagenet_stats: Whether to normalize with standard ImageNet statistics, i.e. mean - (0.485, 0.456, 0.406) and std - (0.229, 0.224, 0.225), raw inputs between range [0, 1]. Use this with the 'torchvision' backbone when loading pretrained weights.
- 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 theimg_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'
- torchvision_models: - 'torchvision' x 'vit_b_16'
- 'torchvision' x 'vit_b_32'
- 'torchvision' x 'vit_l_16'
- 'torchvision' x 'vit_l_32'
- 'torchvision' x 'vit_h_14'
257 def __init__( 258 self, 259 img_size: int = 1024, 260 backbone: str = "sam", 261 encoder: Optional[Union[nn.Module, str]] = "vit_b", 262 decoder: Optional[nn.Module] = None, 263 out_channels: int = 1, 264 use_sam_stats: bool = False, 265 use_mae_stats: bool = False, 266 use_dino_stats: bool = False, 267 use_imagenet_stats: bool = False, 268 resize_input: bool = True, 269 encoder_checkpoint: Optional[Union[str, PathLike, Mapping]] = None, 270 final_activation: Optional[Union[str, nn.Module]] = None, 271 use_skip_connection: bool = True, 272 embed_dim: Optional[int] = None, 273 use_conv_transpose: bool = False, 274 perform_range_checks: bool = True, 275 initial_features: int = 64, 276 **kwargs 277 ) -> None: 278 super().__init__() 279 280 self.img_size = img_size 281 self.use_sam_stats = use_sam_stats 282 self.use_mae_stats = use_mae_stats 283 self.use_dino_stats = use_dino_stats 284 self.use_imagenet_stats = use_imagenet_stats 285 self.use_skip_connection = use_skip_connection 286 self.resize_input = resize_input 287 self.perform_range_checks = perform_range_checks 288 self.use_conv_transpose = use_conv_transpose 289 self.initial_features = initial_features 290 self.backbone = backbone 291 292 if isinstance(encoder, str): # e.g. "vit_b" / "hvit_b" / "vit_pe" 293 print(f"Using {encoder} from {backbone.upper()}") 294 self.encoder = get_vision_transformer(img_size=img_size, backbone=backbone, model=encoder, **kwargs) 295 else: # `nn.Module` ViT backbone 296 self.encoder = encoder 297 298 if encoder_checkpoint is not None: 299 self._load_encoder_from_checkpoint(backbone, encoder_checkpoint) 300 301 # Sort the `embed_dim` out using the encoder output width. 302 if embed_dim is None: 303 have_neck = hasattr(self.encoder, "neck") and not hasattr(self.encoder, "embed_dim") 304 # For SAM1 encoder, if 'apply_neck' is applied, the embedding dimension must change. 305 if getattr(self.encoder, "apply_neck", have_neck): 306 embed_dim = self.encoder.neck[2].out_channels # 256 for SAM1. 307 elif hasattr(self.encoder, "embed_dim"): 308 embed_dim = self.encoder.embed_dim 309 else: 310 embed_dim = self.encoder.patch_embed.proj.out_channels 311 312 self.embed_dim = embed_dim 313 self.final_activation = self._get_activation(final_activation)
Initialize internal Module state, shared by both nn.Module and ScriptModule.
394 @staticmethod 395 def get_preprocess_shape(oldh: int, oldw: int, long_side_length: int) -> Tuple[int, int]: 396 """Compute the output size given input size and target long side length. 397 398 Args: 399 oldh: The input image height. 400 oldw: The input image width. 401 long_side_length: The longest side length for resizing. 402 403 Returns: 404 The new image height. 405 The new image width. 406 """ 407 scale = long_side_length * 1.0 / max(oldh, oldw) 408 newh, neww = oldh * scale, oldw * scale 409 neww = int(neww + 0.5) 410 newh = int(newh + 0.5) 411 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.
413 def resize_longest_side(self, image: torch.Tensor) -> torch.Tensor: 414 """Resize the image so that the longest side has the correct length. 415 416 Expects batched images with shape BxCxHxW OR BxCxDxHxW and float format. 417 418 Args: 419 image: The input image. 420 421 Returns: 422 The resized image. 423 """ 424 if image.ndim == 4: # i.e. 2d image 425 target_size = self.get_preprocess_shape(image.shape[2], image.shape[3], self.encoder.img_size) 426 return F.interpolate(image, target_size, mode="bilinear", align_corners=False, antialias=True) 427 elif image.ndim == 5: # i.e. 3d volume 428 B, C, Z, H, W = image.shape 429 target_size = self.get_preprocess_shape(H, W, self.img_size) 430 return F.interpolate(image, (Z, *target_size), mode="trilinear", align_corners=False) 431 else: 432 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.
444 def encode(self, x: torch.Tensor): 445 """Preprocess the input and run the image encoder. 446 447 Args: 448 x: The input tensor. 449 450 Returns: 451 The encoder features to pass to `decode` and the spatial shape after preprocessing. 452 """ 453 raise NotImplementedError
Preprocess the input and run the image encoder.
Arguments:
- x: The input tensor.
Returns:
The encoder features to pass to
decodeand the spatial shape after preprocessing.
455 def decode(self, features, input_shape: Tuple[int, ...], original_shape: Tuple[int, ...]) -> torch.Tensor: 456 """Run the convolutional decoder on the encoder features. 457 458 Args: 459 features: The encoder features returned by `encode`. 460 input_shape: The spatial shape after preprocessing, returned by `encode`. 461 original_shape: The spatial shape of the original input. 462 463 Returns: 464 The UNETR output, resized to `original_shape`. 465 """ 466 raise NotImplementedError
468 def forward(self, x: torch.Tensor) -> torch.Tensor: 469 """Apply the UNETR to the input data. 470 471 Args: 472 x: The input tensor. 473 474 Returns: 475 The UNETR output. 476 """ 477 features, input_shape = self.encode(x) 478 return self.decode(features, input_shape, tuple(x.shape[2:]))
Apply the UNETR to the input data.
Arguments:
- x: The input tensor.
Returns:
The UNETR output.
534def preprocess_vit_inputs( 535 x: torch.Tensor, 536 use_sam_stats: bool = False, 537 backbone: str = "sam", 538 use_mae_stats: bool = False, 539 use_dino_stats: bool = False, 540 use_imagenet_stats: bool = False, 541 resize_input: bool = True, 542 img_size: int = 1024, 543 encoder_img_size: int = 1024, 544 perform_range_checks: bool = True, 545) -> Tuple[torch.Tensor, Tuple]: 546 """Preprocess inputs for ViT-backbones in UNETR models. 547 548 Handles normalization stat selection, input range validation, optional resizing to the longest side, 549 and padding to `encoder_img_size`. Can be used as a standalone function without a model instance. 550 551 Args: 552 x: Input tensor of shape (B, C, H, W) for 2D or (B, C, Z, H, W) for 3D. 553 use_sam_stats: Whether to normalize with SAM/SAM2/SAM3 backbone statistics. 554 backbone: The backbone name - controls which SAM stats are used when `use_sam_stats=True`. 555 use_mae_stats: Whether to normalize with MAE statistics. 556 use_dino_stats: Whether to normalize with DINOv2/DINOv3 statistics. 557 use_imagenet_stats: Whether to normalize with standard ImageNet statistics 558 (mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225), range [0, 1]). 559 Use this for torchvision pretrained backbones. 560 resize_input: Whether to resize the input to the longest side before padding. 561 img_size: The model image size, used for 3D resize. 562 encoder_img_size: The encoder image size, used for 2D resize and padding. 563 perform_range_checks: Whether to validate the expected input value range before normalization. 564 You can disable the checks to avoid GPU sync overhead during training when inputs are known to be correct. 565 566 Returns: 567 The preprocessed tensor and the spatial shape after resizing (before padding). 568 """ 569 is_3d = (x.ndim == 5) 570 device, dtype = x.device, x.dtype 571 mean, std = (0.0, 0.0, 0.0), (1.0, 1.0, 1.0) 572 expected_range = None 573 unit_scale_max = None 574 575 if use_sam_stats: 576 if backbone == "sam2": 577 mean, std = (0.485, 0.456, 0.406), (0.229, 0.224, 0.225) 578 expected_range = (0.0, 1.0) 579 elif backbone == "sam3": 580 mean, std = (0.5, 0.5, 0.5), (0.5, 0.5, 0.5) 581 expected_range = (0.0, 1.0) 582 else: # sam1 / default 583 mean, std = (123.675, 116.28, 103.53), (58.395, 57.12, 57.375) 584 expected_range = (0.0, 255.0) 585 unit_scale_max = 1.0 586 elif use_mae_stats or use_dino_stats or use_imagenet_stats: 587 # MAE, ScaleMAE, and DINO use the same ImageNet mean and standard deviation. 588 mean, std = (0.485, 0.456, 0.406), (0.229, 0.224, 0.225) 589 expected_range = (0.0, 1.0) 590 else: 591 mean, std = (0.0, 0.0, 0.0), (1.0, 1.0, 1.0) 592 expected_range = None 593 594 if perform_range_checks: 595 _check_input_normalization_range(x, expected_range, unit_scale_max) 596 pixel_mean, pixel_std = _as_stats(mean, std, device=device, dtype=dtype, is_3d=is_3d) 597 598 if resize_input: 599 if x.ndim == 4: 600 target_size = UNETRBase.get_preprocess_shape(x.shape[2], x.shape[3], encoder_img_size) 601 x = F.interpolate(x, target_size, mode="bilinear", align_corners=False, antialias=True) 602 elif x.ndim == 5: 603 B, C, Z, H, W = x.shape 604 target_size = UNETRBase.get_preprocess_shape(H, W, img_size) 605 x = F.interpolate(x, (Z, *target_size), mode="trilinear", align_corners=False) 606 607 input_shape = x.shape[-3:] if is_3d else x.shape[-2:] 608 609 x = (x - pixel_mean) / pixel_std 610 h, w = x.shape[-2:] 611 padh = encoder_img_size - h 612 padw = encoder_img_size - w 613 614 if is_3d: 615 x = F.pad(x, (0, padw, 0, padh, 0, 0)) 616 else: 617 x = F.pad(x, (0, padw, 0, padh)) 618 619 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.
- use_imagenet_stats: Whether to normalize with standard ImageNet statistics (mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225), range [0, 1]). Use this for torchvision pretrained backbones.
- 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).
622class UNETR(UNETRBase): 623 """A (2d-only) UNet Transformer using a vision transformer as encoder and a convolutional decoder. 624 """ 625 def __init__( 626 self, 627 img_size: int = 1024, 628 backbone: str = "sam", 629 encoder: Optional[Union[nn.Module, str]] = "vit_b", 630 decoder: Optional[nn.Module] = None, 631 out_channels: int = 1, 632 use_sam_stats: bool = False, 633 use_mae_stats: bool = False, 634 use_dino_stats: bool = False, 635 use_imagenet_stats: bool = False, 636 resize_input: bool = True, 637 encoder_checkpoint: Optional[Union[str, PathLike, Mapping]] = None, 638 final_activation: Optional[Union[str, nn.Module]] = None, 639 use_skip_connection: bool = True, 640 embed_dim: Optional[int] = None, 641 use_conv_transpose: bool = False, 642 perform_range_checks: bool = True, 643 **kwargs 644 ) -> None: 645 646 super().__init__( 647 img_size=img_size, 648 backbone=backbone, 649 encoder=encoder, 650 decoder=decoder, 651 out_channels=out_channels, 652 use_sam_stats=use_sam_stats, 653 use_mae_stats=use_mae_stats, 654 use_dino_stats=use_dino_stats, 655 use_imagenet_stats=use_imagenet_stats, 656 resize_input=resize_input, 657 encoder_checkpoint=encoder_checkpoint, 658 final_activation=final_activation, 659 use_skip_connection=use_skip_connection, 660 embed_dim=embed_dim, 661 use_conv_transpose=use_conv_transpose, 662 perform_range_checks=perform_range_checks, 663 **kwargs, 664 ) 665 self.init_kwargs = { 666 "img_size": img_size, "backbone": backbone, "encoder": encoder, "decoder": decoder, 667 "out_channels": out_channels, "use_sam_stats": use_sam_stats, 668 "use_mae_stats": use_mae_stats, "use_dino_stats": use_dino_stats, 669 "use_imagenet_stats": use_imagenet_stats, "resize_input": resize_input, 670 "final_activation": final_activation, 671 "use_skip_connection": use_skip_connection, "embed_dim": embed_dim, 672 "use_conv_transpose": use_conv_transpose, "perform_range_checks": perform_range_checks, 673 **kwargs, 674 } 675 676 encoder = self.encoder 677 678 if backbone == "sam2" and hasattr(encoder, "trunk"): 679 in_chans = encoder.trunk.patch_embed.proj.in_channels 680 elif hasattr(encoder, "in_chans"): 681 in_chans = encoder.in_chans 682 else: # `nn.Module` ViT backbone. 683 try: 684 in_chans = encoder.patch_embed.proj.in_channels 685 except AttributeError: # for getting the input channels while using 'vit_t' from MobileSam 686 in_chans = encoder.patch_embed.seq[0].c.in_channels 687 688 # parameters for the decoder network 689 depth = 3 690 gain = 2 691 features_decoder = [self.initial_features * gain ** i for i in range(depth + 1)][::-1] 692 scale_factors = depth * [2] 693 self.out_channels = out_channels 694 695 # choice of upsampler - to use (bilinear interpolation + conv) or conv transpose 696 _upsampler = SingleDeconv2DBlock if use_conv_transpose else Upsampler2d 697 698 self.decoder = decoder or Decoder( 699 features=features_decoder, 700 scale_factors=scale_factors[::-1], 701 conv_block_impl=ConvBlock2d, 702 sampler_impl=_upsampler, 703 ) 704 705 if use_skip_connection: 706 self.deconv1 = Deconv2DBlock( 707 in_channels=self.embed_dim, 708 out_channels=features_decoder[0], 709 use_conv_transpose=use_conv_transpose, 710 ) 711 self.deconv2 = nn.Sequential( 712 Deconv2DBlock( 713 in_channels=self.embed_dim, 714 out_channels=features_decoder[0], 715 use_conv_transpose=use_conv_transpose, 716 ), 717 Deconv2DBlock( 718 in_channels=features_decoder[0], 719 out_channels=features_decoder[1], 720 use_conv_transpose=use_conv_transpose, 721 ) 722 ) 723 self.deconv3 = nn.Sequential( 724 Deconv2DBlock( 725 in_channels=self.embed_dim, 726 out_channels=features_decoder[0], 727 use_conv_transpose=use_conv_transpose, 728 ), 729 Deconv2DBlock( 730 in_channels=features_decoder[0], 731 out_channels=features_decoder[1], 732 use_conv_transpose=use_conv_transpose, 733 ), 734 Deconv2DBlock( 735 in_channels=features_decoder[1], 736 out_channels=features_decoder[2], 737 use_conv_transpose=use_conv_transpose, 738 ) 739 ) 740 self.deconv4 = ConvBlock2d(in_chans, features_decoder[-1]) 741 else: 742 self.deconv1 = Deconv2DBlock( 743 in_channels=self.embed_dim, 744 out_channels=features_decoder[0], 745 use_conv_transpose=use_conv_transpose, 746 ) 747 self.deconv2 = Deconv2DBlock( 748 in_channels=features_decoder[0], 749 out_channels=features_decoder[1], 750 use_conv_transpose=use_conv_transpose, 751 ) 752 self.deconv3 = Deconv2DBlock( 753 in_channels=features_decoder[1], 754 out_channels=features_decoder[2], 755 use_conv_transpose=use_conv_transpose, 756 ) 757 self.deconv4 = Deconv2DBlock( 758 in_channels=features_decoder[2], 759 out_channels=features_decoder[3], 760 use_conv_transpose=use_conv_transpose, 761 ) 762 763 self.base = ConvBlock2d(self.embed_dim, features_decoder[0]) 764 self.out_conv = nn.Conv2d(features_decoder[-1], out_channels, 1) 765 self.deconv_out = _upsampler( 766 scale_factor=2, in_channels=features_decoder[-1], out_channels=features_decoder[-1] 767 ) 768 self.decoder_head = ConvBlock2d(2 * features_decoder[-1], features_decoder[-1]) 769 770 def encode(self, x: torch.Tensor): 771 """Preprocess the input and run the image encoder. 772 773 Args: 774 x: The input tensor of shape (B, C, Y, X). 775 776 Returns: 777 The features as a tuple of the image embeddings, the list of intermediate encoder outputs 778 (None if the encoder returns only the embeddings) and the preprocessed input, which the 779 skip connections consume, and the spatial shape after preprocessing. 780 """ 781 # Reshape the inputs to the shape expected by the encoder 782 # and normalize the inputs if normalization is part of the model. 783 x, input_shape = self.preprocess(x) 784 z12, from_encoder = self._encode_image(x) 785 return (z12, from_encoder, x), input_shape 786 787 def decode(self, features, input_shape: Tuple[int, ...], original_shape: Tuple[int, ...]) -> torch.Tensor: 788 """Run the convolutional decoder on the encoder features. 789 790 Args: 791 features: The tuple returned by `encode`. 792 input_shape: The spatial shape (Y, X) after preprocessing. 793 original_shape: The spatial shape (Y, X) of the original input. 794 795 Returns: 796 The UNETR output, resized to `original_shape`. 797 """ 798 z12, from_encoder, x = features 799 800 if self.use_skip_connection: 801 from_encoder = from_encoder[::-1] 802 z9 = self.deconv1(from_encoder[0]) 803 z6 = self.deconv2(from_encoder[1]) 804 z3 = self.deconv3(from_encoder[2]) 805 z0 = self.deconv4(x) 806 807 else: 808 z9 = self.deconv1(z12) 809 z6 = self.deconv2(z9) 810 z3 = self.deconv3(z6) 811 z0 = self.deconv4(z3) 812 813 updated_from_encoder = [z9, z6, z3] 814 815 x = self.base(z12) 816 x = self.decoder(x, encoder_inputs=updated_from_encoder) 817 x = self.deconv_out(x) 818 819 x = torch.cat([x, z0], dim=1) 820 x = self.decoder_head(x) 821 822 x = self.out_conv(x) 823 if self.final_activation is not None: 824 x = self.final_activation(x) 825 826 return self.postprocess_masks(x, input_shape, original_shape)
A (2d-only) UNet Transformer using a vision transformer as encoder and a convolutional decoder.
625 def __init__( 626 self, 627 img_size: int = 1024, 628 backbone: str = "sam", 629 encoder: Optional[Union[nn.Module, str]] = "vit_b", 630 decoder: Optional[nn.Module] = None, 631 out_channels: int = 1, 632 use_sam_stats: bool = False, 633 use_mae_stats: bool = False, 634 use_dino_stats: bool = False, 635 use_imagenet_stats: bool = False, 636 resize_input: bool = True, 637 encoder_checkpoint: Optional[Union[str, PathLike, Mapping]] = None, 638 final_activation: Optional[Union[str, nn.Module]] = None, 639 use_skip_connection: bool = True, 640 embed_dim: Optional[int] = None, 641 use_conv_transpose: bool = False, 642 perform_range_checks: bool = True, 643 **kwargs 644 ) -> None: 645 646 super().__init__( 647 img_size=img_size, 648 backbone=backbone, 649 encoder=encoder, 650 decoder=decoder, 651 out_channels=out_channels, 652 use_sam_stats=use_sam_stats, 653 use_mae_stats=use_mae_stats, 654 use_dino_stats=use_dino_stats, 655 use_imagenet_stats=use_imagenet_stats, 656 resize_input=resize_input, 657 encoder_checkpoint=encoder_checkpoint, 658 final_activation=final_activation, 659 use_skip_connection=use_skip_connection, 660 embed_dim=embed_dim, 661 use_conv_transpose=use_conv_transpose, 662 perform_range_checks=perform_range_checks, 663 **kwargs, 664 ) 665 self.init_kwargs = { 666 "img_size": img_size, "backbone": backbone, "encoder": encoder, "decoder": decoder, 667 "out_channels": out_channels, "use_sam_stats": use_sam_stats, 668 "use_mae_stats": use_mae_stats, "use_dino_stats": use_dino_stats, 669 "use_imagenet_stats": use_imagenet_stats, "resize_input": resize_input, 670 "final_activation": final_activation, 671 "use_skip_connection": use_skip_connection, "embed_dim": embed_dim, 672 "use_conv_transpose": use_conv_transpose, "perform_range_checks": perform_range_checks, 673 **kwargs, 674 } 675 676 encoder = self.encoder 677 678 if backbone == "sam2" and hasattr(encoder, "trunk"): 679 in_chans = encoder.trunk.patch_embed.proj.in_channels 680 elif hasattr(encoder, "in_chans"): 681 in_chans = encoder.in_chans 682 else: # `nn.Module` ViT backbone. 683 try: 684 in_chans = encoder.patch_embed.proj.in_channels 685 except AttributeError: # for getting the input channels while using 'vit_t' from MobileSam 686 in_chans = encoder.patch_embed.seq[0].c.in_channels 687 688 # parameters for the decoder network 689 depth = 3 690 gain = 2 691 features_decoder = [self.initial_features * gain ** i for i in range(depth + 1)][::-1] 692 scale_factors = depth * [2] 693 self.out_channels = out_channels 694 695 # choice of upsampler - to use (bilinear interpolation + conv) or conv transpose 696 _upsampler = SingleDeconv2DBlock if use_conv_transpose else Upsampler2d 697 698 self.decoder = decoder or Decoder( 699 features=features_decoder, 700 scale_factors=scale_factors[::-1], 701 conv_block_impl=ConvBlock2d, 702 sampler_impl=_upsampler, 703 ) 704 705 if use_skip_connection: 706 self.deconv1 = Deconv2DBlock( 707 in_channels=self.embed_dim, 708 out_channels=features_decoder[0], 709 use_conv_transpose=use_conv_transpose, 710 ) 711 self.deconv2 = nn.Sequential( 712 Deconv2DBlock( 713 in_channels=self.embed_dim, 714 out_channels=features_decoder[0], 715 use_conv_transpose=use_conv_transpose, 716 ), 717 Deconv2DBlock( 718 in_channels=features_decoder[0], 719 out_channels=features_decoder[1], 720 use_conv_transpose=use_conv_transpose, 721 ) 722 ) 723 self.deconv3 = nn.Sequential( 724 Deconv2DBlock( 725 in_channels=self.embed_dim, 726 out_channels=features_decoder[0], 727 use_conv_transpose=use_conv_transpose, 728 ), 729 Deconv2DBlock( 730 in_channels=features_decoder[0], 731 out_channels=features_decoder[1], 732 use_conv_transpose=use_conv_transpose, 733 ), 734 Deconv2DBlock( 735 in_channels=features_decoder[1], 736 out_channels=features_decoder[2], 737 use_conv_transpose=use_conv_transpose, 738 ) 739 ) 740 self.deconv4 = ConvBlock2d(in_chans, features_decoder[-1]) 741 else: 742 self.deconv1 = Deconv2DBlock( 743 in_channels=self.embed_dim, 744 out_channels=features_decoder[0], 745 use_conv_transpose=use_conv_transpose, 746 ) 747 self.deconv2 = Deconv2DBlock( 748 in_channels=features_decoder[0], 749 out_channels=features_decoder[1], 750 use_conv_transpose=use_conv_transpose, 751 ) 752 self.deconv3 = Deconv2DBlock( 753 in_channels=features_decoder[1], 754 out_channels=features_decoder[2], 755 use_conv_transpose=use_conv_transpose, 756 ) 757 self.deconv4 = Deconv2DBlock( 758 in_channels=features_decoder[2], 759 out_channels=features_decoder[3], 760 use_conv_transpose=use_conv_transpose, 761 ) 762 763 self.base = ConvBlock2d(self.embed_dim, features_decoder[0]) 764 self.out_conv = nn.Conv2d(features_decoder[-1], out_channels, 1) 765 self.deconv_out = _upsampler( 766 scale_factor=2, in_channels=features_decoder[-1], out_channels=features_decoder[-1] 767 ) 768 self.decoder_head = ConvBlock2d(2 * features_decoder[-1], features_decoder[-1])
Initialize internal Module state, shared by both nn.Module and ScriptModule.
770 def encode(self, x: torch.Tensor): 771 """Preprocess the input and run the image encoder. 772 773 Args: 774 x: The input tensor of shape (B, C, Y, X). 775 776 Returns: 777 The features as a tuple of the image embeddings, the list of intermediate encoder outputs 778 (None if the encoder returns only the embeddings) and the preprocessed input, which the 779 skip connections consume, and the spatial shape after preprocessing. 780 """ 781 # Reshape the inputs to the shape expected by the encoder 782 # and normalize the inputs if normalization is part of the model. 783 x, input_shape = self.preprocess(x) 784 z12, from_encoder = self._encode_image(x) 785 return (z12, from_encoder, x), input_shape
Preprocess the input and run the image encoder.
Arguments:
- x: The input tensor of shape (B, C, Y, X).
Returns:
The features as a tuple of the image embeddings, the list of intermediate encoder outputs (None if the encoder returns only the embeddings) and the preprocessed input, which the skip connections consume, and the spatial shape after preprocessing.
787 def decode(self, features, input_shape: Tuple[int, ...], original_shape: Tuple[int, ...]) -> torch.Tensor: 788 """Run the convolutional decoder on the encoder features. 789 790 Args: 791 features: The tuple returned by `encode`. 792 input_shape: The spatial shape (Y, X) after preprocessing. 793 original_shape: The spatial shape (Y, X) of the original input. 794 795 Returns: 796 The UNETR output, resized to `original_shape`. 797 """ 798 z12, from_encoder, x = features 799 800 if self.use_skip_connection: 801 from_encoder = from_encoder[::-1] 802 z9 = self.deconv1(from_encoder[0]) 803 z6 = self.deconv2(from_encoder[1]) 804 z3 = self.deconv3(from_encoder[2]) 805 z0 = self.deconv4(x) 806 807 else: 808 z9 = self.deconv1(z12) 809 z6 = self.deconv2(z9) 810 z3 = self.deconv3(z6) 811 z0 = self.deconv4(z3) 812 813 updated_from_encoder = [z9, z6, z3] 814 815 x = self.base(z12) 816 x = self.decoder(x, encoder_inputs=updated_from_encoder) 817 x = self.deconv_out(x) 818 819 x = torch.cat([x, z0], dim=1) 820 x = self.decoder_head(x) 821 822 x = self.out_conv(x) 823 if self.final_activation is not None: 824 x = self.final_activation(x) 825 826 return self.postprocess_masks(x, input_shape, original_shape)
Run the convolutional decoder on the encoder features.
Arguments:
- features: The tuple returned by
encode. - input_shape: The spatial shape (Y, X) after preprocessing.
- original_shape: The spatial shape (Y, X) of the original input.
Returns:
The UNETR output, resized to
original_shape.
829class UNETR2D(UNETR): 830 """A two-dimensional UNet Transformer using a vision transformer as encoder and a convolutional decoder. 831 """ 832 pass
A two-dimensional UNet Transformer using a vision transformer as encoder and a convolutional decoder.
Inherited Members
835class UNETR3D(UNETRBase): 836 """A 3D UNet Transformer with a vision transformer encoder and a convolutional decoder.""" 837 def __init__( 838 self, 839 img_size: int = 1024, 840 backbone: str = "sam", 841 encoder: Optional[Union[nn.Module, str]] = "vit_b", 842 decoder: Optional[nn.Module] = None, 843 out_channels: int = 1, 844 use_sam_stats: bool = False, 845 use_mae_stats: bool = False, 846 use_dino_stats: bool = False, 847 use_imagenet_stats: bool = False, 848 resize_input: bool = True, 849 encoder_checkpoint: Optional[Union[str, PathLike, Mapping]] = None, 850 final_activation: Optional[Union[str, nn.Module]] = None, 851 use_skip_connection: bool = False, 852 embed_dim: Optional[int] = None, 853 use_conv_transpose: bool = False, 854 use_strip_pooling: bool = True, 855 perform_range_checks: bool = True, 856 **kwargs 857 ): 858 if use_skip_connection: 859 raise NotImplementedError("The framework cannot handle skip connections atm.") 860 if use_conv_transpose: 861 raise NotImplementedError("It's not enabled to switch between interpolation and transposed convolutions.") 862 863 super().__init__( 864 img_size=img_size, 865 backbone=backbone, 866 encoder=encoder, 867 decoder=decoder, 868 out_channels=out_channels, 869 use_sam_stats=use_sam_stats, 870 use_mae_stats=use_mae_stats, 871 use_dino_stats=use_dino_stats, 872 use_imagenet_stats=use_imagenet_stats, 873 resize_input=resize_input, 874 encoder_checkpoint=encoder_checkpoint, 875 final_activation=final_activation, 876 use_skip_connection=use_skip_connection, 877 embed_dim=embed_dim, 878 use_conv_transpose=use_conv_transpose, 879 perform_range_checks=perform_range_checks, 880 **kwargs, 881 ) 882 self.init_kwargs = { 883 "img_size": img_size, "backbone": backbone, "encoder": encoder, "decoder": decoder, 884 "out_channels": out_channels, "use_sam_stats": use_sam_stats, 885 "use_mae_stats": use_mae_stats, "use_dino_stats": use_dino_stats, 886 "use_imagenet_stats": use_imagenet_stats, "resize_input": resize_input, 887 "final_activation": final_activation, 888 "use_skip_connection": use_skip_connection, "embed_dim": embed_dim, 889 "use_conv_transpose": use_conv_transpose, "use_strip_pooling": use_strip_pooling, 890 "perform_range_checks": perform_range_checks, **kwargs, 891 } 892 893 # The 3d convolutional decoder. 894 # First, get the important parameters for the decoder. 895 depth = 3 896 gain = 2 897 features_decoder = [self.initial_features * gain ** i for i in range(depth + 1)][::-1] 898 scale_factors = [1, 2, 2] 899 self.out_channels = out_channels 900 901 # The mapping blocks. 902 self.deconv1 = Deconv3DBlock( 903 in_channels=self.embed_dim, 904 out_channels=features_decoder[0], 905 scale_factor=scale_factors, 906 use_strip_pooling=use_strip_pooling, 907 ) 908 self.deconv2 = Deconv3DBlock( 909 in_channels=features_decoder[0], 910 out_channels=features_decoder[1], 911 scale_factor=scale_factors, 912 use_strip_pooling=use_strip_pooling, 913 ) 914 self.deconv3 = Deconv3DBlock( 915 in_channels=features_decoder[1], 916 out_channels=features_decoder[2], 917 scale_factor=scale_factors, 918 use_strip_pooling=use_strip_pooling, 919 ) 920 self.deconv4 = Deconv3DBlock( 921 in_channels=features_decoder[2], 922 out_channels=features_decoder[3], 923 scale_factor=scale_factors, 924 use_strip_pooling=use_strip_pooling, 925 ) 926 927 # The core decoder block. 928 self.decoder = decoder or Decoder( 929 features=features_decoder, 930 scale_factors=[scale_factors] * depth, 931 conv_block_impl=partial(ConvBlock3dWithStrip, use_strip_pooling=use_strip_pooling), 932 sampler_impl=Upsampler3d, 933 ) 934 935 # And the final upsampler to match the expected dimensions. 936 self.deconv_out = Deconv3DBlock( # NOTE: changed `end_up` to `deconv_out` 937 in_channels=features_decoder[-1], 938 out_channels=features_decoder[-1], 939 scale_factor=scale_factors, 940 use_strip_pooling=use_strip_pooling, 941 ) 942 943 # Additional conjunction blocks. 944 self.base = ConvBlock3dWithStrip( 945 in_channels=self.embed_dim, out_channels=features_decoder[0], use_strip_pooling=use_strip_pooling 946 ) 947 948 # And the output layers. 949 self.decoder_head = ConvBlock3dWithStrip( 950 in_channels=2 * features_decoder[-1], 951 out_channels=features_decoder[-1], 952 use_strip_pooling=use_strip_pooling 953 ) 954 self.out_conv = nn.Conv3d(features_decoder[-1], out_channels, 1) 955 956 def encode(self, x: torch.Tensor): 957 """Preprocess the input and run the image encoder on every z-slice. 958 959 Args: 960 x: The input tensor with shape (B, C, Z, Y, X). The depth Z can vary. 961 962 Returns: 963 The encoder features of shape (B, D, Z, Y', X') and the spatial shape after preprocessing. 964 """ 965 Z = x.shape[2] 966 x, input_shape = self.preprocess(x) 967 features = torch.stack([self._encode_image(x[:, :, i])[0] for i in range(Z)], dim=2) 968 return features, input_shape 969 970 def decode(self, features, input_shape: Tuple[int, ...], original_shape: Tuple[int, ...]) -> torch.Tensor: 971 """Run the convolutional decoder on the encoder features. 972 973 Args: 974 features: The encoder features with shape (B, D, Z, Y', X'), returned by `encode`. 975 input_shape: The spatial shape (Z, Y, X) after preprocessing. 976 original_shape: The spatial shape (Z, Y, X) of the original input. 977 978 Returns: 979 The UNETR output, resized to `original_shape`. 980 """ 981 # Prepare the counterparts for the decoder. 982 # NOTE: These mappings are sequential; they do not use intermediate encoder skip connections. 983 z9 = self.deconv1(features) 984 z6 = self.deconv2(z9) 985 z3 = self.deconv3(z6) 986 z0 = self.deconv4(z3) 987 988 updated_from_encoder = [z9, z6, z3] 989 990 # Align the features through the base block. 991 x = self.base(features) 992 # Run the decoder 993 x = self.decoder(x, encoder_inputs=updated_from_encoder) 994 x = self.deconv_out(x) # NOTE: formerly `end_up`. 995 996 # And the final output head. 997 x = torch.cat([x, z0], dim=1) 998 x = self.decoder_head(x) 999 x = self.out_conv(x) 1000 if self.final_activation is not None: 1001 x = self.final_activation(x) 1002 1003 # Postprocess the output back to original size. 1004 return self.postprocess_masks(x, input_shape, original_shape)
A 3D UNet Transformer with a vision transformer encoder and a convolutional decoder.
837 def __init__( 838 self, 839 img_size: int = 1024, 840 backbone: str = "sam", 841 encoder: Optional[Union[nn.Module, str]] = "vit_b", 842 decoder: Optional[nn.Module] = None, 843 out_channels: int = 1, 844 use_sam_stats: bool = False, 845 use_mae_stats: bool = False, 846 use_dino_stats: bool = False, 847 use_imagenet_stats: bool = False, 848 resize_input: bool = True, 849 encoder_checkpoint: Optional[Union[str, PathLike, Mapping]] = None, 850 final_activation: Optional[Union[str, nn.Module]] = None, 851 use_skip_connection: bool = False, 852 embed_dim: Optional[int] = None, 853 use_conv_transpose: bool = False, 854 use_strip_pooling: bool = True, 855 perform_range_checks: bool = True, 856 **kwargs 857 ): 858 if use_skip_connection: 859 raise NotImplementedError("The framework cannot handle skip connections atm.") 860 if use_conv_transpose: 861 raise NotImplementedError("It's not enabled to switch between interpolation and transposed convolutions.") 862 863 super().__init__( 864 img_size=img_size, 865 backbone=backbone, 866 encoder=encoder, 867 decoder=decoder, 868 out_channels=out_channels, 869 use_sam_stats=use_sam_stats, 870 use_mae_stats=use_mae_stats, 871 use_dino_stats=use_dino_stats, 872 use_imagenet_stats=use_imagenet_stats, 873 resize_input=resize_input, 874 encoder_checkpoint=encoder_checkpoint, 875 final_activation=final_activation, 876 use_skip_connection=use_skip_connection, 877 embed_dim=embed_dim, 878 use_conv_transpose=use_conv_transpose, 879 perform_range_checks=perform_range_checks, 880 **kwargs, 881 ) 882 self.init_kwargs = { 883 "img_size": img_size, "backbone": backbone, "encoder": encoder, "decoder": decoder, 884 "out_channels": out_channels, "use_sam_stats": use_sam_stats, 885 "use_mae_stats": use_mae_stats, "use_dino_stats": use_dino_stats, 886 "use_imagenet_stats": use_imagenet_stats, "resize_input": resize_input, 887 "final_activation": final_activation, 888 "use_skip_connection": use_skip_connection, "embed_dim": embed_dim, 889 "use_conv_transpose": use_conv_transpose, "use_strip_pooling": use_strip_pooling, 890 "perform_range_checks": perform_range_checks, **kwargs, 891 } 892 893 # The 3d convolutional decoder. 894 # First, get the important parameters for the decoder. 895 depth = 3 896 gain = 2 897 features_decoder = [self.initial_features * gain ** i for i in range(depth + 1)][::-1] 898 scale_factors = [1, 2, 2] 899 self.out_channels = out_channels 900 901 # The mapping blocks. 902 self.deconv1 = Deconv3DBlock( 903 in_channels=self.embed_dim, 904 out_channels=features_decoder[0], 905 scale_factor=scale_factors, 906 use_strip_pooling=use_strip_pooling, 907 ) 908 self.deconv2 = Deconv3DBlock( 909 in_channels=features_decoder[0], 910 out_channels=features_decoder[1], 911 scale_factor=scale_factors, 912 use_strip_pooling=use_strip_pooling, 913 ) 914 self.deconv3 = Deconv3DBlock( 915 in_channels=features_decoder[1], 916 out_channels=features_decoder[2], 917 scale_factor=scale_factors, 918 use_strip_pooling=use_strip_pooling, 919 ) 920 self.deconv4 = Deconv3DBlock( 921 in_channels=features_decoder[2], 922 out_channels=features_decoder[3], 923 scale_factor=scale_factors, 924 use_strip_pooling=use_strip_pooling, 925 ) 926 927 # The core decoder block. 928 self.decoder = decoder or Decoder( 929 features=features_decoder, 930 scale_factors=[scale_factors] * depth, 931 conv_block_impl=partial(ConvBlock3dWithStrip, use_strip_pooling=use_strip_pooling), 932 sampler_impl=Upsampler3d, 933 ) 934 935 # And the final upsampler to match the expected dimensions. 936 self.deconv_out = Deconv3DBlock( # NOTE: changed `end_up` to `deconv_out` 937 in_channels=features_decoder[-1], 938 out_channels=features_decoder[-1], 939 scale_factor=scale_factors, 940 use_strip_pooling=use_strip_pooling, 941 ) 942 943 # Additional conjunction blocks. 944 self.base = ConvBlock3dWithStrip( 945 in_channels=self.embed_dim, out_channels=features_decoder[0], use_strip_pooling=use_strip_pooling 946 ) 947 948 # And the output layers. 949 self.decoder_head = ConvBlock3dWithStrip( 950 in_channels=2 * features_decoder[-1], 951 out_channels=features_decoder[-1], 952 use_strip_pooling=use_strip_pooling 953 ) 954 self.out_conv = nn.Conv3d(features_decoder[-1], out_channels, 1)
Initialize internal Module state, shared by both nn.Module and ScriptModule.
956 def encode(self, x: torch.Tensor): 957 """Preprocess the input and run the image encoder on every z-slice. 958 959 Args: 960 x: The input tensor with shape (B, C, Z, Y, X). The depth Z can vary. 961 962 Returns: 963 The encoder features of shape (B, D, Z, Y', X') and the spatial shape after preprocessing. 964 """ 965 Z = x.shape[2] 966 x, input_shape = self.preprocess(x) 967 features = torch.stack([self._encode_image(x[:, :, i])[0] for i in range(Z)], dim=2) 968 return features, input_shape
Preprocess the input and run the image encoder on every z-slice.
Arguments:
- x: The input tensor with shape (B, C, Z, Y, X). The depth Z can vary.
Returns:
The encoder features of shape (B, D, Z, Y', X') and the spatial shape after preprocessing.
970 def decode(self, features, input_shape: Tuple[int, ...], original_shape: Tuple[int, ...]) -> torch.Tensor: 971 """Run the convolutional decoder on the encoder features. 972 973 Args: 974 features: The encoder features with shape (B, D, Z, Y', X'), returned by `encode`. 975 input_shape: The spatial shape (Z, Y, X) after preprocessing. 976 original_shape: The spatial shape (Z, Y, X) of the original input. 977 978 Returns: 979 The UNETR output, resized to `original_shape`. 980 """ 981 # Prepare the counterparts for the decoder. 982 # NOTE: These mappings are sequential; they do not use intermediate encoder skip connections. 983 z9 = self.deconv1(features) 984 z6 = self.deconv2(z9) 985 z3 = self.deconv3(z6) 986 z0 = self.deconv4(z3) 987 988 updated_from_encoder = [z9, z6, z3] 989 990 # Align the features through the base block. 991 x = self.base(features) 992 # Run the decoder 993 x = self.decoder(x, encoder_inputs=updated_from_encoder) 994 x = self.deconv_out(x) # NOTE: formerly `end_up`. 995 996 # And the final output head. 997 x = torch.cat([x, z0], dim=1) 998 x = self.decoder_head(x) 999 x = self.out_conv(x) 1000 if self.final_activation is not None: 1001 x = self.final_activation(x) 1002 1003 # Postprocess the output back to original size. 1004 return self.postprocess_masks(x, input_shape, original_shape)
Run the convolutional decoder on the encoder features.
Arguments:
- features: The encoder features with shape (B, D, Z, Y', X'), returned by
encode. - input_shape: The spatial shape (Z, Y, X) after preprocessing.
- original_shape: The spatial shape (Z, Y, X) of the original input.
Returns:
The UNETR output, resized to
original_shape.