nettracer3d 0.8.4__py3-none-any.whl → 0.8.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nettracer3d/community_extractor.py +3 -2
- nettracer3d/neighborhoods.py +140 -31
- nettracer3d/nettracer.py +10 -3
- nettracer3d/nettracer_gui.py +496 -706
- nettracer3d/painting.py +375 -0
- nettracer3d/proximity.py +2 -2
- nettracer3d/segmenter.py +849 -851
- nettracer3d/segmenter_GPU.py +806 -658
- nettracer3d/smart_dilate.py +2 -2
- {nettracer3d-0.8.4.dist-info → nettracer3d-0.8.6.dist-info}/METADATA +5 -2
- nettracer3d-0.8.6.dist-info/RECORD +25 -0
- {nettracer3d-0.8.4.dist-info → nettracer3d-0.8.6.dist-info}/licenses/LICENSE +2 -4
- nettracer3d-0.8.4.dist-info/RECORD +0 -24
- {nettracer3d-0.8.4.dist-info → nettracer3d-0.8.6.dist-info}/WHEEL +0 -0
- {nettracer3d-0.8.4.dist-info → nettracer3d-0.8.6.dist-info}/entry_points.txt +0 -0
- {nettracer3d-0.8.4.dist-info → nettracer3d-0.8.6.dist-info}/top_level.txt +0 -0
nettracer3d/nettracer_gui.py
CHANGED
|
@@ -35,6 +35,7 @@ import queue
|
|
|
35
35
|
from threading import Lock
|
|
36
36
|
from scipy import ndimage
|
|
37
37
|
import os
|
|
38
|
+
from . import painting
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
|
|
@@ -210,7 +211,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
210
211
|
buttons_widget = QWidget()
|
|
211
212
|
buttons_layout = QHBoxLayout(buttons_widget)
|
|
212
213
|
|
|
213
|
-
# Create zoom button
|
|
214
|
+
# "Create" zoom button
|
|
214
215
|
self.zoom_button = QPushButton("🔍")
|
|
215
216
|
self.zoom_button.setCheckable(True)
|
|
216
217
|
self.zoom_button.setFixedSize(40, 40)
|
|
@@ -454,18 +455,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
454
455
|
self.excel_manager.data_received.connect(self.handle_excel_data)
|
|
455
456
|
self.prev_coms = None
|
|
456
457
|
|
|
457
|
-
self.paint_timer = QTimer()
|
|
458
|
-
self.paint_timer.timeout.connect(self.flush_paint_updates)
|
|
459
|
-
self.paint_timer.setSingleShot(True)
|
|
460
|
-
self.pending_paint_update = False
|
|
461
458
|
self.static_background = None
|
|
462
459
|
|
|
463
|
-
# Threading for paint operations
|
|
464
|
-
self.paint_queue = queue.Queue()
|
|
465
|
-
self.paint_lock = Lock()
|
|
466
|
-
self.paint_worker = threading.Thread(target=self.paint_worker_loop, daemon=True)
|
|
467
|
-
self.paint_worker.start()
|
|
468
|
-
|
|
469
460
|
# Background caching for blitting
|
|
470
461
|
self.paint_session_active = False
|
|
471
462
|
|
|
@@ -473,6 +464,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
473
464
|
self.paint_batch = []
|
|
474
465
|
self.last_paint_pos = None
|
|
475
466
|
|
|
467
|
+
self.resume = False
|
|
468
|
+
|
|
476
469
|
def start_left_scroll(self):
|
|
477
470
|
"""Start scrolling left when left arrow is pressed."""
|
|
478
471
|
# Single increment first
|
|
@@ -2170,11 +2163,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2170
2163
|
if self.zoom_mode:
|
|
2171
2164
|
self.pan_button.setChecked(False)
|
|
2172
2165
|
|
|
2173
|
-
if self.pan_mode or self.brush_mode:
|
|
2174
|
-
current_xlim = self.ax.get_xlim()
|
|
2175
|
-
current_ylim = self.ax.get_ylim()
|
|
2176
|
-
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
2177
|
-
|
|
2178
2166
|
self.pen_button.setChecked(False)
|
|
2179
2167
|
self.pan_mode = False
|
|
2180
2168
|
self.brush_mode = False
|
|
@@ -2184,6 +2172,11 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2184
2172
|
if self.machine_window is not None:
|
|
2185
2173
|
self.machine_window.silence_button()
|
|
2186
2174
|
self.canvas.setCursor(Qt.CursorShape.CrossCursor)
|
|
2175
|
+
if self.pan_mode or self.brush_mode:
|
|
2176
|
+
current_xlim = self.ax.get_xlim()
|
|
2177
|
+
current_ylim = self.ax.get_ylim()
|
|
2178
|
+
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
2179
|
+
|
|
2187
2180
|
else:
|
|
2188
2181
|
if self.machine_window is None:
|
|
2189
2182
|
self.canvas.setCursor(Qt.CursorShape.ArrowCursor)
|
|
@@ -2195,10 +2188,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2195
2188
|
"""Toggle pan mode on/off."""
|
|
2196
2189
|
self.pan_mode = self.pan_button.isChecked()
|
|
2197
2190
|
if self.pan_mode:
|
|
2198
|
-
if self.brush_mode:
|
|
2199
|
-
current_xlim = self.ax.get_xlim()
|
|
2200
|
-
current_ylim = self.ax.get_ylim()
|
|
2201
|
-
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
2202
2191
|
|
|
2203
2192
|
self.zoom_button.setChecked(False)
|
|
2204
2193
|
self.pen_button.setChecked(False)
|
|
@@ -2210,6 +2199,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2210
2199
|
if self.machine_window is not None:
|
|
2211
2200
|
self.machine_window.silence_button()
|
|
2212
2201
|
self.canvas.setCursor(Qt.CursorShape.OpenHandCursor)
|
|
2202
|
+
if self.brush_mode:
|
|
2203
|
+
current_xlim = self.ax.get_xlim()
|
|
2204
|
+
current_ylim = self.ax.get_ylim()
|
|
2205
|
+
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
2213
2206
|
else:
|
|
2214
2207
|
current_xlim = self.ax.get_xlim()
|
|
2215
2208
|
current_ylim = self.ax.get_ylim()
|
|
@@ -2224,6 +2217,20 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2224
2217
|
self.brush_mode = self.pen_button.isChecked()
|
|
2225
2218
|
if self.brush_mode:
|
|
2226
2219
|
|
|
2220
|
+
self.pm = painting.PaintManager(parent = self)
|
|
2221
|
+
|
|
2222
|
+
# Start virtual paint session
|
|
2223
|
+
# Get current zoom to preserve it
|
|
2224
|
+
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
2225
|
+
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
2226
|
+
|
|
2227
|
+
if self.pen_button.isChecked():
|
|
2228
|
+
channel = self.active_channel
|
|
2229
|
+
else:
|
|
2230
|
+
channel = 2
|
|
2231
|
+
|
|
2232
|
+
self.pm.initiate_paint_session(channel, current_xlim, current_ylim)
|
|
2233
|
+
|
|
2227
2234
|
if self.pan_mode:
|
|
2228
2235
|
current_xlim = self.ax.get_xlim()
|
|
2229
2236
|
current_ylim = self.ax.get_ylim()
|
|
@@ -2443,37 +2450,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2443
2450
|
|
|
2444
2451
|
painter.end()
|
|
2445
2452
|
|
|
2446
|
-
def get_line_points(self, x0, y0, x1, y1):
|
|
2447
|
-
"""Get all points in a line between (x0,y0) and (x1,y1) using Bresenham's algorithm."""
|
|
2448
|
-
points = []
|
|
2449
|
-
dx = abs(x1 - x0)
|
|
2450
|
-
dy = abs(y1 - y0)
|
|
2451
|
-
x, y = x0, y0
|
|
2452
|
-
sx = 1 if x0 < x1 else -1
|
|
2453
|
-
sy = 1 if y0 < y1 else -1
|
|
2454
|
-
|
|
2455
|
-
if dx > dy:
|
|
2456
|
-
err = dx / 2.0
|
|
2457
|
-
while x != x1:
|
|
2458
|
-
points.append((x, y))
|
|
2459
|
-
err -= dy
|
|
2460
|
-
if err < 0:
|
|
2461
|
-
y += sy
|
|
2462
|
-
err += dx
|
|
2463
|
-
x += sx
|
|
2464
|
-
else:
|
|
2465
|
-
err = dy / 2.0
|
|
2466
|
-
while y != y1:
|
|
2467
|
-
points.append((x, y))
|
|
2468
|
-
err -= dx
|
|
2469
|
-
if err < 0:
|
|
2470
|
-
x += sx
|
|
2471
|
-
err += dy
|
|
2472
|
-
y += sy
|
|
2473
|
-
|
|
2474
|
-
points.append((x, y))
|
|
2475
|
-
return points
|
|
2476
|
-
|
|
2477
2453
|
def get_current_mouse_position(self):
|
|
2478
2454
|
# Get the main application's current mouse position
|
|
2479
2455
|
cursor_pos = QCursor.pos()
|
|
@@ -2486,15 +2462,25 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2486
2462
|
0 <= canvas_pos.y() < self.canvas.height()):
|
|
2487
2463
|
return 0, 0 # Mouse is outside of the matplotlib canvas
|
|
2488
2464
|
|
|
2489
|
-
#
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2465
|
+
# OPTION 1: Use matplotlib's built-in coordinate conversion
|
|
2466
|
+
# This accounts for figure margins, subplot positioning, etc.
|
|
2467
|
+
try:
|
|
2468
|
+
# Get the figure and axes bounds
|
|
2469
|
+
bbox = self.ax.bbox
|
|
2470
|
+
|
|
2471
|
+
# Convert widget coordinates to figure coordinates
|
|
2472
|
+
fig_x = canvas_pos.x()
|
|
2473
|
+
fig_y = self.canvas.height() - canvas_pos.y() # Flip Y coordinate
|
|
2474
|
+
|
|
2475
|
+
# Check if within axes bounds
|
|
2476
|
+
if (bbox.x0 <= fig_x <= bbox.x1 and bbox.y0 <= fig_y <= bbox.y1):
|
|
2477
|
+
# Transform to data coordinates
|
|
2478
|
+
data_coords = self.ax.transData.inverted().transform((fig_x, fig_y))
|
|
2479
|
+
return data_coords[0], data_coords[1]
|
|
2480
|
+
else:
|
|
2481
|
+
return 0, 0
|
|
2482
|
+
except:
|
|
2483
|
+
pass
|
|
2498
2484
|
|
|
2499
2485
|
def on_mouse_press(self, event):
|
|
2500
2486
|
"""Handle mouse press events."""
|
|
@@ -2514,6 +2500,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2514
2500
|
|
|
2515
2501
|
if self.machine_window is not None:
|
|
2516
2502
|
if self.machine_window.segmentation_worker is not None:
|
|
2503
|
+
if not self.machine_window.segmentation_worker._paused:
|
|
2504
|
+
self.resume = True
|
|
2517
2505
|
self.machine_window.segmentation_worker.pause()
|
|
2518
2506
|
|
|
2519
2507
|
# Store current channel visibility state
|
|
@@ -2533,61 +2521,68 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2533
2521
|
|
|
2534
2522
|
|
|
2535
2523
|
elif self.brush_mode:
|
|
2524
|
+
"""Handle brush mode with virtual painting."""
|
|
2536
2525
|
if event.inaxes != self.ax:
|
|
2537
2526
|
return
|
|
2527
|
+
|
|
2528
|
+
"""
|
|
2529
|
+
try:
|
|
2530
|
+
if self.machine_window is not None and not self.machine_window.segmentation_worker._paused:
|
|
2531
|
+
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
2532
|
+
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
2533
|
+
|
|
2534
|
+
if self.pen_button.isChecked():
|
|
2535
|
+
channel = self.active_channel
|
|
2536
|
+
else:
|
|
2537
|
+
channel = 2
|
|
2538
|
+
|
|
2539
|
+
self.pm.initiate_paint_session(channel, current_xlim, current_ylim)
|
|
2540
|
+
except:
|
|
2541
|
+
pass
|
|
2542
|
+
"""
|
|
2538
2543
|
|
|
2539
2544
|
if event.button == 1 or event.button == 3:
|
|
2545
|
+
if self.machine_window is not None:
|
|
2546
|
+
if self.machine_window.segmentation_worker is not None:
|
|
2547
|
+
if not self.machine_window.segmentation_worker._paused:
|
|
2548
|
+
self.resume = True
|
|
2549
|
+
self.machine_window.segmentation_worker.pause()
|
|
2550
|
+
|
|
2540
2551
|
x, y = int(event.xdata), int(event.ydata)
|
|
2541
|
-
|
|
2542
|
-
|
|
2552
|
+
|
|
2553
|
+
# Get current zoom to preserve it
|
|
2543
2554
|
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
2544
2555
|
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
self.update_display(preserve_zoom = (current_xlim, current_ylim))
|
|
2556
|
+
|
|
2557
|
+
if event.button == 1 and getattr(self, 'can', False):
|
|
2558
|
+
self.update_display(preserve_zoom=(current_xlim, current_ylim), skip_paint_reinit = True)
|
|
2549
2559
|
self.handle_can(x, y)
|
|
2550
2560
|
return
|
|
2551
|
-
|
|
2561
|
+
|
|
2562
|
+
# Determine erase mode and foreground/background
|
|
2552
2563
|
if event.button == 3:
|
|
2553
2564
|
self.erase = True
|
|
2554
|
-
self.update_display(preserve_zoom = (current_xlim, current_ylim))
|
|
2555
2565
|
else:
|
|
2556
2566
|
self.erase = False
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2567
|
+
|
|
2568
|
+
# Determine foreground/background for machine window mode
|
|
2569
|
+
foreground = getattr(self, 'foreground', True)
|
|
2570
|
+
|
|
2571
|
+
self.last_virtual_pos = (x, y)
|
|
2572
|
+
|
|
2561
2573
|
if self.pen_button.isChecked():
|
|
2562
2574
|
channel = self.active_channel
|
|
2563
2575
|
else:
|
|
2564
2576
|
channel = 2
|
|
2565
2577
|
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
self
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
# No need to hide other channels or track restore_channels
|
|
2576
|
-
self.restore_channels = []
|
|
2577
|
-
|
|
2578
|
-
if self.static_background is None:
|
|
2579
|
-
if self.machine_window is not None:
|
|
2580
|
-
self.update_display(preserve_zoom = (current_xlim, current_ylim))
|
|
2581
|
-
elif not self.erase:
|
|
2582
|
-
self.temp_chan = channel
|
|
2583
|
-
self.channel_data[4] = self.channel_data[channel]
|
|
2584
|
-
self.min_max[4] = copy.deepcopy(self.min_max[channel])
|
|
2585
|
-
self.channel_brightness[4] = copy.deepcopy(self.channel_brightness[channel])
|
|
2586
|
-
self.load_channel(channel, np.zeros_like(self.channel_data[channel]), data = True, preserve_zoom = (current_xlim, current_ylim), begin_paint = True)
|
|
2587
|
-
self.channel_visible[4] = True
|
|
2588
|
-
self.static_background = self.canvas.copy_from_bbox(self.ax.bbox)
|
|
2589
|
-
|
|
2590
|
-
self.update_display_slice_optimized(channel, preserve_zoom=(current_xlim, current_ylim))
|
|
2578
|
+
self.pm.start_virtual_paint_session(channel, current_xlim, current_ylim)
|
|
2579
|
+
|
|
2580
|
+
# Add first virtual paint stroke
|
|
2581
|
+
brush_size = getattr(self, 'brush_size', 5)
|
|
2582
|
+
self.pm.add_virtual_paint_stroke(x, y, brush_size, self.erase, foreground)
|
|
2583
|
+
|
|
2584
|
+
# Update display with virtual paint
|
|
2585
|
+
self.pm.update_virtual_paint_display()
|
|
2591
2586
|
|
|
2592
2587
|
elif not self.zoom_mode and event.button == 3: # Right click (for context menu)
|
|
2593
2588
|
self.create_context_menu(event)
|
|
@@ -2597,92 +2592,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2597
2592
|
self.selection_start = (event.xdata, event.ydata)
|
|
2598
2593
|
self.selecting = False # Will be set to True if the mouse moves while button is held
|
|
2599
2594
|
|
|
2600
|
-
def paint_at_position(self, center_x, center_y, erase = False, channel = 2):
|
|
2601
|
-
"""Paint pixels within brush radius at given position"""
|
|
2602
|
-
if self.channel_data[channel] is None:
|
|
2603
|
-
return
|
|
2604
|
-
|
|
2605
|
-
if erase:
|
|
2606
|
-
val = 0
|
|
2607
|
-
elif self.machine_window is None:
|
|
2608
|
-
try:
|
|
2609
|
-
val = max(255, self.min_max[4][1])
|
|
2610
|
-
except:
|
|
2611
|
-
val = 255
|
|
2612
|
-
elif self.foreground:
|
|
2613
|
-
val = 1
|
|
2614
|
-
else:
|
|
2615
|
-
val = 2
|
|
2616
|
-
height, width = self.channel_data[channel][self.current_slice].shape
|
|
2617
|
-
radius = self.brush_size // 2
|
|
2618
|
-
|
|
2619
|
-
# Calculate brush area
|
|
2620
|
-
for y in range(max(0, center_y - radius), min(height, center_y + radius + 1)):
|
|
2621
|
-
for x in range(max(0, center_x - radius), min(width, center_x + radius + 1)):
|
|
2622
|
-
# Check if point is within circular brush area
|
|
2623
|
-
if (x - center_x) * 2 + (y - center_y) * 2 <= radius ** 2:
|
|
2624
|
-
if self.threed and self.threedthresh > 1:
|
|
2625
|
-
amount = (self.threedthresh - 1) / 2
|
|
2626
|
-
low = max(0, self.current_slice - amount)
|
|
2627
|
-
high = min(self.channel_data[channel].shape[0] - 1, self.current_slice + amount)
|
|
2628
|
-
for i in range(int(low), int(high + 1)):
|
|
2629
|
-
self.channel_data[channel][i][y, x] = val
|
|
2630
|
-
else:
|
|
2631
|
-
self.channel_data[channel][self.current_slice][y, x] = val
|
|
2632
|
-
|
|
2633
|
-
def paint_at_position_vectorized(self, center_x, center_y, erase=False, channel=2,
|
|
2634
|
-
slice_idx=None, brush_size=None, threed=False,
|
|
2635
|
-
threedthresh=1, foreground=True, machine_window=None):
|
|
2636
|
-
"""Vectorized paint operation for better performance."""
|
|
2637
|
-
if self.channel_data[channel] is None:
|
|
2638
|
-
return
|
|
2639
|
-
|
|
2640
|
-
# Use provided parameters or fall back to instance variables
|
|
2641
|
-
slice_idx = slice_idx if slice_idx is not None else self.current_slice
|
|
2642
|
-
brush_size = brush_size if brush_size is not None else getattr(self, 'brush_size', 5)
|
|
2643
|
-
|
|
2644
|
-
# Determine paint value
|
|
2645
|
-
if erase:
|
|
2646
|
-
val = 0
|
|
2647
|
-
elif machine_window is None:
|
|
2648
|
-
try:
|
|
2649
|
-
val = max(255, self.min_max[4][1])
|
|
2650
|
-
except:
|
|
2651
|
-
val = 255
|
|
2652
|
-
elif foreground:
|
|
2653
|
-
val = 1
|
|
2654
|
-
else:
|
|
2655
|
-
val = 2
|
|
2656
|
-
|
|
2657
|
-
height, width = self.channel_data[channel][slice_idx].shape
|
|
2658
|
-
radius = brush_size // 2
|
|
2659
|
-
|
|
2660
|
-
# Calculate affected region bounds
|
|
2661
|
-
y_min = max(0, center_y - radius)
|
|
2662
|
-
y_max = min(height, center_y + radius + 1)
|
|
2663
|
-
x_min = max(0, center_x - radius)
|
|
2664
|
-
x_max = min(width, center_x + radius + 1)
|
|
2665
|
-
|
|
2666
|
-
if y_min >= y_max or x_min >= x_max:
|
|
2667
|
-
return # No valid region to paint
|
|
2668
|
-
|
|
2669
|
-
# Create coordinate grids for the affected region
|
|
2670
|
-
y_coords, x_coords = np.mgrid[y_min:y_max, x_min:x_max]
|
|
2671
|
-
|
|
2672
|
-
# Calculate distances squared (avoid sqrt for performance)
|
|
2673
|
-
distances_sq = (x_coords - center_x) ** 2 + (y_coords - center_y) ** 2
|
|
2674
|
-
mask = distances_sq <= radius ** 2
|
|
2675
|
-
|
|
2676
|
-
# Apply paint to affected slices
|
|
2677
|
-
if threed and threedthresh > 1:
|
|
2678
|
-
amount = (threedthresh - 1) / 2
|
|
2679
|
-
low = max(0, int(slice_idx - amount))
|
|
2680
|
-
high = min(self.channel_data[channel].shape[0] - 1, int(slice_idx + amount))
|
|
2681
|
-
|
|
2682
|
-
for i in range(low, high + 1):
|
|
2683
|
-
self.channel_data[channel][i][y_min:y_max, x_min:x_max][mask] = val
|
|
2684
|
-
else:
|
|
2685
|
-
self.channel_data[channel][slice_idx][y_min:y_max, x_min:x_max][mask] = val
|
|
2686
2595
|
|
|
2687
2596
|
def handle_can(self, x, y):
|
|
2688
2597
|
|
|
@@ -2823,129 +2732,24 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2823
2732
|
if event.inaxes != self.ax:
|
|
2824
2733
|
return
|
|
2825
2734
|
|
|
2826
|
-
#
|
|
2827
|
-
|
|
2735
|
+
# Throttle updates like selection rectangle
|
|
2736
|
+
current_time = time.time()
|
|
2737
|
+
if current_time - getattr(self, 'last_paint_update_time', 0) < 0.016: # ~60fps
|
|
2738
|
+
return
|
|
2739
|
+
self.last_paint_update_time = current_time
|
|
2828
2740
|
|
|
2829
|
-
|
|
2830
|
-
if not self.pending_paint_update:
|
|
2831
|
-
self.pending_paint_update = True
|
|
2832
|
-
self.paint_timer.start(16) # ~60fps max update rate
|
|
2833
|
-
|
|
2834
|
-
def queue_paint_operation(self, event):
|
|
2835
|
-
"""Queue a paint operation for background processing."""
|
|
2836
|
-
x, y = int(event.xdata), int(event.ydata)
|
|
2837
|
-
|
|
2838
|
-
if self.pen_button.isChecked():
|
|
2839
|
-
channel = self.active_channel
|
|
2840
|
-
else:
|
|
2841
|
-
channel = 2
|
|
2842
|
-
|
|
2843
|
-
if self.channel_data[channel] is not None:
|
|
2844
|
-
# Prepare paint session if needed
|
|
2845
|
-
if not self.paint_session_active:
|
|
2846
|
-
self.prepare_paint_session(channel)
|
|
2847
|
-
|
|
2848
|
-
# Create paint operation
|
|
2849
|
-
paint_op = {
|
|
2850
|
-
'type': 'stroke',
|
|
2851
|
-
'x': x,
|
|
2852
|
-
'y': y,
|
|
2853
|
-
'last_pos': getattr(self, 'last_paint_pos', None),
|
|
2854
|
-
'brush_size': self.brush_size,
|
|
2855
|
-
'erase': self.erase,
|
|
2856
|
-
'channel': channel,
|
|
2857
|
-
'slice': self.current_slice,
|
|
2858
|
-
'threed': getattr(self, 'threed', False),
|
|
2859
|
-
'threedthresh': getattr(self, 'threedthresh', 1),
|
|
2860
|
-
'foreground': getattr(self, 'foreground', True),
|
|
2861
|
-
'machine_window': getattr(self, 'machine_window', None)
|
|
2862
|
-
}
|
|
2741
|
+
x, y = int(event.xdata), int(event.ydata)
|
|
2863
2742
|
|
|
2864
|
-
#
|
|
2865
|
-
|
|
2866
|
-
self.paint_queue.put_nowait(paint_op)
|
|
2867
|
-
except queue.Full:
|
|
2868
|
-
pass # Skip if queue is full to avoid blocking
|
|
2743
|
+
# Determine foreground/background for machine window mode
|
|
2744
|
+
foreground = getattr(self, 'foreground', True)
|
|
2869
2745
|
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
"""Prepare optimized background for blitting during paint session."""
|
|
2874
|
-
if self.paint_session_active:
|
|
2875
|
-
return
|
|
2746
|
+
# Add virtual paint stroke with interpolation
|
|
2747
|
+
brush_size = getattr(self, 'brush_size', 5)
|
|
2748
|
+
self.pm.add_virtual_paint_stroke(x, y, brush_size, self.erase, foreground)
|
|
2876
2749
|
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
self.paint_session_active = True
|
|
2750
|
+
# Update display with virtual paint (super fast)
|
|
2751
|
+
self.pm.update_virtual_paint_display()
|
|
2880
2752
|
|
|
2881
|
-
def end_paint_session(self):
|
|
2882
|
-
"""Clean up after paint session."""
|
|
2883
|
-
self.paint_session_active = False
|
|
2884
|
-
self.last_paint_pos = None
|
|
2885
|
-
|
|
2886
|
-
def paint_worker_loop(self):
|
|
2887
|
-
"""Background thread for processing paint operations."""
|
|
2888
|
-
while True:
|
|
2889
|
-
try:
|
|
2890
|
-
paint_op = self.paint_queue.get(timeout=1.0)
|
|
2891
|
-
if paint_op is None: # Shutdown signal
|
|
2892
|
-
break
|
|
2893
|
-
|
|
2894
|
-
with self.paint_lock:
|
|
2895
|
-
self.execute_paint_operation(paint_op)
|
|
2896
|
-
|
|
2897
|
-
except queue.Empty:
|
|
2898
|
-
continue
|
|
2899
|
-
|
|
2900
|
-
def shutdown(self):
|
|
2901
|
-
"""Clean shutdown of worker thread."""
|
|
2902
|
-
self.paint_queue.put(None) # Signal worker to stop
|
|
2903
|
-
if hasattr(self, 'paint_worker'):
|
|
2904
|
-
self.paint_worker.join(timeout=1.0)
|
|
2905
|
-
|
|
2906
|
-
def execute_paint_operation(self, paint_op):
|
|
2907
|
-
"""Execute a single paint operation on the data arrays."""
|
|
2908
|
-
if paint_op['type'] == 'stroke':
|
|
2909
|
-
channel = paint_op['channel']
|
|
2910
|
-
x, y = paint_op['x'], paint_op['y']
|
|
2911
|
-
last_pos = paint_op['last_pos']
|
|
2912
|
-
|
|
2913
|
-
if last_pos is not None:
|
|
2914
|
-
# Paint line from last position to current
|
|
2915
|
-
points = self.get_line_points(last_pos[0], last_pos[1], x, y)
|
|
2916
|
-
for px, py in points:
|
|
2917
|
-
height, width = self.channel_data[channel][paint_op['slice']].shape
|
|
2918
|
-
if 0 <= px < width and 0 <= py < height:
|
|
2919
|
-
self.paint_at_position_vectorized(
|
|
2920
|
-
px, py, paint_op['erase'], paint_op['channel'],
|
|
2921
|
-
paint_op['slice'], paint_op['brush_size'],
|
|
2922
|
-
paint_op['threed'], paint_op['threedthresh'],
|
|
2923
|
-
paint_op['foreground'], paint_op['machine_window']
|
|
2924
|
-
)
|
|
2925
|
-
else:
|
|
2926
|
-
# Single point paint
|
|
2927
|
-
height, width = self.channel_data[channel][paint_op['slice']].shape
|
|
2928
|
-
if 0 <= x < width and 0 <= y < height:
|
|
2929
|
-
self.paint_at_position_vectorized(
|
|
2930
|
-
x, y, paint_op['erase'], paint_op['channel'],
|
|
2931
|
-
paint_op['slice'], paint_op['brush_size'],
|
|
2932
|
-
paint_op['threed'], paint_op['threedthresh'],
|
|
2933
|
-
paint_op['foreground'], paint_op['machine_window']
|
|
2934
|
-
)
|
|
2935
|
-
|
|
2936
|
-
def flush_paint_updates(self):
|
|
2937
|
-
"""Update the display with batched paint changes."""
|
|
2938
|
-
self.pending_paint_update = False
|
|
2939
|
-
|
|
2940
|
-
# Determine which channel to update
|
|
2941
|
-
channel = self.active_channel if hasattr(self, 'pen_button') and self.pen_button.isChecked() else 2
|
|
2942
|
-
|
|
2943
|
-
# Get current zoom to preserve it
|
|
2944
|
-
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
2945
|
-
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
2946
|
-
|
|
2947
|
-
# Update display
|
|
2948
|
-
self.update_display_slice_optimized(channel, preserve_zoom=(current_xlim, current_ylim))
|
|
2949
2753
|
|
|
2950
2754
|
def create_pan_background(self):
|
|
2951
2755
|
"""Create a static background image from currently visible channels with proper rendering"""
|
|
@@ -3322,33 +3126,23 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3322
3126
|
|
|
3323
3127
|
# Handle brush mode cleanup with paint session management
|
|
3324
3128
|
if self.brush_mode and hasattr(self, 'painting') and self.painting:
|
|
3129
|
+
# Finish current operation
|
|
3130
|
+
self.pm.finish_current_virtual_operation()
|
|
3131
|
+
|
|
3132
|
+
# Reset last position for next stroke
|
|
3133
|
+
self.last_virtual_pos = None
|
|
3134
|
+
|
|
3135
|
+
# End this stroke but keep session active for continuous painting
|
|
3325
3136
|
self.painting = False
|
|
3326
3137
|
|
|
3327
|
-
if self.
|
|
3328
|
-
|
|
3329
|
-
try:
|
|
3330
|
-
for i in self.restore_channels:
|
|
3331
|
-
self.channel_visible[i] = True
|
|
3332
|
-
self.restore_channels = []
|
|
3333
|
-
except:
|
|
3334
|
-
pass
|
|
3335
|
-
|
|
3336
|
-
self.end_paint_session()
|
|
3337
|
-
|
|
3338
|
-
# OPTIMIZED: Stop timer and process any pending paint operations
|
|
3339
|
-
if hasattr(self, 'paint_timer'):
|
|
3340
|
-
self.paint_timer.stop()
|
|
3341
|
-
if hasattr(self, 'pending_paint_update') and self.pending_paint_update:
|
|
3342
|
-
self.flush_paint_updates()
|
|
3343
|
-
|
|
3344
|
-
self.static_background = None
|
|
3345
|
-
|
|
3346
|
-
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
3347
|
-
|
|
3348
|
-
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
3138
|
+
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
3139
|
+
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
3349
3140
|
|
|
3350
|
-
|
|
3141
|
+
self.update_display(preserve_zoom=(current_xlim, current_ylim), continue_paint = True)
|
|
3351
3142
|
|
|
3143
|
+
if self.resume:
|
|
3144
|
+
self.machine_window.segmentation_worker.resume()
|
|
3145
|
+
self.resume = False
|
|
3352
3146
|
|
|
3353
3147
|
|
|
3354
3148
|
def highlight_value_in_tables(self, clicked_value):
|
|
@@ -3675,6 +3469,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3675
3469
|
community_code_action.triggered.connect(lambda: self.show_code_dialog(sort = 'Community'))
|
|
3676
3470
|
id_code_action = overlay_menu.addAction("Code Identities")
|
|
3677
3471
|
id_code_action.triggered.connect(lambda: self.show_code_dialog(sort = 'Identity'))
|
|
3472
|
+
umap_action = overlay_menu.addAction("Centroid UMAP")
|
|
3473
|
+
umap_action.triggered.connect(self.handle_umap)
|
|
3678
3474
|
|
|
3679
3475
|
rand_menu = analysis_menu.addMenu("Randomize")
|
|
3680
3476
|
random_action = rand_menu.addAction("Generate Equivalent Random Network")
|
|
@@ -3779,6 +3575,43 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3779
3575
|
help_button = menubar.addAction("Help")
|
|
3780
3576
|
help_button.triggered.connect(self.help_me)
|
|
3781
3577
|
|
|
3578
|
+
cam_button = QPushButton("📷")
|
|
3579
|
+
cam_button.setFixedSize(40, 40)
|
|
3580
|
+
cam_button.setStyleSheet("font-size: 24px;") # Makes emoji larger
|
|
3581
|
+
cam_button.clicked.connect(self.snap)
|
|
3582
|
+
menubar.setCornerWidget(cam_button, Qt.Corner.TopRightCorner)
|
|
3583
|
+
|
|
3584
|
+
def snap(self):
|
|
3585
|
+
|
|
3586
|
+
try:
|
|
3587
|
+
|
|
3588
|
+
for thing in self.channel_data:
|
|
3589
|
+
if thing is not None:
|
|
3590
|
+
data = True
|
|
3591
|
+
if not data:
|
|
3592
|
+
return
|
|
3593
|
+
|
|
3594
|
+
snap = self.create_composite_for_pan()
|
|
3595
|
+
|
|
3596
|
+
filename, _ = QFileDialog.getSaveFileName(
|
|
3597
|
+
self,
|
|
3598
|
+
f"Save Image As",
|
|
3599
|
+
"", # Default directory
|
|
3600
|
+
"TIFF Files (*.tif *.tiff);;All Files (*)" # File type filter
|
|
3601
|
+
)
|
|
3602
|
+
|
|
3603
|
+
if filename: # Only proceed if user didn't cancel
|
|
3604
|
+
# If user didn't type an extension, add .tif
|
|
3605
|
+
if not filename.endswith(('.tif', '.tiff')):
|
|
3606
|
+
filename += '.tif'
|
|
3607
|
+
|
|
3608
|
+
import tifffile
|
|
3609
|
+
tifffile.imwrite(filename, snap)
|
|
3610
|
+
|
|
3611
|
+
except:
|
|
3612
|
+
pass
|
|
3613
|
+
|
|
3614
|
+
|
|
3782
3615
|
def open_cellpose(self):
|
|
3783
3616
|
|
|
3784
3617
|
try:
|
|
@@ -4555,6 +4388,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4555
4388
|
)
|
|
4556
4389
|
|
|
4557
4390
|
self.last_load = directory
|
|
4391
|
+
|
|
4558
4392
|
|
|
4559
4393
|
if directory != "":
|
|
4560
4394
|
|
|
@@ -4817,6 +4651,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4817
4651
|
- 'mean': averages across color channels
|
|
4818
4652
|
- 'max': takes maximum value across color channels
|
|
4819
4653
|
- 'min': takes minimum value across color channels
|
|
4654
|
+
- 'weight': takes weighted channel averages
|
|
4820
4655
|
|
|
4821
4656
|
Returns:
|
|
4822
4657
|
--------
|
|
@@ -4831,7 +4666,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4831
4666
|
if array.ndim != 4:
|
|
4832
4667
|
raise ValueError(f"Expected 4D array, got {array.ndim}D array")
|
|
4833
4668
|
|
|
4834
|
-
if method not in ['first', 'mean', 'max', 'min']:
|
|
4669
|
+
if method not in ['first', 'mean', 'max', 'min', 'weight']:
|
|
4835
4670
|
raise ValueError(f"Unknown method: {method}")
|
|
4836
4671
|
|
|
4837
4672
|
if method == 'first':
|
|
@@ -4840,6 +4675,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4840
4675
|
return np.mean(array, axis=-1)
|
|
4841
4676
|
elif method == 'max':
|
|
4842
4677
|
return np.max(array, axis=-1)
|
|
4678
|
+
elif method == 'weight':
|
|
4679
|
+
# Apply the luminosity formula
|
|
4680
|
+
return (0.2989 * array[:,:,:,0] + 0.5870 * array[:,:,:,1] + 0.1140 * array[:,:,:,2])
|
|
4843
4681
|
else: # min
|
|
4844
4682
|
return np.min(array, axis=-1)
|
|
4845
4683
|
|
|
@@ -4863,7 +4701,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4863
4701
|
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
4864
4702
|
return msg.exec() == QMessageBox.StandardButton.Yes
|
|
4865
4703
|
|
|
4866
|
-
def load_channel(self, channel_index, channel_data=None, data=False, assign_shape = True, preserve_zoom = None, end_paint = False, begin_paint = False):
|
|
4704
|
+
def load_channel(self, channel_index, channel_data=None, data=False, assign_shape = True, preserve_zoom = None, end_paint = False, begin_paint = False, color = False):
|
|
4867
4705
|
"""Load a channel and enable active channel selection if needed."""
|
|
4868
4706
|
|
|
4869
4707
|
try:
|
|
@@ -4940,12 +4778,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4940
4778
|
except:
|
|
4941
4779
|
pass
|
|
4942
4780
|
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4781
|
+
if not color:
|
|
4782
|
+
try:
|
|
4783
|
+
if len(self.channel_data[channel_index].shape) == 4 and (channel_index == 0 or channel_index == 1):
|
|
4784
|
+
self.channel_data[channel_index] = self.reduce_rgb_dimension(self.channel_data[channel_index], 'weight')
|
|
4785
|
+
except:
|
|
4786
|
+
pass
|
|
4949
4787
|
|
|
4950
4788
|
reset_resize = False
|
|
4951
4789
|
|
|
@@ -5295,25 +5133,29 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5295
5133
|
|
|
5296
5134
|
|
|
5297
5135
|
|
|
5298
|
-
def update_display(self, preserve_zoom=None, dims = None, called = False, reset_resize = False):
|
|
5136
|
+
def update_display(self, preserve_zoom=None, dims = None, called = False, reset_resize = False, continue_paint = False, skip_paint_reinit = False):
|
|
5299
5137
|
"""Update the display with currently visible channels and highlight overlay."""
|
|
5300
|
-
|
|
5301
5138
|
try:
|
|
5302
|
-
|
|
5303
5139
|
self.figure.clear()
|
|
5304
|
-
|
|
5305
5140
|
if self.pan_background_image is not None:
|
|
5306
5141
|
# Restore previously visible channels
|
|
5307
5142
|
self.channel_visible = self.pre_pan_channel_state.copy()
|
|
5308
5143
|
self.is_pan_preview = False
|
|
5309
5144
|
self.pan_background_image = None
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
self.machine_window.segmentation_worker.resume()
|
|
5314
|
-
|
|
5145
|
+
if self.resume:
|
|
5146
|
+
self.machine_window.segmentation_worker.resume()
|
|
5147
|
+
self.resume = False
|
|
5315
5148
|
if self.static_background is not None:
|
|
5316
|
-
|
|
5149
|
+
# NEW: Convert virtual strokes to real data before cleanup
|
|
5150
|
+
if (hasattr(self, 'virtual_draw_operations') and self.virtual_draw_operations) or \
|
|
5151
|
+
(hasattr(self, 'virtual_erase_operations') and self.virtual_erase_operations) or \
|
|
5152
|
+
(hasattr(self, 'current_operation') and self.current_operation):
|
|
5153
|
+
# Finish current operation first
|
|
5154
|
+
if hasattr(self, 'current_operation') and self.current_operation:
|
|
5155
|
+
self.pm.finish_current_virtual_operation()
|
|
5156
|
+
# Now convert to real data
|
|
5157
|
+
self.pm.convert_virtual_strokes_to_data()
|
|
5158
|
+
|
|
5317
5159
|
# Restore hidden channels
|
|
5318
5160
|
try:
|
|
5319
5161
|
for i in self.restore_channels:
|
|
@@ -5321,27 +5163,17 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5321
5163
|
self.restore_channels = []
|
|
5322
5164
|
except:
|
|
5323
5165
|
pass
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
# OPTIMIZED: Stop timer and process any pending paint operations
|
|
5328
|
-
if hasattr(self, 'paint_timer'):
|
|
5329
|
-
self.paint_timer.stop()
|
|
5330
|
-
if hasattr(self, 'pending_paint_update') and self.pending_paint_update:
|
|
5331
|
-
self.flush_paint_updates()
|
|
5332
|
-
|
|
5333
|
-
self.static_background = None
|
|
5166
|
+
if not continue_paint:
|
|
5167
|
+
self.static_background = None
|
|
5334
5168
|
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
except:
|
|
5344
|
-
pass
|
|
5169
|
+
if self.machine_window is None:
|
|
5170
|
+
try:
|
|
5171
|
+
self.channel_data[4][self.current_slice, :, :] = n3d.overlay_arrays_simple(self.channel_data[self.temp_chan][self.current_slice, :, :], self.channel_data[4][self.current_slice, :, :])
|
|
5172
|
+
self.load_channel(self.temp_chan, self.channel_data[4], data = True, end_paint = True)
|
|
5173
|
+
self.channel_data[4] = None
|
|
5174
|
+
self.channel_visible[4] = False
|
|
5175
|
+
except:
|
|
5176
|
+
pass
|
|
5345
5177
|
|
|
5346
5178
|
# Get active channels and their dimensions
|
|
5347
5179
|
active_channels = [i for i in range(4) if self.channel_data[i] is not None]
|
|
@@ -5572,49 +5404,22 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5572
5404
|
|
|
5573
5405
|
self.canvas.draw()
|
|
5574
5406
|
|
|
5407
|
+
if self.brush_mode and not skip_paint_reinit:
|
|
5408
|
+
# Get current zoom to preserve it
|
|
5409
|
+
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
5410
|
+
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
5411
|
+
|
|
5412
|
+
if self.pen_button.isChecked():
|
|
5413
|
+
channel = self.active_channel
|
|
5414
|
+
else:
|
|
5415
|
+
channel = 2
|
|
5416
|
+
|
|
5417
|
+
self.pm.initiate_paint_session(channel, current_xlim, current_ylim)
|
|
5418
|
+
|
|
5575
5419
|
except:
|
|
5576
5420
|
import traceback
|
|
5577
5421
|
print(traceback.format_exc())
|
|
5578
5422
|
|
|
5579
|
-
def update_display_slice_optimized(self, channel, preserve_zoom=None):
|
|
5580
|
-
"""Ultra minimal update that only changes the paint channel's data - OPTIMIZED VERSION"""
|
|
5581
|
-
if not self.channel_visible[channel]:
|
|
5582
|
-
return
|
|
5583
|
-
|
|
5584
|
-
if preserve_zoom:
|
|
5585
|
-
current_xlim, current_ylim = preserve_zoom
|
|
5586
|
-
if current_xlim is not None and current_ylim is not None:
|
|
5587
|
-
self.ax.set_xlim(current_xlim)
|
|
5588
|
-
self.ax.set_ylim(current_ylim)
|
|
5589
|
-
|
|
5590
|
-
# Find the existing image for channel (paint channel)
|
|
5591
|
-
channel_image = None
|
|
5592
|
-
for img in self.ax.images:
|
|
5593
|
-
if img.cmap.name == f'custom_{channel}':
|
|
5594
|
-
channel_image = img
|
|
5595
|
-
break
|
|
5596
|
-
|
|
5597
|
-
if channel_image is not None:
|
|
5598
|
-
# Update the data of the existing image with thread safety
|
|
5599
|
-
with self.paint_lock:
|
|
5600
|
-
channel_image.set_array(self.channel_data[channel][self.current_slice])
|
|
5601
|
-
|
|
5602
|
-
# Restore the static background (all other channels) at current zoom level
|
|
5603
|
-
# This is the key - use static_background from update_display, not paint_background
|
|
5604
|
-
if hasattr(self, 'static_background') and self.static_background is not None:
|
|
5605
|
-
self.canvas.restore_region(self.static_background)
|
|
5606
|
-
# Draw just our paint channel
|
|
5607
|
-
self.ax.draw_artist(channel_image)
|
|
5608
|
-
# Blit everything
|
|
5609
|
-
self.canvas.blit(self.ax.bbox)
|
|
5610
|
-
self.canvas.flush_events()
|
|
5611
|
-
else:
|
|
5612
|
-
# Fallback to full draw if no static background
|
|
5613
|
-
self.canvas.draw()
|
|
5614
|
-
else:
|
|
5615
|
-
# Fallback if channel image not found
|
|
5616
|
-
self.canvas.draw()
|
|
5617
|
-
|
|
5618
5423
|
def get_channel_image(self, channel):
|
|
5619
5424
|
"""Find the matplotlib image object for a specific channel."""
|
|
5620
5425
|
if not hasattr(self.ax, 'images'):
|
|
@@ -5744,6 +5549,13 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5744
5549
|
dialog = CodeDialog(self, sort = sort)
|
|
5745
5550
|
dialog.exec()
|
|
5746
5551
|
|
|
5552
|
+
def handle_umap(self):
|
|
5553
|
+
|
|
5554
|
+
if my_network.node_centroids is None:
|
|
5555
|
+
self.show_centroid_dialog()
|
|
5556
|
+
|
|
5557
|
+
my_network.centroid_umap()
|
|
5558
|
+
|
|
5747
5559
|
def closeEvent(self, event):
|
|
5748
5560
|
"""Override closeEvent to close all windows when main window closes"""
|
|
5749
5561
|
|
|
@@ -7264,24 +7076,29 @@ class ColorOverlayDialog(QDialog):
|
|
|
7264
7076
|
|
|
7265
7077
|
def coloroverlay(self):
|
|
7266
7078
|
|
|
7267
|
-
|
|
7079
|
+
try:
|
|
7268
7080
|
|
|
7269
|
-
|
|
7270
|
-
mode = 0
|
|
7271
|
-
self.sort = 'Node'
|
|
7272
|
-
else:
|
|
7273
|
-
mode = 1
|
|
7274
|
-
self.sort = 'Edge'
|
|
7081
|
+
down_factor = float(self.down_factor.text()) if self.down_factor.text().strip() else None
|
|
7275
7082
|
|
|
7083
|
+
if self.parent().active_channel == 0:
|
|
7084
|
+
mode = 0
|
|
7085
|
+
self.sort = 'Node'
|
|
7086
|
+
else:
|
|
7087
|
+
mode = 1
|
|
7088
|
+
self.sort = 'Edge'
|
|
7276
7089
|
|
|
7277
|
-
result, legend = my_network.node_to_color(down_factor = down_factor, mode = mode)
|
|
7278
7090
|
|
|
7279
|
-
|
|
7091
|
+
result, legend = my_network.node_to_color(down_factor = down_factor, mode = mode)
|
|
7280
7092
|
|
|
7093
|
+
self.parent().format_for_upperright_table(legend, f'{self.sort} Id', f'Encoding Val: {self.sort}', 'Legend')
|
|
7281
7094
|
|
|
7282
|
-
self.parent().load_channel(3, channel_data = result, data = True)
|
|
7283
7095
|
|
|
7284
|
-
|
|
7096
|
+
self.parent().load_channel(3, channel_data = result, data = True)
|
|
7097
|
+
|
|
7098
|
+
self.accept()
|
|
7099
|
+
|
|
7100
|
+
except:
|
|
7101
|
+
pass
|
|
7285
7102
|
|
|
7286
7103
|
|
|
7287
7104
|
class ShuffleDialog(QDialog):
|
|
@@ -7624,7 +7441,7 @@ class ComNeighborDialog(QDialog):
|
|
|
7624
7441
|
# weighted checkbox (default True)
|
|
7625
7442
|
self.proportional = QPushButton("Robust")
|
|
7626
7443
|
self.proportional.setCheckable(True)
|
|
7627
|
-
self.proportional.setChecked(
|
|
7444
|
+
self.proportional.setChecked(True)
|
|
7628
7445
|
layout.addRow("Return Node Type Distribution Robust Heatmaps (ie, will give two more heatmaps that are not beholden to the total number of nodes of each type, representing which structures are overrepresented in a network):", self.proportional)
|
|
7629
7446
|
|
|
7630
7447
|
self.mode = QComboBox()
|
|
@@ -9503,6 +9320,12 @@ class ThresholdDialog(QDialog):
|
|
|
9503
9320
|
|
|
9504
9321
|
def start_ml(self, GPU = False):
|
|
9505
9322
|
|
|
9323
|
+
try:
|
|
9324
|
+
print("Please select image to load into nodes channel for segmentation or press X if you already have the one you want. Note that this load may permit a color image in the nodes channel for segmentation purposes only, which is otherwise not allowed.")
|
|
9325
|
+
self.parent().load_channel(0, color = True)
|
|
9326
|
+
except:
|
|
9327
|
+
pass
|
|
9328
|
+
|
|
9506
9329
|
|
|
9507
9330
|
if self.parent().channel_data[2] is not None or self.parent().channel_data[3] is not None or self.parent().highlight_overlay is not None:
|
|
9508
9331
|
if self.confirm_machine_dialog():
|
|
@@ -9619,206 +9442,210 @@ class MachineWindow(QMainWindow):
|
|
|
9619
9442
|
def __init__(self, parent=None, GPU = False):
|
|
9620
9443
|
super().__init__(parent)
|
|
9621
9444
|
|
|
9622
|
-
|
|
9623
|
-
|
|
9624
|
-
|
|
9625
|
-
|
|
9626
|
-
|
|
9627
|
-
|
|
9445
|
+
try:
|
|
9446
|
+
|
|
9447
|
+
if self.parent().active_channel == 0:
|
|
9448
|
+
if self.parent().channel_data[0] is not None:
|
|
9449
|
+
try:
|
|
9450
|
+
active_data = self.parent().channel_data[0]
|
|
9451
|
+
act_channel = 0
|
|
9452
|
+
except:
|
|
9453
|
+
active_data = self.parent().channel_data[1]
|
|
9454
|
+
act_channel = 1
|
|
9455
|
+
else:
|
|
9628
9456
|
active_data = self.parent().channel_data[1]
|
|
9629
9457
|
act_channel = 1
|
|
9630
|
-
else:
|
|
9631
|
-
active_data = self.parent().channel_data[1]
|
|
9632
|
-
act_channel = 1
|
|
9633
9458
|
|
|
9634
|
-
|
|
9635
|
-
|
|
9636
|
-
|
|
9637
|
-
|
|
9638
|
-
|
|
9459
|
+
try:
|
|
9460
|
+
if len(active_data.shape) == 3:
|
|
9461
|
+
array1 = np.zeros_like(active_data).astype(np.uint8)
|
|
9462
|
+
elif len(active_data.shape) == 4:
|
|
9463
|
+
array1 = np.zeros_like(active_data)[:,:,:,0].astype(np.uint8)
|
|
9464
|
+
except:
|
|
9465
|
+
print("No data in nodes channel")
|
|
9466
|
+
return
|
|
9639
9467
|
|
|
9640
|
-
|
|
9641
|
-
|
|
9642
|
-
|
|
9643
|
-
|
|
9644
|
-
|
|
9645
|
-
|
|
9468
|
+
self.setWindowTitle("Threshold")
|
|
9469
|
+
|
|
9470
|
+
# Create central widget and layout
|
|
9471
|
+
central_widget = QWidget()
|
|
9472
|
+
self.setCentralWidget(central_widget)
|
|
9473
|
+
layout = QVBoxLayout(central_widget)
|
|
9646
9474
|
|
|
9647
9475
|
|
|
9648
|
-
|
|
9649
|
-
|
|
9476
|
+
# Create form layout for inputs
|
|
9477
|
+
form_layout = QFormLayout()
|
|
9650
9478
|
|
|
9651
|
-
|
|
9479
|
+
layout.addLayout(form_layout)
|
|
9652
9480
|
|
|
9653
|
-
|
|
9654
|
-
|
|
9655
|
-
|
|
9656
|
-
|
|
9657
|
-
|
|
9481
|
+
if self.parent().pen_button.isChecked(): #Disable the pen mode if the user is in it because the segmenter pen forks it
|
|
9482
|
+
self.parent().pen_button.click()
|
|
9483
|
+
self.parent().threed = False
|
|
9484
|
+
self.parent().can = False
|
|
9485
|
+
self.parent().last_change = None
|
|
9658
9486
|
|
|
9659
|
-
|
|
9487
|
+
self.parent().pen_button.setEnabled(False)
|
|
9660
9488
|
|
|
9661
|
-
|
|
9662
|
-
|
|
9489
|
+
array3 = np.zeros_like(array1).astype(np.uint8)
|
|
9490
|
+
self.parent().highlight_overlay = array3 #Clear this out for the segmenter to use
|
|
9663
9491
|
|
|
9664
|
-
|
|
9665
|
-
|
|
9666
|
-
|
|
9667
|
-
|
|
9668
|
-
|
|
9669
|
-
|
|
9670
|
-
|
|
9492
|
+
self.parent().load_channel(2, array1, True)
|
|
9493
|
+
# Enable the channel button
|
|
9494
|
+
# Not exactly sure why we need all this but the channel buttons weren't loading like they normally do when load_channel() is called:
|
|
9495
|
+
if not self.parent().channel_buttons[2].isEnabled():
|
|
9496
|
+
self.parent().channel_buttons[2].setEnabled(True)
|
|
9497
|
+
self.parent().channel_buttons[2].click()
|
|
9498
|
+
self.parent().delete_buttons[2].setEnabled(True)
|
|
9671
9499
|
|
|
9672
|
-
|
|
9673
|
-
|
|
9500
|
+
if len(active_data.shape) == 3:
|
|
9501
|
+
self.parent().base_colors[act_channel] = self.parent().color_dictionary['WHITE']
|
|
9502
|
+
self.parent().base_colors[2] = self.parent().color_dictionary['LIGHT_GREEN']
|
|
9674
9503
|
|
|
9675
|
-
|
|
9676
|
-
|
|
9677
|
-
|
|
9678
|
-
|
|
9679
|
-
|
|
9504
|
+
self.parent().update_display()
|
|
9505
|
+
|
|
9506
|
+
# Set a reasonable default size for the window
|
|
9507
|
+
self.setMinimumWidth(600) # Increased to accommodate grouped buttons
|
|
9508
|
+
self.setMinimumHeight(500)
|
|
9509
|
+
|
|
9510
|
+
# Create main layout container
|
|
9511
|
+
main_widget = QWidget()
|
|
9512
|
+
main_layout = QVBoxLayout(main_widget)
|
|
9513
|
+
|
|
9514
|
+
# Group 1: Drawing tools (Brush + Foreground/Background)
|
|
9515
|
+
drawing_group = QGroupBox("Drawing Tools")
|
|
9516
|
+
drawing_layout = QHBoxLayout()
|
|
9517
|
+
|
|
9518
|
+
# Brush button
|
|
9519
|
+
self.brush_button = QPushButton("🖌️")
|
|
9520
|
+
self.brush_button.setCheckable(True)
|
|
9521
|
+
self.brush_button.setFixedSize(40, 40)
|
|
9522
|
+
self.brush_button.clicked.connect(self.toggle_brush_mode)
|
|
9523
|
+
self.brush_button.click()
|
|
9524
|
+
|
|
9525
|
+
# Foreground/Background buttons in their own horizontal layout
|
|
9526
|
+
fb_layout = QHBoxLayout()
|
|
9527
|
+
self.fore_button = QPushButton("Foreground")
|
|
9528
|
+
self.fore_button.setCheckable(True)
|
|
9529
|
+
self.fore_button.setChecked(True)
|
|
9530
|
+
self.fore_button.clicked.connect(self.toggle_foreground)
|
|
9680
9531
|
|
|
9681
|
-
|
|
9682
|
-
|
|
9683
|
-
|
|
9532
|
+
self.back_button = QPushButton("Background")
|
|
9533
|
+
self.back_button.setCheckable(True)
|
|
9534
|
+
self.back_button.setChecked(False)
|
|
9535
|
+
self.back_button.clicked.connect(self.toggle_background)
|
|
9684
9536
|
|
|
9685
|
-
|
|
9686
|
-
|
|
9687
|
-
drawing_layout = QHBoxLayout()
|
|
9537
|
+
fb_layout.addWidget(self.fore_button)
|
|
9538
|
+
fb_layout.addWidget(self.back_button)
|
|
9688
9539
|
|
|
9689
|
-
|
|
9690
|
-
|
|
9691
|
-
|
|
9692
|
-
self.brush_button.setFixedSize(40, 40)
|
|
9693
|
-
self.brush_button.clicked.connect(self.toggle_brush_mode)
|
|
9694
|
-
self.brush_button.click()
|
|
9540
|
+
drawing_layout.addWidget(self.brush_button)
|
|
9541
|
+
drawing_layout.addLayout(fb_layout)
|
|
9542
|
+
drawing_group.setLayout(drawing_layout)
|
|
9695
9543
|
|
|
9696
|
-
|
|
9697
|
-
|
|
9698
|
-
|
|
9699
|
-
self.fore_button.setCheckable(True)
|
|
9700
|
-
self.fore_button.setChecked(True)
|
|
9701
|
-
self.fore_button.clicked.connect(self.toggle_foreground)
|
|
9544
|
+
# Group 2: Processing Options (GPU)
|
|
9545
|
+
processing_group = QGroupBox("Processing Options")
|
|
9546
|
+
processing_layout = QHBoxLayout()
|
|
9702
9547
|
|
|
9703
|
-
|
|
9704
|
-
|
|
9705
|
-
|
|
9706
|
-
|
|
9548
|
+
self.use_gpu = GPU
|
|
9549
|
+
self.two = QPushButton("Train By 2D Slice Patterns")
|
|
9550
|
+
self.two.setCheckable(True)
|
|
9551
|
+
self.two.setChecked(False)
|
|
9552
|
+
self.two.clicked.connect(self.toggle_two)
|
|
9553
|
+
self.use_two = False
|
|
9554
|
+
self.three = QPushButton("Train by 3D Patterns")
|
|
9555
|
+
self.three.setCheckable(True)
|
|
9556
|
+
self.three.setChecked(True)
|
|
9557
|
+
self.three.clicked.connect(self.toggle_three)
|
|
9558
|
+
self.GPU = QPushButton("GPU")
|
|
9559
|
+
self.GPU.setCheckable(True)
|
|
9560
|
+
self.GPU.setChecked(False)
|
|
9561
|
+
self.GPU.clicked.connect(self.toggle_GPU)
|
|
9562
|
+
processing_layout.addWidget(self.GPU)
|
|
9563
|
+
processing_layout.addWidget(self.two)
|
|
9564
|
+
processing_layout.addWidget(self.three)
|
|
9565
|
+
processing_group.setLayout(processing_layout)
|
|
9566
|
+
|
|
9567
|
+
# Group 3: Training Options
|
|
9568
|
+
training_group = QGroupBox("Training")
|
|
9569
|
+
training_layout = QHBoxLayout()
|
|
9570
|
+
train_quick = QPushButton("Train Quick Model (When Good SNR)")
|
|
9571
|
+
train_quick.clicked.connect(lambda: self.train_model(speed=True))
|
|
9572
|
+
train_detailed = QPushButton("Train Detailed Model (For Morphology)")
|
|
9573
|
+
train_detailed.clicked.connect(lambda: self.train_model(speed=False))
|
|
9574
|
+
save = QPushButton("Save Model")
|
|
9575
|
+
save.clicked.connect(self.save_model)
|
|
9576
|
+
load = QPushButton("Load Model")
|
|
9577
|
+
load.clicked.connect(self.load_model)
|
|
9578
|
+
training_layout.addWidget(train_quick)
|
|
9579
|
+
training_layout.addWidget(train_detailed)
|
|
9580
|
+
training_layout.addWidget(save)
|
|
9581
|
+
training_layout.addWidget(load)
|
|
9582
|
+
training_group.setLayout(training_layout)
|
|
9583
|
+
|
|
9584
|
+
# Group 4: Segmentation Options
|
|
9585
|
+
segmentation_group = QGroupBox("Segmentation")
|
|
9586
|
+
segmentation_layout = QHBoxLayout()
|
|
9587
|
+
seg_button = QPushButton("Preview Segment")
|
|
9588
|
+
self.seg_button = seg_button
|
|
9589
|
+
seg_button.clicked.connect(self.start_segmentation)
|
|
9590
|
+
self.pause_button = QPushButton("▶/⏸️")
|
|
9591
|
+
self.pause_button.setFixedSize(40, 40)
|
|
9592
|
+
self.pause_button.clicked.connect(self.toggle_segment)
|
|
9593
|
+
self.lock_button = QPushButton("🔒 Memory lock - (Prioritize RAM)")
|
|
9594
|
+
self.lock_button.setCheckable(True)
|
|
9595
|
+
self.lock_button.setChecked(True)
|
|
9596
|
+
self.lock_button.clicked.connect(self.toggle_lock)
|
|
9597
|
+
self.mem_lock = True
|
|
9598
|
+
full_button = QPushButton("Segment All")
|
|
9599
|
+
full_button.clicked.connect(self.segment)
|
|
9600
|
+
segmentation_layout.addWidget(seg_button)
|
|
9601
|
+
segmentation_layout.addWidget(self.pause_button) # <--- for some reason the segmenter preview is still running even when killed, may be regenerating itself somewhere. May or may not actually try to resolve this because this feature isnt that necessary.
|
|
9602
|
+
#segmentation_layout.addWidget(self.lock_button) # Also turned this off
|
|
9603
|
+
segmentation_layout.addWidget(full_button)
|
|
9604
|
+
segmentation_group.setLayout(segmentation_layout)
|
|
9605
|
+
|
|
9606
|
+
# Add all groups to main layout
|
|
9607
|
+
main_layout.addWidget(drawing_group)
|
|
9608
|
+
if not GPU:
|
|
9609
|
+
main_layout.addWidget(processing_group)
|
|
9610
|
+
main_layout.addWidget(training_group)
|
|
9611
|
+
main_layout.addWidget(segmentation_group)
|
|
9612
|
+
|
|
9613
|
+
# Set the main widget as the central widget
|
|
9614
|
+
self.setCentralWidget(main_widget)
|
|
9615
|
+
|
|
9616
|
+
self.trained = False
|
|
9617
|
+
self.previewing = False
|
|
9707
9618
|
|
|
9708
|
-
|
|
9709
|
-
|
|
9619
|
+
if not GPU:
|
|
9620
|
+
self.segmenter = segmenter.InteractiveSegmenter(active_data, use_gpu=False)
|
|
9621
|
+
else:
|
|
9622
|
+
self.segmenter = seg_GPU.InteractiveSegmenter(active_data)
|
|
9710
9623
|
|
|
9711
|
-
|
|
9712
|
-
drawing_layout.addLayout(fb_layout)
|
|
9713
|
-
drawing_group.setLayout(drawing_layout)
|
|
9624
|
+
self.segmentation_worker = None
|
|
9714
9625
|
|
|
9715
|
-
|
|
9716
|
-
|
|
9717
|
-
processing_layout = QHBoxLayout()
|
|
9718
|
-
|
|
9719
|
-
self.use_gpu = GPU
|
|
9720
|
-
self.two = QPushButton("Train By 2D Slice Patterns")
|
|
9721
|
-
self.two.setCheckable(True)
|
|
9722
|
-
self.two.setChecked(False)
|
|
9723
|
-
self.two.clicked.connect(self.toggle_two)
|
|
9724
|
-
self.use_two = False
|
|
9725
|
-
self.three = QPushButton("Train by 3D Patterns")
|
|
9726
|
-
self.three.setCheckable(True)
|
|
9727
|
-
self.three.setChecked(True)
|
|
9728
|
-
self.three.clicked.connect(self.toggle_three)
|
|
9729
|
-
self.GPU = QPushButton("GPU")
|
|
9730
|
-
self.GPU.setCheckable(True)
|
|
9731
|
-
self.GPU.setChecked(False)
|
|
9732
|
-
self.GPU.clicked.connect(self.toggle_GPU)
|
|
9733
|
-
processing_layout.addWidget(self.GPU)
|
|
9734
|
-
processing_layout.addWidget(self.two)
|
|
9735
|
-
processing_layout.addWidget(self.three)
|
|
9736
|
-
processing_group.setLayout(processing_layout)
|
|
9626
|
+
self.fore_button.click()
|
|
9627
|
+
self.fore_button.click()
|
|
9737
9628
|
|
|
9738
|
-
|
|
9739
|
-
training_group = QGroupBox("Training")
|
|
9740
|
-
training_layout = QHBoxLayout()
|
|
9741
|
-
train_quick = QPushButton("Train Quick Model")
|
|
9742
|
-
train_quick.clicked.connect(lambda: self.train_model(speed=True))
|
|
9743
|
-
train_detailed = QPushButton("Train More Detailed Model")
|
|
9744
|
-
train_detailed.clicked.connect(lambda: self.train_model(speed=False))
|
|
9745
|
-
save = QPushButton("Save Model")
|
|
9746
|
-
save.clicked.connect(self.save_model)
|
|
9747
|
-
load = QPushButton("Load Model")
|
|
9748
|
-
load.clicked.connect(self.load_model)
|
|
9749
|
-
training_layout.addWidget(train_quick)
|
|
9750
|
-
training_layout.addWidget(train_detailed)
|
|
9751
|
-
training_layout.addWidget(save)
|
|
9752
|
-
training_layout.addWidget(load)
|
|
9753
|
-
training_group.setLayout(training_layout)
|
|
9754
|
-
|
|
9755
|
-
# Group 4: Segmentation Options
|
|
9756
|
-
segmentation_group = QGroupBox("Segmentation")
|
|
9757
|
-
segmentation_layout = QHBoxLayout()
|
|
9758
|
-
seg_button = QPushButton("Preview Segment")
|
|
9759
|
-
self.seg_button = seg_button
|
|
9760
|
-
seg_button.clicked.connect(self.start_segmentation)
|
|
9761
|
-
self.pause_button = QPushButton("▶/⏸️")
|
|
9762
|
-
self.pause_button.clicked.connect(self.pause)
|
|
9763
|
-
self.lock_button = QPushButton("🔒 Memory lock - (Prioritize RAM)")
|
|
9764
|
-
self.lock_button.setCheckable(True)
|
|
9765
|
-
self.lock_button.setChecked(True)
|
|
9766
|
-
self.lock_button.clicked.connect(self.toggle_lock)
|
|
9767
|
-
self.mem_lock = True
|
|
9768
|
-
full_button = QPushButton("Segment All")
|
|
9769
|
-
full_button.clicked.connect(self.segment)
|
|
9770
|
-
segmentation_layout.addWidget(seg_button)
|
|
9771
|
-
#segmentation_layout.addWidget(self.pause_button) # <--- for some reason the segmenter preview is still running even when killed, may be regenerating itself somewhere. May or may not actually try to resolve this because this feature isnt that necessary.
|
|
9772
|
-
#segmentation_layout.addWidget(self.lock_button) # Also turned this off
|
|
9773
|
-
segmentation_layout.addWidget(full_button)
|
|
9774
|
-
segmentation_group.setLayout(segmentation_layout)
|
|
9775
|
-
|
|
9776
|
-
# Add all groups to main layout
|
|
9777
|
-
main_layout.addWidget(drawing_group)
|
|
9778
|
-
if not GPU:
|
|
9779
|
-
main_layout.addWidget(processing_group)
|
|
9780
|
-
main_layout.addWidget(training_group)
|
|
9781
|
-
main_layout.addWidget(segmentation_group)
|
|
9782
|
-
|
|
9783
|
-
# Set the main widget as the central widget
|
|
9784
|
-
self.setCentralWidget(main_widget)
|
|
9629
|
+
self.num_chunks = 0
|
|
9785
9630
|
|
|
9786
|
-
|
|
9787
|
-
|
|
9631
|
+
except:
|
|
9632
|
+
return
|
|
9788
9633
|
|
|
9789
|
-
|
|
9790
|
-
self.segmenter = segmenter.InteractiveSegmenter(active_data, use_gpu=False)
|
|
9791
|
-
else:
|
|
9792
|
-
self.segmenter = seg_GPU.InteractiveSegmenter(active_data)
|
|
9634
|
+
def toggle_segment(self):
|
|
9793
9635
|
|
|
9794
|
-
self.segmentation_worker
|
|
9636
|
+
if self.segmentation_worker is not None:
|
|
9637
|
+
if not self.segmentation_worker._paused:
|
|
9638
|
+
self.segmentation_worker.pause()
|
|
9639
|
+
print("Segmentation Worker Paused")
|
|
9640
|
+
elif self.segmentation_worker._paused:
|
|
9641
|
+
self.segmentation_worker.resume()
|
|
9642
|
+
print("Segmentation Worker Resuming")
|
|
9795
9643
|
|
|
9796
|
-
self.fore_button.click()
|
|
9797
|
-
self.fore_button.click()
|
|
9798
9644
|
|
|
9799
9645
|
def toggle_lock(self):
|
|
9800
9646
|
|
|
9801
9647
|
self.mem_lock = self.lock_button.isChecked()
|
|
9802
9648
|
|
|
9803
|
-
def pause(self):
|
|
9804
|
-
|
|
9805
|
-
if self.segmentation_worker is not None:
|
|
9806
|
-
try:
|
|
9807
|
-
print("Pausing segmenter")
|
|
9808
|
-
self.previewing = False
|
|
9809
|
-
self.segmentation_finished
|
|
9810
|
-
del self.segmentation_worker
|
|
9811
|
-
self.segmentation_worker = None
|
|
9812
|
-
except:
|
|
9813
|
-
pass
|
|
9814
|
-
|
|
9815
|
-
else:
|
|
9816
|
-
try:
|
|
9817
|
-
print("Restarting segmenter")
|
|
9818
|
-
self.previewing = True
|
|
9819
|
-
self.start_segmentation
|
|
9820
|
-
except:
|
|
9821
|
-
pass
|
|
9822
9649
|
|
|
9823
9650
|
def save_model(self):
|
|
9824
9651
|
|
|
@@ -9840,6 +9667,9 @@ class MachineWindow(QMainWindow):
|
|
|
9840
9667
|
|
|
9841
9668
|
except Exception as e:
|
|
9842
9669
|
print(f"Error saving model: {e}")
|
|
9670
|
+
import traceback
|
|
9671
|
+
traceback.print_exc()
|
|
9672
|
+
|
|
9843
9673
|
|
|
9844
9674
|
def load_model(self):
|
|
9845
9675
|
|
|
@@ -9935,7 +9765,23 @@ class MachineWindow(QMainWindow):
|
|
|
9935
9765
|
def toggle_brush_mode(self):
|
|
9936
9766
|
"""Toggle brush mode on/off"""
|
|
9937
9767
|
self.parent().brush_mode = self.brush_button.isChecked()
|
|
9768
|
+
|
|
9938
9769
|
if self.parent().brush_mode:
|
|
9770
|
+
|
|
9771
|
+
self.parent().pm = painting.PaintManager(parent = self.parent())
|
|
9772
|
+
|
|
9773
|
+
# Start virtual paint session
|
|
9774
|
+
# Get current zoom to preserve it
|
|
9775
|
+
current_xlim = self.parent().ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
9776
|
+
current_ylim = self.parent().ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
9777
|
+
|
|
9778
|
+
if self.parent().pen_button.isChecked():
|
|
9779
|
+
channel = self.parent().active_channel
|
|
9780
|
+
else:
|
|
9781
|
+
channel = 2
|
|
9782
|
+
|
|
9783
|
+
self.parent().pm.initiate_paint_session(channel, current_xlim, current_ylim)
|
|
9784
|
+
|
|
9939
9785
|
self.parent().pan_button.setChecked(False)
|
|
9940
9786
|
self.parent().zoom_button.setChecked(False)
|
|
9941
9787
|
if self.parent().pan_mode:
|
|
@@ -9962,12 +9808,13 @@ class MachineWindow(QMainWindow):
|
|
|
9962
9808
|
self.kill_segmentation()
|
|
9963
9809
|
# Wait a bit for cleanup
|
|
9964
9810
|
time.sleep(0.1)
|
|
9965
|
-
|
|
9966
|
-
|
|
9811
|
+
|
|
9812
|
+
self.previewing = True
|
|
9967
9813
|
try:
|
|
9968
9814
|
try:
|
|
9969
9815
|
self.segmenter.train_batch(self.parent().channel_data[2], speed = speed, use_gpu = self.use_gpu, use_two = self.use_two, mem_lock = self.mem_lock)
|
|
9970
9816
|
self.trained = True
|
|
9817
|
+
self.start_segmentation()
|
|
9971
9818
|
except Exception as e:
|
|
9972
9819
|
print("Error training. Perhaps you forgot both foreground and background markers? I need both!")
|
|
9973
9820
|
import traceback
|
|
@@ -9983,6 +9830,8 @@ class MachineWindow(QMainWindow):
|
|
|
9983
9830
|
|
|
9984
9831
|
def start_segmentation(self):
|
|
9985
9832
|
|
|
9833
|
+
self.parent().static_background = None
|
|
9834
|
+
|
|
9986
9835
|
self.kill_segmentation()
|
|
9987
9836
|
time.sleep(0.1)
|
|
9988
9837
|
|
|
@@ -9991,12 +9840,10 @@ class MachineWindow(QMainWindow):
|
|
|
9991
9840
|
else:
|
|
9992
9841
|
print("Beginning new segmentation...")
|
|
9993
9842
|
|
|
9994
|
-
|
|
9995
|
-
|
|
9996
|
-
|
|
9997
|
-
|
|
9998
|
-
else:
|
|
9999
|
-
active_data = self.parent().channel_data[1]
|
|
9843
|
+
if self.parent().channel_data[2] is not None:
|
|
9844
|
+
active_data = self.parent().channel_data[2]
|
|
9845
|
+
else:
|
|
9846
|
+
active_data = self.parent().channel_data[0]
|
|
10000
9847
|
|
|
10001
9848
|
array3 = np.zeros_like(active_data).astype(np.uint8)
|
|
10002
9849
|
self.parent().highlight_overlay = array3 #Clear this out for the segmenter to use
|
|
@@ -10005,8 +9852,7 @@ class MachineWindow(QMainWindow):
|
|
|
10005
9852
|
return
|
|
10006
9853
|
else:
|
|
10007
9854
|
self.segmentation_worker = SegmentationWorker(self.parent().highlight_overlay, self.segmenter, self.use_gpu, self.use_two, self.previewing, self, self.mem_lock)
|
|
10008
|
-
self.segmentation_worker.chunk_processed.connect(self.update_display) # Just update display
|
|
10009
|
-
self.segmentation_worker.finished.connect(self.segmentation_finished)
|
|
9855
|
+
self.segmentation_worker.chunk_processed.connect(lambda: self.update_display(skip_paint_reinit = True)) # Just update display
|
|
10010
9856
|
current_xlim = self.parent().ax.get_xlim()
|
|
10011
9857
|
current_ylim = self.parent().ax.get_ylim()
|
|
10012
9858
|
try:
|
|
@@ -10059,7 +9905,7 @@ class MachineWindow(QMainWindow):
|
|
|
10059
9905
|
|
|
10060
9906
|
return changed
|
|
10061
9907
|
|
|
10062
|
-
def update_display(self):
|
|
9908
|
+
def update_display(self, skip_paint_reinit = False):
|
|
10063
9909
|
if not hasattr(self, '_last_update'):
|
|
10064
9910
|
self._last_update = 0
|
|
10065
9911
|
|
|
@@ -10069,8 +9915,7 @@ class MachineWindow(QMainWindow):
|
|
|
10069
9915
|
|
|
10070
9916
|
self._last_z = current_z
|
|
10071
9917
|
|
|
10072
|
-
|
|
10073
|
-
changed = self.check_for_z_change()
|
|
9918
|
+
self.num_chunks += 1
|
|
10074
9919
|
|
|
10075
9920
|
current_time = time.time()
|
|
10076
9921
|
if current_time - self._last_update >= 1: # Match worker's interval
|
|
@@ -10087,71 +9932,40 @@ class MachineWindow(QMainWindow):
|
|
|
10087
9932
|
|
|
10088
9933
|
if not self.parent().painting:
|
|
10089
9934
|
# Only update if view limits are valid
|
|
10090
|
-
self.parent().update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
9935
|
+
self.parent().update_display(preserve_zoom=(current_xlim, current_ylim), skip_paint_reinit = skip_paint_reinit)
|
|
9936
|
+
|
|
9937
|
+
if self.parent().brush_mode:
|
|
9938
|
+
current_xlim = self.parent().ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
9939
|
+
current_ylim = self.parent().ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
9940
|
+
|
|
9941
|
+
if self.parent().pen_button.isChecked():
|
|
9942
|
+
channel = self.parent().active_channel
|
|
9943
|
+
else:
|
|
9944
|
+
channel = 2
|
|
10091
9945
|
|
|
9946
|
+
self.parent().pm.initiate_paint_session(channel, current_xlim, current_ylim)
|
|
10092
9947
|
|
|
10093
9948
|
self._last_update = current_time
|
|
10094
9949
|
except Exception as e:
|
|
10095
9950
|
print(f"Display update error: {e}")
|
|
10096
9951
|
|
|
10097
9952
|
def poke_segmenter(self):
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
|
|
10101
|
-
|
|
10102
|
-
self.segmenter._currently_processing = None
|
|
10103
|
-
|
|
10104
|
-
# Force regenerating the worker
|
|
10105
|
-
if self.segmentation_worker is not None:
|
|
10106
|
-
self.kill_segmentation()
|
|
10107
|
-
|
|
10108
|
-
time.sleep(0.2)
|
|
10109
|
-
self.start_segmentation()
|
|
9953
|
+
try:
|
|
9954
|
+
# Clear any processing flags in the segmenter
|
|
9955
|
+
if hasattr(self.segmenter, '_currently_processing'):
|
|
9956
|
+
self.segmenter._currently_processing = None
|
|
10110
9957
|
|
|
10111
|
-
|
|
10112
|
-
|
|
10113
|
-
|
|
10114
|
-
traceback.print_exc()
|
|
10115
|
-
|
|
10116
|
-
def segmentation_finished(self):
|
|
10117
|
-
|
|
10118
|
-
current_xlim = self.parent().ax.get_xlim()
|
|
10119
|
-
current_ylim = self.parent().ax.get_ylim()
|
|
10120
|
-
self.parent().update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
10121
|
-
|
|
10122
|
-
# Store the current z position before killing the worker
|
|
10123
|
-
current_z = self.parent().current_slice
|
|
10124
|
-
|
|
10125
|
-
# Clean up the worker
|
|
10126
|
-
self.kill_segmentation()
|
|
10127
|
-
self.segmentation_worker = None
|
|
10128
|
-
time.sleep(0.1)
|
|
10129
|
-
|
|
10130
|
-
# Auto-restart for 2D preview mode only if certain conditions are met
|
|
10131
|
-
if self.previewing and self.use_two:
|
|
10132
|
-
# Track when this slice was last processed
|
|
10133
|
-
if not hasattr(self, '_processed_slices'):
|
|
10134
|
-
self._processed_slices = {}
|
|
9958
|
+
# Force regenerating the worker
|
|
9959
|
+
if self.segmentation_worker is not None:
|
|
9960
|
+
self.kill_segmentation()
|
|
10135
9961
|
|
|
10136
|
-
|
|
9962
|
+
time.sleep(0.2)
|
|
9963
|
+
self.start_segmentation()
|
|
10137
9964
|
|
|
10138
|
-
|
|
10139
|
-
|
|
10140
|
-
|
|
10141
|
-
|
|
10142
|
-
recently_processed = time_since_last_attempt < 5.0 # 5 second cooldown
|
|
10143
|
-
|
|
10144
|
-
if not recently_processed:
|
|
10145
|
-
self._processed_slices[current_z] = current_time
|
|
10146
|
-
|
|
10147
|
-
# Reset any processing flags in the segmenter
|
|
10148
|
-
if hasattr(self.segmenter, '_currently_processing'):
|
|
10149
|
-
self.segmenter._currently_processing = None
|
|
10150
|
-
|
|
10151
|
-
if 0 in self.parent().highlight_overlay[current_z, :, :]:
|
|
10152
|
-
# Create a new worker after a brief delay
|
|
10153
|
-
QTimer.singleShot(500, self.start_segmentation)
|
|
10154
|
-
|
|
9965
|
+
except Exception as e:
|
|
9966
|
+
print(f"Error in poke_segmenter: {e}")
|
|
9967
|
+
import traceback
|
|
9968
|
+
traceback.print_exc()
|
|
10155
9969
|
|
|
10156
9970
|
|
|
10157
9971
|
def kill_segmentation(self):
|
|
@@ -10185,11 +9999,10 @@ class MachineWindow(QMainWindow):
|
|
|
10185
9999
|
|
|
10186
10000
|
self.previewing = False
|
|
10187
10001
|
|
|
10188
|
-
if self.parent().
|
|
10189
|
-
|
|
10190
|
-
|
|
10191
|
-
|
|
10192
|
-
active_data = self.parent().channel_data[1]
|
|
10002
|
+
if self.parent().channel_data[2] is not None:
|
|
10003
|
+
active_data = self.parent().channel_data[2]
|
|
10004
|
+
else:
|
|
10005
|
+
active_data = self.parent().channel_data[0]
|
|
10193
10006
|
|
|
10194
10007
|
array3 = np.zeros_like(active_data).astype(np.uint8)
|
|
10195
10008
|
self.parent().highlight_overlay = array3 #Clear this out for the segmenter to use
|
|
@@ -10200,6 +10013,8 @@ class MachineWindow(QMainWindow):
|
|
|
10200
10013
|
self.parent().highlight_overlay = self.segmenter.segment_volume(array = self.parent().highlight_overlay)
|
|
10201
10014
|
except Exception as e:
|
|
10202
10015
|
print(f"Error segmenting (Perhaps retrain the model...): {e}")
|
|
10016
|
+
import traceback
|
|
10017
|
+
traceback.print_exc()
|
|
10203
10018
|
return
|
|
10204
10019
|
|
|
10205
10020
|
# Clean up when done
|
|
@@ -10236,6 +10051,12 @@ class MachineWindow(QMainWindow):
|
|
|
10236
10051
|
# Kill the segmentation thread and wait for it to finish
|
|
10237
10052
|
self.kill_segmentation()
|
|
10238
10053
|
time.sleep(0.2) # Give additional time for cleanup
|
|
10054
|
+
|
|
10055
|
+
try:
|
|
10056
|
+
self.parent().channel_data[0] = self.parent().reduce_rgb_dimension(self.parent().channel_data[0], 'weight')
|
|
10057
|
+
self.update_display()
|
|
10058
|
+
except:
|
|
10059
|
+
pass
|
|
10239
10060
|
|
|
10240
10061
|
self.parent().machine_window = None
|
|
10241
10062
|
else:
|
|
@@ -10296,59 +10117,28 @@ class SegmentationWorker(QThread):
|
|
|
10296
10117
|
|
|
10297
10118
|
# Remember the starting z position
|
|
10298
10119
|
self.starting_z = self.segmenter.current_z
|
|
10299
|
-
|
|
10300
|
-
if self.previewing and self.use_two:
|
|
10301
|
-
# Process current z-slice in chunks
|
|
10302
|
-
current_z = self.segmenter.current_z
|
|
10303
|
-
|
|
10304
|
-
# Process the slice with chunked generator
|
|
10305
|
-
for foreground, background in self.segmenter.segment_slice_chunked(current_z):
|
|
10306
|
-
# Check for pause/stop before processing each chunk
|
|
10307
|
-
self._check_pause()
|
|
10308
|
-
if self._stop:
|
|
10309
|
-
break
|
|
10310
|
-
|
|
10311
|
-
if foreground == None and background == None:
|
|
10312
|
-
self.get_poked()
|
|
10313
10120
|
|
|
10314
|
-
|
|
10315
|
-
|
|
10316
|
-
|
|
10317
|
-
|
|
10318
|
-
|
|
10319
|
-
|
|
10320
|
-
for z,y,x in background:
|
|
10321
|
-
self.overlay[z,y,x] = 2
|
|
10322
|
-
|
|
10323
|
-
# Signal update after each chunk
|
|
10324
|
-
self.chunks_since_update += 1
|
|
10325
|
-
current_time = time.time()
|
|
10326
|
-
if (self.chunks_since_update >= self.chunks_per_update and
|
|
10327
|
-
current_time - self.last_update >= self.update_interval):
|
|
10328
|
-
self.chunk_processed.emit()
|
|
10329
|
-
self.chunks_since_update = 0
|
|
10330
|
-
self.last_update = current_time
|
|
10121
|
+
# Original 3D approach
|
|
10122
|
+
for foreground_coords, background_coords in self.segmenter.segment_volume_realtime(gpu=self.use_gpu):
|
|
10123
|
+
# Check for pause/stop before processing each chunk
|
|
10124
|
+
self._check_pause()
|
|
10125
|
+
if self._stop:
|
|
10126
|
+
break
|
|
10331
10127
|
|
|
10332
|
-
|
|
10333
|
-
|
|
10334
|
-
for
|
|
10335
|
-
|
|
10336
|
-
|
|
10337
|
-
|
|
10338
|
-
|
|
10339
|
-
|
|
10340
|
-
|
|
10341
|
-
|
|
10342
|
-
|
|
10343
|
-
|
|
10344
|
-
|
|
10345
|
-
self.
|
|
10346
|
-
current_time = time.time()
|
|
10347
|
-
if (self.chunks_since_update >= self.chunks_per_update and
|
|
10348
|
-
current_time - self.last_update >= self.update_interval):
|
|
10349
|
-
self.chunk_processed.emit()
|
|
10350
|
-
self.chunks_since_update = 0
|
|
10351
|
-
self.last_update = current_time
|
|
10128
|
+
for z,y,x in foreground_coords:
|
|
10129
|
+
self.overlay[z,y,x] = 1
|
|
10130
|
+
for z,y,x in background_coords:
|
|
10131
|
+
self.overlay[z,y,x] = 2
|
|
10132
|
+
|
|
10133
|
+
self.chunks_since_update += 1
|
|
10134
|
+
current_time = time.time()
|
|
10135
|
+
if (self.chunks_since_update >= self.chunks_per_update and
|
|
10136
|
+
current_time - self.last_update >= self.update_interval):
|
|
10137
|
+
if self.machine_window.parent().shape[1] * self.machine_window.parent().shape[2] > 3000 * 3000: #arbitrary throttle for large arrays.
|
|
10138
|
+
self.msleep(3000)
|
|
10139
|
+
self.chunk_processed.emit()
|
|
10140
|
+
self.chunks_since_update = 0
|
|
10141
|
+
self.last_update = current_time
|
|
10352
10142
|
|
|
10353
10143
|
self.finished.emit()
|
|
10354
10144
|
|