OSCR

Regional astrocyte dysregulation and altered glymphatic-related markers in Alzheimer's disease frontal cortex.

Code ↔ Paper

2 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 2 matches
  1. [1] § METHODS › Image analysis ↔ src/functions.py, lines 451–529 · score 0.67 · DAB positive pixels, combining global, sensitivity, adaptive, RGB, threshold
  2. [2] § METHODS › Image analysis ↔ src/functions.py, lines 451–529 · score 0.66 · tissue pixels, positive pixels, binary, weighted, batch, metrics

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 · 785 lines · 31 KB · no license · 2 matches

  1. #Import required libraries
  2. import cv2
  3. import pandas as pd
  4. import numpy as np
  5. from scipy import stats, ndimage
  6. import matplotlib.pyplot as plt
  7. from skimage import filters, morphology, measure, color
  8. from skimage.color import separate_stains
  9. import seaborn as sns
  10. import gc
  11. from pathlib import Path
  12. class QuPathStainVectors:
  13. #Import and use the stain vectors estimated by QuPath
  14. def __init__(self):
  15. self.hematoxylin_vector = None
  16. self.dab_vector = None
  17. self.stain_matrix = None
  18. self.all_vectors = [] # Store vectors from multiple images
  19. def add_qupath_vectors(self, h_vector, dab_vector, image_name=""):
  20. """
  21. Add stain vectors from QuPath for one image (QuPath vectors are already normalized OD vectors)
  22. Parameters:
  23. h_vector: list or array [R, G, B] for hematoxylin
  24. dab_vector: list or array [R, G, B] for DAB
  25. image_name: optional name to track which image these came from
  26. """
  27. h_vec = np.array(h_vector, dtype=np.float64)
  28. dab_vec = np.array(dab_vector, dtype=np.float64)
  29. self.all_vectors.append({
  30. 'image_name': image_name,
  31. 'hematoxylin': h_vec,
  32. 'dab': dab_vec
  33. })
  34. print(f" Added vectors from: {image_name if image_name else 'Image'}")
  35. print(f" H: [{h_vec[0]:.5f}, {h_vec[1]:.5f}, {h_vec[2]:.5f}]")
  36. print(f" DAB: [{dab_vec[0]:.5f}, {dab_vec[1]:.5f}, {dab_vec[2]:.5f}]")
  37. def calculate_average_vectors(self):
  38. """
  39. Calculate average stain vectors from all added images (representing the entire lab's staining)
  40. """
  41. if len(self.all_vectors) == 0:
  42. raise ValueError("No vectors added yet!")
  43. # Extract all H and DAB vectors
  44. h_vectors = np.array([v['hematoxylin'] for v in self.all_vectors])
  45. dab_vectors = np.array([v['dab'] for v in self.all_vectors])
  46. # Calculate mean
  47. avg_h = np.mean(h_vectors, axis=0)
  48. avg_dab = np.mean(dab_vectors, axis=0)
  49. # Calculate standard deviation (variability across images)
  50. std_h = np.std(h_vectors, axis=0)
  51. std_dab = np.std(dab_vectors, axis=0)
  52. # Store
  53. self.hematoxylin_vector = avg_h
  54. self.dab_vector = avg_dab
  55. self.stain_matrix = np.array([avg_h, avg_dab])
  56. print("\n" + "="*70)
  57. print("AVERAGED STAIN VECTORS FROM QUPATH")
  58. print("="*70)
  59. print(f"Based on {len(self.all_vectors)} images\n")
  60. print(f"Hematoxylin: [{avg_h[0]:.5f}, {avg_h[1]:.5f}, {avg_h[2]:.5f}]")
  61. print(f"DAB: [{avg_dab[0]:.5f}, {avg_dab[1]:.5f}, {avg_dab[2]:.5f}]")
  62. print(f"\nVariability (std dev):")
  63. print(f"Hematoxylin: [{std_h[0]:.5f}, {std_h[1]:.5f}, {std_h[2]:.5f}]")
  64. print(f"DAB: [{std_dab[0]:.5f}, {std_dab[1]:.5f}, {std_dab[2]:.5f}]")
  65. print("="*70)
  66. # Check if variability is acceptable
  67. max_std = max(std_h.max(), std_dab.max())
  68. if max_std < 0.05:
  69. print("Low variability - excellent consistency across images")
  70. elif max_std < 0.10:
  71. print("Moderate variability - good consistency")
  72. else:
  73. print("High variability")
  74. return self.stain_matrix
  75. def visualize_vector_consistency(self):
  76. """
  77. Visualize how consistent the vectors are across images
  78. """
  79. if len(self.all_vectors) < 2:
  80. print("Need at least 2 images to visualize consistency")
  81. return
  82. h_vectors = np.array([v['hematoxylin'] for v in self.all_vectors])
  83. dab_vectors = np.array([v['dab'] for v in self.all_vectors])
  84. names = [v['image_name'] for v in self.all_vectors]
  85. fig, axes = plt.subplots(2, 3, figsize=(15, 10))
  86. # Plot each RGB component
  87. components = ['Red', 'Green', 'Blue']
  88. colors = ['red', 'green', 'blue']
  89. for i, (comp, color) in enumerate(zip(components, colors)):
  90. # Hematoxylin
  91. ax = axes[0, i]
  92. h_vals = h_vectors[:, i]
  93. ax.scatter(range(len(h_vals)), h_vals, c=color, s=100, alpha=0.6)
  94. ax.axhline(h_vals.mean(), color='black', linestyle='--',
  95. label=f'Mean: {h_vals.mean():.4f}')
  96. ax.set_ylabel('Vector Component Value')
  97. ax.set_title(f'Hematoxylin - {comp} Component')
  98. ax.set_xticks(range(len(names)))
  99. ax.set_xticklabels(names, rotation=45, ha='right')
  100. ax.legend()
  101. ax.grid(True, alpha=0.3)
  102. # DAB
  103. ax = axes[1, i]
  104. dab_vals = dab_vectors[:, i]
  105. ax.scatter(range(len(dab_vals)), dab_vals, c=color, s=100, alpha=0.6)
  106. ax.axhline(dab_vals.mean(), color='black', linestyle='--',
  107. label=f'Mean: {dab_vals.mean():.4f}')
  108. ax.set_ylabel('Vector Component Value')
  109. ax.set_title(f'DAB - {comp} Component')
  110. ax.set_xticks(range(len(names)))
  111. ax.set_xticklabels(names, rotation=45, ha='right')
  112. ax.legend()
  113. ax.grid(True, alpha=0.3)
  114. plt.tight_layout()
  115. plt.suptitle('Stain Vector Consistency Across Images',
  116. fontsize=14, fontweight='bold', y=1.02)
  117. plt.show()
  118. def deconvolve_image(self, image):
  119. # 1.Convert to Optical Density (OD)
  120. img_float = image.astype(np.float64) / 255.0
  121. img_od = -np.log(img_float + 1e-6)
  122. # 2.Construct the 3x3 Matrix properly
  123. # We need a 3rd 'Residual' vector that is perpendicular to H and DAB
  124. h_v = self.hematoxylin_vector
  125. d_v = self.dab_vector
  126. res_v = np.cross(h_v, d_v)
  127. res_v = res_v / np.linalg.norm(res_v)
  128. stain_matrix = np.array([h_v, d_v, res_v])
  129. # 3.Matrix Inversion
  130. # This is what creates the separation instead of just a color swap.
  131. inverse_matrix = np.linalg.inv(stain_matrix)
  132. # 4.Multiply OD by the INVERSE matrix
  133. # This subtracts the 'contribution' of one stain from the other.
  134. deconvolved = img_od @ inverse_matrix
  135. # Channel 0 is H, Channel 1 is DAB
  136. h_channel = np.clip(deconvolved[:, :, 0], 0, None)
  137. dab_channel = np.clip(deconvolved[:, :, 1], 0, None)
  138. return h_channel, dab_channel
  139. def test_deconvolution(self, image_path):
  140. img = cv2.imread(image_path)
  141. img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
  142. # 1.Perform deconvolution
  143. h, dab = self.deconvolve_image(img_rgb)
  144. # 2.RECONSTRUCT RGB IMAGES
  145. # Use the Beer-Lambert law: Intensity = 255 * exp(-OD * Vector)
  146. # Add a newaxis so the 2D channel can be multiplied by the 1D [R, G, B] vector
  147. h_rgb_vis = 255 * np.exp(-h[:, :, np.newaxis] * self.hematoxylin_vector)
  148. dab_rgb_vis = 255 * np.exp(-dab[:, :, np.newaxis] * self.dab_vector)
  149. # 3. Finalize for display
  150. h_rgb_vis = np.clip(h_rgb_vis, 0, 255).astype(np.uint8)
  151. dab_rgb_vis = np.clip(dab_rgb_vis, 0, 255).astype(np.uint8)
  152. # Calculate correlation on raw OD data
  153. correlation = np.corrcoef(h.flatten(), dab.flatten())[0, 1]
  154. # Visualize
  155. fig = plt.figure(figsize=(16, 10))
  156. # Row 1: Original and RGB-reconstructed channels
  157. ax1 = plt.subplot(2, 3, 1)
  158. ax1.imshow(img_rgb)
  159. ax1.set_title('Original Image', fontsize=12, fontweight='bold')
  160. ax1.axis('off')
  161. ax2 = plt.subplot(2, 3, 2)
  162. ax2.imshow(h_rgb_vis)
  163. ax2.set_title('Hematoxylin Only\n(Nuclei Reconstructed)', fontsize=12, fontweight='bold')
  164. ax2.axis('off')
  165. ax3 = plt.subplot(2, 3, 3)
  166. ax3.imshow(dab_rgb_vis)
  167. ax3.set_title('DAB Only\n(Protein Reconstructed)', fontsize=12, fontweight='bold')
  168. ax3.axis('off')
  169. # Row 2
  170. ax4 = plt.subplot(2, 3, 4)
  171. ax4.hist(h.flatten(), bins=50, color='blue', alpha=0.7, edgecolor='black')
  172. ax4.set_xlabel('Hematoxylin Intensity (OD)')
  173. ax4.set_title('Hematoxylin Distribution')
  174. ax5 = plt.subplot(2, 3, 5)
  175. ax5.hist(dab.flatten(), bins=50, color='brown', alpha=0.7, edgecolor='black')
  176. ax5.set_xlabel('DAB Intensity (OD)')
  177. ax5.set_title('DAB Distribution')
  178. ax6 = plt.subplot(2, 3, 6)
  179. sample_indices = np.random.choice(h.size, min(10000, h.size), replace=False)
  180. ax6.scatter(h.flatten()[sample_indices], dab.flatten()[sample_indices],
  181. alpha=0.2, s=1, c='black')
  182. ax6.set_title(f'Correlation: {correlation:.3f}\n(L-shape = Excellent)')
  183. plt.tight_layout()
  184. plt.show()
  185. return h, dab
  186. def visualize_vector_consistency(self):
  187. """
  188. Visualize how consistent vectors are across images
  189. """
  190. if len(self.all_vectors) < 2:
  191. print("Need at least 2 images to visualize consistency")
  192. return
  193. h_vectors = np.array([v['hematoxylin'] for v in self.all_vectors])
  194. dab_vectors = np.array([v['dab'] for v in self.all_vectors])
  195. names = [v['image_name'] for v in self.all_vectors]
  196. fig, axes = plt.subplots(2, 3, figsize=(15, 10))
  197. components = ['Red', 'Green', 'Blue']
  198. colors = ['red', 'green', 'blue']
  199. for i, (comp, color) in enumerate(zip(components, colors)):
  200. # Hematoxylin
  201. ax = axes[0, i]
  202. h_vals = h_vectors[:, i]
  203. ax.scatter(range(len(h_vals)), h_vals, c=color, s=100, alpha=0.6)
  204. ax.axhline(h_vals.mean(), color='black', linestyle='--', linewidth=2,
  205. label=f'Mean: {h_vals.mean():.4f}')
  206. ax.set_ylabel('Vector Component', fontsize=10)
  207. ax.set_title(f'Hematoxylin - {comp}', fontsize=11, fontweight='bold')
  208. ax.set_xticks(range(len(names)))
  209. ax.set_xticklabels(names, rotation=45, ha='right', fontsize=8)
  210. ax.legend(fontsize=9)
  211. ax.grid(True, alpha=0.3)
  212. # DAB
  213. ax = axes[1, i]
  214. dab_vals = dab_vectors[:, i]
  215. ax.scatter(range(len(dab_vals)), dab_vals, c=color, s=100, alpha=0.6)
  216. ax.axhline(dab_vals.mean(), color='black', linestyle='--', linewidth=2,
  217. label=f'Mean: {dab_vals.mean():.4f}')
  218. ax.set_ylabel('Vector Component', fontsize=10)
  219. ax.set_title(f'DAB - {comp}', fontsize=11, fontweight='bold')
  220. ax.set_xticks(range(len(names)))
  221. ax.set_xticklabels(names, rotation=45, ha='right', fontsize=8)
  222. ax.legend(fontsize=9)
  223. ax.grid(True, alpha=0.3)
  224. plt.suptitle('Stain Vector Consistency Across Images',
  225. fontsize=14, fontweight='bold')
  226. plt.tight_layout()
  227. plt.show()
  228. def save_vectors(self, filename='qupath_stain_vectors.txt'):
  229. """
  230. Save averaged vectors to file
  231. """
  232. if self.stain_matrix is None:
  233. raise ValueError("Calculate average vectors first!")
  234. with open(filename, 'w') as f:
  235. f.write("# Stain Vectors from QuPath Analysis\n")
  236. f.write(f"# Averaged from {len(self.all_vectors)} images\n")
  237. f.write("# These are NORMALIZED OPTICAL DENSITY vectors\n")
  238. f.write("# Format: [R, G, B]\n\n")
  239. f.write(f"Hematoxylin: [{self.hematoxylin_vector[0]:.6f}, "
  240. f"{self.hematoxylin_vector[1]:.6f}, "
  241. f"{self.hematoxylin_vector[2]:.6f}]\n")
  242. f.write(f"DAB: [{self.dab_vector[0]:.6f}, "
  243. f"{self.dab_vector[1]:.6f}, "
  244. f"{self.dab_vector[2]:.6f}]\n")
  245. f.write("\n# Individual image vectors:\n")
  246. for v in self.all_vectors:
  247. f.write(f"\n{v['image_name']}:\n")
  248. f.write(f" H: [{v['hematoxylin'][0]:.6f}, "
  249. f"{v['hematoxylin'][1]:.6f}, "
  250. f"{v['hematoxylin'][2]:.6f}]\n")
  251. f.write(f" DAB: [{v['dab'][0]:.6f}, "
  252. f"{v['dab'][1]:.6f}, "
  253. f"{v['dab'][2]:.6f}]\n")
  254. print(f"\n✓ Vectors saved to: {filename}")
  255. def load_vectors(self, filename):
  256. """
  257. Load previously saved vectors
  258. """
  259. vectors = {}
  260. with open(filename, 'r') as f:
  261. for line in f:
  262. if line.startswith('Hematoxylin:'):
  263. vec_str = line.split('[')[1].split(']')[0]
  264. vectors['h'] = np.array([float(x) for x in vec_str.split(',')])
  265. elif line.startswith('DAB:'):
  266. vec_str = line.split('[')[1].split(']')[0]
  267. vectors['dab'] = np.array([float(x) for x in vec_str.split(',')])
  268. self.hematoxylin_vector = vectors['h']
  269. self.dab_vector = vectors['dab']
  270. self.stain_matrix = np.array([vectors['h'], vectors['dab']])
  271. print("Stain vectors loaded successfully!")
  272. print(f" H: [{self.hematoxylin_vector[0]:.5f}, {self.hematoxylin_vector[1]:.5f}, {self.hematoxylin_vector[2]:.5f}]")
  273. print(f" DAB: [{self.dab_vector[0]:.5f}, {self.dab_vector[1]:.5f}, {self.dab_vector[2]:.5f}]")
  274. return self.stain_matrix
  275. class DABQuantifier:
  276. """
  277. Optimized DAB quantification for batch processing.
  278. """
  279. def __init__(self, reference_background=None, reference_std=None):
  280. self.reference_background = reference_background
  281. self.reference_std = reference_std
  282. self.dab_vector = np.array([0.368, 0.597, 0.706])
  283. def calibrate_from_background(self, background_images, visualize=True):
  284. """Calibrate background"""
  285. all_od_values = []
  286. image_stats = []
  287. print("="*60)
  288. print("CALIBRATING BACKGROUND FROM WHITE MATTER")
  289. print("="*60)
  290. for img_path in background_images:
  291. img = cv2.imread(str(img_path))
  292. if img is None:
  293. continue
  294. img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
  295. img_od = -np.log((img_rgb.astype(np.float64) / 255.0) + 1e-6) #OD conversion
  296. dab_od = np.dot(img_od, self.dab_vector)
  297. all_od_values.extend(dab_od.flatten()[::10]) # Subsample (every 10th pixel for memory)
  298. image_stats.append({
  299. 'filename': Path(img_path).name,
  300. 'median': np.median(dab_od),
  301. 'mean': np.mean(dab_od)
  302. })
  303. print(f"{Path(img_path).name}: Median={np.median(dab_od):.4f}")
  304. if len(all_od_values) == 0:
  305. print("ERROR: No calibration images found.")
  306. return None, None
  307. all_od_values = np.array(all_od_values)
  308. # Robust statistics
  309. self.reference_background = np.median(all_od_values) #robust percentile
  310. q75 = np.percentile(all_od_values, 75)
  311. q25 = np.percentile(all_od_values, 25)
  312. self.reference_std = (q75 - q25) / 1.349
  313. print(f"\nBackground: {self.reference_background:.4f}")
  314. print(f"Robust SD: {self.reference_std:.4f}")
  315. if visualize:
  316. self._plot_calibration(all_od_values, image_stats)
  317. return self.reference_background, self.reference_std
  318. def save_calibration(self, filepath='background_calibration.npz'):
  319. """Save calibration."""
  320. np.savez(filepath,
  321. reference_background=self.reference_background,
  322. reference_std=self.reference_std)
  323. print(f"✓ Calibration saved: {filepath}")
  324. def load_calibration(self, filepath='background_calibration.npz'):
  325. """Load calibration."""
  326. data = np.load(filepath)
  327. self.reference_background = float(data['reference_background'])
  328. self.reference_std = float(data['reference_std'])
  329. print(f" Calibration loaded: {filepath}")
  330. print(f" Background: {self.reference_background:.4f}")
  331. print(f" SD: {self.reference_std:.4f}")
  332. def _plot_calibration(self, all_od_values, image_stats):
  333. """Simplified calibration visualization."""
  334. fig, axes = plt.subplots(1, 3, figsize=(15, 4))
  335. # Histogram
  336. axes[0].hist(all_od_values, bins=200, alpha=0.7, color='brown', density=True)
  337. axes[0].axvline(self.reference_background, color='red', linestyle='--',
  338. linewidth=2, label=f'Median: {self.reference_background:.4f}')
  339. axes[0].set_xlabel('DAB OD')
  340. axes[0].set_ylabel('Density')
  341. axes[0].set_title('Background Distribution')
  342. axes[0].legend()
  343. axes[0].grid(alpha=0.3)
  344. # Cumulative
  345. sorted_od = np.sort(all_od_values)
  346. cumulative = np.arange(1, len(sorted_od) + 1) / len(sorted_od) * 100
  347. axes[1].plot(sorted_od, cumulative, color='brown', linewidth=2)
  348. axes[1].axvline(self.reference_background, color='red', linestyle='--', linewidth=2)
  349. axes[1].axhline(50, color='gray', linestyle=':', alpha=0.5)
  350. axes[1].set_xlabel('DAB OD')
  351. axes[1].set_ylabel('Cumulative %')
  352. axes[1].set_title('Cumulative Distribution')
  353. axes[1].grid(alpha=0.3)
  354. # Per-image
  355. df_stats = pd.DataFrame(image_stats)
  356. axes[2].scatter(range(len(df_stats)), df_stats['median'],
  357. color='green', s=80, alpha=0.7, edgecolor='black')
  358. axes[2].axhline(self.reference_background, color='red', linestyle='--', linewidth=2)
  359. axes[2].set_xlabel('Image Index')
  360. axes[2].set_ylabel('Median DAB OD')
  361. axes[2].set_title('Per-Image Background')
  362. axes[2].grid(alpha=0.3)
  363. plt.tight_layout()
  364. plt.show()
  365. def quantify_image(self, image_path, total_tissue_pixels, threshold_method='fixed_adaptive',
  366. threshold_param=3.5):
  367. """
  368. Quantify DAB in a single image.
  369. Args:
  370. image_path: Path to image
  371. threshold_method:
  372. 'fixed' - k × background_std (param = k, default 1.5)
  373. 'fixed_adaptive' - Combines global + local stats (param = global_weight, default 1.5)
  374. 'triangle' - Triangle algorithm (param ignored)
  375. 'percentile' - Based on percentile range (param = sensitivity, default 0.3)
  376. 'multiscale' - Multi-scale combination (param = conservativeness, default 1.5)
  377. threshold_param: Parameter for threshold method
  378. Returns:
  379. Dictionary with essential metrics only
  380. """
  381. if self.reference_background is None:
  382. raise ValueError("Must calibrate background first!")
  383. try:
  384. img = cv2.imread(str(image_path))
  385. if img is None:
  386. return None
  387. #OD Conversion and DAB extraction
  388. img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
  389. img_od = -np.log((img_rgb.astype(np.float32) / 255.0) + 1e-6)
  390. dab_od = np.dot(img_od, self.dab_vector)
  391. #Background subtraction
  392. dab_signal = dab_od - self.reference_background
  393. dab_signal = np.clip(dab_signal, 0, None)
  394. # Calculate threshold
  395. threshold = self._calculate_threshold(
  396. dab_signal, threshold_method, threshold_param
  397. )
  398. # Binary mask
  399. dab_positive = (dab_signal > threshold)
  400. # Calculate metrics
  401. positive_pixels = np.sum(dab_positive)
  402. # Essential metrics only
  403. results = {
  404. 'filename': Path(image_path).name,
  405. # RAW COUNTS
  406. 'positive_pixels': int(positive_pixels),
  407. 'total_tissue_pixels': int(total_tissue_pixels),
  408. # AREA METRICS (normalized by tissue)
  409. 'area_percent': round((positive_pixels / total_tissue_pixels * 100), 3) if total_tissue_pixels > 0 else 0,
  410. # INTENSITY METRICS (positive regions only)
  411. 'mean_intensity': round(np.mean(dab_signal[dab_positive]), 4) if positive_pixels > 0 else 0,
  412. # NORMALIZED INTENSITY
  413. 'dab_density': round(np.sum(dab_signal[dab_positive])/ total_tissue_pixels, 6) if total_tissue_pixels > 0 else 0,
  414. # THRESHOLD INFO
  415. 'threshold_value': round(threshold, 4),
  416. 'threshold_method': threshold_method,
  417. # QC
  418. 'tissue_coverage_percent': round((total_tissue_pixels / dab_signal.size * 100), 2),
  419. 'max_dab_signal': round(np.max(dab_signal), 4)
  420. }
  421. # Memory cleanup for batch stability
  422. del img, img_rgb, img_od, dab_od, dab_signal
  423. return results
  424. except Exception as e:
  425. print(f"Error processing {image_path}: {str(e)}")
  426. return None
  427. def _calculate_threshold(self, dab_signal, method, param):
  428. """Calculate threshold using specified method."""
  429. if method == 'fixed':
  430. # Simple: k × SD above background
  431. threshold = param * self.reference_std
  432. elif method == 'fixed_adaptive':
  433. # Combines global background stats with local tissue stats
  434. global_thresh = param * self.reference_std
  435. local_mean = np.mean(dab_signal)
  436. local_std = np.std(dab_signal)
  437. # Only use local if tissue has reasonable signal
  438. if local_mean > 2 * self.reference_std:
  439. local_thresh = local_mean + 0.5 * local_std
  440. # Weight toward global (more stable)
  441. threshold = 0.7 * global_thresh + 0.3 * local_thresh
  442. else:
  443. threshold = global_thresh
  444. elif method == 'triangle':
  445. # Triangle algorithm - good for skewed distributions
  446. dab_norm = (dab_signal / np.max(dab_signal) * 255).astype(np.uint8)
  447. try:
  448. threshold = filters.threshold_triangle(dab_norm)
  449. threshold = (threshold / 255.0) * np.max(dab_signal)
  450. except:
  451. threshold = param * self.reference_std
  452. elif method == 'percentile':
  453. # Percentile-based: find pixels in top percentage
  454. p98 = np.percentile(dab_signal, 98)
  455. p75 = np.percentile(dab_signal, 75)
  456. # param controls sensitivity: 0.3 = moderate, 0.5 = sensitive
  457. threshold = p75 + param * (p98 - p75)
  458. elif method == 'multiscale':
  459. # Multi-scale: combines multiple threshold estimates
  460. # Good for heterogeneous staining
  461. # Method 1: Fixed
  462. t1 = param * self.reference_std
  463. # Method 2: Mean + SD
  464. t2 = np.mean(dab_signal) + np.std(dab_signal)
  465. # Method 3: Percentile
  466. t3 = np.percentile(dab_signal, 75) + 0.3 * (
  467. np.percentile(dab_signal, 98) - np.percentile(dab_signal, 75)
  468. )
  469. # Weight toward more conservative (higher) threshold
  470. threshold = np.median([t1, t2, t3])
  471. else:
  472. # Default fallback
  473. threshold = 1.5 * self.reference_std
  474. return threshold
  475. def test_thresholds(self, test_images, save_fig=True):
  476. """
  477. Test different threshold methods on representative images.
  478. Args:
  479. test_images: List of paths [pale_image, intense_image]
  480. tissue_mask_threshold: Tissue detection threshold
  481. save_fig: Save comparison figure
  482. """
  483. if self.reference_background is None:
  484. raise ValueError("Must calibrate background first!")
  485. methods_to_test = [
  486. ('fixed', 1.0, 'Fixed: 1.0σ'),
  487. ('fixed', 1.5, 'Fixed: 1.5σ'),
  488. ('fixed', 2.0, 'Fixed: 2.0σ'),
  489. ('fixed_adaptive', 3.5, 'Adaptive Fixed'),
  490. ('triangle', None, 'Triangle'),
  491. ('percentile', 0.3, 'Percentile (0.3)'),
  492. ('multiscale', 1.5, 'Multi-scale')
  493. ]
  494. results_table = []
  495. print("\n" + "="*80)
  496. print("THRESHOLD METHOD COMPARISON")
  497. print("="*80)
  498. for img_path in test_images:
  499. print(f"\nImage: {Path(img_path).name}")
  500. print("-"*80)
  501. print(f"{'Method':<20} {'Threshold':<12} {'Area %':<10} {'Mean Int':<12} {'DAB Density'}")
  502. print("-"*80)
  503. for method, param, label in methods_to_test:
  504. result = self.quantify_image(
  505. img_path,
  506. threshold_method=method,
  507. threshold_param=param if param else 1.5,
  508. )
  509. if result:
  510. print(f"{label:<20} {result['threshold_value']:<12.4f} "
  511. f"{result['area_percent']:<10.2f} "
  512. f"{result['mean_positive_intensity']:<12.4f} "
  513. f"{result['dab_density']:.6f}")
  514. results_table.append({
  515. 'image': Path(img_path).name,
  516. 'method': label,
  517. **result
  518. })
  519. # Create comparison visualization
  520. if save_fig and len(test_images) == 2:
  521. self._plot_threshold_comparison(test_images, methods_to_test, tissue_mask_threshold)
  522. return pd.DataFrame(results_table)
  523. def _plot_threshold_comparison(self, test_images, methods, tissue_threshold):
  524. """Create side-by-side comparison of threshold methods."""
  525. fig, axes = plt.subplots(len(test_images), len(methods) + 1,
  526. figsize=(4*(len(methods)+1), 5*len(test_images)))
  527. if len(test_images) == 1:
  528. axes = axes.reshape(1, -1)
  529. for row, img_path in enumerate(test_images):
  530. # Load image
  531. img = cv2.imread(str(img_path))
  532. img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
  533. # Original
  534. axes[row, 0].imshow(img_rgb)
  535. axes[row, 0].set_title(f'Original\n{Path(img_path).name}', fontsize=10)
  536. axes[row, 0].axis('off')
  537. # Each method
  538. for col, (method, param, label) in enumerate(methods, 1):
  539. result = self.quantify_image(
  540. img_path,
  541. threshold_method=method,
  542. threshold_param=param if param else 1.5,
  543. )
  544. if result:
  545. # Create overlay
  546. overlay = img_rgb.copy()
  547. # Recreate mask for visualization
  548. gray = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2GRAY)
  549. img_od = -np.log((img_rgb.astype(np.float32) / 255.0) + 1e-6)
  550. dab_od = np.dot(img_od, self.dab_vector)
  551. dab_signal = np.clip(dab_od - self.reference_background, 0, None)
  552. threshold = result['threshold_value']
  553. dab_positive = (dab_signal > threshold)
  554. overlay[dab_positive] = [255, 0, 0]
  555. axes[row, col].imshow(overlay)
  556. title = (f'{label}\nThresh: {threshold:.3f}\n'
  557. f'Area: {result["area_percent"]:.1f}%\n'
  558. f'Mean: {result["mean_positive_intensity"]:.4f}')
  559. axes[row, col].set_title(title, fontsize=9)
  560. axes[row, col].axis('off')
  561. plt.tight_layout()
  562. plt.savefig('threshold_comparison.png', dpi=200, bbox_inches='tight')
  563. plt.show()
  564. print("\n✓ Comparison saved to: threshold_comparison.png")
  565. def batch_process(self, image_folder, output_csv='results.csv',
  566. output_masks_folder=None, threshold_method='fixed_adaptive',
  567. threshold_param=1.5):
  568. """
  569. Optimized batch processing - saves only essential metrics.
  570. Args:
  571. image_folder: Folder with images
  572. output_csv: Output CSV file
  573. output_masks_folder: Optional folder to save masks
  574. threshold_method: Threshold algorithm
  575. threshold_param: Parameter for threshold
  576. Returns:
  577. DataFrame with results
  578. """
  579. image_folder = Path(image_folder)
  580. image_files = list(image_folder.glob('*.png')) + \
  581. list(image_folder.glob('*.jpg')) + \
  582. list(image_folder.glob('*.tif*'))
  583. if output_masks_folder:
  584. output_masks_folder = Path(output_masks_folder)
  585. output_masks_folder.mkdir(parents=True, exist_ok=True)
  586. results_list = []
  587. print(f"\n{'='*70}")
  588. print(f"BATCH PROCESSING: {len(image_files)} images")
  589. print(f"Method: {threshold_method}, Param: {threshold_param}")
  590. print(f"{'='*70}\n")
  591. for i, img_path in enumerate(image_files, 1):
  592. print(f"[{i}/{len(image_files)}] {img_path.name}...", end=' ')
  593. result = self.quantify_image(
  594. img_path,
  595. threshold_method=threshold_method,
  596. threshold_param=threshold_param
  597. )
  598. if result:
  599. # Save mask if requested
  600. if output_masks_folder:
  601. self._save_mask(img_path, result, output_masks_folder,
  602. threshold_param)
  603. results_list.append(result)
  604. print(f"✓ (Area: {result['area_percent']:.2f}%, DAB: {result['mean_positive_intensity']:.4f})")
  605. else:
  606. print("✗ Failed")
  607. df = pd.DataFrame(results_list)
  608. df.to_csv(output_csv, index=False)
  609. print(f"\n{'='*70}")
  610. print(f"✓ Complete! Processed: {len(results_list)}/{len(image_files)}")
  611. print(f" Results: {output_csv}")
  612. if output_masks_folder:
  613. print(f" Masks: {output_masks_folder}")
  614. print(f"{'='*70}\n")
  615. return df
  616. def _save_mask(self, img_path, result, output_folder, dab_positive, tissue_mask):
  617. """Save visualization mask for QC."""
  618. # Create the high-contrast visualization:
  619. # White = Tissue Area, Red = DAB Positive pixels
  620. mask_vis = np.zeros((*dab_positive.shape, 3), dtype=np.uint8)
  621. # Fill tissue area with white
  622. mask_vis[tissue_mask] = [255, 255, 255]
  623. # Overlay positive DAB with Red
  624. mask_vis[dab_positive] = [255, 0, 0]
  625. mask_filename = output_folder / f"{img_path.stem}_mask.png"
  626. # Convert RGB to BGR for OpenCV saving
  627. cv2.imwrite(str(mask_filename), cv2.cvtColor(mask_vis, cv2.COLOR_RGB2BGR))

