Coverage for nilearn/surface/tests/test_surface.py: 0%

603 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-20 10:58 +0200

1# Tests for functions in surf_plotting.py 

2 

3import warnings 

4from pathlib import Path 

5 

6import numpy as np 

7import pytest 

8from nibabel import Nifti1Image, freesurfer, gifti, load, nifti1 

9from numpy.testing import assert_array_almost_equal, assert_array_equal 

10from scipy.spatial import Delaunay 

11from scipy.stats import pearsonr 

12from sklearn.exceptions import EfficiencyWarning 

13 

14from nilearn import datasets, image 

15from nilearn._utils import data_gen 

16from nilearn.image import resampling 

17from nilearn.surface.surface import ( 

18 FileMesh, 

19 InMemoryMesh, 

20 PolyData, 

21 PolyMesh, 

22 SurfaceImage, 

23 _choose_kind, 

24 _data_to_gifti, 

25 _gifti_img_to_mesh, 

26 _interpolation_sampling, 

27 _load_surf_files_gifti_gzip, 

28 _load_uniform_ball_cloud, 

29 _masked_indices, 

30 _mesh_to_gifti, 

31 _nearest_voxel_sampling, 

32 _projection_matrix, 

33 _sample_locations, 

34 _sample_locations_between_surfaces, 

35 _uniform_ball_cloud, 

36 _vertex_outer_normals, 

37 check_mesh_and_data, 

38 check_mesh_is_fsaverage, 

39 check_surf_img, 

40 extract_data, 

41 get_data, 

42 load_surf_data, 

43 load_surf_mesh, 

44 vol_to_surf, 

45) 

46 

47datadir = Path(__file__).resolve().parent / "data" 

48 

49 

50def flat_mesh(x_s: int, y_s: int, z=0) -> InMemoryMesh: 

51 """Create a flat horizontal mesh.""" 

52 x, y = np.mgrid[:x_s, :y_s] 

53 x, y = x.ravel(), y.ravel() 

54 z = np.ones(len(x)) * z 

55 vertices = np.asarray([x, y, z]).T 

56 triangulation = Delaunay(vertices[:, :2]).simplices 

57 return InMemoryMesh(coordinates=vertices, faces=triangulation) 

58 

59 

60def z_const_img(x_s, y_s, z_s): 

61 """Create an image that is constant in z direction.""" 

62 hslice = np.arange(x_s * y_s).reshape((x_s, y_s)) 

63 return np.ones((x_s, y_s, z_s)) * hslice[:, :, np.newaxis] 

64 

65 

66def test_check_mesh(): 

67 mesh = check_mesh_is_fsaverage("fsaverage5") 

68 assert mesh is check_mesh_is_fsaverage(mesh) 

69 with pytest.raises(ValueError): 

70 check_mesh_is_fsaverage("fsaverage2") 

71 mesh.pop("pial_left") 

72 with pytest.raises(ValueError): 

73 check_mesh_is_fsaverage(mesh) 

74 with pytest.raises(TypeError): 

75 check_mesh_is_fsaverage(load_surf_mesh(mesh["pial_right"])) 

76 

77 

78def test_check_mesh_and_data(rng, in_memory_mesh): 

79 data = in_memory_mesh.coordinates[:, 0] 

80 

81 m, d = check_mesh_and_data(in_memory_mesh, data) 

82 assert (m[0] == in_memory_mesh.coordinates).all() 

83 assert (m[1] == in_memory_mesh.faces).all() 

84 assert (d == data).all() 

85 

86 # Generate faces such that max index is larger than 

87 # the length of coordinates array. 

88 wrong_faces = rng.integers(in_memory_mesh.n_vertices + 1, size=(30, 3)) 

89 

90 # Check that check_mesh_and_data raises an error 

91 # with the resulting wrong mesh 

92 with pytest.raises( 

93 ValueError, 

94 match="Mismatch between .* indices of faces .* number of nodes.", 

95 ): 

96 InMemoryMesh(in_memory_mesh.coordinates, wrong_faces) 

97 

98 # Alter the data and check that an error is raised 

99 data = in_memory_mesh.coordinates[::2, 0] 

100 with pytest.raises( 

101 ValueError, match="Mismatch between number of nodes in mesh" 

102 ): 

103 check_mesh_and_data(in_memory_mesh, data) 

104 

105 

106def test_load_surf_data_numpy_gt_1pt23(): 

107 """Test loading fsaverage surface. 

108 

109 Threw an error with numpy >=1.24.x 

110 but only a deprecaton warning with numpy <1.24.x. 

111 

112 Regression test for 

113 https://github.com/nilearn/nilearn/issues/3638 

114 """ 

115 fsaverage = datasets.fetch_surf_fsaverage() 

116 load_surf_data(fsaverage["pial_left"]) 

117 

118 

119def test_load_surf_data_array(): 

120 # test loading and squeezing data from numpy array 

121 data_flat = np.zeros((20,)) 

122 data_squeeze = np.zeros((20, 1, 3)) 

123 assert_array_equal(load_surf_data(data_flat), np.zeros((20,))) 

124 assert_array_equal(load_surf_data(data_squeeze), np.zeros((20, 3))) 

125 

126 

127def test_load_surf_data_from_gifti_file(tmp_path): 

128 filename_gii = tmp_path / "tmp.gii" 

129 darray = gifti.GiftiDataArray( 

130 data=np.zeros((20,)), datatype="NIFTI_TYPE_FLOAT32" 

131 ) 

132 gii = gifti.GiftiImage(darrays=[darray]) 

133 gii.to_filename(filename_gii) 

134 assert_array_equal(load_surf_data(filename_gii), np.zeros((20,))) 

135 

136 

137def test_load_surf_data_from_empty_gifti_file(tmp_path): 

138 filename_gii_empty = tmp_path / "tmp.gii" 

139 gii_empty = gifti.GiftiImage() 

140 gii_empty.to_filename(filename_gii_empty) 

141 with pytest.raises( 

142 ValueError, match="must contain at least one data array" 

143 ): 

144 load_surf_data(filename_gii_empty) 

145 

146 

147def test_load_surf_data_from_nifti_file(tmp_path): 

148 filename_nii = tmp_path / "tmp.nii" 

149 filename_niigz = tmp_path / "tmp.nii.gz" 

150 nii = Nifti1Image(np.zeros((20,)), affine=None) 

151 nii.to_filename(filename_nii) 

152 nii.to_filename(filename_niigz) 

153 assert_array_equal(load_surf_data(filename_nii), np.zeros((20,))) 

154 assert_array_equal(load_surf_data(filename_niigz), np.zeros((20,))) 

155 

156 

157def test_load_surf_data_gii_gz(): 

158 # Test the loader `load_surf_data` with gzipped fsaverage5 files 

159 

160 # surface data 

161 fsaverage = datasets.fetch_surf_fsaverage().sulc_left 

162 gii = _load_surf_files_gifti_gzip(fsaverage) 

