torch_em.transform.raw

  1from functools import partial
  2from typing import Union, Optional, Tuple, Dict, Callable
  3
  4import numpy as np
  5
  6import torch
  7from torchvision import transforms
  8
  9
 10#
 11# normalization functions
 12#
 13
 14
 15TORCH_DTYPES = {
 16    "float16": torch.float16,
 17    "float32": torch.float32,
 18    "float64": torch.float64,
 19    "complex64": torch.complex64,
 20    "complex128": torch.complex128,
 21    "uint8": torch.uint8,
 22    "int8": torch.int8,
 23    "int16": torch.int16,
 24    "int32": torch.int32,
 25    "int64": torch.int64,
 26    "bool": torch.bool,
 27}
 28"""@private
 29"""
 30
 31
 32def cast(inpt: Union[np.ndarray, torch.tensor], typestring: torch.dtype):
 33    """@private
 34    """
 35    if torch.is_tensor(inpt):
 36        assert typestring in TORCH_DTYPES, f"{typestring} not in TORCH_DTYPES"
 37        return inpt.to(TORCH_DTYPES[typestring])
 38    return inpt.astype(typestring)
 39
 40
 41def standardize(
 42    raw: np.ndarray,
 43    mean: Optional[float] = None,
 44    std: Optional[float] = None,
 45    axis: Optional[Union[int, Tuple[int, ...]]] = None,
 46    eps: float = 1e-7,
 47) -> np.ndarray:
 48    """Standardize the input data by subtracting its mean and dividing by its standard deviation.
 49
 50    Args:
 51        raw: The input data.
 52        mean: The mean value. If None, it will be computed from the data.
 53        std: The standard deviation. If None, it will be computed from the data.
 54        axis: The axis along which to compute the mean and standard deviation.
 55        eps: The epsilon value for numerical stability.
 56
 57    Returns:
 58        The standardized input data.
 59    """
 60    raw = cast(raw, "float32")
 61    mean = raw.mean(axis=axis, keepdims=True) if mean is None else mean
 62    raw -= mean
 63
 64    std = raw.std(axis=axis, keepdims=True) if std is None else std
 65    raw /= (std + eps)
 66    return raw
 67
 68
 69def _normalize_torch(tensor, minval, maxval, axis, eps):
 70    if axis:  # torch returns torch.return_types.min or torch.return_types.max
 71        minval = torch.amin(tensor, dim=axis, keepdim=True) if minval is None else minval
 72        tensor -= minval
 73
 74        maxval = torch.amax(tensor, dim=axis, keepdim=True) if maxval is None else maxval
 75        tensor /= (maxval + eps)
 76
 77        return tensor
 78
 79    # keepdim can only be used in combination with dim
 80    minval = tensor.min() if minval is None else minval
 81    tensor -= minval
 82
 83    maxval = tensor.max() if maxval is None else maxval
 84    tensor /= (maxval + eps)
 85
 86    return tensor
 87
 88
 89def normalize(
 90    raw: Union[torch.tensor, np.ndarray],
 91    minval: Optional[float] = None,
 92    maxval: Optional[float] = None,
 93    axis: Optional[Union[int, Tuple[int, ...]]] = None,
 94    eps: float = 1e-7,
 95) -> np.ndarray:
 96    """Normalize the input data so that it is in range [0, 1].
 97
 98    Args:
 99        raw: The input data.
100        minval: The minimum data value. If None, it will be computed from the data.
101        maxval: The maximum data value. If None, it will be computed from the data.
102        axis: The axis along which to compute the min and max value.
103        eps: The epsilon value for numerical stability.
104
105    Returns:
106        The normalized input data.
107    """
108    raw = cast(raw, "float32")
109    if torch.is_tensor(raw):
110        return _normalize_torch(raw, minval=minval, maxval=maxval, axis=axis, eps=eps)
111
112    minval = raw.min(axis=axis, keepdims=True) if minval is None else minval
113    raw -= minval
114
115    maxval = raw.max(axis=axis, keepdims=True) if maxval is None else maxval
116    raw /= (maxval + eps)
117    return raw
118
119
120def normalize_percentile(
121    raw: np.ndarray,
122    lower: float = 1.0,
123    upper: float = 99.0,
124    axis: Optional[Union[int, Tuple[int, ...]]] = None,
125    eps: float = 1e-7,
126) -> np.ndarray:
127    """Normalize the input data based on percentile values.
128
129    Args:
130        raw: The input data.
131        lower: The lower percentile.
132        upper: The upper percentile.
133        axis: The axis along which to compute the percentiles.
134        eps: The epsilon value for numerical stability.
135
136    Returns:
137        The normalized input data.
138    """
139    v_lower = np.percentile(raw, lower, axis=axis, keepdims=True)
140    v_upper = np.percentile(raw, upper, axis=axis, keepdims=True) - v_lower
141    return normalize(raw, v_lower, v_upper, eps=eps)
142
143
144def _normalize_percentile_to_range(
145    raw: Union[np.ndarray, torch.Tensor], clip_max: int
146) -> Union[np.ndarray, torch.Tensor]:
147    raw = normalize_percentile(raw)
148    if torch.is_tensor(raw):
149        return torch.clamp(raw, min=0.0, max=1.0) * clip_max
150    return np.clip(raw, 0.0, 1.0) * clip_max
151
152
153class RandomPercentileNormalization:
154    """Normalize inputs with randomly sampled percentile bounds.
155
156    By default, the lower and upper percentiles are sampled uniformly from
157    ``lower_percentile_bounds`` and ``upper_percentile_bounds``. If no upper bounds are given,
158    they are inferred by mirroring the lower bounds around 50. Normal (Gaussian) sampling can be
159    enabled explicitly with ``distribution="normal"`` and
160    ``distribution_kwargs={"mean": ..., "std": ...}``. The sampled percentile intensities are
161    mapped to 0 and 1, and values outside them are clipped, so the output is always in ``[0, 1]``.
162
163    Examples:
164        Uniform sampling with the default percentile bounds and reproducible random draws:
165
166        ```python
167        normalization = RandomPercentileNormalization(seed=42)
168        ```
169
170        Normal sampling with explicit distribution parameters:
171
172        ```python
173        normalization = RandomPercentileNormalization(
174            distribution="normal",
175            distribution_kwargs={"mean": 2.0, "std": 1.0},
176            seed=42,
177        )
178        ```
179
180    Args:
181        lower_percentile_bounds: Inclusive clipping bounds for the lower percentile.
182        upper_percentile_bounds: Inclusive clipping bounds for the upper percentile. If None, the
183            bounds are inferred by mirroring ``lower_percentile_bounds`` around 50.
184        distribution: Sampling distribution for the percentiles. Supported values are
185            ``"uniform"`` and ``"normal"``.
186        distribution_kwargs: Parameters for normal sampling, which must contain exactly
187            ``{"mean": ..., "std": ...}``. The upper percentile uses the mirrored normal
188            distribution. Uniform sampling does not take additional parameters.
189        rounding_decimals: Number of decimal places used to round sampled percentiles. Set to None
190            to disable rounding.
191        axis: Axes over which to compute the intensity percentiles.
192        seed: Optional non-negative integer seed for reproducible sampling. NumPy integer types
193            are also supported. Each DataLoader worker derives a distinct stream from this seed.
194            By default, the global NumPy random state is used, which respects DataLoader worker seeding.
195        eps: Epsilon used for numerical stability during normalization.
196    """
197
198    def __init__(
199        self,
200        lower_percentile_bounds: Tuple[float, float] = (0.0, 5.0),
201        upper_percentile_bounds: Optional[Tuple[float, float]] = None,
202        distribution: str = "uniform",
203        distribution_kwargs: Optional[Dict[str, float]] = None,
204        rounding_decimals: Optional[int] = 1,
205        axis: Optional[Union[int, Tuple[int, ...]]] = None,
206        seed: Optional[int] = None,
207        eps: float = 1e-7,
208    ):
209        lower_percentile_bounds = self._validate_bounds(lower_percentile_bounds, upper=False)
210        if upper_percentile_bounds is None:
211            upper_percentile_bounds = tuple(100.0 - bound for bound in reversed(lower_percentile_bounds))
212        upper_percentile_bounds = self._validate_bounds(upper_percentile_bounds, upper=True)
213        if distribution not in ("uniform", "normal"):
214            raise ValueError("distribution must be 'uniform' or 'normal'.")
215
216        if distribution == "uniform":
217            if distribution_kwargs is not None:
218                raise ValueError("Uniform sampling does not accept distribution_kwargs.")
219        else:
220            if not isinstance(distribution_kwargs, dict) or set(distribution_kwargs) != {"mean", "std"}:
221                raise ValueError("Normal sampling requires exactly the distribution_kwargs 'mean' and 'std'.")
222            mean, std = float(distribution_kwargs["mean"]), float(distribution_kwargs["std"])
223            if not np.isfinite(mean) or not lower_percentile_bounds[0] <= mean <= lower_percentile_bounds[1]:
224                raise ValueError("The normal distribution mean must be finite and within lower_percentile_bounds.")
225            if not np.isfinite(std) or std < 0.0:
226                raise ValueError("The normal distribution std must be finite and non-negative.")
227            distribution_kwargs = {"mean": mean, "std": std}
228
229        if rounding_decimals is not None and (
230            not isinstance(rounding_decimals, int) or isinstance(rounding_decimals, bool) or rounding_decimals < 0
231        ):
232            raise ValueError("rounding_decimals must be a non-negative integer or None.")
233        if not np.isfinite(eps) or eps <= 0.0:
234            raise ValueError("eps must be finite and greater than zero.")
235        if seed is not None:
236            if not isinstance(seed, (int, np.integer)) or isinstance(seed, bool):
237                raise TypeError("seed must be an integer or None.")
238            if seed < 0:
239                raise ValueError("seed must be non-negative.")
240            seed = int(seed)
241
242        self.lower_percentile_bounds = lower_percentile_bounds
243        self.upper_percentile_bounds = upper_percentile_bounds
244        self.distribution = distribution
245        self.distribution_kwargs = distribution_kwargs
246        self.rounding_decimals = rounding_decimals
247        self.axis = axis
248        self.seed = seed
249        self.eps = float(eps)
250        self._random_generator = None
251        self._random_generator_worker_id = None
252
253    @staticmethod
254    def _validate_bounds(values, upper):
255        name = "upper_percentile_bounds" if upper else "lower_percentile_bounds"
256        if not isinstance(values, (tuple, list)) or len(values) != 2:
257            raise ValueError(f"{name} must contain exactly two values.")
258        lower_bound, upper_bound = (float(value) for value in values)
259        finite = np.isfinite(lower_bound) and np.isfinite(upper_bound)
260        if upper:
261            valid = 50.0 < lower_bound <= upper_bound <= 100.0
262            interval = "(50, 100]"
263        else:
264            valid = 0.0 <= lower_bound <= upper_bound < 50.0
265            interval = "[0, 50)"
266        if not finite or not valid:
267            raise ValueError(f"{name} must be a finite interval within {interval}.")
268        return lower_bound, upper_bound
269
270    def _round(self, value):
271        return float(value) if self.rounding_decimals is None else round(float(value), self.rounding_decimals)
272
273    def _get_random_generator(self):
274        if self.seed is None:
275            return np.random
276
277        worker_info = torch.utils.data.get_worker_info()
278        worker_id = None if worker_info is None else worker_info.id
279        if self._random_generator is None or self._random_generator_worker_id != worker_id:
280            seed_sequence = np.random.SeedSequence([self.seed, 0 if worker_id is None else worker_id])
281            self._random_generator = np.random.default_rng(seed_sequence)
282            self._random_generator_worker_id = worker_id
283        return self._random_generator
284
285    def sample_percentiles(self) -> Tuple[float, float]:
286        """Sample and return a valid ``(lower, upper)`` percentile pair."""
287        random_generator = self._get_random_generator()
288        if self.distribution == "uniform":
289            lower = random_generator.uniform(*self.lower_percentile_bounds)
290            upper = random_generator.uniform(*self.upper_percentile_bounds)
291        else:
292            mean = self.distribution_kwargs["mean"]
293            std = self.distribution_kwargs["std"]
294            lower = mean if std == 0.0 else random_generator.normal(mean, std)
295            upper = 100.0 - (mean if std == 0.0 else random_generator.normal(mean, std))
296
297        # Normal distribution tails may leave the configured percentile interval.
298        lower = float(np.clip(self._round(lower), *self.lower_percentile_bounds))
299        upper = float(np.clip(self._round(upper), *self.upper_percentile_bounds))
300        return lower, upper
301
302    def __call__(self, raw: Union[np.ndarray, torch.tensor]) -> Union[np.ndarray, torch.tensor]:
303        lower, upper = self.sample_percentiles()
304        normalized = normalize_percentile(raw, lower=lower, upper=upper, axis=self.axis, eps=self.eps)
305        if torch.is_tensor(normalized):
306            return torch.clamp(normalized, min=0.0, max=1.0)
307        return np.clip(normalized, 0.0, 1.0)
308
309
310#
311# Intensity Augmentations / Noise Augmentations.
312#
313
314# modified from https://github.com/kreshuklab/spoco/blob/main/spoco/transforms.py
315class RandomContrast:
316    """Transformation to adjust contrast by scaling image to `mean + alpha * (image - mean)`.
317
318    Args:
319        alpha: Minimal and maximal alpha value for adjusting the contrast.
320            The value for the transformation will be drawn uniformly from the corresponding interval.
321        mean: Mean value for the image data.
322        clip_kwargs: Keyword arguments for clipping the data after the contrast augmentation.
323    """
324    def __init__(
325        self, alpha: Tuple[float, float] = (0.5, 2), mean: float = 0.5, clip_kwargs: Dict = {"a_min": 0, "a_max": 1}
326    ):
327        self.alpha = alpha
328        self.mean = mean
329        self.clip_kwargs = clip_kwargs
330
331    def __call__(self, img: np.ndarray) -> np.ndarray:
332        """Apply the augmentation to data.
333
334        Args:
335            img: The input image.
336
337        Returns:
338            The transformed image.
339        """
340        alpha = np.random.uniform(self.alpha[0], self.alpha[1])
341        result = self.mean + alpha * (img - self.mean)
342        if self.clip_kwargs:
343            return np.clip(result, **self.clip_kwargs)
344        return result
345
346
347class AdditiveGaussianNoise:
348    """Transformation to add random Gaussian noise to image.
349
350    Args:
351        scale: Scale for the noise.
352        clip_kwargs: Keyword arguments for clipping the data after the transformation.
353    """
354    def __init__(self, scale: Tuple[float, float] = (0.0, 0.3), clip_kwargs: Dict = {"a_min": 0, "a_max": 1}):
355        self.scale = scale
356        self.clip_kwargs = clip_kwargs
357
358    def __call__(self, img: np.ndarray) -> np.ndarray:
359        """Apply the augmentation to data.
360
361        Args:
362            img: The input image.
363
364        Returns:
365            The transformed image.
366        """
367        std = np.random.uniform(self.scale[0], self.scale[1])
368        gaussian_noise = np.random.normal(0, std, size=img.shape)
369
370        if self.clip_kwargs:
371            return np.clip(img + gaussian_noise, 0, 1)
372
373        return img + gaussian_noise
374
375
376class AdditivePoissonNoise:
377    """Transformation to add random additive Poisson noise to image.
378
379    Args:
380        lam: Lambda value for the Poisson transformation.
381        clip_kwargs: Keyword arguments for clipping the data after the transformation.
382    """
383    # Not sure if Poisson noise like this does make sense for data that is already normalized
384    def __init__(self, lam: Tuple[float, float] = (0.0, 0.1), clip_kwargs: Dict = {"a_min": 0, "a_max": 1}):
385        self.lam = lam
386        self.clip_kwargs = clip_kwargs
387
388    def __call__(self, img: np.ndarray) -> np.ndarray:
389        """Apply the augmentation to data.
390
391        Args:
392            img: The input image.
393
394        Returns:
395            The transformed image.
396        """
397        lam = np.random.uniform(self.lam[0], self.lam[1])
398        poisson_noise = np.random.poisson(lam, size=img.shape) / lam
399        if self.clip_kwargs:
400            return np.clip(img + poisson_noise, 0, 1)
401        return img + poisson_noise
402
403
404class PoissonNoise:
405    """Transformation to add random data-dependant Poisson noise to image.
406
407    Args:
408        multiplier: Multiplicative factors for deriving the lambda factor from the data.
409            The factor used for the transformation will be uniformly sampled form the range of this parameter.
410        clip_kwargs: Keyword arguments for clipping the data after the transformation.
411    """
412    def __init__(self, multiplier: Tuple[float, float] = (5.0, 10.0), clip_kwargs: Dict = {"a_min": 0, "a_max": 1}):
413        self.multiplier = multiplier
414        self.clip_kwargs = clip_kwargs
415
416    def __call__(self, img: np.ndarray) -> np.ndarray:
417        """Apply the augmentation to data.
418
419        Args:
420            img: The input image.
421
422        Returns:
423            The transformed image.
424        """
425        multiplier = np.random.uniform(self.multiplier[0], self.multiplier[1])
426        offset = img.min()
427        poisson_noise = np.random.poisson((img - offset) * multiplier)
428
429        if isinstance(img, torch.Tensor):
430            poisson_noise = torch.Tensor(poisson_noise)
431        poisson_noise = poisson_noise / multiplier + offset
432
433        if self.clip_kwargs:
434            return np.clip(poisson_noise, **self.clip_kwargs)
435        return poisson_noise
436
437
438class GaussianBlur:
439    """Transformation to blur the image with a randomly drawn sigma value.
440
441    Args:
442        sigma: The sigma value for the transformation.
443            The value used in the transformation will be uniformly drawn from the range specified here.
444    """
445    def __init__(self, sigma: Tuple[float, float] = (0.0, 3.0)):
446        self.sigma = sigma
447
448    def __call__(self, img: np.ndarray) -> np.ndarray:
449        """Apply the augmentation to data.
450
451        Args:
452            img: The input image.
453
454        Returns:
455            The transformed image.
456        """
457        # Sample the sigma value. Note that we switch the bounds to ensure zero is excluded from sampling.
458        sigma = np.random.uniform(self.sigma[1], self.sigma[0])
459        # Determine the kernel size based on the sigma value.
460        kernel_size = int(2 * np.ceil(3 * sigma) + 1)
461        if isinstance(img, np.ndarray):
462            img = torch.from_numpy(img)
463
464        return transforms.GaussianBlur(kernel_size, sigma=sigma)(img)
465
466
467#
468# Default Transformation: Apply intensity augmentations and normalize.
469#
470
471class RawTransform:
472    """The transformation for raw data during training.
473
474    Args:
475        normalizer: The normalization function.
476        augmentation1: Intensity augmentation applied before the normalization.
477        augmentation2: Intensity augmentation applied after the normalization.
478    """
479    def __init__(
480        self, normalizer: Callable, augmentation1: Optional[Callable] = None, augmentation2: Optional[Callable] = None
481    ):
482        self.normalizer = normalizer
483        self.augmentation1 = augmentation1
484        self.augmentation2 = augmentation2
485
486    def __call__(self, raw: np.ndarray) -> np.ndarray:
487        """Apply the raw transformation.
488
489        Args:
490            raw: The raw data.
491
492        Returns:
493            The transformed raw data.
494        """
495        if self.augmentation1 is not None:
496            raw = self.augmentation1(raw)
497
498        raw = self.normalizer(raw)
499
500        if self.augmentation2 is not None:
501            raw = self.augmentation2(raw)
502        return raw
503
504
505def get_raw_transform(
506    normalizer: Callable = standardize,
507    augmentation1: Optional[Callable] = None,
508    augmentation2: Optional[Callable] = None
509) -> Callable:
510    """Get the raw transformation.
511
512    Args:
513        normalizer: The normalization function.
514        augmentation1: Intensity augmentation applied before the normalization.
515        augmentation2: Intensity augmentation applied after the normalization.
516
517    Returns:
518        The raw transformation.
519    """
520    return RawTransform(normalizer, augmentation1=augmentation1, augmentation2=augmentation2)
521
522
523def get_raw_transform_for_pretrained_vit(backbone: Optional[str]) -> Tuple[Optional[Callable], Optional[int]]:
524    """Get the raw transformation and clipping limit for a pretrained ViT backbone.
525
526    Args:
527        backbone: The pretrained ViT backbone. Options: "sam", "sam2", "sam3", "cellpose_sam", "mae", "scalemae",
528            "dinov2", "dinov3", "torchvision", or None.
529
530    Returns:
531        The raw transformation. It normalizes percentiles to [0, 255] for "sam" and [0, 1] for the other backbones.
532        The maximum value `clip_max` for intensity augmentations. Use it to clip their output to [0, clip_max].
533            Both values are None if `backbone` is None, so data loaders use the default standardization.
534    """
535    if backbone is None:
536        return None, None
537
538    from ..model.util import VIT_CONFIGS
539
540    if backbone not in VIT_CONFIGS:
541        raise ValueError(f"Unsupported backbone: {backbone}.")
542
543    clip_max = VIT_CONFIGS[backbone].clip_max
544    normalizer = partial(_normalize_percentile_to_range, clip_max=clip_max)
545    return get_raw_transform(normalizer=normalizer), clip_max
546
547
548def get_default_mean_teacher_augmentations(
549    p: float = 0.3,
550    norm: Optional[Callable] = None,
551    blur_kwargs: Optional[Dict] = None,
552    poisson_kwargs: Optional[Dict] = None,
553    gaussian_kwargs: Optional[Dict] = None,
554) -> Callable:
555    """Get the default augmentations for mean teacher training.
556
557    The default values for the augmentations are designed for an image with pixel values in range [0, 1].
558    By default, a normalization transformation is applied for this reason.
559
560    Args:
561        p: The probability for applying the individual intensity transformations.
562        norm: The noromaization function.
563        blur_kwargs: The keyword arguments for `GaussianBlur`.
564        poisson_kwargs: The keyword arguments for `PoissonNoise`.
565        gaussian_kwargs: The keyword arguments for `AdditiveGaussianNoise`.
566
567    Returns:
568        The raw transformation with augmentations.
569    """
570    if norm is None:
571        norm = normalize
572
573    aug1 = transforms.Compose([
574        norm,
575        transforms.RandomApply([GaussianBlur(**({} if blur_kwargs is None else blur_kwargs))], p=p),
576        transforms.RandomApply([PoissonNoise(**({} if poisson_kwargs is None else poisson_kwargs))], p=p/2),
577        transforms.RandomApply([AdditiveGaussianNoise(**({} if gaussian_kwargs is None else gaussian_kwargs))], p=p/2),
578    ])
579
580    aug2 = transforms.RandomApply([RandomContrast(clip_kwargs={"a_min": 0, "a_max": 1})], p=p)
581    return get_raw_transform(normalizer=norm, augmentation1=aug1, augmentation2=aug2)
def standardize( raw: numpy.ndarray, mean: Optional[float] = None, std: Optional[float] = None, axis: Union[int, Tuple[int, ...], NoneType] = None, eps: float = 1e-07) -> numpy.ndarray:
42def standardize(
43    raw: np.ndarray,
44    mean: Optional[float] = None,
45    std: Optional[float] = None,
46    axis: Optional[Union[int, Tuple[int, ...]]] = None,
47    eps: float = 1e-7,
48) -> np.ndarray:
49    """Standardize the input data by subtracting its mean and dividing by its standard deviation.
50
51    Args:
52        raw: The input data.
53        mean: The mean value. If None, it will be computed from the data.
54        std: The standard deviation. If None, it will be computed from the data.
55        axis: The axis along which to compute the mean and standard deviation.
56        eps: The epsilon value for numerical stability.
57
58    Returns:
59        The standardized input data.
60    """
61    raw = cast(raw, "float32")
62    mean = raw.mean(axis=axis, keepdims=True) if mean is None else mean
63    raw -= mean
64
65    std = raw.std(axis=axis, keepdims=True) if std is None else std
66    raw /= (std + eps)
67    return raw

