nettracer3d 0.6.7__py3-none-any.whl → 0.6.9__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,14 +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
+ #from nettracer3d import segmenter_GPU <--- couldn't get this faster than CPU ¯\_(ツ)_/¯
29
29
 
30
30
 
31
31
 
@@ -118,6 +118,9 @@ class ImageViewerWindow(QMainWindow):
118
118
 
119
119
  #For ML segmenting mode
120
120
  self.brush_mode = False
121
+ self.can = False
122
+ self.threed = False
123
+ self.threedthresh = 5
121
124
  self.painting = False
122
125
  self.foreground = True
123
126
  self.machine_window = None
@@ -815,6 +818,8 @@ class ImageViewerWindow(QMainWindow):
815
818
  link_nodes.triggered.connect(self.handle_link)
816
819
  delink_nodes = highlight_menu.addAction("Split Nodes")
817
820
  delink_nodes.triggered.connect(self.handle_split)
821
+ override_obj = highlight_menu.addAction("Override Channel with Selection")
822
+ override_obj.triggered.connect(self.handle_override)
818
823
  context_menu.addMenu(highlight_menu)
819
824
 
820
825
  # Create measure menu
@@ -1659,7 +1664,9 @@ class ImageViewerWindow(QMainWindow):
1659
1664
  print(f"An error has occurred: {e}")
1660
1665
 
1661
1666
 
1662
-
1667
+ def handle_override(self):
1668
+ dialog = OverrideDialog(self)
1669
+ dialog.exec()
1663
1670
 
1664
1671
 
1665
1672
 
@@ -1712,6 +1719,9 @@ class ImageViewerWindow(QMainWindow):
1712
1719
  self.pen_button.setChecked(False)
1713
1720
  self.pan_mode = False
1714
1721
  self.brush_mode = False
1722
+ self.can = False
1723
+ self.threed = False
1724
+ self.last_change = None
1715
1725
  if self.machine_window is not None:
1716
1726
  self.machine_window.silence_button()
1717
1727
  self.canvas.setCursor(Qt.CursorShape.CrossCursor)
@@ -1729,6 +1739,9 @@ class ImageViewerWindow(QMainWindow):
1729
1739
  self.zoom_button.setChecked(False)
1730
1740
  self.pen_button.setChecked(False)
1731
1741
  self.zoom_mode = False
1742
+ self.can = False
1743
+ self.threed = False
1744
+ self.last_change = None
1732
1745
  self.brush_mode = False
1733
1746
  if self.machine_window is not None:
1734
1747
  self.machine_window.silence_button()
@@ -1749,14 +1762,69 @@ class ImageViewerWindow(QMainWindow):
1749
1762
  self.zoom_mode = False
1750
1763
  self.update_brush_cursor()
1751
1764
  else:
1765
+ self.last_change = None
1766
+ self.can = False
1767
+ self.threed = False
1752
1768
  self.canvas.setCursor(Qt.CursorShape.ArrowCursor)
1753
1769
 
1770
+ def toggle_can(self):
1771
+
1772
+ if not self.can:
1773
+ self.can = True
1774
+ self.update_brush_cursor()
1775
+ else:
1776
+ self.can = False
1777
+ self.last_change = None
1778
+ self.update_brush_cursor()
1779
+
1780
+ def toggle_threed(self):
1781
+
1782
+ if not self.threed:
1783
+ self.threed = True
1784
+ self.threedthresh = 5
1785
+ self.update_brush_cursor()
1786
+ else:
1787
+ self.threed = False
1788
+ self.update_brush_cursor()
1789
+
1754
1790
 
1755
1791
  def on_mpl_scroll(self, event):
1756
1792
  """Handle matplotlib canvas scroll events"""
1757
1793
  #Wheel events
1758
1794
  if self.brush_mode and event.inaxes == self.ax:
1795
+
1796
+ # Get modifiers
1797
+ modifiers = event.guiEvent.modifiers()
1798
+ ctrl_pressed = bool(modifiers & Qt.ControlModifier)
1799
+ shift_pressed = bool(modifiers & Qt.ShiftModifier)
1800
+ alt_pressed = bool(modifiers & Qt.AltModifier)
1801
+
1802
+ # Check if threed is enabled and ONLY if no specific modifiers are pressed
1803
+ if self.threed and not ctrl_pressed and not shift_pressed and not alt_pressed:
1804
+ import math
1805
+ step = 1 if event.button == 'up' else -1
1806
+ self.threedthresh += step
1807
+
1808
+ # Round to appropriate odd integer based on scroll direction
1809
+ if event.button == 'up':
1810
+ # Round up to nearest odd
1811
+ self.threedthresh = math.ceil(self.threedthresh)
1812
+ if self.threedthresh % 2 == 0:
1813
+ self.threedthresh += 1
1814
+ else: # event.button == 'down'
1815
+ # Round down to nearest odd, but not below 1
1816
+ self.threedthresh = math.floor(self.threedthresh)
1817
+ if self.threedthresh % 2 == 0:
1818
+ self.threedthresh -= 1
1819
+ # Ensure not below minimum value of 1
1820
+ self.threedthresh = max(1, self.threedthresh)
1821
+
1822
+ # Update the brush cursor to show the new threshold
1823
+ self.update_brush_cursor()
1824
+ return
1825
+
1759
1826
  # Check if Ctrl is pressed
1827
+
1760
1828
  if event.guiEvent.modifiers() & Qt.ShiftModifier:
1761
1829
  pass
1762
1830
 
@@ -1793,6 +1861,15 @@ class ImageViewerWindow(QMainWindow):
1793
1861
  self.update_display(preserve_zoom=(current_xlim, current_ylim))
