Skip to content

BiasField

Bases: IntensityTransform

Corrupt an image with a smooth multiplicative bias field.

The bias field is generated by:

  1. Sampling a small 3D tensor from \(\mathcal{N}(0, \sigma)\).
  2. Trilinearly upsampling it to the image spatial shape.
  3. Taking the voxel-wise exponential to make it strictly positive.
  4. Multiplying the image by the resulting field.

This follows the approach used in SynthSeg: Segmentation of brain MRI scans of any contrast and resolution without retraining.

Parameters:

Name Type Description Default
std float | tuple[float, float] | Distribution

Standard deviation \(\sigma\) of the normal distribution used to sample the coarse bias field. Larger values produce stronger inhomogeneity. If two values \((a, b)\) are provided, \(\sigma \sim \mathcal{U}(a, b)\). A torch.distributions.Distribution may also be passed.

0.5
scale float

Ratio between the coarse field size and the image spatial shape. Smaller values produce smoother fields.

0.025
**kwargs Any

See Transform.

{}

Examples:

>>> import torchio as tio
>>> transform = tio.BiasField()
>>> transform = tio.BiasField(std=0.8)
>>> transform = tio.BiasField(std=(0.0, 1.0))
Source code in src/torchio/transforms/intensity/bias_field.py
class BiasField(IntensityTransform):
    r"""Corrupt an image with a smooth multiplicative bias field.

    The bias field is generated by:

    1. Sampling a small 3D tensor from $\mathcal{N}(0, \sigma)$.
    2. Trilinearly upsampling it to the image spatial shape.
    3. Taking the voxel-wise exponential to make it strictly positive.
    4. Multiplying the image by the resulting field.

    This follows the approach used in [SynthSeg: Segmentation of brain MRI
    scans of any contrast and resolution without
    retraining](https://www.sciencedirect.com/science/article/pii/S1361841523000506).

    Args:
        std: Standard deviation $\sigma$ of the normal distribution
            used to sample the coarse bias field. Larger values produce
            stronger inhomogeneity. If two values $(a, b)$ are provided,
            $\sigma \sim \mathcal{U}(a, b)$. A
            `torch.distributions.Distribution` may also be passed.
        scale: Ratio between the coarse field size and the image spatial
            shape. Smaller values produce smoother fields.
        **kwargs: See [`Transform`][torchio.Transform].

    Examples:
        >>> import torchio as tio
        >>> transform = tio.BiasField()
        >>> transform = tio.BiasField(std=0.8)
        >>> transform = tio.BiasField(std=(0.0, 1.0))
    """

    def __init__(
        self,
        *,
        std: float | tuple[float, float] | torch.distributions.Distribution = 0.5,
        scale: float = 0.025,
        **kwargs: Any,
    ) -> None:
        super().__init__(**kwargs)
        self.std = to_nonneg_range(std)
        if scale <= 0 or scale > 1:
            msg = f"scale must be in (0, 1], got {scale}"
            raise ValueError(msg)
        self.scale = scale

    def make_params(self, batch: SubjectsBatch) -> dict[str, Any]:
        """Sample the bias field standard deviation (per element when batched)."""
        n = self._resolve_n(batch)
        if n is None:
            std = self.std.sample_1d()
            seed = int(torch.randint(0, 2**31, (1,)).item())
            return {
                "std": std,
                "seed": seed,
                "scale": self.scale,
            }
        keep = self._keep_mask(batch, n)
        std = self._mask_identity(self.std.sample_1d(n), keep, identity=0.0)
        # A per-element seed makes each element's field reproducible from its
        # own recorded parameters, so it inverts correctly after unbatching.
        seeds = [int(torch.randint(0, 2**31, (1,)).item()) for _ in range(n)]
        params = {
            "std": self._serialize_param(std),
            "seed": seeds,
            "scale": self.scale,
        }
        self._tag_batched(params, batch, n, keep, ["std", "seed"])
        return params

    @property
    def supports_per_instance_params(self) -> bool:
        return True

    @property
    def supports_per_instance_p(self) -> bool:
        return True

    def apply_transform(
        self,
        batch: SubjectsBatch,
        params: dict[str, Any],
    ) -> SubjectsBatch:
        """Multiply each selected image by a smooth random bias field."""
        std = params["std"]
        seed = params["seed"]
        scale = params["scale"]

        per_instance = self._is_per_instance_params(params)
        if not per_instance and std == 0:
            return batch

        for _name, img_batch in self._get_images(batch).items():
            if per_instance:
                img_batch.data = _apply_bias_per_element(
                    img_batch.data,
                    std,
                    seed,
                    scale,
                    divide=False,
                )
            else:
                field = _generate_bias_field(
                    img_batch.data.shape,
                    std=std,
                    scale=scale,
                    seed=seed,
                    device=img_batch.data.device,
                )
                img_batch.data = img_batch.data * field

        return batch

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

    def inverse(self, params: dict[str, Any]) -> _BiasFieldInverse:
        """Build the inverse by dividing by the same bias field."""
        return _BiasFieldInverse(
            std=params["std"],
            seed=params["seed"],
            scale=params["scale"],
            copy=False,
        )

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)

Sample the bias field standard deviation (per element when batched).

Source code in src/torchio/transforms/intensity/bias_field.py
def make_params(self, batch: SubjectsBatch) -> dict[str, Any]:
    """Sample the bias field standard deviation (per element when batched)."""
    n = self._resolve_n(batch)
    if n is None:
        std = self.std.sample_1d()
        seed = int(torch.randint(0, 2**31, (1,)).item())
        return {
            "std": std,
            "seed": seed,
            "scale": self.scale,
        }
    keep = self._keep_mask(batch, n)
    std = self._mask_identity(self.std.sample_1d(n), keep, identity=0.0)
    # A per-element seed makes each element's field reproducible from its
    # own recorded parameters, so it inverts correctly after unbatching.
    seeds = [int(torch.randint(0, 2**31, (1,)).item()) for _ in range(n)]
    params = {
        "std": self._serialize_param(std),
        "seed": seeds,
        "scale": self.scale,
    }
    self._tag_batched(params, batch, n, keep, ["std", "seed"])
    return params

apply_transform(batch, params)

Multiply each selected image by a smooth random bias field.

Source code in src/torchio/transforms/intensity/bias_field.py
def apply_transform(
    self,
    batch: SubjectsBatch,
    params: dict[str, Any],
) -> SubjectsBatch:
    """Multiply each selected image by a smooth random bias field."""
    std = params["std"]
    seed = params["seed"]
    scale = params["scale"]

    per_instance = self._is_per_instance_params(params)
    if not per_instance and std == 0:
        return batch

    for _name, img_batch in self._get_images(batch).items():
        if per_instance:
            img_batch.data = _apply_bias_per_element(
                img_batch.data,
                std,
                seed,
                scale,
                divide=False,
            )
        else:
            field = _generate_bias_field(
                img_batch.data.shape,
                std=std,
                scale=scale,
                seed=seed,
                device=img_batch.data.device,
            )
            img_batch.data = img_batch.data * field

    return batch

inverse(params)

Build the inverse by dividing by the same bias field.

Source code in src/torchio/transforms/intensity/bias_field.py
def inverse(self, params: dict[str, Any]) -> _BiasFieldInverse:
    """Build the inverse by dividing by the same bias field."""
    return _BiasFieldInverse(
        std=params["std"],
        seed=params["seed"],
        scale=params["scale"],
        copy=False,
    )