nettracer3d 0.7.4__py3-none-any.whl → 0.7.5__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.
@@ -1,6 +1,6 @@
1
1
  import sys
2
2
  import networkx as nx
3
- from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
3
+ from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QGridLayout,
4
4
  QHBoxLayout, QSlider, QMenuBar, QMenu, QDialog,
5
5
  QFormLayout, QLineEdit, QPushButton, QFileDialog,
6
6
  QLabel, QComboBox, QMessageBox, QTableView, QInputDialog,
@@ -1311,22 +1311,40 @@ class ImageViewerWindow(QMainWindow):
1311
1311
  info_dict['Object Class'] = 'Node'
1312
1312
 
1313
1313
  if my_network.node_identities is not None:
1314
- info_dict['ID'] = my_network.node_identities[label]
1314
+ try:
1315
+ info_dict['ID'] = my_network.node_identities[label]
1316
+ except:
1317
+ pass
1315
1318
 
1316
1319
  if my_network.network is not None:
1317
- info_dict['Degree'] = my_network.network.degree(label)
1320
+ try:
1321
+ info_dict['Degree'] = my_network.network.degree(label)
1322
+ except:
1323
+ pass
1318
1324
 
1319
1325
  if my_network.communities is not None:
1320
- info_dict['Community'] = my_network.communities[label]
1326
+ try:
1327
+ info_dict['Community'] = my_network.communities[label]
1328
+ except:
1329
+ pass
1321
1330
 
1322
1331
  if my_network.node_centroids is not None:
1323
- info_dict['Centroid'] = my_network.node_centroids[label]
1332
+ try:
1333
+ info_dict['Centroid'] = my_network.node_centroids[label]
1334
+ except:
1335
+ pass
1324
1336
 
1325
1337
  if self.volume_dict[0] is not None:
1326
- info_dict['Volume (Scaled)'] = self.volume_dict[0][label]
1338
+ try:
1339
+ info_dict['Volume (Scaled)'] = self.volume_dict[0][label]
1340
+ except:
1341
+ pass
1327
1342
 
1328
1343
  if self.radii_dict[0] is not None:
1329
- info_dict['Max Radius (Scaled)'] = self.radii_dict[0][label]
1344
+ try:
1345
+ info_dict['Max Radius (Scaled)'] = self.radii_dict[0][label]
1346
+ except:
1347
+ pass
1330
1348
 
1331
1349
 
1332
1350
  elif sort == 'edge':
@@ -1338,13 +1356,22 @@ class ImageViewerWindow(QMainWindow):
1338
1356
  info_dict['Object Class'] = 'Edge'
1339
1357
 
1340
1358
  if my_network.edge_centroids is not None:
1341
- info_dict['Centroid'] = my_network.edge_centroids[label]
1359
+ try:
1360
+ info_dict['Centroid'] = my_network.edge_centroids[label]
1361
+ except:
1362
+ pass
1342
1363
 
1343
1364
  if self.volume_dict[1] is not None:
1344
- info_dict['Volume (Scaled)'] = self.volume_dict[1][label]
1365
+ try:
1366
+ info_dict['Volume (Scaled)'] = self.volume_dict[1][label]
1367
+ except:
1368
+ pass
1345
1369
 
1346
1370
  if self.radii_dict[1] is not None:
1347
- info_dict['~Radius (Scaled)'] = self.radii_dict[1][label]
1371
+ try:
1372
+ info_dict['~Radius (Scaled)'] = self.radii_dict[1][label]
1373
+ except:
1374
+ pass
1348
1375
 
1349
1376
  self.format_for_upperright_table(info_dict, title = f'Info on Object')
1350
1377
 
@@ -1353,7 +1380,6 @@ class ImageViewerWindow(QMainWindow):
1353
1380
 
1354
1381
 
1355
1382
 
1356
-
1357
1383
  def handle_combine(self):
1358
1384
 
1359
1385
  try:
@@ -1416,12 +1442,67 @@ class ImageViewerWindow(QMainWindow):
1416
1442
  print(f"An error has occured: {e}")
1417
1443
 
1418
1444
  def handle_seperate(self):
