OSCR

Clinical and pathologic correlations of machine learning quantification of Aβ deposits across 3 brain regions of decedents with Alzheimer disease.

Code ↔ Paper

1 match 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 1 match
  1. [1] § METHODS › Statistical analysis ↔ notebook/3) Analysis - Tissue Separation.ipynb, lines 479–551 · score 0.51 · standard deviations, computation, square, mixed, binary, zero

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,223 lines · 46 KB · GPL-3.0 · 1 match

  1. # %% [markdown]
  2. # ### 3 Analysis - Tissue Separation
  3. #
  4. # A sliding window approach was applied on the whole slide images (WSIs) with the trained CNN model to generate confidence heatmaps.
  5. #
  6. # The WSIs were tiled into 1536 x 1536 images patches in the prepocessing steps. A sliding window approached was applied on evey image patch of a WSI. At each time, the CNN model took a 256x256 pixels region as input, forward propagated and generated a prediction score for cored plaque, diffuse plaque and CAA respectively. By systematically sliding the input region across the entire 1536 x 1536 image patch, the prediction scores were saved and ploted as prediction confidence heatmap for this patch. The heatmap for the WSI was obtained by doing this on all image patches of it.
  7. #
  8. # Based on the heatmap prediction scores (saved as npy), we analyzed the plaque density distribution
  9. # %%
  10. import os, glob, datetime
  11. from time import time
  12. from tqdm import tqdm
  13. import numpy as np
  14. import matplotlib
  15. import matplotlib.pyplot as plt
  16. from PIL import Image
  17. Image.MAX_IMAGE_PIXELS = None
  18. import cppyy
  19. cppyy.load_library('gSLICr/build/libgSLICr_lib.so')
  20. cppyy.load_library('/usr/local/cuda-10.0/targets/x86_64-linux/lib/libcudart.so')
  21. cppyy.add_include_path('/usr/local/cuda-10.0/targets/x86_64-linux/include') # Add cuda library
  22. cppyy.include('gSLICr/gSLICr_Lib/gSLICr.h')
  23. from cppyy.gbl import gSLICr
  24. import cppyy.ll
  25. from skimage.segmentation import find_boundaries, mark_boundaries
  26. from skimage.measure import regionprops, label
  27. from skimage.morphology import binary_dilation, convex_hull_image
  28. # %%
  29. IMG_DIR = 'data/outputs/thres_tissue_png/'
  30. SAVE_DIR = 'data/outputs/tissue_mask_png/'
  31. filenames = glob.glob(IMG_DIR + '*.png')
  32. filenames = [filename.split('/')[-1] for filename in filenames]
  33. print(filenames)
  34. # %% [markdown]
  35. # ### Final Pipeline
  36. # %%
  37. # Separate stains
  38. #from skimage.color import separate_stains # Too much memory burden, separate computation tasks
  39. from skimage.util import img_as_float
  40. from tempfile import mkdtemp
  41. import shutil
  42. def color_deconv(rgb, conv_matrix):
  43. """RGB to stain color space conversion using color deconvolution.
  44. Original implementation in scikit-image
  45. https://github.com/scikit-image/scikit-image/blob/81363d32b94763150125dcf2c8909533c37567d9/skimage/color/colorconv.py#L1364
  46. Parameters
  47. ----------
  48. rgb : array_like
  49. The image in RGB format, in a 3-D array of shape ``(.., .., 3)``.
  50. conv_matrix: ndarray
  51. The stain separation matrix as described by G. Landini [1]_.
  52. Returns
  53. -------
  54. out : ndarray
  55. The image in stain color channel, in a 2-D array of shape
  56. ``(.., ..)``.
  57. """
  58. #rgb = img_as_float(rgb, force_copy=True)
  59. rgb = rgb.astype(dtype='float32')
  60. rgb += 2
  61. rgb = -np.log10(rgb, dtype='float32')
  62. out_shape = (rgb.shape[0], rgb.shape[1], 2)
  63. #rgb = np.reshape(rgb, (-1, 3)) @ (conv_matrix[:,0:2]) # 0:2 because only H&E channels are needed
  64. rgb = np.matmul(np.reshape(rgb, (-1, 3)), (conv_matrix[:,0:2]), dtype='float32')
  65. return np.reshape(rgb, out_shape)
  66. # Scale [0,1] to [0,255]
  67. def uint8scale(arr, _min=0, _max=1):
  68. return ((arr - _min) * 1/(_max - _min) * 255).astype('uint8')
  69. # Scale arr to [new_min, new_max]
  70. def scale_range(arr, new_min, new_max):
  71. return (arr - arr.min()) * (new_max - new_min) / (arr.max() - arr.min()) + new_min
  72. # Scale arr to have new_mean and new_std
  73. def scale_meanstd(arr, new_mean, new_std):
  74. return (arr - np.mean(arr)) / np.std(arr) * new_std + new_mean
  75. # Apply contrast threshold using QuPath data
  76. def apply_contrast_threshold(arr, thres_min, thres_max):
  77. arr[arr < thres_min] = thres_min
  78. arr[arr > thres_max] = thres_max
  79. return scale_range(arr, 0, 1)
  80. # Create a memmap numpy array on disk for float64 WSI
  81. # dirpath = mkdtemp()
  82. # fp = os.path.join(dirpath, 'arr.dat')
  83. # print(dirpath)
  84. startT = time()
  85. # Divide the WSI into regular slices to avoid insufficient memory
  86. # Cannot divide WSI into slices due to contrast threshold and scale_meanstd
  87. # slice_size = 10000
  88. rgb_img = np.array(Image.open('data/outputs/norm_png/NA4077-02_AB.png'))
  89. height, width, _ = rgb_img.shape
  90. print(rgb_img.shape)
  91. # Corresponding stain matrix
  92. #stain_mat = np.array([[0.572, 0.589, 0.572], [0.281, 0.514, 0.811], [0.488, -0.804, 0.341]]) # NA3077
  93. stain_mat = np.array([[0.624, 0.662, 0.415], [0.469, 0.612, 0.637], [0.616, -0.743, 0.261]]) # ref: NA5002_2
  94. stain_mat = np.linalg.inv(stain_mat)
  95. # Corresponding scale and contrast threshold constants
  96. consts = []
  97. consts.append({'mean':0.037, 'std':0.12, 'thres_min':-0.045568, 'thres_max':1.051931})
  98. consts.append({'mean':0.091, 'std':0.106, 'thres_min':0.01, 'thres_max':0.3})
  99. # t = tqdm(total=2)
  100. # for i in range(2):
  101. # Separate stains
  102. # i=0: Hematoxylin, i=1: Eosin, i=2: Residual
  103. print('Load Image: %s' % (time()-startT))
  104. mask_img = color_deconv(rgb_img, stain_mat)
  105. del rgb_img
  106. print('Color Deconv: %s' % (time()-startT))
  107. norm_img = []
  108. for i in range(2):
  109. # Scale using mean and standard deviation
  110. mask_img[:,:,i] = scale_meanstd(mask_img[:,:,i], consts[i]['mean'], consts[i]['std'])
  111. # Apply contrast threshold
  112. mask_img[:,:,i] = apply_contrast_threshold(mask_img[:,:,i], consts[i]['thres_min'], consts[i]['thres_max'])
  113. # Convert into uint8
  114. norm_img.append(np.asarray(uint8scale(mask_img[:,:,i]), dtype='uint8'))
  115. del mask_img
  116. # Convert numpy array into PIL image and save to local
  117. # Divide the WSI into regular slices to avoid PIL buffer overflow
  118. slice_size = 30000
  119. iters = np.uint8(np.ceil([height / slice_size, width / slice_size]))
  120. for i in range(2):
  121. mask_img = Image.new('L', (width, height))
  122. for row in range(iters[0]):
  123. for col in range(iters[1]):
  124. # Get start and end pixel location
  125. start_r, start_c = row * slice_size, col * slice_size
  126. end_r, end_c = min(height, start_r + slice_size), min(width, start_c + slice_size)
  127. # Paste the slice mask to image mask
  128. mask_slice = Image.fromarray(norm_img[i][start_r:end_r, start_c:end_c], 'L')
  129. mask_img.paste(mask_slice, (start_c, start_r))
  130. # Save masked image
  131. if i == 0:
  132. mask_img.save('data/outputs/norm_png/NA4077-02_AB_Hemato_4.png')
  133. elif i == 1:
  134. mask_img.save('data/outputs/norm_png/NA4077-02_AB_Eosin_4.png')
  135. del mask_slice, mask_img
  136. print('Total: %s' % (time()-startT))
  137. # %%
  138. # Function definition for each step
  139. # Helper func: Scale [0,1] to [0,255]
  140. def uint8scale(arr, _min=0, _max=1):
  141. """Scale an array to [0,255].
  142. Parameters
  143. -----------
  144. arr : ndarray
  145. Input array.
  146. _min : int, optional
  147. Minimum value within arr.
  148. _max : int, optional
  149. Maximum value within arr.
  150. Returns
  151. -------
  152. output : ndarray
  153. The scaled array.
  154. """
  155. return ((arr - _min) * 1/(_max - _min) * 255).astype('uint8')
  156. def run_slic_seg(img, n_segments=80000, n_iter=5, compactness=0.01, enforce_connectivity=True, slic_zero=True):
  157. """Run gpu SLICO in C++.
  158. Parameters
  159. -----------
  160. img : 2D ndarray
  161. Input image, must be grayscale.
  162. n_segments : int, optional
  163. The (approximate) number of labels in the segmented output image.
  164. n_iter : int, optional
  165. The number of iterations of k-means.
  166. compactness : float, optional
  167. Balances color proximity and space proximity. Higher values give
  168. more weight to space proximity, making superpixel shapes more
  169. square/cubic. In SLICO mode, this is the initial compactness.
  170. This parameter depends strongly on image contrast and on the
  171. shapes of objects in the image. We recommend exploring possible
  172. values on a log scale, e.g., 0.01, 0.1, 1, 10, 100, before
  173. refining around a chosen value.
  174. enforce_connectivity : bool, optional
  175. Whether the generated segments are connected or not.
  176. slic_zero : bool, optional
  177. Run SLIC-zero, the zero-parameter mode of SLIC. [2]_
  178. Returns
  179. -------
  180. segments_slic : 2D ndarray
  181. Integer mask indicating SLIC segment labels.
  182. References
  183. ----------
  184. .. [1] Radhakrishna Achanta, Appu Shaji, Kevin Smith, Aurelien Lucchi,
  185. Pascal Fua, and Sabine Süsstrunk, SLIC Superpixels Compared to
  186. State-of-the-art Superpixel Methods, TPAMI, May 2012.
  187. .. [2] http://ivrg.epfl.ch/research/superpixels#SLICO
  188. """
  189. height, width = img.shape
  190. # Create a Run_SLIC C++ class
  191. run_slic = gSLICr.Run_SLIC()
  192. # Specify settings
  193. gslic_settings = run_slic.get_settings()
  194. gslic_settings = cppyy.bind_object(gslic_settings, gSLICr.objects.settings)
  195. gslic_settings.img_size.x = width
  196. gslic_settings.img_size.y = height
  197. gslic_settings.no_segs = n_segments
  198. gslic_settings.no_iters = n_iter
  199. gslic_settings.coh_weight = compactness
  200. gslic_settings.do_enforce_connectivity = enforce_connectivity
  201. gslic_settings.slic_zero = slic_zero
  202. gslic_settings.color_space = gSLICr.GRAY
  203. gslic_settings.seg_method = gSLICr.GIVEN_NUM
  204. run_slic.print_settings()
  205. # Start running gSLIC
  206. run_slic.run(img)
  207. # Get the segmentation mask
  208. segments_slic = run_slic.get_mask()
  209. segments_slic.reshape((height*width,))
  210. segments_slic = np.frombuffer(segments_slic, dtype=np.intc, count=height*width).reshape((height, width)).copy()
  211. # Destruct the C++ class
  212. run_slic.__destruct__()
  213. return segments_slic
  214. def slic_mean_intensity_thres(img, segments_slic, thres=60):
  215. """Thresholding based on SLIC segments' mean intensity.
  216. Parameters
  217. -----------
  218. img : 2D ndarray
  219. Input image, must be grayscale.
  220. segments_slic : 2D ndarray
  221. Integer mask indicating SLIC segment labels.
  222. thres : int, optional
  223. Threshold value of mean intensity.
  224. Returns
  225. -------
  226. mask_img : 2D ndarray
  227. Binary integer mask indicating segment labels.
  228. """
  229. mask_img = np.zeros_like(img, dtype='uint8')
  230. for region in regionprops(segments_slic, img):
  231. if region.mean_intensity >= thres:
  232. mask_img.flat[np.ravel_multi_index(region.coords.transpose(), mask_img.shape)] = 255
  233. return mask_img
  234. def get_connected_comp(mask_img, structure_size=9):
  235. """Get connected components inside an image.
  236. Parameters
  237. -----------
  238. mask_img : 2D ndarray
  239. Input image, must be binary.
  240. structure_size : int, optional
  241. Structure size for binary dilation before connected component labeling.
  242. Returns
  243. -------
  244. output : 2D ndarray
  245. Integer mask indicating connected component labels.
  246. """
  247. return label(binary_dilation(mask_img, selem=np.ones((structure_size, structure_size), dtype='int')), connectivity=2)
  248. def find_tissue(img, connected_comp, prop=0.2):
  249. """Find the tissue component within a connected component map.
  250. Parameters
  251. -----------
  252. img : 2D ndarray
  253. Input image, must be grayscale.
  254. connected_comp : 2D ndarray
  255. Integer mask indicating connected component labels.
  256. prop : float, optional
  257. Proportional occupied area of the entire image to be considered as tissue.
  258. Returns
  259. -------
  260. tissue_img : 2D ndarray
  261. Binary integer mask indicating the tissue.
  262. """
  263. height, width = img.shape
  264. area_thres = prop * height * width
  265. tissue_img = np.zeros_like(img, dtype='uint8')
  266. for region in regionprops(connected_comp, img):
  267. if region.area >= area_thres:
  268. tissue_img.flat[np.ravel_multi_index(region.coords.transpose(), tissue_img.shape)] = 255
  269. return tissue_img
  270. def find_refine_boundaries(img, segments_slic, tissue_img):
  271. """Find and refine boundaries of the tissue image using convex hull.
  272. Parameters
  273. -----------
  274. img : 2D ndarray
  275. Input image, must be grayscale.
  276. segments_slic : 2D ndarray
  277. Integer mask indicating SLIC segment labels.
  278. tissue_img : 2D ndarray
  279. Binary integer mask indicating the tissue.
  280. Returns
  281. -------
  282. tissue_img : 2D ndarray
  283. Binary integer refined mask indicating the tissue.
  284. """
  285. # Find boundaries
  286. boundaries = find_boundaries(tissue_img, connectivity=2, mode='thick')
  287. labels = np.unique(segments_slic[boundaries])
  288. # Convex hull on boundaries
  289. for region in regionprops(segments_slic, img):
  290. if region.label in labels:
  291. min_r, min_c, max_r, max_c = region.bbox
  292. img_tile = tissue_img[min_r:max_r, min_c:max_c]
  293. outlier_hull_img = convex_hull_image(img_tile)
  294. tissue_img[min_r:max_r, min_c:max_c] = (outlier_hull_img * 255).astype('uint8')
  295. return tissue_img
  296. def save_grayscale_img(img, filename):
  297. """Save a grayscale image locally as filename.
  298. Parameters
  299. -----------
  300. img : 2D ndarray
  301. Input image, must be grayscale.
  302. filename : string
  303. File path and file name to save.
  304. """
  305. height, width = img.shape
  306. # Convert numpy array into PIL image and save to local
  307. # Divide the WSI into regular slices to avoid PIL buffer overflow
  308. slice_size = 30000
  309. iters = np.uint8(np.ceil([height / slice_size, width / slice_size]))
  310. save_img = Image.new('L', (width, height))
  311. for row in range(iters[0]):
  312. for col in range(iters[1]):
  313. # Get start and end pixel location
  314. start_r, start_c = row * slice_size, col * slice_size
  315. end_r, end_c = min(height, start_r + slice_size), min(width, start_c + slice_size)
  316. # Paste the slice mask to image mask
  317. save_slice = Image.fromarray(img[start_r:end_r, start_c:end_c], 'L')
  318. save_img.paste(save_slice, (start_c, start_r))
  319. # Save masked image
  320. save_img.save(filename)
  321. # %%
  322. # Final Pipeline
  323. currentDT = datetime.datetime.now(); print(str(currentDT)); totalT = 0.0; startT = time()
  324. img = np.array(Image.open('data/outputs/norm_png/NA3777-02_AB_Eosin.png'))
  325. # Binary Thresholding to improve contrast
  326. img = np.where(img < 15, 0, 255).astype('uint8')
  327. print("Loaded Image: %s" % (time()-startT)); totalT += (time()-startT); startT = time()
  328. segments_slic = run_slic_seg(img)
  329. print("Total SLIC: %s" % (time()-startT)); totalT += (time()-startT); startT = time()
  330. # Mean Intensity Thresholding
  331. mask_img = slic_mean_intensity_thres(img, segments_slic)
  332. print("Mean Intensity Thresholding: %s" % (time()-startT)); totalT += (time()-startT); startT = time()
  333. # Connected components
  334. connected_comp = get_connected_comp(mask_img)
  335. del mask_img
  336. print("Connected component labeling: %s" % (time()-startT)); totalT += (time()-startT); startT = time()
  337. # Find tissue component
  338. tissue_img = find_tissue(img, connected_comp)
  339. del connected_comp
  340. print("Find tissue component: %s" % (time()-startT)); totalT += (time()-startT); startT = time()
  341. # Find and refine boundaries
  342. tissue_img = find_refine_boundaries(img, segments_slic, tissue_img)
  343. del img, segments_slic
  344. print("Find and refine boundaries: %s" % (time()-startT)); totalT += (time()-startT); startT = time()
  345. # Save image
  346. save_grayscale_img(tissue_img, 'NA3777-02_AB_Eosin_SLICO_0.01_iter=5_80000_finalmask.png')
  347. print("Saved: %s" % (time()-startT)); totalT += (time()-startT)
  348. print("Total: %s" % totalT)
  349. # %% [markdown]
  350. # ### Timing Results
  351. # | SLIC #Segments | Load Image | SLIC | Mean Intensity Thresholding | Connected Component | Find Tissue | Find Boundaries | Convex Hull | Save Output | Total |
  352. # |:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
  353. # | 40000 | 27.6471 | 232.6231 | 57.3725 | 106.8111 | 58.1894 | 137.0627 | 33.8033 | 48.8813 | 702.3917 |
  354. # | 60000 | 30.0168 | 206.1811 | 60.3673 | 110.6402 | 59.3389 | 140.0935 | 31.8562 | 49.2528 | 687.7481 |
  355. # | 80000 | 27.9608 | 249.4539 | 58.0493 | 107.5019 | 57.5752 | 136.3321 | 30.8568 | 49.1053 | 716.8363 |
  356. # %% [markdown]
  357. # ## Pipeline Development
  358. # ### Latest Scikit-image Canny Algorithm
  359. # %%
  360. """
  361. canny.py - Canny Edge detector
  362. Reference: Canny, J., A Computational Approach To Edge Detection, IEEE Trans.
  363. Pattern Analysis and Machine Intelligence, 8:679-714, 1986
  364. Originally part of CellProfiler, code licensed under both GPL and BSD licenses.
  365. Website: http://www.cellprofiler.org
  366. Copyright (c) 2003-2009 Massachusetts Institute of Technology
  367. Copyright (c) 2009-2011 Broad Institute
  368. All rights reserved.
  369. Original author: Lee Kamentsky
  370. """
  371. import numpy as np
  372. import scipy.ndimage as ndi
  373. from scipy.ndimage import generate_binary_structure
  374. from skimage.filters import gaussian
  375. from skimage import dtype_limits, img_as_float
  376. from skimage._shared.utils import assert_nD
  377. def smooth_with_function_and_mask(image, function, mask):
  378. """Smooth an image with a linear function, ignoring masked pixels
  379. Parameters
  380. ----------
  381. image : array
  382. Image you want to smooth.
  383. function : callable
  384. A function that does image smoothing.
  385. mask : array
  386. Mask with 1's for significant pixels, 0's for masked pixels.
  387. Notes
  388. ------
  389. This function calculates the fractional contribution of masked pixels
  390. by applying the function to the mask (which gets you the fraction of
  391. the pixel data that's due to significant points). We then mask the image
  392. and apply the function. The resulting values will be lower by the
  393. bleed-over fraction, so you can recalibrate by dividing by the function
  394. on the mask to recover the effect of smoothing from just the significant
  395. pixels.
  396. """
  397. bleed_over = function(mask.astype(float))
  398. masked_image = np.zeros(image.shape, image.dtype)
  399. masked_image[mask] = image[mask]
  400. smoothed_image = function(masked_image)
  401. output_image = smoothed_image / (bleed_over + np.finfo(float).eps)
  402. return output_image
  403. def canny(image, sigma=1., low_threshold=None, high_threshold=None, mask=None,
  404. use_quantiles=False):
  405. """Edge filter an image using the Canny algorithm.
  406. Parameters
  407. -----------
  408. image : 2D array
  409. Grayscale input image to detect edges on; can be of any dtype.
  410. sigma : float
  411. Standard deviation of the Gaussian filter.
  412. low_threshold : float
  413. Lower bound for hysteresis thresholding (linking edges).
  414. If None, low_threshold is set to 10% of dtype's max.
  415. high_threshold : float
  416. Upper bound for hysteresis thresholding (linking edges).
  417. If None, high_threshold is set to 20% of dtype's max.
  418. mask : array, dtype=bool, optional
  419. Mask to limit the application of Canny to a certain area.
  420. use_quantiles : bool, optional
  421. If True then treat low_threshold and high_threshold as quantiles of the
  422. edge magnitude image, rather than absolute edge magnitude values. If True
  423. then the thresholds must be in the range [0, 1].
  424. Returns
  425. -------
  426. output : 2D array (image)
  427. The binary edge map.
  428. See also
  429. --------
  430. skimage.sobel
  431. Notes
  432. -----
  433. The steps of the algorithm are as follows:
  434. * Smooth the image using a Gaussian with ``sigma`` width.
  435. * Apply the horizontal and vertical Sobel operators to get the gradients
  436. within the image. The edge strength is the norm of the gradient.
  437. * Thin potential edges to 1-pixel wide curves. First, find the normal
  438. to the edge at each point. This is done by looking at the
  439. signs and the relative magnitude of the X-Sobel and Y-Sobel
  440. to sort the points into 4 categories: horizontal, vertical,
  441. diagonal and antidiagonal. Then look in the normal and reverse
  442. directions to see if the values in either of those directions are
  443. greater than the point in question. Use interpolation to get a mix of
  444. points instead of picking the one that's the closest to the normal.
  445. * Perform a hysteresis thresholding: first label all points above the
  446. high threshold as edges. Then recursively label any point above the
  447. low threshold that is 8-connected to a labeled point as an edge.
  448. References
  449. -----------
  450. .. [1] Canny, J., A Computational Approach To Edge Detection, IEEE Trans.
  451. Pattern Analysis and Machine Intelligence, 8:679-714, 1986
  452. .. [2] William Green's Canny tutorial
  453. http://dasl.unlv.edu/daslDrexel/alumni/bGreen/www.pages.drexel.edu/_weg22/can_tut.html
  454. Examples
  455. --------
  456. >>> from skimage import feature
  457. >>> # Generate noisy image of a square
  458. >>> im = np.zeros((256, 256))
  459. >>> im[64:-64, 64:-64] = 1
  460. >>> im += 0.2 * np.random.rand(*im.shape)
  461. >>> # First trial with the Canny filter, with the default smoothing
  462. >>> edges1 = feature.canny(im)
  463. >>> # Increase the smoothing for better results
  464. >>> edges2 = feature.canny(im, sigma=3)
  465. """
  466. #
  467. # The steps involved:
  468. #
  469. # * Smooth using the Gaussian with sigma above.
  470. #
  471. # * Apply the horizontal and vertical Sobel operators to get the gradients
  472. # within the image. The edge strength is the sum of the magnitudes
  473. # of the gradients in each direction.
  474. #
  475. # * Find the normal to the edge at each point using the arctangent of the
  476. # ratio of the Y sobel over the X sobel - pragmatically, we can
  477. # look at the signs of X and Y and the relative magnitude of X vs Y
  478. # to sort the points into 4 categories: horizontal, vertical,
  479. # diagonal and antidiagonal.
  480. #
  481. # * Look in the normal and reverse directions to see if the values
  482. # in either of those directions are greater than the point in question.
  483. # Use interpolation to get a mix of points instead of picking the one
  484. # that's the closest to the normal.
  485. #
  486. # * Label all points above the high threshold as edges.
  487. # * Recursively label any point above the low threshold that is 8-connected
  488. # to a labeled point as an edge.
  489. #
  490. # Regarding masks, any point touching a masked point will have a gradient
  491. # that is "infected" by the masked point, so it's enough to erode the
  492. # mask by one and then mask the output. We also mask out the border points
  493. # because who knows what lies beyond the edge of the image?
  494. #
  495. assert_nD(image, 2)
  496. dtype_max = dtype_limits(image, clip_negative=False)[1]
  497. if low_threshold is None:
  498. low_threshold = 0.1
  499. else:
  500. low_threshold = low_threshold / dtype_max
  501. if high_threshold is None:
  502. high_threshold = 0.2
  503. else:
  504. high_threshold = high_threshold / dtype_max
  505. if mask is None:
  506. mask = np.ones(image.shape, dtype=bool)
  507. def fsmooth(x):
  508. return img_as_float(gaussian(x, sigma, mode='constant'))
  509. smoothed = smooth_with_function_and_mask(image, fsmooth, mask)
  510. jsobel = ndi.sobel(smoothed, axis=1)
  511. isobel = ndi.sobel(smoothed, axis=0)
  512. abs_isobel = np.abs(isobel)
  513. abs_jsobel = np.abs(jsobel)
  514. magnitude = np.hypot(isobel, jsobel)
  515. #
  516. # Make the eroded mask. Setting the border value to zero will wipe
  517. # out the image edges for us.
  518. #
  519. s = generate_binary_structure(2, 2)
  520. eroded_mask = ndi.binary_erosion(mask, s, border_value=0)
  521. eroded_mask = eroded_mask & (magnitude > 0)
  522. #
  523. #--------- Find local maxima --------------
  524. #
  525. # Assign each point to have a normal of 0-45 degrees, 45-90 degrees,
  526. # 90-135 degrees and 135-180 degrees.
  527. #
  528. local_maxima = np.zeros(image.shape, bool)
  529. #----- 0 to 45 degrees ------
  530. pts_plus = (isobel >= 0) & (jsobel >= 0) & (abs_isobel >= abs_jsobel)
  531. pts_minus = (isobel <= 0) & (jsobel <= 0) & (abs_isobel >= abs_jsobel)
  532. pts = pts_plus | pts_minus
  533. pts = eroded_mask & pts
  534. # Get the magnitudes shifted left to make a matrix of the points to the
  535. # right of pts. Similarly, shift left and down to get the points to the
  536. # top right of pts.
  537. c1 = magnitude[1:, :][pts[:-1, :]]
  538. c2 = magnitude[1:, 1:][pts[:-1, :-1]]
  539. m = magnitude[pts]
  540. w = abs_jsobel[pts] / abs_isobel[pts]
  541. c_plus = c2 * w + c1 * (1 - w) <= m
  542. c1 = magnitude[:-1, :][pts[1:, :]]
  543. c2 = magnitude[:-1, :-1][pts[1:, 1:]]
  544. c_minus = c2 * w + c1 * (1 - w) <= m
  545. local_maxima[pts] = c_plus & c_minus
  546. #----- 45 to 90 degrees ------
  547. # Mix diagonal and vertical
  548. #
  549. pts_plus = (isobel >= 0) & (jsobel >= 0) & (abs_isobel <= abs_jsobel)
  550. pts_minus = (isobel <= 0) & (jsobel <= 0) & (abs_isobel <= abs_jsobel)
  551. pts = pts_plus | pts_minus
  552. pts = eroded_mask & pts
  553. c1 = magnitude[:, 1:][pts[:, :-1]]
  554. c2 = magnitude[1:, 1:][pts[:-1, :-1]]
  555. m = magnitude[pts]
  556. w = abs_isobel[pts] / abs_jsobel[pts]
  557. c_plus = c2 * w + c1 * (1 - w) <= m
  558. c1 = magnitude[:, :-1][pts[:, 1:]]
  559. c2 = magnitude[:-1, :-1][pts[1:, 1:]]
  560. c_minus = c2 * w + c1 * (1 - w) <= m
  561. local_maxima[pts] = c_plus & c_minus
  562. #----- 90 to 135 degrees ------
  563. # Mix anti-diagonal and vertical
  564. #
  565. pts_plus = (isobel <= 0) & (jsobel >= 0) & (abs_isobel <= abs_jsobel)
  566. pts_minus = (isobel >= 0) & (jsobel <= 0) & (abs_isobel <= abs_jsobel)
  567. pts = pts_plus | pts_minus
  568. pts = eroded_mask & pts
  569. c1a = magnitude[:, 1:][pts[:, :-1]]
  570. c2a = magnitude[:-1, 1:][pts[1:, :-1]]
  571. m = magnitude[pts]
  572. w = abs_isobel[pts] / abs_jsobel[pts]
  573. c_plus = c2a * w + c1a * (1.0 - w) <= m
  574. c1 = magnitude[:, :-1][pts[:, 1:]]
  575. c2 = magnitude[1:, :-1][pts[:-1, 1:]]
  576. c_minus = c2 * w + c1 * (1.0 - w) <= m
  577. local_maxima[pts] = c_plus & c_minus
  578. #----- 135 to 180 degrees ------
  579. # Mix anti-diagonal and anti-horizontal
  580. #
  581. pts_plus = (isobel <= 0) & (jsobel >= 0) & (abs_isobel >= abs_jsobel)
  582. pts_minus = (isobel >= 0) & (jsobel <= 0) & (abs_isobel >= abs_jsobel)
  583. pts = pts_plus | pts_minus
  584. pts = eroded_mask & pts
  585. c1 = magnitude[:-1, :][pts[1:, :]]
  586. c2 = magnitude[:-1, 1:][pts[1:, :-1]]
  587. m = magnitude[pts]
  588. w = abs_jsobel[pts] / abs_isobel[pts]
  589. c_plus = c2 * w + c1 * (1 - w) <= m
  590. c1 = magnitude[1:, :][pts[:-1, :]]
  591. c2 = magnitude[1:, :-1][pts[:-1, 1:]]
  592. c_minus = c2 * w + c1 * (1 - w) <= m
  593. local_maxima[pts] = c_plus & c_minus
  594. #
  595. #---- If use_quantiles is set then calculate the thresholds to use
  596. #
  597. if use_quantiles:
  598. if high_threshold > 1.0 or low_threshold > 1.0:
  599. raise ValueError("Quantile thresholds must not be > 1.0")
  600. if high_threshold < 0.0 or low_threshold < 0.0:
  601. raise ValueError("Quantile thresholds must not be < 0.0")
  602. high_threshold = np.percentile(magnitude, 100.0 * high_threshold)
  603. low_threshold = np.percentile(magnitude, 100.0 * low_threshold)
  604. #
  605. #---- Create two masks at the two thresholds.
  606. #
  607. high_mask = local_maxima & (magnitude >= high_threshold)
  608. low_mask = local_maxima & (magnitude >= low_threshold)
  609. #
  610. # Segment the low-mask, then only keep low-segments that have
  611. # some high_mask component in them
  612. #
  613. strel = np.ones((3, 3), bool)
  614. labels, count = ndi.label(low_mask, strel)
  615. if count == 0:
  616. return low_mask
  617. sums = (np.array(ndi.sum(high_mask, labels,
  618. np.arange(count, dtype=np.int32) + 1),
  619. copy=False, ndmin=1))
  620. good_label = np.zeros((count + 1,), bool)
  621. good_label[1:] = sums > 0
  622. output_mask = good_label[labels]
  623. return output_mask
  624. # %% [markdown]
  625. # ### Superpixel
  626. # %%
  627. # Superpixel
  628. from skimage.segmentation import slic, mark_boundaries
  629. from skimage.util import img_as_float
  630. img = np.array(Image.open('data/outputs/norm_png/NA3777-02_AB_Eosin.png'))
  631. # Binary Thresholding
  632. img = np.where(img < 15, 0, 255).astype('uint8')
  633. height, width = img.shape
  634. print(img.shape)
  635. # %%
  636. import cppyy
  637. cppyy.load_library('gSLICr/build/libgSLICr_lib.so')
  638. cppyy.load_library('/usr/local/cuda-10.0/targets/x86_64-linux/lib/libcudart.so')
  639. cppyy.add_include_path('/usr/local/cuda-10.0/targets/x86_64-linux/include') # Add cuda library
  640. cppyy.include('gSLICr/gSLICr_Lib/gSLICr.h')
  641. from cppyy.gbl import gSLICr
  642. import cppyy.ll
  643. from skimage.segmentation import mark_boundaries
  644. currentDT = datetime.datetime.now(); print(str(currentDT)); startT = time()
  645. # Create a Run_SLIC C++ class
  646. run_slic = gSLICr.Run_SLIC()
  647. print("Run_SLIC: %s" % (time()-startT))
  648. # Specify settings
  649. gslic_settings = run_slic.get_settings()
  650. gslic_settings = cppyy.bind_object(gslic_settings, gSLICr.objects.settings)
  651. gslic_settings.img_size.x = width
  652. gslic_settings.img_size.y = height
  653. gslic_settings.no_segs = 60000
  654. gslic_settings.no_iters = 5
  655. gslic_settings.coh_weight = 0.01
  656. gslic_settings.do_enforce_connectivity = True
  657. gslic_settings.slic_zero = True
  658. gslic_settings.color_space = gSLICr.GRAY
  659. gslic_settings.seg_method = gSLICr.GIVEN_NUM
  660. run_slic.print_settings()
  661. # Start running gSLIC
  662. run_slic.run(img)
  663. print("run: %s" % (time()-startT))
  664. # Get the segmentation mask
  665. segments_slic = run_slic.get_mask()
  666. print("get_mask: %s" % (time()-startT))
  667. segments_slic.reshape((height*width,))
  668. segments_slic = np.frombuffer(segments_slic, dtype=np.intc, count=height*width).reshape((height, width)).copy()
  669. print("reshape: %s" % (time()-startT))
  670. run_slic.__destruct__()
  671. print("__destruct__: %s" % (time()-startT))
  672. # 81 to 73 after block-dim=32 using slic iter=0
  673. # Down to 50 after changing from Float3Image to FloatImage
  674. np.save('segments_slico_0.01_iter=5_60000', segments_slic)
  675. print("save as npy: %s" % (time()-startT))
  676. # Scale [0,1] to [0,255]
  677. def uint8scale(arr, _min=0, _max=1):
  678. return ((arr - _min) * 1/(_max - _min) * 255).astype('uint8')
  679. print('SLIC number of segments: {}'.format(len(np.unique(segments_slic))))
  680. mark_img = mark_boundaries(img, segments_slic, outline_color=(1,1,0), mode='thick')
  681. print('Marked')
  682. del segments_slic#, img
  683. norm_img = uint8scale(mark_img)
  684. print('Normalized')
  685. del mark_img
  686. save_img = Image.fromarray(norm_img, 'RGB')
  687. save_img.save('NA3777-02_AB_Eosin_SLICO_0.01_iter=5_60000.png')
  688. del save_img
  689. print('Saved')
  690. # %%
  691. # Superpixel
  692. from skimage.segmentation import slic, mark_boundaries
  693. from skimage.util import img_as_float
  694. # Scale [0,1] to [0,255]
  695. def uint8scale(arr, _min=0, _max=1):
  696. return ((arr - _min) * 1/(_max - _min) * 255).astype('uint8')
  697. img = np.array(Image.open('data/outputs/norm_png/NA3777-02_AB_Eosin.png'))
  698. # Binary Thresholding
  699. img = np.where(img < 15, 0, 255).astype('uint8')
  700. height, width = img.shape
  701. print(img.shape)
  702. img = img_as_float(img)
  703. # Max memory 123+16 using float64. 255 segments. Run time: 1324 seconds = 22 minutes
  704. currentDT = datetime.datetime.now(); print(str(currentDT)); startT = time()
  705. segments_slic = slic(img, n_segments=5000, compactness=0.01, slic_zero=True)
  706. print("SLIC: %s" % (time()-startT))
  707. print('SLIC number of segments: {}'.format(len(np.unique(segments_slic))))
  708. del img
  709. img = np.array(Image.open('data/outputs/norm_png/NA3777-02_AB_Eosin.png'))
  710. # Binary Thresholding
  711. img = np.where(img < 15, 0, 255).astype('uint8')
  712. print('Loaded')
  713. mark_img = mark_boundaries(img, segments_slic, outline_color=(1,1,0), mode='thick')
  714. print('Marked')
  715. del segments_slic, img
  716. norm_img = uint8scale(mark_img)
  717. print('Normalized')
  718. del mark_img
  719. save_img = Image.fromarray(norm_img, 'RGB')
  720. save_img.save('NA3777-02_AB_Eosin_SLIC.png')
  721. print('Saved')
  722. print("Total: %s" % (time()-startT))
  723. # %% [markdown]
  724. # ### Mean Intensity Threshold
  725. # %%
  726. segments_slic = np.load('segments_slico_0.01_iter=5_60000.npy')
  727. img = np.array(Image.open('data/outputs/norm_png/NA3777-02_AB_Eosin.png'))
  728. # Binary Thresholding
  729. img = np.where(img < 15, 0, 255).astype('uint8')
  730. height, width = img.shape
  731. # %%
  732. from skimage.measure import regionprops
  733. mean_intensity_thres = 60
  734. mask_img = np.zeros_like(img, dtype='uint8')
  735. for region in regionprops(segments_slic, img):
  736. if region.mean_intensity >= mean_intensity_thres:
  737. mask_img.flat[np.ravel_multi_index(region.coords.transpose(), mask_img.shape)] = 255
  738. # %%
  739. # Save superpixel with average intensity
  740. from skimage.measure import regionprops
  741. segments_slic = np.load('segments_slico_0.1_iter=5_40000.npy')
  742. img = np.array(Image.open('data/outputs/norm_png/NA3777-02_AB_Eosin.png'))
  743. # Binary Thresholding
  744. img = np.where(img < 15, 0, 255).astype('uint8')
  745. height, width = img.shape
  746. mask_img = np.zeros_like(img, dtype='uint8')
  747. for region in regionprops(segments_slic, img):
  748. mask_img.flat[np.ravel_multi_index(region.coords.transpose(), mask_img.shape)] = region.mean_intensity
  749. # Convert numpy array into PIL image and save to local
  750. # Divide the WSI into regular slices to avoid PIL buffer overflow
  751. slice_size = 30000
  752. iters = np.uint8(np.ceil([height / slice_size, width / slice_size]))
  753. save_img = Image.new('L', (width, height))
  754. for row in range(iters[0]):
  755. for col in range(iters[1]):
  756. # Get start and end pixel location
  757. start_r, start_c = row * slice_size, col * slice_size
  758. end_r, end_c = min(height, start_r + slice_size), min(width, start_c + slice_size)
  759. # Paste the slice mask to image mask
  760. save_slice = Image.fromarray(mask_img[start_r:end_r, start_c:end_c], 'L')
  761. save_img.paste(save_slice, (start_c, start_r))
  762. # Save masked image
  763. save_img.save('NA3777-02_AB_Eosin_SLIC_0.1_iter=5_40000_meanintensity.png')
  764. # %% [markdown]
  765. # ### Connected Component
  766. # %%
  767. from skimage.measure import label
  768. from skimage.morphology import binary_dilation
  769. currentDT = datetime.datetime.now(); print(str(currentDT)); startT = time()
  770. connected_comp = label(binary_dilation(mask_img, selem=np.ones((9, 9), dtype='int')), connectivity=2)
  771. print("Connected component labeling: %s" % (time()-startT))
  772. print('Number of connected components: {}'.format(len(np.unique(connected_comp))))
  773. # %%
  774. for region in regionprops(connected_comp, img):
  775. print(region.area)
  776. # %%
  777. mask_img2 = np.zeros_like(img, dtype='uint8')
  778. for region in regionprops(connected_comp, img):
  779. if region.area >= 1e8:
  780. mask_img2.flat[np.ravel_multi_index(region.coords.transpose(), mask_img2.shape)] = 255
  781. # %%
  782. # Save connected component image
  783. # Convert numpy array into PIL image and save to local
  784. # Divide the WSI into regular slices to avoid PIL buffer overflow
  785. slice_size = 30000
  786. iters = np.uint8(np.ceil([height / slice_size, width / slice_size]))
  787. save_img = Image.new('L', (width, height))
  788. for row in range(iters[0]):
  789. for col in range(iters[1]):
  790. # Get start and end pixel location
  791. start_r, start_c = row * slice_size, col * slice_size
  792. end_r, end_c = min(height, start_r + slice_size), min(width, start_c + slice_size)
  793. # Paste the slice mask to image mask
  794. save_slice = Image.fromarray(mask_img2[start_r:end_r, start_c:end_c], 'L')
  795. save_img.paste(save_slice, (start_c, start_r))
  796. # Save masked image
  797. save_img.save('NA3777-02_AB_Eosin_SLIC_0.01_iter=5_40000_connectedcomp=25.png')
  798. # %% [markdown]
  799. # ### Find and Extend Boundaries
  800. # %%
  801. from skimage.segmentation import find_boundaries
  802. boundaries = find_boundaries(mask_img2, connectivity=2, mode='thick')
  803. # %% [markdown]
  804. # ### Associate with SLICO labels
  805. # %%
  806. labels = np.unique(segments_slic[boundaries])
  807. print(len(labels))
  808. # %%
  809. # Keep only medium intensity labels
  810. lower_thres = 60
  811. edge_labels = []
  812. for region in regionprops(segments_slic, img):
  813. mean_inten = region.mean_intensity
  814. if region.label in labels and mean_inten >= lower_thres:
  815. edge_labels.append(region.label)
  816. print(len(edge_labels))
  817. # %%
  818. # Mark boundaries for visualization
  819. del mask_img, mask_img2, connected_comp, boundaries
  820. from skimage.segmentation import mark_boundaries
  821. mark_img = mark_boundaries(img, segments_slic, mode='thick')
  822. # Scale to uint8
  823. mark_img *= 255
  824. mark_img = mark_img.astype('uint8')
  825. # %%
  826. # dev
  827. import matplotlib.patches as patches
  828. # from skimage.feature import canny
  829. from skimage.morphology import convex_hull_image
  830. from IPython import display
  831. #from pyod.models.ocsvm import OCSVM
  832. #from pyod.models.cof import COF
  833. #from pyod.models.abod import ABOD
  834. #from pyod.models.lscp import LSCP
  835. #from pyod.models.loci import LOCI
  836. from pyod.models.knn import KNN
  837. from pyod.models.iforest import IForest
  838. from pyod.models.cblof import CBLOF
  839. from pyod.models.feature_bagging import FeatureBagging
  840. from pyod.models.lof import LOF
  841. from pyod.models.hbos import HBOS
  842. from skimage.morphology import binary_erosion
  843. from pyod.models.pca import PCA
  844. from pyod.models.mcd import MCD
  845. nrows = 10
  846. ncols = 6
  847. fig, axes = plt.subplots(nrows, ncols, figsize=(16,20), dpi=300)
  848. axes = axes.reshape((-1,))
  849. canny_times = np.zeros(nrows)
  850. outlier_times = np.zeros(nrows)
  851. count = 0
  852. for region in regionprops(segments_slic, img):
  853. #if region.label in edge_labels:
  854. if region.label in labels:
  855. min_r, min_c, max_r, max_c = region.bbox
  856. img_tile = img[min_r:max_r, min_c:max_c]
  857. #axes[count%nrows*ncols].imshow(img_tile, cmap='gray')
  858. axes[count%nrows*ncols].imshow(mask_img[min_r:max_r, min_c:max_c])
  859. axes[count%nrows*ncols].set_title('Region {} Label {}'.format(count+1, region.label))
  860. axes[count%nrows*ncols].set_xticklabels([])
  861. axes[count%nrows*ncols].set_yticklabels([])
  862. # Rough location within WSI
  863. rect = patches.Rectangle((min_c, min_r),max_c-min_c,max_r-min_r,linewidth=10,edgecolor='r',facecolor='r')
  864. axes[count%nrows*ncols+1].clear()
  865. axes[count%nrows*ncols+1].add_patch(rect)
  866. axes[count%nrows*ncols+1].set_xlim((0, img.shape[1]))
  867. axes[count%nrows*ncols+1].set_ylim((0, img.shape[0]))
  868. axes[count%nrows*ncols+1].invert_yaxis()
  869. axes[count%nrows*ncols+1].set_title('Approx Location')
  870. axes[count%nrows*ncols+1].set_xticklabels([])
  871. axes[count%nrows*ncols+1].set_yticklabels([])
  872. # # Canny edge detection
  873. # canny_times[count%nrows] = time()
  874. # canny_img = canny(img_tile, sigma=7)
  875. # canny_times[count%nrows] = time() - canny_times[count%nrows]
  876. # axes[count%nrows*ncols+2].imshow(canny_img, cmap='gray')
  877. # axes[count%nrows*ncols+2].set_title('Canny')
  878. # axes[count%nrows*ncols+2].set_xticklabels([])
  879. # axes[count%nrows*ncols+2].set_yticklabels([])
  880. # canny_hull_img = convex_hull_image(canny_img)
  881. # axes[count%nrows*ncols+3].imshow(canny_hull_img, cmap='gray')
  882. # axes[count%nrows*ncols+3].set_title('Canny Hull')
  883. # axes[count%nrows*ncols+3].set_xticklabels([])
  884. # axes[count%nrows*ncols+3].set_yticklabels([])
  885. # Outlier removal and convex hull
  886. canny_times[count%nrows] = time()
  887. #X = np.column_stack(np.where(img_tile==255)) # (row, col) indices
  888. X = np.column_stack(np.where(binary_erosion(img_tile, selem=np.ones((5,5))))) # (row, col) indices
  889. model = HBOS(contamination=0.03, n_bins=100, tol=0.5) # 0.01s Very Good and doesn't eat edges
  890. model.fit(X)
  891. outlier_img = np.zeros_like(img_tile, dtype='uint8')
  892. outlier_img.flat[np.ravel_multi_index(X[np.invert(model.labels_.astype('bool'))].transpose(), outlier_img.shape)] = 255
  893. outlier_hull_img = convex_hull_image(outlier_img)
  894. canny_times[count%nrows] = time() - canny_times[count%nrows]
  895. axes[count%nrows*ncols+2].imshow(outlier_img, cmap='gray')
  896. axes[count%nrows*ncols+2].set_title('HBOS Outlier Removal')
  897. axes[count%nrows*ncols+2].set_xticklabels([])
  898. axes[count%nrows*ncols+2].set_yticklabels([])
  899. axes[count%nrows*ncols+3].imshow(outlier_hull_img, cmap='gray')
  900. axes[count%nrows*ncols+3].set_title('Outlier Hull')
  901. axes[count%nrows*ncols+3].set_xticklabels([])
  902. axes[count%nrows*ncols+3].set_yticklabels([])
  903. # Outlier removal and convex hull
  904. outlier_times[count%nrows] = time()
  905. #X = np.column_stack(np.where(img_tile==255)) # (row, col) indices
  906. #X = np.column_stack(np.where(binary_erosion(img_tile, selem=np.ones((3,3))))) # (row, col) indices
  907. #model = OCSVM(contamination=0.03) # > 10 min
  908. #model = COF(contamination=0.03) # insufficient memory
  909. #model = ABOD(contamination=0.03, n_neighbors = 5) # nonpython TypingError
  910. #detector_list = [LOF(n_neighbors=5), LOF(n_neighbors=10), LOF(n_neighbors=15),
  911. # LOF(n_neighbors=20), LOF(n_neighbors=25), LOF(n_neighbors=30),
  912. # LOF(n_neighbors=35), LOF(n_neighbors=40), LOF(n_neighbors=45),
  913. # LOF(n_neighbors=50)]
  914. #model = LSCP(detector_list, contamination=0.03) # Combined average LOFs
  915. #model = LOCI(contamination=0.03) # insufficient memory
  916. #model = MCD(contamination=0.03) # 34s Good but eats edges 38s 31s
  917. #model = PCA(contamination=0.03) # 0.02s Good but eats edges 0.04s 0.02s 0.04s
  918. #model = KNN(contamination=0.5, n_neighbors = 9, method = 'mean', n_jobs=-1) # 1s
  919. #model = IForest(contamination=0.03, n_jobs=-1, behaviour='new') # 3s Good
  920. #model = CBLOF(n_clusters=9, contamination=0.03,check_estimator=False, n_jobs=-1) # 1.5-5s OK
  921. #model = FeatureBagging(HBOS(contamination=0.03, n_bins=10, tol=0.5), contamination=0.03, n_jobs=-1) # 9-15s Very Good
  922. #model = HBOS(contamination=0.03, n_bins=100, tol=0.5) # 0.01s Very Good and doesn't eat edges 0.1s 0.01s 0.02s
  923. #model = LOF(n_neighbors=35, contamination=0.03, n_jobs=-1) # 1s Very Good
  924. #model.fit(X)
  925. #outlier_img = np.zeros_like(img_tile, dtype='uint8')
  926. #outlier_img.flat[np.ravel_multi_index(X[np.invert(model.labels_.astype('bool'))].transpose(), outlier_img.shape)] = 255
  927. outlier_hull_img = convex_hull_image(img_tile)
  928. outlier_times[count%nrows] = time() - outlier_times[count%nrows]
  929. axes[count%nrows*ncols+4].imshow(outlier_img, cmap='gray')
  930. axes[count%nrows*ncols+4].set_title('IForest Outlier Removal')
  931. axes[count%nrows*ncols+4].set_xticklabels([])
  932. axes[count%nrows*ncols+4].set_yticklabels([])
  933. axes[count%nrows*ncols+5].imshow(outlier_hull_img, cmap='gray')
  934. axes[count%nrows*ncols+5].set_title('Outlier Hull')
  935. axes[count%nrows*ncols+5].set_xticklabels([])
  936. axes[count%nrows*ncols+5].set_yticklabels([])
  937. if count%nrows == (nrows-1) or count == len(edge_labels)-1:
  938. print('Average HBOS Runtime: {} sec'.format(np.mean(canny_times)))
  939. print('Average IForest Runtime: {} sec'.format(np.mean(outlier_times)))
  940. display.display(fig)
  941. input("Press Enter to continue...")
  942. display.clear_output(wait=True)
  943. count += 1
  944. print('Done')
  945. # %%
  946. # dev-final
  947. import matplotlib.patches as patches
  948. from skimage.morphology import convex_hull_image
  949. from pyod.models.hbos import HBOS
  950. from skimage.morphology import binary_erosion
  951. currentDT = datetime.datetime.now(); print(str(currentDT)); startT = time()
  952. mask_img3 = np.copy(mask_img2)
  953. for region in regionprops(segments_slic, img):
  954. if region.label in labels:
  955. min_r, min_c, max_r, max_c = region.bbox
  956. img_tile = img[min_r:max_r, min_c:max_c]
  957. # Outlier removal and convex hull
  958. X = np.column_stack(np.where(binary_erosion(img_tile, selem=np.ones((5,5))))) # (row, col) indices
  959. model = HBOS(contamination=0.03, n_bins=100, tol=0.5) # 0.01s Very Good and doesn't eat edges
  960. model.fit(X)
  961. outlier_img = np.zeros_like(img_tile, dtype='uint8')
  962. outlier_img.flat[np.ravel_multi_index(X[np.invert(model.labels_.astype('bool'))].transpose(), outlier_img.shape)] = 255
  963. outlier_hull_img = convex_hull_image(outlier_img)
  964. mask_img3[min_r:max_r, min_c:max_c] = (outlier_hull_img * 255).astype('uint8')
  965. print("Outlier removal and convex hull: %s" % (time()-startT))
  966. # Convert numpy array into PIL image and save to local
  967. # Divide the WSI into regular slices to avoid PIL buffer overflow
  968. slice_size = 30000
  969. iters = np.uint8(np.ceil([height / slice_size, width / slice_size]))
  970. save_img = Image.new('L', (width, height))
  971. for row in range(iters[0]):
  972. for col in range(iters[1]):
  973. # Get start and end pixel location
  974. start_r, start_c = row * slice_size, col * slice_size
  975. end_r, end_c = min(height, start_r + slice_size), min(width, start_c + slice_size)
  976. # Paste the slice mask to image mask
  977. save_slice = Image.fromarray(mask_img3[start_r:end_r, start_c:end_c], 'L')
  978. save_img.paste(save_slice, (start_c, start_r))
  979. # Save masked image
  980. save_img.save('NA3777-02_AB_Eosin_SLICO_0.01_iter=5_40000_finalmask.png')
  981. print('Saved')
  982. print("Total: %s" % (time()-startT))
  983. # %%
  984. # Convex hull on boundaries
  985. from skimage.morphology import convex_hull_image
  986. currentDT = datetime.datetime.now(); print(str(currentDT)); startT = time()
  987. mask_img3 = np.copy(mask_img2)
  988. for region in regionprops(segments_slic, img):
  989. if region.label in labels:
  990. min_r, min_c, max_r, max_c = region.bbox
  991. img_tile = mask_img2[min_r:max_r, min_c:max_c]
  992. # Outlier removal and convex hull
  993. outlier_hull_img = convex_hull_image(img_tile)
  994. mask_img3[min_r:max_r, min_c:max_c] = (outlier_hull_img * 255).astype('uint8')
  995. print("Outlier removal and convex hull: %s" % (time()-startT))
  996. # Convert numpy array into PIL image and save to local
  997. # Divide the WSI into regular slices to avoid PIL buffer overflow
  998. slice_size = 30000
  999. iters = np.uint8(np.ceil([height / slice_size, width / slice_size]))
  1000. save_img = Image.new('L', (width, height))
  1001. for row in range(iters[0]):
  1002. for col in range(iters[1]):
  1003. # Get start and end pixel location
  1004. start_r, start_c = row * slice_size, col * slice_size
  1005. end_r, end_c = min(height, start_r + slice_size), min(width, start_c + slice_size)
  1006. # Paste the slice mask to image mask
  1007. save_slice = Image.fromarray(mask_img3[start_r:end_r, start_c:end_c], 'L')
  1008. save_img.paste(save_slice, (start_c, start_r))
  1009. # Save masked image
  1010. save_img.save('NA3777-02_AB_Eosin_SLICO_0.01_iter=5_60000_finalmask.png')
  1011. print('Saved')
  1012. print("Total: %s" % (time()-startT))
  1013. # %%
  1014. from skimage.measure import regionprops
  1015. from scipy.ndimage.morphology import binary_fill_holes
  1016. count = 0
  1017. for region in regionprops(segments_slic, img):
  1018. if count < 2000:
  1019. count+=1
  1020. continue
  1021. print(region.label)
  1022. print(region.bbox)
  1023. print(region.max_intensity)
  1024. print(region.mean_intensity)
  1025. print(region.min_intensity)
  1026. print(region.slice[0])
  1027. print(region.slice[1])
  1028. print(region.coords)
  1029. fig = plt.figure(figsize=(15,30))
  1030. ax = fig.add_subplot(1, 2, 1)
  1031. ax.imshow(img[region.bbox[0]:region.bbox[2], region.bbox[1]:region.bbox[3]], cmap='gray')
  1032. ax = fig.add_subplot(1, 2, 2)
  1033. ax.imshow(binary_fill_holes(img[region.bbox[0]:region.bbox[2], region.bbox[1]:region.bbox[3]]), cmap='gray', vmin=0, vmax=1)
  1034. break

