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.
- {nettracer3d-0.6.4/src/nettracer3d.egg-info → nettracer3d-0.6.5}/PKG-INFO +7 -9
- {nettracer3d-0.6.4 → nettracer3d-0.6.5}/README.md +5 -8
- {nettracer3d-0.6.4 → nettracer3d-0.6.5}/pyproject.toml +4 -2
- {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d/morphology.py +207 -5
- {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d/nettracer.py +37 -0
- {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d/nettracer_gui.py +267 -69
- {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d/smart_dilate.py +2 -31
- {nettracer3d-0.6.4 → nettracer3d-0.6.5/src/nettracer3d.egg-info}/PKG-INFO +7 -9
- {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d.egg-info/requires.txt +1 -0
- {nettracer3d-0.6.4 → nettracer3d-0.6.5}/LICENSE +0 -0
- {nettracer3d-0.6.4 → nettracer3d-0.6.5}/setup.cfg +0 -0
- {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d/__init__.py +0 -0
- {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d/community_extractor.py +0 -0
- {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d/modularity.py +0 -0
- {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d/network_analysis.py +0 -0
- {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d/network_draw.py +0 -0
- {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d/node_draw.py +0 -0
- {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d/proximity.py +0 -0
- {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d/run.py +0 -0
- {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d/segmenter.py +0 -0
- {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d/simple_network.py +0 -0
- {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d.egg-info/SOURCES.txt +0 -0
- {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d.egg-info/dependency_links.txt +0 -0
- {nettracer3d-0.6.4 → nettracer3d-0.6.5}/src/nettracer3d.egg-info/entry_points.txt +0 -0
- {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.
|
|
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.
|
|
49
|
+
-- Version 0.6.5 updates --
|
|
49
50
|
|
|
50
|
-
1.
|
|
51
|
+
1. Added new method for obtaining radii of labeled objects (analyze -> stats -> calculate radii)
|
|
51
52
|
|
|
52
|
-
2.
|
|
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.
|
|
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.
|
|
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.
|
|
11
|
+
-- Version 0.6.5 updates --
|
|
12
12
|
|
|
13
|
-
1.
|
|
13
|
+
1. Added new method for obtaining radii of labeled objects (analyze -> stats -> calculate radii)
|
|
14
14
|
|
|
15
|
-
2.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
1407
|
-
max_val = np.max(my_network.nodes)
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
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
|
|
1427
|
-
max_val = np.max(my_network.edges)
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
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
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
8762
|
-
|
|
8763
|
-
|
|
8764
|
-
|
|
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
|
-
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
|
|
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
|
|
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
|
|
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
|
+
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.
|
|
49
|
+
-- Version 0.6.5 updates --
|
|
49
50
|
|
|
50
|
-
1.
|
|
51
|
+
1. Added new method for obtaining radii of labeled objects (analyze -> stats -> calculate radii)
|
|
51
52
|
|
|
52
|
-
2.
|
|
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.
|
|
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.
|
|
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.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|