22import os
33from concurrent import futures
44from functools import partial
5- from typing import List , Optional
5+ from typing import List , Optional , Tuple
66
77import numpy as np
88import pandas as pd
99import trimesh
10+ from elf .wrapper .resized_volume import ResizedVolume
11+ from nifty .tools import blocking
1012from skimage .measure import marching_cubes , regionprops_table
13+ from scipy .ndimage import binary_dilation
1114from tqdm import tqdm
1215
1316from .file_utils import read_image_data
@@ -29,9 +32,14 @@ def _measure_volume_and_surface(mask, resolution):
2932 return volume , surface
3033
3134
32- def _get_bounding_box_and_center (table , seg_id , resolution , shape ):
35+ def _get_bounding_box_and_center (table , seg_id , resolution , shape , dilation ):
3336 row = table [table .label_id == seg_id ]
3437
38+ if dilation is not None and dilation > 0 :
39+ bb_extension = dilation + 1
40+ else :
41+ bb_extension = 1
42+
3543 bb_min = np .array ([
3644 row .bb_min_z .item (), row .bb_min_y .item (), row .bb_min_x .item ()
3745 ]).astype ("float32" ) / resolution
@@ -43,7 +51,7 @@ def _get_bounding_box_and_center(table, seg_id, resolution, shape):
4351 bb_max = np .round (bb_max , 0 ).astype ("int32" )
4452
4553 bb = tuple (
46- slice (max (bmin - 1 , 0 ), min (bmax + 1 , sh ))
54+ slice (max (bmin - bb_extension , 0 ), min (bmax + bb_extension , sh ))
4755 for bmin , bmax , sh in zip (bb_min , bb_max , shape )
4856 )
4957
@@ -115,13 +123,15 @@ def _normalize_background(measures, image, mask, center, radius, norm, median_on
115123
116124def _default_object_features (
117125 seg_id , table , image , segmentation , resolution ,
118- foreground_mask = None , background_radius = None , norm = np .divide , median_only = False ,
126+ background_mask = None , background_radius = None , norm = np .divide , median_only = False , dilation = None
119127):
120- bb , center = _get_bounding_box_and_center (table , seg_id , resolution , image .shape )
128+ bb , center = _get_bounding_box_and_center (table , seg_id , resolution , image .shape , dilation )
121129
122130 local_image = image [bb ]
123131 mask = segmentation [bb ] == seg_id
124132 assert mask .sum () > 0 , f"Segmentation ID { seg_id } is empty."
133+ if dilation is not None and dilation > 0 :
134+ mask = binary_dilation (mask , iterations = dilation )
125135 masked_intensity = local_image [mask ]
126136
127137 # Do the base intensity measurements.
@@ -141,7 +151,7 @@ def _default_object_features(
141151 # The resolution is given in micrometer per pixel.
142152 # So we have to divide by the resolution to obtain the radius in pixel.
143153 radius_in_pixel = background_radius / resolution
144- measures = _normalize_background (measures , image , foreground_mask , center , radius_in_pixel , norm , median_only )
154+ measures = _normalize_background (measures , image , background_mask , center , radius_in_pixel , norm , median_only )
145155
146156 # Do the volume and surface measurement.
147157 if not median_only :
@@ -151,13 +161,15 @@ def _default_object_features(
151161 return measures
152162
153163
154- def _regionprops_features (seg_id , table , image , segmentation , resolution , foreground_mask = None ):
155- bb , _ = _get_bounding_box_and_center (table , seg_id , resolution , image .shape )
164+ def _regionprops_features (seg_id , table , image , segmentation , resolution , background_mask = None , dilation = None ):
165+ bb , _ = _get_bounding_box_and_center (table , seg_id , resolution , image .shape , dilation )
156166
157167 local_image = image [bb ]
158168 local_segmentation = segmentation [bb ]
159169 mask = local_segmentation == seg_id
160170 assert mask .sum () > 0 , f"Segmentation ID { seg_id } is empty."
171+ if dilation is not None and dilation > 0 :
172+ mask = binary_dilation (mask , iterations = dilation )
161173 local_segmentation [~ mask ] = 0
162174
163175 features = regionprops_table (
@@ -196,16 +208,16 @@ def _regionprops_features(seg_id, table, image, segmentation, resolution, foregr
196208"""
197209
198210
199- # TODO integrate segmentation post-processing, see `_extend_sgns_simple` in `gfp_annotation.py`
200211def compute_object_measures_impl (
201212 image : np .typing .ArrayLike ,
202213 segmentation : np .typing .ArrayLike ,
203214 n_threads : Optional [int ] = None ,
204215 resolution : float = 0.38 ,
205216 table : Optional [pd .DataFrame ] = None ,
206217 feature_set : str = "default" ,
207- foreground_mask : Optional [np .typing .ArrayLike ] = None ,
218+ background_mask : Optional [np .typing .ArrayLike ] = None ,
208219 median_only : bool = False ,
220+ dilation : Optional [int ] = None ,
209221) -> pd .DataFrame :
210222 """Compute simple intensity and morphology measures for each segmented cell in a segmentation.
211223
@@ -218,8 +230,10 @@ def compute_object_measures_impl(
218230 resolution: The resolution / voxel size of the data.
219231 table: The segmentation table. Will be computed on the fly if it is not given.
220232 feature_set: The features to compute for each object. Refer to `FEATURE_FUNCTIONS` for details.
221- foreground_mask : An optional mask indicating the area to use for computing background correction values.
233+ background_mask : An optional mask indicating the area to use for computing background correction values.
222234 median_only: Whether to only compute the median intensity.
235+ dilation: Value for dilating the segmentation before computing measurements.
236+ By default no dilation is applied.
223237
224238 Returns:
225239 The table with per object measurements.
@@ -235,8 +249,9 @@ def compute_object_measures_impl(
235249 image = image ,
236250 segmentation = segmentation ,
237251 resolution = resolution ,
238- foreground_mask = foreground_mask ,
252+ background_mask = background_mask ,
239253 median_only = median_only ,
254+ dilation = dilation ,
240255 )
241256
242257 seg_ids = table .label_id .values
@@ -272,6 +287,9 @@ def compute_object_measures(
272287 feature_set : str = "default" ,
273288 s3_flag : bool = False ,
274289 component_list : List [int ] = [],
290+ dilation : Optional [int ] = None ,
291+ median_only : bool = False ,
292+ background_mask : Optional [np .typing .ArrayLike ] = None ,
275293) -> None :
276294 """Compute simple intensity and morphology measures for each segmented cell in a segmentation.
277295
@@ -291,6 +309,12 @@ def compute_object_measures(
291309 resolution: The resolution / voxel size of the data.
292310 force: Whether to overwrite an existing output table.
293311 feature_set: The features to compute for each object. Refer to `FEATURE_FUNCTIONS` for details.
312+ s3_flag:
313+ component_list:
314+ median_only: Whether to only compute the median intensity.
315+ dilation: Value for dilating the segmentation before computing measurements.
316+ By default no dilation is applied.
317+ background_mask: An optional mask indicating the area to use for computing background correction values.
294318 """
295319 if os .path .exists (output_table_path ) and not force :
296320 return
@@ -315,5 +339,70 @@ def compute_object_measures(
315339
316340 measures = compute_object_measures_impl (
317341 image , segmentation , n_threads , resolution , table = table , feature_set = feature_set ,
342+ median_only = median_only , dilation = dilation , background_mask = background_mask ,
318343 )
319344 measures .to_csv (output_table_path , sep = "\t " , index = False )
345+
346+
347+ def compute_sgn_background_mask (
348+ image_path : str ,
349+ segmentation_path : str ,
350+ image_key : Optional [str ] = None ,
351+ segmentation_key : Optional [str ] = None ,
352+ threshold_percentile : float = 35.0 ,
353+ scale_factor : Tuple [int , int , int ] = (16 , 16 , 16 ),
354+ ) -> np .typing .ArrayLike :
355+ """
356+
357+ Args:
358+ p
359+
360+ Returns:
361+ pass
362+ """
363+ image = read_image_data (image_path , image_key )
364+ segmentation = read_image_data (segmentation_path , segmentation_key )
365+ assert image .shape == segmentation .shape
366+
367+ original_shape = image .shape
368+ downsampled_shape = tuple (int (np .round (sh / sf )) for sh , sf in zip (original_shape , scale_factor ))
369+
370+ low_res_mask = np .zeros (downsampled_shape , dtype = "bool" )
371+
372+ # This corresponds to a block shape of 128 x 512 x 512 in the original resolution,
373+ # which roughly corresponds to the size of the blocks we use for the GFP annotation.
374+ chunk_shape = (8 , 32 , 32 )
375+
376+ blocks = blocking ((0 , 0 , 0 ), downsampled_shape , chunk_shape )
377+ n_blocks = blocks .numberOfBlocks
378+
379+ img_resized = ResizedVolume (image , downsampled_shape )
380+ seg_resized = ResizedVolume (segmentation , downsampled_shape , order = 0 )
381+
382+ def _compute_block (block_id ):
383+ block = blocks .getBlock (block_id )
384+ bb = tuple (slice (beg , end ) for beg , end in zip (block .begin , block .end ))
385+
386+ img = img_resized [bb ]
387+ threshold = np .percentile (img , threshold_percentile )
388+
389+ this_mask = img > threshold
390+ this_seg = seg_resized [bb ] != 0
391+ this_seg = binary_dilation (this_seg )
392+ this_mask [this_seg ] = 0
393+
394+ low_res_mask [bb ] = this_mask
395+
396+ # TODO parallelize
397+ for block_id in range (n_blocks ):
398+ _compute_block (block_id )
399+
400+ # stain_averaged = downscale_local_mean(stain, factors=(16, 16, 16))
401+ # # The 35th percentile seems to be a decent approximation for the background subtraction.
402+ # threshold = np.percentile(stain_averaged, 35)
403+ # mask = stain_averaged > threshold
404+ # mask = resize(mask, seg_extended.shape, order=0, anti_aliasing=False, preserve_range=True).astype(bool)
405+ # mask[seg_extended != 0] = 0
406+
407+ mask = ResizedVolume (low_res_mask , shape = original_shape , order = 0 )
408+ return mask
0 commit comments