Skip to content

HistogramStandardization

Bases: IntensityTransform

Apply piecewise-linear histogram standardization.

Implementation of Nyúl and Udupa (1999).

Landmarks must be precomputed using [compute_histogram_landmarks][torchio.transforms.histogram_standardization.compute_histogram_landmarks] and are passed directly to this transform. Each instance targets one modality; for multi-modal subjects, compose multiple instances with the include parameter:

tio.Compose([
    tio.HistogramStandardization(t1_landmarks, include=["t1"]),
    tio.HistogramStandardization(t2_landmarks, include=["t2"]),
])

Parameters:

Name Type Description Default
landmarks Tensor | Path | str

1-D tensor (or path to a .npy / .pt file) of standard-space landmark values, as returned by [compute_histogram_landmarks][torchio.transforms.histogram_standardization.compute_histogram_landmarks].

required
cutoff tuple[float, float]

Lower and upper quantile bounds.

DEFAULT_CUTOFF
**kwargs Any

See Transform.

{}

Examples:

>>> import torchio as tio
>>> landmarks = torch.linspace(0, 100, 13)
>>> transform = tio.HistogramStandardization(landmarks)
Source code in src/torchio/transforms/intensity/histogram_standardization.py
class HistogramStandardization(IntensityTransform):
    r"""Apply piecewise-linear histogram standardization.

    Implementation of
    [Nyúl and Udupa (1999)](https://ieeexplore.ieee.org/document/836373).

    Landmarks must be precomputed using
    [`compute_histogram_landmarks`][torchio.transforms.histogram_standardization.compute_histogram_landmarks]
    and are passed directly to this transform.  Each instance targets
    **one modality**; for multi-modal subjects, compose multiple
    instances with the `include` parameter:

    ```python
    tio.Compose([
        tio.HistogramStandardization(t1_landmarks, include=["t1"]),
        tio.HistogramStandardization(t2_landmarks, include=["t2"]),
    ])
    ```

    Args:
        landmarks: 1-D tensor (or path to a `.npy` / `.pt` file)
            of standard-space landmark values, as returned by
            [`compute_histogram_landmarks`][torchio.transforms.histogram_standardization.compute_histogram_landmarks].
        cutoff: Lower and upper quantile bounds.
        **kwargs: See [`Transform`][torchio.Transform].

    Examples:
        >>> import torchio as tio
        >>> landmarks = torch.linspace(0, 100, 13)
        >>> transform = tio.HistogramStandardization(landmarks)
    """

    def __init__(
        self,
        landmarks: Tensor | Path | str,
        *,
        cutoff: tuple[float, float] = DEFAULT_CUTOFF,
        **kwargs: Any,
    ) -> None:
        super().__init__(**kwargs)
        self.landmarks = _load_landmarks(landmarks)
        self.cutoff = cutoff

    def make_params(self, batch: SubjectsBatch) -> dict[str, Any]:
        """No random parameters."""
        return {}

    def apply_transform(
        self,
        batch: SubjectsBatch,
        params: dict[str, Any],
    ) -> SubjectsBatch:
        """Apply histogram standardization to each selected image."""
        for _name, img_batch in self._get_images(batch).items():
            for i in range(img_batch.batch_size):
                img_batch.data[i] = _apply_histogram_standardization(
                    img_batch.data[i],
                    self.landmarks,
                    self.cutoff,
                )
        return batch

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

inverse(params)

Return a transform that undoes this one.

Override in invertible subclasses. The returned transform, when applied, reverses the effect of the forward pass with the given parameters.

Parameters:

Name Type Description Default
params dict[str, Any]

The parameters recorded in the forward pass.

required

Returns:

Type Description
Transform

A new Transform instance that inverts this one.

Source code in src/torchio/transforms/transform.py
def inverse(self, params: dict[str, Any]) -> Transform:
    """Return a transform that undoes this one.

    Override in invertible subclasses. The returned transform,
    when applied, reverses the effect of the forward pass with
    the given parameters.

    Args:
        params: The parameters recorded in the forward pass.

    Returns:
        A new `Transform` instance that inverts this one.
    """
    msg = f"{type(self).__name__} is not invertible"
    raise NotImplementedError(msg)

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)

No random parameters.

Source code in src/torchio/transforms/intensity/histogram_standardization.py
def make_params(self, batch: SubjectsBatch) -> dict[str, Any]:
    """No random parameters."""
    return {}

apply_transform(batch, params)

Apply histogram standardization to each selected image.

