OSCR

A Digital Anatomical Atlas of the Human Cerebellum at Subfolial Resolution.

Code ↔ Paper

4 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 4 matches
  1. [1] § Materials and Methods › Arcus ↔ cmb/visualization.py, lines 612–665 · score 0.74 · lobules VIII, anterior lobe, flocculonodular, inferior, IX, vermis
  2. [2] § Materials and Methods › Arcus ↔ cmb/source_space.py, lines 198–281 · score 0.63 · virtual MRI, subject MRI, volumetric atlas, mesh, brains, transformation
  3. [3] § Materials and Methods › Creation of the Atlas ↔ cmb/source_space.py, lines 635–721 · score 0.51 · solid angles, triangular, cross, neighboring, space, vertices
  4. [4] § Materials and Methods › Arcus ↔ cmb/source_space.py, lines 198–281 · score 0.50 · virtual MRI volume, volumetric atlas, transformation, space, segmented, cerebellum

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Python · 863 lines · 31 KB · MIT · 3 matches

  1. #!/usr/bin/env python3
  2. """Source space construction for combined cerebral and cerebellar MEG/EEG analysis.
  3. Provides functions to set up cerebellar surface source spaces, register them
  4. to individual subject anatomy via ANTs diffeomorphic registration, and merge
  5. them with MNE-Python cortical source spaces.
  6. """
  7. # ---------------------------------------------------------------------------
  8. # Authors: John G Samuelson <[email hidden]>
  9. # Christoph Dinh <[email hidden]>
  10. # Teemu Taivainen
  11. # Created: November, 2021 (Modified: July, 2026)
  12. # License: MIT
  13. # ---------------------------------------------------------------------------
  14. import logging
  15. import os
  16. import os.path as op
  17. import pickle
  18. import warnings
  19. from pathlib import Path
  20. from typing import TYPE_CHECKING, Literal, cast
  21. import mne
  22. import numpy as np
  23. from nibabel import Nifti1Image
  24. from nibabel.freesurfer.io import write_geometry
  25. from numpy.typing import NDArray
  26. from .helpers import (
  27. affine_transform,
  28. change_labels,
  29. convert_to_ants_image,
  30. load_image_volume,
  31. )
  32. if TYPE_CHECKING:
  33. from ants.core.ants_image import ANTsImage
  34. from pandas import DataFrame
  35. logger = logging.getLogger(__name__)
  36. def create_cerebellar_surface(
  37. subject: str,
  38. segmentation: Nifti1Image,
  39. subjects_dir: os.PathLike[str] | str | None = None,
  40. cmb_dir: os.PathLike[str] | str | None = None,
  41. cerebellum_subsampling: Literal["full", "sparse", "dense"] = "sparse",
  42. save_mesh: bool | os.PathLike[str] | str = True,
  43. overwrite: bool = False,
  44. registration_caching: bool = False,
  45. ) -> tuple[NDArray, NDArray]:
  46. """Create a cerebellar mesh in the native subject space.
  47. Runs the reconstruction step of ARCUS, fitting a high-resolution cerebellar atlas to
  48. the segmentation of the subject's cerebellum. Outputs a cerebellar mesh in
  49. FreeSurfer surface RAS coordinates.
  50. Parameters
  51. ----------
  52. subject : str
  53. The FreeSurfer subject name.
  54. segmentation : Nifti1Image
  55. The subject's cerebellar segmentation as a Nifti1Image. This can be obtained
  56. using the `segment_cerebellum` function.
  57. subjects_dir : path-like | None, optional
  58. The path to the directory containing the FreeSurfer subjects reconstructions.
  59. If None, defaults to the SUBJECTS_DIR environment variable.
  60. cmb_dir: path-like | None, optional
  61. Path to cerebellum data folder. If None, defaults to the package
  62. installation directory.
  63. cerebellum_subsampling : 'full' | 'sparse' | 'dense'
  64. The spacing to use for the cerebellum.
  65. save_mesh : bool | path-like, optional
  66. If True (default), saves the cerebellar mesh to
  67. ``<subjects_dir>/<subject>/surf/cerebellum_<cerebellum_subsampling>.white``.
  68. If a path is provided, saves the mesh to the specified path.
  69. If False, does not save the mesh to disk.
  70. overwrite : bool, optional
  71. If True, will overwrite any existing mesh file at the save location. If False
  72. (default), raises a `FileExistsError` if the mesh file already exists.
  73. registration_caching : bool, optional
  74. If True, it will attempt to read cached registration transforms from disk, and
  75. if not found, will save the transforms to disk for future use. Defaults to
  76. False, which means that registration will be computed without caching.
  77. Returns
  78. -------
  79. rr_ras : NDArray
  80. n_vertices x 3 array of vertex positions in FreeSurfer surface RAS coordinates.
  81. tris : NDArray
  82. n_faces x 3 array of triangle vertex indices defining the mesh faces.
  83. """
  84. # Use MNE-Python to fall back to SUBJECTS_DIR environment variable if needed.
  85. subjects_dir = mne.utils.get_subjects_dir(subjects_dir, raise_error=True) # pyright: ignore[reportAssignmentType]
  86. assert subjects_dir is not None, (
  87. "subjects_dir returned by mne.utils.get_subjects_dir should not be None."
  88. )
  89. subjects_dir = Path(subjects_dir) # ensure the type is Path
  90. if cmb_dir is None:
  91. from . import CMB_DATA_DIR
  92. cmb_dir = Path(CMB_DATA_DIR)
  93. else:
  94. cmb_dir = Path(cmb_dir)
  95. if isinstance(save_mesh, (str, os.PathLike)):
  96. mesh_file = Path(save_mesh)
  97. elif save_mesh is True:
  98. mesh_file = (
  99. subjects_dir
  100. / subject
  101. / "surf"
  102. / f"cerebellum_{cerebellum_subsampling}.white"
  103. )
  104. else:
  105. mesh_file = None
  106. if mesh_file is not None and mesh_file.exists() and not overwrite:
  107. raise FileExistsError(
  108. f"Mesh file {mesh_file} already exists. Set overwrite=True to overwrite "
  109. "or provide a different file path for 'save_mesh'."
  110. )
  111. data_dir = cmb_dir / "data"
  112. if registration_caching:
  113. # Save registration transforms to a cache directory.
  114. registration_cache_dir = data_dir / "atlas_fitting_cache"
  115. registration_cache_dir.mkdir(parents=True, exist_ok=True)
  116. else:
  117. # No caching.
  118. registration_cache_dir = None
  119. logger.info("Starting to create cerebellar surface for subject %s...", subject)
  120. rr_ras, tris = _create_cerebellar_surface(
  121. subject=subject,
  122. segmentation=segmentation,
  123. subjects_dir=subjects_dir,
  124. cerebellum_subsampling=cerebellum_subsampling,
  125. cerebellum_geo_fname=data_dir / "cerebellum_geo",
  126. registration_cache_dir=registration_cache_dir,
  127. )
  128. if mesh_file is not None:
  129. mesh_file.parent.mkdir(parents=True, exist_ok=True)
  130. # Overwrites existing files.
  131. write_geometry(os.fspath(mesh_file), rr_ras, tris)
  132. return rr_ras, tris
  133. def _create_cerebellar_surface(
  134. subject: str,
  135. segmentation: Nifti1Image,
  136. subjects_dir: Path,
  137. cerebellum_subsampling: Literal["full", "sparse", "dense"],
  138. cerebellum_geo_fname: Path,
  139. registration_cache_dir: Path | None,
  140. ) -> tuple[NDArray, NDArray]:
  141. """Create a cerebellar mesh in the native subject space.
  142. Runs the reconstruction step of ARCUS, fitting a high-resolution cerebellar atlas to
  143. the segmentation of the subject's cerebellum. Outputs a cerebellar mesh in
  144. FreeSurfer surface RAS coordinates.
  145. Parameters
  146. ----------
  147. subject : str
  148. The FreeSurfer subject name.
  149. segmentation : Nifti1Image
  150. The subject's cerebellar segmentation as a Nifti1Image.
  151. subjects_dir : Path
  152. The path to the directory containing the FreeSurfer subjects reconstructions.
  153. cerebellum_subsampling : 'full' | 'sparse' | 'dense'
  154. The spacing to use for the cerebellum.
  155. cerebellum_geo_fname : Path
  156. Path to the cerebellum geometry file (pickle) containing the high-resolution
  157. atlas and mesh data.
  158. registration_cache_dir : Path | None
  159. Directory to cache registration results. If None, registration will be computed
  160. without caching.
  161. Returns
  162. -------
  163. rr_ras : NDArray
  164. n_vertices x 3 array of vertex positions in FreeSurfer surface RAS coordinates.
  165. tris : NDArray
  166. n_faces x 3 array of triangle vertex indices defining the mesh faces.
  167. """
  168. import ants
  169. from ants.registration import (
  170. apply_transforms,
  171. apply_transforms_to_points,
  172. )
  173. from scipy import signal
  174. # Cast to str for compatibility.
  175. registration_cache_dir_str = (
  176. os.fspath(registration_cache_dir)
  177. if registration_cache_dir is not None
  178. else None
  179. )
  180. with open(cerebellum_geo_fname, "rb") as cb_geo_file:
  181. cb_data = pickle.load(cb_geo_file)
  182. # Load the template cerebellar mesh. The mesh is in the voxel space of the
  183. # volumetric atlas.
  184. if cerebellum_subsampling == "full":
  185. rr = cb_data["verts_normal"]
  186. tris = cb_data["faces"]
  187. else:
  188. rr = cb_data["dw_data"][cerebellum_subsampling + "_verts"]
  189. tris = cb_data["dw_data"][cerebellum_subsampling + "_tris"]
  190. rr = affine_transform(1, np.array([0, 0, 0]), [np.pi / 2, 0, 0], rr)
  191. # Get the volumetric atlas (high resolution segmentation).
  192. hr_segm = cb_data["parcellation"]["volume"].copy()
  193. # Get virtual MRI volume of the atlas.
  194. hr_vol = cb_data["hr_vol"]
  195. # Adjust the labels of the volumetric atlas to match the subject's segmentation.
  196. old_labels = [
  197. 12,
  198. 33,
  199. 36,
  200. 43,
  201. 46,
  202. 53,
  203. 56,
  204. 60,
  205. 63,
  206. 66,
  207. 70,
  208. 73,
  209. 74,
  210. 75,
  211. 76,
  212. 77,
  213. 78,
  214. 80,
  215. 83,
  216. 84,
  217. 86,
  218. 87,
  219. 90,
  220. 93,
  221. 96,
  222. 100,
  223. 103,
  224. 106,
  225. ]
  226. hr_segm = change_labels(
  227. hr_segm, old_labels=old_labels, new_labels=list(range(1, 29))
  228. )
  229. # Get subject MRI.
  230. # orig.mgz is in same space as brain.mgz, so segmentation and orig.mgz
  231. # should be aligned.
  232. subj_mri, subj_affine = load_image_volume(
  233. op.join(subjects_dir, subject, "mri", "orig.mgz")
  234. )
  235. seg_affine = segmentation.affine
  236. if seg_affine is None or subj_affine is None:
  237. warnings.warn(
  238. "Segmentation affine and/or subject affine is None. Cannot verify "
  239. "alignment with subject MRI. Proceeding without affine check.",
  240. UserWarning,
  241. stacklevel=2,
  242. )
  243. else:
  244. if not np.allclose(subj_affine, seg_affine):
  245. raise ValueError(
  246. "Subject MRI and segmentation are not aligned based on affine matrices."
  247. )
  248. subject_labels = np.asanyarray(segmentation.dataobj)
  249. if subject_labels.shape != subj_mri.shape:
  250. raise ValueError(
  251. "Subject MRI and segmentation volumes must have the same shape "
  252. f"(got MRI {subj_mri.shape} vs segmentation {subject_labels.shape})."
  253. )
  254. # Crop the segmentation and the MRI to the bounding box of the cerebellum.
  255. pad = 3
  256. cerb_coords = np.nonzero(subject_labels) # cerebellum is nonzero in segmentation
  257. subject_labels, cb_range = _crop_image_volume(subject_labels, cerb_coords, pad=pad)
  258. subj_contrast = np.zeros(subj_mri.shape)
  259. # Fill the cerebellum region with MRI values.
  260. # Can use cerb_coords to index because subject MRI and segmentation are aligned.
  261. subj_contrast[cerb_coords] = subj_mri[cerb_coords]
  262. subj_contrast, _ = _crop_image_volume(subj_contrast, cerb_coords, pad=pad)
  263. logger.info("Setting up adaptation to subject... ")
  264. # Resample the cerebellar volume to the subject's segmentation size.
  265. logger.debug("Resampling template MRI to the subject's segmentation size...")
  266. hr_vol_scaled = hr_vol
  267. for axis in range(3):
  268. hr_vol_scaled = signal.resample(
  269. hr_vol_scaled, num=subject_labels.shape[axis], axis=axis
  270. )
  271. # Help type checkers understand the type of hr_vol_scaled.
  272. assert isinstance(hr_vol_scaled, np.ndarray), (
  273. "Resampled signal should be a NumPy array."
  274. )
  275. scaling_factor = np.array(hr_vol_scaled.shape) / np.array(hr_vol.shape)
  276. logger.debug("Shape of volumetric atlas: %s", hr_vol.shape)
  277. logger.debug("Shape of resampled atlas: %s", hr_vol_scaled.shape)
  278. logger.debug("Scaling factor: %s", scaling_factor)
  279. # Clean up the resampled volume by removing low value voxels.
  280. # Voxels with value below 10 are set to zero.
  281. hr_volume_resampled = np.where(hr_vol_scaled > 10, hr_vol_scaled, 0)
  282. # Resample the segmentation to the subject's segmentation size.
  283. logger.debug(
  284. "Resampling template segmentation to the subject's segmentation size..."
  285. )
  286. hr_labels_scaled = _scale_labels_majority_vote(
  287. hr_segm, subject_labels, scaling_factor
  288. )
  289. # Correct vertices by co-registering lower left posterior and upper right
  290. # anterior corners between scaled volume and mesh.
  291. non_zero_coords_50 = np.argwhere(hr_vol_scaled > 50)
  292. rr = _align_mesh_to_volume(rr, scaling_factor, target_coords=non_zero_coords_50)
  293. logger.info("Done setting up adaptation to subject.")
  294. # Register labels of high resolution atlas to labels of subject's segmentation.
  295. subj_label_ants = ants.from_numpy(subject_labels.astype(float))
  296. hr_label_ants = ants.from_numpy(hr_labels_scaled.astype(float))
  297. logger.info("Fitting labels... ")
  298. # Compute or get cached registration.
  299. reg = _get_registration(
  300. fixed=subj_label_ants,
  301. moving=hr_label_ants,
  302. type_of_transform="SyNCC",
  303. reg_cache_dir=registration_cache_dir_str,
  304. reg_fname_prefix=f"{subject}_labels_",
  305. )
  306. # Apply the registration to the mesh vertices.
  307. # NOTE: Intentionally using invtransforms to warp the mesh to the subject space.
  308. warped_rr = np.array(
  309. apply_transforms_to_points(3, _coords_to_dataframe(rr), reg["invtransforms"])
  310. )
  311. logger.info("Fitting contrast... ")
  312. subj_ants = convert_to_ants_image(subj_contrast, normalize=True)
  313. hr_rs_ants = convert_to_ants_image(hr_volume_resampled, normalize=True)
  314. # Also warp the volume template to the subject space.
  315. hr_rs_ants = cast(
  316. "ANTsImage",
  317. apply_transforms(
  318. fixed=subj_ants, moving=hr_rs_ants, transformlist=reg["fwdtransforms"]
  319. ),
  320. )
  321. # Register the warped atlas volume to the subject volume to refine the registration.
  322. reg = _get_registration(
  323. fixed=subj_ants,
  324. moving=hr_rs_ants,
  325. type_of_transform="SyNCC",
  326. reg_cache_dir=registration_cache_dir_str,
  327. reg_fname_prefix=f"{subject}_contrast_",
  328. )
  329. # Apply the refined registration to both volume and mesh.
  330. rr_double_warped = np.array(
  331. apply_transforms_to_points(
  332. 3, _coords_to_dataframe(warped_rr), reg["invtransforms"]
  333. )
  334. )
  335. # Go from bounding box coordinates back to subject voxel coordinates.
  336. rr_final = rr_double_warped + cb_range[0]
  337. # Convert to FreeSurfer surface RAS coordinates.
  338. rr_ras = _convert_to_surface_ras(rr_final)
  339. return rr_ras, tris
  340. def _coords_to_dataframe(rr) -> "DataFrame":
  341. """Convert (N, 3) array of coords to a pandas DataFrame 'x', 'y', 'z'."""
  342. import pandas as pd
  343. rr_dictionary = {"x": list(rr[:, 0]), "y": list(rr[:, 1]), "z": list(rr[:, 2])}
  344. return pd.DataFrame(data=rr_dictionary)
  345. def _align_mesh_to_volume(
  346. rr: NDArray, scaling_factor: NDArray, target_coords: NDArray
  347. ) -> NDArray:
  348. """Scale and spatially translates surface mesh to align with a target bounding box.
  349. Parameters
  350. ----------
  351. rr : NDArray
  352. The N x 3 array of mesh vertices in the source voxel space.
  353. scaling_factor : NDArray
  354. The scaling factors for the X, Y, and Z axes (3 elements).
  355. target_coords : NDArray
  356. The coordinates of the target volume (e.g., high-intensity voxels) used
  357. to calculate the target bounding box.
  358. Returns
  359. -------
  360. NDArray
  361. The scaled and spatially shifted N x 3 mesh vertices.
  362. """
  363. rr_scaled = rr * scaling_factor
  364. # Correct vertices by co-registering lower left posterior and upper right
  365. # anterior corners.
  366. target_min_coords = np.min(target_coords, axis=0)
  367. target_max_coords = np.max(target_coords, axis=0)
  368. mesh_min = np.min(rr_scaled, axis=0)
  369. mesh_max = np.max(rr_scaled, axis=0)
  370. translation_vector = np.mean(
  371. [
  372. target_min_coords - mesh_min,
  373. target_max_coords - mesh_max,
  374. ],
  375. axis=0,
  376. )
  377. return rr_scaled + translation_vector
  378. def _scale_labels_majority_vote(
  379. hr_segm: NDArray, subj_segm: NDArray, scaling_factor: NDArray
  380. ) -> NDArray:
  381. """Scale the labels from the high-resolution segmentation to subject's segmentation.
  382. NOTE: This function could use some optimization. The custom logic could possibly be
  383. replaced with, for example, `scipy.ndimage.zoom`.
  384. Parameters
  385. ----------
  386. hr_segm : NDArray
  387. High-resolution segmentation volume as a 3D NumPy array.
  388. subj_segm : NDArray
  389. Subject's segmentation volume as a 3D NumPy array.
  390. scaling_factor : NDArray
  391. Scaling factor for each axis as a 1D NumPy array of length 3.
  392. Returns
  393. -------
  394. hr_label_scaled : NDArray
  395. Scaled labels volume as a 3D NumPy array.
  396. """
  397. # scale labels matrix (by type value vote)
  398. hr_label_scaled = np.zeros(
  399. (
  400. subj_segm.shape[0],
  401. subj_segm.shape[1],
  402. subj_segm.shape[2],
  403. )
  404. )
  405. count_matrix = np.zeros(
  406. (
  407. subj_segm.shape[0],
  408. subj_segm.shape[1],
  409. subj_segm.shape[2],
  410. 100,
  411. )
  412. )
  413. count_matrix[:] = np.nan
  414. for x in range(hr_segm.shape[0]):
  415. for y in range(hr_segm.shape[1]):
  416. for z in range(hr_segm.shape[2]):
  417. target_vox = (scaling_factor * (x, y, z)).astype(int)
  418. ind = np.min(
  419. np.where(
  420. np.isnan(
  421. count_matrix[target_vox[0], target_vox[1], target_vox[2], :]
  422. )
  423. )
  424. )
  425. count_matrix[target_vox[0], target_vox[1], target_vox[2], ind] = (
  426. hr_segm[x, y, z]
  427. )
  428. for x in range(subj_segm.shape[0]):
  429. for y in range(subj_segm.shape[1]):
  430. for z in range(subj_segm.shape[2]):
  431. votes = count_matrix[x, y, z, :]
  432. votes = votes[~np.isnan(votes)]
  433. hr_label_scaled[x, y, z] = np.bincount(votes.astype(int)).argmax()
  434. return hr_label_scaled
  435. def _crop_image_volume(
  436. vol: NDArray, coords: tuple[NDArray, ...], pad: int = 3
  437. ) -> tuple[NDArray, list]:
  438. """Crop a 3D volume to the bounding box of given coordinates, with optional padding.
  439. Parameters
  440. ----------
  441. vol : NDArray
  442. The 3D volume to be cropped.
  443. coords : tuple[NDArray, ...]
  444. Indices of elements to be included in the cropped volume. Typically obtained
  445. from `np.nonzero()`.
  446. pad : int, optional
  447. The number of voxels to pad around the bounding box. Default is 3.
  448. Returns
  449. -------
  450. vol_cropped : NDArray
  451. The cropped volume.
  452. coords_range : list
  453. List of two lists containing the minimum and maximum coordinates of the cropped
  454. volume [[min_x, min_y, min_z], [max_x, max_y, max_z]].
  455. """
  456. # coords_range is [[min_x, min_y, min_z], [max_x, max_y, max_z]]
  457. coords_range = [
  458. [max(0, np.min(coords[x]) - pad) for x in range(3)],
  459. [min(vol.shape[x], np.max(coords[x]) + pad + 1) for x in range(3)],
  460. ]
  461. vol_cropped = vol[
  462. coords_range[0][0] : coords_range[1][0],
  463. coords_range[0][1] : coords_range[1][1],
  464. coords_range[0][2] : coords_range[1][2],
  465. ]
  466. return vol_cropped, coords_range
  467. def _convert_to_surface_ras(rr: NDArray) -> NDArray:
  468. """Convert surface geometry to FreeSurfer surface RAS coordinates.
  469. Surface RAS is the FreeSurfer coordinate frame, making the output compatible with
  470. FreeSurfer tools like freeview.
  471. Parameters
  472. ----------
  473. rr : NDArray
  474. N x 3 Array of vertex positions in the FreeSurfer voxel space.
  475. Returns
  476. -------
  477. NDArray
  478. N x 3 Array of vertex positions in FreeSurfer surface RAS coordinates.
  479. """
  480. rotation = np.array([[-1, 0, 0], [0, 0, -1], [0, 1, 0]])
  481. translation = np.array([128, -128, 128])
  482. ras = rr @ rotation + translation
  483. return ras
  484. def _get_registration(
  485. fixed: "ANTsImage",
  486. moving: "ANTsImage",
  487. type_of_transform: str = "SyNCC",
  488. reg_cache_dir: str | None = None,
  489. reg_fname_prefix: str = "",
  490. ) -> dict:
  491. """Get or compute the registration between two images.
  492. Parameters
  493. ----------
  494. fixed : ANTsImage
  495. The fixed image for registration.
  496. moving : ANTsImage
  497. The moving image for registration.
  498. type_of_transform : str
  499. The type of transform to use for registration. Default is "SyNCC".
  500. reg_cache_dir : str | None
  501. Directory to cache registration results. If None (default), registration will
  502. be computed without caching.
  503. reg_fname_prefix : str
  504. Prefix for the registration output filenames. For example, if reg_fname_prefix
  505. is 'subject1_', the forward transform file will be saved as
  506. 'subject1_Composite.h5' and the inverse as
  507. 'subject1_InverseComposite.h5'.
  508. Returns
  509. -------
  510. dict
  511. Dictionary containing file paths for forward and inverse transforms,
  512. as returned by ANTs registration. Keys are 'fwdtransforms' and 'invtransforms'.
  513. """
  514. from ants.registration import registration
  515. if reg_cache_dir is None:
  516. # Perform registration without caching.
  517. logger.info("Performing registration...")
  518. reg = registration(
  519. fixed=fixed, moving=moving, type_of_transform=type_of_transform
  520. )
  521. logger.info("Registration complete.")
  522. return reg
  523. os.makedirs(reg_cache_dir, exist_ok=True)
  524. # Cache consists of forward and inverse tranforms.
  525. reg_forward_fname = f"{reg_fname_prefix}Composite.h5"
  526. reg_inverse_fname = f"{reg_fname_prefix}InverseComposite.h5"
  527. reg_forward_path = op.join(reg_cache_dir, reg_forward_fname)
  528. reg_inverse_path = op.join(reg_cache_dir, reg_inverse_fname)
  529. if op.exists(reg_forward_path) and op.exists(reg_inverse_path):
  530. logger.info(
  531. "Using existing registration transforms:\n %s\n %s",
  532. reg_forward_path,
  533. reg_inverse_path,
  534. )
  535. reg = {
  536. "fwdtransforms": [reg_forward_path],
  537. "invtransforms": [reg_inverse_path],
  538. }
  539. return reg
  540. # Perform registration and save the transforms to the cache directory.
  541. # ANTs will automatically append 'Composite.h5' and 'InverseComposite.h5'
  542. transform_fname_prefix = op.join(reg_cache_dir, reg_fname_prefix)
  543. logger.info(
  544. "Performing registration and saving transforms to cache:\n %s\n %s",
  545. reg_forward_path,
  546. reg_inverse_path,
  547. )
  548. reg = registration(
  549. fixed=fixed,
  550. moving=moving,
  551. type_of_transform=type_of_transform,
  552. outprefix=transform_fname_prefix, # save transforms
  553. write_composite_transform=True, # combine warp and affine to a single file
  554. )
  555. logger.info("Registration complete. Transforms saved to cache.")
  556. return reg
  557. def calculate_normals(
  558. rr, tris, solid_angle_calc=False, obs_point=np.zeros(3), print_info=True
  559. ):
  560. """Calculate vertex normals for a triangular mesh.
  561. Parameters
  562. ----------
  563. rr : ndarray
  564. Array of vertex positions.
  565. tris : ndarray
  566. Triangle indices defining the mesh faces.
  567. solid_angle_calc : bool, optional
  568. Whether to compute the solid angle from ``obs_point``.
  569. obs_point : ndarray, optional
  570. Observation point used for solid angle calculation.
  571. print_info : bool, optional
  572. Whether to print diagnostic information.
  573. Returns
  574. -------
  575. ndarray
  576. Vertex normals computed as an unweighted average of neighboring face normals.
  577. """
  578. A = []
  579. area_list = []
  580. area = 0.0
  581. count = 0
  582. nan_vertices = []
  583. for x in range(len(rr)):
  584. A.append([])
  585. solid_angle = 0
  586. for row in tris:
  587. v1 = rr[row[1], :] - rr[row[0], :]
  588. v2 = rr[row[2], :] - rr[row[0], :]
  589. nml = np.cross(v1, v2)
  590. area = area + np.linalg.norm(nml) / 2.0
  591. area_list.append(area)
  592. nn_fc = nml / np.linalg.norm(nml)
  593. A[row[0]].append(nn_fc)
  594. A[row[1]].append(nn_fc)
  595. A[row[2]].append(nn_fc)
  596. if solid_angle_calc:
  597. R1 = rr[row[0]] - obs_point
  598. R2 = rr[row[1]] - obs_point
  599. R3 = rr[row[2]] - obs_point
  600. solid_angle = solid_angle + 2 * np.arctan(
  601. np.dot(R1, np.cross(R2, R3))
  602. / (
  603. np.linalg.norm(R1) * np.linalg.norm(R2) * np.linalg.norm(R3)
  604. + np.dot(R1, R2) * np.linalg.norm(R3)
  605. + np.dot(R1, R3) * np.linalg.norm(R2)
  606. + np.dot(R2, R3) * np.linalg.norm(R1)
  607. )
  608. )
  609. if solid_angle_calc:
  610. logger.debug(
  611. "solid_angle at the point of observation estimated to be %f", solid_angle
  612. )
  613. nn = np.zeros((len(rr), 3))
  614. for c, ele in enumerate(A):
  615. vert_norm = np.zeros(3)
  616. for vec in ele:
  617. vert_norm = vert_norm + vec
  618. vert_norm = vert_norm / np.linalg.norm(vert_norm)
  619. nn[c, :] = vert_norm
  620. for c, ele in enumerate(A):
  621. if np.isnan(nn[c, :]).any(): # np.linalg.norm(nn[c,:]) == 0:
  622. neighbor_rows = np.where(tris == c)[0]
  623. neighbors = np.unique(tris[neighbor_rows])
  624. neighbors = neighbors[np.where(neighbors != c)]
  625. normal = np.mean(nn[neighbors, :], axis=0)
  626. nn[c, :] = normal / np.linalg.norm(normal)
  627. count = count + 1
  628. if np.isnan(nn[c, :]).any():
  629. nan_vertices.append(c)
  630. if print_info:
  631. logger.debug("number of nan normals that have been smoothed = %d", count)
  632. logger.debug("Remaining NAN normals = %d", len(nan_vertices))
  633. logger.debug("Total surface area: %f", area)
  634. return (nn, area, area_list, nan_vertices)
  635. def setup_full_source_space(
  636. subject: str,
  637. cerebellum_subsampling: Literal["full", "sparse", "dense"],
  638. subjects_dir: os.PathLike[str] | str | None = None,
  639. cerebellum_surf_fname: os.PathLike[str] | str | None = None,
  640. spacing: str | int = "oct6",
  641. ) -> mne.SourceSpaces:
  642. """Set up a full surface source space that includes the cerebellum.
  643. The first element in the returned `SourceSpaces` list is the combined cerebral
  644. hemispheric source space and the second element is the cerebellar source space.
  645. Parameters
  646. ----------
  647. subject : str
  648. The FreeSurfer subject name.
  649. cerebellum_subsampling : 'full' | 'sparse' | 'dense'
  650. The spacing used to create the cerebellar mesh.
  651. subjects_dir : path-like | None
  652. The path to the directory containing the FreeSurfer subjects reconstructions.
  653. If None, defaults to the SUBJECTS_DIR environment variable.
  654. cerebellum_surf_fname : path-like | None
  655. Path to the cerebellum surface mesh file. If None, defaults to
  656. ``<subjects_dir>/<subject>/surf/cerebellum_<cerebellum_subsampling>.white``
  657. spacing : str | int
  658. The spacing parameter for ``mne.setup_source_space``.
  659. Returns
  660. -------
  661. src_whole: mne.SourceSpaces
  662. List containing two source space elements: the cerebral cortex and the
  663. cerebellar cortex.
  664. """
  665. subjects_dir = mne.utils.get_subjects_dir(subjects_dir, raise_error=True) # pyright: ignore[reportAssignmentType]
  666. assert subjects_dir is not None, (
  667. "subjects_dir returned by mne.utils.get_subjects_dir should not be None."
  668. )
  669. subjects_dir = Path(subjects_dir)
  670. logger.info("Setting up cerebral source space for subject %s...", subject)
  671. src_cort = mne.setup_source_space(
  672. subject=subject,
  673. subjects_dir=subjects_dir,
  674. spacing=spacing, # pyright: ignore[reportArgumentType],
  675. add_dist=False,
  676. )
  677. if spacing == "all":
  678. # use_tris is None when spacing is 'all', idk if it is intentional.
  679. src_cort[0]["use_tris"] = src_cort[0]["tris"] # pyright: ignore
  680. src_cort[1]["use_tris"] = src_cort[1]["tris"] # pyright: ignore
  681. # Read the geometry of the cerebellar surface mesh.
  682. if cerebellum_surf_fname is None:
  683. cerebellum_surf_fname = (
  684. subjects_dir
  685. / subject
  686. / "surf"
  687. / f"cerebellum_{cerebellum_subsampling}.white"
  688. )
  689. surface_data = mne.read_surface(cerebellum_surf_fname)
  690. cerb_rr = surface_data[0]
  691. assert isinstance(cerb_rr, np.ndarray), "Surface vertices should be a NumPy array."
  692. cerb_rr = cerb_rr / 1000.0 # Convert from mm to m!
  693. cerb_tris = surface_data[1]
  694. assert isinstance(cerb_tris, np.ndarray), (
  695. "Surface triangles should be a NumPy array."
  696. )
  697. # Calculate normals.
  698. logger.info("Calculating normals on deformed surface...")
  699. (nn, _, _, nan_vertices) = calculate_normals(cerb_rr, cerb_tris, print_info=False)
  700. logger.info("Done.")
  701. # Make a dictionary to hold the cerebellar source space data.
  702. cerb_subj_data = {
  703. "rr": cerb_rr,
  704. "tris": cerb_tris,
  705. "nn": nn,
  706. "nan_nn": nan_vertices,
  707. }
  708. logger.info("Concatenating cerebellar source space with cerebral source space...")
  709. src_whole = src_cort.copy()
  710. hemi_src = _join_source_spaces(src_cort)
  711. src_whole[0] = hemi_src
  712. src_whole[1]["rr"] = cerb_rr # pyright: ignore[reportCallIssue, reportArgumentType]
  713. src_whole[1]["tris"] = cerb_subj_data["tris"] # pyright: ignore[reportArgumentType, reportCallIssue]
  714. src_whole[1]["nn"] = cerb_subj_data["nn"] # pyright: ignore[reportArgumentType, reportCallIssue]
  715. src_whole[1]["ntri"] = src_whole[1]["tris"].shape[0] # pyright: ignore[reportCallIssue, reportAttributeAccessIssue, reportArgumentType]
  716. src_whole[1]["use_tris"] = cerb_subj_data["tris"] # pyright: ignore[reportArgumentType, reportCallIssue]
  717. in_use = np.ones(cerb_rr.shape[0]).astype(int)
  718. in_use[cerb_subj_data["nan_nn"]] = 0
  719. src_whole[1]["inuse"] = in_use # pyright: ignore[reportArgumentType, reportCallIssue]
  720. src_whole[1]["nuse"] = int(np.sum(src_whole[1]["inuse"])) # pyright: ignore[reportArgumentType, reportCallIssue]
  721. src_whole[1]["vertno"] = np.nonzero(src_whole[1]["inuse"])[0] # pyright: ignore[reportArgumentType, reportCallIssue]
  722. src_whole[1]["np"] = src_whole[1]["rr"].shape[0] # pyright: ignore[reportCallIssue, reportAttributeAccessIssue, reportArgumentType]
  723. return src_whole
  724. def _join_source_spaces(src_orig: mne.SourceSpaces) -> dict:
  725. """Join the two hemispheric source spaces into a single source space."""
  726. if len(src_orig) != 2:
  727. raise ValueError("Input must be two source spaces")
  728. # Use the left hemisphere source space as the base for the joined source space.
  729. src_joined = src_orig.copy()[0]
  730. assert isinstance(src_joined, dict), "Source space should be a dictionary."
  731. src_joined["inuse"] = np.concatenate((src_orig[0]["inuse"], src_orig[1]["inuse"]))
  732. src_joined["nn"] = np.concatenate((src_orig[0]["nn"], src_orig[1]["nn"]), axis=0)
  733. src_joined["np"] = src_orig[0]["np"] + src_orig[1]["np"]
  734. src_joined["ntri"] = src_orig[0]["ntri"] + src_orig[1]["ntri"]
  735. src_joined["nuse"] = src_orig[0]["nuse"] + src_orig[1]["nuse"]
  736. src_joined["nuse_tri"] = src_orig[0]["nuse_tri"] + src_orig[1]["nuse_tri"]
  737. src_joined["rr"] = np.concatenate((src_orig[0]["rr"], src_orig[1]["rr"]), axis=0)
  738. src_joined["tris"] = np.concatenate(
  739. (src_orig[0]["tris"], src_orig[1]["tris"] + src_orig[0]["np"]), axis=0
  740. )
  741. if src_orig[0]["use_tris"] is None or src_orig[1]["use_tris"] is None:
  742. logger.info(
  743. "One of the source spaces has 'use_tris' set to None. Setting 'use_tris' "
  744. "to None for the joined source space."
  745. )
  746. src_joined["use_tris"] = None
  747. else:
  748. src_joined["use_tris"] = np.concatenate(
  749. (src_orig[0]["use_tris"], src_orig[1]["use_tris"] + src_orig[0]["np"]),
  750. axis=0,
  751. )
  752. src_joined["vertno"] = np.nonzero(src_joined["inuse"])[0]
  753. return src_joined

source_space.py at commit 7d2e668, under MIT · at the source

Overview

Authors: John G. Samuelsson1,2, Jeremy D. Schmahmann3, Martin I. Sereno4, Bruce Rosen1,2, Matti S. Hämäläinen1,2,5
  1. Mass General BrighamMassachusetts Institute of TechnologyHarvard Medical SchoolHarvard‐MIT Division of Health Sciences and Technology Cambridge Massachusetts USA
  2. Athinoula A. Martinos Center for Biomedical Imaging Charlestown Massachusetts USA
  3. Ataxia Center, Cognitive Behavioral Neurology Unit, Laboratory for Neuroanatomy and Cerebellar Neurobiology, Department of Neurology Mass General Brigham and Harvard Medical School Boston Massachusetts USA
  4. Psychology Department, San Diego State University and Cognitive Science Department University of California San Diego San Diego California USA
  5. Department of Neuroscience and Biomedical Engineering, School of Science Aalto University Espoo Finland
Journal: Human brain mapping, volume 47, issue 4, article e70497
Dates: received 4 August 2025; accepted 27 February 2026; published online 11 March 2026; in print March 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1002/hbm.70497 · PMID 41810813 · PMCID PMC12977127 · OpenAlex W7134946264
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: structural MRI / diffusion (modality), human (organism), methods / tools (subfield)
Methods: Machine learning
MeSH: Atlases as Topic*, Cerebellum*, Magnetic Resonance Imaging*, Adult, Brain Mapping, Female, Humans, Image Processing, Computer-Assisted, Male (* major topic)
Topic: Vestibular and auditory disorders (Neurology, Neuroscience), according to OpenAlex
Funding: NIH (5T32EB001680, 1P41EB030006, 5R01NS104585, 1F32MH127789, MH081990); UK Royal Society Wolfson Fellowship; National Ataxia Foundation; Great Oaks Foundation; Once Upon A Time Foundation; ARSACS Foundation; MINDlink Foundation
Citations: cited by 2 papers (Europe PMC); 62 references in the paper

Abstract

Interest in the cerebellum has surged with the emerging consensus that it supports diverse functions that are topographically arranged across the cerebellar cortex. Further refinement of these in vivo structure–function relationships is limited by the resolution of existing atlases. Here we present a digital atlas derived from a recent reconstruction of the human cerebellar cortical surface with a mean inter‐vertex spacing of 0.16 mm, sufficient to accurately trace the contours of the subfolia, while being consistent with the Schmahmann et al. atlas at the lobular level. We also present ARCUS, a diffeomorphic atlas‐to‐subject registration approach that yields an atlas‐derived, lobule‐labeled cerebellar cortical sheet with macroscale folding geometry in individual subjects from standard‐resolution MRI. Publicly released, this atlas offers an anatomical ground‐truth reference in both volumetric and surface representations at unprecedented granularity, enabling novel and more precise analyses and visualizations of cerebellar data.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repository

Its files are read in the Code ↔ Paper reader above, with 4 matches between paragraphs and lines of code.

johnsam7/ceremegbellum

License: MIT
State: the link answers, verified on 30 September 2026
Evidence: files inventoried
Commit: 7d2e6682b99c59961df992437ddcbdf670ed5810, 10 September 2026
Languages: Python (17)
Size: 23 files, 17 scripts
Software Heritage: not archived
Found in: “Data Availability Statement”
Holds: README, license file, environment (pyproject.toml), tests, continuous integration
Not found: CITATION.cff, documentation
Tools: NumPy (12 files), MNE-Python (10 files), NiBabel (8 files), ANTs (4 files), Matplotlib (2 files), SciPy (2 files), nnU-Net (1 file), pandas (1 file), PyTorch (1 file), scikit-learn (1 file)
Availability: 1 check, the latest on 30 September 2026: the link answers
  • 30 September 2026: the link answers
