nettracer3d 0.9.9__tar.gz → 1.0.1__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.
Potentially problematic release.
This version of nettracer3d might be problematic. Click here for more details.
- {nettracer3d-0.9.9/src/nettracer3d.egg-info → nettracer3d-1.0.1}/PKG-INFO +3 -4
- {nettracer3d-0.9.9 → nettracer3d-1.0.1}/README.md +2 -3
- {nettracer3d-0.9.9 → nettracer3d-1.0.1}/pyproject.toml +1 -1
- {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/community_extractor.py +24 -8
- {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/neighborhoods.py +48 -30
- {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/nettracer.py +57 -1
- {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/nettracer_gui.py +370 -105
- {nettracer3d-0.9.9 → nettracer3d-1.0.1/src/nettracer3d.egg-info}/PKG-INFO +3 -4
- {nettracer3d-0.9.9 → nettracer3d-1.0.1}/LICENSE +0 -0
- {nettracer3d-0.9.9 → nettracer3d-1.0.1}/setup.cfg +0 -0
- {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/__init__.py +0 -0
- {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/cellpose_manager.py +0 -0
- {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/excelotron.py +0 -0
- {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/modularity.py +0 -0
- {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/morphology.py +0 -0
- {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/network_analysis.py +0 -0
- {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/network_draw.py +0 -0
- {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/node_draw.py +0 -0
- {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/painting.py +0 -0
- {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/proximity.py +0 -0
- {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/run.py +0 -0
- {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/segmenter.py +0 -0
- {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/segmenter_GPU.py +0 -0
- {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/simple_network.py +0 -0
- {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/smart_dilate.py +0 -0
- {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d.egg-info/SOURCES.txt +0 -0
- {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d.egg-info/dependency_links.txt +0 -0
- {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d.egg-info/entry_points.txt +0 -0
- {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d.egg-info/requires.txt +0 -0
- {nettracer3d-0.9.9 → nettracer3d-1.0.1}/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.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Scripts for intializing and analyzing networks from segmentations of three dimensional images.
|
|
5
5
|
Author-email: Liam McLaughlin <liamm@wustl.edu>
|
|
6
6
|
Project-URL: Documentation, https://nettracer3d.readthedocs.io/en/latest/
|
|
@@ -110,7 +110,6 @@ McLaughlin, L., Zhang, B., Sharma, S. et al. Three dimensional multiscalar neuro
|
|
|
110
110
|
|
|
111
111
|
NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
|
|
112
112
|
|
|
113
|
-
-- Version 0.
|
|
113
|
+
-- Version 1.0.1 Updates --
|
|
114
114
|
|
|
115
|
-
*
|
|
116
|
-
* Similarly, tables that have the format node id column:numerical values can now be used liberally to threshold the nodes, meaning most outputs of network analysis can be used to threshold nodes.
|
|
115
|
+
* Bug fixes, mainly
|
|
@@ -65,7 +65,6 @@ McLaughlin, L., Zhang, B., Sharma, S. et al. Three dimensional multiscalar neuro
|
|
|
65
65
|
|
|
66
66
|
NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
|
|
67
67
|
|
|
68
|
-
-- Version 0.
|
|
68
|
+
-- Version 1.0.1 Updates --
|
|
69
69
|
|
|
70
|
-
*
|
|
71
|
-
* Similarly, tables that have the format node id column:numerical values can now be used liberally to threshold the nodes, meaning most outputs of network analysis can be used to threshold nodes.
|
|
70
|
+
* Bug fixes, mainly
|
|
@@ -733,17 +733,33 @@ def assign_node_colors(node_list: List[int], labeled_array: np.ndarray) -> Tuple
|
|
|
733
733
|
return rgba_array, node_to_color_names
|
|
734
734
|
|
|
735
735
|
def assign_community_colors(community_dict: Dict[int, int], labeled_array: np.ndarray) -> Tuple[np.ndarray, Dict[int, str]]:
|
|
736
|
-
"""
|
|
736
|
+
"""Fast version using lookup table approach with brown outliers for community 0."""
|
|
737
|
+
|
|
738
|
+
# Separate outliers (community 0) from regular communities
|
|
739
|
+
outliers = {node: comm for node, comm in community_dict.items() if comm == 0}
|
|
740
|
+
non_outlier_dict = {node: comm for node, comm in community_dict.items() if comm != 0}
|
|
737
741
|
|
|
738
|
-
#
|
|
739
|
-
communities = set(
|
|
740
|
-
community_sizes = Counter(community_dict.values())
|
|
741
|
-
sorted_communities = sorted(communities, key=lambda x: community_sizes[x], reverse=True)
|
|
742
|
+
# Get communities excluding outliers
|
|
743
|
+
communities = set(non_outlier_dict.values()) if non_outlier_dict else set()
|
|
742
744
|
|
|
743
|
-
colors
|
|
745
|
+
# Generate colors for non-outlier communities only
|
|
746
|
+
colors = generate_distinct_colors(len(communities)) if communities else []
|
|
744
747
|
colors_rgba = np.array([(r, g, b, 255) for r, g, b in colors], dtype=np.uint8)
|
|
745
748
|
|
|
746
|
-
|
|
749
|
+
# Sort communities by size for consistent color assignment
|
|
750
|
+
if non_outlier_dict:
|
|
751
|
+
community_sizes = Counter(non_outlier_dict.values())
|
|
752
|
+
sorted_communities = sorted(communities, key=lambda x: (-community_sizes[x], x))
|
|
753
|
+
community_to_color = {comm: colors_rgba[i] for i, comm in enumerate(sorted_communities)}
|
|
754
|
+
else:
|
|
755
|
+
community_to_color = {}
|
|
756
|
+
|
|
757
|
+
# Add brown color for outliers (community 0)
|
|
758
|
+
brown_rgba = np.array([139, 69, 19, 255], dtype=np.uint8) # Brown color
|
|
759
|
+
if outliers:
|
|
760
|
+
community_to_color[0] = brown_rgba
|
|
761
|
+
|
|
762
|
+
# Create node to color mapping using original community_dict
|
|
747
763
|
node_to_color = {node: community_to_color[comm] for node, comm in community_dict.items()}
|
|
748
764
|
|
|
749
765
|
# Create lookup table - this is the key optimization
|
|
@@ -756,7 +772,7 @@ def assign_community_colors(community_dict: Dict[int, int], labeled_array: np.nd
|
|
|
756
772
|
# Single vectorized operation - this is much faster!
|
|
757
773
|
rgba_array = color_lut[labeled_array]
|
|
758
774
|
|
|
759
|
-
#
|
|
775
|
+
# Convert to RGB for color names (including brown for outliers)
|
|
760
776
|
community_to_color_rgb = {k: tuple(v[:3]) for k, v in community_to_color.items()}
|
|
761
777
|
node_to_color_names = convert_node_colors_to_names(community_to_color_rgb)
|
|
762
778
|
|
|
@@ -347,7 +347,8 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
|
|
|
347
347
|
id_dictionary: Optional[Dict[int, str]] = None,
|
|
348
348
|
graph_label = "Community ID",
|
|
349
349
|
title = 'UMAP Visualization of Community Compositions',
|
|
350
|
-
neighborhoods: Optional[Dict[int, int]] = None
|
|
350
|
+
neighborhoods: Optional[Dict[int, int]] = None,
|
|
351
|
+
original_communities = None):
|
|
351
352
|
"""
|
|
352
353
|
Convert cluster composition data to UMAP visualization.
|
|
353
354
|
|
|
@@ -394,37 +395,50 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
|
|
|
394
395
|
embedding = reducer.fit_transform(compositions)
|
|
395
396
|
|
|
396
397
|
# Determine coloring scheme based on parameters
|
|
397
|
-
if neighborhoods is not None:
|
|
398
|
+
if neighborhoods is not None and original_communities is not None:
|
|
398
399
|
# Use neighborhood coloring - import the community extractor methods
|
|
399
400
|
from . import community_extractor
|
|
401
|
+
from collections import Counter
|
|
400
402
|
|
|
401
|
-
#
|
|
402
|
-
|
|
403
|
-
for node_id, neighborhood_id in neighborhoods.items()
|
|
404
|
-
if node_id in cluster_ids}
|
|
403
|
+
# Use original_communities (which is {node: neighborhood}) for color generation
|
|
404
|
+
# This ensures we use the proper node counts for sorting
|
|
405
405
|
|
|
406
|
-
#
|
|
407
|
-
|
|
408
|
-
|
|
406
|
+
# Separate outliers (neighborhood 0) from regular neighborhoods in ORIGINAL structure
|
|
407
|
+
outlier_neighborhoods = {node: neighborhood for node, neighborhood in original_communities.items() if neighborhood == 0}
|
|
408
|
+
non_outlier_neighborhoods = {node: neighborhood for node, neighborhood in original_communities.items() if neighborhood != 0}
|
|
409
409
|
|
|
410
|
-
# Get
|
|
411
|
-
|
|
412
|
-
filtered_neighborhoods, dummy_array
|
|
413
|
-
)
|
|
410
|
+
# Get neighborhoods excluding outliers
|
|
411
|
+
unique_neighborhoods = set(non_outlier_neighborhoods.values()) if non_outlier_neighborhoods else set()
|
|
414
412
|
|
|
415
|
-
#
|
|
416
|
-
|
|
417
|
-
colors = community_extractor.generate_distinct_colors(len(unique_neighborhoods))
|
|
418
|
-
neighborhood_to_color = {neighborhood: colors[i] for i, neighborhood in enumerate(unique_neighborhoods)}
|
|
413
|
+
# Generate colors for non-outlier neighborhoods only (same as assign_community_colors)
|
|
414
|
+
colors = community_extractor.generate_distinct_colors(len(unique_neighborhoods)) if unique_neighborhoods else []
|
|
419
415
|
|
|
420
|
-
#
|
|
416
|
+
# Sort neighborhoods by size for consistent color assignment (same logic as assign_community_colors)
|
|
417
|
+
# Use the ORIGINAL node counts from original_communities
|
|
418
|
+
if non_outlier_neighborhoods:
|
|
419
|
+
neighborhood_sizes = Counter(non_outlier_neighborhoods.values())
|
|
420
|
+
sorted_neighborhoods = sorted(unique_neighborhoods, key=lambda x: (-neighborhood_sizes[x], x))
|
|
421
|
+
neighborhood_to_color = {neighborhood: colors[i] for i, neighborhood in enumerate(sorted_neighborhoods)}
|
|
422
|
+
else:
|
|
423
|
+
neighborhood_to_color = {}
|
|
424
|
+
|
|
425
|
+
# Add brown color for outliers (neighborhood 0) - same as assign_community_colors
|
|
426
|
+
if outlier_neighborhoods:
|
|
427
|
+
neighborhood_to_color[0] = (139, 69, 19) # Brown color (RGB, not RGBA here)
|
|
428
|
+
|
|
429
|
+
# Map each cluster to its neighborhood color using 'neighborhoods' ({community: neighborhood}) for assignment
|
|
421
430
|
point_colors = []
|
|
422
431
|
neighborhood_labels = []
|
|
423
432
|
for cluster_id in cluster_ids:
|
|
424
|
-
if cluster_id in
|
|
425
|
-
neighborhood_id =
|
|
426
|
-
|
|
427
|
-
|
|
433
|
+
if cluster_id in neighborhoods:
|
|
434
|
+
neighborhood_id = neighborhoods[cluster_id] # This is {community: neighborhood}
|
|
435
|
+
if neighborhood_id in neighborhood_to_color:
|
|
436
|
+
point_colors.append(neighborhood_to_color[neighborhood_id])
|
|
437
|
+
neighborhood_labels.append(neighborhood_id)
|
|
438
|
+
else:
|
|
439
|
+
# Default color for neighborhoods not found
|
|
440
|
+
point_colors.append((128, 128, 128)) # Gray
|
|
441
|
+
neighborhood_labels.append("Unknown")
|
|
428
442
|
else:
|
|
429
443
|
# Default color for nodes not in any neighborhood
|
|
430
444
|
point_colors.append((128, 128, 128)) # Gray
|
|
@@ -432,6 +446,10 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
|
|
|
432
446
|
|
|
433
447
|
# Normalize RGB values for matplotlib (0-1 range)
|
|
434
448
|
point_colors = [(r/255.0, g/255.0, b/255.0) for r, g, b in point_colors]
|
|
449
|
+
|
|
450
|
+
# Get unique neighborhoods for legend
|
|
451
|
+
unique_neighborhoods_for_legend = sorted(list(set(neighborhood_to_color.keys())))
|
|
452
|
+
|
|
435
453
|
use_neighborhood_coloring = True
|
|
436
454
|
|
|
437
455
|
elif id_dictionary is not None:
|
|
@@ -467,8 +485,8 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
|
|
|
467
485
|
# Add cluster ID labels
|
|
468
486
|
for i, cluster_id in enumerate(cluster_ids):
|
|
469
487
|
display_label = f'{cluster_id}'
|
|
470
|
-
if use_neighborhood_coloring and cluster_id in
|
|
471
|
-
neighborhood_id =
|
|
488
|
+
if use_neighborhood_coloring and cluster_id in neighborhoods:
|
|
489
|
+
neighborhood_id = neighborhoods[cluster_id]
|
|
472
490
|
display_label = f'{cluster_id}\n(N{neighborhood_id})'
|
|
473
491
|
elif id_dictionary is not None:
|
|
474
492
|
identity = id_dictionary.get(cluster_id, "Unknown")
|
|
@@ -483,7 +501,7 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
|
|
|
483
501
|
if use_neighborhood_coloring:
|
|
484
502
|
# Create custom legend for neighborhoods
|
|
485
503
|
legend_elements = []
|
|
486
|
-
for neighborhood_id in
|
|
504
|
+
for neighborhood_id in unique_neighborhoods_for_legend:
|
|
487
505
|
color = neighborhood_to_color[neighborhood_id]
|
|
488
506
|
norm_color = (color[0]/255.0, color[1]/255.0, color[2]/255.0)
|
|
489
507
|
legend_elements.append(
|
|
@@ -530,8 +548,8 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
|
|
|
530
548
|
# Add cluster ID labels
|
|
531
549
|
for i, cluster_id in enumerate(cluster_ids):
|
|
532
550
|
display_label = f'C{cluster_id}'
|
|
533
|
-
if use_neighborhood_coloring and cluster_id in
|
|
534
|
-
neighborhood_id =
|
|
551
|
+
if use_neighborhood_coloring and cluster_id in neighborhoods:
|
|
552
|
+
neighborhood_id = neighborhoods[cluster_id]
|
|
535
553
|
display_label = f'C{cluster_id}\n(N{neighborhood_id})'
|
|
536
554
|
elif id_dictionary is not None:
|
|
537
555
|
identity = id_dictionary.get(cluster_id, "Unknown")
|
|
@@ -554,7 +572,7 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
|
|
|
554
572
|
if use_neighborhood_coloring:
|
|
555
573
|
# Create custom legend for neighborhoods
|
|
556
574
|
legend_elements = []
|
|
557
|
-
for neighborhood_id in
|
|
575
|
+
for neighborhood_id in unique_neighborhoods_for_legend:
|
|
558
576
|
color = neighborhood_to_color[neighborhood_id]
|
|
559
577
|
norm_color = (color[0]/255.0, color[1]/255.0, color[2]/255.0)
|
|
560
578
|
legend_elements.append(
|
|
@@ -585,8 +603,8 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
|
|
|
585
603
|
for i, cluster_id in enumerate(cluster_ids):
|
|
586
604
|
composition = compositions[i]
|
|
587
605
|
additional_info = ""
|
|
588
|
-
if use_neighborhood_coloring and cluster_id in
|
|
589
|
-
neighborhood_id =
|
|
606
|
+
if use_neighborhood_coloring and cluster_id in neighborhoods:
|
|
607
|
+
neighborhood_id = neighborhoods[cluster_id]
|
|
590
608
|
additional_info = f" (Neighborhood: {neighborhood_id})"
|
|
591
609
|
elif id_dictionary is not None:
|
|
592
610
|
identity = id_dictionary.get(cluster_id, "Unknown")
|
|
@@ -992,6 +992,61 @@ def z_project(array3d, method='max'):
|
|
|
992
992
|
raise ValueError("Method must be one of: 'max', 'mean', 'min', 'sum', 'std'")
|
|
993
993
|
|
|
994
994
|
def fill_holes_3d(array, head_on = False, fill_borders = True):
|
|
995
|
+
def process_slice(slice_2d, border_threshold=0.08, fill_borders = True):
|
|
996
|
+
"""
|
|
997
|
+
Process a 2D slice, considering components that touch less than border_threshold
|
|
998
|
+
of any border length as potential holes.
|
|
999
|
+
|
|
1000
|
+
Args:
|
|
1001
|
+
slice_2d: 2D binary array
|
|
1002
|
+
border_threshold: proportion of border that must be touched to be considered background
|
|
1003
|
+
"""
|
|
1004
|
+
from scipy.ndimage import binary_fill_holes
|
|
1005
|
+
|
|
1006
|
+
slice_2d = slice_2d.astype(np.uint8)
|
|
1007
|
+
|
|
1008
|
+
# Apply scipy's binary_fill_holes to the result
|
|
1009
|
+
slice_2d = binary_fill_holes(slice_2d)
|
|
1010
|
+
|
|
1011
|
+
return slice_2d
|
|
1012
|
+
|
|
1013
|
+
print("Filling Holes...")
|
|
1014
|
+
|
|
1015
|
+
array = binarize(array)
|
|
1016
|
+
#inv_array = invert_array(array)
|
|
1017
|
+
|
|
1018
|
+
# Create arrays for all three planes
|
|
1019
|
+
array_xy = np.zeros_like(array, dtype=np.uint8)
|
|
1020
|
+
array_xz = np.zeros_like(array, dtype=np.uint8)
|
|
1021
|
+
array_yz = np.zeros_like(array, dtype=np.uint8)
|
|
1022
|
+
|
|
1023
|
+
# Process XY plane
|
|
1024
|
+
for z in range(array.shape[0]):
|
|
1025
|
+
array_xy[z] = process_slice(array[z], fill_borders = fill_borders)
|
|
1026
|
+
|
|
1027
|
+
if (array.shape[0] > 3) and not head_on: #only use these dimensions for sufficiently large zstacks
|
|
1028
|
+
|
|
1029
|
+
# Process XZ plane
|
|
1030
|
+
for y in range(array.shape[1]):
|
|
1031
|
+
slice_xz = array[:, y, :]
|
|
1032
|
+
array_xz[:, y, :] = process_slice(slice_xz, fill_borders = fill_borders)
|
|
1033
|
+
|
|
1034
|
+
# Process YZ plane
|
|
1035
|
+
for x in range(array.shape[2]):
|
|
1036
|
+
slice_yz = array[:, :, x]
|
|
1037
|
+
array_yz[:, :, x] = process_slice(slice_yz, fill_borders = fill_borders)
|
|
1038
|
+
|
|
1039
|
+
# Combine results from all three planes
|
|
1040
|
+
filled = (array_xy | array_xz | array_yz) * 255
|
|
1041
|
+
return array + filled
|
|
1042
|
+
else:
|
|
1043
|
+
# Apply scipy's binary_fill_holes to each XY slice
|
|
1044
|
+
from scipy.ndimage import binary_fill_holes
|
|
1045
|
+
for z in range(array_xy.shape[0]):
|
|
1046
|
+
array_xy[z] = binary_fill_holes(array_xy[z])
|
|
1047
|
+
return array_xy * 255
|
|
1048
|
+
|
|
1049
|
+
def fill_holes_3d_old(array, head_on = False, fill_borders = True):
|
|
995
1050
|
|
|
996
1051
|
def process_slice(slice_2d, border_threshold=0.08, fill_borders = True):
|
|
997
1052
|
"""
|
|
@@ -5659,7 +5714,8 @@ class Network_3D:
|
|
|
5659
5714
|
neighbor_group[com] = neighbors[node]
|
|
5660
5715
|
except:
|
|
5661
5716
|
neighbor_group[com] = 0
|
|
5662
|
-
|
|
5717
|
+
print(neighbors)
|
|
5718
|
+
neighborhoods.visualize_cluster_composition_umap(umap_dict, id_set, neighborhoods = neighbor_group, original_communities = neighbors)
|
|
5663
5719
|
elif label == 1:
|
|
5664
5720
|
neighborhoods.visualize_cluster_composition_umap(umap_dict, id_set, label = True)
|
|
5665
5721
|
else:
|
|
@@ -1027,7 +1027,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1027
1027
|
|
|
1028
1028
|
"""Highlight overlay generation method specific for the segmenter interactive mode"""
|
|
1029
1029
|
|
|
1030
|
-
|
|
1031
1030
|
def process_chunk_bounds(chunk_data, indices_to_check):
|
|
1032
1031
|
"""Process a single chunk of the array to create highlight mask"""
|
|
1033
1032
|
mask = (chunk_data >= indices_to_check[0]) & (chunk_data <= indices_to_check[1])
|
|
@@ -1106,6 +1105,30 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1106
1105
|
current_ylim = self.ax.get_ylim()
|
|
1107
1106
|
self.update_display_pan_mode(current_xlim, current_ylim)
|
|
1108
1107
|
|
|
1108
|
+
if my_network.network is not None:
|
|
1109
|
+
try:
|
|
1110
|
+
if self.active_channel == 0:
|
|
1111
|
+
|
|
1112
|
+
# Get the existing DataFrame from the model
|
|
1113
|
+
original_df = self.network_table.model()._data
|
|
1114
|
+
|
|
1115
|
+
# Create mask for rows where one column is any original node AND the other column is any neighbor
|
|
1116
|
+
mask = (
|
|
1117
|
+
(original_df.iloc[:, 0].isin(indices)) &
|
|
1118
|
+
(original_df.iloc[:, 1].isin(indices)))
|
|
1119
|
+
|
|
1120
|
+
# Filter the DataFrame to only include direct connections
|
|
1121
|
+
filtered_df = original_df[mask].copy()
|
|
1122
|
+
|
|
1123
|
+
# Create new model with filtered DataFrame and update selection table
|
|
1124
|
+
new_model = PandasModel(filtered_df)
|
|
1125
|
+
self.selection_table.setModel(new_model)
|
|
1126
|
+
|
|
1127
|
+
# Switch to selection table
|
|
1128
|
+
self.selection_button.click()
|
|
1129
|
+
except:
|
|
1130
|
+
pass
|
|
1131
|
+
|
|
1109
1132
|
|
|
1110
1133
|
|
|
1111
1134
|
def create_mini_overlay(self, node_indices = None, edge_indices = None):
|
|
@@ -1297,32 +1320,40 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1297
1320
|
# Create measurement submenu
|
|
1298
1321
|
measure_menu = context_menu.addMenu("Measurements")
|
|
1299
1322
|
|
|
1300
|
-
# Distance measurement options
|
|
1301
1323
|
distance_menu = measure_menu.addMenu("Distance")
|
|
1302
1324
|
if self.current_point is None:
|
|
1303
1325
|
show_point_menu = distance_menu.addAction("Place First Point")
|
|
1304
1326
|
show_point_menu.triggered.connect(
|
|
1305
1327
|
lambda: self.place_distance_point(x_idx, y_idx, self.current_slice))
|
|
1306
|
-
|
|
1328
|
+
elif (self.current_point is not None and
|
|
1329
|
+
hasattr(self, 'measurement_mode') and
|
|
1330
|
+
self.measurement_mode == "distance"):
|
|
1307
1331
|
show_point_menu = distance_menu.addAction("Place Second Point")
|
|
1308
1332
|
show_point_menu.triggered.connect(
|
|
1309
1333
|
lambda: self.place_distance_point(x_idx, y_idx, self.current_slice))
|
|
1310
|
-
|
|
1334
|
+
|
|
1311
1335
|
# Angle measurement options
|
|
1312
1336
|
angle_menu = measure_menu.addMenu("Angle")
|
|
1313
1337
|
if self.current_point is None:
|
|
1314
1338
|
angle_first = angle_menu.addAction("Place First Point (A)")
|
|
1315
1339
|
angle_first.triggered.connect(
|
|
1316
1340
|
lambda: self.place_angle_point(x_idx, y_idx, self.current_slice))
|
|
1317
|
-
elif self.
|
|
1341
|
+
elif (self.current_point is not None and
|
|
1342
|
+
self.current_second_point is None and
|
|
1343
|
+
hasattr(self, 'measurement_mode') and
|
|
1344
|
+
self.measurement_mode == "angle"):
|
|
1318
1345
|
angle_second = angle_menu.addAction("Place Second Point (B - Vertex)")
|
|
1319
1346
|
angle_second.triggered.connect(
|
|
1320
1347
|
lambda: self.place_angle_point(x_idx, y_idx, self.current_slice))
|
|
1321
|
-
|
|
1348
|
+
elif (self.current_point is not None and
|
|
1349
|
+
self.current_second_point is not None and
|
|
1350
|
+
hasattr(self, 'measurement_mode') and
|
|
1351
|
+
self.measurement_mode == "angle"):
|
|
1322
1352
|
angle_third = angle_menu.addAction("Place Third Point (C)")
|
|
1323
1353
|
angle_third.triggered.connect(
|
|
1324
1354
|
lambda: self.place_angle_point(x_idx, y_idx, self.current_slice))
|
|
1325
1355
|
|
|
1356
|
+
|
|
1326
1357
|
show_remove_menu = measure_menu.addAction("Remove All Measurements")
|
|
1327
1358
|
show_remove_menu.triggered.connect(self.handle_remove_all_measurements)
|
|
1328
1359
|
|
|
@@ -1350,15 +1381,22 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1350
1381
|
except IndexError:
|
|
1351
1382
|
pass
|
|
1352
1383
|
|
|
1353
|
-
|
|
1354
1384
|
def place_distance_point(self, x, y, z):
|
|
1355
1385
|
"""Place a measurement point for distance measurement."""
|
|
1356
1386
|
if self.current_point is None:
|
|
1357
1387
|
# This is the first point
|
|
1358
1388
|
self.current_point = (x, y, z)
|
|
1359
|
-
|
|
1360
|
-
|
|
1389
|
+
|
|
1390
|
+
# Create and store the artists
|
|
1391
|
+
pt = self.ax.plot(x, y, 'yo', markersize=8)[0]
|
|
1392
|
+
txt = self.ax.text(x, y+5, f"D{self.current_pair_index}",
|
|
1361
1393
|
color='yellow', ha='center', va='bottom')
|
|
1394
|
+
|
|
1395
|
+
# Add to measurement_artists so they can be managed by update_display
|
|
1396
|
+
if not hasattr(self, 'measurement_artists'):
|
|
1397
|
+
self.measurement_artists = []
|
|
1398
|
+
self.measurement_artists.extend([pt, txt])
|
|
1399
|
+
|
|
1362
1400
|
self.canvas.draw()
|
|
1363
1401
|
self.measurement_mode = "distance"
|
|
1364
1402
|
else:
|
|
@@ -1372,21 +1410,28 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1372
1410
|
((z2-z1)*my_network.z_scale)**2)
|
|
1373
1411
|
distance2 = np.sqrt(((x2-x1))**2 + ((y2-y1))**2 + ((z2-z1))**2)
|
|
1374
1412
|
|
|
1375
|
-
# Store the point pair
|
|
1413
|
+
# Store the point pair with type indicator
|
|
1376
1414
|
self.measurement_points.append({
|
|
1377
1415
|
'pair_index': self.current_pair_index,
|
|
1378
1416
|
'point1': self.current_point,
|
|
1379
1417
|
'point2': (x2, y2, z2),
|
|
1380
1418
|
'distance': distance,
|
|
1381
|
-
'distance2': distance2
|
|
1419
|
+
'distance2': distance2,
|
|
1420
|
+
'type': 'distance' # Added type tracking
|
|
1382
1421
|
})
|
|
1383
1422
|
|
|
1384
|
-
# Draw second point and line
|
|
1385
|
-
self.ax.plot(x2, y2, 'yo', markersize=8)
|
|
1386
|
-
self.ax.text(x2, y2+5, f"D{self.current_pair_index}",
|
|
1423
|
+
# Draw second point and line, storing the artists
|
|
1424
|
+
pt2 = self.ax.plot(x2, y2, 'yo', markersize=8)[0]
|
|
1425
|
+
txt2 = self.ax.text(x2, y2+5, f"D{self.current_pair_index}",
|
|
1387
1426
|
color='yellow', ha='center', va='bottom')
|
|
1427
|
+
|
|
1428
|
+
# Add to measurement_artists
|
|
1429
|
+
self.measurement_artists.extend([pt2, txt2])
|
|
1430
|
+
|
|
1388
1431
|
if z1 == z2: # Only draw line if points are on same slice
|
|
1389
|
-
self.ax.plot([x1, x2], [y1, y2], 'r--', alpha=0.5)
|
|
1432
|
+
line = self.ax.plot([x1, x2], [y1, y2], 'r--', alpha=0.5)[0]
|
|
1433
|
+
self.measurement_artists.append(line)
|
|
1434
|
+
|
|
1390
1435
|
self.canvas.draw()
|
|
1391
1436
|
|
|
1392
1437
|
# Update measurement display
|
|
@@ -1399,12 +1444,19 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1399
1444
|
|
|
1400
1445
|
def place_angle_point(self, x, y, z):
|
|
1401
1446
|
"""Place a measurement point for angle measurement."""
|
|
1447
|
+
if not hasattr(self, 'measurement_artists'):
|
|
1448
|
+
self.measurement_artists = []
|
|
1449
|
+
|
|
1402
1450
|
if self.current_point is None:
|
|
1403
1451
|
# First point (A)
|
|
1404
1452
|
self.current_point = (x, y, z)
|
|
1405
|
-
|
|
1406
|
-
|
|
1453
|
+
|
|
1454
|
+
# Create and store artists
|
|
1455
|
+
pt = self.ax.plot(x, y, 'go', markersize=8)[0]
|
|
1456
|
+
txt = self.ax.text(x, y+5, f"A{self.current_trio_index}",
|
|
1407
1457
|
color='green', ha='center', va='bottom')
|
|
1458
|
+
self.measurement_artists.extend([pt, txt])
|
|
1459
|
+
|
|
1408
1460
|
self.canvas.draw()
|
|
1409
1461
|
self.measurement_mode = "angle"
|
|
1410
1462
|
|
|
@@ -1413,13 +1465,16 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1413
1465
|
self.current_second_point = (x, y, z)
|
|
1414
1466
|
x1, y1, z1 = self.current_point
|
|
1415
1467
|
|
|
1416
|
-
|
|
1417
|
-
self.ax.
|
|
1468
|
+
# Create and store artists
|
|
1469
|
+
pt = self.ax.plot(x, y, 'go', markersize=8)[0]
|
|
1470
|
+
txt = self.ax.text(x, y+5, f"B{self.current_trio_index}",
|
|
1418
1471
|
color='green', ha='center', va='bottom')
|
|
1472
|
+
self.measurement_artists.extend([pt, txt])
|
|
1419
1473
|
|
|
1420
1474
|
# Draw line from A to B
|
|
1421
1475
|
if z1 == z:
|
|
1422
|
-
self.ax.plot([x1, x], [y1, y], 'g--', alpha=0.7)
|
|
1476
|
+
line = self.ax.plot([x1, x], [y1, y], 'g--', alpha=0.7)[0]
|
|
1477
|
+
self.measurement_artists.append(line)
|
|
1423
1478
|
self.canvas.draw()
|
|
1424
1479
|
|
|
1425
1480
|
else:
|
|
@@ -1442,7 +1497,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1442
1497
|
**angle_data
|
|
1443
1498
|
})
|
|
1444
1499
|
|
|
1445
|
-
# Also add the two distances as separate pairs
|
|
1500
|
+
# Also add the two distances as separate pairs with type indicator
|
|
1446
1501
|
dist_ab = np.sqrt(((x2-x1)*my_network.xy_scale)**2 +
|
|
1447
1502
|
((y2-y1)*my_network.xy_scale)**2 +
|
|
1448
1503
|
((z2-z1)*my_network.z_scale)**2)
|
|
@@ -1459,24 +1514,28 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1459
1514
|
'point1': (x1, y1, z1),
|
|
1460
1515
|
'point2': (x2, y2, z2),
|
|
1461
1516
|
'distance': dist_ab,
|
|
1462
|
-
'distance2': dist_ab_voxel
|
|
1517
|
+
'distance2': dist_ab_voxel,
|
|
1518
|
+
'type': 'angle' # Added type tracking
|
|
1463
1519
|
},
|
|
1464
1520
|
{
|
|
1465
1521
|
'pair_index': f"B{self.current_trio_index}-C{self.current_trio_index}",
|
|
1466
1522
|
'point1': (x2, y2, z2),
|
|
1467
1523
|
'point2': (x3, y3, z3),
|
|
1468
1524
|
'distance': dist_bc,
|
|
1469
|
-
'distance2': dist_bc_voxel
|
|
1525
|
+
'distance2': dist_bc_voxel,
|
|
1526
|
+
'type': 'angle' # Added type tracking
|
|
1470
1527
|
}
|
|
1471
1528
|
])
|
|
1472
1529
|
|
|
1473
|
-
# Draw third point and line
|
|
1474
|
-
self.ax.plot(x3, y3, 'go', markersize=8)
|
|
1475
|
-
self.ax.text(x3, y3+5, f"C{self.current_trio_index}",
|
|
1530
|
+
# Draw third point and line, storing artists
|
|
1531
|
+
pt3 = self.ax.plot(x3, y3, 'go', markersize=8)[0]
|
|
1532
|
+
txt3 = self.ax.text(x3, y3+5, f"C{self.current_trio_index}",
|
|
1476
1533
|
color='green', ha='center', va='bottom')
|
|
1534
|
+
self.measurement_artists.extend([pt3, txt3])
|
|
1477
1535
|
|
|
1478
1536
|
if z2 == z3: # Draw line from B to C if on same slice
|
|
1479
|
-
self.ax.plot([x2, x3], [y2, y3], 'g--', alpha=0.7)
|
|
1537
|
+
line = self.ax.plot([x2, x3], [y2, y3], 'g--', alpha=0.7)[0]
|
|
1538
|
+
self.measurement_artists.append(line)
|
|
1480
1539
|
self.canvas.draw()
|
|
1481
1540
|
|
|
1482
1541
|
# Update measurement display
|
|
@@ -1488,6 +1547,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1488
1547
|
self.current_trio_index += 1
|
|
1489
1548
|
self.measurement_mode = "angle"
|
|
1490
1549
|
|
|
1550
|
+
|
|
1491
1551
|
def calculate_3d_angle(self, point_a, point_b, point_c):
|
|
1492
1552
|
"""Calculate 3D angle at vertex B between points A-B-C."""
|
|
1493
1553
|
x1, y1, z1 = point_a
|
|
@@ -1802,23 +1862,27 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1802
1862
|
|
|
1803
1863
|
nodes = list(set(nodes))
|
|
1804
1864
|
|
|
1805
|
-
|
|
1806
|
-
original_df = self.network_table.model()._data
|
|
1865
|
+
try:
|
|
1807
1866
|
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1867
|
+
# Get the existing DataFrame from the model
|
|
1868
|
+
original_df = self.network_table.model()._data
|
|
1869
|
+
|
|
1870
|
+
# Create mask for rows for nodes in question
|
|
1871
|
+
mask = (
|
|
1872
|
+
(original_df.iloc[:, 0].isin(nodes) & original_df.iloc[:, 1].isin(nodes))
|
|
1873
|
+
)
|
|
1874
|
+
|
|
1875
|
+
# Filter the DataFrame to only include direct connections
|
|
1876
|
+
filtered_df = original_df[mask].copy()
|
|
1877
|
+
|
|
1878
|
+
# Create new model with filtered DataFrame and update selection table
|
|
1879
|
+
new_model = PandasModel(filtered_df)
|
|
1880
|
+
self.selection_table.setModel(new_model)
|
|
1881
|
+
|
|
1882
|
+
# Switch to selection table
|
|
1883
|
+
self.selection_button.click()
|
|
1884
|
+
except:
|
|
1885
|
+
pass
|
|
1822
1886
|
|
|
1823
1887
|
if edges:
|
|
1824
1888
|
edge_indices = filtered_df.iloc[:, 2].unique().tolist()
|
|
@@ -3755,6 +3819,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3755
3819
|
self.ax.clear()
|
|
3756
3820
|
self.ax.set_facecolor('black')
|
|
3757
3821
|
|
|
3822
|
+
# Reset measurement artists since we cleared the axes
|
|
3823
|
+
if not hasattr(self, 'measurement_artists'):
|
|
3824
|
+
self.measurement_artists = []
|
|
3825
|
+
else:
|
|
3826
|
+
self.measurement_artists = [] # Reset since ax.clear() removed all artists
|
|
3827
|
+
|
|
3758
3828
|
# Get original dimensions (before downsampling)
|
|
3759
3829
|
if hasattr(self, 'original_dims') and self.original_dims:
|
|
3760
3830
|
height, width = self.original_dims
|
|
@@ -3836,23 +3906,129 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3836
3906
|
for spine in self.ax.spines.values():
|
|
3837
3907
|
spine.set_color('black')
|
|
3838
3908
|
|
|
3839
|
-
# Add measurement points if they exist (
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3909
|
+
# Add measurement points if they exist (using the same logic as main update_display)
|
|
3910
|
+
if hasattr(self, 'measurement_points') and self.measurement_points:
|
|
3911
|
+
for point in self.measurement_points:
|
|
3912
|
+
x1, y1, z1 = point['point1']
|
|
3913
|
+
x2, y2, z2 = point['point2']
|
|
3914
|
+
pair_idx = point['pair_index']
|
|
3915
|
+
point_type = point.get('type', 'distance') # Default to distance for backward compatibility
|
|
3916
|
+
|
|
3917
|
+
# Determine colors based on type
|
|
3918
|
+
if point_type == 'angle':
|
|
3919
|
+
marker_color = 'go'
|
|
3920
|
+
text_color = 'green'
|
|
3921
|
+
line_color = 'g--'
|
|
3922
|
+
else: # distance
|
|
3923
|
+
marker_color = 'yo'
|
|
3924
|
+
text_color = 'yellow'
|
|
3925
|
+
line_color = 'r--'
|
|
3926
|
+
|
|
3927
|
+
# Check if points are in visible region and on current slice
|
|
3928
|
+
point1_visible = (z1 == self.current_slice and
|
|
3929
|
+
current_xlim[0] <= x1 <= current_xlim[1] and
|
|
3930
|
+
current_ylim[1] <= y1 <= current_ylim[0])
|
|
3931
|
+
point2_visible = (z2 == self.current_slice and
|
|
3932
|
+
current_xlim[0] <= x2 <= current_xlim[1] and
|
|
3933
|
+
current_ylim[1] <= y2 <= current_ylim[0])
|
|
3934
|
+
|
|
3935
|
+
# Draw individual points if they're on the current slice
|
|
3936
|
+
if point1_visible:
|
|
3937
|
+
pt1 = self.ax.plot(x1, y1, marker_color, markersize=8)[0]
|
|
3938
|
+
txt1 = self.ax.text(x1, y1+5, str(pair_idx), color=text_color, ha='center', va='bottom')
|
|
3939
|
+
self.measurement_artists.extend([pt1, txt1])
|
|
3853
3940
|
|
|
3854
|
-
|
|
3855
|
-
|
|
3941
|
+
if point2_visible:
|
|
3942
|
+
pt2 = self.ax.plot(x2, y2, marker_color, markersize=8)[0]
|
|
3943
|
+
txt2 = self.ax.text(x2, y2+5, str(pair_idx), color=text_color, ha='center', va='bottom')
|
|
3944
|
+
self.measurement_artists.extend([pt2, txt2])
|
|
3945
|
+
|
|
3946
|
+
# Draw connecting line if both points are on the same slice
|
|
3947
|
+
if z1 == z2 == self.current_slice and (point1_visible or point2_visible):
|
|
3948
|
+
line = self.ax.plot([x1, x2], [y1, y2], line_color, alpha=0.5)[0]
|
|
3949
|
+
self.measurement_artists.append(line)
|
|
3950
|
+
|
|
3951
|
+
# Handle angle measurements if they exist
|
|
3952
|
+
if hasattr(self, 'angle_measurements') and self.angle_measurements:
|
|
3953
|
+
for angle in self.angle_measurements:
|
|
3954
|
+
xa, ya, za = angle['point_a']
|
|
3955
|
+
xb, yb, zb = angle['point_b'] # vertex
|
|
3956
|
+
xc, yc, zc = angle['point_c']
|
|
3957
|
+
trio_idx = angle['trio_index']
|
|
3958
|
+
|
|
3959
|
+
# Check if points are on current slice and visible
|
|
3960
|
+
point_a_visible = (za == self.current_slice and
|
|
3961
|
+
current_xlim[0] <= xa <= current_xlim[1] and
|
|
3962
|
+
current_ylim[1] <= ya <= current_ylim[0])
|
|
3963
|
+
point_b_visible = (zb == self.current_slice and
|
|
3964
|
+
current_xlim[0] <= xb <= current_xlim[1] and
|
|
3965
|
+
current_ylim[1] <= yb <= current_ylim[0])
|
|
3966
|
+
point_c_visible = (zc == self.current_slice and
|
|
3967
|
+
current_xlim[0] <= xc <= current_xlim[1] and
|
|
3968
|
+
current_ylim[1] <= yc <= current_ylim[0])
|
|
3969
|
+
|
|
3970
|
+
# Draw points
|
|
3971
|
+
if point_a_visible:
|
|
3972
|
+
pt_a = self.ax.plot(xa, ya, 'go', markersize=8)[0]
|
|
3973
|
+
txt_a = self.ax.text(xa, ya+5, f"A{trio_idx}", color='green', ha='center', va='bottom')
|
|
3974
|
+
self.measurement_artists.extend([pt_a, txt_a])
|
|
3975
|
+
|
|
3976
|
+
if point_b_visible:
|
|
3977
|
+
pt_b = self.ax.plot(xb, yb, 'go', markersize=8)[0]
|
|
3978
|
+
txt_b = self.ax.text(xb, yb+5, f"B{trio_idx}", color='green', ha='center', va='bottom')
|
|
3979
|
+
self.measurement_artists.extend([pt_b, txt_b])
|
|
3980
|
+
|
|
3981
|
+
if point_c_visible:
|
|
3982
|
+
pt_c = self.ax.plot(xc, yc, 'go', markersize=8)[0]
|
|
3983
|
+
txt_c = self.ax.text(xc, yc+5, f"C{trio_idx}", color='green', ha='center', va='bottom')
|
|
3984
|
+
self.measurement_artists.extend([pt_c, txt_c])
|
|
3985
|
+
|
|
3986
|
+
# Draw lines only if points are on current slice
|
|
3987
|
+
if za == zb == self.current_slice and (point_a_visible or point_b_visible):
|
|
3988
|
+
line_ab = self.ax.plot([xa, xb], [ya, yb], 'g--', alpha=0.7)[0]
|
|
3989
|
+
self.measurement_artists.append(line_ab)
|
|
3990
|
+
|
|
3991
|
+
if zb == zc == self.current_slice and (point_b_visible or point_c_visible):
|
|
3992
|
+
line_bc = self.ax.plot([xb, xc], [yb, yc], 'g--', alpha=0.7)[0]
|
|
3993
|
+
self.measurement_artists.append(line_bc)
|
|
3994
|
+
|
|
3995
|
+
# Handle any partial measurements in progress (individual points without pairs yet)
|
|
3996
|
+
if hasattr(self, 'current_point') and self.current_point is not None:
|
|
3997
|
+
x, y, z = self.current_point
|
|
3998
|
+
if z == self.current_slice:
|
|
3999
|
+
if hasattr(self, 'measurement_mode') and self.measurement_mode == "angle":
|
|
4000
|
+
# Show green for angle mode
|
|
4001
|
+
pt = self.ax.plot(x, y, 'go', markersize=8)[0]
|
|
4002
|
+
if hasattr(self, 'current_trio_index'):
|
|
4003
|
+
txt = self.ax.text(x, y+5, f"A{self.current_trio_index}", color='green', ha='center', va='bottom')
|
|
4004
|
+
else:
|
|
4005
|
+
txt = self.ax.text(x, y+5, "A", color='green', ha='center', va='bottom')
|
|
4006
|
+
else:
|
|
4007
|
+
# Show yellow for distance mode (default)
|
|
4008
|
+
pt = self.ax.plot(x, y, 'yo', markersize=8)[0]
|
|
4009
|
+
if hasattr(self, 'current_pair_index'):
|
|
4010
|
+
txt = self.ax.text(x, y+5, f"D{self.current_pair_index}", color='yellow', ha='center', va='bottom')
|
|
4011
|
+
else:
|
|
4012
|
+
txt = self.ax.text(x, y+5, "D", color='yellow', ha='center', va='bottom')
|
|
4013
|
+
self.measurement_artists.extend([pt, txt])
|
|
4014
|
+
|
|
4015
|
+
# Handle second point in angle measurements
|
|
4016
|
+
if hasattr(self, 'current_second_point') and self.current_second_point is not None:
|
|
4017
|
+
x, y, z = self.current_second_point
|
|
4018
|
+
if z == self.current_slice:
|
|
4019
|
+
pt = self.ax.plot(x, y, 'go', markersize=8)[0]
|
|
4020
|
+
if hasattr(self, 'current_trio_index'):
|
|
4021
|
+
txt = self.ax.text(x, y+5, f"B{self.current_trio_index}", color='green', ha='center', va='bottom')
|
|
4022
|
+
else:
|
|
4023
|
+
txt = self.ax.text(x, y+5, "B", color='green', ha='center', va='bottom')
|
|
4024
|
+
self.measurement_artists.extend([pt, txt])
|
|
4025
|
+
|
|
4026
|
+
# Draw line from A to B if both are on current slice
|
|
4027
|
+
if (hasattr(self, 'current_point') and self.current_point is not None and
|
|
4028
|
+
self.current_point[2] == self.current_slice):
|
|
4029
|
+
x1, y1, z1 = self.current_point
|
|
4030
|
+
line = self.ax.plot([x1, x], [y1, y], 'g--', alpha=0.7)[0]
|
|
4031
|
+
self.measurement_artists.append(line)
|
|
3856
4032
|
|
|
3857
4033
|
#self.canvas.setCursor(Qt.CursorShape.ClosedHandCursor)
|
|
3858
4034
|
|
|
@@ -3968,7 +4144,34 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3968
4144
|
if len(self.clicked_values['edges']):
|
|
3969
4145
|
self.highlight_value_in_tables(self.clicked_values['edges'][-1])
|
|
3970
4146
|
self.handle_info('edge')
|
|
3971
|
-
|
|
4147
|
+
|
|
4148
|
+
try:
|
|
4149
|
+
if len(self.clicked_values['nodes']) > 0 or len(self.clicked_values['edges']) > 0: # Check if we have any nodes selected
|
|
4150
|
+
|
|
4151
|
+
old_nodes = copy.deepcopy(self.clicked_values['nodes'])
|
|
4152
|
+
|
|
4153
|
+
# Get the existing DataFrame from the model
|
|
4154
|
+
original_df = self.network_table.model()._data
|
|
4155
|
+
|
|
4156
|
+
# Create mask for rows where one column is any original node AND the other column is any neighbor
|
|
4157
|
+
mask = (
|
|
4158
|
+
((original_df.iloc[:, 0].isin(self.clicked_values['nodes'])) &
|
|
4159
|
+
(original_df.iloc[:, 1].isin(self.clicked_values['nodes']))) |
|
|
4160
|
+
(original_df.iloc[:, 2].isin(self.clicked_values['edges']))
|
|
4161
|
+
)
|
|
4162
|
+
|
|
4163
|
+
# Filter the DataFrame to only include direct connections
|
|
4164
|
+
filtered_df = original_df[mask].copy()
|
|
4165
|
+
|
|
4166
|
+
# Create new model with filtered DataFrame and update selection table
|
|
4167
|
+
new_model = PandasModel(filtered_df)
|
|
4168
|
+
self.selection_table.setModel(new_model)
|
|
4169
|
+
|
|
4170
|
+
# Switch to selection table
|
|
4171
|
+
self.selection_button.click()
|
|
4172
|
+
except:
|
|
4173
|
+
pass
|
|
4174
|
+
|
|
3972
4175
|
elif not self.selecting and self.selection_start: # If we had a click but never started selection
|
|
3973
4176
|
# Handle as a normal click
|
|
3974
4177
|
self.on_mouse_click(event)
|
|
@@ -4514,7 +4717,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4514
4717
|
|
|
4515
4718
|
|
|
4516
4719
|
# Add after your other buttons
|
|
4517
|
-
self.popup_button = QPushButton("⤴")
|
|
4720
|
+
self.popup_button = QPushButton("⤴")
|
|
4518
4721
|
self.popup_button.setFixedSize(40, 40)
|
|
4519
4722
|
self.popup_button.setToolTip("Pop out canvas")
|
|
4520
4723
|
self.popup_button.clicked.connect(self.popup_canvas)
|
|
@@ -6175,8 +6378,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6175
6378
|
if self.resume:
|
|
6176
6379
|
self.machine_window.segmentation_worker.resume()
|
|
6177
6380
|
self.resume = False
|
|
6178
|
-
if self.prev_down != self.downsample_factor:
|
|
6179
|
-
self.validate_downsample_input(text = self.prev_down)
|
|
6180
6381
|
|
|
6181
6382
|
if self.static_background is not None:
|
|
6182
6383
|
# Your existing virtual strokes conversion logic
|
|
@@ -6233,10 +6434,13 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6233
6434
|
for img in list(self.ax.get_images()):
|
|
6234
6435
|
img.remove()
|
|
6235
6436
|
# Clear measurement points
|
|
6236
|
-
|
|
6237
|
-
artist.
|
|
6238
|
-
|
|
6239
|
-
|
|
6437
|
+
if hasattr(self, 'measurement_artists'):
|
|
6438
|
+
for artist in self.measurement_artists:
|
|
6439
|
+
try:
|
|
6440
|
+
artist.remove()
|
|
6441
|
+
except:
|
|
6442
|
+
pass # Artist might already be removed
|
|
6443
|
+
self.measurement_artists = [] # Reset the list
|
|
6240
6444
|
# Determine the current view bounds (either from preserve_zoom or current state)
|
|
6241
6445
|
if preserve_zoom:
|
|
6242
6446
|
current_xlim, current_ylim = preserve_zoom
|
|
@@ -6303,7 +6507,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6303
6507
|
return cropped[::factor, ::factor, :]
|
|
6304
6508
|
else:
|
|
6305
6509
|
return cropped
|
|
6306
|
-
|
|
6307
6510
|
|
|
6308
6511
|
# Update channel images efficiently with cropping and downsampling
|
|
6309
6512
|
for channel in range(4):
|
|
@@ -6378,10 +6581,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6378
6581
|
|
|
6379
6582
|
im = self.ax.imshow(normalized_image, alpha=0.7, cmap=custom_cmap,
|
|
6380
6583
|
vmin=0, vmax=1, extent=crop_extent)
|
|
6381
|
-
|
|
6382
6584
|
# Handle preview, overlays, and measurements (apply cropping here too)
|
|
6383
|
-
#if self.preview and not called:
|
|
6384
|
-
# self.create_highlight_overlay_slice(self.targs, bounds=self.bounds)
|
|
6385
6585
|
|
|
6386
6586
|
# Overlay handling (optimized with cropping and downsampling)
|
|
6387
6587
|
if self.mini_overlay and self.highlight and self.machine_window is None:
|
|
@@ -6403,34 +6603,88 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6403
6603
|
[(0, 0, 0, 0), (1, 1, 0, 1), (0, 0.7, 1, 1)])
|
|
6404
6604
|
self.ax.imshow(display_highlight, cmap=highlight_cmap, vmin=0, vmax=2, alpha=0.3, extent=crop_extent)
|
|
6405
6605
|
|
|
6406
|
-
# Redraw measurement points efficiently
|
|
6606
|
+
# Redraw measurement points efficiently
|
|
6407
6607
|
# Only draw points that are within the visible region for additional performance
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
|
|
6416
|
-
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
|
|
6424
|
-
|
|
6608
|
+
|
|
6609
|
+
if hasattr(self, 'measurement_points') and self.measurement_points:
|
|
6610
|
+
for point in self.measurement_points:
|
|
6611
|
+
x1, y1, z1 = point['point1']
|
|
6612
|
+
x2, y2, z2 = point['point2']
|
|
6613
|
+
pair_idx = point['pair_index']
|
|
6614
|
+
point_type = point.get('type', 'distance') # Default to distance for backward compatibility
|
|
6615
|
+
|
|
6616
|
+
# Determine colors based on type
|
|
6617
|
+
if point_type == 'angle':
|
|
6618
|
+
marker_color = 'go'
|
|
6619
|
+
text_color = 'green'
|
|
6620
|
+
line_color = 'g--'
|
|
6621
|
+
else: # distance
|
|
6622
|
+
marker_color = 'yo'
|
|
6623
|
+
text_color = 'yellow'
|
|
6624
|
+
line_color = 'r--'
|
|
6425
6625
|
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
6626
|
+
# Check if points are in visible region and on current slice
|
|
6627
|
+
point1_visible = (z1 == self.current_slice and
|
|
6628
|
+
current_xlim[0] <= x1 <= current_xlim[1] and
|
|
6629
|
+
current_ylim[1] <= y1 <= current_ylim[0])
|
|
6630
|
+
point2_visible = (z2 == self.current_slice and
|
|
6631
|
+
current_xlim[0] <= x2 <= current_xlim[1] and
|
|
6632
|
+
current_ylim[1] <= y2 <= current_ylim[0])
|
|
6633
|
+
|
|
6634
|
+
# Always draw individual points if they're on the current slice (even without lines)
|
|
6635
|
+
if point1_visible:
|
|
6636
|
+
pt1 = self.ax.plot(x1, y1, marker_color, markersize=8)[0]
|
|
6637
|
+
txt1 = self.ax.text(x1, y1+5, str(pair_idx), color=text_color, ha='center', va='bottom')
|
|
6638
|
+
self.measurement_artists.extend([pt1, txt1])
|
|
6639
|
+
|
|
6640
|
+
if point2_visible:
|
|
6641
|
+
pt2 = self.ax.plot(x2, y2, marker_color, markersize=8)[0]
|
|
6642
|
+
txt2 = self.ax.text(x2, y2+5, str(pair_idx), color=text_color, ha='center', va='bottom')
|
|
6643
|
+
self.measurement_artists.extend([pt2, txt2])
|
|
6644
|
+
|
|
6645
|
+
# Only draw connecting line if both points are on the same slice AND visible
|
|
6646
|
+
if z1 == z2 == self.current_slice and (point1_visible or point2_visible):
|
|
6647
|
+
line = self.ax.plot([x1, x2], [y1, y2], line_color, alpha=0.5)[0]
|
|
6648
|
+
self.measurement_artists.append(line)
|
|
6649
|
+
|
|
6650
|
+
# Also handle any partial measurements in progress (individual points without pairs yet)
|
|
6651
|
+
# This shows individual points even when a measurement isn't complete
|
|
6652
|
+
if hasattr(self, 'current_point') and self.current_point is not None:
|
|
6653
|
+
x, y, z = self.current_point
|
|
6654
|
+
if z == self.current_slice:
|
|
6655
|
+
if hasattr(self, 'measurement_mode') and self.measurement_mode == "angle":
|
|
6656
|
+
# Show green for angle mode
|
|
6657
|
+
pt = self.ax.plot(x, y, 'go', markersize=8)[0]
|
|
6658
|
+
if hasattr(self, 'current_trio_index'):
|
|
6659
|
+
txt = self.ax.text(x, y+5, f"A{self.current_trio_index}", color='green', ha='center', va='bottom')
|
|
6660
|
+
else:
|
|
6661
|
+
txt = self.ax.text(x, y+5, "A", color='green', ha='center', va='bottom')
|
|
6662
|
+
else:
|
|
6663
|
+
# Show yellow for distance mode (default)
|
|
6664
|
+
pt = self.ax.plot(x, y, 'yo', markersize=8)[0]
|
|
6665
|
+
if hasattr(self, 'current_pair_index'):
|
|
6666
|
+
txt = self.ax.text(x, y+5, f"D{self.current_pair_index}", color='yellow', ha='center', va='bottom')
|
|
6667
|
+
else:
|
|
6668
|
+
txt = self.ax.text(x, y+5, "D", color='yellow', ha='center', va='bottom')
|
|
6669
|
+
self.measurement_artists.extend([pt, txt])
|
|
6670
|
+
|
|
6671
|
+
# Handle second point in angle measurements
|
|
6672
|
+
if hasattr(self, 'current_second_point') and self.current_second_point is not None:
|
|
6673
|
+
x, y, z = self.current_second_point
|
|
6674
|
+
if z == self.current_slice:
|
|
6675
|
+
pt = self.ax.plot(x, y, 'go', markersize=8)[0]
|
|
6676
|
+
if hasattr(self, 'current_trio_index'):
|
|
6677
|
+
txt = self.ax.text(x, y+5, f"B{self.current_trio_index}", color='green', ha='center', va='bottom')
|
|
6678
|
+
else:
|
|
6679
|
+
txt = self.ax.text(x, y+5, "B", color='green', ha='center', va='bottom')
|
|
6680
|
+
self.measurement_artists.extend([pt, txt])
|
|
6430
6681
|
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6682
|
+
# Draw line from A to B if both are on current slice
|
|
6683
|
+
if (hasattr(self, 'current_point') and self.current_point is not None and
|
|
6684
|
+
self.current_point[2] == self.current_slice):
|
|
6685
|
+
x1, y1, z1 = self.current_point
|
|
6686
|
+
line = self.ax.plot([x1, x], [y1, y], 'g--', alpha=0.7)[0]
|
|
6687
|
+
self.measurement_artists.append(line)
|
|
6434
6688
|
|
|
6435
6689
|
# Store current view limits for next update
|
|
6436
6690
|
self.ax._current_xlim = current_xlim
|
|
@@ -6449,9 +6703,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6449
6703
|
if reset_resize:
|
|
6450
6704
|
self.resizing = False
|
|
6451
6705
|
|
|
6452
|
-
#
|
|
6706
|
+
# draw_idle
|
|
6453
6707
|
self.canvas.draw_idle()
|
|
6454
6708
|
|
|
6709
|
+
|
|
6455
6710
|
except Exception as e:
|
|
6456
6711
|
pass
|
|
6457
6712
|
#import traceback
|
|
@@ -12210,13 +12465,23 @@ class HoleDialog(QDialog):
|
|
|
12210
12465
|
borders = self.borders.isChecked()
|
|
12211
12466
|
headon = self.headon.isChecked()
|
|
12212
12467
|
sep_holes = self.sep_holes.isChecked()
|
|
12468
|
+
|
|
12469
|
+
if borders:
|
|
12213
12470
|
|
|
12214
|
-
|
|
12215
|
-
|
|
12216
|
-
|
|
12217
|
-
|
|
12218
|
-
|
|
12219
|
-
|
|
12471
|
+
# Call dilate method with parameters
|
|
12472
|
+
result = n3d.fill_holes_3d_old(
|
|
12473
|
+
active_data,
|
|
12474
|
+
head_on = headon,
|
|
12475
|
+
fill_borders = borders
|
|
12476
|
+
)
|
|
12477
|
+
|
|
12478
|
+
else:
|
|
12479
|
+
# Call dilate method with parameters
|
|
12480
|
+
result = n3d.fill_holes_3d(
|
|
12481
|
+
active_data,
|
|
12482
|
+
head_on = headon,
|
|
12483
|
+
fill_borders = borders
|
|
12484
|
+
)
|
|
12220
12485
|
|
|
12221
12486
|
if not sep_holes:
|
|
12222
12487
|
self.parent().load_channel(self.parent().active_channel, result, True)
|
|
@@ -13108,7 +13373,7 @@ class GenNodesDialog(QDialog):
|
|
|
13108
13373
|
|
|
13109
13374
|
if my_network.edges is None and my_network.nodes is not None:
|
|
13110
13375
|
self.parent().load_channel(1, my_network.nodes, data = True)
|
|
13111
|
-
self.parent().delete_channel(0,
|
|
13376
|
+
self.parent().delete_channel(0, False)
|
|
13112
13377
|
# Get directory (None if empty)
|
|
13113
13378
|
#directory = self.directory.text() if self.directory.text() else None
|
|
13114
13379
|
|
|
@@ -13340,7 +13605,7 @@ class BranchDialog(QDialog):
|
|
|
13340
13605
|
|
|
13341
13606
|
if my_network.edges is None and my_network.nodes is not None:
|
|
13342
13607
|
self.parent().load_channel(1, my_network.nodes, data = True)
|
|
13343
|
-
self.parent().delete_channel(0,
|
|
13608
|
+
self.parent().delete_channel(0, False)
|
|
13344
13609
|
|
|
13345
13610
|
original_shape = my_network.edges.shape
|
|
13346
13611
|
original_array = copy.deepcopy(my_network.edges)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nettracer3d
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Scripts for intializing and analyzing networks from segmentations of three dimensional images.
|
|
5
5
|
Author-email: Liam McLaughlin <liamm@wustl.edu>
|
|
6
6
|
Project-URL: Documentation, https://nettracer3d.readthedocs.io/en/latest/
|
|
@@ -110,7 +110,6 @@ McLaughlin, L., Zhang, B., Sharma, S. et al. Three dimensional multiscalar neuro
|
|
|
110
110
|
|
|
111
111
|
NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
|
|
112
112
|
|
|
113
|
-
-- Version 0.
|
|
113
|
+
-- Version 1.0.1 Updates --
|
|
114
114
|
|
|
115
|
-
*
|
|
116
|
-
* Similarly, tables that have the format node id column:numerical values can now be used liberally to threshold the nodes, meaning most outputs of network analysis can be used to threshold nodes.
|
|
115
|
+
* Bug fixes, mainly
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|