nettracer3d 0.2.5__py3-none-any.whl → 0.2.7__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,20 @@ 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
+ delete_obj = highlight_menu.addAction("Delete Selection")
559
+ delete_obj.triggered.connect(self.handle_delete)
560
+ if len(self.clicked_values['nodes']) > 1:
561
+ link_nodes = highlight_menu.addAction("Link Nodes")
562
+ link_nodes.triggered.connect(self.handle_link)
563
+ delink_nodes = highlight_menu.addAction("Split Nodes")
564
+ delink_nodes.triggered.connect(self.handle_split)
565
+ context_menu.addMenu(highlight_menu)
566
+
477
567
  # Create measure menu
478
568
  measure_menu = QMenu("Measure", self)
479
569
 
@@ -901,6 +991,186 @@ class ImageViewerWindow(QMainWindow):
901
991
  except Exception as e:
902
992
  print(f"Error: {e}")
903
993
 
994
+ def handle_combine(self):
995
+
996
+ try:
997
+
998
+ self.clicked_values['nodes'].sort()
999
+ nodes = copy.deepcopy(self.clicked_values['nodes'])
1000
+ self.clicked_values['edges'].sort()
1001
+ edges = copy.deepcopy(self.clicked_values['edges'])
1002
+
1003
+ if len(nodes) > 1:
1004
+ new_nodes = nodes[0]
1005
+
1006
+ mask = np.isin(self.channel_data[0], nodes)
1007
+ my_network.nodes[mask] = new_nodes
1008
+ self.load_channel(0, my_network.nodes, True)
1009
+ self.clicked_values['nodes'] = new_nodes
1010
+
1011
+ if len(edges) > 1:
1012
+ new_edges = edges[0]
1013
+
1014
+ mask = np.isin(self.channel_data[1], edges)
1015
+ my_network.edges[mask] = new_edges
1016
+ self.load_channel(1, my_network.edges, True)
1017
+ self.clicked_values['edges'] = new_edges
1018
+
1019
+ try:
1020
+
1021
+ for i in range(len(my_network.network_lists[0])):
1022
+ if my_network.network_lists[0][i] in nodes and len(nodes) > 1:
1023
+ my_network.network_lists[0][i] = new_nodes
1024
+ if my_network.network_lists[1][i] in nodes and len(nodes) > 1:
1025
+ my_network.network_lists[1][i] = new_nodes
1026
+ if my_network.network_lists[2][i] in edges and len(edges) > 1:
1027
+ my_network.network_lists[2][i] = new_edges
1028
+
1029
+
1030
+ my_network.network_lists = my_network.network_lists
1031
+
1032
+ if not hasattr(my_network, 'network_lists') or my_network.network_lists is None:
1033
+ empty_df = pd.DataFrame(columns=['Node 1A', 'Node 1B', 'Edge 1C'])
1034
+ model = PandasModel(empty_df)
1035
+ self.network_table.setModel(model)
1036
+ else:
1037
+ model = PandasModel(my_network.network_lists)
1038
+ self.network_table.setModel(model)
1039
+ # Adjust column widths to content
1040
+ for column in range(model.columnCount(None)):
1041
+ self.network_table.resizeColumnToContents(column)
1042
+
1043
+ except Exception as e:
1044
+ print(f"Error, could not update network: {e}")
1045
+
1046
+ except Exception as e:
1047
+ print(f"An error has occured: {e}")
1048
+
1049
+ def handle_delete(self):
1050
+
1051
+ try:
1052
+ if len(self.clicked_values['nodes']) > 0:
1053
+ self.create_highlight_overlay(node_indices = self.clicked_values['nodes'])
1054
+ mask = self.highlight_overlay == 0
1055
+ my_network.nodes = my_network.nodes * mask
1056
+ self.load_channel(0, my_network.nodes, True)
1057
+
1058
+ for i in range(len(my_network.network_lists[0]) - 1, -1, -1):
1059
+ if my_network.network_lists[0][i] in self.clicked_values['nodes'] or my_network.network_lists[0][i] in self.clicked_values['nodes']:
1060
+ del my_network.network_lists[0][i]
1061
+ del my_network.network_lists[1][i]
1062
+ del my_network.network_lists[2][i]
1063
+
1064
+
1065
+
1066
+ if len(self.clicked_values['edges']) > 0:
1067
+ self.create_highlight_overlay(node_indices = self.clicked_values['edges'])
1068
+ mask = self.highlight_overlay == 0
1069
+ my_network.edges = my_network.edges * mask
1070
+ self.load_channel(1, my_network.edges, True)
1071
+
1072
+ for i in range(len(my_network.network_lists[1]) - 1, -1, -1):
1073
+ if my_network.network_lists[2][i] in self.clicked_values['edges']:
1074
+ del my_network.network_lists[0][i]
1075
+ del my_network.network_lists[1][i]
1076
+ del my_network.network_lists[2][i]
1077
+
1078
+ my_network.network_lists = my_network.network_lists
1079
+
1080
+
1081
+ if not hasattr(my_network, 'network_lists') or my_network.network_lists is None:
1082
+ empty_df = pd.DataFrame(columns=['Node 1A', 'Node 1B', 'Edge 1C'])
1083
+ model = PandasModel(empty_df)
1084
+ self.network_table.setModel(model)
1085
+ else:
1086
+ model = PandasModel(my_network.network_lists)
1087
+ self.network_table.setModel(model)
1088
+ # Adjust column widths to content
1089
+ for column in range(model.columnCount(None)):
1090
+ self.network_table.resizeColumnToContents(column)
1091
+
1092
+ self.show_centroid_dialog()
1093
+ except Exception as e:
1094
+ print(f"Error: {e}")
1095
+
1096
+ def handle_link(self):
1097
+
1098
+ try:
1099
+ nodes = self.clicked_values['nodes']
1100
+ from itertools import combinations
1101
+ pairs = list(combinations(nodes, 2))
1102
+
1103
+ # Convert existing connections to a set of tuples for efficient lookup
1104
+ existing_connections = set()
1105
+ for n1, n2 in zip(my_network.network_lists[0], my_network.network_lists[1]):
1106
+ existing_connections.add((n1, n2))
1107
+ existing_connections.add((n2, n1)) # Add reverse pair too
1108
+
1109
+ # Filter out existing connections
1110
+ new_pairs = []
1111
+ for pair in pairs:
1112
+ if pair not in existing_connections:
1113
+ new_pairs.append(pair)
1114
+
1115
+ # Add new connections
1116
+ for pair in new_pairs:
1117
+ my_network.network_lists[0].append(pair[0])
1118
+ my_network.network_lists[1].append(pair[1])
1119
+ my_network.network_lists[2].append(0)
1120
+
1121
+ # Update the table
1122
+ if not hasattr(my_network, 'network_lists') or my_network.network_lists is None:
1123
+ empty_df = pd.DataFrame(columns=['Node 1A', 'Node 1B', 'Edge 1C'])
1124
+ model = PandasModel(empty_df)
1125
+ self.network_table.setModel(model)
1126
+ else:
1127
+ model = PandasModel(my_network.network_lists)
1128
+ self.network_table.setModel(model)
1129
+ # Adjust column widths to content
1130
+ for column in range(model.columnCount(None)):
1131
+ self.network_table.resizeColumnToContents(column)
1132
+ except Exception as e:
1133
+ print(f"An error has occurred: {e}")
1134
+
1135
+
1136
+ def handle_split(self):
1137
+ try:
1138
+ nodes = self.clicked_values['nodes']
1139
+
1140
+ from itertools import combinations
1141
+
1142
+ pairs = list(combinations(nodes, 2))
1143
+
1144
+ print(pairs)
1145
+
1146
+
1147
+ for i in range(len(my_network.network_lists[0]) - 1, -1, -1):
1148
+ print((my_network.network_lists[0][i], my_network.network_lists[1][i]))
1149
+ 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:
1150
+ del my_network.network_lists[0][i]
1151
+ del my_network.network_lists[1][i]
1152
+ del my_network.network_lists[2][i]
1153
+
1154
+ my_network.network_lists = my_network.network_lists
1155
+
1156
+ if not hasattr(my_network, 'network_lists') or my_network.network_lists is None:
1157
+ empty_df = pd.DataFrame(columns=['Node 1A', 'Node 1B', 'Edge 1C'])
1158
+ model = PandasModel(empty_df)
1159
+ self.network_table.setModel(model)
1160
+ else:
1161
+ model = PandasModel(my_network.network_lists)
1162
+ self.network_table.setModel(model)
1163
+ # Adjust column widths to content
1164
+ for column in range(model.columnCount(None)):
1165
+ self.network_table.resizeColumnToContents(column)
1166
+ except Exception as e:
1167
+ print(f"An error has occurred: {e}")
1168
+
1169
+
1170
+
1171
+
1172
+
1173
+
904
1174
 
905
1175
  def handle_highlight_select(self):
906
1176
 
@@ -1264,18 +1534,19 @@ class ImageViewerWindow(QMainWindow):
1264
1534
  # Get clicked value
1265
1535
  x_idx = int(round(event.xdata))
1266
1536
  y_idx = int(round(event.ydata))
1537
+ # Check if Ctrl key is pressed (using matplotlib's key_press system)
1538
+ ctrl_pressed = 'ctrl' in event.modifiers # Note: changed from 'control' to 'ctrl'
1267
1539
  if self.channel_data[self.active_channel][self.current_slice, y_idx, x_idx] != 0:
1268
1540
  clicked_value = self.channel_data[self.active_channel][self.current_slice, y_idx, x_idx]
1269
1541
  else:
1270
- self.clicked_values = {
1271
- 'nodes': [],
1272
- 'edges': []
1273
- }
1274
- self.create_highlight_overlay()
1542
+ if not ctrl_pressed:
1543
+ self.clicked_values = {
1544
+ 'nodes': [],
1545
+ 'edges': []
1546
+ }
1547
+ self.create_highlight_overlay()
1275
1548
  return
1276
1549
 
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
1550
 
1280
1551
  starting_vals = copy.deepcopy(self.clicked_values)
1281
1552
 
@@ -1384,6 +1655,8 @@ class ImageViewerWindow(QMainWindow):
1384
1655
  load_action.triggered.connect(lambda: self.load_misc('Node Centroids'))
1385
1656
  load_action = misc_menu.addAction("Load Edge Centroids")
1386
1657
  load_action.triggered.connect(lambda: self.load_misc('Edge Centroids'))
1658
+ load_action = misc_menu.addAction("Merge Nodes")
1659
+ load_action.triggered.connect(lambda: self.load_misc('Merge Nodes'))
1387
1660
 
1388
1661
 
1389
1662
  # Analysis menu
@@ -1391,11 +1664,19 @@ class ImageViewerWindow(QMainWindow):
1391
1664
  network_menu = analysis_menu.addMenu("Network")
1392
1665
  netshow_action = network_menu.addAction("Show Network")
1393
1666
  netshow_action.triggered.connect(self.show_netshow_dialog)
1394
- partition_action = network_menu.addAction("Community Partition")
1667
+ partition_action = network_menu.addAction("Community Partition + Community Stats")
1395
1668
  partition_action.triggered.connect(self.show_partition_dialog)
1396
1669
  stats_menu = analysis_menu.addMenu("Stats")
1397
1670
  allstats_action = stats_menu.addAction("Calculate Generic Network Stats")
