nettracer3d 0.6.4__tar.gz → 0.6.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. {nettracer3d-0.6.4/src/nettracer3d.egg-info → nettracer3d-0.6.5}/PKG-INFO +7 -9
  2. {nettracer3d-0.6.4 → nettracer3d-0.6.5}/README.md +5 -8
  3. {nettracer3d-0.6.4 → nettracer3d-0.6.5}/pyproject.toml +4 -2
  4. {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d/morphology.py +207 -5
  5. {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d/nettracer.py +37 -0
  6. {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d/nettracer_gui.py +267 -69
  7. {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d/smart_dilate.py +2 -31
  8. {nettracer3d-0.6.4 → nettracer3d-0.6.5/src/nettracer3d.egg-info}/PKG-INFO +7 -9
  9. {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d.egg-info/requires.txt +1 -0
  10. {nettracer3d-0.6.4 → nettracer3d-0.6.5}/LICENSE +0 -0
  11. {nettracer3d-0.6.4 → nettracer3d-0.6.5}/setup.cfg +0 -0
  12. {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d/__init__.py +0 -0
  13. {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d/community_extractor.py +0 -0
  14. {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d/modularity.py +0 -0
  15. {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d/network_analysis.py +0 -0
  16. {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d/network_draw.py +0 -0
  17. {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d/node_draw.py +0 -0
  18. {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d/proximity.py +0 -0
  19. {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d/run.py +0 -0
  20. {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d/segmenter.py +0 -0
  21. {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d/simple_network.py +0 -0
  22. {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d.egg-info/SOURCES.txt +0 -0
  23. {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d.egg-info/dependency_links.txt +0 -0
  24. {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d.egg-info/entry_points.txt +0 -0
  25. {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nettracer3d
3
- Version: 0.6.4
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,15 +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.4 updates --
49
+ -- Version 0.6.5 updates --
49
50
 
50
- 1. Fixed bug with tabulated data in top right having a right click window corresponding to the bottom left.
51
+ 1. Added new method for obtaining radii of labeled objects (analyze -> stats -> calculate radii)
51
52
 
52
- 2. Fixed bug when converting communities to nodes that let the same community connect to itself in the new network.
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. Removed attempted trendline fitting from degree distribution
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.
55
56
 
56
- 4. Added new feature to skeletonization (and corresponding branch labeler/gennodes)
57
- Now you can have the program attempt to auto-correct 3D skeletonization loop artifacts through a method that just runs the 3d fill holes algo and then attempts to reskeletonize the output. This worked well in my own testing.
58
-
59
- 5. Other minor fixes/improvements
57
+ 4. Minor bug fixes/improvements.
@@ -8,15 +8,12 @@ NetTracer3D is free to use/fork for academic/nonprofit use so long as citation i
8
8
 
9
9
  NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
10
10
 
11
- -- Version 0.6.4 updates --
11
+ -- Version 0.6.5 updates --
12
12
 
13
- 1. Fixed bug with tabulated data in top right having a right click window corresponding to the bottom left.
13
+ 1. Added new method for obtaining radii of labeled objects (analyze -> stats -> calculate radii)
14
14
 
15
- 2. Fixed bug when converting communities to nodes that let the same community connect to itself in the new network.
15
+ 2. Updated functionality of split nontouching labels function to handle situations where said label is touching other labeled objects in space.
16
16
 
17
- 3. Removed attempted trendline fitting from degree distribution
17
+ 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.
18
18
 
19
- 4. Added new feature to skeletonization (and corresponding branch labeler/gennodes)
20
- Now you can have the program attempt to auto-correct 3D skeletonization loop artifacts through a method that just runs the 3d fill holes algo and then attempts to reskeletonize the output. This worked well in my own testing.
21
-
22
- 5. Other minor fixes/improvements
19
+ 4. Minor bug fixes/improvements.
@@ -1,10 +1,11 @@
1
1
  [project]
2
2
  name = "nettracer3d"
3
- version = "0.6.4"
3
+ version = "0.6.5"
4
4
  authors = [
5
5
  { name="Liam McLaughlin", email="mclaughlinliam99@gmail.com" },
6
6
  ]
7
7
  description = "Scripts for intializing and analyzing networks from segmentations of three dimensional images."
8
+
8
9
  dependencies = [
9
10
  "numpy == 1.26.4",
10
11
  "scipy == 1.14.1",
@@ -21,7 +22,8 @@ dependencies = [
21
22
  "qtrangeslider == 0.1.5",
22
23
  "PyQt6 == 6.8.0",
23
24
  "scikit-learn == 1.6.1",
24
- "nibabel == 5.2.0"
25
+ "nibabel == 5.2.0",
26
+ "setuptools >= 65.0.0"
25
27
  ]
26
28
 
27
29
  readme = "README.md"
@@ -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
@@ -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
 
@@ -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
 
@@ -2392,6 +2465,8 @@ class ImageViewerWindow(QMainWindow):
2392
2465
  random_action.triggered.connect(self.show_random_dialog)
2393
2466
  vol_action = stats_menu.addAction("Calculate Volumes")
2394
2467
  vol_action.triggered.connect(self.volumes)
2468
+ rad_action = stats_menu.addAction("Calculate Radii")
2469
+ rad_action.triggered.connect(self.show_rad_dialog)
2395
2470
  inter_action = stats_menu.addAction("Calculate Node < > Edge Interaction")
2396
2471
  inter_action.triggered.connect(self.show_interaction_dialog)
2397
2472
  overlay_menu = analysis_menu.addMenu("Data/Overlays")
@@ -3312,8 +3387,7 @@ class ImageViewerWindow(QMainWindow):
3312
3387
 
3313
3388
 
3314
3389
  except Exception as e:
3315
- import traceback
3316
- print(traceback.format_exc())
3390
+
3317
3391
  if not data:
3318
3392
  from PyQt6.QtWidgets import QMessageBox
3319
3393
  QMessageBox.critical(
@@ -3825,6 +3899,9 @@ class ImageViewerWindow(QMainWindow):
3825
3899
  dialog = RandomDialog(self)
3826
3900
  dialog.exec()
3827
3901
 
3902
+ def show_rad_dialog(self):
3903
+ dialog = RadDialog(self)
3904
+ dialog.exec()
3828
3905
 
3829
3906
  def show_interaction_dialog(self):
3830
3907
  dialog = InteractionDialog(self)
@@ -4241,16 +4318,21 @@ class CustomTableView(QTableView):
4241
4318
  self.parent.clicked_values['edges'] = []
4242
4319
  self.parent.clicked_values['nodes'].append(value)
4243
4320
 
4244
- # Highlight the value in both tables if it exists
4245
- self.highlight_value_in_table(self.parent.network_table, value, column)
4246
- 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
4247
4327
  else:
4248
4328
  print(f"Node {value} not found in centroids dictionary")
4249
4329
 
4250
4330
  elif column == 2: # Third column is edges
4331
+ if my_network.edge_centroids is None:
4332
+ self.parent.show_centroid_dialog()
4333
+
4251
4334
  if value in my_network.edge_centroids:
4252
- if my_network.edge_centroids is None:
4253
- self.parent.show_centroid_dialog()
4335
+
4254
4336
  # Get centroid coordinates (Z, Y, X)
4255
4337
  centroid = my_network.edge_centroids[value]
4256
4338
  # Set the active channel to edges (1)
@@ -4271,9 +4353,12 @@ class CustomTableView(QTableView):
4271
4353
  self.parent.clicked_values['edges'] = []
4272
4354
  self.parent.clicked_values['edges'].append(value)
4273
4355
 
4274
- # Highlight the value in both tables if it exists
4275
- self.highlight_value_in_table(self.parent.network_table, value, column)
4276
- 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
4277
4362
  else:
4278
4363
  print(f"Edge {value} not found in centroids dictionary")
4279
4364
  else: #If highlighting paired elements
@@ -4905,6 +4990,52 @@ class ArbitraryDialog(QDialog):
4905
4990
  continue
4906
4991
 
4907
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")
4908
5039
 
4909
5040
  def process_selections(self):
4910
5041
  """Process the selection and deselection inputs."""
@@ -4940,11 +5071,21 @@ class ArbitraryDialog(QDialog):
4940
5071
  except:
4941
5072
  pass #Forgive mistakes
4942
5073
 
4943
- for item in select_list:
4944
- try:
4945
- self.parent().clicked_values[mode].append(item)
4946
- except:
4947
- 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
4948
5089
 
4949
5090
  self.parent().clicked_values[mode] = list(set(self.parent().clicked_values[mode]))
4950
5091
 
@@ -4964,6 +5105,8 @@ class ArbitraryDialog(QDialog):
4964
5105
 
4965
5106
  except Exception as e:
4966
5107
  QMessageBox.critical(self, "Error", f"Error processing selections: {str(e)}")
5108
+ import traceback
5109
+ print(traceback.format_exc())
4967
5110
 
4968
5111
  class Show3dDialog(QDialog):
4969
5112
  def __init__(self, parent=None):
@@ -5257,8 +5400,10 @@ class ShuffleDialog(QDialog):
5257
5400
 
5258
5401
  try:
5259
5402
  if accepted_mode == 4:
5260
-
5261
- 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
5262
5407
  else:
5263
5408
  self.parent().load_channel(accepted_mode, channel_data = target_data, data = True)
5264
5409
  except:
@@ -5271,11 +5416,12 @@ class ShuffleDialog(QDialog):
5271
5416
  self.parent().highlight_overlay = n3d.binarize(active_data)
5272
5417
  except:
5273
5418
  self.parent().highlight_overlay = None
5419
+ else:
5420
+ self.parent().load_channel(accepted_target, channel_data = active_data, data = True)
5274
5421
  except:
5275
5422
  pass
5276
5423
 
5277
- else:
5278
- self.parent().load_channel(accepted_target, channel_data = active_data, data = True)
5424
+
5279
5425
 
5280
5426
 
5281
5427
  self.parent().update_display()
@@ -5604,6 +5750,50 @@ class RandomDialog(QDialog):
5604
5750
 
5605
5751
  self.accept()
5606
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
+
5607
5797
 
5608
5798
 
5609
5799
  class InteractionDialog(QDialog):
@@ -6318,7 +6508,7 @@ class SLabelDialog(QDialog):
6318
6508
  # GPU checkbox (default True)
6319
6509
  self.GPU = QPushButton("GPU")
6320
6510
  self.GPU.setCheckable(True)
6321
- self.GPU.setChecked(True)
6511
+ self.GPU.setChecked(False)
6322
6512
  layout.addRow("Use GPU:", self.GPU)
6323
6513
 
6324
6514
  self.down_factor = QLineEdit("")
@@ -7387,7 +7577,7 @@ class SmartDilateDialog(QDialog):
7387
7577
  # GPU checkbox (default True)
7388
7578
  self.GPU = QPushButton("GPU")
7389
7579
  self.GPU.setCheckable(True)
7390
- self.GPU.setChecked(True)
7580
+ self.GPU.setChecked(False)
7391
7581
  layout.addRow("Use GPU:", self.GPU)
7392
7582
 
7393
7583
  self.down_factor = QLineEdit("")
@@ -7889,7 +8079,7 @@ class WatershedDialog(QDialog):
7889
8079
  # GPU checkbox (default True)
7890
8080
  self.gpu = QPushButton("GPU")
7891
8081
  self.gpu.setCheckable(True)
7892
- self.gpu.setChecked(True)
8082
+ self.gpu.setChecked(False)
7893
8083
  layout.addRow("Use GPU:", self.gpu)
7894
8084
 
7895
8085
  # Smallest radius (empty by default)
@@ -8758,15 +8948,23 @@ class CentroidDialog(QDialog):
8758
8948
  my_network.save_edge_centroids(directory = directory)
8759
8949
 
8760
8950
  elif chan == 0:
8761
- my_network.calculate_node_centroids(
8762
- down_factor = downsample
8763
- )
8764
- 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
8765
8958
 
8766
- my_network.calculate_edge_centroids(
8767
- down_factor = downsample
8768
- )
8769
- 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
8770
8968
 
8771
8969
  if hasattr(my_network, 'node_centroids') and my_network.node_centroids is not None:
8772
8970
  try:
@@ -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.4
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,15 +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.4 updates --
49
+ -- Version 0.6.5 updates --
49
50
 
50
- 1. Fixed bug with tabulated data in top right having a right click window corresponding to the bottom left.
51
+ 1. Added new method for obtaining radii of labeled objects (analyze -> stats -> calculate radii)
51
52
 
52
- 2. Fixed bug when converting communities to nodes that let the same community connect to itself in the new network.
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. Removed attempted trendline fitting from degree distribution
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.
55
56
 
56
- 4. Added new feature to skeletonization (and corresponding branch labeler/gennodes)
57
- Now you can have the program attempt to auto-correct 3D skeletonization loop artifacts through a method that just runs the 3d fill holes algo and then attempts to reskeletonize the output. This worked well in my own testing.
58
-
59
- 5. Other minor fixes/improvements
57
+ 4. Minor bug fixes/improvements.
@@ -14,6 +14,7 @@ qtrangeslider==0.1.5
14
14
  PyQt6==6.8.0
15
15
  scikit-learn==1.6.1
16
16
  nibabel==5.2.0
17
+ setuptools>=65.0.0
17
18
 
18
19
  [CUDA11]
19
20
  cupy-cuda11x
File without changes
File without changes