nettracer3d 0.7.6__py3-none-any.whl → 0.7.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@ from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QG
5
5
  QFormLayout, QLineEdit, QPushButton, QFileDialog,
6
6
  QLabel, QComboBox, QMessageBox, QTableView, QInputDialog,
7
7
  QMenu, QTabWidget, QGroupBox)
8
- from PyQt6.QtCore import (QPoint, Qt, QAbstractTableModel, QTimer, QThread, pyqtSignal)
8
+ from PyQt6.QtCore import (QPoint, Qt, QAbstractTableModel, QTimer, QThread, pyqtSignal, QObject, QCoreApplication)
9
9
  import numpy as np
10
10
  import time
11
11
  from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
@@ -26,6 +26,7 @@ from concurrent.futures import ThreadPoolExecutor
26
26
  from functools import partial
27
27
  from nettracer3d import segmenter
28
28
  from nettracer3d import segmenter_GPU
29
+ from nettracer3d import excelotron
29
30
 
30
31
 
31
32
 
@@ -204,6 +205,7 @@ class ImageViewerWindow(QMainWindow):
204
205
  self.high_button.setChecked(True)
205
206
  buttons_layout.addWidget(self.high_button)
206
207
  self.highlight = True
208
+ self.needs_mini = False
207
209
 
208
210
  self.pen_button = QPushButton("🖊️")
209
211
  self.pen_button.setCheckable(True)
@@ -393,11 +395,14 @@ class ImageViewerWindow(QMainWindow):
393
395
 
394
396
  #self.canvas.mpl_connect('button_press_event', self.on_mouse_click)
395
397
 
396
- # Initialize measurement points tracking
398
+ # Initialize measurement tracking
397
399
  self.measurement_points = [] # List to store point pairs
398
- self.current_point = None # Store first point of current pair
400
+ self.angle_measurements = [] # NEW: List to store angle trios
401
+ self.current_point = None # Store first point of current pair/trio
402
+ self.current_second_point = None # Store second point when building trio
399
403
  self.current_pair_index = 0 # Track pair numbering
400
-
404
+ self.current_trio_index = 0 # Track trio numbering
405
+ self.measurement_mode = "distance" # "distance" or "angle" mode
401
406
 
402
407
  # Add these new methods for handling neighbors and components (FOR RIGHT CLICKIGN)
403
408
  self.show_neighbors_clicked = None
@@ -409,6 +414,11 @@ class ImageViewerWindow(QMainWindow):
409
414
  self.mini_overlay = False # If the program is currently drawing the overlay by frame this will be true
410
415
  self.mini_overlay_data = None #Actual data for mini overlay
411
416
  self.mini_thresh = (500*500*500) # Array volume to start using mini overlays for
417
+ self.shape = None
418
+
419
+ self.excel_manager = ExcelotronManager(self)
420
+ self.excel_manager.data_received.connect(self.handle_excel_data)
421
+ self.prev_coms = None
412
422
 
413
423
  def start_left_scroll(self):
414
424
  """Start scrolling left when left arrow is pressed."""
@@ -658,6 +668,18 @@ class ImageViewerWindow(QMainWindow):
658
668
  edge_indices (list): List of edge indices to highlight
