OSCR

Automated analysis of zebrafish vascular networks using the VISTA-Z pipeline.

Code ↔ Paper

6 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 6 matches
  1. [1] § Materials and methods › Image pre-processing and segmentation ↔ Codes/VISTA-Z_ISV_loss.ipynb, lines 451–548 · score 0.89 · curated segmentation mask, manually curated, sigma range, segment labelling, Maximum intensity projections, binary masks
  2. [2] § Materials and methods › Image pre-processing and segmentation ↔ Codes/VISTA-Z_lateral.ipynb, lines 328–423 · score 0.88 · curated segmentation mask, manually curated, sigma range, segment labelling, Maximum intensity projections, binary masks
  3. [3] § Materials and methods › Automated image-based skeleton, vessel metric analysis and outlier filtering ↔ Codes/VISTA-Z_ROI.ipynb, lines 84–171 · score 0.84 · median absolute deviation, metric normalisation, vessel masks, vessel length, transform, windows
  4. [4] § Materials and methods › Automated image-based skeleton, vessel metric analysis and outlier filtering ↔ Codes/VISTA-Z_lateral.ipynb, lines 76–139 · score 0.83 · median absolute deviation, metric normalisation, vessel masks, vessel length, transform, windows
  5. [5] § Results › Development of VISTA-Z, an automated pipeline for vascular quantification ↔ Codes/VISTA-Z_ROI.ipynb, lines 84–171 · score 0.80 · Median Absolute Deviation, image pre processing, vessel masking, vessel length, noise, selection
  6. [6] § Results › Development of VISTA-Z, an automated pipeline for vascular quantification ↔ Codes/VISTA-Z_lateral.ipynb, lines 76–139 · score 0.79 · Median Absolute Deviation, image pre processing, vessel masking, vessel length, noise, selection

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

Jupyter notebook · 1,054 lines · 61 KB · MIT · 3 matches

  1. # %% [markdown]
  2. # **Table of contents**<a id='toc0_'></a>
  3. # - [Import packages](#toc1_)
  4. # - [Set up working directory](#toc2_)
  5. # - [Vessel analysis - Lateral images](#toc3_)
  6. # - [Define parameters](#toc3_1_)
  7. # - [Define functions](#toc3_2_)
  8. # - [Run samples - Single channel](#toc3_3_)
  9. # - [Run samples - Two or more channels](#toc3_4_)
  10. #
  11. # <!-- vscode-jupyter-toc-config
  12. # numbering=false
  13. # anchor=true
  14. # flat=false
  15. # minLevel=1
  16. # maxLevel=6
  17. # /vscode-jupyter-toc-config -->
  18. # <!-- THIS CELL WILL BE REPLACED ON TOC UPDATE. DO NOT WRITE YOUR TEXT IN THIS CELL -->
  19. # %% [markdown]
  20. # # <a id='toc1_'></a>[Import packages](#toc0_)
  21. # %%
  22. import sys
  23. import os
  24. import numpy as np
  25. import vessel_metrics as vm
  26. import czifile
  27. from aicspylibczi import CziFile
  28. import matplotlib.pyplot as plt
  29. import cv2
  30. import imageio
  31. import matplotlib.pyplot as plt
  32. from matplotlib import rcParams
  33. import matplotlib.colors
  34. from matplotlib.pyplot import rc_context
  35. import math
  36. import seaborn as sns
  37. from scipy.stats import median_abs_deviation
  38. import glob
  39. import time
  40. import pandas as pd
  41. import openpyxl
  42. import seaborn as sns
  43. import plotly.graph_objects as go
  44. from skimage.measure import label, regionprops
  45. from skimage import color
  46. from skimage.color import label2rgb
  47. from matplotlib.cm import get_cmap
  48. from skimage.morphology import remove_small_objects
  49. from aicsimageio import AICSImage, readers
  50. from scipy.spatial import distance
  51. from skimage.morphology import skeletonize
  52. import traceback
  53. import gc
  54. # %% [markdown]
  55. # # <a id='toc2_'></a>[Set up working directory](#toc0_)
  56. # %%
  57. """
  58. Define the path to the working directory and the output folder where the 3D volumes will be saved.
  59. Define the name of the files to be processed.
  60. """
  61. data_path = 'path_to_working_directory'
  62. output_path = 'path_to_output_directory'
  63. file_name = glob.glob(f'{data_path}*.*', recursive = True) # List of file names to process
  64. # %%
  65. """
  66. Ensure the output directory exists and that files are read correctly.
  67. """
  68. total_files = len(file_name)
  69. print(f"Wolking forlder contains {total_files} files")
  70. # %% [markdown]
  71. # # <a id='toc3_'></a>[Vessel analysis - Lateral images](#toc0_)
  72. # %% [markdown]
  73. # ## <a id='toc3_1_'></a>[Define parameters](#toc0_)
  74. # %%
  75. ## Please README before running the following code ##
  76. ## Parameters that the user should define before running VISTA-Z ROI analysis ##
  77. # clahe = (int, int) # CLAHE parameters (clipLimit, tileGridSize)
  78. # channel = int [0,1,2] # Channel number to be analysed per sample (only if multi-channel images) [0]: mCherry, [1]: EGFP, [2]: DAPI, etc.
  79. # name_start = str # Starting string of the file names to be processed
  80. # pdf_dpi = int # DPI for saving PDF figures
  81. # tiles = int # Number of tiles used to image the whole zebrafish trunk
  82. # min_vessel_size = int # Minimum vessel size (in pixels) to be thresholded for analysis
  83. # Vessel segmentation parameters:
  84. # seg_method = str # Vessel segmentation method: 'meijering', 'frangi', 'sato' or 'jerman'
  85. # sigma1 = range(int, int, int) # Sigma range for vessel enhancement filter
  86. # hole_size = int # Maximum hole size to be filled in the vessel mask
  87. # ditzle_size = int # Maximum size of small objects to be removed from the vessel mask
  88. # thresh = int # Threshold value to binarize the vessel enhanced image
  89. # skel_method = str # Skeletonisation method: 'lee' or 'zhang'
  90. # Branch_dist = int # Maximum distance (in pixels) to consider two vessel branches as connected
  91. # MAD filtering threshold: Threshold for median absolute deviation (MAD) filtering of vessel metrics
  92. # threshold_low = float
  93. # threshold_high = float
  94. # Normalisation of vessel metrics:
  95. # window_size = 10 # 10 is to make an 100µm² area of 10x10. Transform into the low int value to create the scale for the density values
  96. # scale = float # Scale factor to normalise vessel metrics (pixels per micron)
  97. # output_name = str # Name of the output files to be saved (excel file)
  98. # %%
  99. # Image pre-processing and ROI selection parameters
  100. clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(4, 4))
  101. channel = 0
  102. name_start = 'kdrlmCh'
  103. pdf_dpi = 300
  104. tiles = 3
  105. # Vessel segmentation parameters
  106. min_vessel_size = 500 # Note: User are reccommended to set this parameter after initial analysis based on median(vessel_length) - std(vessel_length)
  107. seg_method = 'meijering'
  108. sigma1 = range(3, 8, 1)
  109. hole_size = 200
  110. ditzle_size = 50 # Note: Users are reccommended to set hole_size and ditzle_size based on the visualisation of the vessel mask after thresholding. Adjust these parameters to ensure that the vessel mask captures the vessels accurately while minimising noise and artefacts.
  111. thresh = 10 # Note: Users are reccommended to set this parameter based on the visualisation of the vessel enhanced image and the vessel mask after thresholding. Adjust this parameter to ensure that the vessel mask captures the vessels accurately while minimising noise and artefacts.
  112. skel_method = 'lee'
  113. #Branchpoint refinement parameters
  114. Branch_dist = 30 # Note: User are reccommended to set this parameter based median(vessel_length)
  115. # MAD filtering threshold
  116. threshold_low = 1
  117. threshold_high = 3
  118. # Diameter calculation and vessel metrics normalisation parameters
  119. window_size = 10 # 10 is to make an 100µm² area of 10x10. Transform into the low int value to create the scale for the density values
  120. scale = 1.2044
  121. # Output file name
  122. output_name = "VISTA-Z_lateral_Results"
  123. output_tile_name = "VISTA-Z_lateral_tile_Results"
  124. # %% [markdown]
  125. # ## <a id='toc3_2_'></a>[Define functions](#toc0_)
  126. # %%
  127. def load_and_process_czi_single(file_path, file_name, output_path):
  128. """
  129. Loads and processes a fluorescent image (.czi file) with a single channel,
  130. Args:
  131. file_path (str): Full path to the .czi file.
  132. file_name (str): File name used to derive the sample ID.
  133. output_path (str): Directory where output images will be saved.
  134. Returns:
  135. tuple:
  136. image (ndarray): Raw image data after squeezing dimensions.
  137. mip_image (ndarray): Normalized maximum-intensity projection (MIP) image in uint8.
  138. gray_image (ndarray): Greyscale MIP image.
  139. clahe_image (ndarray): CLAHE-enhanced greyscale image.
  140. otsu_mask (ndarray): Binary mask from Otsu thresholding.
  141. output_mip_path (str): Path for saving the MIP image.
  142. output_mask_path (str): Path for saving the Otsu mask image.
  143. Returns (None, None, None, None) if processing fails.
  144. """
  145. try:
  146. sampleID = file_name.split('_MaxInt')[0] # Name of the sample
  147. with czifile.CziFile(file_path) as czi:
  148. data = czi.asarray() # Read confocal image data from .czi file
  149. data_squeezed = np.squeeze(data) # Squeeze data to remove unwanted dimensions/metadata and retrieve image data
  150. image = data_squeezed
  151. mip_image = cv2.normalize(image, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8) # Normalise image to uint8
  152. mip_image_rgb = cv2.cvtColor(mip_image, cv2.COLOR_GRAY2RGB)
  153. gray_image = cv2.cvtColor(mip_image_rgb, cv2.COLOR_RGB2GRAY)
  154. clahe_image = clahe.apply(gray_image) # Apply CLAHE for contrast enhacement
  155. _, otsu_mask = cv2.threshold(clahe_image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # Otsu thresholding
  156. output_mip_path = os.path.join(output_path, f"{sampleID}.tiff") # Define output MIP path
  157. output_mask_path = os.path.join(output_path, f"{sampleID}_mask.tiff") # Define output mask path
  158. return image, mip_image, gray_image, clahe_image, otsu_mask, output_mip_path, output_mask_path
  159. except Exception as e:
  160. print(f"Error processing {sampleID}: {e}") # Print error message if processing fails
  161. return None, None, None, None
  162. def load_and_process_czi_double(file_path, file_name, output_path, channel = channel):
  163. """
  164. Loads and processes a fluorescent image (.czi file) with two or more channels,
  165. Args:
  166. file_path (str): Full path to the .czi file.
  167. file_name (str): File name used to derive the sample ID.
  168. output_path (str): Directory where output images will be saved.
  169. channel (int): Channel index. For example mCherry = 0, EGFP = 1, DAPI = 2, etc.
  170. Returns:
  171. tuple:
  172. image (ndarray): Raw image data after squeezing dimensions.
  173. mip_image (ndarray): Normalized maximum-intensity projection (MIP) image in uint8.
  174. gray_image (ndarray): Greyscale MIP image.
  175. clahe_image (ndarray): CLAHE-enhanced greyscale image.
  176. otsu_mask (ndarray): Binary mask from Otsu thresholding.
  177. output_mip_path (str): Path for saving the MIP image.
  178. output_mask_path (str): Path for saving the Otsu mask image.
  179. Returns (None, None, None, None) if processing fails.
  180. """
  181. try:
  182. sampleID = file_name.split('_MaxInt')[0] # Name of the sample
  183. with czifile.CziFile(file_path) as czi:
  184. data = czi.asarray() # Read confocal image data from .czi file
  185. data_squeezed = np.squeeze(data) # Squeeze data to remove unwanted dimensions/metadata
  186. image = data_squeezed[channel] # Retrieve image data from the specified channel
  187. mip_image = cv2.normalize(image, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8) # Normalise image to uint8
  188. mip_image_rgb = cv2.cvtColor(mip_image, cv2.COLOR_GRAY2RGB)
  189. gray_image = cv2.cvtColor(mip_image_rgb, cv2.COLOR_RGB2GRAY)
  190. clahe_image = clahe.apply(gray_image) # Apply CLAHE for contrast enhacement
  191. _, otsu_mask = cv2.threshold(clahe_image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # Otsu thresholding
  192. output_mip_path = os.path.join(output_path, f"{sampleID}.tiff") # Define output MIP path
  193. output_mask_path = os.path.join(output_path, f"{sampleID}_mask.tiff") # Define output mask path
  194. return image, mip_image, gray_image, clahe_image, otsu_mask, output_mip_path, output_mask_path
  195. except Exception as e:
  196. print(f"Error processing {sampleID}: {e}") # Print error message if processing fails
  197. return None, None, None, None
  198. def save_plot_as_pdf(fig, output_path, dpi=pdf_dpi):
  199. """
  200. Save a Matplotlib figure to a PDF file.
  201. Args:
  202. fig (matplotlib.figure.Figure): Figure to save.
  203. output_path (str): Output path to save the PDF file.
  204. dpi (int): Resolution to use when saving.
  205. Returns:
  206. None
  207. """
  208. try:
  209. fig.savefig(output_path, format='pdf', dpi=dpi, bbox_inches='tight') # Save figure as PDF
  210. print(f"Plot saved as PDF: {output_path}")
  211. except Exception as e: # Print error message if saving fails
  212. print(f"Error saving plot as PDF: {e}")
  213. def display_and_save_mip_otsu(gray_image, clahe_image, otsu_mask, output_mip_path, output_mask_path):
  214. """
  215. Display MIP, CLAHE, and Otsu mask panels and save images.
  216. Args:
  217. gray_image (ndarray): Greyscale MIP image.
  218. clahe_image (ndarray): CLAHE-enhanced greyscale image.
  219. otsu_mask (ndarray): Binary mask from Otsu thresholding.
  220. output_mip_path (str): Path for saving the MIP image.
  221. output_mask_path (str): Path for saving the Otsu mask image.
  222. Returns:
  223. None
  224. """
  225. fig, axes = plt.subplots(1, 3, figsize=(8, 5)) # Create figure with 3 panels
  226. axes[0].imshow(gray_image, cmap="gray") # Display MIP
  227. axes[0].set_title("Maximum Intensity Projection (MIP)")
  228. axes[0].axis("off")
  229. axes[1].imshow(clahe_image, cmap="gray") # Display CLAHE processed image
  230. axes[1].set_title("Clahe processed image")
  231. axes[1].axis("off")
  232. axes[2].imshow(otsu_mask, cmap="gray") # Display Otsu's threshold mask
  233. axes[2].set_title("Otsu’s Threshold Mask")
  234. axes[2].axis("off")
  235. plt.show() # Show the figure
  236. cv2.imwrite(output_mip_path, gray_image) # Save MIP image
  237. cv2.imwrite(output_mask_path, otsu_mask) # Save Otsu mask image
  238. print(f"Images saved: {output_mip_path} and {output_mask_path}")
  239. def visualize_segmented_regions(gray_image, seg_im, cmap_name='hsv', min_vessel_size = min_vessel_size):
  240. """
  241. Visualise labelled vessel segments overlaid on the greyscale image.
  242. Args:
  243. gray_image (ndarray): Greyscale MIP image for background.
  244. seg_im (ndarray): Binary vessel segmentation mask.
  245. cmap_name (str): Matplotlib colormap name for segment colouring.
  246. min_vessel_size (int): Minimum segment size to keep (pixels).
  247. Returns:
  248. labeled_segments (ndarray): Labelled segmentation image with unique integer labels for each segment.
  249. Returns None on failure.
  250. """
  251. try:
  252. gray_image_norm = (gray_image - gray_image.min()) / (gray_image.max() - gray_image.min()) # Normalise greyscale image for better visualisation
  253. labeled_segments = label(seg_im) # Label each vessel segment
  254. num_labels = np.max(labeled_segments) # Get number of unique segments
  255. labeled_segments = remove_small_objects(labeled_segments, min_size=min_vessel_size) # Remove small vessel segments based on minimum vessel size
  256. if num_labels == 0: # Check if any segments are detected
  257. print("No segments detected.")
  258. return None
  259. cmap = matplotlib.colormaps.get_cmap(cmap_name) # Generate colours from the chosen colourmap
  260. colors = cmap(np.linspace(0, 1, num_labels))[:, :3] # Get RGB values for each label
  261. overlay = label2rgb(labeled_segments, image=gray_image_norm, bg_label=0, alpha=0.6, colors=colors) # Overlay coloured segments on greyscale image
  262. # Create a figure
  263. fig, ax = plt.subplots(figsize=(8, 6)) # Set figure size
  264. ax.imshow(overlay, cmap='gray') # Display overlay image
  265. ax.set_title("Vessel Segmentation Visualization")
  266. for region in regionprops(labeled_segments): # Loop through each labeled region
  267. centroid = region.centroid # Plot segment labels at centroids
  268. ax.text(centroid[1], centroid[0], str(region.label), color='white', fontsize=8, ha='center', va='center', fontweight='bold')
  269. ax.axis('off') # Remove axes
  270. plt.show() # Show the figure
  271. return labeled_segments # Return labeled segments for further processing
  272. except Exception as e:
  273. print(f"Error visualizing segmented regions: {e}") # Print error message if visualization fails
  274. return None
  275. def remove_selected_segments(labeled_segments, remove_list):
  276. """
  277. Manual curation: Remove user-selected labelled segments from a segmentation mask.
  278. Args:
  279. labeled_segments (ndarray): Labelled segmentation image.
  280. remove_list (list[int] or None): Segment labels to remove. Use the segment numbers displayed in the visualisation for reference. If None, no segments will be removed.
  281. Returns:
  282. cleaned_mask (ndarray): Binary mask with curated segments
  283. Returns None on failure.
  284. """
  285. try:
  286. mask = np.isin(labeled_segments, remove_list, invert=True) # Create mask to exclude selected segments
  287. cleaned_mask = labeled_segments * mask # Apply mask to labeled segments
  288. return cleaned_mask > 0 # Return manually curated binary mask
  289. except Exception as e:
  290. print(f"Error removing selected segments: {e}") # Print error message if removal fails
  291. return None
  292. def segment_and_analyze_vessels(data_path, file_name, image, gray_image, clahe_image, otsu_mask, output_path,
  293. tile_index, im_filter=seg_method, sigma1=sigma1, hole_size=hole_size,
  294. ditzle_size=ditzle_size, thresh=thresh):
  295. """
  296. Vessel segmentation, optional apply of manual curation and results visualisation
  297. Args:
  298. data_path (str): Directory containing the input image (used for labeling).
  299. file_name (str): File name used to derive the sample ID.
  300. image (ndarray): Raw image data used for segmentation.
  301. gray_image (ndarray): Greyscale MIP image.
  302. clahe_image (ndarray): CLAHE-enhanced greyscale image.
  303. otsu_mask (ndarray): Binary mask from Otsu thresholding.
  304. output_path (str): Directory for saving figures.
  305. tile_index (int): Index of the tile being processed (used for labelling and individual tile vessel metrics analysis).
  306. im_filter (str): Vessel enhancement filter name (meijering, frangi, sato or jerman).
  307. sigma1 (range): Sigma range for vessel enhancement.
  308. hole_size (int): Max hole size to fill in the mask.
  309. ditzle_size (int): Max size of small objects to remove.
  310. thresh (int): Threshold for binarisation of the enhanced image.
  311. Returns:
  312. tuple:
  313. vessel_seg (ndarray): Final vessel segmentation mask (curated if manual curation is applied).
  314. remove_list (list[int] or None): Labels removed by the user.
  315. Returns None on failure.
  316. """
  317. try:
  318. # Segment vessels using specified filtering method and parameters
  319. vessel_seg = vm.segment_image(image,
  320. im_filter=im_filter, # Vessel segmentation method
  321. sigma1=sigma1, # Sigma range for vessel enhancement filter
  322. hole_size=hole_size, # Hole size to be filled in the vessel mask
  323. ditzle_size=ditzle_size, # Size of small objects to be removed from the vessel mask
  324. thresh=thresh) # Threshold value to binarise the vessel enhanced image
  325. seg_im = vessel_seg.astype(np.uint8) # Convert binary mask to uint8 format
  326. # Visualise initial segmentation results
  327. fig, axes = plt.subplots(1, 4, figsize=(15, 5)) # Create figure with 4 panels
  328. axes[0].imshow(gray_image, cmap="gray") # Display MIP
  329. axes[0].set_title("Maximum Intensity Projection (MIP)")
  330. axes[0].axis("off")
  331. axes[1].imshow(clahe_image, cmap="gray") # Display CLAHE processed image
  332. axes[1].set_title("Clahe processed image")
  333. axes[1].axis("off")
  334. axes[2].imshow(seg_im, cmap="gray") # Display vessel segmentation image
  335. axes[2].set_title("Vessel segmentation image")
  336. axes[2].axis("off")
  337. axes[3].imshow(otsu_mask, cmap="gray") # Display Otsu's threshold mask
  338. axes[3].set_title("Otsu’s Threshold Mask")
  339. axes[3].axis("off")
  340. plt.show() # Show the figure
  341. sampleID = file_name.split('_MaxInt')[0]
  342. fig_title = f"{sampleID}_tile{tile_index}_segmentation_mask_prefiltering" # Title for the figure
  343. output_pdf_path = os.path.join(output_path, f"{fig_title}.pdf") # Define output PDF path
  344. save_plot_as_pdf(fig, output_pdf_path) # Save figure as PDF
  345. # Visualise segmented regions with labels for manual curation
  346. labeled_segments = visualize_segmented_regions(gray_image, seg_im, cmap_name='hsv', min_vessel_size = min_vessel_size) # Visualise segmented regions
  347. sampleID = file_name.split('_MaxInt')[0]
  348. fig_title = f"{sampleID}_tile{tile_index}_segmentation_labelled" # Title for the figure
  349. output_pdf_path = os.path.join(output_path, f"{fig_title}.pdf") # Define output PDF path
  350. save_plot_as_pdf(fig, output_pdf_path) # Save figure as PDF
  351. # Manual curation: Remove unwanted segments based on user input
  352. remove_list = input("Enter segment numbers to remove (comma-separated) or press Enter to skip: ").strip() # Get user input for segments to remove
  353. if remove_list:
  354. remove_list = [int(x) for x in remove_list.split(",")] # Convert input string to list of integers
  355. cleaned_mask = remove_selected_segments(labeled_segments, remove_list) # Remove selected segments
  356. labeled_segments = label(cleaned_mask) # Relabel the manually curated mask
  357. vessel_seg = labeled_segments # Update vessel segmentation mask
  358. else:
  359. remove_list = None # No segments removed
  360. cleaned_mask = remove_selected_segments(labeled_segments, remove_list) # Keep original mask
  361. labeled_segments = label(cleaned_mask)
  362. vessel_seg = labeled_segments
  363. # Visualise manually curated segmentation results
  364. fig, axes = plt.subplots(1, 4, figsize=(15, 5)) # Create figure with 4 panels
  365. axes[0].imshow(gray_image, cmap="gray")
  366. axes[0].set_title("Maximum Intensity Projection (MIP)") # Display MIP
  367. axes[0].axis("off")
  368. axes[1].imshow(clahe_image, cmap="gray") # Display CLAHE processed image
  369. axes[1].set_title("Clahe processed image")
  370. axes[1].axis("off")
  371. axes[2].imshow(cleaned_mask, cmap="gray") # Display manually filtered vessel segmentation image
  372. axes[2].set_title("Filtered vessel segmentation image")
  373. axes[2].axis("off")
  374. axes[3].imshow(otsu_mask, cmap="gray") # Display Otsu's threshold mask
  375. axes[3].set_title("Otsu’s Threshold Mask")
  376. axes[3].axis("off")
  377. plt.show() # Show the figure
  378. sampleID = file_name.split('_MaxInt')[0]
  379. fig_title = f"{sampleID}_tile{tile_index}_segmentation_mask" # Title for the figure
  380. output_pdf_path = os.path.join(output_path, f"{fig_title}.pdf") # Define output PDF path
  381. save_plot_as_pdf(fig, output_pdf_path) # Save figure as PDF
  382. return vessel_seg, remove_list
  383. except Exception as e:
  384. print(f"Error segmenting vessels for {file_name}: {e}") # Print error message if segmentation fails
  385. return None
  386. def calculate_and_print_metrics(label_path, vessel_seg):
  387. """
  388. Compute vessel metrics and overlap statistics.
  389. Args:
  390. label_path (str): Path to the labelled image.
  391. vessel_seg (ndarray): Vessel segmentation mask.
  392. Returns:
  393. tuple:
  394. length (float): Vessel length metric.
  395. area (float): Vessel area metric.
  396. conn (float): Connectivity metric.
  397. Q (float): Combined metric of Q = connectivity * length * area.
  398. jaccard (float): Jaccard index between label and segmentation.
  399. label (ndarray): Raw labelled image.
  400. label_im (ndarray): Labelled image cast to uint8.
  401. Returns None on failure.
  402. """
  403. try:
  404. label = cv2.imread(label_path, 0) # Load label image
  405. label_im = label.astype(np.uint8) # Convert label image to uint8
  406. seg_im = vessel_seg.astype(np.uint8) # Convert segmentation mask to uint8, [i] is the index of each ROI
  407. length, area, conn, Q = vm.cal(label_im, seg_im) # Calculate vessel metrics: length, area, connectivity and Q factor (combined metric of connectivity * length * area)
  408. jaccard = vm.jaccard(label_im, seg_im) # Calculate Jaccard index between label and segmentation mask
  409. return length, area, conn, Q, jaccard, label, label_im # Return vessel metrics and labelled images
  410. except Exception as e:
  411. print(f"Error calculating metrics: {e}") # Print error message if vessel metrics calculation fails
  412. return None
  413. def find_branchpoints(skel, min_distance=Branch_dist):
  414. """
  415. Detect branchpoints from a skeleton image and removes nearby branchpoints based on a minimum distance threshold.
  416. Args:
  417. skel (ndarray): Skeletonised vessel image.
  418. min_distance (int): Minimum distance between branchpoints (pixels). Users are reccommended to set this parameter based median(vessel_length)
  419. Returns:
  420. tuple:
  421. edges (ndarray): Skeleton edges after removing branchpoints.
  422. branchpoints (ndarray): Binary image of cleaned branchpoints.
  423. """
  424. skel_binary = np.zeros_like(skel) # Create binary skeleton image
  425. skel_binary[skel > 0] = 1 # Binarise skeleton image
  426. skel_index = np.argwhere(skel_binary == True) # Get coordinates of skeleton pixels
  427. tile_sum = [] # List to store sum of neighbourhood pixels
  428. neighborhood_image = np.zeros(skel.shape) # Image to store neighbourhood sums
  429. for i, j in skel_index: # Loop through each skeleton pixel
  430. this_tile = skel_binary[i - 1 : i + 2, j - 1 : j + 2] # Extract 3x3 neighbourhood
  431. tile_sum.append(np.sum(this_tile)) # Sum of neighbourhood pixels
  432. neighborhood_image[i, j] = np.sum(this_tile) # Store sum in neighbourhood image
  433. branch_points = np.zeros_like(neighborhood_image) # Create branchpoint image
  434. branch_points[neighborhood_image > 3] = 1 # Identify branchpoints (neighbourhood sum > 3)
  435. branch_points = branch_points.astype(np.uint8) # Convert branchpoint image to uint8
  436. branch_coords = np.argwhere(branch_points == 1) # Get coordinates of branchpoints
  437. to_keep = np.ones(len(branch_coords), dtype=bool) # Boolean array to track branchpoints to keep
  438. dists = distance.squareform(distance.pdist(branch_coords)) # Calculate pairwise distances between branchpoints
  439. for i in range(len(branch_coords)): # Loop through each branchpoint
  440. if not to_keep[i]: # Skip if already marked for removal
  441. continue
  442. close = np.where((dists[i] < min_distance) & (dists[i] > 0))[0] # Find nearby branchpoints within min_distance
  443. to_keep[close] = False # Keep only the first point and remove closeby branchpoints
  444. cleaned_branch_points = np.zeros_like(skel, dtype=np.uint8) # Creates empty image to store filtered branchpoints
  445. for idx in np.where(to_keep)[0]: # Loop through the index of kept branchpoints
  446. y, x = branch_coords[idx] # Get coordinates for each branchpoint
  447. cleaned_branch_points[y, x] = 1 # Mark the location of the branchpoint
  448. edges = skel_binary - cleaned_branch_points # Edges are defined as skeleton pixels minus the cleaned branchpoints
  449. edges[edges < 0] = 0 # Ensures no negative values are stored
  450. branchpoints = cleaned_branch_points # Store filtered branchpoints
  451. return edges.astype(np.uint8), branchpoints
  452. def analyze_skeleton(vessel_seg, gray_image, file_name, tile_index):
  453. """
  454. Analyse skeleton and vessel diameter. Visualise skeleton and branchpoints and save figures as PDF.
  455. Args:
  456. vessel_seg (ndarray): Vessel segmentation mask.
  457. gray_image (ndarray): Greyscale MIP image.
  458. file_name (str): File name used to derive the sample ID.
  459. tile_index (int): Index of the tile being processed (used for labelling and individual tile vessel metrics analysis).
  460. Returns:
  461. tuple:
  462. skel (ndarray): Skeletonised image.
  463. edges (ndarray): Skeleton edges with branchpoints removed.
  464. edge_labels (ndarray): Labelled edges image.
  465. branchpoints (ndarray): Binary branchpoint image.
  466. coords (list): Edge coordinates from endpoint analysis.
  467. endpoints (list): Endpoint coordinates from endpoint analysis.
  468. Returns None on failure.
  469. """
  470. try:
  471. skel, edges, _ = vm.skeletonize_vm(vessel_seg, method=skel_method) # Calculate skeleton metrics and edges of each image, [i] is the index of each ROI
  472. _, edge_labels = cv2.connectedComponents(edges) # Label connected edges with a unique identifier
  473. _, branchpoints = find_branchpoints(skel) # Define branhpoints (refined from find_branchpoints)
  474. coords, endpoints = vm.find_endpoints(edges) # Find coordinates of each edge for further analysis and visualisation
  475. # Visualise skeleton plot
  476. cmap1 = matplotlib.colors.LinearSegmentedColormap.from_list("", [(1, 1, 1, 0), (0, 0, 0, 1)]) # Colourmap for skeleton (transparent to black)
  477. cmap2 = matplotlib.colors.LinearSegmentedColormap.from_list("", [(1, 1, 1, 0), (1, 0, 0, 1)]) # Colourmap for overlay (transparent to red)
  478. fig, axes = plt.subplots(1, 3, figsize=(15, 5)) # Create a figure with 3 panels
  479. axes[0].imshow(gray_image, cmap="gray") # Display MIP
  480. axes[0].set_title("Maximum Intensity Projection (MIP)")
  481. axes[0].axis("off")
  482. axes[1].imshow(skel, cmap=cmap1) # Display skeleton
  483. axes[1].set_title("Skeleton Plot")
  484. axes[1].axis("off")
  485. axes[2].imshow(gray_image, cmap="gray") # Display overlay of skeleton over MIP
  486. axes[2].imshow(skel, cmap=cmap2)
  487. axes[2].set_title("Overlap")
  488. axes[2].axis("off")
  489. plt.show() # Show the figure
  490. sampleID = file_name.split('_MaxInt')[0]
  491. fig_title = f"{sampleID}_tile{tile_index}_skeleton_plot" # Title for the figure
  492. output_pdf_path = os.path.join(output_path, f"{fig_title}.pdf") # Define output PDF path
  493. save_plot_as_pdf(fig, output_pdf_path) # Save figure as PDF
  494. # Visualise skeleton lines
  495. kernel = np.ones((10, 10), np.uint8) # Adjust kernel size to make branchpoints bigger
  496. branchpoints_dilated = cv2.dilate(branchpoints.astype(np.uint8), kernel, iterations=1) # Dilate branchpoints to make them more visible on the image (only for visualisation purposes)
  497. fig, axes = plt.subplots(1, 3, figsize=(15, 5)) # Create a figure with 3 panels
  498. axes[0].imshow(gray_image, cmap="gray") # Display MIP
  499. axes[0].set_title("Maximum Intensity Projection (MIP)")
  500. axes[0].axis("off")
  501. axes[1].imshow(branchpoints_dilated, cmap='gray_r') # Display branchpoints
  502. axes[1].set_title("Branchpoints")
  503. axes[1].axis("off")
  504. axes[2].imshow(gray_image, cmap="gray") # Display overlay of branchpoints over MIP
  505. axes[2].imshow(branchpoints_dilated, cmap=cmap2)
  506. axes[2].set_title("Overlap")
  507. axes[2].axis("off")
  508. plt.show() # Show the figure
  509. sampleID = file_name.split('_MaxInt')[0]
  510. fig_title = f"{sampleID}_tile{tile_index}_branchpoint_plot" # Title for the figure
  511. output_pdf_path = os.path.join(output_path, f"{fig_title}.pdf") # Define output PDF path
  512. save_plot_as_pdf(fig, output_pdf_path) # Save figure as PDF
  513. return skel, edges, edge_labels, branchpoints, coords, endpoints
  514. except Exception as e:
  515. print(f"Error analyzing skeleton: {e}") # Print error message if skeletonisation fails
  516. return None
  517. def is_outlier(series, threshold_low=threshold_low, threshold_high=threshold_high):
  518. """
  519. Identify outliers using Median Absolute Deviation (MAD).
  520. Args:
  521. series (pandas.Series): Series of numeric values to calculate central tendency.
  522. threshold_low (float): Lower MAD threshold.
  523. threshold_high (float): Upper MAD threshold.
  524. Returns:
  525. pandas.Series: Boolean mask where True indicates an outlier.
  526. """
  527. median = series.median() # Calculates central tendency using median
  528. mad = median_abs_deviation(series) # Calculates non-scaled MAD dispersion
  529. # Compute assymetric bounds using provided thresholds
  530. lower_bound = median - threshold_low * mad # Lower threshold
  531. upper_bound = median + threshold_high * mad # Upper threshold
  532. return (series < lower_bound) | (series > upper_bound) # Return a boolean with outliers
  533. def DF_filtering(df):
  534. """
  535. Filter outliers from each column of a DataFrame using MAD.
  536. Args:
  537. df (pandas.DataFrame): DataFrame to filter column-wise based on MAD outlier analysis.
  538. Returns:
  539. pandas.DataFrame: DataFrame with outliers removed per column.
  540. """
  541. filtered_dict = {} # Empty dictionary to store filtered dataframes
  542. for column in df.columns: # Iterate through each column
  543. data = df[column].dropna() # Removes missing values to avoid propagating NaNs into MAD and comparisons
  544. outliers = is_outlier(data) # Compute outlier mask for the current column
  545. filtered_data = data[~outliers] # Keep only non-outlier values
  546. filtered_dict[column] = filtered_data.reset_index(drop=True) # Reset index and store filtered data in the dictionary
  547. filtered_values_df = pd.DataFrame(filtered_dict) # Build new dataframe with filtered results
  548. return filtered_values_df
  549. def analyze_vessel_diameters(image, vessel_seg, edge_labels, gray_image, file_name, tile_index):
  550. """
  551. Compute vessel diameters and visualise diameter crosslines.
  552. Args:
  553. image (ndarray): Raw image data used for diameter extraction.
  554. vessel_seg (ndarray): Vessel segmentation mask.
  555. edge_labels (ndarray): Labelled edges image from skeletonisation.
  556. gray_image (ndarray): Greyscale MIP image.
  557. file_name (str): File name used to derive the sample ID.
  558. tile_index (int): Index of the tile being processed (used for labelling and individual tile vessel metrics analysis).
  559. Returns:
  560. tuple:
  561. diam_values (list[float]): Diameter values in pixels.
  562. viz (ndarray): Visualisation image of diameter crosslines.
  563. filtered_diam_values (pandas.DataFrame): MAD-filtered diameters.
  564. Returns ([], None, None) on failure.
  565. """
  566. try:
  567. viz, diameters = vm.whole_anatomy_diameter(image, vessel_seg, edge_labels) # Extract diameter measurements from the entire ROI
  568. cmap2 = matplotlib.colors.LinearSegmentedColormap.from_list("", [(1, 1, 1, 0), (1, 0, 0, 1)]) # Define colourmap for overlay (transparent to red)
  569. fig, axes = plt.subplots(1, 3, figsize=(15, 5)) # Figure with 3 panels
  570. axes[0].imshow(gray_image, cmap="gray") # Display MIP
  571. axes[0].set_title("Maximum Intensity Projection (MIP)")
  572. axes[0].axis("off")
  573. axes[1].imshow(viz, cmap="gray_r") # Display diameter crosslines
  574. axes[1].set_title("Diameters")
  575. axes[1].axis("off")
  576. axes[2].imshow(gray_image, cmap="gray") # Display overlay of diameter crosslines over MIP
  577. axes[2].imshow(viz, cmap=cmap2)
  578. axes[2].set_title("Overlap")
  579. axes[2].axis("off")
  580. plt.show() # Show the figure
  581. sampleID = file_name.split('.tiff')[0]
  582. fig_title = f"{sampleID}_tile{tile_index}_diameter_plot" # Title for the figure
  583. output_pdf_path = os.path.join(output_path, f"{fig_title}.pdf") # Define output PDF path
  584. save_plot_as_pdf(fig, output_pdf_path) # Save figure as PDF
  585. num_diameters = diameters[-1][0] # Re-index diameters with unique identifiers after previous diameter removal
  586. indexed_diameters = [] # Empty list to store new indexed diameters
  587. for new_index in range(1, num_diameters + 1): # Loop through each diameter identifier
  588. found = next((d for d in diameters if d[0] == new_index), None) # Find unique identifiers
  589. if found:
  590. indexed_diameters.append((new_index, found[1])) # If vessel ID was found, keep identifier
  591. filtered_diameters = [(idx, diam) for idx, diam in indexed_diameters if diam > 0] # Remove crosslines that present diameters with zero values
  592. diam_values = [diam for idx, diam in filtered_diameters] # Extract only positive diameter values
  593. filtered_diam_values = DF_filtering(pd.DataFrame(diam_values, columns=['Diameter (px)'])) # MAD-based filtering of outliers
  594. return diam_values, viz, filtered_diam_values # Return the diameter values
  595. except Exception as e:
  596. print(f"Error analyzing vessel diameters: {e}") # Print error message if diameter analysis fails
  597. return [], None, None # Return an empty list in case of an error
  598. def visualize_vessel_metrics(edge_labels, diam_values, scale = scale):
  599. """
  600. Calculate vessel length, tortuosity, and diameter metrics and performs MAD filtering.
  601. Args:
  602. edge_labels (ndarray): Labelled edges image from skeletonisation.
  603. diam_values (list[float]): Vessel diameter values in pixels.
  604. scale (float): Pixels per micron for spatial conversion. Users need to set this parameter based on the imaging resolution of their dataset.
  605. Returns:
  606. tuple:
  607. length (ndarray): Segment lengths in pixels.
  608. vessel_len (float): Mean segment length in pixels.
  609. tort (ndarray): Tortuosity values per segment.
  610. vessel_tor (float): Mean tortuosity.
  611. diam_values_um (ndarray): Diameters converted to microns.
  612. length_um (ndarray): Segment lengths converted to microns.
  613. filtered_length_df (pandas.DataFrame): MAD-filtered lengths (pixels).
  614. filtered_length_um_df (pandas.DataFrame): MAD-filtered lengths (microns).
  615. filtered_diam_values_df (pandas.DataFrame): MAD-filtered diameters (pixels).
  616. filtered_diam_values_um_df (pandas.DataFrame): MAD-filtered diameters (microns).
  617. Returns None on failure.
  618. """
  619. try:
  620. _, length = vm.vessel_length(edge_labels) # Calculate the length of each segment (pixels)
  621. vessel_len = np.mean(length) # Compute the mean of segment lengths
  622. tort, _ = vm.tortuosity(edge_labels) # Calculate tortuosity values
  623. vessel_tor = np.mean(tort) # Compute the mean tortuosity
  624. tort = np.array(tort) # Transform values into array
  625. # Spatial unit convertion: pixels to microns
  626. diam_values_um = np.array(diam_values) / scale # Convert diameter values
  627. length_um = length / scale # Convert segment length values
  628. filtered_length_df = DF_filtering(pd.DataFrame(length)) # Filter outliers for segment length in pixels
  629. filtered_length_um_df = DF_filtering(pd.DataFrame(length_um)) # Filter outliers for segment length in microns
  630. filtered_diam_values_df = DF_filtering(pd.DataFrame(diam_values)) # Filter outliers for vessel diameters in pixels
  631. filtered_diam_values_um_df = DF_filtering(pd.DataFrame(diam_values_um)) # Filter outliers for vessel diameters in microns
  632. return length, vessel_len, tort, vessel_tor, diam_values_um, length_um, filtered_length_df, filtered_length_um_df, filtered_diam_values_df, filtered_diam_values_um_df
  633. except Exception as e:
  634. print(f"Error visualizing vessel metrics: {e}") # Print error message if vessel measurement fails
  635. return None
  636. def analyze_vessel_network(image, vessel_seg, edges, label, scale = scale, window = window_size):
  637. """
  638. Compute network-level metrics such as total length and density.
  639. Args:
  640. image (ndarray): Raw image data.
  641. vessel_seg (ndarray): Vessel segmentation mask.
  642. edges (ndarray): Skeleton edges image.
  643. label (ndarray): Labelled image for branchpoint density.
  644. scale (float): Pixels per micron for spatial conversion.
  645. window (int): Window size (microns) for density binning.
  646. Returns:
  647. tuple:
  648. net_length (float): Total network length in pixels.
  649. net_length_um (float): Total network length in microns.
  650. density (float): Mean vessel density.
  651. density_array (ndarray): Bin density values.
  652. overlay (ndarray): Density overlay image.
  653. vessel_density (float): Fraction of vessel in micron².
  654. bp_density (tuple): Branchpoint density outputs.
  655. Returns None on failure.
  656. """
  657. density_scale = np.floor(scale * window).astype(int) # Convert pixel scale to a 10×10 grid corresponding to 100µm² bins
  658. try:
  659. net_length = vm.network_length(edges) # Calculate total network length (sum of vessel segments)
  660. net_length_um = net_length / scale # Convert total network length into microns
  661. density, density_array, overlay = vm.vessel_density(image, vessel_seg, density_scale, density_scale) # Compute vessel density map
  662. vessel_density = sum(density_array) # Number of vessel pixels versus total pixels
  663. bp_density = vm.branchpoint_density(label) # Optional: Calculates branchpoint density
  664. return net_length, net_length_um, density, density_array, overlay, vessel_density, bp_density
  665. except Exception as e:
  666. print(f"Error analyzing vessel network: {e}") # Print error message if network measurements fail
  667. def save_results_to_excel(results, diam_results):
  668. """
  669. Save vessel analysis metrics and raw values to an Excel file.
  670. Args:
  671. results (list[list]): Summary metrics per sample.
  672. diam_results (dict): Raw and filtered diameters and length results (pixels and microns).
  673. Returns:
  674. None
  675. """
  676. try:
  677. # Sheet 1: Summary embryo metrics
  678. embryo_metrics = pd.DataFrame(results) # Convert results into a dataframe
  679. # Assign columns names to the summary results sheet
  680. embryo_metrics.columns = ["File name", "Area", "Connectivity", "Q", "Jaccard",
  681. "Mean raw vessel length (px)", "Mean raw vessel Length (um)",
  682. "Mean vessel length (px)", "Mean vessel Length (um)",
  683. "Mean raw vessel diameter (px)", "Mean raw vessel diameter (um)",
  684. "Mean vessel diameter (px)", "Mean vessel diameter (um)",
  685. "Vessel Tortuosity", "Network Length (px)", "Network Length (um)",
  686. "Vessel_density", "Branchpoints"]
  687. # Sheet 2: Raw and filtered diameter values (px)
  688. diam_values_df = pd.DataFrame(diam_results["diam_values"]).transpose() # Convert entries into dataframe and transpose so each row corresponds to an image
  689. diam_values_df.columns = [f"Diameter {i+1}" for i in range(diam_values_df.shape[1])] # Rename columns with diameter identifiers for each embryo
  690. # Same for MAD-filtered diameter values
  691. filtered_diam_values_df = pd.DataFrame(diam_results["filtered_diam_values"]).transpose()
  692. filtered_diam_values_df.columns = [f"Diameter {i+1}" for i in range(filtered_diam_values_df.shape[1])]
  693. # Sheet 3: Raw and filtered diameter values (µm)
  694. diam_values_um_df = pd.DataFrame(diam_results["diam_values_um"]).transpose()
  695. diam_values_um_df.columns = [f"Diameter {i+1} (um)" for i in range(diam_values_um_df.shape[1])]
  696. filtered_diam_values_um_df = pd.DataFrame(diam_results["filtered_diam_values_um"]).transpose()
  697. filtered_diam_values_um_df.columns = [f"Diameter {i+1}" for i in range(filtered_diam_values_um_df.shape[1])]
  698. # Sheet 4: Vessel Lengths (px)
  699. vessel_lengths_df = pd.DataFrame(diam_results["length"]).transpose()
  700. vessel_lengths_df.columns = [f"Length {i+1}" for i in range(vessel_lengths_df.shape[1])]
  701. filtered_vessel_lengths_df = pd.DataFrame(diam_results["filtered_length"]).transpose()
  702. filtered_vessel_lengths_df.columns = [f"Length {i+1}" for i in range(filtered_vessel_lengths_df.shape[1])]
  703. # Sheet 5: Vessel Lengths (µm)
  704. vessel_lengths_um_df = pd.DataFrame(diam_results["length_um"]).transpose()
  705. vessel_lengths_um_df.columns = [f"Length {i+1}" for i in range(vessel_lengths_um_df.shape[1])]
  706. filtered_vessel_lengths_um_df = pd.DataFrame(diam_results["filtered_length_um"]).transpose()
  707. filtered_vessel_lengths_um_df.columns = [f"Length {i+1}" for i in range(filtered_vessel_lengths_um_df.shape[1])]
  708. # Save results to Excel
  709. output_excel_path = os.path.join(output_path, f"{output_name}.xlsx") # Construct the output path under within the working directory
  710. with pd.ExcelWriter(output_excel_path) as writer:
  711. # Write each DataFrame to a separate sheet
  712. embryo_metrics.to_excel(writer, sheet_name="Embryo Metrics", index=False)
  713. diam_values_df.to_excel(writer, sheet_name="Diameter Values", index=False)
  714. filtered_diam_values_df.to_excel(writer, sheet_name="MAD filtered diameter Values", index=False)
  715. diam_values_um_df.to_excel(writer, sheet_name="Diameter Values (um)", index=False)
  716. filtered_diam_values_um_df.to_excel(writer, sheet_name="MAD filtered diameter Values (um)", index=False)
  717. vessel_lengths_df.to_excel(writer, sheet_name="Vessel Lengths", index=False)
  718. filtered_vessel_lengths_df.to_excel(writer, sheet_name="MAD filtered vessel lengths", index=False)
  719. vessel_lengths_um_df.to_excel(writer, sheet_name="Vessel Lengths (um)", index=False)
  720. filtered_vessel_lengths_um_df.to_excel(writer, sheet_name="MAD filtered vessel lengths (um)", index=False)
  721. print(f"Results saved to {output_excel_path}")
  722. except Exception as e:
  723. print(f"Error saving results to Excel: {e}") # Print error message if saving results fail
  724. def process_all_files(data_path, output_path):
  725. """
  726. Process all single-channel images from the input directory.
  727. Args:
  728. data_path (str): Directory containing input images.
  729. output_path (str): Directory for saving outputs.
  730. Returns:
  731. None
  732. """
  733. file_names = [f for f in os.listdir(data_path) if f.startswith(name_start) and f.endswith(".czi")] # Find all images matching naming pattern
  734. all_results = [] # Empty list to store final metrics per embryo
  735. # Empty dictionary to store vessel metrics across samples
  736. all_diam_results = {
  737. "diam_values": [],
  738. "filtered_diam_values": [],
  739. "diam_values_um": [],
  740. "filtered_diam_values_um": [],
  741. "length": [],
  742. "filtered_length": [],
  743. "length_um": [],
  744. "filtered_length_um": [],
  745. }
  746. for file_name in file_names: # Loop through each file found in directory
  747. file_path = os.path.join(data_path, file_name) # Find the full file path for each image
  748. sampleID = file_name.split('_MaxInt')[0] # Retrieve sample identifier
  749. print(f"\n--- Processing {sampleID} ---")
  750. with czifile.CziFile(file_path) as czi:
  751. data = czi.asarray() # Read confocal image data from .czi file
  752. data_squeezed = np.squeeze(data) # Squeeze data to remove unwanted dimensions/metadata and retrieve image data
  753. n_tiles = tiles # Split lateral image by the number of horizontal tiles (decrease computational requirements and processing time)
  754. tile_width = data_squeezed.shape[1] // n_tiles # Calculate the width of each tile
  755. tile_metrics = [] # Empty list to store vessel measurements for each tile
  756. # Empty dictionary to store vessel metrics across tiles
  757. tile_diam_results = {
  758. "diam_values": [],
  759. "filtered_diam_values": [],
  760. "diam_values_um": [],
  761. "filtered_diam_values_um": [],
  762. "length": [],
  763. "filtered_length": [],
  764. "length_um": [],
  765. "filtered_length_um": [],
  766. }
  767. for i in range(n_tiles): # Loop through each tile
  768. # Find tile boundaries
  769. start_x = i * tile_width
  770. end_x = (i + 1) * tile_width if i < n_tiles - 1 else data_squeezed.shape[1]
  771. tile = data_squeezed[:, start_x:end_x] # Extract tile from full image
  772. image = tile # Rename tile image for convenience
  773. mip_image = cv2.normalize(image, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8) # Normalise image to uint8
  774. mip_image_rgb = cv2.cvtColor(mip_image, cv2.COLOR_GRAY2RGB)
  775. gray_image = cv2.cvtColor(mip_image_rgb, cv2.COLOR_RGB2GRAY)
  776. clahe_image = clahe.apply(gray_image) # Apply CLAHE for contrast enhacement
  777. _, otsu_mask = cv2.threshold(clahe_image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # Otsu thresholding
  778. output_mip_path = os.path.join(output_path, f"{sampleID}_tile{i+1}.tiff") # Define output MIP path
  779. output_mask_path = os.path.join(output_path, f"{sampleID}_tile{i+1}_mask.tiff") # Define output mask path
  780. display_and_save_mip_otsu(gray_image, clahe_image, otsu_mask, output_mip_path, output_mask_path)
  781. vessel_seg, _ = segment_and_analyze_vessels(data_path, file_name, image, gray_image, clahe_image, otsu_mask, output_path, i+1,
  782. im_filter=seg_method, sigma1=sigma1, hole_size=hole_size,
  783. ditzle_size=ditzle_size, thresh=thresh
  784. ) # Vessel segmentation and topology analysis
  785. if vessel_seg is None: # Skip tile if segmentation failed
  786. continue
  787. label_path = output_mask_path # Use mask path for metrics function
  788. length, area, conn, Q, jaccard, label, _ = calculate_and_print_metrics(label_path, vessel_seg) # Calculate vessel metrics
  789. skel, edges, edge_labels, _, _, _ = analyze_skeleton(vessel_seg, gray_image, file_name, i+1) # Perform skeletonisation and branchpoint analysis
  790. diam_values, viz, filtered_diam_values = analyze_vessel_diameters(image, vessel_seg, edge_labels, gray_image, file_name, i+1) # Measure vessel diameters
  791. # Calculate diameter and vessel length metrics, including spatial conversion
  792. (length, vessel_len, _, vessel_tor, diam_values_um, length_um, filtered_length_df,
  793. filtered_length_um_df, filtered_diam_values_df, filtered_diam_values_um_df) = visualize_vessel_metrics(edge_labels, diam_values, scale = scale)
  794. # Calculate overall network metrics
  795. net_length, net_length_um, _, _, _, vessel_density, bp_density = analyze_vessel_network(image, vessel_seg, edges, label, scale = scale, window = window_size)
  796. # Store metric results for selected tile
  797. tile_metrics.append([
  798. area, conn, Q, jaccard, vessel_len,
  799. np.mean(length_um), filtered_length_df.values.mean(), filtered_length_um_df.values.mean(),
  800. np.mean(diam_values), np.mean(diam_values_um), filtered_diam_values_df.values.mean(), filtered_diam_values_um_df.values.mean(),
  801. vessel_tor, net_length, net_length_um, vessel_density, len(bp_density[0])
  802. ])
  803. # Store raw metrics for further saving
  804. tile_diam_results["diam_values"].append(diam_values)
  805. tile_diam_results["filtered_diam_values"].append(filtered_diam_values.values.flatten().tolist() if filtered_diam_values is not None else [])
  806. tile_diam_results["diam_values_um"].append(diam_values_um)
  807. tile_diam_results["filtered_diam_values_um"].append(filtered_diam_values_um_df.values.flatten().tolist() if filtered_diam_values_um_df is not None else [])
  808. tile_diam_results["length"].append(length)
  809. tile_diam_results["filtered_length"].append(filtered_length_df.values.flatten().tolist() if filtered_length_df is not None else [])
  810. tile_diam_results["length_um"].append(length_um)
  811. tile_diam_results["filtered_length_um"].append(filtered_length_um_df.values.flatten().tolist() if filtered_length_um_df is not None else [])
  812. if tile_metrics: # Ensure all tile metrics were processed successfully
  813. tile_metrics_np = np.array(tile_metrics, dtype=np.float64) # Convert arrays for averaging
  814. avg_metrics = np.nanmean(tile_metrics_np, axis=0) # Average metrics across tiles
  815. sum_metrics = np.nansum(tile_metrics_np[:, 13:17], axis=0) # Sum network metrics
  816. avg_metrics[13:17] = sum_metrics # Replace averaged entries with summed values
  817. all_results.append([sampleID] + avg_metrics.tolist()) # Store final results per embryo
  818. per_tile_results = [] # Save metrics per tile
  819. for idx, metrics in enumerate(tile_metrics):
  820. per_tile_results.append([sampleID, f"Tile {idx+1}"] + metrics)
  821. # Store all tile-specific metrics
  822. if 'all_tile_results' not in locals():
  823. all_tile_results = []
  824. all_tile_results.extend(per_tile_results)
  825. # Flatten lists so each sample has 1 row per metric type
  826. for key in all_diam_results.keys():
  827. flat = [item for sublist in tile_diam_results[key] for item in (sublist if isinstance(sublist, list) else [sublist])]
  828. all_diam_results[key].append(flat)
  829. save_results_to_excel(all_results, all_diam_results) # Save summary results to Excel
  830. try: # Save per-tile results to Excel
  831. per_tile_df = pd.DataFrame(all_tile_results)
  832. per_tile_df.columns = ["File name", "Tile", "Area", "Connectivity", "Q", "Jaccard",
  833. "Mean raw vessel length (px)", "Mean raw vessel Length (um)",
  834. "Mean vessel length (px)", "Mean vessel Length (um)",
  835. "Mean raw vessel diameter (px)", "Mean raw vessel diameter (um)",
  836. "Mean vessel diameter (px)", "Mean vessel diameter (um)",
  837. "Vessel Tortuosity", "Network Length (px)", "Network Length (um)",
  838. "Vessel_density", "Branchpoints"]
  839. output_tile_excel_path = os.path.join(output_path, f"{output_tile_name}.xlsx")
  840. per_tile_df.to_excel(output_tile_excel_path, index=False)
  841. print(f"Per-tile results saved to {output_tile_excel_path}")
  842. except Exception as e:
  843. print(f"Error saving per-tile results to Excel: {e}") # Print error message if tile-specific results were not saved successfully
  844. def process_all_files_double(data_path, output_path):
  845. """
  846. Process all multi-channel images from the input directory.
  847. Args:
  848. data_path (str): Directory containing input images.
  849. output_path (str): Directory for saving outputs.
  850. Returns:
  851. None
  852. """
  853. file_names = [f for f in os.listdir(data_path) if f.startswith(name_start) and f.endswith(".czi")] # Find all images matching naming pattern
  854. all_results = [] # Empty list to store final metrics per embryo
  855. # Empty dictionary to store vessel metrics across samples
  856. all_diam_results = {
  857. "diam_values": [],
  858. "filtered_diam_values": [],
  859. "diam_values_um": [],
  860. "filtered_diam_values_um": [],
  861. "length": [],
  862. "filtered_length": [],
  863. "length_um": [],
  864. "filtered_length_um": [],
  865. }
  866. for file_name in file_names[0:2]: # Loop through each file found in directory
  867. file_path = os.path.join(data_path, file_name) # Find the full file path for each image
  868. sampleID = file_name.split('_MaxInt')[0] # Retrieve sample identifier
  869. print(f"\n--- Processing {sampleID} ---")
  870. with czifile.CziFile(file_path) as czi:
  871. data = czi.asarray() # Read confocal image data from .czi file
  872. data_squeezed = np.squeeze(data)
  873. data_squeezed = data_squeezed[channel]
  874. n_tiles = tiles # Split lateral image by the number of horizontal tiles (decrease computational requirements and processing time)
  875. tile_width = data_squeezed.shape[1] // n_tiles # Calculate the width of each tile
  876. tile_metrics = [] # Empty list to store vessel measurements for each tile
  877. # Empty dictionary to store vessel metrics across tiles
  878. tile_diam_results = {
  879. "diam_values": [],
  880. "filtered_diam_values": [],
  881. "diam_values_um": [],
  882. "filtered_diam_values_um": [],
  883. "length": [],
  884. "filtered_length": [],
  885. "length_um": [],
  886. "filtered_length_um": [],
  887. }
  888. for i in range(n_tiles): # Loop through each tile
  889. # Find tile boundaries
  890. start_x = i * tile_width
  891. end_x = (i + 1) * tile_width if i < n_tiles - 1 else data_squeezed.shape[1]
  892. tile = data_squeezed[:, start_x:end_x] # Extract tile from full image
  893. image = tile # Rename tile image for convenience
  894. mip_image = cv2.normalize(image, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8) # Normalise image to uint8
  895. mip_image_rgb = cv2.cvtColor(mip_image, cv2.COLOR_GRAY2RGB)
  896. gray_image = cv2.cvtColor(mip_image_rgb, cv2.COLOR_RGB2GRAY)
  897. clahe_image = clahe.apply(gray_image) # Apply CLAHE for contrast enhacement
  898. _, otsu_mask = cv2.threshold(clahe_image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # Otsu thresholding
  899. output_mip_path = os.path.join(output_path, f"{sampleID}_tile{i+1}.tiff") # Define output MIP path
  900. output_mask_path = os.path.join(output_path, f"{sampleID}_tile{i+1}_mask.tiff") # Define output mask path
  901. display_and_save_mip_otsu(gray_image, clahe_image, otsu_mask, output_mip_path, output_mask_path)
  902. vessel_seg, _ = segment_and_analyze_vessels(data_path, file_name, image, gray_image, clahe_image, otsu_mask, output_path, i+1,
  903. im_filter=seg_method, sigma1=sigma1, hole_size=hole_size,
  904. ditzle_size=ditzle_size, thresh=thresh
  905. ) # Vessel segmentation and topology analysis
  906. if vessel_seg is None: # Skip tile if segmentation failed
  907. continue
  908. label_path = output_mask_path # Use mask path for metrics function
  909. length, area, conn, Q, jaccard, label, _ = calculate_and_print_metrics(label_path, vessel_seg) # Calculate vessel metrics
  910. skel, edges, edge_labels, _, _, _ = analyze_skeleton(vessel_seg, gray_image, file_name, i+1) # Perform skeletonisation and branchpoint analysis
  911. diam_values, viz, filtered_diam_values = analyze_vessel_diameters(image, vessel_seg, edge_labels, gray_image, file_name, i+1) # Measure vessel diameters
  912. # Calculate diameter and vessel length metrics, including spatial conversion
  913. (length, vessel_len, _, vessel_tor, diam_values_um, length_um, filtered_length_df,
  914. filtered_length_um_df, filtered_diam_values_df, filtered_diam_values_um_df) = visualize_vessel_metrics(edge_labels, diam_values, scale = scale)
  915. # Calculate overall network metrics
  916. net_length, net_length_um, _, _, _, vessel_density, bp_density = analyze_vessel_network(image, vessel_seg, edges, label, scale = scale, window = window_size)
  917. # Store metric results for selected tile
  918. tile_metrics.append([
  919. area, conn, Q, jaccard, vessel_len,
  920. np.mean(length_um), filtered_length_df.values.mean(), filtered_length_um_df.values.mean(),
  921. np.mean(diam_values), np.mean(diam_values_um), filtered_diam_values_df.values.mean(), filtered_diam_values_um_df.values.mean(),
  922. vessel_tor, net_length, net_length_um, vessel_density, len(bp_density[0])
  923. ])
  924. # Store raw metrics for further saving
  925. tile_diam_results["diam_values"].append(diam_values)
  926. tile_diam_results["filtered_diam_values"].append(filtered_diam_values.values.flatten().tolist() if filtered_diam_values is not None else [])
  927. tile_diam_results["diam_values_um"].append(diam_values_um)
  928. tile_diam_results["filtered_diam_values_um"].append(filtered_diam_values_um_df.values.flatten().tolist() if filtered_diam_values_um_df is not None else [])
  929. tile_diam_results["length"].append(length)
  930. tile_diam_results["filtered_length"].append(filtered_length_df.values.flatten().tolist() if filtered_length_df is not None else [])
  931. tile_diam_results["length_um"].append(length_um)
  932. tile_diam_results["filtered_length_um"].append(filtered_length_um_df.values.flatten().tolist() if filtered_length_um_df is not None else [])
  933. if tile_metrics: # Ensure all tile metrics were processed successfully
  934. tile_metrics_np = np.array(tile_metrics, dtype=np.float64) # Convert arrays for averaging
  935. avg_metrics = np.nanmean(tile_metrics_np, axis=0) # Average metrics across tiles
  936. sum_metrics = np.nansum(tile_metrics_np[:, 13:17], axis=0) # Sum network metrics
  937. avg_metrics[13:17] = sum_metrics # Replace averaged entries with summed values
  938. all_results.append([sampleID] + avg_metrics.tolist()) # Store final results per embryo
  939. per_tile_results = [] # Save metrics per tile
  940. for idx, metrics in enumerate(tile_metrics):
  941. per_tile_results.append([sampleID, f"Tile {idx+1}"] + metrics)
  942. # Store all tile-specific metrics
  943. if 'all_tile_results' not in locals():
  944. all_tile_results = []
  945. all_tile_results.extend(per_tile_results)
  946. # Flatten lists so each sample has 1 row per metric type
  947. for key in all_diam_results.keys():
  948. flat = [item for sublist in tile_diam_results[key] for item in (sublist if isinstance(sublist, list) else [sublist])]
  949. all_diam_results[key].append(flat)
  950. save_results_to_excel(all_results, all_diam_results) # Save summary results to Excel
  951. try: # Save per-tile results to Excel
  952. per_tile_df = pd.DataFrame(all_tile_results)
  953. per_tile_df.columns = ["File name", "Tile", "Area", "Connectivity", "Q", "Jaccard",
  954. "Mean raw vessel length (px)", "Mean raw vessel Length (um)",
  955. "Mean vessel length (px)", "Mean vessel Length (um)",
  956. "Mean raw vessel diameter (px)", "Mean raw vessel diameter (um)",
  957. "Mean vessel diameter (px)", "Mean vessel diameter (um)",
  958. "Vessel Tortuosity", "Network Length (px)", "Network Length (um)",
  959. "Vessel_density", "Branchpoints"]
  960. output_tile_excel_path = os.path.join(output_path, f"{output_tile_name}.xlsx")
  961. per_tile_df.to_excel(output_tile_excel_path, index=False)
  962. print(f"Per-tile results saved to {output_tile_excel_path}")
  963. except Exception as e:
  964. print(f"Error saving per-tile results to Excel: {e}") # Print error message if tile-specific results were not saved successfully
  965. # %% [markdown]
  966. # ## <a id='toc3_3_'></a>[Run samples - Single channel](#toc0_)
  967. # %%
  968. if not os.path.exists(output_path):
  969. os.makedirs(output_path)
  970. process_all_files(data_path, output_path)
  971. # %% [markdown]
  972. # ## <a id='toc3_4_'></a>[Run samples - Two or more channels](#toc0_)
  973. # %%
  974. if not os.path.exists(output_path):
  975. os.makedirs(output_path)
  976. process_all_files_double(data_path, output_path)

VISTA-Z_lateral.ipynb at commit 6dd30c4, under MIT · at the source

Overview

Authors: Ignacio Rodriguez-Pastrana1, Joanna Richens1, Robert N Wilkinson1
  1. School of Life Sciences, University of Nottingham, Nottingham, NG7 2UH UK
Institutions: University of Nottingham (United Kingdom)
Journal: Scientific reports, volume 16, issue 1, article 15611
Dates: received 19 December 2025; accepted 3 March 2026; published online 1 April 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1038/s41598-026-43301-5 · PMID 41922411 · PMCID PMC13187487 · OpenAlex W7147264012
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: zebrafish (organism), methods / tools (subfield)
Methods: Statistics
Keywords: Zebrafish, Vascular imaging, Automated image analysis, Vessel segmentation, Angiogenesis, High-throughput phenotyping, Biological techniques, Biotechnology, Computational biology and bioinformatics
MeSH: Angiogenesis*, Blood Vessels*, Image Processing, Computer-Assisted*, Neovascularization, Physiologic*, Zebrafish*, Animals, Animals, Genetically Modified, Embryo, Nonmammalian, Forkhead Transcription Factors, Zebrafish Proteins (* major topic)
Topic: Zebrafish Biomedical Research Applications (Cell Biology, Biochemistry, Genetics and Molecular Biology), according to OpenAlex
Funding: Medical Research Council (MR/X008215/1)
Citations: cited by 1 paper (Europe PMC); 57 references in the paper

Abstract

High-resolution vascular imaging in zebrafish embryos offers unparalleled insight into angiogenesis, yet quantitative analysis remains limited by manual workflows and inconsistent segmentation. To overcome this, we developed VISTA-Z (Vascular Imaging and Segmentation for Topology Analysis in Zebrafish), an automated Python-based pipeline designed to standardise vascular quantification across diverse fluorescent datasets. The workflow combines adaptive contrast enhancement, vessel segmentation using Meijering filtering, artefact removal through segment labelling, and skeleton-based topology analysis with refined branchpoint detection. Metrics are normalised to physical units and imaging depth, enabling reproducible comparisons across experiments. We validated VISTA-Z using multiple endothelial transgenic lines and developmental stages. The pipeline detected subtle and severe phenotypes, including brain vessel loss in foxc1a mutants, brain and trunk vessel loss in kdrl mutants and widespread hyper-angiogenesis in plxnd1 crispants. VISTA-Z is an open-source, scalable platform for reproducible high-throughput quantification of zebrafish vascular architecture, providing a standardised framework for developmental research and preclinical screening

Supplementary Information: The online version contains supplementary material available at 10.1038/s41598-026-43301-5.

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 6 matches between paragraphs and lines of code.

fishyvessels/VISTA-Z

License: MIT
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: 6dd30c47f0092c3becbecf6e8fca0d28e6a16bd5, 3 April 2026
Languages: Jupyter (6)
Size: 12 files, 6 scripts
Software Heritage: not archived
Found in: “Data availability”
Holds: README, license file, environment (Codes/requirements.txt), 6 notebooks
Not found: CITATION.cff, tests, continuous integration, documentation
Tools: NumPy (6 files), SciPy (6 files), Matplotlib (5 files), OpenCV (5 files), Plotly (5 files), scikit-image (5 files), imageio (4 files), pandas (4 files), seaborn (4 files), napari (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
8 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;
  • 6 scripts, each with its path and the digest of its content;
  • 6 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

Code is available via GitHub (https://github.com/fishyvessels/VISTA-Z). Test image datasets are available on BioImages Archive (S-BIAD2914): https://www.ebi.ac.uk/biostudies/bioimages/studies/S-BIAD2914. Other data available upon reasonable request from the corresponding author.

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

Recorded: type, language, journal, volume, issue, pages, dates, 3 authors, 9 keywords, 10 MeSH terms, 1 funder, 55 references.

Cite

This paper

Rodriguez-Pastrana, I., Richens, J., & Wilkinson, R. N. (2026). Automated analysis of zebrafish vascular networks using the VISTA-Z pipeline. Scientific reports, 16(1), 15611. https://doi.org/10.1038/s41598-026-43301-5

BibTeX

@article{rodriguezpastrana2026automated,
author = {Rodriguez-Pastrana, Ignacio and Richens, Joanna and Wilkinson, Robert N},
title = {{Automated analysis of zebrafish vascular networks using the VISTA-Z pipeline}},
journal = {Scientific reports},
year = {2026},
month = apr,
volume = {16},
number = {1},
pages = {15611},
publisher = {Nature Publishing Group},
issn = {2045-2322},
doi = {10.1038/s41598-026-43301-5},
url = {https://doi.org/10.1038/s41598-026-43301-5},
pmid = {41922411},
pmcid = {PMC13187487}
}

RIS

TY - JOUR
AU - Rodriguez-Pastrana, Ignacio
AU - Richens, Joanna
AU - Wilkinson, Robert N
TI - Automated analysis of zebrafish vascular networks using the VISTA-Z pipeline
T2 - Scientific reports
J2 - Sci Rep
PY - 2026
DA - 2026/04/01
VL - 16
IS - 1
SP - 15611
SN - 2045-2322
PB - Nature Publishing Group
DO - 10.1038/s41598-026-43301-5
UR - https://doi.org/10.1038/s41598-026-43301-5
LA - en
ER -

CSL-JSON

{
"id": "10.1038/s41598-026-43301-5",
"type": "article-journal",
"title": "Automated analysis of zebrafish vascular networks using the VISTA-Z pipeline",
"container-title": "Scientific reports",
"author": [
{
"family": "Rodriguez-Pastrana",
"given": "Ignacio"
},
{
"family": "Richens",
"given": "Joanna"
},
{
"family": "Wilkinson",
"given": "Robert N"
}
],
"container-title-short": "Sci Rep",
"volume": "16",
"issue": "1",
"page": "15611",
"DOI": "10.1038/s41598-026-43301-5",
"PMID": "41922411",
"PMCID": "PMC13187487",
"ISSN": "2045-2322",
"publisher": "Nature Publishing Group",
"URL": "https://doi.org/10.1038/s41598-026-43301-5",
"language": "en",
"issued": {
"date-parts": [
[
2026,
4,
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.1038/s41467-026-75352-7 [code]
Mechanosensory encoding of surface mechanics optimizes locomotion.
Journal: Nature communications
In common: napari, imageio, Plotly, 7 other tools
[2] doi:10.1038/s42003-026-10957-8 [code]
Brain defence by the extracellular matrix protein Cochlin.
Journal: Communications biology
In common: Plotly, OpenCV, scikit-image, 5 other tools, zebrafish, 2 references
[3] doi: [code]
Real-time closed-loop feedback system for mouse mesoscale cortical signal and movement control
Journal: eLife
In common: napari, imageio, OpenCV, 6 other tools
[4] doi:10.1371/journal.pcbi.1014571 [code]
SynAPSeg: A novel dataset and image analysis framework for deep learning-based synapse detection and quantification.
Journal: PLoS computational biology
In common: napari, imageio, OpenCV, 6 other tools
[5] doi:10.1016/j.celrep.2026.117420 [code]
Neural population dynamics of direct electrical stimulation of neocortex.
Journal: Cell reports
In common: napari, imageio, OpenCV, 6 other tools
[6] doi:10.1038/s41593-026-02262-8 [code]
Cheese3D enables sensitive detection and analysis of whole-face movement in mice.
Journal: Nature neuroscience
In common: napari, imageio, OpenCV, 6 other tools
[7] doi:10.1038/s41467-026-73045-9 [code]
Aberration-aware 3D localization microscopy via self-supervised neural-physics learning.
Journal: Nature communications
In common: napari, imageio, OpenCV, 5 other tools
[8] doi:10.3389/fendo.2026.1828487 [code]
Castration-induced nigrostriatal deficits are linked to reduced TrkB and loss of mature spines in the dorsal striatum.
Journal: Frontiers in endocrinology
In common: imageio, Plotly, OpenCV, 6 other tools
[9] doi:10.1186/s12880-026-02481-2 [code]
Deep learning-based neuroanatomical profiling reveals population-specific brain changes in multiple sclerosis: a large-scale Middle Eastern study.
Journal: BMC medical imaging
In common: imageio, Plotly, OpenCV, 6 other tools
[10] doi:10.1371/journal.pone.0348866 [code]
Using deep learning to identify inherited retinal diseases based on wide-field retinal imaging data.
Journal: PloS one
In common: imageio, Plotly, OpenCV, 6 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.