163 assert isinstance(gii, gifti.GiftiImage) 

164 

165 data = load_surf_data(fsaverage) 

166 assert isinstance(data, np.ndarray) 

167 

168 # surface mesh 

169 fsaverage = datasets.fetch_surf_fsaverage().pial_left 

170 gii = _load_surf_files_gifti_gzip(fsaverage) 

171 assert isinstance(gii, gifti.GiftiImage) 

172 

173 

174def test_load_surf_data_file_freesurfer(tmp_path): 

175 # test loading of fake data from sulc and thickness files 

176 # using load_surf_data. 

177 # We test load_surf_data by creating fake data with function 

178 # 'write_morph_data' that works only if nibabel 

179 # version is recent with nibabel >= 2.1.0 

180 filename_area = tmp_path / "tmp.area" 

181 data = np.zeros((20,)) 

182 freesurfer.io.write_morph_data(filename_area, data) 

183 assert_array_equal(load_surf_data(filename_area), np.zeros((20,))) 

184 

185 filename_curv = tmp_path / "tmp.curv" 

186 freesurfer.io.write_morph_data(filename_curv, data) 

187 assert_array_equal(load_surf_data(filename_curv), np.zeros((20,))) 

188 

189 filename_sulc = tmp_path / "tmp.sulc" 

190 freesurfer.io.write_morph_data(filename_sulc, data) 

191 assert_array_equal(load_surf_data(filename_sulc), np.zeros((20,))) 

192 

193 filename_thick = tmp_path / "tmp.thickness" 

194 freesurfer.io.write_morph_data(filename_thick, data) 

195 assert_array_equal(load_surf_data(filename_thick), np.zeros((20,))) 

196 

197 # test loading of data from real label and annot files 

198 label_start = np.array([5900, 5899, 5901, 5902, 2638]) 

199 label_end = np.array([8756, 6241, 8757, 1896, 6243]) 

200 label = load_surf_data(datadir / "test.label") 

201 assert_array_equal(label[:5], label_start) 

202 assert_array_equal(label[-5:], label_end) 

203 assert label.shape == (10,) 

204 del label, label_start, label_end 

205 

206 annot_start = np.array([24, 29, 28, 27, 24, 31, 11, 25, 0, 12]) 

207 annot_end = np.array([16, 16, 16, 16, 16, 16, 16, 16, 16, 16]) 

208 annot = load_surf_data(datadir / "test.annot") 

209 assert_array_equal(annot[:10], annot_start) 

210 assert_array_equal(annot[-10:], annot_end) 

211 assert annot.shape == (10242,) 

212 del annot, annot_start, annot_end 

213 

214 

215@pytest.mark.parametrize("suffix", [".vtk", ".obj", ".mnc", ".txt"]) 

216def test_load_surf_data_file_error(tmp_path, suffix): 

217 # test if files with unexpected suffixes raise errors 

218 data = np.zeros((20,)) 

219 filename_wrong = tmp_path / f"tmp{suffix}" 

220 np.savetxt(filename_wrong, data) 

221 with pytest.raises(ValueError, match="input type is not recognized"): 

222 load_surf_data(filename_wrong) 

223 

224 

225def test_load_surf_mesh_list(in_memory_mesh): 

226 # test if correct list is returned 

227 assert hasattr(load_surf_mesh(in_memory_mesh), "coordinates") 

228 assert hasattr(load_surf_mesh(in_memory_mesh), "faces") 

229 assert_array_equal( 

230 load_surf_mesh(in_memory_mesh).coordinates, in_memory_mesh.coordinates 

231 ) 

232 assert_array_equal( 

233 load_surf_mesh(in_memory_mesh).faces, in_memory_mesh.faces 

234 ) 

235 

236 # test if incorrect list, array or dict raises error 

237 with pytest.raises(ValueError, match="it must have two elements"): 

238 load_surf_mesh([]) 

239 with pytest.raises(ValueError, match="it must have two elements"): 

240 load_surf_mesh([in_memory_mesh.coordinates]) 

241 with pytest.raises(ValueError, match="it must have two elements"): 

242 load_surf_mesh( 

243 [ 

244 in_memory_mesh.coordinates, 

245 in_memory_mesh.coordinates, 

246 in_memory_mesh.faces, 

247 ] 

248 ) 

249 with pytest.raises(ValueError, match="input type is not recognized"): 

250 load_surf_mesh(in_memory_mesh.coordinates) 

251 with pytest.raises(ValueError, match="input type is not recognized"): 

252 load_surf_mesh({}) 

253 

254 

255def test_gifti_img_to_mesh(in_memory_mesh): 

256 coord_array = gifti.GiftiDataArray( 

257 data=in_memory_mesh.coordinates, datatype="NIFTI_TYPE_FLOAT32" 

258 ) 

259 coord_array.intent = nifti1.intent_codes["NIFTI_INTENT_POINTSET"] 

260 

261 face_array = gifti.GiftiDataArray( 

262 data=in_memory_mesh.faces, datatype="NIFTI_TYPE_FLOAT32" 

263 ) 

264 face_array.intent = nifti1.intent_codes["NIFTI_INTENT_TRIANGLE"] 

265 

266 gii = gifti.GiftiImage(darrays=[coord_array, face_array]) 

267 coords, faces = _gifti_img_to_mesh(gii) 

268 assert_array_equal(coords, in_memory_mesh.coordinates) 

269 assert_array_equal(faces, in_memory_mesh.faces) 

270 

271 

272def test_load_surf_mesh_file_gii_gz(): 

273 # Test the loader `load_surf_mesh` with gzipped fsaverage5 files 

274 fsaverage = datasets.fetch_surf_fsaverage().pial_left 

275 mesh = load_surf_mesh(fsaverage) 

276 coords = mesh.coordinates 

277 faces = mesh.faces 

278 assert isinstance(coords, np.ndarray) 

279 assert isinstance(faces, np.ndarray) 

280 

281 

282def test_load_surf_mesh_file_gii(tmp_path, in_memory_mesh): 

283 # Test the loader `load_surf_mesh` 

284 # test if correct gii is loaded into correct list 

285 coord_array = gifti.GiftiDataArray( 

286 data=in_memory_mesh.coordinates, 

287 intent=nifti1.intent_codes["NIFTI_INTENT_POINTSET"], 

288 datatype="NIFTI_TYPE_FLOAT32", 

289 ) 

290 face_array = gifti.GiftiDataArray( 

291 data=in_memory_mesh.faces, 

292 intent=nifti1.intent_codes["NIFTI_INTENT_TRIANGLE"], 

293 datatype="NIFTI_TYPE_FLOAT32", 

294 ) 

295 

296 gii = gifti.GiftiImage(darrays=[coord_array, face_array]) 

297 filename_gii_mesh = tmp_path / "tmp.gii" 

298 gii.to_filename(filename_gii_mesh) 

299 

