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

1import numpy as np 

2import pytest 

3from nibabel import Nifti1Image 

4 

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) 

11 

12 

13@pytest.fixture 

14def shape(): 

15 return (9, 10, 11) 

16 

17 

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] 

25 

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])) 

29 

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])) 

33 

34 

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] 

42 

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])) 

46 

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])) 

50 

51 

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] 

59 

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])) 

63 

64 

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 

74 

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]])) 

85 

86 

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) 

110 

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 

118 

119 

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) 

125 

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 

131 

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 

145 

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 

156 

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 

162 

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) 

170 

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() 

177 

178 

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) 

188 

189 cluster_table, label_maps = get_clusters_table( 

190 stat_img, 

191 4, 

192 0, 

193 return_label_maps=True, 

194 ) 

195 

196 # Get cluster ids from clusters table 

197 cluster_ids = cluster_table["Cluster ID"].to_numpy() 

198 

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)] 

202 

203 assert np.array_equal(cluster_ids, lb_cluster_ids) 

204 

205 

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 

225 

226 stat_img = Nifti1Image(data, affine_eye) 

227 data_orig = get_data(stat_img).copy() 

228 

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