Passed
Pull Request — main (#1400)
by
unknown
01:35
created

tests.transforms.test_custom_image_subclass   A

Complexity

Total Complexity 19

Size/Duplication

Total Lines 221
Duplicated Lines 6.33 %

Importance

Changes 0
Metric Value
eloc 117
dl 14
loc 221
rs 10
c 0
b 0
f 0
wmc 19

16 Methods

Rating   Name   Duplication   Size   Complexity  
A TestCustomImageSubclass.history_image() 0 6 1
A HistoryImage.__init__() 3 3 1
A MetadataImage.__init__() 0 3 1
A TestCustomImageSubclass.test_new_like_method_directly() 0 18 1
A HistoryImage.new_like() 7 7 2
A MetadataImage.new_like() 0 7 2
A TestCustomImageSubclass.test_backward_compatibility_standard_images() 0 15 1
A TestCustomImageSubclass.test_chained_transforms_preserve_attributes() 0 20 1
A TestCustomImageSubclass.test_to_reference_space_with_custom_image() 0 13 1
A TestCustomImageSubclass.test_label_map_subclass() 0 35 2
A TestCustomImageSubclass.metadata_image() 0 7 1
A TestCustomImageSubclass.test_new_like_with_default_affine() 0 14 1
A TestCustomImageSubclass.test_crop_with_history_image() 0 13 1
A TestCustomImageSubclass.history_subject() 0 4 1
A TestCustomImageSubclass.metadata_subject() 0 4 1
A TestCustomImageSubclass.test_crop_with_metadata_image() 0 13 1

How to fix   Duplicated Code   

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:

1
"""Tests for custom Image subclasses with transforms."""
2
3
import pytest
4
import torch
5
6
import torchio as tio
7
8
9 View Code Duplication
class HistoryImage(tio.ScalarImage):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
10
    """Test custom Image with required parameter."""
11
12
    def __init__(self, tensor, affine, history, **kwargs):
13
        super().__init__(tensor=tensor, affine=affine, **kwargs)
14
        self.history = history
15
16
    def new_like(self, tensor, affine=None):
17
        return type(self)(
18
            tensor=tensor,
19
            affine=affine if affine is not None else self.affine,
20
            history=self.history,
21
            check_nans=self.check_nans,
22
            reader=self.reader,
23
        )
24
25
26
class MetadataImage(tio.ScalarImage):
27
    """Test custom Image with optional parameter."""
28
29
    def __init__(self, tensor, affine, metadata=None, **kwargs):
30
        super().__init__(tensor=tensor, affine=affine, **kwargs)
31
        self.metadata = metadata or {}
32
33
    def new_like(self, tensor, affine=None):
34
        return type(self)(
35
            tensor=tensor,
36
            affine=affine if affine is not None else self.affine,
37
            metadata=self.metadata,
38
            check_nans=self.check_nans,
39
            reader=self.reader,
40
        )
41
42
43
class TestCustomImageSubclass:
44
    """Test suite for custom Image subclasses with transforms."""
45
46
    @pytest.fixture
47
    def history_image(self):
48
        """Create a HistoryImage for testing."""
49
        tensor = torch.rand(1, 10, 10, 10)
50
        affine = torch.eye(4)
51
        return HistoryImage(tensor=tensor, affine=affine, history=['created'])
52
53
    @pytest.fixture
54
    def metadata_image(self):
55
        """Create a MetadataImage for testing."""
56
        tensor = torch.rand(1, 12, 12, 12)
57
        affine = torch.eye(4)
58
        return MetadataImage(
59
            tensor=tensor, affine=affine, metadata={'id': 123, 'source': 'test'}
60
        )
61
62
    @pytest.fixture
63
    def history_subject(self, history_image):
64
        """Create a Subject with HistoryImage."""
65
        return tio.Subject(image=history_image)
66
67
    @pytest.fixture
68
    def metadata_subject(self, metadata_image):
69
        """Create a Subject with MetadataImage."""
70
        return tio.Subject(image=metadata_image)
71
72
    def test_crop_with_history_image(self, history_subject):
73
        """Test that Crop transform works with custom Image requiring history parameter."""
74
        transform = tio.Crop(cropping=2)
75
        result = transform(history_subject)
76
77
        # Check that the result is still a HistoryImage
78
        assert isinstance(result.image, HistoryImage)
79
80
        # Check that custom attribute is preserved
81
        assert result.image.history == ['created']
82
83
        # Check that cropping worked correctly
84
        assert result.image.shape == (1, 6, 6, 6)
85
86
    def test_crop_with_metadata_image(self, metadata_subject):
87
        """Test that Crop transform works with custom Image with optional parameters."""
88
        transform = tio.Crop(cropping=1)
89
        result = transform(metadata_subject)
90
91
        # Check that the result is still a MetadataImage
92
        assert isinstance(result.image, MetadataImage)
93
94
        # Check that custom attribute is preserved
95
        assert result.image.metadata == {'id': 123, 'source': 'test'}
96
97
        # Check that cropping worked correctly
98
        assert result.image.shape == (1, 10, 10, 10)
99
100
    def test_chained_transforms_preserve_attributes(self, history_subject):
101
        """Test that chained transforms preserve custom attributes."""
102
        # Chain multiple transforms
103
        transform = tio.Compose(
104
            [
105
                tio.Crop(cropping=1),
106
                tio.Crop(cropping=1),
107
            ]
108
        )
109
110
        result = transform(history_subject)
111
112
        # Check that the result is still a HistoryImage after multiple transforms
113
        assert isinstance(result.image, HistoryImage)
114
115
        # Check that custom attribute is preserved through the chain
116
        assert result.image.history == ['created']
117
118
        # Check that both crops were applied
119
        assert result.image.shape == (1, 6, 6, 6)
120
121
    def test_backward_compatibility_standard_images(self):
122
        """Test that standard Images still work with transforms."""
123
        # Create a standard ScalarImage
124
        tensor = torch.rand(1, 10, 10, 10)
125
        affine = torch.eye(4)
126
        image = tio.ScalarImage(tensor=tensor, affine=affine)
127
        subject = tio.Subject(image=image)
128
129
        # Apply transform
130
        transform = tio.Crop(cropping=2)
131
        result = transform(subject)
132
133
        # Check that it still works
134
        assert isinstance(result.image, tio.ScalarImage)
135
        assert result.image.shape == (1, 6, 6, 6)
136
137
    def test_to_reference_space_with_custom_image(self, history_image):
138
        """Test that ToReferenceSpace works with custom images."""
139
        # Create embedding tensor (smaller than reference)
140
        embedding_tensor = torch.rand(1, 10, 10, 10)
141
142
        # Use ToReferenceSpace.from_tensor
143
        result = tio.ToReferenceSpace.from_tensor(embedding_tensor, history_image)
144
145
        # Check that the result preserves the custom class type
146
        assert isinstance(result, HistoryImage)
147
148
        # Check that custom attribute is preserved
149
        assert result.history == ['created']
150
151
    def test_new_like_method_directly(self, history_image):
152
        """Test the new_like method directly."""
153
        new_tensor = torch.rand(1, 5, 5, 5)
154
        new_affine = torch.eye(4) * 2
155
156
        # Create new image using new_like
157
        new_image = history_image.new_like(tensor=new_tensor, affine=new_affine)
158
159
        # Check type preservation
160
        assert isinstance(new_image, HistoryImage)
161
162
        # Check attribute preservation
163
        assert new_image.history == ['created']
164
165
        # Check new data
166
        assert torch.equal(new_image.data, new_tensor)
167
        assert torch.allclose(
168
            torch.tensor(new_image.affine).float(), new_affine.float()
169
        )
170
171
    def test_new_like_with_default_affine(self, metadata_image):
172
        """Test new_like method with default affine (None)."""
173
        new_tensor = torch.rand(1, 8, 8, 8)
174
175
        # Create new image using new_like with default affine
176
        new_image = metadata_image.new_like(tensor=new_tensor)
177
178
        # Check that original affine is used
179
        assert torch.allclose(
180
            torch.tensor(new_image.affine), torch.tensor(metadata_image.affine)
181
        )
182
183
        # Check attribute preservation
184
        assert new_image.metadata == {'id': 123, 'source': 'test'}
185
186
    def test_label_map_subclass(self):
187
        """Test that custom LabelMap subclasses also work."""
188
189
        class CustomLabelMap(tio.LabelMap):
190
            def __init__(self, tensor, affine, labels_info, **kwargs):
191
                super().__init__(tensor=tensor, affine=affine, **kwargs)
192
                self.labels_info = labels_info
193
194
            def new_like(self, tensor, affine=None):
195
                return type(self)(
196
                    tensor=tensor,
197
                    affine=affine if affine is not None else self.affine,
198
                    labels_info=self.labels_info,
199
                    check_nans=self.check_nans,
200
                    reader=self.reader,
201
                )
202
203
        # Create custom label map
204
        tensor = torch.randint(0, 3, (1, 8, 8, 8))
205
        affine = torch.eye(4)
206
        labels_info = {0: 'background', 1: 'tissue1', 2: 'tissue2'}
207
208
        custom_label = CustomLabelMap(
209
            tensor=tensor, affine=affine, labels_info=labels_info
210
        )
211
        subject = tio.Subject(labels=custom_label)
212
213
        # Apply transform
214
        transform = tio.Crop(cropping=1)
215
        result = transform(subject)
216
217
        # Check preservation
218
        assert isinstance(result.labels, CustomLabelMap)
219
        assert result.labels.labels_info == labels_info
220
        assert result.labels.shape == (1, 6, 6, 6)
221