OSCR

Assessment of adult structural plasticity in Drosophila neurons.

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] § Material and methods › Structural complexity quantification ↔ src/image_processor.py, lines 458–572 · score 0.79 · standard deviations, intensity weighted, global spreads, Local spreads, position, metric
  2. [2] § Material and methods › Structural complexity quantification ↔ src/MorphoScope.py, lines 532–575 · score 0.77 · graphical user interface, PySide6, MorphoScope, PyQtGraph, lsm, dimensional
  3. [3] § Material and methods › Structural complexity quantification ↔ src/MorphoScope.py, lines 1318–1418 · score 0.76 · Gaussian blur, median filtering, maximum intensity, noise, thresholding, stack
  4. [4] § Material and methods › Structural complexity quantification ↔ src/image_processor.py, lines 458–572 · score 0.61 · integrated intensity, axonal volume, um3, physical, sum, metrics
  5. [5] § Material and methods › Structural complexity quantification ↔ validation/synthetic_volumes_generator.py, lines 556–593 · score 0.57 · fluorophore content, axonal volume, sum, voxels, intensity
  6. [6] § Material and methods › Structural complexity quantification ↔ validation/synthetic_volumes_generator.py, lines 641–712 · score 0.54 · spread quantification algorithm, MorphoScope, dimensional, axonal

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 · 770 lines · 30 KB · MIT · 2 matches

  1. """
  2. Core Image Processor for Structural Plasticity Analysis
  3. Implements the quantification algorithm described in Petsakou et al., 2015:
  4. "Circadian rhythms in Rho1 activity regulate neuronal plasticity and network hierarchy"
  5. This module provides tools to analyze 3D microscopy images of neuronal projections,
  6. quantifying their structural plasticity by measuring axonal spread in three dimensions.
  7. Key Features:
  8. - PCA-based image alignment for consistent orientation
  9. - Calculation of local and global spread metrics
  10. - Comprehensive fluorescence quantification
  11. Author: Francisco Tassara
  12. Date: 2025-11-12
  13. Based on: Petsakou, Sapsis & Blau, Cell 2015
  14. """
  15. import numpy as np
  16. from scipy import ndimage as ndi
  17. from typing import Tuple, Dict, Optional
  18. import logging
  19. import matplotlib.pyplot as plt
  20. from pathlib import Path
  21. logger = logging.getLogger(__name__)
  22. class ImageProcessor:
  23. """
  24. Core processor for structural plasticity quantification following Petsakou et al., 2015.
  25. Coordinate System Convention
  26. ----------------------------
  27. After PCA rotation and standardization (90° rotation):
  28. - X axis: HORIZONTAL direction (maximum projection length)
  29. - Y axis: VERTICAL direction (perpendicular to X, typically narrower)
  30. - Z axis: Depth direction (confocal slices, dorsal to ventral)
  31. Image Format
  32. ------------
  33. All methods expect images in (Y, X, Z) format = (rows, cols, slices)
  34. This matches numpy's natural indexing: image[y, x, z]
  35. Notes
  36. -----
  37. - The algorithm applies PCA to find the principal axis of the projection
  38. - An additional 90° rotation standardizes the coordinate system
  39. - Spread metrics are calculated along the mean curve of the projection
  40. """
  41. def __init__(self, voxel_size_x: float, voxel_size_y: float, voxel_size_z: float):
  42. """
  43. Initialize processor with voxel dimensions.
  44. Parameters
  45. ----------
  46. voxel_size_x : float
  47. Voxel size in X dimension (µm) - horizontal after standardization
  48. voxel_size_y : float
  49. Voxel size in Y dimension (µm) - vertical after standardization
  50. voxel_size_z : float
  51. Voxel size in Z dimension (µm/slice) - depth direction
  52. Examples
  53. --------
  54. >>> processor = ImageProcessor(voxel_size_x=0.124, voxel_size_y=0.124, voxel_size_z=1.0)
  55. """
  56. self.voxel_size_x = voxel_size_x
  57. self.voxel_size_y = voxel_size_y
  58. self.voxel_size_z = voxel_size_z
  59. logger.info(f"ImageProcessor initialized - Voxel sizes: "
  60. f"X={voxel_size_x:.4f}µm, Y={voxel_size_y:.4f}µm, Z={voxel_size_z:.4f}µm")
  61. def calculate_fluorescence(
  62. self,
  63. image_3d: np.ndarray,
  64. mask_area_pixels: float
  65. ) -> Tuple[float, float]:
  66. """
  67. Calculate normalized fluorescence metrics.
  68. Fluorescence is computed as total intensity normalized by the ROI area,
  69. providing both pixel-based and physical (µm²) measurements.
  70. Parameters
  71. ----------
  72. image_3d : np.ndarray
  73. 3D image stack (any shape compatible with processing)
  74. mask_area_pixels : float
  75. Region of interest (ROI) area in pixels (2D projection area)
  76. Returns
  77. -------
  78. Tuple[float, float]
  79. - fluorescence_per_pixel: Total intensity / ROI area (pixels)
  80. - fluorescence_per_um2: Total intensity / ROI area (µm²)
  81. Notes
  82. -----
  83. The fluorescence metric accounts for differences in imaging parameters
  84. and allows comparison across different experimental conditions.
  85. """
  86. total_intensity = np.sum(image_3d)
  87. # Fluorescence normalized by pixel count
  88. fluor_px = total_intensity / mask_area_pixels
  89. # Fluorescence normalized by physical area (µm²)
  90. area_um2 = mask_area_pixels * (self.voxel_size_x * self.voxel_size_y)
  91. fluor_um = total_intensity / area_um2
  92. logger.debug(f"Fluorescence - Per pixel: {fluor_px:.2f}, Per µm²: {fluor_um:.2f}")
  93. return round(fluor_px, 2), round(fluor_um, 2)
  94. def calculate_pca_rotation_angle(self, image_yxz: np.ndarray) -> float:
  95. """
  96. Calculate optimal rotation angle using Principal Component Analysis.
  97. This method finds the principal axis of the Z-projected image to align
  98. the projection along its maximum length direction. The first principal
  99. component (eigenvector with largest eigenvalue) defines the rotation angle.
  100. Parameters
  101. ----------
  102. image_yxz : np.ndarray
  103. 3D image with shape (Y, X, Z) = (rows, cols, slices)
  104. Returns
  105. -------
  106. float
  107. Rotation angle in degrees. Positive values indicate counter-clockwise rotation.
  108. Notes
  109. -----
  110. - The Z-projection is intensity-weighted for better alignment
  111. - Covariance matrix is computed from the normalized projection
  112. - Returns 0.0 if projection is empty or invalid
  113. References
  114. ----------
  115. Lee, J.M. (2007). Introduction to Smooth Manifolds. Springer.
  116. """
  117. # Create Z-projection (sum along depth axis)
  118. projection_z = np.sum(image_yxz, axis=2) # Shape: (Y, X) = (rows, cols)
  119. # Normalize projection for PCA
  120. sum_projection = np.sum(projection_z)
  121. if sum_projection == 0:
  122. logger.warning("Empty Z-projection - returning 0° rotation angle")
  123. return 0.0
  124. projection_z_norm = projection_z / sum_projection
  125. # Create coordinate grids
  126. nrows, ncols = projection_z.shape # (Y, X)
  127. xx = np.arange(ncols) # X coordinates (columns)
  128. yy = np.arange(nrows) # Y coordinates (rows)
  129. # Create meshgrid with 'ij' indexing for proper row/column correspondence
  130. YY, XX = np.meshgrid(yy, xx, indexing='ij') # Both shape (Y, X)
  131. # Calculate weighted centroid
  132. Mx = np.sum(XX * projection_z_norm)
  133. My = np.sum(YY * projection_z_norm)
  134. # Calculate covariance matrix components
  135. Cxx = np.sum(projection_z_norm * (XX - Mx)**2)
  136. Cyy = np.sum(projection_z_norm * (YY - My)**2)
  137. Cxy = np.sum(projection_z_norm * (XX - Mx) * (YY - My))
  138. # Construct covariance matrix
  139. cov_matrix = np.array([[Cxx, Cxy],
  140. [Cxy, Cyy]])
  141. # Eigenvalue decomposition
  142. eigenvalues, eigenvectors = np.linalg.eig(cov_matrix)
  143. # First eigenvector (largest eigenvalue) defines principal axis
  144. # Calculate rotation angle from eigenvector components
  145. phi = np.arctan(eigenvectors[1, 0] / eigenvectors[0, 0]) * 180 / np.pi
  146. logger.info(f"PCA rotation angle: {phi:.2f}°")
  147. logger.debug(f"Eigenvalues: {eigenvalues}")
  148. return phi
  149. def rotate_image(self, image_yxz: np.ndarray, angle: float) -> np.ndarray:
  150. """
  151. Rotate 3D image by specified angle in the XY plane.
  152. This method applies the rotation slice-by-slice to maintain the Z-axis alignment.
  153. Bilinear interpolation (order=1) is used for smooth rotation while preserving
  154. image properties.
  155. Parameters
  156. ----------
  157. image_yxz : np.ndarray
  158. 3D image with shape (Y, X, Z) = (rows, cols, slices)
  159. angle : float
  160. Rotation angle in degrees (positive = counter-clockwise)
  161. Returns
  162. -------
  163. np.ndarray
  164. Rotated 3D image. Shape may change if reshape=True is used.
  165. Output is in float64 format to preserve intensity values.
  166. Notes
  167. -----
  168. - Each Z-slice is rotated independently
  169. - The image is automatically resized to fit the rotated content
  170. - Intensity is approximately conserved (small losses due to interpolation)
  171. See Also
  172. --------
  173. scipy.ndimage.rotate : Underlying rotation function
  174. """
  175. n_slices = image_yxz.shape[2]
  176. # Determine output shape from first slice rotation
  177. rotated_first = ndi.rotate(
  178. image_yxz[:, :, 0],
  179. angle=angle,
  180. reshape=True, # Adjust canvas size to fit rotated content
  181. order=1 # Bilinear interpolation
  182. )
  183. # Preallocate output array
  184. rotated_image = np.zeros(
  185. (rotated_first.shape[0], rotated_first.shape[1], n_slices),
  186. dtype=image_yxz.dtype
  187. )
  188. # Rotate each Z-slice
  189. logger.debug(f"Rotating {n_slices} slices by {angle:.2f}°")
  190. for zi in range(n_slices):
  191. rotated_image[:, :, zi] = ndi.rotate(
  192. image_yxz[:, :, zi],
  193. angle=angle,
  194. reshape=True,
  195. order=1
  196. )
  197. # Log rotation statistics
  198. intensity_loss = (1 - np.sum(rotated_image) / np.sum(image_yxz)) * 100
  199. logger.debug(f"Rotation complete - New shape: {rotated_image.shape}")
  200. logger.debug(f"Intensity loss: {intensity_loss:.2f}%")
  201. logger.debug(f"Non-zero voxels: {np.count_nonzero(rotated_image)}")
  202. return rotated_image
  203. def plot_z_projection(
  204. self,
  205. image_yxz: np.ndarray,
  206. title: str = "Z-Projection",
  207. save_path: Optional[str] = None
  208. ) -> None:
  209. """
  210. Create and display/save a Z-projection visualization.
  211. This method generates a maximum intensity projection along the Z-axis
  212. for visual inspection of the image orientation and quality.
  213. Parameters
  214. ----------
  215. image_yxz : np.ndarray
  216. 3D image with shape (Y, X, Z) = (rows, cols, slices)
  217. title : str, optional
  218. Plot title (default: "Z-Projection")
  219. save_path : Optional[str], optional
  220. If provided, save figure to this path instead of displaying it
  221. Notes
  222. -----
  223. - Uses 'hot' colormap for better visualization of intensity variations
  224. - Includes intensity statistics in the plot
  225. - Grid overlay helps with spatial reference
  226. """
  227. # Create Z-projection (sum along depth axis)
  228. projection = np.sum(image_yxz, axis=2)
  229. # Create figure and axis
  230. fig, ax = plt.subplots(figsize=(10, 10))
  231. # Display projection with hot colormap
  232. im = ax.imshow(
  233. projection,
  234. cmap='hot',
  235. origin='lower', # Origin at bottom-left
  236. interpolation='nearest'
  237. )
  238. # Set labels and title
  239. ax.set_title(
  240. f'{title}\nShape: {image_yxz.shape}',
  241. fontsize=14,
  242. fontweight='bold'
  243. )
  244. ax.set_xlabel('X axis (pixels) - HORIZONTAL', fontsize=12)
  245. ax.set_ylabel('Y axis (pixels) - VERTICAL', fontsize=12)
  246. # Add colorbar
  247. cbar = plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
  248. cbar.set_label('Integrated Intensity', fontsize=12)
  249. # Add grid for spatial reference
  250. ax.grid(True, alpha=0.3, linestyle='--', linewidth=0.5)
  251. # Add statistics text box
  252. stats_text = (
  253. 'Statistics:\n'
  254. f'Min: {projection.min():.1f}\n'
  255. f'Max: {projection.max():.1f}\n'
  256. f'Mean: {projection.mean():.1f}\n'
  257. f'Total: {np.sum(projection):.0f}'
  258. )
  259. ax.text(
  260. 0.02, 0.98, stats_text,
  261. transform=ax.transAxes,
  262. fontsize=10,
  263. verticalalignment='top',
  264. bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.8)
  265. )
  266. plt.tight_layout()
  267. # Save or display
  268. if save_path:
  269. plt.savefig(save_path, dpi=150, bbox_inches='tight')
  270. logger.info(f"Z-projection saved to: {save_path}")
  271. plt.close(fig)
  272. else:
  273. plt.show()
  274. def calculate_local_spreads(
  275. self,
  276. image_yxz_rotated: np.ndarray,
  277. after_90deg_rotation: bool = False
  278. ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
  279. """
  280. Calculate local spread metrics along the principal axis.
  281. This method computes intensity-weighted variance (spread) in the Y and Z
  282. directions for each position along the X axis. The spreads quantify how
  283. the projection is distributed around its mean curve.
  284. Parameters
  285. ----------
  286. image_yxz_rotated : np.ndarray
  287. Rotated 3D image with shape (Y, X, Z) = (rows, cols, slices)
  288. after_90deg_rotation : bool, optional
  289. If True, assumes the standard coordinate convention where X is horizontal (columns).
  290. If False, uses MATLAB convention where X corresponds to rows.
  291. Default: False
  292. Returns
  293. -------
  294. Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]
  295. - MMsum: Total intensity at each X position
  296. - MMyy: Local variance in Y direction at each X position
  297. - MMzz: Local variance in Z direction at each X position
  298. - xx00: X coordinate array (1-indexed)
  299. Notes
  300. -----
  301. - Variances are intensity-weighted to account for non-uniform distributions
  302. - NaN values indicate positions with no signal
  303. - The method adapts to different coordinate conventions via `after_90deg_rotation`
  304. Mathematical Details
  305. --------------------
  306. For each position x_i:
  307. - MMsum[i] = Σ intensity at x_i
  308. - MMyy[i] = Σ (y - mean_y)² × intensity / MMsum[i] (weighted variance)
  309. - MMzz[i] = Σ (z - mean_z)² × intensity / MMsum[i] (weighted variance)
  310. """
  311. nrows, ncols, depth = image_yxz_rotated.shape
  312. if after_90deg_rotation:
  313. # Standard convention: X=horizontal (columns), Y=vertical (rows)
  314. logger.debug("Processing with standard convention: X=horizontal(cols), Y=vertical(rows)")
  315. xx00 = np.arange(1, ncols + 1) # X coordinates (columns), 1-indexed
  316. yy00 = np.arange(1, nrows + 1) # Y coordinates (rows), 1-indexed
  317. zz00 = np.arange(1, depth + 1) # Z coordinates (slices), 1-indexed
  318. # Preallocate arrays
  319. MMsum = np.zeros(ncols)
  320. MMyy = np.zeros(ncols)
  321. MMzz = np.zeros(ncols)
  322. # Iterate over columns (X axis)
  323. for i in range(ncols):
  324. # Extract all rows for column i: shape (nrows, depth) = (Y, Z)
  325. MM = image_yxz_rotated[:, i, :].T # Transpose to (Z, Y)
  326. MMsum[i] = np.sum(MM)
  327. if MMsum[i] > 0:
  328. # Calculate weighted means
  329. MMz = np.sum(zz00 * np.sum(MM, axis=1)) / MMsum[i]
  330. MMy = np.sum(yy00 * np.sum(MM, axis=0)) / MMsum[i]
  331. # Calculate weighted variances
  332. MMyy[i] = np.sum((yy00 - MMy)**2 * np.sum(MM, axis=0)) / MMsum[i]
  333. MMzz[i] = np.sum((zz00 - MMz)**2 * np.sum(MM, axis=1)) / MMsum[i]
  334. else:
  335. MMyy[i] = np.nan
  336. MMzz[i] = np.nan
  337. else:
  338. # MATLAB convention: iterate over first dimension (rows)
  339. logger.debug("Processing with MATLAB convention: iterating over rows")
  340. xx00 = np.arange(1, nrows + 1) # MATLAB "X" = rows, 1-indexed
  341. yy00 = np.arange(1, ncols + 1) # MATLAB "Y" = columns, 1-indexed
  342. zz00 = np.arange(1, depth + 1) # Z = slices, 1-indexed
  343. # Preallocate arrays
  344. MMsum = np.zeros(nrows)
  345. MMyy = np.zeros(nrows)
  346. MMzz = np.zeros(nrows)
  347. # Iterate over rows (MATLAB "X" axis)
  348. for i in range(nrows):
  349. MM = image_yxz_rotated[i, :, :].T # Shape: (Z, ncols)
  350. MMsum[i] = np.sum(MM)
  351. if MMsum[i] > 0:
  352. # Calculate weighted means
  353. MMz = np.sum(zz00 * np.sum(MM, axis=1)) / MMsum[i]
  354. MMy = np.sum(yy00 * np.sum(MM, axis=0)) / MMsum[i]
  355. # Calculate weighted variances
  356. MMyy[i] = np.sum((yy00 - MMy)**2 * np.sum(MM, axis=0)) / MMsum[i]
  357. MMzz[i] = np.sum((zz00 - MMz)**2 * np.sum(MM, axis=1)) / MMsum[i]
  358. else:
  359. MMyy[i] = np.nan
  360. MMzz[i] = np.nan
  361. logger.debug(f"Local spreads calculated - {len(xx00)} positions")
  362. logger.debug(f"Total intensity: {np.sum(MMsum):.2f}")
  363. return MMsum, MMyy, MMzz, xx00
  364. def calculate_global_spreads(
  365. self,
  366. MMsum: np.ndarray,
  367. MMyy: np.ndarray,
  368. MMzz: np.ndarray,
  369. xx00: np.ndarray
  370. ) -> Dict[str, float]:
  371. """
  372. Calculate global spread metrics from local spreads.
  373. This method aggregates the local spread measurements into global metrics
  374. that quantify the overall spatial extent of the neuronal projection in
  375. all three dimensions.
  376. Parameters
  377. ----------
  378. MMsum : np.ndarray
  379. Total intensity at each X position (from calculate_local_spreads)
  380. MMyy : np.ndarray
  381. Local Y-variances at each X position
  382. MMzz : np.ndarray
  383. Local Z-variances at each X position
  384. xx00 : np.ndarray
  385. X coordinate array
  386. Returns
  387. -------
  388. Dict[str, float]
  389. Dictionary containing:
  390. - spread_x_pixel, spread_y_pixel, spread_z_pixel: Individual axis spreads (pixels)
  391. - spread_xy_pixel, spread_xyz_pixel: Combined spreads (pixels)
  392. - spread_x_um, spread_y_um, spread_z_um: Individual axis spreads (µm)
  393. - spread_xy_um, spread_xyz_um: Combined spreads (µm)
  394. - axonal_volume: Total integrated intensity
  395. - MMsum, MMyy, MMzz: Arrays for further analysis/plotting
  396. Notes
  397. -----
  398. - All spreads are intensity-weighted averages of local spreads
  399. - The 3D spread (spread_xyz) is the product of spreads in all three axes
  400. - Axonal volume represents the total fluorescence signal
  401. Mathematical Details
  402. --------------------
  403. - spread_x = sqrt(Σ (x - mean_x)² × intensity / total_intensity)
  404. - spread_y = sqrt(Σ local_variance_y × intensity / total_intensity)
  405. - spread_z = sqrt(Σ local_variance_z × intensity / total_intensity)
  406. - spread_xyz = spread_x × spread_y × spread_z
  407. References
  408. ----------
  409. Petsakou et al., Cell 2015 - Supplementary Methods
  410. """
  411. total_sum = np.sum(MMsum)
  412. if total_sum == 0:
  413. logger.error("Total sum is zero - cannot calculate spreads")
  414. return self._get_empty_results()
  415. # Calculate X-spread (standard deviation of intensity distribution along X)
  416. MMx = np.sum(xx00 * MMsum) / total_sum
  417. MMxx = np.sum(MMsum * (xx00 - MMx)**2) / total_sum
  418. spread_x_px = np.sqrt(MMxx)
  419. # Calculate Y-spread (intensity-weighted average of local Y-variances)
  420. Total_var_y = np.nansum(MMyy * MMsum) / total_sum
  421. spread_y_px = np.sqrt(Total_var_y)
  422. # Calculate Z-spread (intensity-weighted average of local Z-variances)
  423. Total_var_z = np.nansum(MMzz * MMsum) / total_sum
  424. spread_z_px = np.sqrt(Total_var_z)
  425. logger.debug(f"Global spreads (pixels) - X: {spread_x_px:.2f}, "
  426. f"Y: {spread_y_px:.2f}, Z: {spread_z_px:.2f}")
  427. # Convert spreads to physical units (µm)
  428. spread_x_um = spread_x_px * self.voxel_size_x
  429. spread_y_um = spread_y_px * self.voxel_size_y
  430. spread_z_um = spread_z_px * self.voxel_size_z
  431. # Calculate combined spreads
  432. spread_xy_px = spread_x_px * spread_y_px
  433. spread_xyz_px = spread_x_px * spread_y_px * spread_z_px
  434. spread_xy_um = spread_x_um * spread_y_um
  435. spread_xyz_um = spread_x_um * spread_y_um * spread_z_um
  436. # Axonal volume = total integrated intensity
  437. axonal_volume = total_sum
  438. logger.info(f"Global spreads (µm) - X: {spread_x_um:.2f}, "
  439. f"Y: {spread_y_um:.2f}, Z: {spread_z_um:.2f}")
  440. logger.info(f"3D Spread: {spread_xyz_um:.2f} µm³")
  441. logger.info(f"Axonal volume: {axonal_volume:.2f}")
  442. # Compile results
  443. results = {
  444. 'spread_x_pixel': round(spread_x_px, 2),
  445. 'spread_y_pixel': round(spread_y_px, 2),
  446. 'spread_z_pixel': round(spread_z_px, 2),
  447. 'spread_xy_pixel': round(spread_xy_px, 2),
  448. 'spread_xyz_pixel': round(spread_xyz_px, 2),
  449. 'spread_x_um': round(spread_x_um, 2),
  450. 'spread_y_um': round(spread_y_um, 2),
  451. 'spread_z_um': round(spread_z_um, 2),
  452. 'spread_xy_um': round(spread_xy_um, 2),
  453. 'spread_xyz_um': round(spread_xyz_um, 2),
  454. 'axonal_volume': round(axonal_volume, 2),
  455. # Keep distributions for downstream analysis
  456. 'MMsum': MMsum,
  457. 'MMyy': MMyy,
  458. 'MMzz': MMzz
  459. }
  460. return results
  461. def process_image(
  462. self,
  463. image_3d: np.ndarray,
  464. mask_area_pixels: float
  465. ) -> Dict[str, float]:
  466. """
  467. Execute the complete image processing pipeline.
  468. This is the main entry point for processing 3D microscopy images.
  469. It orchestrates all steps: fluorescence calculation, PCA alignment,
  470. rotation, and spread quantification.
  471. Pipeline Steps:
  472. 1. Calculate fluorescence metrics
  473. 2. Determine optimal rotation angle via PCA
  474. 3. Rotate image to align with principal axis
  475. 4. Apply 90° standardization rotation
  476. 5. Calculate local and global spreads
  477. Parameters
  478. ----------
  479. image_3d : np.ndarray
  480. 3D image stack. Should be in (Y, X, Z) format = (rows, cols, slices).
  481. If image is in (Z, Y, X) format, it will be automatically transposed.
  482. mask_area_pixels : float
  483. Region of interest area in pixels (2D projection area)
  484. Returns
  485. -------
  486. Dict[str, float]
  487. Complete set of metrics including:
  488. - All spread measurements (pixels and µm)
  489. - Fluorescence metrics
  490. - Rotation angles applied
  491. - Axonal volume
  492. - Distribution arrays (MMsum, MMyy, MMzz)
  493. Raises
  494. ------
  495. ValueError
  496. If image contains negative values (indicates preprocessing error)
  497. Notes
  498. -----
  499. - Image format is automatically detected and corrected if needed
  500. - The pipeline preserves total intensity (small losses due to interpolation)
  501. - All intermediate steps are logged for debugging
  502. Examples
  503. --------
  504. >>> processor = ImageProcessor(0.124, 0.124, 1.0)
  505. >>> results = processor.process_image(image_3d, mask_area=5000)
  506. >>> print(f"3D spread: {results['spread_xyz_um']:.2f} µm³")
  507. """
  508. logger.info("="*60)
  509. logger.info("Starting structural plasticity analysis pipeline")
  510. logger.info(f"Input shape: {image_3d.shape}")
  511. # Ensure correct image format: (Y, X, Z) = (rows, cols, slices)
  512. if image_3d.shape[0] < image_3d.shape[2]:
  513. # Likely (Z, Y, X) format - transpose to (Y, X, Z)
  514. image_yxz = image_3d.transpose(1, 2, 0)
  515. logger.info(f"Transposed from (Z,Y,X) to (Y,X,Z): {image_yxz.shape}")
  516. else:
  517. image_yxz = image_3d
  518. logger.info(f"Using image as-is (assumed Y,X,Z format): {image_yxz.shape}")
  519. # Validate image (no negative values should exist)
  520. if np.any(image_yxz < 0):
  521. raise ValueError(f"Image contains {np.sum(image_yxz < 0)} negative values!")
  522. # Step 1: Calculate fluorescence metrics
  523. logger.info("Step 1/4: Calculating fluorescence...")
  524. fluor_px, fluor_um = self.calculate_fluorescence(image_yxz, mask_area_pixels)
  525. # Step 2: Calculate optimal rotation angle using PCA
  526. logger.info("Step 2/4: Calculating PCA rotation angle...")
  527. angle = self.calculate_pca_rotation_angle(image_yxz)
  528. # Step 3: Rotate image to align with principal axis
  529. logger.info("Step 3/4: Rotating image...")
  530. image_rotated = self.rotate_image(image_yxz, angle)
  531. # Validate rotated image
  532. if np.any(image_rotated < 0):
  533. raise ValueError(f"Rotated image contains {np.sum(image_rotated < 0)} negative values!")
  534. # Log intensity conservation
  535. intensity_loss = (1 - np.sum(image_rotated) / np.sum(image_yxz)) * 100
  536. logger.debug(f"Intensity loss after rotation: {intensity_loss:.2f}%")
  537. # Step 3.5: Apply 90° standardization rotation
  538. # This ensures X is horizontal (elongated) and Y is vertical (narrow)
  539. logger.info("Applying 90° standardization rotation (X=horizontal, Y=vertical)...")
  540. image_rotated_90 = np.rot90(image_rotated, k=1, axes=(0, 1))
  541. logger.debug(f"Shape after 90° rotation: {image_rotated_90.shape}")
  542. logger.debug(f"Final coordinate convention: X=horizontal (elongated), Y=vertical (narrow)")
  543. # Step 4: Calculate spread metrics
  544. logger.info("Step 4/4: Calculating spread metrics...")
  545. MMsum, MMyy, MMzz, xx00 = self.calculate_local_spreads(
  546. image_rotated_90,
  547. after_90deg_rotation=True
  548. )
  549. spread_results = self.calculate_global_spreads(MMsum, MMyy, MMzz, xx00)
  550. logger.info(f"Final spreads - X: {spread_results['spread_x_um']:.2f}µm (horizontal), "
  551. f"Y: {spread_results['spread_y_um']:.2f}µm (vertical), "
  552. f"Z: {spread_results['spread_z_um']:.2f}µm")
  553. # Add fluorescence and metadata to results
  554. spread_results['fluorescence_px'] = fluor_px
  555. spread_results['fluorescence_um'] = fluor_um
  556. spread_results['rotation_angle'] = round(angle, 2)
  557. spread_results['additional_rotation'] = 90 # Standardization rotation
  558. logger.info("Processing completed successfully")
  559. logger.info("="*60)
  560. return spread_results
  561. def _get_empty_results(self) -> Dict[str, float]:
  562. """
  563. Return empty results dictionary for error cases.
  564. Returns
  565. -------
  566. Dict[str, float]
  567. Dictionary with all metrics set to 0.0 or empty arrays
  568. """
  569. return {
  570. 'spread_x_pixel': 0.0,
  571. 'spread_y_pixel': 0.0,
  572. 'spread_z_pixel': 0.0,
  573. 'spread_xy_pixel': 0.0,
  574. 'spread_xyz_pixel': 0.0,
  575. 'spread_x_um': 0.0,
  576. 'spread_y_um': 0.0,
  577. 'spread_z_um': 0.0,
  578. 'spread_xy_um': 0.0,
  579. 'spread_xyz_um': 0.0,
  580. 'axonal_volume': 0.0,
  581. 'fluorescence_px': 0.0,
  582. 'fluorescence_um': 0.0,
  583. 'rotation_angle': 0.0,
  584. 'MMsum': np.array([]),
  585. 'MMyy': np.array([]),
  586. 'MMzz': np.array([])
  587. }
  588. def validate_parameters(
  589. voxel_size_x: float,
  590. voxel_size_y: float,
  591. voxel_size_z: float
  592. ) -> Tuple[bool, str]:
  593. """
  594. Validate voxel size parameters for physical plausibility.
  595. Parameters
  596. ----------
  597. voxel_size_x, voxel_size_y, voxel_size_z : float
  598. Voxel dimensions in micrometers (µm)
  599. Returns
  600. -------
  601. Tuple[bool, str]
  602. - is_valid: True if parameters pass all checks
  603. - error_message: Description of validation failure (empty if valid)
  604. Notes
  605. -----
  606. Validation checks:
  607. - All values must be positive
  608. - XY dimensions should be < 10µm (typical for high-resolution confocal)
  609. - Z dimension should be < 50µm (typical z-step size)
  610. Examples
  611. --------
  612. >>> is_valid, msg = validate_parameters(0.124, 0.124, 1.0)
  613. >>> print(is_valid) # True
  614. >>> is_valid, msg = validate_parameters(-1, 0.124, 1.0)
  615. >>> print(msg) # "All voxel sizes must be positive"
  616. """
  617. if voxel_size_x <= 0 or voxel_size_y <= 0 or voxel_size_z <= 0:
  618. return False, "All voxel sizes must be positive"
  619. if voxel_size_x > 10 or voxel_size_y > 10:
  620. return False, "XY voxel sizes > 10µm seem unusual. Please verify your calibration."
  621. if voxel_size_z > 50:
  622. return False, "Z voxel size > 50µm seems unusual. Please verify your z-step."
  623. return True, ""

image_processor.py at commit 6cb847b, under MIT · at the source

Overview

Authors: Micaela Rodriguez-Caron1, Francisco Joaquin Tassara1,2, Juan Ignacio Ispizua1, Christian Mauricio Carpio-Romero1,2, María Fernanda Ceriani1
  1. Laboratorio de Genética del Comportamiento, IIBBA, CONICET-Fundación Instituto Leloir, Buenos Aires, Argentina
  2. Facultad de Ciencias Exactas y Naturales, Universidad de Buenos Aires, Buenos Aires, Argentina
Journal: Open biology, volume 16, issue 9, article 260120
Dates: received 21 March 2026; accepted 20 July 2026; published online 9 September 2026; in print September 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1098/rsob.260120 · PMID 42710896 · PMCID PMC13561074 · OpenAlex W7211940109
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: drosophila (organism), cellular / molecular (subfield)
Methods: Spectral & time-frequency, Statistics, Smoothing, state filtering, decompositions, Evoked potentials, Connectivity, fMRI & imaging
Keywords: circadian rhythms, circadian terminal remodelling, synaptic plasticity, small lateral ventral neurons, ex vivo preparation, membrane overload
MeSH: Drosophila*, Drosophila melanogaster*, Neuronal Plasticity*, Neurons*, Animals, Circadian Rhythm, Pseudopodia (* major topic)
Topic: Neurobiology and Insect Physiology Research (Cellular and Molecular Neuroscience, Neuroscience), according to OpenAlex
Funding: NINDS NIH HHS (R01NS108934, R01 NS108934); Agencia I+D+i (PICT2018-0995); NIH (R01NS108934); Argentine Research Council for Science and Technology (CONICET)
Citations: cited by 1 paper (Europe PMC); 43 references in the paper

Abstract

Unravelling how adult neurons reshape their architecture is key to understanding post-developmental plasticity. Drosophila clock neurons, which remodel their terminals on a daily basis, offer a unique model to examine the mechanisms underlying structural plasticity. In this study, we examine the impact of the experimental design on the remodelling process. We established a simple fixation protocol that preserves tissue integrity and prevents its deformation while enabling the fixation of a larger number of individuals within the appropriate time window. We show that intrinsic (i.e. targeting fluorescent reporters to the membrane) or extrinsic (i.e. temperature) variables may influence this dynamic process. Examining ex vivo preparations, we found that the small lateral ventral neuron (s-LNv) terminals display numerous thin filopodia extending from their synaptic boutons. However, these fine membrane protrusions are lost upon fixation, as they could only be accurately visualized ex vivo. Finally, we present MorphoScope, a Python-based interface that eliminates observer bias in complexity measurements. Altogether, we present a powerful and robust model to investigate the principles of adult neuronal plasticity, with implications extending beyond circadian biology.

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

Repositories

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

FranTassara/MorphoScope

License: MIT
State: the link answers, verified on 26 September 2026
Evidence: files inventoried
Commit: 6cb847ba76bdaeab3d8ba2b247e8ce269cde485c, 16 July 2026
Languages: Python (4)
Size: 15 files, 4 scripts
Software Heritage: not archived
Found in: the references
Holds: README, license file, CITATION.cff, environment (requirements.txt)
Not found: tests, continuous integration, documentation
Tools: NumPy (3 files), Matplotlib (2 files), SciPy (2 files), tifffile (2 files), scikit-image (1 file)
Availability: 1 check, the latest on 26 September 2026: the link answers
  • 26 September 2026: the link answers
6 files

Zenodo 17901990

License: CC-BY-4.0
State: the link answers, verified on 26 September 2026
Evidence: files inventoried
Size: 1 file
Software Heritage: not checked
Found in: the references
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: Matplotlib (2 files), NumPy (2 files), SciPy (2 files), scikit-image (1 file), tifffile (1 file)
Availability: 1 check, the latest on 26 September 2026: the link answers (HTTP 200)
  • 26 September 2026: the link answers (HTTP 200)
5 files
At the source:

doi:10.6084/m9.figshare.c.8627737

License: none: the authors keep all their rights
State: unreachable at the last attempt, verified on 28 September 2026
Evidence: found in the paper
Software Heritage: not checked
Found in: the references
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Availability: 4 checks, the latest on 28 September 2026: unreachable at the last attempt (HTTP 202)
  • 28 September 2026: unreachable at the last attempt (HTTP 202)
  • 28 September 2026: unreachable at the last attempt (HTTP 202)
  • 27 September 2026: unreachable at the last attempt (HTTP 202)
  • 26 September 2026: unreachable at the last attempt (HTTP 202)

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:

  • 3 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 7 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

No dataset and no data link were found in the paper.

Data accessibility

Data and relevant code for this research work are stored in GitHub [40] and have been archived within the Zenodo repository [42].

Supplementary material is available online [43].

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

Recorded: type, language, journal, volume, issue, pages, dates, 5 authors, 6 keywords, 7 MeSH terms, 4 funders, 40 references.

Cite

This paper

Rodriguez-Caron, M., Tassara, F. J., Ispizua, J. I., Carpio-Romero, C. M., & Ceriani, M. F. (2026). Assessment of adult structural plasticity in Drosophila neurons. Open biology, 16(9), 260120. https://doi.org/10.1098/rsob.260120

BibTeX

@article{rodriguezcaron2026assessment,
author = {Rodriguez-Caron, Micaela and Tassara, Francisco Joaquin and Ispizua, Juan Ignacio and Carpio-Romero, Christian Mauricio and Ceriani, María Fernanda},
title = {{Assessment of adult structural plasticity in Drosophila neurons}},
journal = {Open biology},
year = {2026},
month = sep,
volume = {16},
number = {9},
pages = {260120},
publisher = {The Royal Society},
issn = {2046-2441},
doi = {10.1098/rsob.260120},
url = {https://doi.org/10.1098/rsob.260120},
pmid = {42710896},
pmcid = {PMC13561074}
}

RIS

TY - JOUR
AU - Rodriguez-Caron, Micaela
AU - Tassara, Francisco Joaquin
AU - Ispizua, Juan Ignacio
AU - Carpio-Romero, Christian Mauricio
AU - Ceriani, María Fernanda
TI - Assessment of adult structural plasticity in Drosophila neurons
T2 - Open biology
J2 - Open Biol
PY - 2026
DA - 2026/09/01
VL - 16
IS - 9
SP - 260120
SN - 2046-2441
PB - The Royal Society
DO - 10.1098/rsob.260120
UR - https://doi.org/10.1098/rsob.260120
LA - en
ER -

CSL-JSON

{
"id": "10.1098/rsob.260120",
"type": "article-journal",
"title": "Assessment of adult structural plasticity in Drosophila neurons",
"container-title": "Open biology",
"author": [
{
"family": "Rodriguez-Caron",
"given": "Micaela"
},
{
"family": "Tassara",
"given": "Francisco Joaquin"
},
{
"family": "Ispizua",
"given": "Juan Ignacio"
},
{
"family": "Carpio-Romero",
"given": "Christian Mauricio"
},
{
"family": "Ceriani",
"given": "María Fernanda"
}
],
"container-title-short": "Open Biol",
"volume": "16",
"issue": "9",
"page": "260120",
"DOI": "10.1098/rsob.260120",
"PMID": "42710896",
"PMCID": "PMC13561074",
"ISSN": "2046-2441",
"publisher": "The Royal Society",
"URL": "https://doi.org/10.1098/rsob.260120",
"language": "en",
"issued": {
"date-parts": [
[
2026,
9,
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.1177/07487304261434715 [code]
&lt;i&gt;Drosophila&lt;/i&gt; RSK: A Pivotal Regulator of Circadian Plasticity at the Neuronal and Behavioral Level.
Journal: Journal of biological rhythms
In common: tifffile, scikit-image, SciPy, 2 other tools, drosophila, cellular / molecular, 9 references
[2] doi:10.7554/elife.103359
Circadian control of a sex-specific behavior in &lt;i&gt;Drosophila&lt;/i&gt;.
Journal: eLife
In common: drosophila, 1 reference, author María Fernanda Ceriani
[3] doi:10.7554/elife.107276 [code]
Deployment of endocytic machinery to periactive zones of nerve terminals is independent of active zone assembly and evoked release.
Journal: eLife
In common: tifffile, scikit-image, SciPy, 2 other tools, drosophila, cellular / molecular
[4] doi:10.7554/elife.108021 [code]
BetaII-spectrin gaps and patches emerge from the patterned assembly of the actin/spectrin membrane skeleton in human motor neuron axons.
Journal: eLife
In common: tifffile, scikit-image, SciPy, 2 other tools, cellular / molecular, 1 reference
[5] doi:10.1016/j.isci.2026.117010 [code]
Deep learning-assisted mapping of dendritic spines using sequential 2D two-photon calcium imaging.
Journal: iScience
In common: tifffile, scikit-image, SciPy, 2 other tools, cellular / molecular, 1 reference
[6] doi:10.1016/j.isci.2026.116213 [code]
Opioid receptor distribution in the claustrum-dorsal endopiriform complex.
Journal: iScience
In common: tifffile, scikit-image, SciPy, 2 other tools, cellular / molecular, 1 reference
[7] doi:10.1371/journal.pbio.3003901 [code]
Peroxisomal import is circadian in glia and regulates sleep and lipid metabolism.
Journal: PLoS biology
In common: drosophila, cellular / molecular, 3 references
[8] doi:10.1038/s41467-026-72845-3 [code]
The membrane-to-cortex distance regulates mDia1 activity to control cortical mechanics.
Journal: Nature communications
In common: scikit-image, SciPy, Matplotlib, 1 other tool, cellular / molecular, 2 references
[9] doi:10.1038/s41467-026-72710-3 [code]
A modular multi-color fluorescence microscope for simultaneous tracking of cellular activity and behavior.
Journal: Nature communications
In common: tifffile, scikit-image, SciPy, 2 other tools, drosophila
[10] doi:10.1126/sciadv.aef9926 [code]
aDISCO: A clearing method to enable 3D microscopy of large archival paraffin-embedded human tissue blocks.
Journal: Science advances
In common: tifffile, scikit-image, SciPy, 2 other tools, 1 reference

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.