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