tests.transforms.augmentation.test_random_affine   B
last analyzed

Complexity

Total Complexity 44

Size/Duplication

Total Lines 231
Duplicated Lines 6.49 %

Importance

Changes 0
Metric Value
eloc 168
dl 15
loc 231
rs 8.8798
c 0
b 0
f 0
wmc 44

28 Methods

Rating   Name   Duplication   Size   Complexity  
A TestRandomAffine.test_mean() 0 2 1
A TestRandomAffine.test_scales_range_with_negative_min() 0 3 2
A TestRandomAffine.test_no_rotation() 0 24 1
A TestRandomAffine.test_isotropic() 0 2 1
A TestRandomAffine.setUp() 0 5 1
A TestRandomAffine.test_wrong_scales_type() 0 3 2
A TestRandomAffine.test_parse_degrees() 0 8 1
A TestRandomAffine.test_wrong_degrees_type() 0 3 2
A TestRandomAffine.test_bad_center() 0 3 2
A TestRandomAffine.test_parse_scales() 0 8 1
A TestRandomAffine.test_too_many_translation_values() 0 3 2
A TestRandomAffine.test_default_value_label_map() 0 7 1
A TestRandomAffine.test_parse_translation() 0 8 1
A TestRandomAffine.test_wrong_translation_type() 0 3 2
A TestRandomAffine.test_negative_scales() 0 3 2
A TestRandomAffine.test_scale_too_large() 0 3 2
A TestRandomAffine.test_rotation_image() 0 10 1
A TestRandomAffine.test_wrong_image_interpolation_type() 0 3 2
A TestRandomAffine.test_wrong_default_pad_value() 0 3 2
A TestRandomAffine.test_wrong_center() 0 3 2
A TestRandomAffine.test_wrong_image_interpolation_value() 0 3 2
A TestRandomAffine.test_rotation_origin() 0 10 1
A TestRandomAffine.test_incompatible_args_isotropic() 0 3 2
A TestRandomAffine.test_otsu() 0 2 1
A TestRandomAffine.test_default_pad_label_parameter() 0 41 2
A TestRandomAffine.test_different_spaces() 0 7 2
A TestRandomAffine.test_no_inverse() 15 15 1
A TestRandomAffine.test_wrong_default_pad_label() 0 3 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

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:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
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