3) Analysis - Tissue Separation.ipynb at commit d641f57, under GPL-3.0 · at the source

Overview

Authors: David Garcia1, Shivam Rajendra Rai Sharma1,2, Naomi Saito3, Laurel Beckett3, Louise Nicole C Sevilla1, La Rissa Vasquez1, Charles S DeCarli4, David Gutman5,6,7, Juan Vizcarra5,6,7, David G Coughlin8, Andrew F Teich9,10, Lorena Garcia3, Dan M Mungas4, Chen-Nee Chuah11, Brittany N Dugger1
  1. Department of Pathology and Laboratory Medicine, University of California, Davis, Sacramento, CA, United States
  2. Department of Computer Science, University of California, Davis, Davis, CA, United States
  3. Department of Public Health Sciences, University of California, Davis, Davis, CA, United States
  4. Department of Neurology, Alzheimer’s Disease Research Center, University of California, Davis, School of Medicine, Sacramento, CA, United States
  5. Department of Neurology, Emory University School of Medicine, Atlanta, GA, United States
  6. Department of Psychiatry, Emory University School of Medicine, Atlanta, GA, United States
  7. Department of Biomedical Informatics, Emory University School of Medicine, Atlanta, GA, United States
  8. Department of Neurology, University of California, San Diego, La Jolla, CA, United States
  9. Department of Neurology, Taub Institute for Research on Alzheimer’s Disease and Aging Brain, Columbia University Medical Center, New York, NY, United States
  10. Department of Pathology and Cell Biology, Columbia University Medical Center, New York, NY, United States
  11. Department of Electrical and Computer Engineering, University of California, Davis, Davis, CA, United States