1794
1862
 
1795
1863
  def keyPressEvent(self, event):
1864
+
1865
+ if event.key() == Qt.Key_Z and event.modifiers() & Qt.ControlModifier:
1866
+ try:
1867
+ self.load_channel(self.last_change[1], self.last_change[0], True)
1868
+ except:
1869
+ pass
1870
+
1871
+ return # Return to prevent triggering the regular Z key action below
1872
+
1796
1873
  if event.key() == Qt.Key_Z:
1797
1874
  self.zoom_button.click()
1798
1875
  if self.machine_window is not None:
@@ -1800,28 +1877,65 @@ class ImageViewerWindow(QMainWindow):
1800
1877
  self.machine_window.switch_foreground()
1801
1878
  if event.key() == Qt.Key_X:
1802
1879
  self.high_button.click()
1880
+ if self.brush_mode and self.machine_window is None:
1881
+ if event.key() == Qt.Key_F:
1882
+ self.toggle_can()
1883
+ elif event.key() == Qt.Key_D:
1884
+ self.toggle_threed()
1803
1885
 
1804
1886
 
1805
1887
  def update_brush_cursor(self):
1806
1888
  """Update the cursor to show brush size"""
1807
1889
  if not self.brush_mode:
1808
1890
  return
1809
-
1810
- # Create a pixmap for the cursor
1811
- size = self.brush_size * 2 + 2 # Add padding for border
1812
- pixmap = QPixmap(size, size)
1891
+
1892
+ # Get font metrics first to determine text size
1893
+ font = QFont()
1894
+ font.setPointSize(14)
1895
+ font_metrics = QFontMetrics(font)
1896
+ thresh_text = str(self.threedthresh)
1897
+ text_rect = font_metrics.boundingRect(thresh_text)
1898
+
1899
+ # Create a pixmap for the cursor - ensure it's large enough for text
1900
+ brush_size = self.brush_size * 2 + 2 # Add padding for border
1901
+ extra_width = max(0, text_rect.width() + 4 - brush_size) # Extra width for text if needed
1902
+ extra_height = max(0, text_rect.height() + 4 - brush_size) # Extra height for text if needed
1903
+
1904
+ # Make sure pixmap is large enough for both brush and text
1905
+ total_width = brush_size + extra_width
1906
+ total_height = brush_size + extra_height
1907
+ pixmap = QPixmap(total_width, total_height)
1813
1908
  pixmap.fill(Qt.transparent)
1814
1909
 
1815
1910
  # Create painter for the pixmap
1816
1911
  painter = QPainter(pixmap)
1817
1912
  painter.setRenderHint(QPainter.RenderHint.Antialiasing)
1818
1913
 
1914
+ # Calculate center offset for brush ellipse to accommodate text
1915
+ x_offset = extra_width // 2
1916
+ y_offset = extra_height // 2
1917
+
1819
1918
  # Draw circle
1820
- pen = QPen(Qt.white)
1919
+ if not self.threed:
1920
+ pen = QPen(Qt.white)
1921
+ else:
1922
+ pen = QPen(Qt.red)
1821
1923
  pen.setWidth(1)
1822
1924
  painter.setPen(pen)
1823
1925
  painter.setBrush(Qt.transparent)
1824
- painter.drawEllipse(1, 1, size-2, size-2)
1926
+ if not self.can:
1927
+ painter.drawEllipse(1 + x_offset, 1 + y_offset, brush_size-2, brush_size-2)
1928
+
1929
+ # Draw threshold number when threed is True and can is False
1930
+ if self.threed:
1931
+ # Set text properties
1932
+ painter.setFont(font)
1933
+ painter.setPen(QPen(Qt.white)) # White text for visibility
1934
+
1935
+ # Draw the text
1936
+ painter.drawText(2, font_metrics.ascent() + 2, thresh_text)
1937
+ else:
1938
+ painter.drawRect(1 + x_offset, 1 + y_offset, 8, 8)
1825
1939
 
1826
1940
  # Create cursor from pixmap
1827
1941
  cursor = QCursor(pixmap)
@@ -1860,6 +1974,28 @@ class ImageViewerWindow(QMainWindow):
1860
1974
  points.append((x, y))
1861
1975
  return points
1862
1976
 
1977
+ def get_current_mouse_position(self):
1978
+ # Get the main application's current mouse position
1979
+ cursor_pos = QCursor.pos()
1980
+
1981
+ # Convert global screen coordinates to canvas widget coordinates
1982
+ canvas_pos = self.canvas.mapFromGlobal(cursor_pos)
1983
+
1984
+ # Check if the position is within the canvas bounds
1985
+ if not (0 <= canvas_pos.x() < self.canvas.width() and
1986
+ 0 <= canvas_pos.y() < self.canvas.height()):
1987
+ return 0, 0 # Mouse is outside of the matplotlib canvas
1988
+
1989
+ # Convert from canvas widget coordinates to matplotlib data coordinates
1990
+ x = canvas_pos.x()
1991
+ y = canvas_pos.y()
1992
+
1993
+ # Transform display coordinates to data coordinates
1994
+ inv = self.ax.transData.inverted()
1995
+ data_coords = inv.transform((x, y))
1996
+
1997
+ return data_coords[0], data_coords[1]
1998
+
1863
1999
  def on_mouse_press(self, event):
1864
2000
  """Handle mouse press events."""
1865
2001
  if event.inaxes != self.ax:
@@ -1929,13 +2065,20 @@ class ImageViewerWindow(QMainWindow):
1929
2065
 
