torch_em.util.util

  1import os
  2import warnings
  3from collections import OrderedDict
  4from typing import Optional, Tuple, Union
  5
  6import numpy as np
  7import torch
  8import torch_em
  9from matplotlib import colors
 10from numpy.typing import ArrayLike
 11
 12try:
 13    from torch._dynamo.eval_frame import OptimizedModule
 14except ImportError:
 15    OptimizedModule = None
 16
 17# torch doesn't support most unsigned types,
 18# so we map them to their signed equivalent
 19DTYPE_MAP = {
 20    np.dtype("uint16"): np.int16,
 21    np.dtype("uint32"): np.int32,
 22    np.dtype("uint64"): np.int64
 23}
 24"""@private
 25"""
 26
 27
 28def get_cache_directory() -> str:
 29    """Get the torch-em cache directory, for example for downloaded model weights.
 30
 31    Set the TORCH_EM_CACHEDIR environment variable to use a custom cache directory.
 32
 33    Returns:
 34        The path to the cache directory.
 35    """
 36    import pooch  # We import pooch here, so that 'import torch_em' does not need it.
 37
 38    default_cache_directory = os.path.expanduser(pooch.os_cache("torch_em"))
 39    return os.environ.get("TORCH_EM_CACHEDIR", default_cache_directory)
 40
 41
 42# This is a fairly brittle way to check if a module is compiled.
 43# Would be good to find a better solution, ideall something like model.is_compiled().
 44def is_compiled(model):
 45    """@private
 46    """
 47    if OptimizedModule is None:
 48        return False
 49    return isinstance(model, OptimizedModule)
 50
 51
 52def auto_compile(
 53    model: torch.nn.Module, compile_model: Optional[Union[str, bool]] = None, default_compile: bool = True
 54) -> torch.nn.Module:
 55    """Automatically compile a model for pytorch >= 2.
 56
 57    Args:
 58        model: The model.
 59        compile_model: Whether to comile the model.
 60            If None, it will not be compiled for torch < 2, and for torch > 2 the behavior
 61            specificed by 'default_compile' will be used. If a string is given it will be
 62            intepreted as the compile mode (torch.compile(model, mode=compile_model))
 63        default_compile: Whether to use the default compilation behavior for torch 2.
 64
 65    Returns:
 66        The compiled model.
 67    """
 68    torch_major = int(torch.__version__.split(".")[0])
 69
 70    if compile_model is None:
 71        if torch_major < 2:
 72            compile_model = False
 73        elif is_compiled(model):  # model is already compiled
 74            compile_model = False
 75        else:
 76            compile_model = default_compile
 77
 78    if compile_model:
 79        if torch_major < 2:
 80            raise RuntimeError("Model compilation is only supported for pytorch 2")
 81
 82        print("Compiling pytorch model ...")
 83        if isinstance(compile_model, str):
 84            model = torch.compile(model, mode=compile_model)
 85        else:
 86            model = torch.compile(model)
 87
 88    return model
 89
 90
 91def ensure_tensor(tensor: Union[torch.Tensor, ArrayLike], dtype: Optional[str] = None) -> torch.Tensor:
 92    """Ensure that the input is a torch tensor, by converting it if necessary.
 93
 94    Args:
 95        tensor: The input object, either a torch tensor or a numpy-array like object.
 96        dtype: The required data type for the output tensor.
 97
 98    Returns:
 99        The input, converted to a torch tensor if necessary.
100    """
101    if isinstance(tensor, np.ndarray):
102        if np.dtype(tensor.dtype) in DTYPE_MAP:
103            tensor = tensor.astype(DTYPE_MAP[tensor.dtype])
104        # Try to convert the tensor, even if it has wrong byte-order
105        try:
106            tensor = torch.from_numpy(tensor if tensor.flags.writeable else tensor.copy())
107        except ValueError:
108            tensor = tensor.view(tensor.dtype.newbyteorder())
109            if np.dtype(tensor.dtype) in DTYPE_MAP:
110                tensor = tensor.astype(DTYPE_MAP[tensor.dtype])
111            tensor = torch.from_numpy(tensor)
112
113    assert torch.is_tensor(tensor), f"Cannot convert {type(tensor)} to torch"
114    if dtype is not None:
115        tensor = tensor.to(dtype=dtype)
116    return tensor
117
118
119def validate_roi(roi, shape, patch_shape=None):
120    """Normalize an ROI to explicit slices and validate that it is non-empty."""
121    if roi is None:
122        return None
123    if isinstance(roi, slice):
124        roi = (roi,)
125    if not isinstance(roi, tuple):
126        raise TypeError(f"Invalid roi type: {type(roi)}")
127    if len(roi) > len(shape):
128        raise ValueError(f"Invalid roi {roi} for data shape {shape}: too many dimensions")
129
130    normalized_roi = []
131    for this_roi, dim in zip(roi, shape):
132        if not isinstance(this_roi, slice):
133            raise TypeError(f"Invalid roi entry: {this_roi}. Only slices are supported")
134        step = 1 if this_roi.step is None else this_roi.step
135        if step != 1:
136            raise ValueError(f"Invalid roi {roi}: slice steps other than 1 are not supported")
137        start, stop, _ = this_roi.indices(dim)
138        normalized_roi.append(slice(start, stop))
139
140    if len(roi) < len(shape):
141        normalized_roi.extend(slice(0, dim) for dim in shape[len(roi):])
142
143    roi_shape = tuple(sl.stop - sl.start for sl in normalized_roi)
144    if any(sh <= 0 for sh in roi_shape):
145        msg = f"Invalid roi {roi} for data shape {shape}: it results in an empty region"
146        if patch_shape is not None:
147            msg += f" for patch_shape {patch_shape}"
148        raise ValueError(msg)
149
150    return tuple(normalized_roi)
151
152
153def ensure_tensor_with_channels(
154    tensor: Union[torch.Tensor, ArrayLike], ndim: int, dtype: Optional[str] = None
155) -> torch.Tensor:
156    """Ensure that the input is a torch tensor of a given dimensionality with channels.
157
158    Args:
159        tensor: The input tensor or numpy-array like data.
160        ndim: The dimensionality of the output tensor.
161        dtype: The data type of the output tensor.
162
163    Returns:
164        The input converted to a torch tensor of the requested dimensionality.
165    """
166    assert ndim in (2, 3, 4), f"{ndim}"
167    tensor = ensure_tensor(tensor, dtype)
168    if ndim == 2:
169        assert tensor.ndim in (2, 3, 4, 5), f"{tensor.ndim}"
170        if tensor.ndim == 2:
171            tensor = tensor[None]
172        elif tensor.ndim == 4:
173            assert tensor.shape[0] == 1, f"{tensor.shape}"
174            tensor = tensor[0]
175        elif tensor.ndim == 5:
176            assert tensor.shape[:2] == (1, 1), f"{tensor.shape}"
177            tensor = tensor[0, 0]
178    elif ndim == 3:
179        assert tensor.ndim in (3, 4, 5), f"{tensor.ndim}"
180        if tensor.ndim == 3:
181            tensor = tensor[None]
182        elif tensor.ndim == 5:
183            assert tensor.shape[0] == 1, f"{tensor.shape}"
184            tensor = tensor[0]
185    else:
186        assert tensor.ndim in (4, 5), f"{tensor.ndim}"
187        if tensor.ndim == 5:
188            assert tensor.shape[0] == 1, f"{tensor.shape}"
189            tensor = tensor[0]
190    return tensor
191
192
193def ensure_array(array: Union[np.ndarray, torch.Tensor], dtype: str = None) -> np.ndarray:
194    """Ensure that the input is a numpy array, by converting it if necessary.
195
196    Args:
197        array: The input torch tensor or numpy array.
198        dtype: The dtype of the ouptut array.
199
200    Returns:
201        The input converted to a numpy array if necessary.
202    """
203    if torch.is_tensor(array):
204        array = array.detach().cpu().numpy()
205    assert isinstance(array, np.ndarray), f"Cannot convert {type(array)} to numpy"
206    if dtype is not None:
207        array = np.require(array, dtype=dtype)
208    return array
209
210
211def ensure_spatial_array(array: Union[np.ndarray, torch.Tensor], ndim: int, dtype: str = None) -> np.ndarray:
212    """Ensure that the input is a numpy array of a given dimensionality.
213
214    Args:
215        array: The input numpy array or torch tensor.
216        ndim: The requested dimensionality.
217        dtype: The dtype of the output array.
218
219    Returns:
220        A numpy array of the requested dimensionality and data type.
221    """
222    assert ndim in (2, 3)
223    array = ensure_array(array, dtype)
224    if ndim == 2:
225        assert array.ndim in (2, 3, 4, 5), str(array.ndim)
226        if array.ndim == 3:
227            assert array.shape[0] == 1
228            array = array[0]
229        elif array.ndim == 4:
230            assert array.shape[:2] == (1, 1)
231            array = array[0, 0]
232        elif array.ndim == 5:
233            assert array.shape[:3] == (1, 1, 1)
234            array = array[0, 0, 0]
235    else:
236        assert array.ndim in (3, 4, 5), str(array.ndim)
237        if array.ndim == 4:
238            assert array.shape[0] == 1, f"{array.shape}"
239            array = array[0]
240        elif array.ndim == 5:
241            assert array.shape[:2] == (1, 1)
242            array = array[0, 0]
243    return array
244
245
246def ensure_patch_shape(
247    raw: np.ndarray,
248    labels: Optional[np.ndarray],
249    patch_shape: Tuple[int, ...],
250    have_raw_channels: bool = False,
251    have_label_channels: bool = False,
252    channel_first: bool = True,
253) -> Union[Tuple[np.ndarray, np.ndarray], np.ndarray]:
254    """Ensure that the raw data and labels have at least the requested patch shape.
255
256    If either raw data or labels do not have the patch shape they will be padded.
257
258    Args:
259        raw: The input raw data.
260        labels: The input labels.
261        patch_shape: The required minimal patch shape.
262        have_raw_channels: Whether the raw data has channels.
263        have_label_channels: Whether the label data has channels.
264        channel_first: Whether the channel axis is the first or last axis.
265
266    Returns:
267        The raw data.
268        The labels.
269    """
270    raw_shape = raw.shape
271    if labels is not None:
272        labels_shape = labels.shape
273
274    # In case the inputs has channels and they are channels first
275    # IMPORTANT: for ImageCollectionDataset
276    if have_raw_channels and channel_first:
277        raw_shape = raw_shape[1:]
278
279    if have_label_channels and channel_first and labels is not None:
280        labels_shape = labels_shape[1:]
281
282    # Extract the pad_width and pad the raw inputs
283    if any(sh < psh for sh, psh in zip(raw_shape, patch_shape)):
284        pw = [(0, max(0, psh - sh)) for sh, psh in zip(raw_shape, patch_shape)]
285
286        if have_raw_channels and channel_first:
287            pad_width = [(0, 0), *pw]
288        elif have_raw_channels and not channel_first:
289            pad_width = [*pw, (0, 0)]
290        else:
291            pad_width = pw
292
293        raw = np.pad(array=raw, pad_width=pad_width)
294
295    # Extract the pad width and pad the label inputs
296    if labels is not None and any(sh < psh for sh, psh in zip(labels_shape, patch_shape)):
297        pw = [(0, max(0, psh - sh)) for sh, psh in zip(labels_shape, patch_shape)]
298
299        if have_label_channels and channel_first:
300            pad_width = [(0, 0), *pw]
301        elif have_label_channels and not channel_first:
302            pad_width = [*pw, (0, 0)]
303        else:
304            pad_width = pw
305
306        labels = np.pad(array=labels, pad_width=pad_width)
307    if labels is None:
308        return raw
309    else:
310        return raw, labels
311
312
313def get_constructor_arguments(obj):
314    """@private
315    """
316    # All relevant torch_em classes have 'init_kwargs' to directly recover the init call.
317    if hasattr(obj, "init_kwargs"):
318        return getattr(obj, "init_kwargs")
319
320    def _get_args(obj, param_names):
321        return {name: getattr(obj, name) for name in param_names}
322
323    # We don't need to find the constructor arguments for optimizers/schedulers because we deserialize the state later.
324    if isinstance(
325        obj, (
326            torch.optim.Optimizer,
327            torch.optim.lr_scheduler._LRScheduler,
328            # ReduceLROnPlateau does not inherit from _LRScheduler
329            torch.optim.lr_scheduler.ReduceLROnPlateau
330        )
331    ):
332        return {}
333
334    # recover the arguments for torch dataloader
335    elif isinstance(obj, torch.utils.data.DataLoader):
336        # These are all the "simple" arguements.
337        # "sampler", "batch_sampler" and "worker_init_fn" are more complicated
338        # and generally not used in torch_em
339        sampler = getattr(obj, "sampler", None)
340        if sampler is not None and not isinstance(
341            sampler,
342            (
343                torch.utils.data.RandomSampler,
344                torch.utils.data.SequentialSampler,
345                torch.utils.data.SubsetRandomSampler,
346            ),
347        ):
348            warnings.warn(
349                f"DataLoader uses sampler {type(sampler).__name__}, but only its effective `shuffle` setting "
350                "is serialized. `DefaultTrainer.from_checkpoint` will recreate the loader without the original "
351                "sampler, so sampling behavior may change."
352            )
353        shuffle = getattr(obj, "shuffle", None)
354        if shuffle is None:
355            shuffle = getattr(sampler, "shuffle", None)
356        if shuffle is None:
357            # Only randomized samplers map to shuffle=True. SequentialSampler is handled
358            # by the default fallback of shuffle=False and does not need a special case.
359            shuffle = isinstance(sampler, (torch.utils.data.RandomSampler, torch.utils.data.SubsetRandomSampler))
360
361        return {
362            **_get_args(
363                obj, [
364                    "batch_size", "num_workers", "pin_memory", "drop_last",
365                    "persistent_workers", "prefetch_factor", "timeout"
366                ]
367            ),
368            "shuffle": shuffle,
369        }
370
371    # TODO support common torch losses (e.g. CrossEntropy, BCE)
372    warnings.warn(
373        f"Constructor arguments for {type(obj)} cannot be deduced.\n" +
374        "For this object, empty constructor arguments will be used.\n" +
375        "The trainer can probably not be correctly deserialized via 'DefaultTrainer.from_checkpoint'."
376    )
377    return {}
378
379
380def get_trainer(checkpoint: str, name: str = "best", device: Optional[str] = None):
381    """Load trainer from a checkpoint.
382
383    Args:
384        checkpoint: The path to the checkpoint.
385        name: The name of the checkpoint.
386        device: The device to use for loading the checkpoint.
387
388    Returns:
389        The trainer.
390    """
391    # try to load from file
392    if isinstance(checkpoint, str):
393        assert os.path.exists(checkpoint), checkpoint
394        trainer = torch_em.trainer.DefaultTrainer.from_checkpoint(checkpoint, name=name, device=device)
395    else:
396        trainer = checkpoint
397    assert isinstance(trainer, torch_em.trainer.DefaultTrainer)
398    return trainer
399
400
401def get_normalizer(trainer):
402    """@private
403    """
404    dataset = trainer.train_loader.dataset
405    while (
406        isinstance(dataset, torch_em.data.concat_dataset.ConcatDataset) or
407        isinstance(dataset, torch.utils.data.dataset.ConcatDataset)
408    ):
409        dataset = dataset.datasets[0]
410
411    if isinstance(dataset, torch.utils.data.dataset.Subset):
412        dataset = dataset.dataset
413
414    preprocessor = dataset.raw_transform
415
416    if hasattr(preprocessor, "normalizer"):
417        return preprocessor.normalizer
418    else:
419        return preprocessor
420
421
422def load_model(
423    checkpoint: str,
424    model: Optional[torch.nn.Module] = None,
425    name: str = "best",
426    state_key: Optional[str] = "model_state",
427    device: Optional[str] = None,
428) -> torch.nn.Module:
429    """Load a model from a trainer checkpoint or a serialized torch model.
430
431    This function can either load the model directly (`model` is not passed),
432    or deserialize the model state and then load it (`model` is passed).
433
434    The `checkpoint` argument must either point to the checkpoint directory of a torch_em trainer
435    or to a serialized torch model.
436
437    Args:
438        checkpoint: The path to the checkpoint folder or serialized torch model.
439        model: The model for which the state should be loaded.
440            If it is not passed, the model class and parameters will also be loaded from the trainer.
441        name: The name of the checkpoint.
442        state_key: The name of the model state to load. Set to None if the model state is stored top-level.
443        device: The device on which to load the model.
444
445    Returns:
446        The model.
447    """
448    if model is None and os.path.isdir(checkpoint):  # Load the model and its state from a torch_em checkpoint.
449        model = get_trainer(checkpoint, name=name, device=device).model
450
451    elif model is None:  # Load the model from a serialized model.
452        model = torch.load(checkpoint, map_location=device, weights_only=False)
453
454    else:  # Load the model state from a checkpoint.
455        if os.path.isdir(checkpoint):  # From a torch_em checkpoint.
456            ckpt = os.path.join(checkpoint, f"{name}.pt")
457        else:  # From a serialized path.
458            ckpt = checkpoint
459
460        state = torch.load(ckpt, map_location=device, weights_only=False)
461        if state_key is not None:
462            state = state[state_key]
463
464        # To enable loading compiled models.
465        compiled_prefix = "_orig_mod."
466        state = OrderedDict(
467            [(k[len(compiled_prefix):] if k.startswith(compiled_prefix) else k, v) for k, v in state.items()]
468        )
469
470        model.load_state_dict(state)
471        if device is not None:
472            model.to(device)
473
474    return model
475
476
477def model_is_equal(model1, model2):
478    """@private
479    """
480    for p1, p2 in zip(model1.parameters(), model2.parameters()):
481        if p1.data.ne(p2.data).sum() > 0:
482            return False
483    return True
484
485
486def get_random_colors(labels: np.ndarray) -> colors.ListedColormap:
487    """Generate a random color map for a label image.
488
489    Args:
490        labels: The labels.
491
492    Returns:
493        The color map.
494    """
495    unique_labels = np.unique(labels)
496    have_zero = 0 in unique_labels
497    cmap = [[0, 0, 0]] if have_zero else []
498    cmap += np.random.rand(len(unique_labels), 3).tolist()
499    cmap = colors.ListedColormap(cmap)
500    return cmap
def get_cache_directory() -> str:
29def get_cache_directory() -> str:
30    """Get the torch-em cache directory, for example for downloaded model weights.
31
32    Set the TORCH_EM_CACHEDIR environment variable to use a custom cache directory.
33
34    Returns:
35        The path to the cache directory.
36    """
37    import pooch  # We import pooch here, so that 'import torch_em' does not need it.
38
39    default_cache_directory = os.path.expanduser(pooch.os_cache("torch_em"))
40    return os.environ.get("TORCH_EM_CACHEDIR", default_cache_directory)

Get the torch-em cache directory, for example for downloaded model weights.

Set the TORCH_EM_CACHEDIR environment variable to use a custom cache directory.

Returns:

The path to the cache directory.

def auto_compile( model: torch.nn.modules.module.Module, compile_model: Union[str, bool, NoneType] = None, default_compile: bool = True) -> torch.nn.modules.module.Module:
53def auto_compile(
54    model: torch.nn.Module, compile_model: Optional[Union[str, bool]] = None, default_compile: bool = True
55) -> torch.nn.Module:
56    """Automatically compile a model for pytorch >= 2.
57
58    Args:
59        model: The model.
60        compile_model: Whether to comile the model.
61            If None, it will not be compiled for torch < 2, and for torch > 2 the behavior
62            specificed by 'default_compile' will be used. If a string is given it will be
63            intepreted as the compile mode (torch.compile(model, mode=compile_model))
64        default_compile: Whether to use the default compilation behavior for torch 2.
65
66    Returns:
67        The compiled model.
68    """
69    torch_major = int(torch.__version__.split(".")[0])
70
71    if compile_model is None:
72        if torch_major < 2:
73            compile_model = False
74        elif is_compiled(model):  # model is already compiled
75            compile_model = False
76        else:
77            compile_model = default_compile
78
79    if compile_model:
80        if torch_major < 2:
81            raise RuntimeError("Model compilation is only supported for pytorch 2")
82
83        print("Compiling pytorch model ...")
84        if isinstance(compile_model, str):
85            model = torch.compile(model, mode=compile_model)
86        else:
87            model = torch.compile(model)
88
89    return model

