nettracer3d 0.7.9__py3-none-any.whl → 0.8.1__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 +17 -26
- nettracer3d/neighborhoods.py +395 -58
- nettracer3d/nettracer.py +230 -39
- nettracer3d/nettracer_gui.py +1195 -202
- nettracer3d/node_draw.py +22 -12
- nettracer3d/proximity.py +83 -6
- nettracer3d/segmenter.py +1 -1
- nettracer3d/segmenter_GPU.py +1 -1
- nettracer3d/simple_network.py +43 -25
- {nettracer3d-0.7.9.dist-info → nettracer3d-0.8.1.dist-info}/METADATA +5 -3
- nettracer3d-0.8.1.dist-info/RECORD +23 -0
- nettracer3d-0.7.9.dist-info/RECORD +0 -23
- {nettracer3d-0.7.9.dist-info → nettracer3d-0.8.1.dist-info}/WHEEL +0 -0
- {nettracer3d-0.7.9.dist-info → nettracer3d-0.8.1.dist-info}/entry_points.txt +0 -0
- {nettracer3d-0.7.9.dist-info → nettracer3d-0.8.1.dist-info}/licenses/LICENSE +0 -0
- {nettracer3d-0.7.9.dist-info → nettracer3d-0.8.1.dist-info}/top_level.txt +0 -0
nettracer3d/nettracer_gui.py
CHANGED
|
@@ -30,6 +30,9 @@ try:
|
|
|
30
30
|
except:
|
|
31
31
|
pass
|
|
32
32
|
from nettracer3d import excelotron
|
|
33
|
+
import threading
|
|
34
|
+
import queue
|
|
35
|
+
from threading import Lock
|
|
33
36
|
|
|
34
37
|
|
|
35
38
|
|
|
@@ -44,6 +47,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
44
47
|
self.channel_visible = [False] * 4
|
|
45
48
|
self.current_slice = 0
|
|
46
49
|
self.active_channel = 0 # Initialize active channel
|
|
50
|
+
self.node_name = "Root_Nodes"
|
|
47
51
|
|
|
48
52
|
self.color_dictionary = {
|
|
49
53
|
# Reds
|
|
@@ -108,6 +112,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
108
112
|
self.selection_rect = None
|
|
109
113
|
self.click_start_time = None # Add this to track when click started
|
|
110
114
|
self.selection_threshold = 1.0 # Time in seconds before starting rectangle selection
|
|
115
|
+
self.background = None
|
|
116
|
+
self.last_update_time = 0
|
|
117
|
+
self.update_interval = 0.016 # 60 FPS
|
|
111
118
|
|
|
112
119
|
# Initialize zoom mode state
|
|
113
120
|
self.zoom_mode = False
|
|
@@ -119,6 +126,15 @@ class ImageViewerWindow(QMainWindow):
|
|
|
119
126
|
self.pan_mode = False
|
|
120
127
|
self.panning = False
|
|
121
128
|
self.pan_start = None
|
|
129
|
+
self.img_width = None
|
|
130
|
+
self.img_height = None
|
|
131
|
+
self.pre_pan_channel_state = None # Store which channels were visible before pan
|
|
132
|
+
self.pan_background = None # Store the static background image during pan
|
|
133
|
+
self.is_pan_preview = False # Track if we're in pan preview mode
|
|
134
|
+
self.pre_pan_channel_state = None # Store which channels were visible before pan
|
|
135
|
+
self.pan_background_image = None # Store the rendered composite image
|
|
136
|
+
self.pan_zoom_state = None # Store zoom state when pan began
|
|
137
|
+
self.is_pan_preview = False # Track if we're in pan preview mode
|
|
122
138
|
|
|
123
139
|
#For ML segmenting mode
|
|
124
140
|
self.brush_mode = False
|
|
@@ -157,7 +173,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
157
173
|
|
|
158
174
|
self.radii_dict = {
|
|
159
175
|
0: None,
|
|
160
|
-
1: None
|
|
176
|
+
1: None,
|
|
177
|
+
2: None,
|
|
178
|
+
3: None
|
|
161
179
|
}
|
|
162
180
|
|
|
163
181
|
self.original_shape = None #For undoing resamples
|
|
@@ -423,6 +441,25 @@ class ImageViewerWindow(QMainWindow):
|
|
|
423
441
|
self.excel_manager.data_received.connect(self.handle_excel_data)
|
|
424
442
|
self.prev_coms = None
|
|
425
443
|
|
|
444
|
+
self.paint_timer = QTimer()
|
|
445
|
+
self.paint_timer.timeout.connect(self.flush_paint_updates)
|
|
446
|
+
self.paint_timer.setSingleShot(True)
|
|
447
|
+
self.pending_paint_update = False
|
|
448
|
+
|
|
449
|
+
# Threading for paint operations
|
|
450
|
+
self.paint_queue = queue.Queue()
|
|
451
|
+
self.paint_lock = Lock()
|
|
452
|
+
self.paint_worker = threading.Thread(target=self.paint_worker_loop, daemon=True)
|
|
453
|
+
self.paint_worker.start()
|
|
454
|
+
|
|
455
|
+
# Background caching for blitting
|
|
456
|
+
self.paint_background = None
|
|
457
|
+
self.paint_session_active = False
|
|
458
|
+
|
|
459
|
+
# Batch paint operations
|
|
460
|
+
self.paint_batch = []
|
|
461
|
+
self.last_paint_pos = None
|
|
462
|
+
|
|
426
463
|
def start_left_scroll(self):
|
|
427
464
|
"""Start scrolling left when left arrow is pressed."""
|
|
428
465
|
# Single increment first
|
|
@@ -794,6 +831,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
794
831
|
try:
|
|
795
832
|
# Create context menu
|
|
796
833
|
context_menu = QMenu(self)
|
|
834
|
+
|
|
835
|
+
find = context_menu.addAction("Find Node/Edge")
|
|
836
|
+
find.triggered.connect(self.handle_find)
|
|
797
837
|
|
|
798
838
|
# Create "Show Neighbors" submenu
|
|
799
839
|
neighbors_menu = QMenu("Show Neighbors", self)
|
|
@@ -1450,6 +1490,109 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1450
1490
|
except Exception as e:
|
|
1451
1491
|
print(f"Error showing identities: {e}")
|
|
1452
1492
|
|
|
1493
|
+
def handle_find(self):
|
|
1494
|
+
|
|
1495
|
+
class FindDialog(QDialog):
|
|
1496
|
+
def __init__(self, parent=None):
|
|
1497
|
+
super().__init__(parent)
|
|
1498
|
+
self.setWindowTitle("Find Node (or edge?)")
|
|
1499
|
+
self.setModal(True)
|
|
1500
|
+
|
|
1501
|
+
layout = QFormLayout(self)
|
|
1502
|
+
|
|
1503
|
+
self.targ = QLineEdit("")
|
|
1504
|
+
layout.addRow("Node/Edge ID:", self.targ)
|
|
1505
|
+
|
|
1506
|
+
run_button = QPushButton("Enter")
|
|
1507
|
+
run_button.clicked.connect(self.run)
|
|
1508
|
+
layout.addWidget(run_button)
|
|
1509
|
+
|
|
1510
|
+
def run(self):
|
|
1511
|
+
|
|
1512
|
+
try:
|
|
1513
|
+
|
|
1514
|
+
value = int(self.targ.text()) if self.targ.text().strip() else None
|
|
1515
|
+
|
|
1516
|
+
if value is None:
|
|
1517
|
+
return
|
|
1518
|
+
|
|
1519
|
+
if self.parent().active_channel == 1:
|
|
1520
|
+
|
|
1521
|
+
mode = 'edges'
|
|
1522
|
+
|
|
1523
|
+
if my_network.edge_centroids is None:
|
|
1524
|
+
self.parent().show_centroid_dialog()
|
|
1525
|
+
|
|
1526
|
+
num = (self.parent().channel_data[1].shape[0] * self.parent().channel_data[1].shape[1] * self.parent().channel_data[1].shape[2])
|
|
1527
|
+
|
|
1528
|
+
if value in my_network.edge_centroids:
|
|
1529
|
+
|
|
1530
|
+
# Get centroid coordinates (Z, Y, X)
|
|
1531
|
+
centroid = my_network.edge_centroids[value]
|
|
1532
|
+
# Set the active channel to edges (1)
|
|
1533
|
+
self.parent().set_active_channel(1)
|
|
1534
|
+
# Toggle on the edges channel if it's not already visible
|
|
1535
|
+
if not self.parent().channel_visible[1]:
|
|
1536
|
+
self.parent().channel_buttons[1].setChecked(True)
|
|
1537
|
+
self.parent().toggle_channel(1)
|
|
1538
|
+
# Navigate to the Z-slice
|
|
1539
|
+
self.parent().slice_slider.setValue(int(centroid[0]))
|
|
1540
|
+
print(f"Found edge {value} at Z-slice {centroid[0]}")
|
|
1541
|
+
|
|
1542
|
+
else:
|
|
1543
|
+
print(f"Edge {value} not found in centroids dictionary")
|
|
1544
|
+
|
|
1545
|
+
|
|
1546
|
+
else:
|
|
1547
|
+
|
|
1548
|
+
mode = 'nodes'
|
|
1549
|
+
|
|
1550
|
+
if my_network.node_centroids is None:
|
|
1551
|
+
self.parent().show_centroid_dialog()
|
|
1552
|
+
|
|
1553
|
+
num = (self.parent().channel_data[0].shape[0] * self.parent().channel_data[0].shape[1] * self.parent().channel_data[0].shape[2])
|
|
1554
|
+
|
|
1555
|
+
if value in my_network.node_centroids:
|
|
1556
|
+
# Get centroid coordinates (Z, Y, X)
|
|
1557
|
+
centroid = my_network.node_centroids[value]
|
|
1558
|
+
# Set the active channel to nodes (0)
|
|
1559
|
+
self.parent().set_active_channel(0)
|
|
1560
|
+
# Toggle on the nodes channel if it's not already visible
|
|
1561
|
+
if not self.parent().channel_visible[0]:
|
|
1562
|
+
self.parent().channel_buttons[0].setChecked(True)
|
|
1563
|
+
self.parent().toggle_channel(0)
|
|
1564
|
+
# Navigate to the Z-slice
|
|
1565
|
+
self.parent().slice_slider.setValue(int(centroid[0]))
|
|
1566
|
+
print(f"Found node {value} at Z-slice {centroid[0]}")
|
|
1567
|
+
|
|
1568
|
+
else:
|
|
1569
|
+
print(f"Node {value} not found in centroids dictionary")
|
|
1570
|
+
|
|
1571
|
+
self.parent().clicked_values[mode] = [value]
|
|
1572
|
+
|
|
1573
|
+
if num > self.parent().mini_thresh:
|
|
1574
|
+
self.parent().mini_overlay = True
|
|
1575
|
+
self.parent().create_mini_overlay(node_indices = self.parent().clicked_values['nodes'], edge_indices = self.parent().clicked_values['edges'])
|
|
1576
|
+
else:
|
|
1577
|
+
self.parent().create_highlight_overlay(
|
|
1578
|
+
node_indices=self.parent().clicked_values['nodes'],
|
|
1579
|
+
edge_indices=self.parent().clicked_values['edges']
|
|
1580
|
+
)
|
|
1581
|
+
|
|
1582
|
+
|
|
1583
|
+
|
|
1584
|
+
# Close the dialog after processing
|
|
1585
|
+
self.accept()
|
|
1586
|
+
|
|
1587
|
+
except Exception as e:
|
|
1588
|
+
|
|
1589
|
+
print(f"Error: {e}")
|
|
1590
|
+
|
|
1591
|
+
dialog = FindDialog(self)
|
|
1592
|
+
dialog.exec()
|
|
1593
|
+
|
|
1594
|
+
|
|
1595
|
+
|
|
1453
1596
|
|
|
1454
1597
|
def handle_select_all(self, nodes = True, edges = False):
|
|
1455
1598
|
|
|
@@ -1630,63 +1773,32 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1630
1773
|
except Exception as e:
|
|
1631
1774
|
print(f"An error has occured: {e}")
|
|
1632
1775
|
|
|
1633
|
-
def
|
|
1776
|
+
def separate_nontouching_objects(self, input_array, max_val=0):
|
|
1777
|
+
"""
|
|
1778
|
+
optimized version using advanced indexing.
|
|
1779
|
+
"""
|
|
1780
|
+
|
|
1781
|
+
print("Splitting nontouching objects")
|
|
1782
|
+
|
|
1783
|
+
binary_mask = input_array > 0
|
|
1784
|
+
labeled_array, _ = n3d.label_objects(binary_mask)
|
|
1785
|
+
|
|
1786
|
+
# Create a compound key for each (original_label, connected_component) pair
|
|
1787
|
+
# This avoids the need for explicit mapping
|
|
1788
|
+
mask = binary_mask
|
|
1789
|
+
compound_key = input_array[mask] * (labeled_array.max() + 1) + labeled_array[mask]
|
|
1790
|
+
|
|
1791
|
+
# Get unique compound keys and create new labels
|
|
1792
|
+
unique_keys, inverse_indices = np.unique(compound_key, return_inverse=True)
|
|
1793
|
+
new_labels = np.arange(max_val + 1, max_val + 1 + len(unique_keys))
|
|
1794
|
+
|
|
1795
|
+
# Create output array
|
|
1796
|
+
output_array = np.zeros_like(input_array)
|
|
1797
|
+
output_array[mask] = new_labels[inverse_indices]
|
|
1798
|
+
|
|
1799
|
+
return output_array
|
|
1634
1800
|
|
|
1635
|
-
|
|
1636
|
-
from scipy.sparse import csr_matrix
|
|
1637
|
-
|
|
1638
|
-
print("Note, this method is a tad slow...")
|
|
1639
|
-
|
|
1640
|
-
def separate_nontouching_objects(input_array, max_val = 0):
|
|
1641
|
-
"""
|
|
1642
|
-
Efficiently separate non-touching objects in a labeled array.
|
|
1643
|
-
|
|
1644
|
-
Parameters:
|
|
1645
|
-
-----------
|
|
1646
|
-
input_array : numpy.ndarray
|
|
1647
|
-
Input labeled array where each object has a unique label value > 0
|
|
1648
|
-
|
|
1649
|
-
Returns:
|
|
1650
|
-
--------
|
|
1651
|
-
output_array : numpy.ndarray
|
|
1652
|
-
Array with new labels where non-touching components have different labels
|
|
1653
|
-
"""
|
|
1654
|
-
# Step 1: Perform connected component labeling on the entire binary mask
|
|
1655
|
-
binary_mask = input_array > 0
|
|
1656
|
-
structure = np.ones((3,) * input_array.ndim, dtype=bool) # 3x3x3 connectivity for 3D or 3x3 for 2D
|
|
1657
|
-
labeled_array, num_features = ndi.label(binary_mask, structure=structure)
|
|
1658
|
-
|
|
1659
|
-
# Step 2: Map the original labels to the new connected components
|
|
1660
|
-
# Create a sparse matrix to efficiently store label mappings
|
|
1661
|
-
coords = np.nonzero(input_array)
|
|
1662
|
-
original_values = input_array[coords]
|
|
1663
|
-
new_labels = labeled_array[coords]
|
|
1664
|
-
|
|
1665
|
-
# Create a mapping of (original_label, new_connected_component) pairs
|
|
1666
|
-
label_mapping = {}
|
|
1667
|
-
for orig, new in zip(original_values, new_labels):
|
|
1668
|
-
if orig not in label_mapping:
|
|
1669
|
-
label_mapping[orig] = []
|
|
1670
|
-
if new not in label_mapping[orig]:
|
|
1671
|
-
label_mapping[orig].append(new)
|
|
1672
|
-
|
|
1673
|
-
# Step 3: Create a new output array with unique labels for each connected component
|
|
1674
|
-
output_array = np.zeros_like(input_array)
|
|
1675
|
-
next_label = 1 + max_val
|
|
1676
|
-
|
|
1677
|
-
# Map of (original_label, connected_component) -> new_unique_label
|
|
1678
|
-
unique_label_map = {}
|
|
1679
|
-
|
|
1680
|
-
for orig_label, cc_list in label_mapping.items():
|
|
1681
|
-
for cc in cc_list:
|
|
1682
|
-
unique_label_map[(orig_label, cc)] = next_label
|
|
1683
|
-
# Create a mask for this original label and connected component
|
|
1684
|
-
mask = (input_array == orig_label) & (labeled_array == cc)
|
|
1685
|
-
# Assign the new unique label
|
|
1686
|
-
output_array[mask] = next_label
|
|
1687
|
-
next_label += 1
|
|
1688
|
-
|
|
1689
|
-
return output_array
|
|
1801
|
+
def handle_seperate(self):
|
|
1690
1802
|
|
|
1691
1803
|
try:
|
|
1692
1804
|
# Handle nodes
|
|
@@ -1708,7 +1820,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1708
1820
|
max_val = np.max(non_highlighted)
|
|
1709
1821
|
|
|
1710
1822
|
# Process highlighted part
|
|
1711
|
-
processed_highlights = separate_nontouching_objects(highlighted_nodes, max_val)
|
|
1823
|
+
processed_highlights = self.separate_nontouching_objects(highlighted_nodes, max_val)
|
|
1712
1824
|
|
|
1713
1825
|
# Combine back with non-highlighted parts
|
|
1714
1826
|
my_network.nodes = non_highlighted + processed_highlights
|
|
@@ -1729,13 +1841,13 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1729
1841
|
# Get non-highlighted part of the array
|
|
1730
1842
|
non_highlighted = my_network.edges * (~self.highlight_overlay)
|
|
1731
1843
|
|
|
1732
|
-
if (
|
|
1844
|
+
if (highlighted_edges==non_highlighted).all():
|
|
1733
1845
|
max_val = 0
|
|
1734
1846
|
else:
|
|
1735
1847
|
max_val = np.max(non_highlighted)
|
|
1736
1848
|
|
|
1737
1849
|
# Process highlighted part
|
|
1738
|
-
processed_highlights = separate_nontouching_objects(highlighted_edges, max_val)
|
|
1850
|
+
processed_highlights = self.separate_nontouching_objects(highlighted_edges, max_val)
|
|
1739
1851
|
|
|
1740
1852
|
# Combine back with non-highlighted parts
|
|
1741
1853
|
my_network.edges = non_highlighted + processed_highlights
|
|
@@ -2074,8 +2186,11 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2074
2186
|
|
|
2075
2187
|
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
2076
2188
|
|
|
2189
|
+
|
|
2077
2190
|
def keyPressEvent(self, event):
|
|
2078
2191
|
|
|
2192
|
+
"""Key press shortcuts for main class"""
|
|
2193
|
+
|
|
2079
2194
|
if event.key() == Qt.Key_Z and event.modifiers() & Qt.ControlModifier:
|
|
2080
2195
|
try:
|
|
2081
2196
|
self.load_channel(self.last_change[1], self.last_change[0], True)
|
|
@@ -2091,6 +2206,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2091
2206
|
self.machine_window.switch_foreground()
|
|
2092
2207
|
if event.key() == Qt.Key_X:
|
|
2093
2208
|
self.high_button.click()
|
|
2209
|
+
if event.key() == Qt.Key_F and event.modifiers() == Qt.ShiftModifier:
|
|
2210
|
+
self.handle_find()
|
|
2094
2211
|
if self.brush_mode and self.machine_window is None:
|
|
2095
2212
|
if event.key() == Qt.Key_F:
|
|
2096
2213
|
self.toggle_can()
|
|
@@ -2098,6 +2215,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2098
2215
|
self.toggle_threed()
|
|
2099
2216
|
|
|
2100
2217
|
|
|
2218
|
+
|
|
2101
2219
|
def update_brush_cursor(self):
|
|
2102
2220
|
"""Update the cursor to show brush size"""
|
|
2103
2221
|
if not self.brush_mode:
|
|
@@ -2158,7 +2276,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2158
2276
|
painter.end()
|
|
2159
2277
|
|
|
2160
2278
|
def get_line_points(self, x0, y0, x1, y1):
|
|
2161
|
-
"""Get all points in a line between (x0,y0) and (x1,y1) using Bresenham's algorithm"""
|
|
2279
|
+
"""Get all points in a line between (x0,y0) and (x1,y1) using Bresenham's algorithm."""
|
|
2162
2280
|
points = []
|
|
2163
2281
|
dx = abs(x1 - x0)
|
|
2164
2282
|
dy = abs(y1 - y0)
|
|
@@ -2211,7 +2329,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2211
2329
|
return data_coords[0], data_coords[1]
|
|
2212
2330
|
|
|
2213
2331
|
def on_mouse_press(self, event):
|
|
2214
|
-
"""Handle mouse press events."""
|
|
2332
|
+
"""Handle mouse press events - OPTIMIZED VERSION."""
|
|
2215
2333
|
if event.inaxes != self.ax:
|
|
2216
2334
|
return
|
|
2217
2335
|
|
|
@@ -2249,7 +2367,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2249
2367
|
new_xlim = [xdata - x_range, xdata + x_range]
|
|
2250
2368
|
new_ylim = [ydata - y_range, ydata + y_range]
|
|
2251
2369
|
|
|
2252
|
-
|
|
2253
2370
|
if (new_xlim[0] <= self.original_xlim[0] or
|
|
2254
2371
|
new_xlim[1] >= self.original_xlim[1] or
|
|
2255
2372
|
new_ylim[0] <= self.original_ylim[0] or
|
|
@@ -2271,22 +2388,31 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2271
2388
|
self.panning = True
|
|
2272
2389
|
self.pan_start = (event.xdata, event.ydata)
|
|
2273
2390
|
self.canvas.setCursor(Qt.CursorShape.ClosedHandCursor)
|
|
2391
|
+
|
|
2392
|
+
# Store current channel visibility state
|
|
2393
|
+
self.pre_pan_channel_state = self.channel_visible.copy()
|
|
2394
|
+
|
|
2395
|
+
# Create static background from currently visible channels
|
|
2396
|
+
self.create_pan_background()
|
|
2397
|
+
|
|
2398
|
+
# Hide all channels and show only the background
|
|
2399
|
+
self.channel_visible = [False] * 4
|
|
2400
|
+
self.is_pan_preview = True
|
|
2401
|
+
|
|
2402
|
+
# Update display to show only background
|
|
2403
|
+
self.update_display_pan_mode()
|
|
2274
2404
|
|
|
2275
2405
|
elif self.brush_mode:
|
|
2276
2406
|
if event.inaxes != self.ax:
|
|
2277
2407
|
return
|
|
2278
2408
|
|
|
2279
|
-
|
|
2280
2409
|
if event.button == 1 or event.button == 3:
|
|
2281
|
-
|
|
2282
2410
|
x, y = int(event.xdata), int(event.ydata)
|
|
2283
2411
|
|
|
2284
|
-
|
|
2285
2412
|
if event.button == 1 and self.can:
|
|
2286
2413
|
self.handle_can(x, y)
|
|
2287
2414
|
return
|
|
2288
2415
|
|
|
2289
|
-
|
|
2290
2416
|
if event.button == 3:
|
|
2291
2417
|
self.erase = True
|
|
2292
2418
|
else:
|
|
@@ -2300,27 +2426,24 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2300
2426
|
else:
|
|
2301
2427
|
channel = 2
|
|
2302
2428
|
|
|
2303
|
-
#
|
|
2304
|
-
self.paint_at_position(x, y, self.erase, channel)
|
|
2305
|
-
|
|
2429
|
+
# Get current zoom to preserve it
|
|
2306
2430
|
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
2307
2431
|
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
2308
2432
|
|
|
2309
|
-
|
|
2433
|
+
# Paint at initial position
|
|
2434
|
+
self.paint_at_position(x, y, self.erase, channel)
|
|
2435
|
+
|
|
2310
2436
|
self.canvas.draw()
|
|
2311
|
-
#self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
2312
|
-
self.restore_channels = []
|
|
2313
2437
|
|
|
2438
|
+
self.restore_channels = []
|
|
2439
|
+
if not self.channel_visible[channel]:
|
|
2440
|
+
self.channel_visible[channel] = True
|
|
2314
2441
|
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
self.restore_channels.append(i)
|
|
2321
|
-
self.update_display(preserve_zoom = (current_xlim, current_ylim), begin_paint = True)
|
|
2322
|
-
self.update_display_slice(channel, preserve_zoom=(current_xlim, current_ylim))
|
|
2323
|
-
|
|
2442
|
+
# No need to hide other channels or track restore_channels
|
|
2443
|
+
self.restore_channels = []
|
|
2444
|
+
|
|
2445
|
+
self.update_display(preserve_zoom=(current_xlim, current_ylim), begin_paint=True)
|
|
2446
|
+
self.update_display_slice_optimized(channel, preserve_zoom=(current_xlim, current_ylim))
|
|
2324
2447
|
|
|
2325
2448
|
elif event.button == 3: # Right click (for context menu)
|
|
2326
2449
|
self.create_context_menu(event)
|
|
@@ -2334,7 +2457,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2334
2457
|
"""Paint pixels within brush radius at given position"""
|
|
2335
2458
|
if self.channel_data[channel] is None:
|
|
2336
2459
|
return
|
|
2337
|
-
|
|
2460
|
+
|
|
2338
2461
|
if erase:
|
|
2339
2462
|
val = 0
|
|
2340
2463
|
elif self.machine_window is None:
|
|
@@ -2343,26 +2466,74 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2343
2466
|
val = 1
|
|
2344
2467
|
else:
|
|
2345
2468
|
val = 2
|
|
2346
|
-
|
|
2347
2469
|
height, width = self.channel_data[channel][self.current_slice].shape
|
|
2348
2470
|
radius = self.brush_size // 2
|
|
2349
|
-
|
|
2471
|
+
|
|
2350
2472
|
# Calculate brush area
|
|
2351
2473
|
for y in range(max(0, center_y - radius), min(height, center_y + radius + 1)):
|
|
2352
2474
|
for x in range(max(0, center_x - radius), min(width, center_x + radius + 1)):
|
|
2353
2475
|
# Check if point is within circular brush area
|
|
2354
|
-
if (x - center_x)
|
|
2355
|
-
|
|
2476
|
+
if (x - center_x) * 2 + (y - center_y) * 2 <= radius ** 2:
|
|
2356
2477
|
if self.threed and self.threedthresh > 1:
|
|
2357
2478
|
amount = (self.threedthresh - 1) / 2
|
|
2358
2479
|
low = max(0, self.current_slice - amount)
|
|
2359
2480
|
high = min(self.channel_data[channel].shape[0] - 1, self.current_slice + amount)
|
|
2360
|
-
|
|
2361
2481
|
for i in range(int(low), int(high + 1)):
|
|
2362
2482
|
self.channel_data[channel][i][y, x] = val
|
|
2363
2483
|
else:
|
|
2364
2484
|
self.channel_data[channel][self.current_slice][y, x] = val
|
|
2365
2485
|
|
|
2486
|
+
def paint_at_position_vectorized(self, center_x, center_y, erase=False, channel=2,
|
|
2487
|
+
slice_idx=None, brush_size=None, threed=False,
|
|
2488
|
+
threedthresh=1, foreground=True, machine_window=None):
|
|
2489
|
+
"""Vectorized paint operation for better performance."""
|
|
2490
|
+
if self.channel_data[channel] is None:
|
|
2491
|
+
return
|
|
2492
|
+
|
|
2493
|
+
# Use provided parameters or fall back to instance variables
|
|
2494
|
+
slice_idx = slice_idx if slice_idx is not None else self.current_slice
|
|
2495
|
+
brush_size = brush_size if brush_size is not None else getattr(self, 'brush_size', 5)
|
|
2496
|
+
|
|
2497
|
+
# Determine paint value
|
|
2498
|
+
if erase:
|
|
2499
|
+
val = 0
|
|
2500
|
+
elif machine_window is None:
|
|
2501
|
+
val = 255
|
|
2502
|
+
elif foreground:
|
|
2503
|
+
val = 1
|
|
2504
|
+
else:
|
|
2505
|
+
val = 2
|
|
2506
|
+
|
|
2507
|
+
height, width = self.channel_data[channel][slice_idx].shape
|
|
2508
|
+
radius = brush_size // 2
|
|
2509
|
+
|
|
2510
|
+
# Calculate affected region bounds
|
|
2511
|
+
y_min = max(0, center_y - radius)
|
|
2512
|
+
y_max = min(height, center_y + radius + 1)
|
|
2513
|
+
x_min = max(0, center_x - radius)
|
|
2514
|
+
x_max = min(width, center_x + radius + 1)
|
|
2515
|
+
|
|
2516
|
+
if y_min >= y_max or x_min >= x_max:
|
|
2517
|
+
return # No valid region to paint
|
|
2518
|
+
|
|
2519
|
+
# Create coordinate grids for the affected region
|
|
2520
|
+
y_coords, x_coords = np.mgrid[y_min:y_max, x_min:x_max]
|
|
2521
|
+
|
|
2522
|
+
# Calculate distances squared (avoid sqrt for performance)
|
|
2523
|
+
distances_sq = (x_coords - center_x) ** 2 + (y_coords - center_y) ** 2
|
|
2524
|
+
mask = distances_sq <= radius ** 2
|
|
2525
|
+
|
|
2526
|
+
# Apply paint to affected slices
|
|
2527
|
+
if threed and threedthresh > 1:
|
|
2528
|
+
amount = (threedthresh - 1) / 2
|
|
2529
|
+
low = max(0, int(slice_idx - amount))
|
|
2530
|
+
high = min(self.channel_data[channel].shape[0] - 1, int(slice_idx + amount))
|
|
2531
|
+
|
|
2532
|
+
for i in range(low, high + 1):
|
|
2533
|
+
self.channel_data[channel][i][y_min:y_max, x_min:x_max][mask] = val
|
|
2534
|
+
else:
|
|
2535
|
+
self.channel_data[channel][slice_idx][y_min:y_max, x_min:x_max][mask] = val
|
|
2536
|
+
|
|
2366
2537
|
def handle_can(self, x, y):
|
|
2367
2538
|
|
|
2368
2539
|
|
|
@@ -2425,33 +2596,42 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2425
2596
|
|
|
2426
2597
|
|
|
2427
2598
|
def on_mouse_move(self, event):
|
|
2428
|
-
|
|
2429
|
-
if event.inaxes != self.ax:
|
|
2599
|
+
if not event.inaxes or event.xdata is None or event.ydata is None:
|
|
2430
2600
|
return
|
|
2431
|
-
|
|
2601
|
+
|
|
2602
|
+
current_time = time.time()
|
|
2603
|
+
|
|
2432
2604
|
if self.selection_start and not self.selecting and not self.pan_mode and not self.zoom_mode and not self.brush_mode:
|
|
2433
|
-
# If mouse has moved more than a tiny amount while button is held, start selection
|
|
2434
2605
|
if (abs(event.xdata - self.selection_start[0]) > 1 or
|
|
2435
2606
|
abs(event.ydata - self.selection_start[1]) > 1):
|
|
2436
2607
|
self.selecting = True
|
|
2608
|
+
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
|
|
2609
|
+
|
|
2437
2610
|
self.selection_rect = plt.Rectangle(
|
|
2438
2611
|
(self.selection_start[0], self.selection_start[1]), 0, 0,
|
|
2439
|
-
fill=False, color='white', linestyle='--'
|
|
2612
|
+
fill=False, color='white', linestyle='--', animated=True
|
|
2440
2613
|
)
|
|
2441
2614
|
self.ax.add_patch(self.selection_rect)
|
|
2442
2615
|
|
|
2443
2616
|
if self.selecting and self.selection_rect is not None:
|
|
2444
|
-
#
|
|
2445
|
-
|
|
2446
|
-
|
|
2617
|
+
# Throttle updates
|
|
2618
|
+
if current_time - self.last_update_time < self.update_interval:
|
|
2619
|
+
return
|
|
2620
|
+
self.last_update_time = current_time
|
|
2621
|
+
|
|
2622
|
+
# Use blitting
|
|
2623
|
+
self.canvas.restore_region(self.background)
|
|
2624
|
+
|
|
2625
|
+
x_min = min(self.selection_start[0], event.xdata)
|
|
2626
|
+
y_min = min(self.selection_start[1], event.ydata)
|
|
2447
2627
|
width = abs(event.xdata - self.selection_start[0])
|
|
2448
2628
|
height = abs(event.ydata - self.selection_start[1])
|
|
2449
2629
|
|
|
2450
|
-
self.selection_rect.set_bounds(
|
|
2451
|
-
self.
|
|
2630
|
+
self.selection_rect.set_bounds(x_min, y_min, width, height)
|
|
2631
|
+
self.ax.draw_artist(self.selection_rect)
|
|
2632
|
+
self.canvas.blit(self.ax.bbox)
|
|
2452
2633
|
|
|
2453
2634
|
elif self.panning and self.pan_start is not None:
|
|
2454
|
-
|
|
2455
2635
|
# Calculate the movement
|
|
2456
2636
|
dx = event.xdata - self.pan_start[0]
|
|
2457
2637
|
dy = event.ydata - self.pan_start[1]
|
|
@@ -2464,25 +2644,23 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2464
2644
|
new_xlim = [xlim[0] - dx, xlim[1] - dx]
|
|
2465
2645
|
new_ylim = [ylim[0] - dy, ylim[1] - dy]
|
|
2466
2646
|
|
|
2467
|
-
# Get image bounds
|
|
2468
|
-
if self.
|
|
2469
|
-
img_height, img_width = self.channel_data[0][self.current_slice].shape
|
|
2470
|
-
|
|
2647
|
+
# Get image bounds using cached dimensions
|
|
2648
|
+
if self.img_width is not None: # Changed from self.channel_data[0] check
|
|
2471
2649
|
# Ensure new limits don't go beyond image bounds
|
|
2472
2650
|
if new_xlim[0] < 0:
|
|
2473
2651
|
new_xlim = [0, xlim[1] - xlim[0]]
|
|
2474
|
-
elif new_xlim[1] > img_width:
|
|
2475
|
-
new_xlim = [img_width - (xlim[1] - xlim[0]), img_width]
|
|
2652
|
+
elif new_xlim[1] > self.img_width: # Changed from img_width variable lookup
|
|
2653
|
+
new_xlim = [self.img_width - (xlim[1] - xlim[0]), self.img_width]
|
|
2476
2654
|
|
|
2477
2655
|
if new_ylim[0] < 0:
|
|
2478
2656
|
new_ylim = [0, ylim[1] - ylim[0]]
|
|
2479
|
-
elif new_ylim[1] > img_height:
|
|
2480
|
-
new_ylim = [img_height - (ylim[1] - ylim[0]), img_height]
|
|
2657
|
+
elif new_ylim[1] > self.img_height: # Changed from img_height variable lookup
|
|
2658
|
+
new_ylim = [self.img_height - (ylim[1] - ylim[0]), self.img_height]
|
|
2481
2659
|
|
|
2482
2660
|
# Apply new limits
|
|
2483
2661
|
self.ax.set_xlim(new_xlim)
|
|
2484
2662
|
self.ax.set_ylim(new_ylim)
|
|
2485
|
-
self.canvas.draw()
|
|
2663
|
+
self.canvas.draw_idle() # Changed from draw() to draw_idle()
|
|
2486
2664
|
|
|
2487
2665
|
# Update pan start position
|
|
2488
2666
|
self.pan_start = (event.xdata, event.ydata)
|
|
@@ -2490,43 +2668,365 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2490
2668
|
elif self.painting and self.brush_mode:
|
|
2491
2669
|
if event.inaxes != self.ax:
|
|
2492
2670
|
return
|
|
2493
|
-
|
|
2494
|
-
|
|
2671
|
+
|
|
2672
|
+
# OPTIMIZED: Queue paint operation instead of immediate execution
|
|
2673
|
+
self.queue_paint_operation(event)
|
|
2674
|
+
|
|
2675
|
+
# OPTIMIZED: Schedule display update at controlled frequency
|
|
2676
|
+
if not self.pending_paint_update:
|
|
2677
|
+
self.pending_paint_update = True
|
|
2678
|
+
self.paint_timer.start(16) # ~60fps max update rate
|
|
2495
2679
|
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2680
|
+
def queue_paint_operation(self, event):
|
|
2681
|
+
"""Queue a paint operation for background processing."""
|
|
2682
|
+
x, y = int(event.xdata), int(event.ydata)
|
|
2683
|
+
|
|
2684
|
+
if self.pen_button.isChecked():
|
|
2685
|
+
channel = self.active_channel
|
|
2686
|
+
else:
|
|
2687
|
+
channel = 2
|
|
2688
|
+
|
|
2689
|
+
if self.channel_data[channel] is not None:
|
|
2690
|
+
# Prepare paint session if needed
|
|
2691
|
+
if not self.paint_session_active:
|
|
2692
|
+
self.prepare_paint_session(channel)
|
|
2693
|
+
|
|
2694
|
+
# Create paint operation
|
|
2695
|
+
paint_op = {
|
|
2696
|
+
'type': 'stroke',
|
|
2697
|
+
'x': x,
|
|
2698
|
+
'y': y,
|
|
2699
|
+
'last_pos': getattr(self, 'last_paint_pos', None),
|
|
2700
|
+
'brush_size': self.brush_size,
|
|
2701
|
+
'erase': self.erase,
|
|
2702
|
+
'channel': channel,
|
|
2703
|
+
'slice': self.current_slice,
|
|
2704
|
+
'threed': getattr(self, 'threed', False),
|
|
2705
|
+
'threedthresh': getattr(self, 'threedthresh', 1),
|
|
2706
|
+
'foreground': getattr(self, 'foreground', True),
|
|
2707
|
+
'machine_window': getattr(self, 'machine_window', None)
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
# Queue the operation
|
|
2711
|
+
try:
|
|
2712
|
+
self.paint_queue.put_nowait(paint_op)
|
|
2713
|
+
except queue.Full:
|
|
2714
|
+
pass # Skip if queue is full to avoid blocking
|
|
2715
|
+
|
|
2716
|
+
self.last_paint_pos = (x, y)
|
|
2500
2717
|
|
|
2718
|
+
def prepare_paint_session(self, channel):
|
|
2719
|
+
"""Prepare optimized background for blitting during paint session."""
|
|
2720
|
+
if self.paint_session_active:
|
|
2721
|
+
return
|
|
2501
2722
|
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2723
|
+
# IMPORTANT: Don't capture background here - let the main display update handle it
|
|
2724
|
+
# We'll capture the background after the proper channel visibility setup
|
|
2725
|
+
self.paint_session_active = True
|
|
2726
|
+
|
|
2727
|
+
def end_paint_session(self):
|
|
2728
|
+
"""Clean up after paint session."""
|
|
2729
|
+
self.paint_session_active = False
|
|
2730
|
+
self.paint_background = None
|
|
2731
|
+
self.last_paint_pos = None
|
|
2732
|
+
|
|
2733
|
+
def paint_worker_loop(self):
|
|
2734
|
+
"""Background thread for processing paint operations."""
|
|
2735
|
+
while True:
|
|
2736
|
+
try:
|
|
2737
|
+
paint_op = self.paint_queue.get(timeout=1.0)
|
|
2738
|
+
if paint_op is None: # Shutdown signal
|
|
2739
|
+
break
|
|
2506
2740
|
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
points = self.get_line_points(last_x, last_y, x, y)
|
|
2741
|
+
with self.paint_lock:
|
|
2742
|
+
self.execute_paint_operation(paint_op)
|
|
2510
2743
|
|
|
2511
|
-
|
|
2744
|
+
except queue.Empty:
|
|
2745
|
+
continue
|
|
2746
|
+
|
|
2747
|
+
def shutdown(self):
|
|
2748
|
+
"""Clean shutdown of worker thread."""
|
|
2749
|
+
self.paint_queue.put(None) # Signal worker to stop
|
|
2750
|
+
if hasattr(self, 'paint_worker'):
|
|
2751
|
+
self.paint_worker.join(timeout=1.0)
|
|
2752
|
+
|
|
2753
|
+
def execute_paint_operation(self, paint_op):
|
|
2754
|
+
"""Execute a single paint operation on the data arrays."""
|
|
2755
|
+
if paint_op['type'] == 'stroke':
|
|
2756
|
+
channel = paint_op['channel']
|
|
2757
|
+
x, y = paint_op['x'], paint_op['y']
|
|
2758
|
+
last_pos = paint_op['last_pos']
|
|
2759
|
+
|
|
2760
|
+
if last_pos is not None:
|
|
2761
|
+
# Paint line from last position to current
|
|
2762
|
+
points = self.get_line_points(last_pos[0], last_pos[1], x, y)
|
|
2763
|
+
for px, py in points:
|
|
2764
|
+
height, width = self.channel_data[channel][paint_op['slice']].shape
|
|
2765
|
+
if 0 <= px < width and 0 <= py < height:
|
|
2766
|
+
self.paint_at_position_vectorized(
|
|
2767
|
+
px, py, paint_op['erase'], paint_op['channel'],
|
|
2768
|
+
paint_op['slice'], paint_op['brush_size'],
|
|
2769
|
+
paint_op['threed'], paint_op['threedthresh'],
|
|
2770
|
+
paint_op['foreground'], paint_op['machine_window']
|
|
2771
|
+
)
|
|
2772
|
+
else:
|
|
2773
|
+
# Single point paint
|
|
2774
|
+
height, width = self.channel_data[channel][paint_op['slice']].shape
|
|
2775
|
+
if 0 <= x < width and 0 <= y < height:
|
|
2776
|
+
self.paint_at_position_vectorized(
|
|
2777
|
+
x, y, paint_op['erase'], paint_op['channel'],
|
|
2778
|
+
paint_op['slice'], paint_op['brush_size'],
|
|
2779
|
+
paint_op['threed'], paint_op['threedthresh'],
|
|
2780
|
+
paint_op['foreground'], paint_op['machine_window']
|
|
2781
|
+
)
|
|
2512
2782
|
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2783
|
+
def flush_paint_updates(self):
|
|
2784
|
+
"""Update the display with batched paint changes."""
|
|
2785
|
+
self.pending_paint_update = False
|
|
2786
|
+
|
|
2787
|
+
# Determine which channel to update
|
|
2788
|
+
channel = self.active_channel if hasattr(self, 'pen_button') and self.pen_button.isChecked() else 2
|
|
2789
|
+
|
|
2790
|
+
# Get current zoom to preserve it
|
|
2791
|
+
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
2792
|
+
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
2793
|
+
|
|
2794
|
+
# Update display
|
|
2795
|
+
self.update_display_slice_optimized(channel, preserve_zoom=(current_xlim, current_ylim))
|
|
2796
|
+
|
|
2797
|
+
def create_pan_background(self):
|
|
2798
|
+
"""Create a static background image from currently visible channels with proper rendering"""
|
|
2799
|
+
# Store current zoom state
|
|
2800
|
+
current_xlim = self.ax.get_xlim()
|
|
2801
|
+
current_ylim = self.ax.get_ylim()
|
|
2802
|
+
|
|
2803
|
+
# Render all visible channels with proper colors/brightness into a single composite
|
|
2804
|
+
self.pan_background_image = self.create_composite_for_pan()
|
|
2805
|
+
self.pan_zoom_state = (current_xlim, current_ylim)
|
|
2806
|
+
|
|
2807
|
+
def create_composite_for_pan(self):
|
|
2808
|
+
"""Create a properly rendered composite image for panning"""
|
|
2809
|
+
# Get active channels and dimensions (copied from update_display)
|
|
2810
|
+
active_channels = [i for i in range(4) if self.channel_data[i] is not None]
|
|
2811
|
+
if active_channels:
|
|
2812
|
+
dims = [(self.channel_data[i].shape[1:3] if len(self.channel_data[i].shape) >= 3 else
|
|
2813
|
+
self.channel_data[i].shape) for i in active_channels]
|
|
2814
|
+
min_height = min(d[0] for d in dims)
|
|
2815
|
+
min_width = min(d[1] for d in dims)
|
|
2816
|
+
else:
|
|
2817
|
+
return None
|
|
2818
|
+
|
|
2819
|
+
# Create a blank RGBA composite to accumulate all channels
|
|
2820
|
+
composite = np.zeros((min_height, min_width, 4), dtype=np.float32)
|
|
2821
|
+
|
|
2822
|
+
# Process each visible channel exactly like update_display does
|
|
2823
|
+
for channel in range(4):
|
|
2824
|
+
if (self.channel_visible[channel] and
|
|
2825
|
+
self.channel_data[channel] is not None):
|
|
2516
2826
|
|
|
2517
|
-
|
|
2827
|
+
# Get current slice data (same logic as update_display)
|
|
2828
|
+
is_rgb = len(self.channel_data[channel].shape) == 4 and (self.channel_data[channel].shape[-1] == 3 or self.channel_data[channel].shape[-1] == 4)
|
|
2518
2829
|
|
|
2519
|
-
self.
|
|
2520
|
-
|
|
2521
|
-
|
|
2830
|
+
if len(self.channel_data[channel].shape) == 3 and not is_rgb:
|
|
2831
|
+
current_image = self.channel_data[channel][self.current_slice, :, :]
|
|
2832
|
+
elif is_rgb:
|
|
2833
|
+
current_image = self.channel_data[channel][self.current_slice]
|
|
2834
|
+
else:
|
|
2835
|
+
current_image = self.channel_data[channel]
|
|
2836
|
+
|
|
2837
|
+
if is_rgb and self.channel_data[channel].shape[-1] == 3:
|
|
2838
|
+
# RGB image - convert to RGBA and blend
|
|
2839
|
+
rgb_alpha = np.ones((*current_image.shape[:2], 4), dtype=np.float32)
|
|
2840
|
+
rgb_alpha[:, :, :3] = current_image.astype(np.float32) / 255.0
|
|
2841
|
+
rgb_alpha[:, :, 3] = 0.7 # Same alpha as update_display
|
|
2842
|
+
composite = self.blend_layers(composite, rgb_alpha)
|
|
2843
|
+
|
|
2844
|
+
elif is_rgb and self.channel_data[channel].shape[-1] == 4:
|
|
2845
|
+
# RGBA image - blend directly
|
|
2846
|
+
rgba_image = current_image.astype(np.float32) / 255.0
|
|
2847
|
+
composite = self.blend_layers(composite, rgba_image)
|
|
2848
|
+
|
|
2849
|
+
else:
|
|
2850
|
+
# Regular channel processing (same logic as update_display)
|
|
2851
|
+
if self.min_max[channel][0] == None:
|
|
2852
|
+
self.min_max[channel][0] = np.min(current_image)
|
|
2853
|
+
if self.min_max[channel][1] == None:
|
|
2854
|
+
self.min_max[channel][1] = np.max(current_image)
|
|
2855
|
+
|
|
2856
|
+
img_min = self.min_max[channel][0]
|
|
2857
|
+
img_max = self.min_max[channel][1]
|
|
2858
|
+
|
|
2859
|
+
if img_min == img_max:
|
|
2860
|
+
vmin = img_min
|
|
2861
|
+
vmax = img_min + 1
|
|
2862
|
+
else:
|
|
2863
|
+
vmin = img_min + (img_max - img_min) * self.channel_brightness[channel]['min']
|
|
2864
|
+
vmax = img_min + (img_max - img_min) * self.channel_brightness[channel]['max']
|
|
2865
|
+
|
|
2866
|
+
# Normalize the image
|
|
2867
|
+
if vmin == vmax:
|
|
2868
|
+
normalized_image = np.zeros_like(current_image)
|
|
2869
|
+
else:
|
|
2870
|
+
normalized_image = np.clip((current_image - vmin) / (vmax - vmin), 0, 1)
|
|
2871
|
+
|
|
2872
|
+
# Apply channel color and alpha
|
|
2873
|
+
if channel == 2 and self.machine_window is not None:
|
|
2874
|
+
# Special case for machine window channel 2
|
|
2875
|
+
channel_rgba = self.apply_machine_colormap(current_image)
|
|
2876
|
+
else:
|
|
2877
|
+
# Regular channel with custom color
|
|
2878
|
+
color = self.base_colors[channel]
|
|
2879
|
+
channel_rgba = np.zeros((*normalized_image.shape, 4), dtype=np.float32)
|
|
2880
|
+
channel_rgba[:, :, 0] = normalized_image * color[0] # R
|
|
2881
|
+
channel_rgba[:, :, 1] = normalized_image * color[1] # G
|
|
2882
|
+
channel_rgba[:, :, 2] = normalized_image * color[2] # B
|
|
2883
|
+
channel_rgba[:, :, 3] = normalized_image * 0.7 # A (same alpha as update_display)
|
|
2884
|
+
|
|
2885
|
+
# Blend this channel into the composite
|
|
2886
|
+
composite = self.blend_layers(composite, channel_rgba)
|
|
2887
|
+
|
|
2888
|
+
# Add highlight overlays if they exist (same logic as update_display)
|
|
2889
|
+
if self.mini_overlay and self.highlight and self.machine_window is None:
|
|
2890
|
+
highlight_rgba = self.create_highlight_rgba(self.mini_overlay_data, yellow=True)
|
|
2891
|
+
composite = self.blend_layers(composite, highlight_rgba)
|
|
2892
|
+
elif self.highlight_overlay is not None and self.highlight:
|
|
2893
|
+
highlight_slice = self.highlight_overlay[self.current_slice]
|
|
2894
|
+
if self.machine_window is None:
|
|
2895
|
+
highlight_rgba = self.create_highlight_rgba(highlight_slice, yellow=True)
|
|
2896
|
+
else:
|
|
2897
|
+
highlight_rgba = self.create_highlight_rgba(highlight_slice, yellow=False)
|
|
2898
|
+
composite = self.blend_layers(composite, highlight_rgba)
|
|
2899
|
+
|
|
2900
|
+
# Convert to 0-255 range for display
|
|
2901
|
+
return (composite * 255).astype(np.uint8)
|
|
2902
|
+
|
|
2903
|
+
def apply_machine_colormap(self, image):
|
|
2904
|
+
"""Apply the special machine window colormap for channel 2"""
|
|
2905
|
+
rgba = np.zeros((*image.shape, 4), dtype=np.float32)
|
|
2906
|
+
|
|
2907
|
+
# Transparent for 0
|
|
2908
|
+
mask_0 = (image == 0)
|
|
2909
|
+
rgba[mask_0] = [0, 0, 0, 0]
|
|
2910
|
+
|
|
2911
|
+
# Light green for 1
|
|
2912
|
+
mask_1 = (image == 1)
|
|
2913
|
+
rgba[mask_1] = [0.5, 1, 0.5, 0.7]
|
|
2914
|
+
|
|
2915
|
+
# Light red for 2
|
|
2916
|
+
mask_2 = (image == 2)
|
|
2917
|
+
rgba[mask_2] = [1, 0.5, 0.5, 0.7]
|
|
2918
|
+
|
|
2919
|
+
return rgba
|
|
2522
2920
|
|
|
2921
|
+
def create_highlight_rgba(self, highlight_data, yellow=True):
|
|
2922
|
+
"""Create RGBA highlight overlay"""
|
|
2923
|
+
rgba = np.zeros((*highlight_data.shape, 4), dtype=np.float32)
|
|
2924
|
+
|
|
2925
|
+
if yellow:
|
|
2926
|
+
# Yellow highlight
|
|
2927
|
+
mask = highlight_data > 0
|
|
2928
|
+
rgba[mask] = [1, 1, 0, 0.5] # Yellow with alpha 0.5
|
|
2929
|
+
else:
|
|
2930
|
+
# Multi-color highlight for machine window
|
|
2931
|
+
mask_1 = (highlight_data == 1)
|
|
2932
|
+
mask_2 = (highlight_data == 2)
|
|
2933
|
+
rgba[mask_1] = [1, 1, 0, 0.5] # Yellow for 1
|
|
2934
|
+
rgba[mask_2] = [0, 0.7, 1, 0.5] # Blue for 2
|
|
2935
|
+
|
|
2936
|
+
return rgba
|
|
2937
|
+
|
|
2938
|
+
def blend_layers(self, base, overlay):
|
|
2939
|
+
"""Alpha blend two RGBA layers"""
|
|
2940
|
+
# Standard alpha blending formula
|
|
2941
|
+
alpha_overlay = overlay[:, :, 3:4]
|
|
2942
|
+
alpha_base = base[:, :, 3:4]
|
|
2943
|
+
|
|
2944
|
+
# Calculate output alpha
|
|
2945
|
+
alpha_out = alpha_overlay + alpha_base * (1 - alpha_overlay)
|
|
2946
|
+
|
|
2947
|
+
# Calculate output RGB
|
|
2948
|
+
rgb_out = np.zeros_like(base[:, :, :3])
|
|
2949
|
+
mask = alpha_out[:, :, 0] > 0
|
|
2950
|
+
|
|
2951
|
+
rgb_out[mask] = (overlay[mask, :3] * alpha_overlay[mask] +
|
|
2952
|
+
base[mask, :3] * alpha_base[mask] * (1 - alpha_overlay[mask])) / alpha_out[mask]
|
|
2953
|
+
|
|
2954
|
+
# Combine RGB and alpha
|
|
2955
|
+
result = np.zeros_like(base)
|
|
2956
|
+
result[:, :, :3] = rgb_out
|
|
2957
|
+
result[:, :, 3:4] = alpha_out
|
|
2958
|
+
|
|
2959
|
+
return result
|
|
2960
|
+
|
|
2961
|
+
def update_display_pan_mode(self):
|
|
2962
|
+
"""Lightweight display update for pan preview mode"""
|
|
2963
|
+
if self.is_pan_preview and self.pan_background_image is not None:
|
|
2964
|
+
# Clear and setup axes
|
|
2965
|
+
self.ax.clear()
|
|
2966
|
+
self.ax.set_facecolor('black')
|
|
2967
|
+
|
|
2968
|
+
# Get dimensions
|
|
2969
|
+
height, width = self.pan_background_image.shape[:2]
|
|
2970
|
+
|
|
2971
|
+
# Display the composite background with preserved zoom
|
|
2972
|
+
self.ax.imshow(self.pan_background_image,
|
|
2973
|
+
extent=(-0.5, width-0.5, height-0.5, -0.5),
|
|
2974
|
+
aspect='equal')
|
|
2975
|
+
|
|
2976
|
+
# Restore the zoom state from when pan began
|
|
2977
|
+
if hasattr(self, 'pan_zoom_state'):
|
|
2978
|
+
self.ax.set_xlim(self.pan_zoom_state[0])
|
|
2979
|
+
self.ax.set_ylim(self.pan_zoom_state[1])
|
|
2980
|
+
|
|
2981
|
+
# Style the axes (same as update_display)
|
|
2982
|
+
self.ax.set_xlabel('X')
|
|
2983
|
+
self.ax.set_ylabel('Y')
|
|
2984
|
+
self.ax.set_title(f'Slice {self.current_slice}')
|
|
2985
|
+
self.ax.xaxis.label.set_color('black')
|
|
2986
|
+
self.ax.yaxis.label.set_color('black')
|
|
2987
|
+
self.ax.title.set_color('black')
|
|
2988
|
+
self.ax.tick_params(colors='black')
|
|
2989
|
+
for spine in self.ax.spines.values():
|
|
2990
|
+
spine.set_color('black')
|
|
2991
|
+
|
|
2992
|
+
# Add measurement points if they exist (same as update_display)
|
|
2993
|
+
for point in self.measurement_points:
|
|
2994
|
+
x1, y1, z1 = point['point1']
|
|
2995
|
+
x2, y2, z2 = point['point2']
|
|
2996
|
+
pair_idx = point['pair_index']
|
|
2997
|
+
|
|
2998
|
+
if z1 == self.current_slice:
|
|
2999
|
+
self.ax.plot(x1, y1, 'yo', markersize=8)
|
|
3000
|
+
self.ax.text(x1, y1+5, str(pair_idx),
|
|
3001
|
+
color='white', ha='center', va='bottom')
|
|
3002
|
+
if z2 == self.current_slice:
|
|
3003
|
+
self.ax.plot(x2, y2, 'yo', markersize=8)
|
|
3004
|
+
self.ax.text(x2, y2+5, str(pair_idx),
|
|
3005
|
+
color='white', ha='center', va='bottom')
|
|
3006
|
+
|
|
3007
|
+
if z1 == z2 == self.current_slice:
|
|
3008
|
+
self.ax.plot([x1, x2], [y1, y2], 'r--', alpha=0.5)
|
|
3009
|
+
|
|
3010
|
+
self.canvas.draw_idle()
|
|
2523
3011
|
|
|
2524
3012
|
def on_mouse_release(self, event):
|
|
2525
|
-
"""Handle mouse release events."""
|
|
3013
|
+
"""Handle mouse release events - OPTIMIZED VERSION."""
|
|
2526
3014
|
if self.pan_mode:
|
|
3015
|
+
# Get current view limits before restoring channels
|
|
3016
|
+
current_xlim = self.ax.get_xlim()
|
|
3017
|
+
current_ylim = self.ax.get_ylim()
|
|
3018
|
+
|
|
2527
3019
|
self.panning = False
|
|
2528
3020
|
self.pan_start = None
|
|
2529
3021
|
self.canvas.setCursor(Qt.CursorShape.OpenHandCursor)
|
|
3022
|
+
|
|
3023
|
+
# Restore previously visible channels
|
|
3024
|
+
self.channel_visible = self.pre_pan_channel_state.copy()
|
|
3025
|
+
self.is_pan_preview = False
|
|
3026
|
+
self.pan_background = None
|
|
3027
|
+
|
|
3028
|
+
# Update display with preserved zoom at new position
|
|
3029
|
+
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
2530
3030
|
elif event.button == 1: # Left button release
|
|
2531
3031
|
if self.selecting and self.selection_rect is not None:
|
|
2532
3032
|
# Get the rectangle bounds
|
|
@@ -2585,18 +3085,15 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2585
3085
|
# Try to highlight the last selected value in tables
|
|
2586
3086
|
if self.clicked_values['edges']:
|
|
2587
3087
|
self.highlight_value_in_tables(self.clicked_values['edges'][-1])
|
|
2588
|
-
|
|
2589
3088
|
|
|
2590
3089
|
elif not self.selecting and self.selection_start: # If we had a click but never started selection
|
|
2591
3090
|
# Handle as a normal click
|
|
2592
3091
|
self.on_mouse_click(event)
|
|
2593
|
-
|
|
2594
3092
|
|
|
2595
|
-
# Clean up
|
|
3093
|
+
# Clean up selection
|
|
2596
3094
|
self.selection_start = None
|
|
2597
3095
|
self.selecting = False
|
|
2598
3096
|
|
|
2599
|
-
|
|
2600
3097
|
if self.selection_rect is not None:
|
|
2601
3098
|
try:
|
|
2602
3099
|
self.selection_rect.remove()
|
|
@@ -2605,13 +3102,28 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2605
3102
|
self.selection_rect = None
|
|
2606
3103
|
self.canvas.draw()
|
|
2607
3104
|
|
|
2608
|
-
|
|
3105
|
+
# OPTIMIZED: Handle brush mode cleanup with paint session management
|
|
3106
|
+
if self.brush_mode and hasattr(self, 'painting') and self.painting:
|
|
2609
3107
|
self.painting = False
|
|
3108
|
+
|
|
3109
|
+
# Restore hidden channels
|
|
2610
3110
|
try:
|
|
2611
3111
|
for i in self.restore_channels:
|
|
2612
3112
|
self.channel_visible[i] = True
|
|
3113
|
+
self.restore_channels = []
|
|
2613
3114
|
except:
|
|
2614
3115
|
pass
|
|
3116
|
+
|
|
3117
|
+
# OPTIMIZED: End paint session and ensure all operations complete
|
|
3118
|
+
self.end_paint_session()
|
|
3119
|
+
|
|
3120
|
+
# OPTIMIZED: Stop timer and process any pending paint operations
|
|
3121
|
+
if hasattr(self, 'paint_timer'):
|
|
3122
|
+
self.paint_timer.stop()
|
|
3123
|
+
if hasattr(self, 'pending_paint_update') and self.pending_paint_update:
|
|
3124
|
+
self.flush_paint_updates()
|
|
3125
|
+
|
|
3126
|
+
# Get current zoom and do final display update
|
|
2615
3127
|
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
2616
3128
|
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
2617
3129
|
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
@@ -2886,6 +3398,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2886
3398
|
load_action.triggered.connect(lambda: self.load_misc('Edge Centroids'))
|
|
2887
3399
|
load_action = misc_menu.addAction("Merge Nodes")
|
|
2888
3400
|
load_action.triggered.connect(lambda: self.load_misc('Merge Nodes'))
|
|
3401
|
+
load_action = misc_menu.addAction("Merge Node IDs from Images")
|
|
3402
|
+
load_action.triggered.connect(self.show_merge_node_id_dialog)
|
|
2889
3403
|
|
|
2890
3404
|
|
|
2891
3405
|
# Analysis menu
|
|
@@ -2918,6 +3432,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2918
3432
|
ripley_action.triggered.connect(self.show_ripley_dialog)
|
|
2919
3433
|
heatmap_action = stats_menu.addAction("Community Cluster Heatmap")
|
|
2920
3434
|
heatmap_action.triggered.connect(self.show_heatmap_dialog)
|
|
3435
|
+
nearneigh_action = stats_menu.addAction("Average Nearest Neighbors")
|
|
3436
|
+
nearneigh_action.triggered.connect(self.show_nearneigh_dialog)
|
|
2921
3437
|
vol_action = stats_menu.addAction("Calculate Volumes")
|
|
2922
3438
|
vol_action.triggered.connect(self.volumes)
|
|
2923
3439
|
rad_action = stats_menu.addAction("Calculate Radii")
|
|
@@ -3267,6 +3783,11 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3267
3783
|
except:
|
|
3268
3784
|
pass
|
|
3269
3785
|
|
|
3786
|
+
def show_merge_node_id_dialog(self):
|
|
3787
|
+
|
|
3788
|
+
dialog = MergeNodeIdDialog(self)
|
|
3789
|
+
dialog.exec()
|
|
3790
|
+
|
|
3270
3791
|
|
|
3271
3792
|
def show_watershed_dialog(self):
|
|
3272
3793
|
"""Show the watershed parameter dialog."""
|
|
@@ -3585,7 +4106,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3585
4106
|
f"Failed to load {sort}: {str(e)}"
|
|
3586
4107
|
)
|
|
3587
4108
|
|
|
3588
|
-
|
|
4109
|
+
elif sort == 'Merge Nodes':
|
|
3589
4110
|
try:
|
|
3590
4111
|
|
|
3591
4112
|
if len(np.unique(my_network.nodes)) < 3:
|
|
@@ -3623,7 +4144,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3623
4144
|
if dialog.exec() == QFileDialog.DialogCode.Accepted:
|
|
3624
4145
|
selected_path = dialog.directory().absolutePath()
|
|
3625
4146
|
|
|
3626
|
-
my_network.merge_nodes(selected_path)
|
|
4147
|
+
my_network.merge_nodes(selected_path, root_id = self.node_name)
|
|
3627
4148
|
self.load_channel(0, my_network.nodes, True)
|
|
3628
4149
|
|
|
3629
4150
|
|
|
@@ -3641,6 +4162,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3641
4162
|
)
|
|
3642
4163
|
|
|
3643
4164
|
|
|
4165
|
+
|
|
3644
4166
|
# Modify load_from_network_obj method
|
|
3645
4167
|
def load_from_network_obj(self):
|
|
3646
4168
|
try:
|
|
@@ -3952,11 +4474,15 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3952
4474
|
return
|
|
3953
4475
|
|
|
3954
4476
|
file_extension = filename.lower().split('.')[-1]
|
|
4477
|
+
|
|
4478
|
+
if channel_index == 0:
|
|
4479
|
+
self.node_name = filename
|
|
3955
4480
|
|
|
3956
4481
|
try:
|
|
3957
4482
|
if file_extension in ['tif', 'tiff']:
|
|
3958
4483
|
import tifffile
|
|
3959
4484
|
self.channel_data[channel_index] = tifffile.imread(filename)
|
|
4485
|
+
|
|
3960
4486
|
|
|
3961
4487
|
elif file_extension == 'nii':
|
|
3962
4488
|
import nibabel as nib
|
|
@@ -4094,6 +4620,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4094
4620
|
|
|
4095
4621
|
self.shape = self.channel_data[channel_index].shape
|
|
4096
4622
|
|
|
4623
|
+
self.img_height, self.img_width = self.shape[1], self.shape[2]
|
|
4624
|
+
|
|
4625
|
+
|
|
4097
4626
|
self.update_display(reset_resize = reset_resize)
|
|
4098
4627
|
|
|
4099
4628
|
|
|
@@ -4554,36 +5083,55 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4554
5083
|
import traceback
|
|
4555
5084
|
print(traceback.format_exc())
|
|
4556
5085
|
|
|
4557
|
-
def
|
|
4558
|
-
"""Ultra minimal update that only changes the paint channel's data"""
|
|
5086
|
+
def update_display_slice_optimized(self, channel, preserve_zoom=None):
|
|
5087
|
+
"""Ultra minimal update that only changes the paint channel's data - OPTIMIZED VERSION"""
|
|
4559
5088
|
if not self.channel_visible[channel]:
|
|
4560
5089
|
return
|
|
4561
|
-
|
|
5090
|
+
|
|
4562
5091
|
if preserve_zoom:
|
|
4563
5092
|
current_xlim, current_ylim = preserve_zoom
|
|
4564
5093
|
if current_xlim is not None and current_ylim is not None:
|
|
4565
5094
|
self.ax.set_xlim(current_xlim)
|
|
4566
5095
|
self.ax.set_ylim(current_ylim)
|
|
4567
|
-
|
|
4568
|
-
|
|
5096
|
+
|
|
4569
5097
|
# Find the existing image for channel (paint channel)
|
|
4570
5098
|
channel_image = None
|
|
4571
5099
|
for img in self.ax.images:
|
|
4572
5100
|
if img.cmap.name == f'custom_{channel}':
|
|
4573
5101
|
channel_image = img
|
|
4574
5102
|
break
|
|
4575
|
-
|
|
5103
|
+
|
|
4576
5104
|
if channel_image is not None:
|
|
4577
|
-
# Update the data of the existing image
|
|
4578
|
-
|
|
5105
|
+
# Update the data of the existing image with thread safety
|
|
5106
|
+
with self.paint_lock:
|
|
5107
|
+
channel_image.set_array(self.channel_data[channel][self.current_slice])
|
|
4579
5108
|
|
|
4580
5109
|
# Restore the static background (all other channels) at current zoom level
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
5110
|
+
# This is the key - use static_background from update_display, not paint_background
|
|
5111
|
+
if hasattr(self, 'static_background') and self.static_background is not None:
|
|
5112
|
+
self.canvas.restore_region(self.static_background)
|
|
5113
|
+
# Draw just our paint channel
|
|
5114
|
+
self.ax.draw_artist(channel_image)
|
|
5115
|
+
# Blit everything
|
|
5116
|
+
self.canvas.blit(self.ax.bbox)
|
|
5117
|
+
self.canvas.flush_events()
|
|
5118
|
+
else:
|
|
5119
|
+
# Fallback to full draw if no static background
|
|
5120
|
+
self.canvas.draw()
|
|
5121
|
+
else:
|
|
5122
|
+
# Fallback if channel image not found
|
|
5123
|
+
self.canvas.draw()
|
|
5124
|
+
|
|
5125
|
+
def get_channel_image(self, channel):
|
|
5126
|
+
"""Find the matplotlib image object for a specific channel."""
|
|
5127
|
+
if not hasattr(self.ax, 'images'):
|
|
5128
|
+
return None
|
|
5129
|
+
|
|
5130
|
+
for img in self.ax.images:
|
|
5131
|
+
if hasattr(img, 'cmap') and hasattr(img.cmap, 'name'):
|
|
5132
|
+
if img.cmap.name == f'custom_{channel}':
|
|
5133
|
+
return img
|
|
5134
|
+
return None
|
|
4587
5135
|
|
|
4588
5136
|
def show_netshow_dialog(self):
|
|
4589
5137
|
dialog = NetShowDialog(self)
|
|
@@ -4666,6 +5214,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4666
5214
|
dialog = HeatmapDialog(self)
|
|
4667
5215
|
dialog.exec()
|
|
4668
5216
|
|
|
5217
|
+
def show_nearneigh_dialog(self):
|
|
5218
|
+
dialog = NearNeighDialog(self)
|
|
5219
|
+
dialog.exec()
|
|
5220
|
+
|
|
4669
5221
|
def show_random_dialog(self):
|
|
4670
5222
|
dialog = RandomDialog(self)
|
|
4671
5223
|
dialog.exec()
|
|
@@ -5480,6 +6032,10 @@ class PropertiesDialog(QDialog):
|
|
|
5480
6032
|
run_button.clicked.connect(self.run_properties)
|
|
5481
6033
|
layout.addWidget(run_button)
|
|
5482
6034
|
|
|
6035
|
+
report_button = QPushButton("Report Properties (Show in Top Right Tables)")
|
|
6036
|
+
report_button.clicked.connect(self.report)
|
|
6037
|
+
layout.addWidget(report_button)
|
|
6038
|
+
|
|
5483
6039
|
def check_checked(self, ques):
|
|
5484
6040
|
|
|
5485
6041
|
if ques is None:
|
|
@@ -5517,11 +6073,35 @@ class PropertiesDialog(QDialog):
|
|
|
5517
6073
|
except Exception as e:
|
|
5518
6074
|
print(f"Error: {e}")
|
|
5519
6075
|
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
6076
|
+
def report(self):
|
|
6077
|
+
|
|
6078
|
+
try:
|
|
6079
|
+
|
|
6080
|
+
self.parent().format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity', 'Node Identities')
|
|
6081
|
+
except:
|
|
6082
|
+
pass
|
|
6083
|
+
try:
|
|
6084
|
+
|
|
6085
|
+
self.parent().format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
|
|
6086
|
+
except:
|
|
6087
|
+
pass
|
|
6088
|
+
|
|
6089
|
+
try:
|
|
6090
|
+
self.parent().format_for_upperright_table(my_network.edge_centroids, 'EdgeID', ['Z', 'Y', 'X'], 'Edge Centroids')
|
|
6091
|
+
except:
|
|
6092
|
+
pass
|
|
6093
|
+
try:
|
|
6094
|
+
self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'Community', 'Node Communities')
|
|
6095
|
+
except:
|
|
6096
|
+
pass
|
|
6097
|
+
|
|
6098
|
+
|
|
6099
|
+
|
|
6100
|
+
class BrightnessContrastDialog(QDialog):
|
|
6101
|
+
def __init__(self, parent=None):
|
|
6102
|
+
super().__init__(parent)
|
|
6103
|
+
self.setWindowTitle("Brightness/Contrast Controls")
|
|
6104
|
+
self.setModal(False) # Allows interaction with main window while open
|
|
5525
6105
|
|
|
5526
6106
|
layout = QVBoxLayout(self)
|
|
5527
6107
|
|
|
@@ -5896,6 +6476,72 @@ class ArbitraryDialog(QDialog):
|
|
|
5896
6476
|
except Exception as e:
|
|
5897
6477
|
QMessageBox.critical(self, "Error", f"Error processing selections: {str(e)}")
|
|
5898
6478
|
|
|
6479
|
+
class MergeNodeIdDialog(QDialog):
|
|
6480
|
+
|
|
6481
|
+
def __init__(self, parent=None):
|
|
6482
|
+
super().__init__(parent)
|
|
6483
|
+
self.setWindowTitle("Merging Node Identities From Folder Dialog.\nNote that you should prelabel or prewatershed your current node objects before doing this. (See Process -> Image) It does not label them for you.")
|
|
6484
|
+
self.setModal(True)
|
|
6485
|
+
|
|
6486
|
+
layout = QFormLayout(self)
|
|
6487
|
+
|
|
6488
|
+
self.search = QLineEdit("")
|
|
6489
|
+
layout.addRow("Step-out distance (from current nodes image - ignore if you dilated them previously or don't want):", self.search)
|
|
6490
|
+
|
|
6491
|
+
self.xy_scale = QLineEdit(f"{my_network.xy_scale}")
|
|
6492
|
+
layout.addRow("xy_scale:", self.xy_scale)
|
|
6493
|
+
|
|
6494
|
+
self.z_scale = QLineEdit(f"{my_network.z_scale}")
|
|
6495
|
+
layout.addRow("z_scale:", self.z_scale)
|
|
6496
|
+
|
|
6497
|
+
# Add Run button
|
|
6498
|
+
run_button = QPushButton("Get Directory")
|
|
6499
|
+
run_button.clicked.connect(self.run)
|
|
6500
|
+
layout.addWidget(run_button)
|
|
6501
|
+
|
|
6502
|
+
def run(self):
|
|
6503
|
+
|
|
6504
|
+
try:
|
|
6505
|
+
|
|
6506
|
+
search = float(self.search.text()) if self.search.text().strip() else 0
|
|
6507
|
+
xy_scale = float(self.xy_scale.text()) if self.xy_scale.text().strip() else 1
|
|
6508
|
+
z_scale = float(self.z_scale.text()) if self.z_scale.text().strip() else 1
|
|
6509
|
+
|
|
6510
|
+
|
|
6511
|
+
data = self.parent().channel_data[0]
|
|
6512
|
+
|
|
6513
|
+
if data is None:
|
|
6514
|
+
return
|
|
6515
|
+
|
|
6516
|
+
|
|
6517
|
+
|
|
6518
|
+
dialog = QFileDialog(self)
|
|
6519
|
+
dialog.setOption(QFileDialog.Option.DontUseNativeDialog)
|
|
6520
|
+
dialog.setOption(QFileDialog.Option.ReadOnly)
|
|
6521
|
+
dialog.setFileMode(QFileDialog.FileMode.Directory)
|
|
6522
|
+
dialog.setViewMode(QFileDialog.ViewMode.Detail)
|
|
6523
|
+
|
|
6524
|
+
if dialog.exec() == QFileDialog.DialogCode.Accepted:
|
|
6525
|
+
selected_path = dialog.directory().absolutePath()
|
|
6526
|
+
|
|
6527
|
+
if search > 0:
|
|
6528
|
+
data = sdl.smart_dilate(data, 1, 1, GPU = False, fast_dil = False, use_dt_dil_amount = search, xy_scale = xy_scale, z_scale = z_scale)
|
|
6529
|
+
|
|
6530
|
+
my_network.merge_node_ids(selected_path, data)
|
|
6531
|
+
|
|
6532
|
+
self.parent().format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity')
|
|
6533
|
+
|
|
6534
|
+
QMessageBox.critical(
|
|
6535
|
+
self,
|
|
6536
|
+
"Success",
|
|
6537
|
+
"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)"
|
|
6538
|
+
)
|
|
6539
|
+
|
|
6540
|
+
self.accept()
|
|
6541
|
+
|
|
6542
|
+
except Exception as e:
|
|
6543
|
+
print(f"Error: {e}")
|
|
6544
|
+
|
|
5899
6545
|
|
|
5900
6546
|
class Show3dDialog(QDialog):
|
|
5901
6547
|
def __init__(self, parent=None):
|
|
@@ -6629,6 +7275,174 @@ class DegreeDistDialog(QDialog):
|
|
|
6629
7275
|
except Exception as e:
|
|
6630
7276
|
print(f"An error occurred: {e}")
|
|
6631
7277
|
|
|
7278
|
+
class NearNeighDialog(QDialog):
|
|
7279
|
+
def __init__(self, parent=None):
|
|
7280
|
+
super().__init__(parent)
|
|
7281
|
+
self.setWindowTitle(f"Nearest Neighborhood Averages (Using Centroids)")
|
|
7282
|
+
self.setModal(True)
|
|
7283
|
+
|
|
7284
|
+
# Main layout
|
|
7285
|
+
main_layout = QVBoxLayout(self)
|
|
7286
|
+
|
|
7287
|
+
# Identities group box (only if node_identities exists)
|
|
7288
|
+
identities_group = QGroupBox("Identities")
|
|
7289
|
+
identities_layout = QFormLayout(identities_group)
|
|
7290
|
+
|
|
7291
|
+
if my_network.node_identities is not None:
|
|
7292
|
+
|
|
7293
|
+
self.root = QComboBox()
|
|
7294
|
+
self.root.addItems(list(set(my_network.node_identities.values())))
|
|
7295
|
+
self.root.setCurrentIndex(0)
|
|
7296
|
+
identities_layout.addRow("Root Identity to Search for Neighbor's IDs?", self.root)
|
|
7297
|
+
|
|
7298
|
+
self.targ = QComboBox()
|
|
7299
|
+
neighs = list(set(my_network.node_identities.values()))
|
|
7300
|
+
neighs.append("All Others (Excluding Self)")
|
|
7301
|
+
self.targ.addItems(neighs)
|
|
7302
|
+
self.targ.setCurrentIndex(0)
|
|
7303
|
+
identities_layout.addRow("Neighbor Identities to Search For?", self.targ)
|
|
7304
|
+
else:
|
|
7305
|
+
self.root = None
|
|
7306
|
+
self.targ = None
|
|
7307
|
+
|
|
7308
|
+
self.num = QLineEdit("1")
|
|
7309
|
+
identities_layout.addRow("Number of Nearest Neighbors to Evaluate Per Node?:", self.num)
|
|
7310
|
+
|
|
7311
|
+
|
|
7312
|
+
main_layout.addWidget(identities_group)
|
|
7313
|
+
|
|
7314
|
+
|
|
7315
|
+
# Optional Heatmap group box
|
|
7316
|
+
heatmap_group = QGroupBox("Optional Heatmap")
|
|
7317
|
+
heatmap_layout = QFormLayout(heatmap_group)
|
|
7318
|
+
|
|
7319
|
+
self.map = QPushButton("(If getting distribution): Generate Heatmap?")
|
|
7320
|
+
self.map.setCheckable(True)
|
|
7321
|
+
self.map.setChecked(False)
|
|
7322
|
+
heatmap_layout.addRow("Heatmap:", self.map)
|
|
7323
|
+
|
|
7324
|
+
self.threed = QPushButton("(For above): Return 3D map? (uncheck for 2D): ")
|
|
7325
|
+
self.threed.setCheckable(True)
|
|
7326
|
+
self.threed.setChecked(True)
|
|
7327
|
+
heatmap_layout.addRow("3D:", self.threed)
|
|
7328
|
+
|
|
7329
|
+
self.numpy = QPushButton("(For heatmap): Return image overlay instead of graph? (Goes in Overlay 2): ")
|
|
7330
|
+
self.numpy.setCheckable(True)
|
|
7331
|
+
self.numpy.setChecked(False)
|
|
7332
|
+
self.numpy.clicked.connect(self.toggle_map)
|
|
7333
|
+
heatmap_layout.addRow("Overlay:", self.numpy)
|
|
7334
|
+
|
|
7335
|
+
main_layout.addWidget(heatmap_group)
|
|
7336
|
+
|
|
7337
|
+
# Get Distribution group box
|
|
7338
|
+
distribution_group = QGroupBox("Get Distribution")
|
|
7339
|
+
distribution_layout = QVBoxLayout(distribution_group)
|
|
7340
|
+
|
|
7341
|
+
run_button = QPushButton("Get Average Nearest Neighbor (Plus Distribution)")
|
|
7342
|
+
run_button.clicked.connect(self.run)
|
|
7343
|
+
distribution_layout.addWidget(run_button)
|
|
7344
|
+
|
|
7345
|
+
main_layout.addWidget(distribution_group)
|
|
7346
|
+
|
|
7347
|
+
# Get All Averages group box (only if node_identities exists)
|
|
7348
|
+
if my_network.node_identities is not None:
|
|
7349
|
+
averages_group = QGroupBox("Get All Averages")
|
|
7350
|
+
averages_layout = QVBoxLayout(averages_group)
|
|
7351
|
+
|
|
7352
|
+
run_button2 = QPushButton("Get Average Nearest All ID Combinations (No Distribution, No Heatmap)")
|
|
7353
|
+
run_button2.clicked.connect(self.run2)
|
|
7354
|
+
averages_layout.addWidget(run_button2)
|
|
7355
|
+
|
|
7356
|
+
main_layout.addWidget(averages_group)
|
|
7357
|
+
|
|
7358
|
+
def toggle_map(self):
|
|
7359
|
+
|
|
7360
|
+
if self.numpy.isChecked():
|
|
7361
|
+
|
|
7362
|
+
if not self.map.isChecked():
|
|
7363
|
+
|
|
7364
|
+
self.map.click()
|
|
7365
|
+
|
|
7366
|
+
def run(self):
|
|
7367
|
+
|
|
7368
|
+
try:
|
|
7369
|
+
|
|
7370
|
+
try:
|
|
7371
|
+
root = self.root.currentText()
|
|
7372
|
+
except:
|
|
7373
|
+
root = None
|
|
7374
|
+
try:
|
|
7375
|
+
targ = self.targ.currentText()
|
|
7376
|
+
except:
|
|
7377
|
+
targ = None
|
|
7378
|
+
|
|
7379
|
+
heatmap = self.map.isChecked()
|
|
7380
|
+
threed = self.threed.isChecked()
|
|
7381
|
+
numpy = self.numpy.isChecked()
|
|
7382
|
+
num = int(self.num.text()) if self.num.text().strip() else 1
|
|
7383
|
+
|
|
7384
|
+
if root is not None and targ is not None:
|
|
7385
|
+
title = f"Nearest {num} Neighbor(s) Distance of {targ} from {root}"
|
|
7386
|
+
header = f"Shortest Distance to Closest {num} {targ}(s)"
|
|
7387
|
+
header2 = f"{root} Node ID"
|
|
7388
|
+
else:
|
|
7389
|
+
title = f"Nearest {num} Neighbor(s) Distance Between Nodes"
|
|
7390
|
+
header = f"Shortest Distance to Closest {num} Nodes"
|
|
7391
|
+
header2 = "Root Node ID"
|
|
7392
|
+
|
|
7393
|
+
if my_network.node_centroids is None:
|
|
7394
|
+
self.parent().show_centroid_dialog()
|
|
7395
|
+
if my_network.node_centroids is None:
|
|
7396
|
+
return
|
|
7397
|
+
|
|
7398
|
+
if not numpy:
|
|
7399
|
+
avg, output = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, heatmap = heatmap, threed = threed)
|
|
7400
|
+
else:
|
|
7401
|
+
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)
|
|
7402
|
+
self.parent().load_channel(3, overlay, data = True)
|
|
7403
|
+
|
|
7404
|
+
self.parent().format_for_upperright_table([avg], metric = f'Avg {title}', title = f'Avg {title}')
|
|
7405
|
+
self.parent().format_for_upperright_table(output, header2, header, title = title)
|
|
7406
|
+
|
|
7407
|
+
self.accept()
|
|
7408
|
+
|
|
7409
|
+
except Exception as e:
|
|
7410
|
+
import traceback
|
|
7411
|
+
print(traceback.format_exc())
|
|
7412
|
+
|
|
7413
|
+
print(f"Error: {e}")
|
|
7414
|
+
|
|
7415
|
+
def run2(self):
|
|
7416
|
+
|
|
7417
|
+
try:
|
|
7418
|
+
|
|
7419
|
+
available = list(set(my_network.node_identities.values()))
|
|
7420
|
+
|
|
7421
|
+
num = int(self.num.text()) if self.num.text().strip() else 1
|
|
7422
|
+
|
|
7423
|
+
output_dict = {}
|
|
7424
|
+
|
|
7425
|
+
while len(available) > 1:
|
|
7426
|
+
|
|
7427
|
+
root = available[0]
|
|
7428
|
+
|
|
7429
|
+
for targ in available:
|
|
7430
|
+
|
|
7431
|
+
avg, _ = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num)
|
|
7432
|
+
|
|
7433
|
+
output_dict[f"{root} vs {targ}"] = avg
|
|
7434
|
+
|
|
7435
|
+
del available[0]
|
|
7436
|
+
|
|
7437
|
+
self.parent().format_for_upperright_table(output_dict, "ID Combo", "Avg Distance to Nearest", title = "Average Distance to Nearest Neighbors for All ID Combos")
|
|
7438
|
+
|
|
7439
|
+
self.accept()
|
|
7440
|
+
|
|
7441
|
+
except Exception as e:
|
|
7442
|
+
|
|
7443
|
+
print(f"Error: {e}")
|
|
7444
|
+
|
|
7445
|
+
|
|
6632
7446
|
class NeighborIdentityDialog(QDialog):
|
|
6633
7447
|
|
|
6634
7448
|
def __init__(self, parent=None):
|
|
@@ -6815,8 +7629,7 @@ class RipleyDialog(QDialog):
|
|
|
6815
7629
|
"Error:",
|
|
6816
7630
|
f"Failed to preform cluster analysis: {str(e)}"
|
|
6817
7631
|
)
|
|
6818
|
-
|
|
6819
|
-
print(traceback.format_exc())
|
|
7632
|
+
|
|
6820
7633
|
print(f"Error: {e}")
|
|
6821
7634
|
|
|
6822
7635
|
class HeatmapDialog(QDialog):
|
|
@@ -6839,6 +7652,11 @@ class HeatmapDialog(QDialog):
|
|
|
6839
7652
|
self.is3d.setChecked(True)
|
|
6840
7653
|
layout.addRow("Use 3D Plot (uncheck for 2D)?:", self.is3d)
|
|
6841
7654
|
|
|
7655
|
+
self.numpy = QPushButton("(For heatmap): Return image overlay instead of graph? (Goes in Overlay 2): ")
|
|
7656
|
+
self.numpy.setCheckable(True)
|
|
7657
|
+
self.numpy.setChecked(False)
|
|
7658
|
+
layout.addRow("Overlay:", self.numpy)
|
|
7659
|
+
|
|
6842
7660
|
|
|
6843
7661
|
# Add Run button
|
|
6844
7662
|
run_button = QPushButton("Run")
|
|
@@ -6847,25 +7665,40 @@ class HeatmapDialog(QDialog):
|
|
|
6847
7665
|
|
|
6848
7666
|
def run(self):
|
|
6849
7667
|
|
|
6850
|
-
|
|
7668
|
+
try:
|
|
6851
7669
|
|
|
6852
|
-
|
|
7670
|
+
nodecount = int(self.nodecount.text()) if self.nodecount.text().strip() else None
|
|
7671
|
+
|
|
7672
|
+
is3d = self.is3d.isChecked()
|
|
6853
7673
|
|
|
6854
7674
|
|
|
6855
|
-
if my_network.communities is None:
|
|
6856
|
-
if my_network.network is not None:
|
|
6857
|
-
self.parent().show_partition_dialog()
|
|
6858
|
-
else:
|
|
6859
|
-
self.parent().handle_com_cell()
|
|
6860
7675
|
if my_network.communities is None:
|
|
6861
|
-
|
|
7676
|
+
if my_network.network is not None:
|
|
7677
|
+
self.parent().show_partition_dialog()
|
|
7678
|
+
else:
|
|
7679
|
+
self.parent().handle_com_cell()
|
|
7680
|
+
if my_network.communities is None:
|
|
7681
|
+
return
|
|
6862
7682
|
|
|
6863
|
-
|
|
7683
|
+
numpy = self.numpy.isChecked()
|
|
6864
7684
|
|
|
6865
|
-
|
|
7685
|
+
if not numpy:
|
|
6866
7686
|
|
|
6867
|
-
|
|
7687
|
+
heat_dict = my_network.community_heatmap(num_nodes = nodecount, is3d = is3d)
|
|
7688
|
+
|
|
7689
|
+
else:
|
|
6868
7690
|
|
|
7691
|
+
heat_dict, overlay = my_network.community_heatmap(num_nodes = nodecount, is3d = is3d, numpy = True)
|
|
7692
|
+
self.parent().load_channel(3, overlay, data = True)
|
|
7693
|
+
|
|
7694
|
+
|
|
7695
|
+
self.parent().format_for_upperright_table(heat_dict, metric='Community', value='ln(Predicted Community Nodecount/Actual)', title="Community Heatmap")
|
|
7696
|
+
|
|
7697
|
+
self.accept()
|
|
7698
|
+
|
|
7699
|
+
except Exception as e:
|
|
7700
|
+
|
|
7701
|
+
print(f"Error: {e}")
|
|
6869
7702
|
|
|
6870
7703
|
|
|
6871
7704
|
|
|
@@ -7018,6 +7851,10 @@ class RadDialog(QDialog):
|
|
|
7018
7851
|
self.parent().radii_dict[0] = radii
|
|
7019
7852
|
elif self.parent().active_channel == 1:
|
|
7020
7853
|
self.parent().radii_dict[1] = radii
|
|
7854
|
+
elif self.parent().active_channel == 2:
|
|
7855
|
+
self.parent().radii_dict[2] = radii
|
|
7856
|
+
elif self.parent().active_channel == 3:
|
|
7857
|
+
self.parent().radii_dict[3] = radii
|
|
7021
7858
|
|
|
7022
7859
|
self.parent().format_for_upperright_table(radii, title = 'Largest Radii of Objects', metric='ObjectID', value='Largest Radius (Scaled)')
|
|
7023
7860
|
|
|
@@ -7105,7 +7942,7 @@ class DegreeDialog(QDialog):
|
|
|
7105
7942
|
|
|
7106
7943
|
# Add mode selection dropdown
|
|
7107
7944
|
self.mode_selector = QComboBox()
|
|
7108
|
-
self.mode_selector.addItems(["Just make table", "Draw degree of node as overlay (literally draws 1, 2, 3, etc... faster)", "Label nodes by degree (nodes will take on the value 1, 2, 3, etc, based on their degree, to export for array based analysis
|
|
7945
|
+
self.mode_selector.addItems(["Just make table", "Draw degree of node as overlay (literally draws 1, 2, 3, etc... faster)", "Label nodes by degree (nodes will take on the value 1, 2, 3, etc, based on their degree, to export for array based analysis)", "Create Heatmap of Degrees"])
|
|
7109
7946
|
self.mode_selector.setCurrentIndex(0) # Default to Mode 1
|
|
7110
7947
|
layout.addRow("Execution Mode:", self.mode_selector)
|
|
7111
7948
|
|
|
@@ -7126,6 +7963,14 @@ class DegreeDialog(QDialog):
|
|
|
7126
7963
|
|
|
7127
7964
|
accepted_mode = self.mode_selector.currentIndex()
|
|
7128
7965
|
|
|
7966
|
+
if accepted_mode == 3:
|
|
7967
|
+
degree_dict, overlay = my_network.get_degrees(heatmap = True)
|
|
7968
|
+
self.parent().format_for_upperright_table(degree_dict, 'Node ID', 'Degree', title = 'Degrees of nodes')
|
|
7969
|
+
self.parent().load_channel(3, channel_data = overlay, data = True)
|
|
7970
|
+
self.accept()
|
|
7971
|
+
return
|
|
7972
|
+
|
|
7973
|
+
|
|
7129
7974
|
try:
|
|
7130
7975
|
down_factor = float(self.down_factor.text()) if self.down_factor.text() else 1
|
|
7131
7976
|
except ValueError:
|
|
@@ -7964,7 +8809,7 @@ class ThresholdDialog(QDialog):
|
|
|
7964
8809
|
|
|
7965
8810
|
# Add mode selection dropdown
|
|
7966
8811
|
self.mode_selector = QComboBox()
|
|
7967
|
-
self.mode_selector.addItems(["Using Label/Brightness", "Using Volumes"])
|
|
8812
|
+
self.mode_selector.addItems(["Using Label/Brightness", "Using Volumes", "Using Radii", "Using Node Degree"])
|
|
7968
8813
|
self.mode_selector.setCurrentIndex(0) # Default to Mode 1
|
|
7969
8814
|
layout.addRow("Execution Mode:", self.mode_selector)
|
|
7970
8815
|
|
|
@@ -7997,6 +8842,22 @@ class ThresholdDialog(QDialog):
|
|
|
7997
8842
|
if self.parent().volume_dict[self.parent().active_channel] is None:
|
|
7998
8843
|
self.parent().volumes()
|
|
7999
8844
|
|
|
8845
|
+
elif accepted_mode == 2:
|
|
8846
|
+
if len(np.unique(self.parent().channel_data[self.parent().active_channel])) < 3:
|
|
8847
|
+
self.parent().show_label_dialog()
|
|
8848
|
+
|
|
8849
|
+
if self.parent().radii_dict[self.parent().active_channel] is None:
|
|
8850
|
+
self.parent().show_rad_dialog()
|
|
8851
|
+
|
|
8852
|
+
if self.parent().radii_dict[self.parent().active_channel] is None:
|
|
8853
|
+
return
|
|
8854
|
+
|
|
8855
|
+
elif accepted_mode == 3:
|
|
8856
|
+
|
|
8857
|
+
if my_network.nodes is None or my_network.network is None:
|
|
8858
|
+
print("Error - please calculate network first")
|
|
8859
|
+
return
|
|
8860
|
+
|
|
8000
8861
|
if self.parent().mini_overlay_data is not None:
|
|
8001
8862
|
self.parent().mini_overlay_data = None
|
|
8002
8863
|
|
|
@@ -8005,6 +8866,8 @@ class ThresholdDialog(QDialog):
|
|
|
8005
8866
|
self.highlight_overlay = None
|
|
8006
8867
|
self.accept()
|
|
8007
8868
|
except:
|
|
8869
|
+
import traceback
|
|
8870
|
+
traceback.print_exc()
|
|
8008
8871
|
pass
|
|
8009
8872
|
|
|
8010
8873
|
def start_ml(self, GPU = False):
|
|
@@ -8696,7 +9559,11 @@ class MachineWindow(QMainWindow):
|
|
|
8696
9559
|
|
|
8697
9560
|
print("Segmenting entire volume with model...")
|
|
8698
9561
|
#foreground_coords, background_coords = self.segmenter.segment_volume(array = self.parent().highlight_overlay)
|
|
8699
|
-
|
|
9562
|
+
try:
|
|
9563
|
+
self.parent().highlight_overlay = self.segmenter.segment_volume(array = self.parent().highlight_overlay)
|
|
9564
|
+
except Exception as e:
|
|
9565
|
+
print(f"Error segmenting (Perhaps retrain the model...): {e}")
|
|
9566
|
+
return
|
|
8700
9567
|
|
|
8701
9568
|
# Clean up when done
|
|
8702
9569
|
self.segmenter.cleanup()
|
|
@@ -8717,23 +9584,27 @@ class MachineWindow(QMainWindow):
|
|
|
8717
9584
|
print("Finished segmentation moved to Overlay 2. Use File -> Save(As) for disk saving.")
|
|
8718
9585
|
|
|
8719
9586
|
def closeEvent(self, event):
|
|
8720
|
-
|
|
8721
|
-
|
|
8722
|
-
|
|
8723
|
-
if self.
|
|
8724
|
-
|
|
8725
|
-
self.
|
|
8726
|
-
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
|
|
8734
|
-
|
|
8735
|
-
|
|
8736
|
-
|
|
9587
|
+
|
|
9588
|
+
try:
|
|
9589
|
+
if self.parent().isVisible():
|
|
9590
|
+
if self.confirm_close_dialog():
|
|
9591
|
+
# Clean up resources before closing
|
|
9592
|
+
if self.brush_button.isChecked():
|
|
9593
|
+
self.silence_button()
|
|
9594
|
+
self.toggle_brush_mode()
|
|
9595
|
+
|
|
9596
|
+
self.parent().pen_button.setEnabled(True)
|
|
9597
|
+
self.parent().brush_mode = False
|
|
9598
|
+
|
|
9599
|
+
# Kill the segmentation thread and wait for it to finish
|
|
9600
|
+
self.kill_segmentation()
|
|
9601
|
+
time.sleep(0.2) # Give additional time for cleanup
|
|
9602
|
+
|
|
9603
|
+
self.parent().machine_window = None
|
|
9604
|
+
else:
|
|
9605
|
+
event.ignore()
|
|
9606
|
+
except:
|
|
9607
|
+
pass
|
|
8737
9608
|
|
|
8738
9609
|
|
|
8739
9610
|
|
|
@@ -8846,6 +9717,8 @@ class ThresholdWindow(QMainWindow):
|
|
|
8846
9717
|
def __init__(self, parent=None, accepted_mode=0):
|
|
8847
9718
|
super().__init__(parent)
|
|
8848
9719
|
self.setWindowTitle("Threshold")
|
|
9720
|
+
|
|
9721
|
+
self.accepted_mode = accepted_mode
|
|
8849
9722
|
|
|
8850
9723
|
# Create central widget and layout
|
|
8851
9724
|
central_widget = QWidget()
|
|
@@ -8857,6 +9730,27 @@ class ThresholdWindow(QMainWindow):
|
|
|
8857
9730
|
self.histo_list = list(self.parent().volume_dict[self.parent().active_channel].values())
|
|
8858
9731
|
self.bounds = False
|
|
8859
9732
|
self.parent().bounds = False
|
|
9733
|
+
elif accepted_mode == 2:
|
|
9734
|
+
self.histo_list = list(self.parent().radii_dict[self.parent().active_channel].values())
|
|
9735
|
+
self.bounds = False
|
|
9736
|
+
self.parent().bounds = False
|
|
9737
|
+
elif accepted_mode == 3:
|
|
9738
|
+
self.parent().degree_dict = {}
|
|
9739
|
+
self.parent().set_active_channel(0)
|
|
9740
|
+
nodes = list(my_network.network.nodes())
|
|
9741
|
+
img_nodes = list(np.unique(my_network.nodes))
|
|
9742
|
+
if 0 in img_nodes:
|
|
9743
|
+
del img_nodes[0]
|
|
9744
|
+
for node in img_nodes:
|
|
9745
|
+
if node in nodes:
|
|
9746
|
+
self.parent().degree_dict[int(node)] = my_network.network.degree(node)
|
|
9747
|
+
else:
|
|
9748
|
+
self.parent().degree_dict[int(node)] = 0
|
|
9749
|
+
|
|
9750
|
+
self.histo_list = list(self.parent().degree_dict.values())
|
|
9751
|
+
self.bounds = False
|
|
9752
|
+
self.parent().bounds = False
|
|
9753
|
+
|
|
8860
9754
|
elif accepted_mode == 0:
|
|
8861
9755
|
targ_shape = self.parent().channel_data[self.parent().active_channel].shape
|
|
8862
9756
|
if (targ_shape[0] + targ_shape[1] + targ_shape[2]) > 2500: #Take a simpler histogram on big arrays
|
|
@@ -8976,17 +9870,35 @@ class ThresholdWindow(QMainWindow):
|
|
|
8976
9870
|
|
|
8977
9871
|
def get_values_in_range_all_vols(self, chan, min_val, max_val):
|
|
8978
9872
|
output = []
|
|
8979
|
-
|
|
8980
|
-
|
|
8981
|
-
|
|
9873
|
+
if self.accepted_mode == 1:
|
|
9874
|
+
for node, vol in self.parent().volume_dict[chan].items():
|
|
9875
|
+
if min_val <= vol <= max_val:
|
|
9876
|
+
output.append(node)
|
|
9877
|
+
elif self.accepted_mode == 2:
|
|
9878
|
+
for node, vol in self.parent().radii_dict[chan].items():
|
|
9879
|
+
if min_val <= vol <= max_val:
|
|
9880
|
+
output.append(node)
|
|
9881
|
+
elif self.accepted_mode == 3:
|
|
9882
|
+
for node, vol in self.parent().degree_dict.items():
|
|
9883
|
+
if min_val <= vol <= max_val:
|
|
9884
|
+
output.append(node)
|
|
8982
9885
|
return output
|
|
8983
9886
|
|
|
8984
9887
|
def get_values_in_range(self, lst, min_val, max_val):
|
|
8985
9888
|
values = [x for x in lst if min_val <= x <= max_val]
|
|
8986
9889
|
output = []
|
|
8987
|
-
|
|
8988
|
-
|
|
8989
|
-
|
|
9890
|
+
if self.accepted_mode == 1:
|
|
9891
|
+
for item in self.parent().volume_dict[self.parent().active_channel]:
|
|
9892
|
+
if self.parent().volume_dict[self.parent().active_channel][item] in values:
|
|
9893
|
+
output.append(item)
|
|
9894
|
+
elif self.accepted_mode == 2:
|
|
9895
|
+
for item in self.parent().radii_dict[self.parent().active_channel]:
|
|
9896
|
+
if self.parent().radii_dict[self.parent().active_channel][item] in values:
|
|
9897
|
+
output.append(item)
|
|
9898
|
+
elif self.accepted_mode == 3:
|
|
9899
|
+
for item in self.parent().degree_dict:
|
|
9900
|
+
if self.parent().degree_dict[item] in values:
|
|
9901
|
+
output.append(item)
|
|
8990
9902
|
return output
|
|
8991
9903
|
|
|
8992
9904
|
|
|
@@ -9421,15 +10333,20 @@ class HoleDialog(QDialog):
|
|
|
9421
10333
|
# auto checkbox (default True)
|
|
9422
10334
|
self.headon = QPushButton("Head-on")
|
|
9423
10335
|
self.headon.setCheckable(True)
|
|
9424
|
-
self.headon.setChecked(
|
|
10336
|
+
self.headon.setChecked(True)
|
|
9425
10337
|
layout.addRow("Only Use 2D Slicing Dimension:", self.headon)
|
|
9426
10338
|
|
|
9427
10339
|
# auto checkbox (default True)
|
|
9428
10340
|
self.borders = QPushButton("Borders")
|
|
9429
10341
|
self.borders.setCheckable(True)
|
|
9430
|
-
self.borders.setChecked(
|
|
10342
|
+
self.borders.setChecked(False)
|
|
9431
10343
|
layout.addRow("Fill Small Holes Along Borders:", self.borders)
|
|
9432
10344
|
|
|
10345
|
+
self.sep_holes = QPushButton("Seperate Hole Mask")
|
|
10346
|
+
self.sep_holes.setCheckable(True)
|
|
10347
|
+
self.sep_holes.setChecked(False)
|
|
10348
|
+
layout.addRow("Place Hole Mask in Overlay 2 (Instead of Filling):", self.sep_holes)
|
|
10349
|
+
|
|
9433
10350
|
# Add Run button
|
|
9434
10351
|
run_button = QPushButton("Run Fill Holes")
|
|
9435
10352
|
run_button.clicked.connect(self.run_holes)
|
|
@@ -9446,6 +10363,7 @@ class HoleDialog(QDialog):
|
|
|
9446
10363
|
|
|
9447
10364
|
borders = self.borders.isChecked()
|
|
9448
10365
|
headon = self.headon.isChecked()
|
|
10366
|
+
sep_holes = self.sep_holes.isChecked()
|
|
9449
10367
|
|
|
9450
10368
|
# Call dilate method with parameters
|
|
9451
10369
|
result = n3d.fill_holes_3d(
|
|
@@ -9454,7 +10372,11 @@ class HoleDialog(QDialog):
|
|
|
9454
10372
|
fill_borders = borders
|
|
9455
10373
|
)
|
|
9456
10374
|
|
|
9457
|
-
|
|
10375
|
+
if not sep_holes:
|
|
10376
|
+
self.parent().load_channel(self.parent().active_channel, result, True)
|
|
10377
|
+
else:
|
|
10378
|
+
self.parent().load_channel(3, active_data - result, True)
|
|
10379
|
+
|
|
9458
10380
|
|
|
9459
10381
|
self.parent().update_display()
|
|
9460
10382
|
self.accept()
|
|
@@ -9553,7 +10475,7 @@ class CropDialog(QDialog):
|
|
|
9553
10475
|
try:
|
|
9554
10476
|
|
|
9555
10477
|
super().__init__(parent)
|
|
9556
|
-
self.setWindowTitle("Crop Image?")
|
|
10478
|
+
self.setWindowTitle("Crop Image (Will transpose any centroids)?")
|
|
9557
10479
|
self.setModal(True)
|
|
9558
10480
|
|
|
9559
10481
|
layout = QFormLayout(self)
|
|
@@ -9609,10 +10531,70 @@ class CropDialog(QDialog):
|
|
|
9609
10531
|
|
|
9610
10532
|
self.parent().load_channel(i, array, data = True)
|
|
9611
10533
|
|
|
10534
|
+
print("Transposing centroids...")
|
|
10535
|
+
|
|
10536
|
+
try:
|
|
10537
|
+
|
|
10538
|
+
if my_network.node_centroids is not None:
|
|
10539
|
+
nodes = list(my_network.node_centroids.keys())
|
|
10540
|
+
centroids = np.array(list(my_network.node_centroids.values()))
|
|
10541
|
+
|
|
10542
|
+
# Transform all at once
|
|
10543
|
+
transformed = centroids - np.array([zmin, ymin, xmin])
|
|
10544
|
+
transformed = transformed.astype(int)
|
|
10545
|
+
|
|
10546
|
+
# Boolean mask for valid coordinates
|
|
10547
|
+
valid_mask = ((transformed >= 0) &
|
|
10548
|
+
(transformed <= np.array([zmax, ymax, xmax]))).all(axis=1)
|
|
10549
|
+
|
|
10550
|
+
# Rebuild dictionary with only valid entries
|
|
10551
|
+
my_network.node_centroids = {
|
|
10552
|
+
nodes[int(i)]: [int(transformed[i, 0]), int(transformed[i, 1]), int(transformed[i, 2])]
|
|
10553
|
+
for i in range(len(nodes)) if valid_mask[i]
|
|
10554
|
+
}
|
|
10555
|
+
|
|
10556
|
+
self.parent().format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
|
|
10557
|
+
|
|
10558
|
+
except Exception as e:
|
|
10559
|
+
|
|
10560
|
+
print(f"Error transposing node centroids: {e}")
|
|
10561
|
+
|
|
10562
|
+
try:
|
|
10563
|
+
|
|
10564
|
+
if my_network.edge_centroids is not None:
|
|
10565
|
+
|
|
10566
|
+
if my_network.edge_centroids is not None:
|
|
10567
|
+
nodes = list(my_network.edge_centroids.keys())
|
|
10568
|
+
centroids = np.array(list(my_network.edge_centroids.values()))
|
|
10569
|
+
|
|
10570
|
+
# Transform all at once
|
|
10571
|
+
transformed = centroids - np.array([zmin, ymin, xmin])
|
|
10572
|
+
transformed = transformed.astype(int)
|
|
10573
|
+
|
|
10574
|
+
# Boolean mask for valid coordinates
|
|
10575
|
+
valid_mask = ((transformed >= 0) &
|
|
10576
|
+
(transformed <= np.array([zmax, ymax, xmax]))).all(axis=1)
|
|
10577
|
+
|
|
10578
|
+
# Rebuild dictionary with only valid entries
|
|
10579
|
+
my_network.edge_centroids = {
|
|
10580
|
+
nodes[int(i)]: [int(transformed[i, 0]), int(transformed[i, 1]), int(transformed[i, 2])]
|
|
10581
|
+
for i in range(len(nodes)) if valid_mask[i]
|
|
10582
|
+
}
|
|
10583
|
+
|
|
10584
|
+
self.parent().format_for_upperright_table(my_network.edge_centroids, 'EdgeID', ['Z', 'Y', 'X'], 'Edge Centroids')
|
|
10585
|
+
|
|
10586
|
+
except Exception as e:
|
|
10587
|
+
|
|
10588
|
+
print(f"Error transposing edge centroids: {e}")
|
|
10589
|
+
|
|
10590
|
+
|
|
9612
10591
|
self.accept()
|
|
9613
10592
|
|
|
9614
10593
|
except Exception as e:
|
|
9615
10594
|
|
|
10595
|
+
import traceback
|
|
10596
|
+
print(traceback.format_exc())
|
|
10597
|
+
|
|
9616
10598
|
print(f"Error cropping: {e}")
|
|
9617
10599
|
|
|
9618
10600
|
|
|
@@ -10037,7 +11019,7 @@ class CentroidNodeDialog(QDialog):
|
|
|
10037
11019
|
|
|
10038
11020
|
else:
|
|
10039
11021
|
|
|
10040
|
-
my_network.nodes, my_network.
|
|
11022
|
+
my_network.nodes, my_network.node_centroids = my_network.centroid_array(clip = True)
|
|
10041
11023
|
|
|
10042
11024
|
self.parent().format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
|
|
10043
11025
|
|
|
@@ -10315,6 +11297,12 @@ class BranchDialog(QDialog):
|
|
|
10315
11297
|
self.fix2.setChecked(True)
|
|
10316
11298
|
correction_layout.addWidget(QLabel("Auto-Correct Branches by Collapsing Internal Labels: "), 3, 0)
|
|
10317
11299
|
correction_layout.addWidget(self.fix2, 3, 1)
|
|
11300
|
+
|
|
11301
|
+
self.fix3 = QPushButton("Split Nontouching Branches?")
|
|
11302
|
+
self.fix3.setCheckable(True)
|
|
11303
|
+
self.fix3.setChecked(True)
|
|
11304
|
+
correction_layout.addWidget(QLabel("Split Nontouching Branches? (Useful if branch pruning - may want to threshold out small, split branches after): "), 4, 0)
|
|
11305
|
+
correction_layout.addWidget(self.fix3, 4, 1)
|
|
10318
11306
|
|
|
10319
11307
|
correction_group.setLayout(correction_layout)
|
|
10320
11308
|
main_layout.addWidget(correction_group)
|
|
@@ -10385,6 +11373,7 @@ class BranchDialog(QDialog):
|
|
|
10385
11373
|
cubic = self.cubic.isChecked()
|
|
10386
11374
|
fix = self.fix.isChecked()
|
|
10387
11375
|
fix2 = self.fix2.isChecked()
|
|
11376
|
+
fix3 = self.fix3.isChecked()
|
|
10388
11377
|
fix_val = float(self.fix_val.text()) if self.fix_val.text() else None
|
|
10389
11378
|
seed = int(self.seed.text()) if self.seed.text() else None
|
|
10390
11379
|
|
|
@@ -10434,6 +11423,10 @@ class BranchDialog(QDialog):
|
|
|
10434
11423
|
|
|
10435
11424
|
output = temp_network.nodes
|
|
10436
11425
|
|
|
11426
|
+
if fix3:
|
|
11427
|
+
|
|
11428
|
+
output = self.parent().separate_nontouching_objects(output, max_val=np.max(output))
|
|
11429
|
+
|
|
10437
11430
|
|
|
10438
11431
|
if down_factor is not None:
|
|
10439
11432
|
|
|
@@ -10946,11 +11939,11 @@ class CalcAllDialog(QDialog):
|
|
|
10946
11939
|
|
|
10947
11940
|
self.search = QLineEdit(self.prev_search)
|
|
10948
11941
|
self.search.setPlaceholderText("Leave empty for None")
|
|
10949
|
-
important_layout.addRow("Node Search (float):", self.search)
|
|
11942
|
+
important_layout.addRow("Node Search (float - Does not merge nodes):", self.search)
|
|
10950
11943
|
|
|
10951
11944
|
self.diledge = QLineEdit(self.prev_diledge)
|
|
10952
11945
|
self.diledge.setPlaceholderText("Leave empty for None")
|
|
10953
|
-
important_layout.addRow("Edge
|
|
11946
|
+
important_layout.addRow("Edge Search (float - Note that edges that find each other will merge):", self.diledge)
|
|
10954
11947
|
|
|
10955
11948
|
self.label_nodes = QPushButton("Label")
|
|
10956
11949
|
self.label_nodes.setCheckable(True)
|