File: transformations_unit_test.py

package info (click to toggle)
python-sigima 1.0.3-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 24,956 kB
  • sloc: python: 33,326; makefile: 3
file content (375 lines) | stat: -rw-r--r-- 13,180 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.

"""
Unit tests for transformations module
"""

from __future__ import annotations

import numpy as np
import pytest

from sigima.objects.image.roi import CircularROI, PolygonalROI, RectangularROI
from sigima.objects.scalar import GeometryResult, KindShape
from sigima.objects.shape import PointCoordinates
from sigima.proc.image.transformations import GeometryTransformer, transformer


def test_geometry_transformer_singleton() -> None:
    """
    Test that GeometryTransformer follows singleton pattern.
    """
    t1 = GeometryTransformer()
    t2 = GeometryTransformer()
    assert t1 is t2
    assert t1 is transformer


class TestGeometryResultTransformations:
    """Test class for GeometryResult transformations."""

    def test_transform_point(self) -> None:
        """
        Test transformation of GeometryResult with POINT coordinates.
        """
        # Create a GeometryResult with point coordinates
        coords = np.array([[1.0, 2.0], [3.0, 4.0]])
        geometry = GeometryResult(
            title="Test Points",
            kind=KindShape.POINT,
            coords=coords,
            roi_indices=None,
            attrs={},
        )

        # Test rotation
        rotated = transformer.rotate(geometry, np.pi / 2, center=(0, 0))
        expected_coords = np.array([[-2.0, 1.0], [-4.0, 3.0]])
        assert np.allclose(rotated.coords, expected_coords)
        assert rotated.title == geometry.title
        assert rotated.kind == geometry.kind

        # Test translation
        translated = transformer.translate(geometry, 10.0, 20.0)
        expected_coords = np.array([[11.0, 22.0], [13.0, 24.0]])
        assert np.allclose(translated.coords, expected_coords)

        # Original should be unchanged
        assert np.allclose(geometry.coords, coords)

    def test_transform_rectangle(self) -> None:
        """
        Test transformation of GeometryResult with RECTANGLE coordinates.
        """
        # Create a GeometryResult with rectangle coordinates (x0, y0, dx, dy)
        coords = np.array([[0.0, 0.0, 3.0, 1.0], [10.0, 10.0, 2.0, 4.0]])
        geometry = GeometryResult(
            title="Test Rectangles",
            kind=KindShape.RECTANGLE,
            coords=coords,
            roi_indices=None,
            attrs={},
        )

        # Test horizontal flip around x=1
        flipped = transformer.fliph(geometry, cx=1.0)
        expected_coords = np.array([[-1.0, 0.0, 3.0, 1.0], [-10.0, 10.0, 2.0, 4.0]])
        assert np.allclose(flipped.coords, expected_coords)

        # Test transpose
        transposed = transformer.transpose(geometry)
        expected_coords = np.array([[0.0, 0.0, 1.0, 3.0], [10.0, 10.0, 4.0, 2.0]])
        assert np.allclose(transposed.coords, expected_coords)

    def test_transform_circle(self) -> None:
        """
        Test transformation of GeometryResult with CIRCLE coordinates.
        """
        # Create a GeometryResult with circle coordinates
        coords = np.array([[1.0, 2.0, 5.0], [10.0, 20.0, 10.0]])
        geometry = GeometryResult(
            title="Test Circles",
            kind=KindShape.CIRCLE,
            coords=coords,
            roi_indices=None,
            attrs={},
        )

        # Test scaling (only center should be scaled, radius unchanged)
        scaled = transformer.scale(geometry, 2.0, 3.0, center=(0, 0))
        expected_coords = np.array([[2.0, 6.0, 5.0], [20.0, 60.0, 10.0]])
        assert np.allclose(scaled.coords, expected_coords)

    def test_generic_transform_method(self) -> None:
        """
        Test generic transform_geometry method.
        """
        coords = np.array([[1.0, 2.0]])
        geometry = GeometryResult(
            title="Test Point",
            kind=KindShape.POINT,
            coords=coords,
            roi_indices=None,
            attrs={},
        )

        # Test generic method
        rotated = transformer.transform_geometry(
            geometry, "rotate", angle=np.pi / 2, center=(0, 0)
        )
        expected_coords = np.array([[-2.0, 1.0]])
        assert np.allclose(rotated.coords, expected_coords)

    def test_unsupported_operation(self) -> None:
        """
        Test error handling for unsupported operations.
        """
        coords = np.array([[1.0, 2.0]])
        geometry = GeometryResult(
            title="Test Point",
            kind=KindShape.POINT,
            coords=coords,
            roi_indices=None,
            attrs={},
        )

        with pytest.raises(ValueError, match="Unknown operation"):
            transformer.transform_geometry(geometry, "invalid_operation")

    def test_direct_coordinate_transformation(self) -> None:
        """
        Test that transformations use the shape coordinate system correctly.
        """
        # Test that our transformer produces the same results as direct shape
        # coordinate usage
        coords = np.array([[2.0, 3.0]])
        geometry = GeometryResult(
            title="Test Point",
            kind=KindShape.POINT,
            coords=coords,
            roi_indices=None,
            attrs={},
        )

        # Transform using transformer
        rotated_geometry = transformer.rotate(geometry, np.pi / 2, center=(1, 1))

        # Transform using shape coordinates directly
        shape_coords = PointCoordinates([2.0, 3.0])
        shape_coords.rotate(np.pi / 2, center=(1, 1))

        # Results should be identical
        assert np.allclose(rotated_geometry.coords[0], shape_coords.data)


class TestSingleROITransformations:
    """Test class for single ROI transformations."""

    def test_transform_rectangular_roi_rotation(self) -> None:
        """
        Test rotation transformation of RectangularROI.
        """
        # Create a rectangular ROI (x0, y0, dx, dy)
        # Rectangle corners: (0, 0) and (4, 2)
        roi = RectangularROI(coords=[0.0, 0.0, 4.0, 2.0], indices=False, title="Test")
        original_coords = roi.coords.copy()

        # Rotate 90 degrees around origin
        # After rotation: (0, 0) -> (0, 0) and (4, 2) -> (-2, 4)
        # New bounding box: x_min=-2, y_min=0, x_max=0, y_max=4
        # So: x0=-2, y0=0, dx=2, dy=4
        transformer.rotate(roi, np.pi / 2, center=(0, 0))

        expected_coords = np.array([-2.0, 0.0, 2.0, 4.0])
        assert np.allclose(roi.coords, expected_coords)
        assert not np.allclose(roi.coords, original_coords)

    def test_transform_rectangular_roi_translation(self) -> None:
        """
        Test translation transformation of RectangularROI.
        """
        roi = RectangularROI(coords=[1.0, 2.0, 3.0, 4.0], indices=False, title="Test")

        # Translate by (10, 20)
        transformer.translate(roi, 10.0, 20.0)

        expected_coords = np.array([11.0, 22.0, 3.0, 4.0])
        assert np.allclose(roi.coords, expected_coords)

    def test_transform_rectangular_roi_fliph(self) -> None:
        """
        Test horizontal flip transformation of RectangularROI.
        """
        # Create a rectangle with x0=1, dx=3, so corners at x=1 and x=4
        roi = RectangularROI(coords=[1.0, 2.0, 3.0, 4.0], indices=False, title="Test")

        # Flip horizontally around x=0
        # Corner at x=1 -> x=-1
        # Corner at x=4 -> x=-4
        # New bounding box: x_min=-4, x_max=-1, so x0=-4, dx=3
        transformer.fliph(roi, cx=0.0)

        expected_coords = np.array([-4.0, 2.0, 3.0, 4.0])
        assert np.allclose(roi.coords, expected_coords)

    def test_transform_rectangular_roi_flipv(self) -> None:
        """
        Test vertical flip transformation of RectangularROI.
        """
        # Create a rectangle with y0=2, dy=4, so corners at y=2 and y=6
        roi = RectangularROI(coords=[1.0, 2.0, 3.0, 4.0], indices=False, title="Test")

        # Flip vertically around y=0
        # Corner at y=2 -> y=-2
        # Corner at y=6 -> y=-6
        # New bounding box: y_min=-6, y_max=-2, so y0=-6, dy=4
        transformer.flipv(roi, cy=0.0)

        expected_coords = np.array([1.0, -6.0, 3.0, 4.0])
        assert np.allclose(roi.coords, expected_coords)

    def test_transform_rectangular_roi_transpose(self) -> None:
        """
        Test transpose transformation of RectangularROI.
        """
        roi = RectangularROI(coords=[1.0, 2.0, 3.0, 4.0], indices=False, title="Test")

        # Transpose (swap x and y)
        transformer.transpose(roi)

        expected_coords = np.array([2.0, 1.0, 4.0, 3.0])
        assert np.allclose(roi.coords, expected_coords)

    def test_transform_rectangular_roi_scale(self) -> None:
        """
        Test scale transformation of RectangularROI.
        """
        roi = RectangularROI(coords=[2.0, 3.0, 4.0, 6.0], indices=False, title="Test")

        # Scale by 2x in x, 3x in y around origin
        transformer.scale(roi, 2.0, 3.0, center=(0, 0))

        expected_coords = np.array([4.0, 9.0, 8.0, 18.0])
        assert np.allclose(roi.coords, expected_coords)

    def test_transform_circular_roi_rotation(self) -> None:
        """
        Test rotation transformation of CircularROI.
        """
        # Create a circular ROI (xc, yc, r)
        roi = CircularROI(coords=[3.0, 4.0, 5.0], indices=False, title="Test")

        # Rotate 90 degrees around origin
        transformer.rotate(roi, np.pi / 2, center=(0, 0))

        # Expected: center rotated, radius unchanged
        expected_coords = np.array([-4.0, 3.0, 5.0])
        assert np.allclose(roi.coords, expected_coords)

    def test_transform_circular_roi_translation(self) -> None:
        """
        Test translation transformation of CircularROI.
        """
        roi = CircularROI(coords=[1.0, 2.0, 3.0], indices=False, title="Test")

        # Translate by (10, 20)
        transformer.translate(roi, 10.0, 20.0)

        expected_coords = np.array([11.0, 22.0, 3.0])
        assert np.allclose(roi.coords, expected_coords)

    def test_transform_circular_roi_scale(self) -> None:
        """
        Test scale transformation of CircularROI.
        """
        roi = CircularROI(coords=[2.0, 3.0, 5.0], indices=False, title="Test")

        # Scale by 2x in x, 3x in y around origin
        transformer.scale(roi, 2.0, 3.0, center=(0, 0))

        # Expected: center scaled, radius unchanged
        expected_coords = np.array([4.0, 9.0, 5.0])
        assert np.allclose(roi.coords, expected_coords)

    def test_transform_polygonal_roi_rotation(self) -> None:
        """
        Test rotation transformation of PolygonalROI.
        """
        # Create a triangular ROI (x1, y1, x2, y2, x3, y3)
        roi = PolygonalROI(
            coords=[0.0, 0.0, 1.0, 0.0, 0.0, 1.0], indices=False, title="Test"
        )

        # Rotate 90 degrees around origin
        transformer.rotate(roi, np.pi / 2, center=(0, 0))

        # Expected: all points rotated
        expected_coords = np.array([0.0, 0.0, 0.0, 1.0, -1.0, 0.0])
        assert np.allclose(roi.coords, expected_coords, atol=1e-10)

    def test_transform_polygonal_roi_translation(self) -> None:
        """
        Test translation transformation of PolygonalROI.
        """
        roi = PolygonalROI(
            coords=[0.0, 0.0, 1.0, 0.0, 0.0, 1.0], indices=False, title="Test"
        )

        # Translate by (5, 10)
        transformer.translate(roi, 5.0, 10.0)

        expected_coords = np.array([5.0, 10.0, 6.0, 10.0, 5.0, 11.0])
        assert np.allclose(roi.coords, expected_coords)

    def test_transform_polygonal_roi_transpose(self) -> None:
        """
        Test transpose transformation of PolygonalROI.
        """
        roi = PolygonalROI(
            coords=[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], indices=False, title="Test"
        )

        # Transpose (swap x and y for all points)
        transformer.transpose(roi)

        expected_coords = np.array([2.0, 1.0, 4.0, 3.0, 6.0, 5.0])
        assert np.allclose(roi.coords, expected_coords)

    def test_unsupported_roi_type(self) -> None:
        """
        Test error handling for unsupported ROI types.
        """

        # Create a mock ROI class
        class UnsupportedROI:
            """Mock unsupported ROI class."""

            def __init__(self):
                self.coords = np.array([1.0, 2.0])

        unsupported_roi = UnsupportedROI()

        with pytest.raises(ValueError, match="Unsupported ROI type"):
            transformer.transform_single_roi(unsupported_roi, "rotate", angle=0)

    def test_roi_title_preserved(self) -> None:
        """
        Test that ROI title is preserved after transformation.
        """
        roi = RectangularROI(coords=[1.0, 2.0, 3.0, 4.0], indices=False, title="MyROI")

        transformer.translate(roi, 10.0, 20.0)

        assert roi.title == "MyROI"

    def test_roi_inverse_preserved(self) -> None:
        """
        Test that ROI inverse flag is preserved after transformation.
        """
        roi = CircularROI(
            coords=[1.0, 2.0, 3.0], indices=False, title="Test", inverse=True
        )

        transformer.rotate(roi, np.pi / 4, center=(0, 0))

        assert roi.inverse is True