1398
1671
  allstats_action.triggered.connect(self.stats)
1672
+ radial_action = stats_menu.addAction("Radial Distribution Analysis")
1673
+ radial_action.triggered.connect(self.show_radial_dialog)
1674
+ degree_dist_action = stats_menu.addAction("Degree Distribution Analysis")
1675
+ degree_dist_action.triggered.connect(self.show_degree_dist_dialog)
1676
+ neighbor_id_action = stats_menu.addAction("Identity Distribution of Neighbors")
1677
+ neighbor_id_action.triggered.connect(self.show_neighbor_id_dialog)
1678
+ random_action = stats_menu.addAction("Generate Equivalent Random Network")
1679
+ random_action.triggered.connect(self.show_random_dialog)
1399
1680
  vol_action = stats_menu.addAction("Calculate Volumes")
1400
1681
  vol_action.triggered.connect(self.volumes)
1401
1682
  inter_action = stats_menu.addAction("Calculate Node < > Edge Interaction")
@@ -1403,8 +1684,13 @@ class ImageViewerWindow(QMainWindow):
1403
1684
  overlay_menu = analysis_menu.addMenu("Data/Overlays")
1404
1685
  degree_action = overlay_menu.addAction("Get Degree Information")
1405
1686
  degree_action.triggered.connect(self.show_degree_dialog)
1687
+ hub_action = overlay_menu.addAction("Get Hub Information")
1688
+ hub_action.triggered.connect(self.show_hub_dialog)
1406
1689
  mother_action = overlay_menu.addAction("Get Mother Nodes")
1407
1690
  mother_action.triggered.connect(self.show_mother_dialog)
1691
+ community_code_action = overlay_menu.addAction("Code Communities")
1692
+ community_code_action.triggered.connect(self.show_code_dialog)
1693
+
1408
1694
 
1409
1695
  # Process menu
1410
1696
  process_menu = menubar.addMenu("Process")
@@ -1421,25 +1707,37 @@ class ImageViewerWindow(QMainWindow):
1421
1707
  resize_action.triggered.connect(self.show_resize_dialog)
1422
1708
  dilate_action = image_menu.addAction("Dilate")
1423
1709
  dilate_action.triggered.connect(self.show_dilate_dialog)
1710
+ erode_action = image_menu.addAction("Erode")
1711
+ erode_action.triggered.connect(self.show_erode_dialog)
1712
+ hole_action = image_menu.addAction("Fill Holes")
1713
+ hole_action.triggered.connect(self.show_hole_dialog)
1424
1714
  binarize_action = image_menu.addAction("Binarize")
1425
1715
  binarize_action.triggered.connect(self.show_binarize_dialog)
1426
1716
  label_action = image_menu.addAction("Label Objects")
1427
1717
  label_action.triggered.connect(self.show_label_dialog)
1718
+ thresh_action = image_menu.addAction("Threshold/Segment")
1719
+ thresh_action.triggered.connect(self.show_thresh_dialog)
1428
1720
  mask_action = image_menu.addAction("Mask Channel")
1429
1721
  mask_action.triggered.connect(self.show_mask_dialog)
1430
1722
  skeletonize_action = image_menu.addAction("Skeletonize")
1431
1723
  skeletonize_action.triggered.connect(self.show_skeletonize_dialog)
1432
1724
  watershed_action = image_menu.addAction("Watershed")
1433
1725
  watershed_action.triggered.connect(self.show_watershed_dialog)
1726
+ z_proj_action = image_menu.addAction("Z Project")
1727
+ z_proj_action.triggered.connect(self.show_z_dialog)
1434
1728
 
1435
- centroid_node_action = process_menu.addAction("Generate Nodes (From Node Centroids)")
1729
+ generate_menu = process_menu.addMenu("Generate")
1730
+ centroid_node_action = generate_menu.addAction("Generate Nodes (From Node Centroids)")
1436
1731
  centroid_node_action.triggered.connect(self.show_centroid_node_dialog)
1437
-
1438
-
1439
- gennodes_action = process_menu.addAction("Generate Nodes (From 'Edge' Vertices)")
1732
+ gennodes_action = generate_menu.addAction("Generate Nodes (From 'Edge' Vertices)")
1440
1733
  gennodes_action.triggered.connect(self.show_gennodes_dialog)
1441
- branch_action = process_menu.addAction("Label Branches")
1734
+ branch_action = generate_menu.addAction("Label Branches")
1442
1735
  branch_action.triggered.connect(self.show_branch_dialog)
1736
+ genvor_action = generate_menu.addAction("Generate Voronoi Diagram (From Node Centroids) - goes in Overlay2")
1737
+ genvor_action.triggered.connect(self.voronoi)
1738
+
1739
+ modify_action = process_menu.addAction("Modify Network")
1740
+ modify_action.triggered.connect(self.show_modify_dialog)
1443
1741
 
1444
1742
 
1445
1743
  # Image menu
@@ -1477,17 +1775,26 @@ class ImageViewerWindow(QMainWindow):
1477
1775
 
1478
1776
  def volumes(self):
1479
1777
 
1480
- print(self.active_channel)
1481
1778
 
1482
1779
  if self.active_channel == 1:
1483
1780
  output = my_network.volumes('edges')
1484
1781
  self.format_for_upperright_table(output, metric='Edge ID', value = 'Voxel Volume (Scaled)', title = 'Edge Volumes')
1782
+ self.volume_dict[1] = output
1485
1783
 
1486
- else:
1784
+ elif self.active_channel == 0:
1487
1785
  output = my_network.volumes('nodes')
1488
1786
  self.format_for_upperright_table(output, metric='Node ID', value = 'Voxel Volume (Scaled)', title = 'Node Volumes')
1787
+ self.volume_dict[0] = output
1489
1788
 
1789
+ elif self.active_channel == 2:
1790
+ output = my_network.volumes('network_overlay')
1791
+ self.format_for_upperright_table(output, metric='Object ID', value = 'Voxel Volume (Scaled)', title = 'Overlay 1 Volumes')
1792
+ self.volume_dict[2] = output
1490
1793
 
1794
+ elif self.active_channel == 3:
1795
+ output = my_network.volumes('id_overlay')
1796
+ self.format_for_upperright_table(output, metric='Object ID', value = 'Voxel Volume (Scaled)', title = 'Overlay 2 Volumes')
1797
+ self.volume_dict[3] = output
1491
1798
 
1492
1799
 
1493
1800
 
@@ -1582,6 +1889,11 @@ class ImageViewerWindow(QMainWindow):
1582
1889
  dialog = WatershedDialog(self)
1583
1890
  dialog.exec()
1584
1891
 
1892
+ def show_z_dialog(self):
1893
+ """Show the z-proj dialog."""
1894
+ dialog = ZDialog(self)
1895
+ dialog.exec()
1896
+
1585
1897
  def show_calc_all_dialog(self):
1586
1898
  """Show the calculate all parameter dialog."""
1587
1899
  dialog = CalcAllDialog(self)
@@ -1602,11 +1914,26 @@ class ImageViewerWindow(QMainWindow):
1602
1914
  dialog = DilateDialog(self)
1603
1915
  dialog.exec()
1604
1916
 
1917
+ def show_erode_dialog(self):
1918
+ """show the erode dialog"""
1919
+ dialog = ErodeDialog(self)
1920
+ dialog.exec()
1921
+
1922
+ def show_hole_dialog(self):
1923
+ """show the hole dialog"""
1924
+ dialog = HoleDialog(self)
1925
+ dialog.exec()
1926
+
1605
1927
  def show_label_dialog(self):
1606
1928
  """Show the label dialog"""
1607
1929
  dialog = LabelDialog(self)
1608
1930
  dialog.exec()
1609
1931
 
1932
+ def show_thresh_dialog(self):
1933
+ """Show threshold dialog"""
1934
+ thresh_window = ThresholdWindow(self)
1935
+ thresh_window.show() # Non-modal window
1936
+
1610
1937
  def show_mask_dialog(self):
1611
1938
  """Show the mask dialog"""
1612
1939
  dialog = MaskDialog(self)
@@ -1633,6 +1960,33 @@ class ImageViewerWindow(QMainWindow):
1633
1960
  dialog = BranchDialog(self)
1634
1961
  dialog.exec()
1635
1962
 
1963
+ def voronoi(self):
1964
+
1965
+ try:
1966
+
1967
+ if my_network.nodes is not None:
1968
+ shape = my_network.nodes.shape
1969
+ else:
1970
+ shape = None
1971
+
1972
+ if my_network.node_centroids is None:
1973
+ self.show_centroid_dialog()
1974
+ if my_network.node_centroids is None:
1975
+ print("Node centroids must be set")
1976
+ return
1977
+
1978
+ array = pxt.create_voronoi_3d_kdtree(my_network.node_centroids, shape)
1979
+ self.load_channel(3, array, True)
1980
+
1981
+ except Exception as e:
1982
+ print(f"Error generating voronoi: {e}")
1983
+
1984
+
1985
+ def show_modify_dialog(self):
1986
+ """Show the network modify dialog"""
1987
+ dialog = ModifyDialog(self)
1988
+ dialog.exec()
1989
+
1636
1990
 
1637
1991
  def show_binarize_dialog(self):
1638
1992
  """show the binarize dialog"""
@@ -1696,53 +2050,110 @@ class ImageViewerWindow(QMainWindow):
1696
2050
  def load_misc(self, sort):
1697
2051
  """Loads various things"""
1698
2052
 
1699
- try:
1700
-
1701
- filename, _ = QFileDialog.getOpenFileName(
1702
- self,
1703
- f"Load {sort}",
1704
- "",
1705
- "Spreadsheets (*.xlsx *.csv)"
1706
- )
2053
+ if sort != 'Merge Nodes':
1707
2054
 
1708
2055
  try:
1709
- if sort == 'Node Identities':
1710
- my_network.load_node_identities(file_path = filename)
1711
2056
 
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}")
2057
+ filename, _ = QFileDialog.getOpenFileName(
2058
+ self,
2059
+ f"Load {sort}",
2060
+ "",
2061
+ "Spreadsheets (*.xlsx *.csv)"
2062
+ )
1717
2063
 
1718
- elif sort == 'Node Centroids':
1719
- my_network.load_node_centroids(file_path = filename)
2064
+ try:
2065
+ if sort == 'Node Identities':
2066
+ my_network.load_node_identities(file_path = filename)
1720
2067
 
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}")
2068
+ if hasattr(my_network, 'node_identities') and my_network.node_identities is not None:
2069
+ try:
2070
+ self.format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity', 'Node Identities')
2071
+ except Exception as e:
2072
+ print(f"Error loading node identity table: {e}")
1726
2073
 
1727
- elif sort == 'Edge Centroids':
1728
- my_network.load_edge_centroids(file_path = filename)
2074
+ elif sort == 'Node Centroids':
2075
+ my_network.load_node_centroids(file_path = filename)
1729
2076
 
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}")
2077
+ if hasattr(my_network, 'node_centroids') and my_network.node_centroids is not None:
2078
+ try:
2079
+ self.format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
2080
+ except Exception as e:
2081
+ print(f"Error loading node centroid table: {e}")
2082
+
2083
+ elif sort == 'Edge Centroids':
2084
+ my_network.load_edge_centroids(file_path = filename)
1735
2085
 
