nettracer3d 0.2.6__py3-none-any.whl → 0.2.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.
@@ -13,11 +13,17 @@ from matplotlib.figure import Figure
13
13
  import matplotlib.pyplot as plt
14
14
  from qtrangeslider import QRangeSlider
15
15
  from nettracer3d import nettracer as n3d
16
+ from nettracer3d import smart_dilate as sdl
17
+ from nettracer3d import proximity as pxt
16
18
  from matplotlib.colors import LinearSegmentedColormap
17
19
  import pandas as pd
18
20
  from PyQt6.QtGui import (QFont, QCursor, QColor)
19
21
  import tifffile
20
22
  import copy
23
+ import multiprocessing as mp
24
+ from concurrent.futures import ThreadPoolExecutor
25
+ from functools import partial
26
+
21
27
 
22
28
  class ImageViewerWindow(QMainWindow):
23
29
  def __init__(self):
@@ -120,6 +126,15 @@ class ImageViewerWindow(QMainWindow):
120
126
  2: [0,0],
121
127
  3: [0,0]
122
128
  }
129
+
130
+ self.volume_dict = {
131
+ 0: None,
132
+ 1: None,
133
+ 2: None,
134
+ 3: None
135
+ } #For storing thresholding information
136
+
137
+ self.original_shape = None #For undoing resamples
123
138
 
124
139
  # Create control panel
125
140
  control_panel = QWidget()
@@ -379,23 +394,44 @@ class ImageViewerWindow(QMainWindow):
379
394
  elif self.scroll_direction > 0 and new_value <= self.slice_slider.maximum():
380
395
  self.slice_slider.setValue(new_value)
381
396
 
382
- def create_highlight_overlay(self, node_indices=None, edge_indices=None):
397
+
398
+ def create_highlight_overlay(self, node_indices=None, edge_indices=None, overlay1_indices = None, overlay2_indices = None):
383
399
  """
384
- Create a binary overlay highlighting specific nodes and/or edges using boolean indexing.
400
+ Create a binary overlay highlighting specific nodes and/or edges using parallel processing.
385
401
 
386
402
  Args:
387
403
  node_indices (list): List of node indices to highlight
388
404
  edge_indices (list): List of edge indices to highlight
389
405
  """
406
+
407
+ def process_chunk(chunk_data, indices_to_check):
408
+ """Process a single chunk of the array to create highlight mask"""
409
+ mask = np.isin(chunk_data, indices_to_check)
410
+ return mask * 255
411
+
412
+ if node_indices is not None:
413
+ if 0 in node_indices:
414
+ node_indices.remove(0)
415
+ if edge_indices is not None:
416
+ if 0 in edge_indices:
417
+ edge_indices.remove(0)
418
+ if overlay1_indices is not None:
419
+ if 0 in overlay1_indices:
420
+ overlay1_indices.remove(0)
421
+
390
422
  if node_indices is None:
391
423
  node_indices = []
392
424
  if edge_indices is None:
393
425
  edge_indices = []
426
+ if overlay1_indices is None:
427
+ overlay1_indices = []
428
+ if overlay2_indices is None:
429
+ overlay2_indices = []
394
430
 
395
- current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None #Preserve zoom
431
+ current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
396
432
  current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
397
-
398
- if not node_indices and not edge_indices:
433
+
434
+ if not node_indices and not edge_indices and not overlay1_indices and not overlay2_indices:
399
435
  self.highlight_overlay = None
400
436
  self.highlight_bounds = None
401
437
  self.update_display(preserve_zoom=(current_xlim, current_ylim))
@@ -412,22 +448,62 @@ class ImageViewerWindow(QMainWindow):
412
448
  # Initialize full-size overlay
413
449
  self.highlight_overlay = np.zeros(full_shape, dtype=np.uint8)
414
450
 
415
- # Add nodes to highlight using boolean indexing
416
- if node_indices and self.channel_data[0] is not None:
417
- mask = np.isin(self.channel_data[0], node_indices)
418
- self.highlight_overlay[mask] = 255
451
+ # Get number of CPU cores
452
+ num_cores = mp.cpu_count()
453
+
454
+ # Calculate chunk size along y-axis
455
+ chunk_size = full_shape[0] // num_cores
456
+ if chunk_size < 1:
457
+ chunk_size = 1
458
+
459
+ def process_channel(channel_data, indices, array_shape):
460
+ if channel_data is None or not indices:
461
+ return None
462
+
463
+ # Create chunks
464
+ chunks = []
465
+ for i in range(0, array_shape[0], chunk_size):
466
+ end = min(i + chunk_size, array_shape[0])
467
+ chunks.append(channel_data[i:end])
419
468
 
420
- # Add edges to highlight using boolean indexing
421
- if edge_indices and self.channel_data[1] is not None:
422
- mask = np.isin(self.channel_data[1], edge_indices)
423
- self.highlight_overlay[mask] = 255
469
+ # Process chunks in parallel using ThreadPoolExecutor
470
+ process_func = partial(process_chunk, indices_to_check=indices)
424
471
 
425
-
472
+ with ThreadPoolExecutor(max_workers=num_cores) as executor:
473
+ chunk_results = list(executor.map(process_func, chunks))
474
+
475
+ # Reassemble the chunks
476
+ return np.vstack(chunk_results)
477
+
478
+ # Process nodes and edges in parallel using multiprocessing
479
+ with ThreadPoolExecutor(max_workers=2) as executor:
480
+ future_nodes = executor.submit(process_channel, self.channel_data[0], node_indices, full_shape)
481
+ future_edges = executor.submit(process_channel, self.channel_data[1], edge_indices, full_shape)
482
+ future_overlay1 = executor.submit(process_channel, self.channel_data[2], overlay1_indices, full_shape)
483
+ future_overlay2 = executor.submit(process_channel, self.channel_data[3], overlay2_indices, full_shape)
484
+
485
+ # Get results
486
+ node_overlay = future_nodes.result()
487
+ edge_overlay = future_edges.result()
488
+ overlay1_overlay = future_overlay1.result()
489
+ overlay2_overlay = future_overlay2.result()
490
+
491
+ # Combine results
492
+ if node_overlay is not None:
493
+ self.highlight_overlay = np.maximum(self.highlight_overlay, node_overlay)
494
+ if edge_overlay is not None:
495
+ self.highlight_overlay = np.maximum(self.highlight_overlay, edge_overlay)
496
+ if overlay1_overlay is not None:
497
+ self.highlight_overlay = np.maximum(self.highlight_overlay, overlay1_overlay)
498
+ if overlay2_overlay is not None:
499
+ self.highlight_overlay = np.maximum(self.highlight_overlay, overlay2_overlay)
500
+
426
501
  # Update display
427
502
  self.update_display(preserve_zoom=(current_xlim, current_ylim))
428
503
 
429
504
 
430
505
 
506
+
431
507
  #METHODS RELATED TO RIGHT CLICK:
432
508
 
433
509
  def create_context_menu(self, event):
@@ -474,6 +550,22 @@ class ImageViewerWindow(QMainWindow):
474
550
  select_edges = select_all_menu.addAction("Edges")
475
551
  context_menu.addMenu(select_all_menu)
476
552
 
553
+ if len(self.clicked_values['nodes']) > 0 or len(self.clicked_values['edges']) > 0:
554
+ highlight_menu = QMenu("Selection", self)
555
+ if len(self.clicked_values['nodes']) > 1 or len(self.clicked_values['edges']) > 1:
556
+ combine_obj = highlight_menu.addAction("Combine Object Labels")
557
+ combine_obj.triggered.connect(self.handle_combine)
558
+ split_obj = highlight_menu.addAction("Split Non-Touching Labels")
559
+ split_obj.triggered.connect(self.handle_seperate)
560
+ delete_obj = highlight_menu.addAction("Delete Selection")
561
+ delete_obj.triggered.connect(self.handle_delete)
562
+ if len(self.clicked_values['nodes']) > 1:
563
+ link_nodes = highlight_menu.addAction("Link Nodes")
564
+ link_nodes.triggered.connect(self.handle_link)
565
+ delink_nodes = highlight_menu.addAction("Split Nodes")
566
+ delink_nodes.triggered.connect(self.handle_split)
567
+ context_menu.addMenu(highlight_menu)
568
+
477
569
  # Create measure menu
478
570
  measure_menu = QMenu("Measure", self)
479
571
 
@@ -901,6 +993,299 @@ class ImageViewerWindow(QMainWindow):
901
993
  except Exception as e:
902
994
  print(f"Error: {e}")
903
995
 