1419
- print("Note: I search each selected label one at a time and then split it with the ndimage.label method which uses C but still has to search the entire array each time, I may be a very slow with big operations :)")
1445
+
1446
+ import scipy.ndimage as ndi
1447
+ from scipy.sparse import csr_matrix
1448
+
1449
+ print("Note, this method is a tad slow...")
1450
+
1451
+ def separate_nontouching_objects(input_array):
1452
+ """
1453
+ Efficiently separate non-touching objects in a labeled array.
1454
+
1455
+ Parameters:
1456
+ -----------
1457
+ input_array : numpy.ndarray
1458
+ Input labeled array where each object has a unique label value > 0
1459
+
1460
+ Returns:
1461
+ --------
1462
+ output_array : numpy.ndarray
1463
+ Array with new labels where non-touching components have different labels
1464
+ """
1465
+ # Step 1: Perform connected component labeling on the entire binary mask
1466
+ binary_mask = input_array > 0
1467
+ structure = np.ones((3,) * input_array.ndim, dtype=bool) # 3x3x3 connectivity for 3D or 3x3 for 2D
1468
+ labeled_array, num_features = ndi.label(binary_mask, structure=structure)
1469
+
1470
+ # Step 2: Map the original labels to the new connected components
1471
+ # Create a sparse matrix to efficiently store label mappings
1472
+ coords = np.nonzero(input_array)
1473
+ original_values = input_array[coords]
1474
+ new_labels = labeled_array[coords]
1475
+
1476
+ # Create a mapping of (original_label, new_connected_component) pairs
1477
+ label_mapping = {}
1478
+ for orig, new in zip(original_values, new_labels):
1479
+ if orig not in label_mapping:
1480
+ label_mapping[orig] = []
1481
+ if new not in label_mapping[orig]:
1482
+ label_mapping[orig].append(new)
1483
+
1484
+ # Step 3: Create a new output array with unique labels for each connected component
1485
+ output_array = np.zeros_like(input_array)
1486
+ next_label = 1
1487
+
1488
+ # Map of (original_label, connected_component) -> new_unique_label
1489
+ unique_label_map = {}
1490
+
1491
+ for orig_label, cc_list in label_mapping.items():
1492
+ for cc in cc_list:
1493
+ unique_label_map[(orig_label, cc)] = next_label
1494
+ # Create a mask for this original label and connected component
1495
+ mask = (input_array == orig_label) & (labeled_array == cc)
1496
+ # Assign the new unique label
1497
+ output_array[mask] = next_label
1498
+ next_label += 1
1499
+
1500
+ return output_array
1501
+
1420
1502
  try:
1421
1503
  # Handle nodes
1422
1504
  if len(self.clicked_values['nodes']) > 0:
1423
1505
  self.create_highlight_overlay(node_indices=self.clicked_values['nodes'])
1424
- max_val = np.max(my_network.nodes) + 1
1425
1506
 
1426
1507
  # Create a boolean mask for highlighted values
1427
1508
  self.highlight_overlay = self.highlight_overlay != 0
@@ -1429,56 +1510,21 @@ class ImageViewerWindow(QMainWindow):
1429
1510
  # Create array with just the highlighted values
1430
1511
  highlighted_nodes = self.highlight_overlay * my_network.nodes
1431
1512
 
1432
- # Get unique values in the highlighted regions (excluding 0)
1433
- vals = list(np.unique(highlighted_nodes))
1434
- if vals[0] == 0:
1435
- del vals[0]
1436
-
1437
- # Process each value separately
1438
- for val in vals:
1439
- # Create a mask for this value
1440
- val_mask = my_network.nodes == val
1441
-
1442
- # Create an array without this value
1443
- temp = my_network.nodes - (val_mask * val)
1444
-
1445
- # Label the connected components for this value
1446
- labeled_mask, num_components = n3d.label_objects(val_mask)
1447
-
1448
- if num_components > 1:
1449
- # Set appropriate dtype based on max value
1450
- if max_val + num_components < 256:
1451
- dtype = np.uint8
1452
- elif max_val + num_components < 65536:
1453
- dtype = np.uint16
1454
- labeled_mask = labeled_mask.astype(dtype)
1455
- temp = temp.astype(dtype)
1456
- else:
1457
- dtype = np.uint32
1458
- labeled_mask = labeled_mask.astype(dtype)
1459
- temp = temp.astype(dtype)
1460
-
1461
- # Add new labels to the temporary array
1462
- mask_nonzero = labeled_mask != 0
1463
- labeled_mask = labeled_mask + max_val - 1 # -1 because we'll restore the first component
1464
- labeled_mask = labeled_mask * mask_nonzero
1465
-
1466
- # Restore original value for first component
1467
- first_component = labeled_mask == max_val
1468
- labeled_mask = labeled_mask - (first_component * (max_val - val))
1469
-
1470
- # Add labeled components back to the array
1471
- my_network.nodes = temp + labeled_mask
1472
-
1473
- # Update max value for next iteration
1474
- max_val += num_components - 1 # -1 because we kept one original label
1513
+ # Get non-highlighted part of the array
1514
+ non_highlighted = my_network.nodes * (~self.highlight_overlay)
1515
+
1516
+ # Process highlighted part
1517
+ processed_highlights = separate_nontouching_objects(highlighted_nodes)
1518
+
1519
+ # Combine back with non-highlighted parts
1520
+ my_network.nodes = non_highlighted + processed_highlights
1475
1521
 
1476
1522
  self.load_channel(0, my_network.nodes, True)
1477
1523
 
1478
1524
  # Handle edges
1479
1525
  if len(self.clicked_values['edges']) > 0:
1526
+
1480
1527
  self.create_highlight_overlay(edge_indices=self.clicked_values['edges'])
1481
- max_val = np.max(my_network.edges) + 1
1482
1528
 
1483
1529
  # Create a boolean mask for highlighted values
1484
1530
  self.highlight_overlay = self.highlight_overlay != 0
@@ -1486,52 +1532,17 @@ class ImageViewerWindow(QMainWindow):
1486
1532
  # Create array with just the highlighted values
1487
1533
  highlighted_edges = self.highlight_overlay * my_network.edges
1488
1534
 