2086
+ if hasattr(my_network, 'edge_centroids') and my_network.edge_centroids is not None:
2087
+ try:
2088
+ self.format_for_upperright_table(my_network.edge_centroids, 'EdgeID', ['Z', 'Y', 'X'], 'Edge Centroids')
2089
+ except Exception as e:
2090
+ print(f"Error loading edge centroid table: {e}")
2091
+
2092
+
2093
+ except Exception as e:
2094
+ print(f"An error has occured: {e}")
1736
2095
 
1737
2096
  except Exception as e:
1738
- print(f"An error has occured: {e}")
2097
+ QMessageBox.critical(
2098
+ self,
2099
+ "Error Loading",
2100
+ f"Failed to load {sort}: {str(e)}"
2101
+ )
1739
2102
 
1740
- except Exception as e:
1741
- QMessageBox.critical(
1742
- self,
1743
- "Error Loading",
1744
- f"Failed to load {sort}: {str(e)}"
1745
- )
2103
+ else:
2104
+ try:
2105
+
2106
+ if len(np.unique(my_network.nodes)) < 3:
2107
+ self.show_label_dialog()
2108
+
2109
+ # First ask user what they want to select
2110
+ msg = QMessageBox()
2111
+ msg.setWindowTitle("Selection Type")
2112
+ msg.setText("Would you like to select a TIFF file or a directory?")
2113
+ tiff_button = msg.addButton("TIFF File", QMessageBox.ButtonRole.AcceptRole)
2114
+ dir_button = msg.addButton("Directory", QMessageBox.ButtonRole.AcceptRole)
2115
+ msg.addButton("Cancel", QMessageBox.ButtonRole.RejectRole)
2116
+
2117
+ msg.exec()
2118
+
2119
+ if msg.clickedButton() == tiff_button:
2120
+ # Code for selecting TIFF files
2121
+ filename, _ = QFileDialog.getOpenFileName(
2122
+ self,
2123
+ "Select TIFF file",
2124
+ "",
2125
+ "TIFF files (*.tiff *.tif)"
2126
+ )
2127
+ if filename:
2128
+ selected_path = filename
2129
+
2130
+ elif msg.clickedButton() == dir_button:
2131
+ # Code for selecting directories
2132
+ dialog = QFileDialog(self)
2133
+ dialog.setOption(QFileDialog.Option.DontUseNativeDialog)
2134
+ dialog.setOption(QFileDialog.Option.ReadOnly)
2135
+ dialog.setFileMode(QFileDialog.FileMode.Directory)
2136
+ dialog.setViewMode(QFileDialog.ViewMode.Detail)
2137
+
2138
+ if dialog.exec() == QFileDialog.DialogCode.Accepted:
2139
+ selected_path = dialog.directory().absolutePath()
2140
+
2141
+ my_network.merge_nodes(selected_path)
2142
+ self.load_channel(0, my_network.nodes, True)
2143
+
2144
+
2145
+ if hasattr(my_network, 'node_identities') and my_network.node_identities is not None:
2146
+ try:
2147
+ self.format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity', 'Node Identities')
2148
+ except Exception as e:
2149
+ print(f"Error loading node identity table: {e}")
2150
+
2151
+ except Exception as e:
2152
+ QMessageBox.critical(
2153
+ self,
2154
+ "Error Merging",
2155
+ f"Failed to load {sort}: {str(e)}"
2156
+ )
1746
2157
 
1747
2158
 
1748
2159
  # Modify load_from_network_obj method
@@ -1870,7 +2281,7 @@ class ImageViewerWindow(QMainWindow):
1870
2281
  else:
1871
2282
  btn.setStyleSheet("")
1872
2283
 
1873
- def load_channel(self, channel_index, channel_data=None, data=False):
2284
+ def load_channel(self, channel_index, channel_data=None, data=False, assign_shape = True):
1874
2285
  """Load a channel and enable active channel selection if needed."""
1875
2286
 
1876
2287
  try:
@@ -1883,8 +2294,10 @@ class ImageViewerWindow(QMainWindow):
1883
2294
  "TIFF Files (*.tif *.tiff)"
1884
2295
  )
1885
2296
  self.channel_data[channel_index] = tifffile.imread(filename)
2297
+ print(self.channel_data[channel_index].shape)
1886
2298
  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
2299
+ #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
2300
+ self.channel_data[channel_index] = np.expand_dims(self.channel_data[channel_index], axis=0)
1888
2301
 
1889
2302
 
1890
2303
  else:
@@ -1910,18 +2323,21 @@ class ImageViewerWindow(QMainWindow):
1910
2323
  self.active_channel_combo.setEnabled(True)
1911
2324
 
1912
2325
  # 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
2326
+ if len(self.channel_data[channel_index].shape) == 3:
2327
+ if not self.slice_slider.isEnabled():
2328
+ self.slice_slider.setEnabled(True)
2329
+ self.slice_slider.setMinimum(0)
2330
+ self.slice_slider.setMaximum(self.channel_data[channel_index].shape[0] - 1)
2331
+ self.slice_slider.setValue(0)
2332
+ self.current_slice = 0
2333
+ else:
2334
+ self.slice_slider.setEnabled(True)
2335
+ self.slice_slider.setMinimum(0)
2336
+ self.slice_slider.setMaximum(self.channel_data[channel_index].shape[0] - 1)
2337
+ self.slice_slider.setValue(0)
2338
+ self.current_slice = 0
1919
2339
  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
2340
+ self.slice_slider.setEnabled(False)
1925
2341
 
1926
2342
 
1927
2343
  # If this is the first channel loaded, make it active
@@ -1932,6 +2348,10 @@ class ImageViewerWindow(QMainWindow):
1932
2348
  self.channel_buttons[channel_index].click()
1933
2349
  self.min_max[channel_index][0] = np.min(self.channel_data[channel_index])
1934
2350
  self.min_max[channel_index][1] = np.max(self.channel_data[channel_index])
2351
+ self.volume_dict[channel_index] = None #reset volumes
2352
+
2353
+ if assign_shape: #keep original shape tracked to undo resampling.
2354
+ self.original_shape = self.channel_data[channel_index].shape
1935
2355
 
1936
2356
  self.update_display()
1937
2357
 
@@ -2157,117 +2577,132 @@ class ImageViewerWindow(QMainWindow):
2157
2577
  self.update_display(preserve_zoom = (current_xlim, current_ylim))
2158
2578
 
2159
2579
  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)]
2580
+ """Update the display with currently visible channels and highlight overlay."""
2581
+ self.figure.clear()
2582
+
2583
+ # Create subplot with tight layout and white figure background
2584
+ self.figure.patch.set_facecolor('white')
2585
+ self.ax = self.figure.add_subplot(111)
2586
+
2587
+ # Store current zoom limits if they exist and weren't provided
2588
+ if preserve_zoom is None and hasattr(self, 'ax'):
2589
+ current_xlim = self.ax.get_xlim() if self.ax.get_xlim() != (0, 1) else None
2590
+ current_ylim = self.ax.get_ylim() if self.ax.get_ylim() != (0, 1) else None
2591
+ else:
2592
+ current_xlim, current_ylim = preserve_zoom if preserve_zoom else (None, None)
2593
+
2594
+ # Define base colors for each channel with increased intensity
2595
+ base_colors = self.base_colors
2596
+ # Set only the axes (image area) background to black
2597
+ self.ax.set_facecolor('black')
2598
+
2599
+ # Display each visible channel
2600
+ for channel in range(4):
2601
+ if (self.channel_visible[channel] and
2602
+ self.channel_data[channel] is not None):
2603
+
2604
+ # Check if we're dealing with RGB data
2605
+ is_rgb = len(self.channel_data[channel].shape) == 4 and self.channel_data[channel].shape[-1] == 3
2606
+
2607
+ if len(self.channel_data[channel].shape) == 3 and not is_rgb:
2608
+ current_image = self.channel_data[channel][self.current_slice, :, :]
2609
+ elif is_rgb:
2610
+ current_image = self.channel_data[channel][self.current_slice] # Already has RGB channels
2611
+ else:
2612
+ current_image = self.channel_data[channel]
2613
+
2614
+ if is_rgb:
2615
+ # For RGB images, just display directly without colormap
2616
+ self.ax.imshow(current_image,
2617
+ alpha=0.7)
2618
+ else:
2619
+ # Regular channel processing with colormap
2620
+ # Calculate brightness/contrast limits from entire volume
2621
+ img_min = self.min_max[channel][0]
2622
+ img_max = self.min_max[channel][1]
2623
+
2624
+ # Calculate vmin and vmax, ensuring we don't get a zero range
2625
+ if img_min == img_max:
2626
+ vmin = img_min
2627
+ vmax = img_min + 1
2628
+ else:
2629
+ vmin = img_min + (img_max - img_min) * self.channel_brightness[channel]['min']
2630
+ vmax = img_min + (img_max - img_min) * self.channel_brightness[channel]['max']
2631
+
2632
+ # Normalize the image safely
2633
+ if vmin == vmax:
2634
+ normalized_image = np.zeros_like(current_image)
2635
+ else:
2636
+ normalized_image = np.clip((current_image - vmin) / (vmax - vmin), 0, 1)
2637
+
2638
+ # Create custom colormap with higher intensity
2639
+ color = base_colors[channel]
2640
+ custom_cmap = LinearSegmentedColormap.from_list(
2641
+ f'custom_{channel}',
2642
+ [(0,0,0,0), (*color,1)]
2643
+ )
2644
+
2645
+ # Display the image with slightly higher alpha
2646
+ self.ax.imshow(normalized_image,
2647
+ alpha=0.7,
2648
+ cmap=custom_cmap,
2649
+ vmin=0,
2650
+ vmax=1)
2651
+
2652
+ # Rest of the code remains the same...
2653
+ # Add highlight overlay if it exists
2654
+ if self.highlight_overlay is not None:
2655
+ highlight_slice = self.highlight_overlay[self.current_slice]
2656
+ highlight_cmap = LinearSegmentedColormap.from_list(
2657
+ 'highlight',
2658
+ [(0, 0, 0, 0), (1, 1, 0, 1)] # yellow
2208
2659
  )
2660
+ self.ax.imshow(highlight_slice,
2661
+ cmap=highlight_cmap,
2662
+ alpha=0.5)
2663
+
2664
+ # Restore zoom limits if they existed
2665
+ if current_xlim is not None and current_ylim is not None:
2666
+ self.ax.set_xlim(current_xlim)
2667
+ self.ax.set_ylim(current_ylim)
2668
+
2669
+ # Style the axes
2670
+ self.ax.set_xlabel('X')
2671
+ self.ax.set_ylabel('Y')
2672
+ self.ax.set_title(f'Slice {self.current_slice}')
2673
+
2674
+ # Make axis labels and ticks black for visibility against white background
2675
+ self.ax.xaxis.label.set_color('black')
2676
+ self.ax.yaxis.label.set_color('black')
2677
+ self.ax.title.set_color('black')
2678
+ self.ax.tick_params(colors='black')
2679
+ for spine in self.ax.spines.values():
2680
+ spine.set_color('black')
2681
+
2682
+ # Adjust the layout to ensure the plot fits well in the figure
2683
+ self.figure.tight_layout()
2684
+
2685
+ # Redraw measurement points and their labels
2686
+ for point in self.measurement_points:
2687
+ x1, y1, z1 = point['point1']
2688
+ x2, y2, z2 = point['point2']
2689
+ pair_idx = point['pair_index']
2209
2690
 
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
-
2691
+ # Draw points and labels if they're on current slice
2692
+ if z1 == self.current_slice:
2693
+ self.ax.plot(x1, y1, 'yo', markersize=8)
2694
+ self.ax.text(x1, y1+5, str(pair_idx),
2695
+ color='white', ha='center', va='bottom')
2696
+ if z2 == self.current_slice:
2697
+ self.ax.plot(x2, y2, 'yo', markersize=8)
2698
+ self.ax.text(x2, y2+5, str(pair_idx),
2699
+ color='white', ha='center', va='bottom')
2700
+
2701
+ # Draw line if both points are on current slice
2702
+ if z1 == z2 == self.current_slice:
2703
+ self.ax.plot([x1, x2], [y1, y2], 'r--', alpha=0.5)
2269
2704
 
