OSCR

Mapping neuro-vascular unit communications reveals distinct angiogenic programs across developing mouse brain regions.

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] § Methods › Analysis of postnatal brain angiogenesis › Analysis of the vessel density per brain area ↔ liom_toolkit/segmentation/stats.py, lines 148–206 · score 0.61 · vessel density, vessel area, Brain regions, voxel, regional, masks
  2. [2] § Methods › Analysis of postnatal brain angiogenesis › Vessel segmentation ↔ liom_toolkit/segmentation/plane_segmentation.py, lines 9–24 · score 0.61 · Scikit Image, LIOM Toolkit, segmentation module
  3. [3] § Methods › Analysis of postnatal brain angiogenesis › Vessel segmentation ↔ liom_toolkit/segmentation/stats.py, lines 534–584 · score 0.53 · vessel density, scikit-image, Heatmap, creation, module, segmented
  4. [4] § Methods › Spatial transcriptomics › Data processing ↔ liom_toolkit/utils/io.py, lines 260–384 · score 0.50 · nearest neighbor, location, filtered

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 · 725 lines · 27 KB · GPL-3.0 · 2 matches

  1. """Per-region vessel morphometric statistics and Allen atlas region filtering."""
  2. from __future__ import annotations
  3. import math
  4. import tempfile
  5. from pathlib import Path
  6. from typing import Any
  7. import dask.array as da
  8. import imageio.v3 as iio
  9. import numpy as np
  10. import PIL.Image
  11. from dask.distributed import Future
  12. from numpy.typing import NDArray
  13. from tqdm.auto import tqdm
  14. # pandas + scipy + scikit-image are moved into the [seg]/[stats] extras
  15. # (D-01/D-05). The upfront ImportError here is the honest signal on an
  16. # io-only install. pandas is shared between [stats] and [antspy]; scipy is
  17. # shared between [seg] and [stats] -- the message names both so the user
  18. # picks the extra matching their workflow. The `from e` chain preserves the
  19. # underlying error for debugging (AGENTS §2).
  20. try:
  21. import pandas as pd
  22. import scipy.ndimage as ndi
  23. from scipy.ndimage import distance_transform_edt
  24. from skimage import measure
  25. from skimage.color import gray2rgb
  26. from skimage.draw import circle_perimeter
  27. from skimage.measure import label
  28. from skimage.measure._regionprops import RegionProperties
  29. from skimage.morphology import skeletonize
  30. from skimage.util import img_as_ubyte
  31. except ImportError as e:
  32. raise ImportError(
  33. "Please install liom-toolkit[seg] or [stats] to use the segmentation stats module."
  34. ) from e
  35. from liom_toolkit.utils.dask_client import dask_client_manager
  36. PIL.Image.MAX_IMAGE_PIXELS = 2_000_000_000 # finite DoS-guard limit (not None — AGENTS §2)
  37. # Precomputed branching-point detection kernel + valid signature set (PERF-01b).
  38. # The inherited get_branching_points ran 5 structural elements x 4 rotations =
  39. # 20 ndi.binary_hit_or_miss calls. This single-kernel replacement encodes each
  40. # of the 20 valid 3x3 branching patterns as a unique integer signature: the 8
  41. # neighbor positions carry distinct bit weights and the signature is the sum of
  42. # weights at foreground positions (the center carries weight 0 -- it is always
  43. # foreground in a branching point). One ndi.convolve pass with the bit-weight
  44. # kernel produces the per-pixel neighbor signature; a foreground pixel whose
  45. # signature is in the valid set is a branching point. The result is array_equal
  46. # to the old 20-convolution result (numerical-equivalence regression test).
  47. _BRANCHING_KERNEL = np.array([[1, 2, 4], [8, 0, 16], [32, 64, 128]], dtype=np.int32)
  48. def _build_branching_signatures() -> np.ndarray:
  49. """Build the sorted array of valid branching-point neighbor signatures.
  50. Returns
  51. -------
  52. np.ndarray
  53. Sorted 1-D ``int32`` array of the 20 valid neighbor signatures.
  54. """
  55. base_selems = [
  56. np.array([[0, 1, 0], [1, 1, 1], [0, 0, 0]]),
  57. np.array([[1, 0, 1], [0, 1, 0], [1, 0, 0]]),
  58. np.array([[1, 0, 1], [0, 1, 0], [0, 1, 0]]),
  59. np.array([[0, 1, 0], [1, 1, 0], [0, 0, 1]]),
  60. np.array([[0, 0, 1], [1, 1, 1], [0, 1, 0]]),
  61. ]
  62. selems = [np.rot90(s, k=j) for s in base_selems for j in range(4)]
  63. sigs = {int((s * _BRANCHING_KERNEL).sum()) for s in selems}
  64. return np.array(sorted(sigs), dtype=np.int32)
  65. _BRANCHING_SIGNATURES = _build_branching_signatures()
  66. def compute_slice_metrics(
  67. output_dir: str,
  68. image: str,
  69. mask: NDArray[np.number],
  70. vessel_mask: NDArray[np.number],
  71. region_map: NDArray[np.number],
  72. vessel_exclude: NDArray[np.number],
  73. voxel_size: float = 0.65,
  74. ) -> None:
  75. """Compute the metrics for a brain slice and save the results to disk.
  76. ``image`` is a string label (filename/identifier) used in the output
  77. DataFrame and progress messages, not an image array. The actual image
  78. arrays are ``mask``, ``vessel_mask``, ``region_map``, and
  79. ``vessel_exclude``.
  80. Vessel-free regions: regions with no vessels yield a row with vessel
  81. density = 0.0, vessel area = 0.0, and branching points = 0, but the
  82. 'mean diameter (um)' entry is OMITTED (the row has no such column value)
  83. because the mean diameter of an empty vessel set is undefined. The
  84. omitted diameter row is itself a publishable 'no vessels detected'
  85. signal, not a silent gap.
  86. Parameters
  87. ----------
  88. output_dir : str
  89. The directory to save the output to.
  90. image : str
  91. The label (filename/identifier) of the brain slice, used in the
  92. output DataFrame and progress messages.
  93. mask : ArrayLike
  94. The mask of the tissue in the brain slice.
  95. vessel_mask : ArrayLike
  96. The mask of the vessels in the brain slice.
  97. region_map : ArrayLike
  98. The map of the regions in the brain slice.
  99. vessel_exclude : ArrayLike
  100. The mask of the vessels to exclude from the analysis.
  101. voxel_size : float
  102. The size of the voxels in the image.
  103. Notes
  104. -----
  105. ``ValueError`` propagates from :func:`calculate_regional_density` when a
  106. region has zero area (bad region mask, caller error).
  107. """
  108. # Setup output directory (overwrite-safe via the symlink-aware
  109. # create_directory helper from utils.zarr_writer: a second call into an
  110. # existing output_dir shutil.rmtree's the directory then recreates it,
  111. # eliminating the FileExistsError race on re-run. Function-scope import
  112. # avoids a circular import with utils.zarr_writer at module load time,
  113. # matching the conversion.py:save_zarr pattern.)
  114. from liom_toolkit.utils.zarr_writer import create_directory
  115. create_directory(Path(output_dir), overwrite=True)
  116. df = pd.DataFrame(
  117. columns=[
  118. "image",
  119. "region",
  120. "vessel area (um2)",
  121. "tissue area (um2)",
  122. "vessel density (um2/um2)",
  123. "branching points",
  124. "mean diameter (um)",
  125. ]
  126. )
  127. # Get the different brain regions
  128. regions, region_count = label(region_map, return_num=True)
  129. props_list = measure.regionprops(regions)
  130. full_vessel_mask = vessel_mask * mask
  131. full_vessel_mask = full_vessel_mask * vessel_exclude
  132. # Compute metrics per region
  133. for i in tqdm(
  134. range(region_count), desc="Computing metrics per region for " + image, leave=False
  135. ):
  136. region = get_vessel_region(regions, i, full_vessel_mask)
  137. # Calculate vessel density
  138. vessel_area, total_area, density = calculate_regional_density(
  139. region, i, props_list, output_dir, voxel_size
  140. )
  141. # Count branching points
  142. branching_points_count, skeleton, branching_points = get_branching_point_count(
  143. region, output_dir, filename=str(i) + "_skeleton.tif"
  144. )
  145. draw_branch_point_circles(
  146. skeleton, branching_points, output_dir, filename=str(i) + "_skeleton_circled.png"
  147. )
  148. # Calculate average diameter. Vessel-free regions raise ValueError
  149. # from compute_average_diameter (mean diameter of an empty set is
  150. # undefined); the diameter row is OMITTED for those regions while
  151. # the density=0.0 row above is kept. The omitted diameter row is
  152. # itself a publishable 'no vessels detected' signal, not a silent
  153. # gap.
  154. try:
  155. mean_diameter = compute_average_diameter(region, skeleton, voxel_size)
  156. # Save data
  157. entry = pd.DataFrame.from_dict(
  158. {
  159. "image": [image],
  160. "region": [i],
  161. "vessel area (um2)": [vessel_area],
  162. "tissue area (um2)": [total_area],
  163. "vessel density (um2/um2)": [density],
  164. "branching points": [branching_points_count],
  165. "mean diameter (um)": [mean_diameter],
  166. }
  167. )
  168. except ValueError:
  169. # Vessel-free region: density=0.0 row kept, diameter row omitted.
  170. entry = pd.DataFrame.from_dict(
  171. {
  172. "image": [image],
  173. "region": [i],
  174. "vessel area (um2)": [vessel_area],
  175. "tissue area (um2)": [total_area],
  176. "vessel density (um2/um2)": [density],
  177. "branching points": [branching_points_count],
  178. }
  179. )
  180. df = pd.concat([df, entry])
  181. # Compute metrics for the whole slice. Use full_vessel_mask
  182. # (vessel_mask * mask * vessel_exclude) for the density calculation so
  183. # the 'total' row is consistent with the per-region rows, which all
  184. # derive their vessel area from full_vessel_mask via get_vessel_region.
  185. # The pre-fix code passed the raw vessel_mask, so the total row's
  186. # vessel_area and vessel_density included vessels outside the tissue
  187. # mask and explicitly excluded vessels -- making the total density
  188. # higher than the sum of per-region densities (a data inconsistency
  189. # in the same output DataFrame).
  190. tissue_area, vessel_area, vessel_density = calculate_density(full_vessel_mask, mask, voxel_size)
  191. # Use full_vessel_mask (vessel_mask * mask * vessel_exclude) for the
  192. # whole-slice branching and diameter calculations so the 'total' row is
  193. # consistent with the per-region rows, which all derive their vessel area
  194. # from full_vessel_mask via get_vessel_region. The pre-fix code passed
  195. # the raw vessel_mask, so the total row's branching count and diameter
  196. # included vessels outside the tissue mask and explicitly excluded
  197. # vessels -- inconsistent with the per-region rows in the same DataFrame.
  198. branching_points_count, skeleton, branching_points = get_branching_point_count(
  199. full_vessel_mask, output_dir
  200. )
  201. draw_branch_point_circles(skeleton, branching_points, output_dir)
  202. # Whole-slice diameter: wrap in try/except ValueError mirroring the
  203. # per-region row-omission pattern above. A vessel-free slice (no vessels
  204. # anywhere) hits the empty-vessel-set ValueError from
  205. # compute_average_diameter (D-01 contract); without this wrap the whole
  206. # metrics computation crashes and the user loses every per-region row
  207. # computed up to this point. The 'total' row keeps the density=0.0 /
  208. # branching-points / vessel-area entries and OMITS the mean-diameter
  209. # entry, the same publishable 'no vessels detected' signal used for
  210. # vessel-free regions.
  211. try:
  212. mean_diameter = compute_average_diameter(full_vessel_mask, skeleton, voxel_size)
  213. total_entry = {
  214. "image": [image],
  215. "region": "total",
  216. "vessel area (um2)": [vessel_area],
  217. "tissue area (um2)": [tissue_area],
  218. "vessel density (um2/um2)": [vessel_density],
  219. "branching points": [branching_points_count],
  220. "mean diameter (um)": [mean_diameter],
  221. }
  222. except ValueError:
  223. # Vessel-free slice: density=0.0 row kept, diameter row omitted.
  224. total_entry = {
  225. "image": [image],
  226. "region": "total",
  227. "vessel area (um2)": [vessel_area],
  228. "tissue area (um2)": [tissue_area],
  229. "vessel density (um2/um2)": [vessel_density],
  230. "branching points": [branching_points_count],
  231. }
  232. # Save intermediate results. The mask/region/skeleton arrays are
  233. # small-integer label/mask arrays, so an explicit .astype(np.uint8)
  234. # narrowing is used instead of skimage.util.img_as_ubyte: img_as_ubyte
  235. # on an integer input whose max fits in uint8 emits a "Downcasting ...
  236. # without scaling" UserWarning (skimage telling us it skipped the
  237. # rescale), and the explicit astype is identical output with no warning
  238. # and clearer intent (we are narrowing a small-integer mask, not
  239. # rescaling a float image).
  240. iio.imwrite(str(Path(output_dir) / "regions.png"), regions.astype(np.uint8))
  241. iio.imwrite(str(Path(output_dir) / "vessel_exclude.png"), vessel_exclude.astype(np.uint8))
  242. iio.imwrite(str(Path(output_dir) / "_complete_mask.png"), mask.astype(np.uint8))
  243. iio.imwrite(str(Path(output_dir) / "vessels.png"), vessel_mask.astype(np.uint8))
  244. # Save data
  245. entry = pd.DataFrame.from_dict(total_entry)
  246. df = pd.concat([df, entry])
  247. df.to_excel(str(Path(output_dir) / "regions.xlsx"), index=False)
  248. def get_vessel_region(
  249. regions: NDArray[np.number], region_index: int, vessel_mask: NDArray[np.number]
  250. ) -> NDArray[np.number]:
  251. """Get the vessels in a region.
  252. Parameters
  253. ----------
  254. regions : ArrayLike
  255. The regions of the tissue mask.
  256. region_index : int
  257. The index of the region.
  258. vessel_mask : ArrayLike
  259. The mask of the vessels.
  260. Returns
  261. -------
  262. NDArray[np.generic]
  263. The vessel within the masked region.
  264. """
  265. region = regions == region_index + 1
  266. return region * vessel_mask
  267. def calculate_regional_density(
  268. region: NDArray[np.number],
  269. region_index: int,
  270. props_list: list[RegionProperties],
  271. output_dir: str,
  272. voxel_size: float = 0.65,
  273. ) -> tuple[float, float, float]:
  274. """Calculate the density of vessels in a region.
  275. Parameters
  276. ----------
  277. region : ArrayLike
  278. The region to calculate the density of.
  279. region_index : int
  280. The computational index of the region.
  281. props_list : list[RegionProperties]
  282. The list of properties of the regions.
  283. output_dir : str
  284. The directory to save the region mask to.
  285. voxel_size : float
  286. The size of the voxels in the image.
  287. Returns
  288. -------
  289. tuple[float, float, float]
  290. The area of the vessels, the area of the region, and the density of
  291. the vessels in a specific region.
  292. Raises
  293. ------
  294. ValueError
  295. If the region has zero area (bad region mask, caller error).
  296. """
  297. vessel_area = float((region == 1).sum()) * math.pow(voxel_size, 2)
  298. total_area = float(props_list[region_index].area) * math.pow(voxel_size, 2)
  299. if total_area == 0:
  300. raise ValueError("Empty region: regionprops area is 0 (bad region mask, caller error)")
  301. iio.imwrite(str(Path(output_dir) / f"{region_index}.tif"), region.astype(np.uint8))
  302. density = vessel_area / total_area
  303. return vessel_area, total_area, density
  304. def calculate_density(
  305. vessel_mask: NDArray[np.number], mask: NDArray[np.number], voxel_size: float = 0.65
  306. ) -> tuple[float, float, float]:
  307. """Calculate the areas of the tissue and vessel to compute vessel density in a mask.
  308. Empty-result contract:
  309. * Empty vessel mask over positive tissue (``mask.sum() > 0``) returns
  310. ``(tissue_area, 0.0, 0.0)`` -- 0 vessels / positive tissue area is a
  311. well-defined 0 density (the math is defined).
  312. * Empty tissue mask (``mask.sum() == 0``) raises ``ValueError`` -- an
  313. empty tissue mask is a bad region mask and a caller error, not a
  314. valid result.
  315. Parameters
  316. ----------
  317. vessel_mask : ArrayLike
  318. The mask of the vessels.
  319. mask : ArrayLike
  320. The mask of the tissue.
  321. voxel_size : float
  322. The size of the voxels in the image.
  323. Returns
  324. -------
  325. tuple[float, float, float]
  326. The area of the tissue, the area of the vessels, and the density of
  327. the vessels.
  328. Raises
  329. ------
  330. ValueError
  331. If the tissue mask is empty (``mask.sum() == 0``).
  332. """
  333. tissue_area = float(mask.sum()) * math.pow(voxel_size, 2)
  334. if tissue_area == 0:
  335. raise ValueError("Empty tissue mask: mask.sum() == 0 (bad region mask, caller error)")
  336. vessel_area = float(vessel_mask.sum()) * math.pow(voxel_size, 2)
  337. vessel_density = vessel_area / tissue_area
  338. return tissue_area, vessel_area, vessel_density
  339. def get_branching_point_count(
  340. vessel_mask: NDArray[np.number], output_dir: str, filename: str = "skeleton.tif"
  341. ) -> tuple[int, NDArray[np.bool_], NDArray[np.bool_]]:
  342. """Get the number of branching points in a vessel mask.
  343. Parameters
  344. ----------
  345. vessel_mask : ArrayLike
  346. The mask of the vessels.
  347. output_dir : str
  348. The directory to save the skeleton to.
  349. filename : str
  350. The filename to save the skeleton to.
  351. Returns
  352. -------
  353. tuple[int, NDArray[np.bool_], NDArray[np.bool_]]
  354. The number of branching points in the vessel mask, the skeleton of
  355. the vessel mask, and the location of the branching points.
  356. """
  357. skeleton = skeletonize(vessel_mask)
  358. branching_points = get_branching_points(skeleton)
  359. points_count = branching_points.sum()
  360. iio.imwrite(str(Path(output_dir) / filename), skeleton.astype(np.uint8))
  361. return points_count, skeleton, branching_points
  362. def get_branching_points(skeleton: NDArray[np.bool_]) -> NDArray[np.bool_]:
  363. """Get the branching points in a skeleton using a single convolution pass.
  364. Replaces the inherited 20-convolution implementation (5 structural
  365. elements x 4 rotations = 20 ``ndi.binary_hit_or_miss`` calls OR-ed
  366. together) with a single ``ndi.convolve`` pass over a bit-packed 3x3
  367. neighbor-signature kernel, followed by a vectorized membership check
  368. against the 20 valid branching-point signatures.
  369. Each of the 20 valid 3x3 patterns is encoded as a unique integer
  370. signature: the 8 neighbor positions carry distinct bit weights
  371. (1, 2, 4, 8, 16, 32, 64, 128) and the signature is the sum of weights at
  372. foreground positions (the center is always foreground in a branching
  373. point, so it carries weight 0). A single convolution with the bit-weight
  374. kernel produces the per-pixel neighbor signature; a foreground pixel
  375. whose signature is in the valid set is a branching point. The result is
  376. ``array_equal`` to the old 20-convolution result (gated by a
  377. numerical-equivalence regression test asserting ``array_equal``, not
  378. ``allclose`` -- branching-point detection is a boolean topology operation).
  379. Source:
  380. https://stackoverflow.com/questions/43037692/how-to-find-branch-point-from-binary-skeletonize-image
  381. Parameters
  382. ----------
  383. skeleton : ArrayLike
  384. The skeleton of the vessels.
  385. Returns
  386. -------
  387. NDArray[np.bool_]
  388. The branching points in the skeleton.
  389. """
  390. conv = ndi.convolve(skeleton.astype(np.int32), _BRANCHING_KERNEL, mode="constant", cval=0)
  391. return np.isin(conv, _BRANCHING_SIGNATURES) & skeleton.astype(bool)
  392. def draw_branch_point_circles(
  393. skeleton: NDArray[np.bool_],
  394. branching_points: NDArray[np.bool_],
  395. output_dir: str,
  396. filename: str = "skeleton_circled.png",
  397. ) -> None:
  398. """Draw circles around the branching points in a skeleton and save to disk.
  399. Parameters
  400. ----------
  401. skeleton : ArrayLike
  402. The skeleton of the vessels.
  403. branching_points : ArrayLike
  404. The location of the branching points.
  405. output_dir : str
  406. The directory to save the skeleton to.
  407. filename : str
  408. The filename to save the skeleton to.
  409. """
  410. circled_skeleton = gray2rgb(skeleton.astype(np.uint8))
  411. points_to_draw = np.argwhere(branching_points)
  412. for point in points_to_draw:
  413. circy, circx = circle_perimeter(point[0], point[1], 7, shape=skeleton.shape)
  414. circled_skeleton[circy, circx] = (220, 20, 20)
  415. iio.imwrite(str(Path(output_dir) / filename), circled_skeleton)
  416. del circled_skeleton
  417. def compute_average_diameter(
  418. mask: NDArray[np.number], skeleton: NDArray[np.bool_], voxel_size: float = 0.65
  419. ) -> float:
  420. """Compute the average diameter of the vessels in a mask.
  421. Empty-result contract:
  422. * Empty vessel set (no positive radii in the skeleton) raises
  423. ``ValueError`` -- the mean diameter of an empty set is undefined;
  424. returning 0.0 would imply zero-width vessels exist (a
  425. plausible-shaped-but-wrong value), and returning ``NaN`` would let a
  426. silent NaN escape into the published pandas DataFrame. The raise sits
  427. BEFORE ``np.mean`` so no NaN + RuntimeWarning can escape.
  428. * Empty tissue mask raises ``ValueError`` -- mean diameter is undefined
  429. when there is no tissue.
  430. Parameters
  431. ----------
  432. mask : ArrayLike
  433. The vessel mask.
  434. skeleton : ArrayLike
  435. The skeleton of the vessels.
  436. voxel_size : float
  437. The size of the voxels in the image.
  438. Returns
  439. -------
  440. float
  441. The average diameter of the vessels in the mask.
  442. Raises
  443. ------
  444. ValueError
  445. If the vessel mask is empty or no positive radii are found in the
  446. skeleton.
  447. TypeError
  448. If the computed average diameter is not a float.
  449. """
  450. if mask.sum() == 0:
  451. raise ValueError("Empty tissue mask: mean diameter is undefined")
  452. distance = distance_transform_edt(mask.astype(np.float64))
  453. radii = distance * skeleton.astype(bool)
  454. positive_radii = radii[radii > 0]
  455. if positive_radii.size == 0:
  456. raise ValueError(
  457. "Empty vessel set: mean diameter is undefined (no positive radii in skeleton)"
  458. )
  459. mean_radius = np.mean(positive_radii)
  460. mean_diameter = 2 * mean_radius
  461. result = mean_diameter * voxel_size
  462. if not isinstance(result, float):
  463. raise TypeError(f"Expected float, got {type(result)}")
  464. return result
  465. def create_heatmap(image: NDArray[np.number], output_dir: str, square_size: int = 150) -> None:
  466. """Create and save a heatmap of the vessel density in a brain slice.
  467. Parameters
  468. ----------
  469. image : ArrayLike
  470. The image of the brain slice.
  471. output_dir : str
  472. The directory to save the heatmap to.
  473. square_size : int
  474. The size of the squares in the heatmap.
  475. """
  476. # Overwrite-safe output directory creation (same create_directory helper
  477. # as compute_slice_metrics above; function-scope import avoids a circular
  478. # import with utils.zarr_writer at module load time).
  479. from liom_toolkit.utils.zarr_writer import create_directory
  480. create_directory(Path(output_dir), overwrite=True)
  481. image = img_as_ubyte(image)
  482. image = image / 255
  483. image = image.astype(np.uint8)
  484. heatmap = np.zeros_like(image, dtype=np.uint32)
  485. # Compute the per-dimension block counts separately so non-square images
  486. # iterate the correct number of squares per dimension (a single
  487. # shape[0]/square_size count for both dims produces a staircase pattern
  488. # on non-square or multi-row images).
  489. n_x = int(image.shape[0] / square_size)
  490. n_y = int(image.shape[1] / square_size)
  491. x_start = 0
  492. for _ in range(n_x):
  493. y_start = 0 # reset at the start of each outer iteration
  494. for _j in range(n_y):
  495. heatmap[x_start : x_start + square_size, y_start : y_start + square_size] = image[
  496. x_start : x_start + square_size, y_start : y_start + square_size
  497. ].sum()
  498. y_start += square_size
  499. x_start += square_size
  500. # Set final square to max value to ensure same scaling across heatmaps
  501. heatmap[-1, -1] = square_size**2
  502. # heatmap is uint32 with max square_size**2 (e.g. 22500), which fits in
  503. # uint16 without scaling. Use an explicit .astype(np.uint16) narrowing
  504. # instead of skimage.util.img_as_uint: img_as_uint on an integer input
  505. # whose max fits in uint16 emits a "Downcasting ... without scaling"
  506. # UserWarning, and the explicit astype is identical output with no
  507. # warning and clearer intent.
  508. heatmap = heatmap.astype(np.uint16)
  509. heatmap = heatmap.astype(float)
  510. heatmap = heatmap / (square_size**2)
  511. iio.imwrite(str(Path(output_dir) / "heatmap.tif"), heatmap)
  512. def generate_itk_id_list_of_region(region: str, data_dir: str = "") -> list[int]:
  513. """Generate a list of itk ids for a given region.
  514. Reconstructs the structure tree and gets the descendants contained
  515. within the region.
  516. Parameters
  517. ----------
  518. region : str
  519. The region to get the ids for.
  520. data_dir : str
  521. The directory where the atlas and structure tree are saved. Optional.
  522. Returns
  523. -------
  524. list[int]
  525. The list of itk ids for the region and its descendants.
  526. Raises
  527. ------
  528. TypeError
  529. If the extracted itk ids are not a list.
  530. """
  531. # Setup temporary directory if not given. Track whether WE created it
  532. # so the cleanup runs unconditionally (the pre-fix code reassigned
  533. # data_dir = temp_dir.name then re-tested ``if data_dir == ""``, which
  534. # was always False after the reassignment, so temp_dir.cleanup() never
  535. # ran and the temp directory leaked on every call where data_dir="" --
  536. # the default).
  537. use_temp = data_dir == ""
  538. temp_dir: tempfile.TemporaryDirectory[str] | None = None
  539. if use_temp:
  540. temp_dir = tempfile.TemporaryDirectory()
  541. data_dir = temp_dir.name
  542. itk_ids: list[Any] = []
  543. try:
  544. # Construct reference space and get itk ids
  545. from liom_toolkit.utils import construct_reference_space
  546. rs = construct_reference_space(data_dir)
  547. structure_tree = rs.structure_tree
  548. _, labels = rs.export_itksnap_labels()
  549. # Get the itk ids for the region
  550. region_structures = structure_tree.get_structures_by_name([region])
  551. region_id = region_structures[0]["id"]
  552. region_sub = structure_tree.descendant_ids([region_id])
  553. region_sub_acronyms = [
  554. region["acronym"] for region in structure_tree.get_structures_by_id(region_sub[0])
  555. ]
  556. itk_ids = labels.loc[labels["LABEL"].isin(region_sub_acronyms)]["IDX"].to_numpy().tolist()
  557. finally:
  558. if use_temp and temp_dir is not None:
  559. temp_dir.cleanup()
  560. if not isinstance(itk_ids, list):
  561. raise TypeError(f"Expected list, got {type(itk_ids)}")
  562. return [int(x) for x in itk_ids]
  563. def create_filter_image(atlas: da.Array | Future[Any], region_ids: list[int]) -> da.Array:
  564. """Create a filter image based on the region ids.
  565. Parameters
  566. ----------
  567. atlas : da.Array | Future[Any]
  568. The atlas containing the region ids.
  569. region_ids : list[int]
  570. The region ids to filter.
  571. Returns
  572. -------
  573. da.Array
  574. The filter image.
  575. Raises
  576. ------
  577. TypeError
  578. If the gathered filter image is not a Dask array.
  579. """
  580. client = dask_client_manager.get_client()
  581. filter_image = client.submit(da.isin, atlas, region_ids)
  582. result = client.gather(filter_image)
  583. if not isinstance(result, da.Array):
  584. raise TypeError(f"Expected dask Array, got {type(result)}")
  585. return result
  586. def filter_image_to_region(image_filter: da.Array, data: da.Array | Future[Any]) -> da.Array:
  587. """Filter an image to a region based on a filter.
  588. Parameters
  589. ----------
  590. image_filter : da.Array
  591. The filter to apply.
  592. data : da.Array | Future[Any]
  593. The data to filter.
  594. Returns
  595. -------
  596. da.Array
  597. The filtered image.
  598. Raises
  599. ------
  600. TypeError
  601. If the gathered filtered image is not a Dask array.
  602. """
  603. client = dask_client_manager.get_client()
  604. filtered_image = client.submit(da.where, image_filter, data, 0)
  605. result = client.gather(filtered_image)
  606. if not isinstance(result, da.Array):
  607. raise TypeError(f"Expected dask Array, got {type(result)}")
  608. return result
  609. def compute_mask_area(mask: da.Array | Future[Any]) -> np.uint64:
  610. """Compute the area of a mask by summing the binary mask values.
  611. Parameters
  612. ----------
  613. mask : da.Array | Future[Any]
  614. The mask to compute the area of.
  615. Returns
  616. -------
  617. np.uint64
  618. The area of the mask.
  619. """
  620. client = dask_client_manager.get_client()
  621. total_area = client.submit(da.sum, mask)
  622. total_area = client.gather(total_area)
  623. # BOUNDARY-REQUIRED materialization: client.gather returns a 0-dimensional
  624. # dask.array.Array (a scalar Dask array), NOT a Python scalar, so .compute()
  625. # is required to materialize the scalar the function promises to return.
  626. # Removing it would return a Dask array (wrong type) -- KEEP this .compute().
  627. result = total_area.compute()
  628. return np.uint64(result)

