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

@@ -22,7 +22,7 @@ from PyQt6.QtGui import (QFont, QCursor, QColor, QPixmap, QFontMetrics, QPainter
22
22
  import tifffile
23
23
  import copy
24
24
  import multiprocessing as mp
25
- from concurrent.futures import ThreadPoolExecutor
25
+ from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
26
26
  from functools import partial
27
27
  from nettracer3d import segmenter
28
28
  try:
@@ -33,6 +33,8 @@ from nettracer3d import excelotron
33
33
  import threading
34
34
  import queue
35
35
  from threading import Lock
36
+ from scipy import ndimage
37
+ import os
36
38
 
37
39
 
38
40
 
@@ -291,6 +293,14 @@ class ImageViewerWindow(QMainWindow):
291
293
 
292
294
  control_layout.addWidget(channel_container)
293
295
 
296
+ self.show_channels = QPushButton("✓")
297
+ self.show_channels.setCheckable(True)
298
+ self.show_channels.setChecked(True)
299
+ self.show_channels.setFixedSize(20, 20)
300
+ self.show_channels.clicked.connect(self.toggle_chan_load)
301
+ control_layout.addWidget(self.show_channels)
302
+ self.chan_load = True
303
+
294
304
  # Create the main widget and layout
295
305
  main_widget = QWidget()
296
306
  self.setCentralWidget(main_widget)
@@ -386,7 +396,7 @@ class ImageViewerWindow(QMainWindow):
386
396
  # Create both table views
387
397
  self.network_table = CustomTableView(self)
388
398
  self.selection_table = CustomTableView(self)
389
- empty_df = pd.DataFrame(columns=['Node 1A', 'Node 1B', 'Edge 1C'])
399
+ empty_df = pd.DataFrame(columns=['Node A', 'Node B', 'Edge C'])
390
400
  self.selection_table.setModel(PandasModel(empty_df))
391
401
  self.network_table.setAlternatingRowColors(True)
392
402
  self.selection_table.setAlternatingRowColors(True)
@@ -1773,7 +1783,7 @@ class ImageViewerWindow(QMainWindow):
1773
1783
  my_network.network_lists = my_network.network_lists
1774
1784
 
1775
1785
  if not hasattr(my_network, 'network_lists') or my_network.network_lists is None:
1776
- empty_df = pd.DataFrame(columns=['Node 1A', 'Node 1B', 'Edge 1C'])
1786
+ empty_df = pd.DataFrame(columns=['Node A', 'Node B', 'Edge C'])
1777
1787
  model = PandasModel(empty_df)
1778
1788
  self.network_table.setModel(model)
1779
1789
  else:
@@ -1795,58 +1805,149 @@ class ImageViewerWindow(QMainWindow):
1795
1805
  except Exception as e:
1796
1806
  print(f"An error has occured: {e}")
1797
1807
 
1798
- def separate_nontouching_objects(self, input_array, max_val=0):
1808
+
1809
+ def process_single_label_bbox(args):
1799
1810
  """
1800
- optimized version using advanced indexing.
1811
+ Worker function to process a single label within its bounding box
1812
+ This function will run in parallel
1801
1813
  """
1814
+ label_subarray, original_label, bbox_slices, start_new_label = args
1815
+
1816
+ try:
1817
+ # Create binary mask for this label only
1818
+ binary_mask = label_subarray == original_label
1819
+
1820
+ if not np.any(binary_mask):
1821
+ return None, start_new_label, bbox_slices
1822
+
1823
+ # Find connected components in the subarray
1824
+ labeled_cc, num_cc = n3d.label_objects(binary_mask)
1825
+
1826
+ if num_cc == 0:
1827
+ return None, start_new_label, bbox_slices
1828
+
1829
+ # Create output subarray with new labels
1830
+ output_subarray = np.zeros_like(label_subarray)
1831
+
1832
+ # Assign new consecutive labels starting from start_new_label
1833
+ for cc_id in range(1, num_cc + 1):
1834
+ cc_mask = labeled_cc == cc_id
1835
+ new_label = start_new_label + cc_id - 1
1836
+ output_subarray[cc_mask] = new_label
1837
+
1838
+ # Return the processed subarray, number of components created, and bbox info
1839
+ return output_subarray, start_new_label + num_cc, bbox_slices
1840
+
1841
+ except Exception as e:
1842
+ print(f"Error processing label {original_label}: {e}")
1843
+ return None, start_new_label, bbox_slices
1802
1844
 
1845
+ def separate_nontouching_objects(self, input_array, max_val=0):
1846
+ """
1847
+ Ultra-optimized version using find_objects directly without remapping
1848
+ """
1803
1849
  print("Splitting nontouching objects")
1804
-
1850
+
1805
1851
  binary_mask = input_array > 0
1806
- labeled_array, _ = n3d.label_objects(binary_mask)
1852
+ if not np.any(binary_mask):
1853
+ return np.zeros_like(input_array)
1807
1854
 
1808
- # Create a compound key for each (original_label, connected_component) pair
1809
- # This avoids the need for explicit mapping
1810
- mask = binary_mask
1811
- compound_key = input_array[mask] * (labeled_array.max() + 1) + labeled_array[mask]
1855
+ unique_labels = np.unique(input_array[binary_mask])
1856
+ print(f"Processing {len(unique_labels)} unique labels")
1812
1857
 
1813
- # Get unique compound keys and create new labels
1814
- unique_keys, inverse_indices = np.unique(compound_key, return_inverse=True)
1815
- new_labels = np.arange(max_val + 1, max_val + 1 + len(unique_keys))
1858
+ # Get all bounding boxes at once - this is very fast
1859
+ bounding_boxes = ndimage.find_objects(input_array)
1816
1860
 
1817
- # Create output array
1818
- output_array = np.zeros_like(input_array)
1819
- output_array[mask] = new_labels[inverse_indices]
1861
+ # Prepare work items - just check if bounding box exists for each label
1862
+ work_items = []
1863
+ for orig_label in unique_labels:
1864
+ # find_objects returns list where index = label - 1
1865
+ bbox_index = orig_label - 1
1866
+
1867
+ if (bbox_index >= 0 and
1868
+ bbox_index < len(bounding_boxes) and
1869
+ bounding_boxes[bbox_index] is not None):
1870
+
1871
+ bbox = bounding_boxes[bbox_index]
1872
+ work_items.append((orig_label, bbox))
1820
1873
 
1874
+ print(f"Created {len(work_items)} work items")
1875
+
1876
+ # If we have work items, process them
1877
+ if len(work_items) == 0:
1878
+ print("No valid work items found!")
1879
+ return np.zeros_like(input_array)
1880
+
1881
+ def process_label_minimal(item):
1882
+ orig_label, bbox = item
1883
+ try:
1884
+ subarray = input_array[bbox]
1885
+ binary_sub = subarray == orig_label
1886
+
1887
+ if not np.any(binary_sub):
1888
+ return orig_label, bbox, None, 0
1889
+
1890
+ labeled_sub, num_cc = n3d.label_objects(binary_sub)
1891
+ return orig_label, bbox, labeled_sub, num_cc
1892
+
1893
+ except Exception as e:
1894
+ print(f"Error processing label {orig_label}: {e}")
1895
+ return orig_label, bbox, None, 0
1896
+
1897
+ # Execute in parallel
1898
+ max_workers = min(mp.cpu_count(), len(work_items))
1899
+
1900
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
1901
+ results = list(executor.map(process_label_minimal, work_items))
1902
+
1903
+ # Reconstruct output array
1904
+ output_array = np.zeros_like(input_array)
1905
+ current_label = max_val + 1
1906
+ total_components = 0
1907
+
1908
+ for orig_label, bbox, labeled_sub, num_cc in results:
1909
+ if num_cc > 0 and labeled_sub is not None:
1910
+ print(f"Label {orig_label}: {num_cc} components")
1911
+ # Remap labels and place in output
1912
+ for cc_id in range(1, num_cc + 1):
1913
+ mask = labeled_sub == cc_id
1914
+ output_array[bbox][mask] = current_label
1915
+ current_label += 1
1916
+ total_components += 1
1917
+
1918
+ print(f"Total components created: {total_components}")
1821
1919
  return output_array
1822
1920
 
1823
1921
  def handle_seperate(self):
1824
-
1922
+ """
1923
+ Fixed version with proper mask handling and debugging
1924
+ """
1825
1925
  try:
1826
1926
  # Handle nodes
1827
1927
  if len(self.clicked_values['nodes']) > 0:
1828
- self.create_highlight_overlay(node_indices=self.clicked_values['nodes'])
1829
1928
 
1830
- # Create a boolean mask for highlighted values
1831
- self.highlight_overlay = self.highlight_overlay != 0
1832
-
1833
- # Create array with just the highlighted values
1834
- highlighted_nodes = self.highlight_overlay * my_network.nodes
1929
+ # Create highlight overlay (this should preserve original label values)
1930
+ self.create_highlight_overlay(node_indices=self.clicked_values['nodes'])
1931
+
1932
+ # DON'T convert to boolean yet - we need the original labels!
1933
+ # Create a boolean mask for where we have highlighted values
1934
+ highlight_mask = self.highlight_overlay != 0
1835
1935
 
1936
+ # Create array with just the highlighted values (preserving original labels)
1937
+ highlighted_nodes = np.where(highlight_mask, my_network.nodes, 0)
1938
+
1836
1939
  # Get non-highlighted part of the array
1837
- non_highlighted = my_network.nodes * (~self.highlight_overlay)
1838
-
1839
- if (highlighted_nodes==non_highlighted).all():
1840
- max_val = 0
1841
- else:
1842
- max_val = np.max(non_highlighted)
1940
+ non_highlighted = np.where(highlight_mask, 0, my_network.nodes)
1843
1941
 
1942
+ # Calculate max_val properly
1943
+ max_val = np.max(non_highlighted) if np.any(non_highlighted) else 0
1944
+
1844
1945
  # Process highlighted part
1845
1946
  processed_highlights = self.separate_nontouching_objects(highlighted_nodes, max_val)
1846
-
1947
+
1847
1948
  # Combine back with non-highlighted parts
1848
1949
  my_network.nodes = non_highlighted + processed_highlights
1849
-
1950
+
1850
1951
  self.load_channel(0, my_network.nodes, True)
1851
1952
 
1852
1953
  # Handle edges
@@ -1855,18 +1956,15 @@ class ImageViewerWindow(QMainWindow):
1855
1956
  self.create_highlight_overlay(edge_indices=self.clicked_values['edges'])
1856
1957
 
1857
1958
  # Create a boolean mask for highlighted values
1858
- self.highlight_overlay = self.highlight_overlay != 0
1959
+ highlight_mask = self.highlight_overlay != 0
1859
1960
 
1860
1961
  # Create array with just the highlighted values
1861
- highlighted_edges = self.highlight_overlay * my_network.edges
1962
+ highlighted_edges = np.where(highlight_mask, my_network.edges, 0)
1862
1963
 
1863
1964
  # Get non-highlighted part of the array
1864
- non_highlighted = my_network.edges * (~self.highlight_overlay)
1965
+ non_highlighted = np.where(highlight_mask, 0, my_network.edges)
1865
1966
 
1866
- if (highlighted_edges==non_highlighted).all():
1867
- max_val = 0
1868
- else:
1869
- max_val = np.max(non_highlighted)
1967
+ max_val = np.max(non_highlighted) if np.any(non_highlighted) else 0
1870
1968
 
1871
1969
  # Process highlighted part
1872
1970
  processed_highlights = self.separate_nontouching_objects(highlighted_edges, max_val)
@@ -1920,7 +2018,7 @@ class ImageViewerWindow(QMainWindow):
1920
2018
 
1921
2019
 
1922
2020
  if not hasattr(my_network, 'network_lists') or my_network.network_lists is None:
1923
- empty_df = pd.DataFrame(columns=['Node 1A', 'Node 1B', 'Edge 1C'])
2021
+ empty_df = pd.DataFrame(columns=['Node A', 'Node B', 'Edge C'])
1924
2022
  model = PandasModel(empty_df)
1925
2023
  self.network_table.setModel(model)
1926
2024
  else:
@@ -1961,7 +2059,7 @@ class ImageViewerWindow(QMainWindow):
1961
2059
 
1962
2060
  # Update the table
1963
2061
  if not hasattr(my_network, 'network_lists') or my_network.network_lists is None:
1964
- empty_df = pd.DataFrame(columns=['Node 1A', 'Node 1B', 'Edge 1C'])
2062
+ empty_df = pd.DataFrame(columns=['Node A', 'Node B', 'Edge C'])
1965
2063
  model = PandasModel(empty_df)
1966
2064
  self.network_table.setModel(model)
1967
2065
  else:
@@ -1993,7 +2091,7 @@ class ImageViewerWindow(QMainWindow):
1993
2091
  my_network.network_lists = my_network.network_lists
1994
2092
 
1995
2093
  if not hasattr(my_network, 'network_lists') or my_network.network_lists is None:
1996
- empty_df = pd.DataFrame(columns=['Node 1A', 'Node 1B', 'Edge 1C'])
2094
+ empty_df = pd.DataFrame(columns=['Node A', 'Node B', 'Edge C'])
1997
2095
  model = PandasModel(empty_df)
1998
2096
  self.network_table.setModel(model)
1999
2097
  else:
@@ -2044,6 +2142,12 @@ class ImageViewerWindow(QMainWindow):
2044
2142
  print(f"Error: {e}")
2045
2143
 
2046
2144
 
2145
+ def toggle_chan_load(self):
2146
+
2147
+ if self.show_channels.isChecked():
2148
+ self.chan_load = True
2149
+ else:
2150
+ self.chan_load = False
2047
2151
 
2048
2152
  def toggle_highlight(self):
2049
2153
  self.highlight = self.high_button.isChecked()
@@ -2648,6 +2752,7 @@ class ImageViewerWindow(QMainWindow):
2648
2752
  return
2649
2753
 
2650
2754
  current_time = time.time()
2755
+ self.rect_time = current_time
2651
2756
 
2652
2757
  if self.selection_start and not self.selecting and not self.pan_mode and not self.brush_mode:
2653
2758
  if (abs(event.xdata - self.selection_start[0]) > 1 or
@@ -3062,6 +3167,12 @@ class ImageViewerWindow(QMainWindow):
3062
3167
 
3063
3168
  def on_mouse_release(self, event):
3064
3169
  """Handle mouse release events"""
3170
+
3171
+ if self.zoom_mode:
3172
+ rect_condition = (time.time() - self.rect_time) > 0.01 # This is just to prevent non-deliberate rectangle zooming
3173
+ else:
3174
+ rect_condition = True
3175
+
3065
3176
  if self.pan_mode:
3066
3177
 
3067
3178
  self.panning = False
@@ -3069,7 +3180,7 @@ class ImageViewerWindow(QMainWindow):
3069
3180
  self.canvas.setCursor(Qt.CursorShape.OpenHandCursor)
3070
3181
 
3071
3182
  elif event.button == 1: # Left button release
3072
- if self.selecting and self.selection_rect is not None:
3183
+ if rect_condition and self.selecting and self.selection_rect is not None:
3073
3184
  # Get the rectangle bounds
3074
3185
  x0 = min(self.selection_start[0], event.xdata)
3075
3186
  y0 = min(self.selection_start[1], event.ydata)
@@ -3077,7 +3188,13 @@ class ImageViewerWindow(QMainWindow):
3077
3188
  height = abs(event.ydata - self.selection_start[1])
3078
3189
  shift_pressed = 'shift' in event.modifiers
3079
3190
 
3080
- if shift_pressed or self.zoom_mode: #Optional targeted zoom
3191
+ if shift_pressed:
3192
+
3193
+ args = int(x0), int(x0 + width), int(y0), int(y0 + height)
3194
+
3195
+ self.show_crop_dialog(args)
3196
+
3197
+ elif self.zoom_mode: #Optional targeted zoom
3081
3198
 
3082
3199
  self.ax.set_xlim([x0, x0 + width])
3083
3200
  self.ax.set_ylim([y0 + height, y0])
@@ -3539,7 +3656,7 @@ class ImageViewerWindow(QMainWindow):
3539
3656
  ripley_action.triggered.connect(self.show_ripley_dialog)
3540
3657
  heatmap_action = stats_menu.addAction("Community Cluster Heatmap")
3541
3658
  heatmap_action.triggered.connect(self.show_heatmap_dialog)
3542
- nearneigh_action = stats_menu.addAction("Average Nearest Neighbors")
3659
+ nearneigh_action = stats_menu.addAction("Average Nearest Neighbors (With Clustering Heatmaps)")
3543
3660
  nearneigh_action.triggered.connect(self.show_nearneigh_dialog)
3544
3661
  vol_action = stats_menu.addAction("Calculate Volumes")
3545
3662
  vol_action.triggered.connect(self.volumes)
@@ -3574,6 +3691,10 @@ class ImageViewerWindow(QMainWindow):
3574
3691
  calc_all_action.triggered.connect(self.show_calc_all_dialog)
3575
3692
  calc_prox_action = calculate_menu.addAction("Calculate Proximity Network (connect nodes by distance)")
3576
3693
  calc_prox_action.triggered.connect(self.show_calc_prox_dialog)
3694
+ calc_branch_action = calculate_menu.addAction("Calculate Branchpoint Network (Connect Branchpoints of Edge Image - Good for Nerves/Vessels)")
3695
+ calc_branch_action.triggered.connect(self.handle_calc_branch)
3696
+ calc_branchprox_action = calculate_menu.addAction("Calculate Branch Adjacency Network (Of Edges)")
3697
+ calc_branchprox_action.triggered.connect(self.handle_branchprox_calc)
3577
3698
  centroid_action = calculate_menu.addAction("Calculate Centroids (Active Image)")
3578
3699
  centroid_action.triggered.connect(self.show_centroid_dialog)
3579
3700
 
@@ -3597,13 +3718,17 @@ class ImageViewerWindow(QMainWindow):
3597
3718
  mask_action = image_menu.addAction("Mask Channel")
3598
3719
  mask_action.triggered.connect(self.show_mask_dialog)
3599
3720
  crop_action = image_menu.addAction("Crop Channels")
3600
- crop_action.triggered.connect(self.show_crop_dialog)
3721
+ crop_action.triggered.connect(lambda: self.show_crop_dialog(args = None))
3601
3722
  type_action = image_menu.addAction("Channel dtype")
3602
3723
  type_action.triggered.connect(self.show_type_dialog)
3603
3724
  skeletonize_action = image_menu.addAction("Skeletonize")
3604
3725
  skeletonize_action.triggered.connect(self.show_skeletonize_dialog)
3605
- watershed_action = image_menu.addAction("Watershed")
3726
+ dt_action = image_menu.addAction("Distance Transform (For binary images)")
3727
+ dt_action.triggered.connect(self.show_dt_dialog)
3728
+ watershed_action = image_menu.addAction("Binary Watershed")
3606
3729
  watershed_action.triggered.connect(self.show_watershed_dialog)
3730
+ gray_water_action = image_menu.addAction("Gray Watershed")
3731
+ gray_water_action.triggered.connect(self.show_gray_water_dialog)
3607
3732
  invert_action = image_menu.addAction("Invert")
3608
3733
  invert_action.triggered.connect(self.show_invert_dialog)
3609
3734
  z_proj_action = image_menu.addAction("Z Project")
@@ -3615,7 +3740,7 @@ class ImageViewerWindow(QMainWindow):
3615
3740
  gennodes_action = generate_menu.addAction("Generate Nodes (From 'Edge' Vertices)")
3616
3741
  gennodes_action.triggered.connect(self.show_gennodes_dialog)
3617
3742
  branch_action = generate_menu.addAction("Label Branches")
3618
- branch_action.triggered.connect(self.show_branch_dialog)
3743
+ branch_action.triggered.connect(lambda: self.show_branch_dialog())
3619
3744
  genvor_action = generate_menu.addAction("Generate Voronoi Diagram (From Node Centroids) - goes in Overlay2")
3620
3745
  genvor_action.triggered.connect(self.voronoi)
3621
3746
 
@@ -3919,6 +4044,10 @@ class ImageViewerWindow(QMainWindow):
3919
4044
  dialog = MergeNodeIdDialog(self)
3920
4045
  dialog.exec()
3921
4046
 
4047
+ def show_gray_water_dialog(self):
4048
+ """Show the gray watershed parameter dialog."""
4049
+ dialog = GrayWaterDialog(self)
4050
+ dialog.exec()
3922
4051
 
3923
4052
  def show_watershed_dialog(self):
3924
4053
  """Show the watershed parameter dialog."""
@@ -3950,6 +4079,110 @@ class ImageViewerWindow(QMainWindow):
3950
4079
  dialog = ProxDialog(self)
3951
4080
  dialog.exec()
3952
4081
 
4082
+ def table_load_attrs(self):
4083
+
4084
+ # Display network_lists in the network table
4085
+ try:
4086
+ if hasattr(my_network, 'network_lists'):
4087
+ model = PandasModel(my_network.network_lists)
4088
+ self.network_table.setModel(model)
4089
+ # Adjust column widths to content
4090
+ for column in range(model.columnCount(None)):
4091
+ self.network_table.resizeColumnToContents(column)
4092
+ except Exception as e:
4093
+ print(f"Error loading network_lists: {e}")
4094
+
4095
+ #Display the other things if they exist
4096
+ try:
4097
+
4098
+ if hasattr(my_network, 'node_identities') and my_network.node_identities is not None:
4099
+ try:
4100
+ self.format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity', 'Node Identities')
4101
+ except Exception as e:
4102
+ print(f"Error loading node identity table: {e}")
4103
+
4104
+ if hasattr(my_network, 'node_centroids') and my_network.node_centroids is not None:
4105
+ try:
4106
+ self.format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
4107
+ except Exception as e:
4108
+ print(f"Error loading node centroid table: {e}")
4109
+
4110
+
4111
+ if hasattr(my_network, 'edge_centroids') and my_network.edge_centroids is not None:
4112
+ try:
4113
+ self.format_for_upperright_table(my_network.edge_centroids, 'EdgeID', ['Z', 'Y', 'X'], 'Edge Centroids')
4114
+ except Exception as e:
4115
+ print(f"Error loading edge centroid table: {e}")
4116
+
4117
+
4118
+ except Exception as e:
4119
+ print(f"An error has occured: {e}")
4120
+
4121
+ def confirm_calcbranch_dialog(self, message):
4122
+ """Shows a dialog asking user to confirm if they want to proceed below"""
4123
+ msg = QMessageBox()
4124
+ msg.setIcon(QMessageBox.Icon.Question)
4125
+ msg.setText("Alert")
4126
+ msg.setInformativeText(message)
4127
+ msg.setWindowTitle("Proceed?")
4128
+ msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
4129
+ return msg.exec() == QMessageBox.StandardButton.Yes
4130
+
4131
+ def handle_calc_branch(self):
4132
+
4133
+ try:
4134
+
4135
+ if self.channel_data[0] is not None or self.channel_data[3] is not None:
4136
+ if not self.confirm_calcbranch_dialog("Use of this feature will require additional use of the Nodes and Overlay 2 channels. Please save any data and return, or proceed if you do not need those channels' data"):
4137
+ return
4138
+
4139
+ my_network.id_overlay = my_network.edges.copy()
4140
+
4141
+ self.show_gennodes_dialog()
4142
+
4143
+ my_network.edges = (my_network.nodes == 0) * my_network.edges
4144
+
4145
+ my_network.calculate_all(my_network.nodes, my_network.edges, xy_scale = my_network.xy_scale, z_scale = my_network.z_scale, search = None, diledge = None, inners = False, hash_inners = False, remove_trunk = 0, ignore_search_region = True, other_nodes = None, label_nodes = True, directory = None, GPU = False, fast_dil = False, skeletonize = False, GPU_downsample = None)
4146
+
4147
+ self.load_channel(1, my_network.edges, data = True)
4148
+ self.load_channel(0, my_network.nodes, data = True)
4149
+ self.load_channel(3, my_network.id_overlay, data = True)
4150
+
4151
+ self.table_load_attrs()
4152
+
4153
+ except Exception as e:
4154
+
4155
+ try:
4156
+ my_network.edges = my_network.id_overlay
4157
+ my_network.id_overlay = None
4158
+ except:
4159
+ pass
4160
+
4161
+ print(f"Error calculating branchpoint network: {e}")
4162
+
4163
+ def handle_branchprox_calc(self):
4164
+
4165
+ try:
4166
+
4167
+ if self.channel_data[0] is not None:
4168
+ if not self.confirm_calcbranch_dialog("Use of this feature will require additional use of the Nodes and Overlay 2 channels. Please save any data and return, or proceed if you do not need those channels' data"):
4169
+ return
4170
+
4171
+ self.show_branch_dialog(called = True)
4172
+
4173
+ self.load_channel(0, my_network.edges, data = True)
4174
+
4175
+ self.delete_channel(1, False)
4176
+
4177
+ my_network.morph_proximity(search = [3,3], fastdil = True)
4178
+
4179
+ self.table_load_attrs()
4180
+
4181
+ except Exception as e:
4182
+
4183
+ print(f"Error calculating network: {e}")
4184
+
4185
+
3953
4186
  def show_centroid_dialog(self):
3954
4187
  """show the centroid dialog"""
3955
4188
  dialog = CentroidDialog(self)
@@ -3994,9 +4227,9 @@ class ImageViewerWindow(QMainWindow):
3994
4227
  dialog = MaskDialog(self)
3995
4228
  dialog.exec()
3996
4229
 
3997
- def show_crop_dialog(self):
4230
+ def show_crop_dialog(self, args = None):
3998
4231
  """Show the crop dialog"""
3999
- dialog = CropDialog(self)
4232
+ dialog = CropDialog(self, args = args)
4000
4233
  dialog.exec()
4001
4234
 
4002
4235
  def show_type_dialog(self):
@@ -4012,6 +4245,11 @@ class ImageViewerWindow(QMainWindow):
4012
4245
  dialog = SkeletonizeDialog(self)
4013
4246
  dialog.exec()
4014
4247
 
4248
+ def show_dt_dialog(self):
4249
+ """show the dt dialog"""
4250
+ dialog = DistanceDialog(self)
4251
+ dialog.exec()
4252
+
4015
4253
  def show_centroid_node_dialog(self):
4016
4254
  """show the centroid node dialog"""
4017
4255
  dialog = CentroidNodeDialog(self)
@@ -4023,9 +4261,9 @@ class ImageViewerWindow(QMainWindow):
4023
4261
  gennodes = GenNodesDialog(self, down_factor = down_factor, called = called)
4024
4262
  gennodes.exec()
4025
4263
 
4026
- def show_branch_dialog(self):
4264
+ def show_branch_dialog(self, called = False):
4027
4265
  """Show the branch label dialog"""
4028
- dialog = BranchDialog(self)
4266
+ dialog = BranchDialog(self, called = called)
4029
4267
  dialog.exec()
4030
4268
 
4031
4269
  def voronoi(self):
@@ -4178,6 +4416,7 @@ class ImageViewerWindow(QMainWindow):
4178
4416
  if sort == 'Node Identities':
4179
4417
  my_network.load_node_identities(file_path = filename)
4180
4418
 
4419
+ """
4181
4420
  first_value = list(my_network.node_identities.values())[0] # Check that there are not multiple IDs
4182
4421
  if isinstance(first_value, (list, tuple)):
4183
4422
  trump_value, ok = QInputDialog.getText(
@@ -4199,7 +4438,7 @@ class ImageViewerWindow(QMainWindow):
4199
4438
  else:
4200
4439
  trump_value = None
4201
4440
  my_network.node_identities = uncork(my_network.node_identities, trump_value)
4202
-
4441
+ """
4203
4442
 
4204
4443
  if hasattr(my_network, 'node_identities') and my_network.node_identities is not None:
4205
4444
  try:
@@ -4354,7 +4593,7 @@ class ImageViewerWindow(QMainWindow):
4354
4593
  # Display network_lists in the network table
4355
4594
  # Create empty DataFrame for network table if network_lists is None
4356
4595
  if not hasattr(my_network, 'network_lists') or my_network.network_lists is None:
4357
- empty_df = pd.DataFrame(columns=['Node 1A', 'Node 1B', 'Edge 1C'])
4596
+ empty_df = pd.DataFrame(columns=['Node A', 'Node B', 'Edge C'])
4358
4597
  model = PandasModel(empty_df)
4359
4598
  self.network_table.setModel(model)
4360
4599
  else:
@@ -4439,7 +4678,7 @@ class ImageViewerWindow(QMainWindow):
4439
4678
  """Method to close Excelotron"""
4440
4679
  self.excel_manager.close()
4441
4680
 
4442
- def handle_excel_data(self, data_dict, property_name):
4681
+ def handle_excel_data(self, data_dict, property_name, add):
4443
4682
  """Handle data received from Excelotron"""
4444
4683
  print(f"Received data for property: {property_name}")
4445
4684
  print(f"Data keys: {list(data_dict.keys())}")
@@ -4448,12 +4687,19 @@ class ImageViewerWindow(QMainWindow):
4448
4687
 
4449
4688
  try:
4450
4689
 
4690
+ if not add or my_network.node_centroids is None:
4691
+ centroids = {}
4692
+ max_val = 0
4693
+ else:
4694
+ centroids = my_network.node_centroids
4695
+ max_val = max(list(my_network.node_centroids.keys()))
4696
+
4451
4697
  ys = data_dict['Y']
4452
4698
  xs = data_dict['X']
4453
4699
  if 'Numerical IDs' in data_dict:
4454
4700
  nodes = data_dict['Numerical IDs']
4455
4701
  else:
4456
- nodes = np.arange(1, len(ys) + 1)
4702
+ nodes = np.arange(max_val + 1, max_val + len(ys) + 1)
4457
4703
 
4458
4704
 
4459
4705
  if 'Z' in data_dict:
@@ -4461,8 +4707,6 @@ class ImageViewerWindow(QMainWindow):
4461
4707
  else:
4462
4708
  zs = np.zeros(len(ys))
4463
4709
 
4464
- centroids = {}
4465
-
4466
4710
  for i in range(len(nodes)):
4467
4711
 
4468
4712
  centroids[nodes[i]] = [int(zs[i]), int(ys[i]), int(xs[i])]
@@ -4480,15 +4724,26 @@ class ImageViewerWindow(QMainWindow):
4480
4724
 
4481
4725
  try:
4482
4726
 
4727
+ if not add or my_network.node_identities is None:
4728
+ identities = {}
4729
+ max_val = 0
4730
+ else:
4731
+ identities = my_network.node_identities
4732
+ if my_network.node_centroids is not None:
4733
+ max_val = max(list(my_network.node_centroids.keys()))
4734
+ else:
4735
+ max_val = max(list(my_network.node_identities.keys()))
4736
+
4483
4737
  idens = data_dict['Identity Column']
4484
4738
 
4485
4739
  if 'Numerical IDs' in data_dict:
4486
4740
  nodes = data_dict['Numerical IDs']
4487
- else:
4488
- nodes = np.arange(1, len(idens) + 1)
4489
-
4490
- identities = {}
4741
+ if add:
4742
+ for i, node in enumerate(nodes):
4743
+ nodes[i] = node + max_val
4491
4744
 
4745
+ else:
4746
+ nodes = np.arange(max_val + 1, max_val + len(data_dict['Identity Column']) + 1)
4492
4747
 
4493
4748
  for i in range(len(nodes)):
4494
4749
 
@@ -4507,14 +4762,20 @@ class ImageViewerWindow(QMainWindow):
4507
4762
 
4508
4763
  try:
4509
4764
 
4765
+ if not add or my_network.communities is None:
4766
+ communities = {}
4767
+ max_val = 0
4768
+ else:
4769
+ communities = my_network.communities
4770
+ max_val = max(list(my_network.communities.keys()))
4771
+
4772
+
4510
4773
  coms = data_dict['Community Identifier']
4511
4774
 
4512
4775
  if 'Numerical IDs' in data_dict:
4513
4776
  nodes = data_dict['Numerical IDs']
4514
4777
  else:
4515
- nodes = np.arange(1, len(coms) + 1)
4516
-
4517
- communities = {}
4778
+ nodes = np.arange(max_val + 1, max_val + len(data_dict['Community Identifier']) + 1)
4518
4779
 
4519
4780
  for i in range(len(nodes)):
4520
4781
 
@@ -4627,7 +4888,6 @@ class ImageViewerWindow(QMainWindow):
4627
4888
  import tifffile
4628
4889
  self.channel_data[channel_index] = tifffile.imread(filename)
4629
4890
 
4630
-
4631
4891
  elif file_extension == 'nii':
4632
4892
  import nibabel as nib
4633
4893
  nii_img = nib.load(filename)
@@ -4657,9 +4917,20 @@ class ImageViewerWindow(QMainWindow):
4657
4917
  self.channel_buttons[channel_index].setEnabled(False)
4658
4918
  self.delete_buttons[channel_index].setEnabled(False)
4659
4919
 
4920
+ try:
4921
+ #if len(self.channel_data[channel_index].shape) == 4:
4922
+ if 1 in self.channel_data[channel_index].shape:
4923
+ print("Removing singleton dimension (I am assuming this is a channel dimension?)")
4924
+ self.channel_data[channel_index] = np.squeeze(self.channel_data[channel_index])
4925
+ except:
4926
+ pass
4927
+
4660
4928
  if len(self.channel_data[channel_index].shape) == 2: # handle 2d data
4661
4929
  self.channel_data[channel_index] = np.expand_dims(self.channel_data[channel_index], axis=0)
4662
4930
 
4931
+ if self.channel_data[channel_index].dtype == np.bool_: #Promote boolean arrays if they somehow get loaded
4932
+ self.channel_data[channel_index] = self.channel_data[channel_index].astype(np.uint8)
4933
+
4663
4934
  try:
4664
4935
  if len(self.channel_data[channel_index].shape) == 3: # potentially 2D RGB
4665
4936
  if self.channel_data[channel_index].shape[-1] in (3, 4): # last dim is 3 or 4
@@ -4668,6 +4939,7 @@ class ImageViewerWindow(QMainWindow):
4668
4939
  self.channel_data[channel_index] = np.expand_dims(self.channel_data[channel_index], axis=0)
4669
4940
  except:
4670
4941
  pass
4942
+
4671
4943
 
4672
4944
  try:
4673
4945
  if len(self.channel_data[channel_index].shape) == 4 and (channel_index == 0 or channel_index == 1):
@@ -4748,8 +5020,13 @@ class ImageViewerWindow(QMainWindow):
4748
5020
  if all(not btn.isEnabled() for btn in self.channel_buttons[:channel_index]):
4749
5021
  self.set_active_channel(channel_index)
4750
5022
 
4751
- if not self.channel_buttons[channel_index].isChecked():
4752
- self.channel_buttons[channel_index].click()
5023
+ if self.chan_load:
5024
+ if not self.channel_buttons[channel_index].isChecked():
5025
+ self.channel_buttons[channel_index].click()
5026
+ else:
5027
+ if self.channel_buttons[channel_index].isChecked():
5028
+ self.channel_buttons[channel_index].click()
5029
+
4753
5030
  self.min_max[channel_index][0] = np.min(self.channel_data[channel_index])
4754
5031
  self.min_max[channel_index][1] = np.max(self.channel_data[channel_index])
4755
5032
  self.volume_dict[channel_index] = None #reset volumes
@@ -4849,7 +5126,7 @@ class ImageViewerWindow(QMainWindow):
4849
5126
  my_network.communities = None
4850
5127
 
4851
5128
  # Create empty DataFrame
4852
- empty_df = pd.DataFrame(columns=['Node 1A', 'Node 1B', 'Edge 1C'])
5129
+ empty_df = pd.DataFrame(columns=['Node A', 'Node B', 'Edge C'])
4853
5130
 
4854
5131
  # Clear network table
4855
5132
  self.network_table.setModel(PandasModel(empty_df))
@@ -6009,9 +6286,9 @@ class PandasModel(QAbstractTableModel):
6009
6286
  if data is None:
6010
6287
  # Create an empty DataFrame with default columns
6011
6288
  import pandas as pd
6012
- data = pd.DataFrame(columns=['Node 1A', 'Node 1B', 'Edge 1C'])
6289
+ data = pd.DataFrame(columns=['Node A', 'Node B', 'Edge C'])
6013
6290
  elif type(data) == list:
6014
- data = self.lists_to_dataframe(data[0], data[1], data[2], column_names=['Node 1A', 'Node 1B', 'Edge 1C'])
6291
+ data = self.lists_to_dataframe(data[0], data[1], data[2], column_names=['Node A', 'Node B', 'Edge C'])
6015
6292
  self._data = data
6016
6293
  self.bold_cells = set()
6017
6294
  self.highlighted_cells = set()
@@ -6752,7 +7029,7 @@ class MergeNodeIdDialog(QDialog):
6752
7029
 
6753
7030
  self.parent().format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity')
6754
7031
 
6755
- QMessageBox.critical(
7032
+ QMessageBox.information(
6756
7033
  self,
6757
7034
  "Success",
6758
7035
  "Node Identities Merged. New IDs represent presence of corresponding img foreground with +, absence with -. Please save your new identities as csv, then use File -> Load -> Load From Excel Helper to bulk search and rename desired combinations. (Press Help [above] for more info)"
@@ -7211,6 +7488,8 @@ class PartitionDialog(QDialog):
7211
7488
 
7212
7489
  def partition(self):
7213
7490
 
7491
+ self.parent().prev_coms = None
7492
+
7214
7493
  accepted_mode = self.mode_selector.currentIndex()
7215
7494
  weighted = self.weighted.isChecked()
7216
7495
  dostats = self.stats.isChecked()
@@ -7393,7 +7672,7 @@ class ComNeighborDialog(QDialog):
7393
7672
  self.parent().format_for_upperright_table(matrix, 'NeighborhoodID', id_set, title = f'Neighborhood Heatmap {i + 1}')
7394
7673
 
7395
7674
 
7396
- self.parent().format_for_upperright_table(len_dict, 'NeighborhoodID', 'Proportion of Total Nodes', title = 'Neighborhood Counts')
7675
+ self.parent().format_for_upperright_table(len_dict, 'NeighborhoodID', ['Number of Communities', 'Proportion of Total Nodes'], title = 'Neighborhood Counts')
7397
7676
  self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'NeighborhoodID', title = 'Neighborhood Partition')
7398
7677
 
7399
7678
  print("Neighborhoods have been assigned to communities based on similarity")
@@ -7435,6 +7714,8 @@ class ComCellDialog(QDialog):
7435
7714
 
7436
7715
  try:
7437
7716
 
7717
+ self.parent().prev_coms = None
7718
+
7438
7719
  size = float(self.size.text()) if self.size.text().strip() else None
7439
7720
  xy_scale = float(self.xy_scale.text()) if self.xy_scale.text().strip() else 1
7440
7721
  z_scale = float(self.z_scale.text()) if self.z_scale.text().strip() else 1
@@ -7596,6 +7877,16 @@ class NearNeighDialog(QDialog):
7596
7877
  heatmap_layout.addRow("Overlay:", self.numpy)
7597
7878
 
7598
7879
  main_layout.addWidget(heatmap_group)
7880
+
7881
+ quant_group = QGroupBox("Quantifiable Overlay")
7882
+ quant_layout = QFormLayout(quant_group)
7883
+
7884
+ self.quant = QPushButton("Return quantifiable overlay? (Labels nodes by distance, good with intensity-thresholding to isolate targets. Requires labeled nodes image.)")
7885
+ self.quant.setCheckable(True)
7886
+ self.quant.setChecked(False)
7887
+ quant_layout.addRow("Overlay:", self.quant)
7888
+
7889
+ main_layout.addWidget(quant_group)
7599
7890
 
7600
7891
  # Get Distribution group box
7601
7892
  distribution_group = QGroupBox("Get Distribution")
@@ -7643,6 +7934,7 @@ class NearNeighDialog(QDialog):
7643
7934
  threed = self.threed.isChecked()
7644
7935
  numpy = self.numpy.isChecked()
7645
7936
  num = int(self.num.text()) if self.num.text().strip() else 1
7937
+ quant = self.quant.isChecked()
7646
7938
 
7647
7939
  if root is not None and targ is not None:
7648
7940
  title = f"Nearest {num} Neighbor(s) Distance of {targ} from {root}"
@@ -7659,11 +7951,14 @@ class NearNeighDialog(QDialog):
7659
7951
  return
7660
7952
 
7661
7953
  if not numpy:
7662
- avg, output = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, heatmap = heatmap, threed = threed)
7954
+ avg, output, quant_overlay = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, heatmap = heatmap, threed = threed, quant = quant)
7663
7955
  else:
7664
- avg, output, overlay = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, heatmap = heatmap, threed = threed, numpy = True)
7956
+ avg, output, overlay, quant_overlay = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, heatmap = heatmap, threed = threed, numpy = True, quant = quant)
7665
7957
  self.parent().load_channel(3, overlay, data = True)
