nettracer3d 0.7.3__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.

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,
@@ -25,7 +25,7 @@ import multiprocessing as mp
25
25
  from concurrent.futures import ThreadPoolExecutor
26
26
  from functools import partial
27
27
  from nettracer3d import segmenter
28
- #from nettracer3d import segmenter_GPU <--- couldn't get this faster than CPU ¯\_(ツ)_/¯
28
+ from nettracer3d import segmenter_GPU
29
29
 
30
30
 
31
31
 
@@ -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
 
@@ -2870,67 +2979,72 @@ class ImageViewerWindow(QMainWindow):
2870
2979
  return val
2871
2980
  return val
2872
2981
 
2873
- if isinstance(data, (list, tuple, np.ndarray)):
2874
- # Handle list input - create single column DataFrame
2875
- df = pd.DataFrame({
2876
- metric: [convert_to_numeric(val) for val in data]
2877
- })
2878
-
2879
- # Format floating point numbers
2880
- df[metric] = df[metric].apply(lambda x: f"{x:.3f}" if isinstance(x, (float, np.float64)) else str(x))
2881
-
2882
- else: # Dictionary input
2883
- # Get sample value to determine structure
2884
- sample_value = next(iter(data.values()))
2885
- is_multi_value = isinstance(sample_value, (list, tuple, np.ndarray))
2886
-
2887
- if is_multi_value:
2888
- # Handle multi-value case
2889
- if isinstance(value, str):
2890
- # If single string provided for multi-values, generate numbered headers
2891
- n_cols = len(sample_value)
2892
- value_headers = [f"{value}_{i+1}" for i in range(n_cols)]
2893
- else:
2894
- # Use provided list of headers
2895
- value_headers = value
2896
- if len(value_headers) != len(sample_value):
2897
- raise ValueError("Number of headers must match number of values per key")
2898
-
2899
- # Create lists for each column
2900
- dict_data = {metric: list(data.keys())}
2901
- for i, header in enumerate(value_headers):
2902
- # Convert values to numeric when possible before adding to DataFrame
2903
- dict_data[header] = [convert_to_numeric(data[key][i]) for key in data.keys()]
2904
-
2905
- df = pd.DataFrame(dict_data)
2906
-
2907
- # Format floating point numbers in all value columns
2908
- for header in value_headers:
2909
- df[header] = df[header].apply(lambda x: f"{x:.3f}" if isinstance(x, (float, np.float64)) else str(x))
2910
-
2911
- else:
2912
- # Single-value case
2982
+ try:
2983
+
2984
+ if isinstance(data, (list, tuple, np.ndarray)):
2985
+ # Handle list input - create single column DataFrame
2913
2986
  df = pd.DataFrame({
2914
- metric: data.keys(),
2915
- value: [convert_to_numeric(val) for val in data.values()]
2987
+ metric: [convert_to_numeric(val) for val in data]
2916
2988
  })
2917
2989
 
2918
2990
  # Format floating point numbers