300 assert_array_almost_equal( 

301 load_surf_mesh(filename_gii_mesh).coordinates, 

302 in_memory_mesh.coordinates, 

303 ) 

304 assert_array_almost_equal( 

305 load_surf_mesh(filename_gii_mesh).faces, in_memory_mesh.faces 

306 ) 

307 

308 

309def test_load_surf_mesh_file_gii_error(tmp_path, in_memory_mesh): 

310 # test if incorrect gii raises error 

311 coord_array = gifti.GiftiDataArray( 

312 data=in_memory_mesh.coordinates, 

313 intent=nifti1.intent_codes["NIFTI_INTENT_POINTSET"], 

314 datatype="NIFTI_TYPE_FLOAT32", 

315 ) 

316 face_array = gifti.GiftiDataArray( 

317 data=in_memory_mesh.faces, 

318 intent=nifti1.intent_codes["NIFTI_INTENT_TRIANGLE"], 

319 datatype="NIFTI_TYPE_FLOAT32", 

320 ) 

321 

322 filename_gii_mesh_no_point = tmp_path / "tmp.gii" 

323 gii = gifti.GiftiImage(darrays=[face_array, face_array]) 

324 gii.to_filename(filename_gii_mesh_no_point) 

325 

326 with pytest.raises(ValueError, match="NIFTI_INTENT_POINTSET"): 

327 load_surf_mesh(filename_gii_mesh_no_point) 

328 

329 filename_gii_mesh_no_face = tmp_path / "tmp.gii" 

330 gii = gifti.GiftiImage(darrays=[coord_array, coord_array]) 

331 gii.to_filename(filename_gii_mesh_no_face) 

332 

333 with pytest.raises(ValueError, match="NIFTI_INTENT_TRIANGLE"): 

334 load_surf_mesh(filename_gii_mesh_no_face) 

335 

336 

337@pytest.mark.parametrize( 

338 "suffix", [".pial", ".inflated", ".white", ".orig", "sphere"] 

339) 

340def test_load_surf_mesh_file_freesurfer(suffix, tmp_path, in_memory_mesh): 

341 filename_fs_mesh = tmp_path / f"tmp{suffix}" 

342 freesurfer.write_geometry( 

343 filename_fs_mesh, in_memory_mesh.coordinates, in_memory_mesh.faces 

344 ) 

345 

346 assert hasattr(load_surf_mesh(filename_fs_mesh), "coordinates") 

347 assert hasattr(load_surf_mesh(filename_fs_mesh), "faces") 

348 assert_array_almost_equal( 

349 load_surf_mesh(filename_fs_mesh).coordinates, 

350 in_memory_mesh.coordinates, 

351 ) 

352 assert_array_almost_equal( 

353 load_surf_mesh(filename_fs_mesh).faces, in_memory_mesh.faces 

354 ) 

355 

356 

357@pytest.mark.parametrize("suffix", [".vtk", ".obj", ".mnc", ".txt"]) 

358def test_load_surf_mesh_file_error(suffix, tmp_path, in_memory_mesh): 

359 # test if files with unexpected suffixes raise errors 

360 filename_wrong = tmp_path / f"tmp{suffix}" 

361 freesurfer.write_geometry( 

362 filename_wrong, in_memory_mesh.coordinates, in_memory_mesh.faces 

363 ) 

364 

365 with pytest.raises(ValueError, match="input type is not recognized"): 

366 load_surf_mesh(filename_wrong) 

367 

368 

369def test_load_surf_mesh_file_glob(tmp_path, in_memory_mesh): 

370 fname1 = tmp_path / "tmp1.pial" 

371 freesurfer.write_geometry( 

372 fname1, in_memory_mesh.coordinates, in_memory_mesh.faces 

373 ) 

374 

375 fname2 = tmp_path / "tmp2.pial" 

376 freesurfer.write_geometry( 

377 fname2, in_memory_mesh.coordinates, in_memory_mesh.faces 

378 ) 

379 

380 with pytest.raises(ValueError, match="More than one file matching path"): 

381 load_surf_mesh(tmp_path / "*.pial") 

382 with pytest.raises(ValueError, match="No files matching path"): 

383 load_surf_mesh(tmp_path / "*.unlikelysuffix") 

384 assert hasattr(load_surf_mesh(fname1), "coordinates") 

385 assert hasattr(load_surf_mesh(fname1), "faces") 

386 assert_array_almost_equal( 

387 load_surf_mesh(fname1).coordinates, in_memory_mesh.coordinates 

388 ) 

389 assert_array_almost_equal( 

390 load_surf_mesh(fname1).faces, in_memory_mesh.faces 

391 ) 

392 

393 

394def test_load_surf_data_file_glob(tmp_path): 

395 data2D = np.ones((20, 3)) 

396 fnames = [] 

397 for f in range(3): 

398 filename = tmp_path / f"glob_{f}_tmp.gii" 

399 fnames.append(filename) 

400 data2D[:, f] *= f 

401 darray = gifti.GiftiDataArray( 

402 data=data2D[:, f], datatype="NIFTI_TYPE_FLOAT32" 

403 ) 

404 gii = gifti.GiftiImage(darrays=[darray]) 

405 gii.to_filename(fnames[f]) 

406 

407 assert_array_equal( 

408 load_surf_data(tmp_path / "glob*.gii"), 

409 data2D, 

410 ) 

411 

412 # make one more gii file that has more than one dimension 

413 filename = tmp_path / "glob_3_tmp.gii" 

414 fnames.append(filename) 

415 darray1 = gifti.GiftiDataArray( 

416 data=np.ones((20,)), datatype="NIFTI_TYPE_FLOAT32" 

417 ) 

418 gii = gifti.GiftiImage(darrays=[darray1, darray1, darray1]) 

419 gii.to_filename(fnames[-1]) 

420 

421 data2D = np.concatenate((data2D, np.ones((20, 3))), axis=1) 

422 assert_array_equal( 

423 load_surf_data(tmp_path / "glob*.gii"), 

424 data2D, 

425 ) 

426 

427 # make one more gii file that has a different shape in axis=0 

428 filename = tmp_path / "glob_4_tmp.gii" 

429 fnames.append(filename) 

430 darray = gifti.GiftiDataArray( 

431 data=np.ones((15, 1)), datatype="NIFTI_TYPE_FLOAT32" 

432 ) 

433 gii = gifti.GiftiImage(darrays=[darray]) 

434 gii.to_filename(fnames[-1]) 

435 

436 with pytest.raises( 

437 ValueError, match="files must contain data with the same shape" 

438 ): 

439 load_surf_data(tmp_path / "*.gii") 

440 

441 

442@pytest.mark.parametrize("xy", [(10, 7), (5, 5), (3, 2)]) 

443def test_flat_mesh(xy): 

444 mesh = flat_mesh(xy[0], xy[1]) 

445 points = mesh.coordinates 

446 triangles = mesh.faces 