7666
7958
 
7959
+ if quant_overlay is not None:
7960
+ self.parent().load_channel(2, quant_overlay, data = True)
7961
+
7667
7962
  self.parent().format_for_upperright_table([avg], metric = f'Avg {title}', title = f'Avg {title}')
7668
7963
  self.parent().format_for_upperright_table(output, header2, header, title = title)
7669
7964
 
@@ -7691,7 +7986,7 @@ class NearNeighDialog(QDialog):
7691
7986
 
7692
7987
  for targ in available:
7693
7988
 
7694
- avg, _ = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num)
7989
+ avg, _, _ = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num)
7695
7990
 
7696
7991
  output_dict[f"{root} vs {targ}"] = avg
7697
7992
 
@@ -7781,52 +8076,86 @@ class NeighborIdentityDialog(QDialog):
7781
8076
 
7782
8077
 
7783
8078
  class RipleyDialog(QDialog):
7784
-
7785
8079
  def __init__(self, parent=None):
7786
-
7787
8080
  super().__init__(parent)
7788
8081
  self.setWindowTitle(f"Find Ripley's H Function From Centroids")
7789
8082
  self.setModal(True)
7790
-
7791
- layout = QFormLayout(self)
7792
-
8083
+
8084
+ # Main layout
8085
+ main_layout = QVBoxLayout(self)
8086
+
8087
+ # Node Parameters Group (only if node_identities exist)
7793
8088
  if my_network.node_identities is not None:
8089
+ node_group = QGroupBox("Node Parameters")
8090
+ node_layout = QFormLayout(node_group)
8091
+
7794
8092
  self.root = QComboBox()
7795
8093
  self.root.addItems(list(set(my_network.node_identities.values())))
7796
8094
  self.root.setCurrentIndex(0)
7797
- layout.addRow("Root Identity to Search for Neighbors", self.root)
7798
- else:
7799
- self.root = None
7800
-
7801
- if my_network.node_identities is not None:
8095
+ node_layout.addRow("Root Identity to Search for Neighbors:", self.root)
8096
+
7802
8097
  self.targ = QComboBox()
7803
8098
  self.targ.addItems(list(set(my_network.node_identities.values())))
7804
8099
  self.targ.setCurrentIndex(0)
7805
- layout.addRow("Targ Identity to be Searched For", self.targ)
8100
+ node_layout.addRow("Target Identity to be Searched For:", self.targ)
8101
+
8102
+ main_layout.addWidget(node_group)
7806
8103
  else:
8104
+ self.root = None
7807
8105
  self.targ = None
7808
-
8106
+
8107
+ # Search Parameters Group
8108
+ search_group = QGroupBox("Search Parameters")
8109
+ search_layout = QFormLayout(search_group)
8110
+
7809
8111
  self.distance = QLineEdit("5")
