nettracer3d 0.6.6__py3-none-any.whl → 0.6.8__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.
@@ -18,13 +18,14 @@ from nettracer3d import smart_dilate as sdl
18
18
  from matplotlib.colors import LinearSegmentedColormap
19
19
  from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
20
20
  import pandas as pd
21
- from PyQt6.QtGui import (QFont, QCursor, QColor, QPixmap, QPainter, QPen)
21
+ from PyQt6.QtGui import (QFont, QCursor, QColor, QPixmap, QFontMetrics, QPainter, QPen)
22
22
  import tifffile
23
23
  import copy
24
24
  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
28
29
 
29
30
 
30
31
 
@@ -117,6 +118,9 @@ class ImageViewerWindow(QMainWindow):
117
118
 
118
119
  #For ML segmenting mode
119
120
  self.brush_mode = False
121
+ self.can = False
122
+ self.threed = False
123
+ self.threedthresh = 5
120
124
  self.painting = False
121
125
  self.foreground = True
122
126
  self.machine_window = None
@@ -147,6 +151,11 @@ class ImageViewerWindow(QMainWindow):
147
151
  3: None
148
152
  } #For storing thresholding information
149
153
 
154
+ self.radii_dict = {
155
+ 0: None,
156
+ 1: None
157
+ }
158
+
150
159
  self.original_shape = None #For undoing resamples
151
160
 
152
161
  # Create control panel
@@ -1312,7 +1321,10 @@ class ImageViewerWindow(QMainWindow):
1312
1321
  info_dict['Centroid'] = my_network.node_centroids[label]
1313
1322
 
1314
1323
  if self.volume_dict[0] is not None:
1315
- info_dict['Volume'] = self.volume_dict[0][label]
1324
+ info_dict['Volume (Scaled)'] = self.volume_dict[0][label]
1325
+
1326
+ if self.radii_dict[0] is not None:
1327
+ info_dict['Max Radius (Scaled)'] = self.radii_dict[0][label]
1316
1328
 
1317
1329
 
1318
1330
  elif sort == 'edge':
@@ -1327,7 +1339,10 @@ class ImageViewerWindow(QMainWindow):
1327
1339
  info_dict['Centroid'] = my_network.edge_centroids[label]
1328
1340
 
1329
1341
  if self.volume_dict[1] is not None:
1330
- info_dict['Volume'] = self.volume_dict[1][label]
1342
+ info_dict['Volume (Scaled)'] = self.volume_dict[1][label]
1343
+
1344
+ if self.radii_dict[1] is not None:
1345
+ info_dict['~Radius (Scaled)'] = self.radii_dict[1][label]
1331
1346
 
1332
1347
  self.format_for_upperright_table(info_dict, title = f'Info on Object')
1333
1348
 
@@ -1700,6 +1715,9 @@ class ImageViewerWindow(QMainWindow):
1700
1715
  self.pen_button.setChecked(False)
1701
1716
  self.pan_mode = False
1702
1717
  self.brush_mode = False
1718
+ self.can = False
1719
+ self.threed = False
1720
+ self.last_change = None
1703
1721
  if self.machine_window is not None:
1704
1722
  self.machine_window.silence_button()
1705
1723
  self.canvas.setCursor(Qt.CursorShape.CrossCursor)
@@ -1717,6 +1735,9 @@ class ImageViewerWindow(QMainWindow):
1717
1735
  self.zoom_button.setChecked(False)
1718
1736
  self.pen_button.setChecked(False)
1719
1737
  self.zoom_mode = False
1738
+ self.can = False
1739
+ self.threed = False
1740
+ self.last_change = None
1720
1741
  self.brush_mode = False
1721
1742
  if self.machine_window is not None:
1722
1743
  self.machine_window.silence_button()
@@ -1737,14 +1758,69 @@ class ImageViewerWindow(QMainWindow):
1737
1758
  self.zoom_mode = False
1738
1759
  self.update_brush_cursor()
1739
1760
  else:
1761
+ self.last_change = None
1762
+ self.can = False
1763
+ self.threed = False
1740
1764
  self.canvas.setCursor(Qt.CursorShape.ArrowCursor)
1741
1765
 
1766
+ def toggle_can(self):
1767
+
1768
+ if not self.can:
1769
+ self.can = True
1770
+ self.update_brush_cursor()
1771
+ else:
1772
+ self.can = False
1773
+ self.last_change = None
1774
+ self.update_brush_cursor()
1775
+
1776
+ def toggle_threed(self):
1777
+
1778
+ if not self.threed:
1779
+ self.threed = True
1780
+ self.threedthresh = 5
1781
+ self.update_brush_cursor()
1782
+ else:
1783
+ self.threed = False
1784
+ self.update_brush_cursor()
1785
+
1742
1786
 
1743
1787
  def on_mpl_scroll(self, event):
1744
1788
  """Handle matplotlib canvas scroll events"""
1745
1789
  #Wheel events
1746
1790
  if self.brush_mode and event.inaxes == self.ax:
1791
+
1792
+ # Get modifiers
1793
+ modifiers = event.guiEvent.modifiers()
1794
+ ctrl_pressed = bool(modifiers & Qt.ControlModifier)
1795
+ shift_pressed = bool(modifiers & Qt.ShiftModifier)
1796
+ alt_pressed = bool(modifiers & Qt.AltModifier)
1797
+
1798
+ # Check if threed is enabled and ONLY if no specific modifiers are pressed
1799
+ if self.threed and not ctrl_pressed and not shift_pressed and not alt_pressed:
1800
+ import math
1801
+ step = 1 if event.button == 'up' else -1
1802
+ self.threedthresh += step
1803
+
1804
+ # Round to appropriate odd integer based on scroll direction
1805
+ if event.button == 'up':
1806
+ # Round up to nearest odd
1807
+ self.threedthresh = math.ceil(self.threedthresh)
1808
+ if self.threedthresh % 2 == 0:
1809
+ self.threedthresh += 1
1810
+ else: # event.button == 'down'
1811
+ # Round down to nearest odd, but not below 1
1812
+ self.threedthresh = math.floor(self.threedthresh)
1813
+ if self.threedthresh % 2 == 0:
1814
+ self.threedthresh -= 1
1815
+ # Ensure not below minimum value of 1
1816
+ self.threedthresh = max(1, self.threedthresh)
1817
+
1818
+ # Update the brush cursor to show the new threshold
1819
+ self.update_brush_cursor()
1820
+ return
1821
+
1747
1822
  # Check if Ctrl is pressed
1823
+
1748
1824
  if event.guiEvent.modifiers() & Qt.ShiftModifier:
1749
1825
  pass
1750
1826
 
@@ -1781,6 +1857,15 @@ class ImageViewerWindow(QMainWindow):
1781
1857
  self.update_display(preserve_zoom=(current_xlim, current_ylim))
1782
1858
 
1783
1859
  def keyPressEvent(self, event):
1860
+
1861
+ if event.key() == Qt.Key_Z and event.modifiers() & Qt.ControlModifier:
1862
+ try:
1863
+ self.load_channel(self.last_change[1], self.last_change[0], True)
1864
+ except:
1865
+ pass
1866
+
1867
+ return # Return to prevent triggering the regular Z key action below
1868
+
1784
1869
  if event.key() == Qt.Key_Z:
1785
1870
  self.zoom_button.click()
1786
1871
  if self.machine_window is not None:
@@ -1788,28 +1873,65 @@ class ImageViewerWindow(QMainWindow):
1788
1873
  self.machine_window.switch_foreground()
1789
1874
  if event.key() == Qt.Key_X:
1790
1875
  self.high_button.click()
1876
+ if self.brush_mode and self.machine_window is None:
1877
+ if event.key() == Qt.Key_F:
1878
+ self.toggle_can()
1879
+ elif event.key() == Qt.Key_D:
1880
+ self.toggle_threed()
1791
1881
 
1792
1882
 
1793
1883
  def update_brush_cursor(self):
1794
1884
  """Update the cursor to show brush size"""
1795
1885
  if not self.brush_mode:
1796
1886
  return
