Coverage for nilearn/reporting/tests/test_reporting.py: 0%
106 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-16 12:32 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-16 12:32 +0200
1import numpy as np
2import pytest
3from nibabel import Nifti1Image
5from nilearn.image import get_data
6from nilearn.reporting.get_clusters_table import (
7 _cluster_nearest_neighbor,
8 _local_max,
9 get_clusters_table,
10)
13@pytest.fixture
14def shape():
15 return (9, 10, 11)
18def test_local_max_two_maxima(shape, affine_eye):
19 """Basic test of nilearn.reporting._get_clusters_table._local_max()."""
20 # Two maxima (one global, one local), 10 voxels apart.
21 data = np.zeros(shape)
22 data[4, 5, :] = [4, 3, 2, 1, 1, 1, 1, 1, 2, 3, 4]
23 data[5, 5, :] = [5, 4, 3, 2, 1, 1, 1, 2, 3, 4, 6]
24 data[6, 5, :] = [4, 3, 2, 1, 1, 1, 1, 1, 2, 3, 4]
26 ijk, vals = _local_max(data, affine_eye, min_distance=9)
27 assert np.array_equal(ijk, np.array([[5.0, 5.0, 10.0], [5.0, 5.0, 0.0]]))
28 assert np.array_equal(vals, np.array([6, 5]))
30 ijk, vals = _local_max(data, affine_eye, min_distance=11)
31 assert np.array_equal(ijk, np.array([[5.0, 5.0, 10.0]]))
32 assert np.array_equal(vals, np.array([6]))
35def test_local_max_two_global_maxima(shape, affine_eye):
36 """Basic test of nilearn.reporting._get_clusters_table._local_max()."""
37 # Two global (equal) maxima, 10 voxels apart.
38 data = np.zeros(shape)
39 data[4, 5, :] = [4, 3, 2, 1, 1, 1, 1, 1, 2, 3, 4]
40 data[5, 5, :] = [5, 4, 3, 2, 1, 1, 1, 2, 3, 4, 5]
41 data[6, 5, :] = [4, 3, 2, 1, 1, 1, 1, 1, 2, 3, 4]
43 ijk, vals = _local_max(data, affine_eye, min_distance=9)
44 assert np.array_equal(ijk, np.array([[5.0, 5.0, 0.0], [5.0, 5.0, 10.0]]))
45 assert np.array_equal(vals, np.array([5, 5]))
47 ijk, vals = _local_max(data, affine_eye, min_distance=11)
48 assert np.array_equal(ijk, np.array([[5.0, 5.0, 0.0]]))
49 assert np.array_equal(vals, np.array([5]))
52def test_local_max_donut(shape, affine_eye):
53 """Basic test of nilearn.reporting._get_clusters_table._local_max()."""
54 # A donut.
55 data = np.zeros(shape)
56 data[4, 5, :] = [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0]
57 data[5, 5, :] = [0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0]
58 data[6, 5, :] = [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0]
60 ijk, vals = _local_max(data, affine_eye, min_distance=9)
61 assert np.array_equal(ijk, np.array([[4.0, 5.0, 5.0]]))
62 assert np.array_equal(vals, np.array([1]))
65def test_cluster_nearest_neighbor(shape):
66 """Check that _cluster_nearest_neighbor preserves within-cluster voxels, \
67 projects voxels to the correct cluster, \
68 and handles singleton clusters.
69 """
70 labeled = np.zeros(shape)
71 # cluster 1 is half the volume, cluster 2 is a single voxel
72 labeled[:, 5:, :] = 1
73 labeled[4, 2, 6] = 2
75 labels_index = np.array([1, 1, 2])
76 ijk = np.array(
77 [
78 [4, 7, 5], # inside cluster 1
79 [4, 2, 5], # outside, close to 2
80 [4, 3, 6], # outside, close to 2
81 ]
82 )
83 nbrs = _cluster_nearest_neighbor(ijk, labels_index, labeled)
84 assert np.array_equal(nbrs, np.array([[4, 7, 5], [4, 5, 5], [4, 2, 6]]))
87@pytest.mark.parametrize(
88 "stat_threshold, cluster_threshold, two_sided, expected_n_cluster",
89 [
90 (4, 0, False, 1), # test one cluster extracted
91 (6, 0, False, 0), # test empty table on high stat threshold
92 (4, 9, False, 0), # test empty table on high cluster threshold
93 (4, 0, True, 2), # test two clusters with different signs extracted
94 (6, 0, True, 0), # test empty table on high stat threshold
95 (4, 9, True, 0), # test empty table on high cluster threshold
96 ],
97)
98def test_get_clusters_table(
99 shape,
100 affine_eye,
101 stat_threshold,
102 cluster_threshold,
103 two_sided,
104 expected_n_cluster,
105):
106 data = np.zeros(shape)
107 data[2:4, 5:7, 6:8] = 5.0
108 data[4:6, 7:9, 8:10] = -5.0
109 stat_img = Nifti1Image(data, affine_eye)
111 clusters_table = get_clusters_table(
112 stat_img,
113 stat_threshold=stat_threshold,
114 cluster_threshold=cluster_threshold,
115 two_sided=two_sided,
116 )
117 assert len(clusters_table) == expected_n_cluster
120def test_get_clusters_table_more(shape, affine_eye, tmp_path):
121 data = np.zeros(shape)
122 data[2:4, 5:7, 6:8] = 5.0
123 data[4:6, 7:9, 8:10] = -5.0
124 stat_img = Nifti1Image(data, affine_eye)
126 # test with filename
127 fname = str(tmp_path / "stat_img.nii.gz")
128 stat_img.to_filename(fname)
129 cluster_table = get_clusters_table(fname, 4, 0, two_sided=True)
130 assert len(cluster_table) == 2
132 # test with returning label maps
133 cluster_table, label_maps = get_clusters_table(
134 stat_img,
135 4,
136 0,
137 two_sided=True,
138 return_label_maps=True,
139 )
140 label_map_positive_data = label_maps[0].get_fdata()
141 label_map_negative_data = label_maps[1].get_fdata()
142 # make sure positive and negative clusters are returned in the label maps
143 assert np.sum(label_map_positive_data[2:4, 5:7, 6:8] != 0) == 8
144 assert np.sum(label_map_negative_data[4:6, 7:9, 8:10] != 0) == 8
146 # test with extra dimension
147 data_extra_dim = data[..., np.newaxis]
148 stat_img_extra_dim = Nifti1Image(data_extra_dim, affine_eye)
149 cluster_table = get_clusters_table(
150 stat_img_extra_dim,
151 4,
152 0,
153 two_sided=True,
154 )
155 assert len(cluster_table) == 2
157 # Test that nans are handled correctly (No numpy axis errors are raised)
158 data[data == 0] = np.nan
159 stat_img_nans = Nifti1Image(data, affine=affine_eye)
160 cluster_table = get_clusters_table(stat_img_nans, 1e-2, 0, two_sided=False)
161 assert len(cluster_table) == 1
163 # Test that subpeaks are handled correctly for len(subpeak_vals) > 1
164 # 1 cluster and two subpeaks, 10 voxels apart.
165 data = np.zeros(shape)
166 data[4, 5, :] = [4, 3, 2, 1, 1, 1, 1, 1, 2, 3, 4]
167 data[5, 5, :] = [5, 4, 3, 2, 1, 1, 1, 2, 3, 4, 6]
168 data[6, 5, :] = [4, 3, 2, 1, 1, 1, 1, 1, 2, 3, 4]
169 stat_img = Nifti1Image(data, affine_eye)
171 cluster_table = get_clusters_table(
172 stat_img, 0, 0, min_distance=9, two_sided=True
173 )
174 assert len(cluster_table) == 2
175 assert 1 in cluster_table["Cluster ID"].to_numpy()
176 assert "1a" in cluster_table["Cluster ID"].to_numpy()
179def test_get_clusters_table_relabel_label_maps(shape, affine_eye):
180 """Check that the cluster's labels in label_maps match \
181 their corresponding cluster IDs in the clusters table.
182 """
183 data = np.zeros(shape)
184 data[2:4, 5:7, 6:8] = 6.0
185 data[5:7, 7:9, 7:9] = 5.5
186 data[0:3, 0:3, 0:3] = 5.0
187 stat_img = Nifti1Image(data, affine_eye)
189 cluster_table, label_maps = get_clusters_table(
190 stat_img,
191 4,
192 0,
193 return_label_maps=True,
194 )
196 # Get cluster ids from clusters table
197 cluster_ids = cluster_table["Cluster ID"].to_numpy()
199 # Find the cluster ids in the label map using the coords from the table.
200 coords = cluster_table[["X", "Y", "Z"]].to_numpy().astype(int)
201 lb_cluster_ids = label_maps[0].get_fdata()[tuple(coords.T)]
203 assert np.array_equal(cluster_ids, lb_cluster_ids)
206@pytest.mark.parametrize(
207 "stat_threshold, cluster_threshold, two_sided, expected_n_cluster",
208 [
209 (4, 10, True, 1), # test one cluster should be removed
210 (4, 7, False, 2), # test no clusters should be removed
211 (4, None, False, 2), # test cluster threshold is None
212 ],
213)
214def test_get_clusters_table_not_modifying_stat_image(
215 shape,
216 affine_eye,
217 stat_threshold,
218 cluster_threshold,
219 two_sided,
220 expected_n_cluster,
221):
222 data = np.zeros(shape)
223 data[2:4, 5:7, 6:8] = 5.0
224 data[0:3, 0:3, 0:3] = 6.0
226 stat_img = Nifti1Image(data, affine_eye)
227 data_orig = get_data(stat_img).copy()
229 clusters_table = get_clusters_table(
230 stat_img,
231 stat_threshold=stat_threshold,
232 cluster_threshold=cluster_threshold,
233 two_sided=two_sided,
234 )
235 assert np.allclose(data_orig, get_data(stat_img))
236 assert len(clusters_table) == expected_n_cluster