Skip to content

Commit 7df8371

Browse files
anna-grimanna-grim
andauthored
Feat filter transitives (#625)
* feat: removes transitive proposals * refactor: relaxed gt generation, less smoothing --------- Co-authored-by: anna-grim <anna.grim@alleninstitute.org>
1 parent 8810da6 commit 7df8371

6 files changed

Lines changed: 55 additions & 52 deletions

File tree

src/neuron_proofreader/proposal_graph.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,15 +104,15 @@ def __init__(
104104

105105
# Instance attributes - Proposals
106106
self.gt_accepts = set()
107-
self.max_proposals_per_leaf = max_proposals_per_leaf
108107
self.merged_ids = set()
109-
self.min_size_with_proposals = min_size_with_proposals
110108
self.n_merges_blocked = 0
111109
self.node_proposals = defaultdict(set)
112110
self.proposals = set()
113111

114112
self.proposal_generator = ProposalGenerator(
115-
self, filter_transitive_proposals=filter_transitive_proposals
113+
self,
114+
max_proposals_per_leaf=max_proposals_per_leaf,
115+
min_size_with_proposals=min_size_with_proposals
116116
)
117117

118118
# Graph Loader
@@ -196,6 +196,37 @@ def _add_edge(self, edge_id, attrs):
196196
self.add_edge(i, j, radius=attrs["radius"], xyz=attrs["xyz"])
197197
self.xyz_to_edge.update({tuple(xyz): edge_id for xyz in attrs["xyz"]})
198198

199+
def relabel_nodes(self):
200+
"""
201+
Reassigns contiguous node IDs and update all dependent structures.
202+
"""
203+
# Set node ids
204+
old_node_ids = np.array(self.nodes, dtype=int)
205+
new_node_ids = np.arange(len(old_node_ids))
206+
207+
# Set edge ids
208+
old_to_new = dict(zip(old_node_ids, new_node_ids))
209+
old_edge_ids = list(self.edges)
210+
old_irr_edge_ids = self.irreducible.edges
211+
edge_attrs = {(i, j): data for i, j, data in self.edges(data=True)}
212+
213+
# Reset graph
214+
self.clear()
215+
for (i, j) in old_edge_ids:
216+
edge_id = (int(old_to_new[i]), int(old_to_new[j]))
217+
self._add_edge(edge_id, edge_attrs[(i, j)])
218+
219+
self.irreducible.clear()
220+
for (i, j) in old_irr_edge_ids:
221+
self.irreducible.add_edge(old_to_new[i], old_to_new[j])
222+
223+
# Update attributes
224+
self.node_radius = self.node_radius[old_node_ids]
225+
self.node_xyz = self.node_xyz[old_node_ids]
226+
self.node_component_id = self.node_component_id[old_node_ids]
227+
228+
self.reassign_component_ids()
229+
199230
def remove_line_fragment(self, i, j):
200231
"""
201232
Deletes nodes "i" and "j" from "graph", where these nodes form a connected

src/neuron_proofreader/skeleton_graph.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,11 +236,12 @@ def relabel_nodes(self):
236236
old_to_new = dict(zip(old_node_ids, new_node_ids))
237237
old_edge_ids = list(self.edges)
238238
old_irr_edge_ids = self.irreducible.edges
239+
edge_attrs = {(i, j): data for i, j, data in self.edges(data=True)}
239240

240241
# Reset graph
241242
self.clear()
242243
for (i, j) in old_edge_ids:
243-
self.add_edge(old_to_new[i], old_to_new[j])
244+
self.add_edge(old_to_new[i], old_to_new[j], **edge_attrs[(i, j)])
244245

245246
self.irreducible.clear()
246247
for (i, j) in old_irr_edge_ids:

src/neuron_proofreader/split_proofreading/groundtruth_generation.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,6 @@ def run(gt_graph, pred_graph):
7474
if dist > 8:
7575
continue
7676

77-
# Check if nodes are connected via a->b->c and a->c
78-
if accepts_graph.is_transitive_connection(proposal):
79-
continue
80-
8177
# Check if proposal is structurally consistent
8278
gt_id = pred_to_gt[id1]
8379
is_consistent = is_structure_consistent(
@@ -120,7 +116,7 @@ def compute_proposal_proj_dist(gt_graph, pred_graph, proposal):
120116
for pt in geometry_util.make_line(xyz_i, xyz_j, n_pts):
121117
dist, _ = gt_graph.kdtree.query(pt)
122118
proj_dists.append(dist)
123-
return np.max(proj_dists)
119+
return np.percentile(proj_dists, 90)
124120

125121

126122
def find_aligned_component(gt_graph, pred_graph, nodes):
@@ -161,10 +157,10 @@ def find_aligned_component(gt_graph, pred_graph, nodes):
161157
gt_id = util.find_best(dists)
162158
dists = np.array(dists[gt_id])
163159
percent_aligned = len(dists) / n_pts
164-
aligned_score = np.mean(dists[dists < np.percentile(dists, 80)])
160+
aligned_score = np.percentile(dists, 60)
165161

166162
# Deterine whether aligned
167-
if (aligned_score < 4 and gt_id) and percent_aligned > 0.6:
163+
if (aligned_score < 7 and gt_id) and percent_aligned > 0.6:
168164
return gt_id
169165
else:
170166
return None

src/neuron_proofreader/split_proofreading/proposal_generation.py

Lines changed: 8 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ def __init__(
2626
self,
2727
graph,
2828
allow_nonleaf_targets=False,
29-
filter_transitive_proposals=False,
3029
max_attempts=2,
30+
max_proposals_per_leaf=3,
31+
min_size_with_proposals=0,
3132
search_scaling_factor=1.5
3233
):
3334
"""
@@ -40,24 +41,24 @@ def __init__(
4041
allow_nonleaf_targets : bool, optional
4142
Indication of whether to generate proposals between leaf and nodes
4243
with degree 2. Default is False.
43-
filter_transitive_proposals : bool, optional
44-
Indication of whether to filter proposals between fragments with a
45-
connection via other proposals and fragments. Default is False.
4644
max_attempts : int, optional
4745
Number of attempts made to generate proposals from a node with
4846
increasing search radii. Default is 2.
47+
max_proposals_per_leaf : bool, optional
48+
Maximum number of proposals generated at each leaf. Default is 3.
49+
min_size_with_proposals : float, optional
50+
Minimum fragment path length required for proposals. Default is 0.
4951
search_scaling_factor : 1.5, optional
5052
Scaling actor used to enlarge search radius for each search.
5153
Default is 2.
5254
"""
5355
# Instance attributes
5456
self.allow_nonleaf_targets = allow_nonleaf_targets
55-
self.filter_transitive_proposals = filter_transitive_proposals
5657
self.graph = graph
5758
self.kdtree = None
5859
self.max_attempts = max_attempts
59-
self.max_proposals_per_leaf = graph.max_proposals_per_leaf
60-
self.min_size_with_proposals = graph.min_size_with_proposals
60+
self.max_proposals_per_leaf = max_proposals_per_leaf
61+
self.min_size_with_proposals = min_size_with_proposals
6162
self.n_proposals_blocked = 0
6263
self.search_scaling_factor = search_scaling_factor
6364

@@ -115,10 +116,6 @@ def __call__(self, initial_radius):
115116
# Add proposal
116117
proposals.add(pair_id)
117118
connections[pair_component_id] = pair_id
118-
119-
# Filter proposals (if applicable)
120-
if self.filter_transitive_proposals:
121-
proposals = self._filter_transitive_proposals(proposals)
122119
return proposals
123120

124121
def find_node_candidates(self, leaf, radius):
@@ -165,30 +162,6 @@ def get_nearby_points(self, leaf, radius):
165162
pts_dict = self.select_closest_components(pts_dict)
166163
return [val["xyz"] for val in pts_dict.values()]
167164

168-
def _filter_transitive_proposals(self, proposals):
169-
"""
170-
Removes proposals that directly connect fragments a and c when an
171-
indirect path exists via a → b → c.
172-
173-
Parameters
174-
----------
175-
proposals : List[Frozenset[int]]
176-
Proposals to be filtered.
177-
"""
178-
# Initializations
179-
graph = deepcopy(self.graph)
180-
proposals = get_sorted_proposals(graph, proposals)
181-
182-
# Main
183-
filtered_proposals = list()
184-
for proposal in proposals:
185-
if not graph.is_transitive_connection(proposal):
186-
i, j = proposal
187-
graph.add_edge(i, j)
188-
graph.add_proposal(i, j)
189-
filtered_proposals.append(proposal)
190-
return filtered_proposals
191-
192165
# --- Helpers ---
193166
def get_closer_endpoint(self, edge, xyz):
194167
"""

src/neuron_proofreader/split_proofreading/split_datasets.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,14 +135,13 @@ def load_graph(self, swc_pointer, is_gt=False):
135135
# Build graph
136136
graph = ProposalGraph(
137137
anisotropy=self.config.anisotropy,
138-
filter_transitive_proposals=True,
139138
min_size=self.config.min_size
140139
)
141140
graph.load(swc_pointer)
142141

143142
# Filter doubles (if applicable)
144143
if not is_gt:
145-
geometry_util.remove_doubles(graph, 160)
144+
geometry_util.remove_doubles(graph, 200)
146145
return graph
147146

148147
# --- Get Data ---

src/neuron_proofreader/utils/geometry_util.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ def fit_spline_1d(pts, k=3, s=None):
174174
Spline fit to the given points.
175175
"""
176176
t = np.linspace(0, 1, len(pts))
177-
s = len(pts) / s if s else len(pts) / 10
177+
s = len(pts) / s if s else len(pts) / 15
178178
return UnivariateSpline(t, pts, k=k, s=s)
179179

180180

@@ -520,11 +520,13 @@ def remove_doubles(graph, max_length):
520520
"""
521521
# Initializations
522522
components = [c for c in nx.connected_components(graph) if len(c) == 2]
523+
iterator = np.argsort([len(c) for c in components])
523524
kdtree = graph.get_kdtree()
524-
525+
if graph.verbose:
526+
iterator = tqdm(iterator, desc="Filter Doubled Fragments")
527+
525528
# Main
526-
desc = "Filter Doubled Fragments"
527-
for idx in tqdm(np.argsort([len(c) for c in components]), desc=desc):
529+
for idx in iterator:
528530
i, j = tuple(components[idx])
529531
if graph.node_component_id[i] in graph.component_id_to_swc_id:
530532
if graph.edge_length((i, j)) < max_length:
@@ -533,6 +535,7 @@ def remove_doubles(graph, max_length):
533535
hits = compute_projections(graph, kdtree, (i, j))
534536
if is_double(hits, n_pts):
535537
graph.remove_line_fragment(i, j)
538+
graph.relabel_nodes()
536539

537540

538541
def compute_projections(graph, kdtree, edge):

0 commit comments

Comments
 (0)