19 files

The paper's code and data availability statement is in the Data section.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 17 scripts, each with its path and the digest of its content;
  • 4 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

Datasets cited

Data Availability Statement

All atlas data are available for download at https://osf.io/98p3a/?view_only=933654b10152444992b9e7d8ff9f1112 and the original full resolution volume and surface data are available at https://pages.ucsd.edu/~msereno/cereb. ARCUS can be accessed at https://github.com/johnsam7/ceremegbellum.git. The image processing scripts used in the creation of the atlas are available from the corresponding author upon reasonable request.

Reproduced under the paper's license (CC BY), from the paper cited above.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 1, 30 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 5 authors, 9 MeSH terms, 7 funders, 46 references.

Cite

This paper

Samuelsson, J. G., Schmahmann, J. D., Sereno, M. I., Rosen, B., & Hämäläinen, M. S. (2026). A Digital Anatomical Atlas of the Human Cerebellum at Subfolial Resolution. Human brain mapping, 47(4), e70497. https://doi.org/10.1002/hbm.70497

BibTeX

@article{samuelsson2026digital,
author = {Samuelsson, John G. and Schmahmann, Jeremy D. and Sereno, Martin I. and Rosen, Bruce and Hämäläinen, Matti S.},
title = {{A Digital Anatomical Atlas of the Human Cerebellum at Subfolial Resolution}},
journal = {Human brain mapping},
year = {2026},
month = mar,
volume = {47},
number = {4},
pages = {e70497},
publisher = {Wiley},
issn = {1065-9471},
doi = {10.1002/hbm.70497},
url = {https://doi.org/10.1002/hbm.70497},
pmid = {41810813},
pmcid = {PMC12977127}
}

RIS

TY - JOUR
AU - Samuelsson, John G.
AU - Schmahmann, Jeremy D.
AU - Sereno, Martin I.
AU - Rosen, Bruce
AU - Hämäläinen, Matti S.
TI - A Digital Anatomical Atlas of the Human Cerebellum at Subfolial Resolution
T2 - Human brain mapping
J2 - Hum Brain Mapp
PY - 2026
DA - 2026/03/01
VL - 47
IS - 4
SP - e70497
SN - 1065-9471
PB - Wiley
DO - 10.1002/hbm.70497
UR - https://doi.org/10.1002/hbm.70497
LA - en
ER -

CSL-JSON

{
"id": "10.1002/hbm.70497",
"type": "article-journal",
"title": "A Digital Anatomical Atlas of the Human Cerebellum at Subfolial Resolution",
"container-title": "Human brain mapping",
"author": [
{
"family": "Samuelsson",
"given": "John G."
},
{
"family": "Schmahmann",
"given": "Jeremy D."
},
{
"family": "Sereno",
"given": "Martin I."
},
{
"family": "Rosen",
"given": "Bruce"
},
{
"family": "Hämäläinen",
"given": "Matti S."
}
],
"container-title-short": "Hum Brain Mapp",
"volume": "47",
"issue": "4",
"page": "e70497",
"DOI": "10.1002/hbm.70497",
"PMID": "41810813",
"PMCID": "PMC12977127",
"ISSN": "1065-9471",
"publisher": "Wiley",
"URL": "https://doi.org/10.1002/hbm.70497",
"language": "en",
"issued": {
"date-parts": [
[
2026,
3,
1
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1016/j.neuroimage.2026.121930
Quantifying cerebellar signal detectability in MEG and EEG in epilepsy using anatomically informed source modeling.
Journal: NeuroImage
In common: structural MRI / diffusion, 14 references
[2] doi:10.1162/imag.a.1323 [code]
SUITPy: A Python-based toolbox for the analysis of cerebellar functional and anatomical imaging data across the human lifespan.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: ANTs, NiBabel, pandas, 3 other tools, methods / tools, 9 references
[3] doi:10.1038/s41467-026-72940-5 [code]
Cerebellar growth is associated with domain-specific cerebral maturation and socio-linguistic behavior.
Journal: Nature communications
In common: NiBabel, scikit-learn, pandas, 3 other tools, 7 references
[4] doi:10.21203/rs.3.rs-9326213/v1 [code]
Multi-task fMRI outperforms resting-state fMRI for revealing task-invariant organization of the human brain
Journal: Research Square (preprint)
In common: ANTs, NiBabel, PyTorch, 5 other tools, 5 references
[5] doi:10.64898/2026.03.09.710558 [code]
Multi-task fMRI outperforms resting-state fMRI for revealing task-invariant organization of the human brain
Journal: bioRxiv (preprint)
In common: ANTs, NiBabel, PyTorch, 5 other tools, 5 references
[6] doi:10.1038/s41467-026-72931-6 [code]
Three parsimonious spatiotemporal patterns in cerebellum reveal individual traits in function and behavior.
Journal: Nature communications
In common: NiBabel, PyTorch, scikit-learn, 4 other tools, 5 references
[7] doi:10.1002/epi.70296 [code]
Fully automated three-dimensional deep learning-based magnetic resonance imaging segmentation of brain cavities in epilepsy surgery.
Journal: Epilepsia
In common: nnU-Net, ANTs, NiBabel, 5 other tools, structural MRI / diffusion, 3 references
[8] doi:10.1002/alz.71649 [code]
Postmortem brain MRI reveals differential associations of subcortical and limbic volumes with cortical thinning and neurodegenerative pathologies.
Journal: Alzheimer's & dementia : the journal of the Alzheimer's Association
In common: nnU-Net, ANTs, NiBabel, 6 other tools, structural MRI / diffusion, 1 reference
[9] doi:10.3390/jimaging12070276 [code]
Hyperelastic Regularization for Near-Diffeomorphic Transformer-Based Brain MRI Registration.
Journal: Journal of imaging
In common: nnU-Net, ANTs, NiBabel, 6 other tools, methods / tools, structural MRI / diffusion
[10] doi:10.1186/s12880-026-02335-x [code]
Automatic lateral ventricle and choroid plexus segmentation method in infant brain MR images.
Journal: BMC medical imaging
In common: nnU-Net, NiBabel, PyTorch, 5 other tools, structural MRI / diffusion, 2 references

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.