2270
- self.canvas.draw()
2705
+ self.canvas.draw()
2271
2706
 
2272
2707
  def show_netshow_dialog(self):
2273
2708
  dialog = NetShowDialog(self)
@@ -2277,6 +2712,23 @@ class ImageViewerWindow(QMainWindow):
2277
2712
  dialog = PartitionDialog(self)
2278
2713
  dialog.exec()
2279
2714
 
2715
+ def show_radial_dialog(self):
2716
+ dialog = RadialDialog(self)
2717
+ dialog.exec()
2718
+
2719
+ def show_degree_dist_dialog(self):
2720
+ dialog = DegreeDistDialog(self)
2721
+ dialog.exec()
2722
+
2723
+ def show_neighbor_id_dialog(self):
2724
+ dialog = NeighborIdentityDialog(self)
2725
+ dialog.exec()
2726
+
2727
+ def show_random_dialog(self):
2728
+ dialog = RandomDialog(self)
2729
+ dialog.exec()
2730
+
2731
+
2280
2732
  def show_interaction_dialog(self):
2281
2733
  dialog = InteractionDialog(self)
2282
2734
  dialog.exec()
@@ -2285,10 +2737,19 @@ class ImageViewerWindow(QMainWindow):
2285
2737
  dialog = DegreeDialog(self)
2286
2738
  dialog.exec()
2287
2739
 
2740
+
2741
+ def show_hub_dialog(self):
2742
+ dialog = HubDialog(self)
2743
+ dialog.exec()
2744
+
2288
2745
  def show_mother_dialog(self):
2289
2746
  dialog = MotherDialog(self)
2290
2747
  dialog.exec()
2291
2748
 
2749
+ def show_code_dialog(self):
2750
+ dialog = CodeDialog(self)
2751
+ dialog.exec()
2752
+
2292
2753
 
2293
2754
 
2294
2755
  #TABLE RELATED:
@@ -3509,6 +3970,9 @@ class NetShowDialog(QDialog):
3509
3970
  def show_network(self):
3510
3971
  # Get parameters and run analysis
3511
3972
  geo = self.geo_layout.isChecked()
3973
+ if geo:
3974
+ if my_network.node_centroids is None:
3975
+ self.parent().show_centroid_dialog()
3512
3976
  accepted_mode = self.mode_selector.currentIndex() # Convert to 1-based index
3513
3977
  # Get directory (None if empty)
3514
3978
  directory = self.directory.text() if self.directory.text() else None
@@ -3533,6 +3997,8 @@ class NetShowDialog(QDialog):
3533
3997
  self.accept()
3534
3998
  except Exception as e:
3535
3999
  print(f"Error showing network: {e}")
4000
+ import traceback
4001
+ print(traceback.format_exc())
3536
4002
 
3537
4003
  class PartitionDialog(QDialog):
3538
4004
  def __init__(self, parent=None):
@@ -3555,6 +4021,12 @@ class PartitionDialog(QDialog):
3555
4021
  self.mode_selector.setCurrentIndex(0) # Default to Mode 1
3556
4022
  layout.addRow("Execution Mode:", self.mode_selector)
3557
4023
 
4024
+ # stats checkbox (default True)
4025
+ self.stats = QPushButton("Stats")
4026
+ self.stats.setCheckable(True)
4027
+ self.stats.setChecked(True)
4028
+ layout.addRow("Community Stats:", self.stats)
4029
+
3558
4030
  # Add Run button
3559
4031
  run_button = QPushButton("Partition")
3560
4032
  run_button.clicked.connect(self.partition)
@@ -3564,18 +4036,189 @@ class PartitionDialog(QDialog):
3564
4036
 
3565
4037
  accepted_mode = self.mode_selector.currentIndex()
3566
4038
  weighted = self.weighted.isChecked()
4039
+ dostats = self.stats.isChecked()
4040
+
4041
+ my_network.communities = None
3567
4042
 
3568
4043
  try:
3569
- my_network.community_partition(weighted = weighted, style = accepted_mode)
4044
+ stats = my_network.community_partition(weighted = weighted, style = accepted_mode, dostats = dostats)
3570
4045
  print(f"Discovered communities: {my_network.communities}")
3571
4046
 
3572
- self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'CommunityID')
4047
+ self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'CommunityID', title = 'Community Partition')
4048
+
4049
+ if len(stats.keys()) > 0:
4050
+ self.parent().format_for_upperright_table(stats, title = 'Community Stats')
3573
4051
 
3574
4052
  self.accept()
3575
4053
 
3576
4054
  except Exception as e:
3577
4055
  print(f"Error creating communities: {e}")
3578
4056
 
4057
+ class RadialDialog(QDialog):
4058
+
4059
+ def __init__(self, parent=None):
4060
+
4061
+ super().__init__(parent)
4062
+ self.setWindowTitle("Radial Parameters")
4063
+ self.setModal(True)
4064
+
4065
+ layout = QFormLayout(self)
4066
+
4067
+ self.distance = QLineEdit("50")
4068
+ layout.addRow("Bucket Distance for Searching For Node Neighbors (automatically scaled by xy and z scales):", self.distance)
4069
+
4070
+ self.directory = QLineEdit("")
4071
+ layout.addRow("Output Directory:", self.directory)
4072
+
4073
+ # Add Run button
4074
+ run_button = QPushButton("Get Radial Distribution")
4075
+ run_button.clicked.connect(self.radial)
4076
+ layout.addWidget(run_button)
4077
+
4078
+ def radial(self):
4079
+
4080
+ distance = float(self.distance.text()) if self.distance.text().strip() else 50
4081
+
4082
+ directory = str(self.distance.text()) if self.directory.text().strip() else None
4083
+
4084
+ if my_network.node_centroids is None:
4085
+ self.parent().show_centroid_dialog()
4086
+
4087
+ radial = my_network.radial_distribution(distance, directory = directory)
4088
+
4089
+ self.parent().format_for_upperright_table(radial, 'Radial Distance From Any Node', 'Average Number of Neighboring Nodes', title = 'Radial Distribution Analysis')
4090
+
4091
+ self.accept()
4092
+
4093
+ class DegreeDistDialog(QDialog):
4094
+
4095
+ def __init__(self, parent=None):
4096
+
4097
+ super().__init__(parent)
4098
+ self.setWindowTitle("Degree Distribution Parameters")
4099
+ self.setModal(True)
4100
+
4101
+ layout = QFormLayout(self)
4102
+
4103
+ self.directory = QLineEdit("")
4104
+ layout.addRow("Output Directory:", self.directory)
4105
+
4106
+ # Add Run button
4107
+ run_button = QPushButton("Get Degree Distribution")
4108
+ run_button.clicked.connect(self.degreedist)
4109
+ layout.addWidget(run_button)
4110
+
4111
+ def degreedist(self):
4112
+
4113
+ try:
4114
+
4115
+ directory = str(self.distance.text()) if self.directory.text().strip() else None
4116
+
4117
+ degrees = my_network.degree_distribution(directory = directory)
4118
+
4119
+
4120
+ self.parent().format_for_upperright_table(degrees, 'Degree (k)', 'Proportion of nodes with degree (p(k))', title = 'Degree Distribution Analysis')
4121
+
4122
+ self.accept()
4123
+
4124
+ except Excpetion as e:
4125
+ print(f"An error occurred: {e}")
4126
+
4127
+ class NeighborIdentityDialog(QDialog):
4128
+
4129
+ def __init__(self, parent=None):
4130
+
4131
+ super().__init__(parent)
4132
+ 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)")
4133
+ self.setModal(True)
4134
+
4135
+ layout = QFormLayout(self)
4136
+
4137
+ self.root = QComboBox()
4138
+ self.root.addItems(list(set(my_network.node_identities.values())))
4139
+ self.root.setCurrentIndex(0)
4140
+ layout.addRow("Root Identity to Search for Neighbor's IDs (search uses nodes of this ID, finds what IDs they connect to", self.root)
4141
+
4142
+ self.directory = QLineEdit("")
4143
+ layout.addRow("Output Directory:", self.directory)
4144
+
4145
+ self.mode = QComboBox()
4146
+ self.mode.addItems(["From Network - Based on Absolute Connectivity", "Use Labeled Nodes - Based on Neighborhood Densities"])
4147
+ self.mode.setCurrentIndex(0)
4148
+ layout.addRow("Mode", self.mode)
4149
+
4150
+ self.search = QLineEdit("")
4151
+ layout.addRow("Search Radius (Ignore if using network):", self.search)
4152
+
4153
+ # Add Run button
4154
+ run_button = QPushButton("Get Neighborhood Identity Distribution")
4155
+ run_button.clicked.connect(self.neighborids)
4156
+ layout.addWidget(run_button)
4157
+
4158
+ def neighborids(self):
4159
+
4160
+ root = self.root.currentText()
4161
+
4162
+ directory = self.directory.text() if self.directory.text().strip() else None
4163
+
4164
+ mode = self.mode.currentIndex()
4165
+
4166
+ search = float(self.search.text()) if self.search.text().strip() else 0
4167
+
4168
+
4169
+ result, result2, title1, title2 = my_network.neighborhood_identities(root = root, directory = directory, mode = mode, search = search)
4170
+
4171
+ self.parent().format_for_upperright_table(result, 'Node Identity', 'Amount', title = title1)
4172
+ self.parent().format_for_upperright_table(result2, 'Node Identity', 'Proportion', title = title2)
4173
+
4174
+ self.accept()
4175
+
4176
+
4177
+
4178
+
4179
+
4180
+
4181
+
4182
+
4183
+ class RandomDialog(QDialog):
4184
+
4185
+ def __init__(self, parent=None):
4186
+
4187
+ super().__init__(parent)
4188
+ self.setWindowTitle("Degree Distribution Parameters")
4189
+ self.setModal(True)
4190
+
4191
+ layout = QFormLayout(self)
4192
+
4193
+
4194
+ # stats checkbox (default True)
4195
+ self.weighted = QPushButton("weighted")
4196
+ self.weighted.setCheckable(True)
4197
+ self.weighted.setChecked(True)
4198
+ 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)
4199
+
4200
+
4201
+ # Add Run button
4202
+ run_button = QPushButton("Get Random Network (Will go in Selection Table)")
4203
+ run_button.clicked.connect(self.random)
4204
+ layout.addWidget(run_button)
4205
+
4206
+ def random(self):
4207
+
4208
+ weighted = self.weighted.isChecked()
4209
+
4210
+ _, df = my_network.assign_random(weighted = weighted)
4211
+
4212
+ # Create new model with filtered DataFrame and update selection table
4213
+ new_model = PandasModel(df)
4214
+ self.parent().selection_table.setModel(new_model)
4215
+
4216
+ # Switch to selection table
4217
+ self.parent().selection_button.click()
4218
+
4219
+ self.accept()
4220
+
4221
+
3579
4222
 
3580
4223
  class InteractionDialog(QDialog):
3581
4224
 
@@ -3645,6 +4288,9 @@ class DegreeDialog(QDialog):
3645
4288
  self.mode_selector.setCurrentIndex(0) # Default to Mode 1
3646
4289
  layout.addRow("Execution Mode:", self.mode_selector)
3647
4290
 
4291
+ self.mask_limiter = QLineEdit("1")
4292
+ layout.addRow("Masks smaller high degree proportion of nodes (ignore if only returning degrees)", self.mask_limiter)
4293
+
3648
4294
  self.down_factor = QLineEdit("1")
3649
4295
  layout.addRow("down_factor (for speeding up overlay generation - ignore if only returning degrees:", self.down_factor)
3650
4296
 
@@ -3664,6 +4310,11 @@ class DegreeDialog(QDialog):
3664
4310
  except ValueError:
3665
4311
  down_factor = 1
3666
4312
 
4313
+ try:
4314
+ mask_limiter = float(self.mask_limiter.text()) if self.mask_limiter.text() else 1
4315
+ except ValueError:
4316
+ mask_limiter = 1
4317
+
3667
4318
  if self.parent().active_channel == 1:
3668
4319
  active_data = self.parent().channel_data[0]
3669
4320
  else:
@@ -3680,9 +4331,47 @@ class DegreeDialog(QDialog):
3680
4331
 
3681
4332
  original_shape = copy.deepcopy(active_data.shape)
3682
4333
 
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
4334
 
3685
- result, nodes = temp_network.get_degrees(called = True, no_img = accepted_mode, down_factor = down_factor)
4335
+ if mask_limiter < 1 and accepted_mode != 0:
4336
+
4337
+ if len(np.unique(active_data)) < 3:
4338
+ active_data, _ = n3d.label_objects(active_data)
4339
+
4340
+ node_list = list(my_network.network.nodes)
4341
+ node_dict = {}
4342
+
4343
+ for node in node_list:
4344
+ node_dict[node] = (my_network.network.degree(node))
4345
+
4346
+ # Calculate the number of top proportion% entries
4347
+ num_items = len(node_dict)
4348
+ num_top_10_percent = max(1, int(num_items * mask_limiter)) # Ensure at least one item
4349
+
4350
+ # Sort the dictionary by values in descending order and get the top 10%
4351
+ sorted_items = sorted(node_dict.items(), key=lambda item: item[1], reverse=True)
4352
+ top_10_percent_items = sorted_items[:num_top_10_percent]
4353
+
4354
+ # Extract the keys from the top proportion% items
4355
+ top_10_percent_keys = [key for key, value in top_10_percent_items]
4356
+
4357
+ mask = np.isin(active_data, top_10_percent_keys)
4358
+ nodes = mask * active_data
4359
+ new_centroids = {}
4360
+ for node in my_network.node_centroids:
4361
+ if node in top_10_percent_keys:
4362
+ new_centroids[node] = my_network.node_centroids[node]
4363
+ del mask
4364
+
4365
+ temp_network = n3d.Network_3D(nodes = nodes, node_centroids = new_centroids, network = my_network.network, network_lists = my_network.network_lists)
4366
+
4367
+ result, nodes = temp_network.get_degrees(called = True, no_img = accepted_mode, down_factor = down_factor)
4368
+
4369
+ else:
4370
+ temp_network = n3d.Network_3D(nodes = active_data, node_centroids = my_network.node_centroids, network = my_network.network, network_lists = my_network.network_lists)
4371
+
4372
+ result, nodes = temp_network.get_degrees(called = True, no_img = accepted_mode, down_factor = down_factor)
4373
+
4374
+
3686
4375
 
3687
4376
  self.parent().format_for_upperright_table(result, 'Node ID', 'Degree', title = 'Degrees of nodes')
3688
4377
 
@@ -3698,9 +4387,75 @@ class DegreeDialog(QDialog):
3698
4387
 
3699
4388
  except Exception as e:
3700
4389
 
4390
+ import traceback
4391
+ print(traceback.format_exc())
4392
+
3701
4393
  print(f"Error finding degrees: {e}")
3702
4394
 
3703
4395
 
4396
+ class HubDialog(QDialog):
4397
+
4398
+ def __init__(self, parent=None):
4399
+
4400
+ super().__init__(parent)
4401
+ self.setWindowTitle("Hub Parameters")
4402
+ self.setModal(True)
4403
+
4404
+ layout = QFormLayout(self)
4405
+
4406
+ 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:"))
4407
+
4408
+ # Overlay checkbox (default True)
4409
+ self.overlay = QPushButton("Overlay")
4410
+ self.overlay.setCheckable(True)
4411
+ self.overlay.setChecked(True)
4412
+ layout.addRow("Make Overlay?:", self.overlay)
4413
+
4414
+
4415
+ self.proportion = QLineEdit("0.15")
4416
+ layout.addRow("Proportion of most connected hubs to keep (1 would imply returning entire network)", self.proportion)
4417
+
4418
+
4419
+ # Add Run button
4420
+ run_button = QPushButton("Get hubs")
4421
+ run_button.clicked.connect(self.hubs)
4422
+ layout.addWidget(run_button)
4423
+
4424
+ def hubs(self):
4425
+
4426
+ try:
4427
+
4428
+ try:
4429
+ proportion = float(self.proportion.text()) if self.proportion.text() else 1
4430
+ except ValueError:
4431
+ proportion = 1
4432
+
4433
+ overlay = self.overlay.isChecked()
4434
+
4435
+ result, img = my_network.isolate_hubs(proportion = proportion, retimg = overlay)
4436
+
4437
+ hub_dict = {}
4438
+
4439
+ for node in result:
4440
+ hub_dict[node] = my_network.network.degree(node)
4441
+
4442
+ self.parent().format_for_upperright_table(hub_dict, 'NodeID', 'Degree', title = f'Upper {proportion} Hub Nodes')
4443
+
4444
+ if img is not None:
4445
+
4446
+ self.parent().load_channel(3, channel_data = img, data = True)
4447
+
4448
+
4449
+ self.accept()
4450
+
4451
+ except Exception as e:
4452
+
4453
+ import traceback
4454
+ print(traceback.format_exc())
4455
+
4456
+ print(f"Error finding hubs: {e}")
4457
+
4458
+
3704
4459
 
3705
4460
  class MotherDialog(QDialog):
3706
4461
 
@@ -3749,7 +4504,12 @@ class MotherDialog(QDialog):
3749
4504
  G, result = my_network.isolate_mothers(self, louvain = my_network.communities, ret_nodes = False, called = True)
3750
4505
  self.parent().load_channel(2, channel_data = result, data = True)
3751
4506
 
3752
- self.parent().format_for_upperright_table(G.nodes(), 'Mothers', title = 'Mother Nodes')
4507
+ degree_dict = {}
4508
+
4509
+ for node in G.nodes():
4510
+ degree_dict[node] = my_network.network.degree(node)
4511
+
4512
+ self.parent().format_for_upperright_table(degree_dict, 'Mother ID', 'Degree', title = 'Mother Nodes')
3753
4513
 
3754
4514
 
3755
4515
  self.accept()
@@ -3759,6 +4519,58 @@ class MotherDialog(QDialog):
3759
4519
  print(f"Error finding mothers: {e}")
3760
4520
 
3761
4521
 
4522
+ class CodeDialog(QDialog):
4523
+
4524
+ def __init__(self, parent=None):
4525
+
4526
+ super().__init__(parent)
4527
+ self.setWindowTitle("Community Code Parameters (Will go to Overlay2)")
4528
+ self.setModal(True)
4529
+
4530
+ layout = QFormLayout(self)
4531
+
4532
+ self.down_factor = QLineEdit("")
4533
+ layout.addRow("down_factor (for speeding up overlay generation - optional):", self.down_factor)
4534
+
4535
+ # Add mode selection dropdown
4536
+ self.mode_selector = QComboBox()
4537
+ self.mode_selector.addItems(["Color Coded", "Grayscale Coded"])
4538
+ self.mode_selector.setCurrentIndex(0) # Default to Mode 1
4539
+ layout.addRow("Execution Mode:", self.mode_selector)
4540
+
4541
+
4542
+ # Add Run button
4543
+ run_button = QPushButton("Community Code")
4544
+ run_button.clicked.connect(self.code)
4545
+ layout.addWidget(run_button)
4546
+
4547
+ def code(self):
4548
+
4549
+ try:
4550
+
4551
+ mode = self.mode_selector.currentIndex()
4552
+
4553
+ down_factor = float(self.down_factor.text()) if self.down_factor.text().strip() else None
4554
+
4555
+
4556
+
4557
+ if my_network.communities is None:
4558
+ self.parent().show_partition_dialog()
4559
+ if my_network.communities is None:
4560
+ return
4561
+
4562
+ if mode == 0:
4563
+ image = my_network.extract_communities(down_factor = down_factor)
4564
+ elif mode == 1:
4565
+ image = my_network.extract_communities(color_code = False, down_factor = down_factor)
4566
+
4567
+
4568
+ self.parent().load_channel(3, image, True)
4569
+ self.accept()
4570
+
4571
+ except Exception as e:
4572
+ print(f"An error has occurred: {e}")
4573
+
3762
4574
 
3763
4575
 
3764
4576
 
@@ -3790,7 +4602,11 @@ class ResizeDialog(QDialog):
3790
4602
  self.cubic.setChecked(False)
3791
4603
  layout.addRow("Use cubic algorithm:", self.cubic)
3792
4604
 
3793
-
4605
+ if self.parent().original_shape is not None:
4606
+ undo_button = QPushButton(f"Resample to original shape: {self.parent().original_shape}")
4607
+ undo_button.clicked.connect(lambda: self.run_resize(undo = True))
4608
+ layout.addRow(undo_button)
4609
+
3794
4610
  run_button = QPushButton("Run Resize")
3795
4611
  run_button.clicked.connect(self.run_resize)
3796
4612
  layout.addRow(run_button)
@@ -3800,9 +4616,9 @@ class ResizeDialog(QDialog):
3800
4616
  self.resize.clear()
3801
4617
  self.zsize.setText("1")
3802
4618
  self.xsize.setText("1")
3803
- self.ysize.setText("1")
4619
+ self.ysize.setText("1")
3804
4620
 
3805
- def run_resize(self):
4621
+ def run_resize(self, undo = False):
3806
4622
  try:
3807
4623
  # Get parameters
3808
4624
  try:
@@ -3851,16 +4667,31 @@ class ResizeDialog(QDialog):
3851
4667
  self.parent().slice_slider.setValue(0)
3852
4668
  self.parent().current_slice = 0
3853
4669
 
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)
4670
+ if not undo:
4671
+ # Process each channel
4672
+ for channel in range(4):
4673
+ if self.parent().channel_data[channel] is not None:
4674
+ resized_data = n3d.resize(self.parent().channel_data[channel], resize, order)
4675
+ self.parent().load_channel(channel, channel_data=resized_data, data=True, assign_shape = False)
4676
+
4677
+
4678
+ # Process highlight overlay if it exists
4679
+ if self.parent().highlight_overlay is not None:
4680
+ self.parent().highlight_overlay = n3d.resize(self.parent().highlight_overlay, resize, order)
4681
+ else:
4682
+ # Process each channel
4683
+ if array_shape == self.parent().original_shape:
4684
+ return
4685
+ for channel in range(4):
4686
+ if self.parent().channel_data[channel] is not None:
4687
+ resized_data = n3d.upsample_with_padding(self.parent().channel_data[channel], original_shape = self.parent().original_shape)
4688
+ self.parent().load_channel(channel, channel_data=resized_data, data=True, assign_shape = False)
4689
+
4690
+
4691
+ # Process highlight overlay if it exists
4692
+ if self.parent().highlight_overlay is not None:
4693
+ self.parent().highlight_overlay = n3d.upsample_with_padding(self.parent().highlight_overlay, original_shape = self.parent().original_shape)
3859
4694
 
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
4695
 
3865
4696
  # Update slider range based on new z-dimension
3866
4697
  for channel in self.parent().channel_data:
@@ -3984,78 +4815,317 @@ class BinarizeDialog(QDialog):
3984
4815
  f"Error running binarize: {str(e)}"
3985
4816
  )
3986
4817
 
3987
- except Exception as e:
3988
- QMessageBox.critical(
3989
- self,
3990
- "Error",
3991
- f"Error running binarize: {str(e)}"
3992
- )
4818
+ except Exception as e:
4819
+ QMessageBox.critical(
4820
+ self,
4821
+ "Error",
4822
+ f"Error running binarize: {str(e)}"
4823
+ )
4824
+
4825
+ class LabelDialog(QDialog):
4826
+ def __init__(self, parent=None):
4827
+ super().__init__(parent)
4828
+ self.setWindowTitle("Label Active Channel?")
4829
+ self.setModal(True)
4830
+
4831
+ layout = QFormLayout(self)
4832
+
4833
+ # Add Run button
4834
+ run_button = QPushButton("Run Label")
4835
+ run_button.clicked.connect(self.run_label)
4836
+ layout.addRow(run_button)
4837
+
4838
+ def run_label(self):
4839
+
4840
+ try:
4841
+
4842
+ # Get the active channel data from parent
4843
+ active_data = self.parent().channel_data[self.parent().active_channel]
4844
+ if active_data is None:
4845
+ raise ValueError("No active image selected")
4846
+
4847
+ try:
4848
+ # Call watershed method with parameters
4849
+ result, _ = n3d.label_objects(
4850
+ active_data
4851
+ )
4852
+
4853
+ # Update both the display data and the network object
4854
+ self.parent().channel_data[self.parent().active_channel] = result
4855
+
4856
+
4857
+ # Update the corresponding property in my_network
4858
+ setattr(my_network, network_properties[self.parent().active_channel], result)
4859
+
4860
+ self.parent().update_display()
4861
+ self.accept()
4862
+
4863
+ except Exception as e:
4864
+ QMessageBox.critical(
4865
+ self,
4866
+ "Error",
4867
+ f"Error running label: {str(e)}"
4868
+ )
4869
+
4870
+ except Exception as e:
4871
+ QMessageBox.critical(
4872
+ self,
4873
+ "Error",
4874
+ f"Error running label: {str(e)}"
4875
+ )
4876
+
4877
+ class ThresholdWindow(QMainWindow):
4878
+ def __init__(self, parent=None):
4879
+ super().__init__(parent)
4880
+ self.setWindowTitle("Threshold Params (Active Image)")
4881
+
4882
+ # Create central widget and layout
4883
+ central_widget = QWidget()
4884
+ self.setCentralWidget(central_widget)
4885
+ layout = QFormLayout(central_widget)
4886
+
4887
+ self.min = QLineEdit("")
4888
+ layout.addRow("Minimum Value to retain:", self.min)
4889
+
4890
+ # Create widgets
4891
+ self.max = QLineEdit("")
4892
+ layout.addRow("Maximum Value to retain:", self.max)
4893
+
4894
+ # Add mode selection dropdown
4895
+ self.mode_selector = QComboBox()
4896
+ self.mode_selector.addItems(["Using Volumes", "Using Label/Brightness"])
4897
+ self.mode_selector.setCurrentIndex(0) # Default to Mode 1
4898
+ layout.addRow("Execution Mode:", self.mode_selector)
4899
+
4900
+ # Add Run button
4901
+ prev_button = QPushButton("Preview")
4902
+ prev_button.clicked.connect(self.run_preview)
4903
+ layout.addRow(prev_button)
4904
+
4905
+ # Add Run button
4906
+ run_button = QPushButton("Apply Threshold")
4907
+ run_button.clicked.connect(self.thresh)
4908
+ layout.addRow(run_button)
4909
+
4910
+ # Set a reasonable default size
4911
+ self.setMinimumWidth(300)
4912
+
4913
+ def run_preview(self):
4914
+
4915
+ def get_valid_float(text, default_value):
4916
+ try:
4917
+ return float(text) if text.strip() else default_value
4918
+ except ValueError:
4919
+ print(f"Invalid input: {text}")
4920
+ return default_value
4921
+
4922
+ try:
4923
+ channel = self.parent().active_channel
4924
+ accepted_mode = self.mode_selector.currentIndex()
4925
+
4926
+ if accepted_mode == 0:
4927
+ if len(np.unique(self.parent().channel_data[self.parent().active_channel])) < 3:
4928
+ self.parent().show_label_dialog()
4929
+
4930
+ if self.parent().volume_dict[channel] is None:
4931
+ self.parent().volumes()
4932
+
4933
+ volumes = self.parent().volume_dict[channel]
4934
+ default_max = max(volumes.values())
4935
+ default_min = min(volumes.values())
4936
+
4937
+ max_val = get_valid_float(self.max.text(), default_max)
4938
+ min_val = get_valid_float(self.min.text(), default_min)
4939
+
4940
+ valid_indices = [item for item in volumes
4941
+ if min_val <= volumes[item] <= max_val]
4942
+
4943
+ elif accepted_mode == 1:
4944
+ channel_data = self.parent().channel_data[self.parent().active_channel]
4945
+ default_max = np.max(channel_data)
4946
+ default_min = np.min(channel_data)
4947
+
4948
+ max_val = int(get_valid_float(self.max.text(), default_max))
4949
+ min_val = int(get_valid_float(self.min.text(), default_min))
4950
+
4951
+ if min_val > max_val:
4952
+ min_val, max_val = max_val, min_val
4953
+
4954
+ valid_indices = list(range(min_val, max_val + 1))
4955
+
4956
+ if channel == 0:
4957
+ self.parent().create_highlight_overlay(node_indices = valid_indices)
4958
+ elif channel == 1:
4959
+ self.parent().create_highlight_overlay(edge_indices = valid_indices)
4960
+ elif channel == 2:
4961
+ self.parent().create_highlight_overlay(overlay1_indices = valid_indices)
4962
+ elif channel == 3:
4963
+ self.parent().create_highlight_overlay(overlay2_indices = valid_indices)
4964
+
4965
+ except Exception as e:
4966
+ print(f"Error showing preview: {e}")
4967
+
4968
+ def thresh(self):
4969
+ try:
4970
+
4971
+ self.run_preview()
4972
+ channel_data = self.parent().channel_data[self.parent().active_channel]
4973
+ mask = self.parent().highlight_overlay > 0
4974
+ channel_data = channel_data * mask
4975
+ self.parent().load_channel(self.parent().active_channel, channel_data, True)
4976
+ self.parent().update_display()
4977
+ self.close()
4978
+
4979
+ except Exception as e:
4980
+ QMessageBox.critical(
4981
+ self,
4982
+ "Error",
4983
+ f"Error running threshold: {str(e)}"
4984
+ )
4985
+
4986
+
4987
+ class SmartDilateDialog(QDialog):
4988
+ def __init__(self, parent, params):
4989
+ super().__init__(parent)
4990
+ self.setWindowTitle("Additional Smart Dilate Parameters")
4991
+ self.setModal(True)
4992
+
4993
+ layout = QFormLayout(self)
4994
+
4995
+ # GPU checkbox (default True)
4996
+ self.GPU = QPushButton("GPU")
4997
+ self.GPU.setCheckable(True)
4998
+ self.GPU.setChecked(True)
4999
+ layout.addRow("Use GPU:", self.GPU)
5000
+
5001
+ self.down_factor = QLineEdit("")
5002
+ layout.addRow("Internal Downsample for GPU (if needed):", self.down_factor)
5003
+
5004
+ self.params = params
5005
+
5006
+ # Add Run button
5007
+ run_button = QPushButton("Dilate")
5008
+ run_button.clicked.connect(self.smart_dilate)
5009
+ layout.addRow(run_button)
5010
+
5011
+ def smart_dilate(self):
5012
+
5013
+ GPU = self.GPU.isChecked()
5014
+ down_factor = float(self.down_factor.text()) if self.down_factor.text().strip() else None
5015
+ active_data, amount, xy_scale, z_scale = self.params
5016
+
5017
+ dilate_xy, dilate_z = n3d.dilation_length_to_pixels(xy_scale, z_scale, amount, amount)
5018
+
5019
+ result = sdl.smart_dilate(active_data, dilate_xy, dilate_z, GPU = GPU, predownsample = down_factor)
5020
+
5021
+ self.parent().load_channel(self.parent().active_channel, result, True)
5022
+ self.accept()
5023
+
5024
+
5025
+
5026
+ class DilateDialog(QDialog):
5027
+ def __init__(self, parent=None):
5028
+ super().__init__(parent)
5029
+ self.setWindowTitle("Dilate Parameters")
5030
+ self.setModal(True)
5031
+
5032
+ layout = QFormLayout(self)
5033
+
5034
+ self.amount = QLineEdit("1")
5035
+ layout.addRow("Dilation Radius:", self.amount)
5036
+
5037
+ if my_network.xy_scale is not None:
5038
+ xy_scale = f"{my_network.xy_scale}"
5039
+ else:
5040
+ xy_scale = "1"
5041
+
5042
+ self.xy_scale = QLineEdit(xy_scale)
5043
+ layout.addRow("xy_scale:", self.xy_scale)
5044
+
5045
+ if my_network.z_scale is not None:
5046
+ z_scale = f"{my_network.z_scale}"
5047
+ else:
5048
+ z_scale = "1"
5049
+
5050
+ self.z_scale = QLineEdit(z_scale)
5051
+ layout.addRow("z_scale:", self.z_scale)
3993
5052
 
3994
- class LabelDialog(QDialog):
3995
- def __init__(self, parent=None):
3996
- super().__init__(parent)
3997
- self.setWindowTitle("Label Active Channel?")
3998
- self.setModal(True)
3999
-
4000
- layout = QFormLayout(self)
5053
+ # Add mode selection dropdown
5054
+ self.mode_selector = QComboBox()
5055
+ self.mode_selector.addItems(["Binary Dilation", "Preserve Labels (slower)"])
5056
+ self.mode_selector.setCurrentIndex(0) # Default to Mode 1
5057
+ layout.addRow("Execution Mode:", self.mode_selector)
4001
5058
 
4002
5059
  # Add Run button
4003
- run_button = QPushButton("Run Label")
4004
- run_button.clicked.connect(self.run_label)
5060
+ run_button = QPushButton("Run Dilate")
5061
+ run_button.clicked.connect(self.run_dilate)
4005
5062
  layout.addRow(run_button)
