nettracer3d 0.6.3__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.3/src/nettracer3d.egg-info → nettracer3d-0.6.5}/PKG-INFO +8 -5
  2. {nettracer3d-0.6.3 → nettracer3d-0.6.5}/README.md +6 -4
  3. {nettracer3d-0.6.3 → nettracer3d-0.6.5}/pyproject.toml +4 -2
  4. {nettracer3d-0.6.3 → nettracer3d-0.6.5}/src/nettracer3d/morphology.py +207 -5
  5. {nettracer3d-0.6.3 → nettracer3d-0.6.5}/src/nettracer3d/nettracer.py +82 -12
  6. {nettracer3d-0.6.3 → nettracer3d-0.6.5}/src/nettracer3d/nettracer_gui.py +330 -92
  7. {nettracer3d-0.6.3 → nettracer3d-0.6.5}/src/nettracer3d/network_analysis.py +6 -6
  8. {nettracer3d-0.6.3 → nettracer3d-0.6.5}/src/nettracer3d/smart_dilate.py +2 -31
  9. {nettracer3d-0.6.3 → nettracer3d-0.6.5/src/nettracer3d.egg-info}/PKG-INFO +8 -5
  10. {nettracer3d-0.6.3 → nettracer3d-0.6.5}/src/nettracer3d.egg-info/requires.txt +1 -0
  11. {nettracer3d-0.6.3 → nettracer3d-0.6.5}/LICENSE +0 -0
  12. {nettracer3d-0.6.3 → nettracer3d-0.6.5}/setup.cfg +0 -0
  13. {nettracer3d-0.6.3 → nettracer3d-0.6.5}/src/nettracer3d/__init__.py +0 -0
  14. {nettracer3d-0.6.3 → nettracer3d-0.6.5}/src/nettracer3d/community_extractor.py +0 -0
  15. {nettracer3d-0.6.3 → nettracer3d-0.6.5}/src/nettracer3d/modularity.py +0 -0
  16. {nettracer3d-0.6.3 → nettracer3d-0.6.5}/src/nettracer3d/network_draw.py +0 -0
  17. {nettracer3d-0.6.3 → nettracer3d-0.6.5}/src/nettracer3d/node_draw.py +0 -0
  18. {nettracer3d-0.6.3 → nettracer3d-0.6.5}/src/nettracer3d/proximity.py +0 -0
  19. {nettracer3d-0.6.3 → nettracer3d-0.6.5}/src/nettracer3d/run.py +0 -0
  20. {nettracer3d-0.6.3 → nettracer3d-0.6.5}/src/nettracer3d/segmenter.py +0 -0
  21. {nettracer3d-0.6.3 → nettracer3d-0.6.5}/src/nettracer3d/simple_network.py +0 -0
  22. {nettracer3d-0.6.3 → nettracer3d-0.6.5}/src/nettracer3d.egg-info/SOURCES.txt +0 -0
  23. {nettracer3d-0.6.3 → nettracer3d-0.6.5}/src/nettracer3d.egg-info/dependency_links.txt +0 -0
  24. {nettracer3d-0.6.3 → nettracer3d-0.6.5}/src/nettracer3d.egg-info/entry_points.txt +0 -0
  25. {nettracer3d-0.6.3 → 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.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.
@@ -8,10 +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.3 updates --
11
+ -- Version 0.6.5 updates --
12
12
 
13
- 1. Fixed bug with the active channel indicator on the GUI not always updating when the active channel was changed by internal loading.
13
+ 1. Added new method for obtaining radii of labeled objects (analyze -> stats -> calculate radii)
14
14
 
15
- 2. Updated ram_lock mode in the segmenter to garbage collect better... again.
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. 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.
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
+
19
+ 4. Minor bug fixes/improvements.
@@ -1,10 +1,11 @@
1
1
  [project]
2
2
  name = "nettracer3d"
3
- version = "0.6.3"
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
 
@@ -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