Standardize the input data by subtracting its mean and dividing by its standard deviation.

Arguments:
  • raw: The input data.
  • mean: The mean value. If None, it will be computed from the data.
  • std: The standard deviation. If None, it will be computed from the data.
  • axis: The axis along which to compute the mean and standard deviation.
  • eps: The epsilon value for numerical stability.
Returns:

The standardized input data.

def normalize( raw: Union[<built-in method tensor of type object>, numpy.ndarray], minval: Optional[float] = None, maxval: Optional[float] = None, axis: Union[int, Tuple[int, ...], NoneType] = None, eps: float = 1e-07) -> numpy.ndarray:
 90def normalize(
 91    raw: Union[torch.tensor, np.ndarray],
 92    minval: Optional[float] = None,
 93    maxval: Optional[float] = None,
 94    axis: Optional[Union[int, Tuple[int, ...]]] = None,
 95    eps: float = 1e-7,
 96) -> np.ndarray:
 97    """Normalize the input data so that it is in range [0, 1].
 98
 99    Args:
100        raw: The input data.
101        minval: The minimum data value. If None, it will be computed from the data.
102        maxval: The maximum data value. If None, it will be computed from the data.
103        axis: The axis along which to compute the min and max value.
104        eps: The epsilon value for numerical stability.
105
106    Returns:
107        The normalized input data.
108    """
109    raw = cast(raw, "float32")
110    if torch.is_tensor(raw):
111        return _normalize_torch(raw, minval=minval, maxval=maxval, axis=axis, eps=eps)
112
113    minval = raw.min(axis=axis, keepdims=True) if minval is None else minval
114    raw -= minval
115
116    maxval = raw.max(axis=axis, keepdims=True) if maxval is None else maxval
117    raw /= (maxval + eps)
118    return raw

