Skip to content

Commit c649629

Browse files
authored
Merge pull request #50 from computational-cell-analytics/fix_ihc_tonotopic_mapping
Fix tonotopic mapping of IHC segmentation. Documentation and updated dictionaries have been added for tonotopic mapping of inner hair cells (IHCs) and spiral ganglion neurons (SGNs). For tonotopic mapping of IHCs, a graph with a reasonably high maximal edge distance is created based on all IHC centroid locations. If this approach fails because the components of the IHC spiral are too far apart, the parameter will need to be adjusted manually. While not ideal, this solution seems satisfactory for the near future.
2 parents 1efd0d3 + 48c352c commit c649629

File tree

7 files changed

+143
-84
lines changed

7 files changed

+143
-84
lines changed

flamingo_tools/segmentation/cochlea_mapping.py

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import networkx as nx
55
import numpy as np
66
import pandas as pd
7-
from networkx.algorithms.approximation import steiner_tree
87
from scipy.ndimage import distance_transform_edt, binary_dilation, binary_closing
98
from scipy.interpolate import interp1d
109

@@ -146,28 +145,48 @@ def measure_run_length_sgns(centroids: np.ndarray, scale_factor=10):
146145
return total_distance, path, path_dict
147146

148147

149-
def measure_run_length_ihcs(centroids):
148+
def measure_run_length_ihcs(centroids, max_edge_distance=50):
150149
"""Measure the run lengths of the IHC segmentation
151-
by finding the shortest path between the most distant nodes in a Steiner Tree.
150+
by determining the shortest path between the most distant nodes of a graph.
151+
The graph is created based on a maximal edge distance between nodes.
152+
However, this value may not correspond to the max_edge_distance parameter used to process the IHC segmentation,
153+
which may consist of more than one component.
154+
That's why a large max_edge_distance is used to connect different components and identify
155+
the two most distant nodes and the shortest path between them.
156+
The path is then edited using a moving average filter.
152157
153158
Args:
154159
centroids: Centroids of SGN segmentation.
160+
max_edge_distance: Maximal edge distance between graph nodes to create an edge between nodes.
155161
156162
Returns:
157163
Total distance of the path.
158164
Path as an nd.array of positions.
159165
A dictionary containing the position and the length fraction of each point in the path.
160166
"""
161167
graph = nx.Graph()
162-
for num, pos in enumerate(centroids):
168+
coords = {}
169+
labels = [int(i) for i in range(len(centroids))]
170+
for index, element in zip(labels, centroids):
171+
coords[index] = element
172+
173+
for num, pos in coords.items():
163174
graph.add_node(num, pos=pos)
164-
# approximate Steiner tree and find shortest path between the two most distant nodes
165-
terminals = set(graph.nodes()) # All nodes are required
166-
# Approximate Steiner Tree over all nodes
167-
T = steiner_tree(graph, terminals)
168-
u, v = find_most_distant_nodes(T)
169-
path = nx.shortest_path(T, source=u, target=v)
170-
total_distance = nx.path_weight(T, path, weight="weight")
175+
176+
# TODO: Find cleaner option than the creation of a new graph with edges
177+
# to identify start / end point and shortest path.
178+
179+
# create edges between points whose distance is less than threshold max_edge_distance
180+
for num_i, pos_i in coords.items():
181+
for num_j, pos_j in coords.items():
182+
if num_i < num_j:
183+
dist = math.dist(pos_i, pos_j)
184+
if dist <= max_edge_distance:
185+
graph.add_edge(num_i, num_j, weight=dist)
186+
187+
u, v = find_most_distant_nodes(graph)
188+
path = nx.shortest_path(graph, source=u, target=v)
189+
total_distance = nx.path_weight(graph, path, weight="weight")
171190

172191
# assign relative distance to points on path
173192
path_dict = {}
@@ -180,6 +199,9 @@ def measure_run_length_ihcs(centroids):
180199
path_dict[num + 1] = {"pos": graph.nodes[p]["pos"], "length_fraction": rel_dist}
181200
path_dict[len(path)] = {"pos": graph.nodes[path[-1]]["pos"], "length_fraction": 1}
182201

202+
path_pos = np.array([graph.nodes[p]["pos"] for p in path])
203+
path = moving_average_3d(path_pos, window=5)
204+
183205
return total_distance, path, path_dict
184206

185207

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
[
2+
{
3+
"cochlea": "M_AMD_OTOF1_L",
4+
"image_channel": [
5+
"Apha",
6+
"Vglut3",
7+
"IHC_v4b"
8+
],
9+
"segmentation_channel": "IHC_v4b",
10+
"type": "ihc",
11+
"n_blocks": 6,
12+
"component_list": [
13+
3,
14+
11
15+
],
16+
"halo_size": [
17+
256,
18+
256,
19+
128
20+
],
21+
"crop_centers": [
22+
[
23+
981,
24+
1326,
25+
947
26+
],
27+
[
28+
1281,
29+
731,
30+
699
31+
],
32+
[
33+
645,
34+
872,
35+
377
36+
],
37+
[
38+
614,
39+
1126,
40+
1048
41+
],
42+
[
43+
1017,
44+
453,
45+
1106
46+
],
47+
[
48+
715,
49+
37,
50+
527
51+
]
52+
]
53+
}
54+
]