Automatically compile a model for pytorch >= 2.

Arguments:
  • model: The model.
  • compile_model: Whether to comile the model. If None, it will not be compiled for torch < 2, and for torch > 2 the behavior specificed by 'default_compile' will be used. If a string is given it will be intepreted as the compile mode (torch.compile(model, mode=compile_model))
  • default_compile: Whether to use the default compilation behavior for torch 2.
Returns:

The compiled model.

def ensure_tensor( tensor: Union[torch.Tensor, numpy._typing._array_like._Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], complex, bytes, str, numpy._typing._nested_sequence._NestedSequence[complex | bytes | str]], dtype: Optional[str] = None) -> torch.Tensor:
 92def ensure_tensor(tensor: Union[torch.Tensor, ArrayLike], dtype: Optional[str] = None) -> torch.Tensor:
 93    """Ensure that the input is a torch tensor, by converting it if necessary.
 94
 95    Args:
 96        tensor: The input object, either a torch tensor or a numpy-array like object.
 97        dtype: The required data type for the output tensor.
 98
 99    Returns:
100        The input, converted to a torch tensor if necessary.
101    """
102    if isinstance(tensor, np.ndarray):
103        if np.dtype(tensor.dtype) in DTYPE_MAP:
104            tensor = tensor.astype(DTYPE_MAP[tensor.dtype])
105        # Try to convert the tensor, even if it has wrong byte-order
106        try:
107            tensor = torch.from_numpy(tensor if tensor.flags.writeable else tensor.copy())
108        except ValueError:
109            tensor = tensor.view(tensor.dtype.newbyteorder())
110            if np.dtype(tensor.dtype) in DTYPE_MAP:
111                tensor = tensor.astype(DTYPE_MAP[tensor.dtype])
112            tensor = torch.from_numpy(tensor)
113
114    assert torch.is_tensor(tensor), f"Cannot convert {type(tensor)} to torch"
115    if dtype is not None:
116        tensor = tensor.to(dtype=dtype)
117    return tensor