996
+ def handle_info(self, sort = 'node'):
997
+
998
+ try:
999
+
1000
+ info_dict = {}
1001
+
1002
+ if sort == 'node':
1003
+
1004
+ label = self.clicked_values['nodes'][-1]
1005
+
1006
+ info_dict['Label'] = label
1007
+
1008
+ info_dict['Object Class'] = 'Node'
1009
+
1010
+ if my_network.node_identities is not None:
1011
+ info_dict['ID'] = my_network.node_identities[label]
1012
+
1013
+ if my_network.network is not None:
1014
+ info_dict['Degree'] = my_network.network.degree(label)
1015
+
1016
+ if my_network.communities is not None:
1017
+ info_dict['Community'] = my_network.communities[label]
1018
+
1019
+ if my_network.node_centroids is not None:
1020
+ info_dict['Centroid'] = my_network.node_centroids[label]
1021
+
1022
+ if self.volume_dict[0] is not None:
1023
+ info_dict['Volume'] = self.volume_dict[0][label]
1024
+
1025
+
1026
+ elif sort == 'edge':
1027
+
1028
+ label = self.clicked_values['edges'][-1]
1029
+
1030
+ info_dict['Label'] = label
1031
+
1032
+ info_dict['Object Class'] = 'Edge'
1033
+
1034
+ if my_network.edge_centroids is not None:
1035
+ info_dict['Centroid'] = my_network.edge_centroids[label]
1036
+
1037
+ if self.volume_dict[1] is not None:
1038
+ info_dict['Volume'] = self.volume_dict[1][label]
1039
+
1040
+ self.format_for_upperright_table(info_dict, title = f'Info on Object')
1041
+
1042
+ except:
1043
+ pass
1044
+
1045
+
1046
+
1047
+
1048
+ def handle_combine(self):
1049
+
1050
+ try:
1051
+
1052
+ self.clicked_values['nodes'].sort()
1053
+ nodes = copy.deepcopy(self.clicked_values['nodes'])
1054
+ self.clicked_values['edges'].sort()
1055
+ edges = copy.deepcopy(self.clicked_values['edges'])
1056
+
1057
+ if len(nodes) > 1:
1058
+ new_nodes = nodes[0]
1059
+
1060
+ mask = np.isin(self.channel_data[0], nodes)
1061
+ my_network.nodes[mask] = new_nodes
1062
+ self.load_channel(0, my_network.nodes, True)
1063
+ self.clicked_values['nodes'] = new_nodes
1064
+
1065
+ if len(edges) > 1:
1066
+ new_edges = edges[0]
1067
+
1068
+ mask = np.isin(self.channel_data[1], edges)
1069
+ my_network.edges[mask] = new_edges
1070
+ self.load_channel(1, my_network.edges, True)
1071
+ self.clicked_values['edges'] = new_edges
1072
+
1073
+ try:
1074
+
1075
+ for i in range(len(my_network.network_lists[0])):
1076
+ if my_network.network_lists[0][i] in nodes and len(nodes) > 1:
1077
+ my_network.network_lists[0][i] = new_nodes
1078
+ if my_network.network_lists[1][i] in nodes and len(nodes) > 1:
1079
+ my_network.network_lists[1][i] = new_nodes
1080
+ if my_network.network_lists[2][i] in edges and len(edges) > 1:
1081
+ my_network.network_lists[2][i] = new_edges
1082
+
1083
+
1084
+ my_network.network_lists = my_network.network_lists
1085
+
1086
+ if not hasattr(my_network, 'network_lists') or my_network.network_lists is None:
1087
+ empty_df = pd.DataFrame(columns=['Node 1A', 'Node 1B', 'Edge 1C'])
1088
+ model = PandasModel(empty_df)
1089
+ self.network_table.setModel(model)
1090
+ else:
1091
+ model = PandasModel(my_network.network_lists)
1092
+ self.network_table.setModel(model)
1093
+ # Adjust column widths to content
1094
+ for column in range(model.columnCount(None)):
1095
+ self.network_table.resizeColumnToContents(column)
1096
+
1097
+ self.highlight_overlay = None
1098
+ self.update_display()
1099
+
1100
+ self.show_centroid_dialog()
1101
+
1102
+ except Exception as e:
1103
+ print(f"Error, could not update network: {e}")
1104
+
1105
+
1106
+ except Exception as e:
1107
+ print(f"An error has occured: {e}")
1108
+
1109
+ def handle_seperate(self):
1110
+
1111
+ try:
1112
+
1113
+ if len(self.clicked_values['nodes']) > 0:
1114
+ self.create_highlight_overlay(node_indices = self.clicked_values['nodes'])
1115
+ max_val = np.max(my_network.nodes)
1116
+ self.highlight_overlay, num = n3d.label_objects(self.highlight_overlay)
1117
+
1118
+ node_bools = self.highlight_overlay != 0
1119
+ new_max = num + max_val
1120
+ self.highlight_overlay = self.highlight_overlay + max_val
1121
+ self.highlight_overlay = self.highlight_overlay * node_bools
1122
+ if new_max < 256:
1123
+ dtype = np.uint8
1124
+ elif new_max < 65536:
1125
+ dtype = np.uint16
1126
+ else:
1127
+ dtype = np.uint32
1128
+
1129
+ self.highlight_overlay = self.highlight_overlay.astype(dtype)
1130
+ my_network.nodes = my_network.nodes + self.highlight_overlay
1131
+ self.load_channel(0, my_network.nodes, True)
1132
+
1133
+ if len(self.clicked_values['edges']) > 0:
1134
+ self.create_highlight_overlay(edge_indices = self.clicked_values['edges'])
1135
+ max_val = np.max(my_network.edges)
1136
+ self.highlight_overlay, num = n3d.label_objects(self.highlight_overlay)
1137
+ node_bools = self.highlight_overlay != 0
1138
+ new_max = num + max_val
1139
+
1140
+ self.highlight_overlay = self.highlight_overlay + max_val
1141
+ self.highlight_overlay = self.highlight_overlay * node_bools
1142
+ if new_max < 256:
1143
+ dtype = np.uint8
1144
+ elif new_max < 65536:
1145
+ dtype = np.uint16
1146
+ else:
1147
+ dtype = np.uint32
1148
+
1149
+ self.highlight_overlay = self.highlight_overlay.astype(dtype)
1150
+ my_network.edges = my_network.edges + self.highlight_overlay
1151
+ self.load_channel(1, my_network.edges, True)
1152
+ self.highlight_overlay = None
1153
+ self.update_display()
1154
+ print("Network is not updated automatically, please recompute if necesarry. Identities are not automatically updated.")
1155
+ self.show_centroid_dialog()
1156
+
1157
+ except Exception as e:
1158
+ print(f"Error seperating: {e}")
1159
+
1160
+
1161
+
1162
+
1163
+
1164
+ def handle_delete(self):
1165
+
1166
+ try:
1167
+ if len(self.clicked_values['nodes']) > 0:
1168
+ self.create_highlight_overlay(node_indices = self.clicked_values['nodes'])
1169
+ mask = self.highlight_overlay == 0
1170
+ my_network.nodes = my_network.nodes * mask
1171
+ self.load_channel(0, my_network.nodes, True)
1172
+
1173
+ for i in range(len(my_network.network_lists[0]) - 1, -1, -1):
1174
+ if my_network.network_lists[0][i] in self.clicked_values['nodes'] or my_network.network_lists[0][i] in self.clicked_values['nodes']:
1175
+ del my_network.network_lists[0][i]
1176
+ del my_network.network_lists[1][i]
1177
+ del my_network.network_lists[2][i]
1178
+
1179
+
1180
+
1181
+ if len(self.clicked_values['edges']) > 0:
1182
+ self.create_highlight_overlay(node_indices = self.clicked_values['edges'])
1183
+ mask = self.highlight_overlay == 0
1184
+ my_network.edges = my_network.edges * mask
1185
+ self.load_channel(1, my_network.edges, True)
1186
+
1187
+ for i in range(len(my_network.network_lists[1]) - 1, -1, -1):
1188
+ if my_network.network_lists[2][i] in self.clicked_values['edges']:
1189
+ del my_network.network_lists[0][i]
1190
+ del my_network.network_lists[1][i]
1191
+ del my_network.network_lists[2][i]
1192
+
1193
+ my_network.network_lists = my_network.network_lists
1194
+
1195
+
1196
+ if not hasattr(my_network, 'network_lists') or my_network.network_lists is None:
1197
+ empty_df = pd.DataFrame(columns=['Node 1A', 'Node 1B', 'Edge 1C'])
1198
+ model = PandasModel(empty_df)
1199
+ self.network_table.setModel(model)
1200
+ else:
1201
+ model = PandasModel(my_network.network_lists)
1202
+ self.network_table.setModel(model)
1203
+ # Adjust column widths to content
1204
+ for column in range(model.columnCount(None)):
1205
+ self.network_table.resizeColumnToContents(column)
1206
+
1207
+ self.show_centroid_dialog()
1208
+ except Exception as e:
1209
+ print(f"Error: {e}")
1210
+
1211
+ def handle_link(self):
1212
+
1213
+ try:
1214
+ nodes = self.clicked_values['nodes']
1215
+ from itertools import combinations
1216
+ pairs = list(combinations(nodes, 2))
1217
+
1218
+ # Convert existing connections to a set of tuples for efficient lookup
1219
+ existing_connections = set()
1220
+ for n1, n2 in zip(my_network.network_lists[0], my_network.network_lists[1]):
1221
+ existing_connections.add((n1, n2))
1222
+ existing_connections.add((n2, n1)) # Add reverse pair too
1223
+
1224
+ # Filter out existing connections
1225
+ new_pairs = []
1226
+ for pair in pairs:
1227
+ if pair not in existing_connections:
1228
+ new_pairs.append(pair)
1229
+
1230
+ # Add new connections
1231
+ for pair in new_pairs:
1232
+ my_network.network_lists[0].append(pair[0])
1233
+ my_network.network_lists[1].append(pair[1])
1234
+ my_network.network_lists[2].append(0)
1235
+
1236
+ # Update the table
1237
+ if not hasattr(my_network, 'network_lists') or my_network.network_lists is None:
1238
+ empty_df = pd.DataFrame(columns=['Node 1A', 'Node 1B', 'Edge 1C'])
1239
+ model = PandasModel(empty_df)
1240
+ self.network_table.setModel(model)
1241
+ else:
1242
+ model = PandasModel(my_network.network_lists)
1243
+ self.network_table.setModel(model)
1244
+ # Adjust column widths to content
1245
+ for column in range(model.columnCount(None)):
1246
+ self.network_table.resizeColumnToContents(column)
1247
+ except Exception as e:
1248
+ print(f"An error has occurred: {e}")
1249
+
1250
+
1251
+ def handle_split(self):
1252
+ try:
1253
+ nodes = self.clicked_values['nodes']
1254
+
1255
+ from itertools import combinations
1256
+
1257
+ pairs = list(combinations(nodes, 2))
1258
+
1259
+ print(pairs)
1260
+
1261
+
1262
+ for i in range(len(my_network.network_lists[0]) - 1, -1, -1):
1263
+ print((my_network.network_lists[0][i], my_network.network_lists[1][i]))
1264
+ if (my_network.network_lists[0][i], my_network.network_lists[1][i]) in pairs or (my_network.network_lists[1][i], my_network.network_lists[0][i]) in pairs:
1265
+ del my_network.network_lists[0][i]
1266
+ del my_network.network_lists[1][i]
1267
+ del my_network.network_lists[2][i]
1268
+
1269
+ my_network.network_lists = my_network.network_lists
1270
+
1271
+ if not hasattr(my_network, 'network_lists') or my_network.network_lists is None:
1272
+ empty_df = pd.DataFrame(columns=['Node 1A', 'Node 1B', 'Edge 1C'])
1273
+ model = PandasModel(empty_df)
1274
+ self.network_table.setModel(model)
1275
+ else:
1276
+ model = PandasModel(my_network.network_lists)
1277
+ self.network_table.setModel(model)
1278
+ # Adjust column widths to content
1279
+ for column in range(model.columnCount(None)):
1280
+ self.network_table.resizeColumnToContents(column)
1281
+ except Exception as e:
1282
+ print(f"An error has occurred: {e}")
1283
+
1284
+
1285
+
1286
+
1287
+
1288
+
904
1289
 
905
1290
  def handle_highlight_select(self):
906
1291
 
@@ -1129,6 +1514,7 @@ class ImageViewerWindow(QMainWindow):
1129
1514
  # Try to highlight the last selected value in tables
1130
1515
  if self.clicked_values['edges']:
1131
1516
  self.highlight_value_in_tables(self.clicked_values['edges'][-1])
1517
+
1132
1518
 
1133
1519
  elif not self.selecting and self.selection_start: # If we had a click but never started selection
1134
1520
  # Handle as a normal click
@@ -1264,18 +1650,19 @@ class ImageViewerWindow(QMainWindow):
1264
1650
  # Get clicked value
1265
1651
  x_idx = int(round(event.xdata))
1266
1652
  y_idx = int(round(event.ydata))
1653
+ # Check if Ctrl key is pressed (using matplotlib's key_press system)
1654
+ ctrl_pressed = 'ctrl' in event.modifiers # Note: changed from 'control' to 'ctrl'
1267
1655
  if self.channel_data[self.active_channel][self.current_slice, y_idx, x_idx] != 0:
1268
1656
  clicked_value = self.channel_data[self.active_channel][self.current_slice, y_idx, x_idx]
1269
1657
  else:
1270
- self.clicked_values = {
1271
- 'nodes': [],
1272
- 'edges': []
1273
- }
1274
- self.create_highlight_overlay()
1658
+ if not ctrl_pressed:
1659
+ self.clicked_values = {
1660
+ 'nodes': [],
1661
+ 'edges': []
1662
+ }
1663
+ self.create_highlight_overlay()
1275
1664
  return
1276
1665
 
1277
- # Check if Ctrl key is pressed (using matplotlib's key_press system)
1278
- ctrl_pressed = 'ctrl' in event.modifiers # Note: changed from 'control' to 'ctrl'
1279
1666
 
1280
1667
  starting_vals = copy.deepcopy(self.clicked_values)
1281
1668
 
@@ -1293,6 +1680,7 @@ class ImageViewerWindow(QMainWindow):
1293
1680
  self.clicked_values = {'nodes': [clicked_value], 'edges': []}
1294
1681
  # Get latest value (or the last remaining one if we just removed an item)
1295
1682
  latest_value = self.clicked_values['nodes'][-1] if self.clicked_values['nodes'] else None
1683
+ self.handle_info('node')
1296
1684
  elif self.active_channel == 1:
1297
1685
  if ctrl_pressed:
1298
1686
  if clicked_value in self.clicked_values['edges']:
@@ -1306,6 +1694,8 @@ class ImageViewerWindow(QMainWindow):
1306
1694
  self.clicked_values = {'nodes': [], 'edges': [clicked_value]}
1307
1695
  # Get latest value (or the last remaining one if we just removed an item)
1308
1696
  latest_value = self.clicked_values['edges'][-1] if self.clicked_values['edges'] else None
1697
+ self.handle_info('edge')
1698
+
1309
1699
 
1310
1700
  # Try to find and highlight the latest value in the current table
1311
1701
  try:
@@ -1384,6 +1774,8 @@ class ImageViewerWindow(QMainWindow):
1384
1774
  load_action.triggered.connect(lambda: self.load_misc('Node Centroids'))
1385
1775
  load_action = misc_menu.addAction("Load Edge Centroids")
1386
1776
  load_action.triggered.connect(lambda: self.load_misc('Edge Centroids'))
1777
+ load_action = misc_menu.addAction("Merge Nodes")
1778
+ load_action.triggered.connect(lambda: self.load_misc('Merge Nodes'))
1387
1779
 
1388
1780
 
1389
1781
  # Analysis menu
@@ -1391,11 +1783,19 @@ class ImageViewerWindow(QMainWindow):
1391
1783
  network_menu = analysis_menu.addMenu("Network")
1392
1784
  netshow_action = network_menu.addAction("Show Network")
1393
1785
  netshow_action.triggered.connect(self.show_netshow_dialog)
1394
- partition_action = network_menu.addAction("Community Partition")
1786
+ partition_action = network_menu.addAction("Community Partition + Community Stats")
1395
1787
  partition_action.triggered.connect(self.show_partition_dialog)
1396
1788
  stats_menu = analysis_menu.addMenu("Stats")
1397
1789
  allstats_action = stats_menu.addAction("Calculate Generic Network Stats")
1398
1790
  allstats_action.triggered.connect(self.stats)
1791
+ radial_action = stats_menu.addAction("Radial Distribution Analysis")
1792
+ radial_action.triggered.connect(self.show_radial_dialog)
1793
+ degree_dist_action = stats_menu.addAction("Degree Distribution Analysis")
1794
+ degree_dist_action.triggered.connect(self.show_degree_dist_dialog)
1795
+ neighbor_id_action = stats_menu.addAction("Identity Distribution of Neighbors")
1796
+ neighbor_id_action.triggered.connect(self.show_neighbor_id_dialog)
1797
+ random_action = stats_menu.addAction("Generate Equivalent Random Network")
1798
+ random_action.triggered.connect(self.show_random_dialog)
1399
1799
  vol_action = stats_menu.addAction("Calculate Volumes")
1400
1800
  vol_action.triggered.connect(self.volumes)
1401
1801
  inter_action = stats_menu.addAction("Calculate Node < > Edge Interaction")
@@ -1403,8 +1803,15 @@ class ImageViewerWindow(QMainWindow):
1403
1803
  overlay_menu = analysis_menu.addMenu("Data/Overlays")
1404
1804
  degree_action = overlay_menu.addAction("Get Degree Information")
1405
1805
  degree_action.triggered.connect(self.show_degree_dialog)
1806
+ hub_action = overlay_menu.addAction("Get Hub Information")
1807
+ hub_action.triggered.connect(self.show_hub_dialog)
1406
1808
  mother_action = overlay_menu.addAction("Get Mother Nodes")
1407
1809
  mother_action.triggered.connect(self.show_mother_dialog)
1810
+ community_code_action = overlay_menu.addAction("Code Communities")
1811
+ community_code_action.triggered.connect(lambda: self.show_code_dialog(sort = 'Community'))
1812
+ id_code_action = overlay_menu.addAction("Code Identities")
1813
+ id_code_action.triggered.connect(lambda: self.show_code_dialog(sort = 'Identity'))
1814
+
1408
1815
 
1409
1816
  # Process menu
1410
1817
  process_menu = menubar.addMenu("Process")
@@ -1421,25 +1828,37 @@ class ImageViewerWindow(QMainWindow):
1421
1828
  resize_action.triggered.connect(self.show_resize_dialog)
1422
1829
  dilate_action = image_menu.addAction("Dilate")
1423
1830
  dilate_action.triggered.connect(self.show_dilate_dialog)
1831
+ erode_action = image_menu.addAction("Erode")
1832
+ erode_action.triggered.connect(self.show_erode_dialog)
1833
+ hole_action = image_menu.addAction("Fill Holes")
1834
+ hole_action.triggered.connect(self.show_hole_dialog)
1424
1835
  binarize_action = image_menu.addAction("Binarize")
1425
1836
  binarize_action.triggered.connect(self.show_binarize_dialog)
1426
1837
  label_action = image_menu.addAction("Label Objects")
1427
1838
  label_action.triggered.connect(self.show_label_dialog)
1839
+ thresh_action = image_menu.addAction("Threshold/Segment")
1840
+ thresh_action.triggered.connect(self.show_thresh_dialog)
1428
1841
  mask_action = image_menu.addAction("Mask Channel")