2919
- df[value] = df[value].apply(lambda x: f"{x:.3f}" if isinstance(x, (float, np.float64)) else str(x))
2920
-
2921
- # Create new table
2922
- table = CustomTableView(self)
2923
- table.setModel(PandasModel(df))
2924
-
2925
- # Add to tabbed widget
2926
- if title is None:
2927
- self.tabbed_data.add_table(f"{metric} Analysis", table)
2928
- else:
2929
- self.tabbed_data.add_table(f"{title}", table)
2930
-
2931
- # Adjust column widths to content
2932
- for column in range(table.model().columnCount(None)):
2933
- table.resizeColumnToContents(column)
2991
+ df[metric] = df[metric].apply(lambda x: f"{x:.3f}" if isinstance(x, (float, np.float64)) else str(x))
2992
+
2993
+ else: # Dictionary input
2994
+ # Get sample value to determine structure
2995
+ sample_value = next(iter(data.values()))
2996
+ is_multi_value = isinstance(sample_value, (list, tuple, np.ndarray))
2997
+
2998
+ if is_multi_value:
2999
+ # Handle multi-value case
3000
+ if isinstance(value, str):
3001
+ # If single string provided for multi-values, generate numbered headers
3002
+ n_cols = len(sample_value)
3003
+ value_headers = [f"{value}_{i+1}" for i in range(n_cols)]
3004
+ else:
3005
+ # Use provided list of headers
3006
+ value_headers = value
3007
+ if len(value_headers) != len(sample_value):
3008
+ raise ValueError("Number of headers must match number of values per key")
3009
+
3010
+ # Create lists for each column
3011
+ dict_data = {metric: list(data.keys())}
3012
+ for i, header in enumerate(value_headers):
3013
+ # Convert values to numeric when possible before adding to DataFrame
3014
+ dict_data[header] = [convert_to_numeric(data[key][i]) for key in data.keys()]
3015
+
3016
+ df = pd.DataFrame(dict_data)
3017
+
3018
+ # Format floating point numbers in all value columns
3019
+ for header in value_headers:
3020
+ df[header] = df[header].apply(lambda x: f"{x:.3f}" if isinstance(x, (float, np.float64)) else str(x))
3021
+
3022
+ else:
3023
+ # Single-value case
3024
+ df = pd.DataFrame({
3025
+ metric: data.keys(),
3026
+ value: [convert_to_numeric(val) for val in data.values()]
3027
+ })
3028
+
3029
+ # Format floating point numbers
3030
+ df[value] = df[value].apply(lambda x: f"{x:.3f}" if isinstance(x, (float, np.float64)) else str(x))
3031
+
3032
+ # Create new table
3033
+ table = CustomTableView(self)
3034
+ table.setModel(PandasModel(df))
3035
+
3036
+ # Add to tabbed widget
3037
+ if title is None:
3038
+ self.tabbed_data.add_table(f"{metric} Analysis", table)
3039
+ else:
3040
+ self.tabbed_data.add_table(f"{title}", table)
3041
+
3042
+ # Adjust column widths to content
3043
+ for column in range(table.model().columnCount(None)):
3044
+ table.resizeColumnToContents(column)
3045
+
3046
+ except:
3047
+ pass
2934
3048
 
2935
3049
 
2936
3050
  def show_watershed_dialog(self):
@@ -5751,7 +5865,7 @@ class NetShowDialog(QDialog):
5751
5865
 
5752
5866
  # Add mode selection dropdown
5753
5867
  self.mode_selector = QComboBox()
5754
- 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"])
5755
5869
  self.mode_selector.setCurrentIndex(0) # Default to Mode 1
5756
5870
  layout.addRow("Execution Mode:", self.mode_selector)
5757
5871
 
@@ -5773,6 +5887,10 @@ class NetShowDialog(QDialog):
5773
5887
 
5774
5888
  def show_network(self):
5775
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
5776
5894
  geo = self.geo_layout.isChecked()
5777
5895
  if geo:
5778
5896
  if my_network.node_centroids is None:
@@ -5790,12 +5908,6 @@ class NetShowDialog(QDialog):
5790
5908
  my_network.show_communities_flex(geometric=geo, directory = directory, weighted = weighted, partition = my_network.communities)
5791
5909
  self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'CommunityID')
5792
5910
  elif accepted_mode == 2:
5793
- my_network.show_communities_flex(geometric=geo, directory = directory, weighted = weighted, partition = my_network.communities, style = 0)
5794
- self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'CommunityID')
5795
- elif accepted_mode ==3:
5796
- my_network.show_communities_flex(geometric=geo, directory = directory, weighted = weighted, partition = my_network.communities, style = 1)
5797
- self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'CommunityID')
5798
- elif accepted_mode == 4:
5799
5911
  my_network.show_identity_network(geometric=geo, directory = directory)
5800
5912
 
5801
5913
  self.accept()
@@ -5831,6 +5943,9 @@ class PartitionDialog(QDialog):
5831
5943
  self.stats.setChecked(True)
5832
5944
  layout.addRow("Community Stats:", self.stats)
5833
5945
 
5946
+ self.seed = QLineEdit("")
5947
+ layout.addRow("Seed (int):", self.seed)
5948
+
5834
5949
  # Add Run button
5835
5950
  run_button = QPushButton("Partition")
5836
5951
  run_button.clicked.connect(self.partition)
@@ -5842,10 +5957,16 @@ class PartitionDialog(QDialog):
5842
5957
  weighted = self.weighted.isChecked()
5843
5958
  dostats = self.stats.isChecked()
5844
5959
 
5960
+ try:
5961
+ seed = int(self.seed.text()) if self.seed.text() else None
5962
+ except:
5963
+ seed = None
5964
+
5965
+
5845
5966
  my_network.communities = None
5846
5967
 
5847
5968
  try:
5848
- 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)
5849
5970
  print(f"Discovered communities: {my_network.communities}")
5850
5971
 
5851
5972
  self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'CommunityID', title = 'Community Partition')
