nettracer3d 1.0.0__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-1.0.0/src/nettracer3d.egg-info → nettracer3d-1.0.1}/PKG-INFO +3 -4
- {nettracer3d-1.0.0 → nettracer3d-1.0.1}/README.md +2 -3
- {nettracer3d-1.0.0 → nettracer3d-1.0.1}/pyproject.toml +1 -1
- {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/community_extractor.py +24 -8
- {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/neighborhoods.py +48 -30
- {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/nettracer.py +57 -1
- {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/nettracer_gui.py +338 -117
- {nettracer3d-1.0.0 → nettracer3d-1.0.1/src/nettracer3d.egg-info}/PKG-INFO +3 -4
- {nettracer3d-1.0.0 → nettracer3d-1.0.1}/LICENSE +0 -0
- {nettracer3d-1.0.0 → nettracer3d-1.0.1}/setup.cfg +0 -0
- {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/__init__.py +0 -0
- {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/cellpose_manager.py +0 -0
- {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/excelotron.py +0 -0
- {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/modularity.py +0 -0
- {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/morphology.py +0 -0
- {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/network_analysis.py +0 -0
- {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/network_draw.py +0 -0
- {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/node_draw.py +0 -0
- {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/painting.py +0 -0
- {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/proximity.py +0 -0
- {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/run.py +0 -0
- {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/segmenter.py +0 -0
- {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/segmenter_GPU.py +0 -0
- {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/simple_network.py +0 -0
- {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/smart_dilate.py +0 -0
- {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d.egg-info/SOURCES.txt +0 -0
- {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d.egg-info/dependency_links.txt +0 -0
- {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d.egg-info/entry_points.txt +0 -0
- {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d.egg-info/requires.txt +0 -0
- {nettracer3d-1.0.0 → 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: 1.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 1.0.
|
|
113
|
+
-- Version 1.0.1 Updates --
|
|
114
114
|
|
|
115
|
-
*
|
|
116
|
-
* And other minor adjustments/bug fixes
|
|
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 1.0.
|
|
68
|
+
-- Version 1.0.1 Updates --
|
|
69
69
|
|
|
70
|
-
*
|
|
71
|
-
* And other minor adjustments/bug fixes
|
|
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:
|
|
@@ -1320,32 +1320,40 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1320
1320
|
# Create measurement submenu
|
|
1321
1321
|
measure_menu = context_menu.addMenu("Measurements")
|
|
1322
1322
|
|
|
1323
|
-
# Distance measurement options
|
|
1324
1323
|
distance_menu = measure_menu.addMenu("Distance")
|
|
1325
1324
|
if self.current_point is None:
|
|
1326
1325
|
show_point_menu = distance_menu.addAction("Place First Point")
|
|
1327
1326
|
show_point_menu.triggered.connect(
|
|
1328
1327
|
lambda: self.place_distance_point(x_idx, y_idx, self.current_slice))
|
|
1329
|
-
|
|
1328
|
+
elif (self.current_point is not None and
|
|
1329
|
+
hasattr(self, 'measurement_mode') and
|
|
1330
|
+
self.measurement_mode == "distance"):
|
|
1330
1331
|
show_point_menu = distance_menu.addAction("Place Second Point")
|
|
1331
1332
|
show_point_menu.triggered.connect(
|
|
1332
1333
|
lambda: self.place_distance_point(x_idx, y_idx, self.current_slice))
|
|
1333
|
-
|
|
1334
|
+
|
|
1334
1335
|
# Angle measurement options
|
|
1335
1336
|
angle_menu = measure_menu.addMenu("Angle")
|
|
1336
1337
|
if self.current_point is None:
|
|
1337
1338
|
angle_first = angle_menu.addAction("Place First Point (A)")
|
|
1338
1339
|
angle_first.triggered.connect(
|
|
1339
1340
|
lambda: self.place_angle_point(x_idx, y_idx, self.current_slice))
|
|
1340
|
-
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"):
|
|
1341
1345
|
angle_second = angle_menu.addAction("Place Second Point (B - Vertex)")
|
|
1342
1346
|
angle_second.triggered.connect(
|
|
1343
1347
|
lambda: self.place_angle_point(x_idx, y_idx, self.current_slice))
|
|
1344
|
-
|
|
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"):
|
|
1345
1352
|
angle_third = angle_menu.addAction("Place Third Point (C)")
|
|
1346
1353
|
angle_third.triggered.connect(
|
|
1347
1354
|
lambda: self.place_angle_point(x_idx, y_idx, self.current_slice))
|
|
1348
1355
|
|
|
1356
|
+
|
|
1349
1357
|
show_remove_menu = measure_menu.addAction("Remove All Measurements")
|
|
1350
1358
|
show_remove_menu.triggered.connect(self.handle_remove_all_measurements)
|
|
1351
1359
|
|
|
@@ -1373,15 +1381,22 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1373
1381
|
except IndexError:
|
|
1374
1382
|
pass
|
|
1375
1383
|
|
|
1376
|
-
|
|
1377
1384
|
def place_distance_point(self, x, y, z):
|
|
1378
1385
|
"""Place a measurement point for distance measurement."""
|
|
1379
1386
|
if self.current_point is None:
|
|
1380
1387
|
# This is the first point
|
|
1381
1388
|
self.current_point = (x, y, z)
|
|
1382
|
-
|
|
1383
|
-
|
|
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}",
|
|
1384
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
|
+
|
|
1385
1400
|
self.canvas.draw()
|
|
1386
1401
|
self.measurement_mode = "distance"
|
|
1387
1402
|
else:
|
|
@@ -1395,21 +1410,28 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1395
1410
|
((z2-z1)*my_network.z_scale)**2)
|
|
1396
1411
|
distance2 = np.sqrt(((x2-x1))**2 + ((y2-y1))**2 + ((z2-z1))**2)
|
|
1397
1412
|
|
|
1398
|
-
# Store the point pair
|
|
1413
|
+
# Store the point pair with type indicator
|
|
1399
1414
|
self.measurement_points.append({
|
|
1400
1415
|
'pair_index': self.current_pair_index,
|
|
1401
1416
|
'point1': self.current_point,
|
|
1402
1417
|
'point2': (x2, y2, z2),
|
|
1403
1418
|
'distance': distance,
|
|
1404
|
-
'distance2': distance2
|
|
1419
|
+
'distance2': distance2,
|
|
1420
|
+
'type': 'distance' # Added type tracking
|
|
1405
1421
|
})
|
|
1406
1422
|
|
|
1407
|
-
# Draw second point and line
|
|
1408
|
-
self.ax.plot(x2, y2, 'yo', markersize=8)
|
|
1409
|
-
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}",
|
|
1410
1426
|
color='yellow', ha='center', va='bottom')
|
|
1427
|
+
|
|
1428
|
+
# Add to measurement_artists
|
|
1429
|
+
self.measurement_artists.extend([pt2, txt2])
|
|
1430
|
+
|
|
1411
1431
|
if z1 == z2: # Only draw line if points are on same slice
|
|
1412
|
-
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
|
+
|
|
1413
1435
|
self.canvas.draw()
|
|
1414
1436
|
|
|
1415
1437
|
# Update measurement display
|
|
@@ -1422,12 +1444,19 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1422
1444
|
|
|
1423
1445
|
def place_angle_point(self, x, y, z):
|
|
1424
1446
|
"""Place a measurement point for angle measurement."""
|
|
1447
|
+
if not hasattr(self, 'measurement_artists'):
|
|
1448
|
+
self.measurement_artists = []
|
|
1449
|
+
|
|
1425
1450
|
if self.current_point is None:
|
|
1426
1451
|
# First point (A)
|
|
1427
1452
|
self.current_point = (x, y, z)
|
|
1428
|
-
|
|
1429
|
-
|
|
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}",
|
|
1430
1457
|
color='green', ha='center', va='bottom')
|
|
1458
|
+
self.measurement_artists.extend([pt, txt])
|
|
1459
|
+
|
|
1431
1460
|
self.canvas.draw()
|
|
1432
1461
|
self.measurement_mode = "angle"
|
|
1433
1462
|
|
|
@@ -1436,13 +1465,16 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1436
1465
|
self.current_second_point = (x, y, z)
|
|
1437
1466
|
x1, y1, z1 = self.current_point
|
|
1438
1467
|
|
|
1439
|
-
|
|
1440
|
-
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}",
|
|
1441
1471
|
color='green', ha='center', va='bottom')
|
|
1472
|
+
self.measurement_artists.extend([pt, txt])
|
|
1442
1473
|
|
|
1443
1474
|
# Draw line from A to B
|
|
1444
1475
|
if z1 == z:
|
|
1445
|
-
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)
|
|
1446
1478
|
self.canvas.draw()
|
|
1447
1479
|
|
|
1448
1480
|
else:
|
|
@@ -1465,7 +1497,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1465
1497
|
**angle_data
|
|
1466
1498
|
})
|
|
1467
1499
|
|
|
1468
|
-
# Also add the two distances as separate pairs
|
|
1500
|
+
# Also add the two distances as separate pairs with type indicator
|
|
1469
1501
|
dist_ab = np.sqrt(((x2-x1)*my_network.xy_scale)**2 +
|
|
1470
1502
|
((y2-y1)*my_network.xy_scale)**2 +
|
|
1471
1503
|
((z2-z1)*my_network.z_scale)**2)
|
|
@@ -1482,24 +1514,28 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1482
1514
|
'point1': (x1, y1, z1),
|
|
1483
1515
|
'point2': (x2, y2, z2),
|
|
1484
1516
|
'distance': dist_ab,
|
|
1485
|
-
'distance2': dist_ab_voxel
|
|
1517
|
+
'distance2': dist_ab_voxel,
|
|
1518
|
+
'type': 'angle' # Added type tracking
|
|
1486
1519
|
},
|
|
1487
1520
|
{
|
|
1488
1521
|
'pair_index': f"B{self.current_trio_index}-C{self.current_trio_index}",
|
|
1489
1522
|
'point1': (x2, y2, z2),
|
|
1490
1523
|
'point2': (x3, y3, z3),
|
|
1491
1524
|
'distance': dist_bc,
|
|
1492
|
-
'distance2': dist_bc_voxel
|
|
1525
|
+
'distance2': dist_bc_voxel,
|
|
1526
|
+
'type': 'angle' # Added type tracking
|
|
1493
1527
|
}
|
|
1494
1528
|
])
|
|
1495
1529
|
|
|
1496
|
-
# Draw third point and line
|
|
1497
|
-
self.ax.plot(x3, y3, 'go', markersize=8)
|
|
1498
|
-
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}",
|
|
1499
1533
|
color='green', ha='center', va='bottom')
|
|
1534
|
+
self.measurement_artists.extend([pt3, txt3])
|
|
1500
1535
|
|
|
1501
1536
|
if z2 == z3: # Draw line from B to C if on same slice
|
|
1502
|
-
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)
|
|
1503
1539
|
self.canvas.draw()
|
|
1504
1540
|
|
|
1505
1541
|
# Update measurement display
|
|
@@ -1511,6 +1547,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1511
1547
|
self.current_trio_index += 1
|
|
1512
1548
|
self.measurement_mode = "angle"
|
|
1513
1549
|
|
|
1550
|
+
|
|
1514
1551
|
def calculate_3d_angle(self, point_a, point_b, point_c):
|
|
1515
1552
|
"""Calculate 3D angle at vertex B between points A-B-C."""
|
|
1516
1553
|
x1, y1, z1 = point_a
|
|
@@ -1825,23 +1862,27 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1825
1862
|
|
|
1826
1863
|
nodes = list(set(nodes))
|
|
1827
1864
|
|
|
1828
|
-
|
|
1829
|
-
original_df = self.network_table.model()._data
|
|
1865
|
+
try:
|
|
1830
1866
|
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
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
|
|
1845
1886
|
|
|
1846
1887
|
if edges:
|
|
1847
1888
|
edge_indices = filtered_df.iloc[:, 2].unique().tolist()
|
|
@@ -3778,6 +3819,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3778
3819
|
self.ax.clear()
|
|
3779
3820
|
self.ax.set_facecolor('black')
|
|
3780
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
|
+
|
|
3781
3828
|
# Get original dimensions (before downsampling)
|
|
3782
3829
|
if hasattr(self, 'original_dims') and self.original_dims:
|
|
3783
3830
|
height, width = self.original_dims
|
|
@@ -3859,23 +3906,129 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3859
3906
|
for spine in self.ax.spines.values():
|
|
3860
3907
|
spine.set_color('black')
|
|
3861
3908
|
|
|
3862
|
-
# Add measurement points if they exist (
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
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])
|
|
3940
|
+
|
|
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)
|
|
3876
3990
|
|
|
3877
|
-
|
|
3878
|
-
|
|
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)
|
|
3879
4032
|
|
|
3880
4033
|
#self.canvas.setCursor(Qt.CursorShape.ClosedHandCursor)
|
|
3881
4034
|
|
|
@@ -3992,30 +4145,33 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3992
4145
|
self.highlight_value_in_tables(self.clicked_values['edges'][-1])
|
|
3993
4146
|
self.handle_info('edge')
|
|
3994
4147
|
|
|
3995
|
-
|
|
4148
|
+
try:
|
|
4149
|
+
if len(self.clicked_values['nodes']) > 0 or len(self.clicked_values['edges']) > 0: # Check if we have any nodes selected
|
|
3996
4150
|
|
|
3997
|
-
|
|
4151
|
+
old_nodes = copy.deepcopy(self.clicked_values['nodes'])
|
|
3998
4152
|
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
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
|
+
|
|
4019
4175
|
elif not self.selecting and self.selection_start: # If we had a click but never started selection
|
|
4020
4176
|
# Handle as a normal click
|
|
4021
4177
|
self.on_mouse_click(event)
|
|
@@ -6278,8 +6434,13 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6278
6434
|
for img in list(self.ax.get_images()):
|
|
6279
6435
|
img.remove()
|
|
6280
6436
|
# Clear measurement points
|
|
6281
|
-
self
|
|
6282
|
-
|
|
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
|
|
6283
6444
|
# Determine the current view bounds (either from preserve_zoom or current state)
|
|
6284
6445
|
if preserve_zoom:
|
|
6285
6446
|
current_xlim, current_ylim = preserve_zoom
|
|
@@ -6346,7 +6507,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6346
6507
|
return cropped[::factor, ::factor, :]
|
|
6347
6508
|
else:
|
|
6348
6509
|
return cropped
|
|
6349
|
-
|
|
6350
6510
|
|
|
6351
6511
|
# Update channel images efficiently with cropping and downsampling
|
|
6352
6512
|
for channel in range(4):
|
|
@@ -6421,10 +6581,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6421
6581
|
|
|
6422
6582
|
im = self.ax.imshow(normalized_image, alpha=0.7, cmap=custom_cmap,
|
|
6423
6583
|
vmin=0, vmax=1, extent=crop_extent)
|
|
6424
|
-
|
|
6425
6584
|
# Handle preview, overlays, and measurements (apply cropping here too)
|
|
6426
|
-
#if self.preview and not called:
|
|
6427
|
-
# self.create_highlight_overlay_slice(self.targs, bounds=self.bounds)
|
|
6428
6585
|
|
|
6429
6586
|
# Overlay handling (optimized with cropping and downsampling)
|
|
6430
6587
|
if self.mini_overlay and self.highlight and self.machine_window is None:
|
|
@@ -6446,34 +6603,88 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6446
6603
|
[(0, 0, 0, 0), (1, 1, 0, 1), (0, 0.7, 1, 1)])
|
|
6447
6604
|
self.ax.imshow(display_highlight, cmap=highlight_cmap, vmin=0, vmax=2, alpha=0.3, extent=crop_extent)
|
|
6448
6605
|
|
|
6449
|
-
# Redraw measurement points efficiently
|
|
6606
|
+
# Redraw measurement points efficiently
|
|
6450
6607
|
# Only draw points that are within the visible region for additional performance
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6463
|
-
|
|
6464
|
-
|
|
6465
|
-
|
|
6466
|
-
|
|
6467
|
-
|
|
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--'
|
|
6468
6625
|
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
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])
|
|
6473
6681
|
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
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)
|
|
6477
6688
|
|
|
6478
6689
|
# Store current view limits for next update
|
|
6479
6690
|
self.ax._current_xlim = current_xlim
|
|
@@ -12254,13 +12465,23 @@ class HoleDialog(QDialog):
|
|
|
12254
12465
|
borders = self.borders.isChecked()
|
|
12255
12466
|
headon = self.headon.isChecked()
|
|
12256
12467
|
sep_holes = self.sep_holes.isChecked()
|
|
12468
|
+
|
|
12469
|
+
if borders:
|
|
12257
12470
|
|
|
12258
|
-
|
|
12259
|
-
|
|
12260
|
-
|
|
12261
|
-
|
|
12262
|
-
|
|
12263
|
-
|
|
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
|
+
)
|
|
12264
12485
|
|
|
12265
12486
|
if not sep_holes:
|
|
12266
12487
|
self.parent().load_channel(self.parent().active_channel, result, True)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nettracer3d
|
|
3
|
-
Version: 1.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 1.0.
|
|
113
|
+
-- Version 1.0.1 Updates --
|
|
114
114
|
|
|
115
|
-
*
|
|
116
|
-
* And other minor adjustments/bug fixes
|
|
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
|