Institutions: University of California, Davis (United States); Emory University (United States); University of California San Diego (United States); Columbia University Irving Medical Center (United States); Columbia University (United States)
Journal: Journal of neuropathology and experimental neurology, volume 85, issue 6, pages 537-550
Dates: published online 10 March 2026; in print June 2026
Type: Research article · Language: English
License: CC BY-NC
Identifiers: DOI 10.1093/jnen/nlaf152 · PMID 41806384 · PMCID PMC13197125 · OpenAlex W7134954698
Open access: hybrid, a free copy (OpenAlex)
Status: code verified
Categories: human (organism), Alzheimer's / dementia (population), clinical / translational (subfield)
Methods: Connectivity, Statistics, Machine learning
Keywords: Alzheimer’s disease, amyloid-beta, convolutional neural network, digital pathology, machine learning, neurodegeneration
MeSH: Alzheimer Disease*, Amyloid beta-Peptides*, Brain*, Machine Learning*, Plaque, Amyloid*, Temporal Lobe*, Aged, Aged, 80 and over, Cerebral Amyloid Angiopathy, Female, Humans, Male (* major topic)
Topic: Dementia and Cognitive Impairment Research (Psychiatry and Mental health, Medicine), according to OpenAlex
Funding: U.S. Department of Health & Human Services | NIH | National Institute on Aging (U.S. National Institute on Aging); NIH (R01AG062517, P30AG072972, P30AG062429, P50AG008702, P30AG066462, U24NS133949, U01AG061357-S1, R01AG052132, R01AG056519); Alzheimer’s Disease Research Center (U01AG024904); National Institute of Justice (2014-R2-CX-0012)
Citations: not cited yet (Europe PMC); 76 references in the paper