Normalize the input data so that it is in range [0, 1].

Arguments:
  • raw: The input data.
  • minval: The minimum data value. If None, it will be computed from the data.
  • maxval: The maximum data value. If None, it will be computed from the data.
  • axis: The axis along which to compute the min and max value.
  • eps: The epsilon value for numerical stability.
Returns:

The normalized input data.

def normalize_percentile( raw: numpy.ndarray, lower: float = 1.0, upper: float = 99.0, axis: Union[int, Tuple[int, ...], NoneType] = None, eps: float = 1e-07) -> numpy.ndarray:
121def normalize_percentile(
122    raw: np.ndarray,
123    lower: float = 1.0,
124    upper: float = 99.0,
125    axis: Optional[Union[int, Tuple[int, ...]]] = None,
126    eps: float = 1e-7,
127) -> np.ndarray:
128    """Normalize the input data based on percentile values.
129
130    Args:
131        raw: The input data.
132        lower: The lower percentile.
133        upper: The upper percentile.
134        axis: The axis along which to compute the percentiles.
135        eps: The epsilon value for numerical stability.
136
137    Returns:
138        The normalized input data.
139    """
140    v_lower = np.percentile(raw, lower, axis=axis, keepdims=True)
141    v_upper = np.percentile(raw, upper, axis=axis, keepdims=True) - v_lower
142    return normalize(raw, v_lower, v_upper, eps=eps)