@@ -6560,21 +6681,21 @@ class MotherDialog(QDialog):
6560
6681
 
6561
6682
  overlay = self.overlay.isChecked()
6562
6683
 
6684
+ if my_network.communities is None:
6685
+ self.parent().show_partition_dialog()
6686
+ if my_network.communities is None:
6687
+ return
6688
+
6563
6689
  if my_network.node_centroids is None:
6564
6690
  self.parent().show_centroid_dialog()
6565
6691
  if my_network.node_centroids is None:
6566
6692
  print("Error finding centroids")
6567
6693
  overlay = False
6568
6694
 
6569
- if my_network.communities is None:
6570
- self.parent().show_partition_dialog()
6571
- if my_network.communities is None:
6572
- return
6573
-
6574
6695
  if not overlay:
6575
- 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)
6576
6697
  else:
6577
- 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)
6578
6699
  self.parent().load_channel(2, channel_data = result, data = True)
6579
6700
 
6580
6701
  degree_dict = {}
@@ -7412,7 +7533,11 @@ class MachineWindow(QMainWindow):
7412
7533
  self.three.setCheckable(True)
7413
7534
  self.three.setChecked(True)
7414
7535
  self.three.clicked.connect(self.toggle_three)
7415
- #processing_layout.addWidget(self.GPU) [Decided to hold off on this until its more robust]
7536
+ self.GPU = QPushButton("GPU")
7537
+ self.GPU.setCheckable(True)
7538
+ self.GPU.setChecked(False)
7539
+ self.GPU.clicked.connect(self.toggle_GPU)
7540
+ processing_layout.addWidget(self.GPU)
7416
7541
  processing_layout.addWidget(self.two)
7417
7542
  processing_layout.addWidget(self.three)
7418
7543
  processing_group.setLayout(processing_layout)
@@ -7504,31 +7629,41 @@ class MachineWindow(QMainWindow):
7504
7629
 
7505
7630
  def save_model(self):
7506
7631
 
7507
- filename, _ = QFileDialog.getSaveFileName(
7508
- self,
7509
- f"Save Model As",
7510
- "", # Default directory
7511
- "numpy data (*.npz);;All Files (*)" # File type filter
7512
- )
7513
-
7514
- if filename: # Only proceed if user didn't cancel
7515
- # If user didn't type an extension, add .tif
7516
- if not filename.endswith(('.npz')):
7517
- filename += '.npz'
7632
+ try:
7518
7633
 
7519
- self.segmenter.save_model(filename, self.parent().channel_data[2])
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'
7645
+
7646
+ self.segmenter.save_model(filename, self.parent().channel_data[2])
7647
+
7648
+ except Exception as e:
7649
+ print(f"Error saving model: {e}")
7520
7650
 
7521
7651
  def load_model(self):
7522
7652
 
7523
- filename, _ = QFileDialog.getOpenFileName(
7524
- self,
7525
- f"Load Model",
7526
- "",
7527
- "numpy data (*.npz)"
7528
- )
7653
+ try:
7654
+
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
7529
7664
 
7530
- self.segmenter.load_model(filename)
7531
- self.trained = True
7665
+ except Exception as e:
7666
+ print(f"Error loading model: {e}")
7532
7667
 
7533
7668
  def toggle_two(self):
7534
7669
  if self.two.isChecked():
@@ -7550,6 +7685,35 @@ class MachineWindow(QMainWindow):
7550
7685
  self.two.setChecked(True)
7551
7686
  self.use_two = True
7552
7687
 
7688
+ def toggle_GPU(self):
7689
+
7690
+ if self.parent().active_channel == 0:
7691
+ if self.parent().channel_data[0] is not None:
7692
+ try:
7693
+ active_data = self.parent().channel_data[0]
7694
+ act_channel = 0
7695
+ except:
7696
+ active_data = self.parent().channel_data[1]
7697
+ act_channel = 1
7698
+ else:
7699
+ active_data = self.parent().channel_data[1]
7700
+ act_channel = 1
7701
+
7702
+ if self.GPU.isChecked():
7703
+
7704
+ try:
7705
+ self.segmenter = segmenter_GPU.InteractiveSegmenter(active_data)
7706
+ print("Using GPU")
7707
+ except:
7708
+ self.GPU.setChecked(False)
7709
+ print("Could not detect GPU")
7710
+ import traceback
7711
+ traceback.print_exc()
7712
+
7713
+ else:
7714
+ self.segmenter = segmenter.InteractiveSegmenter(active_data, use_gpu=False)
7715
+ print("Using CPU")
7716
+
7553
7717
 
