Skip to content

Commit e84c48f

Browse files
committed
added new unittests
1 parent d4e1b27 commit e84c48f

7 files changed

Lines changed: 502 additions & 29 deletions

File tree

pyproject.toml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,13 +215,21 @@ convention = "google"
215215
# Flag errors (`C901`) whenever the complexity level exceeds 5.
216216
max-complexity = 20
217217

218+
[tool.pytest.ini_options]
219+
filterwarnings = [
220+
"ignore::DeprecationWarning",
221+
"ignore::FutureWarning",
222+
]
223+
218224
[tool.coverage.report]
219225
exclude_lines = [
220226
"pragma: no cover",
221227
"if __name__ == .__main__.:",
222228
"def __repr__",
223229
"except",
224-
"warnings.warn",
230+
"import warnings",
231+
"warnings\\.warn",
232+
"warnings\\.filter",
225233
"print\\(f?\"\\[!\\].*",
226234
"def __str__(self):",
227235
"def __hash__(self) -> int:",

unit_tests/test_nii.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66

77
import operator
88
import random
9-
import sys
109
import unittest
11-
from pathlib import Path
1210

1311
import nibabel as nib
1412
import numpy as np
@@ -46,8 +44,6 @@ class TestNII_MathOperators(unittest.TestCase):
4644
def make_nii(shape=(8, 9, 10), seed=0, dtype=float):
4745
rng = np.random.default_rng(seed)
4846
arr = rng.normal(size=shape) if dtype is float else rng.integers(0, 8, size=shape, dtype=dtype)
49-
import nibabel as nib
50-
5147
nii = NII((arr, np.eye(4), nib.nifti1.Nifti1Header()))
5248
return nii
5349

unit_tests/test_nii_extended.py

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,10 +384,86 @@ def test_rescale_round_trip_shape(self):
384384
arr[3:7, 3:9, 3:11] = 1
385385
nii = _make_nii(arr, zoom=(1.0, 1.0, 1.0))
386386
nii2 = nii.rescale((2.0, 2.0, 2.0)).rescale((1.0, 1.0, 1.0))
387-
# shape should approximately return to original after double rescale
388387
for orig_s, new_s in zip(nii.shape, nii2.shape):
389388
self.assertAlmostEqual(orig_s, new_s, delta=2)
390389

391390

391+
class Test_NII_MorphologyStd(unittest.TestCase):
392+
"""Tests for standard (non-Euclidean) NII.erode_msk and NII.dilate_msk."""
393+
394+
@staticmethod
395+
def _make_cube_seg(cube_size=8, shape=(20, 20, 20)):
396+
arr = np.zeros(shape, dtype=np.uint8)
397+
c = shape[0] // 2
398+
h = cube_size // 2
399+
arr[c - h : c + h, c - h : c + h, c - h : c + h] = 1
400+
return _make_nii(arr)
401+
402+
def test_erode_reduces_volume(self):
403+
nii = self._make_cube_seg()
404+
eroded = nii.erode_msk(n_pixel=1)
405+
self.assertLess(int((eroded.get_array() > 0).sum()), int((nii.get_array() > 0).sum()))
406+
407+
def test_dilate_increases_volume(self):
408+
nii = self._make_cube_seg()
409+
dilated = nii.dilate_msk(n_pixel=1)
410+
self.assertGreater(int((dilated.get_array() > 0).sum()), int((nii.get_array() > 0).sum()))
411+
412+
def test_erode_inplace(self):
413+
nii = self._make_cube_seg()
414+
vol_before = int((nii.get_array() > 0).sum())
415+
nii.erode_msk_(n_pixel=1)
416+
self.assertLess(int((nii.get_array() > 0).sum()), vol_before)
417+
418+
def test_dilate_inplace(self):
419+
nii = self._make_cube_seg()
420+
vol_before = int((nii.get_array() > 0).sum())
421+
nii.dilate_msk_(n_pixel=1)
422+
self.assertGreater(int((nii.get_array() > 0).sum()), vol_before)
423+
424+
425+
class Test_NII_FillHoles(unittest.TestCase):
426+
"""Tests for NII.fill_holes and NII.fill_holes_."""
427+
428+
def test_hollow_cube_filled(self):
429+
arr = np.zeros((15, 15, 15), dtype=np.uint8)
430+
arr[2:13, 2:13, 2:13] = 1
431+
arr[5:10, 5:10, 5:10] = 0
432+
nii = _make_nii(arr)
433+
filled = nii.fill_holes()
434+
self.assertEqual(filled.get_array()[7, 7, 7], 1)
435+
436+
def test_no_holes_volume_unchanged(self):
437+
arr = np.zeros((10, 10, 10), dtype=np.uint8)
438+
arr[2:8, 2:8, 2:8] = 1
439+
nii = _make_nii(arr)
440+
vol_before = int((nii.get_array() > 0).sum())
441+
filled = nii.fill_holes()
442+
self.assertGreaterEqual(int((filled.get_array() > 0).sum()), vol_before)
443+
444+
def test_fill_holes_inplace(self):
445+
arr = np.zeros((12, 12, 12), dtype=np.uint8)
446+
arr[1:11, 1:11, 1:11] = 1
447+
arr[4:8, 4:8, 4:8] = 0
448+
nii = _make_nii(arr)
449+
nii.fill_holes_()
450+
self.assertEqual(nii.get_array()[6, 6, 6], 1)
451+
452+
453+
class Test_NII_GetSegArray(unittest.TestCase):
454+
"""Tests for NII.get_seg_array, including the warning path for non-seg NIIs."""
455+
456+
def test_returns_correct_array_when_seg(self):
457+
arr = np.array([[[0, 1], [2, 3]]], dtype=np.int16)
458+
nii = _make_nii(arr, seg=True)
459+
np.testing.assert_array_equal(nii.get_seg_array(), arr)
460+
461+
def test_returns_array_when_not_seg(self):
462+
arr = np.ones((4, 4, 4), dtype=np.float32)
463+
nii = _make_nii(arr, seg=False)
464+
result = nii.get_seg_array()
465+
self.assertEqual(result.shape, arr.shape)
466+
467+
392468
if __name__ == "__main__":
393469
unittest.main()

unit_tests/test_nii_io.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
"""Round-trip I/O tests for NII.load, NII.save, and NII.from_numpy."""
2+
3+
from __future__ import annotations
4+
5+
import tempfile
6+
import unittest
7+
from pathlib import Path
8+
9+
import numpy as np
10+
11+
from TPTBox import NII
12+
13+
14+
def _make_nii(arr: np.ndarray, seg: bool = True, zoom=(1.0, 1.0, 1.0)) -> NII:
15+
affine = np.diag([*zoom, 1.0])
16+
return NII.from_numpy(arr, affine=affine, seg=seg)
17+
18+
19+
class Test_NII_IO(unittest.TestCase):
20+
"""Verify that NII survives a save→load round-trip with consistent shape and metadata."""
21+
22+
def test_save_load_roundtrip_shape(self):
23+
arr = np.zeros((10, 12, 14), dtype=np.uint8)
24+
arr[3:7, 3:9, 3:11] = 1
25+
nii = _make_nii(arr, zoom=(1.0, 2.0, 3.0))
26+
with tempfile.NamedTemporaryFile(suffix=".nii.gz", delete=False) as f:
27+
path = Path(f.name)
28+
try:
29+
nii.save(path, verbose=False)
30+
loaded = NII.load(path, seg=True)
31+
self.assertEqual(loaded.shape, nii.shape)
32+
finally:
33+
path.unlink(missing_ok=True)
34+
35+
def test_save_load_roundtrip_data(self):
36+
arr = np.arange(27, dtype=np.uint8).reshape(3, 3, 3)
37+
nii = _make_nii(arr, seg=True)
38+
with tempfile.NamedTemporaryFile(suffix=".nii.gz", delete=False) as f:
39+
path = Path(f.name)
40+
try:
41+
nii.save(path, verbose=False)
42+
loaded = NII.load(path, seg=True)
43+
np.testing.assert_array_equal(loaded.get_array(), arr)
44+
finally:
45+
path.unlink(missing_ok=True)
46+
47+
def test_save_load_preserves_zoom(self):
48+
arr = np.zeros((5, 6, 7), dtype=np.uint8)
49+
nii = _make_nii(arr, zoom=(1.5, 2.5, 3.5))
50+
with tempfile.NamedTemporaryFile(suffix=".nii.gz", delete=False) as f:
51+
path = Path(f.name)
52+
try:
53+
nii.save(path, verbose=False)
54+
loaded = NII.load(path, seg=True)
55+
for orig, restored in zip(nii.zoom, loaded.zoom):
56+
self.assertAlmostEqual(orig, restored, places=4)
57+
finally:
58+
path.unlink(missing_ok=True)
59+
60+
def test_save_load_seg_flag(self):
61+
arr = np.zeros((4, 4, 4), dtype=np.uint8)
62+
arr[1:3, 1:3, 1:3] = 1
63+
nii = _make_nii(arr, seg=True)
64+
with tempfile.NamedTemporaryFile(suffix=".nii.gz", delete=False) as f:
65+
path = Path(f.name)
66+
try:
67+
nii.save(path, verbose=False)
68+
loaded = NII.load(path, seg=True)
69+
self.assertTrue(loaded.seg)
70+
finally:
71+
path.unlink(missing_ok=True)
72+
73+
74+
class Test_NII_FromNumpy(unittest.TestCase):
75+
"""Tests for NII.from_numpy factory method."""
76+
77+
def test_shape(self):
78+
arr = np.zeros((8, 9, 10), dtype=np.uint8)
79+
nii = NII.from_numpy(arr, affine=np.eye(4), seg=True)
80+
self.assertEqual(nii.shape, (8, 9, 10))
81+
82+
def test_seg_flag_true(self):
83+
arr = np.zeros((4, 4, 4), dtype=np.uint8)
84+
self.assertTrue(NII.from_numpy(arr, affine=np.eye(4), seg=True).seg)
85+
86+
def test_seg_flag_false(self):
87+
arr = np.zeros((4, 4, 4), dtype=np.uint8)
88+
self.assertFalse(NII.from_numpy(arr, affine=np.eye(4), seg=False).seg)
89+
90+
def test_affine_zoom_extracted(self):
91+
arr = np.zeros((5, 5, 5), dtype=np.uint8)
92+
affine = np.diag([2.0, 3.0, 4.0, 1.0])
93+
nii = NII.from_numpy(arr, affine=affine, seg=True)
94+
self.assertAlmostEqual(nii.zoom[0], 2.0)
95+
self.assertAlmostEqual(nii.zoom[1], 3.0)
96+
self.assertAlmostEqual(nii.zoom[2], 4.0)
97+
98+
def test_data_round_trip(self):
99+
arr = np.arange(24, dtype=np.int16).reshape(2, 3, 4)
100+
nii = NII.from_numpy(arr, affine=np.eye(4), seg=False)
101+
np.testing.assert_array_equal(nii.get_array(), arr)
102+
103+
104+
if __name__ == "__main__":
105+
unittest.main()

unit_tests/test_nputils.py

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,12 @@
1-
# Call 'python -m unittest' on this folder
2-
# coverage run -m unittest
3-
# coverage report
4-
# coverage html
51
from __future__ import annotations
62

7-
import sys
8-
from pathlib import Path
3+
import random
4+
import unittest
95

10-
file = Path(__file__).resolve()
11-
sys.path.append(str(file.parents[2]))
12-
import random # noqa: E402
13-
import unittest # noqa: E402
6+
import numpy as np
147

15-
import numpy as np # noqa: E402
16-
17-
from TPTBox.core import np_utils # noqa: E402
18-
from TPTBox.tests.test_utils import get_nii, repeats # noqa: E402
8+
from TPTBox.core import np_utils
9+
from TPTBox.tests.test_utils import get_nii, repeats
1910

2011

2112
def make_test_array_repeating(shape=(4, 4), labels=(0, 1, 2, 3)) -> np.ndarray:

0 commit comments

Comments
 (0)