Normalize the input data based on percentile values.

Arguments:
  • raw: The input data.
  • lower: The lower percentile.
  • upper: The upper percentile.
  • axis: The axis along which to compute the percentiles.
  • eps: The epsilon value for numerical stability.
Returns:

The normalized input data.

class RandomPercentileNormalization:
154class RandomPercentileNormalization:
155    """Normalize inputs with randomly sampled percentile bounds.
156
157    By default, the lower and upper percentiles are sampled uniformly from
158    ``lower_percentile_bounds`` and ``upper_percentile_bounds``. If no upper bounds are given,
159    they are inferred by mirroring the lower bounds around 50. Normal (Gaussian) sampling can be
160    enabled explicitly with ``distribution="normal"`` and
161    ``distribution_kwargs={"mean": ..., "std": ...}``. The sampled percentile intensities are
162    mapped to 0 and 1, and values outside them are clipped, so the output is always in ``[0, 1]``.
163
164    Examples:
165        Uniform sampling with the default percentile bounds and reproducible random draws:
166
167        ```python
168        normalization = RandomPercentileNormalization(seed=42)
169        ```
170
171        Normal sampling with explicit distribution parameters:
172
173        ```python
174        normalization = RandomPercentileNormalization(
175            distribution="normal",
176            distribution_kwargs={"mean": 2.0, "std": 1.0},
177            seed=42,
178        )
179        ```
180
181    Args:
182        lower_percentile_bounds: Inclusive clipping bounds for the lower percentile.
183        upper_percentile_bounds: Inclusive clipping bounds for the upper percentile. If None, the
184            bounds are inferred by mirroring ``lower_percentile_bounds`` around 50.
185        distribution: Sampling distribution for the percentiles. Supported values are
186            ``"uniform"`` and ``"normal"``.
187        distribution_kwargs: Parameters for normal sampling, which must contain exactly
188            ``{"mean": ..., "std": ...}``. The upper percentile uses the mirrored normal
189            distribution. Uniform sampling does not take additional parameters.
190        rounding_decimals: Number of decimal places used to round sampled percentiles. Set to None
191            to disable rounding.
192        axis: Axes over which to compute the intensity percentiles.
193        seed: Optional non-negative integer seed for reproducible sampling. NumPy integer types
194            are also supported. Each DataLoader worker derives a distinct stream from this seed.
195            By default, the global NumPy random state is used, which respects DataLoader worker seeding.
196        eps: Epsilon used for numerical stability during normalization.
197    """
198
199    def __init__(
200        self,
201        lower_percentile_bounds: Tuple[float, float] = (0.0, 5.0),
202        upper_percentile_bounds: Optional[Tuple[float, float]] = None,
203        distribution: str = "uniform",
204        distribution_kwargs: Optional[Dict[str, float]] = None,
205        rounding_decimals: Optional[int] = 1,
206        axis: Optional[Union[int, Tuple[int, ...]]] = None,
207        seed: Optional[int] = None,
208        eps: float = 1e-7,
209    ):
210        lower_percentile_bounds = self._validate_bounds(lower_percentile_bounds, upper=False)
211        if upper_percentile_bounds is None:
212            upper_percentile_bounds = tuple(100.0 - bound for bound in reversed(lower_percentile_bounds))
213        upper_percentile_bounds = self._validate_bounds(upper_percentile_bounds, upper=True)
214        if distribution not in ("uniform", "normal"):
215            raise ValueError("distribution must be 'uniform' or 'normal'.")
216
217        if distribution == "uniform":
218            if distribution_kwargs is not None:
219                raise ValueError("Uniform sampling does not accept distribution_kwargs.")
220        else:
221            if not isinstance(distribution_kwargs, dict) or set(distribution_kwargs) != {"mean", "std"}:
222                raise ValueError("Normal sampling requires exactly the distribution_kwargs 'mean' and 'std'.")
223            mean, std = float(distribution_kwargs["mean"]), float(distribution_kwargs["std"])
224            if not np.isfinite(mean) or not lower_percentile_bounds[0] <= mean <= lower_percentile_bounds[1]:
225                raise ValueError("The normal distribution mean must be finite and within lower_percentile_bounds.")
226            if not np.isfinite(std) or std < 0.0:
227                raise ValueError("The normal distribution std must be finite and non-negative.")
228            distribution_kwargs = {"mean": mean, "std": std}
229
230        if rounding_decimals is not None and (
231            not isinstance(rounding_decimals, int) or isinstance(rounding_decimals, bool) or rounding_decimals < 0
232        ):
233            raise ValueError("rounding_decimals must be a non-negative integer or None.")
234        if not np.isfinite(eps) or eps <= 0.0:
235            raise ValueError("eps must be finite and greater than zero.")
236        if seed is not None:
237            if not isinstance(seed, (int, np.integer)) or isinstance(seed, bool):
238                raise TypeError("seed must be an integer or None.")
239            if seed < 0:
240                raise ValueError("seed must be non-negative.")
241            seed = int(seed)
242
243        self.lower_percentile_bounds = lower_percentile_bounds
244        self.upper_percentile_bounds = upper_percentile_bounds
245        self.distribution = distribution
246        self.distribution_kwargs = distribution_kwargs
247        self.rounding_decimals = rounding_decimals
248        self.axis = axis
249        self.seed = seed
250        self.eps = float(eps)
251        self._random_generator = None
252        self._random_generator_worker_id = None
253
254    @staticmethod
255    def _validate_bounds(values, upper):
256        name = "upper_percentile_bounds" if upper else "lower_percentile_bounds"
257        if not isinstance(values, (tuple, list)) or len(values) != 2:
258            raise ValueError(f"{name} must contain exactly two values.")
259        lower_bound, upper_bound = (float(value) for value in values)
260        finite = np.isfinite(lower_bound) and np.isfinite(upper_bound)
261        if upper:
262            valid = 50.0 < lower_bound <= upper_bound <= 100.0
263            interval = "(50, 100]"
264        else:
265            valid = 0.0 <= lower_bound <= upper_bound < 50.0
266            interval = "[0, 50)"
267        if not finite or not valid:
268            raise ValueError(f"{name} must be a finite interval within {interval}.")
269        return lower_bound, upper_bound
270
271    def _round(self, value):
272        return float(value) if self.rounding_decimals is None else round(float(value), self.rounding_decimals)
273
274    def _get_random_generator(self):
275        if self.seed is None:
276            return np.random
277
278        worker_info = torch.utils.data.get_worker_info()
279        worker_id = None if worker_info is None else worker_info.id
280        if self._random_generator is None or self._random_generator_worker_id != worker_id:
281            seed_sequence = np.random.SeedSequence([self.seed, 0 if worker_id is None else worker_id])
282            self._random_generator = np.random.default_rng(seed_sequence)
283            self._random_generator_worker_id = worker_id
284        return self._random_generator
285
286    def sample_percentiles(self) -> Tuple[float, float]:
287        """Sample and return a valid ``(lower, upper)`` percentile pair."""
288        random_generator = self._get_random_generator()
289        if self.distribution == "uniform":
290            lower = random_generator.uniform(*self.lower_percentile_bounds)
291            upper = random_generator.uniform(*self.upper_percentile_bounds)
292        else:
293            mean = self.distribution_kwargs["mean"]
294            std = self.distribution_kwargs["std"]
295            lower = mean if std == 0.0 else random_generator.normal(mean, std)
296            upper = 100.0 - (mean if std == 0.0 else random_generator.normal(mean, std))
297
298        # Normal distribution tails may leave the configured percentile interval.
299        lower = float(np.clip(self._round(lower), *self.lower_percentile_bounds))
300        upper = float(np.clip(self._round(upper), *self.upper_percentile_bounds))
301        return lower, upper
302
303    def __call__(self, raw: Union[np.ndarray, torch.tensor]) -> Union[np.ndarray, torch.tensor]:
304        lower, upper = self.sample_percentiles()
305        normalized = normalize_percentile(raw, lower=lower, upper=upper, axis=self.axis, eps=self.eps)
306        if torch.is_tensor(normalized):
307            return torch.clamp(normalized, min=0.0, max=1.0)
308        return np.clip(normalized, 0.0, 1.0)

