Total Complexity | 44 |
Total Lines | 231 |
Duplicated Lines | 6.49 % |
Changes | 0 |
Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like tests.transforms.augmentation.test_random_affine often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
1 | import pytest |
||
2 | import torch |
||
3 | |||
4 | import torchio as tio |
||
5 | |||
6 | from ...utils import TorchioTestCase |
||
7 | |||
8 | |||
9 | class TestRandomAffine(TorchioTestCase): |
||
10 | """Tests for `RandomAffine`.""" |
||
11 | |||
12 | def setUp(self): |
||
13 | # Set image origin far from center |
||
14 | super().setUp() |
||
15 | affine = self.sample_subject.t1.affine |
||
16 | affine[:3, 3] = 1e5 |
||
17 | |||
18 | def test_rotation_image(self): |
||
19 | # Rotation around image center |
||
20 | transform = tio.RandomAffine( |
||
21 | degrees=(90, 90), |
||
22 | default_pad_value=0, |
||
23 | center='image', |
||
24 | ) |
||
25 | transformed = transform(self.sample_subject) |
||
26 | total = transformed.t1.data.sum() |
||
27 | self.assertNotEqual(total, 0) |
||
28 | |||
29 | def test_rotation_origin(self): |
||
30 | # Rotation around far away point, image should be empty |
||
31 | transform = tio.RandomAffine( |
||
32 | degrees=(90, 90), |
||
33 | default_pad_value=0, |
||
34 | center='origin', |
||
35 | ) |
||
36 | transformed = transform(self.sample_subject) |
||
37 | total = transformed.t1.data.sum() |
||
38 | assert total == 0 |
||
39 | |||
40 | def test_no_rotation(self): |
||
41 | transform = tio.RandomAffine( |
||
42 | scales=(1, 1), |
||
43 | degrees=(0, 0), |
||
44 | default_pad_value=0, |
||
45 | center='image', |
||
46 | ) |
||
47 | transformed = transform(self.sample_subject) |
||
48 | self.assert_tensor_almost_equal( |
||
49 | self.sample_subject.t1.data, |
||
50 | transformed.t1.data, |
||
51 | ) |
||
52 | |||
53 | transform = tio.RandomAffine( |
||
54 | scales=(1, 1), |
||
55 | degrees=(180, 180), |
||
56 | default_pad_value=0, |
||
57 | center='image', |
||
58 | ) |
||
59 | transformed = transform(self.sample_subject) |
||
60 | transformed = transform(transformed) |
||
61 | self.assert_tensor_almost_equal( |
||
62 | self.sample_subject.t1.data, |
||
63 | transformed.t1.data, |
||
64 | ) |
||
65 | |||
66 | def test_isotropic(self): |
||
67 | tio.RandomAffine(isotropic=True)(self.sample_subject) |
||
68 | |||
69 | def test_mean(self): |
||
70 | tio.RandomAffine(default_pad_value='mean')(self.sample_subject) |
||
71 | |||
72 | def test_otsu(self): |
||
73 | tio.RandomAffine(default_pad_value='otsu')(self.sample_subject) |
||
74 | |||
75 | def test_bad_center(self): |
||
76 | with pytest.raises(ValueError): |
||
77 | tio.RandomAffine(center='bad') |
||
78 | |||
79 | def test_negative_scales(self): |
||
80 | with pytest.raises(ValueError): |
||
81 | tio.RandomAffine(scales=(-1, 1)) |
||
82 | |||
83 | def test_scale_too_large(self): |
||
84 | with pytest.raises(ValueError): |
||
85 | tio.RandomAffine(scales=1.5) |
||
86 | |||
87 | def test_scales_range_with_negative_min(self): |
||
88 | with pytest.raises(ValueError): |
||
89 | tio.RandomAffine(scales=(-1, 4)) |
||
90 | |||
91 | def test_wrong_scales_type(self): |
||
92 | with pytest.raises(ValueError): |
||
93 | tio.RandomAffine(scales='wrong') |
||
94 | |||
95 | def test_wrong_degrees_type(self): |
||
96 | with pytest.raises(ValueError): |
||
97 | tio.RandomAffine(degrees='wrong') |
||
98 | |||
99 | def test_too_many_translation_values(self): |
||
100 | with pytest.raises(ValueError): |
||
101 | tio.RandomAffine(translation=(-10, 4, 42)) |
||
102 | |||
103 | def test_wrong_translation_type(self): |
||
104 | with pytest.raises(ValueError): |
||
105 | tio.RandomAffine(translation='wrong') |
||
106 | |||
107 | def test_wrong_center(self): |
||
108 | with pytest.raises(ValueError): |
||
109 | tio.RandomAffine(center=0) |
||
110 | |||
111 | def test_wrong_default_pad_value(self): |
||
112 | with pytest.raises(ValueError): |
||
113 | tio.RandomAffine(default_pad_value='wrong') |
||
114 | |||
115 | def test_wrong_image_interpolation_type(self): |
||
116 | with pytest.raises(TypeError): |
||
117 | tio.RandomAffine(image_interpolation=0) |
||
118 | |||
119 | def test_wrong_image_interpolation_value(self): |
||
120 | with pytest.raises(ValueError): |
||
121 | tio.RandomAffine(image_interpolation='wrong') |
||
122 | |||
123 | def test_incompatible_args_isotropic(self): |
||
124 | with pytest.raises(ValueError): |
||
125 | tio.RandomAffine(scales=(0.8, 0.5, 0.1), isotropic=True) |
||
126 | |||
127 | def test_parse_scales(self): |
||
128 | def do_assert(transform): |
||
129 | assert transform.scales == 3 * (0.9, 1.1) |
||
130 | |||
131 | do_assert(tio.RandomAffine(scales=0.1)) |
||
132 | do_assert(tio.RandomAffine(scales=(0.9, 1.1))) |
||
133 | do_assert(tio.RandomAffine(scales=3 * (0.1,))) |
||
134 | do_assert(tio.RandomAffine(scales=3 * [0.9, 1.1])) |
||
135 | |||
136 | def test_parse_degrees(self): |
||
137 | def do_assert(transform): |
||
138 | assert transform.degrees == 3 * (-10, 10) |
||
139 | |||
140 | do_assert(tio.RandomAffine(degrees=10)) |
||
141 | do_assert(tio.RandomAffine(degrees=(-10, 10))) |
||
142 | do_assert(tio.RandomAffine(degrees=3 * (10,))) |
||
143 | do_assert(tio.RandomAffine(degrees=3 * [-10, 10])) |
||
144 | |||
145 | def test_parse_translation(self): |
||
146 | def do_assert(transform): |
||
147 | assert transform.translation == 3 * (-10, 10) |
||
148 | |||
149 | do_assert(tio.RandomAffine(translation=10)) |
||
150 | do_assert(tio.RandomAffine(translation=(-10, 10))) |
||
151 | do_assert(tio.RandomAffine(translation=3 * (10,))) |
||
152 | do_assert(tio.RandomAffine(translation=3 * [-10, 10])) |
||
153 | |||
154 | def test_default_value_label_map(self): |
||
155 | # From https://github.com/TorchIO-project/torchio/issues/626 |
||
156 | a = torch.tensor([[1, 0, 0], [0, 1, 0], [0, 0, 1]]).reshape(1, 3, 3, 1) |
||
157 | image = tio.LabelMap(tensor=a) |
||
158 | aff = tio.RandomAffine(translation=(0, 1, 1), default_pad_value='otsu') |
||
159 | transformed = aff(image) |
||
160 | assert all(n in (0, 1) for n in transformed.data.flatten()) |
||
161 | |||
162 | def test_default_pad_label_parameter(self): |
||
163 | # Test for issue #1304: Using default_pad_value if image is of type LABEL |
||
164 | # Create a simple label map |
||
165 | label_data = torch.ones((1, 2, 2, 2)) |
||
166 | subject = tio.Subject(label=tio.LabelMap(tensor=label_data)) |
||
167 | |||
168 | # Test 1: default_pad_label should be respected |
||
169 | transform = tio.RandomAffine( |
||
170 | translation=(10, 10), |
||
171 | default_pad_label=250, |
||
172 | ) |
||
173 | transformed_subject = transform(subject) |
||
174 | |||
175 | # Should contain the specified pad value for labels |
||
176 | message = 'default_pad_label=250 should be respected for LABEL images' |
||
177 | has_expected_value = (transformed_subject['label'].tensor == 250).any() |
||
178 | assert has_expected_value, message |
||
179 | |||
180 | # Test 2: backward compatibility - default_pad_value should still be ignored for labels |
||
181 | message = 'default_pad_value should still be ignored for LABEL images (backward compatibility)' |
||
182 | aff_old = tio.RandomAffine( |
||
183 | translation=(-10, 10, -10, 10, -10, 10), |
||
184 | default_pad_value=250, # This should be ignored for labels |
||
185 | ) |
||
186 | s_aug_old = aff_old.apply_transform(subject) |
||
187 | |||
188 | # Should still use 0 (default for labels), not the default_pad_value |
||
189 | non_one_values = s_aug_old['label'].data[s_aug_old['label'].data != 1] |
||
190 | all_zeros = (non_one_values == 0).all() if len(non_one_values) > 0 else True |
||
191 | assert all_zeros, message |
||
192 | |||
193 | # Test 3: Test direct Affine class with default_pad_label |
||
194 | affine_transform = tio.Affine( |
||
195 | scales=(1, 1, 1), |
||
196 | degrees=(0, 0, 0), |
||
197 | translation=(5, 0, 0), |
||
198 | default_pad_label=123, |
||
199 | ) |
||
200 | s_affine = affine_transform.apply_transform(subject) |
||
201 | has_affine_value = (s_affine['label'].tensor == 123).any() |
||
202 | assert has_affine_value, 'Direct Affine class should respect default_pad_label' |
||
203 | |||
204 | def test_wrong_default_pad_label(self): |
||
205 | with pytest.raises(ValueError): |
||
206 | tio.RandomAffine(default_pad_label='minimum') |
||
207 | |||
208 | View Code Duplication | def test_no_inverse(self): |
|
|
|||
209 | tensor = torch.zeros((1, 2, 2, 2)) |
||
210 | tensor[0, 1, 1, 1] = 1 # most RAS voxel |
||
211 | expected = torch.zeros((1, 2, 2, 2)) |
||
212 | expected[0, 0, 1, 1] = 1 |
||
213 | scales = 1, 1, 1 |
||
214 | degrees = 0, 0, 90 # anterior should go left |
||
215 | translation = 0, 0, 0 |
||
216 | apply_affine = tio.Affine( |
||
217 | scales, |
||
218 | degrees, |
||
219 | translation, |
||
220 | ) |
||
221 | transformed = apply_affine(tensor) |
||
222 | self.assert_tensor_almost_equal(transformed, expected) |
||
223 | |||
224 | def test_different_spaces(self): |
||
225 | t1 = self.sample_subject.t1 |
||
226 | label = tio.Resample(2)(self.sample_subject.label) |
||
227 | new_subject = tio.Subject(t1=t1, label=label) |
||
228 | with pytest.raises(RuntimeError): |
||
229 | tio.RandomAffine()(new_subject) |
||
230 | tio.RandomAffine(check_shape=False)(new_subject) |
||
231 |