nettracer3d 0.7.4__py3-none-any.whl → 0.7.6__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.

Potentially problematic release.


This version of nettracer3d might be problematic. Click here for more details.

@@ -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, max_val = 0):
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 + max_val
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,26 @@ 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
+ if (highlighted_nodes==non_highlighted).all():
1517
+ max_val = 0
1518
+ else:
1519
+ max_val = np.max(non_highlighted)
1520
+
1521
+ # Process highlighted part
1522
+ processed_highlights = separate_nontouching_objects(highlighted_nodes, max_val)
1523
+
1524
+ # Combine back with non-highlighted parts
1525
+ my_network.nodes = non_highlighted + processed_highlights
1475
1526
 
1476
1527
  self.load_channel(0, my_network.nodes, True)
1477
1528
 
1478
1529
  # Handle edges
1479
1530
  if len(self.clicked_values['edges']) > 0:
1531
+
1480
1532
  self.create_highlight_overlay(edge_indices=self.clicked_values['edges'])
1481
- max_val = np.max(my_network.edges) + 1
1482
1533
 
1483
1534
  # Create a boolean mask for highlighted values
1484
1535
  self.highlight_overlay = self.highlight_overlay != 0
@@ -1486,63 +1537,31 @@ class ImageViewerWindow(QMainWindow):
1486
1537
  # Create array with just the highlighted values
1487
1538
  highlighted_edges = self.highlight_overlay * my_network.edges
1488
1539
 
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
1540
+ # Get non-highlighted part of the array
1541
+ non_highlighted = my_network.edges * (~self.highlight_overlay)
1542
+
1543
+ if (highlighted_nodes==non_highlighted).all():
1544
+ max_val = 0
1545
+ else:
1546
+ max_val = np.max(non_highlighted)
1532
1547
 
1533
- self.load_channel(1, my_network.edges, True)
1548
+ # Process highlighted part
1549
+ processed_highlights = separate_nontouching_objects(highlighted_edges, max_val)
1534
1550
 
1551
+ # Combine back with non-highlighted parts
1552
+ my_network.edges = non_highlighted + processed_highlights
1553
+
1554
+ self.load_channel(1, my_network.edges, True)
1555
+
1535
1556
  self.highlight_overlay = None
1536
1557
  self.update_display()
1537
- print("Network is not updated automatically, please recompute if necessary. Identities are not automatically updated.")
1558
+ print("Network is not updated automatically, please recompute if necessary - this method has a high chance of disrupting the network. Identities are not automatically updated.")
1538
1559
  self.show_centroid_dialog()
1539
1560
  except Exception as e:
1540
1561
  print(f"Error separating: {e}")
1541
1562
 
1542
1563
 
1543
1564
 
1544
-
1545
-
1546
1565
  def handle_delete(self):
1547
1566
 
1548
1567
  try:
@@ -2684,6 +2703,8 @@ class ImageViewerWindow(QMainWindow):
2684
2703
  stats_menu = analysis_menu.addMenu("Stats")
2685
2704
  allstats_action = stats_menu.addAction("Calculate Generic Network Stats")
2686
2705
  allstats_action.triggered.connect(self.stats)
2706
+ histos_action = stats_menu.addAction("Calculate Generic Network Histograms")
2707
+ histos_action.triggered.connect(self.histos)
2687
2708
  radial_action = stats_menu.addAction("Radial Distribution Analysis")
2688
2709
  radial_action.triggered.connect(self.show_radial_dialog)
2689
2710
  degree_dist_action = stats_menu.addAction("Degree Distribution Analysis")
@@ -2822,6 +2843,108 @@ class ImageViewerWindow(QMainWindow):
2822
2843
  except Exception as e:
2823
2844
  print(f"Error finding stats: {e}")
2824
2845
 
2846
+ def histos(self):
2847
+
2848
+ """from networkx documentation"""
2849
+
2850
+ try:
2851
+
2852
+ G = my_network.network
2853
+
2854
+ shortest_path_lengths = dict(nx.all_pairs_shortest_path_length(G))
2855
+ diameter = max(nx.eccentricity(G, sp=shortest_path_lengths).values())
2856
+ # We know the maximum shortest path length (the diameter), so create an array
2857
+ # to store values from 0 up to (and including) diameter
2858
+ path_lengths = np.zeros(diameter + 1, dtype=int)
2859
+
2860
+
2861
+
2862
+ # Extract the frequency of shortest path lengths between two nodes
2863
+ for pls in shortest_path_lengths.values():
2864
+ pl, cnts = np.unique(list(pls.values()), return_counts=True)
2865
+ path_lengths[pl] += cnts
2866
+
2867
+ # Express frequency distribution as a percentage (ignoring path lengths of 0)
2868
+ freq_percent = 100 * path_lengths[1:] / path_lengths[1:].sum()
2869
+
2870
+ # Plot the frequency distribution (ignoring path lengths of 0) as a percentage
2871
+ fig, ax = plt.subplots(figsize=(15, 8))
2872
+ ax.bar(np.arange(1, diameter + 1), height=freq_percent)
2873
+ ax.set_title(
2874
+ "Distribution of shortest path length in G", fontdict={"size": 35}, loc="center"
2875
+ )
2876
+ ax.set_xlabel("Shortest Path Length", fontdict={"size": 22})
2877
+ ax.set_ylabel("Frequency (%)", fontdict={"size": 22})
2878
+
2879
+ plt.show()
2880
+ freq_dict = {freq: length for length, freq in enumerate(freq_percent, start=1)}
2881
+ self.format_for_upperright_table(freq_dict, metric='Frequency (%)', value='Shortest Path Length', title="Distribution of shortest path length in G")
2882
+
2883
+ degree_centrality = nx.centrality.degree_centrality(G)
2884
+ plt.figure(figsize=(15, 8))
2885
+ plt.hist(degree_centrality.values(), bins=25)
2886
+ plt.xticks(ticks=[0, 0.025, 0.05, 0.1, 0.15, 0.2]) # set the x axis ticks
2887
+ plt.title("Degree Centrality Histogram ", fontdict={"size": 35}, loc="center")
2888
+ plt.xlabel("Degree Centrality", fontdict={"size": 20})
2889
+ plt.ylabel("Counts", fontdict={"size": 20})
2890
+ plt.show()
2891
+ self.format_for_upperright_table(degree_centrality, metric='Node', value='Degree Centrality', title="Degree Centrality Table")
2892
+
2893
+
2894
+ betweenness_centrality = nx.centrality.betweenness_centrality(
2895
+ G
2896
+ )
2897
+ plt.figure(figsize=(15, 8))
2898
+ plt.hist(betweenness_centrality.values(), bins=100)
2899
+ plt.xticks(ticks=[0, 0.02, 0.1, 0.2, 0.3, 0.4, 0.5]) # set the x axis ticks
2900
+ plt.title("Betweenness Centrality Histogram ", fontdict={"size": 35}, loc="center")
2901
+ plt.xlabel("Betweenness Centrality", fontdict={"size": 20})
2902
+ plt.ylabel("Counts", fontdict={"size": 20})
2903
+ plt.show()
2904
+ self.format_for_upperright_table(betweenness_centrality, metric='Node', value='Betweenness Centrality', title="Betweenness Centrality Table")
2905
+
2906
+
2907
+ closeness_centrality = nx.centrality.closeness_centrality(
2908
+ G
2909
+ )
2910
+ plt.figure(figsize=(15, 8))
2911
+ plt.hist(closeness_centrality.values(), bins=60)
2912
+ plt.title("Closeness Centrality Histogram ", fontdict={"size": 35}, loc="center")
2913
+ plt.xlabel("Closeness Centrality", fontdict={"size": 20})
2914
+ plt.ylabel("Counts", fontdict={"size": 20})
2915
+ plt.show()
2916
+ self.format_for_upperright_table(closeness_centrality, metric='Node', value='Closeness Centrality', title="Closeness Centrality Table")
2917
+
2918
+
2919
+ eigenvector_centrality = nx.centrality.eigenvector_centrality(
2920
+ G
2921
+ )
2922
+ plt.figure(figsize=(15, 8))
2923
+ plt.hist(eigenvector_centrality.values(), bins=60)
2924
+ plt.xticks(ticks=[0, 0.01, 0.02, 0.04, 0.06, 0.08]) # set the x axis ticks
2925
+ plt.title("Eigenvector Centrality Histogram ", fontdict={"size": 35}, loc="center")
2926
+ plt.xlabel("Eigenvector Centrality", fontdict={"size": 20})
2927
+ plt.ylabel("Counts", fontdict={"size": 20})
2928
+ plt.show()
2929
+ self.format_for_upperright_table(eigenvector_centrality, metric='Node', value='Eigenvector Centrality', title="Eigenvector Centrality Table")
2930
+
2931
+
2932
+
2933
+ clusters = nx.clustering(G)
2934
+ plt.figure(figsize=(15, 8))
2935
+ plt.hist(clusters.values(), bins=50)
2936
+ plt.title("Clustering Coefficient Histogram ", fontdict={"size": 35}, loc="center")
2937
+ plt.xlabel("Clustering Coefficient", fontdict={"size": 20})
2938
+ plt.ylabel("Counts", fontdict={"size": 20})
2939
+ plt.show()
2940
+ self.format_for_upperright_table(clusters, metric='Node', value='Clustering Coefficient', title="Clustering Coefficient Table")
2941
+
2942
+ bridges = list(nx.bridges(G))
2943
+ self.format_for_upperright_table(bridges, metric = 'Node Pair', title="Bridges")
2944
+
2945
+ except Exception as e:
2946
+ print(f"Error generating histograms: {e}")
2947
+
2825
2948
  def volumes(self):
2826
2949
 
2827
2950
 
@@ -5756,7 +5879,7 @@ class NetShowDialog(QDialog):
5756
5879
 
5757
5880
  # Add mode selection dropdown
5758
5881
  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"])
5882
+ self.mode_selector.addItems(["Default", "Community Coded", "Node ID Coded"])
5760
5883
  self.mode_selector.setCurrentIndex(0) # Default to Mode 1
5761
5884
  layout.addRow("Execution Mode:", self.mode_selector)
5762
5885
 
@@ -5778,6 +5901,10 @@ class NetShowDialog(QDialog):
5778
5901
 
5779
5902
  def show_network(self):
5780
5903
  # Get parameters and run analysis
5904
+ if my_network.communities is None:
5905
+ self.parent().show_partition_dialog()
5906
+ if my_network.communities is None:
5907
+ return
5781
5908
  geo = self.geo_layout.isChecked()
5782
5909
  if geo:
5783
5910
  if my_network.node_centroids is None:
@@ -5795,12 +5922,6 @@ class NetShowDialog(QDialog):
5795
5922
  my_network.show_communities_flex(geometric=geo, directory = directory, weighted = weighted, partition = my_network.communities)
5796
5923
  self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'CommunityID')
5797
5924
  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
5925
  my_network.show_identity_network(geometric=geo, directory = directory)
5805
5926
 
5806
5927
  self.accept()
@@ -5836,6 +5957,9 @@ class PartitionDialog(QDialog):
5836
5957
  self.stats.setChecked(True)
5837
5958
  layout.addRow("Community Stats:", self.stats)
5838
5959
 
5960
+ self.seed = QLineEdit("")
5961
+ layout.addRow("Seed (int):", self.seed)
5962
+
5839
5963
  # Add Run button
5840
5964
  run_button = QPushButton("Partition")
5841
5965
  run_button.clicked.connect(self.partition)
@@ -5847,10 +5971,16 @@ class PartitionDialog(QDialog):
5847
5971
  weighted = self.weighted.isChecked()
5848
5972
  dostats = self.stats.isChecked()
5849
5973
 
5974
+ try:
5975
+ seed = int(self.seed.text()) if self.seed.text() else None
5976
+ except:
5977
+ seed = None
5978
+
5979
+
5850
5980
  my_network.communities = None
5851
5981
 
5852
5982
  try:
5853
- stats = my_network.community_partition(weighted = weighted, style = accepted_mode, dostats = dostats)
5983
+ stats = my_network.community_partition(weighted = weighted, style = accepted_mode, dostats = dostats, seed = seed)
5854
5984
  print(f"Discovered communities: {my_network.communities}")
5855
5985
 
5856
5986
  self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'CommunityID', title = 'Community Partition')
@@ -6565,21 +6695,21 @@ class MotherDialog(QDialog):
6565
6695
 
6566
6696
  overlay = self.overlay.isChecked()
6567
6697
 
6698
+ if my_network.communities is None:
6699
+ self.parent().show_partition_dialog()
6700
+ if my_network.communities is None:
6701
+ return
6702
+
6568
6703
  if my_network.node_centroids is None:
6569
6704
  self.parent().show_centroid_dialog()
6570
6705
  if my_network.node_centroids is None:
6571
6706
  print("Error finding centroids")
6572
6707
  overlay = False
6573
6708
 
6574
- if my_network.communities is None:
6575
- self.parent().show_partition_dialog()
6576
- if my_network.communities is None:
6577
- return
6578
-
6579
6709
  if not overlay:
6580
- G = my_network.isolate_mothers(self, louvain = my_network.communities, ret_nodes = True, called = True)
6710
+ G = my_network.isolate_mothers(self, ret_nodes = True, called = True)
6581
6711
  else:
6582
- G, result = my_network.isolate_mothers(self, louvain = my_network.communities, ret_nodes = False, called = True)
6712
+ G, result = my_network.isolate_mothers(self, ret_nodes = False, called = True)
6583
6713
  self.parent().load_channel(2, channel_data = result, data = True)
6584
6714
 
6585
6715
  degree_dict = {}
@@ -7513,31 +7643,41 @@ class MachineWindow(QMainWindow):
7513
7643
 
7514
7644
  def save_model(self):
7515
7645
 
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'
7646
+ try:
7647
+
7648
+ filename, _ = QFileDialog.getSaveFileName(
7649
+ self,
7650
+ f"Save Model As",
7651
+ "", # Default directory
7652
+ "numpy data (*.npz);;All Files (*)" # File type filter
7653
+ )
7654
+
7655
+ if filename: # Only proceed if user didn't cancel
7656
+ # If user didn't type an extension, add .tif
7657
+ if not filename.endswith(('.npz')):
7658
+ filename += '.npz'
7659
+
7660
+ self.segmenter.save_model(filename, self.parent().channel_data[2])
7527
7661
 
7528
- self.segmenter.save_model(filename, self.parent().channel_data[2])
7662
+ except Exception as e:
7663
+ print(f"Error saving model: {e}")
7529
7664
 
7530
7665
  def load_model(self):
7531
7666
 
7532
- filename, _ = QFileDialog.getOpenFileName(
7533
- self,
7534
- f"Load Model",
7535
- "",
7536
- "numpy data (*.npz)"
7537
- )
7667
+ try:
7668
+
7669
+ filename, _ = QFileDialog.getOpenFileName(
7670
+ self,
7671
+ f"Load Model",
7672
+ "",
7673
+ "numpy data (*.npz)"
7674
+ )
7675
+
7676
+ self.segmenter.load_model(filename)
7677
+ self.trained = True
7538
7678
 
7539
- self.segmenter.load_model(filename)
7540
- self.trained = True
7679
+ except Exception as e:
7680
+ print(f"Error loading model: {e}")
7541
7681
 
7542
7682
  def toggle_two(self):
7543
7683
  if self.two.isChecked():
@@ -7791,8 +7931,6 @@ class MachineWindow(QMainWindow):
7791
7931
  traceback.print_exc()
7792
7932
 
7793
7933
  def segmentation_finished(self):
7794
- if not self.use_two:
7795
- print("Segmentation completed")
7796
7934
 
7797
7935
  current_xlim = self.parent().ax.get_xlim()
7798
7936
  current_ylim = self.parent().ax.get_ylim()
@@ -7994,7 +8132,7 @@ class SegmentationWorker(QThread):
7994
8132
  current_time - self.last_update >= self.update_interval):
7995
8133
  self.chunk_processed.emit()
7996
8134
  self.chunks_since_update = 0
7997
- self.last_update = current_time
8135
+ self.last_update = current_time
7998
8136
 
