|
| 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) |
0 commit comments