447 a, b, c = points[triangles[0]] 

448 n = np.cross(b - a, c - a) 

449 assert np.allclose(n, [0.0, 0.0, 1.0]) 

450 

451 

452def test_vertex_outer_normals(): 

453 # compute normals for a flat horizontal mesh, they should all be (0, 0, 1) 

454 mesh = flat_mesh(5, 7) 

455 computed_normals = _vertex_outer_normals(mesh) 

456 true_normals = np.zeros((len(mesh.coordinates), 3)) 

457 true_normals[:, 2] = 1 

458 assert_array_almost_equal(computed_normals, true_normals) 

459 

460 

461def test_load_uniform_ball_cloud(): 

462 # Note: computed and shipped point clouds may differ since KMeans results 

463 # change after 

464 # https://github.com/scikit-learn/scikit-learn/pull/9288 

465 # but the exact position of the points does not matter as long as they are 

466 # well spread inside the unit ball 

467 for n_points in [10, 20, 40, 80, 160]: 

468 with warnings.catch_warnings(record=True) as w: 

469 points = _load_uniform_ball_cloud(n_points=n_points) 

470 assert_array_equal(points.shape, (n_points, 3)) 

471 assert len(w) == 0 

472 with pytest.warns(EfficiencyWarning): 

473 _load_uniform_ball_cloud(n_points=3) 

474 for n_points in [3, 7]: 

475 computed = _uniform_ball_cloud(n_points) 

476 loaded = _load_uniform_ball_cloud(n_points) 

477 assert_array_almost_equal(computed, loaded) 

478 assert (np.std(computed, axis=0) > 0.1).all() 

479 assert (np.linalg.norm(computed, axis=1) <= 1).all() 

480 

481 

482def test_sample_locations(): 

483 # check positions of samples on toy example, with an affine != identity 

484 # flat horizontal mesh 

485 mesh = flat_mesh(5, 7) 

486 affine = np.diagflat([10, 20, 30, 1]) 

487 inv_affine = np.linalg.inv(affine) 

488 # transform vertices to world space 

489 vertices = np.asarray( 

490 resampling.coord_transform(*mesh.coordinates.T, affine=affine) 

491 ).T 

492 # compute by hand the true offsets in voxel space 

493 # (transformed by affine^-1) 

494 ball_offsets = _load_uniform_ball_cloud(10) 

495 ball_offsets = np.asarray( 

496 resampling.coord_transform(*ball_offsets.T, affine=inv_affine) 

497 ).T 

498 line_offsets = np.zeros((10, 3)) 

499 line_offsets[:, 2] = np.linspace(1, -1, 10) 

500 line_offsets = np.asarray( 

501 resampling.coord_transform(*line_offsets.T, affine=inv_affine) 

502 ).T 

503 # check we get the same locations 

504 for kind, offsets in [("line", line_offsets), ("ball", ball_offsets)]: 

505 locations = _sample_locations( 

506 [vertices, mesh.faces], affine, 1.0, kind=kind, n_points=10 

507 ) 

508 true_locations = np.asarray( 

509 [vertex + offsets for vertex in mesh.coordinates] 

510 ) 

511 assert_array_equal(locations.shape, true_locations.shape) 

512 assert_array_almost_equal(true_locations, locations) 

513 with pytest.raises(ValueError): 

514 _sample_locations(mesh, affine, 1.0, kind="bad_kind") 

515 

516 

517@pytest.mark.parametrize("depth", [(0.0,), (-1.0,), (1.0,), (-1.0, 0.0, 0.5)]) 

518@pytest.mark.parametrize("n_points", [None, 10]) 

519def test_sample_locations_depth(depth, n_points, affine_eye): 

520 mesh = flat_mesh(5, 7) 

521 radius = 8.0 

522 locations = _sample_locations( 

523 mesh, affine_eye, radius, n_points=n_points, depth=depth 

524 ) 

525 offsets = np.asarray([[0.0, 0.0, -z * radius] for z in depth]) 

526 expected = np.asarray([vertex + offsets for vertex in mesh.coordinates]) 

527 assert np.allclose(locations, expected) 

528 

529 

530@pytest.mark.parametrize( 

531 "depth,n_points", 

532 [ 

533 (None, 1), 

534 (None, 7), 

535 ([0.0], 8), 

536 ([-1.0], 8), 

537 ([1.0], 8), 

538 ([-1.0, 0.0, 0.5], 8), 

539 ], 

540) 

541def test_sample_locations_between_surfaces(depth, n_points, affine_eye): 

542 inner = flat_mesh(5, 7) 

543 outer = InMemoryMesh( 

544 coordinates=inner.coordinates + np.asarray([0.0, 0.0, 1.0]), 

545 faces=inner.faces, 

546 ) 

547 locations = _sample_locations_between_surfaces( 

548 outer, inner, affine_eye, n_points=n_points, depth=depth 

549 ) 

550 

551 if depth is None: 

552 expected = np.asarray( 

553 [ 

554 np.linspace(b, a, n_points) 

555 for (a, b) in zip( 

556 inner.coordinates.ravel(), outer.coordinates.ravel() 

557 ) 

558 ] 

559 ) 

560 expected = np.rollaxis( 

561 expected.reshape((*outer.coordinates.shape, n_points)), 2, 1 

562 ) 

563 

564 else: 

565 offsets = [[0.0, 0.0, -z] for z in depth] 

566 expected = np.asarray( 

567 [vertex + offsets for vertex in outer.coordinates] 

568 ) 

569 

570 assert np.allclose(locations, expected) 

571 

572 

573def test_vol_to_surf_errors(): 

574 """Test errors thrown by vol_to_surf.""" 

575 img, *_ = data_gen.generate_mni_space_img() 

576 mesh = load_surf_mesh(datasets.fetch_surf_fsaverage()["pial_left"]) 

577 

578 with pytest.raises(ValueError, match=".*does not support.*"): 

579 vol_to_surf(img, mesh, kind="ball", depth=[0.5]) 

580 

581 with pytest.raises(ValueError, match=".*interpolation.*"): 

582 vol_to_surf(img, mesh, interpolation="bad") 

583 

584 

585@pytest.mark.parametrize("kind", ["line", "ball"]) 

586@pytest.mark.parametrize("n_scans", [1, 20]) 

587@pytest.mark.parametrize("use_mask", [True, False]) 

588def test_vol_to_surf(kind, n_scans, use_mask): 

589 img, mask_img = data_gen.generate_mni_space_img(n_scans) 

590 if not use_mask: 

591 mask_img = None 

592 if n_scans == 1: 

593 img = image.new_img_like(img, image.get_data(img).squeeze()) 

594 

595 fsaverage = datasets.fetch_surf_fsaverage() 

596 

597 mesh = load_surf_mesh(fsaverage["pial_left"]) 

598 inner_mesh = load_surf_mesh(fsaverage["white_left"]) 

