nettracer3d 1.0.8__py3-none-any.whl → 1.0.9__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.

nettracer3d/morphology.py CHANGED
@@ -65,7 +65,7 @@ def reslice_3d_array(args):
65
65
  return resliced_array
66
66
 
67
67
 
68
- def _get_node_edge_dict(label_array, edge_array, label, dilate_xy, dilate_z, cores = 0, search = 0, fastdil = False, xy_scale = 1, z_scale = 1):
68
+ def _get_node_edge_dict(label_array, edge_array, label, dilate_xy, dilate_z, cores = 0, search = 0, fastdil = False, length = False, xy_scale = 1, z_scale = 1):
69
69
  """Internal method used for the secondary algorithm to find pixel involvement of nodes around an edge."""
70
70
 
71
71
  # Create a boolean mask where elements with the specified label are True
@@ -74,24 +74,25 @@ def _get_node_edge_dict(label_array, edge_array, label, dilate_xy, dilate_z, cor
74
74
 
75
75
  if cores == 0: #For getting the volume of objects. Cores presumes you want the 'core' included in the interaction.
76
76
  edge_array = edge_array * dil_array # Filter the edges by the label in question
77
- label_array = np.count_nonzero(dil_array)
78
- edge_array = np.count_nonzero(edge_array) # For getting the interacting skeleton
79
-
80
77
  elif cores == 1: #Cores being 1 presumes you do not want to 'core' included in the interaction
81
78
  label_array = dil_array - label_array
82
79
  edge_array = edge_array * label_array
83
- label_array = np.count_nonzero(label_array)
84
- edge_array = np.count_nonzero(edge_array) # For getting the interacting skeleton
85
-
86
80
  elif cores == 2: #Presumes you want skeleton within the core but to only 'count' the stuff around the core for volumes... because of imaging artifacts, perhaps
87
81
  edge_array = edge_array * dil_array
88
82
  label_array = dil_array - label_array
89
- label_array = np.count_nonzero(label_array)
90
- edge_array = np.count_nonzero(edge_array) # For getting the interacting skeleton
91
83
 
84
+ label_count = np.count_nonzero(label_array) * xy_scale * xy_scale * z_scale
92
85
 
93
-
94
- args = [edge_array, label_array]
86
+ if not length:
87
+ edge_count = np.count_nonzero(edge_array) * xy_scale * xy_scale * z_scale # For getting the interacting skeleton
88
+ else:
89
+ edge_count = calculate_skeleton_lengths(
90
+ edge_array,
91
+ xy_scale=xy_scale,
92
+ z_scale=z_scale
93
+ )
94
+
95
+ args = [edge_count, label_count]
95
96
 
96
97
  return args
97
98
 
@@ -115,7 +116,7 @@ def process_label(args):
115
116
 
116
117
 
117
118
 
118
- def create_node_dictionary(nodes, edges, num_nodes, dilate_xy, dilate_z, cores=0, search = 0, fastdil = False, xy_scale = 1, z_scale = 1):
119
+ def create_node_dictionary(nodes, edges, num_nodes, dilate_xy, dilate_z, cores=0, search = 0, fastdil = False, length = False, xy_scale = 1, z_scale = 1):
119
120
  """Modified to pre-compute all bounding boxes using find_objects"""
120
121
  node_dict = {}
121
122
  array_shape = nodes.shape
@@ -135,20 +136,20 @@ def create_node_dictionary(nodes, edges, num_nodes, dilate_xy, dilate_z, cores=0
135
136
  # Process results in parallel
136
137
  for label, sub_nodes, sub_edges in results:
137
138
  executor.submit(create_dict_entry, node_dict, label, sub_nodes, sub_edges,
138
- dilate_xy, dilate_z, cores, search, fastdil, xy_scale, z_scale)
139
+ dilate_xy, dilate_z, cores, search, fastdil, length, xy_scale, z_scale)
139
140
 
140
141
  return node_dict
141
142
 
142
- def create_dict_entry(node_dict, label, sub_nodes, sub_edges, dilate_xy, dilate_z, cores = 0, search = 0, fastdil = False, xy_scale = 1, z_scale = 1):
143
+ def create_dict_entry(node_dict, label, sub_nodes, sub_edges, dilate_xy, dilate_z, cores = 0, search = 0, fastdil = False, length = False, xy_scale = 1, z_scale = 1):
143
144
  """Internal method used for the secondary algorithm to pass around args in parallel."""
144
145
 
145
146
  if label is None:
146
147
  pass
147
148
  else:
148
- node_dict[label] = _get_node_edge_dict(sub_nodes, sub_edges, label, dilate_xy, dilate_z, cores = cores, search = search, fastdil = fastdil, xy_scale = xy_scale, z_scale = z_scale)
149
+ node_dict[label] = _get_node_edge_dict(sub_nodes, sub_edges, label, dilate_xy, dilate_z, cores = cores, search = search, fastdil = fastdil, length = length, xy_scale = xy_scale, z_scale = z_scale)
149
150
 
150
151
 
151
- def quantify_edge_node(nodes, edges, search = 0, xy_scale = 1, z_scale = 1, cores = 0, resize = None, save = True, skele = False, fastdil = False):
152
+ def quantify_edge_node(nodes, edges, search = 0, xy_scale = 1, z_scale = 1, cores = 0, resize = None, save = True, skele = False, length = False, auto = True, fastdil = False):
152
153
 
153
154
  def save_dubval_dict(dict, index_name, val1name, val2name, filename):
154
155
 
@@ -168,6 +169,9 @@ def quantify_edge_node(nodes, edges, search = 0, xy_scale = 1, z_scale = 1, core
168
169
  edges = tifffile.imread(edges)
169
170
 
170
171
  if skele:
172
+ if auto:
173
+ edges = nettracer.skeletonize(edges)
174
+ edges = nettracer.fill_holes_3d(edges)
171
175
  edges = nettracer.skeletonize(edges)
172
176
  else:
173
177
  edges = nettracer.binarize(edges)
@@ -188,7 +192,7 @@ def quantify_edge_node(nodes, edges, search = 0, xy_scale = 1, z_scale = 1, core
188
192
  dilate_xy, dilate_z = 0, 0
189
193
 
190
194
 
191
- edge_quants = create_node_dictionary(nodes, edges, num_nodes, dilate_xy, dilate_z, cores = cores, search = search, fastdil = fastdil, xy_scale = xy_scale, z_scale = z_scale) #Find which edges connect which nodes and put them in a dictionary.
195
+ edge_quants = create_node_dictionary(nodes, edges, num_nodes, dilate_xy, dilate_z, cores = cores, search = search, fastdil = fastdil, length = length, xy_scale = xy_scale, z_scale = z_scale) #Find which edges connect which nodes and put them in a dictionary.
192
196
 
193
197
  if save:
194
198
 
@@ -199,6 +203,93 @@ def quantify_edge_node(nodes, edges, search = 0, xy_scale = 1, z_scale = 1, core
199
203
  return edge_quants
200
204
 
201
205
 
206
+ # Helper methods for counting the lens of skeletons:
207
+
208
+ def calculate_skeleton_lengths(skeleton_binary, xy_scale=1.0, z_scale=1.0):
209
+ """
210
+ Calculate total length of all skeletons in a 3D binary image.
211
+
212
+ skeleton_binary: 3D boolean array where True = skeleton voxel
213
+ xy_scale, z_scale: physical units per voxel
214
+ """
215
+ # Find all skeleton voxels
216
+ skeleton_coords = np.argwhere(skeleton_binary)
217
+
218
+ if len(skeleton_coords) == 0:
219
+ return 0.0
220
+
221
+ # Create a mapping from coordinates to indices for fast lookup
222
+ coord_to_idx = {tuple(coord): idx for idx, coord in enumerate(skeleton_coords)}
223
+
224
+ # Build adjacency graph
225
+ adjacency_list = build_adjacency_graph(skeleton_coords, coord_to_idx, skeleton_binary.shape)
226
+
227
+ # Calculate lengths using scaled distances
228
+ total_length = calculate_graph_length(skeleton_coords, adjacency_list, xy_scale, z_scale)
229
+
230
+ return total_length
231
+
232
+ def build_adjacency_graph(skeleton_coords, coord_to_idx, shape):
233
+ """Build adjacency list for skeleton voxels using 26-connectivity."""
234
+ adjacency_list = [[] for _ in range(len(skeleton_coords))]
235
+
236
+ # 26-connectivity offsets (all combinations of -1,0,1 except 0,0,0)
237
+ offsets = []
238
+ for dz in [-1, 0, 1]:
239
+ for dy in [-1, 0, 1]:
240
+ for dx in [-1, 0, 1]:
241
+ if not (dx == 0 and dy == 0 and dz == 0):
242
+ offsets.append((dz, dy, dx))
243
+
244
+ for idx, coord in enumerate(skeleton_coords):
245
+ z, y, x = coord
246
+
247
+ # Check all 26 neighbors
248
+ for dz, dy, dx in offsets:
249
+ nz, ny, nx = z + dz, y + dy, x + dx
250
+
251
+ # Check bounds
252
+ if (0 <= nz < shape[0] and
253
+ 0 <= ny < shape[1] and
254
+ 0 <= nx < shape[2]):
255
+
256
+ neighbor_coord = (nz, ny, nx)
257
+ if neighbor_coord in coord_to_idx:
258
+ neighbor_idx = coord_to_idx[neighbor_coord]
259
+ adjacency_list[idx].append(neighbor_idx)
260
+
261
+ return adjacency_list
262
+
263
+ def calculate_graph_length(skeleton_coords, adjacency_list, xy_scale, z_scale):
264
+ """Calculate total length by summing distances between adjacent voxels."""
265
+ total_length = 0.0
266
+ processed_edges = set()
267
+
268
+ for idx, neighbors in enumerate(adjacency_list):
269
+ coord = skeleton_coords[idx]
270
+
271
+ for neighbor_idx in neighbors:
272
+ # Avoid double-counting edges
273
+ edge = tuple(sorted([idx, neighbor_idx]))
274
+ if edge in processed_edges:
275
+ continue
276
+ processed_edges.add(edge)
277
+
278
+ neighbor_coord = skeleton_coords[neighbor_idx]
279
+
280
+ # Calculate scaled distance
281
+ dz = (coord[0] - neighbor_coord[0]) * z_scale
282
+ dy = (coord[1] - neighbor_coord[1]) * xy_scale
283
+ dx = (coord[2] - neighbor_coord[2]) * xy_scale
284
+
285
+ distance = np.sqrt(dx*dx + dy*dy + dz*dz)
286
+ total_length += distance
287
+
288
+ return total_length
289
+
290
+ # End helper methods
291
+
292
+
202
293
 
203
294
  def calculate_voxel_volumes(array, xy_scale=1, z_scale=1):
204
295
  """
nettracer3d/nettracer.py CHANGED
@@ -3143,6 +3143,14 @@ class Network_3D:
3143
3143
  Can be called on a Network_3D object to save the nodes property to hard mem as a tif. It will save to the active directory if none is specified.
3144
3144
  :param directory: (Optional - Val = None; String). The path to an indended directory to save the nodes to.
3145
3145
  """
3146
+ imagej_metadata = {
3147
+ 'spacing': self.z_scale,
3148
+ 'slices': self._nodes.shape[0],
3149
+ 'channels': 1,
3150
+ 'axes': 'ZYX'
3151
+ }
3152
+ resolution_value = 1.0 / self.xy_scale if self.xy_scale != 0 else 1
3153
+
3146
3154
  if filename is None:
3147
3155
  filename = "labelled_nodes.tif"
3148
3156
  elif not filename.endswith(('.tif', '.tiff')):
@@ -3151,13 +3159,19 @@ class Network_3D:
3151
3159
  if self._nodes is not None:
3152
3160
  if directory is None:
3153
3161
  try:
3154
- tifffile.imwrite(f"{filename}", self._nodes)
3162
+ if len(self._nodes.shape) == 3:
3163
+ tifffile.imwrite(f"{filename}", self._nodes, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
3164
+ else:
3165
+ tifffile.imwrite(f"{filename}", self._nodes)
3155
3166
  print(f"Nodes saved to {filename}")
3156
3167
  except Exception as e:
3157
3168
  print("Could not save nodes")
3158
3169
  if directory is not None:
3159
3170
  try:
3160
- tifffile.imwrite(f"{directory}/{filename}", self._nodes)
3171
+ if len(self._nodes.shape) == 3:
3172
+ tifffile.imwrite(f"{directory}/{filename}", self._nodes, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
3173
+ else:
3174
+ tifffile.imwrite(f"{directory}/{filename}")
3161
3175
  print(f"Nodes saved to {directory}/{filename}")
3162
3176
  except Exception as e:
3163
3177
  print(f"Could not save nodes to {directory}")
@@ -3170,6 +3184,15 @@ class Network_3D:
3170
3184
  :param directory: (Optional - Val = None; String). The path to an indended directory to save the edges to.
3171
3185
  """
3172
3186
 
3187
+ imagej_metadata = {
3188
+ 'spacing': self.z_scale,
3189
+ 'slices': self._edges.shape[0],
3190
+ 'channels': 1,
3191
+ 'axes': 'ZYX'
3192
+ }
3193
+
3194
+ resolution_value = 1.0 / self.xy_scale if self.xy_scale != 0 else 1
3195
+
3173
3196
  if filename is None:
3174
3197
  filename = "labelled_edges.tif"
3175
3198
  elif not filename.endswith(('.tif', '.tiff')):
@@ -3177,11 +3200,11 @@ class Network_3D:
3177
3200
 
3178
3201
  if self._edges is not None:
3179
3202
  if directory is None:
3180
- tifffile.imwrite(f"{filename}", self._edges)
3203
+ tifffile.imwrite(f"{filename}", self._edges, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
3181
3204
  print(f"Edges saved to {filename}")
3182
3205
 
3183
3206
  if directory is not None:
3184
- tifffile.imwrite(f"{directory}/{filename}", self._edges)
3207
+ tifffile.imwrite(f"{directory}/{filename}", self._edges, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
3185
3208
  print(f"Edges saved to {directory}/{filename}")
3186
3209
 
3187
3210
  if self._edges is None:
@@ -3337,6 +3360,13 @@ class Network_3D:
3337
3360
 
3338
3361
  def save_network_overlay(self, directory = None, filename = None):
3339
3362
 
3363
+ imagej_metadata = {
3364
+ 'spacing': self.z_scale,
3365
+ 'slices': self._network_overlay.shape[0],
3366
+ 'channels': 1,
3367
+ 'axes': 'ZYX'
3368
+ }
3369
+ resolution_value = 1.0 / self.xy_scale if self.xy_scale != 0 else 1
3340
3370
 
3341
3371
  if filename is None:
3342
3372
  filename = "overlay_1.tif"
@@ -3345,15 +3375,29 @@ class Network_3D:
3345
3375
 
3346
3376
  if self._network_overlay is not None:
3347
3377
  if directory is None:
3348
- tifffile.imwrite(f"{filename}", self._network_overlay)
3378
+ if len(self._network_overlay.shape) == 3:
3379
+ tifffile.imwrite(f"{filename}", self._network_overlay, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
3380
+ else:
3381
+ tifffile.imwrite(f"{filename}", self._network_overlay)
3349
3382
  print(f"Network overlay saved to {filename}")
3350
3383
 
3351
3384
  if directory is not None:
3352
- tifffile.imwrite(f"{directory}/{filename}", self._network_overlay)
3385
+ if len(self._network_overlay.shape) == 3:
3386
+ tifffile.imwrite(f"{directory}/{filename}", self._network_overlay, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
3387
+ else:
3388
+ tifffile.imwrite(f"{directory}/{filename}", self._network_overlay)
3353
3389
  print(f"Network overlay saved to {directory}/{filename}")
3354
3390
 
3355
3391
  def save_id_overlay(self, directory = None, filename = None):
3356
3392
 
3393
+ imagej_metadata = {
3394
+ 'spacing': self.z_scale,
3395
+ 'slices': self._id_overlay.shape[0],
3396
+ 'channels': 1,
3397
+ 'axes': 'ZYX'
3398
+ }
3399
+ resolution_value = 1.0 / self.xy_scale if self.xy_scale != 0 else 1
3400
+
3357
3401
  if filename is None:
3358
3402
  filename = "overlay_2.tif"
3359
3403
  if not filename.endswith(('.tif', '.tiff')):
@@ -3361,11 +3405,17 @@ class Network_3D:
3361
3405
 
3362
3406
  if self._id_overlay is not None:
3363
3407
  if directory is None:
3364
- tifffile.imwrite(f"{filename}", self._id_overlay)
3408
+ if len(self._id_overlay.shape) == 3:
3409
+ tifffile.imwrite(f"{filename}", self._id_overlay, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
3410
+ else:
3411
+ tifffile.imwrite(f"{filename}", self._id_overlay, imagej=True)
3365
3412
  print(f"Network overlay saved to {filename}")
3366
3413
 
3367
3414
  if directory is not None:
3368
- tifffile.imwrite(f"{directory}/{filename}", self._id_overlay)
3415
+ if len(self._id_overlay.shape) == 3:
3416
+ tifffile.imwrite(f"{directory}/{filename}", self._id_overlay, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
3417
+ else:
3418
+ tifffile.imwrite(f"{directory}/{filename}", self._id_overlay)
3369
3419
  print(f"ID overlay saved to {directory}/{filename}")
3370
3420
 
3371
3421
 
@@ -3488,7 +3538,7 @@ class Network_3D:
3488
3538
 
3489
3539
  if file_path is not None:
3490
3540
  self._xy_scale, self_z_scale = read_scalings(file_path)
3491
- print("Succesfully loaded voxel_scalings")
3541
+ print(f"Succesfully loaded voxel_scalings; values overriden to xy_scale: {self.xy_scale}, z_scale: {self.z_scale}")
3492
3542
  return
3493
3543
 
3494
3544
  items = directory_info(directory)
@@ -3497,11 +3547,11 @@ class Network_3D:
3497
3547
  if item == 'voxel_scalings.txt':
3498
3548
  if directory is not None:
3499
3549
  self._xy_scale, self._z_scale = read_scalings(f"{directory}/{item}")
3500
- print("Succesfully loaded voxel_scalings")
3550
+ print(f"Succesfully loaded voxel_scalings; values overriden to xy_scale: {self.xy_scale}, z_scale: {self.z_scale}")
3501
3551
  return
3502
3552
  else:
3503
3553
  self._xy_scale, self._z_scale = read_scalings(item)
3504
- print("Succesfully loaded voxel_scalings")
3554
+ print(f"Succesfully loaded voxel_scaling; values overriden to xy_scale: {self.xy_scale}, z_scale: {self.z_scale}s")
3505
3555
  return
3506
3556
 
3507
3557
  print("Could not find voxel scalings. They must be in the specified directory and named 'voxel_scalings.txt'")
@@ -5403,9 +5453,9 @@ class Network_3D:
5403
5453
 
5404
5454
 
5405
5455
 
5406
- def interactions(self, search = 0, cores = 0, resize = None, save = False, skele = False, fastdil = False):
5456
+ def interactions(self, search = 0, cores = 0, resize = None, save = False, skele = False, length = False, auto = True, fastdil = False):
5407
5457
 
5408
- return morphology.quantify_edge_node(self._nodes, self._edges, search = search, xy_scale = self._xy_scale, z_scale = self._z_scale, cores = cores, resize = resize, save = save, skele = skele, fastdil = fastdil)
5458
+ return morphology.quantify_edge_node(self._nodes, self._edges, search = search, xy_scale = self._xy_scale, z_scale = self._z_scale, cores = cores, resize = resize, save = save, skele = skele, length = length, auto = auto, fastdil = fastdil)
5409
5459
 
5410
5460
 
5411
5461