nettracer3d 0.6.3__py3-none-any.whl → 0.6.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nettracer3d/morphology.py +207 -5
- nettracer3d/nettracer.py +82 -12
- nettracer3d/nettracer_gui.py +330 -92
- nettracer3d/network_analysis.py +6 -6
- nettracer3d/smart_dilate.py +2 -31
- {nettracer3d-0.6.3.dist-info → nettracer3d-0.6.5.dist-info}/METADATA +8 -5
- {nettracer3d-0.6.3.dist-info → nettracer3d-0.6.5.dist-info}/RECORD +11 -11
- {nettracer3d-0.6.3.dist-info → nettracer3d-0.6.5.dist-info}/WHEEL +1 -1
- {nettracer3d-0.6.3.dist-info → nettracer3d-0.6.5.dist-info}/entry_points.txt +0 -0
- {nettracer3d-0.6.3.dist-info → nettracer3d-0.6.5.dist-info}/licenses/LICENSE +0 -0
- {nettracer3d-0.6.3.dist-info → nettracer3d-0.6.5.dist-info}/top_level.txt +0 -0
nettracer3d/morphology.py
CHANGED
|
@@ -6,8 +6,17 @@ import multiprocessing as mp
|
|
|
6
6
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
7
7
|
import tifffile
|
|
8
8
|
from functools import partial
|
|
9
|
-
import
|
|
9
|
+
import concurrent.futures
|
|
10
|
+
from functools import partial
|
|
10
11
|
from scipy import ndimage
|
|
12
|
+
import pandas as pd
|
|
13
|
+
# Import CuPy conditionally for GPU support
|
|
14
|
+
try:
|
|
15
|
+
import cupy as cp
|
|
16
|
+
import cupyx.scipy.ndimage as cpx
|
|
17
|
+
HAS_CUPY = True
|
|
18
|
+
except ImportError:
|
|
19
|
+
HAS_CUPY = False
|
|
11
20
|
|
|
12
21
|
def get_reslice_indices(slice_obj, dilate_xy, dilate_z, array_shape):
|
|
13
22
|
"""Convert slice object to padded indices accounting for dilation and boundaries"""
|
|
@@ -279,9 +288,6 @@ def search_neighbor_ids(nodes, targets, id_dict, neighborhood_dict, totals, sear
|
|
|
279
288
|
|
|
280
289
|
|
|
281
290
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
291
|
def get_search_space_dilate(target, centroids, id_dict, search, scaling = 1):
|
|
286
292
|
|
|
287
293
|
ymax = np.max(centroids[:, 0])
|
|
@@ -308,4 +314,200 @@ def get_search_space_dilate(target, centroids, id_dict, search, scaling = 1):
|
|
|
308
314
|
|
|
309
315
|
|
|
310
316
|
|
|
311
|
-
return array
|
|
317
|
+
return array
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# Methods pertaining to getting radii:
|
|
321
|
+
|
|
322
|
+
def process_object_cpu(label, objects, labeled_array):
|
|
323
|
+
"""
|
|
324
|
+
Process a single labeled object to estimate its radius (CPU version).
|
|
325
|
+
This function is designed to be called in parallel.
|
|
326
|
+
|
|
327
|
+
Parameters:
|
|
328
|
+
-----------
|
|
329
|
+
label : int
|
|
330
|
+
The label ID to process
|
|
331
|
+
objects : list
|
|
332
|
+
List of slice objects from ndimage.find_objects
|
|
333
|
+
labeled_array : numpy.ndarray
|
|
334
|
+
The full 3D labeled array
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
--------
|
|
338
|
+
tuple: (label, radius, mask_volume, dimensions)
|
|
339
|
+
"""
|
|
340
|
+
# Get the slice object (bounding box) for this label
|
|
341
|
+
# Index is label-1 because find_objects returns 0-indexed results
|
|
342
|
+
obj_slice = objects[label-1]
|
|
343
|
+
|
|
344
|
+
if obj_slice is None:
|
|
345
|
+
return label, 0, 0, np.array([0, 0, 0])
|
|
346
|
+
|
|
347
|
+
# Extract subarray containing just this object (plus padding)
|
|
348
|
+
# Create padded slices to ensure there's background around the object
|
|
349
|
+
padded_slices = []
|
|
350
|
+
for dim_idx, dim_slice in enumerate(obj_slice):
|
|
351
|
+
start = max(0, dim_slice.start - 1)
|
|
352
|
+
stop = min(labeled_array.shape[dim_idx], dim_slice.stop + 1)
|
|
353
|
+
padded_slices.append(slice(start, stop))
|
|
354
|
+
|
|
355
|
+
# Extract the subarray
|
|
356
|
+
subarray = labeled_array[tuple(padded_slices)]
|
|
357
|
+
|
|
358
|
+
# Create binary mask for this object within the subarray
|
|
359
|
+
mask = (subarray == label)
|
|
360
|
+
|
|
361
|
+
# Compute distance transform on the smaller mask
|
|
362
|
+
dist_transform = compute_distance_transform_distance(mask)
|
|
363
|
+
|
|
364
|
+
# Filter out small values near the edge to focus on more central regions
|
|
365
|
+
radius = np.max(dist_transform)
|
|
366
|
+
|
|
367
|
+
# Calculate basic shape metrics
|
|
368
|
+
volume = np.sum(mask)
|
|
369
|
+
|
|
370
|
+
# Calculate bounding box dimensions
|
|
371
|
+
x_len = obj_slice[0].stop - obj_slice[0].start
|
|
372
|
+
y_len = obj_slice[1].stop - obj_slice[1].start
|
|
373
|
+
z_len = obj_slice[2].stop - obj_slice[2].start
|
|
374
|
+
dimensions = np.array([x_len, y_len, z_len])
|
|
375
|
+
|
|
376
|
+
return label, radius, volume, dimensions
|
|
377
|
+
|
|
378
|
+
def estimate_object_radii_cpu(labeled_array, n_jobs=None):
|
|
379
|
+
"""
|
|
380
|
+
Estimate the radii of labeled objects in a 3D numpy array using distance transform.
|
|
381
|
+
CPU parallel implementation.
|
|
382
|
+
|
|
383
|
+
Parameters:
|
|
384
|
+
-----------
|
|
385
|
+
labeled_array : numpy.ndarray
|
|
386
|
+
3D array where each object has a unique integer label (0 is background)
|
|
387
|
+
n_jobs : int or None
|
|
388
|
+
Number of parallel jobs. If None, uses all available cores.
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
--------
|
|
392
|
+
dict: Dictionary mapping object labels to estimated radii
|
|
393
|
+
dict: (optional) Dictionary of shape statistics for each label
|
|
394
|
+
"""
|
|
395
|
+
# Find bounding box for each labeled object
|
|
396
|
+
objects = ndimage.find_objects(labeled_array)
|
|
397
|
+
|
|
398
|
+
unique_labels = np.unique(labeled_array)
|
|
399
|
+
unique_labels = unique_labels[unique_labels != 0] # Remove background
|
|
400
|
+
|
|
401
|
+
# Create a partial function for parallel processing
|
|
402
|
+
process_func = partial(process_object_cpu, objects=objects, labeled_array=labeled_array)
|
|
403
|
+
|
|
404
|
+
# Process objects in parallel
|
|
405
|
+
results = []
|
|
406
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=n_jobs) as executor:
|
|
407
|
+
# Submit all jobs
|
|
408
|
+
future_to_label = {executor.submit(process_func, label): label for label in unique_labels}
|
|
409
|
+
|
|
410
|
+
# Collect results as they complete
|
|
411
|
+
for future in concurrent.futures.as_completed(future_to_label):
|
|
412
|
+
results.append(future.result())
|
|
413
|
+
|
|
414
|
+
# Organize results
|
|
415
|
+
radii = {}
|
|
416
|
+
|
|
417
|
+
for label, radius, volume, dimensions in results:
|
|
418
|
+
radii[label] = radius
|
|
419
|
+
|
|
420
|
+
return radii
|
|
421
|
+
|
|
422
|
+
def estimate_object_radii_gpu(labeled_array):
|
|
423
|
+
"""
|
|
424
|
+
Estimate the radii of labeled objects in a 3D numpy array using distance transform.
|
|
425
|
+
GPU implementation using CuPy.
|
|
426
|
+
|
|
427
|
+
Parameters:
|
|
428
|
+
-----------
|
|
429
|
+
labeled_array : numpy.ndarray
|
|
430
|
+
3D array where each object has a unique integer label (0 is background)
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
--------
|
|
434
|
+
dict: Dictionary mapping object labels to estimated radii
|
|
435
|
+
dict: (optional) Dictionary of shape statistics for each label
|
|
436
|
+
"""
|
|
437
|
+
|
|
438
|
+
try:
|
|
439
|
+
if not HAS_CUPY:
|
|
440
|
+
raise ImportError("CuPy is required for GPU acceleration")
|
|
441
|
+
|
|
442
|
+
# Find bounding box for each labeled object (on CPU)
|
|
443
|
+
objects = ndimage.find_objects(labeled_array)
|
|
444
|
+
|
|
445
|
+
# Transfer entire labeled array to GPU once
|
|
446
|
+
labeled_array_gpu = cp.asarray(labeled_array)
|
|
447
|
+
|
|
448
|
+
unique_labels = cp.unique(labeled_array_gpu)
|
|
449
|
+
unique_labels = cp.asnumpy(unique_labels)
|
|
450
|
+
unique_labels = unique_labels[unique_labels != 0] # Remove background
|
|
451
|
+
|
|
452
|
+
radii = {}
|
|
453
|
+
|
|
454
|
+
for label in unique_labels:
|
|
455
|
+
# Get the slice object (bounding box) for this label
|
|
456
|
+
obj_slice = objects[label-1]
|
|
457
|
+
|
|
458
|
+
if obj_slice is None:
|
|
459
|
+
continue
|
|
460
|
+
|
|
461
|
+
# Extract subarray from GPU array
|
|
462
|
+
padded_slices = []
|
|
463
|
+
for dim_idx, dim_slice in enumerate(obj_slice):
|
|
464
|
+
start = max(0, dim_slice.start - 1)
|
|
465
|
+
stop = min(labeled_array.shape[dim_idx], dim_slice.stop + 1)
|
|
466
|
+
padded_slices.append(slice(start, stop))
|
|
467
|
+
|
|
468
|
+
# Create binary mask for this object (directly on GPU)
|
|
469
|
+
mask_gpu = (labeled_array_gpu[tuple(padded_slices)] == label)
|
|
470
|
+
|
|
471
|
+
# Compute distance transform on GPU
|
|
472
|
+
dist_transform_gpu = compute_distance_transform_distance_GPU(mask_gpu)
|
|
473
|
+
|
|
474
|
+
radius = float(cp.max(dist_transform_gpu).get())
|
|
475
|
+
|
|
476
|
+
# Store the radius
|
|
477
|
+
radii[label] = radius
|
|
478
|
+
|
|
479
|
+
# Clean up GPU memory
|
|
480
|
+
del labeled_array_gpu
|
|
481
|
+
|
|
482
|
+
return radii
|
|
483
|
+
|
|
484
|
+
except Exception as e:
|
|
485
|
+
print(f"GPU calculation failed, trying CPU instead -> {e}")
|
|
486
|
+
return estimate_object_radii_cpu(labeled_array)
|
|
487
|
+
|
|
488
|
+
def compute_distance_transform_distance_GPU(nodes):
|
|
489
|
+
|
|
490
|
+
is_pseudo_3d = nodes.shape[0] == 1
|
|
491
|
+
if is_pseudo_3d:
|
|
492
|
+
nodes = cp.squeeze(nodes) # Convert to 2D for processing
|
|
493
|
+
|
|
494
|
+
# Compute the distance transform on the GPU
|
|
495
|
+
distance = cpx.distance_transform_edt(nodes)
|
|
496
|
+
|
|
497
|
+
if is_pseudo_3d:
|
|
498
|
+
cp.expand_dims(distance, axis = 0)
|
|
499
|
+
|
|
500
|
+
return distance
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def compute_distance_transform_distance(nodes):
|
|
504
|
+
|
|
505
|
+
is_pseudo_3d = nodes.shape[0] == 1
|
|
506
|
+
if is_pseudo_3d:
|
|
507
|
+
nodes = np.squeeze(nodes) # Convert to 2D for processing
|
|
508
|
+
|
|
509
|
+
# Fallback to CPU if there's an issue with GPU computation
|
|
510
|
+
distance = ndimage.distance_transform_edt(nodes)
|
|
511
|
+
if is_pseudo_3d:
|
|
512
|
+
np.expand_dims(distance, axis = 0)
|
|
513
|
+
return distance
|
nettracer3d/nettracer.py
CHANGED
|
@@ -547,6 +547,43 @@ def remove_branches(skeleton, length):
|
|
|
547
547
|
return image_copy
|
|
548
548
|
|
|
549
549
|
|
|
550
|
+
def estimate_object_radii(labeled_array, gpu=False, n_jobs=None):
|
|
551
|
+
"""
|
|
552
|
+
Estimate the radii of labeled objects in a 3D numpy array.
|
|
553
|
+
Dispatches to appropriate implementation based on parameters.
|
|
554
|
+
|
|
555
|
+
Parameters:
|
|
556
|
+
-----------
|
|
557
|
+
labeled_array : numpy.ndarray
|
|
558
|
+
3D array where each object has a unique integer label (0 is background)
|
|
559
|
+
gpu : bool
|
|
560
|
+
Whether to use GPU acceleration via CuPy (if available)
|
|
561
|
+
n_jobs : int or None
|
|
562
|
+
Number of parallel jobs for CPU version. If None, uses all available cores.
|
|
563
|
+
|
|
564
|
+
Returns:
|
|
565
|
+
--------
|
|
566
|
+
dict: Dictionary mapping object labels to estimated radii
|
|
567
|
+
dict: (optional) Dictionary of shape statistics for each label
|
|
568
|
+
"""
|
|
569
|
+
# Check if GPU is requested but not available
|
|
570
|
+
try:
|
|
571
|
+
import cupy as cp
|
|
572
|
+
import cupyx.scipy.ndimage as cpx
|
|
573
|
+
HAS_CUPY = True
|
|
574
|
+
except ImportError:
|
|
575
|
+
HAS_CUPY = False
|
|
576
|
+
|
|
577
|
+
if gpu and not HAS_CUPY:
|
|
578
|
+
print("Warning: GPU acceleration requested but CuPy not available. Falling back to CPU.")
|
|
579
|
+
gpu = False
|
|
580
|
+
|
|
581
|
+
if gpu:
|
|
582
|
+
return morphology.estimate_object_radii_gpu(labeled_array)
|
|
583
|
+
else:
|
|
584
|
+
return morphology.estimate_object_radii_cpu(labeled_array, n_jobs)
|
|
585
|
+
|
|
586
|
+
|
|
550
587
|
def break_and_label_skeleton(skeleton, peaks = 1, branch_removal = 0, comp_dil = 0, max_vol = 0, directory = None, return_skele = False, nodes = None):
|
|
551
588
|
"""Internal method to break open a skeleton at its branchpoints and label the remaining components, for an 8bit binary array"""
|
|
552
589
|
|
|
@@ -751,6 +788,8 @@ def fill_holes_3d(array):
|
|
|
751
788
|
|
|
752
789
|
return holes_mask
|
|
753
790
|
|
|
791
|
+
print("Filling Holes...")
|
|
792
|
+
|
|
754
793
|
array = binarize(array)
|
|
755
794
|
inv_array = invert_array(array)
|
|
756
795
|
|
|
@@ -3270,7 +3309,10 @@ class Network_3D:
|
|
|
3270
3309
|
self._xy_scale = xy_scale
|
|
3271
3310
|
self._z_scale = z_scale
|
|
3272
3311
|
|
|
3273
|
-
|
|
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
|
-
|
|
3290
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3367
|
+
try:
|
|
3368
|
+
self.save_node_centroids(directory)
|
|
3369
|
+
except:
|
|
3370
|
+
pass
|
|
3310
3371
|
self.calculate_edge_centroids(down_factor)
|
|
3311
|
-
|
|
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]
|
|
3565
|
-
|
|
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 = [
|
|
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
|
nettracer3d/nettracer_gui.py
CHANGED
|
@@ -325,7 +325,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
325
325
|
self.tabbed_data = TabbedDataWidget(self)
|
|
326
326
|
right_layout.addWidget(self.tabbed_data)
|
|
327
327
|
# Initialize data_table property to None - it will be set when tabs are added
|
|
328
|
-
self.data_table =
|
|
328
|
+
self.data_table = []
|
|
329
329
|
|
|
330
330
|
# Create table control panel
|
|
331
331
|
table_control = QWidget()
|
|
@@ -1399,55 +1399,128 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1399
1399
|
print(f"An error has occured: {e}")
|
|
1400
1400
|
|
|
1401
1401
|
def handle_seperate(self):
|
|
1402
|
-
|
|
1402
|
+
print("Note: I search each selected label one at a time and then split it with the ndimage.label method which uses C but still has to search the entire array each time, I may be a very slow with big operations :)")
|
|
1403
1403
|
try:
|
|
1404
|
-
|
|
1404
|
+
# Handle nodes
|
|
1405
1405
|
if len(self.clicked_values['nodes']) > 0:
|
|
1406
|
-
self.create_highlight_overlay(node_indices
|
|
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
|
|
|
@@ -1814,6 +1887,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1814
1887
|
new_xlim = [xdata - x_range, xdata + x_range]
|
|
1815
1888
|
new_ylim = [ydata - y_range, ydata + y_range]
|
|
1816
1889
|
|
|
1890
|
+
|
|
1817
1891
|
if (new_xlim[0] <= self.original_xlim[0] or
|
|
1818
1892
|
new_xlim[1] >= self.original_xlim[1] or
|
|
1819
1893
|
new_ylim[0] <= self.original_ylim[0] or
|
|
@@ -2391,6 +2465,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2391
2465
|
random_action.triggered.connect(self.show_random_dialog)
|
|
2392
2466
|
vol_action = stats_menu.addAction("Calculate Volumes")
|
|
2393
2467
|
vol_action.triggered.connect(self.volumes)
|
|
2468
|
+
rad_action = stats_menu.addAction("Calculate Radii")
|
|
2469
|
+
rad_action.triggered.connect(self.show_rad_dialog)
|
|
2394
2470
|
inter_action = stats_menu.addAction("Calculate Node < > Edge Interaction")
|
|
2395
2471
|
inter_action.triggered.connect(self.show_interaction_dialog)
|
|
2396
2472
|
overlay_menu = analysis_menu.addMenu("Data/Overlays")
|
|
@@ -3228,6 +3304,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3228
3304
|
self.highlight_overlay = None
|
|
3229
3305
|
except:
|
|
3230
3306
|
pass
|
|
3307
|
+
if not data:
|
|
3308
|
+
self.original_xlim = None
|
|
3309
|
+
self.original_ylim = None
|
|
3231
3310
|
continue
|
|
3232
3311
|
else:
|
|
3233
3312
|
old_shape = self.channel_data[i].shape[:3] #Ask user to resize images that are shaped differently
|
|
@@ -3308,8 +3387,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3308
3387
|
|
|
3309
3388
|
|
|
3310
3389
|
except Exception as e:
|
|
3311
|
-
|
|
3312
|
-
print(traceback.format_exc())
|
|
3390
|
+
|
|
3313
3391
|
if not data:
|
|
3314
3392
|
from PyQt6.QtWidgets import QMessageBox
|
|
3315
3393
|
QMessageBox.critical(
|
|
@@ -3821,6 +3899,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3821
3899
|
dialog = RandomDialog(self)
|
|
3822
3900
|
dialog.exec()
|
|
3823
3901
|
|
|
3902
|
+
def show_rad_dialog(self):
|
|
3903
|
+
dialog = RadDialog(self)
|
|
3904
|
+
dialog.exec()
|
|
3824
3905
|
|
|
3825
3906
|
def show_interaction_dialog(self):
|
|
3826
3907
|
dialog = InteractionDialog(self)
|
|
@@ -3987,7 +4068,7 @@ class CustomTableView(QTableView):
|
|
|
3987
4068
|
desc_action.triggered.connect(lambda checked, c=col: self.sort_table(c, ascending=False))
|
|
3988
4069
|
|
|
3989
4070
|
# Different menus for top and bottom tables
|
|
3990
|
-
if self
|
|
4071
|
+
if self in self.parent.data_table: # Top table
|
|
3991
4072
|
save_menu = context_menu.addMenu("Save As")
|
|
3992
4073
|
save_csv = save_menu.addAction("CSV")
|
|
3993
4074
|
save_excel = save_menu.addAction("Excel")
|
|
@@ -4096,7 +4177,7 @@ class CustomTableView(QTableView):
|
|
|
4096
4177
|
df = self.model()._data
|
|
4097
4178
|
|
|
4098
4179
|
# Get table name for the file dialog title
|
|
4099
|
-
if self
|
|
4180
|
+
if self in self.parent.data_table:
|
|
4100
4181
|
table_name = "Statistics"
|
|
4101
4182
|
elif self == self.parent.network_table:
|
|
4102
4183
|
table_name = "Network"
|
|
@@ -4105,7 +4186,7 @@ class CustomTableView(QTableView):
|
|
|
4105
4186
|
|
|
4106
4187
|
# Get save file name
|
|
4107
4188
|
file_filter = ("CSV Files (*.csv)" if file_type == 'csv' else
|
|
4108
|
-
"Excel Files (*.xlsx)" if file_type == '
|
|
4189
|
+
"Excel Files (*.xlsx)" if file_type == 'xlsx' else
|
|
4109
4190
|
"Gephi Graph (*.gexf)" if file_type == 'gexf' else
|
|
4110
4191
|
"GraphML (*.graphml)" if file_type == 'graphml' else
|
|
4111
4192
|
"Pajek Network (*.net)")
|
|
@@ -4237,16 +4318,21 @@ class CustomTableView(QTableView):
|
|
|
4237
4318
|
self.parent.clicked_values['edges'] = []
|
|
4238
4319
|
self.parent.clicked_values['nodes'].append(value)
|
|
4239
4320
|
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4321
|
+
try:
|
|
4322
|
+
# Highlight the value in both tables if it exists
|
|
4323
|
+
self.highlight_value_in_table(self.parent.network_table, value, column)
|
|
4324
|
+
self.highlight_value_in_table(self.parent.selection_table, value, column)
|
|
4325
|
+
except:
|
|
4326
|
+
pass
|
|
4243
4327
|
else:
|
|
4244
4328
|
print(f"Node {value} not found in centroids dictionary")
|
|
4245
4329
|
|
|
4246
4330
|
elif column == 2: # Third column is edges
|
|
4331
|
+
if my_network.edge_centroids is None:
|
|
4332
|
+
self.parent.show_centroid_dialog()
|
|
4333
|
+
|
|
4247
4334
|
if value in my_network.edge_centroids:
|
|
4248
|
-
|
|
4249
|
-
self.parent.show_centroid_dialog()
|
|
4335
|
+
|
|
4250
4336
|
# Get centroid coordinates (Z, Y, X)
|
|
4251
4337
|
centroid = my_network.edge_centroids[value]
|
|
4252
4338
|
# Set the active channel to edges (1)
|
|
@@ -4267,9 +4353,12 @@ class CustomTableView(QTableView):
|
|
|
4267
4353
|
self.parent.clicked_values['edges'] = []
|
|
4268
4354
|
self.parent.clicked_values['edges'].append(value)
|
|
4269
4355
|
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4356
|
+
try:
|
|
4357
|
+
# Highlight the value in both tables if it exists
|
|
4358
|
+
self.highlight_value_in_table(self.parent.network_table, value, column)
|
|
4359
|
+
self.highlight_value_in_table(self.parent.selection_table, value, column)
|
|
4360
|
+
except:
|
|
4361
|
+
pass
|
|
4273
4362
|
else:
|
|
4274
4363
|
print(f"Edge {value} not found in centroids dictionary")
|
|
4275
4364
|
else: #If highlighting paired elements
|
|
@@ -4490,7 +4579,13 @@ class TabbedDataWidget(QTabWidget):
|
|
|
4490
4579
|
"""Add a new table with the given name"""
|
|
4491
4580
|
if name in self.tables:
|
|
4492
4581
|
# If tab already exists, update its content
|
|
4493
|
-
|
|
4582
|
+
old_table = self.tables[name]
|
|
4583
|
+
idx = self.indexOf(old_table)
|
|
4584
|
+
|
|
4585
|
+
# Remove the old table reference from parent's data_table
|
|
4586
|
+
if self.parent_window and old_table in self.parent_window.data_table:
|
|
4587
|
+
self.parent_window.data_table.remove(old_table)
|
|
4588
|
+
|
|
4494
4589
|
self.removeTab(idx)
|
|
4495
4590
|
|
|
4496
4591
|
# Create a new CustomTableView with is_top_table=True
|
|
@@ -4510,7 +4605,7 @@ class TabbedDataWidget(QTabWidget):
|
|
|
4510
4605
|
|
|
4511
4606
|
# Update parent's data_table reference
|
|
4512
4607
|
if self.parent_window:
|
|
4513
|
-
self.parent_window.data_table
|
|
4608
|
+
self.parent_window.data_table.append(new_table)
|
|
4514
4609
|
|
|
4515
4610
|
def close_tab(self, index):
|
|
4516
4611
|
"""Close the tab at the given index"""
|
|
@@ -4525,12 +4620,12 @@ class TabbedDataWidget(QTabWidget):
|
|
|
4525
4620
|
if name_to_remove:
|
|
4526
4621
|
del self.tables[name_to_remove]
|
|
4527
4622
|
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
if self.parent_window and self.count() > 0:
|
|
4532
|
-
self.parent_window.data_table = self.currentWidget()
|
|
4623
|
+
# Update parent's data_table reference by removing the widget
|
|
4624
|
+
if self.parent_window and widget in self.parent_window.data_table:
|
|
4625
|
+
self.parent_window.data_table.remove(widget)
|
|
4533
4626
|
|
|
4627
|
+
self.removeTab(index)
|
|
4628
|
+
|
|
4534
4629
|
def clear_all_tabs(self):
|
|
4535
4630
|
"""Remove all tabs"""
|
|
4536
4631
|
while self.count() > 0:
|
|
@@ -4895,6 +4990,52 @@ class ArbitraryDialog(QDialog):
|
|
|
4895
4990
|
continue
|
|
4896
4991
|
|
|
4897
4992
|
return processed_values
|
|
4993
|
+
|
|
4994
|
+
def handle_find_action(self, mode, value):
|
|
4995
|
+
"""Handle the Find action."""
|
|
4996
|
+
|
|
4997
|
+
# Determine if we're looking for a node or edge
|
|
4998
|
+
if mode == 0:
|
|
4999
|
+
|
|
5000
|
+
if my_network.node_centroids is None:
|
|
5001
|
+
self.parent().show_centroid_dialog()
|
|
5002
|
+
|
|
5003
|
+
if value in my_network.node_centroids:
|
|
5004
|
+
# Get centroid coordinates (Z, Y, X)
|
|
5005
|
+
centroid = my_network.node_centroids[value]
|
|
5006
|
+
# Set the active channel to nodes (0)
|
|
5007
|
+
self.parent().set_active_channel(0)
|
|
5008
|
+
# Toggle on the nodes channel if it's not already visible
|
|
5009
|
+
if not self.parent().channel_visible[0]:
|
|
5010
|
+
self.parent().channel_buttons[0].setChecked(True)
|
|
5011
|
+
self.parent().toggle_channel(0)
|
|
5012
|
+
# Navigate to the Z-slice
|
|
5013
|
+
self.parent().slice_slider.setValue(int(centroid[0]))
|
|
5014
|
+
print(f"Found node {value} at Z-slice {centroid[0]}")
|
|
5015
|
+
|
|
5016
|
+
else:
|
|
5017
|
+
print(f"Node {value} not found in centroids dictionary")
|
|
5018
|
+
|
|
5019
|
+
else: # edges
|
|
5020
|
+
if my_network.edge_centroids is None:
|
|
5021
|
+
self.parent().show_centroid_dialog()
|
|
5022
|
+
|
|
5023
|
+
if value in my_network.edge_centroids:
|
|
5024
|
+
|
|
5025
|
+
# Get centroid coordinates (Z, Y, X)
|
|
5026
|
+
centroid = my_network.edge_centroids[value]
|
|
5027
|
+
# Set the active channel to edges (1)
|
|
5028
|
+
self.parent().set_active_channel(1)
|
|
5029
|
+
# Toggle on the edges channel if it's not already visible
|
|
5030
|
+
if not self.parent().channel_visible[1]:
|
|
5031
|
+
self.parent().channel_buttons[1].setChecked(True)
|
|
5032
|
+
self.parent().toggle_channel(1)
|
|
5033
|
+
# Navigate to the Z-slice
|
|
5034
|
+
self.parent().slice_slider.setValue(int(centroid[0]))
|
|
5035
|
+
print(f"Found edge {value} at Z-slice {centroid[0]}")
|
|
5036
|
+
|
|
5037
|
+
else:
|
|
5038
|
+
print(f"Edge {value} not found in centroids dictionary")
|
|
4898
5039
|
|
|
4899
5040
|
def process_selections(self):
|
|
4900
5041
|
"""Process the selection and deselection inputs."""
|
|
@@ -4930,11 +5071,21 @@ class ArbitraryDialog(QDialog):
|
|
|
4930
5071
|
except:
|
|
4931
5072
|
pass #Forgive mistakes
|
|
4932
5073
|
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
5074
|
+
select_list.reverse()
|
|
5075
|
+
|
|
5076
|
+
self.parent().clicked_values[mode].extend(select_list)
|
|
5077
|
+
|
|
5078
|
+
select_list.reverse()
|
|
5079
|
+
|
|
5080
|
+
try:
|
|
5081
|
+
if mode == 'nodes':
|
|
5082
|
+
self.handle_find_action(0, select_list[0])
|
|
5083
|
+
self.parent().handle_info(sort = 'node')
|
|
5084
|
+
elif mode == 'edges':
|
|
5085
|
+
self.handle_find_action(1, select_list[0])
|
|
5086
|
+
self.parent().handle_info(sort = 'edge')
|
|
5087
|
+
except:
|
|
5088
|
+
pass
|
|
4938
5089
|
|
|
4939
5090
|
self.parent().clicked_values[mode] = list(set(self.parent().clicked_values[mode]))
|
|
4940
5091
|
|
|
@@ -4954,6 +5105,8 @@ class ArbitraryDialog(QDialog):
|
|
|
4954
5105
|
|
|
4955
5106
|
except Exception as e:
|
|
4956
5107
|
QMessageBox.critical(self, "Error", f"Error processing selections: {str(e)}")
|
|
5108
|
+
import traceback
|
|
5109
|
+
print(traceback.format_exc())
|
|
4957
5110
|
|
|
4958
5111
|
class Show3dDialog(QDialog):
|
|
4959
5112
|
def __init__(self, parent=None):
|
|
@@ -5247,8 +5400,10 @@ class ShuffleDialog(QDialog):
|
|
|
5247
5400
|
|
|
5248
5401
|
try:
|
|
5249
5402
|
if accepted_mode == 4:
|
|
5250
|
-
|
|
5251
|
-
|
|
5403
|
+
try:
|
|
5404
|
+
self.parent().highlight_overlay = n3d.binarize(target_data)
|
|
5405
|
+
except:
|
|
5406
|
+
self.parent().highlight_overay = None
|
|
5252
5407
|
else:
|
|
5253
5408
|
self.parent().load_channel(accepted_mode, channel_data = target_data, data = True)
|
|
5254
5409
|
except:
|
|
@@ -5261,11 +5416,12 @@ class ShuffleDialog(QDialog):
|
|
|
5261
5416
|
self.parent().highlight_overlay = n3d.binarize(active_data)
|
|
5262
5417
|
except:
|
|
5263
5418
|
self.parent().highlight_overlay = None
|
|
5419
|
+
else:
|
|
5420
|
+
self.parent().load_channel(accepted_target, channel_data = active_data, data = True)
|
|
5264
5421
|
except:
|
|
5265
5422
|
pass
|
|
5266
5423
|
|
|
5267
|
-
|
|
5268
|
-
self.parent().load_channel(accepted_target, channel_data = active_data, data = True)
|
|
5424
|
+
|
|
5269
5425
|
|
|
5270
5426
|
|
|
5271
5427
|
self.parent().update_display()
|
|
@@ -5433,18 +5589,23 @@ class RadialDialog(QDialog):
|
|
|
5433
5589
|
|
|
5434
5590
|
def radial(self):
|
|
5435
5591
|
|
|
5436
|
-
|
|
5592
|
+
try:
|
|
5593
|
+
|
|
5594
|
+
distance = float(self.distance.text()) if self.distance.text().strip() else 50
|
|
5595
|
+
|
|
5596
|
+
directory = str(self.distance.text()) if self.directory.text().strip() else None
|
|
5437
5597
|
|
|
5438
|
-
|
|
5598
|
+
if my_network.node_centroids is None:
|
|
5599
|
+
self.parent().show_centroid_dialog()
|
|
5439
5600
|
|
|
5440
|
-
|
|
5441
|
-
self.parent().show_centroid_dialog()
|
|
5601
|
+
radial = my_network.radial_distribution(distance, directory = directory)
|
|
5442
5602
|
|
|
5443
|
-
|
|
5603
|
+
self.parent().format_for_upperright_table(radial, 'Radial Distance From Any Node', 'Average Number of Neighboring Nodes', title = 'Radial Distribution Analysis')
|
|
5444
5604
|
|
|
5445
|
-
|
|
5605
|
+
self.accept()
|
|
5446
5606
|
|
|
5447
|
-
|
|
5607
|
+
except Exception as e:
|
|
5608
|
+
print(f"An error occurred: {e}")
|
|
5448
5609
|
|
|
5449
5610
|
class DegreeDistDialog(QDialog):
|
|
5450
5611
|
|
|
@@ -5477,7 +5638,7 @@ class DegreeDistDialog(QDialog):
|
|
|
5477
5638
|
|
|
5478
5639
|
self.accept()
|
|
5479
5640
|
|
|
5480
|
-
except
|
|
5641
|
+
except Exception as e:
|
|
5481
5642
|
print(f"An error occurred: {e}")
|
|
5482
5643
|
|
|
5483
5644
|
class NeighborIdentityDialog(QDialog):
|
|
@@ -5589,6 +5750,50 @@ class RandomDialog(QDialog):
|
|
|
5589
5750
|
|
|
5590
5751
|
self.accept()
|
|
5591
5752
|
|
|
5753
|
+
class RadDialog(QDialog):
|
|
5754
|
+
|
|
5755
|
+
def __init__(self, parent=None):
|
|
5756
|
+
|
|
5757
|
+
super().__init__(parent)
|
|
5758
|
+
self.setWindowTitle("Obtain Radii of Active Image? (Returns Largest Radius for Each Labeled Object)")
|
|
5759
|
+
self.setModal(True)
|
|
5760
|
+
|
|
5761
|
+
layout = QFormLayout(self)
|
|
5762
|
+
|
|
5763
|
+
# GPU checkbox (default False)
|
|
5764
|
+
self.GPU = QPushButton("GPU")
|
|
5765
|
+
self.GPU.setCheckable(True)
|
|
5766
|
+
self.GPU.setChecked(False)
|
|
5767
|
+
layout.addRow("Use GPU:", self.GPU)
|
|
5768
|
+
|
|
5769
|
+
|
|
5770
|
+
# Add Run button
|
|
5771
|
+
run_button = QPushButton("Calculate")
|
|
5772
|
+
run_button.clicked.connect(self.rads)
|
|
5773
|
+
layout.addWidget(run_button)
|
|
5774
|
+
|
|
5775
|
+
def rads(self):
|
|
5776
|
+
|
|
5777
|
+
try:
|
|
5778
|
+
GPU = self.GPU.isChecked()
|
|
5779
|
+
|
|
5780
|
+
active_data = self.parent().channel_data[self.parent().active_channel]
|
|
5781
|
+
|
|
5782
|
+
radii = n3d.estimate_object_radii(active_data, gpu=GPU)
|
|
5783
|
+
|
|
5784
|
+
for key, val in radii.items():
|
|
5785
|
+
|
|
5786
|
+
radii[key] = [val, val * (my_network.xy_scale**2) * my_network.z_scale]
|
|
5787
|
+
|
|
5788
|
+
self.parent().format_for_upperright_table(radii, title = '~Radii of Objects', metric='ObjectID', value=['Largest Radius (Voxels)', 'Largest Radius (Scaled)'])
|
|
5789
|
+
|
|
5790
|
+
self.accept()
|
|
5791
|
+
|
|
5792
|
+
except Exception as e:
|
|
5793
|
+
print(f"Error: {e}")
|
|
5794
|
+
|
|
5795
|
+
|
|
5796
|
+
|
|
5592
5797
|
|
|
5593
5798
|
|
|
5594
5799
|
class InteractionDialog(QDialog):
|
|
@@ -6303,7 +6508,7 @@ class SLabelDialog(QDialog):
|
|
|
6303
6508
|
# GPU checkbox (default True)
|
|
6304
6509
|
self.GPU = QPushButton("GPU")
|
|
6305
6510
|
self.GPU.setCheckable(True)
|
|
6306
|
-
self.GPU.setChecked(
|
|
6511
|
+
self.GPU.setChecked(False)
|
|
6307
6512
|
layout.addRow("Use GPU:", self.GPU)
|
|
6308
6513
|
|
|
6309
6514
|
self.down_factor = QLineEdit("")
|
|
@@ -7372,7 +7577,7 @@ class SmartDilateDialog(QDialog):
|
|
|
7372
7577
|
# GPU checkbox (default True)
|
|
7373
7578
|
self.GPU = QPushButton("GPU")
|
|
7374
7579
|
self.GPU.setCheckable(True)
|
|
7375
|
-
self.GPU.setChecked(
|
|
7580
|
+
self.GPU.setChecked(False)
|
|
7376
7581
|
layout.addRow("Use GPU:", self.GPU)
|
|
7377
7582
|
|
|
7378
7583
|
self.down_factor = QLineEdit("")
|
|
@@ -7783,6 +7988,12 @@ class SkeletonizeDialog(QDialog):
|
|
|
7783
7988
|
self.remove = QLineEdit("0")
|
|
7784
7989
|
layout.addRow("Remove Branches Pixel Length (int):", self.remove)
|
|
7785
7990
|
|
|
7991
|
+
# auto checkbox (default True)
|
|
7992
|
+
self.auto = QPushButton("Auto")
|
|
7993
|
+
self.auto.setCheckable(True)
|
|
7994
|
+
self.auto.setChecked(False)
|
|
7995
|
+
layout.addRow("Attempt to Auto Correct Skeleton Looping:", self.auto)
|
|
7996
|
+
|
|
7786
7997
|
# Add Run button
|
|
7787
7998
|
run_button = QPushButton("Run Skeletonize")
|
|
7788
7999
|
run_button.clicked.connect(self.run_skeletonize)
|
|
@@ -7796,11 +8007,17 @@ class SkeletonizeDialog(QDialog):
|
|
|
7796
8007
|
remove = int(self.remove.text()) if self.remove.text() else 0
|
|
7797
8008
|
except ValueError:
|
|
7798
8009
|
remove = 0
|
|
8010
|
+
|
|
8011
|
+
auto = self.auto.isChecked()
|
|
7799
8012
|
|
|
7800
8013
|
# Get the active channel data from parent
|
|
7801
8014
|
active_data = self.parent().channel_data[self.parent().active_channel]
|
|
7802
8015
|
if active_data is None:
|
|
7803
8016
|
raise ValueError("No active image selected")
|
|
8017
|
+
|
|
8018
|
+
if auto:
|
|
8019
|
+
active_data = n3d.skeletonize(active_data)
|
|
8020
|
+
active_data = n3d.fill_holes_3d(active_data)
|
|
7804
8021
|
|
|
7805
8022
|
# Call dilate method with parameters
|
|
7806
8023
|
result = n3d.skeletonize(
|
|
@@ -7862,7 +8079,7 @@ class WatershedDialog(QDialog):
|
|
|
7862
8079
|
# GPU checkbox (default True)
|
|
7863
8080
|
self.gpu = QPushButton("GPU")
|
|
7864
8081
|
self.gpu.setCheckable(True)
|
|
7865
|
-
self.gpu.setChecked(
|
|
8082
|
+
self.gpu.setChecked(False)
|
|
7866
8083
|
layout.addRow("Use GPU:", self.gpu)
|
|
7867
8084
|
|
|
7868
8085
|
# Smallest radius (empty by default)
|
|
@@ -8112,11 +8329,17 @@ class GenNodesDialog(QDialog):
|
|
|
8112
8329
|
self.branch_removal = QLineEdit("0")
|
|
8113
8330
|
layout.addRow("Skeleton Voxel Branch Length to Remove (int) (Compensates for spines off medial axis):", self.branch_removal)
|
|
8114
8331
|
|
|
8332
|
+
self.comp_dil = QLineEdit("0")
|
|
8333
|
+
layout.addRow("Voxel distance to merge nearby nodes (Int - compensates for multi-branch identification along thick branch regions):", self.comp_dil)
|
|
8334
|
+
|
|
8115
8335
|
self.max_vol = QLineEdit("0")
|
|
8116
8336
|
layout.addRow("Maximum Voxel Volume of Vertices to Retain (int - Compensates for skeleton looping - occurs before any node merging - the smallest objects are always 27 voxels):", self.max_vol)
|
|
8117
8337
|
|
|
8118
|
-
|
|
8119
|
-
|
|
8338
|
+
# auto checkbox (default True)
|
|
8339
|
+
self.auto = QPushButton("Auto")
|
|
8340
|
+
self.auto.setCheckable(True)
|
|
8341
|
+
self.auto.setChecked(False)
|
|
8342
|
+
layout.addRow("Attempt to Auto Correct Skeleton Looping:", self.auto)
|
|
8120
8343
|
|
|
8121
8344
|
if not down_factor:
|
|
8122
8345
|
down_factor = None
|
|
@@ -8194,6 +8417,13 @@ class GenNodesDialog(QDialog):
|
|
|
8194
8417
|
else:
|
|
8195
8418
|
order = 0
|
|
8196
8419
|
|
|
8420
|
+
auto = self.auto.isChecked()
|
|
8421
|
+
|
|
8422
|
+
|
|
8423
|
+
if auto:
|
|
8424
|
+
my_network.edges = n3d.skeletonize(my_network.edges)
|
|
8425
|
+
my_network.edges = n3d.fill_holes_3d(my_network.edges)
|
|
8426
|
+
|
|
8197
8427
|
|
|
8198
8428
|
result, skele = n3d.label_vertices(
|
|
8199
8429
|
my_network.edges,
|
|
@@ -8248,6 +8478,7 @@ class GenNodesDialog(QDialog):
|
|
|
8248
8478
|
)
|
|
8249
8479
|
|
|
8250
8480
|
|
|
8481
|
+
|
|
8251
8482
|
class BranchDialog(QDialog):
|
|
8252
8483
|
|
|
8253
8484
|
def __init__(self, parent=None):
|
|
@@ -8275,8 +8506,7 @@ class BranchDialog(QDialog):
|
|
|
8275
8506
|
self.fix.setChecked(False)
|
|
8276
8507
|
layout.addRow("Attempt to auto-correct branch labels:", self.fix)
|
|
8277
8508
|
|
|
8278
|
-
self.fix_val = QLineEdit()
|
|
8279
|
-
self.fix_val.setPlaceholderText("Empty = default value...")
|
|
8509
|
+
self.fix_val = QLineEdit('4')
|
|
8280
8510
|
layout.addRow("If checked above - Avg Degree of Nearby Branch Communities to Merge (Attempt to fix branch labeling - try 4 to 6 to start or leave empty):", self.fix_val)
|
|
8281
8511
|
|
|
8282
8512
|
self.down_factor = QLineEdit("0")
|
|
@@ -8718,15 +8948,23 @@ class CentroidDialog(QDialog):
|
|
|
8718
8948
|
my_network.save_edge_centroids(directory = directory)
|
|
8719
8949
|
|
|
8720
8950
|
elif chan == 0:
|
|
8721
|
-
|
|
8722
|
-
|
|
8723
|
-
|
|
8724
|
-
|
|
8951
|
+
try:
|
|
8952
|
+
my_network.calculate_node_centroids(
|
|
8953
|
+
down_factor = downsample
|
|
8954
|
+
)
|
|
8955
|
+
my_network.save_node_centroids(directory = directory)
|
|
8956
|
+
except:
|
|
8957
|
+
pass
|
|
8725
8958
|
|
|
8726
|
-
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
|
|
8959
|
+
try:
|
|
8960
|
+
|
|
8961
|
+
my_network.calculate_edge_centroids(
|
|
8962
|
+
down_factor = downsample
|
|
8963
|
+
)
|
|
8964
|
+
my_network.save_edge_centroids(directory = directory)
|
|
8965
|
+
|
|
8966
|
+
except:
|
|
8967
|
+
pass
|
|
8730
8968
|
|
|
8731
8969
|
if hasattr(my_network, 'node_centroids') and my_network.node_centroids is not None:
|
|
8732
8970
|
try:
|
nettracer3d/network_analysis.py
CHANGED
|
@@ -1024,11 +1024,8 @@ def histogram(counts, y_vals, directory = None):
|
|
|
1024
1024
|
plt.ylabel('Avg Number of Neigbhoring Vertices')
|
|
1025
1025
|
|
|
1026
1026
|
try:
|
|
1027
|
-
|
|
1028
1027
|
if directory is not None:
|
|
1029
1028
|
plt.savefig(f'{directory}/radial_plot.png')
|
|
1030
|
-
else:
|
|
1031
|
-
plt.savefig('radial_plot.png')
|
|
1032
1029
|
except:
|
|
1033
1030
|
pass
|
|
1034
1031
|
|
|
@@ -1439,6 +1436,7 @@ def degree_distribution(G, directory = None):
|
|
|
1439
1436
|
|
|
1440
1437
|
def power_trendline(x, y, directory = None):
|
|
1441
1438
|
# Handle zeros in y for logarithmic transformations
|
|
1439
|
+
"""
|
|
1442
1440
|
y = np.array(y)
|
|
1443
1441
|
x = np.array(x)
|
|
1444
1442
|
y[y == 0] += 0.001
|
|
@@ -1460,6 +1458,8 @@ def power_trendline(x, y, directory = None):
|
|
|
1460
1458
|
ss_res = np.sum((y - y_pred) ** 2)
|
|
1461
1459
|
ss_tot = np.sum((y - np.mean(y)) ** 2)
|
|
1462
1460
|
r2 = 1 - (ss_res / ss_tot)
|
|
1461
|
+
"""
|
|
1462
|
+
# ^ I commented out this power trendline stuff because I decided I no longer want it to do that so.
|
|
1463
1463
|
|
|
1464
1464
|
# Create a scatterplot
|
|
1465
1465
|
plt.scatter(x, y, label='Data')
|
|
@@ -1468,9 +1468,10 @@ def power_trendline(x, y, directory = None):
|
|
|
1468
1468
|
plt.title('Degree Distribution of Network')
|
|
1469
1469
|
|
|
1470
1470
|
# Plot the power trendline
|
|
1471
|
-
plt.plot(x_fit, y_fit, color='red', label=f'Power Trendline: $y = {a:.2f}x^{{{b:.2f}}}$')
|
|
1471
|
+
#plt.plot(x_fit, y_fit, color='red', label=f'Power Trendline: $y = {a:.2f}x^{{{b:.2f}}}$')
|
|
1472
1472
|
|
|
1473
1473
|
# Annotate the plot with the trendline equation and R-squared value
|
|
1474
|
+
"""
|
|
1474
1475
|
plt.text(
|
|
1475
1476
|
0.05, 0.95,
|
|
1476
1477
|
f'$y = {a:.2f}x^{{{b:.2f}}}$\n$R^2 = {r2:.2f}$',
|
|
@@ -1478,13 +1479,12 @@ def power_trendline(x, y, directory = None):
|
|
|
1478
1479
|
fontsize=12,
|
|
1479
1480
|
verticalalignment='top'
|
|
1480
1481
|
)
|
|
1482
|
+
"""
|
|
1481
1483
|
|
|
1482
1484
|
try:
|
|
1483
1485
|
|
|
1484
1486
|
if directory is not None:
|
|
1485
1487
|
plt.savefig(f'{directory}/degree_plot.png')
|
|
1486
|
-
else:
|
|
1487
|
-
plt.savefig('degree_plot.png')
|
|
1488
1488
|
except:
|
|
1489
1489
|
pass
|
|
1490
1490
|
|
nettracer3d/smart_dilate.py
CHANGED
|
@@ -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,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.
|
|
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. Updated
|
|
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.
|
|
56
|
+
|
|
57
|
+
4. Minor bug fixes/improvements.
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
nettracer3d/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
nettracer3d/community_extractor.py,sha256=Zq8ZM595CTzeR6zLEZ4I6KvhkNfCPUReWvAKxTlaVfk,33495
|
|
3
3
|
nettracer3d/modularity.py,sha256=V1f3s_vGd8EuVz27mzq6ycIGr0BWIpH7c7NU4QjgAHU,30247
|
|
4
|
-
nettracer3d/morphology.py,sha256=
|
|
5
|
-
nettracer3d/nettracer.py,sha256=
|
|
6
|
-
nettracer3d/nettracer_gui.py,sha256=
|
|
7
|
-
nettracer3d/network_analysis.py,sha256=
|
|
4
|
+
nettracer3d/morphology.py,sha256=yncUj04Noj_mcdJze4qMfYw-21AbebwiIcu1bDWGgCM,17778
|
|
5
|
+
nettracer3d/nettracer.py,sha256=vsEDFlsz7WtWdkaK_uJXjbA_QzJRYW9pAQgLiAZOJKc,209987
|
|
6
|
+
nettracer3d/nettracer_gui.py,sha256=492wQV_3nIAPPJRClXA-NssaCvx9H6GDMg6yaiL-BeM,393682
|
|
7
|
+
nettracer3d/network_analysis.py,sha256=q1q7lxtA3lebxitfC_jfiT9cnpYXJw4q0Oy2_-Aj8qE,48068
|
|
8
8
|
nettracer3d/network_draw.py,sha256=F7fw6Pcf4qWOhdKwLmhwqWdschbDlHzwCVolQC9imeU,14117
|
|
9
9
|
nettracer3d/node_draw.py,sha256=k3sCTfUCJs3aH1C1q1gTNxDz9EAQbBd1hsUIJajxRx8,9823
|
|
10
10
|
nettracer3d/proximity.py,sha256=FnIiI_AzfXd22HwCIFIyQRZxKYJ8YscIDdPnIv-wsO4,10560
|
|
11
11
|
nettracer3d/run.py,sha256=xYeaAc8FCx8MuzTGyL3NR3mK7WZzffAYAH23bNRZYO4,127
|
|
12
12
|
nettracer3d/segmenter.py,sha256=oKQEKQpo3o6cqfN6Z_IAgx8V-HXpegQNjfWFz3Bdu04,83449
|
|
13
13
|
nettracer3d/simple_network.py,sha256=fP1gkDdtQcHruEZpUdasKdZeVacoLOxKhR3bY0L1CAQ,15426
|
|
14
|
-
nettracer3d/smart_dilate.py,sha256=
|
|
15
|
-
nettracer3d-0.6.
|
|
16
|
-
nettracer3d-0.6.
|
|
17
|
-
nettracer3d-0.6.
|
|
18
|
-
nettracer3d-0.6.
|
|
19
|
-
nettracer3d-0.6.
|
|
20
|
-
nettracer3d-0.6.
|
|
14
|
+
nettracer3d/smart_dilate.py,sha256=vnBj2soDGVBioKaNQi-bcyAtg0nuWcNGmlrzUNFFYQE,23191
|
|
15
|
+
nettracer3d-0.6.5.dist-info/licenses/LICENSE,sha256=gM207DhJjWrxLuEWXl0Qz5ISbtWDmADfjHp3yC2XISs,888
|
|
16
|
+
nettracer3d-0.6.5.dist-info/METADATA,sha256=q1Qp-eXkzhqVVG1A1OkPQeOVqMLbHwx7GdpI1fm_6NA,3476
|
|
17
|
+
nettracer3d-0.6.5.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
18
|
+
nettracer3d-0.6.5.dist-info/entry_points.txt,sha256=Nx1rr_0QhJXDBHAQg2vcqCzLMKBzSHfwy3xwGkueVyc,53
|
|
19
|
+
nettracer3d-0.6.5.dist-info/top_level.txt,sha256=zsYy9rZwirfCEOubolhee4TyzqBAL5gSUeFMzhFTX8c,12
|
|
20
|
+
nettracer3d-0.6.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|