Abstract

Machine learning enables scalable quantification of neuropathology, offering deeper phenotyping of Alzheimer’s disease (AD). In this validation study, we quantified amyloid-beta (Aβ) deposits, evaluating multiple brain regions across institutions, and evaluated associations with clinical, demographic, and genetic factors in persons pathologically diagnosed with AD. All linear models were adjusted for sex, age of death, ethnicity, and center. We analyzed densities (#/mm2) of cored plaques, diffuse plaques, and cerebral amyloid angiopathy (CAA) in 273 individuals from 3 Alzheimer’s Disease Research Centers. Formalin-fixed paraffin-embedded sections of frontal, temporal, and parietal cortices were immunostained and digitized, generating 799 whole-slide images (WSIs). Following log transformation, mixed-effects modeling revealed the parietal cortex had the highest cored plaque densities (P < .001); the temporal cortex had the highest diffuse plaque (P < .001); CAA showed no regional differences. Wilcoxon rank-sum test, and covariates adjusted linear models showed ApoE ε4− status was associated with higher cored plaque densities in the temporal lobe (P = .04). ApoE ε4+ status was associated with diffuse plaques in the temporal lobe (P = .001), and CAA in the frontal lobe (P = .004). These findings provide further validation and provide exploratory associations advancing deeper phenotyping of AD.

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

Repository

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

ucdrubinet/BrainSec

License: GPL-3.0
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: d641f5702d35be8c1ef234c704875808b9b6c0d1, 9 March 2026
Languages: Jupyter (27), Python (26), C/C++ (20), C++ (4), Shell (1), CUDA (1)
Size: 105 files, 79 scripts
Software Heritage: not archived
Found in: “DATA AVAILABILITY”
Holds: README, license file, environment (docker/Dockerfile, docker/Dockerfile.old, install/requirements.txt), tests, 27 notebooks
Not found: CITATION.cff, continuous integration, documentation
Tools: NumPy (43 files), Matplotlib (28 files), Pillow (28 files), pandas (16 files), Keras (15 files), TensorFlow (15 files), OpenCV (12 files), scikit-image (11 files), SciPy (10 files), PyTorch (9 files), scikit-learn (3 files)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
81 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;
  • 79 scripts, each with its path and the digest of its content;
  • 1 match 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

The datasets supporting the findings of this study are publicly available on the DRYAD platform. The dataset comprises over 2 TB of WSI of Aβ-immunostained human brain tissue used in this study. Due to file size constraints, the complete WSI dataset is provided in two parts: Part 1 (DOI: 10.5061/dryad.7h44j107j) and Part 2 (DOI: 10.5061/dryad.wstqjq30t). Detailed instructions for downloading the WSIs and reproducing our analysis pipeline are available on DRYAD and in our GitHub repository: https://github.com/ucdrubinet/BrainSec.

Reproduced under the paper's license (CC BY-NC), 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, 15 authors, 6 keywords, 12 MeSH terms, 4 funders, 71 references.

Cite

This paper

Garcia, D., Rai Sharma, S. R., Saito, N., Beckett, L., Sevilla, L. N. C., Vasquez, L. R., DeCarli, C. S., Gutman, D., Vizcarra, J., Coughlin, D. G., Teich, A. F., Garcia, L., Mungas, D. M., Chuah, C.-N., & Dugger, B. N. (2026). Clinical and pathologic correlations of machine learning quantification of Aβ deposits across 3 brain regions of decedents with Alzheimer disease. Journal of neuropathology and experimental neurology, 85(6), 537-550. https://doi.org/10.1093/jnen/nlaf152

BibTeX

@article{garcia2026clinical,
author = {Garcia, David and Rai Sharma, Shivam Rajendra and Saito, Naomi and Beckett, Laurel and Sevilla, Louise Nicole C and Vasquez, La Rissa and DeCarli, Charles S and Gutman, David and Vizcarra, Juan and Coughlin, David G and Teich, Andrew F and Garcia, Lorena and Mungas, Dan M and Chuah, Chen-Nee and Dugger, Brittany N},
title = {{Clinical and pathologic correlations of machine learning quantification of Aβ deposits across 3 brain regions of decedents with Alzheimer disease}},
journal = {Journal of neuropathology and experimental neurology},
year = {2026},
month = jun,
volume = {85},
number = {6},
pages = {537--550},
publisher = {Oxford University Press},
issn = {0022-3069},
doi = {10.1093/jnen/nlaf152},
url = {https://doi.org/10.1093/jnen/nlaf152},
pmid = {41806384},
pmcid = {PMC13197125}
}

RIS

TY - JOUR
AU - Garcia, David
AU - Rai Sharma, Shivam Rajendra
AU - Saito, Naomi
AU - Beckett, Laurel
AU - Sevilla, Louise Nicole C
AU - Vasquez, La Rissa
AU - DeCarli, Charles S
AU - Gutman, David
AU - Vizcarra, Juan
AU - Coughlin, David G
AU - Teich, Andrew F
AU - Garcia, Lorena
AU - Mungas, Dan M
AU - Chuah, Chen-Nee
AU - Dugger, Brittany N
TI - Clinical and pathologic correlations of machine learning quantification of Aβ deposits across 3 brain regions of decedents with Alzheimer disease
T2 - Journal of neuropathology and experimental neurology
J2 - J Neuropathol Exp Neurol
PY - 2026
DA - 2026/06/01
VL - 85
IS - 6
SP - 537
EP - 550
SN - 0022-3069
PB - Oxford University Press
DO - 10.1093/jnen/nlaf152
UR - https://doi.org/10.1093/jnen/nlaf152
LA - en
ER -

CSL-JSON

{
"id": "10.1093/jnen/nlaf152",
"type": "article-journal",
"title": "Clinical and pathologic correlations of machine learning quantification of Aβ deposits across 3 brain regions of decedents with Alzheimer disease",
"container-title": "Journal of neuropathology and experimental neurology",
"author": [
{
"family": "Garcia",
"given": "David"
},
{
"family": "Rai Sharma",
"given": "Shivam Rajendra"
},
{
"family": "Saito",
"given": "Naomi"
},
{
"family": "Beckett",
"given": "Laurel"
},
{
"family": "Sevilla",
"given": "Louise Nicole C"
},
{
"family": "Vasquez",
"given": "La Rissa"
},
{
"family": "DeCarli",
"given": "Charles S"
},
{
"family": "Gutman",
"given": "David"
},
{
"family": "Vizcarra",
"given": "Juan"
},
{
"family": "Coughlin",
"given": "David G"
},
{
"family": "Teich",
"given": "Andrew F"
},
{
"family": "Garcia",
"given": "Lorena"
},
{
"family": "Mungas",
"given": "Dan M"
},
{
"family": "Chuah",
"given": "Chen-Nee"
},
{
"family": "Dugger",
"given": "Brittany N"
}
],
"container-title-short": "J Neuropathol Exp Neurol",
"volume": "85",
"issue": "6",
"page": "537-550",
"DOI": "10.1093/jnen/nlaf152",
"PMID": "41806384",
"PMCID": "PMC13197125",
"ISSN": "0022-3069",
"publisher": "Oxford University Press",
"URL": "https://doi.org/10.1093/jnen/nlaf152",
"language": "en",
"issued": {
"date-parts": [
[
2026,
6,
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/s41598-026-61605-4 [code]
Learning precise segmentation of neurofibrillary tangles from rapid manual point annotations.
Journal: Scientific reports
In common: OpenCV, scikit-image, Pillow, 5 other tools, Alzheimer's / dementia, 9 references
[2] doi:10.1364/boe.605322 [code]
Generalized plaque digitization framework for multi-dimensional mesoscopic images.
Journal: Biomedical optics express
In common: Keras, TensorFlow, OpenCV, 8 other tools, 1 reference
[3] doi:10.1038/s42003-026-10957-8 [code]
Brain defence by the extracellular matrix protein Cochlin.
Journal: Communications biology
In common: Keras, TensorFlow, OpenCV, 8 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: Keras, TensorFlow, OpenCV, 8 other tools
[5] doi:10.1126/sciadv.aed3650 [code]
Truthful visualizations for mass spectrometry imaging enable high-spatial-resolution interactive &lt;i&gt;m/z&lt;/i&gt; mapping and exploration.
Journal: Science advances
In common: Keras, TensorFlow, OpenCV, 8 other tools
[6] doi:10.1038/s41467-026-72057-9 [code]
Sex-specific behavioral feedback modulates sensorimotor processing and drives flexible social behavior.
Journal: Nature communications
In common: Keras, TensorFlow, OpenCV, 8 other tools
[7] doi:10.1002/epi.70296 [code]
Fully automated three-dimensional deep learning-based magnetic resonance imaging segmentation of brain cavities in epilepsy surgery.
Journal: Epilepsia
In common: Keras, TensorFlow, OpenCV, 7 other tools, clinical / translational
[8] 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: Keras, TensorFlow, OpenCV, 7 other tools, clinical / translational
[9] 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: Keras, TensorFlow, OpenCV, 7 other tools
[10] doi:10.1016/j.patter.2026.101538 [code]
A multi-modal foundation model for brain disease diagnosis and medical imaging.
Journal: Patterns (New York, N.Y.)
In common: TensorFlow, OpenCV, scikit-image, 7 other tools, clinical / translational

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.