functions.py at commit e747106, no license · at the source

Overview

Authors: Harry Alexopoulos1, Xanthippi P Louka1, Edoardo Rosario De Natale2, Kelly Koutroubi1, Lisa Cashmore2, Maria T Panayotacopoulou3, Ioannis P Trougakos1, Heather Wilson2, Marios Politis2,4
  1. Department of Cell Biology and Biophysics, Faculty of Biology, National and Kapodistrian University of Athens, Athens, Greece
  2. Neurodegeneration Imaging Group, University of Exeter Medical School, London, UK
  3. Department of Psychiatry, Faculty of Medicine, National and Kapodistrian University of Athens, Athens, Greece
  4. Department of Neurology, Faculty of Medicine, National and Kapodistrian University of Athens, Athens, Greece
Journal: Alzheimer's & dementia : the journal of the Alzheimer's Association, volume 22, issue 8, article e71745
Dates: received 26 February 2026; accepted 17 July 2026; published online 14 August 2026; in print August 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1002/alz.71745 · PMID 42598755 · PMCID PMC13474155 · OpenAlex W7203458904
Open access: hybrid, a free copy (OpenAlex)
Status: code verified
Categories: histology / microscopy (modality), human (organism), Alzheimer's / dementia (population), cellular / molecular (subfield)
Methods: Statistics, fMRI & imaging
Keywords: Alzheimer's disease, amyloid‐beta, aquaporin‐4, astrocyte dysregulation, frontal cortex, glial fibrillary acidic protein, glymphatic system, tau proteins
MeSH: Alzheimer Disease*, Aquaporin 4*, Astrocytes*, Frontal Lobe*, Glial Fibrillary Acidic Protein*, Glymphatic System*, Aged, Aged, 80 and over, Amyloid beta-Peptides, Biomarkers, Female, Gray Matter, Humans, Male, tau Proteins, White Matter (* major topic)
Topic: Cerebrospinal fluid and hydrocephalus (Cellular and Molecular Neuroscience, Neuroscience), according to OpenAlex
Funding: City Electrical Factors Ltd
Citations: not cited yet (Europe PMC); 40 references in the paper

