nettracer3d 1.1.9__tar.gz → 1.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nettracer3d-1.1.9/src/nettracer3d.egg-info → nettracer3d-1.2.3}/PKG-INFO +3 -7
- {nettracer3d-1.1.9 → nettracer3d-1.2.3}/README.md +2 -6
- {nettracer3d-1.1.9 → nettracer3d-1.2.3}/pyproject.toml +1 -1
- nettracer3d-1.2.3/src/nettracer3d/branch_stitcher.py +420 -0
- {nettracer3d-1.1.9 → nettracer3d-1.2.3}/src/nettracer3d/filaments.py +29 -43
- {nettracer3d-1.1.9 → nettracer3d-1.2.3}/src/nettracer3d/nettracer.py +63 -24
- {nettracer3d-1.1.9 → nettracer3d-1.2.3}/src/nettracer3d/nettracer_gui.py +110 -37
- {nettracer3d-1.1.9 → nettracer3d-1.2.3}/src/nettracer3d/tutorial.py +22 -1
- {nettracer3d-1.1.9 → nettracer3d-1.2.3/src/nettracer3d.egg-info}/PKG-INFO +3 -7
- {nettracer3d-1.1.9 → nettracer3d-1.2.3}/src/nettracer3d.egg-info/SOURCES.txt +1 -0
- {nettracer3d-1.1.9 → nettracer3d-1.2.3}/LICENSE +0 -0
- {nettracer3d-1.1.9 → nettracer3d-1.2.3}/setup.cfg +0 -0
- {nettracer3d-1.1.9 → nettracer3d-1.2.3}/src/nettracer3d/__init__.py +0 -0
- {nettracer3d-1.1.9 → nettracer3d-1.2.3}/src/nettracer3d/cellpose_manager.py +0 -0
- {nettracer3d-1.1.9 → nettracer3d-1.2.3}/src/nettracer3d/community_extractor.py +0 -0
- {nettracer3d-1.1.9 → nettracer3d-1.2.3}/src/nettracer3d/excelotron.py +0 -0
- {nettracer3d-1.1.9 → nettracer3d-1.2.3}/src/nettracer3d/modularity.py +0 -0
- {nettracer3d-1.1.9 → nettracer3d-1.2.3}/src/nettracer3d/morphology.py +0 -0
- {nettracer3d-1.1.9 → nettracer3d-1.2.3}/src/nettracer3d/neighborhoods.py +0 -0
- {nettracer3d-1.1.9 → nettracer3d-1.2.3}/src/nettracer3d/network_analysis.py +0 -0
- {nettracer3d-1.1.9 → nettracer3d-1.2.3}/src/nettracer3d/network_draw.py +0 -0
- {nettracer3d-1.1.9 → nettracer3d-1.2.3}/src/nettracer3d/node_draw.py +0 -0
- {nettracer3d-1.1.9 → nettracer3d-1.2.3}/src/nettracer3d/painting.py +0 -0
- {nettracer3d-1.1.9 → nettracer3d-1.2.3}/src/nettracer3d/proximity.py +0 -0
- {nettracer3d-1.1.9 → nettracer3d-1.2.3}/src/nettracer3d/run.py +0 -0
- {nettracer3d-1.1.9 → nettracer3d-1.2.3}/src/nettracer3d/segmenter.py +0 -0
- {nettracer3d-1.1.9 → nettracer3d-1.2.3}/src/nettracer3d/segmenter_GPU.py +0 -0
- {nettracer3d-1.1.9 → nettracer3d-1.2.3}/src/nettracer3d/simple_network.py +0 -0
- {nettracer3d-1.1.9 → nettracer3d-1.2.3}/src/nettracer3d/smart_dilate.py +0 -0
- {nettracer3d-1.1.9 → nettracer3d-1.2.3}/src/nettracer3d/stats.py +0 -0
- {nettracer3d-1.1.9 → nettracer3d-1.2.3}/src/nettracer3d.egg-info/dependency_links.txt +0 -0
- {nettracer3d-1.1.9 → nettracer3d-1.2.3}/src/nettracer3d.egg-info/entry_points.txt +0 -0
- {nettracer3d-1.1.9 → nettracer3d-1.2.3}/src/nettracer3d.egg-info/requires.txt +0 -0
- {nettracer3d-1.1.9 → nettracer3d-1.2.3}/src/nettracer3d.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nettracer3d
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.3
|
|
4
4
|
Summary: Scripts for intializing and analyzing networks from segmentations of three dimensional images.
|
|
5
5
|
Author-email: Liam McLaughlin <liamm@wustl.edu>
|
|
6
6
|
Project-URL: Documentation, https://nettracer3d.readthedocs.io/en/latest/
|
|
@@ -110,12 +110,8 @@ McLaughlin, L., Zhang, B., Sharma, S. et al. Three dimensional multiscalar neuro
|
|
|
110
110
|
|
|
111
111
|
NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
|
|
112
112
|
|
|
113
|
-
-- Version 1.
|
|
113
|
+
-- Version 1.2.3 Updates --
|
|
114
114
|
|
|
115
|
-
*
|
|
116
|
-
* Fixed branch functions not working correctly when a temporary downsample was being applied
|
|
117
|
-
* Added the 'filaments tracer' which can be used to improve vessel based segmentations
|
|
118
|
-
* Removed options for 'cubic' downsampling during processing as this option actually made outputs worse.
|
|
119
|
-
* Rearranged the 'Analyze -> Stats' menu
|
|
115
|
+
* Tweaked the 'branch unification params' - now hard rejects any sharp forks in the branches so branches will mostly be straight.
|
|
120
116
|
|
|
121
117
|
|
|
@@ -65,12 +65,8 @@ McLaughlin, L., Zhang, B., Sharma, S. et al. Three dimensional multiscalar neuro
|
|
|
65
65
|
|
|
66
66
|
NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
|
|
67
67
|
|
|
68
|
-
-- Version 1.
|
|
68
|
+
-- Version 1.2.3 Updates --
|
|
69
69
|
|
|
70
|
-
*
|
|
71
|
-
* Fixed branch functions not working correctly when a temporary downsample was being applied
|
|
72
|
-
* Added the 'filaments tracer' which can be used to improve vessel based segmentations
|
|
73
|
-
* Removed options for 'cubic' downsampling during processing as this option actually made outputs worse.
|
|
74
|
-
* Rearranged the 'Analyze -> Stats' menu
|
|
70
|
+
* Tweaked the 'branch unification params' - now hard rejects any sharp forks in the branches so branches will mostly be straight.
|
|
75
71
|
|
|
76
72
|
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import networkx as nx
|
|
3
|
+
from . import nettracer as n3d
|
|
4
|
+
from scipy.ndimage import distance_transform_edt, gaussian_filter, binary_fill_holes
|
|
5
|
+
from scipy.spatial import cKDTree
|
|
6
|
+
from skimage.morphology import remove_small_objects, skeletonize
|
|
7
|
+
import warnings
|
|
8
|
+
warnings.filterwarnings('ignore')
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class VesselDenoiser:
|
|
12
|
+
"""
|
|
13
|
+
Denoise vessel segmentations using graph-based geometric features
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self,
|
|
17
|
+
score_thresh = 2):
|
|
18
|
+
|
|
19
|
+
self.score_thresh = score_thresh
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def select_kernel_points_topology(self, data, skeleton):
|
|
23
|
+
"""
|
|
24
|
+
ENDPOINTS ONLY version: Returns only skeleton endpoints (degree=1 nodes)
|
|
25
|
+
"""
|
|
26
|
+
skeleton_coords = np.argwhere(skeleton)
|
|
27
|
+
if len(skeleton_coords) == 0:
|
|
28
|
+
return skeleton_coords
|
|
29
|
+
|
|
30
|
+
# Map coord -> index
|
|
31
|
+
coord_to_idx = {tuple(c): i for i, c in enumerate(skeleton_coords)}
|
|
32
|
+
|
|
33
|
+
# Build full 26-connected skeleton graph
|
|
34
|
+
skel_graph = nx.Graph()
|
|
35
|
+
for i, c in enumerate(skeleton_coords):
|
|
36
|
+
skel_graph.add_node(i, pos=c)
|
|
37
|
+
|
|
38
|
+
nbr_offsets = [(dz, dy, dx)
|
|
39
|
+
for dz in (-1, 0, 1)
|
|
40
|
+
for dy in (-1, 0, 1)
|
|
41
|
+
for dx in (-1, 0, 1)
|
|
42
|
+
if not (dz == dy == dx == 0)]
|
|
43
|
+
|
|
44
|
+
for i, c in enumerate(skeleton_coords):
|
|
45
|
+
cz, cy, cx = c
|
|
46
|
+
for dz, dy, dx in nbr_offsets:
|
|
47
|
+
nb = (cz + dz, cy + dy, cx + dx)
|
|
48
|
+
j = coord_to_idx.get(nb, None)
|
|
49
|
+
if j is not None and j > i:
|
|
50
|
+
skel_graph.add_edge(i, j)
|
|
51
|
+
|
|
52
|
+
# Get degree per voxel
|
|
53
|
+
deg = dict(skel_graph.degree())
|
|
54
|
+
|
|
55
|
+
# ONLY keep endpoints (degree=1)
|
|
56
|
+
endpoints = {i for i, d in deg.items() if d == 1}
|
|
57
|
+
|
|
58
|
+
# Return endpoint coordinates
|
|
59
|
+
kernel_coords = np.array([skeleton_coords[i] for i in endpoints])
|
|
60
|
+
return kernel_coords
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def extract_kernel_features(self, skeleton, distance_map, kernel_pos, radius=5):
|
|
64
|
+
"""Extract geometric features for a kernel at a skeleton point"""
|
|
65
|
+
z, y, x = kernel_pos
|
|
66
|
+
shape = skeleton.shape
|
|
67
|
+
|
|
68
|
+
features = {}
|
|
69
|
+
|
|
70
|
+
# Vessel radius at this point
|
|
71
|
+
features['radius'] = distance_map[z, y, x]
|
|
72
|
+
|
|
73
|
+
# Local skeleton density (connectivity measure)
|
|
74
|
+
z_min = max(0, z - radius)
|
|
75
|
+
z_max = min(shape[0], z + radius + 1)
|
|
76
|
+
y_min = max(0, y - radius)
|
|
77
|
+
y_max = min(shape[1], y + radius + 1)
|
|
78
|
+
x_min = max(0, x - radius)
|
|
79
|
+
x_max = min(shape[2], x + radius + 1)
|
|
80
|
+
|
|
81
|
+
local_region = skeleton[z_min:z_max, y_min:y_max, x_min:x_max]
|
|
82
|
+
features['local_density'] = np.sum(local_region) / max(local_region.size, 1)
|
|
83
|
+
|
|
84
|
+
# Local direction vector
|
|
85
|
+
features['direction'] = self._compute_local_direction(
|
|
86
|
+
skeleton, kernel_pos, radius
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Position
|
|
90
|
+
features['pos'] = np.array(kernel_pos)
|
|
91
|
+
|
|
92
|
+
# ALL kernels are endpoints in this version
|
|
93
|
+
features['is_endpoint'] = True
|
|
94
|
+
|
|
95
|
+
return features
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _compute_local_direction(self, skeleton, pos, radius=5):
|
|
99
|
+
"""Compute principal direction of skeleton in local neighborhood"""
|
|
100
|
+
z, y, x = pos
|
|
101
|
+
shape = skeleton.shape
|
|
102
|
+
|
|
103
|
+
z_min = max(0, z - radius)
|
|
104
|
+
z_max = min(shape[0], z + radius + 1)
|
|
105
|
+
y_min = max(0, y - radius)
|
|
106
|
+
y_max = min(shape[1], y + radius + 1)
|
|
107
|
+
x_min = max(0, x - radius)
|
|
108
|
+
x_max = min(shape[2], x + radius + 1)
|
|
109
|
+
|
|
110
|
+
local_skel = skeleton[z_min:z_max, y_min:y_max, x_min:x_max]
|
|
111
|
+
coords = np.argwhere(local_skel)
|
|
112
|
+
|
|
113
|
+
if len(coords) < 2:
|
|
114
|
+
return np.array([0., 0., 1.])
|
|
115
|
+
|
|
116
|
+
# PCA to find principal direction
|
|
117
|
+
centered = coords - coords.mean(axis=0)
|
|
118
|
+
cov = np.cov(centered.T)
|
|
119
|
+
eigenvalues, eigenvectors = np.linalg.eigh(cov)
|
|
120
|
+
principal_direction = eigenvectors[:, -1] # largest eigenvalue
|
|
121
|
+
|
|
122
|
+
return principal_direction / (np.linalg.norm(principal_direction) + 1e-10)
|
|
123
|
+
|
|
124
|
+
def group_endpoints_by_vertex(self, skeleton_points, verts):
|
|
125
|
+
"""
|
|
126
|
+
Group endpoints by which vertex (labeled blob) they belong to
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
--------
|
|
130
|
+
vertex_to_endpoints : dict
|
|
131
|
+
Dictionary mapping vertex_label -> [list of endpoint indices]
|
|
132
|
+
"""
|
|
133
|
+
vertex_to_endpoints = {}
|
|
134
|
+
|
|
135
|
+
for idx, pos in enumerate(skeleton_points):
|
|
136
|
+
z, y, x = pos.astype(int)
|
|
137
|
+
vertex_label = int(verts[z, y, x])
|
|
138
|
+
|
|
139
|
+
# Skip if endpoint is not in any vertex (label=0)
|
|
140
|
+
if vertex_label == 0:
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
if vertex_label not in vertex_to_endpoints:
|
|
144
|
+
vertex_to_endpoints[vertex_label] = []
|
|
145
|
+
|
|
146
|
+
vertex_to_endpoints[vertex_label].append(idx)
|
|
147
|
+
|
|
148
|
+
return vertex_to_endpoints
|
|
149
|
+
|
|
150
|
+
def compute_edge_features(self, feat_i, feat_j):
|
|
151
|
+
"""
|
|
152
|
+
Compute features for potential connection between two endpoints
|
|
153
|
+
NO DISTANCE-BASED FEATURES - only radius and direction
|
|
154
|
+
"""
|
|
155
|
+
features = {}
|
|
156
|
+
|
|
157
|
+
# Euclidean distance (for reference only, not used in scoring)
|
|
158
|
+
pos_diff = feat_j['pos'] - feat_i['pos']
|
|
159
|
+
features['distance'] = np.linalg.norm(pos_diff)
|
|
160
|
+
|
|
161
|
+
# Radius similarity
|
|
162
|
+
r_i, r_j = feat_i['radius'], feat_j['radius']
|
|
163
|
+
features['radius_diff'] = abs(r_i - r_j)
|
|
164
|
+
features['radius_ratio'] = min(r_i, r_j) / (max(r_i, r_j) + 1e-10)
|
|
165
|
+
features['mean_radius'] = (r_i + r_j) / 2.0
|
|
166
|
+
|
|
167
|
+
# Direction alignment
|
|
168
|
+
direction_vec = pos_diff / (features['distance'] + 1e-10)
|
|
169
|
+
|
|
170
|
+
# Alignment with both local directions
|
|
171
|
+
align_i = abs(np.dot(feat_i['direction'], direction_vec))
|
|
172
|
+
align_j = abs(np.dot(feat_j['direction'], direction_vec))
|
|
173
|
+
features['alignment'] = (align_i + align_j) / 2.0
|
|
174
|
+
|
|
175
|
+
# Smoothness: how well does connection align with both local directions
|
|
176
|
+
features['smoothness'] = min(align_i, align_j)
|
|
177
|
+
|
|
178
|
+
# Density similarity
|
|
179
|
+
features['density_diff'] = abs(feat_i['local_density'] - feat_j['local_density'])
|
|
180
|
+
|
|
181
|
+
return features
|
|
182
|
+
|
|
183
|
+
def score_connection(self, edge_features):
|
|
184
|
+
score = 0.0
|
|
185
|
+
|
|
186
|
+
# HARD REJECT for definite forks/sharp turns
|
|
187
|
+
if edge_features['smoothness'] < 0.5: # At least one endpoint pointing away
|
|
188
|
+
return -999
|
|
189
|
+
|
|
190
|
+
# Base similarity scoring
|
|
191
|
+
score += edge_features['radius_ratio'] * 10.0
|
|
192
|
+
score += edge_features['alignment'] * 8.0
|
|
193
|
+
score += edge_features['smoothness'] * 6.0
|
|
194
|
+
score -= edge_features['density_diff'] * 0.5
|
|
195
|
+
|
|
196
|
+
# PENALTY for poor directional alignment (punish forks!)
|
|
197
|
+
# Alignment < 0.5 means vessels are pointing in different directions
|
|
198
|
+
# This doesn't trigger that often so it might be redundant with the above step
|
|
199
|
+
if edge_features['alignment'] < 0.5:
|
|
200
|
+
penalty = (0.5 - edge_features['alignment']) * 15.0
|
|
201
|
+
score -= penalty
|
|
202
|
+
|
|
203
|
+
# ADDITIONAL PENALTY for sharp turns/forks --- no longer in use since we now hard reject these, but I left this in here to reverse it later potentially
|
|
204
|
+
# Smoothness < 0.4 means at least one endpoint points away
|
|
205
|
+
#if edge_features['smoothness'] < 0.4:
|
|
206
|
+
# penalty = (0.4 - edge_features['smoothness']) * 20.0
|
|
207
|
+
# score -= penalty
|
|
208
|
+
|
|
209
|
+
# Size bonus: ONLY if vessels already match well
|
|
210
|
+
|
|
211
|
+
if edge_features['radius_ratio'] > 0.7 and edge_features['alignment'] > 0.5:
|
|
212
|
+
mean_radius = edge_features['mean_radius']
|
|
213
|
+
score += mean_radius * 1.5
|
|
214
|
+
|
|
215
|
+
return score
|
|
216
|
+
|
|
217
|
+
def connect_vertices_across_gaps(self, skeleton_points, kernel_features,
|
|
218
|
+
labeled_skeleton, vertex_to_endpoints, verbose=False):
|
|
219
|
+
"""
|
|
220
|
+
Connect vertices by finding best endpoint pair across each vertex
|
|
221
|
+
Each vertex makes at most one connection
|
|
222
|
+
"""
|
|
223
|
+
# Initialize label dictionary: label -> label (identity mapping)
|
|
224
|
+
unique_labels = np.unique(labeled_skeleton[labeled_skeleton > 0])
|
|
225
|
+
label_dict = {int(label): int(label) for label in unique_labels}
|
|
226
|
+
|
|
227
|
+
# Map endpoint index to its skeleton label
|
|
228
|
+
endpoint_to_label = {}
|
|
229
|
+
for idx, pos in enumerate(skeleton_points):
|
|
230
|
+
z, y, x = pos.astype(int)
|
|
231
|
+
label = int(labeled_skeleton[z, y, x])
|
|
232
|
+
endpoint_to_label[idx] = label
|
|
233
|
+
|
|
234
|
+
# Find root label (union-find helper)
|
|
235
|
+
def find_root(label):
|
|
236
|
+
root = label
|
|
237
|
+
while label_dict[root] != root:
|
|
238
|
+
root = label_dict[root]
|
|
239
|
+
return root
|
|
240
|
+
|
|
241
|
+
# Iterate through each vertex
|
|
242
|
+
for vertex_label, endpoint_indices in vertex_to_endpoints.items():
|
|
243
|
+
if len(endpoint_indices) < 2:
|
|
244
|
+
# Need at least 2 endpoints to make a connection
|
|
245
|
+
continue
|
|
246
|
+
|
|
247
|
+
if verbose and len(endpoint_indices) > 0:
|
|
248
|
+
print(f"\nVertex {vertex_label}: {len(endpoint_indices)} endpoints")
|
|
249
|
+
|
|
250
|
+
# Find best pair of endpoints to connect
|
|
251
|
+
best_i = None
|
|
252
|
+
best_j = None
|
|
253
|
+
best_score = -np.inf
|
|
254
|
+
|
|
255
|
+
# Try all pairs of endpoints within this vertex
|
|
256
|
+
for i in range(len(endpoint_indices)):
|
|
257
|
+
for j in range(i + 1, len(endpoint_indices)):
|
|
258
|
+
idx_i = endpoint_indices[i]
|
|
259
|
+
idx_j = endpoint_indices[j]
|
|
260
|
+
|
|
261
|
+
feat_i = kernel_features[idx_i]
|
|
262
|
+
feat_j = kernel_features[idx_j]
|
|
263
|
+
|
|
264
|
+
label_i = endpoint_to_label[idx_i]
|
|
265
|
+
label_j = endpoint_to_label[idx_j]
|
|
266
|
+
|
|
267
|
+
root_i = find_root(label_i)
|
|
268
|
+
root_j = find_root(label_j)
|
|
269
|
+
|
|
270
|
+
# Skip if already unified
|
|
271
|
+
if root_i == root_j:
|
|
272
|
+
continue
|
|
273
|
+
|
|
274
|
+
# Compute edge features (no skeleton needed, no distance penalty)
|
|
275
|
+
edge_feat = self.compute_edge_features(feat_i, feat_j)
|
|
276
|
+
|
|
277
|
+
# Score this connection
|
|
278
|
+
score = self.score_connection(edge_feat)
|
|
279
|
+
|
|
280
|
+
# Apply threshold
|
|
281
|
+
if score > self.score_thresh and score > best_score:
|
|
282
|
+
best_score = score
|
|
283
|
+
best_i = idx_i
|
|
284
|
+
best_j = idx_j
|
|
285
|
+
|
|
286
|
+
# Make the best connection for this vertex
|
|
287
|
+
if best_i is not None and best_j is not None:
|
|
288
|
+
label_i = endpoint_to_label[best_i]
|
|
289
|
+
label_j = endpoint_to_label[best_j]
|
|
290
|
+
|
|
291
|
+
root_i = find_root(label_i)
|
|
292
|
+
root_j = find_root(label_j)
|
|
293
|
+
|
|
294
|
+
# Unify labels: point larger label to smaller label
|
|
295
|
+
if root_i < root_j:
|
|
296
|
+
label_dict[root_j] = root_i
|
|
297
|
+
unified_label = root_i
|
|
298
|
+
else:
|
|
299
|
+
label_dict[root_i] = root_j
|
|
300
|
+
unified_label = root_j
|
|
301
|
+
|
|
302
|
+
if verbose:
|
|
303
|
+
feat_i = kernel_features[best_i]
|
|
304
|
+
feat_j = kernel_features[best_j]
|
|
305
|
+
print(f" ✓ Connected labels {label_i} <-> {label_j} (unified as {unified_label})")
|
|
306
|
+
print(f" Score: {best_score:.2f} | Radii: {feat_i['radius']:.1f}, {feat_j['radius']:.1f}")
|
|
307
|
+
|
|
308
|
+
return label_dict
|
|
309
|
+
|
|
310
|
+
def denoise(self, data, skeleton, labeled_skeleton, verts, verbose=False):
|
|
311
|
+
"""
|
|
312
|
+
Main pipeline: unify skeleton labels by connecting endpoints at vertices
|
|
313
|
+
|
|
314
|
+
Parameters:
|
|
315
|
+
-----------
|
|
316
|
+
data : ndarray
|
|
317
|
+
3D binary segmentation (for distance transform)
|
|
318
|
+
skeleton : ndarray
|
|
319
|
+
3D binary skeleton
|
|
320
|
+
labeled_skeleton : ndarray
|
|
321
|
+
Labeled skeleton (each branch has unique label)
|
|
322
|
+
verts : ndarray
|
|
323
|
+
Labeled vertices (blobs where branches meet)
|
|
324
|
+
verbose : bool
|
|
325
|
+
Print progress
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
--------
|
|
329
|
+
label_dict : dict
|
|
330
|
+
Dictionary mapping old labels to unified labels
|
|
331
|
+
"""
|
|
332
|
+
if verbose:
|
|
333
|
+
print("Starting skeleton label unification...")
|
|
334
|
+
print(f"Initial unique labels: {len(np.unique(labeled_skeleton[labeled_skeleton > 0]))}")
|
|
335
|
+
|
|
336
|
+
# Compute distance transform
|
|
337
|
+
if verbose:
|
|
338
|
+
print("Computing distance transform...")
|
|
339
|
+
distance_map = distance_transform_edt(data)
|
|
340
|
+
|
|
341
|
+
# Extract endpoints
|
|
342
|
+
if verbose:
|
|
343
|
+
print("Extracting skeleton endpoints...")
|
|
344
|
+
kernel_points = self.select_kernel_points_topology(data, skeleton)
|
|
345
|
+
|
|
346
|
+
if verbose:
|
|
347
|
+
print(f"Found {len(kernel_points)} endpoints")
|
|
348
|
+
|
|
349
|
+
# Group endpoints by vertex
|
|
350
|
+
if verbose:
|
|
351
|
+
print("Grouping endpoints by vertex...")
|
|
352
|
+
vertex_to_endpoints = self.group_endpoints_by_vertex(kernel_points, verts)
|
|
353
|
+
|
|
354
|
+
if verbose:
|
|
355
|
+
print(f"Found {len(vertex_to_endpoints)} vertices with endpoints")
|
|
356
|
+
vertices_with_multiple = sum(1 for v in vertex_to_endpoints.values() if len(v) >= 2)
|
|
357
|
+
print(f" {vertices_with_multiple} vertices have 2+ endpoints (connection candidates)")
|
|
358
|
+
|
|
359
|
+
# Extract features for each endpoint
|
|
360
|
+
if verbose:
|
|
361
|
+
print("Extracting endpoint features...")
|
|
362
|
+
kernel_features = []
|
|
363
|
+
for pt in kernel_points:
|
|
364
|
+
feat = self.extract_kernel_features(skeleton, distance_map, pt)
|
|
365
|
+
kernel_features.append(feat)
|
|
366
|
+
|
|
367
|
+
# Connect vertices
|
|
368
|
+
if verbose:
|
|
369
|
+
print("Connecting endpoints at vertices...")
|
|
370
|
+
label_dict = self.connect_vertices_across_gaps(
|
|
371
|
+
kernel_points, kernel_features, labeled_skeleton,
|
|
372
|
+
vertex_to_endpoints, verbose
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
# Compress label dictionary (path compression for union-find)
|
|
376
|
+
if verbose:
|
|
377
|
+
print("\nCompressing label mappings...")
|
|
378
|
+
for label in list(label_dict.keys()):
|
|
379
|
+
root = label
|
|
380
|
+
while label_dict[root] != root:
|
|
381
|
+
root = label_dict[root]
|
|
382
|
+
label_dict[label] = root
|
|
383
|
+
|
|
384
|
+
# Count final unified components
|
|
385
|
+
final_labels = set(label_dict.values())
|
|
386
|
+
if verbose:
|
|
387
|
+
print(f"Final unified labels: {len(final_labels)}")
|
|
388
|
+
print(f"Reduced from {len(label_dict)} to {len(final_labels)} components")
|
|
389
|
+
|
|
390
|
+
return label_dict
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def trace(data, labeled_skeleton, verts, score_thresh=10, verbose=False):
|
|
394
|
+
"""
|
|
395
|
+
Trace and unify skeleton labels using vertex-based endpoint grouping
|
|
396
|
+
"""
|
|
397
|
+
skeleton = n3d.binarize(labeled_skeleton)
|
|
398
|
+
|
|
399
|
+
# Create denoiser
|
|
400
|
+
denoiser = VesselDenoiser(score_thresh=score_thresh)
|
|
401
|
+
|
|
402
|
+
# Run label unification
|
|
403
|
+
label_dict = denoiser.denoise(data, skeleton, labeled_skeleton, verts, verbose=verbose)
|
|
404
|
+
|
|
405
|
+
# Apply unified labels efficiently (SINGLE PASS)
|
|
406
|
+
# Create lookup array: index by old label, get new label
|
|
407
|
+
max_label = np.max(labeled_skeleton)
|
|
408
|
+
label_map = np.arange(max_label + 1) # Identity mapping by default
|
|
409
|
+
|
|
410
|
+
for old_label, new_label in label_dict.items():
|
|
411
|
+
label_map[old_label] = new_label
|
|
412
|
+
|
|
413
|
+
# Single array indexing operation
|
|
414
|
+
relabeled_skeleton = label_map[labeled_skeleton]
|
|
415
|
+
|
|
416
|
+
return relabeled_skeleton
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
if __name__ == "__main__":
|
|
420
|
+
print("Test area")
|
|
@@ -633,8 +633,6 @@ class VesselDenoiser:
|
|
|
633
633
|
edge_feat["skeleton_steps"] = steps
|
|
634
634
|
G.add_edge(k_id, j_id, **edge_feat)
|
|
635
635
|
|
|
636
|
-
# CRITICAL FIX FOR ISSUE 1: Second pass to catch any directly adjacent kernels
|
|
637
|
-
# that might have been missed due to complex topology
|
|
638
636
|
# This ensures ALL kernels that are neighbors in the skeleton are connected
|
|
639
637
|
for k_id, s_idx in kernel_to_skel_idx.items():
|
|
640
638
|
# Check all neighbors of this kernel in the skeleton
|
|
@@ -656,24 +654,20 @@ class VesselDenoiser:
|
|
|
656
654
|
def connect_endpoints_across_gaps(self, G, skeleton_points, kernel_features, skeleton):
|
|
657
655
|
"""
|
|
658
656
|
Second stage: Let endpoints reach out to connect across gaps
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
Strategy:
|
|
662
|
-
- Within same component: reconnect broken paths (gap filling)
|
|
663
|
-
- Between components: only connect if strong evidence (path support, alignment)
|
|
657
|
+
Optimized version using Union-Find for fast connectivity checks
|
|
664
658
|
"""
|
|
659
|
+
from scipy.cluster.hierarchy import DisjointSet
|
|
660
|
+
|
|
665
661
|
# Identify all endpoints
|
|
666
662
|
endpoint_nodes = [i for i, feat in enumerate(kernel_features) if feat['is_endpoint']]
|
|
667
663
|
|
|
668
664
|
if len(endpoint_nodes) == 0:
|
|
669
665
|
return G
|
|
670
666
|
|
|
671
|
-
#
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
for node in comp:
|
|
676
|
-
node_to_component[node] = comp_idx
|
|
667
|
+
# Initialize Union-Find with existing graph connections
|
|
668
|
+
ds = DisjointSet(range(len(skeleton_points)))
|
|
669
|
+
for u, v in G.edges():
|
|
670
|
+
ds.merge(u, v)
|
|
677
671
|
|
|
678
672
|
# Build KD-tree for all points
|
|
679
673
|
tree = cKDTree(skeleton_points)
|
|
@@ -682,7 +676,6 @@ class VesselDenoiser:
|
|
|
682
676
|
feat_i = kernel_features[endpoint_idx]
|
|
683
677
|
pos_i = skeleton_points[endpoint_idx]
|
|
684
678
|
direction_i = feat_i['direction']
|
|
685
|
-
component_i = node_to_component.get(endpoint_idx, -1)
|
|
686
679
|
|
|
687
680
|
# Use radius-aware connection distance
|
|
688
681
|
if self.radius_aware_distance:
|
|
@@ -698,32 +691,24 @@ class VesselDenoiser:
|
|
|
698
691
|
if endpoint_idx == j:
|
|
699
692
|
continue
|
|
700
693
|
|
|
701
|
-
#
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
continue
|
|
705
|
-
except:
|
|
706
|
-
pass
|
|
694
|
+
# FAST connectivity check - O(1) amortized instead of O(V+E)
|
|
695
|
+
if ds.connected(endpoint_idx, j):
|
|
696
|
+
continue
|
|
707
697
|
|
|
708
698
|
feat_j = kernel_features[j]
|
|
709
699
|
pos_j = skeleton_points[j]
|
|
710
700
|
is_endpoint_j = feat_j['is_endpoint']
|
|
711
|
-
component_j = node_to_component.get(j, -1)
|
|
712
701
|
|
|
713
|
-
# Check if they're in the same component
|
|
714
|
-
same_component = (
|
|
702
|
+
# Check if they're in the same component (using union-find)
|
|
703
|
+
same_component = ds.connected(endpoint_idx, j)
|
|
715
704
|
|
|
716
|
-
# Check directionality
|
|
705
|
+
# Check directionality
|
|
717
706
|
to_target = pos_j - pos_i
|
|
718
707
|
to_target_normalized = to_target / (np.linalg.norm(to_target) + 1e-10)
|
|
719
708
|
direction_dot = np.dot(direction_i, to_target_normalized)
|
|
720
709
|
|
|
721
710
|
# Compute edge features
|
|
722
|
-
edge_feat = self.compute_edge_features(
|
|
723
|
-
feat_i,
|
|
724
|
-
feat_j,
|
|
725
|
-
skeleton
|
|
726
|
-
)
|
|
711
|
+
edge_feat = self.compute_edge_features(feat_i, feat_j, skeleton)
|
|
727
712
|
|
|
728
713
|
# Decide based on component membership
|
|
729
714
|
should_connect = False
|
|
@@ -731,20 +716,16 @@ class VesselDenoiser:
|
|
|
731
716
|
if same_component:
|
|
732
717
|
should_connect = True
|
|
733
718
|
else:
|
|
734
|
-
# Different components
|
|
735
|
-
# Require STRONG evidence
|
|
719
|
+
# Different components - require STRONG evidence
|
|
736
720
|
if edge_feat['path_support'] > 0.5:
|
|
737
|
-
# Strong skeleton path exists
|
|
738
721
|
should_connect = True
|
|
739
|
-
elif direction_dot > 0.3 and edge_feat['radius_ratio'] > 0.5:
|
|
740
|
-
# Well aligned forward, similar radius
|
|
741
|
-
#if is_endpoint_j: # Both endpoints
|
|
722
|
+
elif direction_dot > 0.3 and edge_feat['radius_ratio'] > 0.5:
|
|
742
723
|
score = self.score_connection(edge_feat)
|
|
743
|
-
if score > self.score_thresh:
|
|
724
|
+
if score > self.score_thresh:
|
|
744
725
|
should_connect = True
|
|
745
|
-
elif edge_feat['radius_ratio'] > 0.7:
|
|
726
|
+
elif edge_feat['radius_ratio'] > 0.7:
|
|
746
727
|
score = self.score_connection(edge_feat)
|
|
747
|
-
if score > self.score_thresh:
|
|
728
|
+
if score > self.score_thresh:
|
|
748
729
|
should_connect = True
|
|
749
730
|
|
|
750
731
|
# Special check: if j is internal node, require alignment
|
|
@@ -754,6 +735,8 @@ class VesselDenoiser:
|
|
|
754
735
|
|
|
755
736
|
if should_connect:
|
|
756
737
|
G.add_edge(endpoint_idx, j, **edge_feat)
|
|
738
|
+
# Update union-find structure immediately
|
|
739
|
+
ds.merge(endpoint_idx, j)
|
|
757
740
|
|
|
758
741
|
return G
|
|
759
742
|
|
|
@@ -807,7 +790,7 @@ class VesselDenoiser:
|
|
|
807
790
|
degrees = [G.degree(n) for n in component]
|
|
808
791
|
|
|
809
792
|
# Component statistics
|
|
810
|
-
size = len(component)
|
|
793
|
+
size = len(component) * self.kernel_spacing
|
|
811
794
|
mean_radius = np.mean(radii)
|
|
812
795
|
max_radius = np.max(radii)
|
|
813
796
|
avg_degree = np.mean(degrees)
|
|
@@ -824,13 +807,16 @@ class VesselDenoiser:
|
|
|
824
807
|
|
|
825
808
|
# Measure elongation (max distance / mean deviation from center)
|
|
826
809
|
if len(positions) > 1:
|
|
827
|
-
pairwise_dists = np.linalg.norm(
|
|
828
|
-
positions[:, None] - positions[None, :], axis=2
|
|
829
|
-
)
|
|
830
|
-
max_dist = np.max(pairwise_dists)
|
|
831
810
|
mean_pos = positions.mean(axis=0)
|
|
832
811
|
deviations = np.linalg.norm(positions - mean_pos, axis=1)
|
|
833
812
|
mean_deviation = np.mean(deviations)
|
|
813
|
+
|
|
814
|
+
# FAST APPROXIMATION: Use bounding box diagonal
|
|
815
|
+
# This is O(n) instead of O(n²) and uses minimal memory
|
|
816
|
+
bbox_min = positions.min(axis=0)
|
|
817
|
+
bbox_max = positions.max(axis=0)
|
|
818
|
+
max_dist = np.linalg.norm(bbox_max - bbox_min)
|
|
819
|
+
|
|
834
820
|
elongation = max_dist / (mean_deviation + 1) if mean_deviation > 0 else max_dist
|
|
835
821
|
else:
|
|
836
822
|
elongation = 0
|
|
@@ -781,7 +781,7 @@ def get_surface_areas(labeled, xy_scale=1, z_scale=1):
|
|
|
781
781
|
|
|
782
782
|
return result
|
|
783
783
|
|
|
784
|
-
def break_and_label_skeleton(skeleton, peaks = 1, branch_removal = 0, comp_dil = 0, max_vol = 0, directory = None, return_skele = False, nodes = None, compute = True, xy_scale = 1, z_scale = 1):
|
|
784
|
+
def break_and_label_skeleton(skeleton, peaks = 1, branch_removal = 0, comp_dil = 0, max_vol = 0, directory = None, return_skele = False, nodes = None, compute = True, unify = False, xy_scale = 1, z_scale = 1):
|
|
785
785
|
"""Internal method to break open a skeleton at its branchpoints and label the remaining components, for an 8bit binary array"""
|
|
786
786
|
|
|
787
787
|
if type(skeleton) == str:
|
|
@@ -829,11 +829,16 @@ def break_and_label_skeleton(skeleton, peaks = 1, branch_removal = 0, comp_dil =
|
|
|
829
829
|
tifffile.imwrite(filename, labeled_image, photometric='minisblack')
|
|
830
830
|
print(f"Broken skeleton saved to {filename}")
|
|
831
831
|
|
|
832
|
+
if not unify:
|
|
833
|
+
verts = None
|
|
834
|
+
else:
|
|
835
|
+
verts = invert_array(verts)
|
|
836
|
+
|
|
832
837
|
if compute:
|
|
833
838
|
|
|
834
|
-
return labeled_image,
|
|
839
|
+
return labeled_image, verts, skeleton, None
|
|
835
840
|
|
|
836
|
-
return labeled_image,
|
|
841
|
+
return labeled_image, verts, None, None
|
|
837
842
|
|
|
838
843
|
def compute_optional_branchstats(verts, labeled_array, endpoints, xy_scale = 1, z_scale = 1):
|
|
839
844
|
|
|
@@ -2371,7 +2376,7 @@ def skeletonize(arrayimage, directory = None):
|
|
|
2371
2376
|
|
|
2372
2377
|
return arrayimage
|
|
2373
2378
|
|
|
2374
|
-
def label_branches(array, peaks = 0, branch_removal = 0, comp_dil = 0, max_vol = 0, down_factor = None, directory = None, nodes = None, bonus_array = None, GPU = True, arrayshape = None, compute = False, xy_scale = 1, z_scale = 1):
|
|
2379
|
+
def label_branches(array, peaks = 0, branch_removal = 0, comp_dil = 0, max_vol = 0, down_factor = None, directory = None, nodes = None, bonus_array = None, GPU = True, arrayshape = None, compute = False, unify = False, union_val = 10, xy_scale = 1, z_scale = 1):
|
|
2375
2380
|
"""
|
|
2376
2381
|
Can be used to label branches a binary image. Labelled output will be saved to the active directory if none is specified. Note this works better on already thin filaments and may over-divide larger trunkish objects.
|
|
2377
2382
|
:param array: (Mandatory, string or ndarray) - If string, a path to a tif file to label. Note that the ndarray alternative is for internal use mainly and will not save its output.
|
|
@@ -2401,13 +2406,20 @@ def label_branches(array, peaks = 0, branch_removal = 0, comp_dil = 0, max_vol =
|
|
|
2401
2406
|
|
|
2402
2407
|
other_array = skeletonize(array)
|
|
2403
2408
|
|
|
2404
|
-
other_array, verts, skele, endpoints = break_and_label_skeleton(other_array, peaks = peaks, branch_removal = branch_removal, comp_dil = comp_dil, max_vol = max_vol, nodes = nodes, compute = compute, xy_scale = xy_scale, z_scale = z_scale)
|
|
2409
|
+
other_array, verts, skele, endpoints = break_and_label_skeleton(other_array, peaks = peaks, branch_removal = branch_removal, comp_dil = comp_dil, max_vol = max_vol, nodes = nodes, compute = compute, unify = unify, xy_scale = xy_scale, z_scale = z_scale)
|
|
2405
2410
|
|
|
2406
2411
|
else:
|
|
2407
|
-
|
|
2412
|
+
if down_factor is not None:
|
|
2413
|
+
bonus_array = downsample(bonus_array, down_factor)
|
|
2414
|
+
array, verts, skele, endpoints = break_and_label_skeleton(array, peaks = peaks, branch_removal = branch_removal, comp_dil = comp_dil, max_vol = max_vol, nodes = nodes, compute = compute, unify = unify, xy_scale = xy_scale, z_scale = z_scale)
|
|
2415
|
+
|
|
2416
|
+
if unify is True and nodes is not None:
|
|
2417
|
+
from . import branch_stitcher
|
|
2418
|
+
verts = dilate_3D_old(verts, 3, 3, 3,)
|
|
2419
|
+
verts, _ = label_objects(verts)
|
|
2420
|
+
array = branch_stitcher.trace(bonus_array, array, verts, score_thresh = union_val)
|
|
2421
|
+
verts = None
|
|
2408
2422
|
|
|
2409
|
-
if nodes is not None and down_factor is not None:
|
|
2410
|
-
array = upsample_with_padding(array, down_factor, arrayshape)
|
|
2411
2423
|
|
|
2412
2424
|
if nodes is None:
|
|
2413
2425
|
|
|
@@ -2441,6 +2453,9 @@ def label_branches(array, peaks = 0, branch_removal = 0, comp_dil = 0, max_vol =
|
|
|
2441
2453
|
else:
|
|
2442
2454
|
print("Branches labelled")
|
|
2443
2455
|
|
|
2456
|
+
if nodes is not None and down_factor is not None:
|
|
2457
|
+
array = upsample_with_padding(array, down_factor, arrayshape)
|
|
2458
|
+
|
|
2444
2459
|
|
|
2445
2460
|
return array, verts, skele, endpoints
|
|
2446
2461
|
|
|
@@ -3349,8 +3364,11 @@ class Network_3D:
|
|
|
3349
3364
|
try:
|
|
3350
3365
|
tifffile.imwrite(f"{filename}", self._nodes, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
|
|
3351
3366
|
except:
|
|
3352
|
-
|
|
3353
|
-
|
|
3367
|
+
try:
|
|
3368
|
+
tifffile.imwrite(f"{filename}", self._nodes)
|
|
3369
|
+
except:
|
|
3370
|
+
self._nodes = binarize(self._nodes)
|
|
3371
|
+
tifffile.imwrite(f"{filename}", self._nodes, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
|
|
3354
3372
|
else:
|
|
3355
3373
|
tifffile.imwrite(f"{filename}", self._nodes)
|
|
3356
3374
|
print(f"Nodes saved to {filename}")
|
|
@@ -3362,8 +3380,11 @@ class Network_3D:
|
|
|
3362
3380
|
try:
|
|
3363
3381
|
tifffile.imwrite(f"{directory}/{filename}", self._nodes, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
|
|
3364
3382
|
except:
|
|
3365
|
-
|
|
3366
|
-
|
|
3383
|
+
try:
|
|
3384
|
+
tifffile.imwrite(f"{directory}/{filename}", self._nodes)
|
|
3385
|
+
except:
|
|
3386
|
+
self._nodes = binarize(self._nodes)
|
|
3387
|
+
tifffile.imwrite(f"{directory}/{filename}", self._nodes, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
|
|
3367
3388
|
else:
|
|
3368
3389
|
tifffile.imwrite(f"{directory}/{filename}", self._nodes)
|
|
3369
3390
|
print(f"Nodes saved to {directory}/{filename}")
|
|
@@ -3398,16 +3419,22 @@ class Network_3D:
|
|
|
3398
3419
|
try:
|
|
3399
3420
|
tifffile.imwrite(f"{filename}", self._edges, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
|
|
3400
3421
|
except:
|
|
3401
|
-
|
|
3402
|
-
|
|
3422
|
+
try:
|
|
3423
|
+
tifffile.imwrite(f"{filename}", self._edges)
|
|
3424
|
+
except:
|
|
3425
|
+
self._edges = binarize(self._edges)
|
|
3426
|
+
tifffile.imwrite(f"{filename}", self._edges, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
|
|
3403
3427
|
print(f"Edges saved to {filename}")
|
|
3404
3428
|
|
|
3405
3429
|
if directory is not None:
|
|
3406
3430
|
try:
|
|
3407
3431
|
tifffile.imwrite(f"{directory}/{filename}", self._edges, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
|
|
3408
3432
|
except:
|
|
3409
|
-
|
|
3410
|
-
|
|
3433
|
+
try:
|
|
3434
|
+
tifffile.imwrite(f"{directory}/{filename}", self._edges)
|
|
3435
|
+
except:
|
|
3436
|
+
self._edges = binarize(self._edges)
|
|
3437
|
+
tifffile.imwrite(f"{directory}/{filename}", self._edges, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
|
|
3411
3438
|
print(f"Edges saved to {directory}/{filename}")
|
|
3412
3439
|
|
|
3413
3440
|
if self._edges is None:
|
|
@@ -3583,8 +3610,11 @@ class Network_3D:
|
|
|
3583
3610
|
try:
|
|
3584
3611
|
tifffile.imwrite(f"{filename}", self._network_overlay, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
|
|
3585
3612
|
except:
|
|
3586
|
-
|
|
3587
|
-
|
|
3613
|
+
try:
|
|
3614
|
+
tifffile.imwrite(f"{filename}", self._network_overlay)
|
|
3615
|
+
except:
|
|
3616
|
+
self._network_overlay = binarize(self._network_overlay)
|
|
3617
|
+
tifffile.imwrite(f"{filename}", self._network_overlay, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
|
|
3588
3618
|
else:
|
|
3589
3619
|
tifffile.imwrite(f"{filename}", self._network_overlay)
|
|
3590
3620
|
print(f"Network overlay saved to {filename}")
|
|
@@ -3594,8 +3624,11 @@ class Network_3D:
|
|
|
3594
3624
|
try:
|
|
3595
3625
|
tifffile.imwrite(f"{directory}/{filename}", self._network_overlay, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
|
|
3596
3626
|
except:
|
|
3597
|
-
|
|
3598
|
-
|
|
3627
|
+
try:
|
|
3628
|
+
tifffile.imwrite(f"{directory}/{filename}", self._network_overlay)
|
|
3629
|
+
except:
|
|
3630
|
+
self._network_overlay = binarize(self._network_overlay)
|
|
3631
|
+
tifffile.imwrite(f"{directory}/{filename}", self._network_overlay, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
|
|
3599
3632
|
else:
|
|
3600
3633
|
tifffile.imwrite(f"{directory}/{filename}", self._network_overlay)
|
|
3601
3634
|
print(f"Network overlay saved to {directory}/{filename}")
|
|
@@ -3622,8 +3655,11 @@ class Network_3D:
|
|
|
3622
3655
|
try:
|
|
3623
3656
|
tifffile.imwrite(f"{filename}", self._id_overlay, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
|
|
3624
3657
|
except:
|
|
3625
|
-
|
|
3626
|
-
|
|
3658
|
+
try:
|
|
3659
|
+
tifffile.imwrite(f"{filename}", self._id_overlay)
|
|
3660
|
+
except:
|
|
3661
|
+
self._id_overlay = binarize(self._id_overlay)
|
|
3662
|
+
tifffile.imwrite(f"{filename}", self._id_overlay, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
|
|
3627
3663
|
else:
|
|
3628
3664
|
tifffile.imwrite(f"{filename}", self._id_overlay, imagej=True)
|
|
3629
3665
|
print(f"Network overlay saved to {filename}")
|
|
@@ -3633,8 +3669,11 @@ class Network_3D:
|
|
|
3633
3669
|
try:
|
|
3634
3670
|
tifffile.imwrite(f"{directory}/{filename}", self._id_overlay, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
|
|
3635
3671
|
except:
|
|
3636
|
-
|
|
3637
|
-
|
|
3672
|
+
try:
|
|
3673
|
+
tifffile.imwrite(f"{directory}/{filename}", self._id_overlay)
|
|
3674
|
+
except:
|
|
3675
|
+
self._id_overlay = binarize(self._id_overlay)
|
|
3676
|
+
tifffile.imwrite(f"{directory}/{filename}", self._id_overlay, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
|
|
3638
3677
|
else:
|
|
3639
3678
|
tifffile.imwrite(f"{directory}/{filename}", self._id_overlay)
|
|
3640
3679
|
print(f"ID overlay saved to {directory}/{filename}")
|
|
@@ -4887,7 +4887,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4887
4887
|
gennodes_action.triggered.connect(self.show_gennodes_dialog)
|
|
4888
4888
|
branch_action = generate_menu.addAction("Label Branches")
|
|
4889
4889
|
branch_action.triggered.connect(lambda: self.show_branch_dialog())
|
|
4890
|
-
filament_action = generate_menu.addAction("Trace Filaments")
|
|
4890
|
+
filament_action = generate_menu.addAction("Trace Filaments (For Segmented Data)")
|
|
4891
4891
|
filament_action.triggered.connect(self.show_filament_dialog)
|
|
4892
4892
|
genvor_action = generate_menu.addAction("Generate Voronoi Diagram - goes in Overlay2")
|
|
4893
4893
|
genvor_action.triggered.connect(self.voronoi)
|
|
@@ -6358,11 +6358,13 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6358
6358
|
if 'YResolution' in tags:
|
|
6359
6359
|
y_res = tags['YResolution'].value
|
|
6360
6360
|
y_scale = y_res[1] / y_res[0] if isinstance(y_res, tuple) else 1.0 / y_res
|
|
6361
|
-
|
|
6362
|
-
if x_scale
|
|
6361
|
+
|
|
6362
|
+
if x_scale == None:
|
|
6363
6363
|
x_scale = 1
|
|
6364
|
-
if z_scale
|
|
6364
|
+
if z_scale == None:
|
|
6365
6365
|
z_scale = 1
|
|
6366
|
+
if x_scale == 1 and z_scale == 1:
|
|
6367
|
+
return
|
|
6366
6368
|
|
|
6367
6369
|
return x_scale, z_scale
|
|
6368
6370
|
|
|
@@ -7337,6 +7339,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
7337
7339
|
|
|
7338
7340
|
try:
|
|
7339
7341
|
|
|
7342
|
+
if self.shape[0] == 1:
|
|
7343
|
+
print("The image is 2D and therefore does not have surface areas")
|
|
7344
|
+
return
|
|
7345
|
+
|
|
7340
7346
|
surface_areas = n3d.get_surface_areas(self.channel_data[self.active_channel], xy_scale = my_network.xy_scale, z_scale = my_network.z_scale)
|
|
7341
7347
|
|
|
7342
7348
|
if self.active_channel == 0:
|
|
@@ -7357,6 +7363,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
7357
7363
|
|
|
7358
7364
|
try:
|
|
7359
7365
|
|
|
7366
|
+
if self.shape[0] == 1:
|
|
7367
|
+
print("The image is 2D and therefore does not have sphericities")
|
|
7368
|
+
return
|
|
7369
|
+
|
|
7360
7370
|
self.volumes()
|
|
7361
7371
|
self.handle_sa()
|
|
7362
7372
|
volumes = self.volume_dict[self.active_channel]
|
|
@@ -12627,8 +12637,7 @@ class MachineWindow(QMainWindow):
|
|
|
12627
12637
|
self.num_chunks = 0
|
|
12628
12638
|
self.parent().update_display()
|
|
12629
12639
|
except:
|
|
12630
|
-
|
|
12631
|
-
traceback.print_exc()
|
|
12640
|
+
|
|
12632
12641
|
pass
|
|
12633
12642
|
|
|
12634
12643
|
except:
|
|
@@ -13216,22 +13225,28 @@ class ThresholdWindow(QMainWindow):
|
|
|
13216
13225
|
data = self.parent().channel_data[self.parent().active_channel]
|
|
13217
13226
|
nonzero_data = data[data != 0]
|
|
13218
13227
|
|
|
13219
|
-
|
|
13220
|
-
|
|
13221
|
-
|
|
13222
|
-
|
|
13223
|
-
|
|
13224
|
-
|
|
13225
|
-
|
|
13226
|
-
|
|
13227
|
-
|
|
13228
|
-
|
|
13228
|
+
MAX_SAMPLES_FOR_HISTOGRAM = 10_000_000 # Downsample data above this size
|
|
13229
|
+
MAX_HISTOGRAM_BINS = 512 # Maximum bins for smooth matplotlib interaction
|
|
13230
|
+
MIN_HISTOGRAM_BINS = 128 # Minimum bins for decent resolution
|
|
13231
|
+
|
|
13232
|
+
# Always compute min/max first (before any downsampling)
|
|
13233
|
+
self.data_min = np.min(nonzero_data)
|
|
13234
|
+
self.data_max = np.max(nonzero_data)
|
|
13235
|
+
self.histo_list = [self.data_min, self.data_max]
|
|
13236
|
+
|
|
13237
|
+
# Downsample data if too large
|
|
13238
|
+
if nonzero_data.size > MAX_SAMPLES_FOR_HISTOGRAM:
|
|
13239
|
+
downsample_factor = int(np.ceil(nonzero_data.size / MAX_SAMPLES_FOR_HISTOGRAM))
|
|
13240
|
+
nonzero_data_sampled = n3d.downsample(nonzero_data, downsample_factor)
|
|
13229
13241
|
else:
|
|
13230
|
-
|
|
13231
|
-
|
|
13232
|
-
|
|
13233
|
-
|
|
13234
|
-
|
|
13242
|
+
nonzero_data_sampled = nonzero_data
|
|
13243
|
+
|
|
13244
|
+
# Calculate optimal bin count (capped for matplotlib performance)
|
|
13245
|
+
# Using Sturges' rule but capped to reasonable limits
|
|
13246
|
+
n_bins = int(np.ceil(np.log2(nonzero_data_sampled.size)) + 1)
|
|
13247
|
+
n_bins = np.clip(n_bins, MIN_HISTOGRAM_BINS, MAX_HISTOGRAM_BINS)
|
|
13248
|
+
|
|
13249
|
+
counts, bin_edges = np.histogram(nonzero_data_sampled, bins=n_bins, density=False)
|
|
13235
13250
|
|
|
13236
13251
|
self.bounds = True
|
|
13237
13252
|
self.parent().bounds = True
|
|
@@ -13936,10 +13951,10 @@ class FilamentDialog(QDialog):
|
|
|
13936
13951
|
# Speedup Group
|
|
13937
13952
|
speedup_group = QGroupBox("Speedup")
|
|
13938
13953
|
speedup_layout = QFormLayout()
|
|
13939
|
-
self.kernel_spacing = QLineEdit("
|
|
13954
|
+
self.kernel_spacing = QLineEdit("3")
|
|
13940
13955
|
speedup_layout.addRow("Kernel Spacing (1 is most accurate, can increase to speed up):", self.kernel_spacing)
|
|
13941
13956
|
self.downsample_factor = QLineEdit("1")
|
|
13942
|
-
speedup_layout.addRow("Temporary Downsample Factor (Note that the
|
|
13957
|
+
speedup_layout.addRow("Temporary Downsample Factor (Note that the below distances are not adjusted for this):", self.downsample_factor)
|
|
13943
13958
|
speedup_group.setLayout(speedup_layout)
|
|
13944
13959
|
main_layout.addWidget(speedup_group)
|
|
13945
13960
|
|
|
@@ -13959,7 +13974,7 @@ class FilamentDialog(QDialog):
|
|
|
13959
13974
|
artifact_group = QGroupBox("Artifact Removal")
|
|
13960
13975
|
artifact_layout = QFormLayout()
|
|
13961
13976
|
self.min_component = QLineEdit("20")
|
|
13962
|
-
artifact_layout.addRow("Minimum Component Size to Include
|
|
13977
|
+
artifact_layout.addRow("Minimum Component Size to Include:", self.min_component)
|
|
13963
13978
|
self.blob_sphericity = QLineEdit("1.0")
|
|
13964
13979
|
artifact_layout.addRow("Spherical Objects in the Output can Represent Noise. Enter a val 0 < x < 1 to consider removing spheroids. Larger vals are more spherical. 1.0 = a perfect sphere. 0.3 is usually the lower bound of a spheroid:", self.blob_sphericity)
|
|
13965
13980
|
self.blob_volume = QLineEdit("200")
|
|
@@ -13968,7 +13983,8 @@ class FilamentDialog(QDialog):
|
|
|
13968
13983
|
artifact_layout.addRow("Remove Branch Spines Below this Length?", self.spine_removal)
|
|
13969
13984
|
artifact_group.setLayout(artifact_layout)
|
|
13970
13985
|
main_layout.addWidget(artifact_group)
|
|
13971
|
-
|
|
13986
|
+
|
|
13987
|
+
|
|
13972
13988
|
# Run Button
|
|
13973
13989
|
run_button = QPushButton("Run Filament Tracer (Output Goes in Overlay 2)")
|
|
13974
13990
|
run_button.clicked.connect(self.run)
|
|
@@ -13981,6 +13997,7 @@ class FilamentDialog(QDialog):
|
|
|
13981
13997
|
|
|
13982
13998
|
from . import filaments
|
|
13983
13999
|
|
|
14000
|
+
|
|
13984
14001
|
kernel_spacing = int(self.kernel_spacing.text()) if self.kernel_spacing.text().strip() else 1
|
|
13985
14002
|
max_distance = float(self.max_distance.text()) if self.max_distance.text().strip() else 20
|
|
13986
14003
|
min_component = int(self.min_component.text()) if self.min_component.text().strip() else 20
|
|
@@ -13995,7 +14012,6 @@ class FilamentDialog(QDialog):
|
|
|
13995
14012
|
if downsample_factor and downsample_factor > 1:
|
|
13996
14013
|
data = n3d.downsample(data, downsample_factor)
|
|
13997
14014
|
|
|
13998
|
-
|
|
13999
14015
|
result = filaments.trace(data, kernel_spacing, max_distance, min_component, gap_tolerance, blob_sphericity, blob_volume, spine_removal, score_threshold)
|
|
14000
14016
|
|
|
14001
14017
|
if downsample_factor and downsample_factor > 1:
|
|
@@ -14008,9 +14024,49 @@ class FilamentDialog(QDialog):
|
|
|
14008
14024
|
self.accept()
|
|
14009
14025
|
|
|
14010
14026
|
except Exception as e:
|
|
14011
|
-
|
|
14027
|
+
import traceback
|
|
14028
|
+
print(traceback.format_exc())
|
|
14012
14029
|
print(f"Error: {e}")
|
|
14013
14030
|
|
|
14031
|
+
def wait_for_threshold_processing(self):
|
|
14032
|
+
"""
|
|
14033
|
+
Opens ThresholdWindow and waits for user to process the image.
|
|
14034
|
+
Returns True if completed, False if cancelled.
|
|
14035
|
+
The thresholded image will be available in the main window after completion.
|
|
14036
|
+
"""
|
|
14037
|
+
# Create event loop to wait for user
|
|
14038
|
+
loop = QEventLoop()
|
|
14039
|
+
result = {'completed': False}
|
|
14040
|
+
|
|
14041
|
+
# Create the threshold window
|
|
14042
|
+
thresh_window = ThresholdWindow(self.parent(), 0)
|
|
14043
|
+
|
|
14044
|
+
|
|
14045
|
+
# Connect signals
|
|
14046
|
+
def on_processing_complete():
|
|
14047
|
+
result['completed'] = True
|
|
14048
|
+
loop.quit()
|
|
14049
|
+
|
|
14050
|
+
def on_processing_cancelled():
|
|
14051
|
+
result['completed'] = False
|
|
14052
|
+
loop.quit()
|
|
14053
|
+
|
|
14054
|
+
thresh_window.processing_complete.connect(on_processing_complete)
|
|
14055
|
+
thresh_window.processing_cancelled.connect(on_processing_cancelled)
|
|
14056
|
+
|
|
14057
|
+
# Show window and wait
|
|
14058
|
+
thresh_window.show()
|
|
14059
|
+
thresh_window.raise_()
|
|
14060
|
+
thresh_window.activateWindow()
|
|
14061
|
+
|
|
14062
|
+
# Block until user clicks "Apply Threshold & Continue" or "Cancel"
|
|
14063
|
+
loop.exec()
|
|
14064
|
+
|
|
14065
|
+
# Clean up
|
|
14066
|
+
thresh_window.deleteLater()
|
|
14067
|
+
|
|
14068
|
+
return result['completed']
|
|
14069
|
+
|
|
14014
14070
|
|
|
14015
14071
|
|
|
14016
14072
|
class MaskDialog(QDialog):
|
|
@@ -15139,30 +15195,40 @@ class BranchDialog(QDialog):
|
|
|
15139
15195
|
self.fix = QPushButton("Auto-Correct 1")
|
|
15140
15196
|
self.fix.setCheckable(True)
|
|
15141
15197
|
self.fix.setChecked(False)
|
|
15142
|
-
correction_layout.addWidget(QLabel("Auto-Correct Branches by Collapsing Busy Neighbors: "), 0, 0)
|
|
15143
|
-
correction_layout.addWidget(self.fix, 0, 1)
|
|
15198
|
+
#correction_layout.addWidget(QLabel("Auto-Correct Branches by Collapsing Busy Neighbors: "), 0, 0)
|
|
15199
|
+
#correction_layout.addWidget(self.fix, 0, 1)
|
|
15144
15200
|
|
|
15145
15201
|
# Fix value
|
|
15146
15202
|
self.fix_val = QLineEdit('4')
|
|
15147
|
-
correction_layout.addWidget(QLabel("Avg Degree of Nearby Branch Communities to Merge (4-6 recommended):"), 1, 0)
|
|
15148
|
-
correction_layout.addWidget(self.fix_val, 1, 1)
|
|
15203
|
+
#correction_layout.addWidget(QLabel("(For Auto-Correct 1) Avg Degree of Nearby Branch Communities to Merge (4-6 recommended):"), 1, 0)
|
|
15204
|
+
#correction_layout.addWidget(self.fix_val, 1, 1)
|
|
15149
15205
|
|
|
15150
15206
|
# Seed
|
|
15151
15207
|
self.seed = QLineEdit('')
|
|
15152
|
-
correction_layout.addWidget(QLabel("Random seed for auto correction (int - optional):"), 2, 0)
|
|
15153
|
-
correction_layout.addWidget(self.seed, 2, 1)
|
|
15208
|
+
#correction_layout.addWidget(QLabel("Random seed for auto correction (int - optional):"), 2, 0)
|
|
15209
|
+
#correction_layout.addWidget(self.seed, 2, 1)
|
|
15154
15210
|
|
|
15155
|
-
self.fix2 = QPushButton("Auto-Correct
|
|
15211
|
+
self.fix2 = QPushButton("Auto-Correct Internal Branches")
|
|
15156
15212
|
self.fix2.setCheckable(True)
|
|
15157
15213
|
self.fix2.setChecked(True)
|
|
15158
15214
|
correction_layout.addWidget(QLabel("Auto-Correct Branches by Collapsing Internal Labels: "), 3, 0)
|
|
15159
15215
|
correction_layout.addWidget(self.fix2, 3, 1)
|
|
15160
15216
|
|
|
15161
|
-
self.fix3 = QPushButton("Correct Nontouching Branches
|
|
15217
|
+
self.fix3 = QPushButton("Auto-Correct Nontouching Branches")
|
|
15162
15218
|
self.fix3.setCheckable(True)
|
|
15163
15219
|
self.fix3.setChecked(True)
|
|
15164
|
-
correction_layout.addWidget(QLabel("Correct Nontouching Branches?: "), 4, 0)
|
|
15220
|
+
correction_layout.addWidget(QLabel("Auto-Correct Nontouching Branches?: "), 4, 0)
|
|
15165
15221
|
correction_layout.addWidget(self.fix3, 4, 1)
|
|
15222
|
+
|
|
15223
|
+
self.fix4 = QPushButton("Auto-Attempt to Reunify Main Branches?")
|
|
15224
|
+
self.fix4.setCheckable(True)
|
|
15225
|
+
self.fix4.setChecked(False)
|
|
15226
|
+
correction_layout.addWidget(QLabel("Reunify Main Branches: "), 5, 0)
|
|
15227
|
+
correction_layout.addWidget(self.fix4, 5, 1)
|
|
15228
|
+
|
|
15229
|
+
self.fix4_val = QLineEdit('10')
|
|
15230
|
+
correction_layout.addWidget(QLabel("(For Reunify) Minimum Score to Merge? (Lower vals = More mergers, can be negative):"), 6, 0)
|
|
15231
|
+
correction_layout.addWidget(self.fix4_val, 6, 1)
|
|
15166
15232
|
|
|
15167
15233
|
correction_group.setLayout(correction_layout)
|
|
15168
15234
|
main_layout.addWidget(correction_group)
|
|
@@ -15241,7 +15307,9 @@ class BranchDialog(QDialog):
|
|
|
15241
15307
|
fix = self.fix.isChecked()
|
|
15242
15308
|
fix2 = self.fix2.isChecked()
|
|
15243
15309
|
fix3 = self.fix3.isChecked()
|
|
15310
|
+
fix4 = self.fix4.isChecked()
|
|
15244
15311
|
fix_val = float(self.fix_val.text()) if self.fix_val.text() else None
|
|
15312
|
+
fix4_val = float(self.fix4_val.text()) if self.fix4_val.text() else 10
|
|
15245
15313
|
seed = int(self.seed.text()) if self.seed.text() else None
|
|
15246
15314
|
compute = self.compute.isChecked()
|
|
15247
15315
|
|
|
@@ -15260,7 +15328,12 @@ class BranchDialog(QDialog):
|
|
|
15260
15328
|
|
|
15261
15329
|
if my_network.edges is not None and my_network.nodes is not None and my_network.id_overlay is not None:
|
|
15262
15330
|
|
|
15263
|
-
|
|
15331
|
+
if fix4:
|
|
15332
|
+
unify = True
|
|
15333
|
+
else:
|
|
15334
|
+
unify = False
|
|
15335
|
+
|
|
15336
|
+
output, verts, skeleton, endpoints = n3d.label_branches(my_network.edges, nodes = my_network.nodes, bonus_array = original_array, GPU = GPU, down_factor = down_factor, arrayshape = original_shape, compute = compute, unify = unify, union_val = fix4_val, xy_scale = my_network.xy_scale, z_scale = my_network.z_scale)
|
|
15264
15337
|
|
|
15265
15338
|
if fix2:
|
|
15266
15339
|
|
|
@@ -1765,6 +1765,7 @@ def setup_branch_tutorial(window):
|
|
|
1765
1765
|
pre_action=open_dialog
|
|
1766
1766
|
)
|
|
1767
1767
|
|
|
1768
|
+
"""
|
|
1768
1769
|
tutorial.add_step(
|
|
1769
1770
|
MenuHelper.create_widget_getter(tutorial, 'branch_dialog', 'fix'),
|
|
1770
1771
|
"This first auto-correction option is designed if you feel like the branch labels are generally too busy. Selecting this will have the program attempt to collapse overly-dense regions of branches into a single label. Note that this behavior is somewhat tricky to predict so I generally don't use it but feel free to give it a shot and see how it looks.",
|
|
@@ -1792,6 +1793,8 @@ def setup_branch_tutorial(window):
|
|
|
1792
1793
|
action=MenuHelper.create_widget_interaction(tutorial, 'branch_dialog', 'seed', 'setText("")')
|
|
1793
1794
|
)
|
|
1794
1795
|
|
|
1796
|
+
"""
|
|
1797
|
+
|
|
1795
1798
|
tutorial.add_step(
|
|
1796
1799
|
MenuHelper.create_widget_getter(tutorial, 'branch_dialog', 'fix2'),
|
|
1797
1800
|
"The second auto-correction option will automatically merge any internal labels that arise with their outer-neighbors. This is something that can occasionally happen with fat, trunk-like branches that are tricky to algorithmically decipher. I have found that this merge handles these issues quite well, so this option is enabled by default.",
|
|
@@ -1802,12 +1805,30 @@ def setup_branch_tutorial(window):
|
|
|
1802
1805
|
|
|
1803
1806
|
tutorial.add_step(
|
|
1804
1807
|
MenuHelper.create_widget_getter(tutorial, 'branch_dialog', 'fix3'),
|
|
1805
|
-
"This
|
|
1808
|
+
"This auto-correction step will automatically correct any branches that aren't contiguous in space. Rarely (Depending on the segmentation, really) a branch can initially be labeled non-contiguously, which is usually not correct. This is because the 'meat' of any branch is at first labeled based on which internal filament it's closest to. So if you have a very wide branch it may rarely aquire labels of nearby smaller branches across gaps. Enabling this will split those labels into seperate regions as to not confound the connectivity graph. The largest component is considered the 'correct one' and keeps its label, while smaller components inherit the label of the largest shared border of a 'real' branch they are bordering. It is enabled here by default to mitigate any potential errors, although note this does not apply to the branchpoint networks since they don't actually utilize the branches themselves.",
|
|
1806
1809
|
highlight_type=None,
|
|
1807
1810
|
message_position="beside",
|
|
1808
1811
|
pre_action=MenuHelper.create_widget_interaction(tutorial, 'branch_dialog', 'fix3', 'click()')
|
|
1809
1812
|
)
|
|
1810
1813
|
|
|
1814
|
+
tutorial.add_step(
|
|
1815
|
+
MenuHelper.create_widget_getter(tutorial, 'branch_dialog', 'fix4'),
|
|
1816
|
+
"This final auto-correction step will try to automatically merge any similarly sized branches moving in the same direction, instead of just letting a larger branch with many sub-branches get chopped up. It is off by default because of its less predictable behavior, although its good if you want your branches to be more continuous. Just note each of these fixes does add extra processing time.",
|
|
1817
|
+
highlight_type=None,
|
|
1818
|
+
message_position="beside",
|
|
1819
|
+
pre_action=MenuHelper.create_widget_interaction(tutorial, 'branch_dialog', 'fix4', 'click()'),
|
|
1820
|
+
action=MenuHelper.create_widget_interaction(tutorial, 'branch_dialog', 'fix4', 'toggle()')
|
|
1821
|
+
)
|
|
1822
|
+
|
|
1823
|
+
tutorial.add_step(
|
|
1824
|
+
MenuHelper.create_widget_getter(tutorial, 'branch_dialog', 'fix4_val'),
|
|
1825
|
+
"This threshold values controls how likely a junction is to merge any pair of its nearby branches. Regardless of what you enter here, only two branches at a time can merge at a junction. Values between 20-40 are more meaningful, while those lower tend to merge everything and those higher usually emrge nothing.",
|
|
1826
|
+
highlight_type=None,
|
|
1827
|
+
message_position="beside",
|
|
1828
|
+
pre_action=MenuHelper.create_widget_interaction(tutorial, 'branch_dialog', 'fix4_val', 'setText("FLOAT!")'),
|
|
1829
|
+
action=MenuHelper.create_widget_interaction(tutorial, 'branch_dialog', 'fix4_val', 'setText("")')
|
|
1830
|
+
)
|
|
1831
|
+
|
|
1811
1832
|
tutorial.add_step(
|
|
1812
1833
|
MenuHelper.create_widget_getter(tutorial, 'branch_dialog', 'down_factor'),
|
|
1813
1834
|
"This integer value can be used to temporarily downsample the image while creating branches. Aside from speeding up the process, this may actually improve branch-labeling behavior with thick branches but will lose labeling for smaller branches (instead merging them with nearby thicker branches they arise from). It is disabled by default. Larger values will downsample more aggressively.",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nettracer3d
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.3
|
|
4
4
|
Summary: Scripts for intializing and analyzing networks from segmentations of three dimensional images.
|
|
5
5
|
Author-email: Liam McLaughlin <liamm@wustl.edu>
|
|
6
6
|
Project-URL: Documentation, https://nettracer3d.readthedocs.io/en/latest/
|
|
@@ -110,12 +110,8 @@ McLaughlin, L., Zhang, B., Sharma, S. et al. Three dimensional multiscalar neuro
|
|
|
110
110
|
|
|
111
111
|
NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
|
|
112
112
|
|
|
113
|
-
-- Version 1.
|
|
113
|
+
-- Version 1.2.3 Updates --
|
|
114
114
|
|
|
115
|
-
*
|
|
116
|
-
* Fixed branch functions not working correctly when a temporary downsample was being applied
|
|
117
|
-
* Added the 'filaments tracer' which can be used to improve vessel based segmentations
|
|
118
|
-
* Removed options for 'cubic' downsampling during processing as this option actually made outputs worse.
|
|
119
|
-
* Rearranged the 'Analyze -> Stats' menu
|
|
115
|
+
* Tweaked the 'branch unification params' - now hard rejects any sharp forks in the branches so branches will mostly be straight.
|
|
120
116
|
|
|
121
117
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|