1489
- # Get unique values in the highlighted regions (excluding 0)
1490
- vals = list(np.unique(highlighted_edges))
1491
- if vals[0] == 0:
1492
- del vals[0]
1493
-
1494
- # Process each value separately
1495
- for val in vals:
1496
- # Create a mask for this value
1497
- val_mask = my_network.edges == val
1498
-
1499
- # Create an array without this value
1500
- temp = my_network.edges - (val_mask * val)
1501
-
1502
- # Label the connected components for this value
1503
- labeled_mask, num_components = n3d.label_objects(val_mask)
1504
-
1505
- if num_components > 1:
1506
- # Set appropriate dtype based on max value
1507
- if max_val + num_components < 256:
1508
- dtype = np.uint8
1509
- elif max_val + num_components < 65536:
1510
- dtype = np.uint16
1511
- labeled_mask = labeled_mask.astype(dtype)
1512
- temp = temp.astype(dtype)
1513
- else:
1514
- dtype = np.uint32
1515
- labeled_mask = labeled_mask.astype(dtype)
1516
- temp = temp.astype(dtype)
1517
-
1518
- # Add new labels to the temporary array
1519
- mask_nonzero = labeled_mask != 0
1520
- labeled_mask = labeled_mask + max_val - 1 # -1 because we'll restore the first component
1521
- labeled_mask = labeled_mask * mask_nonzero
1522
-
1523
- # Restore original value for first component
1524
- first_component = labeled_mask == max_val
1525
- labeled_mask = labeled_mask - (first_component * (max_val - val))
1526
-
1527
- # Add labeled components back to the array
1528
- my_network.edges = temp + labeled_mask
1529
-
1530
- # Update max value for next iteration
1531
- max_val += num_components - 1 # -1 because we kept one original label
1535
+ # Get non-highlighted part of the array
1536
+ non_highlighted = my_network.edges * (~self.highlight_overlay)
1532
1537
 
1533
- self.load_channel(1, my_network.edges, True)
1538
+ # Process highlighted part
1539
+ processed_highlights = separate_nontouching_objects(highlighted_edges)
1534
1540
 
1541
+ # Combine back with non-highlighted parts
1542
+ my_network.edges = non_highlighted + processed_highlights
1543
+
1544
+ self.load_channel(1, my_network.edges, True)
1545
+
1535
1546
  self.highlight_overlay = None
1536
1547
  self.update_display()
1537
1548
  print("Network is not updated automatically, please recompute if necessary. Identities are not automatically updated.")
@@ -1541,8 +1552,6 @@ class ImageViewerWindow(QMainWindow):
1541
1552
 
1542
1553
 
1543
1554
 
1544
-
1545
-
1546
1555
  def handle_delete(self):
1547
1556
 
1548
1557
  try:
@@ -2684,6 +2693,8 @@ class ImageViewerWindow(QMainWindow):
2684
2693
  stats_menu = analysis_menu.addMenu("Stats")
2685
2694
  allstats_action = stats_menu.addAction("Calculate Generic Network Stats")
2686
2695
  allstats_action.triggered.connect(self.stats)
2696
+ histos_action = stats_menu.addAction("Calculate Generic Network Histograms")
2697
+ histos_action.triggered.connect(self.histos)
2687
2698
  radial_action = stats_menu.addAction("Radial Distribution Analysis")
2688
2699
  radial_action.triggered.connect(self.show_radial_dialog)
2689
2700
  degree_dist_action = stats_menu.addAction("Degree Distribution Analysis")
@@ -2822,6 +2833,104 @@ class ImageViewerWindow(QMainWindow):
2822
2833
  except Exception as e:
2823
2834
  print(f"Error finding stats: {e}")
2824
2835
 