reproducibility/block_extraction/repro_equidistant_centers.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ def repro_equidistant_centers(
3232

3333
for dic in param_dicts:
3434
cochlea = dic["cochlea"]
35-
img_channel = dic["image_channel"]
3635
seg_channel = dic["segmentation_channel"]
3736

3837
s3_path = os.path.join(f"{cochlea}", "tables", f"{seg_channel}", "default.tsv")
@@ -50,8 +49,7 @@ def repro_equidistant_centers(
5049

5150
centers = equidistant_centers(table, component_label=component_list, cell_type=cell_type, n_blocks=n_blocks)
5251
centers = [[int(c) for c in center] for center in centers]
53-
ddict = {"cochlea": cochlea}
54-
ddict["image_channel"] = img_channel
52+
ddict = dic.copy()
5553
ddict["crop_centers"] = centers
5654
ddict["halo_size"] = halo_size
5755
out_dict.append(ddict)
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
[
22
{
33
"cochlea": "M_LR_000226_L",
4-
"segmentation_channel": "IHC_v3",
4+
"segmentation_channel": "IHC_v4b",
55
"type": "ihc"
66
},
77
{
88
"cochlea": "M_LR_000226_R",
9-
"segmentation_channel": "IHC_v3",
9+
"segmentation_channel": "IHC_v4b",
1010
"type": "ihc"
1111
},
1212
{
1313
"cochlea": "M_LR_000227_L",
14-
"segmentation_channel": "IHC_v3",
14+
"segmentation_channel": "IHC_v4b",
1515
"type": "ihc"
1616
},
1717
{
1818
"cochlea": "M_LR_000227_R",
19-
"segmentation_channel": "IHC_v3",
19+
"segmentation_channel": "IHC_v4b",
2020
"type": "ihc"
2121
}
2222
]

reproducibility/tonotopic_mapping/2025-07-SGN.json

Lines changed: 0 additions & 57 deletions
This file was deleted.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
[
2+
{
3+
"cochlea": "M_LR_000143_L",
4+
"segmentation_channel": "SGN_v2",
5+
"type": "sgn"
6+
},
7+
{
8+
"cochlea": "M_LR_000144_L",
9+
"segmentation_channel": "SGN_v2",
10+
"type": "sgn"
11+
},
12+
{
13+
"cochlea": "M_LR_000145_L",
14+
"segmentation_channel": "SGN_v2",
15+
"type": "sgn"
16+
},
17+
{
18+
"cochlea": "M_LR_000189_L",
19+
"segmentation_channel": "SGN_v2",
20+
"type": "sgn"
21+
},
22+
{
23+
"cochlea": "M_LR_000144_R",
24+
"segmentation_channel": "SGN_v2",
25+
"type": "sgn"
26+
},
27+
{
28+
"cochlea": "M_LR_000145_R",
29+
"segmentation_channel": "SGN_v2",
30+
"type": "sgn"
31+
},
32+
{
33+
"cochlea": "M_LR_000153_R",
34+
"segmentation_channel": "SGN_v2",
35+
"type": "sgn"
36+
},
37+
{
38+
"cochlea": "M_LR_000155_R",
39+
"segmentation_channel": "SGN_v2",
40+
"type": "sgn"
41+
},
42+
{
43+
"cochlea": "M_LR_000189_R",
44+
"segmentation_channel": "SGN_v2",
45+
"type": "sgn"
46+
}
47+
]

reproducibility/tonotopic_mapping/2025-07-SGN_fig2.json

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,21 @@
22
{
33
"cochlea": "M_LR_000226_L",
44
"segmentation_channel": "SGN_v2",
5-
"type": "sgn",
6-
"filter_factor": 0.75
5+
"type": "sgn"
76
},
87
{
98
"cochlea": "M_LR_000226_R",
109
"segmentation_channel": "SGN_v2",
11-
"type": "sgn",
12-
"filter_factor": 0.75
10+
"type": "sgn"
1311
},
1412
{
1513
"cochlea": "M_LR_000227_L",
1614
"segmentation_channel": "SGN_v2",
17-
"type": "sgn",
18-
"max_edge_distance": 70,
19-
"filter_factor": 0.75
15+
"type": "sgn"
2016
},
2117
{
2218
"cochlea": "M_LR_000227_R",
2319
"segmentation_channel": "SGN_v2",
24-
"type": "sgn",
25-
"filter_factor": 0.75
20+
"type": "sgn"
2621
}
2722
]

0 commit comments

Comments
 (0)