1797
-
1798
- # Create a pixmap for the cursor
1799
- size = self.brush_size * 2 + 2 # Add padding for border
1800
- pixmap = QPixmap(size, size)
1887
+
1888
+ # Get font metrics first to determine text size
1889
+ font = QFont()
1890
+ font.setPointSize(14)
1891
+ font_metrics = QFontMetrics(font)
1892
+ thresh_text = str(self.threedthresh)
1893
+ text_rect = font_metrics.boundingRect(thresh_text)
1894
+
1895
+ # Create a pixmap for the cursor - ensure it's large enough for text
1896
+ brush_size = self.brush_size * 2 + 2 # Add padding for border
1897
+ extra_width = max(0, text_rect.width() + 4 - brush_size) # Extra width for text if needed
1898
+ extra_height = max(0, text_rect.height() + 4 - brush_size) # Extra height for text if needed
1899
+
1900
+ # Make sure pixmap is large enough for both brush and text
1901
+ total_width = brush_size + extra_width
1902
+ total_height = brush_size + extra_height
1903
+ pixmap = QPixmap(total_width, total_height)
1801
1904
  pixmap.fill(Qt.transparent)
1802
1905
 
1803
1906
  # Create painter for the pixmap
1804
1907
  painter = QPainter(pixmap)
1805
1908
  painter.setRenderHint(QPainter.RenderHint.Antialiasing)
1806
1909
 
1910
+ # Calculate center offset for brush ellipse to accommodate text
1911
+ x_offset = extra_width // 2
1912
+ y_offset = extra_height // 2
1913
+
1807
1914
  # Draw circle
1808
- pen = QPen(Qt.white)
1915
+ if not self.threed:
1916
+ pen = QPen(Qt.white)
1917
+ else:
1918
+ pen = QPen(Qt.red)
1809
1919
  pen.setWidth(1)
1810
1920
  painter.setPen(pen)
1811
1921
  painter.setBrush(Qt.transparent)
1812
- painter.drawEllipse(1, 1, size-2, size-2)
1922
+ if not self.can:
1923
+ painter.drawEllipse(1 + x_offset, 1 + y_offset, brush_size-2, brush_size-2)
1924
+
1925
+ # Draw threshold number when threed is True and can is False
1926
+ if self.threed:
1927
+ # Set text properties
1928
+ painter.setFont(font)
1929
+ painter.setPen(QPen(Qt.white)) # White text for visibility
1930
+
1931
+ # Draw the text
1932
+ painter.drawText(2, font_metrics.ascent() + 2, thresh_text)
1933
+ else:
1934
+ painter.drawRect(1 + x_offset, 1 + y_offset, 8, 8)
1813
1935
 
1814
1936
  # Create cursor from pixmap
1815
1937
  cursor = QCursor(pixmap)
@@ -1917,13 +2039,20 @@ class ImageViewerWindow(QMainWindow):
1917
2039
 
1918
2040
  if event.button == 1 or event.button == 3:
1919
2041
 
2042
+ x, y = int(event.xdata), int(event.ydata)
2043
+
2044
+
2045
+ if event.button == 1 and self.can:
2046
+ self.handle_can(x, y)
2047
+ return
2048
+
2049
+
1920
2050
  if event.button == 3:
1921
2051
  self.erase = True
1922
2052
  else:
1923
2053
  self.erase = False
1924
2054
 
1925
2055
  self.painting = True
1926
- x, y = int(event.xdata), int(event.ydata)
1927
2056
  self.last_paint_pos = (x, y)
1928
2057
 
1929
2058
  if self.pen_button.isChecked():
@@ -1983,7 +2112,77 @@ class ImageViewerWindow(QMainWindow):
1983
2112
  for x in range(max(0, center_x - radius), min(width, center_x + radius + 1)):
1984
2113
  # Check if point is within circular brush area
1985
2114
  if (x - center_x) ** 2 + (y - center_y) ** 2 <= radius ** 2:
1986
- self.channel_data[channel][self.current_slice][y, x] = val
2115
+
2116
+ if self.threed and self.threedthresh > 1:
2117
+ amount = (self.threedthresh - 1) / 2
2118
+ low = max(0, self.current_slice - amount)
2119
+ high = min(self.channel_data[channel].shape[0] - 1, self.current_slice + amount)
2120
+
2121
+ for i in range(int(low), int(high + 1)):
2122
+ self.channel_data[channel][i][y, x] = val
2123
+ else:
2124
+ self.channel_data[channel][self.current_slice][y, x] = val
2125
+
2126
+ def handle_can(self, x, y):
2127
+
2128
+
2129
+ if self.threed:
2130
+ ref = copy.deepcopy(self.channel_data[self.active_channel])
2131
+ the_slice = self.channel_data[self.active_channel]
2132
+
2133
+ # First invert the boolean array
2134
+ inv = n3d.invert_boolean(the_slice)
2135
+
2136
+ # Label the connected components in the inverted array
2137
+ labeled_array, num_features = n3d.label_objects(inv)
2138
+
2139
+ # Get the target label at the clicked point
2140
+ target_label = labeled_array[self.current_slice][y][x]
2141
+
2142
+ # Only fill if we clicked on a valid region (target_label > 0)
2143
+ if target_label > 0:
2144
+ # Create a mask of the connected component we clicked on
2145
+ fill_mask = (labeled_array == target_label) * 255
2146
+
2147
+ self.last_change = [ref, self.active_channel]
2148
+
2149
+ # Add this mask to the original slice
2150
+ the_slice = the_slice | fill_mask # Use logical OR to add the filled region
2151
+
2152
+ # Update the channel data
2153
+ self.load_channel(self.active_channel, the_slice, True)
2154
+ else:
2155
+
2156
+ ref = copy.deepcopy(self.channel_data[self.active_channel])
2157
+
2158
+ the_slice = self.channel_data[self.active_channel][self.current_slice]
2159
+
2160
+ # First invert the boolean array
2161
+ inv = n3d.invert_boolean(the_slice)
2162
+
2163
+ # Label the connected components in the inverted array
2164
+ labeled_array, num_features = n3d.label_objects(inv)
2165
+
2166
+ # Get the target label at the clicked point
2167
+ target_label = labeled_array[y][x]
2168
+
2169
+ # Only fill if we clicked on a valid region (target_label > 0)
2170
+ if target_label > 0:
2171
+ # Create a mask of the connected component we clicked on
2172
+ fill_mask = (labeled_array == target_label) * 255
2173
+
2174
+ self.last_change = [ref, self.active_channel]
2175
+
2176
+ # Add this mask to the original slice
2177
+ the_slice = the_slice | fill_mask # Use logical OR to add the filled region
2178
+
2179
+ # Update the channel data
2180
+ self.channel_data[self.active_channel][self.current_slice] = the_slice
2181
+ self.load_channel(self.active_channel, self.channel_data[self.active_channel], True)
2182
+
2183
+
2184
+
2185
+
1987
2186
 
1988
2187
  def on_mouse_move(self, event):
1989
2188
  """Handle mouse movement events."""
@@ -2070,6 +2269,7 @@ class ImageViewerWindow(QMainWindow):
2070
2269
  points = self.get_line_points(last_x, last_y, x, y)
2071
2270
 
2072
2271
  # Paint at each point along the line
2272
+
2073
2273
  for px, py in points:
2074
2274
  if 0 <= px < width and 0 <= py < height:
2075
2275
  self.paint_at_position(px, py, self.erase, channel)
@@ -2485,7 +2685,7 @@ class ImageViewerWindow(QMainWindow):
2485
2685
  # Process menu
2486
2686
  process_menu = menubar.addMenu("Process")
2487
2687
  calculate_menu = process_menu.addMenu("Calculate")
2488
- calc_all_action = calculate_menu.addAction("Calculate All (Find Node-Edge-Node Network)")
2688
+ calc_all_action = calculate_menu.addAction("Calculate Connectivity Network (Find Node-Edge-Node Network)")
2489
2689
  calc_all_action.triggered.connect(self.show_calc_all_dialog)
2490
2690
  calc_prox_action = calculate_menu.addAction("Calculate Proximity Network (connect nodes by distance)")
2491
2691
  calc_prox_action.triggered.connect(self.show_calc_prox_dialog)
@@ -2551,8 +2751,8 @@ class ImageViewerWindow(QMainWindow):
2551
2751
  idoverlay_action.triggered.connect(self.show_idoverlay_dialog)
2552
2752
  coloroverlay_action = overlay_menu.addAction("Color Nodes (or Edges)")
