synapse_net.training.models

Model factories and pretrained backbone utilities for training.

  1"""Model factories and pretrained backbone utilities for training."""
  2import warnings
  3from typing import Tuple
  4import numpy as np
  5
  6import torch
  7
  8from torch_em.model import UNet2d, AnisotropicUNet
  9from torch_em.model.cryozoo import get_cryozoounet
 10from torch_em.model.unetr import get_unetr_model
 11from torch_em.transform.raw import normalize_percentile, get_raw_transform_for_pretrained_vit
 12
 13from .dataloaders import _determine_ndim
 14
 15try:
 16    from micro_sam.v1.util import models as microsam_models
 17except ImportError:
 18    microsam_models = None
 19
 20try:
 21    from micro_sam.v2.util import models as microsam2_models
 22except ImportError:
 23    microsam2_models = None
 24
 25try:
 26    from micro_sam.models.vfm import get_vfm_model
 27except ImportError:
 28    get_vfm_model = None
 29
 30
 31def get_3d_model(
 32    out_channels: int,
 33    in_channels: int = 1,
 34    scale_factors: Tuple[Tuple[int, int, int]] = [[1, 2, 2], [2, 2, 2], [2, 2, 2], [2, 2, 2]],
 35    initial_features: int = 32,
 36    final_activation: str = "Sigmoid",
 37) -> torch.nn.Module:
 38    """Get the U-Net model for 3D segmentation tasks.
 39
 40    Args:
 41        out_channels: The number of output channels of the network.
 42        scale_factors: The downscaling factors for each level of the U-Net encoder.
 43        initial_features: The number of features in the first level of the U-Net.
 44            The number of features increases by a factor of two in each level.
 45        final_activation: The activation applied to the last output layer.
 46
 47    Returns:
 48        The U-Net.
 49    """
 50    model = AnisotropicUNet(
 51        scale_factors=scale_factors,
 52        in_channels=in_channels,
 53        out_channels=out_channels,
 54        initial_features=initial_features,
 55        gain=2,
 56        final_activation=final_activation,
 57    )
 58    return model
 59
 60
 61def get_2d_model(
 62    out_channels: int,
 63    in_channels: int = 1,
 64    initial_features: int = 32,
 65    final_activation: str = "Sigmoid",
 66) -> torch.nn.Module:
 67    """Get the U-Net model for 2D segmentation tasks.
 68
 69    Args:
 70        out_channels: The number of output channels of the network.
 71        initial_features: The number of features in the first level of the U-Net.
 72            The number of features increases by a factor of two in each level.
 73        final_activation: The activation applied to the last output layer.
 74
 75    Returns:
 76        The U-Net.
 77    """
 78    model = UNet2d(
 79        in_channels=in_channels,
 80        out_channels=out_channels,
 81        initial_features=initial_features,
 82        gain=2,
 83        depth=4,
 84        final_activation=final_activation,
 85    )
 86    return model
 87
 88
 89def _check_backbone(backbone, model_type, patch_shape):
 90    if backbone == "cryosiam":
 91        if model_type is not None:
 92            raise ValueError(f"The 'cryosiam' backbone has no model_type; got model_type='{model_type}'.")
 93        if _determine_ndim(patch_shape)[0]:
 94            raise ValueError(f"The 'cryosiam' backbone only supports 3D training; got patch_shape={patch_shape}.")
 95        if any(s % 8 for s in patch_shape):
 96            raise ValueError(
 97                f"The patch_shape for 'cryosiam' backbone must be divisible by 8; got patch_shape={patch_shape}."
 98            )
 99    elif (backbone is None) != (model_type is None):
100        raise ValueError(
101            "backbone and model_type must both be set for ViT backbones; "
102            f"got backbone={backbone} and model_type={model_type}."
103        )
104
105
106def _get_model(
107    patch_shape, out_channels, in_channels=1, backbone=None, model_type=None,
108    encoder_checkpoint=None, final_activation="Sigmoid",
109):
110    is_2d, ndim = _determine_ndim(patch_shape)
111
112    if backbone == "cryosiam":
113        assert in_channels == 1
114        return get_cryozoounet(out_channels=out_channels, encoder=backbone, final_activation=final_activation)
115
116    if backbone is not None:
117        assert in_channels == 1
118        if encoder_checkpoint is None:
119            encoder_checkpoint = _get_checkpoint(backbone=backbone, model_type=model_type)
120        return get_unetr_model(
121            ndim=ndim, backbone=backbone, model_type=model_type,
122            encoder_checkpoint=encoder_checkpoint,
123            out_channels=out_channels, final_activation=final_activation,
124        )
125
126    if is_2d:
127        return get_2d_model(out_channels=out_channels, in_channels=in_channels, final_activation=final_activation)
128
129    return get_3d_model(out_channels=out_channels, in_channels=in_channels, final_activation=final_activation)
130
131
132def normalize_percentile_to_0_1_inverted(raw):
133    """Normalize the raw data with percentiles to [0, 1] and invert the contrast."""
134    raw = 1 - normalize_percentile(raw)
135    return torch.clamp(raw, 0, 1) if torch.is_tensor(raw) else np.clip(raw, 0, 1)
136
137
138def get_raw_transform(backbone):
139    """Get the raw transform and the maximum input value for a pretrained backbone.
140
141    The ViT backbones are handled by `torch_em.transform.raw.get_raw_transform_for_pretrained_vit`.
142
143    Args:
144        backbone: The pretrained encoder. Options: "sam", "sam2", "dinov2", "dinov3", "cryosiam", or None.
145
146    Returns:
147        The raw transform. It normalizes the raw data with percentiles to [0, 255] for "sam",
148            and to [0, 1] for "sam2", "dinov2" and "dinov3". For "cryosiam" it also inverts the contrast.
149            None if `backbone` is None, so the loaders use the default torch-em standardization.
150        The maximum input value `clip_max`. The intensity augmentations clip their output to
151            [0, clip_max], so that the input stays in the expected range. None if `backbone` is None.
152    """
153    if backbone == "cryosiam":
154        return normalize_percentile_to_0_1_inverted, 1
155
156    if backbone not in ("sam", "sam2", "dinov2", "dinov3", None):
157        raise ValueError(f"Unsupported backbone '{backbone}'.")
158    return get_raw_transform_for_pretrained_vit(backbone)
159
160
161def _get_checkpoint(backbone, model_type, return_decoder_path=False):
162
163    if backbone == "sam":
164        if microsam_models is None:
165            raise RuntimeError(
166                "The 'sam' backbone requires micro_sam. Install it with 'conda install -c conda-forge micro_sam'."
167                )
168        model_registry = microsam_models()
169        encoder_checkpoint = model_registry.fetch(model_type, progressbar=True)
170
171        if return_decoder_path:
172            decoder_name = f"{model_type}_decoder"
173            decoder_path = model_registry.fetch(
174                decoder_name, progressbar=True
175            ) if decoder_name in model_registry.registry else None
176
177    elif backbone == "sam2":
178        if microsam2_models is None:
179            raise RuntimeError(
180                "The 'sam2' backbone requires micro_sam with SAM2 support ('micro_sam.v2') and sam2."
181                )
182        model_registry = microsam2_models()
183        encoder_checkpoint = model_registry.fetch(model_type, progressbar=True)
184
185        if return_decoder_path:
186            decoder_name = f"{model_type}_decoder"
187            decoder_path = model_registry.fetch(
188                decoder_name, progressbar=True
189            ) if decoder_name in model_registry.registry else None
190
191    elif backbone == "dinov2":
192        if get_vfm_model is None:
193            raise RuntimeError("The 'dinov2' backbone requires micro_sam with the 'micro_sam.models.vfm' module.")
194        encoder_checkpoint = get_vfm_model(f"{model_type}_dinov2", device="cpu").model.state_dict()
195        decoder_path = None
196
197    elif backbone == "dinov3":
198        if get_vfm_model is None:
199            raise RuntimeError("The 'dinov3' backbone requires micro_sam with the 'micro_sam.models.vfm' module.")
200        encoder_checkpoint = get_vfm_model(f"{model_type}_dinov3", device="cpu").model.state_dict()
201        decoder_path = None
202    else:
203        raise ValueError(f"Unsupported backbone '{backbone}'.")
204
205    if return_decoder_path:
206        return encoder_checkpoint, decoder_path
207    else:
208        return encoder_checkpoint
209
210
211def _init_microsam_decoder(model, backbone, model_type, out_channels):
212    # Get the micro-sam decoder path
213    _, decoder_path = _get_checkpoint(backbone=backbone, model_type=model_type, return_decoder_path=True)
214
215    if decoder_path is None:
216        warnings.warn("Oopsie daisy. You choose a `micro-sam` or model without pretrained decoder weights.")
217        return model
218
219    # Load the pretrained decoder weights.
220    decoder_state = torch.load(decoder_path, map_location="cpu", weights_only=True)
221
222    # Super hacky way of initializing decoder weights - please don't try this at home.
223    unetr_state_dict = model.state_dict()
224    for k, v in unetr_state_dict.items():
225        # If the key starts with encoder, we don't care about it.
226        if k.startswith("encoder"):
227            continue
228
229        # If the 'out_channels' are different than expected, i.e. 3, we say ciao ciao to it.
230        if out_channels != 3 and k.startswith("out_conv"):
231            warnings.warn(f"Seems like output channels are different than 'micro-sam'. Hence, we reinitialize '{k}'.")
232            unetr_state_dict[k] = v
233            continue
234
235        # If it's some weird parameters, we don't care about them too.
236        if k not in decoder_state:
237            warnings.warn(f"Could not find '{k}' in the pretrained decoder state dict. Hence, we reinitialize '{k}.")
238            unetr_state_dict[k] = v
239            continue
240
241        # If the parameter shapes mismatch, we don't care about them either.
242        if decoder_state[k].shape != v.shape:
243            warnings.warn(f"There's a shape mismatch for '{k}'. Hence, we reinitialize '{k}'.")
244            unetr_state_dict[k] = v
245            continue
246
247        # Keep the expected decoder weights.
248        unetr_state_dict[k] = decoder_state[k]
249
250    model.load_state_dict(unetr_state_dict)
251
252    return model
def get_3d_model( out_channels: int, in_channels: int = 1, scale_factors: Tuple[Tuple[int, int, int]] = [[1, 2, 2], [2, 2, 2], [2, 2, 2], [2, 2, 2]], initial_features: int = 32, final_activation: str = 'Sigmoid') -> torch.nn.modules.module.Module:
32def get_3d_model(
33    out_channels: int,
34    in_channels: int = 1,
35    scale_factors: Tuple[Tuple[int, int, int]] = [[1, 2, 2], [2, 2, 2], [2, 2, 2], [2, 2, 2]],
36    initial_features: int = 32,
37    final_activation: str = "Sigmoid",
38) -> torch.nn.Module:
39    """Get the U-Net model for 3D segmentation tasks.
40
41    Args:
42        out_channels: The number of output channels of the network.
43        scale_factors: The downscaling factors for each level of the U-Net encoder.
44        initial_features: The number of features in the first level of the U-Net.
45            The number of features increases by a factor of two in each level.
46        final_activation: The activation applied to the last output layer.
47
48    Returns:
49        The U-Net.
50    """
51    model = AnisotropicUNet(
52        scale_factors=scale_factors,
53        in_channels=in_channels,
54        out_channels=out_channels,
55        initial_features=initial_features,
56        gain=2,
57        final_activation=final_activation,
58    )
59    return model

Get the U-Net model for 3D segmentation tasks.

Arguments:
  • out_channels: The number of output channels of the network.
  • scale_factors: The downscaling factors for each level of the U-Net encoder.
  • initial_features: The number of features in the first level of the U-Net. The number of features increases by a factor of two in each level.
  • final_activation: The activation applied to the last output layer.
Returns:

The U-Net.

def get_2d_model( out_channels: int, in_channels: int = 1, initial_features: int = 32, final_activation: str = 'Sigmoid') -> torch.nn.modules.module.Module:
62def get_2d_model(
63    out_channels: int,
64    in_channels: int = 1,
65    initial_features: int = 32,
66    final_activation: str = "Sigmoid",
67) -> torch.nn.Module:
68    """Get the U-Net model for 2D segmentation tasks.
69
70    Args:
71        out_channels: The number of output channels of the network.
72        initial_features: The number of features in the first level of the U-Net.
73            The number of features increases by a factor of two in each level.
74        final_activation: The activation applied to the last output layer.
75
76    Returns:
77        The U-Net.
78    """
79    model = UNet2d(
80        in_channels=in_channels,
81        out_channels=out_channels,
82        initial_features=initial_features,
83        gain=2,
84        depth=4,
85        final_activation=final_activation,
86    )
87    return model

Get the U-Net model for 2D segmentation tasks.

Arguments:
  • out_channels: The number of output channels of the network.
  • initial_features: The number of features in the first level of the U-Net. The number of features increases by a factor of two in each level.
  • final_activation: The activation applied to the last output layer.
Returns:

The U-Net.

def normalize_percentile_to_0_1_inverted(raw):
133def normalize_percentile_to_0_1_inverted(raw):
134    """Normalize the raw data with percentiles to [0, 1] and invert the contrast."""
135    raw = 1 - normalize_percentile(raw)
136    return torch.clamp(raw, 0, 1) if torch.is_tensor(raw) else np.clip(raw, 0, 1)

Normalize the raw data with percentiles to [0, 1] and invert the contrast.

def get_raw_transform(backbone):
139def get_raw_transform(backbone):
140    """Get the raw transform and the maximum input value for a pretrained backbone.
141
142    The ViT backbones are handled by `torch_em.transform.raw.get_raw_transform_for_pretrained_vit`.
143
144    Args:
145        backbone: The pretrained encoder. Options: "sam", "sam2", "dinov2", "dinov3", "cryosiam", or None.
146
147    Returns:
148        The raw transform. It normalizes the raw data with percentiles to [0, 255] for "sam",
149            and to [0, 1] for "sam2", "dinov2" and "dinov3". For "cryosiam" it also inverts the contrast.
150            None if `backbone` is None, so the loaders use the default torch-em standardization.
151        The maximum input value `clip_max`. The intensity augmentations clip their output to
152            [0, clip_max], so that the input stays in the expected range. None if `backbone` is None.
153    """
154    if backbone == "cryosiam":
155        return normalize_percentile_to_0_1_inverted, 1
156
157    if backbone not in ("sam", "sam2", "dinov2", "dinov3", None):
158        raise ValueError(f"Unsupported backbone '{backbone}'.")
159    return get_raw_transform_for_pretrained_vit(backbone)

Get the raw transform and the maximum input value for a pretrained backbone.

The ViT backbones are handled by torch_em.transform.raw.get_raw_transform_for_pretrained_vit.

Arguments:
  • backbone: The pretrained encoder. Options: "sam", "sam2", "dinov2", "dinov3", "cryosiam", or None.
Returns:

The raw transform. It normalizes the raw data with percentiles to [0, 255] for "sam", and to [0, 1] for "sam2", "dinov2" and "dinov3". For "cryosiam" it also inverts the contrast. None if backbone is None, so the loaders use the default torch-em standardization. The maximum input value clip_max. The intensity augmentations clip their output to [0, clip_max], so that the input stays in the expected range. None if backbone is None.