1429
1842
  mask_action.triggered.connect(self.show_mask_dialog)
1430
1843
  skeletonize_action = image_menu.addAction("Skeletonize")
1431
1844
  skeletonize_action.triggered.connect(self.show_skeletonize_dialog)
1432
1845
  watershed_action = image_menu.addAction("Watershed")
1433
1846
  watershed_action.triggered.connect(self.show_watershed_dialog)
1847
+ z_proj_action = image_menu.addAction("Z Project")
1848
+ z_proj_action.triggered.connect(self.show_z_dialog)
1434
1849
 
1435
- centroid_node_action = process_menu.addAction("Generate Nodes (From Node Centroids)")
1850
+ generate_menu = process_menu.addMenu("Generate")
1851
+ centroid_node_action = generate_menu.addAction("Generate Nodes (From Node Centroids)")
1436
1852
  centroid_node_action.triggered.connect(self.show_centroid_node_dialog)
1437
-
1438
-
1439
- gennodes_action = process_menu.addAction("Generate Nodes (From 'Edge' Vertices)")
1853
+ gennodes_action = generate_menu.addAction("Generate Nodes (From 'Edge' Vertices)")
1440
1854
  gennodes_action.triggered.connect(self.show_gennodes_dialog)
1441
- branch_action = process_menu.addAction("Label Branches")
1855
+ branch_action = generate_menu.addAction("Label Branches")
1442
1856
  branch_action.triggered.connect(self.show_branch_dialog)
1857
+ genvor_action = generate_menu.addAction("Generate Voronoi Diagram (From Node Centroids) - goes in Overlay2")
1858
+ genvor_action.triggered.connect(self.voronoi)
1859
+
1860
+ modify_action = process_menu.addAction("Modify Network")
1861
+ modify_action.triggered.connect(self.show_modify_dialog)
1443
1862
 
1444
1863
 
1445
1864
  # Image menu
@@ -1477,17 +1896,26 @@ class ImageViewerWindow(QMainWindow):
1477
1896
 
1478
1897
  def volumes(self):
1479
1898
 
1480
- print(self.active_channel)
1481
1899
 
1482
1900
  if self.active_channel == 1:
1483
1901
  output = my_network.volumes('edges')
1484
1902
  self.format_for_upperright_table(output, metric='Edge ID', value = 'Voxel Volume (Scaled)', title = 'Edge Volumes')
1903
+ self.volume_dict[1] = output
1485
1904
 
1486
- else:
1905
+ elif self.active_channel == 0:
1487
1906
  output = my_network.volumes('nodes')
1488
1907
  self.format_for_upperright_table(output, metric='Node ID', value = 'Voxel Volume (Scaled)', title = 'Node Volumes')
1908
+ self.volume_dict[0] = output
1489
1909
 
1910
+ elif self.active_channel == 2:
1911
+ output = my_network.volumes('network_overlay')
1912
+ self.format_for_upperright_table(output, metric='Object ID', value = 'Voxel Volume (Scaled)', title = 'Overlay 1 Volumes')
1913
+ self.volume_dict[2] = output
1490
1914
 
1915
+ elif self.active_channel == 3:
1916
+ output = my_network.volumes('id_overlay')
1917
+ self.format_for_upperright_table(output, metric='Object ID', value = 'Voxel Volume (Scaled)', title = 'Overlay 2 Volumes')
1918
+ self.volume_dict[3] = output
1491
1919
 
1492
1920
 
1493
1921
 
@@ -1582,6 +2010,11 @@ class ImageViewerWindow(QMainWindow):
1582
2010
  dialog = WatershedDialog(self)
1583
2011
  dialog.exec()
1584
2012
 
2013
+ def show_z_dialog(self):
2014
+ """Show the z-proj dialog."""
2015
+ dialog = ZDialog(self)
2016
+ dialog.exec()
2017
+
1585
2018
  def show_calc_all_dialog(self):
1586
2019
  """Show the calculate all parameter dialog."""
1587
2020
  dialog = CalcAllDialog(self)
@@ -1602,11 +2035,26 @@ class ImageViewerWindow(QMainWindow):
1602
2035
  dialog = DilateDialog(self)
1603
2036
  dialog.exec()
1604
2037
 
2038
+ def show_erode_dialog(self):
2039
+ """show the erode dialog"""
2040
+ dialog = ErodeDialog(self)
2041
+ dialog.exec()
2042
+
2043
+ def show_hole_dialog(self):
2044
+ """show the hole dialog"""
2045
+ dialog = HoleDialog(self)
2046
+ dialog.exec()
2047
+
1605
2048
  def show_label_dialog(self):
1606
2049
  """Show the label dialog"""
1607
2050
  dialog = LabelDialog(self)
1608
2051
  dialog.exec()
1609
2052
 
2053
+ def show_thresh_dialog(self):
2054
+ """Show threshold dialog"""
2055
+ thresh_window = ThresholdWindow(self)
2056
+ thresh_window.show() # Non-modal window
2057
+
1610
2058
  def show_mask_dialog(self):
1611
2059
  """Show the mask dialog"""
1612
2060
  dialog = MaskDialog(self)
@@ -1633,6 +2081,33 @@ class ImageViewerWindow(QMainWindow):
1633
2081
  dialog = BranchDialog(self)
1634
2082
  dialog.exec()
1635
2083
 
2084
+ def voronoi(self):
2085
+
2086
+ try:
2087
+
2088
+ if my_network.nodes is not None:
2089
+ shape = my_network.nodes.shape
2090
+ else:
2091
+ shape = None
2092
+
2093
+ if my_network.node_centroids is None:
2094
+ self.show_centroid_dialog()
2095
+ if my_network.node_centroids is None:
2096
+ print("Node centroids must be set")
2097
+ return
2098
+
2099
+ array = pxt.create_voronoi_3d_kdtree(my_network.node_centroids, shape)
2100
+ self.load_channel(3, array, True)
2101
+
2102
+ except Exception as e:
2103
+ print(f"Error generating voronoi: {e}")
2104
+
2105
+
2106
+ def show_modify_dialog(self):
2107
+ """Show the network modify dialog"""
2108
+ dialog = ModifyDialog(self)
2109
+ dialog.exec()
2110
+
1636
2111
 
1637
2112
  def show_binarize_dialog(self):
1638
2113
  """show the binarize dialog"""
@@ -1696,53 +2171,164 @@ class ImageViewerWindow(QMainWindow):
1696
2171
  def load_misc(self, sort):
1697
2172
  """Loads various things"""
1698
2173
 
1699
- try:
2174
+ def uncork(my_dict, trumper = None):
1700
2175
 
1701
- filename, _ = QFileDialog.getOpenFileName(
1702
- self,
1703
- f"Load {sort}",
1704
- "",
1705
- "Spreadsheets (*.xlsx *.csv)"
1706
- )
2176
+ if trumper is None:
2177
+ for thing in my_dict:
2178
+ val = my_dict[thing]
2179
+ new_val = val[0]
2180
+ for i in range(1, len(val)):
2181
+ try:
2182
+ new_val += f" AND {val[i]}"
2183
+ except:
2184
+ break
2185
+ my_dict[thing] = new_val
2186
+ elif trumper == '-':
2187
+ for key, value in my_dict.items():
2188
+ my_dict[key] = value[0]
2189
+ else:
2190
+ for thing in my_dict:
2191
+ val = my_dict[thing]
2192
+ if trumper in val:
2193
+ my_dict[thing] = trumper
2194
+ else:
2195
+ new_val = val[0]
2196
+ for i in range(1, len(val)):
2197
+ try:
2198
+ new_val += f" AND {val[i]}"
2199
+ except:
2200
+ break
2201
+ my_dict[thing] = new_val
2202
+
2203
+ return my_dict
2204
+
2205
+ if sort != 'Merge Nodes':
1707
2206
 
1708
2207
  try:
1709
- if sort == 'Node Identities':
1710
- my_network.load_node_identities(file_path = filename)
1711
2208
 
1712
- if hasattr(my_network, 'node_identities') and my_network.node_identities is not None:
1713
- try:
1714
- self.format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity', 'Node Identities')
1715
- except Exception as e:
1716
- print(f"Error loading node identity table: {e}")
2209
+ filename, _ = QFileDialog.getOpenFileName(
2210
+ self,
2211
+ f"Load {sort}",
2212
+ "",
2213
+ "Spreadsheets (*.xlsx *.csv *.json)"
2214
+ )
1717
2215
 
1718
- elif sort == 'Node Centroids':
1719
- my_network.load_node_centroids(file_path = filename)
2216
+ try:
2217
+ if sort == 'Node Identities':
2218
+ my_network.load_node_identities(file_path = filename)
2219
+
2220
+ first_value = list(my_network.node_identities.values())[0] # Check that there are not multiple IDs
2221
+ if isinstance(first_value, (list, tuple)):
2222
+ trump_value, ok = QInputDialog.getText(
2223
+ self,
2224
+ 'Multiple IDs Detected',
2225
+ 'The node identities appear to contain multiple ids per node in a list.\n'
2226
+ 'If you desire one node ID to trump all others, enter it here.\n'
2227
+ '(Enter "-" to have the first IDs trump all others or press x to skip)'
2228
+ )
2229
+ if not ok or trump_value.strip() == '':
2230
+ trump_value = None
2231
+ elif trump_value.upper() == '-':
2232
+ trump_value = '-'
2233
+ my_network.node_identities = uncork(my_network.node_identities, trump_value)
2234
+ else:
2235
+ trump_value = None
2236
+ my_network.node_identities = uncork(my_network.node_identities, trump_value)
1720
2237
 
1721
- if hasattr(my_network, 'node_centroids') and my_network.node_centroids is not None:
1722
- try:
1723
- self.format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
1724
- except Exception as e:
1725
- print(f"Error loading node centroid table: {e}")
1726
2238
 
1727
- elif sort == 'Edge Centroids':
1728
- my_network.load_edge_centroids(file_path = filename)
2239
+ if hasattr(my_network, 'node_identities') and my_network.node_identities is not None:
2240
+ try:
2241
+ self.format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity', 'Node Identities')
2242
+ except Exception as e:
2243
+ print(f"Error loading node identity table: {e}")
1729
2244
 
1730
- if hasattr(my_network, 'edge_centroids') and my_network.edge_centroids is not None:
1731
- try:
1732
- self.format_for_upperright_table(my_network.edge_centroids, 'EdgeID', ['Z', 'Y', 'X'], 'Edge Centroids')
1733
- except Exception as e:
1734
- print(f"Error loading edge centroid table: {e}")
2245
+ elif sort == 'Node Centroids':
2246
+ my_network.load_node_centroids(file_path = filename)
1735
2247
 
2248
+ if hasattr(my_network, 'node_centroids') and my_network.node_centroids is not None:
2249
+ try:
2250
+ self.format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
2251
+ except Exception as e:
2252
+ print(f"Error loading node centroid table: {e}")
2253
+
2254
+ elif sort == 'Edge Centroids':
2255
+ my_network.load_edge_centroids(file_path = filename)
2256
+
2257
+ if hasattr(my_network, 'edge_centroids') and my_network.edge_centroids is not None:
2258
+ try:
2259
+ self.format_for_upperright_table(my_network.edge_centroids, 'EdgeID', ['Z', 'Y', 'X'], 'Edge Centroids')
2260
+ except Exception as e:
2261
+ print(f"Error loading edge centroid table: {e}")
2262
+
2263
+
2264
+ except Exception as e:
2265
+ import traceback
2266
+ print(traceback.format_exc())
2267
+ print(f"An error has occured: {e}")
1736
2268
 
1737
2269
  except Exception as e:
1738
- print(f"An error has occured: {e}")
2270
+ import traceback
2271
+ print(traceback.format_exc())
2272
+ QMessageBox.critical(
2273
+ self,
2274
+ "Error Loading",
2275
+ f"Failed to load {sort}: {str(e)}"
2276
+ )
1739
2277
 
1740
- except Exception as e:
1741
- QMessageBox.critical(
1742
- self,
1743
- "Error Loading",
1744
- f"Failed to load {sort}: {str(e)}"
1745
- )
2278
+ else:
2279
+ try:
2280
+
2281
+ if len(np.unique(my_network.nodes)) < 3:
2282
+ self.show_label_dialog()
2283
+
2284
+ # First ask user what they want to select
2285
+ msg = QMessageBox()
2286
+ msg.setWindowTitle("Selection Type")
2287
+ msg.setText("Would you like to select a TIFF file or a directory?")
2288
+ tiff_button = msg.addButton("TIFF File", QMessageBox.ButtonRole.AcceptRole)
2289
+ dir_button = msg.addButton("Directory", QMessageBox.ButtonRole.AcceptRole)
2290
+ msg.addButton("Cancel", QMessageBox.ButtonRole.RejectRole)
2291
+
2292
+ msg.exec()
2293
+
2294
+ if msg.clickedButton() == tiff_button:
2295
+ # Code for selecting TIFF files
2296
+ filename, _ = QFileDialog.getOpenFileName(
2297
+ self,
2298
+ "Select TIFF file",
2299
+ "",
2300
+ "TIFF files (*.tiff *.tif)"
2301
+ )
2302
+ if filename:
2303
+ selected_path = filename
2304
+
2305
+ elif msg.clickedButton() == dir_button:
2306
+ # Code for selecting directories
2307
+ dialog = QFileDialog(self)
2308
+ dialog.setOption(QFileDialog.Option.DontUseNativeDialog)
2309
+ dialog.setOption(QFileDialog.Option.ReadOnly)
2310
+ dialog.setFileMode(QFileDialog.FileMode.Directory)
2311
+ dialog.setViewMode(QFileDialog.ViewMode.Detail)
2312
+
2313
+ if dialog.exec() == QFileDialog.DialogCode.Accepted:
2314
+ selected_path = dialog.directory().absolutePath()
2315
+
2316
+ my_network.merge_nodes(selected_path)
2317
+ self.load_channel(0, my_network.nodes, True)
2318
+
2319
+
2320
+ if hasattr(my_network, 'node_identities') and my_network.node_identities is not None:
2321
+ try:
2322
+ self.format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity', 'Node Identities')
2323
+ except Exception as e:
2324
+ print(f"Error loading node identity table: {e}")
2325
+
2326
+ except Exception as e:
2327
+ QMessageBox.critical(
2328
+ self,
2329
+ "Error Merging",
2330
+ f"Failed to load {sort}: {str(e)}"
2331
+ )
1746
2332
 
1747
2333
 
1748
2334
  # Modify load_from_network_obj method
@@ -1834,7 +2420,7 @@ class ImageViewerWindow(QMainWindow):
1834
2420
  self,
1835
2421
  f"Load Network",
1836
2422
  "",
1837
- "Spreadsheets (*.xlsx *.csv)"
2423
+ "Spreadsheets (*.xlsx *.csv *.json)"
1838
2424
  )
1839
2425
 
1840
2426
  my_network.load_network(file_path = filename)
@@ -1870,7 +2456,7 @@ class ImageViewerWindow(QMainWindow):
1870
2456
  else:
1871
2457
  btn.setStyleSheet("")
1872
2458
 
1873
- def load_channel(self, channel_index, channel_data=None, data=False):
2459
+ def load_channel(self, channel_index, channel_data=None, data=False, assign_shape = True):
1874
2460
  """Load a channel and enable active channel selection if needed."""
1875
2461
 
1876
2462
  try:
@@ -1884,7 +2470,8 @@ class ImageViewerWindow(QMainWindow):
1884
2470
  )
1885
2471
  self.channel_data[channel_index] = tifffile.imread(filename)
1886
2472
  if len(self.channel_data[channel_index].shape) == 2:
1887
- self.channel_data[channel_index] = np.stack((self.channel_data[channel_index], self.channel_data[channel_index]), axis = 0) #currently handle 2d arrays by just making them 3d
2473
+ #self.channel_data[channel_index] = np.stack((self.channel_data[channel_index], self.channel_data[channel_index]), axis = 0) #currently handle 2d arrays by just making them 3d
2474
+ self.channel_data[channel_index] = np.expand_dims(self.channel_data[channel_index], axis=0)
1888
2475
 
1889
2476
 
1890
2477
  else:
@@ -1910,18 +2497,21 @@ class ImageViewerWindow(QMainWindow):
1910
2497
  self.active_channel_combo.setEnabled(True)
1911
2498
 
1912
2499
  # Update slider range if this is the first channel loaded
1913
- if not self.slice_slider.isEnabled():
1914
- self.slice_slider.setEnabled(True)
1915
- self.slice_slider.setMinimum(0)
1916
- self.slice_slider.setMaximum(self.channel_data[channel_index].shape[0] - 1)
1917
- self.slice_slider.setValue(0)
1918
- self.current_slice = 0
1919
- else:
1920
- self.slice_slider.setEnabled(True)
1921
- self.slice_slider.setMinimum(0)
1922
- self.slice_slider.setMaximum(self.channel_data[channel_index].shape[0] - 1)
1923
- self.slice_slider.setValue(0)
1924
- self.current_slice = 0
2500
+ if len(self.channel_data[channel_index].shape) == 3:
2501
+ if not self.slice_slider.isEnabled():
2502
+ self.slice_slider.setEnabled(True)
2503
+ self.slice_slider.setMinimum(0)
2504
+ self.slice_slider.setMaximum(self.channel_data[channel_index].shape[0] - 1)
2505
+ self.slice_slider.setValue(0)
2506
+ self.current_slice = 0
2507
+ else:
2508
+ self.slice_slider.setEnabled(True)
2509
+ self.slice_slider.setMinimum(0)
2510
+ self.slice_slider.setMaximum(self.channel_data[channel_index].shape[0] - 1)
2511
+ self.slice_slider.setValue(0)
2512
+ self.current_slice = 0
2513
+ else:
2514
+ self.slice_slider.setEnabled(False)
1925
2515
 
1926
2516
 
1927
2517
  # If this is the first channel loaded, make it active
@@ -1932,6 +2522,10 @@ class ImageViewerWindow(QMainWindow):
1932
2522
  self.channel_buttons[channel_index].click()
1933
2523
  self.min_max[channel_index][0] = np.min(self.channel_data[channel_index])
1934
2524
  self.min_max[channel_index][1] = np.max(self.channel_data[channel_index])
2525
+ self.volume_dict[channel_index] = None #reset volumes
2526
+
2527
+ if assign_shape: #keep original shape tracked to undo resampling.
2528
+ self.original_shape = self.channel_data[channel_index].shape
1935
2529
 
1936
2530
  self.update_display()
1937
2531
 
@@ -2157,117 +2751,132 @@ class ImageViewerWindow(QMainWindow):
2157
2751
  self.update_display(preserve_zoom = (current_xlim, current_ylim))
2158
2752
 
2159
2753
  def update_display(self, preserve_zoom=None):
2160
- """Update the display with currently visible channels and highlight overlay."""
2161
- self.figure.clear()
2162
-
2163
- # Create subplot with tight layout and white figure background
2164
- self.figure.patch.set_facecolor('white')
2165
- self.ax = self.figure.add_subplot(111)
2166
-
2167
- # Store current zoom limits if they exist and weren't provided
2168
- if preserve_zoom is None and hasattr(self, 'ax'):
2169
- current_xlim = self.ax.get_xlim() if self.ax.get_xlim() != (0, 1) else None
2170
- current_ylim = self.ax.get_ylim() if self.ax.get_ylim() != (0, 1) else None
2171
- else:
2172
- current_xlim, current_ylim = preserve_zoom if preserve_zoom else (None, None)
2173
-
2174
- # Define base colors for each channel with increased intensity
2175
- base_colors = self.base_colors
2176
- # Set only the axes (image area) background to black
2177
- self.ax.set_facecolor('black')
2178
-
2179
- # Display each visible channel
2180
- for channel in range(4):
2181
- if (self.channel_visible[channel] and
2182
- self.channel_data[channel] is not None):
2183
- current_image = self.channel_data[channel][self.current_slice, :, :]
2184
-
2185
- # Calculate brightness/contrast limits from entire volume
2186
- img_min = self.min_max[channel][0]
2187
- img_max = self.min_max[channel][1]
2188
-
2189
- # Calculate vmin and vmax, ensuring we don't get a zero range
2190
- if img_min == img_max:
2191
- vmin = img_min
2192
- vmax = img_min + 1
2193
- else:
2194
- vmin = img_min + (img_max - img_min) * self.channel_brightness[channel]['min']
2195
- vmax = img_min + (img_max - img_min) * self.channel_brightness[channel]['max']
2196
-
2197
- # Normalize the image safely
2198
- if vmin == vmax:
2199
- normalized_image = np.zeros_like(current_image)
2200
- else:
2201
- normalized_image = np.clip((current_image - vmin) / (vmax - vmin), 0, 1)
2202
-
2203
- # Create custom colormap with higher intensity
2204
- color = base_colors[channel]
2205
- custom_cmap = LinearSegmentedColormap.from_list(
2206
- f'custom_{channel}',
2207
- [(0,0,0,0), (*color,1)]
2754
+ """Update the display with currently visible channels and highlight overlay."""
2755
+ self.figure.clear()
2756
+
2757
+ # Create subplot with tight layout and white figure background
2758
+ self.figure.patch.set_facecolor('white')
2759
+ self.ax = self.figure.add_subplot(111)
2760
+
2761
+ # Store current zoom limits if they exist and weren't provided
2762
+ if preserve_zoom is None and hasattr(self, 'ax'):
2763
+ current_xlim = self.ax.get_xlim() if self.ax.get_xlim() != (0, 1) else None
2764
+ current_ylim = self.ax.get_ylim() if self.ax.get_ylim() != (0, 1) else None
2765
+ else:
2766
+ current_xlim, current_ylim = preserve_zoom if preserve_zoom else (None, None)
2767
+
2768
+ # Define base colors for each channel with increased intensity
2769
+ base_colors = self.base_colors
2770
+ # Set only the axes (image area) background to black
2771
+ self.ax.set_facecolor('black')
2772
+
2773
+ # Display each visible channel
2774
+ for channel in range(4):
2775
+ if (self.channel_visible[channel] and
2776
+ self.channel_data[channel] is not None):
2777
+
2778
+ # Check if we're dealing with RGB data
2779
+ is_rgb = len(self.channel_data[channel].shape) == 4 and self.channel_data[channel].shape[-1] == 3
2780
+
2781
+ if len(self.channel_data[channel].shape) == 3 and not is_rgb:
2782
+ current_image = self.channel_data[channel][self.current_slice, :, :]
2783
+ elif is_rgb:
2784
+ current_image = self.channel_data[channel][self.current_slice] # Already has RGB channels
2785
+ else:
2786
+ current_image = self.channel_data[channel]
2787
+
2788
+ if is_rgb:
2789
+ # For RGB images, just display directly without colormap
2790
+ self.ax.imshow(current_image,
2791
+ alpha=0.7)
2792
+ else:
2793
+ # Regular channel processing with colormap
2794
+ # Calculate brightness/contrast limits from entire volume
2795
+ img_min = self.min_max[channel][0]
2796
+ img_max = self.min_max[channel][1]
2797
+
2798
+ # Calculate vmin and vmax, ensuring we don't get a zero range
2799
+ if img_min == img_max:
2800
+ vmin = img_min
2801
+ vmax = img_min + 1
2802
+ else:
2803
+ vmin = img_min + (img_max - img_min) * self.channel_brightness[channel]['min']
2804
+ vmax = img_min + (img_max - img_min) * self.channel_brightness[channel]['max']
2805
+
2806
+ # Normalize the image safely
2807
+ if vmin == vmax:
2808
+ normalized_image = np.zeros_like(current_image)
2809
+ else:
2810
+ normalized_image = np.clip((current_image - vmin) / (vmax - vmin), 0, 1)
2811
+
2812
+ # Create custom colormap with higher intensity
2813
+ color = base_colors[channel]
2814
+ custom_cmap = LinearSegmentedColormap.from_list(
2815
+ f'custom_{channel}',
2816
+ [(0,0,0,0), (*color,1)]
2817
+ )
2818
+
2819
+ # Display the image with slightly higher alpha
2820
+ self.ax.imshow(normalized_image,
2821
+ alpha=0.7,
2822
+ cmap=custom_cmap,
2823
+ vmin=0,
2824
+ vmax=1)
2825
+
2826
+ # Rest of the code remains the same...
2827
+ # Add highlight overlay if it exists
2828
+ if self.highlight_overlay is not None:
2829
+ highlight_slice = self.highlight_overlay[self.current_slice]
2830
+ highlight_cmap = LinearSegmentedColormap.from_list(
2831
+ 'highlight',
2832
+ [(0, 0, 0, 0), (1, 1, 0, 1)] # yellow
2208
2833
  )
2834
+ self.ax.imshow(highlight_slice,
2835
+ cmap=highlight_cmap,
2836
+ alpha=0.5)
2837
+
2838
+ # Restore zoom limits if they existed
2839
+ if current_xlim is not None and current_ylim is not None:
2840
+ self.ax.set_xlim(current_xlim)
2841
+ self.ax.set_ylim(current_ylim)
2842
+
2843
+ # Style the axes
2844
+ self.ax.set_xlabel('X')
2845
+ self.ax.set_ylabel('Y')
2846
+ self.ax.set_title(f'Slice {self.current_slice}')
2847
+
2848
+ # Make axis labels and ticks black for visibility against white background
2849
+ self.ax.xaxis.label.set_color('black')
2850
+ self.ax.yaxis.label.set_color('black')
2851
+ self.ax.title.set_color('black')
2852
+ self.ax.tick_params(colors='black')
2853
+ for spine in self.ax.spines.values():
2854
+ spine.set_color('black')
2855
+
2856
+ # Adjust the layout to ensure the plot fits well in the figure
2857
+ self.figure.tight_layout()
2858
+
2859
+ # Redraw measurement points and their labels
2860
+ for point in self.measurement_points:
2861
+ x1, y1, z1 = point['point1']
2862
+ x2, y2, z2 = point['point2']
2863
+ pair_idx = point['pair_index']
2209
2864
 
2210
- # Display the image with slightly higher alpha
2211
- self.ax.imshow(normalized_image,
2212
- alpha=0.7,
2213
- cmap=custom_cmap,
2214
- vmin=0,
2215
- vmax=1)
2216
-
2217
- # Add highlight overlay if it exists
2218
- if self.highlight_overlay is not None:
2219
- highlight_slice = self.highlight_overlay[self.current_slice]
2220
- highlight_cmap = LinearSegmentedColormap.from_list(
2221
- 'highlight',
2222
- [(0, 0, 0, 0), (1, 1, 0, 1)] # yellow
2223
- )
2224
- self.ax.imshow(highlight_slice,
2225
- cmap=highlight_cmap,
2226
- alpha=0.5)
2227
-
2228
- # Restore zoom limits if they existed
2229
- if current_xlim is not None and current_ylim is not None:
2230
- self.ax.set_xlim(current_xlim)
2231
- self.ax.set_ylim(current_ylim)
2232
-
2233
- # Style the axes
2234
- self.ax.set_xlabel('X')
2235
- self.ax.set_ylabel('Y')
2236
- self.ax.set_title(f'Slice {self.current_slice}')
2237
-
2238
- # Make axis labels and ticks black for visibility against white background
2239
- self.ax.xaxis.label.set_color('black')
2240
- self.ax.yaxis.label.set_color('black')
2241
- self.ax.title.set_color('black')
2242
- self.ax.tick_params(colors='black')
2243
- for spine in self.ax.spines.values():
2244
- spine.set_color('black')
2245
-
2246
- # Adjust the layout to ensure the plot fits well in the figure
2247
- self.figure.tight_layout()
2248
-
2249
- # Redraw measurement points and their labels
2250
- for point in self.measurement_points:
2251
- x1, y1, z1 = point['point1']
2252
- x2, y2, z2 = point['point2']
2253
- pair_idx = point['pair_index']
2254
-
2255
- # Draw points and labels if they're on current slice
2256
- if z1 == self.current_slice:
2257
- self.ax.plot(x1, y1, 'yo', markersize=8)
2258
- self.ax.text(x1, y1+5, str(pair_idx),
2259
- color='white', ha='center', va='bottom')
2260
- if z2 == self.current_slice:
2261
- self.ax.plot(x2, y2, 'yo', markersize=8)
2262
- self.ax.text(x2, y2+5, str(pair_idx),
2263
- color='white', ha='center', va='bottom')
2264
-
2265
- # Draw line if both points are on current slice
2266
- if z1 == z2 == self.current_slice:
2267
- self.ax.plot([x1, x2], [y1, y2], 'r--', alpha=0.5)
2268
-
2865
+ # Draw points and labels if they're on current slice
2866
+ if z1 == self.current_slice:
2867
+ self.ax.plot(x1, y1, 'yo', markersize=8)
2868
+ self.ax.text(x1, y1+5, str(pair_idx),
2869
+ color='white', ha='center', va='bottom')
2870
+ if z2 == self.current_slice:
2871
+ self.ax.plot(x2, y2, 'yo', markersize=8)
2872
+ self.ax.text(x2, y2+5, str(pair_idx),
2873
+ color='white', ha='center', va='bottom')
2874
+
2875
+ # Draw line if both points are on current slice
2876
+ if z1 == z2 == self.current_slice:
2877
+ self.ax.plot([x1, x2], [y1, y2], 'r--', alpha=0.5)
2269
2878
 
2270
- self.canvas.draw()
2879
+ self.canvas.draw()
2271
2880
 
2272
2881
  def show_netshow_dialog(self):
2273
2882
  dialog = NetShowDialog(self)
@@ -2277,6 +2886,23 @@ class ImageViewerWindow(QMainWindow):
2277
2886
  dialog = PartitionDialog(self)
2278
2887
  dialog.exec()
2279
2888
 
2889
+ def show_radial_dialog(self):
2890
+ dialog = RadialDialog(self)
2891
+ dialog.exec()
2892
+
2893
+ def show_degree_dist_dialog(self):
2894
+ dialog = DegreeDistDialog(self)
2895
+ dialog.exec()
2896
+
2897
+ def show_neighbor_id_dialog(self):
2898
+ dialog = NeighborIdentityDialog(self)
2899
+ dialog.exec()
2900
+
2901
+ def show_random_dialog(self):
2902
+ dialog = RandomDialog(self)
2903
+ dialog.exec()
2904
+
2905
+
2280
2906
  def show_interaction_dialog(self):
2281
2907
  dialog = InteractionDialog(self)
2282
2908
  dialog.exec()
@@ -2285,10 +2911,19 @@ class ImageViewerWindow(QMainWindow):
2285
2911
  dialog = DegreeDialog(self)
2286
2912
  dialog.exec()
2287
2913
 
2914
+
2915
+ def show_hub_dialog(self):
2916
+ dialog = HubDialog(self)
2917
+ dialog.exec()
2918
+
2288
2919
  def show_mother_dialog(self):
2289
2920
  dialog = MotherDialog(self)
2290
2921
  dialog.exec()
2291
2922
 
2923
+ def show_code_dialog(self, sort = 'Community'):
2924
+ dialog = CodeDialog(self, sort = sort)
2925
+ dialog.exec()
2926
+
2292
2927
 
2293
2928
 
2294
2929
  #TABLE RELATED:
@@ -3509,6 +4144,9 @@ class NetShowDialog(QDialog):
3509
4144
  def show_network(self):
3510
4145
  # Get parameters and run analysis
3511
4146
  geo = self.geo_layout.isChecked()
4147
+ if geo:
4148
+ if my_network.node_centroids is None:
4149
+ self.parent().show_centroid_dialog()
3512
4150
  accepted_mode = self.mode_selector.currentIndex() # Convert to 1-based index
3513
4151
  # Get directory (None if empty)
3514
4152
  directory = self.directory.text() if self.directory.text() else None
@@ -3533,6 +4171,8 @@ class NetShowDialog(QDialog):
3533
4171
  self.accept()
3534
4172
  except Exception as e:
3535
4173
  print(f"Error showing network: {e}")
4174
+ import traceback
4175
+ print(traceback.format_exc())
3536
4176
 
3537
4177
  class PartitionDialog(QDialog):
3538
4178
  def __init__(self, parent=None):
@@ -3555,6 +4195,12 @@ class PartitionDialog(QDialog):
3555
4195
  self.mode_selector.setCurrentIndex(0) # Default to Mode 1
3556
4196
  layout.addRow("Execution Mode:", self.mode_selector)
3557
4197
 
4198
+ # stats checkbox (default True)
4199
+ self.stats = QPushButton("Stats")
4200
+ self.stats.setCheckable(True)
4201
+ self.stats.setChecked(True)
4202
+ layout.addRow("Community Stats:", self.stats)
4203
+
3558
4204
  # Add Run button
3559
4205
  run_button = QPushButton("Partition")
3560
4206
  run_button.clicked.connect(self.partition)
@@ -3564,18 +4210,204 @@ class PartitionDialog(QDialog):
3564
4210
 
3565
4211
  accepted_mode = self.mode_selector.currentIndex()
3566
4212
  weighted = self.weighted.isChecked()
4213
+ dostats = self.stats.isChecked()
4214
+
4215
+ my_network.communities = None
3567
4216
 
3568
4217
  try:
3569
- my_network.community_partition(weighted = weighted, style = accepted_mode)
4218
+ stats = my_network.community_partition(weighted = weighted, style = accepted_mode, dostats = dostats)
3570
4219
  print(f"Discovered communities: {my_network.communities}")
3571
4220
 
3572
- self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'CommunityID')
4221
+ self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'CommunityID', title = 'Community Partition')
4222
+
4223
+ if len(stats.keys()) > 0:
4224
+ self.parent().format_for_upperright_table(stats, title = 'Community Stats')
3573
4225
 