7999
8137
  self.finished.emit()
8000
8138
 
@@ -9132,65 +9270,110 @@ class CentroidNodeDialog(QDialog):
9132
9270
 
9133
9271
  class GenNodesDialog(QDialog):
9134
9272
 
9135
- def __init__(self, parent=None, down_factor = None, called = False):
9273
+ def __init__(self, parent=None, down_factor=None, called=False):
9136
9274
  super().__init__(parent)
9137
9275
  self.setWindowTitle("Create Nodes from Edge Vertices")
9138
9276
  self.setModal(True)
9139
-
9140
- layout = QFormLayout(self)
9277
+
9278
+ # Main layout
9279
+ main_layout = QVBoxLayout(self)
9141
9280
  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
-
9281
+
9282
+ # Set down_factor and cubic
9147
9283
  if not down_factor:
9148
9284
  down_factor = None
9285
+
9149
9286
  if down_factor is None:
9287
+ # --- Processing Options Group ---
9288
+ process_group = QGroupBox("Processing Options")
9289
+ process_layout = QGridLayout()
9290
+
9291
+ # Downsample factor
9150
9292
  self.down_factor = QLineEdit("0")
9151
- layout.addRow("Downsample Factor (Speeds up calculation at the cost of fidelity):", self.down_factor)
9293
+ process_layout.addWidget(QLabel("Downsample Factor (Speeds up calculation at the cost of fidelity):"), 0, 0)
9294
+ process_layout.addWidget(self.down_factor, 0, 1)
9295
+
9296
+ # Cubic checkbox
9152
9297
  self.cubic = QPushButton("Cubic Downsample")
9153
9298
  self.cubic.setCheckable(True)
9154
9299
  self.cubic.setChecked(False)
9155
- layout.addRow("(if downsampling): Use cubic downsample? (Slower but can preserve structure better)", self.cubic)
9300
+ process_layout.addWidget(QLabel("Use cubic downsample? (Slower but preserves structure better):"), 1, 0)
9301
+ process_layout.addWidget(self.cubic, 1, 1)
9302
+
9303
+ # Fast dilation checkbox
9304
+ self.fast_dil = QPushButton("Fast-Dil")
9305
+ self.fast_dil.setCheckable(True)
9306
+ self.fast_dil.setChecked(True)
9307
+ process_layout.addWidget(QLabel("Use Fast Dilation (Higher speed, less accurate with large search regions):"), 2, 0)
9308
+ process_layout.addWidget(self.fast_dil, 2, 1)
9309
+
9310
+ process_group.setLayout(process_layout)
9311
+ main_layout.addWidget(process_group)
9156
9312
  else:
9157
9313
  self.down_factor = down_factor[0]
9158
9314
  self.cubic = down_factor[1]
9159
-
9315
+
9316
+ # Fast dilation checkbox (still needed even if down_factor is provided)
9317
+ process_group = QGroupBox("Processing Options")
9318
+ process_layout = QGridLayout()
9319
+
9320
+ self.fast_dil = QPushButton("Fast-Dil")
9321
+ self.fast_dil.setCheckable(True)
9322
+ self.fast_dil.setChecked(True)
9323
+ process_layout.addWidget(QLabel("Use Fast Dilation (Higher speed, less accurate with large search regions):"), 0, 0)
9324
+ process_layout.addWidget(self.fast_dil, 0, 1)
9325
+
9326
+ process_group.setLayout(process_layout)
9327
+ main_layout.addWidget(process_group)
9328
+
9329
+ # --- Recommended Corrections Group ---
9330
+ rec_group = QGroupBox("Recommended Corrections")
9331
+ rec_layout = QGridLayout()
9332
+
9333
+ # Branch removal
9160
9334
  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)
9335
+ rec_layout.addWidget(QLabel("Skeleton Voxel Branch Length to Remove (Compensates for spines):"), 0, 0)
9336
+ rec_layout.addWidget(self.branch_removal, 0, 1)
9337
+
9338
+ # Auto checkbox
9175
9339
  self.auto = QPushButton("Auto")
9176
9340
  self.auto.setCheckable(True)
9177
9341
  self.auto.setChecked(True)
9178
- layout.addRow("Attempt to Auto Correct Skeleton Looping:", self.auto)
9179
-
9180
-
9181
- # retain checkbox (default True)
9342
+ rec_layout.addWidget(QLabel("Attempt to Auto Correct Skeleton Looping:"), 1, 0)
9343
+ rec_layout.addWidget(self.auto, 1, 1)
9344
+
9345
+ rec_group.setLayout(rec_layout)
9346
+ main_layout.addWidget(rec_group)
9347
+
9348
+ # --- Optional Corrections Group ---
9349
+ opt_group = QGroupBox("Optional Corrections")
9350
+ opt_layout = QGridLayout()
9351
+
9352
+ # Max volume
9353
+ self.max_vol = QLineEdit("0")
9354
+ opt_layout.addWidget(QLabel("Maximum Voxel Volume to Retain (Compensates for skeleton looping):"), 0, 0)
9355
+ opt_layout.addWidget(self.max_vol, 0, 1)
9356
+
9357
+ # Component dilation
9358
+ self.comp_dil = QLineEdit("0")
9359
+ opt_layout.addWidget(QLabel("Voxel distance to merge nearby nodes (Compensates for multi-branch regions):"), 1, 0)
9360
+ opt_layout.addWidget(self.comp_dil, 1, 1)
9361
+
9362
+ opt_group.setLayout(opt_layout)
9363
+ main_layout.addWidget(opt_group)
9364
+
9365
+ # Set retain variable but don't add to layout
9182
9366
  if not called:
9183
9367
  self.retain = QPushButton("Retain")
9184
9368
  self.retain.setCheckable(True)
9185
9369
  self.retain.setChecked(True)
9186
- #layout.addRow("Retain Original Edges? (Will be moved to overlay 2):", self.retain)
9187
9370
  else:
9188
9371
  self.retain = False
9189
-
9372
+
9190
9373
  # Add Run button
9191
9374
  run_button = QPushButton("Run Node Generation")
9192
9375
  run_button.clicked.connect(self.run_gennodes)
9193
- layout.addRow(run_button)
9376
+ main_layout.addWidget(run_button)
9194
9377
 
9195
9378
  def run_gennodes(self):
9196
9379
 
@@ -9311,49 +9494,84 @@ class BranchDialog(QDialog):
9311
9494
  super().__init__(parent)
9312
9495
  self.setWindowTitle("Label Branches (of edges)")
9313
9496
  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)
9497
+
9498
+ # Main layout
9499
+ main_layout = QVBoxLayout(self)
9500
+
9501
+ # --- Correction Options Group ---
9502
+ correction_group = QGroupBox("Correction Options")
9503
+ correction_layout = QGridLayout()
9504
+
9505
+ # Branch Fix checkbox
9330
9506
  self.fix = QPushButton("Auto-Correct Branches")
9331
9507
  self.fix.setCheckable(True)
9332
9508
  self.fix.setChecked(False)
9333
- layout.addRow("Attempt to auto-correct branch labels:", self.fix)
9334
-
9509
+ correction_layout.addWidget(QLabel("Attempt to auto-correct branch labels:"), 0, 0)
9510
+ correction_layout.addWidget(self.fix, 0, 1)
9511
+
9512
+ # Fix value
9335
9513
  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
-
9514
+ correction_layout.addWidget(QLabel("Avg Degree of Nearby Branch Communities to Merge (4-6 recommended):"), 1, 0)
9515
+ correction_layout.addWidget(self.fix_val, 1, 1)
9516
+
9517
+ # Seed
9518
+ self.seed = QLineEdit('')
9519
+ correction_layout.addWidget(QLabel("Random seed for auto correction (int - optional):"), 2, 0)
9520
+ correction_layout.addWidget(self.seed, 2, 1)
9521
+
9522
+ correction_group.setLayout(correction_layout)
9523
+ main_layout.addWidget(correction_group)
9524
+
9525
+ # --- Processing Options Group ---
9526
+ processing_group = QGroupBox("Processing Options")
9527
+ processing_layout = QGridLayout()
9528
+
9529
+ # Downsample factor
9338
9530
  self.down_factor = QLineEdit("0")
9339
- layout.addRow("Internal downsample (will have to recompute nodes)?:", self.down_factor)
9340
-
9341
- # cubic checkbox (default False)
9531
+ processing_layout.addWidget(QLabel("Internal downsample factor (will recompute nodes):"), 0, 0)
9532
+ processing_layout.addWidget(self.down_factor, 0, 1)
9533
+
9534
+ # Cubic checkbox
9342
9535
  self.cubic = QPushButton("Cubic Downsample")
9343
9536
  self.cubic.setCheckable(True)
9344
9537
  self.cubic.setChecked(False)
9345
- layout.addRow("(if downsampling): Use cubic downsample? (Slower but can preserve structure better)", self.cubic)
9346
-
9538
+ processing_layout.addWidget(QLabel("Use cubic downsample? (Slower but preserves structure better):"), 1, 0)
9539
+ processing_layout.addWidget(self.cubic, 1, 1)
9540
+
9541
+ processing_group.setLayout(processing_layout)
9542
+ main_layout.addWidget(processing_group)
9543
+
9544
+ # --- Misc Options Group ---
9545
+ misc_group = QGroupBox("Misc Options")
9546
+ misc_layout = QGridLayout()
9547
+
9548
+ # Nodes checkbox
9549
+ self.nodes = QPushButton("Generate Nodes")
9550
+ self.nodes.setCheckable(True)
9551
+ self.nodes.setChecked(True)
9552
+ misc_layout.addWidget(QLabel("Generate nodes from edges? (Skip if already completed):"), 0, 0)
9553
+ misc_layout.addWidget(self.nodes, 0, 1)
9554
+
9555
+ # GPU checkbox
9556
+ self.GPU = QPushButton("GPU")
9557
+ self.GPU.setCheckable(True)
9558
+ self.GPU.setChecked(False)
9559
+ misc_layout.addWidget(QLabel("Use GPU (May downsample large images):"), 1, 0)
9560
+ misc_layout.addWidget(self.GPU, 1, 1)
9561
+
9562
+ misc_group.setLayout(misc_layout)
9563
+ main_layout.addWidget(misc_group)
9564
+
9347
9565
  # Add Run button
9348
9566
  run_button = QPushButton("Run Branch Label")
9349
9567
  run_button.clicked.connect(self.branch_label)
9350
- layout.addRow(run_button)
9568
+ main_layout.addWidget(run_button)
9351
9569
 
9352
- if self.parent().channel_data[0] is not None:
9570
+ if self.parent().channel_data[0] is not None or self.parent().channel_data[3] is not None:
9353
9571
  QMessageBox.critical(
9354
9572
  self,
9355
9573
  "Alert",
9356
- "The nodes channel will be intermittently overwritten when running this method"
9574
+ "The nodes and overlay 2 channels will be intermittently overwritten when running this method"
9357
9575
  )
9358
9576
 
9359
9577
  def branch_label(self):
@@ -9370,8 +9588,7 @@ class BranchDialog(QDialog):
9370
9588
  cubic = self.cubic.isChecked()
9371
9589
  fix = self.fix.isChecked()
9372
9590
  fix_val = float(self.fix_val.text()) if self.fix_val.text() else None
9373
-
9374
-
9591
+ seed = int(self.seed.text()) if self.seed.text() else None
9375
9592
 
9376
9593
  original_shape = my_network.edges.shape
9377
9594
  original_array = copy.deepcopy(my_network.edges)
@@ -9392,7 +9609,7 @@ class BranchDialog(QDialog):
9392
9609
 
9393
9610
  temp_network.morph_proximity(search = [3,3], fastdil = True) #Detect network of nearby branches
9394
9611
 
9395
- temp_network.community_partition(weighted = False, style = 1, dostats = False) #Find communities with louvain, unweighted params
9612
+ temp_network.community_partition(weighted = False, style = 1, dostats = False, seed = seed) #Find communities with louvain, unweighted params
9396
9613
 
9397
9614
  targs = n3d.fix_branches(temp_network.nodes, temp_network.network, temp_network.communities, fix_val)
9398
9615