4006
5063
 
4007
- def run_label(self):
4008
-
5064
+ def run_dilate(self):
4009
5065
  try:
4010
5066
 
5067
+ accepted_mode = self.mode_selector.currentIndex()
5068
+
5069
+ # Get amount
5070
+ try:
5071
+ amount = float(self.amount.text()) if self.amount.text() else 1
5072
+ except ValueError:
5073
+ amount = 1
5074
+
5075
+ try:
5076
+ xy_scale = float(self.xy_scale.text()) if self.xy_scale.text() else 1
5077
+ except ValueError:
5078
+ xy_scale = 1
5079
+
5080
+ try:
5081
+ z_scale = float(self.z_scale.text()) if self.z_scale.text() else 1
5082
+ except ValueError:
5083
+ z_scale = 1
5084
+
4011
5085
  # Get the active channel data from parent
4012
5086
  active_data = self.parent().channel_data[self.parent().active_channel]
4013
5087
  if active_data is None:
4014
5088
  raise ValueError("No active image selected")
4015
5089
 
4016
- try:
4017
- # Call watershed method with parameters
4018
- result, _ = n3d.label_objects(
4019
- active_data
4020
- )
4021
-
4022
- # Update both the display data and the network object
4023
- self.parent().channel_data[self.parent().active_channel] = result
4024
-
5090
+ if accepted_mode == 1:
5091
+ dialog = SmartDilateDialog(self.parent(), [active_data, amount, xy_scale, z_scale])
5092
+ dialog.exec()
5093
+ self.accept()
5094
+ return
4025
5095
 
4026
- # Update the corresponding property in my_network
4027
- setattr(my_network, network_properties[self.parent().active_channel], result)
5096
+ # Call dilate method with parameters
5097
+ result = n3d.dilate(
5098
+ active_data,
5099
+ amount,
5100
+ xy_scale = xy_scale,
5101
+ z_scale = z_scale,
5102
+ )
4028
5103
 
4029
- self.parent().update_display()
4030
- self.accept()
4031
-
4032
- except Exception as e:
4033
- QMessageBox.critical(
4034
- self,
4035
- "Error",
4036
- f"Error running label: {str(e)}"
4037
- )
5104
+ # Update both the display data and the network object
5105
+ self.parent().load_channel(self.parent().active_channel, result, True)
4038
5106
 
5107
+ self.parent().update_display()
5108
+ self.accept()
5109
+
4039
5110
  except Exception as e:
5111
+ import traceback
5112
+ print(traceback.format_exc())
4040
5113
  QMessageBox.critical(
4041
5114
  self,
4042
5115
  "Error",
4043
- f"Error running label: {str(e)}"
5116
+ f"Error running dilate: {str(e)}"
4044
5117
  )
4045
5118
 
4046
-
4047
-
4048
-
4049
- class DilateDialog(QDialog):
5119
+ class ErodeDialog(QDialog):
4050
5120
  def __init__(self, parent=None):
4051
5121
  super().__init__(parent)
4052
- self.setWindowTitle("Dilate Parameters")
5122
+ self.setWindowTitle("Erosion Parameters")
4053
5123
  self.setModal(True)
4054
5124
 
4055
5125
  layout = QFormLayout(self)
4056
5126
 
4057
5127
  self.amount = QLineEdit("1")
4058
- layout.addRow("Dilation Radius:", self.amount)
5128
+ layout.addRow("Erosion Radius:", self.amount)
4059
5129
 
4060
5130
  if my_network.xy_scale is not None:
4061
5131
  xy_scale = f"{my_network.xy_scale}"
@@ -4074,11 +5144,11 @@ class DilateDialog(QDialog):
4074
5144
  layout.addRow("z_scale:", self.z_scale)
4075
5145
 
4076
5146
  # Add Run button
4077
- run_button = QPushButton("Run Dilate")
4078
- run_button.clicked.connect(self.run_dilate)
5147
+ run_button = QPushButton("Run Erode")
5148
+ run_button.clicked.connect(self.run_erode)
4079
5149
  layout.addRow(run_button)
4080
5150
 
4081
- def run_dilate(self):
5151
+ def run_erode(self):
4082
5152
  try:
4083
5153
 
4084
5154
  # Get amount
@@ -4103,7 +5173,7 @@ class DilateDialog(QDialog):
4103
5173
  raise ValueError("No active image selected")
4104
5174
 
4105
5175
  # Call dilate method with parameters