Source code in src/torchio/transforms/intensity/histogram_standardization.py
def apply_transform(
    self,
    batch: SubjectsBatch,
    params: dict[str, Any],
) -> SubjectsBatch:
    """Apply histogram standardization to each selected image."""
    for _name, img_batch in self._get_images(batch).items():
        for i in range(img_batch.batch_size):
            img_batch.data[i] = _apply_histogram_standardization(
                img_batch.data[i],
                self.landmarks,
                self.cutoff,
            )
    return batch

Landmark computation

compute_histogram_landmarks(images, *, quantiles=None, cutoff=DEFAULT_CUTOFF, masking_method=None)

Compute average histogram landmarks from training images.

Implements the training phase of Nyúl and Udupa (1999) <https://ieeexplore.ieee.org/document/836373>_. The returned landmarks tensor can be passed directly to HistogramStandardization.

Parameters:

Name Type Description Default
images Sequence[ScalarImage | Path | str]

Training images. Each element can be a ScalarImage, a file path, or a string path.

required
quantiles Sequence[float] | None

Quantile positions in [0, 1] used as control points. Must be sorted and include the cutoff endpoints. None uses the default 13-point scheme (v1-compatible).

None
cutoff tuple[float, float]

Lower and upper quantile bounds for the intensity range of interest. Defaults to (0.01, 0.99).

DEFAULT_CUTOFF
masking_method Callable[[Tensor], Tensor] | None

Optional callable that takes a 4-D tensor (C, I, J, K) and returns a boolean mask of the same shape. Only True voxels are used for percentile computation. None uses all voxels.

None

Returns:

Type Description
Tensor

1-D tensor of landmark values, one per quantile.

Examples:

>>> import torchio as tio
>>> from torchio.transforms.histogram_standardization import (
...     compute_histogram_landmarks,
... )
>>> landmarks = compute_histogram_landmarks([
...     tio.ScalarImage("subject_a_t1.nii"),
...     tio.ScalarImage("subject_b_t1.nii"),
... ])
Source code in src/torchio/transforms/intensity/histogram_standardization.py
def compute_histogram_landmarks(
    images: Sequence[ScalarImage | Path | str],
    *,
    quantiles: Sequence[float] | None = None,
    cutoff: tuple[float, float] = DEFAULT_CUTOFF,
    masking_method: Callable[[Tensor], Tensor] | None = None,
) -> Tensor:
    """Compute average histogram landmarks from training images.

    Implements the training phase of
    `Nyúl and Udupa (1999) <https://ieeexplore.ieee.org/document/836373>`_.
    The returned landmarks tensor can be passed directly to
    [`HistogramStandardization`][torchio.HistogramStandardization].

    Args:
        images: Training images.  Each element can be a
            [`ScalarImage`][torchio.ScalarImage], a file path, or a
            string path.
        quantiles: Quantile positions in `[0, 1]` used as control
            points.  Must be sorted and include the cutoff endpoints.
            `None` uses the default 13-point scheme (v1-compatible).
        cutoff: Lower and upper quantile bounds for the intensity
            range of interest.  Defaults to `(0.01, 0.99)`.
        masking_method: Optional callable that takes a 4-D tensor
            `(C, I, J, K)` and returns a boolean mask of the same
            shape.  Only `True` voxels are used for percentile
            computation.  `None` uses all voxels.

    Returns:
        1-D tensor of landmark values, one per quantile.

    Examples:
        >>> import torchio as tio
        >>> from torchio.transforms.histogram_standardization import (
        ...     compute_histogram_landmarks,
        ... )
        >>> landmarks = compute_histogram_landmarks([  # doctest: +SKIP
        ...     tio.ScalarImage("subject_a_t1.nii"),
        ...     tio.ScalarImage("subject_b_t1.nii"),
        ... ])
    """
    if quantiles is None:
        quantiles = _build_quantiles(cutoff)
    else:
        quantiles = tuple(sorted(set(quantiles)))

    _validate_quantiles(quantiles, cutoff)
    percentiles = [100.0 * q for q in quantiles]

    all_percentile_values: list[np.ndarray] = []
    for img_source in images:
        tensor = _load_tensor(img_source)
        if masking_method is not None:
            mask = masking_method(tensor)
        else:
            mask = torch.ones_like(tensor, dtype=torch.bool)
        values = tensor[mask].numpy()
        pv = np.percentile(values, percentiles)
        all_percentile_values.append(pv)

    database = np.vstack(all_percentile_values)
    landmarks = _compute_average_mapping(database)
    return torch.as_tensor(landmarks, dtype=torch.float32)