Skip to content

Commit 62906da

Browse files
committed
Grouped subtype plots; Manual thresholds
1 parent af8fbb2 commit 62906da

File tree

4 files changed

+211
-103
lines changed

4 files changed

+211
-103
lines changed

scripts/assign_subtypes.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
"M_LR_000184_L": {"seg_data": "SGN_v2b", "subtype": ["Prph"]},
1313
"M_LR_000184_R": {"seg_data": "SGN_v2b", "subtype": ["Prph"]},
1414
"M_LR_000260_L": {"seg_data": "SGN_v2", "subtype": ["Prph", "Tuj1"]},
15+
"M_LR_N110_L": {"seg_data": "SGN_v2", "subtype": ["Calb1", "Ntng1"]},
16+
"M_LR_N110_R": {"seg_data": "SGN_v2", "subtype": ["Calb1", "Ntng1"]},
1517
"M_LR_N152_L": {"seg_data": "SGN_v2", "subtype": ["CR", "Ntng1"]},
1618
"M_AMD_N180_L": {"seg_data": "SGN_merged", "subtype": ["CR", "Ntng1"]},
1719
"M_AMD_N180_R": {"seg_data": "SGN_merged", "subtype": ["CR", "Ntng1"]},
@@ -46,6 +48,12 @@
4648
"CR+/Ntng1-": "Type Ia",
4749
"CR-/Ntng1+": "Type Ic",
4850
"CR-/Ntng1-": "inconclusive",
51+
52+
# Combinations of Calb1 and Ntng1
53+
"Calb1+/Ntng1+": "Type Ib",
54+
"Calb1+/Ntng1-": "inconclusive",
55+
"Calb1-/Ntng1+": "Type Ic",
56+
"Calb1-/Ntng1-": "inconclusive",
4957
}
5058

5159

@@ -110,14 +118,14 @@ def filter_subtypes(cochlea, seg_name, subtype, stains=None):
110118
if len(stain_dict) == 0:
111119
raise ValueError("The dictionary containing stain information must have at least one entry. Check parameters.")
112120

113-
subset = table_seg.copy()
114-
121+
label_ids_subtype = []
115122
for dic in stain_dict:
123+
subset = table_seg.copy()
116124
for stain in dic.keys():
117125
expression_value = 1 if dic[stain] == "+" else 2
118-
subset = subset.loc[subset[f"marker_{stain}"] == expression_value]
126+
subset = subset[subset[f"marker_{stain}"] == expression_value]
119127

120-
label_ids_subtype = list(subset["label_id"])
128+
label_ids_subtype.extend(list(subset["label_id"]))
121129
return label_ids_subtype
122130

123131

@@ -145,6 +153,7 @@ def export_lower_resolution(args):
145153
for subtype in subtypes:
146154

147155
label_ids_subtype = filter_subtypes(cochlea, seg_name=seg_name, subtype=subtype, stains=subtype_stains)
156+
print(f"Subtype '{subtype}' with {len(label_ids_subtype)} instances.")
148157
table.loc[table["label_id"].isin(label_ids_subtype), "subtype_label"] = subtype
149158

150159
table.to_csv(out_path, sep="\t", index=False)
Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,24 @@
1212
png_dpi = 300
1313

1414
COCHLEAE = {
15-
"M_LR_000099_L": {"seg_data": "PV_SGN_v2", "subtype": ["Calb1", "Lypd1"], "intensity": "ratio"},
16-
"M_AMD_N180_L": {"seg_data": "SGN_merged", "subtype": ["CR", "Lypd1", "Ntng1"], "intensity": "absolute"},
17-
"M_AMD_N180_R": {"seg_data": "SGN_merged", "subtype": ["CR", "Ntng1"], "intensity": "absolute"},
1815
"M_LR_000098_L": {"seg_data": "SGN_v2", "subtype": ["CR", "Ntng1"], "intensity": "ratio"},
16+
"M_LR_000099_L": {"seg_data": "PV_SGN_v2", "subtype": ["Calb1", "Lypd1"], "intensity": "ratio"},
1917
"M_LR_000184_L": {"seg_data": "SGN_v2", "subtype": ["Prph"], "output_seg": "SGN_v2b", "intensity": "ratio"},
2018
"M_LR_000184_R": {"seg_data": "SGN_v2", "subtype": ["Prph"], "output_seg": "SGN_v2b", "intensity": "ratio"},
2119
"M_LR_000214_L": {"seg_data": "PV_SGN_v2", "subtype": ["Calb1"], "intensity": "ratio"},
2220
"M_LR_000260_L": {"seg_data": "SGN_v2", "subtype": ["Prph", "Tuj1"], "intensity": "ratio"},
21+
"M_LR_N110_L": {"seg_data": "SGN_v2", "subtype": ["Calb1", "Ntng1"], "intensity": "ratio"},
22+
"M_LR_N110_R": {"seg_data": "SGN_v2", "subtype": ["Calb1", "Ntng1"], "intensity": "ratio"},
2323
"M_LR_N152_L": {"seg_data": "SGN_v2", "subtype": ["CR", "Ntng1"], "intensity": "ratio"},
24+
"M_AMD_N180_L": {"seg_data": "SGN_merged", "subtype": ["CR", "Lypd1", "Ntng1"], "intensity": "absolute"},
25+
"M_AMD_N180_R": {"seg_data": "SGN_merged", "subtype": ["CR", "Ntng1"], "intensity": "absolute"},
2426
}
2527

28+
CUSTOM_THRESHOLDS = {
29+
"M_LR_000099_L": {"Lypd1": 0.65},
30+
"M_LR_000184_L": {"Prph": 1},
31+
"M_LR_000260_L": {"Prph": 0.7},
32+
}
2633

2734
def plot_intensity_thresholds(input_dir, output_dir, cochlea, plot=False):
2835
"""Plot histograms for positive and negative populations of subtype markers based on thresholding.
@@ -55,9 +62,15 @@ def plot_intensity_thresholds(input_dir, output_dir, cochlea, plot=False):
5562
table_measurement_path = f"{cochlea}/tables/{data_name}/{stain}_{seg_str}_object-measures.tsv"
5663
column = "median"
5764

58-
rows = 1
59-
columns = number_plots // rows
60-
fig, ax = plt.subplots(rows, columns, figsize=(columns*4, rows*4), sharex=True)
65+
# check for custom threshold
66+
if CUSTOM_THRESHOLDS.get(cochlea, {}).get(stain) is not None:
67+
rows = 2
68+
else:
69+
rows = 1
70+
71+
columns = number_plots
72+
fig, axes = plt.subplots(rows, columns, figsize=(columns*4, rows*4), sharex=True)
73+
ax = axes.flatten()
6174
table_path_s3, fs = get_s3_path(table_measurement_path)
6275
with fs.open(table_path_s3, "r") as f:
6376
table_measurement = pd.read_csv(f, sep="\t")
@@ -72,8 +85,8 @@ def plot_intensity_thresholds(input_dir, output_dir, cochlea, plot=False):
7285
neg_values = list(subset_neg[column])
7386
pos_values = list(subset_pos[column])
7487

75-
bins = np.linspace(min(min(neg_values), min(pos_values)),
76-
max(max(neg_values), max(pos_values)), 30)
88+
bins = np.linspace(min(neg_values + pos_values),
89+
max(neg_values + pos_values), 30)
7790

7891
ax[num].hist(pos_values, bins=bins, alpha=0.6, label='Positive', color='tab:blue')
7992
ax[num].hist(neg_values, bins=bins, alpha=0.6, label='Negative', color='tab:orange')
@@ -82,6 +95,27 @@ def plot_intensity_thresholds(input_dir, output_dir, cochlea, plot=False):
8295
ax[num].legend()
8396
ax[num].set_title(center_str)
8497

98+
if rows == 2:
99+
for num, center_str in enumerate(center_strs):
100+
seg_ids = param_dicts[center_str]["seg_ids"]
101+
threshold = CUSTOM_THRESHOLDS[cochlea][stain]
102+
103+
subset = table_measurement[table_measurement["label_id"].isin(seg_ids)]
104+
subset_neg = subset[(subset[column] < threshold)]
105+
subset_pos = subset[(subset[column] > threshold)]
106+
neg_values = list(subset_neg[column])
107+
pos_values = list(subset_pos[column])
108+
109+
bins = np.linspace(min(neg_values + pos_values),
110+
max(neg_values + pos_values), 30)
111+
112+
ax[columns + num].hist(pos_values, bins=bins, alpha=0.6, label='Positive (custom)', color='tab:blue')
113+
ax[columns + num].hist(neg_values, bins=bins, alpha=0.6, label='Negative (custom)', color='tab:orange')
114+
ax[columns + num].set_ylabel('Count')
115+
ax[columns + num].set_xlabel('Intensity')
116+
ax[columns + num].legend()
117+
ax[columns + num].set_title(center_str)
118+
85119
fig.suptitle(f"{cochlea} - {stain}", fontsize=30)
86120

87121
plt.tight_layout()
@@ -101,14 +135,14 @@ def main():
101135
description="Assign each segmentation instance a marker based on annotation thresholds."
102136
)
103137
parser.add_argument("-c", "--cochlea", type=str, nargs="+", default=COCHLEAE, help="Cochlea(e) to process.")
138+
parser.add_argument("--figure_dir", "-f", type=str, required=True, help="Output directory for plots.")
104139
parser.add_argument("-i", "--input_dir", type=str,
105-
default="/mnt/vast-nhr/projects/nim00007/data/moser/cochlea-lightsheet/mobie_project/cochlea-lightsheet/tables/Subtype_marker", help="Input directory.") #noqa
106-
parser.add_argument("-o", "--output_dir", type=str,
107-
default="/user/schilling40/u15000/plots/SGNsub_thresholds", help="Output directory.")
140+
default="/mnt/vast-nhr/projects/nim00007/data/moser/cochlea-lightsheet/mobie_project/cochlea-lightsheet/tables/Subtype_marker",
141+
help="Directory containing object measures of the cochleae.") #noqa
108142

109143
args = parser.parse_args()
110144
for cochlea in args.cochlea:
111-
plot_intensity_thresholds(args.input_dir, args.output_dir, cochlea)
145+
plot_intensity_thresholds(args.input_dir, args.figure_dir, cochlea)
112146

113147

114148
if __name__ == "__main__":

scripts/measurements/evaluate_marker_annotations_subtype.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,23 @@
1313
# The cochlea for the CHReef analysis.
1414

1515
COCHLEAE = {
16-
"M_LR_000099_L": {"seg_data": "PV_SGN_v2", "subtype": ["Calb1", "Lypd1"], "intensity": "ratio"},
17-
"M_AMD_N180_L": {"seg_data": "SGN_merged", "subtype": ["CR", "Lypd1", "Ntng1"], "intensity": "absolute"},
18-
"M_AMD_N180_R": {"seg_data": "SGN_merged", "subtype": ["CR", "Ntng1"], "intensity": "absolute"},
1916
"M_LR_000098_L": {"seg_data": "SGN_v2", "subtype": ["CR", "Ntng1"], "intensity": "ratio"},
17+
"M_LR_000099_L": {"seg_data": "PV_SGN_v2", "subtype": ["Calb1", "Lypd1"], "intensity": "ratio"},
2018
"M_LR_000184_L": {"seg_data": "SGN_v2", "subtype": ["Prph"], "output_seg": "SGN_v2b", "intensity": "ratio"},
2119
"M_LR_000184_R": {"seg_data": "SGN_v2", "subtype": ["Prph"], "output_seg": "SGN_v2b", "intensity": "ratio"},
2220
"M_LR_000214_L": {"seg_data": "PV_SGN_v2", "subtype": ["Calb1"], "intensity": "ratio"},
2321
"M_LR_000260_L": {"seg_data": "SGN_v2", "subtype": ["Prph", "Tuj1"], "intensity": "ratio"},
22+
"M_LR_N110_L": {"seg_data": "SGN_v2", "subtype": ["Calb1", "Ntng1"], "intensity": "ratio"},
23+
"M_LR_N110_R": {"seg_data": "SGN_v2", "subtype": ["Calb1", "Ntng1"], "intensity": "ratio"},
2424
"M_LR_N152_L": {"seg_data": "SGN_v2", "subtype": ["CR", "Ntng1"], "intensity": "ratio"},
25+
"M_AMD_N180_L": {"seg_data": "SGN_merged", "subtype": ["CR", "Lypd1", "Ntng1"], "intensity": "absolute"},
26+
"M_AMD_N180_R": {"seg_data": "SGN_merged", "subtype": ["CR", "Ntng1"], "intensity": "absolute"},
27+
}
28+
29+
CUSTOM_THRESHOLDS = {
30+
"M_LR_000099_L": {"Lypd1": 0.65},
31+
"M_LR_000184_L": {"Prph": 1},
32+
"M_LR_000260_L": {"Prph": 0.7},
2533
}
2634

2735

@@ -44,7 +52,7 @@ def get_length_fraction_from_center(table, center_str):
4452
return length_fraction
4553

4654

47-
def apply_nearest_threshold(intensity_dic, table_seg, table_measurement, column="median", suffix="labels"):
55+
def apply_nearest_threshold(intensity_dic, table_seg, table_measurement, column="median", suffix="labels", custom_threshold=None):
4856
"""Apply threshold to nearest segmentation instances.
4957
Crop centers are transformed into the "length fraction" parameter of the segmentation table.
5058
This avoids issues with the spiral shape of the cochlea and maps the assignment onto the Rosenthal"s canal.
@@ -54,7 +62,11 @@ def apply_nearest_threshold(intensity_dic, table_seg, table_measurement, column=
5462
for key in intensity_dic.keys():
5563
length_fraction = get_length_fraction_from_center(table_seg, key)
5664
intensity_dic[key]["length_fraction"] = length_fraction
57-
lf_intensity[length_fraction] = {"threshold": intensity_dic[key]["median_intensity"]}
65+
if custom_threshold is None:
66+
lf_intensity[length_fraction] = {"threshold": intensity_dic[key]["median_intensity"]}
67+
else:
68+
print(f"Using custom threshold {custom_threshold} for crop {key}.")
69+
lf_intensity[length_fraction] = {"threshold": custom_threshold}
5870

5971
# get limits for checking marker thresholds
6072
lf_intensity = dict(sorted(lf_intensity.items()))
@@ -273,6 +285,7 @@ def evaluate_marker_annotation(
273285
else:
274286
annot_table = pd.concat([annot_table, get_annotation_table(annot_dic, subtype)], ignore_index=True)
275287

288+
# create dictionary containing median intensity and segmentation ids for every crop
276289
om_dic = get_object_measures(annot_dic, intensity_dic, intensity_mode, subtype)
277290
om_out_path = os.path.join(output_dir, f"{cochlea_str}_{subtype}_om.json")
278291
with open(om_out_path, "w") as f:
@@ -293,8 +306,13 @@ def evaluate_marker_annotation(
293306
table_measurement = pd.read_csv(f, sep="\t")
294307

295308
# Apply the threshold to all SGNs.
309+
if CUSTOM_THRESHOLDS.get(cochlea, {}).get(subtype) is not None:
310+
custom_threshold = CUSTOM_THRESHOLDS[cochlea][subtype]
311+
else:
312+
custom_threshold = None
296313
table_seg = apply_nearest_threshold(
297314
intensity_dic, table_seg, table_measurement, column=column, suffix=subtype,
315+
custom_threshold=custom_threshold
298316
)
299317

300318
# Save the table with positives / negatives for all SGNs.

0 commit comments

Comments
 (0)