1930
2066
  if event.button == 1 or event.button == 3:
1931
2067
 
2068
+ x, y = int(event.xdata), int(event.ydata)
2069
+
2070
+
2071
+ if event.button == 1 and self.can:
2072
+ self.handle_can(x, y)
2073
+ return
2074
+
2075
+
1932
2076
  if event.button == 3:
1933
2077
  self.erase = True
1934
2078
  else:
1935
2079
  self.erase = False
1936
2080
 
1937
2081
  self.painting = True
1938
- x, y = int(event.xdata), int(event.ydata)
1939
2082
  self.last_paint_pos = (x, y)
1940
2083
 
1941
2084
  if self.pen_button.isChecked():
@@ -1995,7 +2138,77 @@ class ImageViewerWindow(QMainWindow):
1995
2138
  for x in range(max(0, center_x - radius), min(width, center_x + radius + 1)):
1996
2139
  # Check if point is within circular brush area
1997
2140
  if (x - center_x) ** 2 + (y - center_y) ** 2 <= radius ** 2:
1998
- self.channel_data[channel][self.current_slice][y, x] = val
2141
+
2142
+ if self.threed and self.threedthresh > 1:
2143
+ amount = (self.threedthresh - 1) / 2
2144
+ low = max(0, self.current_slice - amount)
2145
+ high = min(self.channel_data[channel].shape[0] - 1, self.current_slice + amount)
2146
+
2147
+ for i in range(int(low), int(high + 1)):
2148
+ self.channel_data[channel][i][y, x] = val
2149
+ else:
2150
+ self.channel_data[channel][self.current_slice][y, x] = val
2151
+
2152
+ def handle_can(self, x, y):
2153
+
2154
+
2155
+ if self.threed:
2156
+ ref = copy.deepcopy(self.channel_data[self.active_channel])
2157
+ the_slice = self.channel_data[self.active_channel]
2158
+
2159
+ # First invert the boolean array
2160
+ inv = n3d.invert_boolean(the_slice)
2161
+
2162
+ # Label the connected components in the inverted array
2163
+ labeled_array, num_features = n3d.label_objects(inv)
2164
+
2165
+ # Get the target label at the clicked point
2166
+ target_label = labeled_array[self.current_slice][y][x]
2167
+
2168
+ # Only fill if we clicked on a valid region (target_label > 0)
2169
+ if target_label > 0:
2170
+ # Create a mask of the connected component we clicked on
2171
+ fill_mask = (labeled_array == target_label) * 255
2172
+
2173
+ self.last_change = [ref, self.active_channel]
2174
+
2175
+ # Add this mask to the original slice
2176
+ the_slice = the_slice | fill_mask # Use logical OR to add the filled region
2177
+
2178
+ # Update the channel data
2179
+ self.load_channel(self.active_channel, the_slice, True)
2180
+ else:
2181
+
2182
+ ref = copy.deepcopy(self.channel_data[self.active_channel])
2183
+
2184
+ the_slice = self.channel_data[self.active_channel][self.current_slice]
2185
+
2186
+ # First invert the boolean array
2187
+ inv = n3d.invert_boolean(the_slice)
2188
+
2189
+ # Label the connected components in the inverted array
2190
+ labeled_array, num_features = n3d.label_objects(inv)
2191
+
2192
+ # Get the target label at the clicked point
2193
+ target_label = labeled_array[y][x]
2194
+
2195
+ # Only fill if we clicked on a valid region (target_label > 0)
2196
+ if target_label > 0:
2197
+ # Create a mask of the connected component we clicked on
2198
+ fill_mask = (labeled_array == target_label) * 255
2199
+
2200
+ self.last_change = [ref, self.active_channel]
2201
+
2202
+ # Add this mask to the original slice
2203
+ the_slice = the_slice | fill_mask # Use logical OR to add the filled region
2204
+
2205
+ # Update the channel data
2206
+ self.channel_data[self.active_channel][self.current_slice] = the_slice
2207
+ self.load_channel(self.active_channel, self.channel_data[self.active_channel], True)
2208
+
2209
+
2210
+
2211
+
1999
2212
 
2000
2213
  def on_mouse_move(self, event):
2001
2214
  """Handle mouse movement events."""
@@ -2082,6 +2295,7 @@ class ImageViewerWindow(QMainWindow):
2082
2295
  points = self.get_line_points(last_x, last_y, x, y)
2083
2296
 
2084
2297
  # Paint at each point along the line
2298
+
2085
2299
  for px, py in points:
2086
2300
  if 0 <= px < width and 0 <= py < height:
2087
2301
  self.paint_at_position(px, py, self.erase, channel)
@@ -2770,8 +2984,11 @@ class ImageViewerWindow(QMainWindow):
2770
2984
 
2771
2985
  def show_type_dialog(self):
2772
2986
  """Show the type dialog"""
2773
- dialog = TypeDialog(self)
2774
- dialog.exec()
2987
+ try:
2988
+ dialog = TypeDialog(self)
2989
+ dialog.exec()
2990
+ except:
2991
+ pass
2775
2992
 
2776
2993
  def show_skeletonize_dialog(self):
2777
2994
  """show the skeletonize dialog"""
@@ -4720,10 +4937,10 @@ class PropertiesDialog(QDialog):
4720
4937
  self.id_overlay.setChecked(self.check_checked(my_network.id_overlay))
4721
4938
  layout.addRow("Overlay 2 Status", self.id_overlay)
4722
4939
 
