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.

Files changed (30) hide show
  1. {nettracer3d-1.0.0/src/nettracer3d.egg-info → nettracer3d-1.0.1}/PKG-INFO +3 -4
  2. {nettracer3d-1.0.0 → nettracer3d-1.0.1}/README.md +2 -3
  3. {nettracer3d-1.0.0 → nettracer3d-1.0.1}/pyproject.toml +1 -1
  4. {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/community_extractor.py +24 -8
  5. {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/neighborhoods.py +48 -30
  6. {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/nettracer.py +57 -1
  7. {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/nettracer_gui.py +338 -117
  8. {nettracer3d-1.0.0 → nettracer3d-1.0.1/src/nettracer3d.egg-info}/PKG-INFO +3 -4
  9. {nettracer3d-1.0.0 → nettracer3d-1.0.1}/LICENSE +0 -0
  10. {nettracer3d-1.0.0 → nettracer3d-1.0.1}/setup.cfg +0 -0
  11. {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/__init__.py +0 -0
  12. {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/cellpose_manager.py +0 -0
  13. {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/excelotron.py +0 -0
  14. {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/modularity.py +0 -0
  15. {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/morphology.py +0 -0
  16. {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/network_analysis.py +0 -0
  17. {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/network_draw.py +0 -0
  18. {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/node_draw.py +0 -0
  19. {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/painting.py +0 -0
  20. {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/proximity.py +0 -0
  21. {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/run.py +0 -0
  22. {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/segmenter.py +0 -0
  23. {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/segmenter_GPU.py +0 -0
  24. {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/simple_network.py +0 -0
  25. {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d/smart_dilate.py +0 -0
  26. {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d.egg-info/SOURCES.txt +0 -0
  27. {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d.egg-info/dependency_links.txt +0 -0
  28. {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d.egg-info/entry_points.txt +0 -0
  29. {nettracer3d-1.0.0 → nettracer3d-1.0.1}/src/nettracer3d.egg-info/requires.txt +0 -0
  30. {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.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.0 Updates --
113
+ -- Version 1.0.1 Updates --
114
114
 
115
- * The 'network selection' table is now auto-populated when using the multiple-identity selector, and when using the node thresholder.
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.0 Updates --
68
+ -- Version 1.0.1 Updates --
69
69
 
70
- * The 'network selection' table is now auto-populated when using the multiple-identity selector, and when using the node thresholder.
71
- * And other minor adjustments/bug fixes
70
+ * Bug fixes, mainly
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nettracer3d"
3
- version = "1.0.0"
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:
@@ -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
- else:
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.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"):
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
- 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"):
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
- self.ax.plot(x, y, 'yo', markersize=8)
1383
- 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}",
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
- self.ax.plot(x, y, 'go', markersize=8)
1429
- 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}",
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
- self.ax.plot(x, y, 'go', markersize=8)
1440
- 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}",
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
- # Get the existing DataFrame from the model
1829
- original_df = self.network_table.model()._data
1865
+ try:
1830
1866
 
1831
- # Create mask for rows for nodes in question
1832
- mask = (
1833
- (original_df.iloc[:, 0].isin(nodes) & original_df.iloc[:, 1].isin(nodes))
1834
- )
1835
-
1836
- # Filter the DataFrame to only include direct connections
1837
- filtered_df = original_df[mask].copy()
1838
-
1839
- # Create new model with filtered DataFrame and update selection table
1840
- new_model = PandasModel(filtered_df)
1841
- self.selection_table.setModel(new_model)
1842
-
1843
- # Switch to selection table
1844
- 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
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 (coordinates remain in original space)
3863
- for point in self.measurement_points:
3864
- x1, y1, z1 = point['point1']
3865
- x2, y2, z2 = point['point2']
3866
- pair_idx = point['pair_index']
3867
-
3868
- if z1 == self.current_slice:
3869
- self.ax.plot(x1, y1, 'yo', markersize=8)
3870
- self.ax.text(x1, y1+5, str(pair_idx),
3871
- color='white', ha='center', va='bottom')
3872
- if z2 == self.current_slice:
3873
- self.ax.plot(x2, y2, 'yo', markersize=8)
3874
- self.ax.text(x2, y2+5, str(pair_idx),
3875
- 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])
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
- if z1 == z2 == self.current_slice:
3878
- self.ax.plot([x1, x2], [y1, y2], 'r--', alpha=0.5)
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
- if len(self.clicked_values['nodes']) > 0 or len(self.clicked_values['edges']) > 0: # Check if we have any nodes selected
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
- old_nodes = copy.deepcopy(self.clicked_values['nodes'])
4151
+ old_nodes = copy.deepcopy(self.clicked_values['nodes'])
3998
4152
 
3999
- # Get the existing DataFrame from the model
4000
- original_df = self.network_table.model()._data
4001
-
4002
- # Create mask for rows where one column is any original node AND the other column is any neighbor
4003
- mask = (
4004
- ((original_df.iloc[:, 0].isin(self.clicked_values['nodes'])) &
4005
- (original_df.iloc[:, 1].isin(self.clicked_values['nodes']))) |
4006
- (original_df.iloc[:, 2].isin(self.clicked_values['edges']))
4007
- )
4008
-
4009
- # Filter the DataFrame to only include direct connections
4010
- filtered_df = original_df[mask].copy()
4011
-
4012
- # Create new model with filtered DataFrame and update selection table
4013
- new_model = PandasModel(filtered_df)
4014
- self.selection_table.setModel(new_model)
4015
-
4016
- # Switch to selection table
4017
- self.selection_button.click()
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.measurement_artists.clear()
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 (no cropping needed - these are vector graphics)
6606
+ # Redraw measurement points efficiently
6450
6607
  # Only draw points that are within the visible region for additional performance
6451
- for point in self.measurement_points:
6452
- x1, y1, z1 = point['point1']
6453
- x2, y2, z2 = point['point2']
6454
- pair_idx = point['pair_index']
6455
-
6456
- # Check if points are in visible region
6457
- point1_visible = (z1 == self.current_slice and
6458
- current_xlim[0] <= x1 <= current_xlim[1] and
6459
- current_ylim[1] <= y1 <= current_ylim[0])
6460
- point2_visible = (z2 == self.current_slice and
6461
- current_xlim[0] <= x2 <= current_xlim[1] and
6462
- current_ylim[1] <= y2 <= current_ylim[0])
6463
-
6464
- if point1_visible:
6465
- pt1 = self.ax.plot(x1, y1, 'yo', markersize=8)[0]
6466
- txt1 = self.ax.text(x1, y1+5, str(pair_idx), color='white', ha='center', va='bottom')
6467
- 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--'
6468
6625
 
6469
- if point2_visible:
6470
- pt2 = self.ax.plot(x2, y2, 'yo', markersize=8)[0]
6471
- txt2 = self.ax.text(x2, y2+5, str(pair_idx), color='white', ha='center', va='bottom')
6472
- 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])
6473
6681
 
6474
- if z1 == z2 == self.current_slice and (point1_visible or point2_visible):
6475
- line = self.ax.plot([x1, x2], [y1, y2], 'r--', alpha=0.5)[0]
6476
- 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)
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
- # Call dilate method with parameters
12259
- result = n3d.fill_holes_3d(
12260
- active_data,
12261
- head_on = headon,
12262
- fill_borders = borders
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.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.0 Updates --
113
+ -- Version 1.0.1 Updates --
114
114
 
115
- * The 'network selection' table is now auto-populated when using the multiple-identity selector, and when using the node thresholder.
116
- * And other minor adjustments/bug fixes
115
+ * Bug fixes, mainly
File without changes
File without changes