Normalize inputs with randomly sampled percentile bounds.

By default, the lower and upper percentiles are sampled uniformly from lower_percentile_bounds and upper_percentile_bounds. If no upper bounds are given, they are inferred by mirroring the lower bounds around 50. Normal (Gaussian) sampling can be enabled explicitly with distribution="normal" and distribution_kwargs={"mean": ..., "std": ...}. The sampled percentile intensities are mapped to 0 and 1, and values outside them are clipped, so the output is always in [0, 1].

Examples:

Uniform sampling with the default percentile bounds and reproducible random draws:

normalization = RandomPercentileNormalization(seed=42)

Normal sampling with explicit distribution parameters:

normalization = RandomPercentileNormalization(
    distribution="normal",
    distribution_kwargs={"mean": 2.0, "std": 1.0},
    seed=42,
)
Arguments:
  • lower_percentile_bounds: Inclusive clipping bounds for the lower percentile.
  • upper_percentile_bounds: Inclusive clipping bounds for the upper percentile. If None, the bounds are inferred by mirroring lower_percentile_bounds around 50.
  • distribution: Sampling distribution for the percentiles. Supported values are "uniform" and "normal".
  • distribution_kwargs: Parameters for normal sampling, which must contain exactly {"mean": ..., "std": ...}. The upper percentile uses the mirrored normal distribution. Uniform sampling does not take additional parameters.
  • rounding_decimals: Number of decimal places used to round sampled percentiles. Set to None to disable rounding.
  • axis: Axes over which to compute the intensity percentiles.
  • seed: Optional non-negative integer seed for reproducible sampling. NumPy integer types are also supported. Each DataLoader worker derives a distinct stream from this seed. By default, the global NumPy random state is used, which respects DataLoader worker seeding.
  • eps: Epsilon used for numerical stability during normalization.
