nettracer3d 0.9.9__py3-none-any.whl → 1.1.5__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.
@@ -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/segmenter.py CHANGED
@@ -1181,12 +1181,22 @@ class InteractiveSegmenter:
1181
1181
  (x[0][2] - curr_x) ** 2))
1182
1182
  return nearest[0]
1183
1183
  else:
1184
- # 3D chunks: use existing center-based distance calculation
1185
- nearest = min(unprocessed_chunks,
1184
+ # 3D chunks: find chunks on nearest Z-plane, then closest in X/Y
1185
+ # First find the nearest Z-plane among available chunks
1186
+ nearest_z = min(unprocessed_chunks,
1187
+ key=lambda x: abs(x[1]['center'][0] - curr_z))[1]['center'][0]
1188
+
1189
+ # Get all chunks on that nearest Z-plane
1190
+ nearest_z_chunks = [chunk for chunk in unprocessed_chunks
1191
+ if chunk[1]['center'][0] == nearest_z]
1192
+
1193
+ # From those chunks, find closest in X/Y
1194
+ nearest = min(nearest_z_chunks,
1186
1195
  key=lambda x: sum((a - b) ** 2 for a, b in
1187
- zip(x[1]['center'], (curr_z, curr_y, curr_x))))
1196
+ zip(x[1]['center'][1:], (curr_y, curr_x))))
1197
+
1188
1198
  return nearest[0]
1189
-
1199
+
1190
1200
  return None
1191
1201
 
1192
1202
  while True:
@@ -1284,12 +1294,14 @@ class InteractiveSegmenter:
1284
1294
 
1285
1295
  return foreground_features, background_features
1286
1296
 
1287
- def compute_3d_chunks(self, chunk_size=None):
1297
+ def compute_3d_chunks(self, chunk_size=None, thickness=49):
1288
1298
  """
1289
- Compute 3D chunks with consistent logic across all operations.
1299
+ Compute 3D chunks as rectangular prisms with consistent logic across all operations.
1300
+ Creates chunks that are thin in Z and square-like in X/Y dimensions.
1290
1301
 
1291
1302
  Args:
1292
- chunk_size: Optional chunk size, otherwise uses dynamic calculation
1303
+ chunk_size: Optional chunk size for volume calculation, otherwise uses dynamic calculation
1304
+ thickness: Z-dimension thickness of chunks (default: 9)
1293
1305
 
1294
1306
  Returns:
1295
1307
  list: List of chunk coordinates [z_start, z_end, y_start, y_end, x_start, x_end]
@@ -1313,27 +1325,57 @@ class InteractiveSegmenter:
1313
1325
  except:
1314
1326
  depth, height, width, rgb = self.image_3d.shape
1315
1327
 
1316
- # Calculate chunk grid dimensions
1317
- z_chunks = (depth + chunk_size - 1) // chunk_size
1318
- y_chunks = (height + chunk_size - 1) // chunk_size
1319
- x_chunks = (width + chunk_size - 1) // chunk_size
1328
+ # Calculate target volume per chunk (same as original cube)
1329
+ target_volume = chunk_size ** 3
1330
+
1331
+ # Calculate XY side length based on thickness and target volume
1332
+ # Volume = thickness * xy_side * xy_side
1333
+ # So xy_side = sqrt(volume / thickness)
1334
+ xy_side = int(np.sqrt(target_volume / thickness))
1335
+ xy_side = max(1, xy_side) # Ensure minimum size of 1
1320
1336
 
1321
- # Generate all chunk start positions
1322
- chunk_starts = np.array(np.meshgrid(
1323
- np.arange(z_chunks) * chunk_size,
1324
- np.arange(y_chunks) * chunk_size,
1325
- np.arange(x_chunks) * chunk_size,
1326
- indexing='ij'
1327
- )).reshape(3, -1).T
1337
+ # Calculate actual chunk dimensions for grid calculation
1338
+ z_chunk_size = thickness
1339
+ xy_chunk_size = xy_side
1328
1340
 
1329
- # Create chunk coordinate list
1341
+ # Calculate number of chunks in each dimension
1342
+ z_chunks = (depth + z_chunk_size - 1) // z_chunk_size
1343
+ y_chunks = (height + xy_chunk_size - 1) // xy_chunk_size
1344
+ x_chunks = (width + xy_chunk_size - 1) // xy_chunk_size
1345
+
1346
+ # Calculate actual chunk sizes to distribute remainder evenly
1347
+ # This ensures all chunks are roughly the same size
1348
+ z_sizes = np.full(z_chunks, depth // z_chunks)
1349
+ z_remainder = depth % z_chunks
1350
+ z_sizes[:z_remainder] += 1
1351
+
1352
+ y_sizes = np.full(y_chunks, height // y_chunks)
1353
+ y_remainder = height % y_chunks
1354
+ y_sizes[:y_remainder] += 1
1355
+
1356
+ x_sizes = np.full(x_chunks, width // x_chunks)
1357
+ x_remainder = width % x_chunks
1358
+ x_sizes[:x_remainder] += 1
1359
+
1360
+ # Calculate cumulative positions
1361
+ z_positions = np.concatenate([[0], np.cumsum(z_sizes)])
1362
+ y_positions = np.concatenate([[0], np.cumsum(y_sizes)])
1363
+ x_positions = np.concatenate([[0], np.cumsum(x_sizes)])
1364
+
1365
+ # Generate all chunk coordinates
1330
1366
  chunks = []
1331
- for z_start, y_start, x_start in chunk_starts:
1332
- z_end = min(z_start + chunk_size, depth)
1333
- y_end = min(y_start + chunk_size, height)
1334
- x_end = min(x_start + chunk_size, width)
1335
- coords = [z_start, z_end, y_start, y_end, x_start, x_end]
1336
- chunks.append(coords)
1367
+ for z_idx in range(z_chunks):
1368
+ for y_idx in range(y_chunks):
1369
+ for x_idx in range(x_chunks):
1370
+ z_start = z_positions[z_idx]
1371
+ z_end = z_positions[z_idx + 1]
1372
+ y_start = y_positions[y_idx]
1373
+ y_end = y_positions[y_idx + 1]
1374
+ x_start = x_positions[x_idx]
1375
+ x_end = x_positions[x_idx + 1]
1376
+
1377
+ coords = [z_start, z_end, y_start, y_end, x_start, x_end]
1378
+ chunks.append(coords)
1337
1379
 
1338
1380
  return chunks
1339
1381
 
@@ -1055,19 +1055,18 @@ class InteractiveSegmenter:
1055
1055
  self.realtimechunks = chunk_dict
1056
1056
  print("Ready!")
1057
1057
 
1058
- def compute_3d_chunks(self, chunk_size=None):
1058
+ def compute_3d_chunks(self, chunk_size=None, thickness=49):
1059
1059
  """