Ensure that the input is a torch tensor, by converting it if necessary.

Arguments:
  • tensor: The input object, either a torch tensor or a numpy-array like object.
  • dtype: The required data type for the output tensor.
Returns:

The input, converted to a torch tensor if necessary.

def validate_roi(roi, shape, patch_shape=None):
120def validate_roi(roi, shape, patch_shape=None):
121    """Normalize an ROI to explicit slices and validate that it is non-empty."""
122    if roi is None:
123        return None
124    if isinstance(roi, slice):
125        roi = (roi,)
126    if not isinstance(roi, tuple):
127        raise TypeError(f"Invalid roi type: {type(roi)}")
128    if len(roi) > len(shape):
129        raise ValueError(f"Invalid roi {roi} for data shape {shape}: too many dimensions")
130
131    normalized_roi = []
132    for this_roi, dim in zip(roi, shape):
133        if not isinstance(this_roi, slice):
134            raise TypeError(f"Invalid roi entry: {this_roi}. Only slices are supported")
135        step = 1 if this_roi.step is None else this_roi.step
136        if step != 1:
137            raise ValueError(f"Invalid roi {roi}: slice steps other than 1 are not supported")
138        start, stop, _ = this_roi.indices(dim)
139        normalized_roi.append(slice(start, stop))
140
141    if len(roi) < len(shape):
142        normalized_roi.extend(slice(0, dim) for dim in shape[len(roi):])
143
144    roi_shape = tuple(sl.stop - sl.start for sl in normalized_roi)
145    if any(sh <= 0 for sh in roi_shape):
146        msg = f"Invalid roi {roi} for data shape {shape}: it results in an empty region"
147        if patch_shape is not None:
148            msg += f" for patch_shape {patch_shape}"
149        raise ValueError(msg)
150
151    return tuple(normalized_roi)