4723
- #self.search_region = QPushButton("search region")
4724
- #self.search_region.setCheckable(True)
4725
- #self.search_region.setChecked(self.check_checked(my_network.search_region))
4726
- #layout.addRow("Node Search Region Status", self.search_region)
4940
+ self.search_region = QPushButton("search region")
4941
+ self.search_region.setCheckable(True)
4942
+ self.search_region.setChecked(self.check_checked(my_network.search_region))
4943
+ layout.addRow("Node Search Region Status", self.search_region)
4727
4944
 
4728
4945
  self.network = QPushButton("Network")
4729
4946
  self.network.setCheckable(True)
@@ -4762,10 +4979,10 @@ class PropertiesDialog(QDialog):
4762
4979
  edges = not self.edges.isChecked()
4763
4980
  network_overlay = not self.network_overlay.isChecked()
4764
4981
  id_overlay = not self.id_overlay.isChecked()
4765
- #search_region = not self.search_region.isChecked()
4982
+ search_region = not self.search_region.isChecked()
4766
4983
  network = not self.network.isChecked()
4767
4984
 
4768
- 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)
4985
+ self.parent().reset(nodes = nodes, edges = edges, search_region = search_region, network_overlay = network_overlay, id_overlay = id_overlay, network = network, xy_scale = xy_scale, z_scale = z_scale)
4769
4986
 
4770
4987
  self.accept()
4771
4988
 
@@ -5769,7 +5986,7 @@ class RandomDialog(QDialog):
5769
5986
  def __init__(self, parent=None):
5770
5987
 
5771
5988
  super().__init__(parent)
5772
- self.setWindowTitle("Degree Distribution Parameters")
5989
+ self.setWindowTitle("Random Parameters")
5773
5990
  self.setModal(True)
5774
5991
 
5775
5992
  layout = QFormLayout(self)
@@ -5813,10 +6030,10 @@ class RadDialog(QDialog):
5813
6030
  layout = QFormLayout(self)
5814
6031
 
5815
6032
  # GPU checkbox (default False)
5816
- #self.GPU = QPushButton("GPU")
5817
- #self.GPU.setCheckable(True)
5818
- #self.GPU.setChecked(False)
5819
- #layout.addRow("Use GPU:", self.GPU)
6033
+ self.GPU = QPushButton("GPU")
6034
+ self.GPU.setCheckable(True)
6035
+ self.GPU.setChecked(False)
6036
+ layout.addRow("Use GPU:", self.GPU)
5820
6037
 
5821
6038
 
5822
6039
  # Add Run button
@@ -5827,7 +6044,7 @@ class RadDialog(QDialog):
5827
6044
  def rads(self):
5828
6045
 
5829
6046
  try:
5830
- #GPU = self.GPU.isChecked() # <- I can never get these to be faster than parallel CPU *shrugs*
6047
+ GPU = self.GPU.isChecked() # <- I can never get these to be faster than parallel CPU *shrugs*
5831
6048
 
5832
6049
  active_data = self.parent().channel_data[self.parent().active_channel]
5833
6050
 
@@ -5854,7 +6071,7 @@ class InteractionDialog(QDialog):
5854
6071
  def __init__(self, parent=None):
5855
6072
 
5856
6073
  super().__init__(parent)
5857
- self.setWindowTitle("Partition Parameters")
6074
+ self.setWindowTitle("Interaction Parameters")
5858
6075
  self.setModal(True)
5859
6076
 
5860
6077
  layout = QFormLayout(self)
@@ -5874,7 +6091,7 @@ class InteractionDialog(QDialog):
5874
6091
  self.fastdil = QPushButton("Fast Dilate")
5875
6092
  self.fastdil.setCheckable(True)
5876
6093
  self.fastdil.setChecked(False)
