Coverage for nilearn/plotting/tests/test_img_plotting/test_img_plotting.py: 0%

127 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-16 12:32 +0200

1"""Tests common to multiple image plotting functions.""" 

2 

3from __future__ import annotations 

4 

5import matplotlib.pyplot as plt 

6import numpy as np 

7import pytest 

8from nibabel import Nifti1Image 

9 

10from nilearn.conftest import _affine_mni 

11from nilearn.datasets import load_mni152_template 

12from nilearn.image import get_data, reorder_img 

13from nilearn.plotting import ( 

14 plot_anat, 

15 plot_carpet, 

16 plot_epi, 

17 plot_glass_brain, 

18 plot_img, 

19 plot_prob_atlas, 

20 plot_roi, 

21 plot_stat_map, 

22) 

23from nilearn.plotting.img_plotting import MNI152TEMPLATE 

24 

25ALL_PLOTTING_FUNCS = { 

26 plot_img, 

27 plot_anat, 

28 plot_stat_map, 

29 plot_roi, 

30 plot_epi, 

31 plot_glass_brain, 

32 plot_carpet, 

33 plot_prob_atlas, 

34} 

35 

36 

37PLOTTING_FUNCS_4D = {plot_prob_atlas, plot_carpet} 

38 

39 

40PLOTTING_FUNCS_3D = ALL_PLOTTING_FUNCS.difference(PLOTTING_FUNCS_4D) 

41 

42 

43def _add_nans_to_img(img, affine_mni=None): 

44 """Add nans in test image data.""" 

45 if affine_mni is None: 

46 affine_mni = _affine_mni() 

47 data = get_data(img) 

48 data[6, 5, 1] = np.nan 

49 data[1, 5, 2] = np.nan 

50 data[1, 3, 2] = np.nan 

51 data[6, 5, 2] = np.inf 

52 return Nifti1Image(data, affine_mni) 

53 

54 

55def test_mni152template_is_reordered(): 

56 """See issue #2550.""" 

57 reordered_mni = reorder_img( 

58 load_mni152_template(resolution=2), copy_header=True 

59 ) 

60 assert np.allclose(get_data(reordered_mni), get_data(MNI152TEMPLATE)) 

61 assert np.allclose(reordered_mni.affine, MNI152TEMPLATE.affine) 

62 assert np.allclose(reordered_mni.shape, MNI152TEMPLATE.shape) 

63 

64 

65@pytest.mark.parametrize( 

66 "plot_func", 

67 { 

68 plot_img, 

69 plot_anat, 

70 plot_stat_map, 

71 plot_roi, 

72 plot_glass_brain, 

73 plot_prob_atlas, 

74 }, 

75) 

76def test_plot_functions_invalid_threshold(plot_func, img_3d_mni, tmp_path): 

77 """Test plot functions for negative threshold value.""" 

78 filename = tmp_path / "temp.png" 

79 

80 with pytest.raises( 

81 ValueError, match="Threshold should be a non-negative number!" 

82 ): 

83 plot_func(img_3d_mni, output_file=filename, threshold=-1) 

84 plt.close() 

85 

86 

87@pytest.mark.parametrize( 

88 "plot_func", PLOTTING_FUNCS_3D.difference({plot_glass_brain}) 

89) 

90@pytest.mark.parametrize("cut_coords", [None, 5, (5, 4, 3)]) 

91def test_plot_functions_mosaic_mode(plot_func, cut_coords, img_3d_mni): 

92 """Smoke-test for plotting functions in mosaic mode.""" 

93 plot_func( 

94 img_3d_mni, 

95 display_mode="mosaic", 

96 title="mosaic mode", 

97 cut_coords=cut_coords, 

98 ) 

99 plt.close() 

100 

101 

102@pytest.mark.parametrize("plot_func", [plot_stat_map, plot_glass_brain]) 

103def test_plot_threshold_for_uint8(affine_eye, plot_func): 

104 """Mask was applied in [-threshold, threshold] which is problematic \ 

105 for uint8 data. 

106 

107 See https://github.com/nilearn/nilearn/issues/611 for more details. 

108 """ 

109 data = 10 * np.ones((10, 10, 10), dtype="uint8") 