stats.py at commit aadfa71, under GPL-3.0 · at the source

Overview

Authors: Mathilde Bizou1, Elise Drapé1,2, Gael Cagnone1, Joel P Howard1, Frans Irgolitsch3, Séverine Leclerc1, Mei Xi Chen1, Blanche Boisseau1, Isabelle Robillard4,5, Matthieu Ruiz4,5,6, Fréderic Lesage3,4, Jean-Sébastien Joyal1,2,7, Gregor Andelfinger1,7, Alexandre Dubrac1,2,8,9
  1. Centre de Recherche, CHU Sainte Justine, Montréal, QC Canada
  2. Département de Pharmacologie et de Physiologie, Université de Montréal, Montréal, QC Canada
  3. Laboratoire d’Imagerie optique et Moléculaire, Polytechnique Montréal, Montréal, QC Canada
  4. Centre de Recherche, Montréal Heart Institute, Montréal, QC Canada
  5. Metabolomics platform, Montréal Heart Institute, Montréal, QC Canada
  6. Département de nutrition, Université de Montréal, Montréal, QC Canada
  7. Département de Pédiatrie, Université de Montréal, Montréal, QC Canada
  8. Département de Pathologie et Biologie Cellulaire, Université de Montréal, Montréal, QC Canada
  9. Département d’Ophtalmologie, Université de Montréal, Montréal, QC Canada