2836
+ def histos(self):
2837
+
2838
+ """from networkx documentation"""
2839
+
2840
+ G = my_network.network
2841
+
2842
+ shortest_path_lengths = dict(nx.all_pairs_shortest_path_length(G))
2843
+ diameter = max(nx.eccentricity(G, sp=shortest_path_lengths).values())
2844
+ # We know the maximum shortest path length (the diameter), so create an array
2845
+ # to store values from 0 up to (and including) diameter
2846
+ path_lengths = np.zeros(diameter + 1, dtype=int)
2847
+
2848
+
2849
+
2850
+ # Extract the frequency of shortest path lengths between two nodes
2851
+ for pls in shortest_path_lengths.values():
2852
+ pl, cnts = np.unique(list(pls.values()), return_counts=True)
2853
+ path_lengths[pl] += cnts
2854
+
2855
+ # Express frequency distribution as a percentage (ignoring path lengths of 0)
2856
+ freq_percent = 100 * path_lengths[1:] / path_lengths[1:].sum()
2857
+
2858
+ # Plot the frequency distribution (ignoring path lengths of 0) as a percentage
2859
+ fig, ax = plt.subplots(figsize=(15, 8))
2860
+ ax.bar(np.arange(1, diameter + 1), height=freq_percent)
2861
+ ax.set_title(
2862
+ "Distribution of shortest path length in G", fontdict={"size": 35}, loc="center"
2863
+ )
2864
+ ax.set_xlabel("Shortest Path Length", fontdict={"size": 22})
2865
+ ax.set_ylabel("Frequency (%)", fontdict={"size": 22})
2866
+
2867
+ plt.show()
2868
+ freq_dict = {freq: length for length, freq in enumerate(freq_percent, start=1)}
2869
+ self.format_for_upperright_table(freq_dict, metric='Frequency (%)', value='Shortest Path Length', title="Distribution of shortest path length in G")
2870
+
2871
+ degree_centrality = nx.centrality.degree_centrality(G)
2872
+ plt.figure(figsize=(15, 8))
2873
+ plt.hist(degree_centrality.values(), bins=25)
2874
+ plt.xticks(ticks=[0, 0.025, 0.05, 0.1, 0.15, 0.2]) # set the x axis ticks
2875
+ plt.title("Degree Centrality Histogram ", fontdict={"size": 35}, loc="center")
2876
+ plt.xlabel("Degree Centrality", fontdict={"size": 20})
2877
+ plt.ylabel("Counts", fontdict={"size": 20})
2878
+ plt.show()
2879
+ self.format_for_upperright_table(degree_centrality, metric='Node', value='Degree Centrality', title="Degree Centrality Table")
2880
+
2881
+
2882
+ betweenness_centrality = nx.centrality.betweenness_centrality(
2883
+ G
2884
+ )
2885
+ plt.figure(figsize=(15, 8))
2886
+ plt.hist(betweenness_centrality.values(), bins=100)
2887
+ plt.xticks(ticks=[0, 0.02, 0.1, 0.2, 0.3, 0.4, 0.5]) # set the x axis ticks
2888
+ plt.title("Betweenness Centrality Histogram ", fontdict={"size": 35}, loc="center")
2889
+ plt.xlabel("Betweenness Centrality", fontdict={"size": 20})
2890
+ plt.ylabel("Counts", fontdict={"size": 20})
2891
+ plt.show()
2892
+ self.format_for_upperright_table(betweenness_centrality, metric='Node', value='Betweenness Centrality', title="Betweenness Centrality Table")
2893
+
2894
+
2895
+ closeness_centrality = nx.centrality.closeness_centrality(
2896
+ G
2897
+ )
2898
+ plt.figure(figsize=(15, 8))
2899
+ plt.hist(closeness_centrality.values(), bins=60)
2900
+ plt.title("Closeness Centrality Histogram ", fontdict={"size": 35}, loc="center")
2901
+ plt.xlabel("Closeness Centrality", fontdict={"size": 20})
2902
+ plt.ylabel("Counts", fontdict={"size": 20})
2903
+ plt.show()
2904
+ self.format_for_upperright_table(closeness_centrality, metric='Node', value='Closeness Centrality', title="Closeness Centrality Table")
2905
+
2906
+
2907
+ eigenvector_centrality = nx.centrality.eigenvector_centrality(
2908
+ G
2909
+ )
2910
+ plt.figure(figsize=(15, 8))
2911
+ plt.hist(eigenvector_centrality.values(), bins=60)
2912
+ plt.xticks(ticks=[0, 0.01, 0.02, 0.04, 0.06, 0.08]) # set the x axis ticks
2913
+ plt.title("Eigenvector Centrality Histogram ", fontdict={"size": 35}, loc="center")
2914
+ plt.xlabel("Eigenvector Centrality", fontdict={"size": 20})
2915
+ plt.ylabel("Counts", fontdict={"size": 20})
2916
+ plt.show()
2917
+ self.format_for_upperright_table(eigenvector_centrality, metric='Node', value='Eigenvector Centrality', title="Eigenvector Centrality Table")
2918
+
2919
+
2920
+
2921
+ clusters = nx.clustering(G)
2922
+ plt.figure(figsize=(15, 8))
2923
+ plt.hist(clusters.values(), bins=50)
2924
+ plt.title("Clustering Coefficient Histogram ", fontdict={"size": 35}, loc="center")
2925
+ plt.xlabel("Clustering Coefficient", fontdict={"size": 20})
2926
+ plt.ylabel("Counts", fontdict={"size": 20})
2927
+ plt.show()
2928
+ self.format_for_upperright_table(clusters, metric='Node', value='Clustering Coefficient', title="Clustering Coefficient Table")
2929
+
2930
+ bridges = list(nx.bridges(G))
2931
+ self.format_for_upperright_table(bridges, metric = 'Node Pair', title="Bridges")
2932
+
2933
+
2825
2934
  def volumes(self):
2826
2935
 
2827
2936
 
@@ -5756,7 +5865,7 @@ class NetShowDialog(QDialog):
5756
5865
 
5757
5866
  # Add mode selection dropdown
5758
5867
  self.mode_selector = QComboBox()
5759
- self.mode_selector.addItems(["Default", "Community Coded (Uses current communities or label propogation by default if no communities have been found)", "Community Coded (Redo Label Propogation Algorithm)", "Community Coded (Redo Louvain Algorithm)", "Node ID Coded"])
5868
+ self.mode_selector.addItems(["Default", "Community Coded", "Node ID Coded"])
5760
5869
  self.mode_selector.setCurrentIndex(0) # Default to Mode 1
5761
5870
  layout.addRow("Execution Mode:", self.mode_selector)
5762
5871
 
@@ -5778,6 +5887,10 @@ class NetShowDialog(QDialog):
5778
5887
 
5779
5888
  def show_network(self):
5780
5889
  # Get parameters and run analysis
5890
+ if my_network.communities is None:
5891
+ self.parent().show_partition_dialog()
5892
+ if my_network.communities is None:
5893
+ return
5781
5894
  geo = self.geo_layout.isChecked()
5782
5895
  if geo:
5783
5896
  if my_network.node_centroids is None:
@@ -5795,12 +5908,6 @@ class NetShowDialog(QDialog):
5795
5908
  my_network.show_communities_flex(geometric=geo, directory = directory, weighted = weighted, partition = my_network.communities)
5796
5909
  self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'CommunityID')
5797
5910
  elif accepted_mode == 2:
5798
- my_network.show_communities_flex(geometric=geo, directory = directory, weighted = weighted, partition = my_network.communities, style = 0)
5799
- self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'CommunityID')
5800
- elif accepted_mode ==3:
5801
- my_network.show_communities_flex(geometric=geo, directory = directory, weighted = weighted, partition = my_network.communities, style = 1)
5802
- self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'CommunityID')
5803
- elif accepted_mode == 4:
5804
5911
  my_network.show_identity_network(geometric=geo, directory = directory)
5805
5912
 
5806
5913
  self.accept()
@@ -5836,6 +5943,9 @@ class PartitionDialog(QDialog):
5836
5943
  self.stats.setChecked(True)
5837
5944
  layout.addRow("Community Stats:", self.stats)
5838
5945
 
5946
+ self.seed = QLineEdit("")
5947
+ layout.addRow("Seed (int):", self.seed)
5948
+
5839
5949
  # Add Run button
5840
5950
  run_button = QPushButton("Partition")
5841
5951
  run_button.clicked.connect(self.partition)
@@ -5847,10 +5957,16 @@ class PartitionDialog(QDialog):
5847
5957
  weighted = self.weighted.isChecked()
5848
5958
  dostats = self.stats.isChecked()
5849
5959
 
5960
+ try:
5961
+ seed = int(self.seed.text()) if self.seed.text() else None
5962
+ except:
5963
+ seed = None
5964
+
5965
+
5850
5966
  my_network.communities = None
5851
5967
 
5852
5968
  try:
5853
- stats = my_network.community_partition(weighted = weighted, style = accepted_mode, dostats = dostats)
5969
+ stats = my_network.community_partition(weighted = weighted, style = accepted_mode, dostats = dostats, seed = seed)
5854
5970
  print(f"Discovered communities: {my_network.communities}")
5855
5971
 
5856
5972
  self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'CommunityID', title = 'Community Partition')
@@ -6565,21 +6681,21 @@ class MotherDialog(QDialog):
6565
6681
 
6566
6682
  overlay = self.overlay.isChecked()
6567
6683
 
6684
+ if my_network.communities is None:
6685
+ self.parent().show_partition_dialog()
6686
+ if my_network.communities is None:
6687
+ return
6688
+
6568
6689
  if my_network.node_centroids is None:
6569
6690
  self.parent().show_centroid_dialog()
6570
6691
  if my_network.node_centroids is None:
6571
6692
  print("Error finding centroids")
6572
6693
  overlay = False
6573
6694
 
6574
- if my_network.communities is None:
6575
- self.parent().show_partition_dialog()
6576
- if my_network.communities is None:
6577
- return
6578
-
6579
6695
  if not overlay:
6580
- G = my_network.isolate_mothers(self, louvain = my_network.communities, ret_nodes = True, called = True)
6696
+ G = my_network.isolate_mothers(self, ret_nodes = True, called = True)
6581
6697
  else:
6582
- G, result = my_network.isolate_mothers(self, louvain = my_network.communities, ret_nodes = False, called = True)
6698
+ G, result = my_network.isolate_mothers(self, ret_nodes = False, called = True)
6583
6699
  self.parent().load_channel(2, channel_data = result, data = True)
6584
6700
 
6585
6701
  degree_dict = {}
@@ -7513,31 +7629,41 @@ class MachineWindow(QMainWindow):
7513
7629
 
7514
7630
  def save_model(self):
7515
7631
 
7516
- filename, _ = QFileDialog.getSaveFileName(
7517
- self,
7518
- f"Save Model As",
7519
- "", # Default directory
7520
- "numpy data (*.npz);;All Files (*)" # File type filter
7521
- )
7522
-
7523
- if filename: # Only proceed if user didn't cancel
7524
- # If user didn't type an extension, add .tif
7525
- if not filename.endswith(('.npz')):
7526
- filename += '.npz'
7632
+ try:
7633
+
7634
+ filename, _ = QFileDialog.getSaveFileName(
7635
+ self,
7636
+ f"Save Model As",
7637
+ "", # Default directory
7638
+ "numpy data (*.npz);;All Files (*)" # File type filter
7639
+ )
7640
+
7641
+ if filename: # Only proceed if user didn't cancel
7642
+ # If user didn't type an extension, add .tif
7643
+ if not filename.endswith(('.npz')):
7644
+ filename += '.npz'
7527
7645
 
7528
- self.segmenter.save_model(filename, self.parent().channel_data[2])
7646
+ self.segmenter.save_model(filename, self.parent().channel_data[2])
7647
+
7648
+ except Exception as e:
7649
+ print(f"Error saving model: {e}")
7529
7650
 
7530
7651
  def load_model(self):
7531
7652
 
7532
- filename, _ = QFileDialog.getOpenFileName(
7533
- self,
7534
- f"Load Model",
7535
- "",
7536
- "numpy data (*.npz)"
7537
- )
7653
+ try:
7538
7654
 