3574
4226
  self.accept()
3575
4227
 
3576
4228
  except Exception as e:
3577
4229
  print(f"Error creating communities: {e}")
3578
4230
 
4231
+ class RadialDialog(QDialog):
4232
+
4233
+ def __init__(self, parent=None):
4234
+
4235
+ super().__init__(parent)
4236
+ self.setWindowTitle("Radial Parameters")
4237
+ self.setModal(True)
4238
+
4239
+ layout = QFormLayout(self)
4240
+
4241
+ self.distance = QLineEdit("50")
4242
+ layout.addRow("Bucket Distance for Searching For Node Neighbors (automatically scaled by xy and z scales):", self.distance)
4243
+
4244
+ self.directory = QLineEdit("")
4245
+ layout.addRow("Output Directory:", self.directory)
4246
+
4247
+ # Add Run button
4248
+ run_button = QPushButton("Get Radial Distribution")
4249
+ run_button.clicked.connect(self.radial)
4250
+ layout.addWidget(run_button)
4251
+
4252
+ def radial(self):
4253
+
4254
+ distance = float(self.distance.text()) if self.distance.text().strip() else 50
4255
+
4256
+ directory = str(self.distance.text()) if self.directory.text().strip() else None
4257
+
4258
+ if my_network.node_centroids is None:
4259
+ self.parent().show_centroid_dialog()
4260
+
4261
+ radial = my_network.radial_distribution(distance, directory = directory)
4262
+
4263
+ self.parent().format_for_upperright_table(radial, 'Radial Distance From Any Node', 'Average Number of Neighboring Nodes', title = 'Radial Distribution Analysis')
4264
+
4265
+ self.accept()
4266
+
4267
+ class DegreeDistDialog(QDialog):
4268
+
4269
+ def __init__(self, parent=None):
4270
+
4271
+ super().__init__(parent)
4272
+ self.setWindowTitle("Degree Distribution Parameters")
4273
+ self.setModal(True)
4274
+
4275
+ layout = QFormLayout(self)
4276
+
4277
+ self.directory = QLineEdit("")
4278
+ layout.addRow("Output Directory:", self.directory)
4279
+
4280
+ # Add Run button
4281
+ run_button = QPushButton("Get Degree Distribution")
4282
+ run_button.clicked.connect(self.degreedist)
4283
+ layout.addWidget(run_button)
4284
+
4285
+ def degreedist(self):
4286
+
4287
+ try:
4288
+
4289
+ directory = str(self.distance.text()) if self.directory.text().strip() else None
4290
+
4291
+ degrees = my_network.degree_distribution(directory = directory)
4292
+
4293
+
4294
+ self.parent().format_for_upperright_table(degrees, 'Degree (k)', 'Proportion of nodes with degree (p(k))', title = 'Degree Distribution Analysis')
4295
+
4296
+ self.accept()
4297
+
4298
+ except Excpetion as e:
4299
+ print(f"An error occurred: {e}")
4300
+
4301
+ class NeighborIdentityDialog(QDialog):
4302
+
4303
+ def __init__(self, parent=None):
4304
+
4305
+ super().__init__(parent)
4306
+ 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)")
4307
+ self.setModal(True)
4308
+
4309
+ layout = QFormLayout(self)
4310
+
4311
+ if my_network.node_identities is not None:
4312
+ self.root = QComboBox()
4313
+ self.root.addItems(list(set(my_network.node_identities.values())))
4314
+ self.root.setCurrentIndex(0)
4315
+ layout.addRow("Root Identity to Search for Neighbor's IDs (search uses nodes of this ID, finds what IDs they connect to", self.root)
4316
+ else:
4317
+ self.root = None
4318
+
4319
+ self.directory = QLineEdit("")
4320
+ layout.addRow("Output Directory:", self.directory)
4321
+
4322
+ self.mode = QComboBox()
4323
+ self.mode.addItems(["From Network - Based on Absolute Connectivity", "Use Labeled Nodes - Based on Morphological Neighborhood Densities"])
4324
+ self.mode.setCurrentIndex(0)
4325
+ layout.addRow("Mode", self.mode)
4326
+
4327
+ self.search = QLineEdit("")
4328
+ layout.addRow("Search Radius (Ignore if using network):", self.search)
4329
+
4330
+ # Add Run button
4331
+ run_button = QPushButton("Get Neighborhood Identity Distribution")
4332
+ run_button.clicked.connect(self.neighborids)
4333
+ layout.addWidget(run_button)
4334
+
4335
+ def neighborids(self):
4336
+
4337
+ try:
4338
+
4339
+ try:
4340
+ root = self.root.currentText()
4341
+ except:
4342
+ pass
4343
+
4344
+ directory = self.directory.text() if self.directory.text().strip() else None
4345
+
4346
+ mode = self.mode.currentIndex()
4347
+
4348
+ search = float(self.search.text()) if self.search.text().strip() else 0
4349
+
4350
+
4351
+ result, result2, title1, title2, densities = my_network.neighborhood_identities(root = root, directory = directory, mode = mode, search = search)
4352
+
4353
+ self.parent().format_for_upperright_table(result, 'Node Identity', 'Amount', title = title1)
4354
+ self.parent().format_for_upperright_table(result2, 'Node Identity', 'Proportion', title = title2)
4355
+
4356
+ if mode == 1:
4357
+
4358
+ self.parent().format_for_upperright_table(densities, 'Node Identity', 'Density in search/density total', title = f'Clustering Factor of Node Identities with {search} from nodes {root}')
4359
+
4360
+
4361
+ self.accept()
4362
+ except Exception as e:
4363
+ print(f"Error: {e}")
4364
+
4365
+
4366
+
4367
+
4368
+
4369
+
4370
+
4371
+
4372
+ class RandomDialog(QDialog):
4373
+
4374
+ def __init__(self, parent=None):
4375
+
4376
+ super().__init__(parent)
4377
+ self.setWindowTitle("Degree Distribution Parameters")
4378
+ self.setModal(True)
4379
+
4380
+ layout = QFormLayout(self)
4381
+
4382
+
4383
+ # stats checkbox (default True)
4384
+ self.weighted = QPushButton("weighted")
4385
+ self.weighted.setCheckable(True)
4386
+ self.weighted.setChecked(True)
4387
+ layout.addRow("Allow Random Network to be weighted? (Whether or not edges can be repeatedly assigned between the same set of nodes to increase their weights, or if they must always find a new partner):", self.weighted)
4388
+
4389
+
4390
+ # Add Run button
4391
+ run_button = QPushButton("Get Random Network (Will go in Selection Table)")
4392
+ run_button.clicked.connect(self.random)
4393
+ layout.addWidget(run_button)
4394
+
4395
+ def random(self):
4396
+
4397
+ weighted = self.weighted.isChecked()
4398
+
4399
+ _, df = my_network.assign_random(weighted = weighted)
4400
+
4401
+ # Create new model with filtered DataFrame and update selection table
4402
+ new_model = PandasModel(df)
4403
+ self.parent().selection_table.setModel(new_model)
4404
+
4405
+ # Switch to selection table
4406
+ self.parent().selection_button.click()
4407
+
4408
+ self.accept()
4409
+
4410
+
3579
4411
 
3580
4412
  class InteractionDialog(QDialog):
3581
4413
 
@@ -3645,6 +4477,9 @@ class DegreeDialog(QDialog):
3645
4477
  self.mode_selector.setCurrentIndex(0) # Default to Mode 1
3646
4478
  layout.addRow("Execution Mode:", self.mode_selector)
3647
4479
 
4480
+ self.mask_limiter = QLineEdit("1")
4481
+ layout.addRow("Masks smaller high degree proportion of nodes (ignore if only returning degrees)", self.mask_limiter)
4482
+
3648
4483
  self.down_factor = QLineEdit("1")
3649
4484
  layout.addRow("down_factor (for speeding up overlay generation - ignore if only returning degrees:", self.down_factor)
3650
4485
 
@@ -3664,6 +4499,11 @@ class DegreeDialog(QDialog):
3664
4499
  except ValueError:
3665
4500
  down_factor = 1
3666
4501
 