5877
- layout.addRow("(If not using network) Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
6094
+ layout.addRow("Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.fastdil)
5878
6095
 
5879
6096
  # Add Run button
5880
6097
  run_button = QPushButton("Calculate")
@@ -5929,7 +6146,7 @@ class DegreeDialog(QDialog):
5929
6146
  layout.addRow("Execution Mode:", self.mode_selector)
5930
6147
 
5931
6148
  self.mask_limiter = QLineEdit("1")
5932
- layout.addRow("Masks smaller high degree proportion of nodes (ignore if only returning degrees)", self.mask_limiter)
6149
+ layout.addRow("Proportion of high degree nodes to keep (ignore if only returning degrees)", self.mask_limiter)
5933
6150
 
5934
6151
  self.down_factor = QLineEdit("1")
5935
6152
  layout.addRow("down_factor (for speeding up overlay generation - ignore if only returning degrees:", self.down_factor)
@@ -6474,6 +6691,116 @@ class ResizeDialog(QDialog):
6474
6691
  QMessageBox.critical(self, "Error", f"Failed to resize: {str(e)}")
6475
6692
 
6476
6693
 
6694
+ class OverrideDialog(QDialog):
6695
+ def __init__(self, parent=None):
6696
+ super().__init__(parent)
6697
+ self.setWindowTitle("Override Parameters")
6698
+ self.setModal(True)
6699
+
6700
+ layout = QFormLayout(self)
6701
+
6702
+ layout.addRow(QLabel("Use Highlight Overlay to Place Data From: "))
6703
+
6704
+ # Add mode selection dropdown
6705
+ self.mode_selector = QComboBox()
6706
+ self.mode_selector.addItems(["Nodes", "Edges"])
6707
+ self.mode_selector.setCurrentIndex(0) # Default to Mode 1
6708
+ layout.addRow("Overrider:", self.mode_selector)
6709
+
6710
+ layout.addRow(QLabel("To Override Corresponding Data In: "))
6711
+
6712
+ # Add mode selection dropdown
6713
+ self.target_selector = QComboBox()
6714
+ self.target_selector.addItems(["Nodes", "Edges", "Overlay 1", "Overlay 2"])
6715
+ self.target_selector.setCurrentIndex(0) # Default to Mode 1
6716
+ layout.addRow("To be overwritten:", self.target_selector)
6717
+
6718
+ layout.addRow(QLabel("Place output in: "))
6719
+
6720
+ # Add mode selection dropdown
6721
+ self.output_selector = QComboBox()
6722
+ self.output_selector.addItems(["Nodes", "Edges", "Overlay 1", "Overlay 2", "Highlight Overlay"])
6723
+ self.output_selector.setCurrentIndex(0) # Default to Mode 1
6724
+ layout.addRow("Output Location:", self.output_selector)
6725
+
6726
+ # Add Run button
6727
+ run_button = QPushButton("Override")
6728
+ run_button.clicked.connect(self.override)
6729
+ layout.addWidget(run_button)
6730
+
6731
+ def override(self):
6732
+
6733
+ try:
6734
+
6735
+ accepted_mode = self.mode_selector.currentIndex()
6736
+ accepted_target = self.target_selector.currentIndex()
6737
+ output_target = self.output_selector.currentIndex()
6738
+
6739
+ if accepted_mode == accepted_target:
6740
+ return
6741
+
6742
+ active_data = self.parent().channel_data[accepted_mode]
6743
+
6744
+ if accepted_mode == 0:
6745
+ self.parent().create_highlight_overlay(node_indices=self.parent().clicked_values['nodes'])
6746
+ else:
6747
+ self.parent().create_highlight_overlay(edge_indices=self.parent().clicked_values['edges'])
6748
+
6749
+ target_data = self.parent().channel_data[accepted_target]
6750
+
6751
+ if target_data is None:
6752
+ target_data = np.zeros_like(active_data)
6753
+
6754
+
6755
+
6756
+ try:
6757
+
6758
+ self.parent().highlight_overlay = self.parent().highlight_overlay > 0 #What we want in override image
6759
+ inv = n3d.invert_boolean(self.parent().highlight_overlay) #what we want to keep in target image
6760
+
6761
+ target_data = target_data * inv #Cut out what we don't want in target image
6762
+ max_val = np.max(target_data) #Ensure non-val overlap
6763
+ other_max = np.max(active_data)
6764
+ true_max = max_val + other_max
6765
+ if true_max < 256:
6766
+ dtype = np.uint8
6767
+ elif true_max < 65536:
6768
+ dtype = np.uint16
6769
+ else:
6770
+ dtype = np.uint32
6771
+
6772
+ active_data = active_data.astype(dtype)
6773
+
6774
+ active_data = active_data + max_val #Transpose override image
6775
+
6776
+ active_data = self.parent().highlight_overlay * active_data #Cut out what we want from old image image
6777
+
6778
+ target_data = target_data.astype(dtype)
6779
+
6780
+ target_data = target_data + active_data #Insert new selection
6781
+
6782
+ if output_target == 4:
6783
+
6784
+ self.parent().highlight_overlay = result
6785
+
6786
+ else:
6787
+
6788
+
6789
+ # Update both the display data and the network object
6790
+ self.parent().load_channel(output_target, channel_data = target_data, data = True)
6791
+
6792
+ self.parent().update_display()
6793
+
6794
+ self.accept()
6795
+
6796
+ except Exception as e:
6797
+ print(f"Error overriding: {e}")
6798
+
6799
+ except Exception as e:
6800
+ print(f"Error overriding: {e}")
6801
+
6802
+
6803
+
6477
6804
  class BinarizeDialog(QDialog):
6478
6805
  def __init__(self, parent=None):
6479
6806
  super().__init__(parent)
@@ -6735,7 +7062,7 @@ class ThresholdDialog(QDialog):
6735
7062
  try:
6736
7063
  import cupy as cp
6737
7064
  except:
6738
- print("Cupy import failed, using CPU version")
7065
+ #print("Cupy import failed, using CPU version")
6739
7066
  GPU = False
6740
7067
 
6741
7068
  if self.parent().mini_overlay_data is not None:
@@ -6776,6 +7103,9 @@ class MachineWindow(QMainWindow):
6776
7103
 
6777
7104
  if self.parent().pen_button.isChecked(): #Disable the pen mode if the user is in it because the segmenter pen forks it
6778
7105
  self.parent().pen_button.click()
7106
+ self.parent().threed = False
7107
+ self.parent().can = False
7108
+ self.parent().last_change = None
6779
7109
 
6780
7110
  self.parent().pen_button.setEnabled(False)
6781
7111
 
@@ -6792,8 +7122,12 @@ class MachineWindow(QMainWindow):
6792
7122
  active_data = self.parent().channel_data[1]
6793
7123
  act_channel = 1
6794
7124
 
7125
+ try:
7126
+ array1 = np.zeros_like(active_data).astype(np.uint8)
7127
+ except:
7128
+ print("No data in nodes channel")
7129
+ return
6795
7130
 
6796
- array1 = np.zeros_like(active_data).astype(np.uint8)
6797
7131
  array3 = np.zeros_like(active_data).astype(np.uint8)
6798
7132
  self.parent().highlight_overlay = array3 #Clear this out for the segmenter to use
6799
7133
 
@@ -7002,6 +7336,8 @@ class MachineWindow(QMainWindow):
7002
7336
  self.parent().zoom_mode = False
7003
7337
  self.parent().update_brush_cursor()
7004
7338
  else:
7339
+ self.threed = False
7340
+ self.can = False
7005
7341
  self.parent().zoom_button.click()
7006
7342
 
7007
7343
  def silence_button(self):
@@ -7034,6 +7370,7 @@ class MachineWindow(QMainWindow):
7034
7370
  )
7035
7371
 
7036
7372
 
7373
+
7037
7374
  def start_segmentation(self):
7038
7375
 
7039
7376
  self.kill_segmentation()
@@ -7062,7 +7399,11 @@ class MachineWindow(QMainWindow):
7062
7399
  self.segmentation_worker.finished.connect(self.segmentation_finished)
7063
7400
  current_xlim = self.parent().ax.get_xlim()
7064
7401
  current_ylim = self.parent().ax.get_ylim()
7065
- self.segmenter.update_position(self.parent().current_slice, int((current_ylim[0] - current_ylim[1])/2), int((current_xlim[1] - current_xlim[0])/2))
7402
+ try:
7403
+ x, y = self.parent().get_current_mouse_position()
7404
+ except:
7405
+ x, y = 0, 0
7406
+ self.segmenter.update_position(self.parent().current_slice, x, y)
7066
7407
  self.segmentation_worker.start()
7067
7408
 
7068
7409
  def confirm_seg_dialog(self):
@@ -7128,7 +7469,12 @@ class MachineWindow(QMainWindow):
7128
7469
  current_xlim = self.parent().ax.get_xlim()
7129
7470
  current_ylim = self.parent().ax.get_ylim()
7130
7471
 
7131
- self.segmenter.update_position(self.parent().current_slice, int((current_ylim[0] - current_ylim[1])/2), int((current_xlim[1] - current_xlim[0])/2))
7472
+ try:
7473
+ x, y = self.parent().get_current_mouse_position()
7474
+ except:
7475
+ x, y = 0, 0
7476
+ self.segmenter.update_position(self.parent().current_slice, x, y)
7477
+
7132
7478
  if not self.parent().painting:
7133
7479
  # Only update if view limits are valid
7134
7480
  self.parent().update_display(preserve_zoom=(current_xlim, current_ylim))
@@ -7423,6 +7769,8 @@ class ThresholdWindow(QMainWindow):
7423
7769
  self.bounds = True
7424
7770
  self.parent().bounds = True
7425
7771
 
7772
+ self.chan = self.parent().active_channel
7773
+
7426
7774
 
7427
7775
  # Create matplotlib figure
7428
7776
  fig = Figure(figsize=(5, 4))
@@ -7491,6 +7839,45 @@ class ThresholdWindow(QMainWindow):
7491
7839
  self.parent().preview = False
7492
7840
  self.parent().targs = None
7493
7841
  self.parent().bounds = False
7842
+ try: # could probably be refactored but this just handles keeping the highlight elements if the user presses X
7843
+ if self.chan == 0:
7844
+ if not self.bounds:
7845
+ self.parent().clicked_values['nodes'] = self.get_values_in_range_all_vols(self.chan, float(self.min.text()), float(self.max.text()))
7846
+ else:
7847
+ vals = np.unique(self.parent().channel_data[self.chan])
7848
+ self.parent().clicked_values['nodes'] = (vals[(vals >= float(self.min.text())) & (vals <= float(self.max.text()))]).tolist()
7849
+
7850
+ if self.parent().channel_data[0].shape[0] * self.parent().channel_data[0].shape[1] * self.parent().channel_data[0].shape[2] > self.parent().mini_thresh:
7851
+ self.parent().mini_overlay = True
7852
+ self.parent().create_mini_overlay(node_indices = self.parent().clicked_values['nodes'])
7853
+ else:
7854
+ self.parent().create_highlight_overlay(
7855
+ node_indices=self.parent().clicked_values['nodes']
7856
+ )
7857
+ elif self.chan == 1:
7858
+ if not self.bounds:
7859
+ self.parent().clicked_values['edges'] = self.get_values_in_range_all_vols(self.chan, float(self.min.text()), float(self.max.text()))
7860
+ else:
7861
+ vals = np.unique(self.parent().channel_data[self.chan])
7862
+ self.parent().clicked_values['edges'] = (vals[(vals >= float(self.min.text())) & (vals <= float(self.max.text()))]).tolist()
7863
+
7864
+ if self.parent().channel_data[1].shape[0] * self.parent().channel_data[1].shape[1] * self.parent().channel_data[1].shape[2] > self.parent().mini_thresh:
7865
+ self.parent().mini_overlay = True
7866
+ self.parent().create_mini_overlay(edge_indices = self.parent().clicked_values['edges'])
7867
+ else:
7868
+ self.parent().create_highlight_overlay(
7869
+ node_indices=self.parent().clicked_values['edges']
7870
+ )
7871
+ except:
7872
+ pass
7873
+
7874
+
7875
+ def get_values_in_range_all_vols(self, chan, min_val, max_val):
7876
+ output = []
7877
+ for node, vol in self.parent().volume_dict[chan].items():
7878
+ if min_val <= vol <= max_val:
7879
+ output.append(node)
7880
+ return output
7494
7881
 
7495
7882
  def get_values_in_range(self, lst, min_val, max_val):
7496
7883
  values = [x for x in lst if min_val <= x <= max_val]
@@ -7689,10 +8076,10 @@ class SmartDilateDialog(QDialog):
7689
8076
  layout.addRow("Use GPU:", self.GPU)
7690
8077
 
7691
8078
  # dt checkbox (default False)
7692
- self.predt = QPushButton("Pre-DT")
8079
+ self.predt = QPushButton("Fast Dilation")
7693
8080
  self.predt.setCheckable(True)
7694
8081
  self.predt.setChecked(False)
7695
- layout.addRow("Use Distance Transform for Predilation (Better at Large Dilations):", self.predt)
8082
+ layout.addRow("Use Fast Dilation (Higher speed, less accurate with search regions much larger than nodes):", self.predt)
7696
8083
 
7697
8084
  self.down_factor = QLineEdit("")
7698
8085
  layout.addRow("Internal Downsample for GPU (if needed):", self.down_factor)
@@ -7708,7 +8095,7 @@ class SmartDilateDialog(QDialog):
7708
8095
 
7709
8096
  GPU = self.GPU.isChecked()
7710
8097
  down_factor = float(self.down_factor.text()) if self.down_factor.text().strip() else None
7711
- predt = not self.predt.isChecked()
8098
+ predt = self.predt.isChecked()
7712
8099
  active_data, amount, xy_scale, z_scale = self.params
7713
8100
 
7714
8101
  dilate_xy, dilate_z = n3d.dilation_length_to_pixels(xy_scale, z_scale, amount, amount)
@@ -7807,7 +8194,8 @@ class DilateDialog(QDialog):
7807
8194
  active_data,
7808
8195
  amount,
7809
8196
  xy_scale = xy_scale,
7810
- z_scale = z_scale)
8197
+ z_scale = z_scale,
8198
+ fast_dil = True)
7811
8199
 
7812
8200
  result = result * 255
7813
8201
 
@@ -8057,6 +8445,7 @@ class MaskDialog(QDialog):
8057
8445
  print(f"Error masking: {e}")
8058
8446
 
8059
8447
 
8448
+
8060
8449
  class TypeDialog(QDialog):
8061
8450
 
8062
8451
  def __init__(self, parent=None):
@@ -8075,7 +8464,7 @@ class TypeDialog(QDialog):
8075
8464
 
8076
8465
  # Add mode selection dropdown
8077
8466
  self.mode_selector = QComboBox()
8078
- self.mode_selector.addItems(["8bit int", "16bit int", "32bit int", "32bit float", "64bit float"])
8467
+ self.mode_selector.addItems(["8bit uint", "16bit uint", "32bit uint", "32bit float", "64bit float"])
8079
8468
  self.mode_selector.setCurrentIndex(0) # Default to Mode 1
8080
8469
  layout.addRow("Change to?:", self.mode_selector)
8081
8470
 
@@ -8086,33 +8475,38 @@ class TypeDialog(QDialog):
8086
8475
 
8087
8476
  def run_type(self, active_data):
8088
8477
 
8089
- mode = self.mode_selector.currentIndex()
8478
+ try:
8090
8479
 
8091
- if mode == 0:
8480
+ mode = self.mode_selector.currentIndex()
8092
8481
 