2553
2753
  coloroverlay_action.triggered.connect(self.show_coloroverlay_dialog)
2554
- searchoverlay_action = overlay_menu.addAction("Show Search Regions")
2555
- searchoverlay_action.triggered.connect(self.show_search_dialog)
2754
+ #searchoverlay_action = overlay_menu.addAction("Show Search Regions")
2755
+ #searchoverlay_action.triggered.connect(self.show_search_dialog)
2556
2756
  shuffle_action = overlay_menu.addAction("Shuffle")
2557
2757
  shuffle_action.triggered.connect(self.show_shuffle_dialog)
2558
2758
  arbitrary_action = image_menu.addAction("Select Objects")
@@ -2887,6 +3087,20 @@ class ImageViewerWindow(QMainWindow):
2887
3087
  elif trumper == '-':
2888
3088
  for key, value in my_dict.items():
2889
3089
  my_dict[key] = value[0]
3090
+ elif trumper == '/':
3091
+ new_dict = {}
3092
+ max_val = max(my_dict.keys()) + 1
3093
+ for key, value in my_dict.items():
3094
+ new_dict[key] = f'{value[0]}'
3095
+ if len(value) > 1:
3096
+ for i in range(1, len(value)):
3097
+ new_dict[max_val] = f'{value[i]}'
3098
+ try:
3099
+ my_network.node_centroids[max_val] = my_network.node_centroids[key]
3100
+ except:
3101
+ pass
3102
+ max_val += 1
3103
+ return new_dict
2890
3104
  else:
2891
3105
  for thing in my_dict:
2892
3106
  val = my_dict[thing]
@@ -2925,12 +3139,16 @@ class ImageViewerWindow(QMainWindow):
2925
3139
  'Multiple IDs Detected',
2926
3140
  'The node identities appear to contain multiple ids per node in a list.\n'
2927
3141
  'If you desire one node ID to trump all others, enter it here.\n'
2928
- '(Enter "-" to have the first IDs trump all others or press x to skip)'
3142
+ '(Enter "-" to have the first IDs trump all others)\n'
3143
+ '(Enter "/" to have multi-ID nodes be split into many nodes sharing a centroid)\n'
3144
+ '(Close this window to continue with multi-ID nodes)'
2929
3145
  )
2930
3146
  if not ok or trump_value.strip() == '':
2931
3147
  trump_value = None
2932
3148
  elif trump_value.upper() == '-':
2933
3149
  trump_value = '-'
3150
+ elif trump_value.upper() == "/":
3151
+ trump_value = '/'
2934
3152
  my_network.node_identities = uncork(my_network.node_identities, trump_value)
2935
3153
  else:
2936
3154
  trump_value = None
@@ -3038,76 +3256,78 @@ class ImageViewerWindow(QMainWindow):
3038
3256
  "",
3039
3257
  QFileDialog.Option.ShowDirsOnly
3040
3258
  )
3041
- self.reset(nodes = True, network = True, xy_scale = 1, z_scale = 1, edges = True, search_region = True, network_overlay = True, id_overlay = True)
3042
3259
 
3260
+ if directory != "":
3043
3261
 
3044
- my_network.assemble(directory)
3045
-
3046
- # Load image channels
3047
- try:
3048
- self.load_channel(0, my_network.nodes, True)
3049
- except Exception as e:
3050
- print(e)
3051
- try:
3052
- self.load_channel(1, my_network.edges, True)
3053
- except Exception as e:
3054
- print(e)
3055
- try:
3056
- self.load_channel(2, my_network.network_overlay, True)
3057
- except Exception as e:
3058
- print(e)
3059
- try:
3060
- self.load_channel(3, my_network.id_overlay, True)
3061
- except Exception as e:
3062
- print(e)
3063
-
3064
- # Update slider range based on new data
3065
- for channel in self.channel_data:
3066
- if channel is not None:
3067
- self.slice_slider.setEnabled(True)
3068
- self.slice_slider.setMinimum(0)
3069
- self.slice_slider.setMaximum(channel.shape[0] - 1)
3070
- self.slice_slider.setValue(0)
3071
- self.current_slice = 0
3072
- break
3262
+ self.reset(network = True, xy_scale = 1, z_scale = 1, edges = True, network_overlay = True, id_overlay = True)
3073
3263
 
3074
- # Display network_lists in the network table
3075
- # Create empty DataFrame for network table if network_lists is None
3076
- if not hasattr(my_network, 'network_lists') or my_network.network_lists is None:
3077
- empty_df = pd.DataFrame(columns=['Node 1A', 'Node 1B', 'Edge 1C'])
3078
- model = PandasModel(empty_df)
3079
- self.network_table.setModel(model)
3080
- else:
3081
- model = PandasModel(my_network.network_lists)
3082
- self.network_table.setModel(model)
3083
- # Adjust column widths to content
3084
- for column in range(model.columnCount(None)):
3085
- self.network_table.resizeColumnToContents(column)
3264
+ my_network.assemble(directory)
3086
3265
 
3087
- if hasattr(my_network, 'node_centroids') and my_network.node_centroids is not None:
3266
+ # Load image channels
3088
3267
  try:
3089
- self.format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
3268
+ self.load_channel(0, my_network.nodes, True)
3090
3269
  except Exception as e:
3091
- print(f"Error loading node centroid table: {e}")
3092
-
3093
- if hasattr(my_network, 'edge_centroids') and my_network.edge_centroids is not None:
3270
+ print(e)
3094
3271
  try:
3095
- self.format_for_upperright_table(my_network.edge_centroids, 'EdgeID', ['Z', 'Y', 'X'], 'Edge Centroids')
3272
+ self.load_channel(1, my_network.edges, True)
3096
3273
  except Exception as e:
3097
- print(f"Error loading edge centroid table: {e}")
3098
-
3099
- if hasattr(my_network, 'node_identities') and my_network.node_identities is not None:
3274
+ print(e)
3100
3275
  try:
3101
- self.format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity', 'Node Identities')
3276
+ self.load_channel(2, my_network.network_overlay, True)
3102
3277
  except Exception as e:
3103
- print(f"Error loading node identity table: {e}")
3104
-
3105
-
3106
- if hasattr(my_network, 'communities') and my_network.communities is not None:
3278
+ print(e)
3107
3279
  try:
3108
- self.format_for_upperright_table(my_network.communities, 'NodeID', 'Community', 'Node Communities')
3280
+ self.load_channel(3, my_network.id_overlay, True)
3109
3281
  except Exception as e:
3110
- print(f"Error loading node community table: {e}")
3282
+ print(e)
3283
+
3284
+ # Update slider range based on new data
3285
+ for channel in self.channel_data:
3286
+ if channel is not None:
3287
+ self.slice_slider.setEnabled(True)
3288
+ self.slice_slider.setMinimum(0)
3289
+ self.slice_slider.setMaximum(channel.shape[0] - 1)
3290
+ self.slice_slider.setValue(0)
3291
+ self.current_slice = 0
3292
+ break
3293
+
3294
+ # Display network_lists in the network table
3295
+ # Create empty DataFrame for network table if network_lists is None
3296
+ if not hasattr(my_network, 'network_lists') or my_network.network_lists is None:
3297
+ empty_df = pd.DataFrame(columns=['Node 1A', 'Node 1B', 'Edge 1C'])
3298
+ model = PandasModel(empty_df)
3299
+ self.network_table.setModel(model)
3300
+ else:
3301
+ model = PandasModel(my_network.network_lists)
3302
+ self.network_table.setModel(model)
3303
+ # Adjust column widths to content
3304
+ for column in range(model.columnCount(None)):
3305
+ self.network_table.resizeColumnToContents(column)
3306
+
3307
+ if hasattr(my_network, 'node_centroids') and my_network.node_centroids is not None:
3308
+ try:
3309
+ self.format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
3310
+ except Exception as e:
3311
+ print(f"Error loading node centroid table: {e}")
3312
+
3313
+ if hasattr(my_network, 'edge_centroids') and my_network.edge_centroids is not None:
3314
+ try:
3315
+ self.format_for_upperright_table(my_network.edge_centroids, 'EdgeID', ['Z', 'Y', 'X'], 'Edge Centroids')
3316
+ except Exception as e:
3317
+ print(f"Error loading edge centroid table: {e}")
3318
+
3319
+ if hasattr(my_network, 'node_identities') and my_network.node_identities is not None:
3320
+ try:
3321
+ self.format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity', 'Node Identities')
3322
+ except Exception as e:
3323
+ print(f"Error loading node identity table: {e}")
3324
+
3325
+
3326
+ if hasattr(my_network, 'communities') and my_network.communities is not None:
3327
+ try:
3328
+ self.format_for_upperright_table(my_network.communities, 'NodeID', 'Community', 'Node Communities')
3329
+ except Exception as e:
3330
+ print(f"Error loading node community table: {e}")
3111
3331
 