Abstract

INTRODUCTION: Astrocyte dysfunction is central to Alzheimer's disease (AD), yet expression patterns of astrocytic markers remain poorly defined. We measured Aquaporin‐4 (AQP4) and glial fibrillary acidic protein (GFAP) in post‐mortem frontal cortex of AD patients and controls across BrainNet Europe (BNE) stages.

METHODS: We assessed marker expression across gray and white matter with immunohistochemistry and immunofluorescence.

RESULTS: In AD, gray‐matter AQP4 area–fraction did not differ significantly overall by immunohistochemistry, while a stage‐dependent increase emerged by BNE VI in both gray and white matter. AQP4/amyloid‐β (Aβ) and AQP4/tau ratios were significantly reduced, consistent with reduced AQP4 retention relative to local proteinopathy burden. GFAP intensity was significantly decreased in both gray and white matter of AD patients, with disorganized peri‐plaque morphology in gray matter.

DISCUSSION: These findings reveal compartment‐ and stage‐specific astrocytic dysregulation in AD frontal cortex and identify local loss of AQP4 around proteinopathy. They support investigation of astrocyte/glymphatic‐related pathways as biomarkers and therapeutic targets.

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

xanthippilouka/Human_Brain_Image_Analysis

License: none: the authors keep all their rights
State: the link answers, verified on 26 September 2026
Evidence: files inventoried
Commit: e74710621c38a0c9bf289713f7dfddf0f84c9a40, 14 May 2026
Languages: Jupyter (7), Python (1)
Size: 17 files, 8 scripts
Software Heritage: not archived
Found in: “DATA AVAILABILITY STATEMENT”
Holds: environment (requirements.txt), 7 notebooks
Not found: README, license file, CITATION.cff, tests, continuous integration, documentation
Tools: NumPy (8 files), OpenCV (8 files), pandas (7 files), SciPy (7 files), Matplotlib (6 files), scikit-image (6 files), seaborn (3 files)
Availability: 1 check, the latest on 26 September 2026: the link answers
  • 26 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;
  • 8 scripts, each with its path and the digest of its content;
  • 2 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 availability statement