110 # Having a zero minimum value is important to reproduce 

111 # https://github.com/nilearn/nilearn/issues/762 

112 if plot_func is plot_stat_map: 

113 data[0, 0, 0] = 0 

114 else: 

115 data[0, 0] = 0 

116 img = Nifti1Image(data, affine_eye) 

117 threshold = 5 

118 kwargs = {"threshold": threshold, "display_mode": "z"} 

119 if plot_func is plot_stat_map: 

120 kwargs["bg_img"] = None 

121 kwargs["cut_coords"] = [0] 

122 display = plot_func(img, colorbar=True, **kwargs) 

123 # Next two lines retrieve the numpy array from the plot 

124 ax = next(iter(display.axes.values())).ax 

125 plotted_array = ax.images[0].get_array() 

126 # Make sure that there is one value masked 

127 assert plotted_array.mask.sum() == 1 

128 # Make sure that the value masked is in the corner. Note that the 

129 # axis orientation seem to be flipped, hence (0, 0) -> (-1, 0) 

130 assert plotted_array.mask[-1, 0] 

131 # Save execution time and memory 

132 plt.close() 

133 

134 

135@pytest.fixture 

136def expected_error_message(display_mode, cut_coords): 

137 """Return the expected error message depending on display_mode \ 

138 and cut_coords. Used in test_invalid_cut_coords_with_display_mode. 

139 """ 

140 if display_mode == "ortho" or ( 

141 display_mode == "tiled" and cut_coords == 2 

142 ): 

143 return ( 

144 f"The input given for display_mode='{display_mode}' needs to " 

145 "be a list of 3d world coordinates." 

146 ) 

147 return "The number cut_coords passed does not match the display_mode" 

148 

149 

150@pytest.mark.parametrize("plot_func", PLOTTING_FUNCS_3D) 

151@pytest.mark.parametrize( 

152 "display_mode,cut_coords", 

153 [("ortho", 2), ("tiled", 2), ("tiled", (2, 2)), ("mosaic", (2, 2))], 

154) 

155def test_invalid_cut_coords_with_display_mode( 

156 plot_func, 

157 display_mode, 

158 cut_coords, 

159 img_3d_mni, 

160 expected_error_message, 

161): 

162 """Tests for invalid combinations of cut_coords and display_mode.""" 

163 if plot_func is plot_glass_brain and display_mode != "ortho": 

164 return 

165 with pytest.raises(ValueError, match=expected_error_message): 

166 plot_func( 

167 img_3d_mni, 

168 display_mode=display_mode, 

169 cut_coords=cut_coords, 

170 ) 

171 

172 

173@pytest.mark.parametrize("plot_func", PLOTTING_FUNCS_3D) 

174def test_plot_with_nans(plot_func, img_3d_mni): 

175 """Smoke test for plotting functions with nans in data image.""" 

176 plot_func(_add_nans_to_img(img_3d_mni)) 

177 

178 

179@pytest.mark.parametrize( 

180 "plot_func", [plot_roi, plot_stat_map, plot_glass_brain] 

181) 

182@pytest.mark.parametrize("cmap", ["Paired", "Set1", "Set2", "Set3", "viridis"]) 

183def test_plotting_functions_with_cmaps(plot_func, cmap, img_3d_mni): 

184 """Some test for plotting functions with different cmaps.""" 

185 plot_func(img_3d_mni, cmap=cmap, colorbar=True) 

186 plt.close() 

187 

188 

189@pytest.mark.parametrize("plot_func", [plot_anat, plot_roi, plot_stat_map]) 

190def test_plotting_functions_with_nans_in_bg_img(plot_func, img_3d_mni): 

191 """Smoke test for plotting functions with nans in background image.""" 

192 bg_img = _add_nans_to_img(img_3d_mni) 

193 if plot_func is plot_anat: 

194 plot_func(bg_img) 

195 else: 

196 plot_func(img_3d_mni, bg_img=bg_img) 

197 plt.close() 

198 

199 

200@pytest.mark.parametrize("plot_func", [plot_stat_map, plot_anat, plot_img]) 

201def test_plotting_functions_with_display_mode_tiled(plot_func, img_3d_mni): 

