Skip to content

Commit 28f9fb7

Browse files
Implement thresholding based nucleus segmentation
1 parent 65f1314 commit 28f9fb7

File tree

2 files changed

+96
-1
lines changed

2 files changed

+96
-1
lines changed
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
from concurrent import futures
2+
from multiprocessing import cpu_count
3+
from typing import Optional
4+
5+
import numpy as np
6+
import pandas as pd
7+
from elf.io import open_file
8+
from scipy.ndimage import binary_opening
9+
from skimage.filters import gaussian, threshold_otsu
10+
from skimage.measure import label
11+
from tqdm import tqdm
12+
13+
from ..file_utils import read_image_data
14+
from ..measurements import _get_bounding_box
15+
from .postprocessing import compute_table_on_the_fly
16+
17+
18+
def _naive_nucleus_segmentation_impl(image, segmentation, table, output, n_threads, resolution):
19+
opening_iterations = 3
20+
21+
# Compute the table on the fly if it wasn't given.
22+
if table is None:
23+
table = compute_table_on_the_fly(segmentation, resolution=resolution)
24+
25+
def segment_nucleus(seg_id):
26+
bb = _get_bounding_box(table, seg_id, resolution, image.shape)
27+
image_local, seg_local = image[bb], segmentation[bb]
28+
mask = seg_local == seg_id
29+
30+
# Smooth before computing the threshold.
31+
image_local = gaussian(image_local)
32+
# Compute threshold only in the mask.
33+
threshold = threshold_otsu(image_local[mask])
34+
35+
nucleus_mask = np.logical_and(image_local < threshold, mask)
36+
nucleus_mask = label(nucleus_mask)
37+
ids, sizes = np.unique(nucleus_mask, return_counts=True)
38+
ids, sizes = ids[1:], sizes[1:]
39+
nucleus_mask = (nucleus_mask == ids[np.argmax(sizes)])
40+
nucleus_mask = binary_opening(nucleus_mask, iterations=opening_iterations)
41+
output[bb][nucleus_mask] = seg_id
42+
43+
n_threads = cpu_count() if n_threads is None else n_threads
44+
seg_ids = table.label_id.values
45+
with futures.ThreadPoolExecutor(n_threads) as tp:
46+
list(tqdm(tp.map(segment_nucleus, seg_ids), total=len(seg_ids), desc="Segment nuclei"))
47+
48+
return output
49+
50+
51+
def naive_nucleus_segmentation(
52+
image_path: str,
53+
segmentation_path: str,
54+
segmentation_table_path: Optional[str],
55+
output_path: str,
56+
output_key: str,
57+
image_key: Optional[str] = None,
58+
segmentation_key: Optional[str] = None,
59+
n_threads: Optional[int] = None,
60+
resolution: float = 0.38,
61+
):
62+
"""Segment nuclei per object with an otsu threshold.
63+
64+
This assumes that the nucleus is stained significantly less.
65+
66+
Args:
67+
image_path: The filepath to the image data. Either a tif or hdf5/zarr/n5 file.
68+
segmentation_path: The filepath to the segmentation data. Either a tif or hdf5/zarr/n5 file.
69+
segmentation_table_path: The path to the segmentation table in MoBIE format.
70+
output_path: The path for saving the nucleus segmentation.
71+
output_key: The key for saving the nucleus segmentation.
72+
image_key: The key (= internal path) for the image data. Not needed fir tif.
73+
segmentation_key: The key (= internal path) for the segmentation data. Not needed for tif.
74+
n_threads: The number of threads to use for computation.
75+
resolution: The resolution / voxel size of the data.
76+
"""
77+
# First, we load the pre-computed segmentation table from MoBIE.
78+
if segmentation_table_path is None:
79+
table = None
80+
else:
81+
table = pd.read_csv(segmentation_table_path, sep="\t")
82+
83+
# Then, open the volumes.
84+
image = read_image_data(image_path, image_key)
85+
segmentation = read_image_data(segmentation_path, segmentation_key)
86+
87+
# Create the output volume.
88+
with open_file(output_path, mode="a") as f:
89+
output = f.create_dataset(
90+
output_key, shape=segmentation.shape, dtype=segmentation.dtype, compression="gzip",
91+
chunks=segmentation.chunks
92+
)
93+
94+
# And run the nucleus segmentation.
95+
_naive_nucleus_segmentation_impl(image, segmentation, table, output, n_threads, resolution)

scripts/validation/check_nucleus_segmentation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def check_segmentation():
3737

3838
def main():
3939
segment_nuclei()
40-
# check_segmentation()
40+
check_segmentation()
4141

4242

4343
if __name__ == "__main__":

0 commit comments

Comments
 (0)