nettracer3d 1.1.0__py3-none-any.whl → 1.2.3__py3-none-any.whl

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.

@@ -77,7 +77,12 @@ def draw_line_inplace(start, end, array):
77
77
  """
78
78
 
79
79
  # Calculate the distances between start and end coordinates
80
- distances = end - start
80
+ try:
81
+ distances = end - start
82
+ except:
83
+ end = np.array(end)
84
+ start = np.array(start)
85
+ distances = end - start
81
86
 
82
87
  # Determine the number of steps along the line
83
88
  num_steps = int(max(np.abs(distances)) + 1)
@@ -242,7 +247,8 @@ def draw_network_from_centroids(nodes, network, centroids, twod_bool, directory
242
247
  centroid_dic[item] = centroid
243
248
 
244
249
  except KeyError:
245
- print(f"Centroid {item} missing")
250
+ pass
251
+ #print(f"Centroid {item} missing")
246
252
  output_stack = np.zeros(np.shape(nodes), dtype=np.uint8)
247
253
 
248
254
  for i, pair1_val in enumerate(pair1):
@@ -253,7 +259,7 @@ def draw_network_from_centroids(nodes, network, centroids, twod_bool, directory
253
259
  pair2_centroid = centroid_dic[pair2_val]
254
260
  draw_line_inplace(pair1_centroid, pair2_centroid, output_stack)
255
261
  except KeyError:
256
- print(f"Missing centroid {i}")
262
+ #print(f"Missing centroid {i}")
257
263
  pass
258
264
 
259
265
  if twod_bool:
nettracer3d/node_draw.py CHANGED
@@ -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,66 @@ 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
- 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
132
 
142
133
  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
-
134
+ z, y, x = centroid.astype(int)
135
+ except:
136
+ z, y, x = centroid
137
+
138
+ for z_offset in [0, 1, -1]:
139
+ z_target = z + z_offset
140
+ if 0 <= z_target < draw_array.shape[0]:
141
+ cv2.putText(draw_array[z_target], str(idx), (x, y),
142
+ cv2.FONT_HERSHEY_SIMPLEX, 0.4, 255, 1, cv2.LINE_AA)
143
+
152
144
  if twod_bool:
153
145
  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
-
146
+
147
+ filename = f'{directory}/labelled_node_indices.tif' if directory else 'labelled_node_indices.tif'
161
148
  try:
162
-
163
- # Save the draw_array as a 3D TIFF file
164
149
  tifffile.imwrite(filename, draw_array)
165
-
166
150
  except Exception as e:
167
151
  print(f"Could not save node indices to {filename}")
168
-
152
+
169
153
  return draw_array
170
154
 
171
155
  def degree_draw(degree_dict, centroid_dict, nodes):
156
+ """Draw node degrees at centroid locations using OpenCV"""
172
157
  # Create a new 3D array to draw on with the same dimensions as the original array
173
158
  draw_array = np.zeros_like(nodes, dtype=np.uint8)
174
- #font_size = 24
175
-
159
+
176
160
  for node in centroid_dict:
177
-
178
- try:
179
- degree = degree_dict[node]
180
- except:
161
+ # Skip if node not in degree_dict
162
+ if node not in degree_dict:
181
163
  continue
182
164
 
165
+ degree = degree_dict[node]
183
166
  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
-
167
+
168
+ # Draw on current z-plane and adjacent planes
169
+ for z_offset in [0, 1, -1]:
170
+ z_target = z + z_offset
171
+ # Check bounds
172
+ if 0 <= z_target < draw_array.shape[0]:
173
+ cv2.putText(
174
+ draw_array[z_target], # Image to draw on
175
+ str(degree), # Text to draw
176
+ (x, y), # Position (x, y)
177
+ cv2.FONT_HERSHEY_SIMPLEX, # Font
178
+ 0.4, # Font scale
179
+ 255, # Color (white)
180
+ 1, # Thickness
181
+ cv2.LINE_AA # Anti-aliasing
182
+ )
183
+
201
184
  return draw_array
202
185
 
203
186
  def degree_infect(degree_dict, nodes, make_floats = False):
nettracer3d/proximity.py CHANGED
@@ -12,6 +12,7 @@ from typing import Dict, Union, Tuple, List, Optional
12
12
  from collections import defaultdict
13
13
  from multiprocessing import Pool, cpu_count
14
14
  import functools
15
+ from . import smart_dilate as sdl
15
16
 
16
17
  # Related to morphological border searching:
17
18
 
@@ -104,7 +105,6 @@ def create_node_dictionary(nodes, num_nodes, dilate_xy, dilate_z, targets=None,
104
105
  """pre-compute all bounding boxes using find_objects"""
105
106
  node_dict = {}
106
107
  array_shape = nodes.shape
107
-
108
108
 
109
109
  # Get all bounding boxes at once
110
110
  bounding_boxes = ndimage.find_objects(nodes)
@@ -1056,4 +1056,125 @@ def create_dict_entry_id(node_dict, label, sub_nodes, sub_edges):
1056
1056
  if label is None:
1057
1057
  pass
1058
1058
  else:
1059
- node_dict[label] = _get_node_edge_dict_id(sub_nodes, sub_edges, label)
1059
+ node_dict[label] = _get_node_edge_dict_id(sub_nodes, sub_edges, label)
1060
+
1061
+
1062
+ # For the continuous structure labeler:
1063
+
1064
+ def get_reslice_space(slice_obj, array_shape):
1065
+ z_slice, y_slice, x_slice = slice_obj
1066
+
1067
+ # Extract min/max from slices
1068
+ z_min, z_max = z_slice.start, z_slice.stop - 1
1069
+ y_min, y_max = y_slice.start, y_slice.stop - 1
1070
+ x_min, x_max = x_slice.start, x_slice.stop - 1
1071
+ # Add padding
1072
+ y_max = y_max + 1
1073
+ y_min = y_min - 1
1074
+ x_max = x_max + 1
1075
+ x_min = x_min - 1
1076
+ z_max = z_max + 1
1077
+ z_min = z_min - 1
1078
+ # Boundary checks
1079
+ y_max = min(y_max, array_shape[1] - 1)
1080
+ x_max = min(x_max, array_shape[2] - 1)
1081
+ z_max = min(z_max, array_shape[0] - 1)
1082
+ y_min = max(y_min, 0)
1083
+ x_min = max(x_min, 0)
1084
+ z_min = max(z_min, 0)
1085
+ return [z_min, z_max], [y_min, y_max], [x_min, x_max]
1086
+
1087
+ def reslice_array(args):
1088
+ """Internal method used for the secondary algorithm to reslice subarrays."""
1089
+ input_array, z_range, y_range, x_range = args
1090
+ z_start, z_end = z_range
1091
+ z_start, z_end = int(z_start), int(z_end)
1092
+ y_start, y_end = y_range
1093
+ y_start, y_end = int(y_start), int(y_end)
1094
+ x_start, x_end = x_range
1095
+ x_start, x_end = int(x_start), int(x_end)
1096
+
1097
+ # Reslice the array
1098
+ resliced_array = input_array[z_start:z_end + 1, y_start:y_end + 1, x_start:x_end + 1]
1099
+
1100
+ return resliced_array
1101
+
1102
+ def _reassign_label_by_continuous_proximity(sub_to_assign, sub_labels, label):
1103
+ """Internal method used for the secondary algorithm to find pixel involvement of component to be labeled based on nearby labels."""
1104
+
1105
+ # Create a boolean mask where elements with the specified label are True
1106
+ sub_to_assign = sub_to_assign == label
1107
+ sub_to_assign = nettracer.dilate_3D_old(sub_to_assign, 3, 3, 3) #Dilate the label by 1 to see where the dilated label overlaps
1108
+ sub_to_assign = sub_to_assign != 0
1109
+ sub_labels = sub_labels * sub_to_assign # Isolate only adjacent label
1110
+ sub_to_assign = sdl.smart_label_single(sub_to_assign, sub_labels) # Assign labeling schema from 'sub_to_assign' to 'sub_labels'
1111
+ return sub_to_assign
1112
+
1113
+ def process_and_write_voxels(args):
1114
+ """Optimized version using vectorized operations"""
1115
+ to_assign, labels, label, array_shape, bounding_boxes, result_array = args
1116
+ print(f"Processing node {label}")
1117
+
1118
+ # Get the pre-computed bounding box for this label
1119
+ slice_obj = bounding_boxes[label-1] # -1 because label numbers start at 1
1120
+
1121
+ z_vals, y_vals, x_vals = get_reslice_space(slice_obj, array_shape)
1122
+ z_start, z_end = z_vals
1123
+ y_start, y_end = y_vals
1124
+ x_start, x_end = x_vals
1125
+
1126
+ # Extract subarrays
1127
+ sub_to_assign = reslice_array((to_assign, z_vals, y_vals, x_vals))
1128
+ sub_labels = reslice_array((labels, z_vals, y_vals, x_vals))
1129
+
1130
+ # Create mask for this label BEFORE relabeling (critical!)
1131
+ label_mask = (sub_to_assign == label)
1132
+
1133
+ # Get local coordinates of voxels belonging to this label
1134
+ local_coords = np.where(label_mask)
1135
+
1136
+ # Do the relabeling on the subarray
1137
+ # Note: relabeled may contain multiple different label values now
1138
+ relabeled = _reassign_label_by_continuous_proximity(sub_to_assign, sub_labels, label)
1139
+
1140
+ # Apply offsets (vectorized)
1141
+ global_coords = (
1142
+ local_coords[0] + z_start,
1143
+ local_coords[1] + y_start,
1144
+ local_coords[2] + x_start
1145
+ )
1146
+
1147
+ # Single vectorized write operation
1148
+ # This copies all new label values (potentially multiple different labels)
1149
+ # for voxels that originally belonged to 'label'
1150
+ result_array[global_coords] = relabeled[local_coords]
1151
+
1152
+ def create_label_map(to_assign, labels, num_labels, array_shape):
1153
+ """Modified to pre-compute all bounding boxes and write voxels in parallel"""
1154
+
1155
+ # Get all bounding boxes at once
1156
+ bounding_boxes = ndimage.find_objects(to_assign)
1157
+
1158
+ # Clone to_assign for modifications (original used for reading subarrays)
1159
+ result_array = to_assign.copy()
1160
+
1161
+ # Use ThreadPoolExecutor for parallel execution
1162
+ with ThreadPoolExecutor(max_workers=mp.cpu_count()) as executor:
1163
+ # Create args list with bounding_boxes and result_array included
1164
+ args_list = [(to_assign, labels, i, array_shape, bounding_boxes, result_array)
1165
+ for i in range(1, num_labels + 1)]
1166
+
1167
+ # Execute parallel tasks - each writes directly to result_array
1168
+ futures = [executor.submit(process_and_write_voxels, args) for args in args_list]
1169
+
1170
+ # Wait for all to complete
1171
+ for future in futures:
1172
+ future.result()
1173
+
1174
+ return result_array
1175
+
1176
+ def label_continuous(to_assign, labels):
1177
+ array_shape = to_assign.shape
1178
+ num_labels = np.max(to_assign)
1179
+ result = create_label_map(to_assign, labels, num_labels, array_shape)
1180
+ return result
@@ -495,6 +495,42 @@ def smart_label(binary_array, label_array, directory = None, GPU = True, predown
495
495
 
496
496
  return dilated_nodes_with_labels
497
497
 
498
+ def smart_label_single(binary_array, label_array):
499
+
500
+ # Step 1: Binarize the labeled array
501
+ binary_core = binarize(label_array)
502
+ binary_array = binarize(binary_array)
503
+
504
+ # Step 3: Isolate the ring (binary dilated mask minus original binary mask)
505
+ ring_mask = binary_array & invert_array(binary_core)
506
+
507
+ nearest_label_indices = compute_distance_transform(invert_array(label_array))
508
+
509
+ # Step 5: Process the entire array without parallelization
510
+ dilated_nodes_with_labels = process_chunk(0, label_array.shape[1], label_array, ring_mask, nearest_label_indices)
511
+
512
+ dilated_nodes_with_labels = dilated_nodes_with_labels * binary_array
513
+
514
+
515
+ return dilated_nodes_with_labels
516
+
517
+ def neighbor_label(binary_array, label_array):
518
+
519
+ binary_array = binary_array != 0
520
+
521
+ targets = binary_array - binarize(label_array)
522
+
523
+ label_array = smart_dilate(label_array, 3, 3, GPU = False, fast_dil = False, xy_scale = 1, z_scale = 1)
524
+
525
+ return None
526
+
527
+ #TBD: This requires the serial dilation strategy methinks, unfortunately. Get labeled components of all outer regions of the binary array to be labeled. For each, cut out a sub array from the old label array and pad by one in all dimensions. Apply boolean threshold to get just the label we want. Apply binary dilation on the binary subarray of just one and multiply that against the labeled sub array - we have isolated the available labelers. Finally do smart dilate on this sub array region.
528
+
529
+
530
+
531
+
532
+
533
+
498
534
  def compute_distance_transform_GPU(nodes, return_dists = False, sampling = [1, 1, 1]):
499
535
  is_pseudo_3d = nodes.shape[0] == 1
500
536
  if is_pseudo_3d: