nettracer3d 1.1.1__tar.gz → 1.1.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.
Potentially problematic release.
This version of nettracer3d might be problematic. Click here for more details.
- {nettracer3d-1.1.1/src/nettracer3d.egg-info → nettracer3d-1.1.3}/PKG-INFO +3 -3
- {nettracer3d-1.1.1 → nettracer3d-1.1.3}/README.md +2 -2
- {nettracer3d-1.1.1 → nettracer3d-1.1.3}/pyproject.toml +2 -2
- {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/morphology.py +9 -4
- {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/neighborhoods.py +3 -3
- {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/nettracer.py +149 -11
- {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/nettracer_gui.py +209 -38
- {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/node_draw.py +38 -59
- {nettracer3d-1.1.1 → nettracer3d-1.1.3/src/nettracer3d.egg-info}/PKG-INFO +3 -3
- {nettracer3d-1.1.1 → nettracer3d-1.1.3}/LICENSE +0 -0
- {nettracer3d-1.1.1 → nettracer3d-1.1.3}/setup.cfg +0 -0
- {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/__init__.py +0 -0
- {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/cellpose_manager.py +0 -0
- {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/community_extractor.py +0 -0
- {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/excelotron.py +0 -0
- {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/modularity.py +0 -0
- {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/network_analysis.py +0 -0
- {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/network_draw.py +0 -0
- {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/painting.py +0 -0
- {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/proximity.py +0 -0
- {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/run.py +0 -0
- {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/segmenter.py +0 -0
- {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/segmenter_GPU.py +0 -0
- {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/simple_network.py +0 -0
- {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/smart_dilate.py +0 -0
- {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/stats.py +0 -0
- {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d.egg-info/SOURCES.txt +0 -0
- {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d.egg-info/dependency_links.txt +0 -0
- {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d.egg-info/entry_points.txt +0 -0
- {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d.egg-info/requires.txt +0 -0
- {nettracer3d-1.1.1 → nettracer3d-1.1.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.1.
|
|
3
|
+
Version: 1.1.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,6 +110,6 @@ 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.1.
|
|
113
|
+
-- Version 1.1.3 Updates --
|
|
114
114
|
|
|
115
|
-
*
|
|
115
|
+
* Some minor text adjustments
|
|
@@ -65,6 +65,6 @@ 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.1.
|
|
68
|
+
-- Version 1.1.3 Updates --
|
|
69
69
|
|
|
70
|
-
*
|
|
70
|
+
* Some minor text adjustments
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "nettracer3d"
|
|
3
|
-
version = "1.1.
|
|
3
|
+
version = "1.1.3"
|
|
4
4
|
authors = [
|
|
5
5
|
{ name="Liam McLaughlin", email="liamm@wustl.edu" },
|
|
6
6
|
]
|
|
@@ -37,7 +37,7 @@ classifiers = [
|
|
|
37
37
|
# GPU options (choose one)
|
|
38
38
|
CUDA11 = ["cupy-cuda11x"]
|
|
39
39
|
CUDA12 = ["cupy-cuda12x"]
|
|
40
|
-
cupy = ["cupy"]
|
|
40
|
+
cupy = ["cupy"]
|
|
41
41
|
|
|
42
42
|
# Features
|
|
43
43
|
cellpose = ["cellpose[GUI]"]
|
|
@@ -205,15 +205,20 @@ def quantify_edge_node(nodes, edges, search = 0, xy_scale = 1, z_scale = 1, core
|
|
|
205
205
|
|
|
206
206
|
# Helper methods for counting the lens of skeletons:
|
|
207
207
|
|
|
208
|
-
def calculate_skeleton_lengths(skeleton_binary, xy_scale=1.0, z_scale=1.0):
|
|
208
|
+
def calculate_skeleton_lengths(skeleton_binary, xy_scale=1.0, z_scale=1.0, skeleton_coords = None):
|
|
209
209
|
"""
|
|
210
210
|
Calculate total length of all skeletons in a 3D binary image.
|
|
211
211
|
|
|
212
212
|
skeleton_binary: 3D boolean array where True = skeleton voxel
|
|
213
213
|
xy_scale, z_scale: physical units per voxel
|
|
214
214
|
"""
|
|
215
|
-
|
|
216
|
-
skeleton_coords
|
|
215
|
+
|
|
216
|
+
if skeleton_coords is None:
|
|
217
|
+
# Find all skeleton voxels
|
|
218
|
+
skeleton_coords = np.argwhere(skeleton_binary)
|
|
219
|
+
shape = skeleton_binary.shape
|
|
220
|
+
else:
|
|
221
|
+
shape = skeleton_binary #Very professional stuff
|
|
217
222
|
|
|
218
223
|
if len(skeleton_coords) == 0:
|
|
219
224
|
return 0.0
|
|
@@ -222,7 +227,7 @@ def calculate_skeleton_lengths(skeleton_binary, xy_scale=1.0, z_scale=1.0):
|
|
|
222
227
|
coord_to_idx = {tuple(coord): idx for idx, coord in enumerate(skeleton_coords)}
|
|
223
228
|
|
|
224
229
|
# Build adjacency graph
|
|
225
|
-
adjacency_list = build_adjacency_graph(skeleton_coords, coord_to_idx,
|
|
230
|
+
adjacency_list = build_adjacency_graph(skeleton_coords, coord_to_idx, shape)
|
|
226
231
|
|
|
227
232
|
# Calculate lengths using scaled distances
|
|
228
233
|
total_length = calculate_graph_length(skeleton_coords, adjacency_list, xy_scale, z_scale)
|
|
@@ -793,7 +793,7 @@ def create_community_heatmap(community_intensity, node_community, node_centroids
|
|
|
793
793
|
return np.array([r, g, b], dtype=np.uint8)
|
|
794
794
|
|
|
795
795
|
# Create lookup table for RGB colors
|
|
796
|
-
max_label = max(max(labeled_array.flat), max(node_to_community_intensity.keys()) if node_to_community_intensity else 0)
|
|
796
|
+
max_label = int(max(max(labeled_array.flat), max(node_to_community_intensity.keys()) if node_to_community_intensity else 0))
|
|
797
797
|
color_lut = np.zeros((max_label + 1, 3), dtype=np.uint8) # Default to black (0,0,0)
|
|
798
798
|
|
|
799
799
|
# Fill lookup table with RGB colors based on community intensity
|
|
@@ -1036,8 +1036,8 @@ def create_node_heatmap(node_intensity, node_centroids, shape=None, is_3d=True,
|
|
|
1036
1036
|
|
|
1037
1037
|
# Modified usage in your main function:
|
|
1038
1038
|
# Create lookup table for RGBA colors (note the 4 channels now)
|
|
1039
|
-
max_label = max(max(labeled_array.flat), max(node_to_intensity.keys()) if node_to_intensity else 0)
|
|
1040
|
-
color_lut = np.zeros((max_label + 1, 4), dtype=np.uint8)
|
|
1039
|
+
max_label = int(max(max(labeled_array.flat), max(node_to_intensity.keys()) if node_to_intensity else 0))
|
|
1040
|
+
color_lut = np.zeros((max_label + 1, 4), dtype=np.uint8)
|
|
1041
1041
|
|
|
1042
1042
|
# Fill lookup table with RGBA colors based on intensity
|
|
1043
1043
|
for node_id, intensity in node_to_intensity.items():
|
|
@@ -738,7 +738,7 @@ def estimate_object_radii(labeled_array, gpu=False, n_jobs=None, xy_scale = 1, z
|
|
|
738
738
|
return morphology.estimate_object_radii_cpu(labeled_array, n_jobs, xy_scale = xy_scale, z_scale = z_scale)
|
|
739
739
|
|
|
740
740
|
|
|
741
|
-
def break_and_label_skeleton(skeleton, peaks = 1, branch_removal = 0, comp_dil = 0, max_vol = 0, directory = None, return_skele = False, nodes = None):
|
|
741
|
+
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):
|
|
742
742
|
"""Internal method to break open a skeleton at its branchpoints and label the remaining components, for an 8bit binary array"""
|
|
743
743
|
|
|
744
744
|
if type(skeleton) == str:
|
|
@@ -747,18 +747,28 @@ def break_and_label_skeleton(skeleton, peaks = 1, branch_removal = 0, comp_dil =
|
|
|
747
747
|
else:
|
|
748
748
|
broken_skele = None
|
|
749
749
|
|
|
750
|
-
#old_skeleton = copy.deepcopy(skeleton) # The skeleton might get modified in label_vertices so we can make a preserved copy of it to use later
|
|
751
|
-
|
|
752
750
|
if nodes is None:
|
|
753
751
|
|
|
754
|
-
verts = label_vertices(skeleton, peaks = peaks, branch_removal = branch_removal, comp_dil = comp_dil, max_vol = max_vol, return_skele = return_skele)
|
|
752
|
+
verts = label_vertices(skeleton, peaks = peaks, branch_removal = branch_removal, comp_dil = comp_dil, max_vol = max_vol, return_skele = return_skele, compute = compute)
|
|
755
753
|
|
|
756
754
|
else:
|
|
757
755
|
verts = nodes
|
|
758
756
|
|
|
759
757
|
verts = invert_array(verts)
|
|
760
758
|
|
|
761
|
-
|
|
759
|
+
"""
|
|
760
|
+
if compute: # We are interested in the endpoints if we are doing the optional computation later
|
|
761
|
+
endpoints = []
|
|
762
|
+
image_copy = np.pad(skeleton, pad_width=1, mode='constant', constant_values=0)
|
|
763
|
+
nonzero_coords = np.transpose(np.nonzero(image_copy))
|
|
764
|
+
for x, y, z in nonzero_coords:
|
|
765
|
+
mini = image_copy[x-1:x+2, y-1:y+2, z-1:z+2]
|
|
766
|
+
nearby_sum = np.sum(mini)
|
|
767
|
+
threshold = 2 * image_copy[x, y, z]
|
|
768
|
+
|
|
769
|
+
if nearby_sum <= threshold:
|
|
770
|
+
endpoints.append((x, y, z))
|
|
771
|
+
"""
|
|
762
772
|
|
|
763
773
|
image_copy = skeleton * verts
|
|
764
774
|
|
|
@@ -776,9 +786,137 @@ def break_and_label_skeleton(skeleton, peaks = 1, branch_removal = 0, comp_dil =
|
|
|
776
786
|
tifffile.imwrite(filename, labeled_image, photometric='minisblack')
|
|
777
787
|
print(f"Broken skeleton saved to {filename}")
|
|
778
788
|
|
|
779
|
-
|
|
789
|
+
if compute:
|
|
790
|
+
|
|
791
|
+
return labeled_image, None, skeleton, None
|
|
792
|
+
|
|
793
|
+
return labeled_image, None, None, None
|
|
794
|
+
|
|
795
|
+
def compute_optional_branchstats(verts, labeled_array, endpoints, xy_scale = 1, z_scale = 1):
|
|
780
796
|
|
|
797
|
+
#Lengths:
|
|
798
|
+
# Get all non-background coordinates and their labels in one pass
|
|
799
|
+
z, y, x = np.where(labeled_array != 0)
|
|
800
|
+
labels = labeled_array[z, y, x]
|
|
781
801
|
|
|
802
|
+
# Sort by label
|
|
803
|
+
sort_idx = np.argsort(labels)
|
|
804
|
+
labels_sorted = labels[sort_idx]
|
|
805
|
+
z_sorted = z[sort_idx]
|
|
806
|
+
y_sorted = y[sort_idx]
|
|
807
|
+
x_sorted = x[sort_idx]
|
|
808
|
+
|
|
809
|
+
# Find where each label starts
|
|
810
|
+
unique_labels, split_idx = np.unique(labels_sorted, return_index=True)
|
|
811
|
+
split_idx = split_idx[1:] # Remove first index for np.split
|
|
812
|
+
|
|
813
|
+
# Split into groups
|
|
814
|
+
z_split = np.split(z_sorted, split_idx)
|
|
815
|
+
y_split = np.split(y_sorted, split_idx)
|
|
816
|
+
x_split = np.split(x_sorted, split_idx)
|
|
817
|
+
|
|
818
|
+
# Build dict
|
|
819
|
+
coords_dict = {label: np.column_stack([z, y, x])
|
|
820
|
+
for label, z, y, x in zip(unique_labels, z_split, y_split, x_split)}
|
|
821
|
+
|
|
822
|
+
from sklearn.neighbors import NearestNeighbors
|
|
823
|
+
from scipy.spatial.distance import pdist, squareform
|
|
824
|
+
len_dict = {}
|
|
825
|
+
tortuosity_dict = {}
|
|
826
|
+
angle_dict = {}
|
|
827
|
+
for label, coords in coords_dict.items():
|
|
828
|
+
len_dict[label] = morphology.calculate_skeleton_lengths(labeled_array.shape, xy_scale=xy_scale, z_scale=z_scale, skeleton_coords=coords)
|
|
829
|
+
|
|
830
|
+
# Find neighbors for all points at once
|
|
831
|
+
nbrs = NearestNeighbors(radius=1.74, algorithm='kd_tree').fit(coords)
|
|
832
|
+
neighbor_counts = nbrs.radius_neighbors(coords, return_distance=False)
|
|
833
|
+
neighbor_counts = np.array([len(n) - 1 for n in neighbor_counts]) # -1 to exclude self
|
|
834
|
+
|
|
835
|
+
# Endpoints have exactly 1 neighbor
|
|
836
|
+
endpoints = coords[neighbor_counts == 1]
|
|
837
|
+
|
|
838
|
+
if len(endpoints) > 1:
|
|
839
|
+
# Scale endpoints
|
|
840
|
+
scaled_endpoints = endpoints.copy().astype(float)
|
|
841
|
+
scaled_endpoints[:, 0] *= z_scale # z dimension
|
|
842
|
+
scaled_endpoints[:, 1] *= xy_scale # y dimension
|
|
843
|
+
scaled_endpoints[:, 2] *= xy_scale # x dimension
|
|
844
|
+
|
|
845
|
+
# calculate distances on scaled coordinates
|
|
846
|
+
distances = pdist(scaled_endpoints, metric='euclidean')
|
|
847
|
+
max_distance = distances.max()
|
|
848
|
+
|
|
849
|
+
tortuosity_dict[label] = len_dict[label]/max_distance
|
|
850
|
+
|
|
851
|
+
"""
|
|
852
|
+
verts = invert_array(verts)
|
|
853
|
+
for x, y, z in endpoints:
|
|
854
|
+
try:
|
|
855
|
+
verts[z,y,x] = 1
|
|
856
|
+
except IndexError:
|
|
857
|
+
print(x, y, z)
|
|
858
|
+
|
|
859
|
+
temp_network = Network_3D(nodes = verts, edges = labeled_array, xy_scale = xy_scale, z_scale = z_scale)
|
|
860
|
+
temp_network.calculate_all(temp_network.nodes, temp_network.edges, xy_scale = temp_network.xy_scale, z_scale = temp_network.z_scale, search = None, diledge = None, inners = False, remove_trunk = 0, ignore_search_region = True, other_nodes = None, label_nodes = True, directory = None, GPU = False, fast_dil = False, skeletonize = False, GPU_downsample = None)
|
|
861
|
+
temp_network.calculate_node_centroids()
|
|
862
|
+
from itertools import combinations
|
|
863
|
+
for node in temp_network.network.nodes:
|
|
864
|
+
neighbors = list(temp_network.network.neighbors(node))
|
|
865
|
+
|
|
866
|
+
# Skip if fewer than 2 neighbors (endpoints or isolated nodes)
|
|
867
|
+
if len(neighbors) < 2:
|
|
868
|
+
continue
|
|
869
|
+
|
|
870
|
+
# Get all unique pairs of neighbors
|
|
871
|
+
neighbor_pairs = combinations(neighbors, 2)
|
|
872
|
+
|
|
873
|
+
angles = []
|
|
874
|
+
for neighbor1, neighbor2 in neighbor_pairs:
|
|
875
|
+
# Get coordinates from centroids
|
|
876
|
+
point_a = temp_network.node_centroids[neighbor1]
|
|
877
|
+
point_b = temp_network.node_centroids[node] # vertex
|
|
878
|
+
point_c = temp_network.node_centroids[neighbor2]
|
|
879
|
+
|
|
880
|
+
# Calculate angle
|
|
881
|
+
angle_result = calculate_3d_angle(point_a, point_b, point_c, xy_scale = xy_scale, z_scale = z_scale)
|
|
882
|
+
angles.append(angle_result)
|
|
883
|
+
|
|
884
|
+
angle_dict[node] = angles
|
|
885
|
+
"""
|
|
886
|
+
|
|
887
|
+
return len_dict, tortuosity_dict, angle_dict
|
|
888
|
+
|
|
889
|
+
def calculate_3d_angle(point_a, point_b, point_c, xy_scale = 1, z_scale = 1):
|
|
890
|
+
"""Calculate 3D angle at vertex B between points A-B-C."""
|
|
891
|
+
z1, y1, x1 = point_a
|
|
892
|
+
z2, y2, x2 = point_b # vertex
|
|
893
|
+
z3, y3, x3 = point_c
|
|
894
|
+
|
|
895
|
+
# Apply scaling
|
|
896
|
+
scaled_a = np.array([x1 * xy_scale, y1 * xy_scale, z1 * z_scale])
|
|
897
|
+
scaled_b = np.array([x2 * xy_scale, y2 * xy_scale, z2 * z_scale])
|
|
898
|
+
scaled_c = np.array([x3 * xy_scale, y3 * xy_scale, z3 * z_scale])
|
|
899
|
+
|
|
900
|
+
# Create vectors from vertex B
|
|
901
|
+
vec_ba = scaled_a - scaled_b
|
|
902
|
+
vec_bc = scaled_c - scaled_b
|
|
903
|
+
|
|
904
|
+
# Calculate angle using dot product
|
|
905
|
+
dot_product = np.dot(vec_ba, vec_bc)
|
|
906
|
+
magnitude_ba = np.linalg.norm(vec_ba)
|
|
907
|
+
magnitude_bc = np.linalg.norm(vec_bc)
|
|
908
|
+
|
|
909
|
+
# Avoid division by zero
|
|
910
|
+
if magnitude_ba == 0 or magnitude_bc == 0:
|
|
911
|
+
return {'angle_degrees': 0}
|
|
912
|
+
|
|
913
|
+
cos_angle = dot_product / (magnitude_ba * magnitude_bc)
|
|
914
|
+
cos_angle = np.clip(cos_angle, -1.0, 1.0) # Handle numerical errors
|
|
915
|
+
|
|
916
|
+
angle_radians = np.arccos(cos_angle)
|
|
917
|
+
angle_degrees = np.degrees(angle_radians)
|
|
918
|
+
|
|
919
|
+
return angle_degrees
|
|
782
920
|
|
|
783
921
|
def threshold(arr, proportion, custom_rad = None):
|
|
784
922
|
|
|
@@ -2129,7 +2267,7 @@ def erode(arrayimage, amount, xy_scale = 1, z_scale = 1, mode = 0, preserve_labe
|
|
|
2129
2267
|
arrayimage = binarize(arrayimage)
|
|
2130
2268
|
erode_xy, erode_z = dilation_length_to_pixels(xy_scale, z_scale, amount, amount)
|
|
2131
2269
|
|
|
2132
|
-
if mode ==
|
|
2270
|
+
if mode == 2:
|
|
2133
2271
|
arrayimage = (erode_3D(arrayimage, erode_xy, erode_xy, erode_z)) * 255
|
|
2134
2272
|
else:
|
|
2135
2273
|
arrayimage = erode_3D_dt(arrayimage, amount, xy_scaling=xy_scale, z_scaling=z_scale, preserve_labels = preserve_labels)
|
|
@@ -2184,7 +2322,7 @@ def skeletonize(arrayimage, directory = None):
|
|
|
2184
2322
|
|
|
2185
2323
|
return arrayimage
|
|
2186
2324
|
|
|
2187
|
-
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):
|
|
2325
|
+
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):
|
|
2188
2326
|
"""
|
|
2189
2327
|
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.
|
|
2190
2328
|
: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.
|
|
@@ -2215,10 +2353,10 @@ def label_branches(array, peaks = 0, branch_removal = 0, comp_dil = 0, max_vol =
|
|
|
2215
2353
|
|
|
2216
2354
|
other_array = skeletonize(array)
|
|
2217
2355
|
|
|
2218
|
-
other_array = break_and_label_skeleton(other_array, peaks = peaks, branch_removal = branch_removal, comp_dil = comp_dil, max_vol = max_vol, nodes = nodes)
|
|
2356
|
+
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)
|
|
2219
2357
|
|
|
2220
2358
|
else:
|
|
2221
|
-
array = break_and_label_skeleton(array, peaks = peaks, branch_removal = branch_removal, comp_dil = comp_dil, max_vol = max_vol, nodes = nodes)
|
|
2359
|
+
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, xy_scale = xy_scale, z_scale = z_scale)
|
|
2222
2360
|
|
|
2223
2361
|
if nodes is not None and down_factor is not None:
|
|
2224
2362
|
array = upsample_with_padding(array, down_factor, arrayshape)
|
|
@@ -2257,7 +2395,7 @@ def label_branches(array, peaks = 0, branch_removal = 0, comp_dil = 0, max_vol =
|
|
|
2257
2395
|
print("Branches labelled")
|
|
2258
2396
|
|
|
2259
2397
|
|
|
2260
|
-
return array
|
|
2398
|
+
return array, verts, skele, endpoints
|
|
2261
2399
|
|
|
2262
2400
|
def fix_branches_network(array, G, communities, fix_val = None):
|
|
2263
2401
|
|
|
@@ -184,6 +184,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
184
184
|
3: None
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
self.branch_dict = {
|
|
188
|
+
0: None,
|
|
189
|
+
1: None
|
|
190
|
+
|
|
191
|
+
}
|
|
192
|
+
|
|
187
193
|
self.original_shape = None #For undoing resamples
|
|
188
194
|
|
|
189
195
|
# Create control panel
|
|
@@ -462,6 +468,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
462
468
|
|
|
463
469
|
self.resume = False
|
|
464
470
|
self._first_pan_done = False
|
|
471
|
+
self.thresh_window_ref = None
|
|
465
472
|
|
|
466
473
|
|
|
467
474
|
def load_file(self):
|
|
@@ -2167,6 +2174,16 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2167
2174
|
except:
|
|
2168
2175
|
pass
|
|
2169
2176
|
|
|
2177
|
+
if self.branch_dict[0] is not None:
|
|
2178
|
+
try:
|
|
2179
|
+
info_dict['Branch Length'] = self.branch_dict[0][0][label]
|
|
2180
|
+
except:
|
|
2181
|
+
pass
|
|
2182
|
+
try:
|
|
2183
|
+
info_dict['Branch Tortuosity'] = self.branch_dict[0][1][label]
|
|
2184
|
+
except:
|
|
2185
|
+
pass
|
|
2186
|
+
|
|
2170
2187
|
|
|
2171
2188
|
elif sort == 'edge':
|
|
2172
2189
|
|
|
@@ -2212,6 +2229,16 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2212
2229
|
except:
|
|
2213
2230
|
pass
|
|
2214
2231
|
|
|
2232
|
+
if self.branch_dict[1] is not None:
|
|
2233
|
+
try:
|
|
2234
|
+
info_dict['Branch Length'] = self.branch_dict[1][0][label]
|
|
2235
|
+
except:
|
|
2236
|
+
pass
|
|
2237
|
+
try:
|
|
2238
|
+
info_dict['Branch Tortuosity'] = self.branch_dict[1][1][label]
|
|
2239
|
+
except:
|
|
2240
|
+
pass
|
|
2241
|
+
|
|
2215
2242
|
self.format_for_upperright_table(info_dict, title = f'Info on Object', sort = False)
|
|
2216
2243
|
|
|
2217
2244
|
except:
|
|
@@ -4191,10 +4218,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4191
4218
|
elif self.zoom_mode:
|
|
4192
4219
|
# Handle zoom mode press
|
|
4193
4220
|
if self.original_xlim is None:
|
|
4194
|
-
self.original_xlim = self.
|
|
4195
|
-
|
|
4196
|
-
self.original_ylim = self.ax.get_ylim()
|
|
4197
|
-
#print(self.original_ylim)
|
|
4221
|
+
self.original_xlim = (-0.5, self.shape[2] - 0.5)
|
|
4222
|
+
self.original_ylim = (self.shape[1] + 0.5, -0.5)
|
|
4198
4223
|
|
|
4199
4224
|
current_xlim = self.ax.get_xlim()
|
|
4200
4225
|
current_ylim = self.ax.get_ylim()
|
|
@@ -4356,8 +4381,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4356
4381
|
if self.zoom_mode:
|
|
4357
4382
|
# Existing zoom functionality
|
|
4358
4383
|
if self.original_xlim is None:
|
|
4359
|
-
self.original_xlim = self.
|
|
4360
|
-
self.original_ylim = self.
|
|
4384
|
+
self.original_xlim = (-0.5, self.shape[2] - 0.5)
|
|
4385
|
+
self.original_ylim = (self.shape[1] + 0.5, -0.5)
|
|
4361
4386
|
|
|
4362
4387
|
current_xlim = self.ax.get_xlim()
|
|
4363
4388
|
current_ylim = self.ax.get_ylim()
|
|
@@ -4634,6 +4659,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4634
4659
|
image_menu = process_menu.addMenu("Image")
|
|
4635
4660
|
resize_action = image_menu.addAction("Resize (Up/Downsample)")
|
|
4636
4661
|
resize_action.triggered.connect(self.show_resize_dialog)
|
|
4662
|
+
clean_action = image_menu.addAction("Clean Segmentation")
|
|
4663
|
+
clean_action.triggered.connect(self.show_clean_dialog)
|
|
4637
4664
|
dilate_action = image_menu.addAction("Dilate")
|
|
4638
4665
|
dilate_action.triggered.connect(self.show_dilate_dialog)
|
|
4639
4666
|
erode_action = image_menu.addAction("Erode")
|
|
@@ -5225,6 +5252,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5225
5252
|
|
|
5226
5253
|
self.load_channel(0, my_network.edges, data = True)
|
|
5227
5254
|
|
|
5255
|
+
try:
|
|
5256
|
+
self.branch_dict[0] = self.branch_dict[1]
|
|
5257
|
+
self.branch_dict[1] = None
|
|
5258
|
+
except:
|
|
5259
|
+
pass
|
|
5260
|
+
|
|
5228
5261
|
self.delete_channel(1, False)
|
|
5229
5262
|
|
|
5230
5263
|
my_network.morph_proximity(search = [3,3], fastdil = True)
|
|
@@ -5241,14 +5274,14 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5241
5274
|
dialog = CentroidDialog(self)
|
|
5242
5275
|
dialog.exec()
|
|
5243
5276
|
|
|
5244
|
-
def show_dilate_dialog(self):
|
|
5277
|
+
def show_dilate_dialog(self, args = None):
|
|
5245
5278
|
"""show the dilate dialog"""
|
|
5246
|
-
dialog = DilateDialog(self)
|
|
5279
|
+
dialog = DilateDialog(self, args)
|
|
5247
5280
|
dialog.exec()
|
|
5248
5281
|
|
|
5249
|
-
def show_erode_dialog(self):
|
|
5282
|
+
def show_erode_dialog(self, args = None):
|
|
5250
5283
|
"""show the erode dialog"""
|
|
5251
|
-
dialog = ErodeDialog(self)
|
|
5284
|
+
dialog = ErodeDialog(self, args)
|
|
5252
5285
|
dialog.exec()
|
|
5253
5286
|
|
|
5254
5287
|
def show_hole_dialog(self):
|
|
@@ -5347,6 +5380,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5347
5380
|
dialog = ResizeDialog(self)
|
|
5348
5381
|
dialog.exec()
|
|
5349
5382
|
|
|
5383
|
+
def show_clean_dialog(self):
|
|
5384
|
+
dialog = CleanDialog(self)
|
|
5385
|
+
dialog.show()
|
|
5350
5386
|
|
|
5351
5387
|
def show_properties_dialog(self):
|
|
5352
5388
|
"""Show the properties dialog"""
|
|
@@ -6029,7 +6065,11 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6029
6065
|
print(f"xy_scale property set to {my_network.xy_scale}; z_scale property set to {my_network.z_scale}")
|
|
6030
6066
|
except:
|
|
6031
6067
|
pass
|
|
6032
|
-
|
|
6068
|
+
test_channel_data = tifffile.imread(filename)
|
|
6069
|
+
if len(test_channel_data.shape) not in (2, 3, 4):
|
|
6070
|
+
print("Invalid Shape")
|
|
6071
|
+
return
|
|
6072
|
+
self.channel_data[channel_index] = test_channel_data
|
|
6033
6073
|
|
|
6034
6074
|
elif file_extension == 'nii':
|
|
6035
6075
|
import nibabel as nib
|
|
@@ -6194,7 +6234,11 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6194
6234
|
|
|
6195
6235
|
if self.shape == self.channel_data[channel_index].shape:
|
|
6196
6236
|
preserve_zoom = (self.ax.get_xlim(), self.ax.get_ylim())
|
|
6197
|
-
|
|
6237
|
+
self.shape = (self.channel_data[channel_index].shape[0], self.channel_data[channel_index].shape[1], self.channel_data[channel_index].shape[2])
|
|
6238
|
+
else:
|
|
6239
|
+
self.shape = (self.channel_data[channel_index].shape[0], self.channel_data[channel_index].shape[1], self.channel_data[channel_index].shape[2])
|
|
6240
|
+
ylim, xlim = (self.shape[1] + 0.5, -0.5), (-0.5, self.shape[2] - 0.5)
|
|
6241
|
+
preserve_zoom = (xlim, ylim)
|
|
6198
6242
|
if self.shape[1] * self.shape[2] > 3000 * 3000 * self.downsample_factor:
|
|
6199
6243
|
self.throttle = True
|
|
6200
6244
|
else:
|
|
@@ -6202,7 +6246,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6202
6246
|
|
|
6203
6247
|
|
|
6204
6248
|
self.img_height, self.img_width = self.shape[1], self.shape[2]
|
|
6205
|
-
self.original_ylim, self.original_xlim = (self.shape[1] + 0.5, -0.5), (-0.5, self.shape[2] - 0.5)
|
|
6206
6249
|
|
|
6207
6250
|
self.completed_paint_strokes = [] #Reset pending paint operations
|
|
6208
6251
|
self.current_stroke_points = []
|
|
@@ -6836,8 +6879,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6836
6879
|
|
|
6837
6880
|
except Exception as e:
|
|
6838
6881
|
pass
|
|
6839
|
-
#import traceback
|
|
6840
|
-
#print(traceback.format_exc())
|
|
6841
6882
|
|
|
6842
6883
|
|
|
6843
6884
|
def get_channel_image(self, channel):
|
|
@@ -8665,6 +8706,9 @@ class Show3dDialog(QDialog):
|
|
|
8665
8706
|
if visible:
|
|
8666
8707
|
arrays_4d.append(channel)
|
|
8667
8708
|
|
|
8709
|
+
if self.parent().thresh_window_ref is not None:
|
|
8710
|
+
self.parent().thresh_window_ref.make_full_highlight()
|
|
8711
|
+
|
|
8668
8712
|
if self.parent().highlight_overlay is not None or self.parent().mini_overlay_data is not None:
|
|
8669
8713
|
if self.parent().mini_overlay == True:
|
|
8670
8714
|
self.parent().create_highlight_overlay(node_indices = self.parent().clicked_values['nodes'], edge_indices = self.parent().clicked_values['edges'])
|
|
@@ -11046,6 +11090,79 @@ class ResizeDialog(QDialog):
|
|
|
11046
11090
|
print(traceback.format_exc())
|
|
11047
11091
|
QMessageBox.critical(self, "Error", f"Failed to resize: {str(e)}")
|
|
11048
11092
|
|
|
11093
|
+
class CleanDialog(QDialog):
|
|
11094
|
+
def __init__(self, parent=None):
|
|
11095
|
+
super().__init__(parent)
|
|
11096
|
+
self.setWindowTitle("Some options for cleaning segmentation")
|
|
11097
|
+
self.setModal(False)
|
|
11098
|
+
|
|
11099
|
+
layout = QFormLayout(self)
|
|
11100
|
+
|
|
11101
|
+
# Add Run button
|
|
11102
|
+
run_button = QPushButton("Close")
|
|
11103
|
+
run_button.clicked.connect(self.close)
|
|
11104
|
+
layout.addRow("Close (Fill Small Gaps - Dilate then Erode by same amount):", run_button)
|
|
11105
|
+
|
|
11106
|
+
# Add Run button
|
|
11107
|
+
run_button = QPushButton("Open")
|
|
11108
|
+
run_button.clicked.connect(self.open)
|
|
11109
|
+
layout.addRow("Open (Eliminate Noise, Jagged Borders, and Small Connections Between Objects - Erode then Dilate by same amount):", run_button)
|
|
11110
|
+
|
|
11111
|
+
# Add Run button
|
|
11112
|
+
run_button = QPushButton("Fill Holes")
|
|
11113
|
+
run_button.clicked.connect(self.holes)
|
|
11114
|
+
layout.addRow("Call the fill holes function:", run_button)
|
|
11115
|
+
|
|
11116
|
+
# Add Run button
|
|
11117
|
+
run_button = QPushButton("Threshold Noise")
|
|
11118
|
+
run_button.clicked.connect(self.thresh)
|
|
11119
|
+
layout.addRow("Threshold Noise By Volume:", run_button)
|
|
11120
|
+
|
|
11121
|
+
def close(self):
|
|
11122
|
+
|
|
11123
|
+
try:
|
|
11124
|
+
self.parent().show_dilate_dialog(args = [1])
|
|
11125
|
+
self.parent().show_erode_dialog(args = [self.parent().last_dil])
|
|
11126
|
+
except:
|
|
11127
|
+
pass
|
|
11128
|
+
|
|
11129
|
+
def open(self):
|
|
11130
|
+
|
|
11131
|
+
try:
|
|
11132
|
+
self.parent().show_erode_dialog(args = [1])
|
|
11133
|
+
self.parent().show_dilate_dialog(args = [self.parent().last_ero])
|
|
11134
|
+
except:
|
|
11135
|
+
pass
|
|
11136
|
+
|
|
11137
|
+
def holes(self):
|
|
11138
|
+
|
|
11139
|
+
try:
|
|
11140
|
+
self.parent().show_hole_dialog()
|
|
11141
|
+
except:
|
|
11142
|
+
pass
|
|
11143
|
+
|
|
11144
|
+
def thresh(self):
|
|
11145
|
+
try:
|
|
11146
|
+
if len(np.unique(self.parent().channel_data[self.parent().active_channel])) < 3:
|
|
11147
|
+
self.parent().show_label_dialog()
|
|
11148
|
+
|
|
11149
|
+
if self.parent().volume_dict[self.parent().active_channel] is None:
|
|
11150
|
+
self.parent().volumes()
|
|
11151
|
+
|
|
11152
|
+
thresh_window = ThresholdWindow(self.parent(), 1)
|
|
11153
|
+
thresh_window.show() # Non-modal window
|
|
11154
|
+
self.parent().highlight_overlay = None
|
|
11155
|
+
#self.mini_overlay = False
|
|
11156
|
+
self.parent().mini_overlay_data = None
|
|
11157
|
+
except:
|
|
11158
|
+
import traceback
|
|
11159
|
+
print(traceback.format_exc())
|
|
11160
|
+
pass
|
|
11161
|
+
|
|
11162
|
+
|
|
11163
|
+
|
|
11164
|
+
|
|
11165
|
+
|
|
11049
11166
|
|
|
11050
11167
|
class OverrideDialog(QDialog):
|
|
11051
11168
|
def __init__(self, parent=None):
|
|
@@ -12356,6 +12473,7 @@ class ThresholdWindow(QMainWindow):
|
|
|
12356
12473
|
|
|
12357
12474
|
def __init__(self, parent=None, accepted_mode=0):
|
|
12358
12475
|
super().__init__(parent)
|
|
12476
|
+
self.parent().thresh_window_ref = self
|
|
12359
12477
|
self.setWindowTitle("Threshold")
|
|
12360
12478
|
|
|
12361
12479
|
self.accepted_mode = accepted_mode
|
|
@@ -12509,10 +12627,8 @@ class ThresholdWindow(QMainWindow):
|
|
|
12509
12627
|
self.processing_cancelled.emit()
|
|
12510
12628
|
self.close()
|
|
12511
12629
|
|
|
12512
|
-
def
|
|
12513
|
-
|
|
12514
|
-
self.parent().targs = None
|
|
12515
|
-
self.parent().bounds = False
|
|
12630
|
+
def make_full_highlight(self):
|
|
12631
|
+
|
|
12516
12632
|
try: # could probably be refactored but this just handles keeping the highlight elements if the user presses X
|
|
12517
12633
|
if self.chan == 0:
|
|
12518
12634
|
if not self.bounds:
|
|
@@ -12546,6 +12662,14 @@ class ThresholdWindow(QMainWindow):
|
|
|
12546
12662
|
pass
|
|
12547
12663
|
|
|
12548
12664
|
|
|
12665
|
+
def closeEvent(self, event):
|
|
12666
|
+
self.parent().preview = False
|
|
12667
|
+
self.parent().targs = None
|
|
12668
|
+
self.parent().bounds = False
|
|
12669
|
+
self.parent().thresh_window_ref = None
|
|
12670
|
+
self.make_full_highlight()
|
|
12671
|
+
|
|
12672
|
+
|
|
12549
12673
|
def get_values_in_range_all_vols(self, chan, min_val, max_val):
|
|
12550
12674
|
output = []
|
|
12551
12675
|
if self.accepted_mode == 1:
|
|
@@ -12812,14 +12936,21 @@ class SmartDilateDialog(QDialog):
|
|
|
12812
12936
|
|
|
12813
12937
|
|
|
12814
12938
|
class DilateDialog(QDialog):
|
|
12815
|
-
def __init__(self, parent=None):
|
|
12939
|
+
def __init__(self, parent=None, args = None):
|
|
12816
12940
|
super().__init__(parent)
|
|
12817
12941
|
self.setWindowTitle("Dilate Parameters")
|
|
12818
12942
|
self.setModal(True)
|
|
12819
12943
|
|
|
12820
12944
|
layout = QFormLayout(self)
|
|
12821
12945
|
|
|
12822
|
-
|
|
12946
|
+
if args:
|
|
12947
|
+
self.parent().last_dil = args[0]
|
|
12948
|
+
self.index = 1
|
|
12949
|
+
else:
|
|
12950
|
+
self.parent().last_dil = 1
|
|
12951
|
+
self.index = 0
|
|
12952
|
+
|
|
12953
|
+
self.amount = QLineEdit(f"{self.parent().last_dil}")
|
|
12823
12954
|
layout.addRow("Dilation Radius:", self.amount)
|
|
12824
12955
|
|
|
12825
12956
|
if my_network.xy_scale is not None:
|
|
@@ -12840,8 +12971,8 @@ class DilateDialog(QDialog):
|
|
|
12840
12971
|
|
|
12841
12972
|
# Add mode selection dropdown
|
|
12842
12973
|
self.mode_selector = QComboBox()
|
|
12843
|
-
self.mode_selector.addItems(["
|
|
12844
|
-
self.mode_selector.setCurrentIndex(
|
|
12974
|
+
self.mode_selector.addItems(["Distance Transform-Based (Slower but more accurate at larger dilations)", "Preserve Labels (slower)", "Pseudo3D Binary Kernels (For Fast, small dilations)"])
|
|
12975
|
+
self.mode_selector.setCurrentIndex(self.index) # Default to Mode 1
|
|
12845
12976
|
layout.addRow("Execution Mode:", self.mode_selector)
|
|
12846
12977
|
|
|
12847
12978
|
# Add Run button
|
|
@@ -12883,13 +13014,15 @@ class DilateDialog(QDialog):
|
|
|
12883
13014
|
if active_data is None:
|
|
12884
13015
|
raise ValueError("No active image selected")
|
|
12885
13016
|
|
|
13017
|
+
self.parent().last_dil = amount
|
|
13018
|
+
|
|
12886
13019
|
if accepted_mode == 1:
|
|
12887
13020
|
dialog = SmartDilateDialog(self.parent(), [active_data, amount, xy_scale, z_scale])
|
|
12888
13021
|
dialog.exec()
|
|
12889
13022
|
self.accept()
|
|
12890
13023
|
return
|
|
12891
13024
|
|
|
12892
|
-
if accepted_mode ==
|
|
13025
|
+
if accepted_mode == 0:
|
|
12893
13026
|
result = n3d.dilate_3D_dt(active_data, amount, xy_scaling = xy_scale, z_scaling = z_scale)
|
|
12894
13027
|
else:
|
|
12895
13028
|
|
|
@@ -12919,14 +13052,21 @@ class DilateDialog(QDialog):
|
|
|
12919
13052
|
)
|
|
12920
13053
|
|
|
12921
13054
|
class ErodeDialog(QDialog):
|
|
12922
|
-
def __init__(self, parent=None):
|
|
13055
|
+
def __init__(self, parent=None, args = None):
|
|
12923
13056
|
super().__init__(parent)
|
|
12924
13057
|
self.setWindowTitle("Erosion Parameters")
|
|
12925
13058
|
self.setModal(True)
|
|
12926
13059
|
|
|
12927
13060
|
layout = QFormLayout(self)
|
|
12928
13061
|
|
|
12929
|
-
|
|
13062
|
+
if args:
|
|
13063
|
+
self.parent().last_ero = args[0]
|
|
13064
|
+
self.index = 1
|
|
13065
|
+
else:
|
|
13066
|
+
self.parent().last_ero = 1
|
|
13067
|
+
self.index = 0
|
|
13068
|
+
|
|
13069
|
+
self.amount = QLineEdit(f"{self.parent().last_ero}")
|
|
12930
13070
|
layout.addRow("Erosion Radius:", self.amount)
|
|
12931
13071
|
|
|
12932
13072
|
if my_network.xy_scale is not None:
|
|
@@ -12947,8 +13087,8 @@ class ErodeDialog(QDialog):
|
|
|
12947
13087
|
|
|
12948
13088
|
# Add mode selection dropdown
|
|
12949
13089
|
self.mode_selector = QComboBox()
|
|
12950
|
-
self.mode_selector.addItems(["
|
|
12951
|
-
self.mode_selector.setCurrentIndex(
|
|
13090
|
+
self.mode_selector.addItems(["Distance Transform-Based (Slower but more accurate at larger erosions)", "Preserve Labels (Slower)", "Pseudo3D Binary Kernels (For Fast, small erosions)"])
|
|
13091
|
+
self.mode_selector.setCurrentIndex(self.index) # Default to Mode 1
|
|
12952
13092
|
layout.addRow("Execution Mode:", self.mode_selector)
|
|
12953
13093
|
|
|
12954
13094
|
# Add Run button
|
|
@@ -12984,8 +13124,7 @@ class ErodeDialog(QDialog):
|
|
|
12984
13124
|
|
|
12985
13125
|
mode = self.mode_selector.currentIndex()
|
|
12986
13126
|
|
|
12987
|
-
if mode ==
|
|
12988
|
-
mode = 1
|
|
13127
|
+
if mode == 1:
|
|
12989
13128
|
preserve_labels = True
|
|
12990
13129
|
else:
|
|
12991
13130
|
preserve_labels = False
|
|
@@ -13007,7 +13146,7 @@ class ErodeDialog(QDialog):
|
|
|
13007
13146
|
|
|
13008
13147
|
|
|
13009
13148
|
self.parent().load_channel(self.parent().active_channel, result, True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
13010
|
-
|
|
13149
|
+
self.parent().last_ero = amount
|
|
13011
13150
|
self.accept()
|
|
13012
13151
|
|
|
13013
13152
|
except Exception as e:
|
|
@@ -13037,6 +13176,11 @@ class HoleDialog(QDialog):
|
|
|
13037
13176
|
self.borders.setChecked(False)
|
|
13038
13177
|
layout.addRow("Fill Small Holes Along Borders:", self.borders)
|
|
13039
13178
|
|
|
13179
|
+
self.preserve_labels = QPushButton("Preserve Labels")
|
|
13180
|
+
self.preserve_labels.setCheckable(True)
|
|
13181
|
+
self.preserve_labels.setChecked(False)
|
|
13182
|
+
layout.addRow("Preserve Labels (Slower):", self.preserve_labels)
|
|
13183
|
+
|
|
13040
13184
|
self.sep_holes = QPushButton("Seperate Hole Mask")
|
|
13041
13185
|
self.sep_holes.setCheckable(True)
|
|
13042
13186
|
self.sep_holes.setChecked(False)
|
|
@@ -13059,6 +13203,9 @@ class HoleDialog(QDialog):
|
|
|
13059
13203
|
borders = self.borders.isChecked()
|
|
13060
13204
|
headon = self.headon.isChecked()
|
|
13061
13205
|
sep_holes = self.sep_holes.isChecked()
|
|
13206
|
+
preserve_labels = self.preserve_labels.isChecked()
|
|
13207
|
+
if preserve_labels:
|
|
13208
|
+
label_copy = np.copy(active_data)
|
|
13062
13209
|
|
|
13063
13210
|
if borders:
|
|
13064
13211
|
|
|
@@ -13077,7 +13224,11 @@ class HoleDialog(QDialog):
|
|
|
13077
13224
|
fill_borders = borders
|
|
13078
13225
|
)
|
|
13079
13226
|
|
|
13227
|
+
|
|
13080
13228
|
if not sep_holes:
|
|
13229
|
+
if preserve_labels:
|
|
13230
|
+
result = sdl.smart_label(result, label_copy, directory = None, GPU = False, remove_template = True)
|
|
13231
|
+
|
|
13081
13232
|
self.parent().load_channel(self.parent().active_channel, result, True)
|
|
13082
13233
|
else:
|
|
13083
13234
|
self.parent().load_channel(3, active_data - result, True)
|
|
@@ -14071,7 +14222,6 @@ class GenNodesDialog(QDialog):
|
|
|
14071
14222
|
order = order,
|
|
14072
14223
|
return_skele = True,
|
|
14073
14224
|
fastdil = fastdil
|
|
14074
|
-
|
|
14075
14225
|
)
|
|
14076
14226
|
|
|
14077
14227
|
if down_factor > 0 and not self.called:
|
|
@@ -14163,7 +14313,7 @@ class BranchDialog(QDialog):
|
|
|
14163
14313
|
self.fix3.setChecked(True)
|
|
14164
14314
|
else:
|
|
14165
14315
|
self.fix3.setChecked(False)
|
|
14166
|
-
correction_layout.addWidget(QLabel("Split Nontouching Branches
|
|
14316
|
+
correction_layout.addWidget(QLabel("Split Nontouching Branches?: "), 4, 0)
|
|
14167
14317
|
correction_layout.addWidget(self.fix3, 4, 1)
|
|
14168
14318
|
|
|
14169
14319
|
correction_group.setLayout(correction_layout)
|
|
@@ -14191,20 +14341,27 @@ class BranchDialog(QDialog):
|
|
|
14191
14341
|
# --- Misc Options Group ---
|
|
14192
14342
|
misc_group = QGroupBox("Misc Options")
|
|
14193
14343
|
misc_layout = QGridLayout()
|
|
14344
|
+
|
|
14345
|
+
# optional computation checkbox
|
|
14346
|
+
self.compute = QPushButton("Branch Stats")
|
|
14347
|
+
self.compute.setCheckable(True)
|
|
14348
|
+
self.compute.setChecked(True)
|
|
14349
|
+
misc_layout.addWidget(QLabel("Compute Branch Stats (Branch Lengths, Tortuosity. Set xy_scale and z_scale in properties first if real distances are desired.):"), 0, 0)
|
|
14350
|
+
misc_layout.addWidget(self.compute, 0, 1)
|
|
14194
14351
|
|
|
14195
14352
|
# Nodes checkbox
|
|
14196
14353
|
self.nodes = QPushButton("Generate Nodes")
|
|
14197
14354
|
self.nodes.setCheckable(True)
|
|
14198
14355
|
self.nodes.setChecked(True)
|
|
14199
|
-
misc_layout.addWidget(QLabel("Generate nodes from edges? (Skip if already completed):"),
|
|
14200
|
-
misc_layout.addWidget(self.nodes,
|
|
14356
|
+
misc_layout.addWidget(QLabel("Generate nodes from edges? (Skip if already completed):"), 1, 0)
|
|
14357
|
+
misc_layout.addWidget(self.nodes, 1, 1)
|
|
14201
14358
|
|
|
14202
14359
|
# GPU checkbox
|
|
14203
14360
|
self.GPU = QPushButton("GPU")
|
|
14204
14361
|
self.GPU.setCheckable(True)
|
|
14205
14362
|
self.GPU.setChecked(False)
|
|
14206
|
-
misc_layout.addWidget(QLabel("Use GPU (May downsample large images):"),
|
|
14207
|
-
misc_layout.addWidget(self.GPU,
|
|
14363
|
+
misc_layout.addWidget(QLabel("Use GPU (May downsample large images):"), 2, 0)
|
|
14364
|
+
misc_layout.addWidget(self.GPU, 2, 1)
|
|
14208
14365
|
|
|
14209
14366
|
misc_group.setLayout(misc_layout)
|
|
14210
14367
|
main_layout.addWidget(misc_group)
|
|
@@ -14238,6 +14395,7 @@ class BranchDialog(QDialog):
|
|
|
14238
14395
|
fix3 = self.fix3.isChecked()
|
|
14239
14396
|
fix_val = float(self.fix_val.text()) if self.fix_val.text() else None
|
|
14240
14397
|
seed = int(self.seed.text()) if self.seed.text() else None
|
|
14398
|
+
compute = self.compute.isChecked()
|
|
14241
14399
|
|
|
14242
14400
|
if my_network.edges is None and my_network.nodes is not None:
|
|
14243
14401
|
self.parent().load_channel(1, my_network.nodes, data = True)
|
|
@@ -14254,7 +14412,7 @@ class BranchDialog(QDialog):
|
|
|
14254
14412
|
|
|
14255
14413
|
if my_network.edges is not None and my_network.nodes is not None and my_network.id_overlay is not None:
|
|
14256
14414
|
|
|
14257
|
-
output = n3d.label_branches(my_network.edges, nodes = my_network.nodes, bonus_array = original_array, GPU = GPU, down_factor = down_factor, arrayshape = original_shape)
|
|
14415
|
+
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, xy_scale = my_network.xy_scale, z_scale = my_network.z_scale)
|
|
14258
14416
|
|
|
14259
14417
|
if fix2:
|
|
14260
14418
|
|
|
@@ -14293,6 +14451,19 @@ class BranchDialog(QDialog):
|
|
|
14293
14451
|
|
|
14294
14452
|
output = self.parent().separate_nontouching_objects(output, max_val=np.max(output))
|
|
14295
14453
|
|
|
14454
|
+
if compute:
|
|
14455
|
+
labeled_image = (skeleton != 0) * output
|
|
14456
|
+
len_dict, tortuosity_dict, angle_dict = n3d.compute_optional_branchstats(verts, labeled_image, endpoints, xy_scale = my_network.xy_scale, z_scale = my_network.z_scale)
|
|
14457
|
+
self.parent().branch_dict[1] = [len_dict, tortuosity_dict]
|
|
14458
|
+
#max_length = max(len(v) for v in angle_dict.values())
|
|
14459
|
+
#title = [str(i+1) if i < 2 else i+1 for i in range(max_length)]
|
|
14460
|
+
|
|
14461
|
+
#del labeled_image
|
|
14462
|
+
|
|
14463
|
+
self.parent().format_for_upperright_table(len_dict, 'BranchID', 'Length (Scaled)', 'Branch Lengths')
|
|
14464
|
+
self.parent().format_for_upperright_table(tortuosity_dict, 'BranchID', 'Tortuosity', 'Branch Tortuosities')
|
|
14465
|
+
#self.parent().format_for_upperright_table(angle_dict, 'Vertex ID', title, 'Branch Angles')
|
|
14466
|
+
|
|
14296
14467
|
|
|
14297
14468
|
if down_factor is not None:
|
|
14298
14469
|
|
|
@@ -3,7 +3,7 @@ import tifffile
|
|
|
3
3
|
from scipy import ndimage
|
|
4
4
|
from PIL import Image, ImageDraw, ImageFont
|
|
5
5
|
from scipy.ndimage import zoom
|
|
6
|
-
|
|
6
|
+
import cv2
|
|
7
7
|
|
|
8
8
|
def downsample(data, factor, directory=None, order=0):
|
|
9
9
|
"""
|
|
@@ -121,83 +121,62 @@ def draw_nodes(nodes, num_nodes):
|
|
|
121
121
|
# Save the draw_array as a 3D TIFF file
|
|
122
122
|
tifffile.imwrite("labelled_nodes.tif", draw_array)
|
|
123
123
|
|
|
124
|
-
def draw_from_centroids(nodes, num_nodes, centroids, twod_bool, directory
|
|
125
|
-
"""
|
|
126
|
-
print("Drawing node IDs
|
|
127
|
-
# Create a new 3D array to draw on with the same dimensions as the original array
|
|
124
|
+
def draw_from_centroids(nodes, num_nodes, centroids, twod_bool, directory=None):
|
|
125
|
+
"""Optimized version using OpenCV"""
|
|
126
|
+
print("Drawing node IDs...")
|
|
128
127
|
draw_array = np.zeros_like(nodes, dtype=np.uint8)
|
|
129
|
-
|
|
130
|
-
#
|
|
131
|
-
|
|
132
|
-
# Iterate through each centroid
|
|
128
|
+
|
|
129
|
+
# Draw text using OpenCV (no PIL conversions needed)
|
|
133
130
|
for idx in centroids.keys():
|
|
134
131
|
centroid = centroids[idx]
|
|
135
132
|
z, y, x = centroid.astype(int)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
draw_array = _draw_at_plane(z + 1, y, x, draw_array, idx)
|
|
144
|
-
except IndexError:
|
|
145
|
-
pass
|
|
146
|
-
|
|
147
|
-
try:
|
|
148
|
-
draw_array = _draw_at_plane(z - 1, y, x, draw_array, idx)
|
|
149
|
-
except IndexError:
|
|
150
|
-
pass
|
|
151
|
-
|
|
133
|
+
|
|
134
|
+
for z_offset in [0, 1, -1]:
|
|
135
|
+
z_target = z + z_offset
|
|
136
|
+
if 0 <= z_target < draw_array.shape[0]:
|
|
137
|
+
cv2.putText(draw_array[z_target], str(idx), (x, y),
|
|
138
|
+
cv2.FONT_HERSHEY_SIMPLEX, 0.4, 255, 1, cv2.LINE_AA)
|
|
139
|
+
|
|
152
140
|
if twod_bool:
|
|
153
141
|
draw_array = draw_array[0,:,:] | draw_array[1,:,:]
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if directory is None:
|
|
157
|
-
filename = 'labelled_node_indices.tif'
|
|
158
|
-
else:
|
|
159
|
-
filename = f'{directory}/labelled_node_indices.tif'
|
|
160
|
-
|
|
142
|
+
|
|
143
|
+
filename = f'{directory}/labelled_node_indices.tif' if directory else 'labelled_node_indices.tif'
|
|
161
144
|
try:
|
|
162
|
-
|
|
163
|
-
# Save the draw_array as a 3D TIFF file
|
|
164
145
|
tifffile.imwrite(filename, draw_array)
|
|
165
|
-
|
|
166
146
|
except Exception as e:
|
|
167
147
|
print(f"Could not save node indices to {filename}")
|
|
168
|
-
|
|
148
|
+
|
|
169
149
|
return draw_array
|
|
170
150
|
|
|
171
151
|
def degree_draw(degree_dict, centroid_dict, nodes):
|
|
152
|
+
"""Draw node degrees at centroid locations using OpenCV"""
|
|
172
153
|
# Create a new 3D array to draw on with the same dimensions as the original array
|
|
173
154
|
draw_array = np.zeros_like(nodes, dtype=np.uint8)
|
|
174
|
-
|
|
175
|
-
|
|
155
|
+
|
|
176
156
|
for node in centroid_dict:
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
degree = degree_dict[node]
|
|
180
|
-
except:
|
|
157
|
+
# Skip if node not in degree_dict
|
|
158
|
+
if node not in degree_dict:
|
|
181
159
|
continue
|
|
182
160
|
|
|
161
|
+
degree = degree_dict[node]
|
|
183
162
|
z, y, x = centroid_dict[node].astype(int)
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
163
|
+
|
|
164
|
+
# Draw on current z-plane and adjacent planes
|
|
165
|
+
for z_offset in [0, 1, -1]:
|
|
166
|
+
z_target = z + z_offset
|
|
167
|
+
# Check bounds
|
|
168
|
+
if 0 <= z_target < draw_array.shape[0]:
|
|
169
|
+
cv2.putText(
|
|
170
|
+
draw_array[z_target], # Image to draw on
|
|
171
|
+
str(degree), # Text to draw
|
|
172
|
+
(x, y), # Position (x, y)
|
|
173
|
+
cv2.FONT_HERSHEY_SIMPLEX, # Font
|
|
174
|
+
0.4, # Font scale
|
|
175
|
+
255, # Color (white)
|
|
176
|
+
1, # Thickness
|
|
177
|
+
cv2.LINE_AA # Anti-aliasing
|
|
178
|
+
)
|
|
179
|
+
|
|
201
180
|
return draw_array
|
|
202
181
|
|
|
203
182
|
def degree_infect(degree_dict, nodes, make_floats = False):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nettracer3d
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.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,6 +110,6 @@ 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.1.
|
|
113
|
+
-- Version 1.1.3 Updates --
|
|
114
114
|
|
|
115
|
-
*
|
|
115
|
+
* Some minor text adjustments
|
|
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
|