Skip to content

Standardize

Bases: IntensityTransform

Subtract mean and divide by standard deviation (z-score).

\[v_{\text{out}} = \frac{v - \mu}{\sigma}\]

The statistics \(\mu\) and \(\sigma\) are computed from the (optionally masked) voxels and applied to the entire image.

Parameters:

Name Type Description Default
masking_method str | Callable[[Tensor], Tensor] | None

Which voxels to include when computing the mean and standard deviation. None uses all voxels. A str is interpreted as a key to a LabelMap in the subject. A callable receives the image tensor and returns a boolean mask.

None
**kwargs Any

See Transform.

{}

Examples:

>>> import torchio as tio
>>> transform = tio.Standardize()
>>> # Use only brain voxels for statistics
>>> transform = tio.Standardize(masking_method="brain")
>>> # Use voxels above mean
>>> transform = tio.Standardize(masking_method=lambda x: x > x.mean())
Source code in src/torchio/transforms/intensity/standardize.py
class Standardize(IntensityTransform):
    r"""Subtract mean and divide by standard deviation (z-score).

    $$v_{\text{out}} = \frac{v - \mu}{\sigma}$$

    The statistics $\mu$ and $\sigma$ are computed from the (optionally
    masked) voxels and applied to the entire image.

    Args:
        masking_method: Which voxels to include when computing the
            mean and standard deviation. `None` uses all voxels.
            A `str` is interpreted as a key to a
            [`LabelMap`][torchio.LabelMap] in the subject.
            A callable receives the image tensor and returns a boolean
            mask.
        **kwargs: See [`Transform`][torchio.Transform].

    Examples:
        >>> import torchio as tio
        >>> transform = tio.Standardize()
        >>> # Use only brain voxels for statistics
        >>> transform = tio.Standardize(masking_method="brain")
        >>> # Use voxels above mean
        >>> transform = tio.Standardize(masking_method=lambda x: x > x.mean())
    """

    def __init__(
        self,
        *,
        masking_method: str | Callable[[Tensor], Tensor] | None = None,
        **kwargs: Any,
    ) -> None:
        super().__init__(**kwargs)
        self.masking_method = masking_method

    def make_params(self, batch: SubjectsBatch) -> dict[str, Any]:
        """Compute per-image mean and std from the first sample.

        Returns:
            Dict mapping image names to `(mean, std)` pairs.
        """
        images = self._get_images(batch)
        stats: dict[str, tuple[float, float]] = {}
        for name, img_batch in images.items():
            mask = _get_mask(self.masking_method, img_batch, batch)
            tensor = img_batch.data[0]
            values = (
                tensor[mask.expand_as(tensor)]
                if mask is not None
                else tensor.reshape(-1)
            )
            if values.numel() == 0:
                warnings.warn(
                    f'Mask is empty for "{name}". Using all voxels.',
                    RuntimeWarning,
                    stacklevel=2,
                )
                values = tensor.reshape(-1)
            mean = float(values.float().mean().item())
            std = float(values.float().std().item())
            stats[name] = (mean, std)
        return {"stats": stats}

    def apply_transform(
        self,
        batch: SubjectsBatch,
        params: dict[str, Any],
    ) -> SubjectsBatch:
        """Subtract mean and divide by std for each selected image."""
        stats = params["stats"]
        for name, img_batch in self._get_images(batch).items():
            if name not in stats:
                continue
            mean, std = stats[name]
            if std == 0:
                msg = (
                    f'Standard deviation is zero for masked values in "{name}".'
                    " Cannot standardize."
                )
                raise RuntimeError(msg)
            img_batch.data = (img_batch.data.float() - mean) / std
        return batch

    @property
    def invertible(self) -> bool:
        """Whether this transform can be inverted."""
        return True

    def inverse(self, params: dict[str, Any]) -> _StandardizeInverse:
        """Build the inverse using the recorded mean and std."""
        return _StandardizeInverse(stats=params["stats"], copy=False)

supports_per_instance_params property

Whether this transform can sample parameters per batch element.

Defaults to False. Transforms that implement per-instance parameter sampling override this to return True. When False, the transform always uses batch-shared parameters regardless of the per_instance flag, preserving the legacy behavior.

supports_per_instance_p property

Whether this transform can gate each batch element independently.

Defaults to False. Shape-preserving transforms that implement per-element probability override this to return True. Shape-changing transforms must leave it False because masked and unmasked elements would have incompatible shapes.

invertible property

Whether this transform can be inverted.

forward(data)

forward(data: Subject) -> Subject
forward(data: Image) -> Image
forward(data: Tensor) -> Tensor
forward(data: np.ndarray) -> np.ndarray
forward(data: sitk.Image) -> sitk.Image
forward(data: nib.Nifti1Image) -> nib.Nifti1Image
forward(data: dict) -> dict
forward(data: ImagesBatch) -> ImagesBatch
forward(data: SubjectsBatch) -> SubjectsBatch

Apply the transform.

The output type always matches the input type.

Parameters:

Name Type Description Default
data Any

Input data to transform.

required
Source code in src/torchio/transforms/transform.py
def forward(self, data: Any) -> Any:
    """Apply the transform.

    The output type always matches the input type.

    Args:
        data: Input data to transform.
    """
    if self.copy:
        data = _copy.deepcopy(data)
    batch, unwrap = self._wrap(data)
    # When per-element gating is active, the transform handles the
    # probability itself (masked-out elements get identity params),
    # so skip the batch-wide coin flip here. Apply iff rand < p, so
    # p=0 is always a no-op and p=1 always applies.
    if not self._per_instance_p_active(batch) and torch.rand(1).item() >= self.p:
        return unwrap(batch)
    params = self.make_params(batch)
    batch = self.apply_transform(batch, params)
    # Record history on the batch, unless every element was gated out by
    # per-element probability: that is an exact no-op, and recording it
    # would let history replay (e.g. an invertible spatial transform)
    # trigger an unnecessary identity resample.
    if not _all_elements_gated_out(params):
        trace = AppliedTransform(name=type(self).__name__, params=params)
        if not hasattr(batch, "applied_transforms"):
            batch.applied_transforms = []
        batch.applied_transforms.append(trace)
    result = unwrap(batch)
    # Propagate history to outputs that can carry it
    if (
        hasattr(batch, "applied_transforms")
        and not isinstance(result, (SubjectsBatch, Tensor, np.ndarray))
        and not isinstance(result, dict)
    ):
        with contextlib.suppress(AttributeError):
            result.applied_transforms = list(batch.applied_transforms)
    return result

to_hydra()

Export as a Hydra-compatible config dict.

Returns a dict with _target_ set to the fully qualified class name and only non-default field values included.

Returns:

Type Description
dict[str, Any]

Dict suitable for hydra.utils.instantiate().

Source code in src/torchio/transforms/transform.py
def to_hydra(self) -> dict[str, Any]:
    """Export as a Hydra-compatible config dict.

    Returns a dict with `_target_` set to the fully qualified
    class name and only non-default field values included.

    Returns:
        Dict suitable for `hydra.utils.instantiate()`.
    """
    from .parameter_range import _ParameterRange

    cls = type(self)
    target = f"torchio.{cls.__qualname__}"
    cfg: dict[str, Any] = {"_target_": target}

    for name, default in _collect_init_params(cls).items():
        value = getattr(self, name, default)
        if isinstance(value, _ParameterRange):
            if value._original == default:
                continue
            value = _hydra_value(value._original)
        elif value == default:
            continue
        else:
            value = _hydra_value(value)
        cfg[name] = value
    return cfg

make_params(batch)

Compute per-image mean and std from the first sample.

Returns:

Type Description
dict[str, Any]

Dict mapping image names to (mean, std) pairs.

Source code in src/torchio/transforms/intensity/standardize.py
def make_params(self, batch: SubjectsBatch) -> dict[str, Any]:
    """Compute per-image mean and std from the first sample.

    Returns:
        Dict mapping image names to `(mean, std)` pairs.
    """
    images = self._get_images(batch)
    stats: dict[str, tuple[float, float]] = {}
    for name, img_batch in images.items():
        mask = _get_mask(self.masking_method, img_batch, batch)
        tensor = img_batch.data[0]
        values = (
            tensor[mask.expand_as(tensor)]
            if mask is not None
            else tensor.reshape(-1)
        )
        if values.numel() == 0:
            warnings.warn(
                f'Mask is empty for "{name}". Using all voxels.',
                RuntimeWarning,
                stacklevel=2,
            )
            values = tensor.reshape(-1)
        mean = float(values.float().mean().item())
        std = float(values.float().std().item())
        stats[name] = (mean, std)
    return {"stats": stats}

apply_transform(batch, params)

Subtract mean and divide by std for each selected image.

Source code in src/torchio/transforms/intensity/standardize.py
def apply_transform(
    self,
    batch: SubjectsBatch,
    params: dict[str, Any],
) -> SubjectsBatch:
    """Subtract mean and divide by std for each selected image."""
    stats = params["stats"]
    for name, img_batch in self._get_images(batch).items():
        if name not in stats:
            continue
        mean, std = stats[name]
        if std == 0:
            msg = (
                f'Standard deviation is zero for masked values in "{name}".'
                " Cannot standardize."
            )
            raise RuntimeError(msg)
        img_batch.data = (img_batch.data.float() - mean) / std
    return batch

inverse(params)

Build the inverse using the recorded mean and std.

Source code in src/torchio/transforms/intensity/standardize.py
def inverse(self, params: dict[str, Any]) -> _StandardizeInverse:
    """Build the inverse using the recorded mean and std."""
    return _StandardizeInverse(stats=params["stats"], copy=False)