nettracer3d 0.8.4__py3-none-any.whl → 0.8.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of nettracer3d might be problematic. Click here for more details.
- nettracer3d/community_extractor.py +3 -2
- nettracer3d/neighborhoods.py +140 -31
- nettracer3d/nettracer.py +10 -3
- nettracer3d/nettracer_gui.py +467 -703
- nettracer3d/painting.py +373 -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.5.dist-info}/METADATA +5 -2
- nettracer3d-0.8.5.dist-info/RECORD +25 -0
- {nettracer3d-0.8.4.dist-info → nettracer3d-0.8.5.dist-info}/licenses/LICENSE +2 -4
- nettracer3d-0.8.4.dist-info/RECORD +0 -24
- {nettracer3d-0.8.4.dist-info → nettracer3d-0.8.5.dist-info}/WHEEL +0 -0
- {nettracer3d-0.8.4.dist-info → nettracer3d-0.8.5.dist-info}/entry_points.txt +0 -0
- {nettracer3d-0.8.4.dist-info → nettracer3d-0.8.5.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
|
|
|
@@ -2170,11 +2161,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2170
2161
|
if self.zoom_mode:
|
|
2171
2162
|
self.pan_button.setChecked(False)
|
|
2172
2163
|
|
|
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
2164
|
self.pen_button.setChecked(False)
|
|
2179
2165
|
self.pan_mode = False
|
|
2180
2166
|
self.brush_mode = False
|
|
@@ -2184,6 +2170,11 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2184
2170
|
if self.machine_window is not None:
|
|
2185
2171
|
self.machine_window.silence_button()
|
|
2186
2172
|
self.canvas.setCursor(Qt.CursorShape.CrossCursor)
|
|
2173
|
+
if self.pan_mode or self.brush_mode:
|
|
2174
|
+
current_xlim = self.ax.get_xlim()
|
|
2175
|
+
current_ylim = self.ax.get_ylim()
|
|
2176
|
+
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
2177
|
+
|
|
2187
2178
|
else:
|
|
2188
2179
|
if self.machine_window is None:
|
|
2189
2180
|
self.canvas.setCursor(Qt.CursorShape.ArrowCursor)
|
|
@@ -2195,10 +2186,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2195
2186
|
"""Toggle pan mode on/off."""
|
|
2196
2187
|
self.pan_mode = self.pan_button.isChecked()
|
|
2197
2188
|
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
2189
|
|
|
2203
2190
|
self.zoom_button.setChecked(False)
|
|
2204
2191
|
self.pen_button.setChecked(False)
|
|
@@ -2210,6 +2197,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2210
2197
|
if self.machine_window is not None:
|
|
2211
2198
|
self.machine_window.silence_button()
|
|
2212
2199
|
self.canvas.setCursor(Qt.CursorShape.OpenHandCursor)
|
|
2200
|
+
if self.brush_mode:
|
|
2201
|
+
current_xlim = self.ax.get_xlim()
|
|
2202
|
+
current_ylim = self.ax.get_ylim()
|
|
2203
|
+
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
2213
2204
|
else:
|
|
2214
2205
|
current_xlim = self.ax.get_xlim()
|
|
2215
2206
|
current_ylim = self.ax.get_ylim()
|
|
@@ -2224,6 +2215,20 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2224
2215
|
self.brush_mode = self.pen_button.isChecked()
|
|
2225
2216
|
if self.brush_mode:
|
|
2226
2217
|
|
|
2218
|
+
self.pm = painting.PaintManager(parent = self)
|
|
2219
|
+
|
|
2220
|
+
# Start virtual paint session
|
|
2221
|
+
# Get current zoom to preserve it
|
|
2222
|
+
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
2223
|
+
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
2224
|
+
|
|
2225
|
+
if self.pen_button.isChecked():
|
|
2226
|
+
channel = self.active_channel
|
|
2227
|
+
else:
|
|
2228
|
+
channel = 2
|
|
2229
|
+
|
|
2230
|
+
self.pm.initiate_paint_session(channel, current_xlim, current_ylim)
|
|
2231
|
+
|
|
2227
2232
|
if self.pan_mode:
|
|
2228
2233
|
current_xlim = self.ax.get_xlim()
|
|
2229
2234
|
current_ylim = self.ax.get_ylim()
|
|
@@ -2443,37 +2448,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2443
2448
|
|
|
2444
2449
|
painter.end()
|
|
2445
2450
|
|
|
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
2451
|
def get_current_mouse_position(self):
|
|
2478
2452
|
# Get the main application's current mouse position
|
|
2479
2453
|
cursor_pos = QCursor.pos()
|
|
@@ -2486,15 +2460,25 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2486
2460
|
0 <= canvas_pos.y() < self.canvas.height()):
|
|
2487
2461
|
return 0, 0 # Mouse is outside of the matplotlib canvas
|
|
2488
2462
|
|
|
2489
|
-
#
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2463
|
+
# OPTION 1: Use matplotlib's built-in coordinate conversion
|
|
2464
|
+
# This accounts for figure margins, subplot positioning, etc.
|
|
2465
|
+
try:
|
|
2466
|
+
# Get the figure and axes bounds
|
|
2467
|
+
bbox = self.ax.bbox
|
|
2468
|
+
|
|
2469
|
+
# Convert widget coordinates to figure coordinates
|
|
2470
|
+
fig_x = canvas_pos.x()
|
|
2471
|
+
fig_y = self.canvas.height() - canvas_pos.y() # Flip Y coordinate
|
|
2472
|
+
|
|
2473
|
+
# Check if within axes bounds
|
|
2474
|
+
if (bbox.x0 <= fig_x <= bbox.x1 and bbox.y0 <= fig_y <= bbox.y1):
|
|
2475
|
+
# Transform to data coordinates
|
|
2476
|
+
data_coords = self.ax.transData.inverted().transform((fig_x, fig_y))
|
|
2477
|
+
return data_coords[0], data_coords[1]
|
|
2478
|
+
else:
|
|
2479
|
+
return 0, 0
|
|
2480
|
+
except:
|
|
2481
|
+
pass
|
|
2498
2482
|
|
|
2499
2483
|
def on_mouse_press(self, event):
|
|
2500
2484
|
"""Handle mouse press events."""
|
|
@@ -2533,61 +2517,50 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2533
2517
|
|
|
2534
2518
|
|
|
2535
2519
|
elif self.brush_mode:
|
|
2520
|
+
"""Handle brush mode with virtual painting."""
|
|
2536
2521
|
if event.inaxes != self.ax:
|
|
2537
2522
|
return
|
|
2538
2523
|
|
|
2539
2524
|
if event.button == 1 or event.button == 3:
|
|
2525
|
+
if self.machine_window is not None:
|
|
2526
|
+
if self.machine_window.segmentation_worker is not None:
|
|
2527
|
+
self.machine_window.segmentation_worker.pause()
|
|
2528
|
+
|
|
2540
2529
|
x, y = int(event.xdata), int(event.ydata)
|
|
2541
|
-
|
|
2542
|
-
|
|
2530
|
+
|
|
2531
|
+
# Get current zoom to preserve it
|
|
2543
2532
|
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
2544
2533
|
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))
|
|
2534
|
+
|
|
2535
|
+
if event.button == 1 and getattr(self, 'can', False):
|
|
2536
|
+
self.update_display(preserve_zoom=(current_xlim, current_ylim), skip_paint_reinit = True)
|
|
2549
2537
|
self.handle_can(x, y)
|
|
2550
2538
|
return
|
|
2551
|
-
|
|
2539
|
+
|
|
2540
|
+
# Determine erase mode and foreground/background
|
|
2552
2541
|
if event.button == 3:
|
|
2553
2542
|
self.erase = True
|
|
2554
|
-
self.update_display(preserve_zoom = (current_xlim, current_ylim))
|
|
2555
2543
|
else:
|
|
2556
2544
|
self.erase = False
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2545
|
+
|
|
2546
|
+
# Determine foreground/background for machine window mode
|
|
2547
|
+
foreground = getattr(self, 'foreground', True)
|
|
2548
|
+
|
|
2549
|
+
self.last_virtual_pos = (x, y)
|
|
2550
|
+
|
|
2561
2551
|
if self.pen_button.isChecked():
|
|
2562
2552
|
channel = self.active_channel
|
|
2563
2553
|
else:
|
|
2564
2554
|
channel = 2
|
|
2565
2555
|
|
|
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))
|
|
2556
|
+
self.pm.start_virtual_paint_session(channel, current_xlim, current_ylim)
|
|
2557
|
+
|
|
2558
|
+
# Add first virtual paint stroke
|
|
2559
|
+
brush_size = getattr(self, 'brush_size', 5)
|
|
2560
|
+
self.pm.add_virtual_paint_stroke(x, y, brush_size, self.erase, foreground)
|
|
2561
|
+
|
|
2562
|
+
# Update display with virtual paint
|
|
2563
|
+
self.pm.update_virtual_paint_display()
|
|
2591
2564
|
|
|
2592
2565
|
elif not self.zoom_mode and event.button == 3: # Right click (for context menu)
|
|
2593
2566
|
self.create_context_menu(event)
|
|
@@ -2597,92 +2570,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2597
2570
|
self.selection_start = (event.xdata, event.ydata)
|
|
2598
2571
|
self.selecting = False # Will be set to True if the mouse moves while button is held
|
|
2599
2572
|
|
|
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
2573
|
|
|
2687
2574
|
def handle_can(self, x, y):
|
|
2688
2575
|
|
|
@@ -2823,129 +2710,24 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2823
2710
|
if event.inaxes != self.ax:
|
|
2824
2711
|
return
|
|
2825
2712
|
|
|
2826
|
-
#
|
|
2827
|
-
|
|
2713
|
+
# Throttle updates like selection rectangle
|
|
2714
|
+
current_time = time.time()
|
|
2715
|
+
if current_time - getattr(self, 'last_paint_update_time', 0) < 0.016: # ~60fps
|
|
2716
|
+
return
|
|
2717
|
+
self.last_paint_update_time = current_time
|
|
2828
2718
|
|
|
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
|
-
}
|
|
2719
|
+
x, y = int(event.xdata), int(event.ydata)
|
|
2863
2720
|
|
|
2864
|
-
#
|
|
2865
|
-
|
|
2866
|
-
self.paint_queue.put_nowait(paint_op)
|
|
2867
|
-
except queue.Full:
|
|
2868
|
-
pass # Skip if queue is full to avoid blocking
|
|
2721
|
+
# Determine foreground/background for machine window mode
|
|
2722
|
+
foreground = getattr(self, 'foreground', True)
|
|
2869
2723
|
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
"""Prepare optimized background for blitting during paint session."""
|
|
2874
|
-
if self.paint_session_active:
|
|
2875
|
-
return
|
|
2724
|
+
# Add virtual paint stroke with interpolation
|
|
2725
|
+
brush_size = getattr(self, 'brush_size', 5)
|
|
2726
|
+
self.pm.add_virtual_paint_stroke(x, y, brush_size, self.erase, foreground)
|
|
2876
2727
|
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
self.paint_session_active = True
|
|
2728
|
+
# Update display with virtual paint (super fast)
|
|
2729
|
+
self.pm.update_virtual_paint_display()
|
|
2880
2730
|
|
|
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
2731
|
|
|
2950
2732
|
def create_pan_background(self):
|
|
2951
2733
|
"""Create a static background image from currently visible channels with proper rendering"""
|
|
@@ -3322,32 +3104,23 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3322
3104
|
|
|
3323
3105
|
# Handle brush mode cleanup with paint session management
|
|
3324
3106
|
if self.brush_mode and hasattr(self, 'painting') and self.painting:
|
|
3107
|
+
# Finish current operation
|
|
3108
|
+
self.pm.finish_current_virtual_operation()
|
|
3109
|
+
|
|
3110
|
+
# Reset last position for next stroke
|
|
3111
|
+
self.last_virtual_pos = None
|
|
3112
|
+
|
|
3113
|
+
# End this stroke but keep session active for continuous painting
|
|
3325
3114
|
self.painting = False
|
|
3326
3115
|
|
|
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
|
|
3116
|
+
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
3117
|
+
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
3347
3118
|
|
|
3348
|
-
|
|
3119
|
+
self.update_display(preserve_zoom=(current_xlim, current_ylim), continue_paint = True)
|
|
3349
3120
|
|
|
3350
|
-
|
|
3121
|
+
if self.machine_window is not None:
|
|
3122
|
+
if self.machine_window.segmentation_worker is not None:
|
|
3123
|
+
self.machine_window.segmentation_worker.resume()
|
|
3351
3124
|
|
|
3352
3125
|
|
|
3353
3126
|
|
|
@@ -3675,6 +3448,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3675
3448
|
community_code_action.triggered.connect(lambda: self.show_code_dialog(sort = 'Community'))
|
|
3676
3449
|
id_code_action = overlay_menu.addAction("Code Identities")
|
|
3677
3450
|
id_code_action.triggered.connect(lambda: self.show_code_dialog(sort = 'Identity'))
|
|
3451
|
+
umap_action = overlay_menu.addAction("Centroid UMAP")
|
|
3452
|
+
umap_action.triggered.connect(self.handle_umap)
|
|
3678
3453
|
|
|
3679
3454
|
rand_menu = analysis_menu.addMenu("Randomize")
|
|
3680
3455
|
random_action = rand_menu.addAction("Generate Equivalent Random Network")
|
|
@@ -3779,6 +3554,43 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3779
3554
|
help_button = menubar.addAction("Help")
|
|
3780
3555
|
help_button.triggered.connect(self.help_me)
|
|
3781
3556
|
|
|
3557
|
+
cam_button = QPushButton("📷")
|
|
3558
|
+
cam_button.setFixedSize(40, 40)
|
|
3559
|
+
cam_button.setStyleSheet("font-size: 24px;") # Makes emoji larger
|
|
3560
|
+
cam_button.clicked.connect(self.snap)
|
|
3561
|
+
menubar.setCornerWidget(cam_button, Qt.Corner.TopRightCorner)
|
|
3562
|
+
|
|
3563
|
+
def snap(self):
|
|
3564
|
+
|
|
3565
|
+
try:
|
|
3566
|
+
|
|
3567
|
+
for thing in self.channel_data:
|
|
3568
|
+
if thing is not None:
|
|
3569
|
+
data = True
|
|
3570
|
+
if not data:
|
|
3571
|
+
return
|
|
3572
|
+
|
|
3573
|
+
snap = self.create_composite_for_pan()
|
|
3574
|
+
|
|
3575
|
+
filename, _ = QFileDialog.getSaveFileName(
|
|
3576
|
+
self,
|
|
3577
|
+
f"Save Image As",
|
|
3578
|
+
"", # Default directory
|
|
3579
|
+
"TIFF Files (*.tif *.tiff);;All Files (*)" # File type filter
|
|
3580
|
+
)
|
|
3581
|
+
|
|
3582
|
+
if filename: # Only proceed if user didn't cancel
|
|
3583
|
+
# If user didn't type an extension, add .tif
|
|
3584
|
+
if not filename.endswith(('.tif', '.tiff')):
|
|
3585
|
+
filename += '.tif'
|
|
3586
|
+
|
|
3587
|
+
import tifffile
|
|
3588
|
+
tifffile.imwrite(filename, snap)
|
|
3589
|
+
|
|
3590
|
+
except:
|
|
3591
|
+
pass
|
|
3592
|
+
|
|
3593
|
+
|
|
3782
3594
|
def open_cellpose(self):
|
|
3783
3595
|
|
|
3784
3596
|
try:
|
|
@@ -4555,6 +4367,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4555
4367
|
)
|
|
4556
4368
|
|
|
4557
4369
|
self.last_load = directory
|
|
4370
|
+
|
|
4558
4371
|
|
|
4559
4372
|
if directory != "":
|
|
4560
4373
|
|
|
@@ -4817,6 +4630,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4817
4630
|
- 'mean': averages across color channels
|
|
4818
4631
|
- 'max': takes maximum value across color channels
|
|
4819
4632
|
- 'min': takes minimum value across color channels
|
|
4633
|
+
- 'weight': takes weighted channel averages
|
|
4820
4634
|
|
|
4821
4635
|
Returns:
|
|
4822
4636
|
--------
|
|
@@ -4831,7 +4645,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4831
4645
|
if array.ndim != 4:
|
|
4832
4646
|
raise ValueError(f"Expected 4D array, got {array.ndim}D array")
|
|
4833
4647
|
|
|
4834
|
-
if method not in ['first', 'mean', 'max', 'min']:
|
|
4648
|
+
if method not in ['first', 'mean', 'max', 'min', 'weight']:
|
|
4835
4649
|
raise ValueError(f"Unknown method: {method}")
|
|
4836
4650
|
|
|
4837
4651
|
if method == 'first':
|
|
@@ -4840,6 +4654,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4840
4654
|
return np.mean(array, axis=-1)
|
|
4841
4655
|
elif method == 'max':
|
|
4842
4656
|
return np.max(array, axis=-1)
|
|
4657
|
+
elif method == 'weight':
|
|
4658
|
+
# Apply the luminosity formula
|
|
4659
|
+
return (0.2989 * array[:,:,:,0] + 0.5870 * array[:,:,:,1] + 0.1140 * array[:,:,:,2])
|
|
4843
4660
|
else: # min
|
|
4844
4661
|
return np.min(array, axis=-1)
|
|
4845
4662
|
|
|
@@ -4863,7 +4680,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4863
4680
|
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
4864
4681
|
return msg.exec() == QMessageBox.StandardButton.Yes
|
|
4865
4682
|
|
|
4866
|
-
def load_channel(self, channel_index, channel_data=None, data=False, assign_shape = True, preserve_zoom = None, end_paint = False, begin_paint = False):
|
|
4683
|
+
def load_channel(self, channel_index, channel_data=None, data=False, assign_shape = True, preserve_zoom = None, end_paint = False, begin_paint = False, color = False):
|
|
4867
4684
|
"""Load a channel and enable active channel selection if needed."""
|
|
4868
4685
|
|
|
4869
4686
|
try:
|
|
@@ -4940,12 +4757,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4940
4757
|
except:
|
|
4941
4758
|
pass
|
|
4942
4759
|
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4760
|
+
if not color:
|
|
4761
|
+
try:
|
|
4762
|
+
if len(self.channel_data[channel_index].shape) == 4 and (channel_index == 0 or channel_index == 1):
|
|
4763
|
+
self.channel_data[channel_index] = self.reduce_rgb_dimension(self.channel_data[channel_index], 'weight')
|
|
4764
|
+
except:
|
|
4765
|
+
pass
|
|
4949
4766
|
|
|
4950
4767
|
reset_resize = False
|
|
4951
4768
|
|
|
@@ -5295,25 +5112,29 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5295
5112
|
|
|
5296
5113
|
|
|
5297
5114
|
|
|
5298
|
-
def update_display(self, preserve_zoom=None, dims = None, called = False, reset_resize = False):
|
|
5115
|
+
def update_display(self, preserve_zoom=None, dims = None, called = False, reset_resize = False, continue_paint = False, skip_paint_reinit = False):
|
|
5299
5116
|
"""Update the display with currently visible channels and highlight overlay."""
|
|
5300
|
-
|
|
5301
5117
|
try:
|
|
5302
|
-
|
|
5303
5118
|
self.figure.clear()
|
|
5304
|
-
|
|
5305
5119
|
if self.pan_background_image is not None:
|
|
5306
5120
|
# Restore previously visible channels
|
|
5307
5121
|
self.channel_visible = self.pre_pan_channel_state.copy()
|
|
5308
5122
|
self.is_pan_preview = False
|
|
5309
5123
|
self.pan_background_image = None
|
|
5310
|
-
|
|
5311
5124
|
if self.machine_window is not None:
|
|
5312
5125
|
if self.machine_window.segmentation_worker is not None:
|
|
5313
5126
|
self.machine_window.segmentation_worker.resume()
|
|
5314
|
-
|
|
5315
5127
|
if self.static_background is not None:
|
|
5316
|
-
|
|
5128
|
+
# NEW: Convert virtual strokes to real data before cleanup
|
|
5129
|
+
if (hasattr(self, 'virtual_draw_operations') and self.virtual_draw_operations) or \
|
|
5130
|
+
(hasattr(self, 'virtual_erase_operations') and self.virtual_erase_operations) or \
|
|
5131
|
+
(hasattr(self, 'current_operation') and self.current_operation):
|
|
5132
|
+
# Finish current operation first
|
|
5133
|
+
if hasattr(self, 'current_operation') and self.current_operation:
|
|
5134
|
+
self.pm.finish_current_virtual_operation()
|
|
5135
|
+
# Now convert to real data
|
|
5136
|
+
self.pm.convert_virtual_strokes_to_data()
|
|
5137
|
+
|
|
5317
5138
|
# Restore hidden channels
|
|
5318
5139
|
try:
|
|
5319
5140
|
for i in self.restore_channels:
|
|
@@ -5321,27 +5142,17 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5321
5142
|
self.restore_channels = []
|
|
5322
5143
|
except:
|
|
5323
5144
|
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
|
|
5334
|
-
|
|
5335
|
-
if self.machine_window is None:
|
|
5145
|
+
if not continue_paint:
|
|
5146
|
+
self.static_background = None
|
|
5336
5147
|
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5148
|
+
if self.machine_window is None:
|
|
5149
|
+
try:
|
|
5150
|
+
self.channel_data[4][self.current_slice, :, :] = n3d.overlay_arrays_simple(self.channel_data[self.temp_chan][self.current_slice, :, :], self.channel_data[4][self.current_slice, :, :])
|
|
5151
|
+
self.load_channel(self.temp_chan, self.channel_data[4], data = True, end_paint = True)
|
|
5152
|
+
self.channel_data[4] = None
|
|
5153
|
+
self.channel_visible[4] = False
|
|
5154
|
+
except:
|
|
5155
|
+
pass
|
|
5345
5156
|
|
|
5346
5157
|
# Get active channels and their dimensions
|
|
5347
5158
|
active_channels = [i for i in range(4) if self.channel_data[i] is not None]
|
|
@@ -5572,49 +5383,22 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5572
5383
|
|
|
5573
5384
|
self.canvas.draw()
|
|
5574
5385
|
|
|
5386
|
+
if self.brush_mode and not skip_paint_reinit:
|
|
5387
|
+
# Get current zoom to preserve it
|
|
5388
|
+
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
5389
|
+
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
5390
|
+
|
|
5391
|
+
if self.pen_button.isChecked():
|
|
5392
|
+
channel = self.active_channel
|
|
5393
|
+
else:
|
|
5394
|
+
channel = 2
|
|
5395
|
+
|
|
5396
|
+
self.pm.initiate_paint_session(channel, current_xlim, current_ylim)
|
|
5397
|
+
|
|
5575
5398
|
except:
|
|
5576
5399
|
import traceback
|
|
5577
5400
|
print(traceback.format_exc())
|
|
5578
5401
|
|
|
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
5402
|
def get_channel_image(self, channel):
|
|
5619
5403
|
"""Find the matplotlib image object for a specific channel."""
|
|
5620
5404
|
if not hasattr(self.ax, 'images'):
|
|
@@ -5744,6 +5528,13 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5744
5528
|
dialog = CodeDialog(self, sort = sort)
|
|
5745
5529
|
dialog.exec()
|
|
5746
5530
|
|
|
5531
|
+
def handle_umap(self):
|
|
5532
|
+
|
|
5533
|
+
if my_network.node_centroids is None:
|
|
5534
|
+
self.show_centroid_dialog()
|
|
5535
|
+
|
|
5536
|
+
my_network.centroid_umap()
|
|
5537
|
+
|
|
5747
5538
|
def closeEvent(self, event):
|
|
5748
5539
|
"""Override closeEvent to close all windows when main window closes"""
|
|
5749
5540
|
|
|
@@ -7264,24 +7055,29 @@ class ColorOverlayDialog(QDialog):
|
|
|
7264
7055
|
|
|
7265
7056
|
def coloroverlay(self):
|
|
7266
7057
|
|
|
7267
|
-
|
|
7058
|
+
try:
|
|
7268
7059
|
|
|
7269
|
-
|
|
7270
|
-
|
|
7271
|
-
self.
|
|
7272
|
-
|
|
7273
|
-
|
|
7274
|
-
|
|
7060
|
+
down_factor = float(self.down_factor.text()) if self.down_factor.text().strip() else None
|
|
7061
|
+
|
|
7062
|
+
if self.parent().active_channel == 0:
|
|
7063
|
+
mode = 0
|
|
7064
|
+
self.sort = 'Node'
|
|
7065
|
+
else:
|
|
7066
|
+
mode = 1
|
|
7067
|
+
self.sort = 'Edge'
|
|
7275
7068
|
|
|
7276
7069
|
|
|
7277
|
-
|
|
7070
|
+
result, legend = my_network.node_to_color(down_factor = down_factor, mode = mode)
|
|
7278
7071
|
|
|
7279
|
-
|
|
7072
|
+
self.parent().format_for_upperright_table(legend, f'{self.sort} Id', f'Encoding Val: {self.sort}', 'Legend')
|
|
7280
7073
|
|
|
7281
7074
|
|
|
7282
|
-
|
|
7075
|
+
self.parent().load_channel(3, channel_data = result, data = True)
|
|
7283
7076
|
|
|
7284
|
-
|
|
7077
|
+
self.accept()
|
|
7078
|
+
|
|
7079
|
+
except:
|
|
7080
|
+
pass
|
|
7285
7081
|
|
|
7286
7082
|
|
|
7287
7083
|
class ShuffleDialog(QDialog):
|
|
@@ -7624,7 +7420,7 @@ class ComNeighborDialog(QDialog):
|
|
|
7624
7420
|
# weighted checkbox (default True)
|
|
7625
7421
|
self.proportional = QPushButton("Robust")
|
|
7626
7422
|
self.proportional.setCheckable(True)
|
|
7627
|
-
self.proportional.setChecked(
|
|
7423
|
+
self.proportional.setChecked(True)
|
|
7628
7424
|
layout.addRow("Return Node Type Distribution Robust Heatmaps (ie, will give two more heatmaps that are not beholden to the total number of nodes of each type, representing which structures are overrepresented in a network):", self.proportional)
|
|
7629
7425
|
|
|
7630
7426
|
self.mode = QComboBox()
|
|
@@ -9503,6 +9299,12 @@ class ThresholdDialog(QDialog):
|
|
|
9503
9299
|
|
|
9504
9300
|
def start_ml(self, GPU = False):
|
|
9505
9301
|
|
|
9302
|
+
try:
|
|
9303
|
+
print("Please select image to load into nodes channel for segmentation or press X if you already have the one you want. Note that this load may permit a color image in the nodes channel for segmentation purposes only, which is otherwise not allowed.")
|
|
9304
|
+
self.parent().load_channel(0, color = True)
|
|
9305
|
+
except:
|
|
9306
|
+
pass
|
|
9307
|
+
|
|
9506
9308
|
|
|
9507
9309
|
if self.parent().channel_data[2] is not None or self.parent().channel_data[3] is not None or self.parent().highlight_overlay is not None:
|
|
9508
9310
|
if self.confirm_machine_dialog():
|
|
@@ -9619,206 +9421,208 @@ class MachineWindow(QMainWindow):
|
|
|
9619
9421
|
def __init__(self, parent=None, GPU = False):
|
|
9620
9422
|
super().__init__(parent)
|
|
9621
9423
|
|
|
9622
|
-
|
|
9623
|
-
|
|
9624
|
-
|
|
9625
|
-
|
|
9626
|
-
|
|
9627
|
-
|
|
9424
|
+
try:
|
|
9425
|
+
|
|
9426
|
+
if self.parent().active_channel == 0:
|
|
9427
|
+
if self.parent().channel_data[0] is not None:
|
|
9428
|
+
try:
|
|
9429
|
+
active_data = self.parent().channel_data[0]
|
|
9430
|
+
act_channel = 0
|
|
9431
|
+
except:
|
|
9432
|
+
active_data = self.parent().channel_data[1]
|
|
9433
|
+
act_channel = 1
|
|
9434
|
+
else:
|
|
9628
9435
|
active_data = self.parent().channel_data[1]
|
|
9629
9436
|
act_channel = 1
|
|
9630
|
-
else:
|
|
9631
|
-
active_data = self.parent().channel_data[1]
|
|
9632
|
-
act_channel = 1
|
|
9633
|
-
|
|
9634
|
-
try:
|
|
9635
|
-
array1 = np.zeros_like(active_data).astype(np.uint8)
|
|
9636
|
-
except:
|
|
9637
|
-
print("No data in nodes channel")
|
|
9638
|
-
return
|
|
9639
9437
|
|
|
9640
|
-
|
|
9641
|
-
|
|
9642
|
-
|
|
9643
|
-
|
|
9644
|
-
|
|
9645
|
-
|
|
9438
|
+
try:
|
|
9439
|
+
if len(active_data.shape) == 3:
|
|
9440
|
+
array1 = np.zeros_like(active_data).astype(np.uint8)
|
|
9441
|
+
elif len(active_data.shape) == 4:
|
|
9442
|
+
array1 = np.zeros_like(active_data)[:,:,:,0].astype(np.uint8)
|
|
9443
|
+
except:
|
|
9444
|
+
print("No data in nodes channel")
|
|
9445
|
+
return
|
|
9646
9446
|
|
|
9447
|
+
self.setWindowTitle("Threshold")
|
|
9448
|
+
|
|
9449
|
+
# Create central widget and layout
|
|
9450
|
+
central_widget = QWidget()
|
|
9451
|
+
self.setCentralWidget(central_widget)
|
|
9452
|
+
layout = QVBoxLayout(central_widget)
|
|
9647
9453
|
|
|
9648
|
-
# Create form layout for inputs
|
|
9649
|
-
form_layout = QFormLayout()
|
|
9650
9454
|
|
|
9651
|
-
|
|
9455
|
+
# Create form layout for inputs
|
|
9456
|
+
form_layout = QFormLayout()
|
|
9652
9457
|
|
|
9653
|
-
|
|
9654
|
-
self.parent().pen_button.click()
|
|
9655
|
-
self.parent().threed = False
|
|
9656
|
-
self.parent().can = False
|
|
9657
|
-
self.parent().last_change = None
|
|
9458
|
+
layout.addLayout(form_layout)
|
|
9658
9459
|
|
|
9659
|
-
|
|
9460
|
+
if self.parent().pen_button.isChecked(): #Disable the pen mode if the user is in it because the segmenter pen forks it
|
|
9461
|
+
self.parent().pen_button.click()
|
|
9462
|
+
self.parent().threed = False
|
|
9463
|
+
self.parent().can = False
|
|
9464
|
+
self.parent().last_change = None
|
|
9660
9465
|
|
|
9661
|
-
|
|
9662
|
-
self.parent().highlight_overlay = array3 #Clear this out for the segmenter to use
|
|
9466
|
+
self.parent().pen_button.setEnabled(False)
|
|
9663
9467
|
|
|
9664
|
-
|
|
9665
|
-
|
|
9666
|
-
# Not exactly sure why we need all this but the channel buttons weren't loading like they normally do when load_channel() is called:
|
|
9667
|
-
if not self.parent().channel_buttons[2].isEnabled():
|
|
9668
|
-
self.parent().channel_buttons[2].setEnabled(True)
|
|
9669
|
-
self.parent().channel_buttons[2].click()
|
|
9670
|
-
self.parent().delete_buttons[2].setEnabled(True)
|
|
9468
|
+
array3 = np.zeros_like(array1).astype(np.uint8)
|
|
9469
|
+
self.parent().highlight_overlay = array3 #Clear this out for the segmenter to use
|
|
9671
9470
|
|
|
9672
|
-
|
|
9673
|
-
|
|
9471
|
+
self.parent().load_channel(2, array1, True)
|
|
9472
|
+
# Enable the channel button
|
|
9473
|
+
# Not exactly sure why we need all this but the channel buttons weren't loading like they normally do when load_channel() is called:
|
|
9474
|
+
if not self.parent().channel_buttons[2].isEnabled():
|
|
9475
|
+
self.parent().channel_buttons[2].setEnabled(True)
|
|
9476
|
+
self.parent().channel_buttons[2].click()
|
|
9477
|
+
self.parent().delete_buttons[2].setEnabled(True)
|
|
9674
9478
|
|
|
9675
|
-
|
|
9676
|
-
|
|
9677
|
-
|
|
9678
|
-
self.setMinimumWidth(600) # Increased to accommodate grouped buttons
|
|
9679
|
-
self.setMinimumHeight(500)
|
|
9479
|
+
if len(active_data.shape) == 3:
|
|
9480
|
+
self.parent().base_colors[act_channel] = self.parent().color_dictionary['WHITE']
|
|
9481
|
+
self.parent().base_colors[2] = self.parent().color_dictionary['LIGHT_GREEN']
|
|
9680
9482
|
|
|
9681
|
-
|
|
9682
|
-
|
|
9683
|
-
|
|
9483
|
+
self.parent().update_display()
|
|
9484
|
+
|
|
9485
|
+
# Set a reasonable default size for the window
|
|
9486
|
+
self.setMinimumWidth(600) # Increased to accommodate grouped buttons
|
|
9487
|
+
self.setMinimumHeight(500)
|
|
9488
|
+
|
|
9489
|
+
# Create main layout container
|
|
9490
|
+
main_widget = QWidget()
|
|
9491
|
+
main_layout = QVBoxLayout(main_widget)
|
|
9492
|
+
|
|
9493
|
+
# Group 1: Drawing tools (Brush + Foreground/Background)
|
|
9494
|
+
drawing_group = QGroupBox("Drawing Tools")
|
|
9495
|
+
drawing_layout = QHBoxLayout()
|
|
9496
|
+
|
|
9497
|
+
# Brush button
|
|
9498
|
+
self.brush_button = QPushButton("🖌️")
|
|
9499
|
+
self.brush_button.setCheckable(True)
|
|
9500
|
+
self.brush_button.setFixedSize(40, 40)
|
|
9501
|
+
self.brush_button.clicked.connect(self.toggle_brush_mode)
|
|
9502
|
+
self.brush_button.click()
|
|
9503
|
+
|
|
9504
|
+
# Foreground/Background buttons in their own horizontal layout
|
|
9505
|
+
fb_layout = QHBoxLayout()
|
|
9506
|
+
self.fore_button = QPushButton("Foreground")
|
|
9507
|
+
self.fore_button.setCheckable(True)
|
|
9508
|
+
self.fore_button.setChecked(True)
|
|
9509
|
+
self.fore_button.clicked.connect(self.toggle_foreground)
|
|
9684
9510
|
|
|
9685
|
-
|
|
9686
|
-
|
|
9687
|
-
|
|
9511
|
+
self.back_button = QPushButton("Background")
|
|
9512
|
+
self.back_button.setCheckable(True)
|
|
9513
|
+
self.back_button.setChecked(False)
|
|
9514
|
+
self.back_button.clicked.connect(self.toggle_background)
|
|
9688
9515
|
|
|
9689
|
-
|
|
9690
|
-
|
|
9691
|
-
self.brush_button.setCheckable(True)
|
|
9692
|
-
self.brush_button.setFixedSize(40, 40)
|
|
9693
|
-
self.brush_button.clicked.connect(self.toggle_brush_mode)
|
|
9694
|
-
self.brush_button.click()
|
|
9516
|
+
fb_layout.addWidget(self.fore_button)
|
|
9517
|
+
fb_layout.addWidget(self.back_button)
|
|
9695
9518
|
|
|
9696
|
-
|
|
9697
|
-
|
|
9698
|
-
|
|
9699
|
-
self.fore_button.setCheckable(True)
|
|
9700
|
-
self.fore_button.setChecked(True)
|
|
9701
|
-
self.fore_button.clicked.connect(self.toggle_foreground)
|
|
9519
|
+
drawing_layout.addWidget(self.brush_button)
|
|
9520
|
+
drawing_layout.addLayout(fb_layout)
|
|
9521
|
+
drawing_group.setLayout(drawing_layout)
|
|
9702
9522
|
|
|
9703
|
-
|
|
9704
|
-
|
|
9705
|
-
|
|
9706
|
-
self.back_button.clicked.connect(self.toggle_background)
|
|
9523
|
+
# Group 2: Processing Options (GPU)
|
|
9524
|
+
processing_group = QGroupBox("Processing Options")
|
|
9525
|
+
processing_layout = QHBoxLayout()
|
|
9707
9526
|
|
|
9708
|
-
|
|
9709
|
-
|
|
9527
|
+
self.use_gpu = GPU
|
|
9528
|
+
self.two = QPushButton("Train By 2D Slice Patterns")
|
|
9529
|
+
self.two.setCheckable(True)
|
|
9530
|
+
self.two.setChecked(False)
|
|
9531
|
+
self.two.clicked.connect(self.toggle_two)
|
|
9532
|
+
self.use_two = False
|
|
9533
|
+
self.three = QPushButton("Train by 3D Patterns")
|
|
9534
|
+
self.three.setCheckable(True)
|
|
9535
|
+
self.three.setChecked(True)
|
|
9536
|
+
self.three.clicked.connect(self.toggle_three)
|
|
9537
|
+
self.GPU = QPushButton("GPU")
|
|
9538
|
+
self.GPU.setCheckable(True)
|
|
9539
|
+
self.GPU.setChecked(False)
|
|
9540
|
+
self.GPU.clicked.connect(self.toggle_GPU)
|
|
9541
|
+
processing_layout.addWidget(self.GPU)
|
|
9542
|
+
processing_layout.addWidget(self.two)
|
|
9543
|
+
processing_layout.addWidget(self.three)
|
|
9544
|
+
processing_group.setLayout(processing_layout)
|
|
9545
|
+
|
|
9546
|
+
# Group 3: Training Options
|
|
9547
|
+
training_group = QGroupBox("Training")
|
|
9548
|
+
training_layout = QHBoxLayout()
|
|
9549
|
+
train_quick = QPushButton("Train Quick Model (When Good SNR)")
|
|
9550
|
+
train_quick.clicked.connect(lambda: self.train_model(speed=True))
|
|
9551
|
+
train_detailed = QPushButton("Train Detailed Model (For Morphology)")
|
|
9552
|
+
train_detailed.clicked.connect(lambda: self.train_model(speed=False))
|
|
9553
|
+
save = QPushButton("Save Model")
|
|
9554
|
+
save.clicked.connect(self.save_model)
|
|
9555
|
+
load = QPushButton("Load Model")
|
|
9556
|
+
load.clicked.connect(self.load_model)
|
|
9557
|
+
training_layout.addWidget(train_quick)
|
|
9558
|
+
training_layout.addWidget(train_detailed)
|
|
9559
|
+
training_layout.addWidget(save)
|
|
9560
|
+
training_layout.addWidget(load)
|
|
9561
|
+
training_group.setLayout(training_layout)
|
|
9562
|
+
|
|
9563
|
+
# Group 4: Segmentation Options
|
|
9564
|
+
segmentation_group = QGroupBox("Segmentation")
|
|
9565
|
+
segmentation_layout = QHBoxLayout()
|
|
9566
|
+
seg_button = QPushButton("Preview Segment")
|
|
9567
|
+
self.seg_button = seg_button
|
|
9568
|
+
seg_button.clicked.connect(self.start_segmentation)
|
|
9569
|
+
self.pause_button = QPushButton("▶/⏸️")
|
|
9570
|
+
self.pause_button.setFixedSize(40, 40)
|
|
9571
|
+
self.pause_button.clicked.connect(self.toggle_segment)
|
|
9572
|
+
self.lock_button = QPushButton("🔒 Memory lock - (Prioritize RAM)")
|
|
9573
|
+
self.lock_button.setCheckable(True)
|
|
9574
|
+
self.lock_button.setChecked(True)
|
|
9575
|
+
self.lock_button.clicked.connect(self.toggle_lock)
|
|
9576
|
+
self.mem_lock = True
|
|
9577
|
+
full_button = QPushButton("Segment All")
|
|
9578
|
+
full_button.clicked.connect(self.segment)
|
|
9579
|
+
segmentation_layout.addWidget(seg_button)
|
|
9580
|
+
segmentation_layout.addWidget(self.pause_button) # <--- for some reason the segmenter preview is still running even when killed, may be regenerating itself somewhere. May or may not actually try to resolve this because this feature isnt that necessary.
|
|
9581
|
+
#segmentation_layout.addWidget(self.lock_button) # Also turned this off
|
|
9582
|
+
segmentation_layout.addWidget(full_button)
|
|
9583
|
+
segmentation_group.setLayout(segmentation_layout)
|
|
9584
|
+
|
|
9585
|
+
# Add all groups to main layout
|
|
9586
|
+
main_layout.addWidget(drawing_group)
|
|
9587
|
+
if not GPU:
|
|
9588
|
+
main_layout.addWidget(processing_group)
|
|
9589
|
+
main_layout.addWidget(training_group)
|
|
9590
|
+
main_layout.addWidget(segmentation_group)
|
|
9591
|
+
|
|
9592
|
+
# Set the main widget as the central widget
|
|
9593
|
+
self.setCentralWidget(main_widget)
|
|
9594
|
+
|
|
9595
|
+
self.trained = False
|
|
9596
|
+
self.previewing = False
|
|
9710
9597
|
|
|
9711
|
-
|
|
9712
|
-
|
|
9713
|
-
|
|
9598
|
+
if not GPU:
|
|
9599
|
+
self.segmenter = segmenter.InteractiveSegmenter(active_data, use_gpu=False)
|
|
9600
|
+
else:
|
|
9601
|
+
self.segmenter = seg_GPU.InteractiveSegmenter(active_data)
|
|
9714
9602
|
|
|
9715
|
-
|
|
9716
|
-
processing_group = QGroupBox("Processing Options")
|
|
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)
|
|
9603
|
+
self.segmentation_worker = None
|
|
9737
9604
|
|
|
9738
|
-
|
|
9739
|
-
|
|
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)
|
|
9605
|
+
self.fore_button.click()
|
|
9606
|
+
self.fore_button.click()
|
|
9785
9607
|
|
|
9786
|
-
|
|
9787
|
-
|
|
9608
|
+
except:
|
|
9609
|
+
return
|
|
9788
9610
|
|
|
9789
|
-
|
|
9790
|
-
self.segmenter = segmenter.InteractiveSegmenter(active_data, use_gpu=False)
|
|
9791
|
-
else:
|
|
9792
|
-
self.segmenter = seg_GPU.InteractiveSegmenter(active_data)
|
|
9611
|
+
def toggle_segment(self):
|
|
9793
9612
|
|
|
9794
|
-
self.segmentation_worker
|
|
9613
|
+
if self.segmentation_worker is not None:
|
|
9614
|
+
if not self.segmentation_worker._paused:
|
|
9615
|
+
self.segmentation_worker.pause()
|
|
9616
|
+
print("Segmentation Worker Paused")
|
|
9617
|
+
elif self.segmentation_worker._paused:
|
|
9618
|
+
self.segmentation_worker.resume()
|
|
9619
|
+
print("Segmentation Worker Resuming")
|
|
9795
9620
|
|
|
9796
|
-
self.fore_button.click()
|
|
9797
|
-
self.fore_button.click()
|
|
9798
9621
|
|
|
9799
9622
|
def toggle_lock(self):
|
|
9800
9623
|
|
|
9801
9624
|
self.mem_lock = self.lock_button.isChecked()
|
|
9802
9625
|
|
|
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
9626
|
|
|
9823
9627
|
def save_model(self):
|
|
9824
9628
|
|
|
@@ -9840,6 +9644,9 @@ class MachineWindow(QMainWindow):
|
|
|
9840
9644
|
|
|
9841
9645
|
except Exception as e:
|
|
9842
9646
|
print(f"Error saving model: {e}")
|
|
9647
|
+
import traceback
|
|
9648
|
+
traceback.print_exc()
|
|
9649
|
+
|
|
9843
9650
|
|
|
9844
9651
|
def load_model(self):
|
|
9845
9652
|
|
|
@@ -9935,7 +9742,23 @@ class MachineWindow(QMainWindow):
|
|
|
9935
9742
|
def toggle_brush_mode(self):
|
|
9936
9743
|
"""Toggle brush mode on/off"""
|
|
9937
9744
|
self.parent().brush_mode = self.brush_button.isChecked()
|
|
9745
|
+
|
|
9938
9746
|
if self.parent().brush_mode:
|
|
9747
|
+
|
|
9748
|
+
self.parent().pm = painting.PaintManager(parent = self.parent())
|
|
9749
|
+
|
|
9750
|
+
# Start virtual paint session
|
|
9751
|
+
# Get current zoom to preserve it
|
|
9752
|
+
current_xlim = self.parent().ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
9753
|
+
current_ylim = self.parent().ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
9754
|
+
|
|
9755
|
+
if self.parent().pen_button.isChecked():
|
|
9756
|
+
channel = self.parent().active_channel
|
|
9757
|
+
else:
|
|
9758
|
+
channel = 2
|
|
9759
|
+
|
|
9760
|
+
self.parent().pm.initiate_paint_session(channel, current_xlim, current_ylim)
|
|
9761
|
+
|
|
9939
9762
|
self.parent().pan_button.setChecked(False)
|
|
9940
9763
|
self.parent().zoom_button.setChecked(False)
|
|
9941
9764
|
if self.parent().pan_mode:
|
|
@@ -9962,12 +9785,13 @@ class MachineWindow(QMainWindow):
|
|
|
9962
9785
|
self.kill_segmentation()
|
|
9963
9786
|
# Wait a bit for cleanup
|
|
9964
9787
|
time.sleep(0.1)
|
|
9965
|
-
|
|
9966
|
-
|
|
9788
|
+
|
|
9789
|
+
self.previewing = True
|
|
9967
9790
|
try:
|
|
9968
9791
|
try:
|
|
9969
9792
|
self.segmenter.train_batch(self.parent().channel_data[2], speed = speed, use_gpu = self.use_gpu, use_two = self.use_two, mem_lock = self.mem_lock)
|
|
9970
9793
|
self.trained = True
|
|
9794
|
+
self.start_segmentation()
|
|
9971
9795
|
except Exception as e:
|
|
9972
9796
|
print("Error training. Perhaps you forgot both foreground and background markers? I need both!")
|
|
9973
9797
|
import traceback
|
|
@@ -9983,6 +9807,8 @@ class MachineWindow(QMainWindow):
|
|
|
9983
9807
|
|
|
9984
9808
|
def start_segmentation(self):
|
|
9985
9809
|
|
|
9810
|
+
self.parent().static_background = None
|
|
9811
|
+
|
|
9986
9812
|
self.kill_segmentation()
|
|
9987
9813
|
time.sleep(0.1)
|
|
9988
9814
|
|
|
@@ -9991,12 +9817,10 @@ class MachineWindow(QMainWindow):
|
|
|
9991
9817
|
else:
|
|
9992
9818
|
print("Beginning new segmentation...")
|
|
9993
9819
|
|
|
9994
|
-
|
|
9995
|
-
|
|
9996
|
-
|
|
9997
|
-
|
|
9998
|
-
else:
|
|
9999
|
-
active_data = self.parent().channel_data[1]
|
|
9820
|
+
if self.parent().channel_data[2] is not None:
|
|
9821
|
+
active_data = self.parent().channel_data[2]
|
|
9822
|
+
else:
|
|
9823
|
+
active_data = self.parent().channel_data[0]
|
|
10000
9824
|
|
|
10001
9825
|
array3 = np.zeros_like(active_data).astype(np.uint8)
|
|
10002
9826
|
self.parent().highlight_overlay = array3 #Clear this out for the segmenter to use
|
|
@@ -10005,8 +9829,7 @@ class MachineWindow(QMainWindow):
|
|
|
10005
9829
|
return
|
|
10006
9830
|
else:
|
|
10007
9831
|
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)
|
|
9832
|
+
self.segmentation_worker.chunk_processed.connect(lambda: self.update_display(skip_paint_reinit = True)) # Just update display
|
|
10010
9833
|
current_xlim = self.parent().ax.get_xlim()
|
|
10011
9834
|
current_ylim = self.parent().ax.get_ylim()
|
|
10012
9835
|
try:
|
|
@@ -10059,7 +9882,7 @@ class MachineWindow(QMainWindow):
|
|
|
10059
9882
|
|
|
10060
9883
|
return changed
|
|
10061
9884
|
|
|
10062
|
-
def update_display(self):
|
|
9885
|
+
def update_display(self, skip_paint_reinit = False):
|
|
10063
9886
|
if not hasattr(self, '_last_update'):
|
|
10064
9887
|
self._last_update = 0
|
|
10065
9888
|
|
|
@@ -10069,8 +9892,6 @@ class MachineWindow(QMainWindow):
|
|
|
10069
9892
|
|
|
10070
9893
|
self._last_z = current_z
|
|
10071
9894
|
|
|
10072
|
-
if self.previewing:
|
|
10073
|
-
changed = self.check_for_z_change()
|
|
10074
9895
|
|
|
10075
9896
|
current_time = time.time()
|
|
10076
9897
|
if current_time - self._last_update >= 1: # Match worker's interval
|
|
@@ -10087,71 +9908,40 @@ class MachineWindow(QMainWindow):
|
|
|
10087
9908
|
|
|
10088
9909
|
if not self.parent().painting:
|
|
10089
9910
|
# Only update if view limits are valid
|
|
10090
|
-
self.parent().update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
9911
|
+
self.parent().update_display(preserve_zoom=(current_xlim, current_ylim), skip_paint_reinit = skip_paint_reinit)
|
|
9912
|
+
|
|
9913
|
+
if self.parent().brush_mode:
|
|
9914
|
+
current_xlim = self.parent().ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
9915
|
+
current_ylim = self.parent().ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
10091
9916
|
|
|
9917
|
+
if self.parent().pen_button.isChecked():
|
|
9918
|
+
channel = self.parent().active_channel
|
|
9919
|
+
else:
|
|
9920
|
+
channel = 2
|
|
9921
|
+
|
|
9922
|
+
self.parent().pm.initiate_paint_session(channel, current_xlim, current_ylim)
|
|
10092
9923
|
|
|
10093
9924
|
self._last_update = current_time
|
|
10094
9925
|
except Exception as e:
|
|
10095
9926
|
print(f"Display update error: {e}")
|
|
10096
9927
|
|
|
10097
9928
|
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()
|
|
9929
|
+
try:
|
|
9930
|
+
# Clear any processing flags in the segmenter
|
|
9931
|
+
if hasattr(self.segmenter, '_currently_processing'):
|
|
9932
|
+
self.segmenter._currently_processing = None
|
|
10110
9933
|
|
|
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 = {}
|
|
9934
|
+
# Force regenerating the worker
|
|
9935
|
+
if self.segmentation_worker is not None:
|
|
9936
|
+
self.kill_segmentation()
|
|
10135
9937
|
|
|
10136
|
-
|
|
9938
|
+
time.sleep(0.2)
|
|
9939
|
+
self.start_segmentation()
|
|
10137
9940
|
|
|
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
|
-
|
|
9941
|
+
except Exception as e:
|
|
9942
|
+
print(f"Error in poke_segmenter: {e}")
|
|
9943
|
+
import traceback
|
|
9944
|
+
traceback.print_exc()
|
|
10155
9945
|
|
|
10156
9946
|
|
|
10157
9947
|
def kill_segmentation(self):
|
|
@@ -10185,11 +9975,10 @@ class MachineWindow(QMainWindow):
|
|
|
10185
9975
|
|
|
10186
9976
|
self.previewing = False
|
|
10187
9977
|
|
|
10188
|
-
if self.parent().
|
|
10189
|
-
|
|
10190
|
-
|
|
10191
|
-
|
|
10192
|
-
active_data = self.parent().channel_data[1]
|
|
9978
|
+
if self.parent().channel_data[2] is not None:
|
|
9979
|
+
active_data = self.parent().channel_data[2]
|
|
9980
|
+
else:
|
|
9981
|
+
active_data = self.parent().channel_data[0]
|
|
10193
9982
|
|
|
10194
9983
|
array3 = np.zeros_like(active_data).astype(np.uint8)
|
|
10195
9984
|
self.parent().highlight_overlay = array3 #Clear this out for the segmenter to use
|
|
@@ -10200,6 +9989,8 @@ class MachineWindow(QMainWindow):
|
|
|
10200
9989
|
self.parent().highlight_overlay = self.segmenter.segment_volume(array = self.parent().highlight_overlay)
|
|
10201
9990
|
except Exception as e:
|
|
10202
9991
|
print(f"Error segmenting (Perhaps retrain the model...): {e}")
|
|
9992
|
+
import traceback
|
|
9993
|
+
traceback.print_exc()
|
|
10203
9994
|
return
|
|
10204
9995
|
|
|
10205
9996
|
# Clean up when done
|
|
@@ -10236,6 +10027,12 @@ class MachineWindow(QMainWindow):
|
|
|
10236
10027
|
# Kill the segmentation thread and wait for it to finish
|
|
10237
10028
|
self.kill_segmentation()
|
|
10238
10029
|
time.sleep(0.2) # Give additional time for cleanup
|
|
10030
|
+
|
|
10031
|
+
try:
|
|
10032
|
+
self.parent().channel_data[0] = self.parent().reduce_rgb_dimension(self.parent().channel_data[0], 'weight')
|
|
10033
|
+
self.update_display()
|
|
10034
|
+
except:
|
|
10035
|
+
pass
|
|
10239
10036
|
|
|
10240
10037
|
self.parent().machine_window = None
|
|
10241
10038
|
else:
|
|
@@ -10296,59 +10093,26 @@ class SegmentationWorker(QThread):
|
|
|
10296
10093
|
|
|
10297
10094
|
# Remember the starting z position
|
|
10298
10095
|
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
10096
|
|
|
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
|
|
10097
|
+
# Original 3D approach
|
|
10098
|
+
for foreground_coords, background_coords in self.segmenter.segment_volume_realtime(gpu=self.use_gpu):
|
|
10099
|
+
# Check for pause/stop before processing each chunk
|
|
10100
|
+
self._check_pause()
|
|
10101
|
+
if self._stop:
|
|
10102
|
+
break
|
|
10331
10103
|
|
|
10332
|
-
|
|
10333
|
-
|
|
10334
|
-
for
|
|
10335
|
-
|
|
10336
|
-
|
|
10337
|
-
|
|
10338
|
-
|
|
10339
|
-
|
|
10340
|
-
|
|
10341
|
-
|
|
10342
|
-
|
|
10343
|
-
|
|
10344
|
-
|
|
10345
|
-
self.chunks_since_update += 1
|
|
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
|
|
10104
|
+
for z,y,x in foreground_coords:
|
|
10105
|
+
self.overlay[z,y,x] = 1
|
|
10106
|
+
for z,y,x in background_coords:
|
|
10107
|
+
self.overlay[z,y,x] = 2
|
|
10108
|
+
|
|
10109
|
+
self.chunks_since_update += 1
|
|
10110
|
+
current_time = time.time()
|
|
10111
|
+
if (self.chunks_since_update >= self.chunks_per_update and
|
|
10112
|
+
current_time - self.last_update >= self.update_interval):
|
|
10113
|
+
self.chunk_processed.emit()
|
|
10114
|
+
self.chunks_since_update = 0
|
|
10115
|
+
self.last_update = current_time
|
|
10352
10116
|
|
|
10353
10117
|
self.finished.emit()
|
|
10354
10118
|
|