8093
- active_data = active_data.astype(np.uint8)
8482
+ if mode == 0:
8094
8483
 
8095
- elif mode == 1:
8484
+ active_data = active_data.astype(np.uint8)
8096
8485
 
8097
- active_data = active_data.astype(np.uint16)
8486
+ elif mode == 1:
8098
8487
 
8099
- elif mode == 2:
8488
+ active_data = active_data.astype(np.uint16)
8100
8489
 
8101
- active_data = active_data.astype(np.uint32)
8490
+ elif mode == 2:
8102
8491
 
8103
- elif mode == 3:
8492
+ active_data = active_data.astype(np.uint32)
8104
8493
 
8105
- active_data = active_data.astype(np.float32)
8494
+ elif mode == 3:
8106
8495
 
8107
- elif mode == 4:
8496
+ active_data = active_data.astype(np.float32)
8108
8497
 
8109
- active_data = active_data.astype(np.float64)
8498
+ elif mode == 4:
8110
8499
 
8111
- self.parent().load_channel(self.active_chan, active_data, True)
8500
+ active_data = active_data.astype(np.float64)
8112
8501
 
8502
+ self.parent().load_channel(self.active_chan, active_data, True)
8113
8503
 
8114
- print(f"Channel {self.active_chan}) dtype now: {self.parent().channel_data[self.active_chan].dtype}")
8115
- self.accept()
8504
+
8505
+ print(f"Channel {self.active_chan}) dtype now: {self.parent().channel_data[self.active_chan].dtype}")
8506
+ self.accept()
8507
+
8508
+ except Exception as E:
8509
+ print(f"Error: {e}")
8116
8510
 
8117
8511
 
8118
8512
 
@@ -8230,7 +8624,7 @@ class WatershedDialog(QDialog):
8230
8624
  # Predownsample (empty by default)
8231
8625
  self.predownsample = QLineEdit()
8232
8626
  self.predownsample.setPlaceholderText("Leave empty for None")
8233
- layout.addRow("Smart Dilate GPU Downsample:", self.predownsample)
8627
+ layout.addRow("Kernel Obtainment GPU Downsample:", self.predownsample)
8234
8628
 
8235
8629
  # Predownsample2 (empty by default)
8236
8630
  self.predownsample2 = QLineEdit()
@@ -8465,9 +8859,9 @@ class GenNodesDialog(QDialog):
8465
8859
  layout = QFormLayout(self)
8466
8860
  self.called = called
8467
8861
 
8468
- self.directory = QLineEdit()
8469
- self.directory.setPlaceholderText("Leave empty to save in active dir")
8470
- layout.addRow("Output Directory:", self.directory)
8862
+ #self.directory = QLineEdit()
8863
+ #self.directory.setPlaceholderText("Leave empty to save in active dir")
8864
+ #layout.addRow("Output Directory:", self.directory)
8471
8865
 
8472
8866
  if not down_factor:
8473
8867
  down_factor = None
