OSCR

Retinoic acid drives cell fate specification, maturation and retinal regionality in human retinal organoids.

Code ↔ Paper

The paper beside its authors' code: matches between them have not been computed for this paper yet.

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 · 2,149 lines · 82 KB · CC0-1.0

  1. """
  2. Written by: Pete, 2023, (peterlionelnewman @ github)
  3. Helpful for students
  4. 1. GUI interface to:
  5. 2. search a folder for .czi files
  6. 3. export mips of various kinds for each czi channel
  7. 4. morphometrics about the images
  8. Go check out the Allen Institue of Cell Science package !!!
  9. This exists to help students with mip generation, and because of the bugs in the mosaic builder
  10. """
  11. # std
  12. import csv
  13. from functools import partial
  14. import itertools
  15. import os
  16. from multiprocessing import Pool, cpu_count
  17. import pickle
  18. import platform
  19. import re
  20. import time
  21. from tkinter.filedialog import askdirectory
  22. import tkinter as tk
  23. from tkinter import ttk
  24. import warnings
  25. import xml.etree.ElementTree as ET
  26. # 3rd
  27. import aicspylibczi
  28. import attr
  29. from cellpose import models
  30. import cv2
  31. from itertools import product
  32. from matplotlib import pyplot as plt
  33. import numpy as np
  34. from PIL import Image, ImageOps, ImageDraw, ImageFont, ImageChops, ImageTk
  35. import pyvista as pv
  36. from readlif.reader import LifFile
  37. from scipy.ndimage import zoom, binary_fill_holes, distance_transform_edt, binary_dilation, binary_erosion
  38. from skimage.measure import regionprops, label
  39. from skimage import morphology
  40. from torch import device
  41. import torch
  42. from tqdm import tqdm
  43. import webcolors
  44. global SO, DEVICE
  45. if torch.cuda.is_available():
  46. print("CUDA is available. PyTorch can use GPUs!")
  47. DEVICE = torch.device('cuda')
  48. elif torch.backends.mps.is_available():
  49. print("PyTorch using GPU on mac.")
  50. DEVICE = torch.device('mps')
  51. else:
  52. print("CPU ONLY. PyTorch can only use CPUs.")
  53. DEVICE = torch.device('cpu')
  54. @attr.s(auto_attribs=True, auto_detect=True)
  55. class ProcessOptions:
  56. # image path
  57. image_path: str = attr.ib(default='')
  58. save_path: str = attr.ib(default='')
  59. # save options
  60. save_mip_channels: bool = attr.ib(default=False)
  61. save_mip_panel: bool = attr.ib(default=True)
  62. save_mip_merge: bool = attr.ib(default=False)
  63. save_dye_overlaid: bool = attr.ib(default=True)
  64. save_colors: bool = attr.ib(default=True)
  65. use_multiprocessing: bool = attr.ib(default=False)
  66. # segmentation options
  67. segment_image: bool = attr.ib(default=False)
  68. mask_ch: int = attr.ib(default=0)
  69. cyto: bool = attr.ib(default=False)
  70. nuc: bool = attr.ib(default=False)
  71. channels_of_interest: list = attr.ib(default=[0])
  72. # same_for_all_images: bool = attr.ib(default=False)
  73. def __setattr__(self, name, value):
  74. super().__setattr__(name, value)
  75. if name == 'image_path':
  76. self.save_path = os.path.join(os.path.dirname(value), 'save')
  77. @attr.s(auto_attribs=True, auto_detect=True)
  78. class SegmentationOptions:
  79. mask_channel_var: int = attr.ib(default=0)
  80. cyto_var: bool = attr.ib(default=False)
  81. nuc_var: bool = attr.ib(default=True)
  82. segment_2D: bool = attr.ib(default=True)
  83. segment_3D: bool = attr.ib(default=False)
  84. same_for_all_images_var: bool = attr.ib(default=True)
  85. channel_vars: list = attr.ib(default=[0])
  86. mask_channel: int = attr.ib(default=0)
  87. channels_of_interest_vars: list = attr.ib(default=[])
  88. @attr.s(auto_attribs=True, auto_detect=True)
  89. class MaskProperties2D:
  90. centroid: np.array = attr.ib(default=np.array([[],[]]).T)
  91. id: np.array = attr.ib(default=np.array([]))
  92. area: np.array = attr.ib(default=np.array([]))
  93. perimeter: np.array = attr.ib(default=np.array([]))
  94. form_factor: np.array = attr.ib(default=np.array([]))
  95. minor_ax: np.array = attr.ib(default=np.array([]))
  96. major_ax: np.array = attr.ib(default=np.array([]))
  97. eccentricity: np.array = attr.ib(default=np.array([]))
  98. convexity: np.array = attr.ib(default=np.array([]))
  99. orientation: np.array = attr.ib(default=np.array([]))
  100. mask_sat: np.array = attr.ib(default=np.array([]))
  101. tf_sat: np.array = attr.ib(default=np.array([]))
  102. dist_from_edge: np.array = attr.ib(default=np.array([]))
  103. @attr.s(auto_attribs=True, auto_detect=True)
  104. class MaskProperties3D:
  105. id: np.array = attr.ib(default=np.array([]))
  106. volume: np.array = attr.ib(default=np.array([]))
  107. centroid: np.array = attr.ib(default=np.array([[], [], []]).T)
  108. surface_area: np.array = attr.ib(default=np.array([]))
  109. sphericity: np.array = attr.ib(default=np.array([]))
  110. major_ax: np.array = attr.ib(default=np.array([]))
  111. minor_ax: np.array = attr.ib(default=np.array([]))
  112. least_ax: np.array = attr.ib(default=np.array([]))
  113. eccentricity: np.array = attr.ib(default=np.array([]))
  114. convexity: np.array = attr.ib(default=np.array([]))
  115. orientation: np.array = attr.ib(default=np.array([]))
  116. mask_sat: np.array = attr.ib(default=np.array([]))
  117. tf_sat: np.array = attr.ib(default=np.array([[], []]).T)
  118. @attr.s(auto_attribs=True, auto_detect=True)
  119. class BioImage:
  120. """
  121. czi image class for lite image processing
  122. wrapper class around the ACIS wrapper class;
  123. around the czilib library
  124. """
  125. path: str = attr.ib(default='')
  126. imfile: aicspylibczi.CziFile = attr.ib(default=None)
  127. czi: tuple = attr.ib(default=())
  128. im: np.ndarray = attr.ib(default=[0., 0.])
  129. cell_mass: np.ndarray = attr.ib(default=[0., 0.])
  130. mosaic: np.ndarray = attr.ib(default=[0., 0.])
  131. num_channels: int = attr.ib(default=0)
  132. num_z_slices: int = attr.ib(default=0)
  133. height: int = attr.ib(default=0)
  134. width: int = attr.ib(default=0)
  135. num_timepoints: int = attr.ib(default=0)
  136. num_scenes: int = attr.ib(default=0)
  137. num_blocks: int = attr.ib(default=0)
  138. num_mosaics: int = attr.ib(default=0)
  139. metadata: str = attr.ib(default='')
  140. nmip: np.ndarray = attr.ib(default=[0., 0.])
  141. mip: np.ndarray = attr.ib(default=[0., 0.])
  142. colours: list = attr.ib(default=[])
  143. dyes: list = attr.ib(default=[])
  144. scale: np.array = attr.ib(default=[1., 1., 1.])
  145. bbox: np.array = attr.ib(default=np.array([0., 0.]))
  146. mask: np.ndarray = attr.ib(default=np.array([]))
  147. mask_ch: int = attr.ib(default=0)
  148. mask_props: MaskProperties2D = attr.ib(default=MaskProperties2D())
  149. big_image: bool = attr.ib(default=False)
  150. def load_tif(self):
  151. """
  152. load a tif file
  153. """
  154. # get the number of channels, z_slices and shape
  155. os.chdir(os.path.dirname(self.path))
  156. all_images = []
  157. self.num_channels = 0
  158. # search for tif files with same name
  159. for root, _, files in os.walk(os.path.dirname(self.path)):
  160. for file in files:
  161. if file.endswith('.tif'):
  162. if file[0] == '.':
  163. continue
  164. if os.path.basename(self.path)[:-4] in file\
  165. and not file == os.path.basename(self.path):
  166. all_images.append(file)
  167. self.num_channels += 1
  168. self.num_z_slices = 1
  169. if not all_images:
  170. print('No tif files found')
  171. return
  172. (self.width, self.height, _) = plt.imread(all_images[0]).shape
  173. self.metadata = ''
  174. self.scale = -1
  175. self.im = np.zeros((self.num_channels, self.num_z_slices, self.height, self.width))
  176. self.colours = []
  177. self.dyes = []
  178. # convert im to f64
  179. for c in range(self.num_channels):
  180. im = plt.imread(all_images[c]).astype(np.float64)
  181. # Get the R, G, and B channels
  182. r, g, b = im[:, :, 0], im[:, :, 1], im[:, :, 2]
  183. # r, g, b = im[:, :, 1], im[:, :, 2], im[:, :, 0]
  184. # Calculate the grayscale image
  185. self.im[c, 0, :, :] = 0.2989 * r + 0.5870 * g + 0.1140 * b
  186. color = [r.max(), g.max(), b.max()]
  187. color /= np.max(color)
  188. color = (color * 255).astype(int)
  189. self.colours.append(color)
  190. self.dyes.append(webcolors.rgb_to_name(color))
  191. self.scale = np.array([1., 1., 1.])
  192. def load_czi(self):
  193. """
  194. load a czi file
  195. """
  196. # get the image
  197. if self.path.endswith('.czi'):
  198. self.imfile = aicspylibczi.CziFile(self.path)
  199. else:
  200. # Set color to red
  201. print('\033[91m', end='')
  202. print(f'Failed to process: {self.path}')
  203. # Reset color to default
  204. print('\033[0m', end='')
  205. dims = self.imfile.get_dims_shape()[0]
  206. if dims['X'][1] == 0 and dims['Y'][1] == 0:
  207. return 'metadata_only'
  208. # get the number of channels, z_slices and shape
  209. self.num_channels = dims['C'][1]
  210. self.num_z_slices = dims['Z'][1]
  211. self.width = dims['X'][1]
  212. self.height = dims['Y'][1]
  213. self.metadata = self.imfile.meta
  214. # looks like the first 3 distance/values are camera props?
  215. self.scale = np.array([float(self.metadata.findall('.//Distance/Value')[i].text) for i in range(3, 6)])[::-1]
  216. self.scale = self.scale * 1e6
  217. # load each image
  218. if self.imfile.is_mosaic():
  219. # additional checks
  220. if not 'M' in dims:
  221. raise ValueError('Mosaic image found, but no M dimension found')
  222. # load in all the bounding boxes
  223. self.num_mosaics = dims['M'][1]
  224. self.bbox = np.zeros((self.num_mosaics, 4)).astype(int)
  225. for m in range(self.num_mosaics):
  226. _ = self.imfile.get_mosaic_tile_bounding_box(C=0, Z=0, M=m)
  227. self.bbox[m, :] = int(_.x), int(_.y), int(_.w), int(_.h)
  228. # check that w and h is the same
  229. if not np.all(self.bbox[:, 2] == self.bbox[0, 2]) or not np.all(self.bbox[:, 3] == self.bbox[0, 3]):
  230. raise ValueError('Mosaic bounding boxes are not the same size')
  231. # simplify the box
  232. self.bbox[:, 0] = self.bbox[:, 0] - np.min(self.bbox[:, 0])
  233. self.bbox[:, 1] = self.bbox[:, 1] - np.min(self.bbox[:, 1])
  234. # initialize the image
  235. self.width = np.max(self.bbox[:, 0]) + self.bbox[0, 2]
  236. self.height = np.max(self.bbox[:, 1]) + self.bbox[0, 3]
  237. # get the 'im' == (czyx)
  238. self.mosaic = np.moveaxis(self.imfile.read_image()[0],
  239. [self.imfile.dims.index('C'),
  240. self.imfile.dims.index('Z'),
  241. self.imfile.dims.index('Y'),
  242. self.imfile.dims.index('X'),
  243. self.imfile.dims.index('M')],
  244. [0, 1, 2, 3, 4])
  245. # index the last dimensions at 0
  246. for i in range(len(dims) - 5):
  247. self.mosaic = self.mosaic[..., 0]
  248. # look at the size of im and cry if really large
  249. if (self.mosaic.nbytes / 1e9) > 5:
  250. self.big_image = True
  251. print(f'WARNING: Image size is {self.mosaic.nbytes / 1e9} GB, this may cause memory issues')
  252. return
  253. # move the mosaics into im - some rounding bug? means I need to add 1 to the width and height
  254. self.im = np.zeros((self.num_channels, self.num_z_slices, self.height + 1, self.width + 1))
  255. print(f'loading mosaic')
  256. for m in range(self.num_mosaics):
  257. if m % (self.num_mosaics // 10) == 0:
  258. print(f'.', end='')
  259. self.im[:, :, self.bbox[m, 1]:self.bbox[m, 1] + self.bbox[m, 3], self.bbox[m, 0]:self.bbox[m, 0] + self.bbox[m, 2]] = self.mosaic[:, :, :, :, m]
  260. else:
  261. self.im = self.imfile.read_image()
  262. # get the 'im' == (czyx)
  263. self.im = np.moveaxis(self.im[0],
  264. [self.imfile.dims.index('C'),
  265. self.imfile.dims.index('Z'),
  266. self.imfile.dims.index('Y'),
  267. self.imfile.dims.index('X')],
  268. [0, 1, 2, 3])
  269. # index the last dimensions at 0
  270. for i in range(len(dims) - 4):
  271. self.im = self.im[..., 0]
  272. # convert im to f64
  273. self.im = self.im.astype(np.float64)
  274. # get the other info:
  275. if 'T' in dims: # timepoints
  276. # self.num_timepoints = dims['T'][1]
  277. # warnings.warn('this script throws away this info')
  278. pass
  279. if 'S' in dims: # scenes
  280. pass
  281. if 'B' in dims: # blocks
  282. pass
  283. if 'V' in dims:
  284. # The V-dimension ('view').
  285. pass
  286. if 'I' in dims:
  287. # The I-dimension ('illumination').
  288. pass
  289. if 'R' in dims:
  290. # The R-dimension ('rotation').
  291. pass
  292. if 'H' in dims:
  293. # The H-dimension ('phase').
  294. pass
  295. def load_lif(self):
  296. """
  297. load a lif file
  298. """
  299. # get the image
  300. if path.endswith('.lif'):
  301. self.imfile = LifFile(self.path)
  302. else:
  303. # Set color to red
  304. print('\033[91m', end='')
  305. print(f'Failed to process: {path}')
  306. # Reset color to default
  307. print('\033[0m', end='')
  308. return
  309. dims = self.imfile.image_list[0]
  310. if dims['dims'].x == 0 and dims['dims'].y == 0:
  311. return 'metadata_only'
  312. # get the number of channels, z_slices and shape
  313. self.num_channels = dims['channels']
  314. self.num_z_slices = dims['dims'].z
  315. self.width = dims['dims'].x
  316. self.height = dims['dims'].y
  317. # get scale
  318. self.scale = np.array(self.imfile.image_list[0]['scale'][0:3])
  319. if self.scale[2] == None:
  320. self.scale[2] = 0
  321. self.scale = self.scale[::-1] # xyz to zyx
  322. # load each image for mosaics
  323. if dims['dims'].m > 1:
  324. # Set color to red
  325. print('\033[91m', end='')
  326. print(f'Pete hasn''t implemened mosaics yet:')
  327. # Reset color to default
  328. print('\033[0m', end='')
  329. return
  330. # TBD additional checks
  331. if not 'M' in dims:
  332. raise ValueError('Mosaic image found, but no M dimension found')
  333. # TDB look at the size of im and cry if really large
  334. if (self.mosaic.nbytes / 1e9) > 5:
  335. self.big_image = True
  336. print(
  337. f'WARNING: Image size is {self.im.nbytes / 1e9} GB, this will cause memory issues')
  338. return
  339. # load each image for non-mosaics
  340. else:
  341. # create an empty array C-Z-Y-X
  342. self.im = np.zeros((self.num_channels, self.num_z_slices, self.height, self.width))
  343. for c in range(self.num_channels):
  344. for z in range(self.num_z_slices):
  345. self.im[c, z, :, :] = self.imfile.get_image(0).get_frame(z=z, t=0, c=c)
  346. # convert im to f64
  347. self.im = self.im.astype(np.float64)
  348. # get the other info:
  349. if dims['dims'].t > 1: # timepoints
  350. token = 'T'
  351. self.num_timepoints = dims['dims'].t
  352. warnings.warn(f'this script throws away this info: {token}')
  353. pass
  354. def extract_colors(self):
  355. if self.path.endswith('.lif'):
  356. self.metadata = xml_to_dict(self.imfile.xml_header)
  357. # not sure whats up with lif files, they dont seem to store laser or dye info?
  358. results = []
  359. def find_key(dictionary, target):
  360. for key, value in dictionary.items():
  361. if key == target:
  362. results.append(value)
  363. elif isinstance(value, dict):
  364. find_key(value, target)
  365. find_key(self.metadata, 'dye')
  366. find_key(self.metadata, 'color')
  367. find_key(self.metadata, 'laser')
  368. # assuming colors are in order blue to red
  369. self.colours = []
  370. self.dyes = []
  371. for c in range(self.num_channels):
  372. if c == 0:
  373. self.colours.append([0, 255, 255])
  374. self.dyes.append('cyan')
  375. elif c == 1:
  376. self.colours.append([255, 255, 0])
  377. self.dyes.append('yellow')
  378. elif c == 2:
  379. self.colours.append([255, 0, 0])
  380. self.dyes.append('red')
  381. elif c == 3:
  382. self.colours.append([255, 0, 255])
  383. self.dyes.append('magenta')
  384. elif c == 4:
  385. self.colours.append([255, 255, 255])
  386. self.dyes.append('white')
  387. elif self.path.endswith('.czi'):
  388. self.metadata = self.imfile.meta
  389. dyes = self.metadata.findall('.//DyeName')
  390. # looks like the first 3 distance/values are camera props?
  391. self.scale = np.array([float(self.metadata.findall('.//Distance/Value')[i].text) for i in range(3, 6)])[::-1]
  392. if len(dyes) != self.num_channels:
  393. warnings.warn('num channel != num dyes')
  394. return
  395. self.dyes = [None] * self.num_channels
  396. for c in range(self.num_channels):
  397. self.dyes[c] = dyes[c].text
  398. if 'DAPI' in self.dyes[c]\
  399. or 'dapi' in self.dyes[c] \
  400. or 'Hoechst 33342' in self.dyes[c] \
  401. or 'Hoechst 33258' in self.dyes[c]:
  402. self.colours.append([0, 255, 255])
  403. continue
  404. elif 'FITC' in self.dyes[c]:
  405. self.colours.append([255, 255, 0])
  406. continue
  407. elif 'Cy3' in self.dyes[c] or 'Rhodamine' in self.dyes[c]:
  408. self.colours.append([255, 0, 0])
  409. continue
  410. elif 'Cy5' in self.dyes[c]:
  411. self.colours.append([255, 0, 255])
  412. continue
  413. # extract all numbers from dye
  414. try:
  415. dye_nums = float(re.findall(r'\d+', self.dyes[c])[0])
  416. except:
  417. dye_nums = -1
  418. if dye_nums < 0:
  419. self.colours.append([255, 255, 255])
  420. elif dye_nums < 405:
  421. self.colours.append([0, 255, 255])
  422. elif dye_nums < 500:
  423. self.colours.append([255, 255, 0])
  424. elif dye_nums < 600:
  425. self.colours.append([255, 0, 0])
  426. elif dye_nums < 700:
  427. self.colours.append([255, 0, 255])
  428. else:
  429. self.colours.append([0, 0, 0])
  430. warnings.warn(f'no color found for {self.path}; channel {c}, {self.dyes[c]}')
  431. def project_mip(self, side_projections=False, z_scale=1):
  432. """
  433. make a maximum intensity projection
  434. """
  435. # check for z slices
  436. if self.num_z_slices == 1:
  437. print('\033[96m', end='')
  438. print(f'no z slices found in {self.path} returning')
  439. print('\033[0m', end='')
  440. self.mip = np.zeros((self.num_channels,
  441. self.im.shape[2],
  442. self.im.shape[3]))
  443. for c in range(self.num_channels):
  444. self.mip[c, :, :] = self.im[c, 0, :, :]
  445. return
  446. # initialize the mip
  447. if side_projections:
  448. if not self.big_image:
  449. self.mip = np.zeros((self.num_channels,
  450. self.im.shape[2] + self.im.shape[1] * z_scale + 1,
  451. self.im.shape[3] + self.im.shape[1] * z_scale + 1))
  452. for c in range(self.num_channels):
  453. # check for z slices
  454. self.mip[c,
  455. 0:self.im.shape[2],
  456. 0:self.im.shape[3]] = np.max(self.im[c, :, :, :], axis=0)
  457. projection_yz = np.max(self.im[c, :, :, :], axis=1)
  458. projection_yz = zoom(projection_yz, (z_scale, 1))
  459. self.mip[c,
  460. (self.im.shape[2] + 1):(self.im.shape[2] + 1 + self.im.shape[1] * z_scale),
  461. 0:self.im.shape[3]] = projection_yz
  462. projection_xz = np.max(self.im[c, :, :, :], axis=2).T
  463. projection_xz = zoom(projection_xz, (1, z_scale))
  464. self.mip[c,
  465. 0:self.im.shape[2],
  466. (self.im.shape[3] + 1):(self.im.shape[3] + 1 + self.im.shape[1] * z_scale)] = projection_xz
  467. return
  468. if self.big_image:
  469. self.mip = np.zeros((self.num_channels,
  470. self.height + self.num_z_slices * z_scale + 1,
  471. self.width + self.num_z_slices * z_scale + 1))
  472. for m in tqdm(range(self.num_mosaics), desc=f'Generating mip for *big image*:'):
  473. if m % (self.num_mosaics // 10) == 0:
  474. print(f'.', end='')
  475. for c in range(self.num_channels):
  476. self.mip[c,
  477. self.bbox[m, 1]:self.bbox[m, 1] + self.bbox[m, 3],
  478. self.bbox[m, 0]:self.bbox[m, 0] + self.bbox[m, 2]] = np.max(self.mosaic[c, :, :, :, m], axis=0)
  479. projection_yz = np.max(self.mosaic[c, :, :, :, m], axis=1)
  480. projection_yz = zoom(projection_yz, (z_scale, 1))
  481. self.mip[c,
  482. -(self.num_z_slices * z_scale + 1):-1,
  483. self.bbox[m, 0]:self.bbox[m, 0] + self.bbox[m, 2]] = projection_yz
  484. projection_xz = np.max(self.mosaic[c, :, :, :, m], axis=2).T
  485. projection_xz = zoom(projection_xz, (1, z_scale))
  486. self.mip[c,
  487. self.bbox[m, 1]:self.bbox[m, 1] + self.bbox[m, 3],
  488. -(self.num_z_slices * z_scale + 1):-1,] = projection_xz
  489. return
  490. else:
  491. if not self.big_image:
  492. self.mip = np.zeros((self.num_channels,
  493. self.im.shape[2],
  494. self.im.shape[3]))
  495. for c in range(self.num_channels):
  496. self.mip[c, :, :] = np.max(self.im[c, :, :, :], axis=0)
  497. return
  498. if self.big_image:
  499. self.mip = np.zeros((self.num_channels,
  500. self.height + 1,
  501. self.width + 1))
  502. for m in tqdm(range(self.num_mosaics), desc=f'Generating mip for *big image*:'):
  503. for c in range(self.num_channels):
  504. self.mip[c,
  505. self.bbox[m, 1]:self.bbox[m, 1] + self.bbox[m, 3],
  506. self.bbox[m, 0]:self.bbox[m, 0] + self.bbox[m, 2]] = np.max(self.mosaic[c, :, :, :, m], axis=0)
  507. def normalize(self, max=False, gamma=1):
  508. """
  509. modifies '.mip' normalizing the image
  510. """
  511. #if self.mip == [0, 0]:
  512. if np.all(self.mip == 0):
  513. print('No mip to normalize')
  514. return
  515. self.nmip = np.zeros((self.num_channels, self.mip.shape[2], self.mip.shape[1]))
  516. for c in range(self.num_channels):
  517. if max:
  518. mip = (self.mip[c, :, :] - np.nanmin(self.mip[c, :, :])) / \
  519. (np.nanmax(self.mip[c, :, :]) - np.nanmin(self.mip[c, :, :]))
  520. else:
  521. mip = self.mip[c, :, :] - np.nanmin(self.mip[c, :, :])
  522. if np.nansum(mip) != 0:
  523. mip_p = np.percentile(mip, 99.8)
  524. mip[mip > mip_p] = mip_p
  525. mip = mip/mip_p
  526. if gamma != 1:
  527. mip = mip ** gamma
  528. self.nmip[c, :, :] = mip * 255
  529. def save(self,
  530. save_path,
  531. save_mip_channels,
  532. save_mip_panel,
  533. save_mip_merge,
  534. save_dye_overlaid,
  535. save_colors):
  536. #if self.mip == [0, 0]:
  537. if np.all(self.mip == 0):
  538. print('No mip to save')
  539. return
  540. cwd = os.getcwd()
  541. try:
  542. os.chdir(os.path.dirname(self.path))
  543. except:
  544. os.mkdir(os.path.dirname(self.path))
  545. os.chdir(os.path.dirname(self.path))
  546. if self.big_image:
  547. optimize = False
  548. else:
  549. optimize = True
  550. file_stem = os.path.splitext(os.path.basename(self.path))[0]
  551. for c in range(self.num_channels):
  552. # PIL on each channel
  553. mip = Image.fromarray(self.mip[c, :, :])
  554. mip = mip.convert('L')
  555. base = np.ceil(self.num_channels ** 0.5).astype('int')
  556. if save_colors and len(self.colours[c]) > 0:
  557. mip = ImageOps.colorize(mip, (0, 0, 0), tuple(self.colours[c]))
  558. else:
  559. mip = mip.convert('RGB')
  560. if save_dye_overlaid:
  561. font_color = tuple(self.colours[c])
  562. font_size = self.height // 50
  563. draw = ImageDraw.Draw(mip)
  564. if platform == 'linux' or platform == 'linux2' or platform == 'darwin':
  565. text_overlay = [[]]
  566. text_overlay.append(c * ['\n'])
  567. text_overlay.append([self.dyes[c]])
  568. text_overlay = ''.join([item for sublist in text_overlay for item in sublist])
  569. draw.text((0, 0), text_overlay,
  570. font_color,
  571. ImageFont.truetype('Arial.ttf', size=font_size))
  572. elif platform == 'win32':
  573. draw.text((0, 0), self.dyes[c],
  574. font_color,
  575. ImageFont.truetype('arial.ttf', size=font_size))
  576. # create an image of all 'c' merged
  577. if save_mip_merge or \
  578. save_mip_panel and self.num_channels > 1 and self.num_channels < base ** 2:
  579. if c == 0:
  580. self.mip_merge = mip.copy()
  581. else:
  582. self.mip_merge = Image.merge('RGB', (
  583. ImageChops.add(self.mip_merge.getchannel('R'), mip.getchannel('R')),
  584. ImageChops.add(self.mip_merge.getchannel('G'), mip.getchannel('G')),
  585. ImageChops.add(self.mip_merge.getchannel('B'), mip.getchannel('B'))))
  586. if c == 0:
  587. self.mip_panel = np.zeros((mip.height * base, mip.width * base, 3))
  588. self.mip_panel[c % base * mip.height:(c % base + 1) * mip.height,
  589. c // base * mip.width:(c // base + 1) * mip.width,
  590. :] = np.array(mip).copy()
  591. # save
  592. if save_mip_channels:
  593. # mip.save(f'{file_stem}_ch{c}_.png', optimize=optimize)
  594. cv2.imwrite(f'{file_stem}_ch{c}_.png', cv2.cvtColor(np.array(mip), cv2.COLOR_RGB2BGR))
  595. # save merged images
  596. if save_mip_merge:
  597. # self.mip_merge.save(f'{file_stem}_merge.png', optimize=optimize)
  598. cv2.imwrite(f'{file_stem}_merge.png', cv2.cvtColor(np.array(self.mip_merge), cv2.COLOR_RGB2BGR))
  599. # save panel images
  600. if save_mip_panel and self.num_channels > 1:
  601. # add the merge to the panel if there is space
  602. if self.num_channels < base ** 2:
  603. self.mip_panel[self.num_channels % base * mip.height:(self.num_channels % base + 1) * mip.height,
  604. self.num_channels // base * mip.width:(self.num_channels // base + 1) * mip.width,
  605. :] \
  606. = np.array(self.mip_merge).copy()
  607. # remove black space
  608. self.mip_panel = self.mip_panel[~np.all(self.mip_panel == 0, axis=(1, 2))]
  609. # save mip panel cv2 (is much faster)
  610. cv2.imwrite(f'{file_stem}_panel.png',
  611. cv2.cvtColor(self.mip_panel.astype('uint8'), cv2.COLOR_RGB2BGR))
  612. print(f'converted {file_stem} and saved')
  613. os.chdir(cwd)
  614. def combine_big_image_masks(self, mask):
  615. mask = np.transpose(mask, (2, 1, 0))
  616. # define mosaic bounds
  617. window_size = 200
  618. overlap = 100
  619. mask = mask[::-1]
  620. yl = np.hstack([0, np.arange(window_size - overlap, mask.shape[1], window_size)])
  621. yu = np.hstack([window_size, np.arange(window_size - overlap + window_size, mask.shape[1], window_size), mask.shape[1]])
  622. xl = np.hstack([0, np.arange(window_size - overlap, mask.shape[2], window_size)])
  623. xu = np.hstack([window_size, np.arange(window_size - overlap + window_size, mask.shape[2], window_size), mask.shape[2]])
  624. # process a mosaic of the areas (speeds up processing significantly due to morphological ops)
  625. my = np.vstack([yl, yu]).T
  626. mx = np.vstack([xl, xu]).T
  627. mosaic = product(my, mx)
  628. if not np.any(mask):
  629. print(f'\033[94mError: {self.path} has no labels\033[0m')
  630. return
  631. combine_threshold = 0.7
  632. for nm, m in enumerate(mosaic):
  633. # get the mask for this mosaic
  634. vol = mask[:, m[1][0]:m[1][1], m[0][0]:m[0][1]]
  635. for nz in range(1, vol.shape[0]):
  636. slice_j = vol[nz, :, :]
  637. slice_i = vol[nz - 1, :, :]
  638. # # get id for slices
  639. # slice_j_labels = np.unique(slice_j)
  640. # slice_i_labels = np.unique(slice_i)
  641. check_labels_mask = slice_i - slice_j < 0
  642. # check_labels_j = slice_j_labels * check_labels_mask
  643. # check_labels_i = slice_i_labels * check_labels_mask
  644. check_labels_j = slice_j * check_labels_mask
  645. check_labels_i = slice_i * check_labels_mask
  646. uni_slice_j = np.unique(check_labels_j)
  647. uni_slice_i = np.unique(check_labels_i)
  648. uni_slice_j = uni_slice_j[uni_slice_j != 0]
  649. uni_slice_i = uni_slice_i[uni_slice_i != 0]
  650. # check that theres something in both slices
  651. if reduction(uni_slice_j, np.logical_or, 'any', None, None, None) \
  652. and reduction(uni_slice_i, np.logical_or, 'any', None, None, None):
  653. for j in uni_slice_j:
  654. for i in uni_slice_i:
  655. img_j = np.array(slice_j == j)
  656. img_i = np.array(slice_i == i)
  657. img_ij = np.logical_and(img_i, img_j)
  658. sum_img_j = img_j.sum()
  659. if sum_img_j > 2000: # remove the object if its got a really big 2D size note doesnt work for slice 0 objects
  660. slice_j[slice_j == j] = 0
  661. changed = True
  662. elif reduction(img_ij, np.logical_or, 'any', None, None, None):
  663. # if an object on adjacent layers has an intersection > 'combine_threshold' make them the same
  664. sum_img_i = img_i.sum()
  665. sum_img_ij = img_ij.sum()
  666. if sum_img_ij / sum_img_i > combine_threshold or sum_img_ij / sum_img_j > combine_threshold:
  667. slice_j[slice_j == j] = i
  668. changed = True
  669. if changed:
  670. mask[nz, m[1][0]:m[1][1], m[0][0]:m[0][1]] = slice_j
  671. # condense the mask
  672. mask = np.max(mask, axis=0)
  673. # convert back to xy
  674. mask = np.transpose(mask, (1, 0))
  675. return mask
  676. def process_2d_mask(self):
  677. print('processing 2D mask...')
  678. # process vars
  679. count = 0
  680. try:
  681. SO.channels_of_interest_vars[0].get()
  682. ch_of_interest = [i.get() for i in SO.channels_of_interest_vars]
  683. except:
  684. ch_of_interest = [i for i in SO.channels_of_interest_vars]
  685. self.mask_props = MaskProperties2D()
  686. if not len(self.mask.shape) == 2:
  687. print(f'\033[94mError: {self.path} isn''t 2D?\033[0m')
  688. return
  689. # rescale image to make yx of an pixel the same, then 1 µm, or if scale < 1 µm, scale.min()
  690. min_scale = np.min(np.hstack([self.scale[1::], 1]))
  691. zoom_by = self.scale[1::] / min_scale
  692. im = np.zeros(np.hstack([self.num_channels, (np.array(self.mip.shape[1::]) * zoom_by).astype('int')]))
  693. for c in range(self.num_channels):
  694. im[c, :, :] = zoom(np.squeeze(self.mip[c, :, :]), zoom_by, order=1)
  695. mask = zoom(self.mask, zoom_by, order=0)
  696. # define mosaic bounds
  697. window_size = 200
  698. overlap = 100
  699. yl = np.hstack([0, np.arange(window_size - overlap, im.shape[1], window_size)])
  700. yu = np.hstack([window_size, np.arange(window_size - overlap + window_size, im.shape[1], window_size), im.shape[1]])
  701. xl = np.hstack([0, np.arange(window_size - overlap, im.shape[2], window_size)])
  702. xu = np.hstack([window_size, np.arange(window_size - overlap + window_size, im.shape[2], window_size), im.shape[2]])
  703. # process a mosaic of the areas (speeds up processing significantly due to morphological ops)
  704. my = np.vstack([yl, yu]).T
  705. mx = np.vstack([xl, xu]).T
  706. mosaic = product(my, mx)
  707. if not np.any(mask):
  708. print(f'\033[94mError: {self.path} has no labels\033[0m')
  709. return
  710. mask_id_processed = np.array([0])
  711. for nm, m in enumerate(mosaic):
  712. # get the mask for this mosaic
  713. area = mask[m[1][0]:m[1][1], m[0][0]:m[0][1]]
  714. area_im = im[:, m[1][0]:m[1][1], m[0][0]:m[0][1]]
  715. # if there are no labels in this area, skip
  716. if not np.any(area):
  717. continue
  718. # remove mask ids that have already been processed
  719. area_id = np.unique(area)
  720. # if area_id is in mask_id_processed, remove it
  721. for i in np.intersect1d(area_id, mask_id_processed):
  722. area[area == i] = 0
  723. area_id = np.unique(area)
  724. area_id = area_id[area_id != 0]
  725. rp = regionprops(area)
  726. for rn, r in enumerate(rp):
  727. # filter out large & small objects
  728. if r.area < 10 or r.area > 10_000:
  729. continue
  730. # save properties
  731. self.mask_props.id = np.hstack((self.mask_props.id, area_id[rn]))
  732. self.mask_props.area = np.hstack((self.mask_props.area, r.area))
  733. self.mask_props.centroid = np.vstack((self.mask_props.centroid, np.array(r.centroid) + np.array([m[1][0], m[0][0]])))
  734. self.mask_props.perimeter = np.hstack((self.mask_props.perimeter, r.perimeter))
  735. self.mask_props.form_factor = np.hstack((self.mask_props.form_factor, r.perimeter ** 2 / r.area))
  736. self.mask_props.minor_ax = np.hstack((self.mask_props.minor_ax, r.minor_axis_length))
  737. self.mask_props.major_ax = np.hstack((self.mask_props.major_ax, r.major_axis_length))
  738. self.mask_props.eccentricity = np.hstack((self.mask_props.eccentricity, r.eccentricity))
  739. self.mask_props.convexity = np.hstack((self.mask_props.convexity, r.convex_area / r.area))
  740. self.mask_props.orientation = np.hstack((self.mask_props.orientation, r.orientation))
  741. # saturation intensity in mask of the other channels
  742. tf_sat = np.hstack([
  743. np.mean(area_im[c][r.coords[:, 0], r.coords[:, 1]])
  744. for c in range(self.num_channels)
  745. ])
  746. if count == 0:
  747. self.mask_props.tf_sat = tf_sat
  748. else:
  749. self.mask_props.tf_sat = np.vstack((self.mask_props.tf_sat, tf_sat))
  750. count += 1
  751. # add all area_id to mask_id_processed
  752. mask_id_processed = np.hstack([area_id, mask_id_processed])
  753. # calculate distance of each cell in mask from edge of cell mass
  754. print('Calculating distances of cells from edge of cell mass...')
  755. # cellpose_model = models.Cellpose(gpu=True, model_type='nuclei')
  756. # nuc_mask = cellpose_model.eval(
  757. # self.nmip[nuclear_channel].astype(np.uint8),
  758. # diameter=diam,
  759. # flow_threshold=flow_threshold,
  760. # cellprob_threshold=cellprob_threshold)[0]
  761. # # region props centroids
  762. # properties = regionprops(label(nuc_mask))
  763. # centroids = np.array([prop.centroid for prop in properties]).astype(np.int64)
  764. #
  765. # threshold = 1
  766. # cell_mass = (nuc_mask > 0 * (threshold + 1)).astype(np.float64)
  767. # tri_upper_ind = np.triu_indices(centroids.shape[0], k=1)
  768. # centroid_combinations = centroids[np.array(tri_upper_ind)]
  769. #
  770. # # thin out centroid combinations
  771. # dist = centroid_combinations[0, :, :] - centroid_combinations[1, :, :]
  772. # dist = (dist[:, 0] ** 2 + dist[:, 1] ** 2) ** 0.5
  773. # dist = dist < cell_mass.shape[1] / 2
  774. # centroid_combinations = centroid_combinations[:, dist, :]
  775. #
  776. # z = np.zeros_like(cell_mass).astype(np.float64)
  777. # for c1, c2 in tqdm(centroid_combinations.transpose((1, 0, 2)), total=centroid_combinations.shape[1]):
  778. # temp = z.copy()
  779. # cell_mass += cv2.line(temp, c1[::-1], c2[::-1], (1,), 5).astype(np.float64) / centroid_combinations.shape[1]
  780. #
  781. combo = self.nmip.astype(np.float64)
  782. combo = combo.sum(axis=0)
  783. combo = combo / combo.max() * 255
  784. combo = combo.astype(np.uint8)
  785. # bw = 255 - cv2.adaptiveThreshold(combo, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 5)
  786. combo = cv2.medianBlur(combo, 33)
  787. th, _ = cv2.threshold(combo, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
  788. bw = combo > (th * 0.33)
  789. bw = np.pad(bw, 2, 'constant', constant_values=255)
  790. bw[0:2,0:2] = 0
  791. bw[0:2,-3:-1] = 0
  792. bw[-3:-1,0:2] = 0
  793. bw[-3:-1,-3:-1] = 0
  794. bw = bw > 0
  795. bw = binary_fill_holes(bw)
  796. bw = bw[2:-2, 2:-2]
  797. diam = np.mean(self.mask_props.area) + 3 * np.std(self.mask_props.area)
  798. bw = morphology.remove_small_objects(bw, int(3 * (diam/2)**2))
  799. # calculate distance of each mask from the edge of the cell mass
  800. self.mask_props.dist_from_edge = np.zeros(self.mask_props.id.shape[0])
  801. dist_transform = distance_transform_edt(bw)
  802. for n, i in enumerate(self.mask_props.id):
  803. obj = (mask == i)
  804. if obj.sum() == 0:
  805. continue
  806. mean_distance = np.nanmean(dist_transform[obj])
  807. if np.isfinite(mean_distance):
  808. self.mask_props.dist_from_edge[n] = mean_distance
  809. else:
  810. self.mask_props.dist_from_edge[n] = -1
  811. if np.isnan(self.mask_props.dist_from_edge).any():
  812. print('tell pete, there shouldn''t be nans in the dist from edge')
  813. fig, ax = plt.subplots(1, 3, figsize=(15, 5))
  814. ax[0].imshow(im.transpose((1, 2, 0)))
  815. ax[1].imshow(bw * 255)
  816. cell_mass_im = im[0, :, :] / 2 + bw.astype(np.uint8) * 255 / 2
  817. ax[2].imshow(cell_mass_im)
  818. ax[2].scatter(self.mask_props.centroid[:, 1],
  819. self.mask_props.centroid[:, 0],
  820. c='r',
  821. s=self.mask_props.dist_from_edge / 50)
  822. self.cell_mass = cell_mass_im
  823. def process_3d_mask(self):
  824. print('processing 3D mask...')
  825. # process vars
  826. count = 0
  827. try:
  828. SO.channels_of_interest_vars[0].get()
  829. ch_of_interest = [i.get() for i in SO.channels_of_interest_vars]
  830. except:
  831. ch_of_interest = [i for i in SO.channels_of_interest_vars]
  832. self.mask_props = MaskProperties3D()
  833. if not len(self.mask.shape) == 3:
  834. print(f'\033[94mError: {self.path} isn''t 3D?\033[0m')
  835. return
  836. # rescale image to make xyz of a voxel the same, then 1 µm, or if scale < 1 µm, scale.min()
  837. min_scale = np.min(np.hstack([self.scale[1::], 1]))
  838. zoom_by = self.scale / min_scale
  839. im = np.zeros(np.hstack([self.num_channels, np.round((np.array(self.mask.shape) * zoom_by)).astype('int')]))
  840. for c in range(self.num_channels):
  841. im[c, :, :, :] = zoom(self.im[c, :, :, :], zoom_by, order=1)
  842. mask = zoom(self.mask, zoom_by, order=0)
  843. # define mosaic bounds
  844. window_size = 200
  845. overlap = 100
  846. zl = np.hstack([0, np.arange(window_size - overlap, im.shape[0], window_size)])
  847. zu = np.hstack([window_size, np.arange(window_size - overlap + window_size, im.shape[0], window_size), im.shape[0]])
  848. yl = np.hstack([0, np.arange(window_size - overlap, im.shape[1], window_size)])
  849. yu = np.hstack([window_size, np.arange(window_size - overlap + window_size, im.shape[1], window_size), im.shape[1]])
  850. xl = np.hstack([0, np.arange(window_size - overlap, im.shape[2], window_size)])
  851. xu = np.hstack([window_size, np.arange(window_size - overlap + window_size, im.shape[2], window_size), im.shape[2]])
  852. # process a mosaic of the areas (speeds up processing significantly due to morphological ops)
  853. mz = np.vstack([zl, zu]).T
  854. my = np.vstack([yl, yu]).T
  855. mx = np.vstack([xl, xu]).T
  856. mosaic = product(mz, my, mx)
  857. if not np.any(mask):
  858. print(f'\033[94mError: {self.path} has no labels\033[0m')
  859. return
  860. mask_id_processed = np.array([0])
  861. combine_threshold = 0.7
  862. for nm, m in enumerate(mosaic):
  863. # get the mask for this mosaic
  864. vol = mask[:, m[1][0]:m[1][1], m[0][0]:m[0][1]]
  865. for nz in range(1, vol.shape[0]):
  866. slice_j = vol[nz, :, :]
  867. slice_i = vol[nz - 1, :, :]
  868. # get id for slices
  869. slice_j_labels = np.unique(slice_j)
  870. slice_i_labels = np.unique(slice_i)
  871. check_labels_mask = slice_i - slice_j < 0
  872. check_labels_j = slice_j_labels * check_labels_mask
  873. check_labels_i = slice_i_labels * check_labels_mask
  874. uni_slice_j = np.unique(check_labels_j)
  875. uni_slice_i = np.unique(check_labels_i)
  876. uni_slice_j = uni_slice_j[uni_slice_j != 0]
  877. uni_slice_i = uni_slice_i[uni_slice_i != 0]
  878. # check that theres something in both slices
  879. if reduction(uni_slice_j, np.logical_or, 'any', None, None, None) \
  880. and reduction(uni_slice_i, np.logical_or, 'any', None, None, None):
  881. for j in uni_slice_j:
  882. for i in uni_slice_i:
  883. img_j = np.array(slice_j == j)
  884. img_i = np.array(slice_i == i)
  885. img_ij = np.logical_and(img_i, img_j)
  886. sum_img_j = img_j.sum()
  887. if sum_img_j > 2000: # remove the object if its got a really big 2D size note doesnt work for slice 0 objects
  888. slice_j[slice_j == j] = 0
  889. changed = True
  890. elif reduction(img_ij, np.logical_or, 'any', None, None, None):
  891. # if an object on adjacent layers has an intersection > 'combine_threshold' make them the same
  892. sum_img_i = img_i.sum()
  893. sum_img_ij = img_ij.sum()
  894. if sum_img_ij / sum_img_i > combine_threshold or sum_img_ij / sum_img_j > combine_threshold:
  895. slice_j[slice_j == j] = i
  896. changed = True
  897. if changed:
  898. mask[nz, m[1][0]:m[1][1], m[0][0]:m[0][1]] = slice_j
  899. for nm, m in enumerate(mosaic):
  900. # get the mask for this mosaic
  901. vol = mask[m[2][0]:m[2][1], m[1][0]:m[1][1], m[0][0]:m[0][1]]
  902. vol_im = im[:, m[2][0]:m[2][1], m[1][0]:m[1][1], m[0][0]:m[0][1]]
  903. # if there are no labels in this area, skip
  904. if not np.any(vol):
  905. continue
  906. # remove mask ids that have already been processed
  907. area_id = np.unique(vol)
  908. # if area_id is in mask_id_processed, remove it
  909. for i in np.intersect1d(area_id, mask_id_processed):
  910. vol[vol == i] = 0
  911. area_id = np.unique(vol)
  912. area_id = area_id[area_id != 0]
  913. rp = regionprops(vol)
  914. for rn, r in enumerate(rp):
  915. # filter out large & small objects
  916. if r.area < 10 or r.area > 10_000:
  917. continue
  918. # save properties
  919. self.mask_props.id = np.hstack((self.mask_props.id, area_id[rn]))
  920. self.mask_props.volume = np.hstack((self.mask_props.volume, r.area))
  921. self.mask_props.centroid = np.vstack((self.mask_props.centroid, np.array(r.centroid) + np.array([m[2][0], m[1][0], m[0][0]])))
  922. self.mask_props.bounding_box = np.vstack((self.mask_props.bounding_box, r.bbox))
  923. self.mask_props.form_factor = np.hstack((self.mask_props.form_factor, r.perimeter ** 2 / r.area))
  924. self.mask_props.eccentricity = np.hstack((self.mask_props.eccentricity, r.eccentricity))
  925. self.mask_props.orientation = np.hstack((self.mask_props.orientation, r.orientation))
  926. # saturation intensity in mask of the other channels
  927. tf_sat = np.hstack([
  928. np.mean(vol_im[c][r.coords[:, 0], r.coords[:, 1], r.coords[:, 2]])
  929. for c in range(self.num_channels)
  930. ])
  931. if count == 0:
  932. self.mask_props.tf_sat = tf_sat
  933. else:
  934. self.mask_props.tf_sat = np.vstack((self.mask_props.tf_sat, tf_sat))
  935. count += 1
  936. # add all area_id to mask_id_processed
  937. mask_id_processed = np.hstack([area_id, mask_id_processed])
  938. def plot_2d_mask(self):
  939. try:
  940. SO.channels_of_interest_vars[0].get()
  941. ch_of_interest = [i.get() for i in SO.channels_of_interest_vars]
  942. except:
  943. ch_of_interest = [i for i in SO.channels_of_interest_vars]
  944. # with num_channel axes
  945. fig, ax = plt.subplots(2,
  946. np.max([sum(ch_of_interest) + 1, 3]),
  947. figsize=(4 * np.max([3, sum(ch_of_interest) + 1]), 8))
  948. for a in ax.flatten():
  949. a.set_xticks([])
  950. a.set_yticks([])
  951. a.set_facecolor('black')
  952. fig.set_facecolor('black')
  953. x = self.mask_props.centroid[:,1]
  954. y = self.mask_props.centroid[:,0]
  955. if len(y) == 0:
  956. return
  957. colors = ['cyan', 'yellow', 'red', 'magenta', 'green']
  958. for c in range(self.mask_props.tf_sat.shape[1]):
  959. ax[0, c+1].scatter(x, y,
  960. s=(self.mask_props.tf_sat[:, c] / self.mask_props.tf_sat[:, c].max()),
  961. c=colors[c],
  962. alpha=0.99)
  963. # ax[1, 0].imshow(self.mip[self.mask_ch, :, :], cmap='hot')
  964. # ax[1, 0].invert_yaxis()
  965. # ax[1, 1].imshow(self.mask, cmap='hot')
  966. # ax[1, 1].invert_yaxis()
  967. # plt.tight_layout()
  968. # plt.show()
  969. def plot_3d_mask(self):
  970. try:
  971. SO.channels_of_interest_vars[0].get()
  972. ch_of_interest = [i.get() for i in SO.channels_of_interest_vars]
  973. except:
  974. ch_of_interest = [i for i in SO.channels_of_interest_vars]
  975. # Create PyVista plotter
  976. plotter = pv.Plotter(shape=(1, sum(ch_of_interest) + 1),
  977. window_size=(300 * (sum(ch_of_interest) + 1), 300))
  978. plotter.set_background('black')
  979. # Get centroids and properties
  980. x = self.mask_props.centroid[:, 0]
  981. y = self.mask_props.centroid[:, 1]
  982. z = self.mask_props.centroid[:, 2]
  983. centroids = np.column_stack((x, y, z))
  984. # Create glyph source
  985. glyph_source = pv.PolyData(centroids)
  986. # Add mask saturation glyphs
  987. if self.mask_props.mask_sat.size > 0:
  988. mask_sat_scale = (self.mask_props.mask_sat / self.mask_props.mask_sat.max())
  989. else:
  990. mask_sat_scale = np.array([])
  991. plotter.subplot(0, 0)
  992. plotter.add_glyph(glyph_source, scale=mask_sat_scale, color='white', render_points_as_spheres=True)
  993. plotter.add_title("Mask Saturation", color='white')
  994. # Add glyphs for other channels
  995. colors = ['cyan', 'yellow', 'red', 'magenta', 'green']
  996. for c in range(self.mask_props.tf_sat.shape[1]):
  997. if self.mask_props.tf_sat[:, c].size > 0:
  998. tf_sat_scale = (self.mask_props.tf_sat[:, c] / self.mask_props.tf_sat[:, c].max())
  999. else:
  1000. tf_sat_scale = np.array([])
  1001. plotter.subplot(0, c + 1)
  1002. plotter.add_glyph(glyph_source, scale=tf_sat_scale, color=colors[c], render_points_as_spheres=True)
  1003. plotter.add_title(f"Ch of Int Sat {c}", color='white')
  1004. # Show the plot
  1005. plotter.show()
  1006. def save_2d_mask(self, po):
  1007. print(f'saving segmentation results{self.path}')
  1008. if not os.path.exists(po.save_path):
  1009. os.mkdir(po.save_path)
  1010. os.chdir(po.save_path)
  1011. file_stem = os.path.basename(self.path)
  1012. if self.big_image:
  1013. optimize = False
  1014. else:
  1015. optimize = True
  1016. # save mask image
  1017. mask = self.mask.astype('uint16')
  1018. # gray to rgb
  1019. mask_color = np.zeros((mask.shape[0], mask.shape[1], 3)).astype('uint8')
  1020. # loop through mask for each object and make it a random color
  1021. for c in range(3):
  1022. mask_c = mask
  1023. for i in range(1, mask.max()):
  1024. mask_c[mask_c == i] = 30 + np.random.randint(0, 255 - 30)
  1025. mask_color[:, :, c] = mask_c
  1026. # save images
  1027. try: #if cellmass is empty this will avoid crash
  1028. cv2.imwrite(f'{file_stem}_masks.png', cv2.cvtColor(np.array(mask), cv2.COLOR_GRAY2BGR))
  1029. cv2.imwrite(f'{file_stem}_masks_color.png', mask_color)
  1030. cv2.imwrite(f'{file_stem}_cell_mass.png', self.cell_mass)
  1031. except:
  1032. pass
  1033. # save data as csv
  1034. with open(file_stem + '_2Dresults.csv', 'w') as f:
  1035. # column names
  1036. var = 'id,' \
  1037. 'area,' \
  1038. 'centroid_x,' \
  1039. 'centroid_y,' \
  1040. 'perimeter,' \
  1041. 'form_factor,' \
  1042. 'minor_ax,' \
  1043. 'major_ax,' \
  1044. 'eccentricity,' \
  1045. 'convexity,' \
  1046. 'orientation,' \
  1047. 'dist_from_edge'
  1048. tf = ''
  1049. try:
  1050. SO.channels_of_interest_vars[0].get()
  1051. for i in range(sum([i.get() for i in SO.channels_of_interest_vars])):
  1052. tf += f',ch_of_int_sat{i}'
  1053. except:
  1054. for i in range(sum([i for i in SO.channels_of_interest_vars])):
  1055. tf += f',ch_of_int_sat{i}'
  1056. var = var + tf + '\n'
  1057. # write column names
  1058. f.write(var)
  1059. for i in range(self.mask_props.id.size):
  1060. # write row
  1061. tf = ''
  1062. #skip if tf_sat is empty w/ print error
  1063. if len(self.mask_props.tf_sat.shape) < 2 or self.mask_props.tf_sat.shape[1] == 0:
  1064. print("tf_sat is not 2D or the second dimension is empty.")
  1065. return
  1066. for c in range(self.mask_props.tf_sat.shape[1]):
  1067. tf += f',{self.mask_props.tf_sat[i, c]}'
  1068. f.write(f'{self.mask_props.id[i]},'
  1069. f'{self.mask_props.area[i]},'
  1070. f'{self.mask_props.centroid[i, 0]},'
  1071. f'{self.mask_props.centroid[i, 1]},'
  1072. f'{self.mask_props.perimeter[i]},'
  1073. f'{self.mask_props.form_factor[i]},'
  1074. f'{self.mask_props.minor_ax[i]},'
  1075. f'{self.mask_props.major_ax[i]},'
  1076. f'{self.mask_props.eccentricity[i]},'
  1077. f'{self.mask_props.convexity[i]},'
  1078. f'{self.mask_props.orientation[i]},'
  1079. f'{self.mask_props.dist_from_edge[i]}' # <- shouldn't be a comma here
  1080. + tf + '\n')
  1081. def save_3d_mask(self, po):
  1082. print(f'saving segmentation results{self.path}')
  1083. if not os.path.exists(po.save_path):
  1084. os.mkdir(po.save_path)
  1085. os.chdir(po.save_path)
  1086. file_stem = os.path.basename(self.path)
  1087. if self.big_image:
  1088. optimize = False
  1089. else:
  1090. optimize = True
  1091. # save mask image
  1092. mask = Image.fromarray(np.max(self.mask.astype('uint16'), axis=0))
  1093. mask.save(f'{file_stem}_masks.png', optimize=optimize)
  1094. # save data as csv
  1095. with open(file_stem + '_3Dresults.csv', 'w') as f:
  1096. # column names
  1097. var = 'id,' \
  1098. 'volume,' \
  1099. 'centroid_x,' \
  1100. 'centroid_y,' \
  1101. 'centroid_z,' \
  1102. 'surface_area,' \
  1103. 'sphericity,' \
  1104. 'minor_ax,' \
  1105. 'major_ax,' \
  1106. 'least_ax,' \
  1107. 'eccentricity,' \
  1108. 'convexity,' \
  1109. 'orientation,' \
  1110. 'mask_sat,' \
  1111. 'tf_sat'
  1112. tf = ''
  1113. try:
  1114. SO.channels_of_interest_vars[0].get()
  1115. for i in range(sum([i.get() for i in SO.channels_of_interest_vars])):
  1116. tf += f',ch_of_int_sat{i}'
  1117. except:
  1118. for i in range(sum([i for i in SO.channels_of_interest_vars])):
  1119. tf += f',ch_of_int_sat{i}'
  1120. var = var + tf + '\n'
  1121. # write column names
  1122. f.write(var)
  1123. for i in range(self.mask_props.id.size):
  1124. # write row
  1125. tf = ''
  1126. for c in range(self.mask_props.tf_sat.shape[1]):
  1127. tf += f',{self.mask_props.tf_sat[i, c]}'
  1128. f.write(f'{self.mask_props.id[i]},'
  1129. f'{self.mask_props.volume[i]},'
  1130. f'{self.mask_props.centroid[i, 0]},'
  1131. f'{self.mask_props.centroid[i, 1]},'
  1132. f'{self.mask_props.centroid[i, 2]},'
  1133. f'{self.mask_props.surface_area[i]},'
  1134. f'{self.mask_props.sphericity[i]},'
  1135. f'{self.mask_props.minor_ax[i]},'
  1136. f'{self.mask_props.major_ax[i]},'
  1137. f'{self.mask_props.least_ax[i]},'
  1138. f'{self.mask_props.eccentricity[i]},'
  1139. f'{self.mask_props.convexity[i]},'
  1140. f'{self.mask_props.orientation[i]},'
  1141. f'{self.mask_props.mask_sat[i]}' + tf + '\n')
  1142. # @attr.s(auto_attribs=True, auto_detect=True)
  1143. class MainGUI(tk.Tk):
  1144. """
  1145. Create a tkinter window to select options:
  1146. - select a folder to search for czi files
  1147. - select check boxes for:
  1148. - side projections (w/ side project scaling)
  1149. - gamma correction
  1150. - save mip channels
  1151. - save mip panel
  1152. - save mip merge
  1153. - save dye overlaid
  1154. - save colors
  1155. - use multiprocessing
  1156. - select a folder to save the images to
  1157. """
  1158. def __init__(self):
  1159. # create a tkinter window
  1160. self.root = tk.Tk()
  1161. self.root.title(' ')
  1162. # initalize variables
  1163. self.search_path = tk.StringVar()
  1164. # self.search_path.set('.set/search/path')
  1165. self.search_path.set('/Users/peternewman/Desktop/im/ben')
  1166. self.save_path = tk.StringVar()
  1167. # self.save_path.set('.set/save/path')
  1168. self.save_path.set('/Users/peternewman/Desktop/im/ben')
  1169. # establish a scale factor
  1170. s = 1.0
  1171. w = int(420 * s)
  1172. h = int(250 * s)
  1173. # size the window
  1174. self.root.minsize(w, h)
  1175. self.root.maxsize(w, h)
  1176. self.root.geometry(f'{w}x{h}')
  1177. # add a title
  1178. tk.Label(self.root, text='czi2png', font=('Arial', 25))\
  1179. .place(relx=10 / w, rely=10 / h) # width=135/w, height=36/h)
  1180. # # add search and save path buttons
  1181. tk.Button(self.root, text='Search Path', command=self.specify_search_path,
  1182. width=8, height=1).place(relx=10 / w, rely=53 / h)
  1183. tk.Button(self.root, text='Save Path', command=self.specify_save_path,
  1184. width=8, height=1).place(relx=10 / w, rely=83 / h)
  1185. self.search_path_label = tk.Label(self.root, textvariable=self.search_path, font=('Arial', 12), fg='gray')\
  1186. .place(relx=124 / w, rely=58 / h) # width=135/w, height=36/h)
  1187. self.save_path_label = tk.Label(self.root, textvariable=self.save_path, font=('Arial', 12), fg='gray') \
  1188. .place(relx=124 / w, rely=88 / h) # width=135/w, height=36/h)
  1189. # add check box for file type czi, lif or tif
  1190. self.file_type = tk.StringVar()
  1191. self.file_type.set('czi')
  1192. tk.Radiobutton(self.root, text='czi', variable=self.file_type,
  1193. value='czi') \
  1194. .place(relx=10 / w, rely=140 / h)
  1195. tk.Radiobutton(self.root, text='lif', variable=self.file_type,
  1196. value='lif') \
  1197. .place(relx=80 / w, rely=140 / h)
  1198. tk.Radiobutton(self.root, text='tif', variable=self.file_type,
  1199. value='tif') \
  1200. .place(relx=150 / w, rely=140 / h)
  1201. # # add a button to run the program
  1202. tk.Button(self.root, text='Convert 2 png!', command=self.main,
  1203. width=20, height=2).place(relx=10 / w, rely=180 / h)
  1204. # # add checkbox to save mip channels, mip panel, mip merge, dye overlaid, colors, multiprocessing
  1205. tk.Label(self.root, text='Options', font=('Arial', 12)) \
  1206. .place(relx=260 / w, rely=22 / h) # width=135/w, height=36/h)
  1207. self.save_mip_channels = tk.BooleanVar()
  1208. self.save_mip_channels.set(False)
  1209. tk.Checkbutton(self.root, text='save mip channels', command = self.display_input,
  1210. variable=self.save_mip_channels, onvalue=1, offvalue=0)\
  1211. .place(relx=260 / w, rely=53 / h)
  1212. self.save_mip_panel = tk.BooleanVar()
  1213. self.save_mip_panel.set(False)
  1214. tk.Checkbutton(self.root, text='save mip panel', command = self.display_input,
  1215. variable=self.save_mip_panel, onvalue=1, offvalue=0) \
  1216. .place(relx=260 / w, rely=80 / h)
  1217. self.save_mip_merge = tk.BooleanVar()
  1218. self.save_mip_merge.set(True)
  1219. tk.Checkbutton(self.root, text='save mip merge', command = self.display_input,
  1220. variable=self.save_mip_merge, onvalue=1, offvalue=0) \
  1221. .place(relx=260 / w, rely=107 / h)
  1222. self.save_dye_overlaid = tk.BooleanVar()
  1223. self.save_dye_overlaid.set(False)
  1224. tk.Checkbutton(self.root, text='save dye overlaid', command = self.display_input,
  1225. variable=self.save_dye_overlaid, onvalue=1, offvalue=0) \
  1226. .place(relx=260 / w, rely=133 / h)
  1227. self.save_colors = tk.BooleanVar()
  1228. self.save_colors.set(True)
  1229. tk.Checkbutton(self.root, text='save colors', command = self.display_input,
  1230. variable=self.save_colors, onvalue=1, offvalue=0) \
  1231. .place(relx=260 / w, rely=160 / h)
  1232. self.use_multiprocessing = tk.BooleanVar()
  1233. self.use_multiprocessing.set(False)
  1234. tk.Checkbutton(self.root, text='use multiprocessing', command = self.display_input,
  1235. variable=self.use_multiprocessing, onvalue=1, offvalue=0) \
  1236. .place(relx=260 / w, rely=186 / h)
  1237. self.segment_image = tk.BooleanVar()
  1238. self.segment_image.set(True)
  1239. tk.Checkbutton(self.root, text='segment image',
  1240. variable=self.segment_image, onvalue=1, offvalue=0) \
  1241. .place(relx=260 / w, rely=213 / h)
  1242. self.root.mainloop()
  1243. # debugging
  1244. def display_input(self):
  1245. print(f'search path: {self.search_path}')
  1246. print(f'save path: {self.save_path}')
  1247. print(f'save mip channels: {self.save_mip_channels.get()}')
  1248. print(f'save mip panel: {self.save_mip_panel.get()}')
  1249. print(f'save mip merge: {self.save_mip_merge.get()}')
  1250. print(f'save dye overlaid: {self.save_dye_overlaid.get()}')
  1251. print(f'save colors: {self.save_colors.get()}')
  1252. print(f'use multiprocessing: {self.use_multiprocessing.get()}')
  1253. def specify_search_path(self,):
  1254. self.search_path.set(tk.filedialog.askdirectory(parent=self.root, initialdir='/',
  1255. title='Please select a directory'))
  1256. def specify_save_path(self,):
  1257. self.save_path.set(tk.filedialog.askdirectory(parent=self.root, initialdir='/',
  1258. title='Please select a directory'))
  1259. def main(self):
  1260. try:
  1261. if self.search_path.get() == '.set/search/path' or self.save_path.get() == '.set/save/path':
  1262. tk.messagebox.showerror('Python Error', 'please select Search and Save paths * unassigned *')
  1263. return
  1264. if not os.path.isdir(self.search_path.get()) or not os.path.isdir(self.save_path.get()):
  1265. tk.messagebox.showerror('Python Error', 'Search and Save paths not directories')
  1266. return
  1267. # check that at least one save option is selected
  1268. if self.save_mip_channels.get() + self.save_mip_panel.get() + self.save_mip_merge.get() < 1:
  1269. tk.messagebox.showerror('Python Error', 'select at least channels, panel or merge image to save')
  1270. return
  1271. except:
  1272. print('incomplete error checks')
  1273. # set path of image files
  1274. search_path = self.search_path.get()
  1275. # get images
  1276. image_files = find_all_images_in_path(search_path, filetype=self.file_type.get())
  1277. all_process_files = [None] * len(image_files)
  1278. for n, image_file in enumerate(image_files):
  1279. po = ProcessOptions()
  1280. po.save_path = self.save_path.get()
  1281. po.save_mip_channels = self.save_mip_channels.get()
  1282. po.save_mip_panel = self.save_mip_panel.get()
  1283. po.save_mip_merge = self.save_mip_merge.get()
  1284. po.save_dye_overlaid = self.save_dye_overlaid.get()
  1285. po.save_colors = self.save_colors.get()
  1286. po.use_multiprocessing = self.use_multiprocessing.get()
  1287. po.segment_image = self.segment_image.get()
  1288. all_process_files[n] = po
  1289. all_process_files[n].image_path = image_file
  1290. self.root.destroy()
  1291. im_file_sizes = [os.path.getsize(image_file) for image_file in image_files]
  1292. cumulative_file_size = np.cumsum(im_file_sizes)
  1293. process_file(all_process_files[0])
  1294. # pop all_process_files[0] off the list
  1295. all_process_files.pop(0)
  1296. # time the function
  1297. start_time = time.perf_counter()
  1298. # run the processing routine on the czi images
  1299. if all_process_files[0].use_multiprocessing:
  1300. print(f'running multi processed: {image_files}')
  1301. with multiprocessing.Pool(multiprocessing.cpu_count()) as p:
  1302. p.map(process_file, all_process_files)
  1303. else:
  1304. print(f'running single threaded on: {image_files}')
  1305. for n, process_param in enumerate(all_process_files):
  1306. process_file(process_param)
  1307. # calculate time remaining use the files size to estimate processing time
  1308. time_remaining = (time.perf_counter() - start_time) / \
  1309. cumulative_file_size[n] * \
  1310. (cumulative_file_size[-1] -
  1311. cumulative_file_size[n])
  1312. # time taken
  1313. # per byte
  1314. # * bytes remaining
  1315. print(f'Time remaining: {time_remaining:0.2f} seconds')
  1316. # print the run time
  1317. print(
  1318. f'\nTime elapsed: {time.perf_counter() - start_time:0.2f} seconds')
  1319. # display a message box to indicate that the processing is complete
  1320. tk.messagebox.showinfo('Python Info', 'Images saved as png')
  1321. # close the window
  1322. self.root.destroy()
  1323. @attr.s(auto_attribs=True, auto_detect=True)
  1324. class SegmentationGUI:
  1325. def __init__(self, parent, image):
  1326. self.root = parent
  1327. self.root.title('Segmentation Options')
  1328. # the reason we are here
  1329. global SO
  1330. SO = SegmentationOptions()
  1331. window_size = (1500, 550)
  1332. self.root.geometry(f"{window_size[0]}x{window_size[1]}")
  1333. image.mip = np.array(image.mip)
  1334. self.image = image
  1335. self.mask_channel_var = tk.IntVar()
  1336. self.mask_channel_var.set(0)
  1337. SO.mask_channel_var = self.mask_channel_var.get()
  1338. self.cyto_var = tk.BooleanVar()
  1339. self.cyto_var.set(False)
  1340. SO.cyto_var = self.cyto_var.get()
  1341. self.nuc_var = tk.BooleanVar()
  1342. self.nuc_var.set(True)
  1343. SO.nuc_var = self.nuc_var.get()
  1344. self.segment_2D = tk.BooleanVar()
  1345. self.segment_2D.set(True)
  1346. SO.segment_2D = self.segment_2D.get()
  1347. self.segment_3D = tk.BooleanVar()
  1348. self.segment_3D.set(False)
  1349. SO.segment_3D = self.segment_3D.get()
  1350. self.same_for_all_images_var = tk.BooleanVar()
  1351. self.same_for_all_images_var.set(False)
  1352. SO.same_for_all_images_var = self.same_for_all_images_var.get()
  1353. tk.Label(self.root, text="Mask channel:").place(relx=0.01, rely=0.05)
  1354. ttk.Combobox(self.root, values=list(range(self.image.num_channels)), textvariable=self.mask_channel_var, width=int(0.005 * window_size[0])).place(relx=0.1, rely=0.05)
  1355. self.cyto_checkbox = tk.Checkbutton(self.root, text="cyto", variable=self.cyto_var, command=self.toggle_nuc)
  1356. self.cyto_checkbox.place(relx=0.01, rely=0.1)
  1357. self.nuc_checkbox = tk.Checkbutton(self.root, text="nuc", variable=self.nuc_var, command=self.toggle_cyto)
  1358. self.nuc_checkbox.place(relx=0.1, rely=0.1)
  1359. tk.Label(self.root, text="Channels of interest:").place(relx=0.01, rely=0.15)
  1360. self.channel_vars = []
  1361. col = 0.1
  1362. for i in range(self.image.num_channels):
  1363. channel_var = tk.BooleanVar()
  1364. channel_var.set(True)
  1365. self.channel_vars.append(channel_var)
  1366. tk.Label(self.root, text=f"{i+1}:").place(relx=col, rely=0.15)
  1367. col += 0.015
  1368. tk.Checkbutton(self.root, text="", variable=channel_var).place(relx=col, rely=0.15)
  1369. col += 0.015
  1370. SO.channel_vars = self.channel_vars
  1371. tk.Checkbutton(self.root, text="2D", variable=self.segment_2D).place(relx=0.01, rely=0.20)
  1372. tk.Checkbutton(self.root, text="3D", variable=self.segment_3D).place(relx=0.07, rely=0.20)
  1373. # tk.Checkbutton(self.root, text="Same for all images?", variable=self.same_for_all_images_var).place(relx=0.01, rely=0.40)
  1374. tk.Button(self.root, text="Segment and Process", command=self.segment_and_process).place(relx=0.01, rely=0.30, relwidth=0.2, relheight=0.1)
  1375. tk.Label(self.root, text="Channels: (top-to-bottom, left-to-right) 0 -> n").place(relx=0.33, rely=0.01)
  1376. mip_panel = ImageTk.PhotoImage(Image.fromarray(cv2.resize(np.array(self.image.mip_panel).astype('uint8'), (int(0.35 * window_size[0]), int(0.9 * window_size[1])))))
  1377. self.mip_panel_label = tk.Label(self.root, image=mip_panel)
  1378. self.mip_panel_label.place(relx=0.25, rely=0.05)
  1379. tk.Label(self.root, text="Mask channel").place(relx=0.78, rely=0.01)
  1380. mip = ImageTk.PhotoImage(Image.fromarray(cv2.resize(self.image.mip[0, :, :].astype('uint8'), (int(0.35 * window_size[0]), int(0.9 * window_size[1])))))
  1381. self.mask_label = tk.Label(self.root, image=mip)
  1382. self.mask_label.place(relx=0.64, rely=0.05)
  1383. self.mask_channel_var.trace('w', self.mask_channel_changed)
  1384. self.root.mainloop()
  1385. def wait_and_get_values(self):
  1386. self.root.wait_window()
  1387. return {
  1388. 'mask_channel': self.mask_channel_var.get(),
  1389. 'cyto': self.cyto_var.get(),
  1390. 'nuc': self.nuc_var.get(),
  1391. 'channels_of_interest': [var.get() for var in self.channels_of_interest_vars],
  1392. 'same_for_all_images': self.same_for_all_images_var.get()
  1393. }
  1394. def toggle_cyto(self):
  1395. if self.nuc_var.get():
  1396. self.cyto_var.set(False)
  1397. else:
  1398. self.cyto_var.set(True)
  1399. SO.cyto_var = self.cyto_var.get()
  1400. def toggle_nuc(self):
  1401. if self.cyto_var.get():
  1402. self.nuc_var.set(False)
  1403. else:
  1404. self.nuc_var.set(True)
  1405. SO.nuc_var = self.nuc_var.get()
  1406. def mask_channel_changed(self, *args):
  1407. mask_channel = self.mask_channel_var.get()
  1408. mask = self.image.mip[mask_channel, :, :]
  1409. mask_resized = cv2.resize(mask, (500, 500))
  1410. self.mask_image = ImageTk.PhotoImage(Image.fromarray(mask_resized))
  1411. self.mask_label.config(image=self.mask_image)
  1412. self.mask_label.image = self.mask_image
  1413. SO.mask_channel = mask_channel
  1414. def segment_and_process(self):
  1415. self.channels_of_interest_vars = [var for var in self.channel_vars if var.get()]
  1416. SO.channels_of_interest_vars = self.channels_of_interest_vars
  1417. self.root.destroy()
  1418. def reduction(obj, ufunc, method, axis, dtype, out):
  1419. return ufunc.reduce(obj, axis, dtype, out)
  1420. def xml_to_dict(xml_str):
  1421. def recursive_dictify(element):
  1422. children = list(element)
  1423. if not children:
  1424. return element.text
  1425. return {child.tag: recursive_dictify(child) for child in children}
  1426. root = ET.fromstring(xml_str)
  1427. return recursive_dictify(root)
  1428. def ends_with_ch0X_tif(filename):
  1429. # The regular expression pattern to match the filename
  1430. pattern = r'_ch0[0-9]\.tif$'
  1431. # Use the search function to check if the pattern matches the end of the filename
  1432. match = re.search(pattern, filename)
  1433. # If match is not None, that means the pattern was found in the filename
  1434. return match is not None
  1435. def find_all_images_in_path(search_path, filetype=''):
  1436. """
  1437. search a given directory for all images czi, lif, tif
  1438. """
  1439. os.chdir(search_path)
  1440. image_files = []
  1441. for root, _, files in os.walk('.'):
  1442. for file in files:
  1443. if file[0] == '.':
  1444. continue # since mac folder info
  1445. if file.endswith(filetype):
  1446. if ends_with_ch0X_tif(file):
  1447. continue
  1448. image_files.append(os.path.join(root, file))
  1449. if not image_files:
  1450. raise ValueError('no images found in search path')
  1451. return
  1452. return image_files
  1453. def process_mosaic(m, mosaic, cellpose_model, diam, flow_threshold, cellprob_threshold, channels):
  1454. print(f'Processing mosaic {m}')
  1455. cellpose_out = cellpose_model.eval(
  1456. np.max(mosaic[:, :, :, :, m], axis=1).astype('uint8'),
  1457. diameter=diam,
  1458. flow_threshold=flow_threshold,
  1459. cellprob_threshold=cellprob_threshold,
  1460. channels=channels,
  1461. tile=True
  1462. )
  1463. # Return the necessary data for further processing
  1464. return m, cellpose_out[0], cellpose_out[1][0]
  1465. def process_file(po):
  1466. """
  1467. processes a file
  1468. """
  1469. global SO,\
  1470. model_choice,\
  1471. diam, \
  1472. flow_threshold, \
  1473. cellprob_threshold, \
  1474. alter_mask_channel, \
  1475. mask_ch, \
  1476. image
  1477. print('\033[93m', end='')
  1478. print(f'\nprocessing path: {po.image_path}\n'
  1479. f'with save path: {po.save_path}; '
  1480. f'and save options: \n'
  1481. f'channels: {po.save_mip_channels}'
  1482. f', panel: {po.save_mip_panel}'
  1483. f', merge: {po.save_mip_merge}'
  1484. f', dye overlay: {po.save_dye_overlaid}'
  1485. f', colors: {po.save_colors}')
  1486. print('\033[0m', end='')
  1487. image = BioImage() # initiate
  1488. image.path = po.image_path
  1489. if po.image_path.endswith('.czi'):
  1490. if image.load_czi() == 'metadata_only':
  1491. print(f'Image contained metadata only, skipping: {po.image_path}')
  1492. return
  1493. elif po.image_path.endswith('.lif'):
  1494. image.load_lif()
  1495. elif po.image_path.endswith('.tif'):
  1496. image.load_tif()
  1497. if np.all(image.im == 0) and not image.big_image:
  1498. print(f"image empty: {image.path}, skipping")
  1499. return
  1500. #if image.im == [0, 0] and not image.big_image:
  1501. # print(f"image empty: {image.path}, skipping")
  1502. # return
  1503. # loading and saving the image
  1504. image.extract_colors()
  1505. image.project_mip(side_projections=True, z_scale=3)
  1506. image.normalize(gamma=1)
  1507. image.save(po.save_path,
  1508. po.save_mip_channels,
  1509. po.save_mip_panel,
  1510. po.save_mip_merge,
  1511. po.save_dye_overlaid,
  1512. po.save_colors)
  1513. if not po.segment_image:
  1514. return
  1515. # check if variable 'so' exists
  1516. if 'SO' not in globals():
  1517. # create segmentation GUI
  1518. SegmentationGUI(tk.Tk(), image)
  1519. # if this is still empty but you're segmenting, then assume you want every channel
  1520. if not SO.channels_of_interest_vars:
  1521. SO.channels_of_interest_vars = [True] * image.num_channels
  1522. # if 'model_choice' is not defined or is None, then set its value based on SO.nuc_var
  1523. using_pretrained_model = False
  1524. if not globals().get('model_choice'):
  1525. if SO.nuc_var:
  1526. model_choice = "nuclei" # @param ["cyto", "nuclei", "cyto2", "tissuenet", "livecell"]
  1527. else:
  1528. model_choice = "cyto"
  1529. else:
  1530. using_pretrained_model = True
  1531. image.mask_ch = SO.mask_channel_var
  1532. if using_pretrained_model:
  1533. cellpose_model = models.CellposeModel(gpu=True, pretrained_model=model_choice)
  1534. else:
  1535. cellpose_model = models.Cellpose(gpu=True, model_type=model_choice)
  1536. cellpose_model.device = DEVICE
  1537. if globals().get('alter_mask_channel'):
  1538. if len(mask_ch) == 1:
  1539. channels = [image.mask_ch, image.mask_ch]
  1540. elif len(mask_ch) == 2:
  1541. channels = mask_ch
  1542. image.mask_ch = mask_ch[0]
  1543. else:
  1544. # image.mask_ch was fine before you started messing with this.
  1545. channels = [[image.mask_ch, image.mask_ch]]
  1546. if not ('diam' in globals()):
  1547. diam = None # for autofit
  1548. # diam = 7 * image.scale[1] # ~7 µm diameter for hiPSCs
  1549. if not ('flow_threshold' in globals()):
  1550. flow_threshold = 0.3 # @{type:"slider", min:0.1, max:1.1, step:0.1}
  1551. if not ('cellprob_threshold' in globals()):
  1552. cellprob_threshold = -1.5 # @{type:"slider", min:-6, max:6, step:1}
  1553. if not ('stitch_threshold' in globals()):
  1554. stitch_threshold = 0.0 # @{type:"slider", min:-6, max:6, step:1}
  1555. if SO.segment_2D or image.im.shape[1] == 1:
  1556. if not image.big_image:
  1557. print('segmenting 2D image / mip ...')
  1558. # reorganize to a rgb
  1559. cellpose_image = np.zeros((3, image.mip.shape[2], image.mip.shape[1]))
  1560. ''' number on the RHS is the channel number from the image name (i.e. X in _ch0X.tif) '''
  1561. cellpose_image[0, :, :] = image.mip[2, :, :] # R
  1562. cellpose_image[1, :, :] = image.mip[1, :, :] # G
  1563. cellpose_image[2, :, :] = image.mip[0, :, :] # B
  1564. # note that image.mask_ch is not transformed with the cellpose bindings
  1565. # cellpose_image = cellpose_image.transpose(2, 1, 0).astype('uint8')
  1566. # plt.imshow(cellpose_image)
  1567. # print(f'diam:{diam}, ft:{flow_threshold}, cpt:{cellprob_threshold}, ch:{channels}')
  1568. # plt.savefig('out.png')
  1569. cellpose_out = cellpose_model.eval(cellpose_image.astype('uint8'),
  1570. diameter=diam,
  1571. flow_threshold=flow_threshold,
  1572. cellprob_threshold=cellprob_threshold,
  1573. channels=channels)
  1574. image.mask = cellpose_out[0]
  1575. image.flow = cellpose_out[1]
  1576. try:
  1577. image.diam = cellpose_out[3]
  1578. except:
  1579. print('no diameter returned')
  1580. image.diam = -1
  1581. if image.big_image:
  1582. image.mask = np.zeros((image.mosaic.shape[2], image.mosaic.shape[3], image.num_mosaics))
  1583. image.flow = np.zeros((image.mosaic.shape[2], image.mosaic.shape[3], 3, image.num_mosaics))
  1584. image.masks_combined = np.zeros(
  1585. (image.bbox[:, 1].max() + image.bbox[:, 3].max(),
  1586. image.bbox[:, 0].max() + image.bbox[:, 2].max(),
  1587. 4), dtype=image.mask.dtype)
  1588. """number on the RHS is the channel number from the image name
  1589. (i.e. X in _ch0X.tif), in order RGB"""
  1590. image.mosaic = image.mosaic[[2, 1, 0], :, :, :, :]
  1591. # # parallel -> untested, something like this might work
  1592. # # Create a partial function with the fixed parameters
  1593. # partial_process_mosaic = partial(
  1594. # process_mosaic,
  1595. # mosaic=image.mosaic,
  1596. # cellpose_model=cellpose_model,
  1597. # diam=diam,
  1598. # flow_threshold=flow_threshold,
  1599. # cellprob_threshold=cellprob_threshold,
  1600. # channels=channels
  1601. # )
  1602. #
  1603. # # Use the partial function with the multiprocessing Pool
  1604. # print('Initializing a parallel process...')
  1605. # pool = Pool(processes=cpu_count())
  1606. # results = list(tqdm(pool.imap(partial_process_mosaic, range(image.num_mosaics)), desc="Segmenting a big mosaic"))
  1607. #
  1608. # # multiprocessing for big images is necessary
  1609. # for result in tqdm(results, desc="Processing results"):
  1610. # m, mask, flow = result
  1611. #
  1612. # # Determine the # rows and columns in the mosaic (assumes square im)
  1613. # row = image.bbox[m, 0] // image.bbox[1, 0]
  1614. # col = image.bbox[m, 1] // image.bbox[1, 0]
  1615. #
  1616. # image.mask[:, :, m] = mask
  1617. # image.flow[:, :, :, m] = flow
  1618. #
  1619. # # Add the mask to the appropriate z-stack
  1620. # image.masks_combined[image.bbox[m, 1]:image.bbox[m, 1] + image.bbox[m, 3],
  1621. # image.bbox[m, 0]:image.bbox[m, 0] + image.bbox[m, 2],
  1622. # (row % 2) * 2 + (col % 2)
  1623. # ] = image.mask[:, :, m]
  1624. # serial
  1625. max_in_masks = 0
  1626. for m in tqdm(range(image.num_mosaics), desc="Segmenting a big mosaic"):
  1627. row = image.bbox[m, 0] // image.bbox[1, 0]
  1628. col = image.bbox[m, 1] // image.bbox[1, 0]
  1629. # # here for debugging / testing. check to see if the image is being segmented correctly
  1630. # if row > 2 or col > 2:
  1631. # continue
  1632. cellpose_image = np.max(image.mosaic[:, :, :, :, m], axis=1)
  1633. cellpose_out = cellpose_model.eval(
  1634. cellpose_image.astype('uint8'),
  1635. diameter=diam,
  1636. flow_threshold=flow_threshold,
  1637. cellprob_threshold=cellprob_threshold,
  1638. channels=channels,
  1639. tile=True)
  1640. image.mask[:, :, m] = cellpose_out[0] + max_in_masks * (cellpose_out[0] > 0)
  1641. image.flow[:, :, :, m] = cellpose_out[1][0]
  1642. # Add the mask to the appropriate z-stack
  1643. image.masks_combined[image.bbox[m, 1]:image.bbox[m, 1] + image.bbox[m, 3],
  1644. image.bbox[m, 0]:image.bbox[m, 0] + image.bbox[m, 2],
  1645. (row % 2) * 2 + (col % 2)] = image.mask[:, :, m]
  1646. # max of all in image
  1647. max_in_masks = image.masks_combined.max()
  1648. # combine masks handling overlap
  1649. image.mask = image.combine_big_image_masks(image.masks_combined).astype('uint16')
  1650. # process
  1651. image.process_2d_mask()
  1652. # plot
  1653. #image.plot_2d_mask()
  1654. #This function doesn't seem to be doing anything because plotshow is commmented out
  1655. # save into a comma separated value file
  1656. image.save_2d_mask(po)
  1657. if SO.segment_3D and image.im.shape[1] > 1:
  1658. print('no implementation for 3D as yet')
  1659. return image.mask_props
  1660. def scripting():
  1661. global model_choice, diam, flow_threshold, cellprob_threshold, alter_mask_channel, mask_ch
  1662. po = ProcessOptions()
  1663. files = find_all_images_in_path(r"C:\Users\Ben\Downloads\A81 16 Weeks BMSRAD70 Ki67 BL54_D111 scRNA\ki67", '.tif')
  1664. model_choice = r"C:\Users\Ben\Documents\RA PAPER IHC CELLPOSE\cellseg\Cellpose Models\CP_nrl_crx"
  1665. diam = 12 # autofit, or change for purpose
  1666. flow_threshold = 0.9 # defaults ~ 0.1 to 3.0 (fewer to more cells), comment if not needed 0.9
  1667. cellprob_threshold = 0.0001 # defaults ~ -6 to 6 (more to fewer pixels), comment if not need 0.0001
  1668. alter_mask_channel = True
  1669. #""" I think the inputs here 0-3 are: 0 for gray, 1 for red, 2 for green, 3 for blue """
  1670. mask_ch = [2,3]# this might accept two channels like this in a list, but a len(1) int should work as well
  1671. # ch00 = blue = 3
  1672. # ch01 = green = 2
  1673. # ch02 = red = 1
  1674. mask_properties = [None] * len(files)
  1675. for n, file in enumerate(files):
  1676. po.image_path = file
  1677. po.save_path = os.path.dirname(file)
  1678. po.save_mip_channels = False
  1679. po.save_mip_panel = False
  1680. po.save_mip_merge = False
  1681. po.save_dye_overlaid = False
  1682. po.save_colors = False
  1683. global SO
  1684. SO = SegmentationOptions()
  1685. po.segment_image = True
  1686. mask_properties[n] = process_file(po)
  1687. # save a csv file with the name of all files, and the total number of mask objects in each image
  1688. print('saving master cell count')
  1689. with open(os.path.join(po.save_path, 'image_count.csv'), 'w') as f:
  1690. writer = csv.writer(f)
  1691. writer.writerow(['file', 'num_mask_objects'])
  1692. files_and_masks = zip(files, mask_properties)
  1693. for file, mask in files_and_masks:
  1694. writer.writerow([file, len(mask.area)])
  1695. if __name__ == '__main__':
  1696. # gui = MainGUI()
  1697. scripting()
  1698. # check that GUI still works with the introduction of the new class SO.
  1699. # check out line ~1140 to change the segmentation model for cell pose
  1700. # TBD
  1701. # (1) add 3D segmentation characterisations,
  1702. # (2) add stardist,
  1703. print('Ok.')

main.py at commit 59a4cac, under CC0-1.0 · at the source

Overview

Authors: Benjamin Y. Lim1,2, Carissa Chen2,3, Anna Fredericks2,3, Elham Nilli1, Santiago Mesa Mora1, Megan Weatherstone4, To Ha Loi5, Peter Newman6, Nader Aryamanesh7, Hala Zreiqat6, Patrick Tam2,8, Pengyi Yang3,9, Anai Gonzalez-Cordero1,2
  1. Stem Cell Medicine Unit, Children’s Medical Research Institute,Westmead, NSW Australia
  2. School of Medical Sciences, Faculty of Medicine and Health, The University of Sydney,Camperdown, NSW Australia
  3. Computational Systems Biology Unit, Children’s Medical Research Institute,Westmead, NSW Australia
  4. Single Cell Analytics Facility, Children’s Medical Research Institute,Westmead, NSW Australia
  5. Eye Genetics Research Unit, Children’s Medical Research Institute,Westmead, NSW Australia
  6. School of Biomedical Engineering, Faculty of Engineering, University of Sydney,Camperdown, NSW Australia
  7. Bioinformatics Facility, Children’s Medical Research Institute,Westmead, NSW Australia
  8. Embryology Unit, Children’s Medical Research Institute,Westmead, NSW Australia
  9. School of Mathematics and Statistics, The University of Sydney,Camperdown, NSW Australia
Journal: Nature communications, volume 17, issue 1, article 5702
Dates: received 8 October 2025; accepted 8 April 2026; published online 25 April 2026
Type: Research article · Language: English
License: CC BY-NC-ND
Identifiers: DOI 10.1038/s41467-026-72130-3 · PMID 42031716 · PMCID PMC13319458 · OpenAlex W7155499606
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: human (organism)
Methods: Statistics, Smoothing, state filtering, decompositions, Connectivity
Keywords: Differentiation, Cellular signalling networks, Stem-cell differentiation
MeSH: Cell Differentiation*, Organoids*, Retina*, Tretinoin*, Gene Expression Regulation, Developmental, Humans, Signal Transduction (* major topic)
Topic: Retinal Development and Disorders (Molecular Biology, Biochemistry, Genetics and Molecular Biology), according to OpenAlex
Citations: cited by 2 papers (Europe PMC); 101 references in the paper

Abstract

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

Repositories

Its files are read in the Code ↔ Paper reader above.

Stem-Cell-Medicine-Lab/Cellpose_pipeline-models

License: CC0-1.0
State: the link answers, verified on 30 September 2026
Evidence: files inventoried
Commit: 59a4cac17caef1533be9d96ba91c626f00694d88, 4 April 2025
Languages: Python (1)
Size: 9 files, 1 script
Software Heritage: not archived
Found in: “Code availability”
Holds: README, license file, environment (requirements.txt)
Not found: CITATION.cff, tests, continuous integration, documentation
Tools: Cellpose (1 file), Matplotlib (1 file), NumPy (1 file), OpenCV (1 file), Pillow (1 file), PyTorch (1 file), scikit-image (1 file), SciPy (1 file)
Availability: 1 check, the latest on 30 September 2026: the link answers
  • 30 September 2026: the link answers
3 files

Zenodo 19324349

License: CC0-1.0
State: the link answers, verified on 30 September 2026
Evidence: files inventoried
Size: 1 file
Software Heritage: not checked
Found in: “Code availability”
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Availability: 1 check, the latest on 30 September 2026: the link answers (HTTP 200)
  • 30 September 2026: the link answers (HTTP 200)
At the source:

blim934/cellpose_pipeline-models

License: CC0-1.0
State: the link answers, verified on 30 September 2026
Evidence: files inventoried
Commit: 59a4cac17caef1533be9d96ba91c626f00694d88, 4 April 2025
Languages: Python (1)
Size: 9 files, 1 script
Software Heritage: not archived
Found in: the Zenodo archive record
Holds: README, license file, environment (requirements.txt)
Not found: CITATION.cff, tests, continuous integration, documentation
Tools: Cellpose (1 file), Matplotlib (1 file), NumPy (1 file), OpenCV (1 file), Pillow (1 file), PyTorch (1 file), scikit-image (1 file), SciPy (1 file)
Availability: 1 check, the latest on 30 September 2026: the link answers
  • 30 September 2026: the link answers
3 files

Code availability statement

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

Read it in the paper: doi.org/10.1038/s41467-026-72130-3.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 3 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 2 scripts, each with its path and the digest of its content;
  • no match between paragraphs and code yet;
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

Datasets cited

Code and data availability statement

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

Read it in the paper: doi.org/10.1038/s41467-026-72130-3.

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

Recorded: type, language, journal, volume, issue, pages, dates, 13 authors, 3 keywords, 7 MeSH terms, 1 funder, 98 references.

Cite

This paper

Lim, B. Y., Chen, C., Fredericks, A., Nilli, E., Mesa Mora, S., Weatherstone, M., Loi, T. H., Newman, P., Aryamanesh, N., Zreiqat, H., Tam, P., Yang, P., & Gonzalez-Cordero, A. (2026). Retinoic acid drives cell fate specification, maturation and retinal regionality in human retinal organoids. Nature communications, 17(1), 5702. https://doi.org/10.1038/s41467-026-72130-3

BibTeX

@article{lim2026retinoic,
author = {Lim, Benjamin Y. and Chen, Carissa and Fredericks, Anna and Nilli, Elham and Mesa Mora, Santiago and Weatherstone, Megan and Loi, To Ha and Newman, Peter and Aryamanesh, Nader and Zreiqat, Hala and Tam, Patrick and Yang, Pengyi and Gonzalez-Cordero, Anai},
title = {{Retinoic acid drives cell fate specification, maturation and retinal regionality in human retinal organoids}},
journal = {Nature communications},
year = {2026},
month = apr,
volume = {17},
number = {1},
pages = {5702},
publisher = {Nature Publishing Group},
issn = {2041-1723},
doi = {10.1038/s41467-026-72130-3},
url = {https://doi.org/10.1038/s41467-026-72130-3},
pmid = {42031716},
pmcid = {PMC13319458}
}

RIS

TY - JOUR
AU - Lim, Benjamin Y.
AU - Chen, Carissa
AU - Fredericks, Anna
AU - Nilli, Elham
AU - Mesa Mora, Santiago
AU - Weatherstone, Megan
AU - Loi, To Ha
AU - Newman, Peter
AU - Aryamanesh, Nader
AU - Zreiqat, Hala
AU - Tam, Patrick
AU - Yang, Pengyi
AU - Gonzalez-Cordero, Anai
TI - Retinoic acid drives cell fate specification, maturation and retinal regionality in human retinal organoids
T2 - Nature communications
J2 - Nat Commun
PY - 2026
DA - 2026/04/25
VL - 17
IS - 1
SP - 5702
SN - 2041-1723
PB - Nature Publishing Group
DO - 10.1038/s41467-026-72130-3
UR - https://doi.org/10.1038/s41467-026-72130-3
LA - en
ER -

CSL-JSON

{
"id": "10.1038/s41467-026-72130-3",
"type": "article-journal",
"title": "Retinoic acid drives cell fate specification, maturation and retinal regionality in human retinal organoids",
"container-title": "Nature communications",
"author": [
{
"family": "Lim",
"given": "Benjamin Y."
},
{
"family": "Chen",
"given": "Carissa"
},
{
"family": "Fredericks",
"given": "Anna"
},
{
"family": "Nilli",
"given": "Elham"
},
{
"family": "Mesa Mora",
"given": "Santiago"
},
{
"family": "Weatherstone",
"given": "Megan"
},
{
"family": "Loi",
"given": "To Ha"
},
{
"family": "Newman",
"given": "Peter"
},
{
"family": "Aryamanesh",
"given": "Nader"
},
{
"family": "Zreiqat",
"given": "Hala"
},
{
"family": "Tam",
"given": "Patrick"
},
{
"family": "Yang",
"given": "Pengyi"
},
{
"family": "Gonzalez-Cordero",
"given": "Anai"
}
],
"container-title-short": "Nat Commun",
"volume": "17",
"issue": "1",
"page": "5702",
"DOI": "10.1038/s41467-026-72130-3",
"PMID": "42031716",
"PMCID": "PMC13319458",
"ISSN": "2041-1723",
"publisher": "Nature Publishing Group",
"URL": "https://doi.org/10.1038/s41467-026-72130-3",
"language": "en",
"issued": {
"date-parts": [
[
2026,
4,
25
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1038/s41467-026-76112-3
Photostimulation improves maturation of human photoreceptors.
Journal: Nature communications
In common: 9 references
[2] doi:10.1016/j.celrep.2026.117270 [code]
Blocking apoptosis promotes survival and alters developmental dynamics of human retinal ganglion cells in retinal organoids.
Journal: Cell reports
In common: SciPy, Matplotlib, NumPy, 7 references
[3] doi:10.64898/2026.03.30.714220 [code]
An integrated single cell and spatial omics atlas of human prenatal development
Journal: bioRxiv (preprint)
In common: Pillow, PyTorch, SciPy, 2 other tools, 5 references
[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: Cellpose, OpenCV, scikit-image, 5 other tools, 2 references
[5] doi:10.1038/s44320-026-00208-7 [code]
Interpretable deep generative ensemble learning for single-cell omics with Hydra.
Journal: Molecular systems biology
In common: PyTorch, SciPy, Matplotlib, 1 other tool, 2 references, author Pengyi Yang
[6] doi:10.1038/s41467-026-71458-0 [code]
Early differential impact of MeCP2 mutations on functional networks in Rett syndrome patient-derived human cortical organoids.
Journal: Nature communications
In common: Cellpose, OpenCV, scikit-image, 4 other tools, 2 references
[7] doi:10.1038/s41586-026-10391-0 [code]
Cell-type-targeted mitochondrial transplantation rescues cell degeneration.
Journal: Nature
In common: PyTorch, SciPy, Matplotlib, 1 other tool, 5 references
[8] doi:10.1186/s13287-026-05027-z
Functional development of photoreceptors in human retinal organoids.
Journal: Stem cell research & therapy
In common: 6 references
[9] doi:10.1016/j.isci.2026.116355 [code]
Tera-MIND: Tera-scale mouse brain simulation via spatial mRNA-guided diffusion.
Journal: iScience
In common: Cellpose, OpenCV, Pillow, 4 other tools, 1 reference
[10] doi:10.1038/s41467-026-74320-5 [code]
Spatial architecture of autism pathogenesis reveals mosaic structural disarray during early development.
Journal: Nature communications
In common: OpenCV, scikit-image, Pillow, 3 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.