1060
- Compute 3D chunks with consistent logic across all operations (GPU version).
1060
+ Compute 3D chunks as rectangular prisms with consistent logic across all operations.
1061
+ Creates chunks that are thin in Z and square-like in X/Y dimensions.
1061
1062
 
1062
1063
  Args:
1063
- chunk_size: Optional chunk size, otherwise uses dynamic calculation
1064
+ chunk_size: Optional chunk size for volume calculation, otherwise uses dynamic calculation
1065
+ thickness: Z-dimension thickness of chunks (default: 9)
1064
1066
 
1065
1067
  Returns:
1066
1068
  list: List of chunk coordinates [z_start, z_end, y_start, y_end, x_start, x_end]
1067
1069
  """
1068
- import cupy as cp
1069
- import multiprocessing
1070
-
1071
1070
  # Use consistent chunk size calculation
1072
1071
  if chunk_size is None:
1073
1072
  if hasattr(self, 'master_chunk') and self.master_chunk is not None:
@@ -1075,10 +1074,10 @@ class InteractiveSegmenter:
1075
1074
  else:
1076
1075
  # Dynamic calculation (same as segmentation)
1077
1076
  total_cores = multiprocessing.cpu_count()
1078
- total_volume = cp.prod(cp.array(self.image_3d.shape))
1077
+ total_volume = np.prod(self.image_3d.shape)
1079
1078
  target_volume_per_chunk = total_volume / (total_cores * 4)
1080
1079
 
1081
- chunk_size = int(cp.cbrt(target_volume_per_chunk))
1080
+ chunk_size = int(np.cbrt(target_volume_per_chunk))
1082
1081
  chunk_size = max(16, min(chunk_size, min(self.image_3d.shape) // 2))
1083
1082
  chunk_size = ((chunk_size + 7) // 16) * 16
1084
1083
 
@@ -1087,28 +1086,57 @@ class InteractiveSegmenter:
1087
1086
  except:
1088
1087
  depth, height, width, rgb = self.image_3d.shape
1089
1088
 
1090
- # Calculate chunk grid dimensions
1091
- z_chunks = (depth + chunk_size - 1) // chunk_size
1092
- y_chunks = (height + chunk_size - 1) // chunk_size
1093
- x_chunks = (width + chunk_size - 1) // chunk_size
1089
+ # Calculate target volume per chunk (same as original cube)
1090
+ target_volume = chunk_size ** 3
1091
+
1092
+ # Calculate XY side length based on thickness and target volume
1093
+ # Volume = thickness * xy_side * xy_side
1094
+ # So xy_side = sqrt(volume / thickness)
1095
+ xy_side = int(np.sqrt(target_volume / thickness))
1096
+ xy_side = max(1, xy_side) # Ensure minimum size of 1
1097
+
1098
+ # Calculate actual chunk dimensions for grid calculation
1099
+ z_chunk_size = thickness
1100
+ xy_chunk_size = xy_side
1101
+
1102
+ # Calculate number of chunks in each dimension
1103
+ z_chunks = (depth + z_chunk_size - 1) // z_chunk_size
1104
+ y_chunks = (height + xy_chunk_size - 1) // xy_chunk_size
1105
+ x_chunks = (width + xy_chunk_size - 1) // xy_chunk_size
1106
+
1107
+ # Calculate actual chunk sizes to distribute remainder evenly
1108
+ # This ensures all chunks are roughly the same size
1109
+ z_sizes = np.full(z_chunks, depth // z_chunks)
1110
+ z_remainder = depth % z_chunks
1111
+ z_sizes[:z_remainder] += 1
1094
1112
 
1095
- # Generate all chunk start positions using CuPy
1096
- chunk_starts = cp.array(cp.meshgrid(
1097
- cp.arange(z_chunks) * chunk_size,
1098
- cp.arange(y_chunks) * chunk_size,
1099
- cp.arange(x_chunks) * chunk_size,
1100
- indexing='ij'
1101
- )).reshape(3, -1).T
1113
+ y_sizes = np.full(y_chunks, height // y_chunks)
1114
+ y_remainder = height % y_chunks
1115
+ y_sizes[:y_remainder] += 1
1102
1116
 
1117
+ x_sizes = np.full(x_chunks, width // x_chunks)
1118
+ x_remainder = width % x_chunks
1119
+ x_sizes[:x_remainder] += 1
1103
1120
 
1104
- # Create chunk coordinate list
1121
+ # Calculate cumulative positions
1122
+ z_positions = np.concatenate([[0], np.cumsum(z_sizes)])
1123
+ y_positions = np.concatenate([[0], np.cumsum(y_sizes)])
1124
+ x_positions = np.concatenate([[0], np.cumsum(x_sizes)])
1125
+
1126
+ # Generate all chunk coordinates
1105
1127
  chunks = []
1106
- for z_start, y_start, x_start in chunk_starts:
1107
- z_end = min(z_start + chunk_size, depth)
1108
- y_end = min(y_start + chunk_size, height)
1109
- x_end = min(x_start + chunk_size, width)
1110
- coords = [int(z_start), int(z_end), int(y_start), int(y_end), int(x_start), int(x_end)]
1111
- chunks.append(coords)
1128
+ for z_idx in range(z_chunks):
1129
+ for y_idx in range(y_chunks):
1130
+ for x_idx in range(x_chunks):
1131
+ z_start = z_positions[z_idx]
1132
+ z_end = z_positions[z_idx + 1]
1133
+ y_start = y_positions[y_idx]
1134
+ y_end = y_positions[y_idx + 1]
1135
+ x_start = x_positions[x_idx]
1136
+ x_end = x_positions[x_idx + 1]
1137
+
1138
+ coords = [z_start, z_end, y_start, y_end, x_start, x_end]
1139
+ chunks.append(coords)
1112
1140
 
1113
1141
  return chunks
1114
1142
 
@@ -1196,10 +1224,20 @@ class InteractiveSegmenter:
1196
1224
  (x[0][2] - curr_x) ** 2))
1197
1225
  return nearest[0]
1198
1226
  else:
1199
- # 3D chunks: use existing center-based distance calculation
1200
- nearest = min(unprocessed_chunks,
1227
+ # 3D chunks: find chunks on nearest Z-plane, then closest in X/Y
1228
+ # First find the nearest Z-plane among available chunks
1229
+ nearest_z = min(unprocessed_chunks,
1230
+ key=lambda x: abs(x[1]['center'][0] - curr_z))[1]['center'][0]
1231
+
1232
+ # Get all chunks on that nearest Z-plane
1233
+ nearest_z_chunks = [chunk for chunk in unprocessed_chunks
1234
+ if chunk[1]['center'][0] == nearest_z]
1235
+
1236
+ # From those chunks, find closest in X/Y
1237
+ nearest = min(nearest_z_chunks,
1201
1238
  key=lambda x: sum((a - b) ** 2 for a, b in
1202
- zip(x[1]['center'], (curr_z, curr_y, curr_x))))
1239
+ zip(x[1]['center'][1:], (curr_y, curr_x))))
1240
+
1203
1241
  return nearest[0]
1204
1242
 
1205
1243
  return None