7539
- self.segmenter.load_model(filename)
7540
- self.trained = True
7655
+ filename, _ = QFileDialog.getOpenFileName(
7656
+ self,
7657
+ f"Load Model",
7658
+ "",
7659
+ "numpy data (*.npz)"
7660
+ )
7661
+
7662
+ self.segmenter.load_model(filename)
7663
+ self.trained = True
7664
+
7665
+ except Exception as e:
7666
+ print(f"Error loading model: {e}")
7541
7667
 
7542
7668
  def toggle_two(self):
7543
7669
  if self.two.isChecked():
@@ -7791,8 +7917,6 @@ class MachineWindow(QMainWindow):
7791
7917
  traceback.print_exc()
7792
7918
 
7793
7919
  def segmentation_finished(self):
7794
- if not self.use_two:
7795
- print("Segmentation completed")
7796
7920
 
7797
7921
  current_xlim = self.parent().ax.get_xlim()
7798
7922
  current_ylim = self.parent().ax.get_ylim()
@@ -7994,7 +8118,7 @@ class SegmentationWorker(QThread):
7994
8118
  current_time - self.last_update >= self.update_interval):
7995
8119
  self.chunk_processed.emit()
7996
8120
  self.chunks_since_update = 0
7997
- self.last_update = current_time
8121
+ self.last_update = current_time
7998
8122
 
7999
8123
  self.finished.emit()
8000
8124
 
@@ -9132,65 +9256,110 @@ class CentroidNodeDialog(QDialog):
9132
9256
 
9133
9257
  class GenNodesDialog(QDialog):
9134
9258
 
9135
- def __init__(self, parent=None, down_factor = None, called = False):
9259
+ def __init__(self, parent=None, down_factor=None, called=False):
9136
9260
  super().__init__(parent)
9137
9261
  self.setWindowTitle("Create Nodes from Edge Vertices")
9138
9262
  self.setModal(True)
9139
-
9140
- layout = QFormLayout(self)
9263
+
9264
+ # Main layout
9265
+ main_layout = QVBoxLayout(self)
9141
9266
  self.called = called
9142
-
9143
- #self.directory = QLineEdit()
9144
- #self.directory.setPlaceholderText("Leave empty to save in active dir")
9145
- #layout.addRow("Output Directory:", self.directory)
9146
-
9267
+
9268
+ # Set down_factor and cubic
9147
9269
  if not down_factor:
9148
9270
  down_factor = None
9271
+
9149
9272
  if down_factor is None:
9273
+ # --- Processing Options Group ---
9274
+ process_group = QGroupBox("Processing Options")
9275
+ process_layout = QGridLayout()
9276
+
9277
+ # Downsample factor
9150
9278
  self.down_factor = QLineEdit("0")
9151
- layout.addRow("Downsample Factor (Speeds up calculation at the cost of fidelity):", self.down_factor)
9279
+ process_layout.addWidget(QLabel("Downsample Factor (Speeds up calculation at the cost of fidelity):"), 0, 0)
9280
+ process_layout.addWidget(self.down_factor, 0, 1)
9281
+
9282
+ # Cubic checkbox
9152
9283
  self.cubic = QPushButton("Cubic Downsample")
9153
9284
  self.cubic.setCheckable(True)
9154
9285
  self.cubic.setChecked(False)
9155
- layout.addRow("(if downsampling): Use cubic downsample? (Slower but can preserve structure better)", self.cubic)
9286
+ process_layout.addWidget(QLabel("Use cubic downsample? (Slower but preserves structure better):"), 1, 0)
9287
+ process_layout.addWidget(self.cubic, 1, 1)
9288
+
9289
+ # Fast dilation checkbox
9290
+ self.fast_dil = QPushButton("Fast-Dil")
9291
+ self.fast_dil.setCheckable(True)
9292
+ self.fast_dil.setChecked(True)
9293
+ process_layout.addWidget(QLabel("Use Fast Dilation (Higher speed, less accurate with large search regions):"), 2, 0)
9294
+ process_layout.addWidget(self.fast_dil, 2, 1)
9295
+
9296
+ process_group.setLayout(process_layout)
9297
+ main_layout.addWidget(process_group)
9156
9298
  else:
9157
9299
  self.down_factor = down_factor[0]
9158
9300
  self.cubic = down_factor[1]
9159
-
9301
+
9302
+ # Fast dilation checkbox (still needed even if down_factor is provided)
9303
+ process_group = QGroupBox("Processing Options")
9304
+ process_layout = QGridLayout()
9305
+
9306
+ self.fast_dil = QPushButton("Fast-Dil")
9307
+ self.fast_dil.setCheckable(True)
9308
+ self.fast_dil.setChecked(True)
9309
+ process_layout.addWidget(QLabel("Use Fast Dilation (Higher speed, less accurate with large search regions):"), 0, 0)
9310
+ process_layout.addWidget(self.fast_dil, 0, 1)
9311
+
9312
+ process_group.setLayout(process_layout)
9313
+ main_layout.addWidget(process_group)
9314
+
9315
+ # --- Recommended Corrections Group ---
9316
+ rec_group = QGroupBox("Recommended Corrections")
9317
+ rec_layout = QGridLayout()
9318
+
9319
+ # Branch removal
9160
9320
  self.branch_removal = QLineEdit("0")