Normalize an ROI to explicit slices and validate that it is non-empty.

def ensure_tensor_with_channels( tensor: Union[torch.Tensor, numpy._typing._array_like._Buffer, numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], complex, bytes, str, numpy._typing._nested_sequence._NestedSequence[complex | bytes | str]], ndim: int, dtype: Optional[str] = None) -> torch.Tensor:
154def ensure_tensor_with_channels(
155    tensor: Union[torch.Tensor, ArrayLike], ndim: int, dtype: Optional[str] = None
156) -> torch.Tensor:
157    """Ensure that the input is a torch tensor of a given dimensionality with channels.
158
159    Args:
160        tensor: The input tensor or numpy-array like data.
161        ndim: The dimensionality of the output tensor.
162        dtype: The data type of the output tensor.
163
164    Returns:
165        The input converted to a torch tensor of the requested dimensionality.
166    """
167    assert ndim in (2, 3, 4), f"{ndim}"
168    tensor = ensure_tensor(tensor, dtype)
169    if ndim == 2:
170        assert tensor.ndim in (2, 3, 4, 5), f"{tensor.ndim}"
171        if tensor.ndim == 2:
172            tensor = tensor[None]
173        elif tensor.ndim == 4:
174            assert tensor.shape[0] == 1, f"{tensor.shape}"
175            tensor = tensor[0]
176        elif tensor.ndim == 5:
177            assert tensor.shape[:2] == (1, 1), f"{tensor.shape}"
178            tensor = tensor[0, 0]
179    elif ndim == 3:
180        assert tensor.ndim in (3, 4, 5), f"{tensor.ndim}"
181        if tensor.ndim == 3:
182            tensor = tensor[None]
183        elif tensor.ndim == 5:
184            assert tensor.shape[0] == 1, f"{tensor.shape}"
185            tensor = tensor[0]
186    else:
187        assert tensor.ndim in (4, 5), f"{tensor.ndim}"
188        if tensor.ndim == 5:
189            assert tensor.shape[0] == 1, f"{tensor.shape}"
190            tensor = tensor[0]
191    return tensor

