Skip to content

Commit 03338b2

Browse files
Improve Affine transform documentation and add compute_w_affine tests (#8727)
## Summary This PR improves the documentation for the `Affine` transform and adds unit tests for the `compute_w_affine` method. Fixes #7092 ## Changes ### Documentation improvements (`monai/transforms/spatial/array.py`) - **Added Note section** to `Affine` class documenting the center-origin coordinate system assumption - **Clarified `normalized` parameter** documentation with user-friendly explanation - **Added comprehensive docstring** to `compute_w_affine` classmethod (previously undocumented) ### Unit tests (`tests/transforms/test_affine.py`) - Added `TestComputeWAffine` test class with focused tests: - 2D/3D identity matrix with same input/output size - Different input/output sizes with expected translation offsets - Output shape validation - Torch tensor input compatibility ## Verification - All existing Affine tests pass (no regressions) - All new `compute_w_affine` tests pass - Documentation matches actual code logic ## Type of change - [x] Documentation improvement - [x] Test coverage improvement - [ ] Breaking change Signed-off-by: Mohamed Salah <eng.mohamed.tawab@gmail.com> Signed-off-by: Eric Kerfoot <17726042+ericspod@users.noreply.github.com> Co-authored-by: Eric Kerfoot <17726042+ericspod@users.noreply.github.com>
1 parent 26326b5 commit 03338b2

2 files changed

Lines changed: 74 additions & 4 deletions

File tree

monai/transforms/spatial/array.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2189,6 +2189,13 @@ class Affine(InvertibleTransform, LazyTransform):
21892189
21902190
This transform is capable of lazy execution. See the :ref:`Lazy Resampling topic<lazy_resampling>`
21912191
for more information.
2192+
2193+
Note:
2194+
This transform assumes that the origin of the coordinate system is at the spatial center
2195+
of the image. When applying transformations (rotation, scaling, etc.), they are performed
2196+
relative to this center point. If you need transformations around a different origin,
2197+
you may need to compose this transform with translation operations or adjust your affine
2198+
matrix accordingly.
21922199
"""
21932200

21942201
backend = list(set(AffineGrid.backend) & set(Resample.backend))
@@ -2251,10 +2258,12 @@ def __init__(
22512258
When `mode` is an integer, using numpy/cupy backends, this argument accepts
22522259
{'reflect', 'grid-mirror', 'constant', 'grid-constant', 'nearest', 'mirror', 'grid-wrap', 'wrap'}.
22532260
See also: https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.map_coordinates.html
2254-
normalized: indicating whether the provided `affine` is defined to include a normalization
2255-
transform converting the coordinates from `[-(size-1)/2, (size-1)/2]` (defined in ``create_grid``) to
2256-
`[0, size - 1]` or `[-1, 1]` in order to be compatible with the underlying resampling API.
2257-
If `normalized=False`, additional coordinate normalization will be applied before resampling.
2261+
normalized: indicates whether the provided `affine` matrix already includes coordinate
2262+
normalization. Set to ``True`` if your affine matrix is designed to work with normalized
2263+
coordinates (e.g., from image processing libraries that use normalized coordinate systems).
2264+
Set to ``False`` (default) if your affine matrix works with pixel/voxel coordinates centered
2265+
at the image center. When ``False``, MONAI will automatically apply the necessary coordinate
2266+
transformations. Most users should use the default ``False``.
22582267
See also: :py:func:`monai.networks.utils.normalize_transform`.
22592268
device: device on which the tensor will be allocated.
22602269
dtype: data type for resampling computation. Defaults to ``float32``.
@@ -2346,6 +2355,25 @@ def __call__(
23462355

23472356
@classmethod
23482357
def compute_w_affine(cls, spatial_rank, mat, img_size, sp_size, align_corners: bool = False):
2358+
"""
2359+
Compute the affine matrix for transforming image coordinates, accounting for
2360+
center-based coordinate system.
2361+
2362+
This function adjusts the provided affine transformation matrix to work with images
2363+
where transformations are applied relative to the image center rather than the origin.
2364+
It composes the input matrix with translation operations that shift between
2365+
corner-based and center-based coordinate systems.
2366+
2367+
Args:
2368+
spatial_rank: number of spatial dimensions (e.g., 2 for 2D, 3 for 3D).
2369+
mat: the base affine transformation matrix to be adjusted.
2370+
img_size: spatial dimensions of the input image.
2371+
sp_size: spatial dimensions of the output (transformed) image.
2372+
align_corners: if True, align the corners of the initial and transformed volumes.
2373+
2374+
Returns:
2375+
The adjusted affine matrix that can be applied to image coordinates.
2376+
"""
23492377
r = int(spatial_rank)
23502378
mat = to_affine_nd(r, mat)
23512379
shift_1 = create_translate(r, [float(d - 1) / 2 for d in img_size[:r]])

tests/transforms/test_affine.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,48 @@ def test_affine(self, input_param, input_data, expected_val):
199199
)
200200

201201

202+
class TestComputeWAffine(unittest.TestCase):
203+
def test_identity_2d(self):
204+
"""Identity matrix with same input/output size should produce pure translation to/from center."""
205+
mat = np.eye(3)
206+
img_size = (4, 4)
207+
sp_size = (4, 4)
208+
result = Affine.compute_w_affine(2, mat, img_size, sp_size)
209+
# For identity transform with same sizes, result should be identity
210+
assert_allclose(result, np.eye(3), atol=1e-6)
211+
212+
def test_identity_3d(self):
213+
"""Identity matrix in 3D with same input/output size."""
214+
mat = np.eye(4)
215+
img_size = (6, 6, 6)
216+
sp_size = (6, 6, 6)
217+
result = Affine.compute_w_affine(3, mat, img_size, sp_size)
218+
assert_allclose(result, np.eye(4), atol=1e-6)
219+
220+
def test_different_sizes(self):
221+
"""When img_size != sp_size, result should include net translation."""
222+
mat = np.eye(3)
223+
img_size = (4, 4)
224+
sp_size = (8, 8)
225+
result = Affine.compute_w_affine(2, mat, img_size, sp_size)
226+
# Translation should account for the shift: (4-1)/2 - (8-1)/2 = 1.5 - 3.5 = -2.0
227+
expected_translation = np.array([(d1 - 1) / 2 - (d2 - 1) / 2 for d1, d2 in zip(img_size, sp_size)])
228+
assert_allclose(result[:2, 2], expected_translation, atol=1e-6)
229+
230+
def test_output_shape(self):
231+
"""Output should be (r+1) x (r+1) matrix."""
232+
for r in [2, 3]:
233+
mat = np.eye(r + 1)
234+
result = Affine.compute_w_affine(r, mat, (4,) * r, (4,) * r)
235+
self.assertEqual(result.shape, (r + 1, r + 1))
236+
237+
def test_torch_input(self):
238+
"""Method should accept torch tensor input."""
239+
mat = torch.eye(3)
240+
result = Affine.compute_w_affine(2, mat, (4, 4), (4, 4))
241+
assert_allclose(result, np.eye(3), atol=1e-6)
242+
243+
202244
@unittest.skipUnless(optional_import("scipy")[1], "Requires scipy library.")
203245
class TestAffineConsistency(unittest.TestCase):
204246
@parameterized.expand([[7], [8], [9]])

0 commit comments

Comments
 (0)