4502
+ try:
4503
+ mask_limiter = float(self.mask_limiter.text()) if self.mask_limiter.text() else 1
4504
+ except ValueError:
4505
+ mask_limiter = 1
4506
+
3667
4507
  if self.parent().active_channel == 1:
3668
4508
  active_data = self.parent().channel_data[0]
3669
4509
  else:
@@ -3680,9 +4520,47 @@ class DegreeDialog(QDialog):
3680
4520
 
3681
4521
  original_shape = copy.deepcopy(active_data.shape)
3682
4522
 
3683
- temp_network = n3d.Network_3D(nodes = active_data, node_centroids = my_network.node_centroids, network = my_network.network, network_lists = my_network.network_lists)
3684
4523
 
3685
- result, nodes = temp_network.get_degrees(called = True, no_img = accepted_mode, down_factor = down_factor)
4524
+ if mask_limiter < 1 and accepted_mode != 0:
4525
+
4526
+ if len(np.unique(active_data)) < 3:
4527
+ active_data, _ = n3d.label_objects(active_data)
4528
+
4529
+ node_list = list(my_network.network.nodes)
4530
+ node_dict = {}
4531
+
4532
+ for node in node_list:
4533
+ node_dict[node] = (my_network.network.degree(node))
4534
+
4535
+ # Calculate the number of top proportion% entries
4536
+ num_items = len(node_dict)
4537
+ num_top_10_percent = max(1, int(num_items * mask_limiter)) # Ensure at least one item
4538
+
4539
+ # Sort the dictionary by values in descending order and get the top 10%
4540
+ sorted_items = sorted(node_dict.items(), key=lambda item: item[1], reverse=True)
4541
+ top_10_percent_items = sorted_items[:num_top_10_percent]
4542
+
4543
+ # Extract the keys from the top proportion% items
4544
+ top_10_percent_keys = [key for key, value in top_10_percent_items]
4545
+
4546
+ mask = np.isin(active_data, top_10_percent_keys)
4547
+ nodes = mask * active_data
4548
+ new_centroids = {}
4549
+ for node in my_network.node_centroids:
4550
+ if node in top_10_percent_keys:
4551
+ new_centroids[node] = my_network.node_centroids[node]
4552
+ del mask
4553
+
4554
+ temp_network = n3d.Network_3D(nodes = nodes, node_centroids = new_centroids, network = my_network.network, network_lists = my_network.network_lists)
4555
+
4556
+ result, nodes = temp_network.get_degrees(called = True, no_img = accepted_mode, down_factor = down_factor)
4557
+
4558
+ else:
4559
+ temp_network = n3d.Network_3D(nodes = active_data, node_centroids = my_network.node_centroids, network = my_network.network, network_lists = my_network.network_lists)
4560
+
4561
+ result, nodes = temp_network.get_degrees(called = True, no_img = accepted_mode, down_factor = down_factor)
4562
+
4563
+
3686
4564
 
3687
4565
  self.parent().format_for_upperright_table(result, 'Node ID', 'Degree', title = 'Degrees of nodes')
3688
4566
 
@@ -3698,9 +4576,75 @@ class DegreeDialog(QDialog):
3698
4576
 
3699
4577
  except Exception as e:
3700
4578
 
4579
+ import traceback
4580
+ print(traceback.format_exc())
4581
+
3701
4582
  print(f"Error finding degrees: {e}")
3702
4583
 
3703
4584
 
4585
+ class HubDialog(QDialog):
4586
+
4587
+ def __init__(self, parent=None):
4588
+
4589
+ super().__init__(parent)
4590
+ self.setWindowTitle("Hub Parameters")
4591
+ self.setModal(True)
4592
+
4593
+ layout = QFormLayout(self)
4594
+
4595
+ layout.addRow("Note:", QLabel(f"Finds hubs, which are nodes in the network that have the shortest number of steps to the other nodes\nWe can draw optional overlays to Overlay 2 as described below:"))
4596
+
4597
+ # Overlay checkbox (default True)
4598
+ self.overlay = QPushButton("Overlay")
4599
+ self.overlay.setCheckable(True)
4600
+ self.overlay.setChecked(True)
4601
+ layout.addRow("Make Overlay?:", self.overlay)
4602
+
4603
+
4604
+ self.proportion = QLineEdit("0.15")
4605
+ layout.addRow("Proportion of most connected hubs to keep (1 would imply returning entire network)", self.proportion)
4606
+
4607
+
4608
+ # Add Run button
4609
+ run_button = QPushButton("Get hubs")
4610
+ run_button.clicked.connect(self.hubs)
4611
+ layout.addWidget(run_button)
4612
+
4613
+ def hubs(self):
4614
+
4615
+ try:
4616
+
4617
+ try:
4618
+ proportion = float(self.proportion.text()) if self.proportion.text() else 1
4619
+ except ValueError:
4620
+ proportion = 1
4621
+
4622
+ overlay = self.overlay.isChecked()
4623
+
4624
+ result, img = my_network.isolate_hubs(proportion = proportion, retimg = overlay)
4625
+
4626
+ hub_dict = {}
4627
+
4628
+ for node in result:
4629
+ hub_dict[node] = my_network.network.degree(node)
4630
+
4631
+ self.parent().format_for_upperright_table(hub_dict, 'NodeID', 'Degree', title = f'Upper {proportion} Hub Nodes')
4632
+
4633
+ if img is not None:
4634
+
4635
+ self.parent().load_channel(3, channel_data = img, data = True)
4636
+
4637
+
4638
+ self.accept()
4639
+
4640
+ except Exception as e:
4641
+
4642
+ import traceback
4643
+ print(traceback.format_exc())
4644
+
4645
+ print(f"Error finding hubs: {e}")
4646
+
4647
+
3704
4648
 
3705
4649
  class MotherDialog(QDialog):
3706
4650
 
@@ -3749,7 +4693,12 @@ class MotherDialog(QDialog):
3749
4693
  G, result = my_network.isolate_mothers(self, louvain = my_network.communities, ret_nodes = False, called = True)
3750
4694
  self.parent().load_channel(2, channel_data = result, data = True)
3751
4695
 
3752
- self.parent().format_for_upperright_table(G.nodes(), 'Mothers', title = 'Mother Nodes')
4696
+ degree_dict = {}
4697
+
4698
+ for node in G.nodes():
4699
+ degree_dict[node] = my_network.network.degree(node)
4700
+
4701
+ self.parent().format_for_upperright_table(degree_dict, 'Mother ID', 'Degree', title = 'Mother Nodes')
3753
4702
 
3754
4703
 
3755
4704
  self.accept()
@@ -3759,11 +4708,78 @@ class MotherDialog(QDialog):
3759
4708
  print(f"Error finding mothers: {e}")
3760
4709
 
3761
4710
 
4711
+ class CodeDialog(QDialog):
3762
4712
 
4713
+ def __init__(self, parent=None, sort = 'Community'):
3763
4714
 
4715
+ super().__init__(parent)
4716
+ self.setWindowTitle(f"{sort} Code Parameters (Will go to Overlay2)")
4717
+ self.setModal(True)
3764
4718
 
4719
+ layout = QFormLayout(self)
3765
4720
 
3766
- # PROCESS MENU RELATED:
4721
+ self.sort = sort
4722
+
4723
+ self.down_factor = QLineEdit("")
4724
+ layout.addRow("down_factor (for speeding up overlay generation - optional):", self.down_factor)
4725
+
4726
+ # Add mode selection dropdown
4727
+ self.mode_selector = QComboBox()
4728
+ self.mode_selector.addItems(["Color Coded", "Grayscale Coded"])
4729
+ self.mode_selector.setCurrentIndex(0) # Default to Mode 1
4730
+ layout.addRow("Execution Mode:", self.mode_selector)
4731
+
4732
+
4733
+ # Add Run button
4734
+ run_button = QPushButton(f"{sort} Code")
4735
+ run_button.clicked.connect(self.code)
4736
+ layout.addWidget(run_button)
4737
+
4738
+ def code(self):
4739
+
4740
+ try:
4741
+
4742
+ mode = self.mode_selector.currentIndex()
4743
+
4744
+ down_factor = float(self.down_factor.text()) if self.down_factor.text().strip() else None
4745
+
4746
+
4747
+ if self.sort == 'Community':
4748
+ if my_network.communities is None:
4749
+ self.parent().show_partition_dialog()
4750
+ if my_network.communities is None:
4751
+ return
4752
+ elif my_network.node_identities is None:
4753
+ print("Node identities are not set")
4754
+ return
4755
+
4756
+ if self.sort == 'Community':
4757
+ if mode == 0:
4758
+ image, output = my_network.extract_communities(down_factor = down_factor)
4759
+ elif mode == 1:
4760
+ image, output = my_network.extract_communities(color_code = False, down_factor = down_factor)
4761
+ else:
4762
+ if mode == 0:
4763
+ image, output = my_network.extract_communities(down_factor = down_factor, identities = True)
4764
+ elif mode == 1:
4765
+ image, output = my_network.extract_communities(color_code = False, down_factor = down_factor, identities = True)
4766
+
4767
+ self.parent().format_for_upperright_table(output, f'{self.sort} Id', f'Encoding Val: {self.sort}', 'Legend')
4768
+
4769
+
4770
+ self.parent().load_channel(3, image, True)
4771
+ self.accept()
4772
+
4773
+ except Exception as e:
4774
+ print(f"An error has occurred: {e}")
4775
+ import traceback
4776
+ print(traceback.format_exc())
4777
+
4778
+
4779
+
4780
+
4781
+
4782
+ # PROCESS MENU RELATED:
3767
4783
 
3768
4784
 
3769
4785
  class ResizeDialog(QDialog):
@@ -3790,7 +4806,11 @@ class ResizeDialog(QDialog):
3790
4806
  self.cubic.setChecked(False)
3791
4807
  layout.addRow("Use cubic algorithm:", self.cubic)
3792
4808
 
3793
-
4809
+ if self.parent().original_shape is not None:
4810
+ undo_button = QPushButton(f"Resample to original shape: {self.parent().original_shape}")
4811
+ undo_button.clicked.connect(lambda: self.run_resize(undo = True))
4812
+ layout.addRow(undo_button)
4813
+
3794
4814
  run_button = QPushButton("Run Resize")
3795
4815
  run_button.clicked.connect(self.run_resize)
3796
4816
  layout.addRow(run_button)
@@ -3800,9 +4820,9 @@ class ResizeDialog(QDialog):
3800
4820
  self.resize.clear()
3801
4821
  self.zsize.setText("1")
3802
4822
  self.xsize.setText("1")
3803
- self.ysize.setText("1")
4823
+ self.ysize.setText("1")
3804
4824
 
3805
- def run_resize(self):
4825
+ def run_resize(self, undo = False):
3806
4826
  try:
3807
4827
  # Get parameters
3808
4828
  try:
@@ -3851,16 +4871,31 @@ class ResizeDialog(QDialog):
3851
4871
  self.parent().slice_slider.setValue(0)
3852
4872
  self.parent().current_slice = 0
3853
4873
 
3854
- # Process each channel
3855
- for channel in range(4):
3856
- if self.parent().channel_data[channel] is not None:
3857
- resized_data = n3d.resize(self.parent().channel_data[channel], resize, order)
3858
- self.parent().load_channel(channel, channel_data=resized_data, data=True)
4874
+ if not undo:
4875
+ # Process each channel
4876
+ for channel in range(4):
4877
+ if self.parent().channel_data[channel] is not None:
4878
+ resized_data = n3d.resize(self.parent().channel_data[channel], resize, order)
4879
+ self.parent().load_channel(channel, channel_data=resized_data, data=True, assign_shape = False)
4880
+
4881
+
4882
+ # Process highlight overlay if it exists
4883
+ if self.parent().highlight_overlay is not None:
4884
+ self.parent().highlight_overlay = n3d.resize(self.parent().highlight_overlay, resize, order)
4885
+ else:
4886
+ # Process each channel
4887
+ if array_shape == self.parent().original_shape:
4888
+ return
4889
+ for channel in range(4):
4890
+ if self.parent().channel_data[channel] is not None:
4891
+ resized_data = n3d.upsample_with_padding(self.parent().channel_data[channel], original_shape = self.parent().original_shape)
4892
+ self.parent().load_channel(channel, channel_data=resized_data, data=True, assign_shape = False)
4893
+
4894
+
4895
+ # Process highlight overlay if it exists
4896
+ if self.parent().highlight_overlay is not None:
4897
+ self.parent().highlight_overlay = n3d.upsample_with_padding(self.parent().highlight_overlay, original_shape = self.parent().original_shape)
3859
4898
 
3860
-
3861
- # Process highlight overlay if it exists
3862
- if self.parent().highlight_overlay is not None:
3863
- self.parent().highlight_overlay = n3d.resize(self.parent().highlight_overlay, resize, order)
3864
4899
 
3865
4900
  # Update slider range based on new z-dimension
3866
4901
  for channel in self.parent().channel_data:
@@ -4043,6 +5078,152 @@ class LabelDialog(QDialog):
4043
5078
  f"Error running label: {str(e)}"
4044
5079
  )
4045
5080
 
5081
+ class ThresholdWindow(QMainWindow):
5082
+ def __init__(self, parent=None):
5083
+ super().__init__(parent)
5084
+ self.setWindowTitle("Threshold Params (Active Image)")
5085
+
5086
+ # Create central widget and layout
5087
+ central_widget = QWidget()
5088
+ self.setCentralWidget(central_widget)
5089
+ layout = QFormLayout(central_widget)
5090
+
5091
+ self.min = QLineEdit("")
5092
+ layout.addRow("Minimum Value to retain:", self.min)
5093
+
5094
+ # Create widgets
5095
+ self.max = QLineEdit("")
5096
+ layout.addRow("Maximum Value to retain:", self.max)
5097
+
5098
+ # Add mode selection dropdown
5099
+ self.mode_selector = QComboBox()
5100
+ self.mode_selector.addItems(["Using Volumes", "Using Label/Brightness"])
5101
+ self.mode_selector.setCurrentIndex(0) # Default to Mode 1
5102
+ layout.addRow("Execution Mode:", self.mode_selector)
5103
+
5104
+ # Add Run button
5105
+ prev_button = QPushButton("Preview")
5106
+ prev_button.clicked.connect(self.run_preview)
5107
+ layout.addRow(prev_button)
5108
+
5109
+ # Add Run button
5110
+ run_button = QPushButton("Apply Threshold")
5111
+ run_button.clicked.connect(self.thresh)
5112
+ layout.addRow(run_button)
5113
+
5114
+ # Set a reasonable default size
5115
+ self.setMinimumWidth(300)
5116
+
5117
+ def run_preview(self):
5118
+
5119
+ def get_valid_float(text, default_value):
5120
+ try:
5121
+ return float(text) if text.strip() else default_value
5122
+ except ValueError:
5123
+ print(f"Invalid input: {text}")
5124
+ return default_value
5125
+
5126
+ try:
5127
+ channel = self.parent().active_channel
5128
+ accepted_mode = self.mode_selector.currentIndex()
5129
+
5130
+ if accepted_mode == 0:
5131
+ if len(np.unique(self.parent().channel_data[self.parent().active_channel])) < 3:
5132
+ self.parent().show_label_dialog()
5133
+
5134
+ if self.parent().volume_dict[channel] is None:
5135
+ self.parent().volumes()
5136
+
5137
+ volumes = self.parent().volume_dict[channel]
5138
+ default_max = max(volumes.values())
5139
+ default_min = min(volumes.values())
5140
+
5141
+ max_val = get_valid_float(self.max.text(), default_max)
5142
+ min_val = get_valid_float(self.min.text(), default_min)
5143
+
5144
+ valid_indices = [item for item in volumes
5145
+ if min_val <= volumes[item] <= max_val]
5146
+
5147
+ elif accepted_mode == 1:
5148
+ channel_data = self.parent().channel_data[self.parent().active_channel]
5149
+ default_max = np.max(channel_data)
5150
+ default_min = np.min(channel_data)
5151
+
5152
+ max_val = int(get_valid_float(self.max.text(), default_max))
5153
+ min_val = int(get_valid_float(self.min.text(), default_min))
5154
+
5155
+ if min_val > max_val:
5156
+ min_val, max_val = max_val, min_val
5157
+
5158
+ valid_indices = list(range(min_val, max_val + 1))
5159
+
5160
+ if channel == 0:
5161
+ self.parent().create_highlight_overlay(node_indices = valid_indices)
5162
+ elif channel == 1:
5163
+ self.parent().create_highlight_overlay(edge_indices = valid_indices)
5164
+ elif channel == 2:
5165
+ self.parent().create_highlight_overlay(overlay1_indices = valid_indices)
5166
+ elif channel == 3:
5167
+ self.parent().create_highlight_overlay(overlay2_indices = valid_indices)
5168
+
5169
+ except Exception as e:
5170
+ print(f"Error showing preview: {e}")
5171
+
5172
+ def thresh(self):
5173
+ try:
5174
+
5175
+ self.run_preview()
5176
+ channel_data = self.parent().channel_data[self.parent().active_channel]
5177
+ mask = self.parent().highlight_overlay > 0
5178
+ channel_data = channel_data * mask
5179
+ self.parent().load_channel(self.parent().active_channel, channel_data, True)
5180
+ self.parent().update_display()
5181
+ self.close()
5182
+
5183
+ except Exception as e:
5184
+ QMessageBox.critical(
5185
+ self,
5186
+ "Error",
5187
+ f"Error running threshold: {str(e)}"
5188
+ )
5189
+
5190
+
5191
+ class SmartDilateDialog(QDialog):
5192
+ def __init__(self, parent, params):
5193
+ super().__init__(parent)
5194
+ self.setWindowTitle("Additional Smart Dilate Parameters")
5195
+ self.setModal(True)
5196
+
5197
+ layout = QFormLayout(self)
5198
+
5199
+ # GPU checkbox (default True)
5200
+ self.GPU = QPushButton("GPU")
5201
+ self.GPU.setCheckable(True)
5202
+ self.GPU.setChecked(True)
5203
+ layout.addRow("Use GPU:", self.GPU)
5204
+
5205
+ self.down_factor = QLineEdit("")
5206
+ layout.addRow("Internal Downsample for GPU (if needed):", self.down_factor)
5207
+
5208
+ self.params = params
5209
+
5210
+ # Add Run button
5211
+ run_button = QPushButton("Dilate")
5212
+ run_button.clicked.connect(self.smart_dilate)
5213
+ layout.addRow(run_button)
5214
+
5215
+ def smart_dilate(self):
5216
+
5217
+ GPU = self.GPU.isChecked()
5218
+ down_factor = float(self.down_factor.text()) if self.down_factor.text().strip() else None
5219
+ active_data, amount, xy_scale, z_scale = self.params
5220
+
5221
+ dilate_xy, dilate_z = n3d.dilation_length_to_pixels(xy_scale, z_scale, amount, amount)
5222
+
5223
+ result = sdl.smart_dilate(active_data, dilate_xy, dilate_z, GPU = GPU, predownsample = down_factor)
5224
+
5225
+ self.parent().load_channel(self.parent().active_channel, result, True)
5226
+ self.accept()
4046
5227
 
4047
5228
 
4048
5229
 
@@ -4073,6 +5254,12 @@ class DilateDialog(QDialog):
4073
5254
  self.z_scale = QLineEdit(z_scale)
4074
5255
  layout.addRow("z_scale:", self.z_scale)
4075
5256
 
5257
+ # Add mode selection dropdown
5258
+ self.mode_selector = QComboBox()
5259
+ self.mode_selector.addItems(["Binary Dilation", "Preserve Labels (slower)", "Recursive Binary Dilation (Use if the dilation radius is much larger than your objects)"])
5260
+ self.mode_selector.setCurrentIndex(0) # Default to Mode 1
5261
+ layout.addRow("Execution Mode:", self.mode_selector)
5262
+
4076
5263
  # Add Run button
4077
5264
  run_button = QPushButton("Run Dilate")
4078
5265
  run_button.clicked.connect(self.run_dilate)
@@ -4080,6 +5267,8 @@ class DilateDialog(QDialog):
4080
5267
 
4081
5268
  def run_dilate(self):
4082
5269
  try:
5270
+
5271
+ accepted_mode = self.mode_selector.currentIndex()
4083
5272
 
4084
5273
  # Get amount
4085
5274
  try:
@@ -4101,13 +5290,104 @@ class DilateDialog(QDialog):
4101
5290
  active_data = self.parent().channel_data[self.parent().active_channel]
4102
5291
  if active_data is None:
4103
5292
  raise ValueError("No active image selected")
4104
-
5293
+
5294
+ if accepted_mode == 1:
5295
+ dialog = SmartDilateDialog(self.parent(), [active_data, amount, xy_scale, z_scale])
5296
+ dialog.exec()
5297
+ self.accept()
5298
+ return
5299
+
5300
+ if accepted_mode == 2:
5301
+ recursive = True
5302
+ else:
5303
+ recursive = False
5304
+
4105
5305
  # Call dilate method with parameters
4106
5306
  result = n3d.dilate(
4107
5307
  active_data,
4108
5308
  amount,
4109
5309
  xy_scale = xy_scale,
4110
5310
  z_scale = z_scale,
5311
+ recursive = recursive
5312
+ )
5313
+
5314
+ # Update both the display data and the network object
5315
+ self.parent().load_channel(self.parent().active_channel, result, True)
5316
+
5317
+ self.parent().update_display()
5318
+ self.accept()
5319
+
5320
+ except Exception as e:
5321
+ import traceback
5322
+ print(traceback.format_exc())
5323
+ QMessageBox.critical(
5324
+ self,
5325
+ "Error",
5326
+ f"Error running dilate: {str(e)}"
5327
+ )
5328
+
5329
+ class ErodeDialog(QDialog):
5330
+ def __init__(self, parent=None):
5331
+ super().__init__(parent)
5332
+ self.setWindowTitle("Erosion Parameters")
5333
+ self.setModal(True)
5334
+
5335
+ layout = QFormLayout(self)
5336
+
5337
+ self.amount = QLineEdit("1")
5338
+ layout.addRow("Erosion Radius:", self.amount)
5339
+
5340
+ if my_network.xy_scale is not None:
5341
+ xy_scale = f"{my_network.xy_scale}"
5342
+ else:
5343
+ xy_scale = "1"
5344
+
5345
+ self.xy_scale = QLineEdit(xy_scale)
5346
+ layout.addRow("xy_scale:", self.xy_scale)
5347
+
5348
+ if my_network.z_scale is not None:
5349
+ z_scale = f"{my_network.z_scale}"
5350
+ else:
5351
+ z_scale = "1"
5352
+
5353
+ self.z_scale = QLineEdit(z_scale)
5354
+ layout.addRow("z_scale:", self.z_scale)
5355
+
5356
+ # Add Run button
5357
+ run_button = QPushButton("Run Erode")
5358
+ run_button.clicked.connect(self.run_erode)
5359
+ layout.addRow(run_button)
5360
+
5361
+ def run_erode(self):
5362
+ try:
5363
+
5364
+ # Get amount
5365
+ try:
5366
+ amount = float(self.amount.text()) if self.amount.text() else 1
5367
+ except ValueError:
5368
+ amount = 1
5369
+
5370
+ try:
5371
+ xy_scale = float(self.xy_scale.text()) if self.xy_scale.text() else 1
5372
+ except ValueError:
5373
+ xy_scale = 1
5374
+
5375
+ try:
5376
+ z_scale = float(self.z_scale.text()) if self.z_scale.text() else 1
5377
+ except ValueError:
5378
+ z_scale = 1
5379
+
5380
+ # Get the active channel data from parent
5381
+ active_data = self.parent().channel_data[self.parent().active_channel]
5382
+ if active_data is None:
5383
+ raise ValueError("No active image selected")
5384
+
5385
+ # Call dilate method with parameters
5386
+ result = n3d.erode(
5387
+ active_data,
5388
+ amount,
5389
+ xy_scale = xy_scale,
5390
+ z_scale = z_scale,
4111
5391
  )
4112
5392
 
4113
5393
  # Update both the display data and the network object
@@ -4124,7 +5404,46 @@ class DilateDialog(QDialog):
4124
5404
  QMessageBox.critical(
4125
5405
  self,
4126
5406
  "Error",
4127
- f"Error running dilate: {str(e)}"
5407
+ f"Error running erode: {str(e)}"
5408
+ )
5409
+
5410
+ class HoleDialog(QDialog):
5411
+ def __init__(self, parent=None):
5412
+ super().__init__(parent)
5413
+ self.setWindowTitle("Fill Holes? (Active Image)")
5414
+ self.setModal(True)
5415
+
5416
+ layout = QFormLayout(self)
5417
+
5418
+ # Add Run button
5419
+ run_button = QPushButton("Run Fill Holes")
5420
+ run_button.clicked.connect(self.run_holes)
5421
+ layout.addRow(run_button)
5422
+
5423
+ def run_holes(self):
5424
+ try:
5425
+
5426
+
5427
+ # Get the active channel data from parent
5428
+ active_data = self.parent().channel_data[self.parent().active_channel]
5429
+ if active_data is None:
5430
+ raise ValueError("No active image selected")
5431
+
5432
+ # Call dilate method with parameters
5433
+ result = n3d.fill_holes_3d(
5434
+ active_data
5435
+ )
5436
+
5437
+ self.parent().load_channel(self.parent().active_channel, result, True)
5438
+
5439
+ self.parent().update_display()
5440
+ self.accept()
5441
+
5442
+ except Exception as e:
5443
+ QMessageBox.critical(
5444
+ self,
5445
+ "Error",
5446
+ f"Error running fill holes: {str(e)}"
4128
5447
  )
4129
5448
 
4130
5449
  class MaskDialog(QDialog):
@@ -4372,6 +5691,40 @@ class WatershedDialog(QDialog):
4372
5691
  f"Error running watershed: {str(e)}"
4373
5692
  )
4374
5693
 
5694
+ class ZDialog(QDialog):
5695
+
5696
+ def __init__(self, parent=None):
5697
+ super().__init__(parent)
5698
+ self.setWindowTitle("Z Parameters (Save your network first - this will alter all channels into 2D versions)")
5699
+ self.setModal(True)
5700
+
5701
+ layout = QFormLayout(self)
5702
+
5703
+ # Add mode selection dropdown
5704
+ self.mode_selector = QComboBox()
5705
+ self.mode_selector.addItems(["max", "mean", "min", "sum", "std"])
5706
+ self.mode_selector.setCurrentIndex(0) # Default to Mode 1
5707
+ layout.addRow("Execution Mode:", self.mode_selector)
5708
+
5709
+ # Add Run button
5710
+ run_button = QPushButton("Run Z Project")
5711
+ run_button.clicked.connect(self.run_z)
5712
+ layout.addRow(run_button)
5713
+
5714
+ def run_z(self):
5715
+
5716
+ mode = self.mode_selector.currentText()
5717
+
5718
+ for i in range(len(self.parent().channel_data)):
5719
+ try:
5720
+ self.parent().channel_data[i] = n3d.z_project(self.parent().channel_data[i], mode)
5721
+ self.parent().load_channel(i, self.parent().channel_data[i], True)
5722
+ except:
5723
+ pass
5724
+
5725
+ self.accept()
5726
+
5727
+
4375
5728
  class CentroidNodeDialog(QDialog):
4376
5729
  def __init__(self, parent=None):
4377
5730
  super().__init__(parent)
@@ -4624,6 +5977,275 @@ class BranchDialog(QDialog):
4624
5977
 
4625
5978
 
4626
5979
 
