Skip to content

Commit bd03d5e

Browse files
authored
Merge pull request #60 from computational-cell-analytics/marker_annotation_checkup
Intensity values for visualizing SGNs can now be read from their object measure table. A warning is issued if the label ID of a segmentation instance is not found. The merge also includes minor changes to the export of synapses and marker volumes at lower resolutions.
2 parents ce76346 + 379efe0 commit bd03d5e

File tree

5 files changed

+161
-50
lines changed

5 files changed

+161
-50
lines changed

flamingo_tools/measurements.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import multiprocessing as mp
22
import os
3+
import warnings
34
from concurrent import futures
45
from functools import partial
56
from typing import List, Optional, Tuple
@@ -188,6 +189,25 @@ def _regionprops_features(seg_id, table, image, segmentation, resolution, backgr
188189
return features
189190

190191

192+
def get_object_measures_from_table(arr_seg, table):
193+
"""Return object measurements for label IDs wthin array.
194+
"""
195+
# iterate through segmentation ids in reference mask
196+
ref_ids = list(np.unique(arr_seg)[1:])
197+
measure_ids = list(table["label_id"])
198+
object_ids = [id for id in ref_ids if id in measure_ids]
199+
if len(object_ids) < len(ref_ids):
200+
warnings.warn(f"Not all IDs were found in measurement table. Using {len(object_ids)}/{len(ref_ids)}.")
201+
202+
median_values = [table.at[table.index[table["label_id"] == label_id][0], "median"] for label_id in object_ids]
203+
204+
measures = pd.DataFrame({
205+
"label_id": object_ids,
206+
"median": median_values,
207+
})
208+
return measures
209+
210+
191211
# Maybe also support:
192212
# - spherical harmonics
193213
# - line profiles

scripts/export_lower_resolution.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,31 @@ def filter_cochlea(
9696
dilation_iterations=dilation_iterations)
9797

9898

99+
def filter_marker_instances(cochlea, segmentation, seg_name):
100+
"""Filter segmentation with marker labels.
101+
Positive segmentation instances are set to 1, negative to 2.
102+
"""
103+
internal_path = os.path.join(cochlea, "tables", seg_name, "default.tsv")
104+
tsv_path, fs = get_s3_path(internal_path, bucket_name=BUCKET_NAME, service_endpoint=SERVICE_ENDPOINT)
105+
with fs.open(tsv_path, "r") as f:
106+
table_seg = pd.read_csv(f, sep="\t")
107+
108+
label_ids_positive = list(table_seg.loc[table_seg["marker_labels"] == 1, "label_id"])
109+
label_ids_negative = list(table_seg.loc[table_seg["marker_labels"] == 2, "label_id"])
110+
111+
label_ids_marker = label_ids_positive + label_ids_negative
112+
filter_mask = ~np.isin(segmentation, label_ids_marker)
113+
segmentation[filter_mask] = 0
114+
115+
filter_mask = np.isin(segmentation, label_ids_positive)
116+
segmentation[filter_mask] = 1
117+
filter_mask = np.isin(segmentation, label_ids_negative)
118+
segmentation[filter_mask] = 2
119+
120+
segmentation = segmentation.astype("uint16")
121+
return segmentation
122+
123+
99124
def upscale_volume(
100125
target_data: np.ndarray,
101126
downscaled_volume: np.ndarray,
@@ -146,6 +171,9 @@ def export_lower_resolution(args):
146171
input_key = f"s{scale}"
147172
for channel in args.channels:
148173
out_path = os.path.join(output_folder, f"{channel}.tif")
174+
if args.filter_marker_labels:
175+
out_path = os.path.join(output_folder, f"{channel}_marker.tif")
176+
149177
if os.path.exists(out_path):
150178
continue
151179

@@ -156,7 +184,11 @@ def export_lower_resolution(args):
156184
data = f[input_key][:]
157185
print("Data shape", data.shape)
158186
if args.filter_by_components is not None:
187+
print(f"Filtering channel {channel} by components {args.filter_by_components}.")
159188
data = filter_component(fs, data, args.cochlea, channel, args.filter_by_components)
189+
if args.filter_marker_labels:
190+
print(f"Filtering marker instances for channel {channel}.")
191+
data = filter_marker_instances(args.cochlea, data, channel)
160192
if args.filter_cochlea_channels is not None:
161193
us_factor = ds_factor // (2 ** scale)
162194
upscaled_filter = upscale_volume(data, filter_volume, upscale_factor=us_factor)
@@ -181,6 +213,7 @@ def main():
181213
parser.add_argument("--filter_ihc_components", nargs="+", type=int, default=[1])
182214
parser.add_argument("--binarize", action="store_true")
183215
parser.add_argument("--filter_cochlea_channels", nargs="+", type=str, default=None)
216+
parser.add_argument("--filter_marker_labels", action="store_true")
184217
parser.add_argument("--filter_dilation_iterations", type=int, default=8)
185218
args = parser.parse_args()
186219

scripts/export_synapse_detections.py

Lines changed: 69 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import argparse
22
import json
33
import os
4+
from typing import List
45

56
import numpy as np
67
import pandas as pd
@@ -12,7 +13,30 @@
1213
from tqdm import tqdm
1314

1415

15-
def export_synapse_detections(cochlea, scale, output_folder, synapse_name, reference_ihcs, max_dist, radius, id_offset):
16+
def export_synapse_detections(
17+
cochlea: str,
18+
scales: List[int],
19+
output_folder: str,
20+
synapse_name: str,
21+
reference_ihcs: str,
22+
max_dist: float,
23+
radius: float,
24+
id_offset: int,
25+
filter_ihc_components: List[int],
26+
):
27+
"""Export synapse detections fro lower resolutions.
28+
29+
Args:
30+
cochlea: Cochlea name on S3 bucket.
31+
scales: Scale for export of lower resolution.
32+
output_folder:
33+
synapse_name:
34+
reference_ihcs: Name of IHC segmentation.
35+
max_dist: Maximal distance of synapse to IHC segmentation.
36+
radius:
37+
id_offset: Offset of label id of synapse output to have different colours for visualization.
38+
filter_ihc_components: Component label(s) for filtering IHC segmentation.
39+
"""
1640
s3 = create_s3_target()
1741

1842
content = s3.open(f"{BUCKET_NAME}/{cochlea}/dataset.json", mode="r", encoding="utf-8")
@@ -36,67 +60,69 @@ def export_synapse_detections(cochlea, scale, output_folder, synapse_name, refer
3660
seg_table = pd.read_csv(seg_table_content, sep="\t")
3761

3862
# Only keep synapses that match to segmented IHCs of the main component.
39-
valid_ihcs = seg_table[seg_table.component_labels == 1].label_id
63+
valid_ihcs = seg_table[seg_table.component_labels.isin(filter_ihc_components)].label_id
4064
syn_table = syn_table[syn_table.matched_ihc.isin(valid_ihcs)]
4165

42-
# Get the reference shape at the given scale level.
43-
seg_path = os.path.join(cochlea, reference_seg_info["imageData"]["ome.zarr"]["relativePath"])
44-
s3_store, _ = get_s3_path(seg_path, bucket_name=BUCKET_NAME, service_endpoint=SERVICE_ENDPOINT)
45-
input_key = f"s{scale}"
46-
with zarr.open(s3_store, mode="r") as f:
47-
shape = f[input_key].shape
48-
49-
# Scale the coordinates according to the scale level.
50-
resolution = 0.38
51-
coordinates = syn_table[["z", "y", "x"]].values
52-
coordinates /= resolution
53-
coordinates /= (2 ** scale)
54-
coordinates = np.round(coordinates, 0).astype("int")
55-
56-
ihc_ids = syn_table["matched_ihc"].values
57-
58-
# Create the output.
59-
output = np.zeros(shape, dtype="uint16")
60-
mask = ball(radius).astype(bool)
61-
62-
for coord, matched_ihc in tqdm(
63-
zip(coordinates, ihc_ids), total=len(coordinates), desc="Writing synapses to volume"
64-
):
65-
bb = tuple(slice(c - radius, c + radius + 1) for c in coord)
66-
try:
67-
output[bb][mask] = matched_ihc + id_offset
68-
except IndexError:
69-
print("Index error for", coord)
70-
continue
71-
72-
# Write the output.
73-
out_folder = os.path.join(output_folder, cochlea, f"scale{scale}")
74-
os.makedirs(out_folder, exist_ok=True)
75-
if id_offset != 0:
76-
out_path = os.path.join(out_folder, f"{synapse_name}_offset{id_offset}.tif")
77-
else:
78-
out_path = os.path.join(out_folder, f"{synapse_name}.tif")
79-
print("Writing synapses to", out_path)
80-
tifffile.imwrite(out_path, output, bigtiff=True, compression="zlib")
66+
for scale in scales:
67+
# Get the reference shape at the given scale level.
68+
seg_path = os.path.join(cochlea, reference_seg_info["imageData"]["ome.zarr"]["relativePath"])
69+
s3_store, _ = get_s3_path(seg_path, bucket_name=BUCKET_NAME, service_endpoint=SERVICE_ENDPOINT)
70+
input_key = f"s{scale}"
71+
with zarr.open(s3_store, mode="r") as f:
72+
shape = f[input_key].shape
73+
74+
# Scale the coordinates according to the scale level.
75+
resolution = 0.38
76+
coordinates = syn_table[["z", "y", "x"]].values
77+
coordinates /= resolution
78+
coordinates /= (2 ** scale)
79+
coordinates = np.round(coordinates, 0).astype("int")
80+
81+
ihc_ids = syn_table["matched_ihc"].values
82+
83+
# Create the output.
84+
output = np.zeros(shape, dtype="uint16")
85+
mask = ball(radius).astype(bool)
86+
87+
for coord, matched_ihc in tqdm(
88+
zip(coordinates, ihc_ids), total=len(coordinates), desc="Writing synapses to volume"
89+
):
90+
bb = tuple(slice(c - radius, c + radius + 1) for c in coord)
91+
try:
92+
output[bb][mask] = matched_ihc + id_offset
93+
except IndexError:
94+
print("Index error for", coord)
95+
continue
96+
97+
# Write the output.
98+
out_folder = os.path.join(output_folder, cochlea, f"scale{scale}")
99+
os.makedirs(out_folder, exist_ok=True)
100+
if id_offset != 0:
101+
out_path = os.path.join(out_folder, f"{synapse_name}_offset{id_offset}.tif")
102+
else:
103+
out_path = os.path.join(out_folder, f"{synapse_name}.tif")
104+
print("Writing synapses to", out_path)
105+
tifffile.imwrite(out_path, output, bigtiff=True, compression="zlib")
81106

82107

83108
def main():
84109
parser = argparse.ArgumentParser()
85110
parser.add_argument("--cochlea", "-c", required=True)
86-
parser.add_argument("--scale", "-s", type=int, required=True)
111+
parser.add_argument("--scale", "-s", nargs="+", type=int, required=True)
87112
parser.add_argument("--output_folder", "-o", required=True)
88113
parser.add_argument("--synapse_name", default="synapse_v3_ihc_v4b")
89114
parser.add_argument("--reference_ihcs", default="IHC_v4b")
90115
parser.add_argument("--max_dist", type=float, default=3.0)
91116
parser.add_argument("--radius", type=int, default=3)
92117
parser.add_argument("--id_offset", type=int, default=0)
118+
parser.add_argument("--filter_ihc_components", nargs="+", type=int, default=[1])
93119
args = parser.parse_args()
94120

95121
export_synapse_detections(
96122
args.cochlea, args.scale, args.output_folder,
97123
args.synapse_name, args.reference_ihcs,
98124
args.max_dist, args.radius,
99-
args.id_offset,
125+
args.id_offset, args.filter_ihc_components,
100126
)
101127

102128

scripts/intensity_annotation/gfp_annotation.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import imageio.v3 as imageio
55
import napari
66
import numpy as np
7+
import pandas as pd
78

89
import matplotlib.pyplot as plt
910
import seaborn as sns
@@ -14,7 +15,8 @@
1415
from elf.parallel.distance_transform import distance_transform
1516
from elf.parallel.seeded_watershed import seeded_watershed
1617

17-
from flamingo_tools.measurements import compute_object_measures_impl
18+
from flamingo_tools.measurements import compute_object_measures_impl, get_object_measures_from_table
19+
from flamingo_tools.s3_utils import get_s3_path
1820

1921

2022
class HistogramWidget(QWidget):
@@ -132,7 +134,7 @@ def _create_mask(seg_extended, stain):
132134
return mask
133135

134136

135-
def gfp_annotation(prefix, default_stat="median", background_norm=None, is_otof=False):
137+
def gfp_annotation(prefix, default_stat="median", background_norm=None, is_otof=False, seg_version=None):
136138
assert background_norm in (None, "division", "subtraction")
137139

138140
direc = os.path.dirname(os.path.abspath(prefix))
@@ -179,9 +181,22 @@ def gfp_annotation(prefix, default_stat="median", background_norm=None, is_otof=
179181
mask = _create_mask(seg_extended, stain1)
180182
assert mask.shape == seg_extended.shape
181183
feature_set = "default_background_norm" if background_norm == "division" else "default_background_subtract"
182-
statistics = compute_object_measures_impl(
183-
stain1, seg_extended, feature_set=feature_set, background_mask=mask, median_only=True
184-
)
184+
185+
if seg_version is not None:
186+
seg_string = "-".join(seg_version.split("_"))
187+
cochlea = os.path.basename(prefix).split("_crop_")[0]
188+
189+
table_measurement_path = f"{cochlea}/tables/{seg_version}/{stain1_name}_{seg_string}_object-measures.tsv"
190+
table_path_s3, fs = get_s3_path(table_measurement_path)
191+
with fs.open(table_path_s3, "r") as f:
192+
table_measurement = pd.read_csv(f, sep="\t")
193+
194+
statistics = get_object_measures_from_table(seg, table=table_measurement)
195+
196+
else:
197+
statistics = compute_object_measures_impl(
198+
stain1, seg_extended, feature_set=feature_set, background_mask=mask, median_only=True
199+
)
185200

186201
# Open the napari viewer.
187202
v = napari.Viewer()
@@ -281,9 +296,11 @@ def main():
281296
parser.add_argument("--otof", action="store_true",
282297
help="Whether to run the annotation tool for otof samples with VGlut3, "
283298
"Alphatag and IHC segmentation.") # noqa
299+
parser.add_argument("--seg_version", type=str, default=None,
300+
help="Supply segmentation version, e.g. SGN_v2, to use intensities from object measure table.")
284301
args = parser.parse_args()
285302

286-
gfp_annotation(args.prefix, background_norm=args.background_norm, is_otof=args.otof)
303+
gfp_annotation(args.prefix, background_norm=args.background_norm, is_otof=args.otof, seg_version=args.seg_version)
287304

288305

289306
if __name__ == "__main__":

scripts/measurements/evaluate_marker_annotations.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,20 +107,35 @@ def find_thresholds(cochlea_annotations, cochlea, data_seg, table_measurement):
107107
# loop over all annotated blocks
108108
for annotated_center in annotated_centers:
109109
intensities = []
110+
annotator_success = []
111+
annotator_failure = []
112+
annotator_missing = []
110113
# loop over annotated block from single user
111114
for annotator_key in annotation_dics.keys():
112115
if annotated_center not in annotation_dics[annotator_key]["center_strings"]:
116+
annotator_missing.append(os.path.basename(annotator_key))
113117
continue
114118
else:
115119
median_intensity = annotation_dics[annotator_key][annotated_center]["median_intensity"]
116120
if median_intensity is None:
117121
print(f"No threshold for {os.path.basename(annotator_key)} and crop {annotated_center}.")
122+
annotator_failure.append(os.path.basename(annotator_key))
118123
else:
119124
intensities.append(median_intensity)
125+
annotator_success.append(os.path.basename(annotator_key))
126+
120127
if len(intensities) == 0:
121128
print(f"No viable annotation for cochlea {cochlea} and crop {annotated_center}.")
129+
median_int_avg = None
122130
else:
123-
intensity_dic[annotated_center] = {"median_intensity": float(sum(intensities) / len(intensities))}
131+
median_int_avg = float(sum(intensities) / len(intensities)),
132+
133+
intensity_dic[annotated_center] = {
134+
"median_intensity": median_int_avg,
135+
"annotation_success": annotator_success,
136+
"annotation_failure": annotator_failure,
137+
"annotation_missing": annotator_missing,
138+
}
124139

125140
return intensity_dic
126141

0 commit comments

Comments
 (0)