7810
- layout.addRow("Bucket Distance for Searching For Clusters (automatically scaled by xy and z scales):", self.distance)
7811
-
7812
-
8112
+ search_layout.addRow("1. Bucket Distance for Searching For Clusters\n(automatically scaled by xy and z scales):", self.distance)
8113
+
7813
8114
  self.proportion = QLineEdit("0.5")
7814
- layout.addRow("Proportion of image to search? (0-1, high vals increase border artifacts): ", self.proportion)
7815
-
8115
+ search_layout.addRow("2. Proportion of image to search?\n(0-1, high vals increase border artifacts):", self.proportion)
8116
+
8117
+ main_layout.addWidget(search_group)
8118
+
8119
+ # Border Safety Group
8120
+ border_group = QGroupBox("Border Safety")
8121
+ border_layout = QFormLayout(border_group)
8122
+
8123
+ self.ignore = QPushButton("Ignore Border Roots")
8124
+ self.ignore.setCheckable(True)
8125
+ self.ignore.setChecked(True)
8126
+ border_layout.addRow("3. Exclude Root Nodes Near Borders?:", self.ignore)
8127
+
8128
+ self.factor = QLineEdit("0.5")
8129
+ border_layout.addRow("4. (If param 3): Proportion of most internal nodes to use? (0 < n < 1) (Higher = more internal)?:", self.factor)
8130
+
8131
+ self.mode = QComboBox()
8132
+ self.mode.addItems(["Boundaries of Entire Image", "Boundaries of Edge Image Mask",
8133
+ "Boundaries of Overlay1 Mask", "Boundaries of Overlay2 Mask"])
8134
+ self.mode.setCurrentIndex(0)
8135
+ border_layout.addRow("5. (If param 3): Define Boundaries How?:", self.mode)
8136
+
8137
+ self.safe = QPushButton("Ignore Border Radii")
8138
+ self.safe.setCheckable(True)
8139
+ self.safe.setChecked(True)
8140
+ border_layout.addRow("6. (If param 3): Keep search radii within border (overrides Param 2, also assigns volume to that of mask)?:", self.safe)
8141
+
8142
+ main_layout.addWidget(border_group)
8143
+
8144
+ # Experimental Border Safety Group
8145
+ experimental_group = QGroupBox("Aggressive Border Safety (Creates duplicate centroids reflected across the image border - if you really need to search there for whatever reason - Not meant to be used if confining search to a masked object)")
8146
+ experimental_layout = QFormLayout(experimental_group)
8147
+
7816
8148
  self.edgecorrect = QPushButton("Border Correction")