Ensure that the input is a torch tensor of a given dimensionality with channels.

Arguments:
  • tensor: The input tensor or numpy-array like data.
  • ndim: The dimensionality of the output tensor.
  • dtype: The data type of the output tensor.
Returns:

The input converted to a torch tensor of the requested dimensionality.

def ensure_array( array: Union[numpy.ndarray, torch.Tensor], dtype: str = None) -> numpy.ndarray:
194def ensure_array(array: Union[np.ndarray, torch.Tensor], dtype: str = None) -> np.ndarray:
195    """Ensure that the input is a numpy array, by converting it if necessary.
196
197    Args:
198        array: The input torch tensor or numpy array.
199        dtype: The dtype of the ouptut array.
200
201    Returns:
202        The input converted to a numpy array if necessary.
203    """
204    if torch.is_tensor(array):
205        array = array.detach().cpu().numpy()
206    assert isinstance(array, np.ndarray), f"Cannot convert {type(array)} to numpy"
207    if dtype is not None:
208        array = np.require(array, dtype=dtype)
209    return array

Ensure that the input is a numpy array, by converting it if necessary.

Arguments:
  • array: The input torch tensor or numpy array.
  • dtype: The dtype of the ouptut array.
Returns:

The input converted to a numpy array if necessary.

def ensure_spatial_array( array: Union[numpy.ndarray, torch.Tensor], ndim: int, dtype: str = None) -> numpy.ndarray:
212def ensure_spatial_array(array: Union[np.ndarray, torch.Tensor], ndim: int, dtype: str = None) -> np.ndarray:
213    """Ensure that the input is a numpy array of a given dimensionality.
214
215    Args:
216        array: The input numpy array or torch tensor.
217        ndim: The requested dimensionality.
218        dtype: The dtype of the output array.
219
220    Returns:
221        A numpy array of the requested dimensionality and data type.
222    """
223    assert ndim in (2, 3)
224    array = ensure_array(array, dtype)
225    if ndim == 2:
226        assert array.ndim in (2, 3, 4, 5), str(array.ndim)
227        if array.ndim == 3:
228            assert array.shape[0] == 1
229            array = array[0]
230        elif array.ndim == 4:
231            assert array.shape[:2] == (1, 1)
232            array = array[0, 0]
233        elif array.ndim == 5:
234            assert array.shape[:3] == (1, 1, 1)
235            array = array[0, 0, 0]
236    else:
237        assert array.ndim in (3, 4, 5), str(array.ndim)
238        if array.ndim == 4:
239            assert array.shape[0] == 1, f"{array.shape}"
240            array = array[0]
241        elif array.ndim == 5:
242            assert array.shape[:2] == (1, 1)
243            array = array[0, 0]
244    return array

Ensure that the input is a numpy array of a given dimensionality.

Arguments:
  • array: The input numpy array or torch tensor.
  • ndim: The requested dimensionality.
  • dtype: The dtype of the output array.
Returns:

A numpy array of the requested dimensionality and data type.

def ensure_patch_shape( raw: numpy.ndarray, labels: Optional[numpy.ndarray], patch_shape: Tuple[int, ...], have_raw_channels: bool = False, have_label_channels: bool = False, channel_first: bool = True) -> Union[Tuple[numpy.ndarray, numpy.ndarray], numpy.ndarray]:
247def ensure_patch_shape(
248    raw: np.ndarray,
249    labels: Optional[np.ndarray],
250    patch_shape: Tuple[int, ...],
251    have_raw_channels: bool = False,
252    have_label_channels: bool = False,
253    channel_first: bool = True,
254) -> Union[Tuple[np.ndarray, np.ndarray], np.ndarray]:
255    """Ensure that the raw data and labels have at least the requested patch shape.
256
257    If either raw data or labels do not have the patch shape they will be padded.
258
259    Args:
260        raw: The input raw data.
261        labels: The input labels.
262        patch_shape: The required minimal patch shape.
263        have_raw_channels: Whether the raw data has channels.
264        have_label_channels: Whether the label data has channels.
265        channel_first: Whether the channel axis is the first or last axis.
266
267    Returns:
268        The raw data.
269        The labels.
270    """
271    raw_shape = raw.shape
272    if labels is not None:
273        labels_shape = labels.shape
274
275    # In case the inputs has channels and they are channels first
276    # IMPORTANT: for ImageCollectionDataset
277    if have_raw_channels and channel_first:
278        raw_shape = raw_shape[1:]
279
280    if have_label_channels and channel_first and labels is not None:
281        labels_shape = labels_shape[1:]
282
283    # Extract the pad_width and pad the raw inputs
284    if any(sh < psh for sh, psh in zip(raw_shape, patch_shape)):
285        pw = [(0, max(0, psh - sh)) for sh, psh in zip(raw_shape, patch_shape)]
286
287        if have_raw_channels and channel_first:
288            pad_width = [(0, 0), *pw]
289        elif have_raw_channels and not channel_first:
290            pad_width = [*pw, (0, 0)]
291        else:
292            pad_width = pw
293
294        raw = np.pad(array=raw, pad_width=pad_width)
295
296    # Extract the pad width and pad the label inputs
297    if labels is not None and any(sh < psh for sh, psh in zip(labels_shape, patch_shape)):
298        pw = [(0, max(0, psh - sh)) for sh, psh in zip(labels_shape, patch_shape)]
299
300        if have_label_channels and channel_first:
301            pad_width = [(0, 0), *pw]
302        elif have_label_channels and not channel_first:
303            pad_width = [*pw, (0, 0)]
304        else:
305            pad_width = pw
306
307        labels = np.pad(array=labels, pad_width=pad_width)
308    if labels is None:
309        return raw
310    else:
311        return raw, labels

Ensure that the raw data and labels have at least the requested patch shape.

If either raw data or labels do not have the patch shape they will be padded.

Arguments:
  • raw: The input raw data.
  • labels: The input labels.
  • patch_shape: The required minimal patch shape.
  • have_raw_channels: Whether the raw data has channels.
  • have_label_channels: Whether the label data has channels.
  • channel_first: Whether the channel axis is the first or last axis.
Returns:

The raw data. The labels.

def get_trainer(checkpoint: str, name: str = 'best', device: Optional[str] = None):
381def get_trainer(checkpoint: str, name: str = "best", device: Optional[str] = None):
382    """Load trainer from a checkpoint.
383
384    Args:
385        checkpoint: The path to the checkpoint.
386        name: The name of the checkpoint.
387        device: The device to use for loading the checkpoint.
388
389    Returns:
390        The trainer.
391    """
392    # try to load from file
393    if isinstance(checkpoint, str):
394        assert os.path.exists(checkpoint), checkpoint
395        trainer = torch_em.trainer.DefaultTrainer.from_checkpoint(checkpoint, name=name, device=device)
396    else:
397        trainer = checkpoint
398    assert isinstance(trainer, torch_em.trainer.DefaultTrainer)
399    return trainer

Load trainer from a checkpoint.

Arguments:
  • checkpoint: The path to the checkpoint.
  • name: The name of the checkpoint.
  • device: The device to use for loading the checkpoint.
Returns:

The trainer.

def load_model( checkpoint: str, model: Optional[torch.nn.modules.module.Module] = None, name: str = 'best', state_key: Optional[str] = 'model_state', device: Optional[str] = None) -> torch.nn.modules.module.Module:
423def load_model(
424    checkpoint: str,
425    model: Optional[torch.nn.Module] = None,
426    name: str = "best",
427    state_key: Optional[str] = "model_state",
428    device: Optional[str] = None,
429) -> torch.nn.Module:
430    """Load a model from a trainer checkpoint or a serialized torch model.
431
432    This function can either load the model directly (`model` is not passed),
433    or deserialize the model state and then load it (`model` is passed).
434
435    The `checkpoint` argument must either point to the checkpoint directory of a torch_em trainer
436    or to a serialized torch model.
437
438    Args:
439        checkpoint: The path to the checkpoint folder or serialized torch model.
440        model: The model for which the state should be loaded.
441            If it is not passed, the model class and parameters will also be loaded from the trainer.
442        name: The name of the checkpoint.
443        state_key: The name of the model state to load. Set to None if the model state is stored top-level.
444        device: The device on which to load the model.
445
446    Returns:
447        The model.
448    """
449    if model is None and os.path.isdir(checkpoint):  # Load the model and its state from a torch_em checkpoint.
450        model = get_trainer(checkpoint, name=name, device=device).model
451
452    elif model is None:  # Load the model from a serialized model.
453        model = torch.load(checkpoint, map_location=device, weights_only=False)
454
455    else:  # Load the model state from a checkpoint.
456        if os.path.isdir(checkpoint):  # From a torch_em checkpoint.
457            ckpt = os.path.join(checkpoint, f"{name}.pt")
458        else:  # From a serialized path.
459            ckpt = checkpoint
460
461        state = torch.load(ckpt, map_location=device, weights_only=False)
462        if state_key is not None:
463            state = state[state_key]
464
465        # To enable loading compiled models.
466        compiled_prefix = "_orig_mod."
467        state = OrderedDict(
468            [(k[len(compiled_prefix):] if k.startswith(compiled_prefix) else k, v) for k, v in state.items()]
469        )
470
471        model.load_state_dict(state)
472        if device is not None:
473            model.to(device)
474
475    return model

Load a model from a trainer checkpoint or a serialized torch model.

This function can either load the model directly (model is not passed), or deserialize the model state and then load it (model is passed).

The checkpoint argument must either point to the checkpoint directory of a torch_em trainer or to a serialized torch model.

Arguments:
  • checkpoint: The path to the checkpoint folder or serialized torch model.
  • model: The model for which the state should be loaded. If it is not passed, the model class and parameters will also be loaded from the trainer.
  • name: The name of the checkpoint.
  • state_key: The name of the model state to load. Set to None if the model state is stored top-level.
  • device: The device on which to load the model.
Returns:

The model.

def get_random_colors(labels: numpy.ndarray) -> matplotlib.colors.ListedColormap:
487def get_random_colors(labels: np.ndarray) -> colors.ListedColormap:
488    """Generate a random color map for a label image.
489
490    Args:
491        labels: The labels.
492
493    Returns:
494        The color map.
495    """
496    unique_labels = np.unique(labels)
497    have_zero = 0 in unique_labels
498    cmap = [[0, 0, 0]] if have_zero else []
499    cmap += np.random.rand(len(unique_labels), 3).tolist()
500    cmap = colors.ListedColormap(cmap)
501    return cmap

Generate a random color map for a label image.

Arguments:
  • labels: The labels.
Returns:

The color map.