|
| 1 | +import argparse |
| 2 | +import os |
| 3 | +from typing import List, Optional |
| 4 | + |
| 5 | +import pandas as pd |
| 6 | + |
| 7 | +from flamingo_tools.s3_utils import get_s3_path |
| 8 | +from flamingo_tools.file_utils import read_image_data |
| 9 | +from flamingo_tools.segmentation.chreef_utils import localize_median_intensities, find_annotations |
| 10 | + |
| 11 | +MARKER_DIR = "/mnt/vast-nhr/projects/nim00007/data/moser/cochlea-lightsheet/ChReef_PV-GFP/2025-07_PV_GFP_SGN" |
| 12 | + |
| 13 | + |
| 14 | +def get_length_fraction_from_center(table, center_str): |
| 15 | + """ Get 'length_fraction' parameter for center coordinate by averaging nearby segmentation instances. |
| 16 | + """ |
| 17 | + center_coord = tuple([int(c) for c in center_str.split("-")]) |
| 18 | + (cx, cy, cz) = center_coord |
| 19 | + offset = 20 |
| 20 | + subset = table[ |
| 21 | + (cx - offset < table["anchor_x"]) & |
| 22 | + (table["anchor_x"] < cx + offset) & |
| 23 | + (cy - offset < table["anchor_y"]) & |
| 24 | + (table["anchor_y"] < cy + offset) & |
| 25 | + (cz - offset < table["anchor_z"]) & |
| 26 | + (table["anchor_z"] < cz + offset) |
| 27 | + ] |
| 28 | + length_fraction = list(subset["length_fraction"]) |
| 29 | + length_fraction = float(sum(length_fraction) / len(length_fraction)) |
| 30 | + return length_fraction |
| 31 | + |
| 32 | + |
| 33 | +def apply_nearest_threshold(intensity_dic, table_seg, table_measurement): |
| 34 | + """Apply threshold to nearest segmentation instances. |
| 35 | + Crop centers are transformed into the 'length fraction' parameter of the segmentation table. |
| 36 | + This avoids issues with the spiral shape of the cochlea and maps the assignment onto the Rosenthal's canal. |
| 37 | + """ |
| 38 | + # assign crop centers to length fraction of Rosenthal's canal |
| 39 | + lf_intensity = {} |
| 40 | + for key in intensity_dic.keys(): |
| 41 | + length_fraction = get_length_fraction_from_center(table_seg, key) |
| 42 | + intensity_dic[key]["length_fraction"] = length_fraction |
| 43 | + lf_intensity[length_fraction] = {"threshold": intensity_dic[key]["median_intensity"]} |
| 44 | + |
| 45 | + # get limits for checking marker thresholds |
| 46 | + lf_intensity = dict(sorted(lf_intensity.items())) |
| 47 | + lf_fractions = list(lf_intensity.keys()) |
| 48 | + # start of cochlea |
| 49 | + lf_limits = [0] |
| 50 | + # half distance between block centers |
| 51 | + for i in range(len(lf_fractions) - 1): |
| 52 | + lf_limits.append((lf_fractions[i] + lf_fractions[i+1]) / 2) |
| 53 | + # end of cochlea |
| 54 | + lf_limits.append(1) |
| 55 | + |
| 56 | + marker_labels = [0 for _ in range(len(table_seg))] |
| 57 | + table_seg.loc[:, "marker_labels"] = marker_labels |
| 58 | + for num, fraction in enumerate(lf_fractions): |
| 59 | + subset_seg = table_seg[ |
| 60 | + (table_seg["length_fraction"] > lf_limits[num]) & |
| 61 | + (table_seg["length_fraction"] < lf_limits[num + 1]) |
| 62 | + ] |
| 63 | + # assign values based on limits |
| 64 | + threshold = lf_intensity[fraction]["threshold"] |
| 65 | + label_ids_seg = subset_seg["label_id"] |
| 66 | + |
| 67 | + subset_measurement = table_measurement[table_measurement["label_id"].isin(label_ids_seg)] |
| 68 | + subset_positive = subset_measurement[subset_measurement["median"] >= threshold] |
| 69 | + subset_negative = subset_measurement[subset_measurement["median"] < threshold] |
| 70 | + label_ids_pos = list(subset_positive["label_id"]) |
| 71 | + label_ids_neg = list(subset_negative["label_id"]) |
| 72 | + |
| 73 | + table_seg.loc[table_seg["label_id"].isin(label_ids_pos), "marker_labels"] = 1 |
| 74 | + table_seg.loc[table_seg["label_id"].isin(label_ids_neg), "marker_labels"] = 2 |
| 75 | + |
| 76 | + return table_seg |
| 77 | + |
| 78 | + |
| 79 | +def evaluate_marker_annotation( |
| 80 | + cochleae, |
| 81 | + output_dir: str, |
| 82 | + annotation_dirs: Optional[List[str]] = None, |
| 83 | + seg_name: str = "SGN_v2", |
| 84 | + marker_name: str = "GFP", |
| 85 | +): |
| 86 | + """Evaluate marker annotations of a single or multiple annotators. |
| 87 | + Segmentation instances are assigned a positive (1) or negative label (2) |
| 88 | + in form of the "marker_label" component of the output segmentation table. |
| 89 | + The assignment is based on the median intensity supplied by a measurement table. |
| 90 | + Instances not considered for the assignment are labeled as 0. |
| 91 | +
|
| 92 | + Args: |
| 93 | + cochleae: List of cochlea |
| 94 | + output_dir: Output directory for segmentation table with 'marker_label' in format <cochlea>_<marker>_<seg>.tsv |
| 95 | + annotation_dirs: List of directories containing marker annotations by annotator(s). |
| 96 | + seg_name: Identifier for segmentation. |
| 97 | + marker_name: Identifier for marker stain. |
| 98 | + """ |
| 99 | + input_key = "s0" |
| 100 | + |
| 101 | + if annotation_dirs is None: |
| 102 | + if "MARKER_DIR" in globals(): |
| 103 | + marker_dir = MARKER_DIR |
| 104 | + annotation_dirs = [entry.path for entry in os.scandir(marker_dir) |
| 105 | + if os.path.isdir(entry) and "Results" in entry.name] |
| 106 | + |
| 107 | + for cochlea in cochleae: |
| 108 | + cochlea_annotations = [a for a in annotation_dirs if len(find_annotations(a, cochlea)["center_strings"]) != 0] |
| 109 | + print(f"Evaluating data for cochlea {cochlea} in {cochlea_annotations}.") |
| 110 | + |
| 111 | + # get segmentation data |
| 112 | + input_path = f"{cochlea}/images/ome-zarr/{seg_name}.ome.zarr" |
| 113 | + input_path, fs = get_s3_path(input_path) |
| 114 | + data_seg = read_image_data(input_path, input_key) |
| 115 | + |
| 116 | + table_seg_path = f"{cochlea}/tables/{seg_name}/default.tsv" |
| 117 | + table_path_s3, fs = get_s3_path(table_seg_path) |
| 118 | + with fs.open(table_path_s3, "r") as f: |
| 119 | + table_seg = pd.read_csv(f, sep="\t") |
| 120 | + |
| 121 | + seg_string = "-".join(seg_name.split("_")) |
| 122 | + table_measurement_path = f"{cochlea}/tables/{seg_name}/{marker_name}_{seg_string}_object-measures.tsv" |
| 123 | + table_path_s3, fs = get_s3_path(table_measurement_path) |
| 124 | + with fs.open(table_path_s3, "r") as f: |
| 125 | + table_measurement = pd.read_csv(f, sep="\t") |
| 126 | + |
| 127 | + # find median intensities by averaging all individual annotations for specific crops |
| 128 | + annotation_dics = {} |
| 129 | + annotated_centers = [] |
| 130 | + for annotation_dir in cochlea_annotations: |
| 131 | + |
| 132 | + annotation_dic = localize_median_intensities(annotation_dir, cochlea, data_seg, table_measurement) |
| 133 | + annotated_centers.extend(annotation_dic["center_strings"]) |
| 134 | + annotation_dics[annotation_dir] = annotation_dic |
| 135 | + |
| 136 | + annotated_centers = list(set(annotated_centers)) |
| 137 | + intensity_dic = {} |
| 138 | + # loop over all annotated blocks |
| 139 | + for annotated_center in annotated_centers: |
| 140 | + intensities = [] |
| 141 | + # loop over annotated block from single user |
| 142 | + for annotator_key in annotation_dics.keys(): |
| 143 | + if annotated_center not in annotation_dics[annotator_key]["center_strings"]: |
| 144 | + continue |
| 145 | + else: |
| 146 | + intensities.append(annotation_dics[annotator_key][annotated_center]["median_intensity"]) |
| 147 | + intensity_dic[annotated_center] = {"median_intensity": float(sum(intensities) / len(intensities))} |
| 148 | + |
| 149 | + table_seg = apply_nearest_threshold(intensity_dic, table_seg, table_measurement) |
| 150 | + cochlea_str = "-".join(cochlea.split("_")) |
| 151 | + out_path = os.path.join(output_dir, f"{cochlea_str}_{marker_name}_{seg_string}.tsv") |
| 152 | + table_seg.to_csv(out_path, sep="\t", index=False) |
| 153 | + |
| 154 | + |
| 155 | +def main(): |
| 156 | + parser = argparse.ArgumentParser( |
| 157 | + description="Assign each segmentation instance a marker based on annotation thresholds.") |
| 158 | + |
| 159 | + parser.add_argument('-c', "--cochlea", type=str, nargs="+", required=True, |
| 160 | + help="Cochlea(e) to process.") |
| 161 | + parser.add_argument('-o', "--output", type=str, required=True, help="Output directory.") |
| 162 | + |
| 163 | + parser.add_argument('-a', '--annotation_dirs', type=str, nargs="+", default=None, |
| 164 | + help="Directories containing marker annotations.") |
| 165 | + |
| 166 | + args = parser.parse_args() |
| 167 | + |
| 168 | + evaluate_marker_annotation( |
| 169 | + args.cochlea, args.output, args.annotation_dirs, |
| 170 | + ) |
| 171 | + |
| 172 | + |
| 173 | +if __name__ == "__main__": |
| 174 | + |
| 175 | + main() |
0 commit comments