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.

Files changed (31) hide show
  1. {nettracer3d-1.1.1/src/nettracer3d.egg-info → nettracer3d-1.1.3}/PKG-INFO +3 -3
  2. {nettracer3d-1.1.1 → nettracer3d-1.1.3}/README.md +2 -2
  3. {nettracer3d-1.1.1 → nettracer3d-1.1.3}/pyproject.toml +2 -2
  4. {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/morphology.py +9 -4
  5. {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/neighborhoods.py +3 -3
  6. {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/nettracer.py +149 -11
  7. {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/nettracer_gui.py +209 -38
  8. {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/node_draw.py +38 -59
  9. {nettracer3d-1.1.1 → nettracer3d-1.1.3/src/nettracer3d.egg-info}/PKG-INFO +3 -3
  10. {nettracer3d-1.1.1 → nettracer3d-1.1.3}/LICENSE +0 -0
  11. {nettracer3d-1.1.1 → nettracer3d-1.1.3}/setup.cfg +0 -0
  12. {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/__init__.py +0 -0
  13. {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/cellpose_manager.py +0 -0
  14. {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/community_extractor.py +0 -0
  15. {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/excelotron.py +0 -0
  16. {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/modularity.py +0 -0
  17. {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/network_analysis.py +0 -0
  18. {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/network_draw.py +0 -0
  19. {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/painting.py +0 -0
  20. {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/proximity.py +0 -0
  21. {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/run.py +0 -0
  22. {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/segmenter.py +0 -0
  23. {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/segmenter_GPU.py +0 -0
  24. {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/simple_network.py +0 -0
  25. {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/smart_dilate.py +0 -0
  26. {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d/stats.py +0 -0
  27. {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d.egg-info/SOURCES.txt +0 -0
  28. {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d.egg-info/dependency_links.txt +0 -0
  29. {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d.egg-info/entry_points.txt +0 -0
  30. {nettracer3d-1.1.1 → nettracer3d-1.1.3}/src/nettracer3d.egg-info/requires.txt +0 -0
  31. {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.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.1 Updates --
113
+ -- Version 1.1.3 Updates --
114
114
 
115
- * Can now intermittently downsample while making the network and id overlays now to make their relevant elements larger in the actual rendered output.
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.1 Updates --
68
+ -- Version 1.1.3 Updates --
69
69
 
70
- * Can now intermittently downsample while making the network and id overlays now to make their relevant elements larger in the actual rendered output.
70
+ * Some minor text adjustments
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nettracer3d"
3
- version = "1.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
- # Find all skeleton voxels
216
- skeleton_coords = np.argwhere(skeleton_binary)
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, skeleton_binary.shape)
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) # Default to transparent (0,0,0,0)
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
- #skeleton = old_skeleton
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
- return labeled_image
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 == 0:
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.ax.get_xlim()
4195
- #print(self.original_xlim)
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.ax.get_xlim()
4360
- self.original_ylim = self.ax.get_ylim()
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
- self.channel_data[channel_index] = tifffile.imread(filename)
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
- self.shape = (self.channel_data[channel_index].shape[0], self.channel_data[channel_index].shape[1], self.channel_data[channel_index].shape[2])
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 closeEvent(self, event):
12513
- self.parent().preview = False
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
- self.amount = QLineEdit("1")
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(["Pseudo3D Binary Kernels (For Fast, small dilations)", "Preserve Labels (slower)", "Distance Transform-Based (Slower but more accurate at larger dilations)"])
12844
- self.mode_selector.setCurrentIndex(0) # Default to Mode 1
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 == 2:
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
- self.amount = QLineEdit("1")
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(["Pseudo3D Binary Kernels (For Fast, small erosions)", "Distance Transform-Based (Slower but more accurate at larger dilations)", "Preserve Labels (Slower)"])
12951
- self.mode_selector.setCurrentIndex(0) # Default to Mode 1
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 == 2:
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? (Useful if branch pruning - may want to threshold out small, split branches after): "), 4, 0)
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):"), 0, 0)
14200
- misc_layout.addWidget(self.nodes, 0, 1)
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):"), 1, 0)
14207
- misc_layout.addWidget(self.GPU, 1, 1)
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 = None):
125
- """Presumes a centroid dictionary has been obtained"""
126
- print("Drawing node IDs. (Must find all centroids. Network lattice itself may be drawn from network_draw script with fewer centroids)")
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
- # Use the default font from ImageFont
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
- try:
138
- draw_array = _draw_at_plane(z, y, x, draw_array, idx)
139
- except IndexError:
140
- pass
141
-
142
- try:
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
- #font_size = 24
175
-
155
+
176
156
  for node in centroid_dict:
177
-
178
- try:
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
- try:
186
- draw_array = _draw_at_plane(z, y, x, draw_array, degree)
187
- except IndexError:
188
- pass
189
-
190
- try:
191
- draw_array = _draw_at_plane(z + 1, y, x, draw_array, degree)
192
- except IndexError:
193
- pass
194
-
195
- try:
196
- draw_array = _draw_at_plane(z - 1, y, x, draw_array, degree)
197
- except IndexError:
198
- pass
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.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.1 Updates --
113
+ -- Version 1.1.3 Updates --
114
114
 
115
- * Can now intermittently downsample while making the network and id overlays now to make their relevant elements larger in the actual rendered output.
115
+ * Some minor text adjustments
File without changes
File without changes