From cc83869fee4f498c791e4df79617fda12821c31b Mon Sep 17 00:00:00 2001 From: Fernando Date: Sun, 24 Oct 2021 20:04:17 +0100 Subject: [PATCH 1/2] Make Affine take RAS parameters of floating->ref --- .../augmentation/test_random_affine.py | 36 +++++++++---------- .../augmentation/spatial/random_affine.py | 26 ++++++++++---- 2 files changed, 36 insertions(+), 26 deletions(-) diff --git a/tests/transforms/augmentation/test_random_affine.py b/tests/transforms/augmentation/test_random_affine.py index d656eb019..bb86c159c 100644 --- a/tests/transforms/augmentation/test_random_affine.py +++ b/tests/transforms/augmentation/test_random_affine.py @@ -72,26 +72,6 @@ def test_bad_center(self): with self.assertRaises(ValueError): tio.RandomAffine(center='bad') - def test_translation(self): - transform = tio.RandomAffine( - scales=(1, 1), - degrees=0, - translation=(5, 5) - ) - transformed = transform(self.sample_subject) - - # I think the right test should be the following one: - # self.assertTensorAlmostEqual( - # self.sample_subject.t1.data[:, :-5, :-5, :-5], - # transformed.t1.data[:, 5:, 5:, 5:] - # ) - - # However the passing test is this one: - self.assertTensorAlmostEqual( - self.sample_subject.t1.data[:, :-5, :-5, 5:], - transformed.t1.data[:, 5:, 5:, :-5] - ) - def test_negative_scales(self): with self.assertRaises(ValueError): tio.RandomAffine(scales=(-1, 1)) @@ -171,3 +151,19 @@ def test_default_value_label_map(self): aff = tio.RandomAffine(translation=(0, 1, 1), default_pad_value='otsu') transformed = aff(image) assert all(n in (0, 1) for n in transformed.data.flatten()) + + def test_no_inverse(self): + tensor = torch.zeros((1, 2, 2, 2)) + tensor[0, 1, 1, 1] = 1 # most RAS voxel + expected = torch.zeros((1, 2, 2, 2)) + expected[0, 0, 1, 1] = 1 + scales = 1, 1, 1 + degrees = 0, 0, 90 # anterior should go left + translation = 0, 0, 0 + apply_affine = tio.Affine( + scales, + degrees, + translation, + ) + transformed = apply_affine(tensor) + self.assertTensorAlmostEqual(transformed, expected) diff --git a/torchio/transforms/augmentation/spatial/random_affine.py b/torchio/transforms/augmentation/spatial/random_affine.py index 32d9c7dfc..b27d00cbb 100644 --- a/torchio/transforms/augmentation/spatial/random_affine.py +++ b/torchio/transforms/augmentation/spatial/random_affine.py @@ -230,15 +230,17 @@ def _get_scaling_transform( scaling_params: Sequence[float], center_lps: Optional[TypeTripletFloat] = None, ) -> sitk.ScaleTransform: - # scaling_params are inverted so that they are more intuitive - # For example, 1.5 means the objects look 1.5 times larger + # 1.5 means the objects look 1.5 times larger transform = sitk.ScaleTransform(3) - scaling_params = 1 / np.array(scaling_params) + scaling_params = np.array(scaling_params).astype(float) transform.SetScale(scaling_params) if center_lps is not None: transform.SetCenter(center_lps) return transform + def _ras_to_lps(self): + return np.array((-1, -1, 1), dtype=float) + @staticmethod def _get_rotation_transform( degrees: Sequence[float], @@ -247,9 +249,14 @@ def _get_rotation_transform( ) -> sitk.Euler3DTransform: transform = sitk.Euler3DTransform() radians = np.radians(degrees) - transform.SetRotation(*radians) - translation = np.array(translation).astype(float) - transform.SetTranslation(translation) + + # SimpleITK uses LPS + ras_to_lps = np.array((-1, -1, 1), dtype=float) + radians_lps = ras_to_lps * radians + translation_lps = np.array(translation) * ras_to_lps + + transform.SetRotation(*radians_lps) + transform.SetTranslation(translation_lps) if center_lps is not None: transform.SetCenter(center_lps) return transform @@ -287,6 +294,13 @@ def get_affine_transform(self, image): transforms = [scaling_transform, rotation_transform] transform = sitk.CompositeTransform(transforms) + # ResampleImageFilter expects the transform from the output space to + # the input space. Intuitively, the passed arguments should take us + # from the input space to the output space, so we need to invert the + # transform. + # More info at https://github.com/fepegar/torchio/discussions/693 + transform = transform.GetInverse() + if self.invert_transform: transform = transform.GetInverse() From e1b8f7d5125783019e9ad6a47e3e2be6dcb7b96a Mon Sep 17 00:00:00 2001 From: Fernando Perez-Garcia Date: Mon, 25 Oct 2021 10:50:26 +0100 Subject: [PATCH 2/2] Address comments about code --- .../augmentation/spatial/random_affine.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/torchio/transforms/augmentation/spatial/random_affine.py b/torchio/transforms/augmentation/spatial/random_affine.py index b27d00cbb..079feafdd 100644 --- a/torchio/transforms/augmentation/spatial/random_affine.py +++ b/torchio/transforms/augmentation/spatial/random_affine.py @@ -238,8 +238,7 @@ def _get_scaling_transform( transform.SetCenter(center_lps) return transform - def _ras_to_lps(self): - return np.array((-1, -1, 1), dtype=float) + @staticmethod def _get_rotation_transform( @@ -247,13 +246,16 @@ def _get_rotation_transform( translation: Sequence[float], center_lps: Optional[TypeTripletFloat] = None, ) -> sitk.Euler3DTransform: + + def ras_to_lps(triplet: np.ndarray): + return np.array((-1, -1, 1), dtype=float) * np.asarray(triplet) + transform = sitk.Euler3DTransform() radians = np.radians(degrees) # SimpleITK uses LPS - ras_to_lps = np.array((-1, -1, 1), dtype=float) - radians_lps = ras_to_lps * radians - translation_lps = np.array(translation) * ras_to_lps + radians_lps = ras_to_lps(radians) + translation_lps = ras_to_lps(translation) transform.SetRotation(*radians_lps) transform.SetTranslation(translation_lps) @@ -262,9 +264,9 @@ def _get_rotation_transform( return transform def get_affine_transform(self, image): - scaling = np.array(self.scales).copy() - rotation = np.array(self.degrees).copy() - translation = np.array(self.translation).copy() + scaling = np.asarray(self.scales).copy() + rotation = np.asarray(self.degrees).copy() + translation = np.asarray(self.translation).copy() if image.is_2d(): scaling[2] = 1