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