@@ -8477,7 +8871,7 @@ class GenNodesDialog(QDialog):
8477
8871
  self.cubic = QPushButton("Cubic Downsample")
8478
8872
  self.cubic.setCheckable(True)
8479
8873
  self.cubic.setChecked(False)
8480
- layout.addRow("(if downsampling): Use cubic dilation? (Slower but can preserve structure better)", self.cubic)
8874
+ layout.addRow("(if downsampling): Use cubic downsample? (Slower but can preserve structure better)", self.cubic)
8481
8875
  else:
8482
8876
  self.down_factor = down_factor[0]
8483
8877
  self.cubic = down_factor[1]
@@ -8521,7 +8915,7 @@ class GenNodesDialog(QDialog):
8521
8915
 
8522
8916
  try:
8523
8917
  # Get directory (None if empty)
8524
- directory = self.directory.text() if self.directory.text() else None
8918
+ #directory = self.directory.text() if self.directory.text() else None
8525
8919
 
8526
8920
  # Get branch_removal
8527
8921
  try:
@@ -8667,7 +9061,7 @@ class BranchDialog(QDialog):
8667
9061
  self.cubic = QPushButton("Cubic Downsample")
8668
9062
  self.cubic.setCheckable(True)
8669
9063
  self.cubic.setChecked(False)
8670
- layout.addRow("(if downsampling): Use cubic dilation? (Slower but can preserve structure better)", self.cubic)
9064
+ layout.addRow("(if downsampling): Use cubic downsample? (Slower but can preserve structure better)", self.cubic)
8671
9065
 
8672
9066
  # Add Run button
8673
9067
  run_button = QPushButton("Run Branch Label")
@@ -9090,20 +9484,23 @@ class CentroidDialog(QDialog):
9090
9484
  my_network.calculate_node_centroids(
9091
9485
  down_factor = downsample
9092
9486
  )
9093
- my_network.save_node_centroids(directory = directory)
9487
+ if directory:
9488
+ my_network.save_node_centroids(directory = directory)
9094
9489
 
9095
9490
  elif chan == 2:
9096
9491
  my_network.calculate_edge_centroids(
9097
9492
  down_factor = downsample
9098
9493
  )
9099
- my_network.save_edge_centroids(directory = directory)
9494
+ if directory:
9495
+ my_network.save_edge_centroids(directory = directory)
9100
9496
 
9101
9497
  elif chan == 0:
9102
9498
  try:
9103
9499
  my_network.calculate_node_centroids(
9104
9500
  down_factor = downsample
9105
9501
  )
9106
- my_network.save_node_centroids(directory = directory)
9502
+ if directory:
9503
+ my_network.save_node_centroids(directory = directory)
9107
9504
  except:
9108
9505
  pass
9109
9506
 
@@ -9112,7 +9509,8 @@ class CentroidDialog(QDialog):
9112
9509
  my_network.calculate_edge_centroids(
9113
9510
  down_factor = downsample
9114
9511
  )
9115
- my_network.save_edge_centroids(directory = directory)
9512
+ if directory:
9513
+ my_network.save_edge_centroids(directory = directory)
9116
9514
 
9117
9515
  except:
9118
9516
  pass
@@ -9153,7 +9551,7 @@ class CalcAllDialog(QDialog):
9153
9551
  prev_GPU_downsample = ""
9154
9552
  prev_other_nodes = ""
9155
9553
  prev_remove_trunk = ""
9156
- prev_gpu = True
9554
+ prev_gpu = False
9157
9555
  prev_label_nodes = True
9158
9556
  prev_inners = True
9159
9557
  prev_fastdil = False
@@ -9185,7 +9583,7 @@ class CalcAllDialog(QDialog):
9185
9583
 
9186
9584
  self.diledge = QLineEdit(self.prev_diledge)
9187
9585
  self.diledge.setPlaceholderText("Leave empty for None")
9188
- layout.addRow("Edge Reconnection Distance (int):", self.diledge)
9586
+ layout.addRow("Edge Reconnection Distance (float):", self.diledge)
9189
9587
 
9190
9588
  self.down_factor = QLineEdit(self.prev_down_factor)
9191
9589
  self.down_factor.setPlaceholderText("Leave empty for None")
@@ -9212,7 +9610,7 @@ class CalcAllDialog(QDialog):
9212
9610
  self.label_nodes = QPushButton("Label")
9213
9611
  self.label_nodes.setCheckable(True)
9214
9612
  self.label_nodes.setChecked(self.prev_label_nodes)
9215
- layout.addRow("Label Nodes:", self.label_nodes)
9613
+ layout.addRow("Re-Label Nodes (WARNING - OVERRIDES ANY CURRENT LABELS):", self.label_nodes)
9216
9614
 
9217
9615
  self.inners = QPushButton("Inner Edges")
9218
9616
  self.inners.setCheckable(True)
@@ -9551,13 +9949,13 @@ class ProxDialog(QDialog):
9551
9949
  return
9552
9950
 
9553
9951
  if populate:
9554
- my_network.nodes = my_network.kd_network(distance = search, targets = targets)
9952
+ my_network.nodes = my_network.kd_network(distance = search, targets = targets, make_array = True)
9555
9953
  self.parent().load_channel(0, channel_data = my_network.nodes, data = True)
9556
9954
  else:
9557
9955
  my_network.kd_network(distance = search, targets = targets)
9558
9956
 
9559
-
9560
- my_network.dump(directory = directory)
9957
+ if directory is not None:
9958
+ my_network.dump(directory = directory)
9561
9959
 
9562
9960
 
9563
9961
  # Then handle overlays