nettracer3d 0.6.3__py3-none-any.whl → 0.6.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.
nettracer3d/morphology.py CHANGED
@@ -6,8 +6,17 @@ import multiprocessing as mp
6
6
  from concurrent.futures import ThreadPoolExecutor, as_completed
7
7
  import tifffile
8
8
  from functools import partial
9
- import pandas as pd
9
+ import concurrent.futures
10
+ from functools import partial
10
11
  from scipy import ndimage
12
+ import pandas as pd
13
+ # Import CuPy conditionally for GPU support
14
+ try:
15
+ import cupy as cp
16
+ import cupyx.scipy.ndimage as cpx
17
+ HAS_CUPY = True
18
+ except ImportError:
19
+ HAS_CUPY = False
11
20
 
12
21
  def get_reslice_indices(slice_obj, dilate_xy, dilate_z, array_shape):
13
22
  """Convert slice object to padded indices accounting for dilation and boundaries"""
@@ -279,9 +288,6 @@ def search_neighbor_ids(nodes, targets, id_dict, neighborhood_dict, totals, sear
279
288
 
280
289
 
281
290
 
282
-
283
-
284
-
285
291
  def get_search_space_dilate(target, centroids, id_dict, search, scaling = 1):
286
292
 
287
293
  ymax = np.max(centroids[:, 0])
@@ -308,4 +314,200 @@ def get_search_space_dilate(target, centroids, id_dict, search, scaling = 1):
308
314
 
309
315
 
310
316
 
311
- return array
317
+ return array
318
+
319
+
320
+ # Methods pertaining to getting radii:
321
+
322
+ def process_object_cpu(label, objects, labeled_array):
323
+ """
324
+ Process a single labeled object to estimate its radius (CPU version).
325
+ This function is designed to be called in parallel.
326
+
327
+ Parameters:
328
+ -----------
329
+ label : int
330
+ The label ID to process
331
+ objects : list
332
+ List of slice objects from ndimage.find_objects
333
+ labeled_array : numpy.ndarray
334
+ The full 3D labeled array
335
+
336
+ Returns:
337
+ --------
338
+ tuple: (label, radius, mask_volume, dimensions)
339
+ """
340
+ # Get the slice object (bounding box) for this label
341
+ # Index is label-1 because find_objects returns 0-indexed results
342
+ obj_slice = objects[label-1]
343
+
344
+ if obj_slice is None:
345
+ return label, 0, 0, np.array([0, 0, 0])
346
+
347
+ # Extract subarray containing just this object (plus padding)
348
+ # Create padded slices to ensure there's background around the object
349
+ padded_slices = []
350
+ for dim_idx, dim_slice in enumerate(obj_slice):
351
+ start = max(0, dim_slice.start - 1)
352
+ stop = min(labeled_array.shape[dim_idx], dim_slice.stop + 1)
353
+ padded_slices.append(slice(start, stop))
354
+
355
+ # Extract the subarray
356
+ subarray = labeled_array[tuple(padded_slices)]
357
+
358
+ # Create binary mask for this object within the subarray
359
+ mask = (subarray == label)
360
+
361
+ # Compute distance transform on the smaller mask
362
+ dist_transform = compute_distance_transform_distance(mask)
363
+
364
+ # Filter out small values near the edge to focus on more central regions
365
+ radius = np.max(dist_transform)
366
+
367
+ # Calculate basic shape metrics
368
+ volume = np.sum(mask)
369
+
370
+ # Calculate bounding box dimensions
371
+ x_len = obj_slice[0].stop - obj_slice[0].start
372
+ y_len = obj_slice[1].stop - obj_slice[1].start
373
+ z_len = obj_slice[2].stop - obj_slice[2].start
374
+ dimensions = np.array([x_len, y_len, z_len])
375
+
376
+ return label, radius, volume, dimensions
377
+
378
+ def estimate_object_radii_cpu(labeled_array, n_jobs=None):
379
+ """
380
+ Estimate the radii of labeled objects in a 3D numpy array using distance transform.
381
+ CPU parallel implementation.
382
+
383
+ Parameters:
384
+ -----------
385
+ labeled_array : numpy.ndarray
386
+ 3D array where each object has a unique integer label (0 is background)
387
+ n_jobs : int or None
388
+ Number of parallel jobs. If None, uses all available cores.
389
+
390
+ Returns:
391
+ --------
392
+ dict: Dictionary mapping object labels to estimated radii
393
+ dict: (optional) Dictionary of shape statistics for each label
394
+ """
395
+ # Find bounding box for each labeled object
396
+ objects = ndimage.find_objects(labeled_array)
397
+
398
+ unique_labels = np.unique(labeled_array)
399
+ unique_labels = unique_labels[unique_labels != 0] # Remove background
400
+
401
+ # Create a partial function for parallel processing
402
+ process_func = partial(process_object_cpu, objects=objects, labeled_array=labeled_array)
403
+
404
+ # Process objects in parallel
405
+ results = []
406
+ with concurrent.futures.ThreadPoolExecutor(max_workers=n_jobs) as executor:
407
+ # Submit all jobs
408
+ future_to_label = {executor.submit(process_func, label): label for label in unique_labels}
409
+
410
+ # Collect results as they complete
411
+ for future in concurrent.futures.as_completed(future_to_label):
412
+ results.append(future.result())
413
+
414
+ # Organize results
415
+ radii = {}
416
+
417
+ for label, radius, volume, dimensions in results:
418
+ radii[label] = radius
419
+
420
+ return radii
421
+
422
+ def estimate_object_radii_gpu(labeled_array):
423
+ """
424
+ Estimate the radii of labeled objects in a 3D numpy array using distance transform.
425
+ GPU implementation using CuPy.
426
+
427
+ Parameters:
428
+ -----------
429
+ labeled_array : numpy.ndarray
430
+ 3D array where each object has a unique integer label (0 is background)
431
+
432
+ Returns:
433
+ --------
434
+ dict: Dictionary mapping object labels to estimated radii
435
+ dict: (optional) Dictionary of shape statistics for each label
436
+ """
437
+
438
+ try:
439
+ if not HAS_CUPY:
440
+ raise ImportError("CuPy is required for GPU acceleration")
441
+
442
+ # Find bounding box for each labeled object (on CPU)
443
+ objects = ndimage.find_objects(labeled_array)
444
+
445
+ # Transfer entire labeled array to GPU once
446
+ labeled_array_gpu = cp.asarray(labeled_array)
447
+
448
+ unique_labels = cp.unique(labeled_array_gpu)
449
+ unique_labels = cp.asnumpy(unique_labels)
450
+ unique_labels = unique_labels[unique_labels != 0] # Remove background
451
+
452
+ radii = {}
453
+
454
+ for label in unique_labels:
455
+ # Get the slice object (bounding box) for this label
456
+ obj_slice = objects[label-1]
457
+
458
+ if obj_slice is None:
459
+ continue
460
+
461
+ # Extract subarray from GPU array
462
+ padded_slices = []
463
+ for dim_idx, dim_slice in enumerate(obj_slice):
464
+ start = max(0, dim_slice.start - 1)
465
+ stop = min(labeled_array.shape[dim_idx], dim_slice.stop + 1)
466
+ padded_slices.append(slice(start, stop))
467
+
468
+ # Create binary mask for this object (directly on GPU)
469
+ mask_gpu = (labeled_array_gpu[tuple(padded_slices)] == label)
470
+
471
+ # Compute distance transform on GPU
472
+ dist_transform_gpu = compute_distance_transform_distance_GPU(mask_gpu)
473
+
474
+ radius = float(cp.max(dist_transform_gpu).get())
475
+
476
+ # Store the radius
477
+ radii[label] = radius
478
+
479
+ # Clean up GPU memory
480
+ del labeled_array_gpu
481
+
482
+ return radii
483
+
484
+ except Exception as e:
485
+ print(f"GPU calculation failed, trying CPU instead -> {e}")
486
+ return estimate_object_radii_cpu(labeled_array)
487
+
488
+ def compute_distance_transform_distance_GPU(nodes):
489
+
490
+ is_pseudo_3d = nodes.shape[0] == 1
491
+ if is_pseudo_3d:
492
+ nodes = cp.squeeze(nodes) # Convert to 2D for processing
493
+
494
+ # Compute the distance transform on the GPU
495
+ distance = cpx.distance_transform_edt(nodes)
496
+
497
+ if is_pseudo_3d:
498
+ cp.expand_dims(distance, axis = 0)
499
+
500
+ return distance
501
+
502
+
503
+ def compute_distance_transform_distance(nodes):
504
+
505
+ is_pseudo_3d = nodes.shape[0] == 1
506
+ if is_pseudo_3d:
507
+ nodes = np.squeeze(nodes) # Convert to 2D for processing
508
+
509
+ # Fallback to CPU if there's an issue with GPU computation
510
+ distance = ndimage.distance_transform_edt(nodes)
511
+ if is_pseudo_3d:
512
+ np.expand_dims(distance, axis = 0)
513
+ return distance
nettracer3d/nettracer.py CHANGED
@@ -547,6 +547,43 @@ def remove_branches(skeleton, length):
547
547
  return image_copy
548
548
 
549
549
 
550
+ def estimate_object_radii(labeled_array, gpu=False, n_jobs=None):
551
+ """
552
+ Estimate the radii of labeled objects in a 3D numpy array.
553
+ Dispatches to appropriate implementation based on parameters.
554
+
555
+ Parameters:
556
+ -----------
557
+ labeled_array : numpy.ndarray
558
+ 3D array where each object has a unique integer label (0 is background)
559
+ gpu : bool
560
+ Whether to use GPU acceleration via CuPy (if available)
561
+ n_jobs : int or None
562
+ Number of parallel jobs for CPU version. If None, uses all available cores.
563
+
564
+ Returns:
565
+ --------
566
+ dict: Dictionary mapping object labels to estimated radii
567
+ dict: (optional) Dictionary of shape statistics for each label
568
+ """
569
+ # Check if GPU is requested but not available
570
+ try:
571
+ import cupy as cp
572
+ import cupyx.scipy.ndimage as cpx
573
+ HAS_CUPY = True
574
+ except ImportError:
575
+ HAS_CUPY = False
576
+
577
+ if gpu and not HAS_CUPY:
578
+ print("Warning: GPU acceleration requested but CuPy not available. Falling back to CPU.")
579
+ gpu = False
580
+
581
+ if gpu:
582
+ return morphology.estimate_object_radii_gpu(labeled_array)
583
+ else:
584
+ return morphology.estimate_object_radii_cpu(labeled_array, n_jobs)
585
+
586
+
550
587
  def break_and_label_skeleton(skeleton, peaks = 1, branch_removal = 0, comp_dil = 0, max_vol = 0, directory = None, return_skele = False, nodes = None):
551
588
  """Internal method to break open a skeleton at its branchpoints and label the remaining components, for an 8bit binary array"""
552
589
 
@@ -751,6 +788,8 @@ def fill_holes_3d(array):
751
788
 
752
789
  return holes_mask
753
790
 
791
+ print("Filling Holes...")
792
+
754
793
  array = binarize(array)
755
794
  inv_array = invert_array(array)
756
795
 
@@ -3270,7 +3309,10 @@ class Network_3D:
3270
3309
  self._xy_scale = xy_scale
3271
3310
  self._z_scale = z_scale
3272
3311
 
3273
- self.save_scaling(directory)
3312
+ try:
3313
+ self.save_scaling(directory)
3314
+ except:
3315
+ pass
3274
3316
 
3275
3317
  if search is None and ignore_search_region == False:
3276
3318
  search = 0
@@ -3286,29 +3328,51 @@ class Network_3D:
3286
3328
  if other_nodes is not None:
3287
3329
  self.merge_nodes(other_nodes, label_nodes)
3288
3330
 
3289
- self.save_nodes(directory)
3290
- self.save_node_identities(directory)
3331
+ try:
3332
+ self.save_nodes(directory)
3333
+ except:
3334
+ pass
3335
+ try:
3336
+ self.save_node_identities(directory)
3337
+ except:
3338
+ pass
3291
3339
 
3292
3340
  if not ignore_search_region:
3293
3341
  self.calculate_search_region(search, GPU = GPU, fast_dil = fast_dil, GPU_downsample = GPU_downsample)
3294
- self._nodes = None
3342
+ #self._nodes = None # I originally put this here to micromanage RAM a little bit (it writes it to disk so I wanted to purge it from mem briefly but now idt thats necessary and I'd rather give it flexibility when lacking write permissions)
3295
3343
  search = None
3296
- self.save_search_region(directory)
3344
+ try:
3345
+ self.save_search_region(directory)
3346
+ except:
3347
+ pass
3297
3348
 
3298
3349
  self.calculate_edges(edges, diledge = diledge, inners = inners, hash_inner_edges = hash_inners, search = search, remove_edgetrunk = remove_trunk, GPU = GPU, fast_dil = fast_dil, skeletonized = skeletonize)
3299
3350
  del edges
3300
- self.save_edges(directory)
3351
+ try:
3352
+ self.save_edges(directory)
3353
+ except:
3354
+ pass
3301
3355
 
3302
3356
  self.calculate_network(search = search, ignore_search_region = ignore_search_region)
3303
- self.save_network(directory)
3357
+
3358
+ try:
3359
+ self.save_network(directory)
3360
+ except:
3361
+ pass
3304
3362
 
3305
3363
  if self._nodes is None:
3306
3364
  self.load_nodes(directory)
3307
3365
 
3308
3366
  self.calculate_node_centroids(down_factor)
3309
- self.save_node_centroids(directory)
3367
+ try:
3368
+ self.save_node_centroids(directory)
3369
+ except:
3370
+ pass
3310
3371
  self.calculate_edge_centroids(down_factor)
3311
- self.save_edge_centroids(directory)
3372
+ try:
3373
+ self.save_edge_centroids(directory)
3374
+ except:
3375
+ pass
3312
3376
 
3313
3377
 
3314
3378
  def draw_network(self, directory = None, down_factor = None, GPU = False):
@@ -3557,15 +3621,21 @@ class Network_3D:
3557
3621
  list1 = self._network_lists[0] #Get network lists to change
3558
3622
  list2 = self._network_lists[1]
3559
3623
  list3 = self._network_lists[2]
3624
+ return1 = []
3625
+ return2 = []
3626
+ return3 = []
3560
3627
 
3561
3628
  for i in range(len(list1)):
3562
3629
  list1[i] = self.communities[list1[i]] #Set node at network list spot to its community instead
3563
3630
  list2[i] = self.communities[list2[i]]
3564
- if list1[i] == list2[i]: #If the edge corresponding there joins different communities, it will not be set to 0
3565
- list3[i] = 0
3631
+ if list1[i] != list2[i]: #Avoid self - self connections
3632
+ return1.append(list1[i])
3633
+ return2.append(list2[i])
3634
+ return3.append(list3[i])
3635
+
3566
3636
 
3567
3637
 
3568
- self.network_lists = [list1, list2, list3]
3638
+ self.network_lists = [return1, return2, return3]
3569
3639
 
3570
3640
  if self._nodes is not None:
3571
3641
  self._nodes = update_array(self._nodes, inverted, targets = targets) #Set the array to match the new network
@@ -325,7 +325,7 @@ class ImageViewerWindow(QMainWindow):
325
325
  self.tabbed_data = TabbedDataWidget(self)
326
326
  right_layout.addWidget(self.tabbed_data)
327
327
  # Initialize data_table property to None - it will be set when tabs are added
328
- self.data_table = None
328
+ self.data_table = []
329
329
 
330
330
  # Create table control panel
331
331
  table_control = QWidget()
@@ -1399,55 +1399,128 @@ class ImageViewerWindow(QMainWindow):
1399
1399
  print(f"An error has occured: {e}")
1400
1400
 
1401
1401
  def handle_seperate(self):
1402
-
1402
+ print("Note: I search each selected label one at a time and then split it with the ndimage.label method which uses C but still has to search the entire array each time, I may be a very slow with big operations :)")
1403
1403
  try:
1404
-
1404
+ # Handle nodes
1405
1405
  if len(self.clicked_values['nodes']) > 0:
1406
- self.create_highlight_overlay(node_indices = self.clicked_values['nodes'])
1407
- max_val = np.max(my_network.nodes)
1408
- self.highlight_overlay, num = n3d.label_objects(self.highlight_overlay)
1409
-
1410
- node_bools = self.highlight_overlay != 0
1411
- new_max = num + max_val
1412
- self.highlight_overlay = self.highlight_overlay + max_val
1413
- self.highlight_overlay = self.highlight_overlay * node_bools
1414
- if new_max < 256:
1415
- dtype = np.uint8
1416
- elif new_max < 65536:
1417
- dtype = np.uint16
1418
- else:
1419
- dtype = np.uint32
1420
-
1421
- self.highlight_overlay = self.highlight_overlay.astype(dtype)
1422
- my_network.nodes = my_network.nodes + self.highlight_overlay
1406
+ self.create_highlight_overlay(node_indices=self.clicked_values['nodes'])
1407
+ max_val = np.max(my_network.nodes) + 1
1408
+
1409
+ # Create a boolean mask for highlighted values
1410
+ self.highlight_overlay = self.highlight_overlay != 0
1411
+
1412
+ # Create array with just the highlighted values
1413
+ highlighted_nodes = self.highlight_overlay * my_network.nodes
1414
+
1415
+ # Get unique values in the highlighted regions (excluding 0)
1416
+ vals = list(np.unique(highlighted_nodes))
1417
+ if vals[0] == 0:
1418
+ del vals[0]
1419
+
1420
+ # Process each value separately
1421
+ for val in vals:
1422
+ # Create a mask for this value
1423
+ val_mask = my_network.nodes == val
1424
+
1425
+ # Create an array without this value
1426
+ temp = my_network.nodes - (val_mask * val)
1427
+
1428
+ # Label the connected components for this value
1429
+ labeled_mask, num_components = n3d.label_objects(val_mask)
1430
+
1431
+ if num_components > 1:
1432
+ # Set appropriate dtype based on max value
1433
+ if max_val + num_components < 256:
1434
+ dtype = np.uint8
1435
+ elif max_val + num_components < 65536:
1436
+ dtype = np.uint16
1437
+ labeled_mask = labeled_mask.astype(dtype)
1438
+ temp = temp.astype(dtype)
1439
+ else:
1440
+ dtype = np.uint32
1441
+ labeled_mask = labeled_mask.astype(dtype)
1442
+ temp = temp.astype(dtype)
1443
+
1444
+ # Add new labels to the temporary array
1445
+ mask_nonzero = labeled_mask != 0
1446
+ labeled_mask = labeled_mask + max_val - 1 # -1 because we'll restore the first component
1447
+ labeled_mask = labeled_mask * mask_nonzero
1448
+
1449
+ # Restore original value for first component
1450
+ first_component = labeled_mask == max_val
1451
+ labeled_mask = labeled_mask - (first_component * (max_val - val))
1452
+
1453
+ # Add labeled components back to the array
1454
+ my_network.nodes = temp + labeled_mask
1455
+
1456
+ # Update max value for next iteration
1457
+ max_val += num_components - 1 # -1 because we kept one original label
1458
+
1423
1459
  self.load_channel(0, my_network.nodes, True)
1424
-
1460
+
1461
+ # Handle edges
1425
1462
  if len(self.clicked_values['edges']) > 0:
1426
- self.create_highlight_overlay(edge_indices = self.clicked_values['edges'])
1427
- max_val = np.max(my_network.edges)
1428
- self.highlight_overlay, num = n3d.label_objects(self.highlight_overlay)
1429
- node_bools = self.highlight_overlay != 0
1430
- new_max = num + max_val
1431
-
1432
- self.highlight_overlay = self.highlight_overlay + max_val
1433
- self.highlight_overlay = self.highlight_overlay * node_bools
1434
- if new_max < 256:
1435
- dtype = np.uint8
1436
- elif new_max < 65536:
1437
- dtype = np.uint16
1438
- else:
1439
- dtype = np.uint32
1440
-
1441
- self.highlight_overlay = self.highlight_overlay.astype(dtype)
1442
- my_network.edges = my_network.edges + self.highlight_overlay
1463
+ self.create_highlight_overlay(edge_indices=self.clicked_values['edges'])
1464
+ max_val = np.max(my_network.edges) + 1
1465
+
1466
+ # Create a boolean mask for highlighted values
1467
+ self.highlight_overlay = self.highlight_overlay != 0
1468
+
1469
+ # Create array with just the highlighted values
1470
+ highlighted_edges = self.highlight_overlay * my_network.edges
1471
+
1472
+ # Get unique values in the highlighted regions (excluding 0)
1473
+ vals = list(np.unique(highlighted_edges))
1474
+ if vals[0] == 0:
1475
+ del vals[0]
1476
+
1477
+ # Process each value separately
1478
+ for val in vals:
1479
+ # Create a mask for this value
1480
+ val_mask = my_network.edges == val
1481
+
1482
+ # Create an array without this value
1483
+ temp = my_network.edges - (val_mask * val)
1484
+
1485
+ # Label the connected components for this value
1486
+ labeled_mask, num_components = n3d.label_objects(val_mask)
1487
+
1488
+ if num_components > 1:
1489
+ # Set appropriate dtype based on max value
1490
+ if max_val + num_components < 256:
1491
+ dtype = np.uint8
1492
+ elif max_val + num_components < 65536:
1493
+ dtype = np.uint16
1494
+ labeled_mask = labeled_mask.astype(dtype)
1495
+ temp = temp.astype(dtype)
1496
+ else:
1497
+ dtype = np.uint32
1498
+ labeled_mask = labeled_mask.astype(dtype)
1499
+ temp = temp.astype(dtype)
1500
+
1501
+ # Add new labels to the temporary array
1502
+ mask_nonzero = labeled_mask != 0
1503
+ labeled_mask = labeled_mask + max_val - 1 # -1 because we'll restore the first component
1504
+ labeled_mask = labeled_mask * mask_nonzero
1505
+
1506
+ # Restore original value for first component
1507
+ first_component = labeled_mask == max_val
1508
+ labeled_mask = labeled_mask - (first_component * (max_val - val))
1509
+
1510
+ # Add labeled components back to the array
1511
+ my_network.edges = temp + labeled_mask
1512
+
1513
+ # Update max value for next iteration
1514
+ max_val += num_components - 1 # -1 because we kept one original label
1515
+
1443
1516
  self.load_channel(1, my_network.edges, True)
1517
+
1444
1518
  self.highlight_overlay = None
1445
1519
  self.update_display()
1446
1520
  print("Network is not updated automatically, please recompute if necessary. Identities are not automatically updated.")
1447
1521
  self.show_centroid_dialog()
1448
-
1449
1522
  except Exception as e:
1450
- print(f"Error seperating: {e}")
1523
+ print(f"Error separating: {e}")
1451
1524
 
1452
1525
 
1453
1526
 
@@ -1814,6 +1887,7 @@ class ImageViewerWindow(QMainWindow):
1814
1887
  new_xlim = [xdata - x_range, xdata + x_range]
1815
1888
  new_ylim = [ydata - y_range, ydata + y_range]
1816
1889
 
1890
+
1817
1891
  if (new_xlim[0] <= self.original_xlim[0] or
1818
1892
  new_xlim[1] >= self.original_xlim[1] or
1819
1893
  new_ylim[0] <= self.original_ylim[0] or
@@ -2391,6 +2465,8 @@ class ImageViewerWindow(QMainWindow):
2391
2465
  random_action.triggered.connect(self.show_random_dialog)
2392
2466
  vol_action = stats_menu.addAction("Calculate Volumes")
2393
2467
  vol_action.triggered.connect(self.volumes)
2468
+ rad_action = stats_menu.addAction("Calculate Radii")
2469
+ rad_action.triggered.connect(self.show_rad_dialog)
2394
2470
  inter_action = stats_menu.addAction("Calculate Node < > Edge Interaction")
2395
2471
  inter_action.triggered.connect(self.show_interaction_dialog)
2396
2472
  overlay_menu = analysis_menu.addMenu("Data/Overlays")
@@ -3228,6 +3304,9 @@ class ImageViewerWindow(QMainWindow):
3228
3304
  self.highlight_overlay = None
3229
3305
  except:
3230
3306
  pass
3307
+ if not data:
3308
+ self.original_xlim = None
3309
+ self.original_ylim = None
3231
3310
  continue
3232
3311
  else:
3233
3312
  old_shape = self.channel_data[i].shape[:3] #Ask user to resize images that are shaped differently
@@ -3308,8 +3387,7 @@ class ImageViewerWindow(QMainWindow):
3308
3387
 
3309
3388
 
3310
3389
  except Exception as e:
3311
- import traceback
3312
- print(traceback.format_exc())
3390
+
3313
3391
  if not data:
3314
3392
  from PyQt6.QtWidgets import QMessageBox
3315
3393
  QMessageBox.critical(
@@ -3821,6 +3899,9 @@ class ImageViewerWindow(QMainWindow):
3821
3899
  dialog = RandomDialog(self)
3822
3900
  dialog.exec()
3823
3901
 
3902
+ def show_rad_dialog(self):
3903
+ dialog = RadDialog(self)
3904
+ dialog.exec()
3824
3905
 
3825
3906
  def show_interaction_dialog(self):
3826
3907
  dialog = InteractionDialog(self)
@@ -3987,7 +4068,7 @@ class CustomTableView(QTableView):
3987
4068
  desc_action.triggered.connect(lambda checked, c=col: self.sort_table(c, ascending=False))
3988
4069
 
3989
4070
  # Different menus for top and bottom tables
3990
- if self == self.parent.data_table: # Top table
4071
+ if self in self.parent.data_table: # Top table
3991
4072
  save_menu = context_menu.addMenu("Save As")
3992
4073
  save_csv = save_menu.addAction("CSV")
3993
4074
  save_excel = save_menu.addAction("Excel")
@@ -4096,7 +4177,7 @@ class CustomTableView(QTableView):
4096
4177
  df = self.model()._data
4097
4178
 
4098
4179
  # Get table name for the file dialog title
4099
- if self == self.parent.data_table:
4180
+ if self in self.parent.data_table:
4100
4181
  table_name = "Statistics"
4101
4182
  elif self == self.parent.network_table:
4102
4183
  table_name = "Network"
@@ -4105,7 +4186,7 @@ class CustomTableView(QTableView):
4105
4186
 
4106
4187
  # Get save file name
4107
4188
  file_filter = ("CSV Files (*.csv)" if file_type == 'csv' else
4108
- "Excel Files (*.xlsx)" if file_type == 'excel' else
4189
+ "Excel Files (*.xlsx)" if file_type == 'xlsx' else
4109
4190
  "Gephi Graph (*.gexf)" if file_type == 'gexf' else
4110
4191
  "GraphML (*.graphml)" if file_type == 'graphml' else
4111
4192
  "Pajek Network (*.net)")
@@ -4237,16 +4318,21 @@ class CustomTableView(QTableView):
4237
4318
  self.parent.clicked_values['edges'] = []
4238
4319
  self.parent.clicked_values['nodes'].append(value)
4239
4320
 
4240
- # Highlight the value in both tables if it exists
4241
- self.highlight_value_in_table(self.parent.network_table, value, column)
4242
- self.highlight_value_in_table(self.parent.selection_table, value, column)
4321
+ try:
4322
+ # Highlight the value in both tables if it exists
4323
+ self.highlight_value_in_table(self.parent.network_table, value, column)
4324
+ self.highlight_value_in_table(self.parent.selection_table, value, column)
4325
+ except:
4326
+ pass
4243
4327
  else:
4244
4328
  print(f"Node {value} not found in centroids dictionary")
4245
4329
 
4246
4330
  elif column == 2: # Third column is edges
4331
+ if my_network.edge_centroids is None:
4332
+ self.parent.show_centroid_dialog()
4333
+
4247
4334
  if value in my_network.edge_centroids:
4248
- if my_network.edge_centroids is None:
4249
- self.parent.show_centroid_dialog()
4335
+
4250
4336
  # Get centroid coordinates (Z, Y, X)
4251
4337
  centroid = my_network.edge_centroids[value]
4252
4338
  # Set the active channel to edges (1)
@@ -4267,9 +4353,12 @@ class CustomTableView(QTableView):
4267
4353
  self.parent.clicked_values['edges'] = []
4268
4354
  self.parent.clicked_values['edges'].append(value)
4269
4355
 
4270
- # Highlight the value in both tables if it exists
4271
- self.highlight_value_in_table(self.parent.network_table, value, column)
4272
- self.highlight_value_in_table(self.parent.selection_table, value, column)
4356
+ try:
4357
+ # Highlight the value in both tables if it exists
4358
+ self.highlight_value_in_table(self.parent.network_table, value, column)
4359
+ self.highlight_value_in_table(self.parent.selection_table, value, column)
4360
+ except:
4361
+ pass
4273
4362
  else:
4274
4363
  print(f"Edge {value} not found in centroids dictionary")
4275
4364
  else: #If highlighting paired elements
@@ -4490,7 +4579,13 @@ class TabbedDataWidget(QTabWidget):
4490
4579
  """Add a new table with the given name"""
4491
4580
  if name in self.tables:
4492
4581
  # If tab already exists, update its content
4493
- idx = self.indexOf(self.tables[name])
4582
+ old_table = self.tables[name]
4583
+ idx = self.indexOf(old_table)
4584
+
4585
+ # Remove the old table reference from parent's data_table
4586
+ if self.parent_window and old_table in self.parent_window.data_table:
4587
+ self.parent_window.data_table.remove(old_table)
4588
+
4494
4589
  self.removeTab(idx)
4495
4590
 
4496
4591
  # Create a new CustomTableView with is_top_table=True
@@ -4510,7 +4605,7 @@ class TabbedDataWidget(QTabWidget):
4510
4605
 
4511
4606
  # Update parent's data_table reference
4512
4607
  if self.parent_window:
4513
- self.parent_window.data_table = new_table
4608
+ self.parent_window.data_table.append(new_table)
4514
4609
 
4515
4610
  def close_tab(self, index):
4516
4611
  """Close the tab at the given index"""
@@ -4525,12 +4620,12 @@ class TabbedDataWidget(QTabWidget):
4525
4620
  if name_to_remove:
4526
4621
  del self.tables[name_to_remove]
4527
4622
 
4528
- self.removeTab(index)
4529
-
4530
- # Update parent's data_table reference to current table
4531
- if self.parent_window and self.count() > 0:
4532
- self.parent_window.data_table = self.currentWidget()
4623
+ # Update parent's data_table reference by removing the widget
4624
+ if self.parent_window and widget in self.parent_window.data_table:
4625
+ self.parent_window.data_table.remove(widget)
4533
4626
 
4627
+ self.removeTab(index)
4628
+
4534
4629
  def clear_all_tabs(self):
4535
4630
  """Remove all tabs"""
4536
4631
  while self.count() > 0:
@@ -4895,6 +4990,52 @@ class ArbitraryDialog(QDialog):
4895
4990
  continue
4896
4991
 
4897
4992
  return processed_values
4993
+
4994
+ def handle_find_action(self, mode, value):
4995
+ """Handle the Find action."""
4996
+
4997
+ # Determine if we're looking for a node or edge
4998
+ if mode == 0:
4999
+
5000
+ if my_network.node_centroids is None:
5001
+ self.parent().show_centroid_dialog()
5002
+
5003
+ if value in my_network.node_centroids:
5004
+ # Get centroid coordinates (Z, Y, X)
5005
+ centroid = my_network.node_centroids[value]
5006
+ # Set the active channel to nodes (0)
5007
+ self.parent().set_active_channel(0)
5008
+ # Toggle on the nodes channel if it's not already visible
5009
+ if not self.parent().channel_visible[0]:
5010
+ self.parent().channel_buttons[0].setChecked(True)
5011
+ self.parent().toggle_channel(0)
5012
+ # Navigate to the Z-slice
5013
+ self.parent().slice_slider.setValue(int(centroid[0]))
5014
+ print(f"Found node {value} at Z-slice {centroid[0]}")
5015
+
5016
+ else:
5017
+ print(f"Node {value} not found in centroids dictionary")
5018
+
5019
+ else: # edges
5020
+ if my_network.edge_centroids is None:
5021
+ self.parent().show_centroid_dialog()
5022
+
5023
+ if value in my_network.edge_centroids:
5024
+
5025
+ # Get centroid coordinates (Z, Y, X)
5026
+ centroid = my_network.edge_centroids[value]
5027
+ # Set the active channel to edges (1)
5028
+ self.parent().set_active_channel(1)
5029
+ # Toggle on the edges channel if it's not already visible
5030
+ if not self.parent().channel_visible[1]:
5031
+ self.parent().channel_buttons[1].setChecked(True)
5032
+ self.parent().toggle_channel(1)
5033
+ # Navigate to the Z-slice
5034
+ self.parent().slice_slider.setValue(int(centroid[0]))
5035
+ print(f"Found edge {value} at Z-slice {centroid[0]}")
5036
+
5037
+ else:
5038
+ print(f"Edge {value} not found in centroids dictionary")
4898
5039
 
4899
5040
  def process_selections(self):
4900
5041
  """Process the selection and deselection inputs."""
@@ -4930,11 +5071,21 @@ class ArbitraryDialog(QDialog):
4930
5071
  except:
4931
5072
  pass #Forgive mistakes
4932
5073
 
4933
- for item in select_list:
4934
- try:
4935
- self.parent().clicked_values[mode].append(item)
4936
- except:
4937
- pass
5074
+ select_list.reverse()
5075
+
5076
+ self.parent().clicked_values[mode].extend(select_list)
5077
+
5078
+ select_list.reverse()
5079
+
5080
+ try:
5081
+ if mode == 'nodes':
5082
+ self.handle_find_action(0, select_list[0])
5083
+ self.parent().handle_info(sort = 'node')
5084
+ elif mode == 'edges':
5085
+ self.handle_find_action(1, select_list[0])
5086
+ self.parent().handle_info(sort = 'edge')
5087
+ except:
5088
+ pass
4938
5089
 
4939
5090
  self.parent().clicked_values[mode] = list(set(self.parent().clicked_values[mode]))
4940
5091
 
@@ -4954,6 +5105,8 @@ class ArbitraryDialog(QDialog):
4954
5105
 
4955
5106
  except Exception as e:
4956
5107
  QMessageBox.critical(self, "Error", f"Error processing selections: {str(e)}")
5108
+ import traceback
5109
+ print(traceback.format_exc())
4957
5110
 
4958
5111
  class Show3dDialog(QDialog):
4959
5112
  def __init__(self, parent=None):
@@ -5247,8 +5400,10 @@ class ShuffleDialog(QDialog):
5247
5400
 
5248
5401
  try:
5249
5402
  if accepted_mode == 4:
5250
-
5251
- self.parent().highlight_overlay = n3d.binarize(target_data)
5403
+ try:
5404
+ self.parent().highlight_overlay = n3d.binarize(target_data)
5405
+ except:
5406
+ self.parent().highlight_overay = None
5252
5407
  else:
5253
5408
  self.parent().load_channel(accepted_mode, channel_data = target_data, data = True)
5254
5409
  except:
@@ -5261,11 +5416,12 @@ class ShuffleDialog(QDialog):
5261
5416
  self.parent().highlight_overlay = n3d.binarize(active_data)
5262
5417
  except:
5263
5418
  self.parent().highlight_overlay = None
5419
+ else:
5420
+ self.parent().load_channel(accepted_target, channel_data = active_data, data = True)
5264
5421
  except:
5265
5422
  pass
5266
5423
 
5267
- else:
5268
- self.parent().load_channel(accepted_target, channel_data = active_data, data = True)
5424
+
5269
5425
 
5270
5426
 
5271
5427
  self.parent().update_display()
@@ -5433,18 +5589,23 @@ class RadialDialog(QDialog):
5433
5589
 
5434
5590
  def radial(self):
5435
5591
 
5436
- distance = float(self.distance.text()) if self.distance.text().strip() else 50
5592
+ try:
5593
+
5594
+ distance = float(self.distance.text()) if self.distance.text().strip() else 50
5595
+
5596
+ directory = str(self.distance.text()) if self.directory.text().strip() else None
5437
5597
 
5438
- directory = str(self.distance.text()) if self.directory.text().strip() else None
5598
+ if my_network.node_centroids is None:
5599
+ self.parent().show_centroid_dialog()
5439
5600
 
5440
- if my_network.node_centroids is None:
5441
- self.parent().show_centroid_dialog()
5601
+ radial = my_network.radial_distribution(distance, directory = directory)
5442
5602
 
5443
- radial = my_network.radial_distribution(distance, directory = directory)
5603
+ self.parent().format_for_upperright_table(radial, 'Radial Distance From Any Node', 'Average Number of Neighboring Nodes', title = 'Radial Distribution Analysis')
5444
5604
 
5445
- self.parent().format_for_upperright_table(radial, 'Radial Distance From Any Node', 'Average Number of Neighboring Nodes', title = 'Radial Distribution Analysis')
5605
+ self.accept()
5446
5606
 
5447
- self.accept()
5607
+ except Exception as e:
5608
+ print(f"An error occurred: {e}")
5448
5609
 
5449
5610
  class DegreeDistDialog(QDialog):
5450
5611
 
@@ -5477,7 +5638,7 @@ class DegreeDistDialog(QDialog):
5477
5638
 
5478
5639
  self.accept()
5479
5640
 
5480
- except Excpetion as e:
5641
+ except Exception as e:
5481
5642
  print(f"An error occurred: {e}")
5482
5643
 
5483
5644
  class NeighborIdentityDialog(QDialog):
@@ -5589,6 +5750,50 @@ class RandomDialog(QDialog):
5589
5750
 
5590
5751
  self.accept()
5591
5752
 
5753
+ class RadDialog(QDialog):
5754
+
5755
+ def __init__(self, parent=None):
5756
+
5757
+ super().__init__(parent)
5758
+ self.setWindowTitle("Obtain Radii of Active Image? (Returns Largest Radius for Each Labeled Object)")
5759
+ self.setModal(True)
5760
+
5761
+ layout = QFormLayout(self)
5762
+
5763
+ # GPU checkbox (default False)
5764
+ self.GPU = QPushButton("GPU")
5765
+ self.GPU.setCheckable(True)
5766
+ self.GPU.setChecked(False)
5767
+ layout.addRow("Use GPU:", self.GPU)
5768
+
5769
+
5770
+ # Add Run button
5771
+ run_button = QPushButton("Calculate")
5772
+ run_button.clicked.connect(self.rads)
5773
+ layout.addWidget(run_button)
5774
+
5775
+ def rads(self):
5776
+
5777
+ try:
5778
+ GPU = self.GPU.isChecked()
5779
+
5780
+ active_data = self.parent().channel_data[self.parent().active_channel]
5781
+
5782
+ radii = n3d.estimate_object_radii(active_data, gpu=GPU)
5783
+
5784
+ for key, val in radii.items():
5785
+
5786
+ radii[key] = [val, val * (my_network.xy_scale**2) * my_network.z_scale]
5787
+
5788
+ self.parent().format_for_upperright_table(radii, title = '~Radii of Objects', metric='ObjectID', value=['Largest Radius (Voxels)', 'Largest Radius (Scaled)'])
5789
+
5790
+ self.accept()
5791
+
5792
+ except Exception as e:
5793
+ print(f"Error: {e}")
5794
+
5795
+
5796
+
5592
5797
 
5593
5798
 
5594
5799
  class InteractionDialog(QDialog):
@@ -6303,7 +6508,7 @@ class SLabelDialog(QDialog):
6303
6508
  # GPU checkbox (default True)
6304
6509
  self.GPU = QPushButton("GPU")
6305
6510
  self.GPU.setCheckable(True)
6306
- self.GPU.setChecked(True)
6511
+ self.GPU.setChecked(False)
6307
6512
  layout.addRow("Use GPU:", self.GPU)
6308
6513
 
6309
6514
  self.down_factor = QLineEdit("")
@@ -7372,7 +7577,7 @@ class SmartDilateDialog(QDialog):
7372
7577
  # GPU checkbox (default True)
7373
7578
  self.GPU = QPushButton("GPU")
7374
7579
  self.GPU.setCheckable(True)
7375
- self.GPU.setChecked(True)
7580
+ self.GPU.setChecked(False)
7376
7581
  layout.addRow("Use GPU:", self.GPU)
7377
7582
 
7378
7583
  self.down_factor = QLineEdit("")
@@ -7783,6 +7988,12 @@ class SkeletonizeDialog(QDialog):
7783
7988
  self.remove = QLineEdit("0")
7784
7989
  layout.addRow("Remove Branches Pixel Length (int):", self.remove)
7785
7990
 
7991
+ # auto checkbox (default True)
7992
+ self.auto = QPushButton("Auto")
7993
+ self.auto.setCheckable(True)
7994
+ self.auto.setChecked(False)
7995
+ layout.addRow("Attempt to Auto Correct Skeleton Looping:", self.auto)
7996
+
7786
7997
  # Add Run button
7787
7998
  run_button = QPushButton("Run Skeletonize")
7788
7999
  run_button.clicked.connect(self.run_skeletonize)
@@ -7796,11 +8007,17 @@ class SkeletonizeDialog(QDialog):
7796
8007
  remove = int(self.remove.text()) if self.remove.text() else 0
7797
8008
  except ValueError:
7798
8009
  remove = 0
8010
+
8011
+ auto = self.auto.isChecked()
7799
8012
 
7800
8013
  # Get the active channel data from parent
7801
8014
  active_data = self.parent().channel_data[self.parent().active_channel]
7802
8015
  if active_data is None:
7803
8016
  raise ValueError("No active image selected")
8017
+
8018
+ if auto:
8019
+ active_data = n3d.skeletonize(active_data)
8020
+ active_data = n3d.fill_holes_3d(active_data)
7804
8021
 
7805
8022
  # Call dilate method with parameters
7806
8023
  result = n3d.skeletonize(
@@ -7862,7 +8079,7 @@ class WatershedDialog(QDialog):
7862
8079
  # GPU checkbox (default True)
7863
8080
  self.gpu = QPushButton("GPU")
7864
8081
  self.gpu.setCheckable(True)
7865
- self.gpu.setChecked(True)
8082
+ self.gpu.setChecked(False)
7866
8083
  layout.addRow("Use GPU:", self.gpu)
7867
8084
 
7868
8085
  # Smallest radius (empty by default)
@@ -8112,11 +8329,17 @@ class GenNodesDialog(QDialog):
8112
8329
  self.branch_removal = QLineEdit("0")
8113
8330
  layout.addRow("Skeleton Voxel Branch Length to Remove (int) (Compensates for spines off medial axis):", self.branch_removal)
8114
8331
 
8332
+ self.comp_dil = QLineEdit("0")
8333
+ layout.addRow("Voxel distance to merge nearby nodes (Int - compensates for multi-branch identification along thick branch regions):", self.comp_dil)
8334
+
8115
8335
  self.max_vol = QLineEdit("0")
8116
8336
  layout.addRow("Maximum Voxel Volume of Vertices to Retain (int - Compensates for skeleton looping - occurs before any node merging - the smallest objects are always 27 voxels):", self.max_vol)
8117
8337
 
8118
- self.comp_dil = QLineEdit("0")
8119
- layout.addRow("Voxel distance to merge nearby nodes (Int - compensates for multi-branch identification along thick branch regions):", self.comp_dil)
8338
+ # auto checkbox (default True)
8339
+ self.auto = QPushButton("Auto")
8340
+ self.auto.setCheckable(True)
8341
+ self.auto.setChecked(False)
8342
+ layout.addRow("Attempt to Auto Correct Skeleton Looping:", self.auto)
8120
8343
 
8121
8344
  if not down_factor:
8122
8345
  down_factor = None
@@ -8194,6 +8417,13 @@ class GenNodesDialog(QDialog):
8194
8417
  else:
8195
8418
  order = 0
8196
8419
 
8420
+ auto = self.auto.isChecked()
8421
+
8422
+
8423
+ if auto:
8424
+ my_network.edges = n3d.skeletonize(my_network.edges)
8425
+ my_network.edges = n3d.fill_holes_3d(my_network.edges)
8426
+
8197
8427
 
8198
8428
  result, skele = n3d.label_vertices(
8199
8429
  my_network.edges,
@@ -8248,6 +8478,7 @@ class GenNodesDialog(QDialog):
8248
8478
  )
8249
8479
 
8250
8480
 
8481
+
8251
8482
  class BranchDialog(QDialog):
8252
8483
 
8253
8484
  def __init__(self, parent=None):
@@ -8275,8 +8506,7 @@ class BranchDialog(QDialog):
8275
8506
  self.fix.setChecked(False)
8276
8507
  layout.addRow("Attempt to auto-correct branch labels:", self.fix)
8277
8508
 
8278
- self.fix_val = QLineEdit()
8279
- self.fix_val.setPlaceholderText("Empty = default value...")
8509
+ self.fix_val = QLineEdit('4')
8280
8510
  layout.addRow("If checked above - Avg Degree of Nearby Branch Communities to Merge (Attempt to fix branch labeling - try 4 to 6 to start or leave empty):", self.fix_val)
8281
8511
 
8282
8512
  self.down_factor = QLineEdit("0")
@@ -8718,15 +8948,23 @@ class CentroidDialog(QDialog):
8718
8948
  my_network.save_edge_centroids(directory = directory)
8719
8949
 
8720
8950
  elif chan == 0:
8721
- my_network.calculate_node_centroids(
8722
- down_factor = downsample
8723
- )
8724
- my_network.save_node_centroids(directory = directory)
8951
+ try:
8952
+ my_network.calculate_node_centroids(
8953
+ down_factor = downsample
8954
+ )
8955
+ my_network.save_node_centroids(directory = directory)
8956
+ except:
8957
+ pass
8725
8958
 
8726
- my_network.calculate_edge_centroids(
8727
- down_factor = downsample
8728
- )
8729
- my_network.save_edge_centroids(directory = directory)
8959
+ try:
8960
+
8961
+ my_network.calculate_edge_centroids(
8962
+ down_factor = downsample
8963
+ )
8964
+ my_network.save_edge_centroids(directory = directory)
8965
+
8966
+ except:
8967
+ pass
8730
8968
 
8731
8969
  if hasattr(my_network, 'node_centroids') and my_network.node_centroids is not None:
8732
8970
  try:
@@ -1024,11 +1024,8 @@ def histogram(counts, y_vals, directory = None):
1024
1024
  plt.ylabel('Avg Number of Neigbhoring Vertices')
1025
1025
 
1026
1026
  try:
1027
-
1028
1027
  if directory is not None:
1029
1028
  plt.savefig(f'{directory}/radial_plot.png')
1030
- else:
1031
- plt.savefig('radial_plot.png')
1032
1029
  except:
1033
1030
  pass
1034
1031
 
@@ -1439,6 +1436,7 @@ def degree_distribution(G, directory = None):
1439
1436
 
1440
1437
  def power_trendline(x, y, directory = None):
1441
1438
  # Handle zeros in y for logarithmic transformations
1439
+ """
1442
1440
  y = np.array(y)
1443
1441
  x = np.array(x)
1444
1442
  y[y == 0] += 0.001
@@ -1460,6 +1458,8 @@ def power_trendline(x, y, directory = None):
1460
1458
  ss_res = np.sum((y - y_pred) ** 2)
1461
1459
  ss_tot = np.sum((y - np.mean(y)) ** 2)
1462
1460
  r2 = 1 - (ss_res / ss_tot)
1461
+ """
1462
+ # ^ I commented out this power trendline stuff because I decided I no longer want it to do that so.
1463
1463
 
1464
1464
  # Create a scatterplot
1465
1465
  plt.scatter(x, y, label='Data')
@@ -1468,9 +1468,10 @@ def power_trendline(x, y, directory = None):
1468
1468
  plt.title('Degree Distribution of Network')
1469
1469
 
1470
1470
  # Plot the power trendline
1471
- plt.plot(x_fit, y_fit, color='red', label=f'Power Trendline: $y = {a:.2f}x^{{{b:.2f}}}$')
1471
+ #plt.plot(x_fit, y_fit, color='red', label=f'Power Trendline: $y = {a:.2f}x^{{{b:.2f}}}$')
1472
1472
 
1473
1473
  # Annotate the plot with the trendline equation and R-squared value
1474
+ """
1474
1475
  plt.text(
1475
1476
  0.05, 0.95,
1476
1477
  f'$y = {a:.2f}x^{{{b:.2f}}}$\n$R^2 = {r2:.2f}$',
@@ -1478,13 +1479,12 @@ def power_trendline(x, y, directory = None):
1478
1479
  fontsize=12,
1479
1480
  verticalalignment='top'
1480
1481
  )
1482
+ """
1481
1483
 
1482
1484
  try:
1483
1485
 
1484
1486
  if directory is not None:
1485
1487
  plt.savefig(f'{directory}/degree_plot.png')
1486
- else:
1487
- plt.savefig('degree_plot.png')
1488
1488
  except:
1489
1489
  pass
1490
1490
 
@@ -9,11 +9,9 @@ import math
9
9
  import re
10
10
  from . import nettracer
11
11
  import multiprocessing as mp
12
- from skimage.feature import peak_local_max
13
12
  try:
14
13
  import cupy as cp
15
14
  import cupyx.scipy.ndimage as cpx
16
- from cupyx.scipy.ndimage import maximum_filter
17
15
  except:
18
16
  pass
19
17
 
@@ -467,7 +465,7 @@ def compute_distance_transform_distance_GPU(nodes):
467
465
  nodes_cp = cp.asarray(nodes)
468
466
 
469
467
  # Compute the distance transform on the GPU
470
- distance, _ = cpx.distance_transform_edt(nodes_cp, return_indices=True)
468
+ distance = cpx.distance_transform_edt(nodes_cp)
471
469
 
472
470
  # Convert results back to numpy arrays
473
471
  distance = cp.asnumpy(distance)
@@ -485,7 +483,7 @@ def compute_distance_transform_distance(nodes):
485
483
  nodes = np.squeeze(nodes) # Convert to 2D for processing
486
484
 
487
485
  # Fallback to CPU if there's an issue with GPU computation
488
- distance, _ = distance_transform_edt(nodes, return_indices=True)
486
+ distance = distance_transform_edt(nodes)
489
487
  if is_pseudo_3d:
490
488
  np.expand_dims(distance, axis = 0)
491
489
  return distance
@@ -519,33 +517,6 @@ def gaussian(search_region, GPU = True):
519
517
  blurred_search = gaussian_filter(search_region, sigma = 1)
520
518
  return blurred_search
521
519
 
522
- def get_local_maxima(distance, image):
523
- try:
524
- if cp.cuda.runtime.getDeviceCount() > 0:
525
- print("GPU detected. Using CuPy for local maxima.")
526
-
527
- distance = cp.asarray(distance)
528
-
529
- # Perform a maximum filter to find local maxima
530
- footprint = cp.ones((3, 3, 3)) # Define your footprint
531
- filtered = maximum_filter(distance, footprint=footprint)
532
-
533
- # Find local maxima by comparing with the original array
534
- local_max = (distance == filtered) # Peaks are where the filtered result matches the original
535
-
536
- # Extract coordinates of local maxima
537
- coords = cp.argwhere(local_max)
538
- coords = cp.asnumpy(coords)
539
-
540
- return coords
541
- else:
542
- print("No GPU detected. Using CPU for local maxima.")
543
- coords = peak_local_max(distance, footprint=np.ones((3, 3, 3)), labels=image)
544
- return coords
545
- except Exception as e:
546
- print("GPU operation failed or did not detect GPU (cupy must be installed with a CUDA toolkit set up...). Computing CPU local maxima instead.")
547
- coords = peak_local_max(distance, footprint=np.ones((3, 3, 3)), labels=image)
548
- return coords
549
520
 
550
521
 
551
522
  def catch_memory(e):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nettracer3d
3
- Version: 0.6.3
3
+ Version: 0.6.5
4
4
  Summary: Scripts for intializing and analyzing networks from segmentations of three dimensional images.
5
5
  Author-email: Liam McLaughlin <mclaughlinliam99@gmail.com>
6
6
  Project-URL: User_Tutorial, https://www.youtube.com/watch?v=cRatn5VTWDY
@@ -27,6 +27,7 @@ Requires-Dist: qtrangeslider==0.1.5
27
27
  Requires-Dist: PyQt6==6.8.0
28
28
  Requires-Dist: scikit-learn==1.6.1
29
29
  Requires-Dist: nibabel==5.2.0
30
+ Requires-Dist: setuptools>=65.0.0
30
31
  Provides-Extra: cuda11
31
32
  Requires-Dist: cupy-cuda11x; extra == "cuda11"
32
33
  Provides-Extra: cuda12
@@ -45,10 +46,12 @@ NetTracer3D is free to use/fork for academic/nonprofit use so long as citation i
45
46
 
46
47
  NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
47
48
 
48
- -- Version 0.6.3 updates --
49
+ -- Version 0.6.5 updates --
49
50
 
50
- 1. Fixed bug with the active channel indicator on the GUI not always updating when the active channel was changed by internal loading.
51
+ 1. Added new method for obtaining radii of labeled objects (analyze -> stats -> calculate radii)
51
52
 
52
- 2. Updated ram_lock mode in the segmenter to garbage collect better... again.
53
+ 2. Updated functionality of split nontouching labels function to handle situations where said label is touching other labeled objects in space.
53
54
 
54
- 3. Added new function (Label neighborhoods). Find it in process -> image -> label neighborhoods. Allows a 3d labeled array to act as a kernel and extend its labels along a secondary binary array. This can be useful in evaluating nearest neighbors en-masse. The method behind this (smart label) was previously used internally for some methods (ie watershedding) but I thought I'd include it as an actual function since it has some more general uses.
55
+ 3. Image -> Select Objects will now navigate to the first selected object in the array for user, allowing it to be used to also find whatever labeled object.
56
+
57
+ 4. Minor bug fixes/improvements.
@@ -1,20 +1,20 @@
1
1
  nettracer3d/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  nettracer3d/community_extractor.py,sha256=Zq8ZM595CTzeR6zLEZ4I6KvhkNfCPUReWvAKxTlaVfk,33495
3
3
  nettracer3d/modularity.py,sha256=V1f3s_vGd8EuVz27mzq6ycIGr0BWIpH7c7NU4QjgAHU,30247
4
- nettracer3d/morphology.py,sha256=yQ0GuieMVXOQpaohZlPnkEXEuCUjf8Fg352axyK8nbM,10755
5
- nettracer3d/nettracer.py,sha256=AS3r7Wg3MWv-FARApRCrzyuzb0l0_h944EfXFI75lng,207912
6
- nettracer3d/nettracer_gui.py,sha256=bTVauxgJXEF1Ufay3gXCqjy5J5DAHMnt5MlviSRzn2A,383537
7
- nettracer3d/network_analysis.py,sha256=MJBBjslA1k_R8ymid77U-qGSgzxFVfzGVQhE0IdhnbE,48046
4
+ nettracer3d/morphology.py,sha256=yncUj04Noj_mcdJze4qMfYw-21AbebwiIcu1bDWGgCM,17778
5
+ nettracer3d/nettracer.py,sha256=vsEDFlsz7WtWdkaK_uJXjbA_QzJRYW9pAQgLiAZOJKc,209987
6
+ nettracer3d/nettracer_gui.py,sha256=492wQV_3nIAPPJRClXA-NssaCvx9H6GDMg6yaiL-BeM,393682
7
+ nettracer3d/network_analysis.py,sha256=q1q7lxtA3lebxitfC_jfiT9cnpYXJw4q0Oy2_-Aj8qE,48068
8
8
  nettracer3d/network_draw.py,sha256=F7fw6Pcf4qWOhdKwLmhwqWdschbDlHzwCVolQC9imeU,14117
9
9
  nettracer3d/node_draw.py,sha256=k3sCTfUCJs3aH1C1q1gTNxDz9EAQbBd1hsUIJajxRx8,9823
10
10
  nettracer3d/proximity.py,sha256=FnIiI_AzfXd22HwCIFIyQRZxKYJ8YscIDdPnIv-wsO4,10560
11
11
  nettracer3d/run.py,sha256=xYeaAc8FCx8MuzTGyL3NR3mK7WZzffAYAH23bNRZYO4,127
12
12
  nettracer3d/segmenter.py,sha256=oKQEKQpo3o6cqfN6Z_IAgx8V-HXpegQNjfWFz3Bdu04,83449
13
13
  nettracer3d/simple_network.py,sha256=fP1gkDdtQcHruEZpUdasKdZeVacoLOxKhR3bY0L1CAQ,15426
14
- nettracer3d/smart_dilate.py,sha256=6m03KHtRMv0zfJ2aHc1Om4Fhh2abPy-IqDzRCmIEHCY,24588
15
- nettracer3d-0.6.3.dist-info/licenses/LICENSE,sha256=gM207DhJjWrxLuEWXl0Qz5ISbtWDmADfjHp3yC2XISs,888
16
- nettracer3d-0.6.3.dist-info/METADATA,sha256=qTU9_FA2xsowZID-6_tB7kMJ2cQH8EkAipAKaEu0VzU,3668
17
- nettracer3d-0.6.3.dist-info/WHEEL,sha256=tTnHoFhvKQHCh4jz3yCn0WPTYIy7wXx3CJtJ7SJGV7c,91
18
- nettracer3d-0.6.3.dist-info/entry_points.txt,sha256=Nx1rr_0QhJXDBHAQg2vcqCzLMKBzSHfwy3xwGkueVyc,53
19
- nettracer3d-0.6.3.dist-info/top_level.txt,sha256=zsYy9rZwirfCEOubolhee4TyzqBAL5gSUeFMzhFTX8c,12
20
- nettracer3d-0.6.3.dist-info/RECORD,,
14
+ nettracer3d/smart_dilate.py,sha256=vnBj2soDGVBioKaNQi-bcyAtg0nuWcNGmlrzUNFFYQE,23191
15
+ nettracer3d-0.6.5.dist-info/licenses/LICENSE,sha256=gM207DhJjWrxLuEWXl0Qz5ISbtWDmADfjHp3yC2XISs,888
16
+ nettracer3d-0.6.5.dist-info/METADATA,sha256=q1Qp-eXkzhqVVG1A1OkPQeOVqMLbHwx7GdpI1fm_6NA,3476
17
+ nettracer3d-0.6.5.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
18
+ nettracer3d-0.6.5.dist-info/entry_points.txt,sha256=Nx1rr_0QhJXDBHAQg2vcqCzLMKBzSHfwy3xwGkueVyc,53
19
+ nettracer3d-0.6.5.dist-info/top_level.txt,sha256=zsYy9rZwirfCEOubolhee4TyzqBAL5gSUeFMzhFTX8c,12
20
+ nettracer3d-0.6.5.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (77.0.1)
2
+ Generator: setuptools (78.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5