5980
+ class IsolateDialog(QDialog):
5981
+ def __init__(self, parent=None):
5982
+ super().__init__(parent)
5983
+ self.setWindowTitle("Select Node types to isolate")
5984
+ self.setModal(True)
5985
+ layout = QFormLayout(self)
5986
+
5987
+ self.combo1 = QComboBox()
5988
+ self.combo1.addItems(list(set(my_network.node_identities.values())))
5989
+ self.combo1.setCurrentIndex(0)
5990
+ layout.addRow("ID 1:", self.combo1)
5991
+
5992
+ self.combo2 = QComboBox()
5993
+ self.combo2.addItems(list(set(my_network.node_identities.values())))
5994
+ self.combo2.setCurrentIndex(1)
5995
+ layout.addRow("ID 2:", self.combo2)
5996
+
5997
+ # Add submit button
5998
+ sub_button = QPushButton("Submit")
5999
+ sub_button.clicked.connect(self.submit_ids)
6000
+ layout.addRow(sub_button)
6001
+
6002
+ def submit_ids(self):
6003
+ try:
6004
+ id1 = self.combo1.currentText()
6005
+ id2 = self.combo2.currentText()
6006
+ if id1 == id2:
6007
+ print("Please select different identities")
6008
+ self.parent().show_isolate_dialog()
6009
+ return
6010
+ else:
6011
+ my_network.isolate_internode_connections(id1, id2)
6012
+ self.accept()
6013
+ except Exception as e:
6014
+ print(f"An error occurred: {e}")
6015
+
6016
+ class AlterDialog(QDialog):
6017
+ def __init__(self, parent=None):
6018
+ super().__init__(parent)
6019
+ self.setWindowTitle("Enter Node/Edge groups to add/remove")
6020
+ self.setModal(True)
6021
+ layout = QFormLayout(self)
6022
+
6023
+ # Node 1
6024
+ self.node1 = QLineEdit()
6025
+ self.node1.setPlaceholderText("Enter integer")
6026
+ layout.addRow("Node1:", self.node1)
6027
+
6028
+ # Node 2
6029
+ self.node2 = QLineEdit()
6030
+ self.node2.setPlaceholderText("Enter integer")
6031
+ layout.addRow("Node2:", self.node2)
6032
+
6033
+ # Edge
6034
+ self.edge = QLineEdit()
6035
+ self.edge.setPlaceholderText("Optional - Enter integer")
6036
+ layout.addRow("Edge:", self.edge)
6037
+
6038
+ # Add add button
6039
+ addbutton = QPushButton("Add pair")
6040
+ addbutton.clicked.connect(self.add)
6041
+ layout.addRow(addbutton)
6042
+
6043
+ # Add remove button
6044
+ removebutton = QPushButton("Remove pair")
6045
+ removebutton.clicked.connect(self.remove)
6046
+ layout.addRow(removebutton)
6047
+
6048
+ def add(self):
6049
+ try:
6050
+ node1 = int(self.node1.text()) if self.node1.text().strip() else None
6051
+ node2 = int(self.node2.text()) if self.node2.text().strip() else None
6052
+ edge = int(self.edge.text()) if self.edge.text().strip() else None
6053
+
6054
+ # Check if we have valid node pairs
6055
+ if node1 is not None and node2 is not None:
6056
+ # Add the node pair and its reverse
6057
+ my_network.network_lists[0].append(node1)
6058
+ my_network.network_lists[1].append(node2)
6059
+ # Add edge value (0 if none provided)
6060
+ my_network.network_lists[2].append(edge if edge is not None else 0)
6061
+
6062
+ # Add reverse pair with same edge value
6063
+ my_network.network_lists[0].append(node2)
6064
+ my_network.network_lists[1].append(node1)
6065
+ my_network.network_lists[2].append(edge if edge is not None else 0)
6066
+ try:
6067
+ if hasattr(my_network, 'network_lists'):
6068
+ model = PandasModel(my_network.network_lists)
6069
+ self.parent().network_table.setModel(model)
6070
+ # Adjust column widths to content
6071
+ for column in range(model.columnCount(None)):
6072
+ self.parent().network_table.resizeColumnToContents(column)
6073
+ except Exception as e:
6074
+ print(f"Error showing network table: {e}")
6075
+ except ValueError:
6076
+ import traceback
6077
+ print(traceback.format_exc())
6078
+ pass # Invalid input - do nothing
6079
+
6080
+ def remove(self):
6081
+ try:
6082
+ node1 = int(self.node1.text()) if self.node1.text().strip() else None
6083
+ node2 = int(self.node2.text()) if self.node2.text().strip() else None
6084
+ edge = int(self.edge.text()) if self.edge.text().strip() else None
6085
+
6086
+ # Check if we have valid node pairs
6087
+ if node1 is not None and node2 is not None:
6088
+ # Create lists for indices to remove
6089
+ indices_to_remove = []
6090
+
6091
+ # Loop through the lists to find matching pairs
6092
+ for i in range(len(my_network.network_lists[0])):
6093
+ forward_match = (my_network.network_lists[0][i] == node1 and
6094
+ my_network.network_lists[1][i] == node2)
6095
+ reverse_match = (my_network.network_lists[0][i] == node2 and
6096
+ my_network.network_lists[1][i] == node1)
6097
+
6098
+ if forward_match or reverse_match:
6099
+ # If edge value specified, only remove if edge matches
6100
+ if edge is not None:
6101
+ if my_network.network_lists[2][i] == edge:
6102
+ indices_to_remove.append(i)
6103
+ else:
6104
+ # If no edge specified, remove all matching pairs
6105
+ indices_to_remove.append(i)
6106
+
6107
+ # Remove elements in reverse order to maintain correct indices
6108
+ for i in sorted(indices_to_remove, reverse=True):
6109
+ my_network.network_lists[0].pop(i)
6110
+ my_network.network_lists[1].pop(i)
6111
+ my_network.network_lists[2].pop(i)
6112
+
6113
+ try:
6114
+ if hasattr(my_network, 'network_lists'):
6115
+ model = PandasModel(my_network.network_lists)
6116
+ self.parent().network_table.setModel(model)
6117
+ # Adjust column widths to content
6118
+ for column in range(model.columnCount(None)):
6119
+ self.parent().network_table.resizeColumnToContents(column)
6120
+ except Exception as e:
6121
+ print(f"Error showing network table: {e}")
6122
+
6123
+ except ValueError:
6124
+ import traceback
6125
+ print(traceback.format_exc())
6126
+ pass # Invalid input - do nothing
6127
+
6128
+
6129
+ class ModifyDialog(QDialog):
6130
+ def __init__(self, parent=None):
6131
+ super().__init__(parent)
6132
+ self.setWindowTitle("Create Nodes from Edge Vertices")
6133
+ self.setModal(True)
6134
+ layout = QFormLayout(self)
6135
+
6136
+ # trunk checkbox (default false)
6137
+ self.trunk = QPushButton("Remove Trunk")
6138
+ self.trunk.setCheckable(True)
6139
+ self.trunk.setChecked(False)
6140
+ layout.addRow("Remove Trunk? (Most connected edge - overrides below):", self.trunk)
6141
+
6142
+ # trunk checkbox (default false)
6143
+ self.trunknode = QPushButton("Trunk -> Node")
6144
+ self.trunknode.setCheckable(True)
6145
+ self.trunknode.setChecked(False)
6146
+ layout.addRow("Convert Trunk to Node? (Most connected edge):", self.trunknode)
6147
+
6148
+ # edgenode checkbox (default false)
6149
+ self.edgenode = QPushButton("Edges -> Nodes")
6150
+ self.edgenode.setCheckable(True)
6151
+ self.edgenode.setChecked(False)
6152
+ layout.addRow("Convert 'Edges (Labeled objects)' to node objects?:", self.edgenode)
6153
+
6154
+ # edgeweight checkbox (default false)
6155
+ self.edgeweight = QPushButton("Remove weights")
6156
+ self.edgeweight.setCheckable(True)
6157
+ self.edgeweight.setChecked(False)
6158
+ layout.addRow("Remove network weights?:", self.edgeweight)
6159
+
6160
+ # prune checkbox (default false)
6161
+ self.prune = QPushButton("Prune Same Type")
6162
+ self.prune.setCheckable(True)
6163
+ self.prune.setChecked(False)
6164
+ layout.addRow("Prune connections between nodes of the same type (if assigned)?:", self.prune)
6165
+
6166
+ # isolate checkbox (default false)
6167
+ self.isolate = QPushButton("Isolate Two Types")
6168
+ self.isolate.setCheckable(True)
6169
+ self.isolate.setChecked(False)
6170
+ layout.addRow("Isolate connections between two specific node types (if assigned)?:", self.isolate)
6171
+
6172
+ #change button
6173
+ change_button = QPushButton("Add/Remove Network Pairs")
6174
+ change_button.clicked.connect(self.show_alter_dialog)
6175
+ layout.addRow(change_button)
6176
+
6177
+ # Add Run button
6178
+ run_button = QPushButton("Make Changes")
6179
+ run_button.clicked.connect(self.run_changes)
6180
+ layout.addRow(run_button)
6181
+
6182
+ def show_isolate_dialog(self):
6183
+
6184
+ dialog = IsolateDialog(self)
6185
+ dialog.exec()
6186
+
6187
+ def show_alter_dialog(self):
6188
+
6189
+ dialog = AlterDialog(self.parent())
6190
+ dialog.exec()
6191
+
6192
+ def run_changes(self):
6193
+
6194
+ try:
6195
+
6196
+ trunk = self.trunk.isChecked()
6197
+ if not trunk:
6198
+ trunknode = self.trunknode.isChecked()
6199
+ else:
6200
+ trunknode = False
6201
+ edgenode = self.edgenode.isChecked()
6202
+ edgeweight = self.edgeweight.isChecked()
6203
+ prune = self.prune.isChecked()
6204
+ isolate = self.isolate.isChecked()
6205
+
6206
+ if isolate and my_network.node_identities is not None:
6207
+ self.show_isolate_dialog()
6208
+
6209
+ if edgeweight:
6210
+ my_network.remove_edge_weights()
6211
+ if prune and my_network.node_identities is not None:
6212
+ my_network.prune_samenode_connections()
6213
+ if trunk:
6214
+ my_network.remove_trunk_post()
6215
+ if trunknode:
6216
+ if my_network.node_centroids is None or my_network.edge_centroids is None:
6217
+ self.parent().show_centroid_dialog()
6218
+ my_network.trunk_to_node()
6219
+ self.parent().load_channel(0, my_network.nodes, True)
6220
+ if edgenode:
6221
+ if my_network.node_centroids is None or my_network.edge_centroids is None:
6222
+ self.parent().show_centroid_dialog()
6223
+ my_network.edge_to_node()
6224
+ self.parent().load_channel(0, my_network.nodes, True)
6225
+ self.parent().load_channel(1, my_network.edges, True)
6226
+
6227
+ try:
6228
+ if hasattr(my_network, 'network_lists'):
6229
+ model = PandasModel(my_network.network_lists)
6230
+ self.parent().network_table.setModel(model)
6231
+ # Adjust column widths to content
6232
+ for column in range(model.columnCount(None)):
6233
+ self.parent().network_table.resizeColumnToContents(column)
6234
+ except Exception as e:
6235
+ print(f"Error showing network table: {e}")
6236
+
6237
+ if hasattr(my_network, 'node_identities') and my_network.node_identities is not None:
6238
+ try:
6239
+ self.parent().format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity', 'Node Identities')
6240
+ except Exception as e:
6241
+ print(f"Error loading node identity table: {e}")
6242
+
6243
+ self.parent().update_display()
6244
+ self.accept()
6245
+
6246
+ except Exception as e:
6247
+ print(f"An error occurred: {e}")
6248
+
4627
6249
 
4628
6250
 
4629
6251
 
@@ -5031,6 +6653,15 @@ class ProxDialog(QDialog):
5031
6653
  self.mode_selector.setCurrentIndex(0) # Default to Mode 1
5032
6654
  layout.addRow("Execution Mode:", self.mode_selector)
5033
6655
 
6656
+ if my_network.node_identities is not None:
6657
+ self.id_selector = QComboBox()
6658
+ # Add all options from id dictionary
6659
+ self.id_selector.addItems(['None'] + list(set(my_network.node_identities.values())))
6660
+ self.id_selector.setCurrentIndex(0) # Default to Mode 1
6661
+ layout.addRow("Create Networks only from a specific node identity?:", self.id_selector)
6662
+ else:
6663
+ self.id_selector = None
6664
+
5034
6665
  self.overlays = QPushButton("Overlays")
5035
6666
  self.overlays.setCheckable(True)
5036
6667
  self.overlays.setChecked(True)
@@ -5054,6 +6685,15 @@ class ProxDialog(QDialog):
5054
6685
 
5055
6686
  mode = self.mode_selector.currentIndex()
5056
6687
 
6688
+ if self.id_selector is not None and self.id_selector.currentText() != 'None':
6689
+ target = self.id_selector.currentText()
6690
+ targets = []
6691
+ for node in my_network.node_identities:
6692
+ if target == my_network.node_identities[node]:
6693
+ targets.append(int(node))
6694
+ else:
6695
+ targets = None
6696
+
5057
6697
  try:
5058
6698
  directory = self.directory.text() if self.directory.text() else None
5059
6699
  except:
@@ -5087,7 +6727,7 @@ class ProxDialog(QDialog):
5087
6727
  my_network.nodes, _ = n3d.label_objects(my_network.nodes)
5088
6728
  if my_network.node_centroids is None:
5089
6729
  self.parent().show_centroid_dialog()
5090
- my_network.morph_proximity(search = search)
6730
+ my_network.morph_proximity(search = search, targets = targets)
5091
6731
 
5092
6732
  self.parent().load_channel(0, channel_data = my_network.nodes, data = True)
5093
6733
  elif mode == 0:
@@ -5113,10 +6753,10 @@ class ProxDialog(QDialog):
5113
6753
  return
5114
6754
 
5115
6755
  if populate:
5116
- my_network.nodes = my_network.kd_network(distance = search)
6756
+ my_network.nodes = my_network.kd_network(distance = search, targets = targets)
5117
6757
  self.parent().load_channel(0, channel_data = my_network.nodes, data = True)
5118
6758
  else:
5119
- my_network.kd_network(distance = search)
6759
+ my_network.kd_network(distance = search, targets = targets)
5120
6760
 
5121
6761
 
5122
6762
  my_network.dump(directory = directory)