7817
8149
  self.edgecorrect.setCheckable(True)
7818
8150
  self.edgecorrect.setChecked(False)
7819
- layout.addRow("Use Border Correction (Extrapolate for points beyond the border):", self.edgecorrect)
7820
-
7821
- self.ignore = QPushButton("Ignore Border Roots")
7822
- self.ignore.setCheckable(True)
7823
- self.ignore.setChecked(False)
7824
- layout.addRow("Exclude Root Nodes Near Borders?:", self.ignore)
7825
-
8151
+ experimental_layout.addRow("7. Use Border Correction\n(Extrapolate for points beyond the border):", self.edgecorrect)
8152
+
8153
+ main_layout.addWidget(experimental_group)
8154
+
7826
8155
  # Add Run button
7827
8156
  run_button = QPushButton("Get Ripley's H")
7828
8157
  run_button.clicked.connect(self.ripley)
7829
- layout.addWidget(run_button)
8158
+ main_layout.addWidget(run_button)
7830
8159
 
7831
8160
  def ripley(self):
7832
8161
 
@@ -7856,6 +8185,16 @@ class RipleyDialog(QDialog):
7856
8185
  except:
7857
8186
  proportion = 0.5
7858
8187
 
8188
+ try:
8189
+ factor = abs(float(self.factor.text()))
8190
+
8191
+ except:
8192
+ factor = 0.25
8193
+
8194
+ if factor > 1 or factor <= 0:
8195
+ print("Utilizing factor = 0.25")
8196
+ factor = 0.25
8197
+
7859
8198
  if proportion > 1 or proportion <= 0:
7860
8199
  print("Utilizing proportion = 0.5")
7861
8200
  proportion = 0.5
@@ -7865,6 +8204,13 @@ class RipleyDialog(QDialog):
7865
8204
 
7866
8205
  ignore = self.ignore.isChecked()
7867
8206
 
8207
+ safe = self.safe.isChecked()
8208
+
8209
+ mode = self.mode.currentIndex()
8210
+
8211
+ if mode == 0:
8212
+ factor = factor/2 #The logic treats this as distance to border later, only if mode is 0, but its supposed to represent proportion internal.
8213
+
7868
8214
  if my_network.nodes is not None:
7869
8215
 
7870
8216
  if my_network.nodes.shape[0] == 1:
@@ -7874,7 +8220,7 @@ class RipleyDialog(QDialog):
7874
8220
  else:
7875
8221
  bounds = None
7876
8222
 
7877
- r_vals, k_vals, h_vals = my_network.get_ripley(root, targ, distance, edgecorrect, bounds, ignore, proportion)
8223
+ r_vals, k_vals, h_vals = my_network.get_ripley(root, targ, distance, edgecorrect, bounds, ignore, proportion, mode, safe, factor)
7878
8224
 
7879
8225
  k_dict = dict(zip(r_vals, k_vals))
7880
8226
  h_dict = dict(zip(r_vals, h_vals))
@@ -7887,6 +8233,9 @@ class RipleyDialog(QDialog):
7887
8233
  self.accept()
7888
8234
 
7889
8235
  except Exception as e:
8236
+ import traceback
8237
+ print(traceback.format_exc())
8238
+
7890
8239
  QMessageBox.critical(
7891
8240
  self,
7892
8241
  "Error:",
@@ -8629,10 +8978,10 @@ class ResizeDialog(QDialog):
8629
8978
  else:
8630
8979
  new_shape = tuple(int(dim * factor) for dim, factor in zip(array_shape, resize))
8631
8980
 
8632
- if any(dim < 1 for dim in new_shape):
8633
- QMessageBox.critical(self, "Error", f"Resize would result in invalid dimensions: {new_shape}")
8634
- self.reset_fields()
8635
- return
8981
+ #if any(dim < 1 for dim in new_shape):
8982
+ #QMessageBox.critical(self, "Error", f"Resize would result in invalid dimensions: {new_shape}")
8983
+ #self.reset_fields()
8984
+ #return
8636
8985
 
8637
8986
  cubic = self.cubic.isChecked()
8638
8987
  order = 3 if cubic else 0
@@ -8706,7 +9055,12 @@ class ResizeDialog(QDialog):
8706
9055
  centroids = copy.deepcopy(my_network.node_centroids)
8707
9056
  if isinstance(resize, (int, float)):
8708
9057
  for item in my_network.node_centroids:
8709
- centroids[item] = np.round((my_network.node_centroids[item]) * resize)
9058
+ try:
9059
+ centroids[item] = np.round((my_network.node_centroids[item]) * resize)
9060
+ except:
9061
+ temp = np.array(my_network.node_centroids[item])
9062
+ centroids[item] = np.round((temp) * resize)
9063
+
8710
9064
  else:
8711
9065
  for item in my_network.node_centroids:
8712
9066
  centroids[item][0] = int(np.round((my_network.node_centroids[item][0]) * resize[0]))
@@ -9188,13 +9542,14 @@ class ThresholdDialog(QDialog):
9188
9542
 
9189
9543
  class ExcelotronManager(QObject):
9190
9544
  # Signal to emit when data is received from Excelotron
9191
- data_received = pyqtSignal(dict, str) # dictionary, property_name
9545
+ data_received = pyqtSignal(dict, str, bool) # dictionary, property_name
9192
9546
 
9193
9547
  def __init__(self, parent=None):
9194
9548
  super().__init__(parent)
9195
9549
  self.excelotron_window = None
9196
9550
  self.last_data = None
9197
9551
  self.last_property = None
9552
+ self.last_add = None
9198
9553
 
9199
9554
  def launch(self):
9200
9555
  """Launch the Excelotron window"""
@@ -9243,12 +9598,13 @@ class ExcelotronManager(QObject):
9243
9598
  is_open = self.excelotron_window is not None
9244
9599
  return is_open
9245
9600
 
9246
- def _on_data_exported(self, data_dict, property_name):
9601
+ def _on_data_exported(self, data_dict, property_name, add):
9247
9602
  """Internal slot to handle data from Excelotron"""
9248
9603
  self.last_data = data_dict
9249
9604
  self.last_property = property_name
9605
+ self.last_add = add
9250
9606
  # Re-emit the signal for parent to handle
9251
- self.data_received.emit(data_dict, property_name)
9607
+ self.data_received.emit(data_dict, property_name, add)
9252
9608
 
9253
9609
  def _on_window_destroyed(self):
9254
9610
  """Handle when the Excelotron window is destroyed/closed"""
@@ -9256,7 +9612,7 @@ class ExcelotronManager(QObject):
9256
9612
 
9257
9613
  def get_last_data(self):
9258
9614
  """Get the last exported data"""
9259
- return self.last_data, self.last_property
9615
+ return self.last_data, self.last_property, self.last_add
9260
9616
 
9261
9617
  class MachineWindow(QMainWindow):
9262
9618
 
@@ -10780,7 +11136,7 @@ class MaskDialog(QDialog):
10780
11136
 
10781
11137
  class CropDialog(QDialog):
10782
11138
 
10783
- def __init__(self, parent=None):
11139
+ def __init__(self, parent=None, args = None):
10784
11140
 
10785
11141
  try:
10786
11142
 
@@ -10788,18 +11144,26 @@ class CropDialog(QDialog):
10788
11144
  self.setWindowTitle("Crop Image (Will transpose any centroids)?")
10789
11145
  self.setModal(True)
10790
11146
 
11147
+ if args is None:
11148
+ xmin = 0
11149
+ xmax = self.parent().shape[2]
11150
+ ymin = 0
11151
+ ymax = self.parent().shape[1]
11152
+ else:
11153
+ xmin, xmax, ymin, ymax = args
11154
+
10791
11155
  layout = QFormLayout(self)
10792
11156
 
10793
- self.xmin = QLineEdit("0")
11157
+ self.xmin = QLineEdit(f"{xmin}")
10794
11158
  layout.addRow("X Min", self.xmin)
10795
11159
 
10796
- self.xmax = QLineEdit(f"{self.parent().shape[2]}")
11160
+ self.xmax = QLineEdit(f"{xmax}")
10797
11161
  layout.addRow("X Max", self.xmax)
10798
11162
 
10799
- self.ymin = QLineEdit("0")
11163
+ self.ymin = QLineEdit(f"{ymin}")
10800
11164
  layout.addRow("Y Min", self.ymin)
10801
11165
 
10802
- self.ymax = QLineEdit(f"{self.parent().shape[1]}")
11166
+ self.ymax = QLineEdit(f"{ymax}")
10803
11167
  layout.addRow("Y Max", self.ymax)
10804
11168
 
10805
11169
  self.zmin = QLineEdit("0")
@@ -10853,10 +11217,16 @@ class CropDialog(QDialog):
10853
11217
  transformed = centroids - np.array([zmin, ymin, xmin])
10854
11218
  transformed = transformed.astype(int)
10855
11219
 
10856
- # Boolean mask for valid coordinates
10857
- valid_mask = ((transformed >= 0) &
10858
- (transformed <= np.array([zmax, ymax, xmax]))).all(axis=1)
11220
+ # Create upper bounds array with same shape
11221
+ upper_bounds = np.array([zmax - zmin, ymax - ymin, xmax - xmin])
11222
+
11223
+ # Boolean mask for valid coordinates - check each dimension separately
11224
+ z_valid = (transformed[:, 0] >= 0) & (transformed[:, 0] <= upper_bounds[0])
11225
+ y_valid = (transformed[:, 1] >= 0) & (transformed[:, 1] <= upper_bounds[1])
11226
+ x_valid = (transformed[:, 2] >= 0) & (transformed[:, 2] <= upper_bounds[2])
10859
11227
 
11228
+ valid_mask = z_valid & y_valid & x_valid
11229
+
10860
11230
  # Rebuild dictionary with only valid entries
10861
11231
  my_network.node_centroids = {
10862
11232
  nodes[int(i)]: [int(transformed[i, 0]), int(transformed[i, 1]), int(transformed[i, 2])]
@@ -10865,6 +11235,15 @@ class CropDialog(QDialog):
10865
11235
 
10866
11236
  self.parent().format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
10867
11237
 
11238
+ if my_network.node_identities is not None:
11239
+ new_idens = {}
11240
+ for node, iden in my_network.node_identities.items():
11241
+ if node in my_network.node_centroids:
11242
+ new_idens[node] = iden
11243
+ my_network.node_identities = new_idens
11244
+
11245
+ self.parent().format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity', 'Node Identities')
11246
+
10868
11247
  except Exception as e:
10869
11248
 
10870
11249
  print(f"Error transposing node centroids: {e}")
@@ -11058,6 +11437,74 @@ class SkeletonizeDialog(QDialog):
11058
11437
  f"Error running skeletonize: {str(e)}"
11059
11438
  )
11060
11439
 
11440
+ class DistanceDialog(QDialog):
11441
+ def __init__(self, parent=None):
11442
+ super().__init__(parent)
11443
+ self.setWindowTitle("Compute Distance Transform (Applies xy and z scaling, set them to 1 if you want voxel correspondence)?")
11444
+ self.setModal(True)
11445
+
11446
+ layout = QFormLayout(self)
11447
+
11448
+ # Add Run button
11449
+ run_button = QPushButton("Run")
11450
+ run_button.clicked.connect(self.run)
11451
+ layout.addRow(run_button)
11452
+
11453
+ def run(self):
11454
+
11455
+ try:
11456
+
11457
+ data = self.parent().channel_data[self.parent().active_channel]
11458
+
11459
+ data = sdl.compute_distance_transform_distance(data, sampling = [my_network.z_scale, my_network.xy_scale, my_network.xy_scale])
11460
+
11461
+ self.parent().load_channel(self.parent().active_channel, data, data = True)
11462
+
11463
+ except Exception as e:
11464
+
11465
+ print(f"Error: {e}")
11466
+
11467
+ class GrayWaterDialog(QDialog):
11468
+ def __init__(self, parent=None):
11469
+ super().__init__(parent)
11470
+ self.setWindowTitle(f"Gray Watershed - Please segment out your background first (ie with intensity thresholding) or this will not work correctly. \nAt the moment, this is designed for similarly sized objects. Having mixed large/small objects may not work correctly.")
11471
+ self.setModal(True)
11472
+
11473
+ layout = QFormLayout(self)
11474
+
11475
+ self.min_peak_distance = QLineEdit("1")
11476
+ layout.addRow("Minimum Peak Distance (To any other peak - Recommended) (This is true voxel distance here)", self.min_peak_distance)
11477
+
11478
+ # Minimum Intensity
11479
+ self.min_intensity = QLineEdit("")
11480
+ layout.addRow("Minimum Peak Intensity (Optional):", self.min_intensity)
11481
+
11482
+ # Add Run button
11483
+ run_button = QPushButton("Run Watershed")
11484
+ run_button.clicked.connect(self.run_watershed)
11485
+ layout.addRow(run_button)
11486
+
11487
+ def run_watershed(self):
11488
+
11489
+ try:
11490
+
11491
+ min_intensity = float(self.min_intensity.text()) if self.min_intensity.text().strip() else None
11492
+
11493
+ min_peak_distance = int(self.min_peak_distance.text()) if self.min_peak_distance.text().strip() else 1
11494
+
11495
+ data = self.parent().channel_data[self.parent().active_channel]
11496
+
11497
+ data = n3d.gray_watershed(data, min_peak_distance, min_intensity)
11498
+
11499
+ self.parent().load_channel(self.parent().active_channel, data, data = True)
11500
+
11501
+ self.accept()
11502
+
11503
+ except Exception as e:
11504
+ print(f"Error: {e}")
11505
+
11506
+
11507
+
11061
11508
 
11062
11509
  class WatershedDialog(QDialog):
11063
11510
  def __init__(self, parent=None):
@@ -11084,10 +11531,14 @@ class WatershedDialog(QDialog):
11084
11531
  except:
11085
11532
  self.default = 0.05
11086
11533
 
11534
+ # Smallest radius (empty by default)
11535
+ self.smallest_rad = QLineEdit()
11536
+ self.smallest_rad.setPlaceholderText("Leave empty for None")
11537
+ layout.addRow(f"Smallest Radius (Objects any smaller may get thresholded out - this value always overrides below 'proportion' param). \n Somewhat more intuitive param then below, use a conservative value a bit smaller than your smallest object's radius:", self.smallest_rad)
11087
11538
 
11088
11539
  # Proportion (default 0.1)
11089
11540
  self.proportion = QLineEdit(f"{self.default}")
11090
- layout.addRow("Proportion:", self.proportion)
11541
+ layout.addRow(f"Proportion (0-1) of distance transform value set [ie unique elements] to exclude (ie 0.2 = 20% of the set of all values of the distance transform get excluded).\n Essentially, vals closer to 0 are less likely to split objects but also won't kick out small objects from the output, vals slightly further from 0 will split more aggressively, but vals closer to 1 become unstable, leading to objects being evicted or labelling errors. \nRecommend something between 0.05 and 0.4, but it depends on the data (Or just enter a smallest radius above to avoid using this). \nWill tell you in command window what equivalent 'smallest radius' this is):", self.proportion)
11091
11542
 
11092
11543
  # GPU checkbox (default True)
11093
11544
  self.gpu = QPushButton("GPU")
@@ -11095,10 +11546,6 @@ class WatershedDialog(QDialog):
11095
11546
  self.gpu.setChecked(False)
11096
11547
  layout.addRow("Use GPU:", self.gpu)
11097
11548
 
11098
- # Smallest radius (empty by default)
11099
- self.smallest_rad = QLineEdit()
11100
- self.smallest_rad.setPlaceholderText("Leave empty for None")
11101
- layout.addRow("Smallest Radius:", self.smallest_rad)
11102
11549
 
11103
11550
  # Predownsample (empty by default)
11104
11551
  self.predownsample = QLineEdit()
@@ -11106,11 +11553,11 @@ class WatershedDialog(QDialog):
11106
11553
  layout.addRow("Kernel Obtainment GPU Downsample:", self.predownsample)
11107
11554
 
11108
11555
  # Predownsample2 (empty by default)
11109
- self.predownsample2 = QLineEdit()
11110
- self.predownsample2.setPlaceholderText("Leave empty for None")
11111
- layout.addRow("Smart Label GPU Downsample:", self.predownsample2)
11556
+ #self.predownsample2 = QLineEdit()
11557
+ #self.predownsample2.setPlaceholderText("Leave empty for None")
11558
+ #layout.addRow("Smart Label GPU Downsample:", self.predownsample2)
11112
11559
 
11113
- layout.addRow("Note:", QLabel(f"If the optimal proportion watershed output is still labeling spatially seperated objects with the same label, try right placing the result in nodes or edges\nthen right click the image and choose 'select all', followed by right clicking and 'selection' -> 'split non-touching labels'."))
11560
+ #layout.addRow("Note:", QLabel(f"If the optimal proportion watershed output is still labeling spatially seperated objects with the same label, try right placing the result in nodes or edges\nthen right click the image and choose 'select all', followed by right clicking and 'selection' -> 'split non-touching labels'."))
11114
11561
 
11115
11562
 
11116
11563
  # Add Run button
@@ -11147,7 +11594,7 @@ class WatershedDialog(QDialog):
11147
11594
  # Get predownsample2 (None if empty)
11148
11595
  try:
11149
11596
  predownsample2 = float(self.predownsample2.text()) if self.predownsample2.text() else None
11150
- except ValueError:
11597
+ except:
11151
11598
  predownsample2 = None
11152
11599
 
11153
11600
  # Get the active channel data from parent
@@ -11325,7 +11772,22 @@ class CentroidNodeDialog(QDialog):
11325
11772
 
11326
11773
  if mode == 0:
11327
11774
 
11328
- my_network.nodes = my_network.centroid_array()
11775
+ try:
11776
+ shape = my_network.nodes.shape
11777
+
11778
+ except:
11779
+ try:
11780
+ shape = my_network.edges.shape
11781
+ except:
11782
+ try:
11783
+ shape = my_network.network_overlay.shape
11784
+ except:
11785
+ try:
11786
+ shape = my_network.id_overlay.shape
11787
+ except:
11788
+ shape = None
11789
+
11790
+ my_network.nodes = my_network.centroid_array(shape = shape)
11329
11791
 
11330
11792
  else:
11331
11793
 
@@ -11439,7 +11901,7 @@ class GenNodesDialog(QDialog):
11439
11901
 
11440
11902
  # Component dilation
11441
11903
  self.comp_dil = QLineEdit("0")
11442
- opt_layout.addWidget(QLabel("Voxel distance to merge nearby nodes (Compensates for multi-branch regions):"), 1, 0)
11904
+ opt_layout.addWidget(QLabel("Amount to expand nodes (Merges nearby nodes, say if they are overassigned, good for broader branch breaking):"), 1, 0)
11443
11905
  opt_layout.addWidget(self.comp_dil, 1, 1)
11444
11906
 
11445
11907
  opt_group.setLayout(opt_layout)
@@ -11573,11 +12035,11 @@ class GenNodesDialog(QDialog):
11573
12035
 
11574
12036
  class BranchDialog(QDialog):
11575
12037
 
11576
- def __init__(self, parent=None):
12038
+ def __init__(self, parent=None, called = False):
11577
12039
  super().__init__(parent)
11578
12040
  self.setWindowTitle("Label Branches (of edges)")
11579
12041
  self.setModal(True)
11580
-
12042
+
11581
12043
  # Main layout
11582
12044
  main_layout = QVBoxLayout(self)
11583
12045
 
@@ -11610,7 +12072,10 @@ class BranchDialog(QDialog):
11610
12072
 
11611
12073
  self.fix3 = QPushButton("Split Nontouching Branches?")
11612
12074
  self.fix3.setCheckable(True)
11613
- self.fix3.setChecked(True)
12075
+ if called:
12076
+ self.fix3.setChecked(True)
12077
+ else:
12078
+ self.fix3.setChecked(False)
11614
12079
  correction_layout.addWidget(QLabel("Split Nontouching Branches? (Useful if branch pruning - may want to threshold out small, split branches after): "), 4, 0)
11615
12080
  correction_layout.addWidget(self.fix3, 4, 1)
11616
12081
 
@@ -12137,6 +12602,8 @@ class CentroidDialog(QDialog):
12137
12602
 
12138
12603
  try:
12139
12604
 
12605
+ print("Calculating centroids...")
12606
+
12140
12607
  chan = self.mode_selector.currentIndex()
12141
12608
 
12142
12609
  # Get directory (None if empty)
@@ -12499,6 +12966,9 @@ class CalcAllDialog(QDialog):
12499
12966
 
12500
12967
 
12501
12968
  except Exception as e:
12969
+ import traceback
12970
+ print(traceback.format_exc())
12971
+
12502
12972
  QMessageBox.critical(
12503
12973
  self,
12504
12974
  "Error",
@@ -12506,6 +12976,7 @@ class CalcAllDialog(QDialog):
12506
12976
  )
12507
12977
 
12508
12978
 
12979
+
12509
12980
  class ProxDialog(QDialog):
12510
12981
  def __init__(self, parent=None):
12511
12982
  super().__init__(parent)
@@ -12744,11 +13215,6 @@ class ProxDialog(QDialog):
12744
13215
 
12745
13216
 
12746
13217
 
12747
-
12748
-
12749
-
12750
-
12751
-
12752
13218
  # Initiating this program from the script line:
12753
13219
 
12754
13220
  def run_gui():