RandomPercentileNormalization( lower_percentile_bounds: Tuple[float, float] = (0.0, 5.0), upper_percentile_bounds: Optional[Tuple[float, float]] = None, distribution: str = 'uniform', distribution_kwargs: Optional[Dict[str, float]] = None, rounding_decimals: Optional[int] = 1, axis: Union[int, Tuple[int, ...], NoneType] = None, seed: Optional[int] = None, eps: float = 1e-07)
199    def __init__(
200        self,
201        lower_percentile_bounds: Tuple[float, float] = (0.0, 5.0),
202        upper_percentile_bounds: Optional[Tuple[float, float]] = None,
203        distribution: str = "uniform",
204        distribution_kwargs: Optional[Dict[str, float]] = None,
205        rounding_decimals: Optional[int] = 1,
206        axis: Optional[Union[int, Tuple[int, ...]]] = None,
207        seed: Optional[int] = None,
208        eps: float = 1e-7,
209    ):
210        lower_percentile_bounds = self._validate_bounds(lower_percentile_bounds, upper=False)
211        if upper_percentile_bounds is None:
212            upper_percentile_bounds = tuple(100.0 - bound for bound in reversed(lower_percentile_bounds))
213        upper_percentile_bounds = self._validate_bounds(upper_percentile_bounds, upper=True)
214        if distribution not in ("uniform", "normal"):
215            raise ValueError("distribution must be 'uniform' or 'normal'.")
216
217        if distribution == "uniform":
218            if distribution_kwargs is not None:
219                raise ValueError("Uniform sampling does not accept distribution_kwargs.")
220        else:
221            if not isinstance(distribution_kwargs, dict) or set(distribution_kwargs) != {"mean", "std"}:
222                raise ValueError("Normal sampling requires exactly the distribution_kwargs 'mean' and 'std'.")
223            mean, std = float(distribution_kwargs["mean"]), float(distribution_kwargs["std"])
224            if not np.isfinite(mean) or not lower_percentile_bounds[0] <= mean <= lower_percentile_bounds[1]:
225                raise ValueError("The normal distribution mean must be finite and within lower_percentile_bounds.")
226            if not np.isfinite(std) or std < 0.0:
227                raise ValueError("The normal distribution std must be finite and non-negative.")
228            distribution_kwargs = {"mean": mean, "std": std}
229
230        if rounding_decimals is not None and (
231            not isinstance(rounding_decimals, int) or isinstance(rounding_decimals, bool) or rounding_decimals < 0
232        ):
233            raise ValueError("rounding_decimals must be a non-negative integer or None.")
234        if not np.isfinite(eps) or eps <= 0.0:
235            raise ValueError("eps must be finite and greater than zero.")
236        if seed is not None:
237            if not isinstance(seed, (int, np.integer)) or isinstance(seed, bool):
238                raise TypeError("seed must be an integer or None.")
239            if seed < 0:
240                raise ValueError("seed must be non-negative.")
241            seed = int(seed)
242
243        self.lower_percentile_bounds = lower_percentile_bounds
244        self.upper_percentile_bounds = upper_percentile_bounds
245        self.distribution = distribution
246        self.distribution_kwargs = distribution_kwargs
247        self.rounding_decimals = rounding_decimals
248        self.axis = axis
249        self.seed = seed
250        self.eps = float(eps)
251        self._random_generator = None
252        self._random_generator_worker_id = None
lower_percentile_bounds
upper_percentile_bounds
distribution
distribution_kwargs
rounding_decimals
axis
seed
eps
def sample_percentiles(self) -> Tuple[float, float]:
286    def sample_percentiles(self) -> Tuple[float, float]:
287        """Sample and return a valid ``(lower, upper)`` percentile pair."""
288        random_generator = self._get_random_generator()
289        if self.distribution == "uniform":
290            lower = random_generator.uniform(*self.lower_percentile_bounds)
291            upper = random_generator.uniform(*self.upper_percentile_bounds)
292        else:
293            mean = self.distribution_kwargs["mean"]
294            std = self.distribution_kwargs["std"]
295            lower = mean if std == 0.0 else random_generator.normal(mean, std)
296            upper = 100.0 - (mean if std == 0.0 else random_generator.normal(mean, std))
297
298        # Normal distribution tails may leave the configured percentile interval.
299        lower = float(np.clip(self._round(lower), *self.lower_percentile_bounds))
300        upper = float(np.clip(self._round(upper), *self.upper_percentile_bounds))
301        return lower, upper

Sample and return a valid (lower, upper) percentile pair.

class RandomContrast:
316class RandomContrast:
317    """Transformation to adjust contrast by scaling image to `mean + alpha * (image - mean)`.
318
319    Args:
320        alpha: Minimal and maximal alpha value for adjusting the contrast.
321            The value for the transformation will be drawn uniformly from the corresponding interval.
322        mean: Mean value for the image data.
323        clip_kwargs: Keyword arguments for clipping the data after the contrast augmentation.
324    """
325    def __init__(
326        self, alpha: Tuple[float, float] = (0.5, 2), mean: float = 0.5, clip_kwargs: Dict = {"a_min": 0, "a_max": 1}
327    ):
328        self.alpha = alpha
329        self.mean = mean
330        self.clip_kwargs = clip_kwargs
331
332    def __call__(self, img: np.ndarray) -> np.ndarray:
333        """Apply the augmentation to data.
334
335        Args:
336            img: The input image.
337
338        Returns:
339            The transformed image.
340        """
341        alpha = np.random.uniform(self.alpha[0], self.alpha[1])
342        result = self.mean + alpha * (img - self.mean)
343        if self.clip_kwargs:
344            return np.clip(result, **self.clip_kwargs)
345        return result

Transformation to adjust contrast by scaling image to mean + alpha * (image - mean).

Arguments:
  • alpha: Minimal and maximal alpha value for adjusting the contrast. The value for the transformation will be drawn uniformly from the corresponding interval.
  • mean: Mean value for the image data.
  • clip_kwargs: Keyword arguments for clipping the data after the contrast augmentation.
RandomContrast( alpha: Tuple[float, float] = (0.5, 2), mean: float = 0.5, clip_kwargs: Dict = {'a_min': 0, 'a_max': 1})
325    def __init__(
326        self, alpha: Tuple[float, float] = (0.5, 2), mean: float = 0.5, clip_kwargs: Dict = {"a_min": 0, "a_max": 1}
327    ):
328        self.alpha = alpha
329        self.mean = mean
330        self.clip_kwargs = clip_kwargs
alpha
mean
clip_kwargs
class AdditiveGaussianNoise:
348class AdditiveGaussianNoise:
349    """Transformation to add random Gaussian noise to image.
350
351    Args:
352        scale: Scale for the noise.
353        clip_kwargs: Keyword arguments for clipping the data after the transformation.
354    """
355    def __init__(self, scale: Tuple[float, float] = (0.0, 0.3), clip_kwargs: Dict = {"a_min": 0, "a_max": 1}):
356        self.scale = scale
357        self.clip_kwargs = clip_kwargs
358
359    def __call__(self, img: np.ndarray) -> np.ndarray:
360        """Apply the augmentation to data.
361
362        Args:
363            img: The input image.
364
365        Returns:
366            The transformed image.
367        """
368        std = np.random.uniform(self.scale[0], self.scale[1])
369        gaussian_noise = np.random.normal(0, std, size=img.shape)
370
371        if self.clip_kwargs:
372            return np.clip(img + gaussian_noise, 0, 1)
373
374        return img + gaussian_noise

Transformation to add random Gaussian noise to image.

Arguments:
  • scale: Scale for the noise.
  • clip_kwargs: Keyword arguments for clipping the data after the transformation.
AdditiveGaussianNoise( scale: Tuple[float, float] = (0.0, 0.3), clip_kwargs: Dict = {'a_min': 0, 'a_max': 1})
355    def __init__(self, scale: Tuple[float, float] = (0.0, 0.3), clip_kwargs: Dict = {"a_min": 0, "a_max": 1}):
356        self.scale = scale
357        self.clip_kwargs = clip_kwargs
scale
clip_kwargs
class AdditivePoissonNoise:
377class AdditivePoissonNoise:
378    """Transformation to add random additive Poisson noise to image.
379
380    Args:
381        lam: Lambda value for the Poisson transformation.
382        clip_kwargs: Keyword arguments for clipping the data after the transformation.
383    """
384    # Not sure if Poisson noise like this does make sense for data that is already normalized
385    def __init__(self, lam: Tuple[float, float] = (0.0, 0.1), clip_kwargs: Dict = {"a_min": 0, "a_max": 1}):
386        self.lam = lam
387        self.clip_kwargs = clip_kwargs
388
389    def __call__(self, img: np.ndarray) -> np.ndarray:
390        """Apply the augmentation to data.
391
392        Args:
393            img: The input image.
394
395        Returns:
396            The transformed image.
397        """
398        lam = np.random.uniform(self.lam[0], self.lam[1])
399        poisson_noise = np.random.poisson(lam, size=img.shape) / lam
400        if self.clip_kwargs:
401            return np.clip(img + poisson_noise, 0, 1)
402        return img + poisson_noise

Transformation to add random additive Poisson noise to image.

Arguments:
  • lam: Lambda value for the Poisson transformation.
  • clip_kwargs: Keyword arguments for clipping the data after the transformation.
AdditivePoissonNoise( lam: Tuple[float, float] = (0.0, 0.1), clip_kwargs: Dict = {'a_min': 0, 'a_max': 1})
385    def __init__(self, lam: Tuple[float, float] = (0.0, 0.1), clip_kwargs: Dict = {"a_min": 0, "a_max": 1}):
386        self.lam = lam
387        self.clip_kwargs = clip_kwargs
lam
clip_kwargs
class PoissonNoise:
405class PoissonNoise:
406    """Transformation to add random data-dependant Poisson noise to image.
407
408    Args:
409        multiplier: Multiplicative factors for deriving the lambda factor from the data.
410            The factor used for the transformation will be uniformly sampled form the range of this parameter.
411        clip_kwargs: Keyword arguments for clipping the data after the transformation.
412    """
413    def __init__(self, multiplier: Tuple[float, float] = (5.0, 10.0), clip_kwargs: Dict = {"a_min": 0, "a_max": 1}):
414        self.multiplier = multiplier
415        self.clip_kwargs = clip_kwargs
416
417    def __call__(self, img: np.ndarray) -> np.ndarray:
418        """Apply the augmentation to data.
419
420        Args:
421            img: The input image.
422
423        Returns:
424            The transformed image.
425        """
426        multiplier = np.random.uniform(self.multiplier[0], self.multiplier[1])
427        offset = img.min()
428        poisson_noise = np.random.poisson((img - offset) * multiplier)
429
430        if isinstance(img, torch.Tensor):
431            poisson_noise = torch.Tensor(poisson_noise)
432        poisson_noise = poisson_noise / multiplier + offset
433
434        if self.clip_kwargs:
435            return np.clip(poisson_noise, **self.clip_kwargs)
436        return poisson_noise

Transformation to add random data-dependant Poisson noise to image.

Arguments:
  • multiplier: Multiplicative factors for deriving the lambda factor from the data. The factor used for the transformation will be uniformly sampled form the range of this parameter.
  • clip_kwargs: Keyword arguments for clipping the data after the transformation.
PoissonNoise( multiplier: Tuple[float, float] = (5.0, 10.0), clip_kwargs: Dict = {'a_min': 0, 'a_max': 1})
413    def __init__(self, multiplier: Tuple[float, float] = (5.0, 10.0), clip_kwargs: Dict = {"a_min": 0, "a_max": 1}):
414        self.multiplier = multiplier
415        self.clip_kwargs = clip_kwargs
multiplier
clip_kwargs
class GaussianBlur:
439class GaussianBlur:
440    """Transformation to blur the image with a randomly drawn sigma value.
441
442    Args:
443        sigma: The sigma value for the transformation.
444            The value used in the transformation will be uniformly drawn from the range specified here.
445    """
446    def __init__(self, sigma: Tuple[float, float] = (0.0, 3.0)):
447        self.sigma = sigma
448
449    def __call__(self, img: np.ndarray) -> np.ndarray:
450        """Apply the augmentation to data.
451
452        Args:
453            img: The input image.
454
455        Returns:
456            The transformed image.
457        """
458        # Sample the sigma value. Note that we switch the bounds to ensure zero is excluded from sampling.
459        sigma = np.random.uniform(self.sigma[1], self.sigma[0])
460        # Determine the kernel size based on the sigma value.
461        kernel_size = int(2 * np.ceil(3 * sigma) + 1)
462        if isinstance(img, np.ndarray):
463            img = torch.from_numpy(img)
464
465        return transforms.GaussianBlur(kernel_size, sigma=sigma)(img)

Transformation to blur the image with a randomly drawn sigma value.

