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.

Files changed (30) hide show
  1. {nettracer3d-0.9.9/src/nettracer3d.egg-info → nettracer3d-1.0.1}/PKG-INFO +3 -4
  2. {nettracer3d-0.9.9 → nettracer3d-1.0.1}/README.md +2 -3
  3. {nettracer3d-0.9.9 → nettracer3d-1.0.1}/pyproject.toml +1 -1
  4. {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/community_extractor.py +24 -8
  5. {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/neighborhoods.py +48 -30
  6. {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/nettracer.py +57 -1
  7. {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/nettracer_gui.py +370 -105
  8. {nettracer3d-0.9.9 → nettracer3d-1.0.1/src/nettracer3d.egg-info}/PKG-INFO +3 -4
  9. {nettracer3d-0.9.9 → nettracer3d-1.0.1}/LICENSE +0 -0
  10. {nettracer3d-0.9.9 → nettracer3d-1.0.1}/setup.cfg +0 -0
  11. {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/__init__.py +0 -0
  12. {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/cellpose_manager.py +0 -0
  13. {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/excelotron.py +0 -0
  14. {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/modularity.py +0 -0
  15. {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/morphology.py +0 -0
  16. {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/network_analysis.py +0 -0
  17. {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/network_draw.py +0 -0
  18. {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/node_draw.py +0 -0
  19. {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/painting.py +0 -0
  20. {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/proximity.py +0 -0
  21. {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/run.py +0 -0
  22. {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/segmenter.py +0 -0
  23. {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/segmenter_GPU.py +0 -0
  24. {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/simple_network.py +0 -0
  25. {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d/smart_dilate.py +0 -0
  26. {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d.egg-info/SOURCES.txt +0 -0
  27. {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d.egg-info/dependency_links.txt +0 -0
  28. {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d.egg-info/entry_points.txt +0 -0
  29. {nettracer3d-0.9.9 → nettracer3d-1.0.1}/src/nettracer3d.egg-info/requires.txt +0 -0
  30. {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.9.9
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.9.9 Updates --
113
+ -- Version 1.0.1 Updates --
114
114
 
115
- * Tables can now be opened to the rightside upper widget if they are the right format.
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.9.9 Updates --
68
+ -- Version 1.0.1 Updates --
69
69
 
70
- * Tables can now be opened to the rightside upper widget if they are the right format.
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nettracer3d"
3
- version = "0.9.9"
3
+ version = "1.0.1"
4
4
  authors = [
5
5
  { name="Liam McLaughlin", email="liamm@wustl.edu" },
6
6
  ]
@@ -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
- """fast version using lookup table approach."""
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
- # Same setup as before
739
- communities = set(community_dict.values())
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 = generate_distinct_colors(len(communities))
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
- community_to_color = {comm: colors_rgba[i] for i, comm in enumerate(sorted_communities)}
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
- # Rest remains the same
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
- # Filter neighborhoods to only include cluster_ids that exist in our data
402
- filtered_neighborhoods = {node_id: neighborhood_id
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
- # Create a dummy labeled array just for the coloring function
407
- # We only need the coloring logic, not actual clustering
408
- dummy_array = np.array(cluster_ids)
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 colors using the community coloration method
411
- _, neighborhood_color_names = community_extractor.assign_community_colors(
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
- # Create color mapping for our points
416
- unique_neighborhoods = sorted(list(set(filtered_neighborhoods.values())))
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
- # Map each cluster to its neighborhood color
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 filtered_neighborhoods:
425
- neighborhood_id = filtered_neighborhoods[cluster_id]
426
- point_colors.append(neighborhood_to_color[neighborhood_id])
427
- neighborhood_labels.append(neighborhood_id)
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 filtered_neighborhoods:
471
- neighborhood_id = filtered_neighborhoods[cluster_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 unique_neighborhoods:
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 filtered_neighborhoods:
534
- neighborhood_id = filtered_neighborhoods[cluster_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 unique_neighborhoods:
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 filtered_neighborhoods:
589
- neighborhood_id = filtered_neighborhoods[cluster_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
- neighborhoods.visualize_cluster_composition_umap(umap_dict, id_set, neighborhoods = neighbor_group)
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
- else:
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.current_second_point is None:
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
- else:
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
- self.ax.plot(x, y, 'yo', markersize=8)
1360
- self.ax.text(x, y+5, f"D{self.current_pair_index}",
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
- self.ax.plot(x, y, 'go', markersize=8)
1406
- self.ax.text(x, y+5, f"A{self.current_trio_index}",
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
- self.ax.plot(x, y, 'go', markersize=8)
1417
- self.ax.text(x, y+5, f"B{self.current_trio_index}",
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
- # Get the existing DataFrame from the model
1806
- original_df = self.network_table.model()._data
1865
+ try:
1807
1866
 
1808
- # Create mask for rows for nodes in question
1809
- mask = (
1810
- (original_df.iloc[:, 0].isin(nodes) & original_df.iloc[:, 1].isin(nodes))
1811
- )
1812
-
1813
- # Filter the DataFrame to only include direct connections
1814
- filtered_df = original_df[mask].copy()
1815
-
1816
- # Create new model with filtered DataFrame and update selection table
1817
- new_model = PandasModel(filtered_df)
1818
- self.selection_table.setModel(new_model)
1819
-
1820
- # Switch to selection table
1821
- self.selection_button.click()
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 (coordinates remain in original space)
3840
- for point in self.measurement_points:
3841
- x1, y1, z1 = point['point1']
3842
- x2, y2, z2 = point['point2']
3843
- pair_idx = point['pair_index']
3844
-
3845
- if z1 == self.current_slice:
3846
- self.ax.plot(x1, y1, 'yo', markersize=8)
3847
- self.ax.text(x1, y1+5, str(pair_idx),
3848
- color='white', ha='center', va='bottom')
3849
- if z2 == self.current_slice:
3850
- self.ax.plot(x2, y2, 'yo', markersize=8)
3851
- self.ax.text(x2, y2+5, str(pair_idx),
3852
- color='white', ha='center', va='bottom')
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
- if z1 == z2 == self.current_slice:
3855
- self.ax.plot([x1, x2], [y1, y2], 'r--', alpha=0.5)
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("⤴") # or "🔗" or "⤴"
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
- for artist in self.measurement_artists:
6237
- artist.remove()
6238
- self.measurement_artists.clear()
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 (no cropping needed - these are vector graphics)
6606
+ # Redraw measurement points efficiently
6407
6607
  # Only draw points that are within the visible region for additional performance
6408
- for point in self.measurement_points:
6409
- x1, y1, z1 = point['point1']
6410
- x2, y2, z2 = point['point2']
6411
- pair_idx = point['pair_index']
6412
-
6413
- # Check if points are in visible region
6414
- point1_visible = (z1 == self.current_slice and
6415
- current_xlim[0] <= x1 <= current_xlim[1] and
6416
- current_ylim[1] <= y1 <= current_ylim[0])
6417
- point2_visible = (z2 == self.current_slice and
6418
- current_xlim[0] <= x2 <= current_xlim[1] and
6419
- current_ylim[1] <= y2 <= current_ylim[0])
6420
-
6421
- if point1_visible:
6422
- pt1 = self.ax.plot(x1, y1, 'yo', markersize=8)[0]
6423
- txt1 = self.ax.text(x1, y1+5, str(pair_idx), color='white', ha='center', va='bottom')
6424
- self.measurement_artists.extend([pt1, txt1])
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
- if point2_visible:
6427
- pt2 = self.ax.plot(x2, y2, 'yo', markersize=8)[0]
6428
- txt2 = self.ax.text(x2, y2+5, str(pair_idx), color='white', ha='center', va='bottom')
6429
- self.measurement_artists.extend([pt2, txt2])
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
- if z1 == z2 == self.current_slice and (point1_visible or point2_visible):
6432
- line = self.ax.plot([x1, x2], [y1, y2], 'r--', alpha=0.5)[0]
6433
- self.measurement_artists.append(line)
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
- # Use draw_idle for better performance
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
- # Call dilate method with parameters
12215
- result = n3d.fill_holes_3d(
12216
- active_data,
12217
- head_on = headon,
12218
- fill_borders = borders
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, True)
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, True)
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.9.9
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.9.9 Updates --
113
+ -- Version 1.0.1 Updates --
114
114
 
115
- * Tables can now be opened to the rightside upper widget if they are the right format.
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