7554
7718
 
7555
7719
  def toggle_foreground(self):
@@ -7753,8 +7917,6 @@ class MachineWindow(QMainWindow):
7753
7917
  traceback.print_exc()
7754
7918
 
7755
7919
  def segmentation_finished(self):
7756
- if not self.use_two:
7757
- print("Segmentation completed")
7758
7920
 
7759
7921
  current_xlim = self.parent().ax.get_xlim()
7760
7922
  current_ylim = self.parent().ax.get_ylim()
@@ -7836,18 +7998,12 @@ class MachineWindow(QMainWindow):
7836
7998
  self.parent().highlight_overlay = array3 #Clear this out for the segmenter to use
7837
7999
 
7838
8000
  print("Segmenting entire volume with model...")
7839
- foreground_coords, background_coords = self.segmenter.segment_volume(gpu = self.use_gpu)
8001
+ #foreground_coords, background_coords = self.segmenter.segment_volume(array = self.parent().highlight_overlay)
8002
+ self.parent().highlight_overlay = self.segmenter.segment_volume(array = self.parent().highlight_overlay)
7840
8003
 
7841
8004
  # Clean up when done
7842
8005
  self.segmenter.cleanup()
7843
8006
 
7844
- fg_array = np.array(list(foreground_coords))
7845
- if len(fg_array) > 0: # Check if we have any foreground coordinates
7846
- # Unpack into separate coordinate arrays
7847
- z_coords, y_coords, x_coords = fg_array[:, 0], fg_array[:, 1], fg_array[:, 2]
7848
- # Assign values in a single vectorized operation
7849
- self.parent().highlight_overlay[z_coords, y_coords, x_coords] = 255
7850
-
7851
8007
  self.parent().load_channel(3, self.parent().highlight_overlay, True)
7852
8008
 
7853
8009
  # Not exactly sure why we need all this but the channel buttons weren't loading like they normally do when load_channel() is called:
@@ -7962,7 +8118,7 @@ class SegmentationWorker(QThread):
7962
8118
  current_time - self.last_update >= self.update_interval):
7963
8119
  self.chunk_processed.emit()
7964
8120
  self.chunks_since_update = 0
7965
- self.last_update = current_time
8121
+ self.last_update = current_time
7966
8122
 
7967
8123
  self.finished.emit()
7968
8124
 
@@ -9100,65 +9256,110 @@ class CentroidNodeDialog(QDialog):
9100
9256
 
9101
9257
  class GenNodesDialog(QDialog):
9102
9258
 
9103
- def __init__(self, parent=None, down_factor = None, called = False):
9259
+ def __init__(self, parent=None, down_factor=None, called=False):
9104
9260
  super().__init__(parent)
9105
9261
  self.setWindowTitle("Create Nodes from Edge Vertices")
9106
9262
  self.setModal(True)
9107
-
9108
- layout = QFormLayout(self)
9263
+
9264
+ # Main layout
9265
+ main_layout = QVBoxLayout(self)
9109
9266
  self.called = called
9110
-
9111
- #self.directory = QLineEdit()
9112
- #self.directory.setPlaceholderText("Leave empty to save in active dir")
9113
- #layout.addRow("Output Directory:", self.directory)
9114
-
9267
+
9268
+ # Set down_factor and cubic
9115
9269
  if not down_factor:
9116
9270
  down_factor = None
9271
+
9117
9272
  if down_factor is None:
9273
+ # --- Processing Options Group ---
9274
+ process_group = QGroupBox("Processing Options")
9275
+ process_layout = QGridLayout()
9276
+
9277
+ # Downsample factor
9118
9278
  self.down_factor = QLineEdit("0")
9119
- 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
9120
9283
  self.cubic = QPushButton("Cubic Downsample")
9121
9284
  self.cubic.setCheckable(True)
9122
9285
  self.cubic.setChecked(False)
9123
- 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)
9124
9298
  else:
9125
9299
  self.down_factor = down_factor[0]
9126
9300
  self.cubic = down_factor[1]
9127
-
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
9128
9320
  self.branch_removal = QLineEdit("0")