Arguments:
  • sigma: The sigma value for the transformation. The value used in the transformation will be uniformly drawn from the range specified here.
GaussianBlur(sigma: Tuple[float, float] = (0.0, 3.0))
446    def __init__(self, sigma: Tuple[float, float] = (0.0, 3.0)):
447        self.sigma = sigma
sigma
class RawTransform:
472class RawTransform:
473    """The transformation for raw data during training.
474
475    Args:
476        normalizer: The normalization function.
477        augmentation1: Intensity augmentation applied before the normalization.
478        augmentation2: Intensity augmentation applied after the normalization.
479    """
480    def __init__(
481        self, normalizer: Callable, augmentation1: Optional[Callable] = None, augmentation2: Optional[Callable] = None
482    ):
483        self.normalizer = normalizer
484        self.augmentation1 = augmentation1
485        self.augmentation2 = augmentation2
486
487    def __call__(self, raw: np.ndarray) -> np.ndarray:
488        """Apply the raw transformation.
489
490        Args:
491            raw: The raw data.
492
493        Returns:
494            The transformed raw data.
495        """
496        if self.augmentation1 is not None:
497            raw = self.augmentation1(raw)
498
499        raw = self.normalizer(raw)
500
501        if self.augmentation2 is not None:
502            raw = self.augmentation2(raw)
503        return raw

The transformation for raw data during training.

Arguments:
  • normalizer: The normalization function.
  • augmentation1: Intensity augmentation applied before the normalization.
  • augmentation2: Intensity augmentation applied after the normalization.
RawTransform( normalizer: Callable, augmentation1: Optional[Callable] = None, augmentation2: Optional[Callable] = None)
480    def __init__(
481        self, normalizer: Callable, augmentation1: Optional[Callable] = None, augmentation2: Optional[Callable] = None
482    ):
483        self.normalizer = normalizer
484        self.augmentation1 = augmentation1
485        self.augmentation2 = augmentation2
normalizer
augmentation1
augmentation2
def get_raw_transform( normalizer: Callable = <function standardize>, augmentation1: Optional[Callable] = None, augmentation2: Optional[Callable] = None) -> Callable:
506def get_raw_transform(
507    normalizer: Callable = standardize,
508    augmentation1: Optional[Callable] = None,
509    augmentation2: Optional[Callable] = None
510) -> Callable:
511    """Get the raw transformation.
512
513    Args:
514        normalizer: The normalization function.
515        augmentation1: Intensity augmentation applied before the normalization.
516        augmentation2: Intensity augmentation applied after the normalization.
517
518    Returns:
519        The raw transformation.
520    """
521    return RawTransform(normalizer, augmentation1=augmentation1, augmentation2=augmentation2)

Get the raw transformation.

Arguments:
  • normalizer: The normalization function.
  • augmentation1: Intensity augmentation applied before the normalization.
  • augmentation2: Intensity augmentation applied after the normalization.
Returns:

The raw transformation.

def get_raw_transform_for_pretrained_vit(backbone: Optional[str]) -> Tuple[Optional[Callable], Optional[int]]:
524def get_raw_transform_for_pretrained_vit(backbone: Optional[str]) -> Tuple[Optional[Callable], Optional[int]]:
525    """Get the raw transformation and clipping limit for a pretrained ViT backbone.
526
527    Args:
528        backbone: The pretrained ViT backbone. Options: "sam", "sam2", "sam3", "cellpose_sam", "mae", "scalemae",
529            "dinov2", "dinov3", "torchvision", or None.
530
531    Returns:
532        The raw transformation. It normalizes percentiles to [0, 255] for "sam" and [0, 1] for the other backbones.
533        The maximum value `clip_max` for intensity augmentations. Use it to clip their output to [0, clip_max].
534            Both values are None if `backbone` is None, so data loaders use the default standardization.
535    """
536    if backbone is None:
537        return None, None
538
539    from ..model.util import VIT_CONFIGS
540
541    if backbone not in VIT_CONFIGS:
542        raise ValueError(f"Unsupported backbone: {backbone}.")
543
544    clip_max = VIT_CONFIGS[backbone].clip_max
545    normalizer = partial(_normalize_percentile_to_range, clip_max=clip_max)
546    return get_raw_transform(normalizer=normalizer), clip_max

Get the raw transformation and clipping limit for a pretrained ViT backbone.

Arguments:
  • backbone: The pretrained ViT backbone. Options: "sam", "sam2", "sam3", "cellpose_sam", "mae", "scalemae", "dinov2", "dinov3", "torchvision", or None.
Returns:

The raw transformation. It normalizes percentiles to [0, 255] for "sam" and [0, 1] for the other backbones. The maximum value clip_max for intensity augmentations. Use it to clip their output to [0, clip_max]. Both values are None if backbone is None, so data loaders use the default standardization.

def get_default_mean_teacher_augmentations( p: float = 0.3, norm: Optional[Callable] = None, blur_kwargs: Optional[Dict] = None, poisson_kwargs: Optional[Dict] = None, gaussian_kwargs: Optional[Dict] = None) -> Callable:
549def get_default_mean_teacher_augmentations(
550    p: float = 0.3,
551    norm: Optional[Callable] = None,
552    blur_kwargs: Optional[Dict] = None,
553    poisson_kwargs: Optional[Dict] = None,
554    gaussian_kwargs: Optional[Dict] = None,
555) -> Callable:
556    """Get the default augmentations for mean teacher training.
557
558    The default values for the augmentations are designed for an image with pixel values in range [0, 1].
559    By default, a normalization transformation is applied for this reason.
560
561    Args:
562        p: The probability for applying the individual intensity transformations.
563        norm: The noromaization function.
564        blur_kwargs: The keyword arguments for `GaussianBlur`.
565        poisson_kwargs: The keyword arguments for `PoissonNoise`.
566        gaussian_kwargs: The keyword arguments for `AdditiveGaussianNoise`.
567
568    Returns:
569        The raw transformation with augmentations.
570    """
571    if norm is None:
572        norm = normalize
573
574    aug1 = transforms.Compose([
575        norm,
576        transforms.RandomApply([GaussianBlur(**({} if blur_kwargs is None else blur_kwargs))], p=p),
577        transforms.RandomApply([PoissonNoise(**({} if poisson_kwargs is None else poisson_kwargs))], p=p/2),
578        transforms.RandomApply([AdditiveGaussianNoise(**({} if gaussian_kwargs is None else gaussian_kwargs))], p=p/2),
579    ])
580
581    aug2 = transforms.RandomApply([RandomContrast(clip_kwargs={"a_min": 0, "a_max": 1})], p=p)
582    return get_raw_transform(normalizer=norm, augmentation1=aug1, augmentation2=aug2)

Get the default augmentations for mean teacher training.

The default values for the augmentations are designed for an image with pixel values in range [0, 1]. By default, a normalization transformation is applied for this reason.

Arguments:
  • p: The probability for applying the individual intensity transformations.
  • norm: The noromaization function.
  • blur_kwargs: The keyword arguments for GaussianBlur.
  • poisson_kwargs: The keyword arguments for PoissonNoise.
  • gaussian_kwargs: The keyword arguments for AdditiveGaussianNoise.
Returns:

The raw transformation with augmentations.