Journal: Nature communications, volume 17, issue 1, article 6746
Dates: received 18 August 2023; accepted 28 April 2026; published online 22 May 2026
Type: Research article · Language: English
License: CC BY-NC-ND
Identifiers: DOI 10.1038/s41467-026-73373-w · PMID 42173924 · PMCID PMC13385802 · OpenAlex W7162147048
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: genetics / omics (modality), mouse (organism)
Methods: Spectral & time-frequency, Statistics, Smoothing, state filtering, decompositions, Connectivity, Machine learning
Keywords: Angiogenesis, Neuro-vascular interactions
MeSH: Angiogenesis*, Brain*, Neovascularization, Physiologic*, Animals, Endothelial Cells, Female, Male, Mice, Mice, Inbred C57BL, Neurodevelopment, Neuroglia, Neurons, Signal Transduction, Spatial Transcriptomics, Thalamus, TOR Serine-Threonine Kinases (* major topic)
Topic: Axon Guidance and Neuronal Signaling (Cellular and Molecular Neuroscience, Neuroscience), according to OpenAlex
Funding: Gouvernement du Canada | Instituts de Recherche en Santé du Canada | CIHR Skin Research Training Centre (Skin Research Training Centre) (202203PJT-183658, 202403PJT-517269, 2019PJT-165871); Canadian Network for Research and Innovation in Machining Technology, Natural Sciences and Engineering Research Council of Canada (RGPIN-2022-04726)
Citations: cited by 1 paper (Europe PMC); 66 references in the paper

Abstract

The abstract is not reproduced here: the paper's license (CC BY-NC-ND) does not allow it. Read it in the paper, at the publisher or on Europe PMC.

Repository

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

LIOMLab/liom-toolkit

License: GPL-3.0
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: aadfa715e14ba44154053d8c1cd3cd15e0fa4c45, 28 August 2026
Languages: Python (88), Jupyter (6)
Size: 149 files, 94 scripts
Software Heritage: not archived
Found in: the text, “Vessel segmentation”
Holds: README, license file, environment (pyproject.toml, uv.lock), tests, continuous integration, documentation, 6 notebooks
Not found: CITATION.cff
Tools: NumPy (45 files), imageio (16 files), ANTs (11 files), PyTorch (11 files), scikit-image (10 files), pandas (6 files), Pillow (4 files), SciPy (4 files), h5py (3 files), NiBabel (2 files), tifffile (2 files), Matplotlib (1 file), OpenCV (1 file), scikit-learn (1 file), SimpleITK (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
96 files

Code availability statement

The paper has a code availability statement. Its license (CC BY-NC-ND) does not allow reproducing it here; in short, from what the harvester recognized in it:

  • it says that the code is available on request

Read it in the paper: doi.org/10.1038/s41467-026-73373-w.

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;
  • 94 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

Code and data availability statement

The paper has a code and data availability statement. Its license (CC BY-NC-ND) does not allow reproducing it here; in short, from what the harvester recognized in it:

Read it in the paper: doi.org/10.1038/s41467-026-73373-w.

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, 28 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 14 authors, 2 keywords, 16 MeSH terms, 2 funders, 66 references.

Cite

This paper

Bizou, M., Drapé, E., Cagnone, G., Howard, J. P., Irgolitsch, F., Leclerc, S., Chen, M. X., Boisseau, B., Robillard, I., Ruiz, M., Lesage, F., Joyal, J.-S., Andelfinger, G., & Dubrac, A. (2026). Mapping neuro-vascular unit communications reveals distinct angiogenic programs across developing mouse brain regions. Nature communications, 17(1), 6746. https://doi.org/10.1038/s41467-026-73373-w

BibTeX

@article{bizou2026mapping,
author = {Bizou, Mathilde and Drapé, Elise and Cagnone, Gael and Howard, Joel P and Irgolitsch, Frans and Leclerc, Séverine and Chen, Mei Xi and Boisseau, Blanche and Robillard, Isabelle and Ruiz, Matthieu and Lesage, Fréderic and Joyal, Jean-Sébastien and Andelfinger, Gregor and Dubrac, Alexandre},
title = {{Mapping neuro-vascular unit communications reveals distinct angiogenic programs across developing mouse brain regions}},
journal = {Nature communications},
year = {2026},
month = may,
volume = {17},
number = {1},
pages = {6746},
publisher = {Nature Publishing Group},
issn = {2041-1723},
doi = {10.1038/s41467-026-73373-w},
url = {https://doi.org/10.1038/s41467-026-73373-w},
pmid = {42173924},
pmcid = {PMC13385802}
}

RIS

TY - JOUR
AU - Bizou, Mathilde
AU - Drapé, Elise
AU - Cagnone, Gael
AU - Howard, Joel P
AU - Irgolitsch, Frans
AU - Leclerc, Séverine
AU - Chen, Mei Xi
AU - Boisseau, Blanche
AU - Robillard, Isabelle
AU - Ruiz, Matthieu
AU - Lesage, Fréderic
AU - Joyal, Jean-Sébastien
AU - Andelfinger, Gregor
AU - Dubrac, Alexandre
TI - Mapping neuro-vascular unit communications reveals distinct angiogenic programs across developing mouse brain regions
T2 - Nature communications
J2 - Nat Commun
PY - 2026
DA - 2026/05/22
VL - 17
IS - 1
SP - 6746
SN - 2041-1723
PB - Nature Publishing Group
DO - 10.1038/s41467-026-73373-w
UR - https://doi.org/10.1038/s41467-026-73373-w
LA - en
ER -

CSL-JSON

{
"id": "10.1038/s41467-026-73373-w",
"type": "article-journal",
"title": "Mapping neuro-vascular unit communications reveals distinct angiogenic programs across developing mouse brain regions",
"container-title": "Nature communications",
"author": [
{
"family": "Bizou",
"given": "Mathilde"
},
{
"family": "Drapé",
"given": "Elise"
},
{
"family": "Cagnone",
"given": "Gael"
},
{
"family": "Howard",
"given": "Joel P"
},
{
"family": "Irgolitsch",
"given": "Frans"
},
{
"family": "Leclerc",
"given": "Séverine"
},
{
"family": "Chen",
"given": "Mei Xi"
},
{
"family": "Boisseau",
"given": "Blanche"
},
{
"family": "Robillard",
"given": "Isabelle"
},
{
"family": "Ruiz",
"given": "Matthieu"
},
{
"family": "Lesage",
"given": "Fréderic"
},
{
"family": "Joyal",
"given": "Jean-Sébastien"
},
{
"family": "Andelfinger",
"given": "Gregor"
},
{
"family": "Dubrac",
"given": "Alexandre"
}
],
"container-title-short": "Nat Commun",
"volume": "17",
"issue": "1",
"page": "6746",
"DOI": "10.1038/s41467-026-73373-w",
"PMID": "42173924",
"PMCID": "PMC13385802",
"ISSN": "2041-1723",
"publisher": "Nature Publishing Group",
"URL": "https://doi.org/10.1038/s41467-026-73373-w",
"language": "en",
"issued": {
"date-parts": [
[
2026,
5,
22
]
]
}
}

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.1364/boe.605322 [code]
Generalized plaque digitization framework for multi-dimensional mesoscopic images.
Journal: Biomedical optics express
In common: imageio, SimpleITK, tifffile, 10 other tools, mouse
[2] doi:10.1038/s41598-026-57519-w [code]
Automated segmentation of neurons and spinal cord structures in immunofluorescence images using SpineDL.
Journal: Scientific reports
In common: imageio, tifffile, OpenCV, 10 other tools, mouse
[3] doi:10.1016/j.xcrm.2026.102766 [code]
A longitudinal single-cell and spatial multiomic atlas of pediatric high-grade glioma.
Journal: Cell reports. Medicine
In common: imageio, tifffile, OpenCV, 8 other tools, genetics / omics, 2 references
[4] doi:10.3389/frai.2026.1771088 [code]
Few-shot deployment of pretrained MRI transformers in brain imaging tasks.
Journal: Frontiers in artificial intelligence
In common: imageio, SimpleITK, OpenCV, 10 other tools
[5] doi:10.1038/s41597-026-07248-6 [code]
A large-scale fMRI dataset for vision-language semantic association.
Journal: Scientific data
In common: imageio, ANTs, OpenCV, 10 other tools
[6] doi:10.3389/fnins.2026.1870124 [code]
An end-to-end pipeline for automated fetal brain segmentation and biometry from 3D SSFP MRI.
Journal: Frontiers in neuroscience
In common: imageio, SimpleITK, tifffile, 9 other tools
[7] doi:10.1162/imag.a.1326 [code]
RAVEN: Robust, generalizable, multi-resolution structural MRI upsampling using autoencoders.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: imageio, SimpleITK, OpenCV, 9 other tools
[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: SimpleITK, ANTs, OpenCV, 9 other tools
[9] doi:10.1038/s41586-026-10679-1 [code]
Cortical development dynamics across autism spectrum disorder mouse models.
Journal: Nature
In common: imageio, tifffile, OpenCV, 7 other tools, genetics / omics, mouse, 1 reference
[10] doi:10.1371/journal.pcbi.1014263 [code]
MIRAGE: Robust multi-modal architectures translate fMRI-to-image models from vision to mental imagery.
Journal: PLoS computational biology
In common: imageio, OpenCV, scikit-image, 9 other tools

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.