The custom Python scripts developed and utilized for digital image analysis have been uploaded to a public GitHub repository and are available at: https://github.com/xanthippilouka/Human_Brain_Image_Analysis.git. The raw datasets analyzed during the current study are available from the corresponding author on reasonable request.

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

Versions

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

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 9 authors, 8 keywords, 16 MeSH terms, 1 funder, 40 references.

Cite

This paper

Alexopoulos, H., Louka, X. P., De Natale, E. R., Koutroubi, K., Cashmore, L., Panayotacopoulou, M. T., Trougakos, I. P., Wilson, H., & Politis, M. (2026). Regional astrocyte dysregulation and altered glymphatic-related markers in Alzheimer's disease frontal cortex. Alzheimer's & dementia : the journal of the Alzheimer's Association, 22(8), e71745. https://doi.org/10.1002/alz.71745

BibTeX

@article{alexopoulos2026regional,
author = {Alexopoulos, Harry and Louka, Xanthippi P and De Natale, Edoardo Rosario and Koutroubi, Kelly and Cashmore, Lisa and Panayotacopoulou, Maria T and Trougakos, Ioannis P and Wilson, Heather and Politis, Marios},
title = {{Regional astrocyte dysregulation and altered glymphatic-related markers in Alzheimer's disease frontal cortex}},
journal = {Alzheimer's \& dementia : the journal of the Alzheimer's Association},
year = {2026},
month = aug,
volume = {22},
number = {8},
pages = {e71745},
publisher = {Wiley},
issn = {1552-5260},
doi = {10.1002/alz.71745},
url = {https://doi.org/10.1002/alz.71745},
pmid = {42598755},
pmcid = {PMC13474155}
}

RIS

TY - JOUR
AU - Alexopoulos, Harry
AU - Louka, Xanthippi P
AU - De Natale, Edoardo Rosario
AU - Koutroubi, Kelly
AU - Cashmore, Lisa
AU - Panayotacopoulou, Maria T
AU - Trougakos, Ioannis P
AU - Wilson, Heather
AU - Politis, Marios
TI - Regional astrocyte dysregulation and altered glymphatic-related markers in Alzheimer's disease frontal cortex
T2 - Alzheimer's & dementia : the journal of the Alzheimer's Association
J2 - Alzheimers Dement
PY - 2026
DA - 2026/08/01
VL - 22
IS - 8
SP - e71745
SN - 1552-5260
PB - Wiley
DO - 10.1002/alz.71745
UR - https://doi.org/10.1002/alz.71745
LA - en
ER -

CSL-JSON

{
"id": "10.1002/alz.71745",
"type": "article-journal",
"title": "Regional astrocyte dysregulation and altered glymphatic-related markers in Alzheimer's disease frontal cortex",
"container-title": "Alzheimer's & dementia : the journal of the Alzheimer's Association",
"author": [
{
"family": "Alexopoulos",
"given": "Harry"
},
{
"family": "Louka",
"given": "Xanthippi P"
},
{
"family": "De Natale",
"given": "Edoardo Rosario"
},
{
"family": "Koutroubi",
"given": "Kelly"
},
{
"family": "Cashmore",
"given": "Lisa"
},
{
"family": "Panayotacopoulou",
"given": "Maria T"
},
{
"family": "Trougakos",
"given": "Ioannis P"
},
{
"family": "Wilson",
"given": "Heather"
},
{
"family": "Politis",
"given": "Marios"
}
],
"container-title-short": "Alzheimers Dement",
"volume": "22",
"issue": "8",
"page": "e71745",
"DOI": "10.1002/alz.71745",
"PMID": "42598755",
"PMCID": "PMC13474155",
"ISSN": "1552-5260",
"publisher": "Wiley",
"URL": "https://doi.org/10.1002/alz.71745",
"language": "en",
"issued": {
"date-parts": [
[
2026,
8,
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.1093/brain/awag080 [code]
Early glymphatic failure in AppNL-F knock-in mice is linked to parenchymal border macrophages loss.
Journal: Brain : a journal of neurology
In common: Alzheimer's / dementia, cellular / molecular, 6 references
[2] 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: OpenCV, scikit-image, seaborn, 4 other tools, histology / microscopy, cellular / molecular, 1 reference
[3] doi:10.1038/s41467-026-76837-1 [code]
Drug screen and machine learning predict neuroprotective agents in a preclinical human model of childhood dementia.
Journal: Nature communications
In common: OpenCV, scikit-image, seaborn, 4 other tools, Alzheimer's / dementia, 1 reference
[4] 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: OpenCV, scikit-image, seaborn, 4 other tools, histology / microscopy, cellular / molecular, 1 reference
[5] doi:10.1002/alz.71649 [code]
Postmortem brain MRI reveals differential associations of subcortical and limbic volumes with cortical thinning and neurodegenerative pathologies.
Journal: Alzheimer's & dementia : the journal of the Alzheimer's Association
In common: OpenCV, scikit-image, seaborn, 4 other tools, histology / microscopy, Alzheimer's / dementia, cellular / molecular
[6] doi:10.1126/sciadv.aeb0404 [code]
MR-AIV reveals in vivo brain-wide fluid flow with physics-informed AI.
Journal: Science advances
In common: OpenCV, pandas, SciPy, 2 other tools, 3 references
[7] doi:10.1016/j.xcrm.2026.102766 [code]
A longitudinal single-cell and spatial multiomic atlas of pediatric high-grade glioma.
Journal: Cell reports. Medicine
In common: OpenCV, scikit-image, seaborn, 4 other tools, cellular / molecular, 1 reference
[8] 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: OpenCV, scikit-image, seaborn, 4 other tools, 1 reference
[9] doi:10.1038/s41593-026-02358-1 [code]
Cerebral venous blood flow regulates intracerebral pressure and brain clearance via meningeal lymphatic vessels.
Journal: Nature neuroscience
In common: scikit-image, SciPy, Matplotlib, 1 other tool, 3 references
[10] doi:10.1002/mrm.70514 [code]
Brain Clearance of Contrast Agent in Intravenous DCE-MRI Is Measurable and Cannot Be Explained by Clearance Across the BBB Alone.
Journal: Magnetic resonance in medicine
In common: seaborn, pandas, SciPy, 2 other tools, 3 references

Contribute

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

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

Request its removal

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

Discussion, reproductions, activity

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

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

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