3112
3332
  except Exception as e:
3113
3333
  QMessageBox.critical(
@@ -3471,9 +3691,11 @@ class ImageViewerWindow(QMainWindow):
3471
3691
 
3472
3692
  if edges:
3473
3693
  self.delete_channel(1, False)
3474
-
3475
- if search_region:
3476
- my_network.search_region = None
3694
+ try:
3695
+ if search_region:
3696
+ my_network.search_region = None
3697
+ except:
3698
+ pass
3477
3699
 
3478
3700
  if network_overlay:
3479
3701
  self.delete_channel(2, False)
@@ -3483,8 +3705,7 @@ class ImageViewerWindow(QMainWindow):
3483
3705
 
3484
3706
 
3485
3707
 
3486
- def save_network_3d(self, asbool = True):
3487
-
3708
+ def save_network_3d(self, asbool=True):
3488
3709
  try:
3489
3710
  if asbool: # Save As
3490
3711
  # First let user select parent directory
@@ -3494,25 +3715,28 @@ class ImageViewerWindow(QMainWindow):
3494
3715
  "",
3495
3716
  QFileDialog.Option.ShowDirsOnly
3496
3717
  )
3497
-
3498
- if parent_dir: # If user didn't cancel
3499
- # Prompt user for new folder name
3500
- new_folder_name, ok = QInputDialog.getText(
3501
- self,
3502
- "New Folder",
3503
- "Enter name for new output folder:"
3504
- )
3718
+ if not parent_dir: # If user canceled the directory selection
3719
+ return # Exit the method early
3720
+
3721
+ # Prompt user for new folder name
3722
+ new_folder_name, ok = QInputDialog.getText(
3723
+ self,
3724
+ "New Folder",
3725
+ "Enter name for new output folder:"
3726
+ )
3505
3727
 
3728
+ # Check if user canceled the folder name dialog
3729
+ if not ok or not new_folder_name:
3730
+ return # Exit the method early
3731
+
3506
3732
  else: # Save
3507
3733
  parent_dir = None # Let the backend handle default save location
3508
3734
 
3509
3735
  # Call appropriate save method
3510
- if parent_dir is not None or not asbool: # Proceed if we have a filename OR if it's a regular save
3511
- if asbool:
3512
- my_network.dump(parent_dir = parent_dir, name = new_folder_name)
3513
- else:
3514
- my_network.dump(name = 'my_network')
3515
-
3736
+ if asbool:
3737
+ my_network.dump(parent_dir=parent_dir, name=new_folder_name)
3738
+ else:
3739
+ my_network.dump(name='my_network')
3516
3740
 
3517
3741
  except Exception as e:
3518
3742
  QMessageBox.critical(
@@ -3689,6 +3913,11 @@ class ImageViewerWindow(QMainWindow):
3689
3913
  else:
3690
3914
  # Regular channel processing with colormap
3691
3915
  # Calculate brightness/contrast limits from entire volume
3916
+ if self.min_max[channel][0] == None:
3917
+ self.min_max[channel][0] = np.min(channel)
3918
+ if self.min_max[channel][1] == None:
3919
+ self.min_max[channel][1] = np.max(channel)
3920
+
3692
3921
  img_min = self.min_max[channel][0]
3693
3922
  img_max = self.min_max[channel][1]
3694
3923
 
@@ -3699,6 +3928,9 @@ class ImageViewerWindow(QMainWindow):
3699
3928
  else:
3700
3929
  vmin = img_min + (img_max - img_min) * self.channel_brightness[channel]['min']
3701
3930
  vmax = img_min + (img_max - img_min) * self.channel_brightness[channel]['max']
3931
+
3932
+
3933
+
3702
3934
 
3703
3935
  # Normalize the image safely
3704
3936
  if vmin == vmax:
@@ -3827,7 +4059,8 @@ class ImageViewerWindow(QMainWindow):
3827
4059
  self.canvas.draw()
3828
4060
 
3829
4061
  except:
3830
- pass
4062
+ import traceback
4063
+ print(traceback.format_exc())
3831
4064
 
3832
4065
  def update_display_slice(self, channel, preserve_zoom=None):
3833
4066
  """Ultra minimal update that only changes the paint channel's data"""
@@ -4675,10 +4908,10 @@ class PropertiesDialog(QDialog):
4675
4908
  self.id_overlay.setChecked(self.check_checked(my_network.id_overlay))
4676
4909
  layout.addRow("Overlay 2 Status", self.id_overlay)
4677
4910
 
4678
- self.search_region = QPushButton("search region")
4679
- self.search_region.setCheckable(True)
4680
- self.search_region.setChecked(self.check_checked(my_network.search_region))
4681
- layout.addRow("Node Search Region Status", self.search_region)
4911
+ #self.search_region = QPushButton("search region")
4912
+ #self.search_region.setCheckable(True)
4913
+ #self.search_region.setChecked(self.check_checked(my_network.search_region))
4914
+ #layout.addRow("Node Search Region Status", self.search_region)
4682
4915
 
4683
4916
  self.network = QPushButton("Network")
4684
4917
  self.network.setCheckable(True)
@@ -4717,10 +4950,10 @@ class PropertiesDialog(QDialog):
4717
4950
  edges = not self.edges.isChecked()
4718
4951
  network_overlay = not self.network_overlay.isChecked()
4719
4952
  id_overlay = not self.id_overlay.isChecked()
4720
- search_region = not self.search_region.isChecked()
4953
+ #search_region = not self.search_region.isChecked()
4721
4954
  network = not self.network.isChecked()
4722
4955
 
4723
- self.parent().reset(nodes = nodes, edges = edges, network_overlay = network_overlay, id_overlay = id_overlay, search_region = search_region, network = network, xy_scale = xy_scale, z_scale = z_scale)
4956
+ self.parent().reset(nodes = nodes, edges = edges, network_overlay = network_overlay, id_overlay = id_overlay, network = network, xy_scale = xy_scale, z_scale = z_scale)
4724
4957
 
4725
4958
  self.accept()
4726
4959
 
@@ -5670,6 +5903,11 @@ class NeighborIdentityDialog(QDialog):
5670
5903
  self.search = QLineEdit("")
5671
5904
  layout.addRow("Search Radius (Ignore if using network):", self.search)
5672
5905
 
5906
+ self.fastdil = QPushButton("Fast Dilate")
5907
+ self.fastdil.setCheckable(True)
5908
+ self.fastdil.setChecked(False)
5909
+ layout.addRow("(If not using network) Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
5910
+
5673
5911
  # Add Run button
5674
5912
  run_button = QPushButton("Get Neighborhood Identity Distribution")
5675
5913
  run_button.clicked.connect(self.neighborids)
@@ -5690,8 +5928,10 @@ class NeighborIdentityDialog(QDialog):
5690
5928
 
5691
5929
  search = float(self.search.text()) if self.search.text().strip() else 0
5692
5930
 
5931
+ fastdil = self.fastdil.isChecked()
5932
+
5693
5933
 
5694
- result, result2, title1, title2, densities = my_network.neighborhood_identities(root = root, directory = directory, mode = mode, search = search)
5934
+ result, result2, title1, title2, densities = my_network.neighborhood_identities(root = root, directory = directory, mode = mode, search = search, fastdil = fastdil)
5695
5935
 
5696
5936
  self.parent().format_for_upperright_table(result, 'Node Identity', 'Amount', title = title1)
5697
5937
  self.parent().format_for_upperright_table(result2, 'Node Identity', 'Proportion', title = title2)
@@ -5761,10 +6001,10 @@ class RadDialog(QDialog):
5761
6001
  layout = QFormLayout(self)
5762
6002
 
5763
6003
  # GPU checkbox (default False)
5764
- self.GPU = QPushButton("GPU")
5765
- self.GPU.setCheckable(True)
5766
- self.GPU.setChecked(False)
5767
- layout.addRow("Use GPU:", self.GPU)
6004
+ #self.GPU = QPushButton("GPU")
6005
+ #self.GPU.setCheckable(True)
6006
+ #self.GPU.setChecked(False)
6007
+ #layout.addRow("Use GPU:", self.GPU)
5768
6008
 
5769
6009
 
5770
6010
  # Add Run button
@@ -5775,17 +6015,18 @@ class RadDialog(QDialog):
5775
6015
  def rads(self):
5776
6016
 
5777
6017
  try:
5778
- GPU = self.GPU.isChecked()
6018
+ #GPU = self.GPU.isChecked() # <- I can never get these to be faster than parallel CPU *shrugs*
5779
6019
 
5780
6020
  active_data = self.parent().channel_data[self.parent().active_channel]
5781
6021
 
5782
- radii = n3d.estimate_object_radii(active_data, gpu=GPU)
6022
+ radii = n3d.estimate_object_radii(active_data, gpu=False, xy_scale = my_network.xy_scale, z_scale = my_network.z_scale)
5783
6023
 
5784
- for key, val in radii.items():
5785
-
5786
- radii[key] = [val, val * (my_network.xy_scale**2) * my_network.z_scale]
6024
+ if self.parent().active_channel == 0:
6025
+ self.parent().radii_dict[0] = radii
6026
+ elif self.parent().active_channel == 1:
6027
+ self.parent().radii_dict[1] = radii
5787
6028
 
5788
- self.parent().format_for_upperright_table(radii, title = '~Radii of Objects', metric='ObjectID', value=['Largest Radius (Voxels)', 'Largest Radius (Scaled)'])
6029
+ self.parent().format_for_upperright_table(radii, title = 'Largest Radii of Objects', metric='ObjectID', value='Largest Radius (Scaled)')
5789
6030
 
5790
6031
  self.accept()
5791
6032
 
@@ -5818,6 +6059,11 @@ class InteractionDialog(QDialog):
5818
6059
  self.mode_selector.setCurrentIndex(0) # Default to Mode 1
5819
6060
  layout.addRow("Execution Mode:", self.mode_selector)
5820
6061
 
6062
+ self.fastdil = QPushButton("Fast Dilate")
6063
+ self.fastdil.setCheckable(True)
6064
+ self.fastdil.setChecked(False)
6065
+ layout.addRow("(If not using network) Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
6066
+
5821
6067
  # Add Run button
5822
6068
  run_button = QPushButton("Calculate")
5823
6069
  run_button.clicked.connect(self.interaction)
@@ -5833,8 +6079,11 @@ class InteractionDialog(QDialog):
5833
6079
  node_search = float(self.node_search.text()) if self.node_search.text() else 0
5834
6080
  except ValueError:
5835
6081
  node_search = 0
6082
+
6083
+
6084
+ fastdil = self.fastdil.isChecked()
5836
6085
 
5837
- result = my_network.interactions(search = node_search, cores = accepted_mode)
6086
+ result = my_network.interactions(search = node_search, cores = accepted_mode, fastdil = fastdil)
5838
6087
 
5839
6088
  self.parent().format_for_upperright_table(result, 'Node ID', ['Volume of Nearby Edge (Scaled)', 'Volume of Search Region'], title = 'Node/Edge Interactions')
5840
6089
 
@@ -5842,6 +6091,9 @@ class InteractionDialog(QDialog):
5842
6091
 
5843
6092
  except Exception as e:
5844
6093
 
6094
+ import traceback
6095
+ print(traceback.format_exc())
6096
+
5845
6097
  print(f"Error finding interactions: {e}")
5846
6098
 
5847
6099
 
@@ -6198,6 +6450,15 @@ class ResizeDialog(QDialog):
6198
6450
  undo_button.clicked.connect(lambda: self.run_resize(undo = True))
6199
6451
  layout.addRow(undo_button)
6200
6452
 
6453
+ if my_network.xy_scale != my_network.z_scale:
6454
+ norm_button_upsize = QPushButton(f"Normalize Scaling with Upsample")
6455
+ norm_button_upsize.clicked.connect(lambda: self.run_resize(upsize = True, special = True))
6456
+ layout.addRow(norm_button_upsize)
6457
+
6458
+ norm_button_downsize = QPushButton("Normalize Scaling with Downsample")
6459
+ norm_button_downsize.clicked.connect(lambda: self.run_resize(upsize = False, special = True))
6460
+ layout.addRow(norm_button_downsize)
6461
+
6201
6462
  run_button = QPushButton("Run Resize")
6202
6463
  run_button.clicked.connect(self.run_resize)
6203
6464
  layout.addRow(run_button)
@@ -6209,7 +6470,7 @@ class ResizeDialog(QDialog):
6209
6470
  self.xsize.setText("1")
6210
6471
  self.ysize.setText("1")
6211
6472
 
6212
- def run_resize(self, undo = False):
6473
+ def run_resize(self, undo = False, upsize = True, special = False):
6213
6474
  try:
6214
6475
  self.parent().resizing = True
6215
6476
  # Get parameters
@@ -6224,7 +6485,27 @@ class ResizeDialog(QDialog):
6224
6485
  return
6225
6486
 
6226
6487
  resize = resize if resize is not None else (zsize, ysize, xsize)
6227
-
6488
+
6489
+ if special:
6490
+ if upsize:
6491
+ if (my_network.z_scale > my_network.xy_scale):
6492
+ # Z dimension needs to be stretched
6493
+ resize = [my_network.z_scale/my_network.xy_scale, 1, 1] # Scale factor for [z, y, x]
6494
+ cardinal = my_network.xy_scale
6495
+ elif (my_network.xy_scale > my_network.z_scale):
6496
+ # XY dimensions need to be stretched
6497
+ resize = [1, my_network.xy_scale/my_network.z_scale, my_network.xy_scale/my_network.z_scale] # Scale factor for [z, y, x]
6498
+ cardinal = my_network.z_scale
6499
+ else:
6500
+ if (my_network.z_scale > my_network.xy_scale):
6501
+ # XY dimension needs to be shrunk
6502
+ resize = [1, my_network.xy_scale/my_network.z_scale, my_network.xy_scale/my_network.z_scale] # Scale factor for [z, y, x]
6503
+ cardinal = my_network.z_scale
6504
+ elif (my_network.xy_scale > my_network.z_scale):
6505
+ # Z dimensions need to be shrunk
6506
+ resize = [my_network.z_scale/my_network.xy_scale, 1, 1] # Scale factor for [z, y, x]
6507
+ cardinal = my_network.xy_scale
6508
+
6228
6509
  # Get the shape from whichever array exists
6229
6510
  array_shape = None
6230
6511
  if my_network.nodes is not None:
@@ -6306,14 +6587,18 @@ class ResizeDialog(QDialog):
6306
6587
  self.parent().slice_slider.setMaximum(channel.shape[0] - 1)
6307
6588
  break
6308
6589
 
6309
- if isinstance(resize, (int, float)):
6310
- my_network.xy_scale = my_network.xy_scale/resize
6311
- my_network.z_scale = my_network.z_scale/resize
6312
- print("xy_scales and z_scales have been adjusted per resample. Check image -> properties to manually reset them to 1 if desired.")
6590
+ if not special:
6591
+ if isinstance(resize, (int, float)):
6592
+ my_network.xy_scale = my_network.xy_scale/resize
6593
+ my_network.z_scale = my_network.z_scale/resize
6594
+ print("xy_scales and z_scales have been adjusted per resample. Check image -> properties to manually reset them to 1 if desired.")
6595
+ else:
6596
+ my_network.xy_scale = my_network.xy_scale/resize[1]
6597
+ my_network.z_scale = my_network.z_scale/resize[0]
6598
+ print("xy_scales and z_scales have been adjusted per resample. Check image -> properties to manually reset them to 1 if desired. Note that xy_scale will not correspond if you made your XY plane a non-square.")
6313
6599
  else:
6314
- my_network.xy_scale = my_network.xy_scale/resize[1]
6315
- my_network.z_scale = my_network.z_scale/resize[0]
6316
- print("xy_scales and z_scales have been adjusted per resample. Check image -> properties to manually reset them to 1 if desired. Note that xy_scale will not correspond if you made your XY plane a non-square.")
6600
+ my_network.xy_scale = cardinal
6601
+ my_network.z_scale = cardinal
6317
6602
 
6318
6603
  try:
6319
6604
  if my_network.node_centroids is not None:
@@ -6587,9 +6872,14 @@ class ThresholdDialog(QDialog):
6587
6872
 
6588
6873
  # Add ML button
6589
6874
  ML = QPushButton("Machine Learning")
6590
- ML.clicked.connect(self.start_ml)
6875
+ ML.clicked.connect(lambda: self.start_ml(GPU = False))
6591
6876
  layout.addRow(ML)
6592
6877
 
6878
+ # Add ML button
6879
+ #ML2 = QPushButton("Machine Learning (GPU)")
6880
+ #ML2.clicked.connect(lambda: self.start_ml(GPU = True))
6881
+ #layout.addRow(ML2)
6882
+
6593
6883
 
6594
6884
  def thresh_mode(self):
6595
6885
 
@@ -6614,7 +6904,7 @@ class ThresholdDialog(QDialog):
6614
6904
  except:
6615
6905
  pass
6616
6906
 
6617
- def start_ml(self):
6907
+ def start_ml(self, GPU = False):
6618
6908
 
6619
6909
 
6620
6910
  if self.parent().channel_data[2] is not None or self.parent().channel_data[3] is not None or self.parent().highlight_overlay is not None:
@@ -6630,10 +6920,16 @@ class ThresholdDialog(QDialog):
6630
6920
  )
6631
6921
  return
6632
6922
 
6923
+ try:
6924
+ import cupy as cp
6925
+ except:
6926
+ print("Cupy import failed, using CPU version")
6927
+ GPU = False
6928
+
6633
6929
  if self.parent().mini_overlay_data is not None:
6634
6930
  self.parent().mini_overlay_data = None
6635
6931
 
6636
- self.parent().machine_window = MachineWindow(self.parent())
6932
+ self.parent().machine_window = MachineWindow(self.parent(), GPU = GPU)
6637
6933
  self.parent().machine_window.show() # Non-modal window
6638
6934
  self.accept()
6639
6935
 
@@ -6650,7 +6946,7 @@ class ThresholdDialog(QDialog):
6650
6946
 
6651
6947
  class MachineWindow(QMainWindow):
6652
6948
 
6653
- def __init__(self, parent=None):
6949
+ def __init__(self, parent=None, GPU = False):
6654
6950
  super().__init__(parent)
6655
6951
 
6656
6952
  self.setWindowTitle("Threshold")
@@ -6668,6 +6964,9 @@ class MachineWindow(QMainWindow):
6668
6964
 
6669
6965
  if self.parent().pen_button.isChecked(): #Disable the pen mode if the user is in it because the segmenter pen forks it
6670
6966
  self.parent().pen_button.click()
6967
+ self.parent().threed = False
6968
+ self.parent().can = False
6969
+ self.parent().last_change = None
6671
6970
 
6672
6971
  self.parent().pen_button.setEnabled(False)
6673
6972
 
@@ -6743,11 +7042,8 @@ class MachineWindow(QMainWindow):
6743
7042
  # Group 2: Processing Options (GPU)
6744
7043
  processing_group = QGroupBox("Processing Options")
6745
7044
  processing_layout = QHBoxLayout()
6746
- self.GPU = QPushButton("GPU (Beta)")
6747
- self.GPU.setCheckable(True)
6748
- self.GPU.setChecked(False)
6749
- self.GPU.clicked.connect(self.toggle_GPU)
6750
- self.use_gpu = False
7045
+
7046
+ self.use_gpu = GPU
6751
7047
  self.two = QPushButton("Train By 2D Slice Patterns")
6752
7048
  self.two.setCheckable(True)
6753
7049
  self.two.setChecked(False)
@@ -6796,7 +7092,8 @@ class MachineWindow(QMainWindow):
6796
7092
 
6797
7093
  # Add all groups to main layout
6798
7094
  main_layout.addWidget(drawing_group)
6799
- main_layout.addWidget(processing_group)
7095
+ if not GPU:
7096
+ main_layout.addWidget(processing_group)
6800
7097
  main_layout.addWidget(training_group)
6801
7098
  main_layout.addWidget(segmentation_group)
6802
7099
 
@@ -6806,10 +7103,16 @@ class MachineWindow(QMainWindow):
6806
7103
  self.trained = False
6807
7104
  self.previewing = False
6808
7105
 
7106
+ if not GPU:
7107
+ self.segmenter = segmenter.InteractiveSegmenter(active_data, use_gpu=False)
7108
+ else:
7109
+ self.segmenter = segmenter_GPU.InteractiveSegmenter(active_data)
6809
7110
 
6810
- self.segmenter = segmenter.InteractiveSegmenter(active_data, use_gpu=True)
6811
7111
  self.segmentation_worker = None
6812
7112
 
7113
+ self.fore_button.click()
7114
+ self.fore_button.click()
7115
+
6813
7116
  def toggle_lock(self):
6814
7117
 
6815
7118
  self.mem_lock = self.lock_button.isChecked()
@@ -6835,12 +7138,6 @@ class MachineWindow(QMainWindow):
6835
7138
  pass
6836
7139
 
6837
7140
 
6838
-
6839
- def toggle_GPU(self):
6840
-
6841
-
6842
- self.use_gpu = self.GPU.isChecked()
6843
-
6844
7141
  def toggle_two(self):
6845
7142
  if self.two.isChecked():
6846
7143
  # If button two is checked, ensure button three is unchecked
@@ -6896,6 +7193,8 @@ class MachineWindow(QMainWindow):
6896
7193
  self.parent().zoom_mode = False
6897
7194
  self.parent().update_brush_cursor()
6898
7195
  else:
7196
+ self.threed = False
7197
+ self.can = False
6899
7198
  self.parent().zoom_button.click()
6900
7199
 
6901
7200
  def silence_button(self):
@@ -6918,6 +7217,8 @@ class MachineWindow(QMainWindow):
6918
7217
  self.trained = True
6919
7218
  except Exception as e:
6920
7219
  print("Error training. Perhaps you forgot both foreground and background markers? I need both!")
7220
+ import traceback
7221
+ traceback.print_exc()
6921
7222
  except MemoryError:
6922
7223
  QMessageBox.critical(
6923
7224
  self,
@@ -7580,6 +7881,12 @@ class SmartDilateDialog(QDialog):
7580
7881
  self.GPU.setChecked(False)
7581
7882
  layout.addRow("Use GPU:", self.GPU)
7582
7883
 
7884
+ # dt checkbox (default False)
7885
+ self.predt = QPushButton("Pre-DT")
7886
+ self.predt.setCheckable(True)
7887
+ self.predt.setChecked(False)
7888
+ layout.addRow("Use Distance Transform for Predilation (Better at Large Dilations):", self.predt)
7889
+
7583
7890
  self.down_factor = QLineEdit("")
7584
7891
  layout.addRow("Internal Downsample for GPU (if needed):", self.down_factor)
7585
7892
 
@@ -7594,11 +7901,12 @@ class SmartDilateDialog(QDialog):
7594
7901
 
7595
7902
  GPU = self.GPU.isChecked()
7596
7903
  down_factor = float(self.down_factor.text()) if self.down_factor.text().strip() else None
7904
+ predt = not self.predt.isChecked()
7597
7905
  active_data, amount, xy_scale, z_scale = self.params
7598
7906
 
7599
7907
  dilate_xy, dilate_z = n3d.dilation_length_to_pixels(xy_scale, z_scale, amount, amount)
7600
7908
 
7601
- result = sdl.smart_dilate(active_data, dilate_xy, dilate_z, GPU = GPU, predownsample = down_factor)
7909
+ result = sdl.smart_dilate(active_data, dilate_xy, dilate_z, GPU = GPU, predownsample = down_factor, fast_dil = predt, use_dt_dil_amount = amount, xy_scale = xy_scale, z_scale = z_scale)
7602
7910
 
7603
7911
  self.parent().load_channel(self.parent().active_channel, result, True)
7604
7912
  self.accept()
@@ -7634,7 +7942,7 @@ class DilateDialog(QDialog):
7634
7942
 
7635
7943
  # Add mode selection dropdown
7636
7944
  self.mode_selector = QComboBox()
7637
- self.mode_selector.addItems(["Binary Dilation", "Preserve Labels (slower)", "Recursive Binary Dilation (Use if the dilation radius is much larger than your objects)"])
7945
+ self.mode_selector.addItems(["Pseudo3D Binary Kernels (For Fast, small dilations)", "Preserve Labels (slower)", "Distance Transform-Based (Slower but more accurate at larger dilations)"])
7638
7946
  self.mode_selector.setCurrentIndex(0) # Default to Mode 1
7639
7947
  layout.addRow("Execution Mode:", self.mode_selector)
7640
7948
 
@@ -7684,18 +7992,17 @@ class DilateDialog(QDialog):
7684
7992
  return
7685
7993
 
7686
7994
  if accepted_mode == 2:
7687
- recursive = True
7995
+ result = n3d.dilate_3D_dt(active_data, amount, xy_scaling = xy_scale, z_scaling = z_scale)
7688
7996
  else:
7689
- recursive = False
7690
7997
 
7691
- # Call dilate method with parameters
7692
- result = n3d.dilate(
7693
- active_data,
7694
- amount,
7695
- xy_scale = xy_scale,
7696
- z_scale = z_scale,
7697
- recursive = recursive
7698
- )
7998
+ # Call dilate method with parameters
7999
+ result = n3d.dilate(
8000
+ active_data,
8001
+ amount,
8002
+ xy_scale = xy_scale,
8003
+ z_scale = z_scale)
8004
+
8005
+ result = result * 255
7699
8006
 
7700
8007
  # Update both the display data and the network object
7701
8008
  self.parent().load_channel(self.parent().active_channel, result, True)
@@ -7739,6 +8046,12 @@ class ErodeDialog(QDialog):
7739
8046
  self.z_scale = QLineEdit(z_scale)
7740
8047
  layout.addRow("z_scale:", self.z_scale)
7741
8048
 
8049
+ # Add mode selection dropdown
8050
+ self.mode_selector = QComboBox()
8051
+ self.mode_selector.addItems(["Pseudo3D Binary Kernels (For Fast, small erosions)", "Distance Transform-Based (Slower but more accurate at larger dilations)"])
8052
+ self.mode_selector.setCurrentIndex(0) # Default to Mode 1
8053
+ layout.addRow("Execution Mode:", self.mode_selector)
8054
+
7742
8055
  # Add Run button
7743
8056
  run_button = QPushButton("Run Erode")
7744
8057
  run_button.clicked.connect(self.run_erode)
@@ -7769,6 +8082,8 @@ class ErodeDialog(QDialog):
7769
8082
  z_scale = float(self.z_scale.text()) if self.z_scale.text() else 1
7770
8083
  except ValueError:
7771
8084
  z_scale = 1
8085
+
8086
+ mode = self.mode_selector.currentIndex()
7772
8087
 
7773
8088
  # Get the active channel data from parent
7774
8089
  active_data = self.parent().channel_data[self.parent().active_channel]
@@ -7781,6 +8096,7 @@ class ErodeDialog(QDialog):
7781
8096
  amount,
7782
8097
  xy_scale = xy_scale,
7783
8098
  z_scale = z_scale,
8099
+ mode = mode
7784
8100
  )
7785
8101
 
7786
8102
 
@@ -8182,8 +8498,7 @@ class WatershedDialog(QDialog):
8182
8498
  self.accept()
8183
8499
 
8184
8500
  except Exception as e:
8185
- import traceback
8186
- print(traceback.format_exc())
8501
+
8187
8502
  QMessageBox.critical(
8188
8503
  self,
8189
8504
  "Error",
@@ -8343,20 +8658,9 @@ class GenNodesDialog(QDialog):
8343
8658
  layout = QFormLayout(self)
8344
8659
  self.called = called
8345
8660
 
8346
- self.branch_removal = QLineEdit("0")
8347
- layout.addRow("Skeleton Voxel Branch Length to Remove (int) (Compensates for spines off medial axis):", self.branch_removal)
8348
-
8349
- self.comp_dil = QLineEdit("0")
8350
- layout.addRow("Voxel distance to merge nearby nodes (Int - compensates for multi-branch identification along thick branch regions):", self.comp_dil)
8351
-
8352
- self.max_vol = QLineEdit("0")
8353
- 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)
8354
-
8355
- # auto checkbox (default True)
8356
- self.auto = QPushButton("Auto")
8357
- self.auto.setCheckable(True)
8358
- self.auto.setChecked(False)
8359
- layout.addRow("Attempt to Auto Correct Skeleton Looping:", self.auto)
8661
+ self.directory = QLineEdit()
8662
+ self.directory.setPlaceholderText("Leave empty to save in active dir")
8663
+ layout.addRow("Output Directory:", self.directory)
8360
8664
 
8361
8665
  if not down_factor:
8362
8666
  down_factor = None
@@ -8371,16 +8675,33 @@ class GenNodesDialog(QDialog):
8371
8675
  self.down_factor = down_factor[0]
8372
8676
  self.cubic = down_factor[1]
8373
8677
 
8374
- self.directory = QLineEdit()
8375
- self.directory.setPlaceholderText("Leave empty to save in active dir")
8376
- layout.addRow("Output Directory:", self.directory)
8678
+ self.branch_removal = QLineEdit("0")
8679
+ layout.addRow("Skeleton Voxel Branch Length to Remove (int) (Compensates for spines off medial axis):", self.branch_removal)
8680
+
8681
+ self.max_vol = QLineEdit("0")
8682
+ 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)
8683
+
8684
+ self.comp_dil = QLineEdit("0")
8685
+ layout.addRow("Voxel distance to merge nearby nodes (Int - compensates for multi-branch identification along thick branch regions):", self.comp_dil)
8686
+
8687
+ self.fast_dil = QPushButton("Fast-Dil")
8688
+ self.fast_dil.setCheckable(True)
8689
+ self.fast_dil.setChecked(True)
8690
+ layout.addRow("(If using above) Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fast_dil)
8691
+
8692
+ # auto checkbox (default True)
8693
+ self.auto = QPushButton("Auto")
8694
+ self.auto.setCheckable(True)
8695
+ self.auto.setChecked(True)
8696
+ layout.addRow("Attempt to Auto Correct Skeleton Looping:", self.auto)
8697
+
8377
8698
 
8378
8699
  # retain checkbox (default True)
8379
8700
  if not called:
8380
8701
  self.retain = QPushButton("Retain")
8381
8702
  self.retain.setCheckable(True)
8382
8703
  self.retain.setChecked(True)
8383
- layout.addRow("Retain Original Edges? (Will be moved to overlay 2):", self.retain)
8704
+ #layout.addRow("Retain Original Edges? (Will be moved to overlay 2):", self.retain)
8384
8705
  else:
8385
8706
  self.retain = False
8386
8707
 
@@ -8436,6 +8757,8 @@ class GenNodesDialog(QDialog):
8436
8757
 
8437
8758
  auto = self.auto.isChecked()
8438
8759
 
8760
+ fastdil = self.fast_dil.isChecked()
8761
+
8439
8762
 
8440
8763
  if auto:
8441
8764
  my_network.edges = n3d.skeletonize(my_network.edges)
@@ -8449,7 +8772,8 @@ class GenNodesDialog(QDialog):
8449
8772
  comp_dil=comp_dil,
8450
8773
  down_factor=down_factor,
8451
8774
  order = order,
8452
- return_skele = True
8775
+ return_skele = True,
8776
+ fastdil = fastdil
8453
8777
 
8454
8778
  )
8455
8779
 
@@ -8476,7 +8800,7 @@ class GenNodesDialog(QDialog):
8476
8800
 
8477
8801
  self.parent().load_channel(0, channel_data = result, data = True)
8478
8802
 
8479
- if retain:
8803
+ if retain and self.called:
8480
8804
  self.parent().load_channel(3, channel_data = my_network.edges, data = True)
8481
8805
 
8482
8806
 
@@ -8487,6 +8811,9 @@ class GenNodesDialog(QDialog):
8487
8811
 
8488
8812
  except Exception as e:
8489
8813
 
8814
+ import traceback
8815
+ print(traceback.format_exc())
8816
+
8490
8817
 
8491
8818
  QMessageBox.critical(
8492
8819
  self,
@@ -8581,7 +8908,7 @@ class BranchDialog(QDialog):
8581
8908
 
8582
8909
  temp_network = n3d.Network_3D(nodes = output)
8583
8910
 
8584
- temp_network.morph_proximity(search = 1) #Detect network of nearby branches
8911
+ temp_network.morph_proximity(search = [3,3], fastdil = True) #Detect network of nearby branches
8585
8912
 
8586
8913
  temp_network.community_partition(weighted = False, style = 1, dostats = False) #Find communities with louvain, unweighted params
8587
8914
 
@@ -9013,8 +9340,6 @@ class CentroidDialog(QDialog):
9013
9340
  class CalcAllDialog(QDialog):
9014
9341
  # Class variables to store previous settings
9015
9342
  prev_directory = ""
9016
- prev_xy_scale = "1"
9017
- prev_z_scale = "1"
9018
9343
  prev_search = ""
9019
9344
  prev_diledge = ""
9020
9345
  prev_down_factor = ""
@@ -9024,7 +9349,7 @@ class CalcAllDialog(QDialog):
9024
9349
  prev_gpu = True
9025
9350
  prev_label_nodes = True
9026
9351
  prev_inners = True
9027
- prev_skeletonize = False
9352
+ prev_fastdil = False
9028
9353
  prev_overlays = False
9029
9354
  prev_updates = True
9030
9355
 
@@ -9041,10 +9366,10 @@ class CalcAllDialog(QDialog):
9041
9366
  layout.addRow("Output Directory:", self.directory)
9042
9367
 
9043
9368
  # Load previous values for all inputs
9044
- self.xy_scale = QLineEdit(self.prev_xy_scale)
9369
+ self.xy_scale = QLineEdit(f'{my_network.xy_scale}')
9045
9370
  layout.addRow("xy_scale:", self.xy_scale)
9046
9371
 
9047
- self.z_scale = QLineEdit(self.prev_z_scale)
9372
+ self.z_scale = QLineEdit(f'{my_network.z_scale}')
9048
9373
  layout.addRow("z_scale:", self.z_scale)
9049
9374
 
9050
9375
  self.search = QLineEdit(self.prev_search)
@@ -9087,10 +9412,10 @@ class CalcAllDialog(QDialog):
9087
9412
  self.inners.setChecked(self.prev_inners)
9088
9413
  layout.addRow("Use Inner Edges:", self.inners)
9089
9414
 
9090
- self.skeletonize = QPushButton("Skeletonize")
9091
- self.skeletonize.setCheckable(True)
9092
- self.skeletonize.setChecked(self.prev_skeletonize)
9093
- layout.addRow("Skeletonize Edges:", self.skeletonize)
9415
+ self.fastdil = QPushButton("Fast Dilate")
9416
+ self.fastdil.setCheckable(True)
9417
+ self.fastdil.setChecked(self.prev_fastdil)
9418
+ layout.addRow("Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
9094
9419
 
9095
9420
  self.overlays = QPushButton("Overlays")
9096
9421
  self.overlays.setCheckable(True)
@@ -9161,7 +9486,7 @@ class CalcAllDialog(QDialog):
9161
9486
  gpu = self.gpu.isChecked()
9162
9487
  label_nodes = self.label_nodes.isChecked()
9163
9488
  inners = self.inners.isChecked()
9164
- skeletonize = self.skeletonize.isChecked()
9489
+ fastdil = self.fastdil.isChecked()
9165
9490
  overlays = self.overlays.isChecked()
9166
9491
  update = self.update.isChecked()
9167
9492
 
@@ -9184,13 +9509,11 @@ class CalcAllDialog(QDialog):
9184
9509
  GPU=gpu,
9185
9510
  label_nodes=label_nodes,
9186
9511
  inners=inners,
9187
- skeletonize=skeletonize
9512
+ fast_dil=fastdil
9188
9513
  )
9189
9514
 
9190
9515
  # Store current values as previous values
9191
9516
  CalcAllDialog.prev_directory = self.directory.text()
9192
- CalcAllDialog.prev_xy_scale = self.xy_scale.text()
9193
- CalcAllDialog.prev_z_scale = self.z_scale.text()
9194
9517
  CalcAllDialog.prev_search = self.search.text()
9195
9518
  CalcAllDialog.prev_diledge = self.diledge.text()
9196
9519
  CalcAllDialog.prev_down_factor = self.down_factor.text()
@@ -9200,22 +9523,22 @@ class CalcAllDialog(QDialog):
9200
9523
  CalcAllDialog.prev_gpu = self.gpu.isChecked()
9201
9524
  CalcAllDialog.prev_label_nodes = self.label_nodes.isChecked()
9202
9525
  CalcAllDialog.prev_inners = self.inners.isChecked()
9203
- CalcAllDialog.prev_skeletonize = self.skeletonize.isChecked()
9526
+ CalcAllDialog.prev_fastdil = self.fastdil.isChecked()
9204
9527
  CalcAllDialog.prev_overlays = self.overlays.isChecked()
9205
9528
  CalcAllDialog.prev_updates = self.update.isChecked()
9206
9529
 
9207
9530
 
9208
9531
  # Update both the display data and the network object
9209
9532
  if update:
9210
- self.parent().channel_data[0] = my_network.nodes
9211
- self.parent().channel_data[1] = my_network.edges
9533
+ self.parent().load_channel(0, my_network.nodes, True)
9534
+ self.parent().load_channel(1, my_network.edges, True)
9212
9535
  else:
9213
9536
  my_network.nodes = temp_nodes.copy()
9214
9537
  del temp_nodes
9215
9538
  my_network.edges = temp_edges.copy()
9216
9539
  del temp_edges
9217
- self.parent().channel_data[0] = my_network.nodes
9218
- self.parent().channel_data[1] = my_network.edges
9540
+ self.parent().load_channel(0, my_network.nodes, True)
9541
+ self.parent().load_channel(1, my_network.edges, True)
9219
9542
 
9220
9543
 
9221
9544
  # Then handle overlays
@@ -9228,8 +9551,8 @@ class CalcAllDialog(QDialog):
9228
9551
  my_network.id_overlay = my_network.draw_node_indices(directory=directory)
9229
9552
 
9230
9553
  # Update channel data
9231
- self.parent().channel_data[2] = my_network.network_overlay
9232
- self.parent().channel_data[3] = my_network.id_overlay
9554
+ self.parent().load_channel(2, my_network.network_overlay, True)
9555
+ self.parent().load_channel(3, my_network.id_overlay, True)
9233
9556
 
9234
9557
  # Enable the overlay channel buttons
9235
9558
  self.parent().channel_buttons[2].setEnabled(True)
@@ -9315,6 +9638,11 @@ class ProxDialog(QDialog):
9315
9638
  self.mode_selector.setCurrentIndex(0) # Default to Mode 1
9316
9639
  layout.addRow("Execution Mode:", self.mode_selector)
9317
9640
 
9641
+ self.fastdil = QPushButton("Fast Dilate")
9642
+ self.fastdil.setCheckable(True)
9643
+ self.fastdil.setChecked(False)
9644
+ layout.addRow("(If using morphological) Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
9645
+
9318
9646
  if my_network.node_identities is not None:
9319
9647
  self.id_selector = QComboBox()
9320
9648
  # Add all options from id dictionary
@@ -9378,7 +9706,8 @@ class ProxDialog(QDialog):
9378
9706
  except ValueError:
9379
9707
  search = None
9380
9708
 
9381
- overlays = self.overlays.isChecked()
9709
+ overlays = self.overlays.isChecked()
9710
+ fastdil = self.fastdil.isChecked()
9382
9711
 
9383
9712
  my_network.xy_scale = xy_scale
9384
9713
  my_network.z_scale = z_scale
@@ -9389,7 +9718,7 @@ class ProxDialog(QDialog):
9389
9718
  my_network.nodes, _ = n3d.label_objects(my_network.nodes)
9390
9719
  if my_network.node_centroids is None:
9391
9720
  self.parent().show_centroid_dialog()
9392
- my_network.morph_proximity(search = search, targets = targets)
9721
+ my_network.morph_proximity(search = search, targets = targets, fastdil = fastdil)
9393
9722
 
9394
9723
  self.parent().load_channel(0, channel_data = my_network.nodes, data = True)
9395
9724
  elif mode == 0: