Skip to content

Commit 7b15e28

Browse files
authored
Merge pull request #85 from computational-cell-analytics/cli_postprocessing
**Re-factor post-processing functions for usage with the command-line interface (CLI)** The post-processing functions for the labeling of connected components, the tonotopic mapping of SGN and IHC segmentation, and the calculation of object measures have been added to the CLI.
2 parents 92303d7 + 31cf877 commit 7b15e28

File tree

90 files changed

+1302
-820
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

90 files changed

+1302
-820
lines changed

flamingo_tools/measurements.py

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import warnings
77
from concurrent import futures
88
from functools import partial
9+
from multiprocessing import cpu_count
910
from typing import List, Optional, Tuple, Union
1011

1112
import numpy as np
@@ -22,13 +23,13 @@
2223
from tqdm import tqdm
2324

2425
from .file_utils import read_image_data
25-
from .segmentation.postprocessing import compute_table_on_the_fly
26+
from .postprocessing.label_components import compute_table_on_the_fly
2627
import flamingo_tools.s3_utils as s3_utils
2728

2829

2930
def _measure_volume_and_surface(mask, resolution):
3031
# Use marching_cubes for 3D data
31-
verts, faces, normals, _ = marching_cubes(mask, spacing=(resolution,) * 3)
32+
verts, faces, normals, _ = marching_cubes(mask, spacing=resolution)
3233

3334
mesh = trimesh.Trimesh(vertices=verts, faces=faces, vertex_normals=normals)
3435
surface = mesh.area
@@ -166,6 +167,8 @@ def _default_object_features(
166167

167168
# Do the volume and surface measurement.
168169
if not median_only:
170+
if isinstance(resolution, float):
171+
resolution = (resolution,) * 3
169172
volume, surface = _measure_volume_and_surface(mask, resolution)
170173
measures["volume"] = volume
171174
measures["surface"] = surface
@@ -181,6 +184,8 @@ def _morphology_features(seg_id, table, image, segmentation, resolution, **kwarg
181184
# Hard-coded value for LaVision cochleae. This is a hack for the wrong voxel size in MoBIE.
182185
# resolution = (3.0, 0.76, 0.76)
183186

187+
if isinstance(resolution, float):
188+
resolution = (resolution,) * 3
184189
volume, surface = _measure_volume_and_surface(mask, resolution)
185190
measures["volume"] = volume
186191
measures["surface"] = surface
@@ -498,3 +503,102 @@ def _compute_block(block_id):
498503

499504
mask = ResizedVolume(low_res_mask, shape=original_shape, order=0)
500505
return mask
506+
507+
508+
def object_measures_single(
509+
table_path: str,
510+
seg_path: str,
511+
image_paths: List[str],
512+
out_paths: List[str],
513+
force_overwrite: bool = False,
514+
component_list: List[int] = [1],
515+
background_mask: Optional[np.typing.ArrayLike] = None,
516+
resolution: List[float] = [0.38, 0.38, 0.38],
517+
s3: bool = False,
518+
s3_credentials: Optional[str] = None,
519+
s3_bucket_name: Optional[str] = None,
520+
s3_service_endpoint: Optional[str] = None,
521+
**_
522+
):
523+
"""Compute object measures for a single or multiple image channels in respect to a single segmentation channel.
524+
525+
Args:
526+
table_path: File path to segmentationt table.
527+
seg_path: Path to segmentation channel in ome.zarr format.
528+
image_paths: Path(s) to image channel(s) in ome.zarr format.
529+
out_paths: Paths(s) for calculated object measures.
530+
force_overwrite: Forcefully overwrite existing files.
531+
component_list: Only calculate object measures for specific components.
532+
background_mask: Use background mask for calculating object measures.
533+
resolution: Resolution of input in micrometer.
534+
s3: Use S3 file paths.
535+
s3_credentials:
536+
s3_bucket_name:
537+
s3_service_endpoint:
538+
"""
539+
input_key = "s0"
540+
out_paths = [os.path.realpath(o) for o in out_paths]
541+
542+
if not isinstance(resolution, float):
543+
if len(resolution) == 1:
544+
resolution = resolution * 3
545+
assert len(resolution) == 3
546+
resolution = np.array(resolution)[::-1]
547+
else:
548+
resolution = (resolution,) * 3
549+
550+
for (img_path, out_path) in zip(image_paths, out_paths):
551+
n_threads = int(os.environ.get("SLURM_CPUS_ON_NODE", cpu_count()))
552+
553+
# overwrite input file
554+
if os.path.realpath(out_path) == os.path.realpath(table_path) and not s3:
555+
force_overwrite = True
556+
557+
if os.path.isfile(out_path) and not force_overwrite:
558+
print(f"Skipping {out_path}. Table already exists.")
559+
560+
else:
561+
if background_mask is None:
562+
feature_set = "default"
563+
dilation = None
564+
median_only = False
565+
else:
566+
print("Using background mask for calculating object measures.")
567+
feature_set = "default_background_subtract"
568+
dilation = 4
569+
median_only = True
570+
571+
if s3:
572+
img_path, fs = s3_utils.get_s3_path(img_path, bucket_name=s3_bucket_name,
573+
service_endpoint=s3_service_endpoint,
574+
credential_file=s3_credentials)
575+
seg_path, fs = s3_utils.get_s3_path(seg_path, bucket_name=s3_bucket_name,
576+
service_endpoint=s3_service_endpoint,
577+
credential_file=s3_credentials)
578+
579+
mask_cache_path = os.path.join(os.path.dirname(out_path), "bg-mask.zarr")
580+
background_mask = compute_sgn_background_mask(
581+
image_path=img_path,
582+
segmentation_path=seg_path,
583+
image_key=input_key,
584+
segmentation_key=input_key,
585+
n_threads=n_threads,
586+
cache_path=mask_cache_path,
587+
)
588+
589+
compute_object_measures(
590+
image_path=img_path,
591+
segmentation_path=seg_path,
592+
segmentation_table_path=table_path,
593+
output_table_path=out_path,
594+
image_key=input_key,
595+
segmentation_key=input_key,
596+
feature_set=feature_set,
597+
s3_flag=s3,
598+
component_list=component_list,
599+
dilation=dilation,
600+
median_only=median_only,
601+
background_mask=background_mask,
602+
n_threads=n_threads,
603+
resolution=resolution,
604+
)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"""This module implements the functionality to filter isolated objects from a segmentation.
2+
"""
3+
4+
from .label_components import filter_segmentation
File renamed without changes.
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
"""private
2+
"""
3+
import argparse
4+
5+
from .label_components import label_components_single
6+
from .cochlea_mapping import tonotopic_mapping_single
7+
from flamingo_tools.measurements import object_measures_single
8+
9+
10+
def label_components():
11+
parser = argparse.ArgumentParser(
12+
description="Script to label segmentation using a segmentation table and graph connected components.")
13+
14+
parser.add_argument("-i", "--input", type=str, required=True, help="Input path to segmentation table.")
15+
parser.add_argument("-o", "--output", type=str, required=True,
16+
help="Output path. Either directory (for --json) or specific file otherwise.")
17+
parser.add_argument("--force", action="store_true", help="Forcefully overwrite output.")
18+
19+
# options for post-processing
20+
parser.add_argument("--cell_type", type=str, default="sgn",
21+
help="Cell type of segmentation. Either 'sgn' or 'ihc'.")
22+
parser.add_argument("--min_size", type=int, default=1000,
23+
help="Minimal number of pixels for filtering small instances.")
24+
parser.add_argument("--min_component_length", type=int, default=50,
25+
help="Minimal length for filtering out connected components.")
26+
parser.add_argument("--max_edge_distance", type=float, default=30,
27+
help="Maximal distance in micrometer between points to create edges for connected components.")
28+
parser.add_argument("-c", "--components", type=int, nargs="+", default=[1], help="List of connected components.")
29+
30+
# options for S3 bucket
31+
parser.add_argument("--s3", action="store_true", help="Flag for using S3 bucket.")
32+
parser.add_argument("--s3_credentials", type=str, default=None,
33+
help="Input file containing S3 credentials. "
34+
"Optional if AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY were exported.")
35+
parser.add_argument("--s3_bucket_name", type=str, default=None,
36+
help="S3 bucket name. Optional if BUCKET_NAME was exported.")
37+
parser.add_argument("--s3_service_endpoint", type=str, default=None,
38+
help="S3 service endpoint. Optional if SERVICE_ENDPOINT was exported.")
39+
40+
args = parser.parse_args()
41+
42+
label_components_single(
43+
table_path=args.input,
44+
out_path=args.output,
45+
cell_type=args.cell_type,
46+
component_list=args.components,
47+
max_edge_distance=args.max_edge_distance,
48+
min_component_length=args.min_component_length,
49+
min_size=args.min_size,
50+
force_overwrite=args.force,
51+
s3=args.s3,
52+
s3_credentials=args.s3_credentials,
53+
s3_bucket_name=args.s3_bucket_name,
54+
s3_service_endpoint=args.s3_service_endpoint,
55+
)
56+
57+
58+
def tonotopic_mapping():
59+
parser = argparse.ArgumentParser(
60+
description="Script to extract region of interest (ROI) block around center coordinate.")
61+
62+
parser.add_argument("-i", "--input", type=str, required=True, help="Input path to segmentation table.")
63+
parser.add_argument("-o", "--output", type=str, required=True,
64+
help="Output path. Either directory or specific file.")
65+
parser.add_argument("--force", action="store_true", help="Forcefully overwrite output.")
66+
67+
# options for tonotopic mapping
68+
parser.add_argument("--animal", type=str, default="mouse",
69+
help="Animal type to be used for frequency mapping. Either 'mouse' or 'gerbil'.")
70+
parser.add_argument("--otof", action="store_true", help="Use frequency mapping for OTOF cochleae.")
71+
parser.add_argument("--apex_position", type=str, default="apex_higher",
72+
help="Use frequency mapping for OTOF cochleae.")
73+
74+
# options for post-processing
75+
parser.add_argument("--cell_type", type=str, default="sgn",
76+
help="Cell type of segmentation. Either 'sgn' or 'ihc'.")
77+
parser.add_argument("--max_edge_distance", type=float, default=30,
78+
help="Maximal distance in micrometer between points to create edges for connected components.")
79+
parser.add_argument("-c", "--components", type=int, nargs="+", default=[1], help="List of connected components.")
80+
81+
# options for S3 bucket
82+
parser.add_argument("--s3", action="store_true", help="Flag for using S3 bucket.")
83+
parser.add_argument("--s3_credentials", type=str, default=None,
84+
help="Input file containing S3 credentials. "
85+
"Optional if AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY were exported.")
86+
parser.add_argument("--s3_bucket_name", type=str, default=None,
87+
help="S3 bucket name. Optional if BUCKET_NAME was exported.")
88+
parser.add_argument("--s3_service_endpoint", type=str, default=None,
89+
help="S3 service endpoint. Optional if SERVICE_ENDPOINT was exported.")
90+
91+
args = parser.parse_args()
92+
93+
tonotopic_mapping_single(
94+
table_path=args.input,
95+
out_path=args.output,
96+
force_overwrite=args.force,
97+
animal=args.animal,
98+
otof=args.otof,
99+
apex_position=args.apex_position,
100+
cell_type=args.cell_type,
101+
max_edge_distance=args.max_edge_distance,
102+
component_list=args.components,
103+
s3=args.s3,
104+
s3_credentials=args.s3_credentials,
105+
s3_bucket_name=args.s3_bucket_name,
106+
s3_service_endpoint=args.s3_service_endpoint,
107+
)
108+
109+
110+
def object_measures():
111+
parser = argparse.ArgumentParser(
112+
description="Script to compute object measures for different stainings.")
113+
114+
parser.add_argument("-o", "--output", type=str, nargs="+", required=True,
115+
help="Output path(s). Either directory or specific file(s).")
116+
parser.add_argument("-i", "--image_paths", type=str, nargs="+", default=None,
117+
help="Input path to one or multiple image channels in ome.zarr format.")
118+
parser.add_argument("-t", "--seg_table", type=str, default=None,
119+
help="Input path to segmentation table.")
120+
parser.add_argument("-s", "--seg_path", type=str, default=None,
121+
help="Input path to segmentation channel in ome.zarr format.")
122+
parser.add_argument("--force", action="store_true", help="Forcefully overwrite output.")
123+
124+
# options for object measures
125+
parser.add_argument("-c", "--components", type=int, nargs="+", default=[1], help="List of components.")
126+
parser.add_argument("-r", "--resolution", type=float, nargs="+", default=[0.38, 0.38, 0.38],
127+
help="Resolution of input in micrometer.")
128+
parser.add_argument("--bg_mask", action="store_true", help="Use background mask for calculating object measures.")
129+
130+
# options for S3 bucket
131+
parser.add_argument("--s3", action="store_true", help="Flag for using S3 bucket.")
132+
parser.add_argument("--s3_credentials", type=str, default=None,
133+
help="Input file containing S3 credentials. "
134+
"Optional if AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY were exported.")
135+
parser.add_argument("--s3_bucket_name", type=str, default=None,
136+
help="S3 bucket name. Optional if BUCKET_NAME was exported.")
137+
parser.add_argument("--s3_service_endpoint", type=str, default=None,
138+
help="S3 service endpoint. Optional if SERVICE_ENDPOINT was exported.")
139+
140+
args = parser.parse_args()
141+
142+
object_measures_single(
143+
out_paths=args.output,
144+
image_paths=args.image_paths,
145+
table_path=args.seg_table,
146+
seg_path=args.seg_path,
147+
force_overwrite=args.force,
148+
s3=args.s3,
149+
)

0 commit comments

Comments
 (0)