202 """Smoke test for plotting functions with tiled display mode.""" 

203 if plot_func is plot_anat: 

204 plot_func(display_mode="tiled") 

205 else: 

206 plot_func(img_3d_mni, display_mode="tiled") 

207 plt.close() 

208 

209 

210functions = [plot_stat_map, plot_img] 

211EXPECTED = [(i, ["-10", "-5", "0", "5", "10"]) for i in [0, 0.1, 0.9, 1]] 

212EXPECTED += [ 

213 (i, ["-10", f"-{i}", "0", f"{i}", "10"]) for i in [1.3, 2.5, 3, 4.9, 7.5] 

214] 

215EXPECTED += [(i, [f"-{i}", "-5", "0", "5", f"{i}"]) for i in [7.6, 8, 9.9]] 

216 

217 

218@pytest.mark.parametrize( 

219 "plot_func, threshold, expected_ticks", 

220 [(f, e[0], e[1]) for e in EXPECTED for f in functions], 

221) 

222def test_plot_symmetric_colorbar_threshold( 

223 tmp_path, plot_func, threshold, expected_ticks 

224): 

225 img_data = np.zeros((10, 10, 10)) 

226 img_data[4:6, 2:4, 4:6] = -10 

227 img_data[5:7, 3:7, 3:6] = 10 

228 img = Nifti1Image(img_data, affine=np.eye(4)) 

229 display = plot_func(img, threshold=threshold, colorbar=True) 

230 plt.savefig(tmp_path / "test.png") 

231 assert [ 

232 tick.get_text() for tick in display._cbar.ax.get_yticklabels() 

233 ] == expected_ticks 

234 plt.close() 

235 

236 

237functions = [plot_stat_map] 

238EXPECTED2: list[tuple[float | int, list[str]]] = [ 

239 (0, ["0", "2.5", "5", "7.5", "10"]) 

240] 

241EXPECTED2 += [(i, [f"{i}", "2.5", "5", "7.5", "10"]) for i in [0.1, 0.3, 1.2]] 

242EXPECTED2 += [ 

243 (i, ["0", f"{i}", "5", "7.5", "10"]) for i in [1.3, 1.9, 2.5, 3, 3.7] 

244] 

245EXPECTED2 += [(i, ["0", "2.5", f"{i}", "7.5", "10"]) for i in [3.8, 4, 5, 6.2]] 

246EXPECTED2 += [(i, ["0", "2.5", "5", f"{i}", "10"]) for i in [6.3, 7.5, 8, 8.7]] 

247EXPECTED2 += [(i, ["0", "2.5", "5", "7.5", f"{i}"]) for i in [8.8, 9, 9.9]] 

248 

249 

250@pytest.mark.parametrize( 

251 "plot_func, threshold, expected_ticks", 

252 [(f, e[0], e[1]) for e in EXPECTED2 for f in functions], 

253) 

254def test_plot_asymmetric_colorbar_threshold( 

255 tmp_path, plot_func, threshold, expected_ticks 

256): 

257 img_data = np.zeros((10, 10, 10)) 

258 img_data[4:6, 2:4, 4:6] = 5 

259 img_data[5:7, 3:7, 3:6] = 10 

260 img = Nifti1Image(img_data, affine=np.eye(4)) 

261 display = plot_func(img, threshold=threshold, colorbar=True) 

262 plt.savefig(tmp_path / "test.png") 

263 assert [ 

264 tick.get_text() for tick in display._cbar.ax.get_yticklabels() 

265 ] == expected_ticks 

266 plt.close() 

267 

268 

269@pytest.mark.parametrize("plot_func", [plot_stat_map, plot_img]) 

270@pytest.mark.parametrize("vmax", [None, 0]) 

271def test_img_plotting_vmax_equal_to_zero(plot_func, vmax): 

272 """Make sure image plotting works if the maximum value is zero. 

273 

274 Regression test for: https://github.com/nilearn/nilearn/issues/4203 

275 """ 

276 img_data = np.zeros((10, 10, 10)) 

277 img_data[4:6, 2:4, 4:6] = -5 

278 img = Nifti1Image(img_data, affine=np.eye(4)) 

279 plot_func(img, colorbar=True, vmax=vmax)