9129
- layout.addRow("Skeleton Voxel Branch Length to Remove (int) (Compensates for spines off medial axis):", self.branch_removal)
9130
-
9131
- self.max_vol = QLineEdit("0")
9132
- 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)
9133
-
9134
- self.comp_dil = QLineEdit("0")
9135
- layout.addRow("Voxel distance to merge nearby nodes (Int - compensates for multi-branch identification along thick branch regions):", self.comp_dil)
9136
-
9137
- self.fast_dil = QPushButton("Fast-Dil")
9138
- self.fast_dil.setCheckable(True)
9139
- self.fast_dil.setChecked(True)
9140
- layout.addRow("(If using above) Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fast_dil)
9141
-
9142
- # 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
9143
9325
  self.auto = QPushButton("Auto")
9144
9326
  self.auto.setCheckable(True)
9145
9327
  self.auto.setChecked(True)
9146
- layout.addRow("Attempt to Auto Correct Skeleton Looping:", self.auto)
9147
-
9148
-
9149
- # 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
9150
9352
  if not called:
9151
9353
  self.retain = QPushButton("Retain")
9152
9354
  self.retain.setCheckable(True)
9153
9355
  self.retain.setChecked(True)
9154
- #layout.addRow("Retain Original Edges? (Will be moved to overlay 2):", self.retain)
9155
9356
  else:
9156
9357
  self.retain = False
9157
-
9358
+
9158
9359
  # Add Run button
9159
9360
  run_button = QPushButton("Run Node Generation")
9160
9361
  run_button.clicked.connect(self.run_gennodes)
9161
- layout.addRow(run_button)
9362
+ main_layout.addWidget(run_button)
9162
9363
 
9163
9364
  def run_gennodes(self):
9164
9365
 
@@ -9279,49 +9480,84 @@ class BranchDialog(QDialog):
9279
9480
  super().__init__(parent)
9280
9481
  self.setWindowTitle("Label Branches (of edges)")
9281
9482
  self.setModal(True)
9282
-
9283
- layout = QFormLayout(self)
9284
-
9285
- # Nodes checkbox (default True)
9286
- self.nodes = QPushButton("Generate Nodes")
9287
- self.nodes.setCheckable(True)
9288
- self.nodes.setChecked(True)
9289
- 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)
9290
-
9291
- # GPU checkbox (default False)
9292
- self.GPU = QPushButton("GPU")
9293
- self.GPU.setCheckable(True)
9294
- self.GPU.setChecked(False)
9295
- 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)
9296
-
9297
- # 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
9298
9492
  self.fix = QPushButton("Auto-Correct Branches")
9299
9493
  self.fix.setCheckable(True)
9300
9494
  self.fix.setChecked(False)
9301
- layout.addRow("Attempt to auto-correct branch labels:", self.fix)
9302
-
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
9303
9499
  self.fix_val = QLineEdit('4')
9304
- 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)
9305
-
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
9306
9516
  self.down_factor = QLineEdit("0")
9307
- layout.addRow("Internal downsample (will have to recompute nodes)?:", self.down_factor)
9308
-
9309
- # 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
9310
9521
  self.cubic = QPushButton("Cubic Downsample")
9311
9522
  self.cubic.setCheckable(True)
9312
9523
  self.cubic.setChecked(False)
9313
- layout.addRow("(if downsampling): Use cubic downsample? (Slower but can preserve structure better)", self.cubic)
9314
-
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
+
9315
9551
  # Add Run button
9316
9552
  run_button = QPushButton("Run Branch Label")
9317
9553
  run_button.clicked.connect(self.branch_label)
9318
- layout.addRow(run_button)
9554
+ main_layout.addWidget(run_button)
9319
9555
 
9320
- 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:
9321
9557
  QMessageBox.critical(
9322
9558
  self,
9323
9559
  "Alert",
9324
- "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"
9325
9561
  )
9326
9562
 
9327
9563
  def branch_label(self):
@@ -9338,8 +9574,7 @@ class BranchDialog(QDialog):
9338
9574
  cubic = self.cubic.isChecked()
9339
9575
  fix = self.fix.isChecked()
9340
9576
  fix_val = float(self.fix_val.text()) if self.fix_val.text() else None
9341
-
9342
-
9577
+ seed = int(self.seed.text()) if self.seed.text() else None
9343
9578
 
9344
9579
  original_shape = my_network.edges.shape
9345
9580
  original_array = copy.deepcopy(my_network.edges)
@@ -9360,7 +9595,7 @@ class BranchDialog(QDialog):
9360
9595
 
9361
9596
  temp_network.morph_proximity(search = [3,3], fastdil = True) #Detect network of nearby branches
9362
9597
 
9363
- 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
9364
9599
 
9365
9600
  targs = n3d.fix_branches(temp_network.nodes, temp_network.network, temp_network.communities, fix_val)
9366
9601