599 center_mesh = ( 

600 np.mean([mesh.coordinates, inner_mesh.coordinates], axis=0), 

601 mesh.faces, 

602 ) 

603 

604 proj = vol_to_surf( 

605 img, mesh, kind="depth", inner_mesh=inner_mesh, mask_img=mask_img 

606 ) 

607 other_proj = vol_to_surf(img, center_mesh, kind=kind, mask_img=mask_img) 

608 

609 correlation = pearsonr(proj.ravel(), other_proj.ravel())[0] 

610 

611 assert correlation > 0.99 

612 

613 

614def test_vol_to_surf_nearest_most_frequent(img_labels): 

615 """Test nearest most frequent interpolation method in vol_to_surf when 

616 converting deterministic atlases with integer labels. 

617 """ 

618 img_labels_data = img_labels.get_fdata() 

619 uniques_vol = np.unique(img_labels_data) 

620 

621 mesh = flat_mesh(5, 7) 

622 mesh_labels = vol_to_surf( 

623 img_labels, mesh, interpolation="nearest_most_frequent" 

624 ) 

625 

626 uniques_surf = np.unique(mesh_labels) 

627 assert set(uniques_surf) <= set(uniques_vol) 

628 

629 

630def test_vol_to_surf_nearest_deprecation(img_labels): 

631 """Test deprecation warning for nearest interpolation method in 

632 vol_to_surf. 

633 """ 

634 mesh = flat_mesh(5, 7) 

635 with pytest.warns( 

636 FutureWarning, match="interpolation method will be deprecated" 

637 ): 

638 vol_to_surf(img_labels, mesh, interpolation="nearest") 

639 

640 

641def test_masked_indices(): 

642 mask = np.ones((4, 3, 8)) 

643 mask[:, :, ::2] = 0 

644 locations = np.mgrid[:5, :3, :8].ravel().reshape((3, -1)) 

645 masked = _masked_indices(locations.T, mask.shape, mask) 

646 # These elements are masked by the mask 

647 assert (masked[::2] == 1).all() 

648 # The last element of locations is one row beyond first image dimension 

649 assert (masked[-24:] == 1).all() 

650 # 4 * 3 * 8 / 2 elements should remain unmasked 

651 assert (1 - masked).sum() == 48 

652 

653 

654def test_projection_matrix(affine_eye): 

655 mesh = flat_mesh(5, 7, 4) 

656 img = z_const_img(5, 7, 13) 

657 proj = _projection_matrix( 

658 mesh, affine_eye, img.shape, radius=2.0, n_points=10 

659 ) 

660 # proj matrix has shape (n_vertices, img_size) 

661 assert proj.shape == (5 * 7, 5 * 7 * 13) 

662 # proj.dot(img) should give the values of img at the vertices' locations 

663 values = proj.dot(img.ravel()).reshape((5, 7)) 

664 assert_array_almost_equal(values, img[:, :, 0]) 

665 mesh = flat_mesh(5, 7) 

666 proj = _projection_matrix( 

667 mesh, affine_eye, (5, 7, 1), radius=0.1, n_points=10 

668 ) 

669 assert_array_almost_equal(proj.toarray(), np.eye(proj.shape[0])) 

670 mask = np.ones(img.shape, dtype=int) 

671 mask[0] = 0 

672 proj = _projection_matrix( 

673 mesh, affine_eye, img.shape, radius=2.0, n_points=10, mask=mask 

674 ) 

675 proj = proj.toarray() 

676 # first row of the mesh is masked 

677 assert_array_almost_equal(proj.sum(axis=1)[:7], np.zeros(7)) 

678 assert_array_almost_equal(proj.sum(axis=1)[7:], np.ones(proj.shape[0] - 7)) 

679 # mask and img should have the same shape 

680 with pytest.raises(ValueError): 

681 _projection_matrix( 

682 mesh, affine_eye, img.shape, mask=np.ones((3, 3, 2)) 

683 ) 

684 

685 

686def test_sampling_affine(affine_eye, rng): 

687 # check sampled (projected) values on a toy image 

688 img = np.ones((4, 4, 4)) 

689 img[1, :, :] = 2 

690 

691 coords = np.asarray([[1, 1, 2], [10, 10, 20], [30, 30, 30]]) 

692 mesh = [coords, rng.integers(coords.shape[0], size=(30, 3))] 

693 

694 affine = 10 * affine_eye 

695 affine[-1, -1] = 1 

696 texture = _nearest_voxel_sampling( 

697 [img], mesh, affine=affine, radius=1, kind="ball" 

698 ) 

699 assert_array_almost_equal(texture[0], [1.0, 2.0, 1.0], decimal=15) 

700 texture = _interpolation_sampling( 

701 [img], mesh, affine=affine, radius=0, kind="ball" 

702 ) 

703 assert_array_almost_equal(texture[0], [1.1, 2.0, 1.0], decimal=15) 

704 

705 

706@pytest.mark.parametrize("kind", ["auto", "line", "ball"]) 

707@pytest.mark.parametrize("use_inner_mesh", [True, False]) 

708@pytest.mark.parametrize("projection", ["linear", "nearest"]) 

709def test_sampling(kind, use_inner_mesh, projection, affine_eye): 

710 mesh = flat_mesh(5, 7, 4) 

711 img = z_const_img(5, 7, 13) 

712 mask = np.ones(img.shape, dtype=int) 

713 mask[0] = 0 

714 projector = { 

715 "nearest": _nearest_voxel_sampling, 

716 "linear": _interpolation_sampling, 

717 }[projection] 

718 inner_mesh = mesh if use_inner_mesh else None 

719 projection = projector( 

720 [img], mesh, affine_eye, kind=kind, radius=0.0, inner_mesh=inner_mesh 

721 ) 

722 assert_array_almost_equal(projection.ravel(), img[:, :, 0].ravel()) 

723 projection = projector( 

724 [img], 

725 mesh, 

726 affine_eye, 

727 kind=kind, 

728 radius=0.0, 

729 mask=mask, 

730 inner_mesh=inner_mesh, 

731 ) 

732 assert_array_almost_equal(projection.ravel()[7:], img[1:, :, 0].ravel()) 

733 assert np.isnan(projection.ravel()[:7]).all() 

734 

735 

736@pytest.mark.parametrize("projection", ["linear", "nearest"]) 

737def test_sampling_between_surfaces(projection, affine_eye): 

738 projector = { 

739 "nearest": _nearest_voxel_sampling, 

740 "linear": _interpolation_sampling, 

741 }[projection] 

742 mesh = flat_mesh(13, 7, 3.0) 

743 inner_mesh = flat_mesh(13, 7, 1) 

744 img = z_const_img(5, 7, 13).T 

745 projection = projector( 

746 [img], 

747 mesh, 

748 affine_eye, 

749 kind="auto", 

750 n_points=100, 

751 inner_mesh=inner_mesh, 

752 ) 

753 assert_array_almost_equal( 

754 projection.ravel(), img[:, :, 1:4].mean(axis=-1).ravel() 

755 ) 

756 

757 

758def test_choose_kind(): 

759 kind = _choose_kind("abc", None) 

760 assert kind == "abc" 

761 kind = _choose_kind("abc", "mesh") 

762 assert kind == "abc" 

763 kind = _choose_kind("auto", None) 

764 assert kind == "line" 

765 kind = _choose_kind("auto", "mesh") 

766 assert kind == "depth" 

767 with pytest.raises(TypeError, match=".*sampling strategy"): 

768 kind = _choose_kind("depth", None) 

769 

770 

771def test_validate_mesh(rng): 

772 """Ensures that invalid meshes cannot be instantiated.""" 

773 # valid mesh is fine 

774 coords = rng.random((20, 3)) 

775 faces = rng.integers(coords.shape[0], size=(30, 3)) 

776 InMemoryMesh(coordinates=coords, faces=faces) 

777 

778 # Face is None 

779 coords = rng.random((20, 3)) 

780 coords[0] = np.array([np.nan, np.nan, np.nan]) 

781 faces = None 

782 

783 with pytest.raises(TypeError, match="must be numpy arrays."): 

784 InMemoryMesh(coordinates=coords, faces=faces) 

785 

786 # coordinates is None 

787 faces = rng.integers(20, size=(30, 3)) 

788 coords = None 

789 

790 with pytest.raises(TypeError, match="must be numpy arrays."): 

791 InMemoryMesh(coordinates=coords, faces=faces) 

792 

793 # coordinates with non finite values 

794 coords = rng.random((20, 3)) 

795 coords[0] = np.array([np.nan, np.nan, np.nan]) 

796 faces = rng.integers(coords.shape[0], size=(30, 3)) 

797 

798 with pytest.raises(ValueError, match="Mesh coordinates must be finite."): 

799 InMemoryMesh(coordinates=coords, faces=faces) 

800 

801 # faces with indices that do not correspond to any coordinate 

802 coords = rng.random((20, 3)) 

803 

804 # values too high 

805 faces = rng.integers(low=30, high=50, size=(30, 3)) 

806 

807 with pytest.raises( 

808 ValueError, 

809 match="Mismatch between the indices of faces and the number of nodes.", 

810 ): 

811 InMemoryMesh(coordinates=coords, faces=faces) 

812 

813 # negative values 

814 faces = rng.integers(low=-50, high=-30, size=(30, 3)) 

815 

816 with pytest.raises( 

817 ValueError, 

818 match="Mismatch between the indices of faces and the number of nodes.", 

819 ): 

820 InMemoryMesh(coordinates=coords, faces=faces) 

821 

822 

823@pytest.mark.parametrize( 

824 "dtype", 

825 [ 

826 np.uint16, 

827 np.uint32, 

828 np.uint64, 

829 np.int8, 

830 np.int16, 

831 np.int32, 

832 np.int64, 

833 np.float32, 

834 np.float64, 

835 ], 

836) 

837def test_data_to_gifti(rng, tmp_path, dtype): 

838 """Check saving several data type to gifti. 

839 

840 - check that strings and Path work 

841 - make sure files can be loaded with nibabel 

842 """ 

843 data = rng.random((5, 6)).astype(dtype) 

844 _data_to_gifti(data=data, gifti_file=tmp_path / "data.gii") 

845 _data_to_gifti(data=data, gifti_file=str(tmp_path / "data.gii")) 

846 load(tmp_path / "data.gii") 

847 

848 

849@pytest.mark.parametrize("dtype", [np.complex64, np.complex128]) 

850def test_data_to_gifti_unsupported_dtype(rng, tmp_path, dtype): 

851 """Check saving unsupported data type raises an error.""" 

852 data = rng.random((5, 6)).astype(dtype) 

853 with pytest.raises(ValueError, match="supports uint8, int32 and float32"): 

854 _data_to_gifti(data=data, gifti_file=tmp_path / "data.gii") 

855 

856 

857@pytest.mark.parametrize("shape", [(5,), (5, 1), (5, 2)]) 

858def test_polydata_shape(shape): 

859 data = PolyData(left=np.ones(shape), right=np.ones(shape)) 

860 

861 assert len(data.shape) == len(shape) 

862 assert data.shape[0] == shape[0] * 2 

863 

864 data = PolyData(left=np.ones(shape)) 

865 

866 assert len(data.shape) == len(shape) 

867 assert data.shape[0] == shape[0] 

868 

869 

870def test_polydata_1d_check_parts(): 

871 """Smoke test for check parts. 

872 

873 - passing a 1D array at instantiation is fine: 

874 they are convertd to 2D 

875 - _check_parts can be used make sure parts are fine 

876 if assigned after instantiation. 

877 """ 

878 data = PolyData(left=np.ones((7,)), right=np.ones((5,))) 

879 

880 data.parts["left"] = np.ones((1,)) 

881 

882 data._check_parts() 

883 

884 

885def test_mesh_to_gifti(single_mesh, tmp_path): 

886 """Check saving mesh to gifti. 

887 

888 - check that strings and Path work 

889 - make sure files can be loaded with nibabel 

890 """ 

891 coordinates, faces = single_mesh 

892 _mesh_to_gifti( 

893 coordinates=coordinates, faces=faces, gifti_file=tmp_path / "mesh.gii" 

894 ) 

895 _mesh_to_gifti( 

896 coordinates=coordinates, 

897 faces=faces, 

898 gifti_file=str(tmp_path / "mesh.gii"), 

899 ) 

900 load(tmp_path / "mesh.gii") 

901 

902 

903def test_compare_file_and_inmemory_mesh(surf_mesh, tmp_path): 

904 left = surf_mesh.parts["left"] 

905 gifti_file = tmp_path / "left.gii" 

906 left.to_gifti(gifti_file) 

907 

908 left_read = FileMesh(gifti_file) 

909 left_read.__repr__() # for coverage 

910 assert left.n_vertices == left_read.n_vertices 

911 assert np.array_equal(left.coordinates, left_read.coordinates) 

912 assert np.array_equal(left.faces, left_read.faces) 

913 

914 left_loaded = left_read.loaded() 

915 assert isinstance(left_loaded, InMemoryMesh) 

916 assert left.n_vertices == left_loaded.n_vertices 

917 assert np.array_equal(left.coordinates, left_loaded.coordinates) 

918 assert np.array_equal(left.faces, left_loaded.faces) 

919 

920 

921@pytest.mark.parametrize("shape", [1, 3]) 

922def test_surface_image_shape(surf_img_2d, shape): 

923 assert surf_img_2d(shape).shape == (9, shape) 

924 

925 

926def test_data_shape_not_matching_mesh(surf_img_1d, flip_surf_img_parts): 

927 with pytest.raises(ValueError, match="shape.*vertices"): 

928 SurfaceImage(surf_img_1d.mesh, flip_surf_img_parts(surf_img_1d.data)) 

929 

930 

931def test_data_shape_inconsistent(surf_img_2d): 

932 bad_data = { 

933 "left": surf_img_2d(7).data.parts["left"], 

934 "right": surf_img_2d(7).data.parts["right"][0][:4], 

935 } 

936 with pytest.raises(ValueError, match="incompatible shapes"): 

937 SurfaceImage(surf_img_2d(7).mesh, bad_data) 

938 

939 

940def test_data_keys_not_matching_mesh(surf_img_1d): 

941 with pytest.raises(ValueError, match="same keys"): 

942 SurfaceImage( 

943 {"left": surf_img_1d.mesh.parts["left"]}, 

944 surf_img_1d.data, 

945 ) 

946 

947 

948@pytest.mark.parametrize("use_path", [True, False]) 

949@pytest.mark.parametrize( 

950 "output_filename, expected_files, unexpected_files", 

951 [ 

952 ("foo.gii", ["foo_hemi-L.gii", "foo_hemi-L.gii"], ["foo.gii"]), 

953 ("foo_hemi-L_T1w.gii", ["foo_hemi-L_T1w.gii"], ["foo_hemi-R_T1w.gii"]), 

954 ("foo_hemi-R_T1w.gii", ["foo_hemi-R_T1w.gii"], ["foo_hemi-L_T1w.gii"]), 

955 ], 

956) 

957def test_load_save_mesh( 

958 tmp_path, output_filename, expected_files, unexpected_files, use_path 

959): 

960 """Load fsaverage5 from filename or Path and save. 

961 

962 Check that 

963 - the appropriate hemisphere information is added to the filename 

964 - only one hemisphere is saved if hemi- is in the filename 

965 - the roundtrip does not change the data 

966 """ 

967 mesh_right = datasets.fetch_surf_fsaverage().pial_right 

968 mesh_left = datasets.fetch_surf_fsaverage().pial_left 

969 data_right = datasets.fetch_surf_fsaverage().sulc_right 

970 data_left = datasets.fetch_surf_fsaverage().sulc_left 

971 

972 if use_path: 

973 img = SurfaceImage( 

974 mesh={"left": Path(mesh_left), "right": Path(mesh_right)}, 

975 data={"left": Path(data_left), "right": Path(data_right)}, 

976 ) 

977 else: 

978 img = SurfaceImage( 

979 mesh={"left": mesh_left, "right": mesh_right}, 

980 data={"left": data_left, "right": data_right}, 

981 ) 

982 

983 if use_path: 

984 img.mesh.to_filename(tmp_path / output_filename) 

985 else: 

986 img.mesh.to_filename(str(tmp_path / output_filename)) 

987 

988 for file in unexpected_files: 

989 assert not (tmp_path / file).exists() 

990 

991 for file in expected_files: 

992 assert (tmp_path / file).exists() 

993 

994 mesh = load_surf_mesh(tmp_path / file) 

995 if "hemi-L" in file: 

996 expected_mesh = load_surf_mesh(mesh_left) 

997 elif "hemi-R" in file: 

998 expected_mesh = load_surf_mesh(mesh_right) 

999 assert np.array_equal(mesh.faces, expected_mesh.faces) 

1000 assert np.array_equal(mesh.coordinates, expected_mesh.coordinates) 

1001 

1002 

1003def test_save_mesh_default_suffix(tmp_path, surf_img_1d): 

1004 """Check default .gii extension is added.""" 

1005 surf_img_1d.mesh.to_filename( 

1006 tmp_path / "give_me_a_default_suffix_hemi-L_mesh" 

1007 ) 

1008 assert (tmp_path / "give_me_a_default_suffix_hemi-L_mesh.gii").exists() 

1009 

1010 

1011def test_save_mesh_error(tmp_path, surf_img_1d): 

1012 with pytest.raises(ValueError, match="cannot contain both"): 

1013 surf_img_1d.mesh.to_filename( 

1014 tmp_path / "hemi-L_hemi-R_cannot_have_both.gii" 

1015 ) 

1016 

1017 

1018def test_save_mesh_error_wrong_suffix(tmp_path, surf_img_1d): 

1019 with pytest.raises(ValueError, match="with the extension '.gii'"): 

1020 surf_img_1d.mesh.to_filename( 

1021 tmp_path / "hemi-L_hemi-R_cannot_have_both.foo" 

1022 ) 

1023 

1024 

1025@pytest.mark.parametrize("use_path", [True, False]) 

1026@pytest.mark.parametrize( 

1027 "output_filename, expected_files, unexpected_files", 

1028 [ 

1029 ("foo.gii", ["foo_hemi-L.gii", "foo_hemi-L.gii"], ["foo.gii"]), 

1030 ("foo_hemi-L_T1w.gii", ["foo_hemi-L_T1w.gii"], ["foo_hemi-R_T1w.gii"]), 

1031 ("foo_hemi-R_T1w.gii", ["foo_hemi-R_T1w.gii"], ["foo_hemi-L_T1w.gii"]), 

1032 ], 

1033) 

1034def test_load_save_data( 

1035 tmp_path, output_filename, expected_files, unexpected_files, use_path 

1036): 

1037 """Load and save gifti leaves them unchanged.""" 

1038 mesh_right = datasets.fetch_surf_fsaverage().pial_right 

1039 mesh_left = datasets.fetch_surf_fsaverage().pial_left 

1040 data_right = datasets.fetch_surf_fsaverage().sulc_right 

1041 data_left = datasets.fetch_surf_fsaverage().sulc_left 

1042 

1043 if use_path: 

1044 img = SurfaceImage( 

1045 mesh={"left": Path(mesh_left), "right": Path(mesh_right)}, 

1046 data={"left": Path(data_left), "right": Path(data_right)}, 

1047 ) 

1048 else: 

1049 img = SurfaceImage( 

1050 mesh={"left": mesh_left, "right": mesh_right}, 

1051 data={"left": data_left, "right": data_right}, 

1052 ) 

1053 

1054 if use_path: 

1055 img.data.to_filename(tmp_path / output_filename) 

1056 else: 

1057 img.data.to_filename(str(tmp_path / output_filename)) 

1058 

1059 for file in unexpected_files: 

1060 assert not (tmp_path / file).exists() 

1061 

1062 for file in expected_files: 

1063 assert (tmp_path / file).exists() 

1064 

1065 data = load_surf_data(tmp_path / file) 

1066 if "hemi-L" in file: 

1067 expected_data = load_surf_data(data_left) 

1068 elif "hemi-R" in file: 

1069 expected_data = load_surf_data(data_right) 

1070 assert np.array_equal(data, expected_data) 

1071 

1072 

1073def test_load_save_data_1d(rng, tmp_path, surf_mesh): 

1074 """Load and save 1D gifti leaves them unchanged.""" 

1075 data = {} 

1076 for hemi in ["left", "right"]: 

1077 size = (surf_mesh.parts[hemi].n_vertices,) 

1078 data[hemi] = rng.random(size=size).astype(np.uint8) 

1079 darray = gifti.GiftiDataArray( 

1080 data=data[hemi], datatype="NIFTI_TYPE_UINT8" 

1081 ) 

1082 gii = gifti.GiftiImage(darrays=[darray]) 

1083 gii.to_filename(tmp_path / f"original_{hemi}.gii") 

1084 

1085 img = SurfaceImage( 

1086 mesh=surf_mesh, 

1087 data={ 

1088 "left": tmp_path / "original_left.gii", 

1089 "right": tmp_path / "original_right.gii", 

1090 }, 

1091 ) 

1092 

1093 img.data.to_filename(tmp_path / "nilearn.gii") 

1094 

1095 for hemi in ["left", "right"]: 

1096 original_surf_img = load(tmp_path / f"original_{hemi}.gii") 

1097 nilearn_surf_img = load( 

1098 tmp_path / f"nilearn_hemi-{hemi[0].upper()}.gii" 

1099 ) 

1100 assert_array_equal( 

1101 original_surf_img.agg_data(), nilearn_surf_img.agg_data() 

1102 ) 

1103 

1104 

1105@pytest.mark.parametrize( 

1106 "dtype", 

1107 [ 

1108 np.uint16, 

1109 np.uint32, 

1110 np.uint64, 

1111 np.int8, 

1112 np.int16, 

1113 np.int32, 

1114 np.int64, 

1115 np.float32, 

1116 np.float64, 

1117 ], 

1118) 

1119def test_save_dtype(surf_img_1d, tmp_path, dtype): 

1120 """Check saving several data type.""" 

1121 surf_img_1d.data.parts["right"] = surf_img_1d.data.parts["right"].astype( 

1122 dtype 

1123 ) 

1124 surf_img_1d.data.to_filename(tmp_path / "data.gii") 

1125 

1126 

1127def test_load_from_volume_3d_nifti(img_3d_mni, surf_mesh, tmp_path): 

1128 """Instantiate surface image with 3D Niftiimage object or file for data.""" 

1129 SurfaceImage.from_volume(mesh=surf_mesh, volume_img=img_3d_mni) 

1130 

1131 img_3d_mni.to_filename(tmp_path / "tmp.nii.gz") 

1132 

1133 SurfaceImage.from_volume( 

1134 mesh=surf_mesh, 

1135 volume_img=tmp_path / "tmp.nii.gz", 

1136 ) 

1137 

1138 

1139def test_load_from_volume_4d_nifti(img_4d_mni, surf_mesh, tmp_path): 

1140 """Instantiate surface image with 4D Niftiimage object or file for data.""" 

1141 img = SurfaceImage.from_volume(mesh=surf_mesh, volume_img=img_4d_mni) 

1142 # check that we have the correct number of time points 

1143 assert img.shape[1] == img_4d_mni.shape[3] 

1144 

1145 img_4d_mni.to_filename(tmp_path / "tmp.nii.gz") 

1146 

1147 SurfaceImage.from_volume( 

1148 mesh=surf_mesh, 

1149 volume_img=tmp_path / "tmp.nii.gz", 

1150 ) 

1151 

1152 

1153def test_surface_image_error(): 

1154 """Instantiate surface image with Niftiimage object or file for data.""" 

1155 mesh_right = datasets.fetch_surf_fsaverage().pial_right 

1156 mesh_left = datasets.fetch_surf_fsaverage().pial_left 

1157 

1158 with pytest.raises(TypeError, match="[PolyData, dict]"): 

1159 SurfaceImage(mesh={"left": mesh_left, "right": mesh_right}, data=3) 

1160 

1161 

1162def test_polydata_error(): 

1163 with pytest.raises(ValueError, match="Either left or right"): 

1164 PolyData(left=None, right=None) 

1165 

1166 

1167def test_polymesh_error(): 

1168 with pytest.raises(ValueError, match="Either left or right"): 

1169 PolyMesh(left=None, right=None) 

1170 

1171 

1172def test_inmemorymesh_index_error(in_memory_mesh): 

1173 with pytest.raises( 

1174 IndexError, match="Use 0 for coordinates and 1 for faces" 

1175 ): 

1176 in_memory_mesh[2] 

1177 

1178 

1179def test_get_min_max(surf_img_2d): 

1180 """Make sure we get the min and max across hemispheres.""" 

1181 img = surf_img_2d() 

1182 img.data.parts["left"][:, 0] = np.zeros(shape=(4)) 

1183 img.data.parts["left"][0][0] = 10 

1184 img.data.parts["right"][:, 0] = np.zeros(shape=(5)) 

1185 img.data.parts["right"][0][0] = -3.5 

1186 

1187 vmin, vmax = img.data._get_min_max() 

1188 

1189 assert vmin == -3.5 

1190 assert vmax == 10 

1191 

1192 

1193def test_extract_data_wrong_input(): 

1194 """Check that only SurfaceImage is accepted as input.""" 

1195 with pytest.raises(TypeError, match="Input must a be SurfaceImage"): 

1196 extract_data(1, index=1) 

1197 

1198 

1199def test_get_data(surf_img_1d): 

1200 """Check that getting data from image or polydata gives same result.""" 

1201 data_from_image = get_data(surf_img_1d) 

1202 data_from_polydata = get_data(surf_img_1d.data) 

1203 assert_array_equal(data_from_image, data_from_polydata) 

1204 

1205 

1206@pytest.mark.parametrize("ensure_finite", [True, False]) 

1207def test_get_data_ensure_finite(surf_img_1d, ensure_finite): 

1208 """Check get data can deal with non finite values.""" 

1209 surf_img_1d.data.parts["left"][1] = np.nan 

1210 surf_img_1d.data.parts["left"][2] = np.inf 

1211 

1212 if ensure_finite is True: 

1213 with pytest.warns(UserWarning, match="Non-finite values detected."): 

1214 data_from_image = get_data( 

1215 surf_img_1d, ensure_finite=ensure_finite 

1216 ) 

1217 assert np.all(np.isfinite(data_from_image)) 

1218 else: 

1219 data_from_image = get_data(surf_img_1d, ensure_finite=ensure_finite) 

1220 assert np.logical_not(np.all(np.isfinite(data_from_image))) 

1221 

1222 

1223def test_check_surf_img(surf_img_1d, surf_img_2d): 

1224 """Check that surface image are properly validated.""" 

1225 check_surf_img(surf_img_1d) 

1226 check_surf_img(surf_img_2d()) 

1227 

1228 data = { 

1229 part: np.empty(0).reshape((surf_img_1d.data.parts[part].shape[0], 0)) 

1230 for part in surf_img_1d.data.parts 

1231 } 

1232 imgs = SurfaceImage(surf_img_1d.mesh, data) 

1233 with pytest.raises(ValueError, match="empty"): 

1234 check_surf_img(imgs)