659
669
  """
660
670
 
671
+ if not self.high_button.isChecked():
672
+
673
+ if len(self.clicked_values['edges']) > 0:
674
+ self.format_for_upperright_table(self.clicked_values['edges'], title = 'Selected Edges')
675
+ self.needs_mini = True
676
+ if len(self.clicked_values['nodes']) > 0:
677
+ self.format_for_upperright_table(self.clicked_values['nodes'], title = 'Selected Nodes')
678
+ self.needs_mini = True
679
+
680
+ return
681
+
682
+
661
683
  def process_chunk(chunk_data, indices_to_check):
662
684
  """Process a single chunk of the array to create highlight mask"""
663
685
  mask = np.isin(chunk_data, indices_to_check)
@@ -822,21 +844,38 @@ class ImageViewerWindow(QMainWindow):
822
844
  override_obj.triggered.connect(self.handle_override)
823
845
  context_menu.addMenu(highlight_menu)
824
846
 
825
- # Create measure menu
826
- measure_menu = QMenu("Measure", self)
827
-
847
+ # Create measurement submenu
848
+ measure_menu = context_menu.addMenu("Measurements")
849
+
850
+ # Distance measurement options
851
+ distance_menu = measure_menu.addMenu("Distance")
828
852
  if self.current_point is None:
829
- # If no point is placed, show option to place first point
830
- show_point_menu = measure_menu.addAction("Place Measurement Point")
853
+ show_point_menu = distance_menu.addAction("Place First Point")
831
854
  show_point_menu.triggered.connect(
832
- lambda: self.place_point(x_idx, y_idx, self.current_slice))
855
+ lambda: self.place_distance_point(x_idx, y_idx, self.current_slice))
833
856
  else:
834
- # If first point is placed, show option to place second point
835
- show_point_menu = measure_menu.addAction("Place Second Point")
857
+ show_point_menu = distance_menu.addAction("Place Second Point")
836
858
  show_point_menu.triggered.connect(
837
- lambda: self.place_point(x_idx, y_idx, self.current_slice))
838
-
839
- show_remove_menu = measure_menu.addAction("Remove Measurement Points")
859
+ lambda: self.place_distance_point(x_idx, y_idx, self.current_slice))
860
+
861
+ # Angle measurement options
862
+ angle_menu = measure_menu.addMenu("Angle")
863
+ if self.current_point is None:
864
+ angle_first = angle_menu.addAction("Place First Point (A)")
865
+ angle_first.triggered.connect(
866
+ lambda: self.place_angle_point(x_idx, y_idx, self.current_slice))
867
+ elif self.current_second_point is None:
868
+ angle_second = angle_menu.addAction("Place Second Point (B - Vertex)")
869
+ angle_second.triggered.connect(
870
+ lambda: self.place_angle_point(x_idx, y_idx, self.current_slice))
871
+ else:
872
+ angle_third = angle_menu.addAction("Place Third Point (C)")
873
+ angle_third.triggered.connect(
874
+ lambda: self.place_angle_point(x_idx, y_idx, self.current_slice))
875
+
876
+ show_remove_menu = measure_menu.addAction("Remove All Measurements")
877
+ show_remove_menu.triggered.connect(self.handle_remove_all_measurements)
878
+
840
879
  context_menu.addMenu(measure_menu)
841
880
 
842
881
  # Connect actions to callbacks
@@ -854,7 +893,6 @@ class ImageViewerWindow(QMainWindow):
854
893
  if self.highlight_overlay is not None or self.mini_overlay_data is not None:
855
894
  highlight_select = context_menu.addAction("Add highlight in network selection")
856
895
  highlight_select.triggered.connect(self.handle_highlight_select)
857
- show_remove_menu.triggered.connect(self.handle_remove_points)
858
896
 
859
897
  cursor_pos = QCursor.pos()
860
898
  context_menu.exec(cursor_pos)
@@ -863,24 +901,25 @@ class ImageViewerWindow(QMainWindow):
863
901
  pass
864
902
 
865
903
 
866
- def place_point(self, x, y, z):
867
- """Place a measurement point at the specified coordinates."""
904
+ def place_distance_point(self, x, y, z):
905
+ """Place a measurement point for distance measurement."""
868
906
  if self.current_point is None:
869
907
  # This is the first point
870
908
  self.current_point = (x, y, z)
871
909
  self.ax.plot(x, y, 'yo', markersize=8)
872
- # Add pair index label above the point
873
- self.ax.text(x, y+5, str(self.current_pair_index),
874
- color='white', ha='center', va='bottom')
910
+ self.ax.text(x, y+5, f"D{self.current_pair_index}",
911
+ color='yellow', ha='center', va='bottom')
875
912
  self.canvas.draw()
876
-
913
+ self.measurement_mode = "distance"
877
914
  else:
878
915
  # This is the second point
879
916
  x1, y1, z1 = self.current_point
880
917
  x2, y2, z2 = x, y, z
881
918
 
882
919
  # Calculate distance
883
- distance = np.sqrt(((x2-x1)*my_network.xy_scale)**2 + ((y2-y1)*my_network.xy_scale)**2 + ((z2-z1)*my_network.z_scale)**2)
920
+ distance = np.sqrt(((x2-x1)*my_network.xy_scale)**2 +
921
+ ((y2-y1)*my_network.xy_scale)**2 +
922
+ ((z2-z1)*my_network.z_scale)**2)
884
923
  distance2 = np.sqrt(((x2-x1))**2 + ((y2-y1))**2 + ((z2-z1))**2)
885
924
 
886
925
  # Store the point pair
@@ -894,9 +933,8 @@ class ImageViewerWindow(QMainWindow):
894
933
 
895
934
  # Draw second point and line
896
935
  self.ax.plot(x2, y2, 'yo', markersize=8)
897
- # Add pair index label above the second point
898
- self.ax.text(x2, y2+5, str(self.current_pair_index),
899
- color='white', ha='center', va='bottom')
936
+ self.ax.text(x2, y2+5, f"D{self.current_pair_index}",
937
+ color='yellow', ha='center', va='bottom')
900
938
  if z1 == z2: # Only draw line if points are on same slice
901
939
  self.ax.plot([x1, x2], [y1, y2], 'r--', alpha=0.5)
902
940
  self.canvas.draw()
@@ -907,46 +945,194 @@ class ImageViewerWindow(QMainWindow):
907
945
  # Reset for next pair
908
946
  self.current_point = None
909
947
  self.current_pair_index += 1
948
+ self.measurement_mode = "distance"
949
+
950
+ def place_angle_point(self, x, y, z):
951
+ """Place a measurement point for angle measurement."""
952
+ if self.current_point is None:
953
+ # First point (A)
954
+ self.current_point = (x, y, z)
955
+ self.ax.plot(x, y, 'go', markersize=8)
956
+ self.ax.text(x, y+5, f"A{self.current_trio_index}",
957
+ color='green', ha='center', va='bottom')
958
+ self.canvas.draw()
959
+ self.measurement_mode = "angle"
960
+
961
+ elif self.current_second_point is None:
962
+ # Second point (B - vertex)
963
+ self.current_second_point = (x, y, z)
964
+ x1, y1, z1 = self.current_point
965
+
966
+ self.ax.plot(x, y, 'go', markersize=8)
967
+ self.ax.text(x, y+5, f"B{self.current_trio_index}",
968
+ color='green', ha='center', va='bottom')
969
+
970
+ # Draw line from A to B
971
+ if z1 == z:
972
+ self.ax.plot([x1, x], [y1, y], 'g--', alpha=0.7)
973
+ self.canvas.draw()
974
+
975
+ else:
976
+ # Third point (C)
977
+ x1, y1, z1 = self.current_point # Point A
978
+ x2, y2, z2 = self.current_second_point # Point B (vertex)
979
+ x3, y3, z3 = x, y, z # Point C
980
+
981
+ # Calculate angles and distances
982
+ angle_data = self.calculate_3d_angle(
983
+ (x1, y1, z1), (x2, y2, z2), (x3, y3, z3)
984
+ )
985
+
986
+ # Store the trio
987
+ self.angle_measurements.append({
988
+ 'trio_index': self.current_trio_index,
989
+ 'point_a': (x1, y1, z1),
990
+ 'point_b': (x2, y2, z2), # vertex
991
+ 'point_c': (x3, y3, z3),
992
+ **angle_data
993
+ })
994
+
995
+ # Also add the two distances as separate pairs
996
+ dist_ab = np.sqrt(((x2-x1)*my_network.xy_scale)**2 +
997
+ ((y2-y1)*my_network.xy_scale)**2 +
998
+ ((z2-z1)*my_network.z_scale)**2)
999
+ dist_bc = np.sqrt(((x3-x2)*my_network.xy_scale)**2 +
1000
+ ((y3-y2)*my_network.xy_scale)**2 +
1001
+ ((z3-z2)*my_network.z_scale)**2)
1002
+
1003
+ dist_ab_voxel = np.sqrt((x2-x1)**2 + (y2-y1)**2 + (z2-z1)**2)
1004
+ dist_bc_voxel = np.sqrt((x3-x2)**2 + (y3-y2)**2 + (z3-z2)**2)
1005
+
1006
+ self.measurement_points.extend([
1007
+ {
1008
+ 'pair_index': f"A{self.current_trio_index}-B{self.current_trio_index}",
1009
+ 'point1': (x1, y1, z1),
1010
+ 'point2': (x2, y2, z2),
1011
+ 'distance': dist_ab,
1012
+ 'distance2': dist_ab_voxel
1013
+ },
1014
+ {
1015
+ 'pair_index': f"B{self.current_trio_index}-C{self.current_trio_index}",
1016
+ 'point1': (x2, y2, z2),
1017
+ 'point2': (x3, y3, z3),
1018
+ 'distance': dist_bc,
1019
+ 'distance2': dist_bc_voxel
1020
+ }
1021
+ ])
1022
+
1023
+ # Draw third point and line
1024
+ self.ax.plot(x3, y3, 'go', markersize=8)
1025
+ self.ax.text(x3, y3+5, f"C{self.current_trio_index}",
1026
+ color='green', ha='center', va='bottom')
1027
+
1028
+ if z2 == z3: # Draw line from B to C if on same slice
1029
+ self.ax.plot([x2, x3], [y2, y3], 'g--', alpha=0.7)
1030
+ self.canvas.draw()
1031
+
1032
+ # Update measurement display
1033
+ self.update_measurement_display()
1034
+
1035
+ # Reset for next trio
1036
+ self.current_point = None
1037
+ self.current_second_point = None
1038
+ self.current_trio_index += 1
1039
+ self.measurement_mode = "angle"
1040
+
1041
+ def calculate_3d_angle(self, point_a, point_b, point_c):
1042
+ """Calculate 3D angle at vertex B between points A-B-C."""
1043
+ x1, y1, z1 = point_a
1044
+ x2, y2, z2 = point_b # vertex
1045
+ x3, y3, z3 = point_c
1046
+
1047
+ # Apply scaling
1048
+ scaled_a = np.array([x1 * my_network.xy_scale, y1 * my_network.xy_scale, z1 * my_network.z_scale])
1049
+ scaled_b = np.array([x2 * my_network.xy_scale, y2 * my_network.xy_scale, z2 * my_network.z_scale])
1050
+ scaled_c = np.array([x3 * my_network.xy_scale, y3 * my_network.xy_scale, z3 * my_network.z_scale])
1051
+
1052
+ # Create vectors from vertex B
1053
+ vec_ba = scaled_a - scaled_b
1054
+ vec_bc = scaled_c - scaled_b
1055
+
1056
+ # Calculate angle using dot product
1057
+ dot_product = np.dot(vec_ba, vec_bc)
1058
+ magnitude_ba = np.linalg.norm(vec_ba)
1059
+ magnitude_bc = np.linalg.norm(vec_bc)
1060
+
1061
+ # Avoid division by zero
1062
+ if magnitude_ba == 0 or magnitude_bc == 0:
1063
+ return {'angle_degrees': 0}
1064
+
1065
+ cos_angle = dot_product / (magnitude_ba * magnitude_bc)
1066
+ cos_angle = np.clip(cos_angle, -1.0, 1.0) # Handle numerical errors
1067
+
1068
+ angle_radians = np.arccos(cos_angle)
1069
+ angle_degrees = np.degrees(angle_radians)
1070
+
1071
+ return {'angle_degrees': angle_degrees}
910
1072
 
911
- def handle_remove_points(self):
912
- """Remove all measurement points."""
1073
+ def handle_remove_all_measurements(self):
1074
+ """Remove all measurement points and angles."""
913
1075
  self.measurement_points = []
1076
+ self.angle_measurements = []
914
1077
  self.current_point = None
1078
+ self.current_second_point = None
915
1079
  self.current_pair_index = 0
1080
+ self.current_trio_index = 0
1081
+ self.measurement_mode = "distance"
916
1082
  self.update_display()
917
1083
  self.update_measurement_display()
918
1084
 
919
- # Modify the update_measurement_display method:
920
1085
  def update_measurement_display(self):
921
1086
  """Update the measurement information display in the top right widget."""
1087
+ # Distance measurements
922
1088
  if not self.measurement_points:
923
- # Create empty DataFrame with no specific headers
924
- df = pd.DataFrame()
1089
+ distance_df = pd.DataFrame()
925
1090
  else:
926
- # Create data for DataFrame with measurement-specific headers
927
- data = []
1091
+ distance_data = []
928
1092
  for point in self.measurement_points:
929
1093
  x1, y1, z1 = point['point1']
930
1094
  x2, y2, z2 = point['point2']
931
- data.append({
1095
+ distance_data.append({
932
1096
  'Pair ID': point['pair_index'],
933
1097
  'Point 1 (X,Y,Z)': f"({x1:.1f}, {y1:.1f}, {z1})",
934
1098
  'Point 2 (X,Y,Z)': f"({x2:.1f}, {y2:.1f}, {z2})",
935
1099
  'Scaled Distance': f"{point['distance']:.2f}",
936
1100
  'Voxel Distance': f"{point['distance2']:.2f}"
937
1101
  })
938
- df = pd.DataFrame(data)
1102
+ distance_df = pd.DataFrame(distance_data)
939
1103
 
940
- # Create new table for measurements
941
- table = CustomTableView(self)
942
- table.setModel(PandasModel(df))
1104
+ # Angle measurements
1105
+ if not self.angle_measurements:
1106
+ angle_df = pd.DataFrame()
1107
+ else:
1108
+ angle_data = []
1109
+ for angle in self.angle_measurements:
1110
+ xa, ya, za = angle['point_a']
1111
+ xb, yb, zb = angle['point_b']
1112
+ xc, yc, zc = angle['point_c']
1113
+ angle_data.append({
1114
+ 'Trio ID': f"A{angle['trio_index']}-B{angle['trio_index']}-C{angle['trio_index']}",
1115
+ 'Point A (X,Y,Z)': f"({xa:.1f}, {ya:.1f}, {za})",
1116
+ 'Point B (X,Y,Z)': f"({xb:.1f}, {yb:.1f}, {zb})",
1117
+ 'Point C (X,Y,Z)': f"({xc:.1f}, {yc:.1f}, {zc})",
1118
+ 'Angle (°)': f"{angle['angle_degrees']:.1f}"
1119
+ })
1120
+ angle_df = pd.DataFrame(angle_data)
943
1121
 
944
- # Add to tabbed widget
945
- self.tabbed_data.add_table("Measurements", table)
1122
+ # Create tables
1123
+ if not distance_df.empty:
1124
+ distance_table = CustomTableView(self)
1125
+ distance_table.setModel(PandasModel(distance_df))
1126
+ self.tabbed_data.add_table("Distance Measurements", distance_table)
1127
+ for column in range(distance_table.model().columnCount(None)):
1128
+ distance_table.resizeColumnToContents(column)
946
1129
 
947
- # Adjust column widths to content
948
- for column in range(table.model().columnCount(None)):
949
- table.resizeColumnToContents(column)
1130
+ if not angle_df.empty:
1131
+ angle_table = CustomTableView(self)
1132
+ angle_table.setModel(PandasModel(angle_df))
1133
+ self.tabbed_data.add_table("Angle Measurements", angle_table)
1134
+ for column in range(angle_table.model().columnCount(None)):
1135
+ angle_table.resizeColumnToContents(column)
950
1136
 
951
1137
 
952
1138
  def show_network_table(self):
@@ -1726,6 +1912,12 @@ class ImageViewerWindow(QMainWindow):
1726
1912
  self.highlight = self.high_button.isChecked()
1727
1913
  current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
1728
1914
  current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
1915
+
1916
+ if self.high_button.isChecked():
1917
+ if self.highlight_overlay is None and ((len(self.clicked_values['nodes']) + len(self.clicked_values['edges'])) > 0):
1918
+ if self.needs_mini:
1919
+ self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
1920
+ self.needs_mini = False
1729
1921
 
1730
1922
  self.update_display(preserve_zoom=(current_xlim, current_ylim))
1731
1923
 
@@ -2680,6 +2872,8 @@ class ImageViewerWindow(QMainWindow):
2680
2872
  load_action.triggered.connect(lambda checked, ch=i: self.load_channel(ch))
2681
2873
  load_action = load_menu.addAction("Load Network")
2682
2874
  load_action.triggered.connect(self.load_network)
2875
+ load_action = load_menu.addAction("Load From Excel Helper")
2876
+ load_action.triggered.connect(self.launch_excelotron)
2683
2877
  misc_menu = load_menu.addMenu("Load Misc Properties")
2684
2878
  load_action = misc_menu.addAction("Load Node IDs")
2685
2879
  load_action.triggered.connect(lambda: self.load_misc('Node Identities'))
@@ -2696,10 +2890,16 @@ class ImageViewerWindow(QMainWindow):
2696
2890
  network_menu = analysis_menu.addMenu("Network")
2697
2891
  netshow_action = network_menu.addAction("Show Network")
2698
2892
  netshow_action.triggered.connect(self.show_netshow_dialog)
2699
- partition_action = network_menu.addAction("Community Partition +Generic Community Stats")
2893
+ report_action = network_menu.addAction("Generic Network Report")
2894
+ report_action.triggered.connect(self.handle_report)
2895
+ partition_action = network_menu.addAction("Community Partition + Generic Community Stats")
2700
2896
  partition_action.triggered.connect(self.show_partition_dialog)
2701
- com_identity_action = network_menu.addAction("Identity Makeup of Network Communities (Weighted avg by community size)")
2897
+ com_identity_action = network_menu.addAction("Identity Makeup of Network Communities (and UMAP)")
2702
2898
  com_identity_action.triggered.connect(self.handle_com_id)
2899
+ com_neighbor_action = network_menu.addAction("Convert Network Communities into Neighborhoods?")
2900
+ com_neighbor_action.triggered.connect(self.handle_com_neighbor)
2901
+ com_cell_action = network_menu.addAction("Create Communities Based on Cuboidal Proximity Cells?")
2902
+ com_cell_action.triggered.connect(self.handle_com_cell)
2703
2903
  stats_menu = analysis_menu.addMenu("Stats")
2704
2904
  allstats_action = stats_menu.addAction("Calculate Generic Network Stats")
2705
2905
  allstats_action.triggered.connect(self.stats)
@@ -2711,8 +2911,10 @@ class ImageViewerWindow(QMainWindow):
2711
2911
  degree_dist_action.triggered.connect(self.show_degree_dist_dialog)
2712
2912
  neighbor_id_action = stats_menu.addAction("Identity Distribution of Neighbors")
2713
2913
  neighbor_id_action.triggered.connect(self.show_neighbor_id_dialog)
2714
- ripley_action = stats_menu.addAction("Clustering Analysis")
2914
+ ripley_action = stats_menu.addAction("Ripley Clustering Analysis")
2715
2915
  ripley_action.triggered.connect(self.show_ripley_dialog)
2916
+ heatmap_action = stats_menu.addAction("Community Cluster Heatmap")
2917
+ heatmap_action.triggered.connect(self.show_heatmap_dialog)
2716
2918
  vol_action = stats_menu.addAction("Calculate Volumes")
2717
2919
  vol_action.triggered.connect(self.volumes)
2718
2920
  rad_action = stats_menu.addAction("Calculate Radii")
@@ -2768,6 +2970,8 @@ class ImageViewerWindow(QMainWindow):
2768
2970
  thresh_action.triggered.connect(self.show_thresh_dialog)
2769
2971
  mask_action = image_menu.addAction("Mask Channel")
2770
2972
  mask_action.triggered.connect(self.show_mask_dialog)
2973
+ crop_action = image_menu.addAction("Crop Channels")
2974
+ crop_action.triggered.connect(self.show_crop_dialog)
2771
2975
  type_action = image_menu.addAction("Channel dtype")
2772
2976
  type_action.triggered.connect(self.show_type_dialog)
2773
2977
  skeletonize_action = image_menu.addAction("Skeletonize")
@@ -3135,6 +3339,11 @@ class ImageViewerWindow(QMainWindow):
3135
3339
  dialog = MaskDialog(self)
3136
3340
  dialog.exec()
3137
3341
 
3342
+ def show_crop_dialog(self):
3343
+ """Show the crop dialog"""
3344
+ dialog = CropDialog(self)
3345
+ dialog.exec()
3346
+
3138
3347
  def show_type_dialog(self):
3139
3348
  """Show the type dialog"""
3140
3349
  try:
@@ -3553,6 +3762,103 @@ class ImageViewerWindow(QMainWindow):
3553
3762
  f"Failed to load network: {str(e)}"
3554
3763
  )
3555
3764
 
3765
+ def launch_excelotron(self):
3766
+ """Method to launch Excelotron - call this from a button or menu"""
3767
+ self.excel_manager.launch()
3768
+
3769
+ def close_excelotron(self):
3770
+ """Method to close Excelotron"""
3771
+ self.excel_manager.close()
3772
+
3773
+ def handle_excel_data(self, data_dict, property_name):
3774
+ """Handle data received from Excelotron"""
3775
+ print(f"Received data for property: {property_name}")
3776
+ print(f"Data keys: {list(data_dict.keys())}")
3777
+
3778
+ if property_name == 'Node Centroids':
3779
+
3780
+ try:
3781
+
3782
+ ys = data_dict['Y']
3783
+ xs = data_dict['X']
3784
+ if 'Numerical IDs' in data_dict:
3785
+ nodes = data_dict['Numerical IDs']
3786
+ else:
3787
+ nodes = np.arange(1, len(ys) + 1)
3788
+
3789
+
3790
+ if 'Z' in data_dict:
3791
+ zs = data_dict['Z']
3792
+ else:
3793
+ zs = np.zeros(len(ys))
3794
+
3795
+ centroids = {}
3796
+
3797
+ for i in range(len(nodes)):
3798
+
3799
+ centroids[nodes[i]] = [int(zs[i]), int(ys[i]), int(xs[i])]
3800
+
3801
+ my_network.node_centroids = centroids
3802
+
3803
+ self.format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
3804
+
3805
+ print("Centroids succesfully set")
3806
+
3807
+ except Exception as e:
3808
+ print(f"Error: {e}")
3809
+
3810
+ elif property_name == 'Node Identities':
3811
+
3812
+ try:
3813
+
3814
+ idens = data_dict['Identity Column']
3815
+
3816
+ if 'Numerical IDs' in data_dict:
3817
+ nodes = data_dict['Numerical IDs']
3818
+ else:
3819
+ nodes = np.arange(1, len(idens) + 1)
3820
+
3821
+ identities = {}
3822
+
3823
+
3824
+ for i in range(len(nodes)):
3825
+
3826
+ identities[nodes[i]] = str(idens[i])
3827
+
3828
+ my_network.node_identities = identities
3829
+
3830
+ self.format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity', title = 'Node Identities')
3831
+
3832
+ print("Identities succesfully set")
3833
+
3834
+ except Exception as e:
3835
+ print(f"Error: {e}")
3836
+
3837
+ elif property_name == 'Node Communities':
3838
+
3839
+ try:
3840
+
3841
+ coms = data_dict['Community Identifier']
3842
+
3843
+ if 'Numerical IDs' in data_dict:
3844
+ nodes = data_dict['Numerical IDs']
3845
+ else:
3846
+ nodes = np.arange(1, len(coms) + 1)
3847
+
3848
+ communities = {}
3849
+
3850
+ for i in range(len(nodes)):
3851
+
3852
+ communities[nodes[i]] = [str(coms[i])]
3853
+
3854
+ my_network.communities = communities
3855
+
3856
+ self.format_for_upperright_table(my_network.communities, 'NodeID', 'CommunityID', title = 'Community Partition')
3857
+
3858
+ print("Communities succesfully set")
3859
+
3860
+ except Exception as e:
3861
+ print(f"Error: {e}")
3556
3862
 
3557
3863
 
3558
3864
  def set_active_channel(self, index):
@@ -3783,6 +4089,8 @@ class ImageViewerWindow(QMainWindow):
3783
4089
  except:
3784
4090
  pass
3785
4091
 
4092
+ self.shape = self.channel_data[channel_index].shape
4093
+
3786
4094
  self.update_display(reset_resize = reset_resize)
3787
4095
 
3788
4096
 
@@ -4278,24 +4586,62 @@ class ImageViewerWindow(QMainWindow):
4278
4586
  dialog = NetShowDialog(self)
4279
4587
  dialog.exec()
4280
4588
 
4589
+ def handle_report(self):
4590
+
4591
+ def invert_dict(d):
4592
+ inverted = {}
4593
+ for key, value in d.items():
4594
+ inverted.setdefault(value, []).append(key)
4595
+ return inverted
4596
+
4597
+ stats = {}
4598
+
4599
+ try:
4600
+ # Basic graph properties
4601
+ stats['num_nodes'] = my_network.network.number_of_nodes()
4602
+ stats['num_edges'] = my_network.network.number_of_edges()
4603
+ except:
4604
+ pass
4605
+
4606
+ try:
4607
+ idens = invert_dict(my_network.node_identities)
4608
+
4609
+ for iden, nodes in idens.items():
4610
+ stats[f'num_nodes_{iden}'] = len(nodes)
4611
+ except:
4612
+ pass
4613
+
4614
+ try:
4615
+
4616
+ coms = invert_dict(my_network.communities)
4617
+
4618
+ for com, nodes in coms.items():
4619
+ stats[f'num_nodes_community_{com}'] = len(nodes)
4620
+ except:
4621
+ pass
4622
+
4623
+ self.format_for_upperright_table(stats, title = 'Network Report')
4624
+
4625
+
4626
+
4281
4627
  def show_partition_dialog(self):
4282
4628
  dialog = PartitionDialog(self)
4283
4629
  dialog.exec()
4284
4630
 
4285
4631
  def handle_com_id(self):
4286
- if my_network.node_identities is None:
4287
- print("Node identities must be set")
4288
4632
 
4289
- if my_network.communities is None:
4290
- self.show_partition_dialog()
4633
+ dialog = ComIdDialog(self)
4634
+ dialog.exec()
4291
4635
 
4292
- if my_network.communities is None:
4293
- return
4636
+ def handle_com_neighbor(self):
4294
4637
 
4295
- info = my_network.community_id_info()
4638
+ dialog = ComNeighborDialog(self)
4639
+ dialog.exec()
4296
4640
 
4297
- self.format_for_upperright_table(info, 'Node Identity Type', 'Weighted Proportion in Communities', 'Weighted Average of Community Makeup')
4641
+ def handle_com_cell(self):
4298
4642
 
4643
+ dialog = ComCellDialog(self)
4644
+ dialog.exec()
4299
4645
 
4300
4646
  def show_radial_dialog(self):
4301
4647
  dialog = RadialDialog(self)
@@ -4313,6 +4659,10 @@ class ImageViewerWindow(QMainWindow):
4313
4659
  dialog = RipleyDialog(self)
4314
4660
  dialog.exec()
4315
4661
 
4662
+ def show_heatmap_dialog(self):
4663
+ dialog = HeatmapDialog(self)
4664
+ dialog.exec()
4665
+
4316
4666
  def show_random_dialog(self):
4317
4667
  dialog = RandomDialog(self)
4318
4668
  dialog.exec()
@@ -4346,6 +4696,21 @@ class ImageViewerWindow(QMainWindow):
4346
4696
  dialog = CodeDialog(self, sort = sort)
4347
4697
  dialog.exec()
4348
4698
 
4699
+ def closeEvent(self, event):
4700
+ """Override closeEvent to close all windows when main window closes"""
4701
+
4702
+ # Close all Qt windows
4703
+ QApplication.closeAllWindows()
4704
+
4705
+ # Close all matplotlib figures
4706
+ plt.close('all')
4707
+
4708
+ # Accept the close event
4709
+ event.accept()
4710
+
4711
+ # Force quit the application
4712
+ QCoreApplication.quit()
4713
+
4349
4714
 
4350
4715
 
4351
4716
  #TABLE RELATED:
@@ -5527,8 +5892,7 @@ class ArbitraryDialog(QDialog):
5527
5892
 
5528
5893
  except Exception as e:
5529
5894
  QMessageBox.critical(self, "Error", f"Error processing selections: {str(e)}")
5530
- import traceback
5531
- print(traceback.format_exc())
5895
+
5532
5896
 
5533
5897
  class Show3dDialog(QDialog):
5534
5898
  def __init__(self, parent=None):
@@ -5542,10 +5906,15 @@ class Show3dDialog(QDialog):
5542
5906
  layout.addRow("Downsample Factor (Optional to speed up display):", self.downsample)
5543
5907
 
5544
5908
  # Network Overlay checkbox (default True)
5545
- self.cubic = QPushButton("cubic")
5909
+ self.cubic = QPushButton("Cubic")
5546
5910
  self.cubic.setCheckable(True)
5547
5911
  self.cubic.setChecked(False)
5548
5912
  layout.addRow("Use cubic downsample (Slower but preserves shape better potentially)?", self.cubic)
5913
+
5914
+ self.box = QPushButton("Box")
5915
+ self.box.setCheckable(True)
5916
+ self.box.setChecked(False)
5917
+ layout.addRow("Include bounding box?", self.box)
5549
5918
 
5550
5919
  # Add Run button
5551
5920
  run_button = QPushButton("Show 3D")
@@ -5564,6 +5933,7 @@ class Show3dDialog(QDialog):
5564
5933
  downsample = None
5565
5934
 
5566
5935
  cubic = self.cubic.isChecked()
5936
+ box = self.box.isChecked()
5567
5937
 
5568
5938
  if cubic:
5569
5939
  order = 3
@@ -5596,7 +5966,7 @@ class Show3dDialog(QDialog):
5596
5966
  arrays_3d.append(self.parent().highlight_overlay)
5597
5967
  colors.append(color_template[4])
5598
5968
 
5599
- n3d.show_3d(arrays_3d, arrays_4d, down_factor = downsample, order = order, xy_scale = my_network.xy_scale, z_scale = my_network.z_scale, colors = colors)
5969
+ n3d.show_3d(arrays_3d, arrays_4d, down_factor = downsample, order = order, xy_scale = my_network.xy_scale, z_scale = my_network.z_scale, colors = colors, box = box)
5600
5970
 
5601
5971
  self.accept()
5602
5972
 
@@ -5972,7 +6342,7 @@ class PartitionDialog(QDialog):
5972
6342
  dostats = self.stats.isChecked()
5973
6343
 
5974
6344
  try:
5975
- seed = int(self.seed.text()) if self.seed.text() else None
6345
+ seed = int(self.seed.text()) if self.seed.text() else 42
5976
6346
  except:
5977
6347
  seed = None
5978
6348
 
@@ -5993,134 +6363,320 @@ class PartitionDialog(QDialog):
5993
6363
  except Exception as e:
5994
6364
  print(f"Error creating communities: {e}")
5995
6365
 
5996
-
5997
-
5998
- class RadialDialog(QDialog):
6366
+ class ComIdDialog(QDialog):
5999
6367
 
6000
6368
  def __init__(self, parent=None):
6001
6369
 
6002
6370
  super().__init__(parent)
6003
- self.setWindowTitle("Radial Parameters")
6371
+ self.setWindowTitle("Select Mode")
6004
6372
  self.setModal(True)
6005
6373
 
6006
6374
  layout = QFormLayout(self)
6007
6375
 
6008
- self.distance = QLineEdit("50")
6009
- layout.addRow("Bucket Distance for Searching For Node Neighbors (automatically scaled by xy and z scales):", self.distance)
6376
+ self.mode = QComboBox()
6377
+ self.mode.addItems(["Average Identities Per Community", "Weighted Average Identity of All Communities", ])
6378
+ self.mode.setCurrentIndex(0)
6379
+ layout.addRow("Mode", self.mode)
6380
+
6381
+ # umap checkbox (default True)
6382
+ self.umap = QPushButton("UMAP")
6383
+ self.umap.setCheckable(True)
6384
+ self.umap.setChecked(True)
6385
+ layout.addRow("Generate UMAP?:", self.umap)
6386
+
6387
+ # weighted checkbox (default True)
6388
+ self.label = QPushButton("Label")
6389
+ self.label.setCheckable(True)
6390
+ self.label.setChecked(False)
6391
+ layout.addRow("If using above - label UMAP points?:", self.label)
6010
6392
 
6011
- self.directory = QLineEdit("")
6012
- layout.addRow("Output Directory:", self.directory)
6013
6393
 
6014
6394
  # Add Run button
6015
- run_button = QPushButton("Get Radial Distribution")
6016
- run_button.clicked.connect(self.radial)
6395
+ run_button = QPushButton("Get Community ID Info")
6396
+ run_button.clicked.connect(self.run)
6017
6397
  layout.addWidget(run_button)
6018
6398
 
6019
- def radial(self):
6399
+ def run(self):
6020
6400
 
6021
6401
  try:
6022
6402
 
6023
- distance = float(self.distance.text()) if self.distance.text().strip() else 50
6403
+ if my_network.node_identities is None:
6404
+ print("Node identities must be set")
6024
6405
 
6025
- directory = str(self.distance.text()) if self.directory.text().strip() else None
6406
+ if my_network.communities is None:
6407
+ self.parent().show_partition_dialog()
6026
6408
 
6027
- if my_network.node_centroids is None:
6028
- self.parent().show_centroid_dialog()
6409
+ if my_network.communities is None:
6410
+ return
6029
6411
 
6030
- radial = my_network.radial_distribution(distance, directory = directory)
6412
+ mode = self.mode.currentIndex()
6031
6413
 
6032
- self.parent().format_for_upperright_table(radial, 'Radial Distance From Any Node', 'Average Number of Neighboring Nodes', title = 'Radial Distribution Analysis')
6414
+ umap = self.umap.isChecked()
6415
+ label = self.label.isChecked()
6416
+
6417
+ if mode == 1:
6418
+
6419
+ info = my_network.community_id_info()
6420
+
6421
+ self.parent().format_for_upperright_table(info, 'Node Identity Type', 'Weighted Proportion in Communities', 'Weighted Average of Community Makeup')
6422
+
6423
+ else:
6424
+
6425
+ info, names = my_network.community_id_info_per_com(umap = umap, label = label)
6426
+
6427
+ self.parent().format_for_upperright_table(info, 'Community', names, 'Average of Community Makeup')
6033
6428
 
6034
6429
  self.accept()
6035
6430
 
6036
6431
  except Exception as e:
6037
- print(f"An error occurred: {e}")
6038
6432
 
6039
- class DegreeDistDialog(QDialog):
6433
+ print(f"Error: {e}")
6434
+
6435
+
6436
+
6437
+ class ComNeighborDialog(QDialog):
6040
6438
 
6041
6439
  def __init__(self, parent=None):
6042
6440
 
6043
6441
  super().__init__(parent)
6044
- self.setWindowTitle("Degree Distribution Parameters")
6442
+ self.setWindowTitle("Reassign Communities Based on Identity Similarity?")
6045
6443
  self.setModal(True)
6046
6444
 
6047
6445
  layout = QFormLayout(self)
6048
6446
 
6049
- self.directory = QLineEdit("")
6050
- layout.addRow("Output Directory:", self.directory)
6447
+ self.neighborcount = QLineEdit("5")
6448
+ layout.addRow("Num Neighborhoods:", self.neighborcount)
6449
+
6450
+ self.seed = QLineEdit("")
6451
+ layout.addRow("Clustering Seed:", self.seed)
6452
+
6453
+ self.limit = QLineEdit("")
6454
+ layout.addRow("Min Community Size to be grouped (Smaller communities will be placed in neighborhood 0 - does not apply if empty)", self.limit)
6051
6455
 
6052
6456
  # Add Run button
6053
- run_button = QPushButton("Get Degree Distribution")
6054
- run_button.clicked.connect(self.degreedist)
6457
+ run_button = QPushButton("Get Communities")
6458
+ run_button.clicked.connect(self.run)
6055
6459
  layout.addWidget(run_button)
6056
6460
 
6057
- def degreedist(self):
6461
+ def run(self):
6058
6462
 
6059
6463
  try:
6060
6464
 
6061
- directory = str(self.distance.text()) if self.directory.text().strip() else None
6465
+ if my_network.node_identities is None:
6466
+ print("Node identities must be set")
6062
6467
 
6063
- degrees = my_network.degree_distribution(directory = directory)
6468
+ if my_network.communities is None:
6469
+ self.parent().show_partition_dialog()
6470
+
6471
+ if my_network.communities is None:
6472
+ return
6064
6473
 
6474
+ seed = float(self.seed.text()) if self.seed.text().strip() else 42
6065
6475
 
6066
- self.parent().format_for_upperright_table(degrees, 'Degree (k)', 'Proportion of nodes with degree (p(k))', title = 'Degree Distribution Analysis')
6476
+ limit = int(self.limit.text()) if self.limit.text().strip() else None
6477
+
6478
+
6479
+ neighborcount = int(self.neighborcount.text()) if self.neighborcount.text().strip() else 5
6480
+
6481
+ if self.parent().prev_coms is None:
6482
+
6483
+ self.parent().prev_coms = copy.deepcopy(my_network.communities)
6484
+ my_network.assign_neighborhoods(seed, neighborcount, limit = limit)
6485
+ else:
6486
+ my_network.assign_neighborhoods(seed, neighborcount, limit = limit, prev_coms = self.parent().prev_coms)
6487
+
6488
+ self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'NeighborhoodID', title = 'Neighborhood Partition')
6489
+
6490
+ print("Neighborhoods have been assigned to communities based on similarity")
6067
6491
 
6068
6492
  self.accept()
6069
6493
 
6070
6494
  except Exception as e:
6071
- print(f"An error occurred: {e}")
6072
6495
 
6073
- class NeighborIdentityDialog(QDialog):
6496
+ print(f"Error assigning neighborhoods: {e}")
6497
+
6498
+ class ComCellDialog(QDialog):
6074
6499
 
6075
6500
  def __init__(self, parent=None):
6076
6501
 
6077
6502
  super().__init__(parent)
6078
- self.setWindowTitle(f"Neighborhood Identity Distribution Parameters \n(Note - the same node is not included more than once as a neighbor even if it borders multiple nodes of the root ID)")
6503
+ self.setWindowTitle("Assign Communities Based on Proximity Within Cuboidal Cells?")
6079
6504
  self.setModal(True)
6080
6505
 
6081
6506
  layout = QFormLayout(self)
6082
6507
 
6083
- if my_network.node_identities is not None:
6084
- self.root = QComboBox()
6085
- self.root.addItems(list(set(my_network.node_identities.values())))
6086
- self.root.setCurrentIndex(0)
6087
- layout.addRow("Root Identity to Search for Neighbor's IDs (search uses nodes of this ID, finds what IDs they connect to", self.root)
6088
- else:
6089
- self.root = None
6090
-
6091
- self.directory = QLineEdit("")
6092
- layout.addRow("Output Directory:", self.directory)
6093
-
6094
- self.mode = QComboBox()
6095
- self.mode.addItems(["From Network - Based on Absolute Connectivity", "Use Labeled Nodes - Based on Morphological Neighborhood Densities"])
6096
- self.mode.setCurrentIndex(0)
6097
- layout.addRow("Mode", self.mode)
6508
+ self.size = QLineEdit("")
6509
+ layout.addRow("Cell Size:", self.size)
6098
6510
 
6099
- self.search = QLineEdit("")
6100
- layout.addRow("Search Radius (Ignore if using network):", self.search)
6511
+ self.xy_scale = QLineEdit(f"{my_network.xy_scale}")
6512
+ layout.addRow("xy scale:", self.xy_scale)
6101
6513
 
6102
- self.fastdil = QPushButton("Fast Dilate")
6103
- self.fastdil.setCheckable(True)
6104
- self.fastdil.setChecked(False)
6105
- layout.addRow("(If not using network) Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
6514
+ self.z_scale = QLineEdit(f"{my_network.z_scale}")
6515
+ layout.addRow("z scale:", self.z_scale)
6106
6516
 
6107
6517
  # Add Run button
6108
- run_button = QPushButton("Get Neighborhood Identity Distribution")
6109
- run_button.clicked.connect(self.neighborids)
6518
+ run_button = QPushButton("Get Neighborhoods (Note this overwrites current communities - save your coms first)")
6519
+ run_button.clicked.connect(self.run)
6110
6520
  layout.addWidget(run_button)
6111
6521
 
6112
- def neighborids(self):
6522
+ def run(self):
6113
6523
 
6114
6524
  try:
6115
6525
 
6116
- try:
6117
- root = self.root.currentText()
6118
- except:
6119
- pass
6526
+ size = float(self.size.text()) if self.size.text().strip() else None
6527
+ xy_scale = float(self.xy_scale.text()) if self.xy_scale.text().strip() else 1
6528
+ z_scale = float(self.z_scale.text()) if self.z_scale.text().strip() else 1
6120
6529
 
6121
- directory = self.directory.text() if self.directory.text().strip() else None
6122
-
6123
- mode = self.mode.currentIndex()
6530
+ if size is None:
6531
+ return
6532
+
6533
+ if my_network.node_centroids is None:
6534
+ self.parent().show_centroid_dialog()
6535
+ if my_network.node_centroids is None:
6536
+ return
6537
+
6538
+ my_network.community_cells(size = size, xy_scale = xy_scale, z_scale = z_scale)
6539
+
6540
+ self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'CommunityID')
6541
+
6542
+ self.accept()
6543
+
6544
+ except Exception as e:
6545
+
6546
+ print(f"Error: {e}")
6547
+
6548
+
6549
+
6550
+
6551
+
6552
+
6553
+
6554
+ class RadialDialog(QDialog):
6555
+
6556
+ def __init__(self, parent=None):
6557
+
6558
+ super().__init__(parent)
6559
+ self.setWindowTitle("Radial Parameters")
6560
+ self.setModal(True)
6561
+
6562
+ layout = QFormLayout(self)
6563
+
6564
+ self.distance = QLineEdit("50")
6565
+ layout.addRow("Bucket Distance for Searching For Node Neighbors (automatically scaled by xy and z scales):", self.distance)
6566
+
6567
+ self.directory = QLineEdit("")
6568
+ layout.addRow("Output Directory:", self.directory)
6569
+
6570
+ # Add Run button
6571
+ run_button = QPushButton("Get Radial Distribution")
6572
+ run_button.clicked.connect(self.radial)
6573
+ layout.addWidget(run_button)
6574
+
6575
+ def radial(self):
6576
+
6577
+ try:
6578
+
6579
+ distance = float(self.distance.text()) if self.distance.text().strip() else 50
6580
+
6581
+ directory = str(self.distance.text()) if self.directory.text().strip() else None
6582
+
6583
+ if my_network.node_centroids is None:
6584
+ self.parent().show_centroid_dialog()
6585
+
6586
+ radial = my_network.radial_distribution(distance, directory = directory)
6587
+
6588
+ self.parent().format_for_upperright_table(radial, 'Radial Distance From Any Node', 'Average Number of Neighboring Nodes', title = 'Radial Distribution Analysis')
6589
+
6590
+ self.accept()
6591
+
6592
+ except Exception as e:
6593
+ print(f"An error occurred: {e}")
6594
+
6595
+ class DegreeDistDialog(QDialog):
6596
+
6597
+ def __init__(self, parent=None):
6598
+
6599
+ super().__init__(parent)
6600
+ self.setWindowTitle("Degree Distribution Parameters")
6601
+ self.setModal(True)
6602
+
6603
+ layout = QFormLayout(self)
6604
+
6605
+ self.directory = QLineEdit("")
6606
+ layout.addRow("Output Directory:", self.directory)
6607
+
6608
+ # Add Run button
6609
+ run_button = QPushButton("Get Degree Distribution")
6610
+ run_button.clicked.connect(self.degreedist)
6611
+ layout.addWidget(run_button)
6612
+
6613
+ def degreedist(self):
6614
+
6615
+ try:
6616
+
6617
+ directory = str(self.distance.text()) if self.directory.text().strip() else None
6618
+
6619
+ degrees = my_network.degree_distribution(directory = directory)
6620
+
6621
+
6622
+ self.parent().format_for_upperright_table(degrees, 'Degree (k)', 'Proportion of nodes with degree (p(k))', title = 'Degree Distribution Analysis')
6623
+
6624
+ self.accept()
6625
+
6626
+ except Exception as e:
6627
+ print(f"An error occurred: {e}")
6628
+
6629
+ class NeighborIdentityDialog(QDialog):
6630
+
6631
+ def __init__(self, parent=None):
6632
+
6633
+ super().__init__(parent)
6634
+ self.setWindowTitle(f"Neighborhood Identity Distribution Parameters \n(Note - the same node is not included more than once as a neighbor even if it borders multiple nodes of the root ID)")
6635
+ self.setModal(True)
6636
+
6637
+ layout = QFormLayout(self)
6638
+
6639
+ if my_network.node_identities is not None:
6640
+ self.root = QComboBox()
6641
+ self.root.addItems(list(set(my_network.node_identities.values())))
6642
+ self.root.setCurrentIndex(0)
6643
+ layout.addRow("Root Identity to Search for Neighbor's IDs (search uses nodes of this ID, finds what IDs they connect to", self.root)
6644
+ else:
6645
+ self.root = None
6646
+
6647
+ self.directory = QLineEdit("")
6648
+ layout.addRow("Output Directory:", self.directory)
6649
+
6650
+ self.mode = QComboBox()
6651
+ self.mode.addItems(["From Network - Based on Absolute Connectivity", "Use Labeled Nodes - Based on Morphological Neighborhood Densities"])
6652
+ self.mode.setCurrentIndex(0)
6653
+ layout.addRow("Mode", self.mode)
6654
+
6655
+ self.search = QLineEdit("")
6656
+ layout.addRow("Search Radius (Ignore if using network):", self.search)
6657
+
6658
+ self.fastdil = QPushButton("Fast Dilate")
6659
+ self.fastdil.setCheckable(True)
6660
+ self.fastdil.setChecked(False)
6661
+ layout.addRow("(If not using network) Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
6662
+
6663
+ # Add Run button
6664
+ run_button = QPushButton("Get Neighborhood Identity Distribution")
6665
+ run_button.clicked.connect(self.neighborids)
6666
+ layout.addWidget(run_button)
6667
+
6668
+ def neighborids(self):
6669
+
6670
+ try:
6671
+
6672
+ try:
6673
+ root = self.root.currentText()
6674
+ except:
6675
+ pass
6676
+
6677
+ directory = self.directory.text() if self.directory.text().strip() else None
6678
+
6679
+ mode = self.mode.currentIndex()
6124
6680
 
6125
6681
  search = float(self.search.text()) if self.search.text().strip() else 0
6126
6682
 
@@ -6260,6 +6816,60 @@ class RipleyDialog(QDialog):
6260
6816
  print(traceback.format_exc())
6261
6817
  print(f"Error: {e}")
6262
6818
 
6819
+ class HeatmapDialog(QDialog):
6820
+
6821
+ def __init__(self, parent = None):
6822
+
6823
+ super().__init__(parent)
6824
+ self.setWindowTitle("Heatmap Parameters")
6825
+ self.setModal(True)
6826
+
6827
+ layout = QFormLayout(self)
6828
+
6829
+ self.nodecount = QLineEdit("")
6830
+ layout.addRow("(Optional) Total Number of Nodes?:", self.nodecount)
6831
+
6832
+
6833
+ # stats checkbox (default True)
6834
+ self.is3d = QPushButton("3D")
6835
+ self.is3d.setCheckable(True)
6836
+ self.is3d.setChecked(True)
6837
+ layout.addRow("Use 3D Plot (uncheck for 2D)?:", self.is3d)
6838
+
6839
+
6840
+ # Add Run button
6841
+ run_button = QPushButton("Run")
6842
+ run_button.clicked.connect(self.run)
6843
+ layout.addWidget(run_button)
6844
+
6845
+ def run(self):
6846
+
6847
+ nodecount = int(self.nodecount.text()) if self.nodecount.text().strip() else None
6848
+
6849
+ is3d = self.is3d.isChecked()
6850
+
6851
+
6852
+ if my_network.communities is None:
6853
+ if my_network.network is not None:
6854
+ self.parent().show_partition_dialog()
6855
+ else:
6856
+ self.parent().handle_com_cell()
6857
+ if my_network.communities is None:
6858
+ return
6859
+
6860
+ heat_dict = my_network.community_heatmap(num_nodes = nodecount, is3d = is3d)
6861
+
6862
+ self.parent().format_for_upperright_table(heat_dict, metric='Community', value='ln(Predicted Community Nodecount/Actual)', title="Community Heatmap")
6863
+
6864
+ self.accept()
6865
+
6866
+
6867
+
6868
+
6869
+
6870
+
6871
+
6872
+
6263
6873
  class RandomDialog(QDialog):
6264
6874
 
6265
6875
  def __init__(self, parent=None):
@@ -7316,7 +7926,7 @@ class SLabelDialog(QDialog):
7316
7926
  try:
7317
7927
 
7318
7928
  # Update both the display data and the network object
7319
- binary_array = sdl.smart_label(binary_array, label_array, directory = None, GPU = GPU, predownsample = down_factor)
7929
+ binary_array = sdl.smart_label(binary_array, label_array, directory = None, GPU = GPU, predownsample = down_factor, remove_template = True)
7320
7930
 
7321
7931
  label_array = sdl.invert_array(label_array)
7322
7932
 
@@ -7433,12 +8043,101 @@ class ThresholdDialog(QDialog):
7433
8043
  msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
7434
8044
  return msg.exec() == QMessageBox.StandardButton.Yes
7435
8045
 
8046
+ class ExcelotronManager(QObject):
8047
+ # Signal to emit when data is received from Excelotron
8048
+ data_received = pyqtSignal(dict, str) # dictionary, property_name
8049
+
8050
+ def __init__(self, parent=None):
8051
+ super().__init__(parent)
8052
+ self.excelotron_window = None
8053
+ self.last_data = None
8054
+ self.last_property = None
8055
+
8056
+ def launch(self):
8057
+ """Launch the Excelotron window"""
8058
+
8059
+ if self.excelotron_window is None:
8060
+ ExcelGUIClass = excelotron.main(standalone=False)
8061
+ self.excelotron_window = ExcelGUIClass()
8062
+ self.excelotron_window.data_exported.connect(self._on_data_exported)
8063
+ # Connect to both close event and destroyed signal
8064
+ self.excelotron_window.destroyed.connect(self._on_window_destroyed)
8065
+ self.excelotron_window.closeEvent = self._create_close_handler(self.excelotron_window.closeEvent)
8066
+ self.excelotron_window.show()
8067
+ else:
8068
+ self.excelotron_window.raise_()
8069
+ self.excelotron_window.activateWindow()
8070
+
8071
+ def _create_close_handler(self, original_close_event):
8072
+ """Create a close event handler that cleans up properly"""
8073
+ def close_handler(event):
8074
+ self._cleanup_window()
8075
+ original_close_event(event)
8076
+ return close_handler
8077
+
8078
+ def close(self):
8079
+ """Close the Excelotron window"""
8080
+ if self.excelotron_window is not None:
8081
+ self.excelotron_window.close()
8082
+ self._cleanup_window()
8083
+
8084
+ def _cleanup_window(self):
8085
+ """Properly cleanup the window reference"""
8086
+ if self.excelotron_window is not None:
8087
+ try:
8088
+ # Disconnect all signals to prevent issues
8089
+ self.excelotron_window.data_exported.disconnect()
8090
+ self.excelotron_window.destroyed.disconnect()
8091
+ except:
8092
+ pass # Ignore if already disconnected
8093
+
8094
+ # Schedule for deletion
8095
+ self.excelotron_window.deleteLater()
8096
+ self.excelotron_window = None
8097
+
8098
+ def is_open(self):
8099
+ """Check if Excelotron window is open"""
8100
+ is_open = self.excelotron_window is not None
8101
+ return is_open
8102
+
8103
+ def _on_data_exported(self, data_dict, property_name):
8104
+ """Internal slot to handle data from Excelotron"""
8105
+ self.last_data = data_dict
8106
+ self.last_property = property_name
8107
+ # Re-emit the signal for parent to handle
8108
+ self.data_received.emit(data_dict, property_name)
8109
+
8110
+ def _on_window_destroyed(self):
8111
+ """Handle when the Excelotron window is destroyed/closed"""
8112
+ self.excelotron_window = None
8113
+
8114
+ def get_last_data(self):
8115
+ """Get the last exported data"""
8116
+ return self.last_data, self.last_property
7436
8117
 
7437
8118
  class MachineWindow(QMainWindow):
7438
8119
 
7439
8120
  def __init__(self, parent=None, GPU = False):
7440
8121
  super().__init__(parent)
7441
8122
 
8123
+ if self.parent().active_channel == 0:
8124
+ if self.parent().channel_data[0] is not None:
8125
+ try:
8126
+ active_data = self.parent().channel_data[0]
8127
+ act_channel = 0
8128
+ except:
8129
+ active_data = self.parent().channel_data[1]
8130
+ act_channel = 1
8131
+ else:
8132
+ active_data = self.parent().channel_data[1]
8133
+ act_channel = 1
8134
+
8135
+ try:
8136
+ array1 = np.zeros_like(active_data).astype(np.uint8)
8137
+ except:
8138
+ print("No data in nodes channel")
8139
+ return
8140
+
7442
8141
  self.setWindowTitle("Threshold")
7443
8142
 
7444
8143
  # Create central widget and layout
@@ -7460,25 +8159,6 @@ class MachineWindow(QMainWindow):
7460
8159
 
7461
8160
  self.parent().pen_button.setEnabled(False)
7462
8161
 
7463
-
7464
- if self.parent().active_channel == 0:
7465
- if self.parent().channel_data[0] is not None:
7466
- try:
7467
- active_data = self.parent().channel_data[0]
7468
- act_channel = 0
7469
- except:
7470
- active_data = self.parent().channel_data[1]
7471
- act_channel = 1
7472
- else:
7473
- active_data = self.parent().channel_data[1]
7474
- act_channel = 1
7475
-
7476
- try:
7477
- array1 = np.zeros_like(active_data).astype(np.uint8)
7478
- except:
7479
- print("No data in nodes channel")
7480
- return
7481
-
7482
8162
  array3 = np.zeros_like(active_data).astype(np.uint8)
7483
8163
  self.parent().highlight_overlay = array3 #Clear this out for the segmenter to use
7484
8164
 
@@ -8863,6 +9543,91 @@ class MaskDialog(QDialog):
8863
9543
  except Exception as e:
8864
9544
  print(f"Error masking: {e}")
8865
9545
 
9546
+ class CropDialog(QDialog):
9547
+
9548
+ def __init__(self, parent=None):
9549
+
9550
+ try:
9551
+
9552
+ super().__init__(parent)
9553
+ self.setWindowTitle("Crop Image?")
9554
+ self.setModal(True)
9555
+
9556
+ layout = QFormLayout(self)
9557
+
9558
+ self.xmin = QLineEdit("0")
9559
+ layout.addRow("X Min", self.xmin)
9560
+
9561
+ self.xmax = QLineEdit(f"{self.parent().shape[2]}")
9562
+ layout.addRow("X Max", self.xmax)
9563
+
9564
+ self.ymin = QLineEdit("0")
9565
+ layout.addRow("Y Min", self.ymin)
9566
+
9567
+ self.ymax = QLineEdit(f"{self.parent().shape[1]}")
9568
+ layout.addRow("Y Max", self.ymax)
9569
+
9570
+ self.zmin = QLineEdit("0")
9571
+ layout.addRow("Z Min", self.zmin)
9572
+
9573
+ self.zmax = QLineEdit(f"{self.parent().shape[0]}")
9574
+ layout.addRow("Z Max", self.zmax)
9575
+
9576
+ # Add Run button
9577
+ run_button = QPushButton("Run")
9578
+ run_button.clicked.connect(self.run)
9579
+ layout.addRow(run_button)
9580
+
9581
+ except:
9582
+ pass
9583
+
9584
+ def run(self):
9585
+
9586
+ try:
9587
+
9588
+ xmin = int(self.xmin.text()) if self.xmin.text() else 0
9589
+ ymin = int(self.ymin.text()) if self.ymin.text() else 0
9590
+ zmin = int(self.zmin.text()) if self.zmin.text() else 0
9591
+ xmax = int(self.xmax.text()) if self.xmax.text() else self.parent().shape[2]
9592
+ ymax = int(self.ymax.text()) if self.xmax.text() else self.parent().shape[1]
9593
+ zmax = int(self.zmax.text()) if self.xmax.text() else self.parent().shape[0]
9594
+
9595
+ args = xmin, ymin, zmin, xmax, ymax, zmax
9596
+
9597
+ for i, array in enumerate(self.parent().channel_data):
9598
+
9599
+ if array is None:
9600
+
9601
+ continue
9602
+
9603
+ else:
9604
+
9605
+ array = self.reslice_3d_array(array, args)
9606
+
9607
+ self.parent().load_channel(i, array, data = True)
9608
+
9609
+ self.accept()
9610
+
9611
+ except Exception as e:
9612
+
9613
+ print(f"Error cropping: {e}")
9614
+
9615
+
9616
+
9617
+
9618
+
9619
+
9620
+
9621
+
9622
+ def reslice_3d_array(self, array, args):
9623
+ """Internal method used for the secondary algorithm to reslice subarrays around nodes."""
9624
+
9625
+ x_start, y_start, z_start, x_end, y_end, z_end = args
9626
+
9627
+ # Reslice the array
9628
+ array = array[z_start:z_end+1, y_start:y_end+1, x_start:x_end+1]
9629
+
9630
+ return array
8866
9631
 
8867
9632
 
8868
9633
  class TypeDialog(QDialog):
@@ -9226,6 +9991,12 @@ class CentroidNodeDialog(QDialog):
9226
9991
 
9227
9992
  layout = QFormLayout(self)
9228
9993
 
9994
+ # Add mode selection dropdown
9995
+ self.mode_selector = QComboBox()
9996
+ self.mode_selector.addItems(["Starting at 0", "Starting at Min Centroids (will transpose centroids)"])
9997
+ self.mode_selector.setCurrentIndex(0) # Default to Mode 1
9998
+ layout.addRow("Execution Mode:", self.mode_selector)
9999
+
9229
10000
  # Add Run button
9230
10001
  run_button = QPushButton("Run Node Generation? (Will override current nodes). Note it is presumed your nodes begin at 1, not 0.")
9231
10002
  run_button.clicked.connect(self.run_nodes)
@@ -9255,7 +10026,18 @@ class CentroidNodeDialog(QDialog):
9255
10026
  )
9256
10027
  return
9257
10028
 
9258
- my_network.nodes = my_network.centroid_array()
10029
+ mode = self.mode_selector.currentIndex()
10030
+
10031
+ if mode == 0:
10032
+
10033
+ my_network.nodes = my_network.centroid_array()
10034
+
10035
+ else:
10036
+
10037
+ my_network.nodes, my_network.centroids = my_network.centroid_array(clip = True)
10038
+
10039
+ self.parent().format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
10040
+
9259
10041
 
9260
10042
  self.parent().load_channel(0, channel_data = my_network.nodes, data = True)
9261
10043
 
@@ -9338,7 +10120,13 @@ class GenNodesDialog(QDialog):
9338
10120
  # Auto checkbox
9339
10121
  self.auto = QPushButton("Auto")
9340
10122
  self.auto.setCheckable(True)
9341
- self.auto.setChecked(True)
10123
+ try:
10124
+ if my_network.edges.shape[0] == 1:
10125
+ self.auto.setChecked(False)
10126
+ else:
10127
+ self.auto.setChecked(True)
10128
+ except:
10129
+ self.auto.setChecked(True)
9342
10130
  rec_layout.addWidget(QLabel("Attempt to Auto Correct Skeleton Looping:"), 1, 0)
9343
10131
  rec_layout.addWidget(self.auto, 1, 1)
9344
10132
 
@@ -9503,10 +10291,10 @@ class BranchDialog(QDialog):
9503
10291
  correction_layout = QGridLayout()
9504
10292
 
9505
10293
  # Branch Fix checkbox
9506
- self.fix = QPushButton("Auto-Correct Branches")
10294
+ self.fix = QPushButton("Auto-Correct 1")
9507
10295
  self.fix.setCheckable(True)
9508
10296
  self.fix.setChecked(False)
9509
- correction_layout.addWidget(QLabel("Attempt to auto-correct branch labels:"), 0, 0)
10297
+ correction_layout.addWidget(QLabel("Auto-Correct Branches by Collapsing Busy Neighbors: "), 0, 0)
9510
10298
  correction_layout.addWidget(self.fix, 0, 1)
9511
10299
 
9512
10300
  # Fix value
@@ -9518,6 +10306,12 @@ class BranchDialog(QDialog):
9518
10306
  self.seed = QLineEdit('')
9519
10307
  correction_layout.addWidget(QLabel("Random seed for auto correction (int - optional):"), 2, 0)
9520
10308
  correction_layout.addWidget(self.seed, 2, 1)
10309
+
10310
+ self.fix2 = QPushButton("Auto-Correct 2")
10311
+ self.fix2.setCheckable(True)
10312
+ self.fix2.setChecked(True)
10313
+ correction_layout.addWidget(QLabel("Auto-Correct Branches by Collapsing Internal Labels: "), 3, 0)
10314
+ correction_layout.addWidget(self.fix2, 3, 1)
9521
10315
 
9522
10316
  correction_group.setLayout(correction_layout)
9523
10317
  main_layout.addWidget(correction_group)
@@ -9587,6 +10381,7 @@ class BranchDialog(QDialog):
9587
10381
  GPU = self.GPU.isChecked()
9588
10382
  cubic = self.cubic.isChecked()
9589
10383
  fix = self.fix.isChecked()
10384
+ fix2 = self.fix2.isChecked()
9590
10385
  fix_val = float(self.fix_val.text()) if self.fix_val.text() else None
9591
10386
  seed = int(self.seed.text()) if self.seed.text() else None
9592
10387
 
@@ -9603,6 +10398,25 @@ class BranchDialog(QDialog):
9603
10398
 
9604
10399
  output = n3d.label_branches(my_network.edges, nodes = my_network.nodes, bonus_array = original_array, GPU = GPU, down_factor = down_factor, arrayshape = original_shape)
9605
10400
 
10401
+ if fix2:
10402
+
10403
+ temp_network = n3d.Network_3D(nodes = output)
10404
+
10405
+ max_val = np.max(temp_network.nodes)
10406
+
10407
+ background = temp_network.nodes == 0
10408
+
10409
+ background = background * max_val
10410
+
10411
+ temp_network.nodes = temp_network.nodes + background
10412
+
10413
+ del background
10414
+
10415
+ temp_network.morph_proximity(search = [3,3], fastdil = True) #Detect network of nearby branches
10416
+
10417
+ output = n3d.fix_branches(output, temp_network.network, max_val)
10418
+
10419
+
9606
10420
  if fix:
9607
10421
 
9608
10422
  temp_network = n3d.Network_3D(nodes = output)
@@ -9611,7 +10425,7 @@ class BranchDialog(QDialog):
9611
10425
 
9612
10426
  temp_network.community_partition(weighted = False, style = 1, dostats = False, seed = seed) #Find communities with louvain, unweighted params
9613
10427
 
9614
- targs = n3d.fix_branches(temp_network.nodes, temp_network.network, temp_network.communities, fix_val)
10428
+ targs = n3d.fix_branches_network(temp_network.nodes, temp_network.network, temp_network.communities, fix_val)
9615
10429
 
9616
10430
  temp_network.com_to_node(targs)
9617
10431
 
@@ -9798,6 +10612,11 @@ class ModifyDialog(QDialog):
9798
10612
  self.revid.setCheckable(True)
9799
10613
  self.revid.setChecked(False)
9800
10614
  layout.addRow("Remove Unassigned IDs from Centroid List?:", self.revid)
10615
+
10616
+ self.remove = QPushButton("Remove Missing")
10617
+ self.remove.setCheckable(True)
10618
+ self.remove.setChecked(False)
10619
+ layout.addRow("Remove Any Nodes Not in Nodes Channel From Properties?:", self.remove)
9801
10620
 
9802
10621
  # trunk checkbox (default false)
9803
10622
  self.trunk = QPushButton("Remove Trunk")
@@ -9835,6 +10654,12 @@ class ModifyDialog(QDialog):
9835
10654
  self.isolate.setChecked(False)
9836
10655
  layout.addRow("Isolate connections between two specific node types (if assigned)?:", self.isolate)
9837
10656
 
10657
+ # isolate checkbox (default false)
10658
+ self.com_sizes = QPushButton("Communities By Size")
10659
+ self.com_sizes.setCheckable(True)
10660
+ self.com_sizes.setChecked(False)
10661
+ layout.addRow("Rearrange Community IDs by size?:", self.com_sizes)
10662
+
9838
10663
  # Community collapse checkbox (default False)
9839
10664
  self.comcollapse = QPushButton("Communities -> nodes")
9840
10665
  self.comcollapse.setCheckable(True)
@@ -9876,6 +10701,8 @@ class ModifyDialog(QDialog):
9876
10701
  prune = self.prune.isChecked()
9877
10702
  isolate = self.isolate.isChecked()
9878
10703
  comcollapse = self.comcollapse.isChecked()
10704
+ remove = self.remove.isChecked()
10705
+ com_size = self.com_sizes.isChecked()
9879
10706
 
9880
10707
 
9881
10708
  if isolate and my_network.node_identities is not None:
@@ -9889,6 +10716,21 @@ class ModifyDialog(QDialog):
9889
10716
  pass
9890
10717
 
9891
10718
 
10719
+ if remove:
10720
+ my_network.purge_properties()
10721
+ try:
10722
+ self.parent().format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
10723
+ except:
10724
+ pass
10725
+ try:
10726
+ self.parent().format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity', 'Node Identities')
10727
+ except:
10728
+ pass
10729
+ try:
10730
+ self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'Community', 'Node Communities')
10731
+ except:
10732
+ pass
10733
+
9892
10734
 
9893
10735
  if edgeweight:
9894
10736
  my_network.remove_edge_weights()
@@ -9911,6 +10753,14 @@ class ModifyDialog(QDialog):
9911
10753
  self.parent().format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
9912
10754
  except:
9913
10755
  pass
10756
+ if com_size:
10757
+ if my_network.communities is None:
10758
+ self.parent().show_partition_dialog()
10759
+ if my_network.communities is None:
10760
+ return
10761
+ my_network.com_by_size()
10762
+ self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'Community', 'Node Communities')
10763
+
9914
10764
  if comcollapse:
9915
10765
  if my_network.communities is None:
9916
10766
  self.parent().show_partition_dialog()
@@ -9940,6 +10790,8 @@ class ModifyDialog(QDialog):
9940
10790
  self.accept()
9941
10791
 
9942
10792
  except Exception as e:
10793
+ import traceback
10794
+ print(traceback.format_exc())
9943
10795
  print(f"An error occurred: {e}")
9944
10796
 
9945
10797
 
@@ -10070,85 +10922,107 @@ class CalcAllDialog(QDialog):
10070
10922
  prev_fastdil = False
10071
10923
  prev_overlays = False
10072
10924
  prev_updates = True
10073
-
10925
+
10074
10926
  def __init__(self, parent=None):
10075
10927
  super().__init__(parent)
10076
- self.setWindowTitle("Calculate All Parameters")
10928
+ self.setWindowTitle("Calculate Connectivity Network Parameters")
10077
10929
  self.setModal(True)
10078
10930
 
10079
- layout = QFormLayout(self)
10931
+ # Main layout
10932
+ main_layout = QVBoxLayout(self)
10080
10933
 
10081
- # Directory (empty by default)
10082
- self.directory = QLineEdit(self.prev_directory)
10083
- self.directory.setPlaceholderText("Leave empty for 'my_network'")
10084
- layout.addRow("Output Directory:", self.directory)
10934
+ # Important Parameters Group
10935
+ important_group = QGroupBox("Important Parameters")
10936
+ important_layout = QFormLayout(important_group)
10085
10937
 
10086
- # Load previous values for all inputs
10087
10938
  self.xy_scale = QLineEdit(f'{my_network.xy_scale}')
10088
- layout.addRow("xy_scale:", self.xy_scale)
10939
+ important_layout.addRow("xy_scale:", self.xy_scale)
10089
10940
 
10090
10941
  self.z_scale = QLineEdit(f'{my_network.z_scale}')
10091
- layout.addRow("z_scale:", self.z_scale)
10092
-
10942
+ important_layout.addRow("z_scale:", self.z_scale)
10943
+
10093
10944
  self.search = QLineEdit(self.prev_search)
10094
10945
  self.search.setPlaceholderText("Leave empty for None")
10095
- layout.addRow("Node Search (float):", self.search)
10096
-
10946
+ important_layout.addRow("Node Search (float):", self.search)
10947
+
10097
10948
  self.diledge = QLineEdit(self.prev_diledge)
10098
10949
  self.diledge.setPlaceholderText("Leave empty for None")
10099
- layout.addRow("Edge Reconnection Distance (float):", self.diledge)
10100
-
10101
- self.down_factor = QLineEdit(self.prev_down_factor)
10102
- self.down_factor.setPlaceholderText("Leave empty for None")
10103
- layout.addRow("Downsample for Centroids (int):", self.down_factor)
10104
-
10105
- self.GPU_downsample = QLineEdit(self.prev_GPU_downsample)
10106
- self.GPU_downsample.setPlaceholderText("Leave empty for None")
10107
- layout.addRow("Downsample for Distance Transform (GPU) (int):", self.GPU_downsample)
10108
-
10950
+ important_layout.addRow("Edge Reconnection Distance (float):", self.diledge)
10951
+
10952
+ self.label_nodes = QPushButton("Label")
10953
+ self.label_nodes.setCheckable(True)
10954
+ self.label_nodes.setChecked(self.prev_label_nodes)
10955
+ important_layout.addRow("Re-Label Nodes (WARNING - OVERRIDES ANY CURRENT LABELS):", self.label_nodes)
10956
+
10957
+ main_layout.addWidget(important_group)
10958
+
10959
+ # Optional Parameters Group
10960
+ optional_group = QGroupBox("Optional Parameters")
10961
+ optional_layout = QFormLayout(optional_group)
10962
+
10109
10963
  self.other_nodes = QLineEdit(self.prev_other_nodes)
10110
10964
  self.other_nodes.setPlaceholderText("Leave empty for None")
10111
- layout.addRow("Filepath or directory containing additional node images:", self.other_nodes)
10112
-
10965
+ optional_layout.addRow("Filepath or directory containing additional node images:", self.other_nodes)
10966
+
10113
10967
  self.remove_trunk = QLineEdit(self.prev_remove_trunk)
10114
10968
  self.remove_trunk.setPlaceholderText("Leave empty for 0")
10115
- layout.addRow("Times to remove edge trunks (int): ", self.remove_trunk)
10116
-
10117
- # Load previous button states
10118
- self.gpu = QPushButton("GPU")
10119
- self.gpu.setCheckable(True)
10120
- self.gpu.setChecked(self.prev_gpu)
10121
- layout.addRow("Use GPU:", self.gpu)
10122
-
10123
- self.label_nodes = QPushButton("Label")
10124
- self.label_nodes.setCheckable(True)
10125
- self.label_nodes.setChecked(self.prev_label_nodes)
10126
- layout.addRow("Re-Label Nodes (WARNING - OVERRIDES ANY CURRENT LABELS):", self.label_nodes)
10127
-
10969
+ optional_layout.addRow("Times to remove edge trunks (int):", self.remove_trunk)
10970
+
10128
10971
  self.inners = QPushButton("Inner Edges")
10129
10972
  self.inners.setCheckable(True)
10130
10973
  self.inners.setChecked(self.prev_inners)
10131
- layout.addRow("Use Inner Edges:", self.inners)
10132
-
10974
+ optional_layout.addRow("Use Inner Edges:", self.inners)
10975
+
10976
+ main_layout.addWidget(optional_group)
10977
+
10978
+ # Speed Up Options Group
10979
+ speedup_group = QGroupBox("Speed Up Options")
10980
+ speedup_layout = QFormLayout(speedup_group)
10981
+
10982
+ self.down_factor = QLineEdit(self.prev_down_factor)
10983
+ self.down_factor.setPlaceholderText("Leave empty for None")
10984
+ speedup_layout.addRow("Downsample for Centroids (int):", self.down_factor)
10985
+
10986
+ self.GPU_downsample = QLineEdit(self.prev_GPU_downsample)
10987
+ self.GPU_downsample.setPlaceholderText("Leave empty for None")
10988
+ speedup_layout.addRow("Downsample for Distance Transform (GPU) (int):", self.GPU_downsample)
10989
+
10990
+ self.gpu = QPushButton("GPU")
10991
+ self.gpu.setCheckable(True)
10992
+ self.gpu.setChecked(self.prev_gpu)
10993
+ speedup_layout.addRow("Use GPU:", self.gpu)
10994
+
10133
10995
  self.fastdil = QPushButton("Fast Dilate")
10134
10996
  self.fastdil.setCheckable(True)
10135
10997
  self.fastdil.setChecked(self.prev_fastdil)
10136
- layout.addRow("Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
10137
-
10998
+ speedup_layout.addRow("Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
10999
+
11000
+ main_layout.addWidget(speedup_group)
11001
+
11002
+ # Output Options Group
11003
+ output_group = QGroupBox("Output Options")
11004
+ output_layout = QFormLayout(output_group)
11005
+
11006
+ self.directory = QLineEdit(self.prev_directory)
11007
+ self.directory.setPlaceholderText("Will Have to Save Manually If Empty")
11008
+ output_layout.addRow("Output Directory:", self.directory)
11009
+
10138
11010
  self.overlays = QPushButton("Overlays")
10139
11011
  self.overlays.setCheckable(True)
10140
11012
  self.overlays.setChecked(self.prev_overlays)
10141
- layout.addRow("Generate Overlays:", self.overlays)
10142
-
11013
+ output_layout.addRow("Generate Overlays:", self.overlays)
11014
+
10143
11015
  self.update = QPushButton("Update")
10144
11016
  self.update.setCheckable(True)
10145
11017
  self.update.setChecked(self.prev_updates)
10146
- layout.addRow("Update Node/Edge in NetTracer3D:", self.update)
11018
+ output_layout.addRow("Update Node/Edge in NetTracer3D:", self.update)
11019
+
11020
+ main_layout.addWidget(output_group)
10147
11021
 
10148
11022
  # Add Run button
10149
11023
  run_button = QPushButton("Run Calculate All")
10150
11024
  run_button.clicked.connect(self.run_calc_all)
10151
- layout.addRow(run_button)
11025
+ main_layout.addWidget(run_button)
10152
11026
 
10153
11027
  def run_calc_all(self):
10154
11028
 
@@ -10325,65 +11199,87 @@ class CalcAllDialog(QDialog):
10325
11199
  f"Error running calculate all: {str(e)}"
10326
11200
  )
10327
11201
 
10328
- class ProxDialog(QDialog):
10329
11202
 
11203
+ class ProxDialog(QDialog):
10330
11204
  def __init__(self, parent=None):
10331
11205
  super().__init__(parent)
10332
11206
  self.setWindowTitle("Calculate Proximity Network")
10333
11207
  self.setModal(True)
10334
-
10335
- layout = QFormLayout(self)
10336
-
10337
- # Directory (empty by default)
10338
- self.directory = QLineEdit('')
10339
- self.directory.setPlaceholderText("Leave empty for 'my_network'")
10340
- layout.addRow("Output Directory:", self.directory)
10341
-
11208
+
11209
+ # Main layout
11210
+ main_layout = QVBoxLayout(self)
11211
+
11212
+ # Important Parameters Group
11213
+ important_group = QGroupBox("Important Parameters")
11214
+ important_layout = QFormLayout(important_group)
11215
+
10342
11216
  self.search = QLineEdit()
10343
11217
  self.search.setPlaceholderText("search")
10344
- layout.addRow("Search Region Distance? (enter true value corresponding to scaling, ie in microns):", self.search)
10345
-
10346
- # Load previous values for all inputs
11218
+ important_layout.addRow("Search Region Distance? (enter true value corresponding to scaling, ie in microns):", self.search)
11219
+
10347
11220
  self.xy_scale = QLineEdit(f"{my_network.xy_scale}")
10348
- layout.addRow("xy_scale:", self.xy_scale)
11221
+ important_layout.addRow("xy_scale:", self.xy_scale)
10349
11222
 
10350
11223
  self.z_scale = QLineEdit(f"{my_network.z_scale}")
10351
- layout.addRow("z_scale:", self.z_scale)
10352
-
10353
- # Add mode selection dropdown
11224
+ important_layout.addRow("z_scale:", self.z_scale)
11225
+
11226
+ main_layout.addWidget(important_group)
11227
+
11228
+ # Mode Group
11229
+ mode_group = QGroupBox("Mode")
11230
+ mode_layout = QFormLayout(mode_group)
11231
+
10354
11232
  self.mode_selector = QComboBox()
10355
11233
  self.mode_selector.addItems(["From Centroids (fast but ignores shape - use for small or spherical objects - search STARTS at centroid)", "From Morphological Shape (slower but preserves shape - use for oddly shaped objects - search STARTS at object border)"])
10356
11234
  self.mode_selector.setCurrentIndex(0) # Default to Mode 1
10357
- layout.addRow("Execution Mode:", self.mode_selector)
10358
-
10359
- self.fastdil = QPushButton("Fast Dilate")
10360
- self.fastdil.setCheckable(True)
10361
- self.fastdil.setChecked(False)
10362
- layout.addRow("(If using morphological) Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
10363
-
11235
+ mode_layout.addRow("Execution Mode:", self.mode_selector)
11236
+
10364
11237
  if my_network.node_identities is not None:
10365
11238
  self.id_selector = QComboBox()
10366
11239
  # Add all options from id dictionary
10367
11240
  self.id_selector.addItems(['None'] + list(set(my_network.node_identities.values())))
10368
11241
  self.id_selector.setCurrentIndex(0) # Default to Mode 1
10369
- layout.addRow("Create Networks only from a specific node identity?:", self.id_selector)
11242
+ mode_layout.addRow("Create Networks only from a specific node identity?:", self.id_selector)
10370
11243
  else:
10371
11244
  self.id_selector = None
10372
-
11245
+
11246
+ main_layout.addWidget(mode_group)
11247
+
11248
+ # Output Options Group
11249
+ output_group = QGroupBox("Output Options")
11250
+ output_layout = QFormLayout(output_group)
11251
+
11252
+ self.directory = QLineEdit('')
11253
+ self.directory.setPlaceholderText("Leave empty for 'my_network'")
11254
+ output_layout.addRow("Output Directory:", self.directory)
11255
+
10373
11256
  self.overlays = QPushButton("Overlays")
10374
11257
  self.overlays.setCheckable(True)
10375
11258
  self.overlays.setChecked(True)
10376
- layout.addRow("Generate Overlays:", self.overlays)
10377
-
11259
+ output_layout.addRow("Generate Overlays:", self.overlays)
11260
+
10378
11261
  self.populate = QPushButton("Populate Nodes from Centroids?")
10379
11262
  self.populate.setCheckable(True)
10380
11263
  self.populate.setChecked(False)
10381
- layout.addRow("If using centroid search:", self.populate)
10382
-
11264
+ output_layout.addRow("If using centroid search:", self.populate)
11265
+
11266
+ main_layout.addWidget(output_group)
11267
+
11268
+ # Speed Up Options Group
11269
+ speedup_group = QGroupBox("Speed Up Options")
11270
+ speedup_layout = QFormLayout(speedup_group)
11271
+
11272
+ self.fastdil = QPushButton("Fast Dilate")
11273
+ self.fastdil.setCheckable(True)
11274
+ self.fastdil.setChecked(False)
11275
+ speedup_layout.addRow("(If using morphological) Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
11276
+
11277
+ main_layout.addWidget(speedup_group)
11278
+
10383
11279
  # Add Run button
10384
11280
  run_button = QPushButton("Run Proximity Network")
10385
11281
  run_button.clicked.connect(self.prox)
10386
- layout.addRow(run_button)
11282
+ main_layout.addWidget(run_button)
10387
11283
 
10388
11284
  def prox(self):
10389
11285
 
@@ -10483,14 +11379,9 @@ class ProxDialog(QDialog):
10483
11379
  my_network.id_overlay = my_network.draw_node_indices(directory=directory)
10484
11380
 
10485
11381
  # Update channel data
10486
- self.parent().channel_data[2] = my_network.network_overlay
10487
- self.parent().channel_data[3] = my_network.id_overlay
11382
+ self.parent().load_channel(2, channel_data = my_network.network_overlay, data = True)
11383
+ self.parent().load_channel(3, channel_data = my_network.id_overlay, data = True)
10488
11384
 
10489
- # Enable the overlay channel buttons
10490
- self.parent().channel_buttons[2].setEnabled(True)
10491
- self.parent().channel_buttons[3].setEnabled(True)
10492
-
10493
-
10494
11385
  self.parent().update_display()
10495
11386
  self.accept()
10496
11387