9161
- layout.addRow("Skeleton Voxel Branch Length to Remove (int) (Compensates for spines off medial axis):", self.branch_removal)
9162
-
9163
- self.max_vol = QLineEdit("0")
9164
- layout.addRow("Maximum Voxel Volume of Vertices to Retain (int - Compensates for skeleton looping - occurs before any node merging - the smallest objects are always 27 voxels):", self.max_vol)
9165
-
9166
- self.comp_dil = QLineEdit("0")
9167
- layout.addRow("Voxel distance to merge nearby nodes (Int - compensates for multi-branch identification along thick branch regions):", self.comp_dil)
9168
-
9169
- self.fast_dil = QPushButton("Fast-Dil")
9170
- self.fast_dil.setCheckable(True)
9171
- self.fast_dil.setChecked(True)
9172
- layout.addRow("(If using above) Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fast_dil)
9173
-
9174
- # auto checkbox (default True)
9321
+ rec_layout.addWidget(QLabel("Skeleton Voxel Branch Length to Remove (Compensates for spines):"), 0, 0)
9322
+ rec_layout.addWidget(self.branch_removal, 0, 1)
9323
+
9324
+ # Auto checkbox
9175
9325
  self.auto = QPushButton("Auto")
9176
9326
  self.auto.setCheckable(True)
9177
9327
  self.auto.setChecked(True)
9178
- layout.addRow("Attempt to Auto Correct Skeleton Looping:", self.auto)
9179
-
9180
-
9181
- # retain checkbox (default True)
9328
+ rec_layout.addWidget(QLabel("Attempt to Auto Correct Skeleton Looping:"), 1, 0)
9329
+ rec_layout.addWidget(self.auto, 1, 1)
9330
+
9331
+ rec_group.setLayout(rec_layout)
9332
+ main_layout.addWidget(rec_group)
9333
+
9334
+ # --- Optional Corrections Group ---
9335
+ opt_group = QGroupBox("Optional Corrections")
9336
+ opt_layout = QGridLayout()
9337
+
9338
+ # Max volume
9339
+ self.max_vol = QLineEdit("0")
9340
+ opt_layout.addWidget(QLabel("Maximum Voxel Volume to Retain (Compensates for skeleton looping):"), 0, 0)
9341
+ opt_layout.addWidget(self.max_vol, 0, 1)
9342
+
9343
+ # Component dilation
9344
+ self.comp_dil = QLineEdit("0")
9345
+ opt_layout.addWidget(QLabel("Voxel distance to merge nearby nodes (Compensates for multi-branch regions):"), 1, 0)
9346
+ opt_layout.addWidget(self.comp_dil, 1, 1)
9347
+
9348
+ opt_group.setLayout(opt_layout)
9349
+ main_layout.addWidget(opt_group)
9350
+
9351
+ # Set retain variable but don't add to layout
9182
9352
  if not called:
9183
9353
  self.retain = QPushButton("Retain")
9184
9354
  self.retain.setCheckable(True)
9185
9355
  self.retain.setChecked(True)
9186
- #layout.addRow("Retain Original Edges? (Will be moved to overlay 2):", self.retain)
9187
9356
  else:
9188
9357
  self.retain = False
9189
-
9358
+
9190
9359
  # Add Run button
9191
9360
  run_button = QPushButton("Run Node Generation")
9192
9361
  run_button.clicked.connect(self.run_gennodes)
9193
- layout.addRow(run_button)
9362
+ main_layout.addWidget(run_button)
9194
9363
 
9195
9364
  def run_gennodes(self):
9196
9365
 
@@ -9311,49 +9480,84 @@ class BranchDialog(QDialog):
9311
9480
  super().__init__(parent)
9312
9481
  self.setWindowTitle("Label Branches (of edges)")
9313
9482
  self.setModal(True)
9314
-
9315
- layout = QFormLayout(self)
9316
-
9317
- # Nodes checkbox (default True)
9318
- self.nodes = QPushButton("Generate Nodes")
9319
- self.nodes.setCheckable(True)
9320
- self.nodes.setChecked(True)
9321
- layout.addRow("Generate nodes from edges? (Skip if already completed - presumes your edge skeleton from generate nodes is in Edges and that your original Edges are in Overlay 2):", self.nodes)
9322
-
9323
- # GPU checkbox (default False)
9324
- self.GPU = QPushButton("GPU")
9325
- self.GPU.setCheckable(True)
9326
- self.GPU.setChecked(False)
9327
- layout.addRow("Use GPU (Note this may need to temporarily downsample your large images which may simplify outputs - Only memory errors but not permission errors for accessing VRAM are handled by default - CPU will never try to downsample):", self.GPU)
9328
-
9329
- # Branch Fix checkbox (default False)
9483
+
9484
+ # Main layout
9485
+ main_layout = QVBoxLayout(self)
9486
+
9487
+ # --- Correction Options Group ---
9488
+ correction_group = QGroupBox("Correction Options")
9489
+ correction_layout = QGridLayout()
9490
+
9491
+ # Branch Fix checkbox
9330
9492
  self.fix = QPushButton("Auto-Correct Branches")
9331
9493
  self.fix.setCheckable(True)
9332
9494
  self.fix.setChecked(False)
9333
- layout.addRow("Attempt to auto-correct branch labels:", self.fix)
9334
-
9495
+ correction_layout.addWidget(QLabel("Attempt to auto-correct branch labels:"), 0, 0)
9496
+ correction_layout.addWidget(self.fix, 0, 1)
9497
+
9498
+ # Fix value
9335
9499
  self.fix_val = QLineEdit('4')
9336
- layout.addRow("If checked above - Avg Degree of Nearby Branch Communities to Merge (Attempt to fix branch labeling - try 4 to 6 to start or leave empty):", self.fix_val)
9337
-
9500
+ correction_layout.addWidget(QLabel("Avg Degree of Nearby Branch Communities to Merge (4-6 recommended):"), 1, 0)
9501
+ correction_layout.addWidget(self.fix_val, 1, 1)
9502
+
9503
+ # Seed
9504
+ self.seed = QLineEdit('')
9505
+ correction_layout.addWidget(QLabel("Random seed for auto correction (int - optional):"), 2, 0)
9506
+ correction_layout.addWidget(self.seed, 2, 1)
9507
+
9508
+ correction_group.setLayout(correction_layout)
9509
+ main_layout.addWidget(correction_group)
9510
+
9511
+ # --- Processing Options Group ---
9512
+ processing_group = QGroupBox("Processing Options")
9513
+ processing_layout = QGridLayout()
9514
+
9515
+ # Downsample factor
9338
9516
  self.down_factor = QLineEdit("0")
9339
- layout.addRow("Internal downsample (will have to recompute nodes)?:", self.down_factor)
9340
-
9341
- # cubic checkbox (default False)
9517
+ processing_layout.addWidget(QLabel("Internal downsample factor (will recompute nodes):"), 0, 0)
9518
+ processing_layout.addWidget(self.down_factor, 0, 1)
9519
+
9520
+ # Cubic checkbox
9342
9521
  self.cubic = QPushButton("Cubic Downsample")
9343
9522
  self.cubic.setCheckable(True)
9344
9523
  self.cubic.setChecked(False)
9345
- layout.addRow("(if downsampling): Use cubic downsample? (Slower but can preserve structure better)", self.cubic)
9346
-
9524
+ processing_layout.addWidget(QLabel("Use cubic downsample? (Slower but preserves structure better):"), 1, 0)
9525
+ processing_layout.addWidget(self.cubic, 1, 1)
9526
+
9527
+ processing_group.setLayout(processing_layout)
9528
+ main_layout.addWidget(processing_group)
9529
+
9530
+ # --- Misc Options Group ---
9531
+ misc_group = QGroupBox("Misc Options")
9532
+ misc_layout = QGridLayout()
9533
+
9534
+ # Nodes checkbox
9535
+ self.nodes = QPushButton("Generate Nodes")
9536
+ self.nodes.setCheckable(True)
9537
+ self.nodes.setChecked(True)
9538
+ misc_layout.addWidget(QLabel("Generate nodes from edges? (Skip if already completed):"), 0, 0)
9539
+ misc_layout.addWidget(self.nodes, 0, 1)
9540
+
9541
+ # GPU checkbox
9542
+ self.GPU = QPushButton("GPU")
9543
+ self.GPU.setCheckable(True)
9544
+ self.GPU.setChecked(False)
9545
+ misc_layout.addWidget(QLabel("Use GPU (May downsample large images):"), 1, 0)
9546
+ misc_layout.addWidget(self.GPU, 1, 1)
9547
+
9548
+ misc_group.setLayout(misc_layout)
9549
+ main_layout.addWidget(misc_group)
9550
+
9347
9551
  # Add Run button
9348
9552
  run_button = QPushButton("Run Branch Label")
9349
9553
  run_button.clicked.connect(self.branch_label)
9350
- layout.addRow(run_button)
9554
+ main_layout.addWidget(run_button)
9351
9555
 
9352
- if self.parent().channel_data[0] is not None:
9556
+ if self.parent().channel_data[0] is not None or self.parent().channel_data[3] is not None:
9353
9557
  QMessageBox.critical(
9354
9558
  self,
9355
9559
  "Alert",
9356
- "The nodes channel will be intermittently overwritten when running this method"
9560
+ "The nodes and overlay 2 channels will be intermittently overwritten when running this method"
9357
9561
  )
9358
9562
 
9359
9563
  def branch_label(self):
@@ -9370,8 +9574,7 @@ class BranchDialog(QDialog):
9370
9574
  cubic = self.cubic.isChecked()
9371
9575
  fix = self.fix.isChecked()
9372
9576
  fix_val = float(self.fix_val.text()) if self.fix_val.text() else None
9373
-
9374
-
9577
+ seed = int(self.seed.text()) if self.seed.text() else None
9375
9578
 
9376
9579
  original_shape = my_network.edges.shape
9377
9580
  original_array = copy.deepcopy(my_network.edges)
@@ -9392,7 +9595,7 @@ class BranchDialog(QDialog):
9392
9595
 
9393
9596
  temp_network.morph_proximity(search = [3,3], fastdil = True) #Detect network of nearby branches
9394
9597
 
9395
- temp_network.community_partition(weighted = False, style = 1, dostats = False) #Find communities with louvain, unweighted params
9598
+ temp_network.community_partition(weighted = False, style = 1, dostats = False, seed = seed) #Find communities with louvain, unweighted params
9396
9599
 
9397
9600
  targs = n3d.fix_branches(temp_network.nodes, temp_network.network, temp_network.communities, fix_val)
9398
9601