From 698c1dec19dac83ed5bf2484a7037acb1f5143b5 Mon Sep 17 00:00:00 2001 From: Habib Ur Rehman Date: Wed, 7 Oct 2026 23:47:30 +0500 Subject: [PATCH 1/4] Add translate_relative option to RandAffine for relative translation ranges Added translate_relative parameter to interpret translate_range as fractions of spatial dimension sizes. Signed-off-by: Habib Ur Rehman --- monai/transforms/spatial/array.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index 2d3edaeed57..6be9d984344 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -1861,14 +1861,14 @@ def __init__( shear_range: RandRange = None, translate_range: RandRange = None, scale_range: RandRange = None, + translate_relative: bool = False, device: torch.device | None = None, dtype: DtypeLike = np.float32, lazy: bool = False, ) -> None: """ Args: - rotate_range: angle range in radians. If element `i` is a pair of (min, max) values, then - `uniform[rotate_range[i][0], rotate_range[i][1])` will be used to generate the rotation parameter + rotate_range: angle range in radians. If element `i` is a pair of (min, max) values, then `uniform[rotate_range[i][0], rotate_range[i][1])` will be used to generate the rotation parameter for the `i`th spatial dimension. If not, `uniform[-rotate_range[i], rotate_range[i])` will be used. This can be altered on a per-dimension basis. E.g., `((0,3), 1, ...)`: for dim0, rotation will be in range `[0, 3]`, and for dim1 `[-1, 1]` will be used. Setting a single value will use `[-x, x]` @@ -1889,6 +1889,9 @@ def __init__( scale_range: scaling range with format matching `rotate_range`. it defines the range to randomly select the scale factor to translate for every spatial dims. A value of 1.0 is added to the result. This allows 0 to correspond to no change (i.e., a scaling of 1.0). + translate_relative: if True, `translate_range` values are interpreted as fractions of the + corresponding spatial dimension size (e.g. 0.5 allows translating up to half the image + size), instead of absolute voxels. Defaults to False. device: device to store the output grid data. dtype: data type for the grid computation. Defaults to ``np.float32``. If ``None``, use the data type of input data (if `grid` is provided). @@ -1907,6 +1910,7 @@ def __init__( self.shear_range = ensure_tuple(shear_range) self.translate_range = ensure_tuple(translate_range) self.scale_range = ensure_tuple(scale_range) + self.translate_relative = translate_relative self.rotate_params: list[float] | None = None self.shear_params: list[float] | None = None @@ -1956,10 +1960,16 @@ def __call__( if randomize: self.randomize() lazy_ = self.lazy if lazy is None else lazy + translate_params = self.translate_params + if self.translate_relative and translate_params is not None: + # interpret the sampled parameters as fractions of the spatial dims + sp_size = spatial_size if spatial_size is not None else (tuple(grid.shape[1:]) if grid is not None else None) + if sp_size is not None: + translate_params = [p * d for p, d in zip(translate_params, sp_size)] affine_grid = AffineGrid( rotate_params=self.rotate_params, shear_params=self.shear_params, - translate_params=self.translate_params, + translate_params=translate_params, scale_params=self.scale_params, device=self.device, dtype=self.dtype, @@ -2455,6 +2465,7 @@ def __init__( shear_range: RandRange = None, translate_range: RandRange = None, scale_range: RandRange = None, + translate_relative: bool = False, spatial_size: Sequence[int] | int | None = None, mode: str | int = GridSampleMode.BILINEAR, padding_mode: str = GridSamplePadMode.REFLECTION, @@ -2485,6 +2496,9 @@ def __init__( translate_range: translate range with format matching `rotate_range`, it defines the range to randomly select pixel/voxel to translate for every spatial dims. + translate_relative: if True, `translate_range` values are interpreted as fractions of the + corresponding spatial dimension size (e.g. 0.5 allows translating up to half the image + size), instead of absolute voxels. Defaults to False. scale_range: scaling range with format matching `rotate_range`. it defines the range to randomly select the scale factor to translate for every spatial dims. A value of 1.0 is added to the result. This allows 0 to correspond to no change (i.e., a scaling of 1.0). @@ -2532,6 +2546,7 @@ def __init__( shear_range=shear_range, translate_range=translate_range, scale_range=scale_range, + translate_relative=translate_relative, device=device, lazy=lazy, ) From 17a5805aad92d84e0c3b0c4d68eabf7d1f516149 Mon Sep 17 00:00:00 2001 From: Habib Ur Rehman Date: Wed, 7 Oct 2026 23:50:42 +0500 Subject: [PATCH 2/4] Add translate_relative option to RandAffine for relative translation ranges Added translate_relative parameter to allow relative translation based on spatial dimensions. Signed-off-by: Habib Ur Rehman --- monai/transforms/spatial/dictionary.py | 31 +++++++++++++++++--------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/monai/transforms/spatial/dictionary.py b/monai/transforms/spatial/dictionary.py index 58307377118..d626ee5ae0e 100644 --- a/monai/transforms/spatial/dictionary.py +++ b/monai/transforms/spatial/dictionary.py @@ -1048,6 +1048,7 @@ def __init__( shear_range: Sequence[tuple[float, float] | float] | float | None = None, translate_range: Sequence[tuple[float, float] | float] | float | None = None, scale_range: Sequence[tuple[float, float] | float] | float | None = None, + translate_relative: bool = False, mode: SequenceStr = GridSampleMode.BILINEAR, padding_mode: SequenceStr = GridSamplePadMode.REFLECTION, cache_grid: bool = False, @@ -1085,6 +1086,9 @@ def __init__( translate_range: translate range with format matching `rotate_range`, it defines the range to randomly select pixel/voxel to translate for every spatial dims. + translate_relative: if True, `translate_range` values are interpreted as fractions of the + corresponding spatial dimension size (e.g. 0.5 allows translating up to half the image + size), instead of absolute voxels. Defaults to False. scale_range: scaling range with format matching `rotate_range`. it defines the range to randomly select the scale factor to translate for every spatial dims. A value of 1.0 is added to the result. This allows 0 to correspond to no change (i.e., a scaling of 1.0). @@ -1131,6 +1135,7 @@ def __init__( shear_range=shear_range, translate_range=translate_range, scale_range=scale_range, + translate_relative=translate_relative, spatial_size=spatial_size, cache_grid=cache_grid, device=device, @@ -1167,7 +1172,8 @@ def __call__( d = dict(data) first_key: Hashable = self.first_key(d) if first_key == (): - return d + out: dict[Hashable, NdarrayOrTensor] = convert_to_tensor(d, track_meta=get_track_meta()) + return out self.randomize(None) # all the keys share the same random Affine factor @@ -1327,7 +1333,8 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> dict[Hashable, N first_key: Hashable = self.first_key(d) if first_key == (): - return d + out: dict[Hashable, NdarrayOrTensor] = convert_to_tensor(d, track_meta=get_track_meta()) + return out self.randomize(None) device = self.rand_2d_elastic.device @@ -1362,8 +1369,7 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> dict[Hashable, N class Rand3DElasticd(RandomizableTransform, MapTransform): """ - Dictionary-based wrapper of :py:class:`monai.transforms.Rand3DElastic`. - """ + Dictionary-based wrapper of :py:class:`monai.transforms.Rand3DElastic`. """ backend = Rand3DElastic.backend @@ -1477,7 +1483,8 @@ def __call__(self, data: Mapping[Hashable, torch.Tensor]) -> dict[Hashable, torc first_key: Hashable = self.first_key(d) if first_key == (): - return d + out: dict[Hashable, torch.Tensor] = convert_to_tensor(d, track_meta=get_track_meta()) + return out self.randomize(None) if isinstance(d[first_key], MetaTensor) and d[first_key].pending_operations: # type: ignore @@ -2141,7 +2148,8 @@ def __call__(self, data: Mapping[Hashable, torch.Tensor], lazy: bool | None = No d = dict(data) first_key: Hashable = self.first_key(d) if first_key == (): - return d + out: dict[Hashable, torch.Tensor] = convert_to_tensor(d, track_meta=get_track_meta()) + return out self.randomize(None) @@ -2311,13 +2319,13 @@ def __call__(self, data: Mapping[Hashable, torch.Tensor]) -> dict[Hashable, torc d = dict(data) self.randomize(None) if not self._do_transform: - for key in self.key_iterator(d): - d[key] = convert_to_tensor(d[key], track_meta=get_track_meta()) - return d + out: dict[Hashable, torch.Tensor] = convert_to_tensor(d, track_meta=get_track_meta()) + return out first_key: Hashable = self.first_key(d) if first_key == (): - return d + out = convert_to_tensor(d, track_meta=get_track_meta()) + return out if isinstance(d[first_key], MetaTensor) and d[first_key].pending_operations: # type: ignore warnings.warn(f"data['{first_key}'] has pending operations, transform may return incorrect results.") self.rand_grid_distortion.randomize(d[first_key].shape[1:]) @@ -2639,7 +2647,8 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> dict[Hashable, N d = dict(data) first_key: Hashable = self.first_key(d) if first_key == (): - return d + out: dict[Hashable, NdarrayOrTensor] = convert_to_tensor(d, track_meta=get_track_meta()) + return out self.randomize(None) From 539a13299ae212c0539e7d2720853d6f25c1a43f Mon Sep 17 00:00:00 2001 From: Habib Ur Rehman Date: Thu, 8 Oct 2026 00:17:15 +0500 Subject: [PATCH 3/4] Add translate_relative option to RandAffine for relative translation ranges Signed-off-by: Habib Ur Rehman --- monai/transforms/spatial/array.py | 9 ++++---- monai/transforms/spatial/dictionary.py | 32 +++++++++++--------------- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index 6be9d984344..5cce31c0a30 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -1868,7 +1868,8 @@ def __init__( ) -> None: """ Args: - rotate_range: angle range in radians. If element `i` is a pair of (min, max) values, then `uniform[rotate_range[i][0], rotate_range[i][1])` will be used to generate the rotation parameter + rotate_range: angle range in radians. If element `i` is a pair of (min, max) values, then + `uniform[rotate_range[i][0], rotate_range[i][1])` will be used to generate the rotation parameter for the `i`th spatial dimension. If not, `uniform[-rotate_range[i], rotate_range[i])` will be used. This can be altered on a per-dimension basis. E.g., `((0,3), 1, ...)`: for dim0, rotation will be in range `[0, 3]`, and for dim1 `[-1, 1]` will be used. Setting a single value will use `[-x, x]` @@ -2496,12 +2497,12 @@ def __init__( translate_range: translate range with format matching `rotate_range`, it defines the range to randomly select pixel/voxel to translate for every spatial dims. - translate_relative: if True, `translate_range` values are interpreted as fractions of the - corresponding spatial dimension size (e.g. 0.5 allows translating up to half the image - size), instead of absolute voxels. Defaults to False. scale_range: scaling range with format matching `rotate_range`. it defines the range to randomly select the scale factor to translate for every spatial dims. A value of 1.0 is added to the result. This allows 0 to correspond to no change (i.e., a scaling of 1.0). + translate_relative: if True, `translate_range` values are interpreted as fractions of the + corresponding spatial dimension size (e.g. 0.5 allows translating up to half the image + size), instead of absolute voxels. Defaults to False. spatial_size: output image spatial size. if `spatial_size` and `self.spatial_size` are not defined, or smaller than 1, the transform will use the spatial size of `img`. diff --git a/monai/transforms/spatial/dictionary.py b/monai/transforms/spatial/dictionary.py index d626ee5ae0e..4bd822457d9 100644 --- a/monai/transforms/spatial/dictionary.py +++ b/monai/transforms/spatial/dictionary.py @@ -1086,12 +1086,12 @@ def __init__( translate_range: translate range with format matching `rotate_range`, it defines the range to randomly select pixel/voxel to translate for every spatial dims. - translate_relative: if True, `translate_range` values are interpreted as fractions of the - corresponding spatial dimension size (e.g. 0.5 allows translating up to half the image - size), instead of absolute voxels. Defaults to False. scale_range: scaling range with format matching `rotate_range`. it defines the range to randomly select the scale factor to translate for every spatial dims. A value of 1.0 is added to the result. This allows 0 to correspond to no change (i.e., a scaling of 1.0). + translate_relative: if True, `translate_range` values are interpreted as fractions of the + corresponding spatial dimension size (e.g. 0.5 allows translating up to half the image + size), instead of absolute voxels. Defaults to False. mode: {``"bilinear"``, ``"nearest"``} or spline interpolation order 0-5 (integers). Interpolation mode to calculate output values. Defaults to ``"bilinear"``. See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html @@ -1172,8 +1172,7 @@ def __call__( d = dict(data) first_key: Hashable = self.first_key(d) if first_key == (): - out: dict[Hashable, NdarrayOrTensor] = convert_to_tensor(d, track_meta=get_track_meta()) - return out + return d self.randomize(None) # all the keys share the same random Affine factor @@ -1333,8 +1332,7 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> dict[Hashable, N first_key: Hashable = self.first_key(d) if first_key == (): - out: dict[Hashable, NdarrayOrTensor] = convert_to_tensor(d, track_meta=get_track_meta()) - return out + return d self.randomize(None) device = self.rand_2d_elastic.device @@ -1369,7 +1367,8 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> dict[Hashable, N class Rand3DElasticd(RandomizableTransform, MapTransform): """ - Dictionary-based wrapper of :py:class:`monai.transforms.Rand3DElastic`. """ + Dictionary-based wrapper of :py:class:`monai.transforms.Rand3DElastic`. + """ backend = Rand3DElastic.backend @@ -1483,8 +1482,7 @@ def __call__(self, data: Mapping[Hashable, torch.Tensor]) -> dict[Hashable, torc first_key: Hashable = self.first_key(d) if first_key == (): - out: dict[Hashable, torch.Tensor] = convert_to_tensor(d, track_meta=get_track_meta()) - return out + return d self.randomize(None) if isinstance(d[first_key], MetaTensor) and d[first_key].pending_operations: # type: ignore @@ -2148,8 +2146,7 @@ def __call__(self, data: Mapping[Hashable, torch.Tensor], lazy: bool | None = No d = dict(data) first_key: Hashable = self.first_key(d) if first_key == (): - out: dict[Hashable, torch.Tensor] = convert_to_tensor(d, track_meta=get_track_meta()) - return out + return d self.randomize(None) @@ -2319,13 +2316,13 @@ def __call__(self, data: Mapping[Hashable, torch.Tensor]) -> dict[Hashable, torc d = dict(data) self.randomize(None) if not self._do_transform: - out: dict[Hashable, torch.Tensor] = convert_to_tensor(d, track_meta=get_track_meta()) - return out + for key in self.key_iterator(d): + d[key] = convert_to_tensor(d[key], track_meta=get_track_meta()) + return d first_key: Hashable = self.first_key(d) if first_key == (): - out = convert_to_tensor(d, track_meta=get_track_meta()) - return out + return d if isinstance(d[first_key], MetaTensor) and d[first_key].pending_operations: # type: ignore warnings.warn(f"data['{first_key}'] has pending operations, transform may return incorrect results.") self.rand_grid_distortion.randomize(d[first_key].shape[1:]) @@ -2647,8 +2644,7 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> dict[Hashable, N d = dict(data) first_key: Hashable = self.first_key(d) if first_key == (): - out: dict[Hashable, NdarrayOrTensor] = convert_to_tensor(d, track_meta=get_track_meta()) - return out + return d self.randomize(None) From e34ea94dc44b40793fce800e94940d6e76ee1fa7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:25:10 +0000 Subject: [PATCH 4/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- monai/transforms/spatial/array.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index 5cce31c0a30..0635e843c68 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -1964,7 +1964,9 @@ def __call__( translate_params = self.translate_params if self.translate_relative and translate_params is not None: # interpret the sampled parameters as fractions of the spatial dims - sp_size = spatial_size if spatial_size is not None else (tuple(grid.shape[1:]) if grid is not None else None) + sp_size = ( + spatial_size if spatial_size is not None else (tuple(grid.shape[1:]) if grid is not None else None) + ) if sp_size is not None: translate_params = [p * d for p, d in zip(translate_params, sp_size)] affine_grid = AffineGrid(