4106
- result = n3d.dilate(
5176
+ result = n3d.erode(
4107
5177
  active_data,
4108
5178
  amount,
4109
5179
  xy_scale = xy_scale,
@@ -4124,7 +5194,46 @@ class DilateDialog(QDialog):
4124
5194
  QMessageBox.critical(
4125
5195
  self,
4126
5196
  "Error",
4127
- f"Error running dilate: {str(e)}"
5197
+ f"Error running erode: {str(e)}"
5198
+ )
5199
+
5200
+ class HoleDialog(QDialog):
5201
+ def __init__(self, parent=None):
5202
+ super().__init__(parent)
5203
+ self.setWindowTitle("Fill Holes? (Active Image)")
5204
+ self.setModal(True)
5205
+
5206
+ layout = QFormLayout(self)
5207
+
5208
+ # Add Run button
5209
+ run_button = QPushButton("Run Fill Holes")
5210
+ run_button.clicked.connect(self.run_holes)
5211
+ layout.addRow(run_button)
5212
+
5213
+ def run_holes(self):
5214
+ try:
5215
+
5216
+
5217
+ # Get the active channel data from parent
5218
+ active_data = self.parent().channel_data[self.parent().active_channel]
5219
+ if active_data is None:
5220
+ raise ValueError("No active image selected")
5221
+
5222
+ # Call dilate method with parameters
5223
+ result = n3d.fill_holes_3d(
5224
+ active_data
5225
+ )
5226
+
5227
+ self.parent().load_channel(self.parent().active_channel, result, True)
5228
+
5229
+ self.parent().update_display()
5230
+ self.accept()
5231
+
5232
+ except Exception as e:
5233
+ QMessageBox.critical(
5234
+ self,
5235
+ "Error",
5236
+ f"Error running fill holes: {str(e)}"
4128
5237
  )
4129
5238
 
4130
5239
  class MaskDialog(QDialog):
@@ -4372,6 +5481,40 @@ class WatershedDialog(QDialog):
4372
5481
  f"Error running watershed: {str(e)}"
4373
5482
  )
4374
5483
 
5484
+ class ZDialog(QDialog):
5485
+
5486
+ def __init__(self, parent=None):
5487
+ super().__init__(parent)
5488
+ self.setWindowTitle("Z Parameters (Save your network first - this will alter all channels into 2D versions)")
5489
+ self.setModal(True)
5490
+
5491
+ layout = QFormLayout(self)
5492
+
5493
+ # Add mode selection dropdown
5494
+ self.mode_selector = QComboBox()
5495
+ self.mode_selector.addItems(["max", "mean", "min", "sum", "std"])
5496
+ self.mode_selector.setCurrentIndex(0) # Default to Mode 1
5497
+ layout.addRow("Execution Mode:", self.mode_selector)
5498
+
5499
+ # Add Run button
5500
+ run_button = QPushButton("Run Z Project")
5501
+ run_button.clicked.connect(self.run_z)
5502
+ layout.addRow(run_button)
5503
+
5504
+ def run_z(self):
5505
+
5506
+ mode = self.mode_selector.currentText()
5507
+
5508
+ for i in range(len(self.parent().channel_data)):
5509
+ try:
5510
+ self.parent().channel_data[i] = n3d.z_project(self.parent().channel_data[i], mode)
5511
+ self.parent().load_channel(i, self.parent().channel_data[i], True)
5512
+ except:
5513
+ pass
5514
+
5515
+ self.accept()
5516
+
5517
+
4375
5518
  class CentroidNodeDialog(QDialog):
4376
5519
  def __init__(self, parent=None):
4377
5520
  super().__init__(parent)
@@ -4624,6 +5767,275 @@ class BranchDialog(QDialog):
4624
5767
 
4625
5768
 
4626
5769
 
5770
+ class IsolateDialog(QDialog):
5771
+ def __init__(self, parent=None):
5772
+ super().__init__(parent)
5773
+ self.setWindowTitle("Select Node types to isolate")
5774
+ self.setModal(True)
5775
+ layout = QFormLayout(self)
5776
+
5777
+ self.combo1 = QComboBox()
5778
+ self.combo1.addItems(list(set(my_network.node_identities.values())))
5779
+ self.combo1.setCurrentIndex(0)
5780
+ layout.addRow("ID 1:", self.combo1)
5781
+
5782
+ self.combo2 = QComboBox()
5783
+ self.combo2.addItems(list(set(my_network.node_identities.values())))
5784
+ self.combo2.setCurrentIndex(1)
5785
+ layout.addRow("ID 2:", self.combo2)
5786
+
5787
+ # Add submit button
5788
+ sub_button = QPushButton("Submit")
5789
+ sub_button.clicked.connect(self.submit_ids)
5790
+ layout.addRow(sub_button)
5791
+
5792
+ def submit_ids(self):
5793
+ try:
5794
+ id1 = self.combo1.currentText()
5795
+ id2 = self.combo2.currentText()
5796
+ if id1 == id2:
5797
+ print("Please select different identities")
5798
+ self.parent().show_isolate_dialog()
5799
+ return
5800
+ else:
5801
+ my_network.isolate_internode_connections(id1, id2)
5802
+ self.accept()
5803
+ except Exception as e:
5804
+ print(f"An error occurred: {e}")
5805
+
5806
+ class AlterDialog(QDialog):
5807
+ def __init__(self, parent=None):
5808
+ super().__init__(parent)
5809
+ self.setWindowTitle("Enter Node/Edge groups to add/remove")
5810
+ self.setModal(True)
5811
+ layout = QFormLayout(self)
5812
+
5813
+ # Node 1
5814
+ self.node1 = QLineEdit()
5815
+ self.node1.setPlaceholderText("Enter integer")
5816
+ layout.addRow("Node1:", self.node1)
5817
+
5818
+ # Node 2
5819
+ self.node2 = QLineEdit()
5820
+ self.node2.setPlaceholderText("Enter integer")
5821
+ layout.addRow("Node2:", self.node2)
5822
+
5823
+ # Edge
5824
+ self.edge = QLineEdit()
5825
+ self.edge.setPlaceholderText("Optional - Enter integer")
5826
+ layout.addRow("Edge:", self.edge)
5827
+
5828
+ # Add add button
5829
+ addbutton = QPushButton("Add pair")
5830
+ addbutton.clicked.connect(self.add)
5831
+ layout.addRow(addbutton)
5832
+
5833
+ # Add remove button
5834
+ removebutton = QPushButton("Remove pair")
5835
+ removebutton.clicked.connect(self.remove)
5836
+ layout.addRow(removebutton)
5837
+
5838
+ def add(self):
5839
+ try:
5840
+ node1 = int(self.node1.text()) if self.node1.text().strip() else None
5841
+ node2 = int(self.node2.text()) if self.node2.text().strip() else None
5842
+ edge = int(self.edge.text()) if self.edge.text().strip() else None
5843
+
5844
+ # Check if we have valid node pairs
5845
+ if node1 is not None and node2 is not None:
5846
+ # Add the node pair and its reverse
5847
+ my_network.network_lists[0].append(node1)
5848
+ my_network.network_lists[1].append(node2)
5849
+ # Add edge value (0 if none provided)
5850
+ my_network.network_lists[2].append(edge if edge is not None else 0)
5851
+
5852
+ # Add reverse pair with same edge value
5853
+ my_network.network_lists[0].append(node2)
5854
+ my_network.network_lists[1].append(node1)
5855
+ my_network.network_lists[2].append(edge if edge is not None else 0)
5856
+ try:
5857
+ if hasattr(my_network, 'network_lists'):
5858
+ model = PandasModel(my_network.network_lists)
5859
+ self.parent().network_table.setModel(model)
5860
+ # Adjust column widths to content
5861
+ for column in range(model.columnCount(None)):
5862
+ self.parent().network_table.resizeColumnToContents(column)
5863
+ except Exception as e:
5864
+ print(f"Error showing network table: {e}")
5865
+ except ValueError:
5866
+ import traceback
5867
+ print(traceback.format_exc())
5868
+ pass # Invalid input - do nothing
5869
+
5870
+ def remove(self):
5871
+ try:
5872
+ node1 = int(self.node1.text()) if self.node1.text().strip() else None
5873
+ node2 = int(self.node2.text()) if self.node2.text().strip() else None
5874
+ edge = int(self.edge.text()) if self.edge.text().strip() else None
5875
+
5876
+ # Check if we have valid node pairs
5877
+ if node1 is not None and node2 is not None:
5878
+ # Create lists for indices to remove
5879
+ indices_to_remove = []
5880
+
5881
+ # Loop through the lists to find matching pairs
5882
+ for i in range(len(my_network.network_lists[0])):
5883
+ forward_match = (my_network.network_lists[0][i] == node1 and
5884
+ my_network.network_lists[1][i] == node2)
5885
+ reverse_match = (my_network.network_lists[0][i] == node2 and
5886
+ my_network.network_lists[1][i] == node1)
5887
+
5888
+ if forward_match or reverse_match:
5889
+ # If edge value specified, only remove if edge matches
5890
+ if edge is not None:
5891
+ if my_network.network_lists[2][i] == edge:
5892
+ indices_to_remove.append(i)
5893
+ else:
5894
+ # If no edge specified, remove all matching pairs
5895
+ indices_to_remove.append(i)
5896
+
5897
+ # Remove elements in reverse order to maintain correct indices
5898
+ for i in sorted(indices_to_remove, reverse=True):
5899
+ my_network.network_lists[0].pop(i)
5900
+ my_network.network_lists[1].pop(i)
5901
+ my_network.network_lists[2].pop(i)
5902
+
5903
+ try:
5904
+ if hasattr(my_network, 'network_lists'):
5905
+ model = PandasModel(my_network.network_lists)
5906
+ self.parent().network_table.setModel(model)
5907
+ # Adjust column widths to content
5908
+ for column in range(model.columnCount(None)):
5909
+ self.parent().network_table.resizeColumnToContents(column)
5910
+ except Exception as e:
5911
+ print(f"Error showing network table: {e}")
5912
+
5913
+ except ValueError:
5914
+ import traceback
5915
+ print(traceback.format_exc())
5916
+ pass # Invalid input - do nothing
5917
+
5918
+
5919
+ class ModifyDialog(QDialog):
5920
+ def __init__(self, parent=None):
5921
+ super().__init__(parent)
5922
+ self.setWindowTitle("Create Nodes from Edge Vertices")
5923
+ self.setModal(True)
5924
+ layout = QFormLayout(self)
5925
+
5926
+ # trunk checkbox (default false)
5927
+ self.trunk = QPushButton("Remove Trunk")
5928
+ self.trunk.setCheckable(True)
5929
+ self.trunk.setChecked(False)
5930
+ layout.addRow("Remove Trunk? (Most connected edge - overrides below):", self.trunk)
5931
+
5932
+ # trunk checkbox (default false)
5933
+ self.trunknode = QPushButton("Trunk -> Node")
5934
+ self.trunknode.setCheckable(True)
5935
+ self.trunknode.setChecked(False)
5936
+ layout.addRow("Convert Trunk to Node? (Most connected edge):", self.trunknode)
5937
+
5938
+ # edgenode checkbox (default false)
5939
+ self.edgenode = QPushButton("Edges -> Nodes")
5940
+ self.edgenode.setCheckable(True)
5941
+ self.edgenode.setChecked(False)
5942
+ layout.addRow("Convert 'Edges (Labeled objects)' to node objects?:", self.edgenode)
5943
+
5944
+ # edgeweight checkbox (default false)
5945
+ self.edgeweight = QPushButton("Remove weights")
5946
+ self.edgeweight.setCheckable(True)
5947
+ self.edgeweight.setChecked(False)
5948
+ layout.addRow("Remove network weights?:", self.edgeweight)
5949
+
5950
+ # prune checkbox (default false)
5951
+ self.prune = QPushButton("Prune Same Type")
5952
+ self.prune.setCheckable(True)
5953
+ self.prune.setChecked(False)
5954
+ layout.addRow("Prune connections between nodes of the same type (if assigned)?:", self.prune)
5955
+
5956
+ # isolate checkbox (default false)
5957
+ self.isolate = QPushButton("Isolate Two Types")
5958
+ self.isolate.setCheckable(True)
5959
+ self.isolate.setChecked(False)
5960
+ layout.addRow("Isolate connections between two specific node types (if assigned)?:", self.isolate)
5961
+
5962
+ #change button
5963
+ change_button = QPushButton("Add/Remove Network Pairs")
5964
+ change_button.clicked.connect(self.show_alter_dialog)
5965
+ layout.addRow(change_button)
5966
+
5967
+ # Add Run button
5968
+ run_button = QPushButton("Make Changes")
5969
+ run_button.clicked.connect(self.run_changes)
5970
+ layout.addRow(run_button)
5971
+
5972
+ def show_isolate_dialog(self):
5973
+
5974
+ dialog = IsolateDialog(self)
5975
+ dialog.exec()
5976
+
5977
+ def show_alter_dialog(self):
5978
+
5979
+ dialog = AlterDialog(self.parent())
5980
+ dialog.exec()
5981
+
5982
+ def run_changes(self):
5983
+
5984
+ try:
5985
+
5986
+ trunk = self.trunk.isChecked()
5987
+ if not trunk:
5988
+ trunknode = self.trunknode.isChecked()
5989
+ else:
5990
+ trunknode = False
5991
+ edgenode = self.edgenode.isChecked()
5992
+ edgeweight = self.edgeweight.isChecked()
5993
+ prune = self.prune.isChecked()
5994
+ isolate = self.isolate.isChecked()
5995
+
5996
+ if isolate and my_network.node_identities is not None:
5997
+ self.show_isolate_dialog()
5998
+
5999
+ if edgeweight:
6000
+ my_network.remove_edge_weights()
6001
+ if prune and my_network.node_identities is not None:
6002
+ my_network.prune_samenode_connections()
6003
+ if trunk:
6004
+ my_network.remove_trunk_post()
6005
+ if trunknode:
6006
+ if my_network.node_centroids is None or my_network.edge_centroids is None:
6007
+ self.parent().show_centroid_dialog()
6008
+ my_network.trunk_to_node()
6009
+ self.parent().load_channel(0, my_network.nodes, True)
6010
+ if edgenode:
6011
+ if my_network.node_centroids is None or my_network.edge_centroids is None:
6012
+ self.parent().show_centroid_dialog()
6013
+ my_network.edge_to_node()
6014
+ self.parent().load_channel(0, my_network.nodes, True)
6015
+ self.parent().load_channel(1, my_network.edges, True)
6016
+
6017
+ try:
6018
+ if hasattr(my_network, 'network_lists'):
6019
+ model = PandasModel(my_network.network_lists)
6020
+ self.parent().network_table.setModel(model)
6021
+ # Adjust column widths to content
6022
+ for column in range(model.columnCount(None)):
6023
+ self.parent().network_table.resizeColumnToContents(column)
6024
+ except Exception as e:
6025
+ print(f"Error showing network table: {e}")
6026
+
6027
+ if hasattr(my_network, 'node_identities') and my_network.node_identities is not None:
6028
+ try:
6029
+ self.parent().format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity', 'Node Identities')
6030
+ except Exception as e:
6031
+ print(f"Error loading node identity table: {e}")
6032
+
6033
+ self.parent().update_display()
6034
+ self.accept()
6035
+
6036
+ except Exception as e:
6037
+ print(f"An error occurred: {e}")
6038
+
4627
6039
 
4628
6040
 
4629
6041
 
@@ -5031,6 +6443,15 @@ class ProxDialog(QDialog):
5031
6443
  self.mode_selector.setCurrentIndex(0) # Default to Mode 1
5032
6444
  layout.addRow("Execution Mode:", self.mode_selector)
5033
6445
 
6446
+ if my_network.node_identities is not None:
6447
+ self.id_selector = QComboBox()
6448
+ # Add all options from id dictionary
6449
+ self.id_selector.addItems(['None'] + list(set(my_network.node_identities.values())))
6450
+ self.id_selector.setCurrentIndex(0) # Default to Mode 1
6451
+ layout.addRow("Create Networks only from a specific node identity?:", self.id_selector)
6452
+ else:
6453
+ self.id_selector = None
6454
+
5034
6455
  self.overlays = QPushButton("Overlays")
5035
6456
  self.overlays.setCheckable(True)
5036
6457
  self.overlays.setChecked(True)
@@ -5054,6 +6475,15 @@ class ProxDialog(QDialog):
5054
6475
 
5055
6476
  mode = self.mode_selector.currentIndex()
5056
6477
 
6478
+ if self.id_selector is not None and self.id_selector.currentText() != 'None':
6479
+ target = self.id_selector.currentText()
6480
+ targets = []
6481
+ for node in my_network.node_identities:
6482
+ if target == my_network.node_identities[node]:
6483
+ targets.append(int(node))
6484
+ else:
6485
+ targets = None
6486
+
5057
6487
  try:
5058
6488
  directory = self.directory.text() if self.directory.text() else None
5059
6489
  except:
@@ -5087,7 +6517,7 @@ class ProxDialog(QDialog):
5087
6517
  my_network.nodes, _ = n3d.label_objects(my_network.nodes)
5088
6518
  if my_network.node_centroids is None:
5089
6519
  self.parent().show_centroid_dialog()
5090
- my_network.morph_proximity(search = search)
6520
+ my_network.morph_proximity(search = search, targets = targets)
5091
6521
 
5092
6522
  self.parent().load_channel(0, channel_data = my_network.nodes, data = True)
5093
6523
  elif mode == 0:
@@ -5113,10 +6543,10 @@ class ProxDialog(QDialog):
5113
6543
  return
5114
6544
 
5115
6545
  if populate:
5116
- my_network.nodes = my_network.kd_network(distance = search)
6546
+ my_network.nodes = my_network.kd_network(distance = search, targets = targets)
5117
6547
  self.parent().load_channel(0, channel_data = my_network.nodes, data = True)
5118
6548
  else:
5119
- my_network.kd_network(distance = search)
6549
+ my_network.kd_network(distance = search, targets = targets)
5120
6550
 
5121
6551
 
5122
6552
  my_network.dump(directory = directory)