nettracer3d 0.8.3__py3-none-any.whl → 0.8.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nettracer3d/community_extractor.py +3 -3
- nettracer3d/excelotron.py +21 -2
- nettracer3d/neighborhoods.py +140 -31
- nettracer3d/nettracer.py +516 -82
- nettracer3d/nettracer_gui.py +1072 -842
- nettracer3d/network_analysis.py +90 -29
- nettracer3d/node_draw.py +6 -2
- nettracer3d/painting.py +373 -0
- nettracer3d/proximity.py +52 -103
- nettracer3d/segmenter.py +849 -851
- nettracer3d/segmenter_GPU.py +806 -658
- nettracer3d/smart_dilate.py +44 -10
- {nettracer3d-0.8.3.dist-info → nettracer3d-0.8.5.dist-info}/METADATA +6 -3
- nettracer3d-0.8.5.dist-info/RECORD +25 -0
- {nettracer3d-0.8.3.dist-info → nettracer3d-0.8.5.dist-info}/licenses/LICENSE +2 -4
- nettracer3d-0.8.3.dist-info/RECORD +0 -24
- {nettracer3d-0.8.3.dist-info → nettracer3d-0.8.5.dist-info}/WHEEL +0 -0
- {nettracer3d-0.8.3.dist-info → nettracer3d-0.8.5.dist-info}/entry_points.txt +0 -0
- {nettracer3d-0.8.3.dist-info → nettracer3d-0.8.5.dist-info}/top_level.txt +0 -0
nettracer3d/nettracer_gui.py
CHANGED
|
@@ -22,7 +22,7 @@ from PyQt6.QtGui import (QFont, QCursor, QColor, QPixmap, QFontMetrics, QPainter
|
|
|
22
22
|
import tifffile
|
|
23
23
|
import copy
|
|
24
24
|
import multiprocessing as mp
|
|
25
|
-
from concurrent.futures import ThreadPoolExecutor
|
|
25
|
+
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
|
|
26
26
|
from functools import partial
|
|
27
27
|
from nettracer3d import segmenter
|
|
28
28
|
try:
|
|
@@ -33,6 +33,9 @@ from nettracer3d import excelotron
|
|
|
33
33
|
import threading
|
|
34
34
|
import queue
|
|
35
35
|
from threading import Lock
|
|
36
|
+
from scipy import ndimage
|
|
37
|
+
import os
|
|
38
|
+
from . import painting
|
|
36
39
|
|
|
37
40
|
|
|
38
41
|
|
|
@@ -208,7 +211,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
208
211
|
buttons_widget = QWidget()
|
|
209
212
|
buttons_layout = QHBoxLayout(buttons_widget)
|
|
210
213
|
|
|
211
|
-
# Create zoom button
|
|
214
|
+
# "Create" zoom button
|
|
212
215
|
self.zoom_button = QPushButton("🔍")
|
|
213
216
|
self.zoom_button.setCheckable(True)
|
|
214
217
|
self.zoom_button.setFixedSize(40, 40)
|
|
@@ -291,6 +294,14 @@ class ImageViewerWindow(QMainWindow):
|
|
|
291
294
|
|
|
292
295
|
control_layout.addWidget(channel_container)
|
|
293
296
|
|
|
297
|
+
self.show_channels = QPushButton("✓")
|
|
298
|
+
self.show_channels.setCheckable(True)
|
|
299
|
+
self.show_channels.setChecked(True)
|
|
300
|
+
self.show_channels.setFixedSize(20, 20)
|
|
301
|
+
self.show_channels.clicked.connect(self.toggle_chan_load)
|
|
302
|
+
control_layout.addWidget(self.show_channels)
|
|
303
|
+
self.chan_load = True
|
|
304
|
+
|
|
294
305
|
# Create the main widget and layout
|
|
295
306
|
main_widget = QWidget()
|
|
296
307
|
self.setCentralWidget(main_widget)
|
|
@@ -386,7 +397,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
386
397
|
# Create both table views
|
|
387
398
|
self.network_table = CustomTableView(self)
|
|
388
399
|
self.selection_table = CustomTableView(self)
|
|
389
|
-
empty_df = pd.DataFrame(columns=['Node
|
|
400
|
+
empty_df = pd.DataFrame(columns=['Node A', 'Node B', 'Edge C'])
|
|
390
401
|
self.selection_table.setModel(PandasModel(empty_df))
|
|
391
402
|
self.network_table.setAlternatingRowColors(True)
|
|
392
403
|
self.selection_table.setAlternatingRowColors(True)
|
|
@@ -444,18 +455,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
444
455
|
self.excel_manager.data_received.connect(self.handle_excel_data)
|
|
445
456
|
self.prev_coms = None
|
|
446
457
|
|
|
447
|
-
self.paint_timer = QTimer()
|
|
448
|
-
self.paint_timer.timeout.connect(self.flush_paint_updates)
|
|
449
|
-
self.paint_timer.setSingleShot(True)
|
|
450
|
-
self.pending_paint_update = False
|
|
451
458
|
self.static_background = None
|
|
452
459
|
|
|
453
|
-
# Threading for paint operations
|
|
454
|
-
self.paint_queue = queue.Queue()
|
|
455
|
-
self.paint_lock = Lock()
|
|
456
|
-
self.paint_worker = threading.Thread(target=self.paint_worker_loop, daemon=True)
|
|
457
|
-
self.paint_worker.start()
|
|
458
|
-
|
|
459
460
|
# Background caching for blitting
|
|
460
461
|
self.paint_session_active = False
|
|
461
462
|
|
|
@@ -1773,7 +1774,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1773
1774
|
my_network.network_lists = my_network.network_lists
|
|
1774
1775
|
|
|
1775
1776
|
if not hasattr(my_network, 'network_lists') or my_network.network_lists is None:
|
|
1776
|
-
empty_df = pd.DataFrame(columns=['Node
|
|
1777
|
+
empty_df = pd.DataFrame(columns=['Node A', 'Node B', 'Edge C'])
|
|
1777
1778
|
model = PandasModel(empty_df)
|
|
1778
1779
|
self.network_table.setModel(model)
|
|
1779
1780
|
else:
|
|
@@ -1795,58 +1796,149 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1795
1796
|
except Exception as e:
|
|
1796
1797
|
print(f"An error has occured: {e}")
|
|
1797
1798
|
|
|
1798
|
-
|
|
1799
|
+
|
|
1800
|
+
def process_single_label_bbox(args):
|
|
1799
1801
|
"""
|
|
1800
|
-
|
|
1802
|
+
Worker function to process a single label within its bounding box
|
|
1803
|
+
This function will run in parallel
|
|
1801
1804
|
"""
|
|
1805
|
+
label_subarray, original_label, bbox_slices, start_new_label = args
|
|
1806
|
+
|
|
1807
|
+
try:
|
|
1808
|
+
# Create binary mask for this label only
|
|
1809
|
+
binary_mask = label_subarray == original_label
|
|
1810
|
+
|
|
1811
|
+
if not np.any(binary_mask):
|
|
1812
|
+
return None, start_new_label, bbox_slices
|
|
1813
|
+
|
|
1814
|
+
# Find connected components in the subarray
|
|
1815
|
+
labeled_cc, num_cc = n3d.label_objects(binary_mask)
|
|
1816
|
+
|
|
1817
|
+
if num_cc == 0:
|
|
1818
|
+
return None, start_new_label, bbox_slices
|
|
1819
|
+
|
|
1820
|
+
# Create output subarray with new labels
|
|
1821
|
+
output_subarray = np.zeros_like(label_subarray)
|
|
1822
|
+
|
|
1823
|
+
# Assign new consecutive labels starting from start_new_label
|
|
1824
|
+
for cc_id in range(1, num_cc + 1):
|
|
1825
|
+
cc_mask = labeled_cc == cc_id
|
|
1826
|
+
new_label = start_new_label + cc_id - 1
|
|
1827
|
+
output_subarray[cc_mask] = new_label
|
|
1828
|
+
|
|
1829
|
+
# Return the processed subarray, number of components created, and bbox info
|
|
1830
|
+
return output_subarray, start_new_label + num_cc, bbox_slices
|
|
1831
|
+
|
|
1832
|
+
except Exception as e:
|
|
1833
|
+
print(f"Error processing label {original_label}: {e}")
|
|
1834
|
+
return None, start_new_label, bbox_slices
|
|
1802
1835
|
|
|
1836
|
+
def separate_nontouching_objects(self, input_array, max_val=0):
|
|
1837
|
+
"""
|
|
1838
|
+
Ultra-optimized version using find_objects directly without remapping
|
|
1839
|
+
"""
|
|
1803
1840
|
print("Splitting nontouching objects")
|
|
1804
|
-
|
|
1841
|
+
|
|
1805
1842
|
binary_mask = input_array > 0
|
|
1806
|
-
|
|
1843
|
+
if not np.any(binary_mask):
|
|
1844
|
+
return np.zeros_like(input_array)
|
|
1807
1845
|
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
mask = binary_mask
|
|
1811
|
-
compound_key = input_array[mask] * (labeled_array.max() + 1) + labeled_array[mask]
|
|
1846
|
+
unique_labels = np.unique(input_array[binary_mask])
|
|
1847
|
+
print(f"Processing {len(unique_labels)} unique labels")
|
|
1812
1848
|
|
|
1813
|
-
# Get
|
|
1814
|
-
|
|
1815
|
-
new_labels = np.arange(max_val + 1, max_val + 1 + len(unique_keys))
|
|
1849
|
+
# Get all bounding boxes at once - this is very fast
|
|
1850
|
+
bounding_boxes = ndimage.find_objects(input_array)
|
|
1816
1851
|
|
|
1817
|
-
#
|
|
1818
|
-
|
|
1819
|
-
|
|
1852
|
+
# Prepare work items - just check if bounding box exists for each label
|
|
1853
|
+
work_items = []
|
|
1854
|
+
for orig_label in unique_labels:
|
|
1855
|
+
# find_objects returns list where index = label - 1
|
|
1856
|
+
bbox_index = orig_label - 1
|
|
1857
|
+
|
|
1858
|
+
if (bbox_index >= 0 and
|
|
1859
|
+
bbox_index < len(bounding_boxes) and
|
|
1860
|
+
bounding_boxes[bbox_index] is not None):
|
|
1861
|
+
|
|
1862
|
+
bbox = bounding_boxes[bbox_index]
|
|
1863
|
+
work_items.append((orig_label, bbox))
|
|
1864
|
+
|
|
1865
|
+
print(f"Created {len(work_items)} work items")
|
|
1866
|
+
|
|
1867
|
+
# If we have work items, process them
|
|
1868
|
+
if len(work_items) == 0:
|
|
1869
|
+
print("No valid work items found!")
|
|
1870
|
+
return np.zeros_like(input_array)
|
|
1871
|
+
|
|
1872
|
+
def process_label_minimal(item):
|
|
1873
|
+
orig_label, bbox = item
|
|
1874
|
+
try:
|
|
1875
|
+
subarray = input_array[bbox]
|
|
1876
|
+
binary_sub = subarray == orig_label
|
|
1877
|
+
|
|
1878
|
+
if not np.any(binary_sub):
|
|
1879
|
+
return orig_label, bbox, None, 0
|
|
1880
|
+
|
|
1881
|
+
labeled_sub, num_cc = n3d.label_objects(binary_sub)
|
|
1882
|
+
return orig_label, bbox, labeled_sub, num_cc
|
|
1883
|
+
|
|
1884
|
+
except Exception as e:
|
|
1885
|
+
print(f"Error processing label {orig_label}: {e}")
|
|
1886
|
+
return orig_label, bbox, None, 0
|
|
1887
|
+
|
|
1888
|
+
# Execute in parallel
|
|
1889
|
+
max_workers = min(mp.cpu_count(), len(work_items))
|
|
1890
|
+
|
|
1891
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
1892
|
+
results = list(executor.map(process_label_minimal, work_items))
|
|
1820
1893
|
|
|
1894
|
+
# Reconstruct output array
|
|
1895
|
+
output_array = np.zeros_like(input_array)
|
|
1896
|
+
current_label = max_val + 1
|
|
1897
|
+
total_components = 0
|
|
1898
|
+
|
|
1899
|
+
for orig_label, bbox, labeled_sub, num_cc in results:
|
|
1900
|
+
if num_cc > 0 and labeled_sub is not None:
|
|
1901
|
+
print(f"Label {orig_label}: {num_cc} components")
|
|
1902
|
+
# Remap labels and place in output
|
|
1903
|
+
for cc_id in range(1, num_cc + 1):
|
|
1904
|
+
mask = labeled_sub == cc_id
|
|
1905
|
+
output_array[bbox][mask] = current_label
|
|
1906
|
+
current_label += 1
|
|
1907
|
+
total_components += 1
|
|
1908
|
+
|
|
1909
|
+
print(f"Total components created: {total_components}")
|
|
1821
1910
|
return output_array
|
|
1822
1911
|
|
|
1823
1912
|
def handle_seperate(self):
|
|
1824
|
-
|
|
1913
|
+
"""
|
|
1914
|
+
Fixed version with proper mask handling and debugging
|
|
1915
|
+
"""
|
|
1825
1916
|
try:
|
|
1826
1917
|
# Handle nodes
|
|
1827
1918
|
if len(self.clicked_values['nodes']) > 0:
|
|
1828
|
-
self.create_highlight_overlay(node_indices=self.clicked_values['nodes'])
|
|
1829
|
-
|
|
1830
|
-
# Create a boolean mask for highlighted values
|
|
1831
|
-
self.highlight_overlay = self.highlight_overlay != 0
|
|
1832
1919
|
|
|
1833
|
-
# Create
|
|
1834
|
-
|
|
1920
|
+
# Create highlight overlay (this should preserve original label values)
|
|
1921
|
+
self.create_highlight_overlay(node_indices=self.clicked_values['nodes'])
|
|
1922
|
+
|
|
1923
|
+
# DON'T convert to boolean yet - we need the original labels!
|
|
1924
|
+
# Create a boolean mask for where we have highlighted values
|
|
1925
|
+
highlight_mask = self.highlight_overlay != 0
|
|
1835
1926
|
|
|
1927
|
+
# Create array with just the highlighted values (preserving original labels)
|
|
1928
|
+
highlighted_nodes = np.where(highlight_mask, my_network.nodes, 0)
|
|
1929
|
+
|
|
1836
1930
|
# Get non-highlighted part of the array
|
|
1837
|
-
non_highlighted =
|
|
1838
|
-
|
|
1839
|
-
if (highlighted_nodes==non_highlighted).all():
|
|
1840
|
-
max_val = 0
|
|
1841
|
-
else:
|
|
1842
|
-
max_val = np.max(non_highlighted)
|
|
1931
|
+
non_highlighted = np.where(highlight_mask, 0, my_network.nodes)
|
|
1843
1932
|
|
|
1933
|
+
# Calculate max_val properly
|
|
1934
|
+
max_val = np.max(non_highlighted) if np.any(non_highlighted) else 0
|
|
1935
|
+
|
|
1844
1936
|
# Process highlighted part
|
|
1845
1937
|
processed_highlights = self.separate_nontouching_objects(highlighted_nodes, max_val)
|
|
1846
|
-
|
|
1938
|
+
|
|
1847
1939
|
# Combine back with non-highlighted parts
|
|
1848
1940
|
my_network.nodes = non_highlighted + processed_highlights
|
|
1849
|
-
|
|
1941
|
+
|
|
1850
1942
|
self.load_channel(0, my_network.nodes, True)
|
|
1851
1943
|
|
|
1852
1944
|
# Handle edges
|
|
@@ -1855,18 +1947,15 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1855
1947
|
self.create_highlight_overlay(edge_indices=self.clicked_values['edges'])
|
|
1856
1948
|
|
|
1857
1949
|
# Create a boolean mask for highlighted values
|
|
1858
|
-
|
|
1950
|
+
highlight_mask = self.highlight_overlay != 0
|
|
1859
1951
|
|
|
1860
1952
|
# Create array with just the highlighted values
|
|
1861
|
-
highlighted_edges =
|
|
1953
|
+
highlighted_edges = np.where(highlight_mask, my_network.edges, 0)
|
|
1862
1954
|
|
|
1863
1955
|
# Get non-highlighted part of the array
|
|
1864
|
-
non_highlighted =
|
|
1956
|
+
non_highlighted = np.where(highlight_mask, 0, my_network.edges)
|
|
1865
1957
|
|
|
1866
|
-
|
|
1867
|
-
max_val = 0
|
|
1868
|
-
else:
|
|
1869
|
-
max_val = np.max(non_highlighted)
|
|
1958
|
+
max_val = np.max(non_highlighted) if np.any(non_highlighted) else 0
|
|
1870
1959
|
|
|
1871
1960
|
# Process highlighted part
|
|
1872
1961
|
processed_highlights = self.separate_nontouching_objects(highlighted_edges, max_val)
|
|
@@ -1920,7 +2009,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1920
2009
|
|
|
1921
2010
|
|
|
1922
2011
|
if not hasattr(my_network, 'network_lists') or my_network.network_lists is None:
|
|
1923
|
-
empty_df = pd.DataFrame(columns=['Node
|
|
2012
|
+
empty_df = pd.DataFrame(columns=['Node A', 'Node B', 'Edge C'])
|
|
1924
2013
|
model = PandasModel(empty_df)
|
|
1925
2014
|
self.network_table.setModel(model)
|
|
1926
2015
|
else:
|
|
@@ -1961,7 +2050,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1961
2050
|
|
|
1962
2051
|
# Update the table
|
|
1963
2052
|
if not hasattr(my_network, 'network_lists') or my_network.network_lists is None:
|
|
1964
|
-
empty_df = pd.DataFrame(columns=['Node
|
|
2053
|
+
empty_df = pd.DataFrame(columns=['Node A', 'Node B', 'Edge C'])
|
|
1965
2054
|
model = PandasModel(empty_df)
|
|
1966
2055
|
self.network_table.setModel(model)
|
|
1967
2056
|
else:
|
|
@@ -1993,7 +2082,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1993
2082
|
my_network.network_lists = my_network.network_lists
|
|
1994
2083
|
|
|
1995
2084
|
if not hasattr(my_network, 'network_lists') or my_network.network_lists is None:
|
|
1996
|
-
empty_df = pd.DataFrame(columns=['Node
|
|
2085
|
+
empty_df = pd.DataFrame(columns=['Node A', 'Node B', 'Edge C'])
|
|
1997
2086
|
model = PandasModel(empty_df)
|
|
1998
2087
|
self.network_table.setModel(model)
|
|
1999
2088
|
else:
|
|
@@ -2044,6 +2133,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2044
2133
|
print(f"Error: {e}")
|
|
2045
2134
|
|
|
2046
2135
|
|
|
2136
|
+
def toggle_chan_load(self):
|
|
2137
|
+
|
|
2138
|
+
if self.show_channels.isChecked():
|
|
2139
|
+
self.chan_load = True
|
|
2140
|
+
else:
|
|
2141
|
+
self.chan_load = False
|
|
2047
2142
|
|
|
2048
2143
|
def toggle_highlight(self):
|
|
2049
2144
|
self.highlight = self.high_button.isChecked()
|
|
@@ -2066,11 +2161,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2066
2161
|
if self.zoom_mode:
|
|
2067
2162
|
self.pan_button.setChecked(False)
|
|
2068
2163
|
|
|
2069
|
-
if self.pan_mode or self.brush_mode:
|
|
2070
|
-
current_xlim = self.ax.get_xlim()
|
|
2071
|
-
current_ylim = self.ax.get_ylim()
|
|
2072
|
-
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
2073
|
-
|
|
2074
2164
|
self.pen_button.setChecked(False)
|
|
2075
2165
|
self.pan_mode = False
|
|
2076
2166
|
self.brush_mode = False
|
|
@@ -2080,6 +2170,11 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2080
2170
|
if self.machine_window is not None:
|
|
2081
2171
|
self.machine_window.silence_button()
|
|
2082
2172
|
self.canvas.setCursor(Qt.CursorShape.CrossCursor)
|
|
2173
|
+
if self.pan_mode or self.brush_mode:
|
|
2174
|
+
current_xlim = self.ax.get_xlim()
|
|
2175
|
+
current_ylim = self.ax.get_ylim()
|
|
2176
|
+
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
2177
|
+
|
|
2083
2178
|
else:
|
|
2084
2179
|
if self.machine_window is None:
|
|
2085
2180
|
self.canvas.setCursor(Qt.CursorShape.ArrowCursor)
|
|
@@ -2091,10 +2186,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2091
2186
|
"""Toggle pan mode on/off."""
|
|
2092
2187
|
self.pan_mode = self.pan_button.isChecked()
|
|
2093
2188
|
if self.pan_mode:
|
|
2094
|
-
if self.brush_mode:
|
|
2095
|
-
current_xlim = self.ax.get_xlim()
|
|
2096
|
-
current_ylim = self.ax.get_ylim()
|
|
2097
|
-
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
2098
2189
|
|
|
2099
2190
|
self.zoom_button.setChecked(False)
|
|
2100
2191
|
self.pen_button.setChecked(False)
|
|
@@ -2106,6 +2197,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2106
2197
|
if self.machine_window is not None:
|
|
2107
2198
|
self.machine_window.silence_button()
|
|
2108
2199
|
self.canvas.setCursor(Qt.CursorShape.OpenHandCursor)
|
|
2200
|
+
if self.brush_mode:
|
|
2201
|
+
current_xlim = self.ax.get_xlim()
|
|
2202
|
+
current_ylim = self.ax.get_ylim()
|
|
2203
|
+
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
2109
2204
|
else:
|
|
2110
2205
|
current_xlim = self.ax.get_xlim()
|
|
2111
2206
|
current_ylim = self.ax.get_ylim()
|
|
@@ -2120,6 +2215,20 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2120
2215
|
self.brush_mode = self.pen_button.isChecked()
|
|
2121
2216
|
if self.brush_mode:
|
|
2122
2217
|
|
|
2218
|
+
self.pm = painting.PaintManager(parent = self)
|
|
2219
|
+
|
|
2220
|
+
# Start virtual paint session
|
|
2221
|
+
# Get current zoom to preserve it
|
|
2222
|
+
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
2223
|
+
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
2224
|
+
|
|
2225
|
+
if self.pen_button.isChecked():
|
|
2226
|
+
channel = self.active_channel
|
|
2227
|
+
else:
|
|
2228
|
+
channel = 2
|
|
2229
|
+
|
|
2230
|
+
self.pm.initiate_paint_session(channel, current_xlim, current_ylim)
|
|
2231
|
+
|
|
2123
2232
|
if self.pan_mode:
|
|
2124
2233
|
current_xlim = self.ax.get_xlim()
|
|
2125
2234
|
current_ylim = self.ax.get_ylim()
|
|
@@ -2339,37 +2448,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2339
2448
|
|
|
2340
2449
|
painter.end()
|
|
2341
2450
|
|
|
2342
|
-
def get_line_points(self, x0, y0, x1, y1):
|
|
2343
|
-
"""Get all points in a line between (x0,y0) and (x1,y1) using Bresenham's algorithm."""
|
|
2344
|
-
points = []
|
|
2345
|
-
dx = abs(x1 - x0)
|
|
2346
|
-
dy = abs(y1 - y0)
|
|
2347
|
-
x, y = x0, y0
|
|
2348
|
-
sx = 1 if x0 < x1 else -1
|
|
2349
|
-
sy = 1 if y0 < y1 else -1
|
|
2350
|
-
|
|
2351
|
-
if dx > dy:
|
|
2352
|
-
err = dx / 2.0
|
|
2353
|
-
while x != x1:
|
|
2354
|
-
points.append((x, y))
|
|
2355
|
-
err -= dy
|
|
2356
|
-
if err < 0:
|
|
2357
|
-
y += sy
|
|
2358
|
-
err += dx
|
|
2359
|
-
x += sx
|
|
2360
|
-
else:
|
|
2361
|
-
err = dy / 2.0
|
|
2362
|
-
while y != y1:
|
|
2363
|
-
points.append((x, y))
|
|
2364
|
-
err -= dx
|
|
2365
|
-
if err < 0:
|
|
2366
|
-
x += sx
|
|
2367
|
-
err += dy
|
|
2368
|
-
y += sy
|
|
2369
|
-
|
|
2370
|
-
points.append((x, y))
|
|
2371
|
-
return points
|
|
2372
|
-
|
|
2373
2451
|
def get_current_mouse_position(self):
|
|
2374
2452
|
# Get the main application's current mouse position
|
|
2375
2453
|
cursor_pos = QCursor.pos()
|
|
@@ -2382,15 +2460,25 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2382
2460
|
0 <= canvas_pos.y() < self.canvas.height()):
|
|
2383
2461
|
return 0, 0 # Mouse is outside of the matplotlib canvas
|
|
2384
2462
|
|
|
2385
|
-
#
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2463
|
+
# OPTION 1: Use matplotlib's built-in coordinate conversion
|
|
2464
|
+
# This accounts for figure margins, subplot positioning, etc.
|
|
2465
|
+
try:
|
|
2466
|
+
# Get the figure and axes bounds
|
|
2467
|
+
bbox = self.ax.bbox
|
|
2468
|
+
|
|
2469
|
+
# Convert widget coordinates to figure coordinates
|
|
2470
|
+
fig_x = canvas_pos.x()
|
|
2471
|
+
fig_y = self.canvas.height() - canvas_pos.y() # Flip Y coordinate
|
|
2472
|
+
|
|
2473
|
+
# Check if within axes bounds
|
|
2474
|
+
if (bbox.x0 <= fig_x <= bbox.x1 and bbox.y0 <= fig_y <= bbox.y1):
|
|
2475
|
+
# Transform to data coordinates
|
|
2476
|
+
data_coords = self.ax.transData.inverted().transform((fig_x, fig_y))
|
|
2477
|
+
return data_coords[0], data_coords[1]
|
|
2478
|
+
else:
|
|
2479
|
+
return 0, 0
|
|
2480
|
+
except:
|
|
2481
|
+
pass
|
|
2394
2482
|
|
|
2395
2483
|
def on_mouse_press(self, event):
|
|
2396
2484
|
"""Handle mouse press events."""
|
|
@@ -2429,61 +2517,50 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2429
2517
|
|
|
2430
2518
|
|
|
2431
2519
|
elif self.brush_mode:
|
|
2520
|
+
"""Handle brush mode with virtual painting."""
|
|
2432
2521
|
if event.inaxes != self.ax:
|
|
2433
2522
|
return
|
|
2434
2523
|
|
|
2435
2524
|
if event.button == 1 or event.button == 3:
|
|
2525
|
+
if self.machine_window is not None:
|
|
2526
|
+
if self.machine_window.segmentation_worker is not None:
|
|
2527
|
+
self.machine_window.segmentation_worker.pause()
|
|
2528
|
+
|
|
2436
2529
|
x, y = int(event.xdata), int(event.ydata)
|
|
2437
|
-
|
|
2438
|
-
|
|
2530
|
+
|
|
2531
|
+
# Get current zoom to preserve it
|
|
2439
2532
|
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
2440
2533
|
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
self.update_display(preserve_zoom = (current_xlim, current_ylim))
|
|
2534
|
+
|
|
2535
|
+
if event.button == 1 and getattr(self, 'can', False):
|
|
2536
|
+
self.update_display(preserve_zoom=(current_xlim, current_ylim), skip_paint_reinit = True)
|
|
2445
2537
|
self.handle_can(x, y)
|
|
2446
2538
|
return
|
|
2447
|
-
|
|
2539
|
+
|
|
2540
|
+
# Determine erase mode and foreground/background
|
|
2448
2541
|
if event.button == 3:
|
|
2449
2542
|
self.erase = True
|
|
2450
|
-
self.update_display(preserve_zoom = (current_xlim, current_ylim))
|
|
2451
2543
|
else:
|
|
2452
2544
|
self.erase = False
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2545
|
+
|
|
2546
|
+
# Determine foreground/background for machine window mode
|
|
2547
|
+
foreground = getattr(self, 'foreground', True)
|
|
2548
|
+
|
|
2549
|
+
self.last_virtual_pos = (x, y)
|
|
2550
|
+
|
|
2457
2551
|
if self.pen_button.isChecked():
|
|
2458
2552
|
channel = self.active_channel
|
|
2459
2553
|
else:
|
|
2460
2554
|
channel = 2
|
|
2461
2555
|
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
self
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
# No need to hide other channels or track restore_channels
|
|
2472
|
-
self.restore_channels = []
|
|
2473
|
-
|
|
2474
|
-
if self.static_background is None:
|
|
2475
|
-
if self.machine_window is not None:
|
|
2476
|
-
self.update_display(preserve_zoom = (current_xlim, current_ylim))
|
|
2477
|
-
elif not self.erase:
|
|
2478
|
-
self.temp_chan = channel
|
|
2479
|
-
self.channel_data[4] = self.channel_data[channel]
|
|
2480
|
-
self.min_max[4] = copy.deepcopy(self.min_max[channel])
|
|
2481
|
-
self.channel_brightness[4] = copy.deepcopy(self.channel_brightness[channel])
|
|
2482
|
-
self.load_channel(channel, np.zeros_like(self.channel_data[channel]), data = True, preserve_zoom = (current_xlim, current_ylim), begin_paint = True)
|
|
2483
|
-
self.channel_visible[4] = True
|
|
2484
|
-
self.static_background = self.canvas.copy_from_bbox(self.ax.bbox)
|
|
2485
|
-
|
|
2486
|
-
self.update_display_slice_optimized(channel, preserve_zoom=(current_xlim, current_ylim))
|
|
2556
|
+
self.pm.start_virtual_paint_session(channel, current_xlim, current_ylim)
|
|
2557
|
+
|
|
2558
|
+
# Add first virtual paint stroke
|
|
2559
|
+
brush_size = getattr(self, 'brush_size', 5)
|
|
2560
|
+
self.pm.add_virtual_paint_stroke(x, y, brush_size, self.erase, foreground)
|
|
2561
|
+
|
|
2562
|
+
# Update display with virtual paint
|
|
2563
|
+
self.pm.update_virtual_paint_display()
|
|
2487
2564
|
|
|
2488
2565
|
elif not self.zoom_mode and event.button == 3: # Right click (for context menu)
|
|
2489
2566
|
self.create_context_menu(event)
|
|
@@ -2493,92 +2570,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2493
2570
|
self.selection_start = (event.xdata, event.ydata)
|
|
2494
2571
|
self.selecting = False # Will be set to True if the mouse moves while button is held
|
|
2495
2572
|
|
|
2496
|
-
def paint_at_position(self, center_x, center_y, erase = False, channel = 2):
|
|
2497
|
-
"""Paint pixels within brush radius at given position"""
|
|
2498
|
-
if self.channel_data[channel] is None:
|
|
2499
|
-
return
|
|
2500
|
-
|
|
2501
|
-
if erase:
|
|
2502
|
-
val = 0
|
|
2503
|
-
elif self.machine_window is None:
|
|
2504
|
-
try:
|
|
2505
|
-
val = max(255, self.min_max[4][1])
|
|
2506
|
-
except:
|
|
2507
|
-
val = 255
|
|
2508
|
-
elif self.foreground:
|
|
2509
|
-
val = 1
|
|
2510
|
-
else:
|
|
2511
|
-
val = 2
|
|
2512
|
-
height, width = self.channel_data[channel][self.current_slice].shape
|
|
2513
|
-
radius = self.brush_size // 2
|
|
2514
|
-
|
|
2515
|
-
# Calculate brush area
|
|
2516
|
-
for y in range(max(0, center_y - radius), min(height, center_y + radius + 1)):
|
|
2517
|
-
for x in range(max(0, center_x - radius), min(width, center_x + radius + 1)):
|
|
2518
|
-
# Check if point is within circular brush area
|
|
2519
|
-
if (x - center_x) * 2 + (y - center_y) * 2 <= radius ** 2:
|
|
2520
|
-
if self.threed and self.threedthresh > 1:
|
|
2521
|
-
amount = (self.threedthresh - 1) / 2
|
|
2522
|
-
low = max(0, self.current_slice - amount)
|
|
2523
|
-
high = min(self.channel_data[channel].shape[0] - 1, self.current_slice + amount)
|
|
2524
|
-
for i in range(int(low), int(high + 1)):
|
|
2525
|
-
self.channel_data[channel][i][y, x] = val
|
|
2526
|
-
else:
|
|
2527
|
-
self.channel_data[channel][self.current_slice][y, x] = val
|
|
2528
|
-
|
|
2529
|
-
def paint_at_position_vectorized(self, center_x, center_y, erase=False, channel=2,
|
|
2530
|
-
slice_idx=None, brush_size=None, threed=False,
|
|
2531
|
-
threedthresh=1, foreground=True, machine_window=None):
|
|
2532
|
-
"""Vectorized paint operation for better performance."""
|
|
2533
|
-
if self.channel_data[channel] is None:
|
|
2534
|
-
return
|
|
2535
|
-
|
|
2536
|
-
# Use provided parameters or fall back to instance variables
|
|
2537
|
-
slice_idx = slice_idx if slice_idx is not None else self.current_slice
|
|
2538
|
-
brush_size = brush_size if brush_size is not None else getattr(self, 'brush_size', 5)
|
|
2539
|
-
|
|
2540
|
-
# Determine paint value
|
|
2541
|
-
if erase:
|
|
2542
|
-
val = 0
|
|
2543
|
-
elif machine_window is None:
|
|
2544
|
-
try:
|
|
2545
|
-
val = max(255, self.min_max[4][1])
|
|
2546
|
-
except:
|
|
2547
|
-
val = 255
|
|
2548
|
-
elif foreground:
|
|
2549
|
-
val = 1
|
|
2550
|
-
else:
|
|
2551
|
-
val = 2
|
|
2552
|
-
|
|
2553
|
-
height, width = self.channel_data[channel][slice_idx].shape
|
|
2554
|
-
radius = brush_size // 2
|
|
2555
|
-
|
|
2556
|
-
# Calculate affected region bounds
|
|
2557
|
-
y_min = max(0, center_y - radius)
|
|
2558
|
-
y_max = min(height, center_y + radius + 1)
|
|
2559
|
-
x_min = max(0, center_x - radius)
|
|
2560
|
-
x_max = min(width, center_x + radius + 1)
|
|
2561
|
-
|
|
2562
|
-
if y_min >= y_max or x_min >= x_max:
|
|
2563
|
-
return # No valid region to paint
|
|
2564
|
-
|
|
2565
|
-
# Create coordinate grids for the affected region
|
|
2566
|
-
y_coords, x_coords = np.mgrid[y_min:y_max, x_min:x_max]
|
|
2567
|
-
|
|
2568
|
-
# Calculate distances squared (avoid sqrt for performance)
|
|
2569
|
-
distances_sq = (x_coords - center_x) ** 2 + (y_coords - center_y) ** 2
|
|
2570
|
-
mask = distances_sq <= radius ** 2
|
|
2571
|
-
|
|
2572
|
-
# Apply paint to affected slices
|
|
2573
|
-
if threed and threedthresh > 1:
|
|
2574
|
-
amount = (threedthresh - 1) / 2
|
|
2575
|
-
low = max(0, int(slice_idx - amount))
|
|
2576
|
-
high = min(self.channel_data[channel].shape[0] - 1, int(slice_idx + amount))
|
|
2577
|
-
|
|
2578
|
-
for i in range(low, high + 1):
|
|
2579
|
-
self.channel_data[channel][i][y_min:y_max, x_min:x_max][mask] = val
|
|
2580
|
-
else:
|
|
2581
|
-
self.channel_data[channel][slice_idx][y_min:y_max, x_min:x_max][mask] = val
|
|
2582
2573
|
|
|
2583
2574
|
def handle_can(self, x, y):
|
|
2584
2575
|
|
|
@@ -2648,6 +2639,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2648
2639
|
return
|
|
2649
2640
|
|
|
2650
2641
|
current_time = time.time()
|
|
2642
|
+
self.rect_time = current_time
|
|
2651
2643
|
|
|
2652
2644
|
if self.selection_start and not self.selecting and not self.pan_mode and not self.brush_mode:
|
|
2653
2645
|
if (abs(event.xdata - self.selection_start[0]) > 1 or
|
|
@@ -2718,129 +2710,24 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2718
2710
|
if event.inaxes != self.ax:
|
|
2719
2711
|
return
|
|
2720
2712
|
|
|
2721
|
-
#
|
|
2722
|
-
|
|
2713
|
+
# Throttle updates like selection rectangle
|
|
2714
|
+
current_time = time.time()
|
|
2715
|
+
if current_time - getattr(self, 'last_paint_update_time', 0) < 0.016: # ~60fps
|
|
2716
|
+
return
|
|
2717
|
+
self.last_paint_update_time = current_time
|
|
2723
2718
|
|
|
2724
|
-
|
|
2725
|
-
if not self.pending_paint_update:
|
|
2726
|
-
self.pending_paint_update = True
|
|
2727
|
-
self.paint_timer.start(16) # ~60fps max update rate
|
|
2728
|
-
|
|
2729
|
-
def queue_paint_operation(self, event):
|
|
2730
|
-
"""Queue a paint operation for background processing."""
|
|
2731
|
-
x, y = int(event.xdata), int(event.ydata)
|
|
2732
|
-
|
|
2733
|
-
if self.pen_button.isChecked():
|
|
2734
|
-
channel = self.active_channel
|
|
2735
|
-
else:
|
|
2736
|
-
channel = 2
|
|
2737
|
-
|
|
2738
|
-
if self.channel_data[channel] is not None:
|
|
2739
|
-
# Prepare paint session if needed
|
|
2740
|
-
if not self.paint_session_active:
|
|
2741
|
-
self.prepare_paint_session(channel)
|
|
2742
|
-
|
|
2743
|
-
# Create paint operation
|
|
2744
|
-
paint_op = {
|
|
2745
|
-
'type': 'stroke',
|
|
2746
|
-
'x': x,
|
|
2747
|
-
'y': y,
|
|
2748
|
-
'last_pos': getattr(self, 'last_paint_pos', None),
|
|
2749
|
-
'brush_size': self.brush_size,
|
|
2750
|
-
'erase': self.erase,
|
|
2751
|
-
'channel': channel,
|
|
2752
|
-
'slice': self.current_slice,
|
|
2753
|
-
'threed': getattr(self, 'threed', False),
|
|
2754
|
-
'threedthresh': getattr(self, 'threedthresh', 1),
|
|
2755
|
-
'foreground': getattr(self, 'foreground', True),
|
|
2756
|
-
'machine_window': getattr(self, 'machine_window', None)
|
|
2757
|
-
}
|
|
2719
|
+
x, y = int(event.xdata), int(event.ydata)
|
|
2758
2720
|
|
|
2759
|
-
#
|
|
2760
|
-
|
|
2761
|
-
self.paint_queue.put_nowait(paint_op)
|
|
2762
|
-
except queue.Full:
|
|
2763
|
-
pass # Skip if queue is full to avoid blocking
|
|
2721
|
+
# Determine foreground/background for machine window mode
|
|
2722
|
+
foreground = getattr(self, 'foreground', True)
|
|
2764
2723
|
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
"""Prepare optimized background for blitting during paint session."""
|
|
2769
|
-
if self.paint_session_active:
|
|
2770
|
-
return
|
|
2724
|
+
# Add virtual paint stroke with interpolation
|
|
2725
|
+
brush_size = getattr(self, 'brush_size', 5)
|
|
2726
|
+
self.pm.add_virtual_paint_stroke(x, y, brush_size, self.erase, foreground)
|
|
2771
2727
|
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
self.paint_session_active = True
|
|
2775
|
-
|
|
2776
|
-
def end_paint_session(self):
|
|
2777
|
-
"""Clean up after paint session."""
|
|
2778
|
-
self.paint_session_active = False
|
|
2779
|
-
self.last_paint_pos = None
|
|
2780
|
-
|
|
2781
|
-
def paint_worker_loop(self):
|
|
2782
|
-
"""Background thread for processing paint operations."""
|
|
2783
|
-
while True:
|
|
2784
|
-
try:
|
|
2785
|
-
paint_op = self.paint_queue.get(timeout=1.0)
|
|
2786
|
-
if paint_op is None: # Shutdown signal
|
|
2787
|
-
break
|
|
2788
|
-
|
|
2789
|
-
with self.paint_lock:
|
|
2790
|
-
self.execute_paint_operation(paint_op)
|
|
2791
|
-
|
|
2792
|
-
except queue.Empty:
|
|
2793
|
-
continue
|
|
2794
|
-
|
|
2795
|
-
def shutdown(self):
|
|
2796
|
-
"""Clean shutdown of worker thread."""
|
|
2797
|
-
self.paint_queue.put(None) # Signal worker to stop
|
|
2798
|
-
if hasattr(self, 'paint_worker'):
|
|
2799
|
-
self.paint_worker.join(timeout=1.0)
|
|
2800
|
-
|
|
2801
|
-
def execute_paint_operation(self, paint_op):
|
|
2802
|
-
"""Execute a single paint operation on the data arrays."""
|
|
2803
|
-
if paint_op['type'] == 'stroke':
|
|
2804
|
-
channel = paint_op['channel']
|
|
2805
|
-
x, y = paint_op['x'], paint_op['y']
|
|
2806
|
-
last_pos = paint_op['last_pos']
|
|
2807
|
-
|
|
2808
|
-
if last_pos is not None:
|
|
2809
|
-
# Paint line from last position to current
|
|
2810
|
-
points = self.get_line_points(last_pos[0], last_pos[1], x, y)
|
|
2811
|
-
for px, py in points:
|
|
2812
|
-
height, width = self.channel_data[channel][paint_op['slice']].shape
|
|
2813
|
-
if 0 <= px < width and 0 <= py < height:
|
|
2814
|
-
self.paint_at_position_vectorized(
|
|
2815
|
-
px, py, paint_op['erase'], paint_op['channel'],
|
|
2816
|
-
paint_op['slice'], paint_op['brush_size'],
|
|
2817
|
-
paint_op['threed'], paint_op['threedthresh'],
|
|
2818
|
-
paint_op['foreground'], paint_op['machine_window']
|
|
2819
|
-
)
|
|
2820
|
-
else:
|
|
2821
|
-
# Single point paint
|
|
2822
|
-
height, width = self.channel_data[channel][paint_op['slice']].shape
|
|
2823
|
-
if 0 <= x < width and 0 <= y < height:
|
|
2824
|
-
self.paint_at_position_vectorized(
|
|
2825
|
-
x, y, paint_op['erase'], paint_op['channel'],
|
|
2826
|
-
paint_op['slice'], paint_op['brush_size'],
|
|
2827
|
-
paint_op['threed'], paint_op['threedthresh'],
|
|
2828
|
-
paint_op['foreground'], paint_op['machine_window']
|
|
2829
|
-
)
|
|
2728
|
+
# Update display with virtual paint (super fast)
|
|
2729
|
+
self.pm.update_virtual_paint_display()
|
|
2830
2730
|
|
|
2831
|
-
def flush_paint_updates(self):
|
|
2832
|
-
"""Update the display with batched paint changes."""
|
|
2833
|
-
self.pending_paint_update = False
|
|
2834
|
-
|
|
2835
|
-
# Determine which channel to update
|
|
2836
|
-
channel = self.active_channel if hasattr(self, 'pen_button') and self.pen_button.isChecked() else 2
|
|
2837
|
-
|
|
2838
|
-
# Get current zoom to preserve it
|
|
2839
|
-
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
2840
|
-
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
2841
|
-
|
|
2842
|
-
# Update display
|
|
2843
|
-
self.update_display_slice_optimized(channel, preserve_zoom=(current_xlim, current_ylim))
|
|
2844
2731
|
|
|
2845
2732
|
def create_pan_background(self):
|
|
2846
2733
|
"""Create a static background image from currently visible channels with proper rendering"""
|
|
@@ -3062,6 +2949,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3062
2949
|
|
|
3063
2950
|
def on_mouse_release(self, event):
|
|
3064
2951
|
"""Handle mouse release events"""
|
|
2952
|
+
|
|
2953
|
+
if self.zoom_mode:
|
|
2954
|
+
rect_condition = (time.time() - self.rect_time) > 0.01 # This is just to prevent non-deliberate rectangle zooming
|
|
2955
|
+
else:
|
|
2956
|
+
rect_condition = True
|
|
2957
|
+
|
|
3065
2958
|
if self.pan_mode:
|
|
3066
2959
|
|
|
3067
2960
|
self.panning = False
|
|
@@ -3069,7 +2962,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3069
2962
|
self.canvas.setCursor(Qt.CursorShape.OpenHandCursor)
|
|
3070
2963
|
|
|
3071
2964
|
elif event.button == 1: # Left button release
|
|
3072
|
-
if self.selecting and self.selection_rect is not None:
|
|
2965
|
+
if rect_condition and self.selecting and self.selection_rect is not None:
|
|
3073
2966
|
# Get the rectangle bounds
|
|
3074
2967
|
x0 = min(self.selection_start[0], event.xdata)
|
|
3075
2968
|
y0 = min(self.selection_start[1], event.ydata)
|
|
@@ -3077,7 +2970,13 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3077
2970
|
height = abs(event.ydata - self.selection_start[1])
|
|
3078
2971
|
shift_pressed = 'shift' in event.modifiers
|
|
3079
2972
|
|
|
3080
|
-
if shift_pressed
|
|
2973
|
+
if shift_pressed:
|
|
2974
|
+
|
|
2975
|
+
args = int(x0), int(x0 + width), int(y0), int(y0 + height)
|
|
2976
|
+
|
|
2977
|
+
self.show_crop_dialog(args)
|
|
2978
|
+
|
|
2979
|
+
elif self.zoom_mode: #Optional targeted zoom
|
|
3081
2980
|
|
|
3082
2981
|
self.ax.set_xlim([x0, x0 + width])
|
|
3083
2982
|
self.ax.set_ylim([y0 + height, y0])
|
|
@@ -3205,32 +3104,23 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3205
3104
|
|
|
3206
3105
|
# Handle brush mode cleanup with paint session management
|
|
3207
3106
|
if self.brush_mode and hasattr(self, 'painting') and self.painting:
|
|
3107
|
+
# Finish current operation
|
|
3108
|
+
self.pm.finish_current_virtual_operation()
|
|
3109
|
+
|
|
3110
|
+
# Reset last position for next stroke
|
|
3111
|
+
self.last_virtual_pos = None
|
|
3112
|
+
|
|
3113
|
+
# End this stroke but keep session active for continuous painting
|
|
3208
3114
|
self.painting = False
|
|
3209
3115
|
|
|
3210
|
-
if self.
|
|
3211
|
-
|
|
3212
|
-
try:
|
|
3213
|
-
for i in self.restore_channels:
|
|
3214
|
-
self.channel_visible[i] = True
|
|
3215
|
-
self.restore_channels = []
|
|
3216
|
-
except:
|
|
3217
|
-
pass
|
|
3218
|
-
|
|
3219
|
-
self.end_paint_session()
|
|
3220
|
-
|
|
3221
|
-
# OPTIMIZED: Stop timer and process any pending paint operations
|
|
3222
|
-
if hasattr(self, 'paint_timer'):
|
|
3223
|
-
self.paint_timer.stop()
|
|
3224
|
-
if hasattr(self, 'pending_paint_update') and self.pending_paint_update:
|
|
3225
|
-
self.flush_paint_updates()
|
|
3226
|
-
|
|
3227
|
-
self.static_background = None
|
|
3228
|
-
|
|
3229
|
-
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
3116
|
+
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
3117
|
+
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
3230
3118
|
|
|
3231
|
-
|
|
3119
|
+
self.update_display(preserve_zoom=(current_xlim, current_ylim), continue_paint = True)
|
|
3232
3120
|
|
|
3233
|
-
|
|
3121
|
+
if self.machine_window is not None:
|
|
3122
|
+
if self.machine_window.segmentation_worker is not None:
|
|
3123
|
+
self.machine_window.segmentation_worker.resume()
|
|
3234
3124
|
|
|
3235
3125
|
|
|
3236
3126
|
|
|
@@ -3539,7 +3429,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3539
3429
|
ripley_action.triggered.connect(self.show_ripley_dialog)
|
|
3540
3430
|
heatmap_action = stats_menu.addAction("Community Cluster Heatmap")
|
|
3541
3431
|
heatmap_action.triggered.connect(self.show_heatmap_dialog)
|
|
3542
|
-
nearneigh_action = stats_menu.addAction("Average Nearest Neighbors")
|
|
3432
|
+
nearneigh_action = stats_menu.addAction("Average Nearest Neighbors (With Clustering Heatmaps)")
|
|
3543
3433
|
nearneigh_action.triggered.connect(self.show_nearneigh_dialog)
|
|
3544
3434
|
vol_action = stats_menu.addAction("Calculate Volumes")
|
|
3545
3435
|
vol_action.triggered.connect(self.volumes)
|
|
@@ -3558,6 +3448,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3558
3448
|
community_code_action.triggered.connect(lambda: self.show_code_dialog(sort = 'Community'))
|
|
3559
3449
|
id_code_action = overlay_menu.addAction("Code Identities")
|
|
3560
3450
|
id_code_action.triggered.connect(lambda: self.show_code_dialog(sort = 'Identity'))
|
|
3451
|
+
umap_action = overlay_menu.addAction("Centroid UMAP")
|
|
3452
|
+
umap_action.triggered.connect(self.handle_umap)
|
|
3561
3453
|
|
|
3562
3454
|
rand_menu = analysis_menu.addMenu("Randomize")
|
|
3563
3455
|
random_action = rand_menu.addAction("Generate Equivalent Random Network")
|
|
@@ -3574,6 +3466,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3574
3466
|
calc_all_action.triggered.connect(self.show_calc_all_dialog)
|
|
3575
3467
|
calc_prox_action = calculate_menu.addAction("Calculate Proximity Network (connect nodes by distance)")
|
|
3576
3468
|
calc_prox_action.triggered.connect(self.show_calc_prox_dialog)
|
|
3469
|
+
calc_branch_action = calculate_menu.addAction("Calculate Branchpoint Network (Connect Branchpoints of Edge Image - Good for Nerves/Vessels)")
|
|
3470
|
+
calc_branch_action.triggered.connect(self.handle_calc_branch)
|
|
3471
|
+
calc_branchprox_action = calculate_menu.addAction("Calculate Branch Adjacency Network (Of Edges)")
|
|
3472
|
+
calc_branchprox_action.triggered.connect(self.handle_branchprox_calc)
|
|
3577
3473
|
centroid_action = calculate_menu.addAction("Calculate Centroids (Active Image)")
|
|
3578
3474
|
centroid_action.triggered.connect(self.show_centroid_dialog)
|
|
3579
3475
|
|
|
@@ -3597,13 +3493,17 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3597
3493
|
mask_action = image_menu.addAction("Mask Channel")
|
|
3598
3494
|
mask_action.triggered.connect(self.show_mask_dialog)
|
|
3599
3495
|
crop_action = image_menu.addAction("Crop Channels")
|
|
3600
|
-
crop_action.triggered.connect(self.show_crop_dialog)
|
|
3496
|
+
crop_action.triggered.connect(lambda: self.show_crop_dialog(args = None))
|
|
3601
3497
|
type_action = image_menu.addAction("Channel dtype")
|
|
3602
3498
|
type_action.triggered.connect(self.show_type_dialog)
|
|
3603
3499
|
skeletonize_action = image_menu.addAction("Skeletonize")
|
|
3604
3500
|
skeletonize_action.triggered.connect(self.show_skeletonize_dialog)
|
|
3605
|
-
|
|
3501
|
+
dt_action = image_menu.addAction("Distance Transform (For binary images)")
|
|
3502
|
+
dt_action.triggered.connect(self.show_dt_dialog)
|
|
3503
|
+
watershed_action = image_menu.addAction("Binary Watershed")
|
|
3606
3504
|
watershed_action.triggered.connect(self.show_watershed_dialog)
|
|
3505
|
+
gray_water_action = image_menu.addAction("Gray Watershed")
|
|
3506
|
+
gray_water_action.triggered.connect(self.show_gray_water_dialog)
|
|
3607
3507
|
invert_action = image_menu.addAction("Invert")
|
|
3608
3508
|
invert_action.triggered.connect(self.show_invert_dialog)
|
|
3609
3509
|
z_proj_action = image_menu.addAction("Z Project")
|
|
@@ -3615,7 +3515,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3615
3515
|
gennodes_action = generate_menu.addAction("Generate Nodes (From 'Edge' Vertices)")
|
|
3616
3516
|
gennodes_action.triggered.connect(self.show_gennodes_dialog)
|
|
3617
3517
|
branch_action = generate_menu.addAction("Label Branches")
|
|
3618
|
-
branch_action.triggered.connect(self.show_branch_dialog)
|
|
3518
|
+
branch_action.triggered.connect(lambda: self.show_branch_dialog())
|
|
3619
3519
|
genvor_action = generate_menu.addAction("Generate Voronoi Diagram (From Node Centroids) - goes in Overlay2")
|
|
3620
3520
|
genvor_action.triggered.connect(self.voronoi)
|
|
3621
3521
|
|
|
@@ -3654,6 +3554,43 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3654
3554
|
help_button = menubar.addAction("Help")
|
|
3655
3555
|
help_button.triggered.connect(self.help_me)
|
|
3656
3556
|
|
|
3557
|
+
cam_button = QPushButton("📷")
|
|
3558
|
+
cam_button.setFixedSize(40, 40)
|
|
3559
|
+
cam_button.setStyleSheet("font-size: 24px;") # Makes emoji larger
|
|
3560
|
+
cam_button.clicked.connect(self.snap)
|
|
3561
|
+
menubar.setCornerWidget(cam_button, Qt.Corner.TopRightCorner)
|
|
3562
|
+
|
|
3563
|
+
def snap(self):
|
|
3564
|
+
|
|
3565
|
+
try:
|
|
3566
|
+
|
|
3567
|
+
for thing in self.channel_data:
|
|
3568
|
+
if thing is not None:
|
|
3569
|
+
data = True
|
|
3570
|
+
if not data:
|
|
3571
|
+
return
|
|
3572
|
+
|
|
3573
|
+
snap = self.create_composite_for_pan()
|
|
3574
|
+
|
|
3575
|
+
filename, _ = QFileDialog.getSaveFileName(
|
|
3576
|
+
self,
|
|
3577
|
+
f"Save Image As",
|
|
3578
|
+
"", # Default directory
|
|
3579
|
+
"TIFF Files (*.tif *.tiff);;All Files (*)" # File type filter
|
|
3580
|
+
)
|
|
3581
|
+
|
|
3582
|
+
if filename: # Only proceed if user didn't cancel
|
|
3583
|
+
# If user didn't type an extension, add .tif
|
|
3584
|
+
if not filename.endswith(('.tif', '.tiff')):
|
|
3585
|
+
filename += '.tif'
|
|
3586
|
+
|
|
3587
|
+
import tifffile
|
|
3588
|
+
tifffile.imwrite(filename, snap)
|
|
3589
|
+
|
|
3590
|
+
except:
|
|
3591
|
+
pass
|
|
3592
|
+
|
|
3593
|
+
|
|
3657
3594
|
def open_cellpose(self):
|
|
3658
3595
|
|
|
3659
3596
|
try:
|
|
@@ -3919,6 +3856,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3919
3856
|
dialog = MergeNodeIdDialog(self)
|
|
3920
3857
|
dialog.exec()
|
|
3921
3858
|
|
|
3859
|
+
def show_gray_water_dialog(self):
|
|
3860
|
+
"""Show the gray watershed parameter dialog."""
|
|
3861
|
+
dialog = GrayWaterDialog(self)
|
|
3862
|
+
dialog.exec()
|
|
3922
3863
|
|
|
3923
3864
|
def show_watershed_dialog(self):
|
|
3924
3865
|
"""Show the watershed parameter dialog."""
|
|
@@ -3950,6 +3891,110 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3950
3891
|
dialog = ProxDialog(self)
|
|
3951
3892
|
dialog.exec()
|
|
3952
3893
|
|
|
3894
|
+
def table_load_attrs(self):
|
|
3895
|
+
|
|
3896
|
+
# Display network_lists in the network table
|
|
3897
|
+
try:
|
|
3898
|
+
if hasattr(my_network, 'network_lists'):
|
|
3899
|
+
model = PandasModel(my_network.network_lists)
|
|
3900
|
+
self.network_table.setModel(model)
|
|
3901
|
+
# Adjust column widths to content
|
|
3902
|
+
for column in range(model.columnCount(None)):
|
|
3903
|
+
self.network_table.resizeColumnToContents(column)
|
|
3904
|
+
except Exception as e:
|
|
3905
|
+
print(f"Error loading network_lists: {e}")
|
|
3906
|
+
|
|
3907
|
+
#Display the other things if they exist
|
|
3908
|
+
try:
|
|
3909
|
+
|
|
3910
|
+
if hasattr(my_network, 'node_identities') and my_network.node_identities is not None:
|
|
3911
|
+
try:
|
|
3912
|
+
self.format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity', 'Node Identities')
|
|
3913
|
+
except Exception as e:
|
|
3914
|
+
print(f"Error loading node identity table: {e}")
|
|
3915
|
+
|
|
3916
|
+
if hasattr(my_network, 'node_centroids') and my_network.node_centroids is not None:
|
|
3917
|
+
try:
|
|
3918
|
+
self.format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
|
|
3919
|
+
except Exception as e:
|
|
3920
|
+
print(f"Error loading node centroid table: {e}")
|
|
3921
|
+
|
|
3922
|
+
|
|
3923
|
+
if hasattr(my_network, 'edge_centroids') and my_network.edge_centroids is not None:
|
|
3924
|
+
try:
|
|
3925
|
+
self.format_for_upperright_table(my_network.edge_centroids, 'EdgeID', ['Z', 'Y', 'X'], 'Edge Centroids')
|
|
3926
|
+
except Exception as e:
|
|
3927
|
+
print(f"Error loading edge centroid table: {e}")
|
|
3928
|
+
|
|
3929
|
+
|
|
3930
|
+
except Exception as e:
|
|
3931
|
+
print(f"An error has occured: {e}")
|
|
3932
|
+
|
|
3933
|
+
def confirm_calcbranch_dialog(self, message):
|
|
3934
|
+
"""Shows a dialog asking user to confirm if they want to proceed below"""
|
|
3935
|
+
msg = QMessageBox()
|
|
3936
|
+
msg.setIcon(QMessageBox.Icon.Question)
|
|
3937
|
+
msg.setText("Alert")
|
|
3938
|
+
msg.setInformativeText(message)
|
|
3939
|
+
msg.setWindowTitle("Proceed?")
|
|
3940
|
+
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
3941
|
+
return msg.exec() == QMessageBox.StandardButton.Yes
|
|
3942
|
+
|
|
3943
|
+
def handle_calc_branch(self):
|
|
3944
|
+
|
|
3945
|
+
try:
|
|
3946
|
+
|
|
3947
|
+
if self.channel_data[0] is not None or self.channel_data[3] is not None:
|
|
3948
|
+
if not self.confirm_calcbranch_dialog("Use of this feature will require additional use of the Nodes and Overlay 2 channels. Please save any data and return, or proceed if you do not need those channels' data"):
|
|
3949
|
+
return
|
|
3950
|
+
|
|
3951
|
+
my_network.id_overlay = my_network.edges.copy()
|
|
3952
|
+
|
|
3953
|
+
self.show_gennodes_dialog()
|
|
3954
|
+
|
|
3955
|
+
my_network.edges = (my_network.nodes == 0) * my_network.edges
|
|
3956
|
+
|
|
3957
|
+
my_network.calculate_all(my_network.nodes, my_network.edges, xy_scale = my_network.xy_scale, z_scale = my_network.z_scale, search = None, diledge = None, inners = False, hash_inners = False, remove_trunk = 0, ignore_search_region = True, other_nodes = None, label_nodes = True, directory = None, GPU = False, fast_dil = False, skeletonize = False, GPU_downsample = None)
|
|
3958
|
+
|
|
3959
|
+
self.load_channel(1, my_network.edges, data = True)
|
|
3960
|
+
self.load_channel(0, my_network.nodes, data = True)
|
|
3961
|
+
self.load_channel(3, my_network.id_overlay, data = True)
|
|
3962
|
+
|
|
3963
|
+
self.table_load_attrs()
|
|
3964
|
+
|
|
3965
|
+
except Exception as e:
|
|
3966
|
+
|
|
3967
|
+
try:
|
|
3968
|
+
my_network.edges = my_network.id_overlay
|
|
3969
|
+
my_network.id_overlay = None
|
|
3970
|
+
except:
|
|
3971
|
+
pass
|
|
3972
|
+
|
|
3973
|
+
print(f"Error calculating branchpoint network: {e}")
|
|
3974
|
+
|
|
3975
|
+
def handle_branchprox_calc(self):
|
|
3976
|
+
|
|
3977
|
+
try:
|
|
3978
|
+
|
|
3979
|
+
if self.channel_data[0] is not None:
|
|
3980
|
+
if not self.confirm_calcbranch_dialog("Use of this feature will require additional use of the Nodes and Overlay 2 channels. Please save any data and return, or proceed if you do not need those channels' data"):
|
|
3981
|
+
return
|
|
3982
|
+
|
|
3983
|
+
self.show_branch_dialog(called = True)
|
|
3984
|
+
|
|
3985
|
+
self.load_channel(0, my_network.edges, data = True)
|
|
3986
|
+
|
|
3987
|
+
self.delete_channel(1, False)
|
|
3988
|
+
|
|
3989
|
+
my_network.morph_proximity(search = [3,3], fastdil = True)
|
|
3990
|
+
|
|
3991
|
+
self.table_load_attrs()
|
|
3992
|
+
|
|
3993
|
+
except Exception as e:
|
|
3994
|
+
|
|
3995
|
+
print(f"Error calculating network: {e}")
|
|
3996
|
+
|
|
3997
|
+
|
|
3953
3998
|
def show_centroid_dialog(self):
|
|
3954
3999
|
"""show the centroid dialog"""
|
|
3955
4000
|
dialog = CentroidDialog(self)
|
|
@@ -3994,9 +4039,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3994
4039
|
dialog = MaskDialog(self)
|
|
3995
4040
|
dialog.exec()
|
|
3996
4041
|
|
|
3997
|
-
def show_crop_dialog(self):
|
|
4042
|
+
def show_crop_dialog(self, args = None):
|
|
3998
4043
|
"""Show the crop dialog"""
|
|
3999
|
-
dialog = CropDialog(self)
|
|
4044
|
+
dialog = CropDialog(self, args = args)
|
|
4000
4045
|
dialog.exec()
|
|
4001
4046
|
|
|
4002
4047
|
def show_type_dialog(self):
|
|
@@ -4012,6 +4057,11 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4012
4057
|
dialog = SkeletonizeDialog(self)
|
|
4013
4058
|
dialog.exec()
|
|
4014
4059
|
|
|
4060
|
+
def show_dt_dialog(self):
|
|
4061
|
+
"""show the dt dialog"""
|
|
4062
|
+
dialog = DistanceDialog(self)
|
|
4063
|
+
dialog.exec()
|
|
4064
|
+
|
|
4015
4065
|
def show_centroid_node_dialog(self):
|
|
4016
4066
|
"""show the centroid node dialog"""
|
|
4017
4067
|
dialog = CentroidNodeDialog(self)
|
|
@@ -4023,9 +4073,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4023
4073
|
gennodes = GenNodesDialog(self, down_factor = down_factor, called = called)
|
|
4024
4074
|
gennodes.exec()
|
|
4025
4075
|
|
|
4026
|
-
def show_branch_dialog(self):
|
|
4076
|
+
def show_branch_dialog(self, called = False):
|
|
4027
4077
|
"""Show the branch label dialog"""
|
|
4028
|
-
dialog = BranchDialog(self)
|
|
4078
|
+
dialog = BranchDialog(self, called = called)
|
|
4029
4079
|
dialog.exec()
|
|
4030
4080
|
|
|
4031
4081
|
def voronoi(self):
|
|
@@ -4178,6 +4228,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4178
4228
|
if sort == 'Node Identities':
|
|
4179
4229
|
my_network.load_node_identities(file_path = filename)
|
|
4180
4230
|
|
|
4231
|
+
"""
|
|
4181
4232
|
first_value = list(my_network.node_identities.values())[0] # Check that there are not multiple IDs
|
|
4182
4233
|
if isinstance(first_value, (list, tuple)):
|
|
4183
4234
|
trump_value, ok = QInputDialog.getText(
|
|
@@ -4199,7 +4250,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4199
4250
|
else:
|
|
4200
4251
|
trump_value = None
|
|
4201
4252
|
my_network.node_identities = uncork(my_network.node_identities, trump_value)
|
|
4202
|
-
|
|
4253
|
+
"""
|
|
4203
4254
|
|
|
4204
4255
|
if hasattr(my_network, 'node_identities') and my_network.node_identities is not None:
|
|
4205
4256
|
try:
|
|
@@ -4316,6 +4367,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4316
4367
|
)
|
|
4317
4368
|
|
|
4318
4369
|
self.last_load = directory
|
|
4370
|
+
|
|
4319
4371
|
|
|
4320
4372
|
if directory != "":
|
|
4321
4373
|
|
|
@@ -4354,7 +4406,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4354
4406
|
# Display network_lists in the network table
|
|
4355
4407
|
# Create empty DataFrame for network table if network_lists is None
|
|
4356
4408
|
if not hasattr(my_network, 'network_lists') or my_network.network_lists is None:
|
|
4357
|
-
empty_df = pd.DataFrame(columns=['Node
|
|
4409
|
+
empty_df = pd.DataFrame(columns=['Node A', 'Node B', 'Edge C'])
|
|
4358
4410
|
model = PandasModel(empty_df)
|
|
4359
4411
|
self.network_table.setModel(model)
|
|
4360
4412
|
else:
|
|
@@ -4439,7 +4491,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4439
4491
|
"""Method to close Excelotron"""
|
|
4440
4492
|
self.excel_manager.close()
|
|
4441
4493
|
|
|
4442
|
-
def handle_excel_data(self, data_dict, property_name):
|
|
4494
|
+
def handle_excel_data(self, data_dict, property_name, add):
|
|
4443
4495
|
"""Handle data received from Excelotron"""
|
|
4444
4496
|
print(f"Received data for property: {property_name}")
|
|
4445
4497
|
print(f"Data keys: {list(data_dict.keys())}")
|
|
@@ -4448,12 +4500,19 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4448
4500
|
|
|
4449
4501
|
try:
|
|
4450
4502
|
|
|
4503
|
+
if not add or my_network.node_centroids is None:
|
|
4504
|
+
centroids = {}
|
|
4505
|
+
max_val = 0
|
|
4506
|
+
else:
|
|
4507
|
+
centroids = my_network.node_centroids
|
|
4508
|
+
max_val = max(list(my_network.node_centroids.keys()))
|
|
4509
|
+
|
|
4451
4510
|
ys = data_dict['Y']
|
|
4452
4511
|
xs = data_dict['X']
|
|
4453
4512
|
if 'Numerical IDs' in data_dict:
|
|
4454
4513
|
nodes = data_dict['Numerical IDs']
|
|
4455
4514
|
else:
|
|
4456
|
-
nodes = np.arange(1, len(ys) + 1)
|
|
4515
|
+
nodes = np.arange(max_val + 1, max_val + len(ys) + 1)
|
|
4457
4516
|
|
|
4458
4517
|
|
|
4459
4518
|
if 'Z' in data_dict:
|
|
@@ -4461,8 +4520,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4461
4520
|
else:
|
|
4462
4521
|
zs = np.zeros(len(ys))
|
|
4463
4522
|
|
|
4464
|
-
centroids = {}
|
|
4465
|
-
|
|
4466
4523
|
for i in range(len(nodes)):
|
|
4467
4524
|
|
|
4468
4525
|
centroids[nodes[i]] = [int(zs[i]), int(ys[i]), int(xs[i])]
|
|
@@ -4480,15 +4537,26 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4480
4537
|
|
|
4481
4538
|
try:
|
|
4482
4539
|
|
|
4540
|
+
if not add or my_network.node_identities is None:
|
|
4541
|
+
identities = {}
|
|
4542
|
+
max_val = 0
|
|
4543
|
+
else:
|
|
4544
|
+
identities = my_network.node_identities
|
|
4545
|
+
if my_network.node_centroids is not None:
|
|
4546
|
+
max_val = max(list(my_network.node_centroids.keys()))
|
|
4547
|
+
else:
|
|
4548
|
+
max_val = max(list(my_network.node_identities.keys()))
|
|
4549
|
+
|
|
4483
4550
|
idens = data_dict['Identity Column']
|
|
4484
4551
|
|
|
4485
4552
|
if 'Numerical IDs' in data_dict:
|
|
4486
4553
|
nodes = data_dict['Numerical IDs']
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
identities = {}
|
|
4554
|
+
if add:
|
|
4555
|
+
for i, node in enumerate(nodes):
|
|
4556
|
+
nodes[i] = node + max_val
|
|
4491
4557
|
|
|
4558
|
+
else:
|
|
4559
|
+
nodes = np.arange(max_val + 1, max_val + len(data_dict['Identity Column']) + 1)
|
|
4492
4560
|
|
|
4493
4561
|
for i in range(len(nodes)):
|
|
4494
4562
|
|
|
@@ -4507,14 +4575,20 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4507
4575
|
|
|
4508
4576
|
try:
|
|
4509
4577
|
|
|
4578
|
+
if not add or my_network.communities is None:
|
|
4579
|
+
communities = {}
|
|
4580
|
+
max_val = 0
|
|
4581
|
+
else:
|
|
4582
|
+
communities = my_network.communities
|
|
4583
|
+
max_val = max(list(my_network.communities.keys()))
|
|
4584
|
+
|
|
4585
|
+
|
|
4510
4586
|
coms = data_dict['Community Identifier']
|
|
4511
4587
|
|
|
4512
4588
|
if 'Numerical IDs' in data_dict:
|
|
4513
4589
|
nodes = data_dict['Numerical IDs']
|
|
4514
4590
|
else:
|
|
4515
|
-
nodes = np.arange(1, len(
|
|
4516
|
-
|
|
4517
|
-
communities = {}
|
|
4591
|
+
nodes = np.arange(max_val + 1, max_val + len(data_dict['Community Identifier']) + 1)
|
|
4518
4592
|
|
|
4519
4593
|
for i in range(len(nodes)):
|
|
4520
4594
|
|
|
@@ -4556,6 +4630,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4556
4630
|
- 'mean': averages across color channels
|
|
4557
4631
|
- 'max': takes maximum value across color channels
|
|
4558
4632
|
- 'min': takes minimum value across color channels
|
|
4633
|
+
- 'weight': takes weighted channel averages
|
|
4559
4634
|
|
|
4560
4635
|
Returns:
|
|
4561
4636
|
--------
|
|
@@ -4570,7 +4645,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4570
4645
|
if array.ndim != 4:
|
|
4571
4646
|
raise ValueError(f"Expected 4D array, got {array.ndim}D array")
|
|
4572
4647
|
|
|
4573
|
-
if method not in ['first', 'mean', 'max', 'min']:
|
|
4648
|
+
if method not in ['first', 'mean', 'max', 'min', 'weight']:
|
|
4574
4649
|
raise ValueError(f"Unknown method: {method}")
|
|
4575
4650
|
|
|
4576
4651
|
if method == 'first':
|
|
@@ -4579,6 +4654,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4579
4654
|
return np.mean(array, axis=-1)
|
|
4580
4655
|
elif method == 'max':
|
|
4581
4656
|
return np.max(array, axis=-1)
|
|
4657
|
+
elif method == 'weight':
|
|
4658
|
+
# Apply the luminosity formula
|
|
4659
|
+
return (0.2989 * array[:,:,:,0] + 0.5870 * array[:,:,:,1] + 0.1140 * array[:,:,:,2])
|
|
4582
4660
|
else: # min
|
|
4583
4661
|
return np.min(array, axis=-1)
|
|
4584
4662
|
|
|
@@ -4602,7 +4680,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4602
4680
|
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
4603
4681
|
return msg.exec() == QMessageBox.StandardButton.Yes
|
|
4604
4682
|
|
|
4605
|
-
def load_channel(self, channel_index, channel_data=None, data=False, assign_shape = True, preserve_zoom = None, end_paint = False, begin_paint = False):
|
|
4683
|
+
def load_channel(self, channel_index, channel_data=None, data=False, assign_shape = True, preserve_zoom = None, end_paint = False, begin_paint = False, color = False):
|
|
4606
4684
|
"""Load a channel and enable active channel selection if needed."""
|
|
4607
4685
|
|
|
4608
4686
|
try:
|
|
@@ -4627,7 +4705,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4627
4705
|
import tifffile
|
|
4628
4706
|
self.channel_data[channel_index] = tifffile.imread(filename)
|
|
4629
4707
|
|
|
4630
|
-
|
|
4631
4708
|
elif file_extension == 'nii':
|
|
4632
4709
|
import nibabel as nib
|
|
4633
4710
|
nii_img = nib.load(filename)
|
|
@@ -4657,9 +4734,20 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4657
4734
|
self.channel_buttons[channel_index].setEnabled(False)
|
|
4658
4735
|
self.delete_buttons[channel_index].setEnabled(False)
|
|
4659
4736
|
|
|
4737
|
+
try:
|
|
4738
|
+
#if len(self.channel_data[channel_index].shape) == 4:
|
|
4739
|
+
if 1 in self.channel_data[channel_index].shape:
|
|
4740
|
+
print("Removing singleton dimension (I am assuming this is a channel dimension?)")
|
|
4741
|
+
self.channel_data[channel_index] = np.squeeze(self.channel_data[channel_index])
|
|
4742
|
+
except:
|
|
4743
|
+
pass
|
|
4744
|
+
|
|
4660
4745
|
if len(self.channel_data[channel_index].shape) == 2: # handle 2d data
|
|
4661
4746
|
self.channel_data[channel_index] = np.expand_dims(self.channel_data[channel_index], axis=0)
|
|
4662
4747
|
|
|
4748
|
+
if self.channel_data[channel_index].dtype == np.bool_: #Promote boolean arrays if they somehow get loaded
|
|
4749
|
+
self.channel_data[channel_index] = self.channel_data[channel_index].astype(np.uint8)
|
|
4750
|
+
|
|
4663
4751
|
try:
|
|
4664
4752
|
if len(self.channel_data[channel_index].shape) == 3: # potentially 2D RGB
|
|
4665
4753
|
if self.channel_data[channel_index].shape[-1] in (3, 4): # last dim is 3 or 4
|
|
@@ -4668,12 +4756,13 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4668
4756
|
self.channel_data[channel_index] = np.expand_dims(self.channel_data[channel_index], axis=0)
|
|
4669
4757
|
except:
|
|
4670
4758
|
pass
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
self.channel_data[channel_index]
|
|
4675
|
-
|
|
4676
|
-
|
|
4759
|
+
|
|
4760
|
+
if not color:
|
|
4761
|
+
try:
|
|
4762
|
+
if len(self.channel_data[channel_index].shape) == 4 and (channel_index == 0 or channel_index == 1):
|
|
4763
|
+
self.channel_data[channel_index] = self.reduce_rgb_dimension(self.channel_data[channel_index], 'weight')
|
|
4764
|
+
except:
|
|
4765
|
+
pass
|
|
4677
4766
|
|
|
4678
4767
|
reset_resize = False
|
|
4679
4768
|
|
|
@@ -4748,8 +4837,13 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4748
4837
|
if all(not btn.isEnabled() for btn in self.channel_buttons[:channel_index]):
|
|
4749
4838
|
self.set_active_channel(channel_index)
|
|
4750
4839
|
|
|
4751
|
-
if
|
|
4752
|
-
self.channel_buttons[channel_index].
|
|
4840
|
+
if self.chan_load:
|
|
4841
|
+
if not self.channel_buttons[channel_index].isChecked():
|
|
4842
|
+
self.channel_buttons[channel_index].click()
|
|
4843
|
+
else:
|
|
4844
|
+
if self.channel_buttons[channel_index].isChecked():
|
|
4845
|
+
self.channel_buttons[channel_index].click()
|
|
4846
|
+
|
|
4753
4847
|
self.min_max[channel_index][0] = np.min(self.channel_data[channel_index])
|
|
4754
4848
|
self.min_max[channel_index][1] = np.max(self.channel_data[channel_index])
|
|
4755
4849
|
self.volume_dict[channel_index] = None #reset volumes
|
|
@@ -4849,7 +4943,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4849
4943
|
my_network.communities = None
|
|
4850
4944
|
|
|
4851
4945
|
# Create empty DataFrame
|
|
4852
|
-
empty_df = pd.DataFrame(columns=['Node
|
|
4946
|
+
empty_df = pd.DataFrame(columns=['Node A', 'Node B', 'Edge C'])
|
|
4853
4947
|
|
|
4854
4948
|
# Clear network table
|
|
4855
4949
|
self.network_table.setModel(PandasModel(empty_df))
|
|
@@ -5018,25 +5112,29 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5018
5112
|
|
|
5019
5113
|
|
|
5020
5114
|
|
|
5021
|
-
def update_display(self, preserve_zoom=None, dims = None, called = False, reset_resize = False):
|
|
5115
|
+
def update_display(self, preserve_zoom=None, dims = None, called = False, reset_resize = False, continue_paint = False, skip_paint_reinit = False):
|
|
5022
5116
|
"""Update the display with currently visible channels and highlight overlay."""
|
|
5023
|
-
|
|
5024
5117
|
try:
|
|
5025
|
-
|
|
5026
5118
|
self.figure.clear()
|
|
5027
|
-
|
|
5028
5119
|
if self.pan_background_image is not None:
|
|
5029
5120
|
# Restore previously visible channels
|
|
5030
5121
|
self.channel_visible = self.pre_pan_channel_state.copy()
|
|
5031
5122
|
self.is_pan_preview = False
|
|
5032
5123
|
self.pan_background_image = None
|
|
5033
|
-
|
|
5034
5124
|
if self.machine_window is not None:
|
|
5035
5125
|
if self.machine_window.segmentation_worker is not None:
|
|
5036
5126
|
self.machine_window.segmentation_worker.resume()
|
|
5037
|
-
|
|
5038
5127
|
if self.static_background is not None:
|
|
5039
|
-
|
|
5128
|
+
# NEW: Convert virtual strokes to real data before cleanup
|
|
5129
|
+
if (hasattr(self, 'virtual_draw_operations') and self.virtual_draw_operations) or \
|
|
5130
|
+
(hasattr(self, 'virtual_erase_operations') and self.virtual_erase_operations) or \
|
|
5131
|
+
(hasattr(self, 'current_operation') and self.current_operation):
|
|
5132
|
+
# Finish current operation first
|
|
5133
|
+
if hasattr(self, 'current_operation') and self.current_operation:
|
|
5134
|
+
self.pm.finish_current_virtual_operation()
|
|
5135
|
+
# Now convert to real data
|
|
5136
|
+
self.pm.convert_virtual_strokes_to_data()
|
|
5137
|
+
|
|
5040
5138
|
# Restore hidden channels
|
|
5041
5139
|
try:
|
|
5042
5140
|
for i in self.restore_channels:
|
|
@@ -5044,27 +5142,17 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5044
5142
|
self.restore_channels = []
|
|
5045
5143
|
except:
|
|
5046
5144
|
pass
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
# OPTIMIZED: Stop timer and process any pending paint operations
|
|
5051
|
-
if hasattr(self, 'paint_timer'):
|
|
5052
|
-
self.paint_timer.stop()
|
|
5053
|
-
if hasattr(self, 'pending_paint_update') and self.pending_paint_update:
|
|
5054
|
-
self.flush_paint_updates()
|
|
5055
|
-
|
|
5056
|
-
self.static_background = None
|
|
5057
|
-
|
|
5058
|
-
if self.machine_window is None:
|
|
5059
|
-
|
|
5060
|
-
try:
|
|
5145
|
+
if not continue_paint:
|
|
5146
|
+
self.static_background = None
|
|
5061
5147
|
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5148
|
+
if self.machine_window is None:
|
|
5149
|
+
try:
|
|
5150
|
+
self.channel_data[4][self.current_slice, :, :] = n3d.overlay_arrays_simple(self.channel_data[self.temp_chan][self.current_slice, :, :], self.channel_data[4][self.current_slice, :, :])
|
|
5151
|
+
self.load_channel(self.temp_chan, self.channel_data[4], data = True, end_paint = True)
|
|
5152
|
+
self.channel_data[4] = None
|
|
5153
|
+
self.channel_visible[4] = False
|
|
5154
|
+
except:
|
|
5155
|
+
pass
|
|
5068
5156
|
|
|
5069
5157
|
# Get active channels and their dimensions
|
|
5070
5158
|
active_channels = [i for i in range(4) if self.channel_data[i] is not None]
|
|
@@ -5295,49 +5383,22 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5295
5383
|
|
|
5296
5384
|
self.canvas.draw()
|
|
5297
5385
|
|
|
5386
|
+
if self.brush_mode and not skip_paint_reinit:
|
|
5387
|
+
# Get current zoom to preserve it
|
|
5388
|
+
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
5389
|
+
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
5390
|
+
|
|
5391
|
+
if self.pen_button.isChecked():
|
|
5392
|
+
channel = self.active_channel
|
|
5393
|
+
else:
|
|
5394
|
+
channel = 2
|
|
5395
|
+
|
|
5396
|
+
self.pm.initiate_paint_session(channel, current_xlim, current_ylim)
|
|
5397
|
+
|
|
5298
5398
|
except:
|
|
5299
5399
|
import traceback
|
|
5300
5400
|
print(traceback.format_exc())
|
|
5301
5401
|
|
|
5302
|
-
def update_display_slice_optimized(self, channel, preserve_zoom=None):
|
|
5303
|
-
"""Ultra minimal update that only changes the paint channel's data - OPTIMIZED VERSION"""
|
|
5304
|
-
if not self.channel_visible[channel]:
|
|
5305
|
-
return
|
|
5306
|
-
|
|
5307
|
-
if preserve_zoom:
|
|
5308
|
-
current_xlim, current_ylim = preserve_zoom
|
|
5309
|
-
if current_xlim is not None and current_ylim is not None:
|
|
5310
|
-
self.ax.set_xlim(current_xlim)
|
|
5311
|
-
self.ax.set_ylim(current_ylim)
|
|
5312
|
-
|
|
5313
|
-
# Find the existing image for channel (paint channel)
|
|
5314
|
-
channel_image = None
|
|
5315
|
-
for img in self.ax.images:
|
|
5316
|
-
if img.cmap.name == f'custom_{channel}':
|
|
5317
|
-
channel_image = img
|
|
5318
|
-
break
|
|
5319
|
-
|
|
5320
|
-
if channel_image is not None:
|
|
5321
|
-
# Update the data of the existing image with thread safety
|
|
5322
|
-
with self.paint_lock:
|
|
5323
|
-
channel_image.set_array(self.channel_data[channel][self.current_slice])
|
|
5324
|
-
|
|
5325
|
-
# Restore the static background (all other channels) at current zoom level
|
|
5326
|
-
# This is the key - use static_background from update_display, not paint_background
|
|
5327
|
-
if hasattr(self, 'static_background') and self.static_background is not None:
|
|
5328
|
-
self.canvas.restore_region(self.static_background)
|
|
5329
|
-
# Draw just our paint channel
|
|
5330
|
-
self.ax.draw_artist(channel_image)
|
|
5331
|
-
# Blit everything
|
|
5332
|
-
self.canvas.blit(self.ax.bbox)
|
|
5333
|
-
self.canvas.flush_events()
|
|
5334
|
-
else:
|
|
5335
|
-
# Fallback to full draw if no static background
|
|
5336
|
-
self.canvas.draw()
|
|
5337
|
-
else:
|
|
5338
|
-
# Fallback if channel image not found
|
|
5339
|
-
self.canvas.draw()
|
|
5340
|
-
|
|
5341
5402
|
def get_channel_image(self, channel):
|
|
5342
5403
|
"""Find the matplotlib image object for a specific channel."""
|
|
5343
5404
|
if not hasattr(self.ax, 'images'):
|
|
@@ -5467,6 +5528,13 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5467
5528
|
dialog = CodeDialog(self, sort = sort)
|
|
5468
5529
|
dialog.exec()
|
|
5469
5530
|
|
|
5531
|
+
def handle_umap(self):
|
|
5532
|
+
|
|
5533
|
+
if my_network.node_centroids is None:
|
|
5534
|
+
self.show_centroid_dialog()
|
|
5535
|
+
|
|
5536
|
+
my_network.centroid_umap()
|
|
5537
|
+
|
|
5470
5538
|
def closeEvent(self, event):
|
|
5471
5539
|
"""Override closeEvent to close all windows when main window closes"""
|
|
5472
5540
|
|
|
@@ -6009,9 +6077,9 @@ class PandasModel(QAbstractTableModel):
|
|
|
6009
6077
|
if data is None:
|
|
6010
6078
|
# Create an empty DataFrame with default columns
|
|
6011
6079
|
import pandas as pd
|
|
6012
|
-
data = pd.DataFrame(columns=['Node
|
|
6080
|
+
data = pd.DataFrame(columns=['Node A', 'Node B', 'Edge C'])
|
|
6013
6081
|
elif type(data) == list:
|
|
6014
|
-
data = self.lists_to_dataframe(data[0], data[1], data[2], column_names=['Node
|
|
6082
|
+
data = self.lists_to_dataframe(data[0], data[1], data[2], column_names=['Node A', 'Node B', 'Edge C'])
|
|
6015
6083
|
self._data = data
|
|
6016
6084
|
self.bold_cells = set()
|
|
6017
6085
|
self.highlighted_cells = set()
|
|
@@ -6752,7 +6820,7 @@ class MergeNodeIdDialog(QDialog):
|
|
|
6752
6820
|
|
|
6753
6821
|
self.parent().format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity')
|
|
6754
6822
|
|
|
6755
|
-
QMessageBox.
|
|
6823
|
+
QMessageBox.information(
|
|
6756
6824
|
self,
|
|
6757
6825
|
"Success",
|
|
6758
6826
|
"Node Identities Merged. New IDs represent presence of corresponding img foreground with +, absence with -. Please save your new identities as csv, then use File -> Load -> Load From Excel Helper to bulk search and rename desired combinations. (Press Help [above] for more info)"
|
|
@@ -6987,24 +7055,29 @@ class ColorOverlayDialog(QDialog):
|
|
|
6987
7055
|
|
|
6988
7056
|
def coloroverlay(self):
|
|
6989
7057
|
|
|
6990
|
-
|
|
7058
|
+
try:
|
|
6991
7059
|
|
|
6992
|
-
|
|
6993
|
-
|
|
6994
|
-
self.
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
|
|
7060
|
+
down_factor = float(self.down_factor.text()) if self.down_factor.text().strip() else None
|
|
7061
|
+
|
|
7062
|
+
if self.parent().active_channel == 0:
|
|
7063
|
+
mode = 0
|
|
7064
|
+
self.sort = 'Node'
|
|
7065
|
+
else:
|
|
7066
|
+
mode = 1
|
|
7067
|
+
self.sort = 'Edge'
|
|
6998
7068
|
|
|
6999
7069
|
|
|
7000
|
-
|
|
7070
|
+
result, legend = my_network.node_to_color(down_factor = down_factor, mode = mode)
|
|
7001
7071
|
|
|
7002
|
-
|
|
7072
|
+
self.parent().format_for_upperright_table(legend, f'{self.sort} Id', f'Encoding Val: {self.sort}', 'Legend')
|
|
7003
7073
|
|
|
7004
7074
|
|
|
7005
|
-
|
|
7075
|
+
self.parent().load_channel(3, channel_data = result, data = True)
|
|
7006
7076
|
|
|
7007
|
-
|
|
7077
|
+
self.accept()
|
|
7078
|
+
|
|
7079
|
+
except:
|
|
7080
|
+
pass
|
|
7008
7081
|
|
|
7009
7082
|
|
|
7010
7083
|
class ShuffleDialog(QDialog):
|
|
@@ -7211,6 +7284,8 @@ class PartitionDialog(QDialog):
|
|
|
7211
7284
|
|
|
7212
7285
|
def partition(self):
|
|
7213
7286
|
|
|
7287
|
+
self.parent().prev_coms = None
|
|
7288
|
+
|
|
7214
7289
|
accepted_mode = self.mode_selector.currentIndex()
|
|
7215
7290
|
weighted = self.weighted.isChecked()
|
|
7216
7291
|
dostats = self.stats.isChecked()
|
|
@@ -7345,7 +7420,7 @@ class ComNeighborDialog(QDialog):
|
|
|
7345
7420
|
# weighted checkbox (default True)
|
|
7346
7421
|
self.proportional = QPushButton("Robust")
|
|
7347
7422
|
self.proportional.setCheckable(True)
|
|
7348
|
-
self.proportional.setChecked(
|
|
7423
|
+
self.proportional.setChecked(True)
|
|
7349
7424
|
layout.addRow("Return Node Type Distribution Robust Heatmaps (ie, will give two more heatmaps that are not beholden to the total number of nodes of each type, representing which structures are overrepresented in a network):", self.proportional)
|
|
7350
7425
|
|
|
7351
7426
|
self.mode = QComboBox()
|
|
@@ -7393,7 +7468,7 @@ class ComNeighborDialog(QDialog):
|
|
|
7393
7468
|
self.parent().format_for_upperright_table(matrix, 'NeighborhoodID', id_set, title = f'Neighborhood Heatmap {i + 1}')
|
|
7394
7469
|
|
|
7395
7470
|
|
|
7396
|
-
self.parent().format_for_upperright_table(len_dict, 'NeighborhoodID', 'Proportion of Total Nodes', title = 'Neighborhood Counts')
|
|
7471
|
+
self.parent().format_for_upperright_table(len_dict, 'NeighborhoodID', ['Number of Communities', 'Proportion of Total Nodes'], title = 'Neighborhood Counts')
|
|
7397
7472
|
self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'NeighborhoodID', title = 'Neighborhood Partition')
|
|
7398
7473
|
|
|
7399
7474
|
print("Neighborhoods have been assigned to communities based on similarity")
|
|
@@ -7435,6 +7510,8 @@ class ComCellDialog(QDialog):
|
|
|
7435
7510
|
|
|
7436
7511
|
try:
|
|
7437
7512
|
|
|
7513
|
+
self.parent().prev_coms = None
|
|
7514
|
+
|
|
7438
7515
|
size = float(self.size.text()) if self.size.text().strip() else None
|
|
7439
7516
|
xy_scale = float(self.xy_scale.text()) if self.xy_scale.text().strip() else 1
|
|
7440
7517
|
z_scale = float(self.z_scale.text()) if self.z_scale.text().strip() else 1
|
|
@@ -7596,6 +7673,16 @@ class NearNeighDialog(QDialog):
|
|
|
7596
7673
|
heatmap_layout.addRow("Overlay:", self.numpy)
|
|
7597
7674
|
|
|
7598
7675
|
main_layout.addWidget(heatmap_group)
|
|
7676
|
+
|
|
7677
|
+
quant_group = QGroupBox("Quantifiable Overlay")
|
|
7678
|
+
quant_layout = QFormLayout(quant_group)
|
|
7679
|
+
|
|
7680
|
+
self.quant = QPushButton("Return quantifiable overlay? (Labels nodes by distance, good with intensity-thresholding to isolate targets. Requires labeled nodes image.)")
|
|
7681
|
+
self.quant.setCheckable(True)
|
|
7682
|
+
self.quant.setChecked(False)
|
|
7683
|
+
quant_layout.addRow("Overlay:", self.quant)
|
|
7684
|
+
|
|
7685
|
+
main_layout.addWidget(quant_group)
|
|
7599
7686
|
|
|
7600
7687
|
# Get Distribution group box
|
|
7601
7688
|
distribution_group = QGroupBox("Get Distribution")
|
|
@@ -7643,6 +7730,7 @@ class NearNeighDialog(QDialog):
|
|
|
7643
7730
|
threed = self.threed.isChecked()
|
|
7644
7731
|
numpy = self.numpy.isChecked()
|
|
7645
7732
|
num = int(self.num.text()) if self.num.text().strip() else 1
|
|
7733
|
+
quant = self.quant.isChecked()
|
|
7646
7734
|
|
|
7647
7735
|
if root is not None and targ is not None:
|
|
7648
7736
|
title = f"Nearest {num} Neighbor(s) Distance of {targ} from {root}"
|
|
@@ -7659,11 +7747,14 @@ class NearNeighDialog(QDialog):
|
|
|
7659
7747
|
return
|
|
7660
7748
|
|
|
7661
7749
|
if not numpy:
|
|
7662
|
-
avg, output = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, heatmap = heatmap, threed = threed)
|
|
7750
|
+
avg, output, quant_overlay = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, heatmap = heatmap, threed = threed, quant = quant)
|
|
7663
7751
|
else:
|
|
7664
|
-
avg, output, overlay = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, heatmap = heatmap, threed = threed, numpy = True)
|
|
7752
|
+
avg, output, overlay, quant_overlay = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, heatmap = heatmap, threed = threed, numpy = True, quant = quant)
|
|
7665
7753
|
self.parent().load_channel(3, overlay, data = True)
|
|
7666
7754
|
|
|
7755
|
+
if quant_overlay is not None:
|
|
7756
|
+
self.parent().load_channel(2, quant_overlay, data = True)
|
|
7757
|
+
|
|
7667
7758
|
self.parent().format_for_upperright_table([avg], metric = f'Avg {title}', title = f'Avg {title}')
|
|
7668
7759
|
self.parent().format_for_upperright_table(output, header2, header, title = title)
|
|
7669
7760
|
|
|
@@ -7691,7 +7782,7 @@ class NearNeighDialog(QDialog):
|
|
|
7691
7782
|
|
|
7692
7783
|
for targ in available:
|
|
7693
7784
|
|
|
7694
|
-
avg, _ = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num)
|
|
7785
|
+
avg, _, _ = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num)
|
|
7695
7786
|
|
|
7696
7787
|
output_dict[f"{root} vs {targ}"] = avg
|
|
7697
7788
|
|
|
@@ -7781,52 +7872,86 @@ class NeighborIdentityDialog(QDialog):
|
|
|
7781
7872
|
|
|
7782
7873
|
|
|
7783
7874
|
class RipleyDialog(QDialog):
|
|
7784
|
-
|
|
7785
7875
|
def __init__(self, parent=None):
|
|
7786
|
-
|
|
7787
7876
|
super().__init__(parent)
|
|
7788
7877
|
self.setWindowTitle(f"Find Ripley's H Function From Centroids")
|
|
7789
7878
|
self.setModal(True)
|
|
7790
|
-
|
|
7791
|
-
|
|
7792
|
-
|
|
7879
|
+
|
|
7880
|
+
# Main layout
|
|
7881
|
+
main_layout = QVBoxLayout(self)
|
|
7882
|
+
|
|
7883
|
+
# Node Parameters Group (only if node_identities exist)
|
|
7793
7884
|
if my_network.node_identities is not None:
|
|
7885
|
+
node_group = QGroupBox("Node Parameters")
|
|
7886
|
+
node_layout = QFormLayout(node_group)
|
|
7887
|
+
|
|
7794
7888
|
self.root = QComboBox()
|
|
7795
7889
|
self.root.addItems(list(set(my_network.node_identities.values())))
|
|
7796
7890
|
self.root.setCurrentIndex(0)
|
|
7797
|
-
|
|
7798
|
-
|
|
7799
|
-
self.root = None
|
|
7800
|
-
|
|
7801
|
-
if my_network.node_identities is not None:
|
|
7891
|
+
node_layout.addRow("Root Identity to Search for Neighbors:", self.root)
|
|
7892
|
+
|
|
7802
7893
|
self.targ = QComboBox()
|
|
7803
7894
|
self.targ.addItems(list(set(my_network.node_identities.values())))
|
|
7804
7895
|
self.targ.setCurrentIndex(0)
|
|
7805
|
-
|
|
7896
|
+
node_layout.addRow("Target Identity to be Searched For:", self.targ)
|
|
7897
|
+
|
|
7898
|
+
main_layout.addWidget(node_group)
|
|
7806
7899
|
else:
|
|
7900
|
+
self.root = None
|
|
7807
7901
|
self.targ = None
|
|
7808
|
-
|
|
7902
|
+
|
|
7903
|
+
# Search Parameters Group
|
|
7904
|
+
search_group = QGroupBox("Search Parameters")
|
|
7905
|
+
search_layout = QFormLayout(search_group)
|
|
7906
|
+
|
|
7809
7907
|
self.distance = QLineEdit("5")
|
|
7810
|
-
|
|
7811
|
-
|
|
7812
|
-
|
|
7908
|
+
search_layout.addRow("1. Bucket Distance for Searching For Clusters\n(automatically scaled by xy and z scales):", self.distance)
|
|
7909
|
+
|
|
7813
7910
|
self.proportion = QLineEdit("0.5")
|
|
7814
|
-
|
|
7815
|
-
|
|
7911
|
+
search_layout.addRow("2. Proportion of image to search?\n(0-1, high vals increase border artifacts):", self.proportion)
|
|
7912
|
+
|
|
7913
|
+
main_layout.addWidget(search_group)
|
|
7914
|
+
|
|
7915
|
+
# Border Safety Group
|
|
7916
|
+
border_group = QGroupBox("Border Safety")
|
|
7917
|
+
border_layout = QFormLayout(border_group)
|
|
7918
|
+
|
|
7919
|
+
self.ignore = QPushButton("Ignore Border Roots")
|
|
7920
|
+
self.ignore.setCheckable(True)
|
|
7921
|
+
self.ignore.setChecked(True)
|
|
7922
|
+
border_layout.addRow("3. Exclude Root Nodes Near Borders?:", self.ignore)
|
|
7923
|
+
|
|
7924
|
+
self.factor = QLineEdit("0.5")
|
|
7925
|
+
border_layout.addRow("4. (If param 3): Proportion of most internal nodes to use? (0 < n < 1) (Higher = more internal)?:", self.factor)
|
|
7926
|
+
|
|
7927
|
+
self.mode = QComboBox()
|
|
7928
|
+
self.mode.addItems(["Boundaries of Entire Image", "Boundaries of Edge Image Mask",
|
|
7929
|
+
"Boundaries of Overlay1 Mask", "Boundaries of Overlay2 Mask"])
|
|
7930
|
+
self.mode.setCurrentIndex(0)
|
|
7931
|
+
border_layout.addRow("5. (If param 3): Define Boundaries How?:", self.mode)
|
|
7932
|
+
|
|
7933
|
+
self.safe = QPushButton("Ignore Border Radii")
|
|
7934
|
+
self.safe.setCheckable(True)
|
|
7935
|
+
self.safe.setChecked(True)
|
|
7936
|
+
border_layout.addRow("6. (If param 3): Keep search radii within border (overrides Param 2, also assigns volume to that of mask)?:", self.safe)
|
|
7937
|
+
|
|
7938
|
+
main_layout.addWidget(border_group)
|
|
7939
|
+
|
|
7940
|
+
# Experimental Border Safety Group
|
|
7941
|
+
experimental_group = QGroupBox("Aggressive Border Safety (Creates duplicate centroids reflected across the image border - if you really need to search there for whatever reason - Not meant to be used if confining search to a masked object)")
|
|
7942
|
+
experimental_layout = QFormLayout(experimental_group)
|
|
7943
|
+
|
|
7816
7944
|
self.edgecorrect = QPushButton("Border Correction")
|
|
7817
7945
|
self.edgecorrect.setCheckable(True)
|
|
7818
7946
|
self.edgecorrect.setChecked(False)
|
|
7819
|
-
|
|
7820
|
-
|
|
7821
|
-
|
|
7822
|
-
|
|
7823
|
-
self.ignore.setChecked(False)
|
|
7824
|
-
layout.addRow("Exclude Root Nodes Near Borders?:", self.ignore)
|
|
7825
|
-
|
|
7947
|
+
experimental_layout.addRow("7. Use Border Correction\n(Extrapolate for points beyond the border):", self.edgecorrect)
|
|
7948
|
+
|
|
7949
|
+
main_layout.addWidget(experimental_group)
|
|
7950
|
+
|
|
7826
7951
|
# Add Run button
|
|
7827
7952
|
run_button = QPushButton("Get Ripley's H")
|
|
7828
7953
|
run_button.clicked.connect(self.ripley)
|
|
7829
|
-
|
|
7954
|
+
main_layout.addWidget(run_button)
|
|
7830
7955
|
|
|
7831
7956
|
def ripley(self):
|
|
7832
7957
|
|
|
@@ -7856,6 +7981,16 @@ class RipleyDialog(QDialog):
|
|
|
7856
7981
|
except:
|
|
7857
7982
|
proportion = 0.5
|
|
7858
7983
|
|
|
7984
|
+
try:
|
|
7985
|
+
factor = abs(float(self.factor.text()))
|
|
7986
|
+
|
|
7987
|
+
except:
|
|
7988
|
+
factor = 0.25
|
|
7989
|
+
|
|
7990
|
+
if factor > 1 or factor <= 0:
|
|
7991
|
+
print("Utilizing factor = 0.25")
|
|
7992
|
+
factor = 0.25
|
|
7993
|
+
|
|
7859
7994
|
if proportion > 1 or proportion <= 0:
|
|
7860
7995
|
print("Utilizing proportion = 0.5")
|
|
7861
7996
|
proportion = 0.5
|
|
@@ -7865,6 +8000,13 @@ class RipleyDialog(QDialog):
|
|
|
7865
8000
|
|
|
7866
8001
|
ignore = self.ignore.isChecked()
|
|
7867
8002
|
|
|
8003
|
+
safe = self.safe.isChecked()
|
|
8004
|
+
|
|
8005
|
+
mode = self.mode.currentIndex()
|
|
8006
|
+
|
|
8007
|
+
if mode == 0:
|
|
8008
|
+
factor = factor/2 #The logic treats this as distance to border later, only if mode is 0, but its supposed to represent proportion internal.
|
|
8009
|
+
|
|
7868
8010
|
if my_network.nodes is not None:
|
|
7869
8011
|
|
|
7870
8012
|
if my_network.nodes.shape[0] == 1:
|
|
@@ -7874,7 +8016,7 @@ class RipleyDialog(QDialog):
|
|
|
7874
8016
|
else:
|
|
7875
8017
|
bounds = None
|
|
7876
8018
|
|
|
7877
|
-
r_vals, k_vals, h_vals = my_network.get_ripley(root, targ, distance, edgecorrect, bounds, ignore, proportion)
|
|
8019
|
+
r_vals, k_vals, h_vals = my_network.get_ripley(root, targ, distance, edgecorrect, bounds, ignore, proportion, mode, safe, factor)
|
|
7878
8020
|
|
|
7879
8021
|
k_dict = dict(zip(r_vals, k_vals))
|
|
7880
8022
|
h_dict = dict(zip(r_vals, h_vals))
|
|
@@ -7887,6 +8029,9 @@ class RipleyDialog(QDialog):
|
|
|
7887
8029
|
self.accept()
|
|
7888
8030
|
|
|
7889
8031
|
except Exception as e:
|
|
8032
|
+
import traceback
|
|
8033
|
+
print(traceback.format_exc())
|
|
8034
|
+
|
|
7890
8035
|
QMessageBox.critical(
|
|
7891
8036
|
self,
|
|
7892
8037
|
"Error:",
|
|
@@ -8629,10 +8774,10 @@ class ResizeDialog(QDialog):
|
|
|
8629
8774
|
else:
|
|
8630
8775
|
new_shape = tuple(int(dim * factor) for dim, factor in zip(array_shape, resize))
|
|
8631
8776
|
|
|
8632
|
-
if any(dim < 1 for dim in new_shape):
|
|
8633
|
-
QMessageBox.critical(self, "Error", f"Resize would result in invalid dimensions: {new_shape}")
|
|
8634
|
-
self.reset_fields()
|
|
8635
|
-
return
|
|
8777
|
+
#if any(dim < 1 for dim in new_shape):
|
|
8778
|
+
#QMessageBox.critical(self, "Error", f"Resize would result in invalid dimensions: {new_shape}")
|
|
8779
|
+
#self.reset_fields()
|
|
8780
|
+
#return
|
|
8636
8781
|
|
|
8637
8782
|
cubic = self.cubic.isChecked()
|
|
8638
8783
|
order = 3 if cubic else 0
|
|
@@ -8706,7 +8851,12 @@ class ResizeDialog(QDialog):
|
|
|
8706
8851
|
centroids = copy.deepcopy(my_network.node_centroids)
|
|
8707
8852
|
if isinstance(resize, (int, float)):
|
|
8708
8853
|
for item in my_network.node_centroids:
|
|
8709
|
-
|
|
8854
|
+
try:
|
|
8855
|
+
centroids[item] = np.round((my_network.node_centroids[item]) * resize)
|
|
8856
|
+
except:
|
|
8857
|
+
temp = np.array(my_network.node_centroids[item])
|
|
8858
|
+
centroids[item] = np.round((temp) * resize)
|
|
8859
|
+
|
|
8710
8860
|
else:
|
|
8711
8861
|
for item in my_network.node_centroids:
|
|
8712
8862
|
centroids[item][0] = int(np.round((my_network.node_centroids[item][0]) * resize[0]))
|
|
@@ -9149,6 +9299,12 @@ class ThresholdDialog(QDialog):
|
|
|
9149
9299
|
|
|
9150
9300
|
def start_ml(self, GPU = False):
|
|
9151
9301
|
|
|
9302
|
+
try:
|
|
9303
|
+
print("Please select image to load into nodes channel for segmentation or press X if you already have the one you want. Note that this load may permit a color image in the nodes channel for segmentation purposes only, which is otherwise not allowed.")
|
|
9304
|
+
self.parent().load_channel(0, color = True)
|
|
9305
|
+
except:
|
|
9306
|
+
pass
|
|
9307
|
+
|
|
9152
9308
|
|
|
9153
9309
|
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:
|
|
9154
9310
|
if self.confirm_machine_dialog():
|
|
@@ -9188,13 +9344,14 @@ class ThresholdDialog(QDialog):
|
|
|
9188
9344
|
|
|
9189
9345
|
class ExcelotronManager(QObject):
|
|
9190
9346
|
# Signal to emit when data is received from Excelotron
|
|
9191
|
-
data_received = pyqtSignal(dict, str) # dictionary, property_name
|
|
9347
|
+
data_received = pyqtSignal(dict, str, bool) # dictionary, property_name
|
|
9192
9348
|
|
|
9193
9349
|
def __init__(self, parent=None):
|
|
9194
9350
|
super().__init__(parent)
|
|
9195
9351
|
self.excelotron_window = None
|
|
9196
9352
|
self.last_data = None
|
|
9197
9353
|
self.last_property = None
|
|
9354
|
+
self.last_add = None
|
|
9198
9355
|
|
|
9199
9356
|
def launch(self):
|
|
9200
9357
|
"""Launch the Excelotron window"""
|
|
@@ -9243,12 +9400,13 @@ class ExcelotronManager(QObject):
|
|
|
9243
9400
|
is_open = self.excelotron_window is not None
|
|
9244
9401
|
return is_open
|
|
9245
9402
|
|
|
9246
|
-
def _on_data_exported(self, data_dict, property_name):
|
|
9403
|
+
def _on_data_exported(self, data_dict, property_name, add):
|
|
9247
9404
|
"""Internal slot to handle data from Excelotron"""
|
|
9248
9405
|
self.last_data = data_dict
|
|
9249
9406
|
self.last_property = property_name
|
|
9407
|
+
self.last_add = add
|
|
9250
9408
|
# Re-emit the signal for parent to handle
|
|
9251
|
-
self.data_received.emit(data_dict, property_name)
|
|
9409
|
+
self.data_received.emit(data_dict, property_name, add)
|
|
9252
9410
|
|
|
9253
9411
|
def _on_window_destroyed(self):
|
|
9254
9412
|
"""Handle when the Excelotron window is destroyed/closed"""
|
|
@@ -9256,213 +9414,215 @@ class ExcelotronManager(QObject):
|
|
|
9256
9414
|
|
|
9257
9415
|
def get_last_data(self):
|
|
9258
9416
|
"""Get the last exported data"""
|
|
9259
|
-
return self.last_data, self.last_property
|
|
9417
|
+
return self.last_data, self.last_property, self.last_add
|
|
9260
9418
|
|
|
9261
9419
|
class MachineWindow(QMainWindow):
|
|
9262
9420
|
|
|
9263
9421
|
def __init__(self, parent=None, GPU = False):
|
|
9264
9422
|
super().__init__(parent)
|
|
9265
9423
|
|
|
9266
|
-
|
|
9267
|
-
|
|
9268
|
-
|
|
9269
|
-
|
|
9270
|
-
|
|
9271
|
-
|
|
9424
|
+
try:
|
|
9425
|
+
|
|
9426
|
+
if self.parent().active_channel == 0:
|
|
9427
|
+
if self.parent().channel_data[0] is not None:
|
|
9428
|
+
try:
|
|
9429
|
+
active_data = self.parent().channel_data[0]
|
|
9430
|
+
act_channel = 0
|
|
9431
|
+
except:
|
|
9432
|
+
active_data = self.parent().channel_data[1]
|
|
9433
|
+
act_channel = 1
|
|
9434
|
+
else:
|
|
9272
9435
|
active_data = self.parent().channel_data[1]
|
|
9273
9436
|
act_channel = 1
|
|
9274
|
-
else:
|
|
9275
|
-
active_data = self.parent().channel_data[1]
|
|
9276
|
-
act_channel = 1
|
|
9277
|
-
|
|
9278
|
-
try:
|
|
9279
|
-
array1 = np.zeros_like(active_data).astype(np.uint8)
|
|
9280
|
-
except:
|
|
9281
|
-
print("No data in nodes channel")
|
|
9282
|
-
return
|
|
9283
9437
|
|
|
9284
|
-
|
|
9285
|
-
|
|
9286
|
-
|
|
9287
|
-
|
|
9288
|
-
|
|
9289
|
-
|
|
9438
|
+
try:
|
|
9439
|
+
if len(active_data.shape) == 3:
|
|
9440
|
+
array1 = np.zeros_like(active_data).astype(np.uint8)
|
|
9441
|
+
elif len(active_data.shape) == 4:
|
|
9442
|
+
array1 = np.zeros_like(active_data)[:,:,:,0].astype(np.uint8)
|
|
9443
|
+
except:
|
|
9444
|
+
print("No data in nodes channel")
|
|
9445
|
+
return
|
|
9290
9446
|
|
|
9447
|
+
self.setWindowTitle("Threshold")
|
|
9448
|
+
|
|
9449
|
+
# Create central widget and layout
|
|
9450
|
+
central_widget = QWidget()
|
|
9451
|
+
self.setCentralWidget(central_widget)
|
|
9452
|
+
layout = QVBoxLayout(central_widget)
|
|
9291
9453
|
|
|
9292
|
-
# Create form layout for inputs
|
|
9293
|
-
form_layout = QFormLayout()
|
|
9294
9454
|
|
|
9295
|
-
|
|
9455
|
+
# Create form layout for inputs
|
|
9456
|
+
form_layout = QFormLayout()
|
|
9296
9457
|
|
|
9297
|
-
|
|
9298
|
-
self.parent().pen_button.click()
|
|
9299
|
-
self.parent().threed = False
|
|
9300
|
-
self.parent().can = False
|
|
9301
|
-
self.parent().last_change = None
|
|
9458
|
+
layout.addLayout(form_layout)
|
|
9302
9459
|
|
|
9303
|
-
|
|
9460
|
+
if self.parent().pen_button.isChecked(): #Disable the pen mode if the user is in it because the segmenter pen forks it
|
|
9461
|
+
self.parent().pen_button.click()
|
|
9462
|
+
self.parent().threed = False
|
|
9463
|
+
self.parent().can = False
|
|
9464
|
+
self.parent().last_change = None
|
|
9304
9465
|
|
|
9305
|
-
|
|
9306
|
-
self.parent().highlight_overlay = array3 #Clear this out for the segmenter to use
|
|
9466
|
+
self.parent().pen_button.setEnabled(False)
|
|
9307
9467
|
|
|
9308
|
-
|
|
9309
|
-
|
|
9310
|
-
# Not exactly sure why we need all this but the channel buttons weren't loading like they normally do when load_channel() is called:
|
|
9311
|
-
if not self.parent().channel_buttons[2].isEnabled():
|
|
9312
|
-
self.parent().channel_buttons[2].setEnabled(True)
|
|
9313
|
-
self.parent().channel_buttons[2].click()
|
|
9314
|
-
self.parent().delete_buttons[2].setEnabled(True)
|
|
9468
|
+
array3 = np.zeros_like(array1).astype(np.uint8)
|
|
9469
|
+
self.parent().highlight_overlay = array3 #Clear this out for the segmenter to use
|
|
9315
9470
|
|
|
9316
|
-
|
|
9317
|
-
|
|
9471
|
+
self.parent().load_channel(2, array1, True)
|
|
9472
|
+
# Enable the channel button
|
|
9473
|
+
# Not exactly sure why we need all this but the channel buttons weren't loading like they normally do when load_channel() is called:
|
|
9474
|
+
if not self.parent().channel_buttons[2].isEnabled():
|
|
9475
|
+
self.parent().channel_buttons[2].setEnabled(True)
|
|
9476
|
+
self.parent().channel_buttons[2].click()
|
|
9477
|
+
self.parent().delete_buttons[2].setEnabled(True)
|
|
9318
9478
|
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
|
|
9322
|
-
self.setMinimumWidth(600) # Increased to accommodate grouped buttons
|
|
9323
|
-
self.setMinimumHeight(500)
|
|
9479
|
+
if len(active_data.shape) == 3:
|
|
9480
|
+
self.parent().base_colors[act_channel] = self.parent().color_dictionary['WHITE']
|
|
9481
|
+
self.parent().base_colors[2] = self.parent().color_dictionary['LIGHT_GREEN']
|
|
9324
9482
|
|
|
9325
|
-
|
|
9326
|
-
|
|
9327
|
-
|
|
9483
|
+
self.parent().update_display()
|
|
9484
|
+
|
|
9485
|
+
# Set a reasonable default size for the window
|
|
9486
|
+
self.setMinimumWidth(600) # Increased to accommodate grouped buttons
|
|
9487
|
+
self.setMinimumHeight(500)
|
|
9488
|
+
|
|
9489
|
+
# Create main layout container
|
|
9490
|
+
main_widget = QWidget()
|
|
9491
|
+
main_layout = QVBoxLayout(main_widget)
|
|
9492
|
+
|
|
9493
|
+
# Group 1: Drawing tools (Brush + Foreground/Background)
|
|
9494
|
+
drawing_group = QGroupBox("Drawing Tools")
|
|
9495
|
+
drawing_layout = QHBoxLayout()
|
|
9496
|
+
|
|
9497
|
+
# Brush button
|
|
9498
|
+
self.brush_button = QPushButton("🖌️")
|
|
9499
|
+
self.brush_button.setCheckable(True)
|
|
9500
|
+
self.brush_button.setFixedSize(40, 40)
|
|
9501
|
+
self.brush_button.clicked.connect(self.toggle_brush_mode)
|
|
9502
|
+
self.brush_button.click()
|
|
9503
|
+
|
|
9504
|
+
# Foreground/Background buttons in their own horizontal layout
|
|
9505
|
+
fb_layout = QHBoxLayout()
|
|
9506
|
+
self.fore_button = QPushButton("Foreground")
|
|
9507
|
+
self.fore_button.setCheckable(True)
|
|
9508
|
+
self.fore_button.setChecked(True)
|
|
9509
|
+
self.fore_button.clicked.connect(self.toggle_foreground)
|
|
9328
9510
|
|
|
9329
|
-
|
|
9330
|
-
|
|
9331
|
-
|
|
9511
|
+
self.back_button = QPushButton("Background")
|
|
9512
|
+
self.back_button.setCheckable(True)
|
|
9513
|
+
self.back_button.setChecked(False)
|
|
9514
|
+
self.back_button.clicked.connect(self.toggle_background)
|
|
9332
9515
|
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
self.brush_button.setCheckable(True)
|
|
9336
|
-
self.brush_button.setFixedSize(40, 40)
|
|
9337
|
-
self.brush_button.clicked.connect(self.toggle_brush_mode)
|
|
9338
|
-
self.brush_button.click()
|
|
9516
|
+
fb_layout.addWidget(self.fore_button)
|
|
9517
|
+
fb_layout.addWidget(self.back_button)
|
|
9339
9518
|
|
|
9340
|
-
|
|
9341
|
-
|
|
9342
|
-
|
|
9343
|
-
self.fore_button.setCheckable(True)
|
|
9344
|
-
self.fore_button.setChecked(True)
|
|
9345
|
-
self.fore_button.clicked.connect(self.toggle_foreground)
|
|
9519
|
+
drawing_layout.addWidget(self.brush_button)
|
|
9520
|
+
drawing_layout.addLayout(fb_layout)
|
|
9521
|
+
drawing_group.setLayout(drawing_layout)
|
|
9346
9522
|
|
|
9347
|
-
|
|
9348
|
-
|
|
9349
|
-
|
|
9350
|
-
self.back_button.clicked.connect(self.toggle_background)
|
|
9523
|
+
# Group 2: Processing Options (GPU)
|
|
9524
|
+
processing_group = QGroupBox("Processing Options")
|
|
9525
|
+
processing_layout = QHBoxLayout()
|
|
9351
9526
|
|
|
9352
|
-
|
|
9353
|
-
|
|
9527
|
+
self.use_gpu = GPU
|
|
9528
|
+
self.two = QPushButton("Train By 2D Slice Patterns")
|
|
9529
|
+
self.two.setCheckable(True)
|
|
9530
|
+
self.two.setChecked(False)
|
|
9531
|
+
self.two.clicked.connect(self.toggle_two)
|
|
9532
|
+
self.use_two = False
|
|
9533
|
+
self.three = QPushButton("Train by 3D Patterns")
|
|
9534
|
+
self.three.setCheckable(True)
|
|
9535
|
+
self.three.setChecked(True)
|
|
9536
|
+
self.three.clicked.connect(self.toggle_three)
|
|
9537
|
+
self.GPU = QPushButton("GPU")
|
|
9538
|
+
self.GPU.setCheckable(True)
|
|
9539
|
+
self.GPU.setChecked(False)
|
|
9540
|
+
self.GPU.clicked.connect(self.toggle_GPU)
|
|
9541
|
+
processing_layout.addWidget(self.GPU)
|
|
9542
|
+
processing_layout.addWidget(self.two)
|
|
9543
|
+
processing_layout.addWidget(self.three)
|
|
9544
|
+
processing_group.setLayout(processing_layout)
|
|
9545
|
+
|
|
9546
|
+
# Group 3: Training Options
|
|
9547
|
+
training_group = QGroupBox("Training")
|
|
9548
|
+
training_layout = QHBoxLayout()
|
|
9549
|
+
train_quick = QPushButton("Train Quick Model (When Good SNR)")
|
|
9550
|
+
train_quick.clicked.connect(lambda: self.train_model(speed=True))
|
|
9551
|
+
train_detailed = QPushButton("Train Detailed Model (For Morphology)")
|
|
9552
|
+
train_detailed.clicked.connect(lambda: self.train_model(speed=False))
|
|
9553
|
+
save = QPushButton("Save Model")
|
|
9554
|
+
save.clicked.connect(self.save_model)
|
|
9555
|
+
load = QPushButton("Load Model")
|
|
9556
|
+
load.clicked.connect(self.load_model)
|
|
9557
|
+
training_layout.addWidget(train_quick)
|
|
9558
|
+
training_layout.addWidget(train_detailed)
|
|
9559
|
+
training_layout.addWidget(save)
|
|
9560
|
+
training_layout.addWidget(load)
|
|
9561
|
+
training_group.setLayout(training_layout)
|
|
9562
|
+
|
|
9563
|
+
# Group 4: Segmentation Options
|
|
9564
|
+
segmentation_group = QGroupBox("Segmentation")
|
|
9565
|
+
segmentation_layout = QHBoxLayout()
|
|
9566
|
+
seg_button = QPushButton("Preview Segment")
|
|
9567
|
+
self.seg_button = seg_button
|
|
9568
|
+
seg_button.clicked.connect(self.start_segmentation)
|
|
9569
|
+
self.pause_button = QPushButton("▶/⏸️")
|
|
9570
|
+
self.pause_button.setFixedSize(40, 40)
|
|
9571
|
+
self.pause_button.clicked.connect(self.toggle_segment)
|
|
9572
|
+
self.lock_button = QPushButton("🔒 Memory lock - (Prioritize RAM)")
|
|
9573
|
+
self.lock_button.setCheckable(True)
|
|
9574
|
+
self.lock_button.setChecked(True)
|
|
9575
|
+
self.lock_button.clicked.connect(self.toggle_lock)
|
|
9576
|
+
self.mem_lock = True
|
|
9577
|
+
full_button = QPushButton("Segment All")
|
|
9578
|
+
full_button.clicked.connect(self.segment)
|
|
9579
|
+
segmentation_layout.addWidget(seg_button)
|
|
9580
|
+
segmentation_layout.addWidget(self.pause_button) # <--- for some reason the segmenter preview is still running even when killed, may be regenerating itself somewhere. May or may not actually try to resolve this because this feature isnt that necessary.
|
|
9581
|
+
#segmentation_layout.addWidget(self.lock_button) # Also turned this off
|
|
9582
|
+
segmentation_layout.addWidget(full_button)
|
|
9583
|
+
segmentation_group.setLayout(segmentation_layout)
|
|
9584
|
+
|
|
9585
|
+
# Add all groups to main layout
|
|
9586
|
+
main_layout.addWidget(drawing_group)
|
|
9587
|
+
if not GPU:
|
|
9588
|
+
main_layout.addWidget(processing_group)
|
|
9589
|
+
main_layout.addWidget(training_group)
|
|
9590
|
+
main_layout.addWidget(segmentation_group)
|
|
9591
|
+
|
|
9592
|
+
# Set the main widget as the central widget
|
|
9593
|
+
self.setCentralWidget(main_widget)
|
|
9594
|
+
|
|
9595
|
+
self.trained = False
|
|
9596
|
+
self.previewing = False
|
|
9354
9597
|
|
|
9355
|
-
|
|
9356
|
-
|
|
9357
|
-
|
|
9598
|
+
if not GPU:
|
|
9599
|
+
self.segmenter = segmenter.InteractiveSegmenter(active_data, use_gpu=False)
|
|
9600
|
+
else:
|
|
9601
|
+
self.segmenter = seg_GPU.InteractiveSegmenter(active_data)
|
|
9358
9602
|
|
|
9359
|
-
|
|
9360
|
-
processing_group = QGroupBox("Processing Options")
|
|
9361
|
-
processing_layout = QHBoxLayout()
|
|
9362
|
-
|
|
9363
|
-
self.use_gpu = GPU
|
|
9364
|
-
self.two = QPushButton("Train By 2D Slice Patterns")
|
|
9365
|
-
self.two.setCheckable(True)
|
|
9366
|
-
self.two.setChecked(False)
|
|
9367
|
-
self.two.clicked.connect(self.toggle_two)
|
|
9368
|
-
self.use_two = False
|
|
9369
|
-
self.three = QPushButton("Train by 3D Patterns")
|
|
9370
|
-
self.three.setCheckable(True)
|
|
9371
|
-
self.three.setChecked(True)
|
|
9372
|
-
self.three.clicked.connect(self.toggle_three)
|
|
9373
|
-
self.GPU = QPushButton("GPU")
|
|
9374
|
-
self.GPU.setCheckable(True)
|
|
9375
|
-
self.GPU.setChecked(False)
|
|
9376
|
-
self.GPU.clicked.connect(self.toggle_GPU)
|
|
9377
|
-
processing_layout.addWidget(self.GPU)
|
|
9378
|
-
processing_layout.addWidget(self.two)
|
|
9379
|
-
processing_layout.addWidget(self.three)
|
|
9380
|
-
processing_group.setLayout(processing_layout)
|
|
9603
|
+
self.segmentation_worker = None
|
|
9381
9604
|
|
|
9382
|
-
|
|
9383
|
-
|
|
9384
|
-
training_layout = QHBoxLayout()
|
|
9385
|
-
train_quick = QPushButton("Train Quick Model")
|
|
9386
|
-
train_quick.clicked.connect(lambda: self.train_model(speed=True))
|
|
9387
|
-
train_detailed = QPushButton("Train More Detailed Model")
|
|
9388
|
-
train_detailed.clicked.connect(lambda: self.train_model(speed=False))
|
|
9389
|
-
save = QPushButton("Save Model")
|
|
9390
|
-
save.clicked.connect(self.save_model)
|
|
9391
|
-
load = QPushButton("Load Model")
|
|
9392
|
-
load.clicked.connect(self.load_model)
|
|
9393
|
-
training_layout.addWidget(train_quick)
|
|
9394
|
-
training_layout.addWidget(train_detailed)
|
|
9395
|
-
training_layout.addWidget(save)
|
|
9396
|
-
training_layout.addWidget(load)
|
|
9397
|
-
training_group.setLayout(training_layout)
|
|
9398
|
-
|
|
9399
|
-
# Group 4: Segmentation Options
|
|
9400
|
-
segmentation_group = QGroupBox("Segmentation")
|
|
9401
|
-
segmentation_layout = QHBoxLayout()
|
|
9402
|
-
seg_button = QPushButton("Preview Segment")
|
|
9403
|
-
self.seg_button = seg_button
|
|
9404
|
-
seg_button.clicked.connect(self.start_segmentation)
|
|
9405
|
-
self.pause_button = QPushButton("▶/⏸️")
|
|
9406
|
-
self.pause_button.clicked.connect(self.pause)
|
|
9407
|
-
self.lock_button = QPushButton("🔒 Memory lock - (Prioritize RAM)")
|
|
9408
|
-
self.lock_button.setCheckable(True)
|
|
9409
|
-
self.lock_button.setChecked(True)
|
|
9410
|
-
self.lock_button.clicked.connect(self.toggle_lock)
|
|
9411
|
-
self.mem_lock = True
|
|
9412
|
-
full_button = QPushButton("Segment All")
|
|
9413
|
-
full_button.clicked.connect(self.segment)
|
|
9414
|
-
segmentation_layout.addWidget(seg_button)
|
|
9415
|
-
#segmentation_layout.addWidget(self.pause_button) # <--- for some reason the segmenter preview is still running even when killed, may be regenerating itself somewhere. May or may not actually try to resolve this because this feature isnt that necessary.
|
|
9416
|
-
#segmentation_layout.addWidget(self.lock_button) # Also turned this off
|
|
9417
|
-
segmentation_layout.addWidget(full_button)
|
|
9418
|
-
segmentation_group.setLayout(segmentation_layout)
|
|
9419
|
-
|
|
9420
|
-
# Add all groups to main layout
|
|
9421
|
-
main_layout.addWidget(drawing_group)
|
|
9422
|
-
if not GPU:
|
|
9423
|
-
main_layout.addWidget(processing_group)
|
|
9424
|
-
main_layout.addWidget(training_group)
|
|
9425
|
-
main_layout.addWidget(segmentation_group)
|
|
9426
|
-
|
|
9427
|
-
# Set the main widget as the central widget
|
|
9428
|
-
self.setCentralWidget(main_widget)
|
|
9605
|
+
self.fore_button.click()
|
|
9606
|
+
self.fore_button.click()
|
|
9429
9607
|
|
|
9430
|
-
|
|
9431
|
-
|
|
9608
|
+
except:
|
|
9609
|
+
return
|
|
9432
9610
|
|
|
9433
|
-
|
|
9434
|
-
self.segmenter = segmenter.InteractiveSegmenter(active_data, use_gpu=False)
|
|
9435
|
-
else:
|
|
9436
|
-
self.segmenter = seg_GPU.InteractiveSegmenter(active_data)
|
|
9611
|
+
def toggle_segment(self):
|
|
9437
9612
|
|
|
9438
|
-
self.segmentation_worker
|
|
9613
|
+
if self.segmentation_worker is not None:
|
|
9614
|
+
if not self.segmentation_worker._paused:
|
|
9615
|
+
self.segmentation_worker.pause()
|
|
9616
|
+
print("Segmentation Worker Paused")
|
|
9617
|
+
elif self.segmentation_worker._paused:
|
|
9618
|
+
self.segmentation_worker.resume()
|
|
9619
|
+
print("Segmentation Worker Resuming")
|
|
9439
9620
|
|
|
9440
|
-
self.fore_button.click()
|
|
9441
|
-
self.fore_button.click()
|
|
9442
9621
|
|
|
9443
9622
|
def toggle_lock(self):
|
|
9444
9623
|
|
|
9445
9624
|
self.mem_lock = self.lock_button.isChecked()
|
|
9446
9625
|
|
|
9447
|
-
def pause(self):
|
|
9448
|
-
|
|
9449
|
-
if self.segmentation_worker is not None:
|
|
9450
|
-
try:
|
|
9451
|
-
print("Pausing segmenter")
|
|
9452
|
-
self.previewing = False
|
|
9453
|
-
self.segmentation_finished
|
|
9454
|
-
del self.segmentation_worker
|
|
9455
|
-
self.segmentation_worker = None
|
|
9456
|
-
except:
|
|
9457
|
-
pass
|
|
9458
|
-
|
|
9459
|
-
else:
|
|
9460
|
-
try:
|
|
9461
|
-
print("Restarting segmenter")
|
|
9462
|
-
self.previewing = True
|
|
9463
|
-
self.start_segmentation
|
|
9464
|
-
except:
|
|
9465
|
-
pass
|
|
9466
9626
|
|
|
9467
9627
|
def save_model(self):
|
|
9468
9628
|
|
|
@@ -9484,6 +9644,9 @@ class MachineWindow(QMainWindow):
|
|
|
9484
9644
|
|
|
9485
9645
|
except Exception as e:
|
|
9486
9646
|
print(f"Error saving model: {e}")
|
|
9647
|
+
import traceback
|
|
9648
|
+
traceback.print_exc()
|
|
9649
|
+
|
|
9487
9650
|
|
|
9488
9651
|
def load_model(self):
|
|
9489
9652
|
|
|
@@ -9579,7 +9742,23 @@ class MachineWindow(QMainWindow):
|
|
|
9579
9742
|
def toggle_brush_mode(self):
|
|
9580
9743
|
"""Toggle brush mode on/off"""
|
|
9581
9744
|
self.parent().brush_mode = self.brush_button.isChecked()
|
|
9745
|
+
|
|
9582
9746
|
if self.parent().brush_mode:
|
|
9747
|
+
|
|
9748
|
+
self.parent().pm = painting.PaintManager(parent = self.parent())
|
|
9749
|
+
|
|
9750
|
+
# Start virtual paint session
|
|
9751
|
+
# Get current zoom to preserve it
|
|
9752
|
+
current_xlim = self.parent().ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
9753
|
+
current_ylim = self.parent().ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
9754
|
+
|
|
9755
|
+
if self.parent().pen_button.isChecked():
|
|
9756
|
+
channel = self.parent().active_channel
|
|
9757
|
+
else:
|
|
9758
|
+
channel = 2
|
|
9759
|
+
|
|
9760
|
+
self.parent().pm.initiate_paint_session(channel, current_xlim, current_ylim)
|
|
9761
|
+
|
|
9583
9762
|
self.parent().pan_button.setChecked(False)
|
|
9584
9763
|
self.parent().zoom_button.setChecked(False)
|
|
9585
9764
|
if self.parent().pan_mode:
|
|
@@ -9606,12 +9785,13 @@ class MachineWindow(QMainWindow):
|
|
|
9606
9785
|
self.kill_segmentation()
|
|
9607
9786
|
# Wait a bit for cleanup
|
|
9608
9787
|
time.sleep(0.1)
|
|
9609
|
-
|
|
9610
|
-
|
|
9788
|
+
|
|
9789
|
+
self.previewing = True
|
|
9611
9790
|
try:
|
|
9612
9791
|
try:
|
|
9613
9792
|
self.segmenter.train_batch(self.parent().channel_data[2], speed = speed, use_gpu = self.use_gpu, use_two = self.use_two, mem_lock = self.mem_lock)
|
|
9614
9793
|
self.trained = True
|
|
9794
|
+
self.start_segmentation()
|
|
9615
9795
|
except Exception as e:
|
|
9616
9796
|
print("Error training. Perhaps you forgot both foreground and background markers? I need both!")
|
|
9617
9797
|
import traceback
|
|
@@ -9627,6 +9807,8 @@ class MachineWindow(QMainWindow):
|
|
|
9627
9807
|
|
|
9628
9808
|
def start_segmentation(self):
|
|
9629
9809
|
|
|
9810
|
+
self.parent().static_background = None
|
|
9811
|
+
|
|
9630
9812
|
self.kill_segmentation()
|
|
9631
9813
|
time.sleep(0.1)
|
|
9632
9814
|
|
|
@@ -9635,12 +9817,10 @@ class MachineWindow(QMainWindow):
|
|
|
9635
9817
|
else:
|
|
9636
9818
|
print("Beginning new segmentation...")
|
|
9637
9819
|
|
|
9638
|
-
|
|
9639
|
-
|
|
9640
|
-
|
|
9641
|
-
|
|
9642
|
-
else:
|
|
9643
|
-
active_data = self.parent().channel_data[1]
|
|
9820
|
+
if self.parent().channel_data[2] is not None:
|
|
9821
|
+
active_data = self.parent().channel_data[2]
|
|
9822
|
+
else:
|
|
9823
|
+
active_data = self.parent().channel_data[0]
|
|
9644
9824
|
|
|
9645
9825
|
array3 = np.zeros_like(active_data).astype(np.uint8)
|
|
9646
9826
|
self.parent().highlight_overlay = array3 #Clear this out for the segmenter to use
|
|
@@ -9649,8 +9829,7 @@ class MachineWindow(QMainWindow):
|
|
|
9649
9829
|
return
|
|
9650
9830
|
else:
|
|
9651
9831
|
self.segmentation_worker = SegmentationWorker(self.parent().highlight_overlay, self.segmenter, self.use_gpu, self.use_two, self.previewing, self, self.mem_lock)
|
|
9652
|
-
self.segmentation_worker.chunk_processed.connect(self.update_display) # Just update display
|
|
9653
|
-
self.segmentation_worker.finished.connect(self.segmentation_finished)
|
|
9832
|
+
self.segmentation_worker.chunk_processed.connect(lambda: self.update_display(skip_paint_reinit = True)) # Just update display
|
|
9654
9833
|
current_xlim = self.parent().ax.get_xlim()
|
|
9655
9834
|
current_ylim = self.parent().ax.get_ylim()
|
|
9656
9835
|
try:
|
|
@@ -9703,7 +9882,7 @@ class MachineWindow(QMainWindow):
|
|
|
9703
9882
|
|
|
9704
9883
|
return changed
|
|
9705
9884
|
|
|
9706
|
-
def update_display(self):
|
|
9885
|
+
def update_display(self, skip_paint_reinit = False):
|
|
9707
9886
|
if not hasattr(self, '_last_update'):
|
|
9708
9887
|
self._last_update = 0
|
|
9709
9888
|
|
|
@@ -9713,8 +9892,6 @@ class MachineWindow(QMainWindow):
|
|
|
9713
9892
|
|
|
9714
9893
|
self._last_z = current_z
|
|
9715
9894
|
|
|
9716
|
-
if self.previewing:
|
|
9717
|
-
changed = self.check_for_z_change()
|
|
9718
9895
|
|
|
9719
9896
|
current_time = time.time()
|
|
9720
9897
|
if current_time - self._last_update >= 1: # Match worker's interval
|
|
@@ -9731,71 +9908,40 @@ class MachineWindow(QMainWindow):
|
|
|
9731
9908
|
|
|
9732
9909
|
if not self.parent().painting:
|
|
9733
9910
|
# Only update if view limits are valid
|
|
9734
|
-
self.parent().update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
9911
|
+
self.parent().update_display(preserve_zoom=(current_xlim, current_ylim), skip_paint_reinit = skip_paint_reinit)
|
|
9912
|
+
|
|
9913
|
+
if self.parent().brush_mode:
|
|
9914
|
+
current_xlim = self.parent().ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
9915
|
+
current_ylim = self.parent().ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
9916
|
+
|
|
9917
|
+
if self.parent().pen_button.isChecked():
|
|
9918
|
+
channel = self.parent().active_channel
|
|
9919
|
+
else:
|
|
9920
|
+
channel = 2
|
|
9735
9921
|
|
|
9922
|
+
self.parent().pm.initiate_paint_session(channel, current_xlim, current_ylim)
|
|
9736
9923
|
|
|
9737
9924
|
self._last_update = current_time
|
|
9738
9925
|
except Exception as e:
|
|
9739
9926
|
print(f"Display update error: {e}")
|
|
9740
9927
|
|
|
9741
9928
|
def poke_segmenter(self):
|
|
9742
|
-
|
|
9743
|
-
|
|
9744
|
-
|
|
9745
|
-
|
|
9746
|
-
self.segmenter._currently_processing = None
|
|
9747
|
-
|
|
9748
|
-
# Force regenerating the worker
|
|
9749
|
-
if self.segmentation_worker is not None:
|
|
9750
|
-
self.kill_segmentation()
|
|
9751
|
-
|
|
9752
|
-
time.sleep(0.2)
|
|
9753
|
-
self.start_segmentation()
|
|
9929
|
+
try:
|
|
9930
|
+
# Clear any processing flags in the segmenter
|
|
9931
|
+
if hasattr(self.segmenter, '_currently_processing'):
|
|
9932
|
+
self.segmenter._currently_processing = None
|
|
9754
9933
|
|
|
9755
|
-
|
|
9756
|
-
|
|
9757
|
-
|
|
9758
|
-
traceback.print_exc()
|
|
9759
|
-
|
|
9760
|
-
def segmentation_finished(self):
|
|
9761
|
-
|
|
9762
|
-
current_xlim = self.parent().ax.get_xlim()
|
|
9763
|
-
current_ylim = self.parent().ax.get_ylim()
|
|
9764
|
-
self.parent().update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
9765
|
-
|
|
9766
|
-
# Store the current z position before killing the worker
|
|
9767
|
-
current_z = self.parent().current_slice
|
|
9768
|
-
|
|
9769
|
-
# Clean up the worker
|
|
9770
|
-
self.kill_segmentation()
|
|
9771
|
-
self.segmentation_worker = None
|
|
9772
|
-
time.sleep(0.1)
|
|
9773
|
-
|
|
9774
|
-
# Auto-restart for 2D preview mode only if certain conditions are met
|
|
9775
|
-
if self.previewing and self.use_two:
|
|
9776
|
-
# Track when this slice was last processed
|
|
9777
|
-
if not hasattr(self, '_processed_slices'):
|
|
9778
|
-
self._processed_slices = {}
|
|
9934
|
+
# Force regenerating the worker
|
|
9935
|
+
if self.segmentation_worker is not None:
|
|
9936
|
+
self.kill_segmentation()
|
|
9779
9937
|
|
|
9780
|
-
|
|
9938
|
+
time.sleep(0.2)
|
|
9939
|
+
self.start_segmentation()
|
|
9781
9940
|
|
|
9782
|
-
|
|
9783
|
-
|
|
9784
|
-
|
|
9785
|
-
|
|
9786
|
-
recently_processed = time_since_last_attempt < 5.0 # 5 second cooldown
|
|
9787
|
-
|
|
9788
|
-
if not recently_processed:
|
|
9789
|
-
self._processed_slices[current_z] = current_time
|
|
9790
|
-
|
|
9791
|
-
# Reset any processing flags in the segmenter
|
|
9792
|
-
if hasattr(self.segmenter, '_currently_processing'):
|
|
9793
|
-
self.segmenter._currently_processing = None
|
|
9794
|
-
|
|
9795
|
-
if 0 in self.parent().highlight_overlay[current_z, :, :]:
|
|
9796
|
-
# Create a new worker after a brief delay
|
|
9797
|
-
QTimer.singleShot(500, self.start_segmentation)
|
|
9798
|
-
|
|
9941
|
+
except Exception as e:
|
|
9942
|
+
print(f"Error in poke_segmenter: {e}")
|
|
9943
|
+
import traceback
|
|
9944
|
+
traceback.print_exc()
|
|
9799
9945
|
|
|
9800
9946
|
|
|
9801
9947
|
def kill_segmentation(self):
|
|
@@ -9829,11 +9975,10 @@ class MachineWindow(QMainWindow):
|
|
|
9829
9975
|
|
|
9830
9976
|
self.previewing = False
|
|
9831
9977
|
|
|
9832
|
-
if self.parent().
|
|
9833
|
-
|
|
9834
|
-
|
|
9835
|
-
|
|
9836
|
-
active_data = self.parent().channel_data[1]
|
|
9978
|
+
if self.parent().channel_data[2] is not None:
|
|
9979
|
+
active_data = self.parent().channel_data[2]
|
|
9980
|
+
else:
|
|
9981
|
+
active_data = self.parent().channel_data[0]
|
|
9837
9982
|
|
|
9838
9983
|
array3 = np.zeros_like(active_data).astype(np.uint8)
|
|
9839
9984
|
self.parent().highlight_overlay = array3 #Clear this out for the segmenter to use
|
|
@@ -9844,6 +9989,8 @@ class MachineWindow(QMainWindow):
|
|
|
9844
9989
|
self.parent().highlight_overlay = self.segmenter.segment_volume(array = self.parent().highlight_overlay)
|
|
9845
9990
|
except Exception as e:
|
|
9846
9991
|
print(f"Error segmenting (Perhaps retrain the model...): {e}")
|
|
9992
|
+
import traceback
|
|
9993
|
+
traceback.print_exc()
|
|
9847
9994
|
return
|
|
9848
9995
|
|
|
9849
9996
|
# Clean up when done
|
|
@@ -9880,6 +10027,12 @@ class MachineWindow(QMainWindow):
|
|
|
9880
10027
|
# Kill the segmentation thread and wait for it to finish
|
|
9881
10028
|
self.kill_segmentation()
|
|
9882
10029
|
time.sleep(0.2) # Give additional time for cleanup
|
|
10030
|
+
|
|
10031
|
+
try:
|
|
10032
|
+
self.parent().channel_data[0] = self.parent().reduce_rgb_dimension(self.parent().channel_data[0], 'weight')
|
|
10033
|
+
self.update_display()
|
|
10034
|
+
except:
|
|
10035
|
+
pass
|
|
9883
10036
|
|
|
9884
10037
|
self.parent().machine_window = None
|
|
9885
10038
|
else:
|
|
@@ -9940,59 +10093,26 @@ class SegmentationWorker(QThread):
|
|
|
9940
10093
|
|
|
9941
10094
|
# Remember the starting z position
|
|
9942
10095
|
self.starting_z = self.segmenter.current_z
|
|
9943
|
-
|
|
9944
|
-
if self.previewing and self.use_two:
|
|
9945
|
-
# Process current z-slice in chunks
|
|
9946
|
-
current_z = self.segmenter.current_z
|
|
9947
|
-
|
|
9948
|
-
# Process the slice with chunked generator
|
|
9949
|
-
for foreground, background in self.segmenter.segment_slice_chunked(current_z):
|
|
9950
|
-
# Check for pause/stop before processing each chunk
|
|
9951
|
-
self._check_pause()
|
|
9952
|
-
if self._stop:
|
|
9953
|
-
break
|
|
9954
|
-
|
|
9955
|
-
if foreground == None and background == None:
|
|
9956
|
-
self.get_poked()
|
|
9957
10096
|
|
|
9958
|
-
|
|
9959
|
-
|
|
9960
|
-
|
|
9961
|
-
|
|
9962
|
-
|
|
9963
|
-
|
|
9964
|
-
for z,y,x in background:
|
|
9965
|
-
self.overlay[z,y,x] = 2
|
|
9966
|
-
|
|
9967
|
-
# Signal update after each chunk
|
|
9968
|
-
self.chunks_since_update += 1
|
|
9969
|
-
current_time = time.time()
|
|
9970
|
-
if (self.chunks_since_update >= self.chunks_per_update and
|
|
9971
|
-
current_time - self.last_update >= self.update_interval):
|
|
9972
|
-
self.chunk_processed.emit()
|
|
9973
|
-
self.chunks_since_update = 0
|
|
9974
|
-
self.last_update = current_time
|
|
10097
|
+
# Original 3D approach
|
|
10098
|
+
for foreground_coords, background_coords in self.segmenter.segment_volume_realtime(gpu=self.use_gpu):
|
|
10099
|
+
# Check for pause/stop before processing each chunk
|
|
10100
|
+
self._check_pause()
|
|
10101
|
+
if self._stop:
|
|
10102
|
+
break
|
|
9975
10103
|
|
|
9976
|
-
|
|
9977
|
-
|
|
9978
|
-
for
|
|
9979
|
-
|
|
9980
|
-
|
|
9981
|
-
|
|
9982
|
-
|
|
9983
|
-
|
|
9984
|
-
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
|
|
9988
|
-
|
|
9989
|
-
self.chunks_since_update += 1
|
|
9990
|
-
current_time = time.time()
|
|
9991
|
-
if (self.chunks_since_update >= self.chunks_per_update and
|
|
9992
|
-
current_time - self.last_update >= self.update_interval):
|
|
9993
|
-
self.chunk_processed.emit()
|
|
9994
|
-
self.chunks_since_update = 0
|
|
9995
|
-
self.last_update = current_time
|
|
10104
|
+
for z,y,x in foreground_coords:
|
|
10105
|
+
self.overlay[z,y,x] = 1
|
|
10106
|
+
for z,y,x in background_coords:
|
|
10107
|
+
self.overlay[z,y,x] = 2
|
|
10108
|
+
|
|
10109
|
+
self.chunks_since_update += 1
|
|
10110
|
+
current_time = time.time()
|
|
10111
|
+
if (self.chunks_since_update >= self.chunks_per_update and
|
|
10112
|
+
current_time - self.last_update >= self.update_interval):
|
|
10113
|
+
self.chunk_processed.emit()
|
|
10114
|
+
self.chunks_since_update = 0
|
|
10115
|
+
self.last_update = current_time
|
|
9996
10116
|
|
|
9997
10117
|
self.finished.emit()
|
|
9998
10118
|
|
|
@@ -10780,7 +10900,7 @@ class MaskDialog(QDialog):
|
|
|
10780
10900
|
|
|
10781
10901
|
class CropDialog(QDialog):
|
|
10782
10902
|
|
|
10783
|
-
def __init__(self, parent=None):
|
|
10903
|
+
def __init__(self, parent=None, args = None):
|
|
10784
10904
|
|
|
10785
10905
|
try:
|
|
10786
10906
|
|
|
@@ -10788,18 +10908,26 @@ class CropDialog(QDialog):
|
|
|
10788
10908
|
self.setWindowTitle("Crop Image (Will transpose any centroids)?")
|
|
10789
10909
|
self.setModal(True)
|
|
10790
10910
|
|
|
10911
|
+
if args is None:
|
|
10912
|
+
xmin = 0
|
|
10913
|
+
xmax = self.parent().shape[2]
|
|
10914
|
+
ymin = 0
|
|
10915
|
+
ymax = self.parent().shape[1]
|
|
10916
|
+
else:
|
|
10917
|
+
xmin, xmax, ymin, ymax = args
|
|
10918
|
+
|
|
10791
10919
|
layout = QFormLayout(self)
|
|
10792
10920
|
|
|
10793
|
-
self.xmin = QLineEdit("
|
|
10921
|
+
self.xmin = QLineEdit(f"{xmin}")
|
|
10794
10922
|
layout.addRow("X Min", self.xmin)
|
|
10795
10923
|
|
|
10796
|
-
self.xmax = QLineEdit(f"{
|
|
10924
|
+
self.xmax = QLineEdit(f"{xmax}")
|
|
10797
10925
|
layout.addRow("X Max", self.xmax)
|
|
10798
10926
|
|
|
10799
|
-
self.ymin = QLineEdit("
|
|
10927
|
+
self.ymin = QLineEdit(f"{ymin}")
|
|
10800
10928
|
layout.addRow("Y Min", self.ymin)
|
|
10801
10929
|
|
|
10802
|
-
self.ymax = QLineEdit(f"{
|
|
10930
|
+
self.ymax = QLineEdit(f"{ymax}")
|
|
10803
10931
|
layout.addRow("Y Max", self.ymax)
|
|
10804
10932
|
|
|
10805
10933
|
self.zmin = QLineEdit("0")
|
|
@@ -10853,10 +10981,16 @@ class CropDialog(QDialog):
|
|
|
10853
10981
|
transformed = centroids - np.array([zmin, ymin, xmin])
|
|
10854
10982
|
transformed = transformed.astype(int)
|
|
10855
10983
|
|
|
10856
|
-
#
|
|
10857
|
-
|
|
10858
|
-
(transformed <= np.array([zmax, ymax, xmax]))).all(axis=1)
|
|
10984
|
+
# Create upper bounds array with same shape
|
|
10985
|
+
upper_bounds = np.array([zmax - zmin, ymax - ymin, xmax - xmin])
|
|
10859
10986
|
|
|
10987
|
+
# Boolean mask for valid coordinates - check each dimension separately
|
|
10988
|
+
z_valid = (transformed[:, 0] >= 0) & (transformed[:, 0] <= upper_bounds[0])
|
|
10989
|
+
y_valid = (transformed[:, 1] >= 0) & (transformed[:, 1] <= upper_bounds[1])
|
|
10990
|
+
x_valid = (transformed[:, 2] >= 0) & (transformed[:, 2] <= upper_bounds[2])
|
|
10991
|
+
|
|
10992
|
+
valid_mask = z_valid & y_valid & x_valid
|
|
10993
|
+
|
|
10860
10994
|
# Rebuild dictionary with only valid entries
|
|
10861
10995
|
my_network.node_centroids = {
|
|
10862
10996
|
nodes[int(i)]: [int(transformed[i, 0]), int(transformed[i, 1]), int(transformed[i, 2])]
|
|
@@ -10865,6 +10999,15 @@ class CropDialog(QDialog):
|
|
|
10865
10999
|
|
|
10866
11000
|
self.parent().format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
|
|
10867
11001
|
|
|
11002
|
+
if my_network.node_identities is not None:
|
|
11003
|
+
new_idens = {}
|
|
11004
|
+
for node, iden in my_network.node_identities.items():
|
|
11005
|
+
if node in my_network.node_centroids:
|
|
11006
|
+
new_idens[node] = iden
|
|
11007
|
+
my_network.node_identities = new_idens
|
|
11008
|
+
|
|
11009
|
+
self.parent().format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity', 'Node Identities')
|
|
11010
|
+
|
|
10868
11011
|
except Exception as e:
|
|
10869
11012
|
|
|
10870
11013
|
print(f"Error transposing node centroids: {e}")
|
|
@@ -11058,6 +11201,74 @@ class SkeletonizeDialog(QDialog):
|
|
|
11058
11201
|
f"Error running skeletonize: {str(e)}"
|
|
11059
11202
|
)
|
|
11060
11203
|
|
|
11204
|
+
class DistanceDialog(QDialog):
|
|
11205
|
+
def __init__(self, parent=None):
|
|
11206
|
+
super().__init__(parent)
|
|
11207
|
+
self.setWindowTitle("Compute Distance Transform (Applies xy and z scaling, set them to 1 if you want voxel correspondence)?")
|
|
11208
|
+
self.setModal(True)
|
|
11209
|
+
|
|
11210
|
+
layout = QFormLayout(self)
|
|
11211
|
+
|
|
11212
|
+
# Add Run button
|
|
11213
|
+
run_button = QPushButton("Run")
|
|
11214
|
+
run_button.clicked.connect(self.run)
|
|
11215
|
+
layout.addRow(run_button)
|
|
11216
|
+
|
|
11217
|
+
def run(self):
|
|
11218
|
+
|
|
11219
|
+
try:
|
|
11220
|
+
|
|
11221
|
+
data = self.parent().channel_data[self.parent().active_channel]
|
|
11222
|
+
|
|
11223
|
+
data = sdl.compute_distance_transform_distance(data, sampling = [my_network.z_scale, my_network.xy_scale, my_network.xy_scale])
|
|
11224
|
+
|
|
11225
|
+
self.parent().load_channel(self.parent().active_channel, data, data = True)
|
|
11226
|
+
|
|
11227
|
+
except Exception as e:
|
|
11228
|
+
|
|
11229
|
+
print(f"Error: {e}")
|
|
11230
|
+
|
|
11231
|
+
class GrayWaterDialog(QDialog):
|
|
11232
|
+
def __init__(self, parent=None):
|
|
11233
|
+
super().__init__(parent)
|
|
11234
|
+
self.setWindowTitle(f"Gray Watershed - Please segment out your background first (ie with intensity thresholding) or this will not work correctly. \nAt the moment, this is designed for similarly sized objects. Having mixed large/small objects may not work correctly.")
|
|
11235
|
+
self.setModal(True)
|
|
11236
|
+
|
|
11237
|
+
layout = QFormLayout(self)
|
|
11238
|
+
|
|
11239
|
+
self.min_peak_distance = QLineEdit("1")
|
|
11240
|
+
layout.addRow("Minimum Peak Distance (To any other peak - Recommended) (This is true voxel distance here)", self.min_peak_distance)
|
|
11241
|
+
|
|
11242
|
+
# Minimum Intensity
|
|
11243
|
+
self.min_intensity = QLineEdit("")
|
|
11244
|
+
layout.addRow("Minimum Peak Intensity (Optional):", self.min_intensity)
|
|
11245
|
+
|
|
11246
|
+
# Add Run button
|
|
11247
|
+
run_button = QPushButton("Run Watershed")
|
|
11248
|
+
run_button.clicked.connect(self.run_watershed)
|
|
11249
|
+
layout.addRow(run_button)
|
|
11250
|
+
|
|
11251
|
+
def run_watershed(self):
|
|
11252
|
+
|
|
11253
|
+
try:
|
|
11254
|
+
|
|
11255
|
+
min_intensity = float(self.min_intensity.text()) if self.min_intensity.text().strip() else None
|
|
11256
|
+
|
|
11257
|
+
min_peak_distance = int(self.min_peak_distance.text()) if self.min_peak_distance.text().strip() else 1
|
|
11258
|
+
|
|
11259
|
+
data = self.parent().channel_data[self.parent().active_channel]
|
|
11260
|
+
|
|
11261
|
+
data = n3d.gray_watershed(data, min_peak_distance, min_intensity)
|
|
11262
|
+
|
|
11263
|
+
self.parent().load_channel(self.parent().active_channel, data, data = True)
|
|
11264
|
+
|
|
11265
|
+
self.accept()
|
|
11266
|
+
|
|
11267
|
+
except Exception as e:
|
|
11268
|
+
print(f"Error: {e}")
|
|
11269
|
+
|
|
11270
|
+
|
|
11271
|
+
|
|
11061
11272
|
|
|
11062
11273
|
class WatershedDialog(QDialog):
|
|
11063
11274
|
def __init__(self, parent=None):
|
|
@@ -11084,10 +11295,14 @@ class WatershedDialog(QDialog):
|
|
|
11084
11295
|
except:
|
|
11085
11296
|
self.default = 0.05
|
|
11086
11297
|
|
|
11298
|
+
# Smallest radius (empty by default)
|
|
11299
|
+
self.smallest_rad = QLineEdit()
|
|
11300
|
+
self.smallest_rad.setPlaceholderText("Leave empty for None")
|
|
11301
|
+
layout.addRow(f"Smallest Radius (Objects any smaller may get thresholded out - this value always overrides below 'proportion' param). \n Somewhat more intuitive param then below, use a conservative value a bit smaller than your smallest object's radius:", self.smallest_rad)
|
|
11087
11302
|
|
|
11088
11303
|
# Proportion (default 0.1)
|
|
11089
11304
|
self.proportion = QLineEdit(f"{self.default}")
|
|
11090
|
-
layout.addRow("Proportion:", self.proportion)
|
|
11305
|
+
layout.addRow(f"Proportion (0-1) of distance transform value set [ie unique elements] to exclude (ie 0.2 = 20% of the set of all values of the distance transform get excluded).\n Essentially, vals closer to 0 are less likely to split objects but also won't kick out small objects from the output, vals slightly further from 0 will split more aggressively, but vals closer to 1 become unstable, leading to objects being evicted or labelling errors. \nRecommend something between 0.05 and 0.4, but it depends on the data (Or just enter a smallest radius above to avoid using this). \nWill tell you in command window what equivalent 'smallest radius' this is):", self.proportion)
|
|
11091
11306
|
|
|
11092
11307
|
# GPU checkbox (default True)
|
|
11093
11308
|
self.gpu = QPushButton("GPU")
|
|
@@ -11095,10 +11310,6 @@ class WatershedDialog(QDialog):
|
|
|
11095
11310
|
self.gpu.setChecked(False)
|
|
11096
11311
|
layout.addRow("Use GPU:", self.gpu)
|
|
11097
11312
|
|
|
11098
|
-
# Smallest radius (empty by default)
|
|
11099
|
-
self.smallest_rad = QLineEdit()
|
|
11100
|
-
self.smallest_rad.setPlaceholderText("Leave empty for None")
|
|
11101
|
-
layout.addRow("Smallest Radius:", self.smallest_rad)
|
|
11102
11313
|
|
|
11103
11314
|
# Predownsample (empty by default)
|
|
11104
11315
|
self.predownsample = QLineEdit()
|
|
@@ -11106,11 +11317,11 @@ class WatershedDialog(QDialog):
|
|
|
11106
11317
|
layout.addRow("Kernel Obtainment GPU Downsample:", self.predownsample)
|
|
11107
11318
|
|
|
11108
11319
|
# Predownsample2 (empty by default)
|
|
11109
|
-
self.predownsample2 = QLineEdit()
|
|
11110
|
-
self.predownsample2.setPlaceholderText("Leave empty for None")
|
|
11111
|
-
layout.addRow("Smart Label GPU Downsample:", self.predownsample2)
|
|
11320
|
+
#self.predownsample2 = QLineEdit()
|
|
11321
|
+
#self.predownsample2.setPlaceholderText("Leave empty for None")
|
|
11322
|
+
#layout.addRow("Smart Label GPU Downsample:", self.predownsample2)
|
|
11112
11323
|
|
|
11113
|
-
layout.addRow("Note:", QLabel(f"If the optimal proportion watershed output is still labeling spatially seperated objects with the same label, try right placing the result in nodes or edges\nthen right click the image and choose 'select all', followed by right clicking and 'selection' -> 'split non-touching labels'."))
|
|
11324
|
+
#layout.addRow("Note:", QLabel(f"If the optimal proportion watershed output is still labeling spatially seperated objects with the same label, try right placing the result in nodes or edges\nthen right click the image and choose 'select all', followed by right clicking and 'selection' -> 'split non-touching labels'."))
|
|
11114
11325
|
|
|
11115
11326
|
|
|
11116
11327
|
# Add Run button
|
|
@@ -11147,7 +11358,7 @@ class WatershedDialog(QDialog):
|
|
|
11147
11358
|
# Get predownsample2 (None if empty)
|
|
11148
11359
|
try:
|
|
11149
11360
|
predownsample2 = float(self.predownsample2.text()) if self.predownsample2.text() else None
|
|
11150
|
-
except
|
|
11361
|
+
except:
|
|
11151
11362
|
predownsample2 = None
|
|
11152
11363
|
|
|
11153
11364
|
# Get the active channel data from parent
|
|
@@ -11325,7 +11536,22 @@ class CentroidNodeDialog(QDialog):
|
|
|
11325
11536
|
|
|
11326
11537
|
if mode == 0:
|
|
11327
11538
|
|
|
11328
|
-
|
|
11539
|
+
try:
|
|
11540
|
+
shape = my_network.nodes.shape
|
|
11541
|
+
|
|
11542
|
+
except:
|
|
11543
|
+
try:
|
|
11544
|
+
shape = my_network.edges.shape
|
|
11545
|
+
except:
|
|
11546
|
+
try:
|
|
11547
|
+
shape = my_network.network_overlay.shape
|
|
11548
|
+
except:
|
|
11549
|
+
try:
|
|
11550
|
+
shape = my_network.id_overlay.shape
|
|
11551
|
+
except:
|
|
11552
|
+
shape = None
|
|
11553
|
+
|
|
11554
|
+
my_network.nodes = my_network.centroid_array(shape = shape)
|
|
11329
11555
|
|
|
11330
11556
|
else:
|
|
11331
11557
|
|
|
@@ -11439,7 +11665,7 @@ class GenNodesDialog(QDialog):
|
|
|
11439
11665
|
|
|
11440
11666
|
# Component dilation
|
|
11441
11667
|
self.comp_dil = QLineEdit("0")
|
|
11442
|
-
opt_layout.addWidget(QLabel("
|
|
11668
|
+
opt_layout.addWidget(QLabel("Amount to expand nodes (Merges nearby nodes, say if they are overassigned, good for broader branch breaking):"), 1, 0)
|
|
11443
11669
|
opt_layout.addWidget(self.comp_dil, 1, 1)
|
|
11444
11670
|
|
|
11445
11671
|
opt_group.setLayout(opt_layout)
|
|
@@ -11573,11 +11799,11 @@ class GenNodesDialog(QDialog):
|
|
|
11573
11799
|
|
|
11574
11800
|
class BranchDialog(QDialog):
|
|
11575
11801
|
|
|
11576
|
-
def __init__(self, parent=None):
|
|
11802
|
+
def __init__(self, parent=None, called = False):
|
|
11577
11803
|
super().__init__(parent)
|
|
11578
11804
|
self.setWindowTitle("Label Branches (of edges)")
|
|
11579
11805
|
self.setModal(True)
|
|
11580
|
-
|
|
11806
|
+
|
|
11581
11807
|
# Main layout
|
|
11582
11808
|
main_layout = QVBoxLayout(self)
|
|
11583
11809
|
|
|
@@ -11610,7 +11836,10 @@ class BranchDialog(QDialog):
|
|
|
11610
11836
|
|
|
11611
11837
|
self.fix3 = QPushButton("Split Nontouching Branches?")
|
|
11612
11838
|
self.fix3.setCheckable(True)
|
|
11613
|
-
|
|
11839
|
+
if called:
|
|
11840
|
+
self.fix3.setChecked(True)
|
|
11841
|
+
else:
|
|
11842
|
+
self.fix3.setChecked(False)
|
|
11614
11843
|
correction_layout.addWidget(QLabel("Split Nontouching Branches? (Useful if branch pruning - may want to threshold out small, split branches after): "), 4, 0)
|
|
11615
11844
|
correction_layout.addWidget(self.fix3, 4, 1)
|
|
11616
11845
|
|
|
@@ -12137,6 +12366,8 @@ class CentroidDialog(QDialog):
|
|
|
12137
12366
|
|
|
12138
12367
|
try:
|
|
12139
12368
|
|
|
12369
|
+
print("Calculating centroids...")
|
|
12370
|
+
|
|
12140
12371
|
chan = self.mode_selector.currentIndex()
|
|
12141
12372
|
|
|
12142
12373
|
# Get directory (None if empty)
|
|
@@ -12499,6 +12730,9 @@ class CalcAllDialog(QDialog):
|
|
|
12499
12730
|
|
|
12500
12731
|
|
|
12501
12732
|
except Exception as e:
|
|
12733
|
+
import traceback
|
|
12734
|
+
print(traceback.format_exc())
|
|
12735
|
+
|
|
12502
12736
|
QMessageBox.critical(
|
|
12503
12737
|
self,
|
|
12504
12738
|
"Error",
|
|
@@ -12506,6 +12740,7 @@ class CalcAllDialog(QDialog):
|
|
|
12506
12740
|
)
|
|
12507
12741
|
|
|
12508
12742
|
|
|
12743
|
+
|
|
12509
12744
|
class ProxDialog(QDialog):
|
|
12510
12745
|
def __init__(self, parent=None):
|
|
12511
12746
|
super().__init__(parent)
|
|
@@ -12744,11 +12979,6 @@ class ProxDialog(QDialog):
|
|
|
12744
12979
|
|
|
12745
12980
|
|
|
12746
12981
|
|
|
12747
|
-
|
|
12748
|
-
|
|
12749
|
-
|
|
12750
|
-
|
|
12751
|
-
|
|
12752
12982
|
# Initiating this program from the script line:
|
|
12753
12983
|
|
|
12754
12984
|
def run_gui():
|