nettracer3d 0.9.0__py3-none-any.whl → 0.9.2__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/neighborhoods.py +107 -16
- nettracer3d/nettracer.py +60 -31
- nettracer3d/nettracer_gui.py +991 -351
- nettracer3d/segmenter.py +514 -372
- nettracer3d/segmenter_GPU.py +434 -281
- {nettracer3d-0.9.0.dist-info → nettracer3d-0.9.2.dist-info}/METADATA +6 -14
- {nettracer3d-0.9.0.dist-info → nettracer3d-0.9.2.dist-info}/RECORD +11 -11
- {nettracer3d-0.9.0.dist-info → nettracer3d-0.9.2.dist-info}/WHEEL +0 -0
- {nettracer3d-0.9.0.dist-info → nettracer3d-0.9.2.dist-info}/entry_points.txt +0 -0
- {nettracer3d-0.9.0.dist-info → nettracer3d-0.9.2.dist-info}/licenses/LICENSE +0 -0
- {nettracer3d-0.9.0.dist-info → nettracer3d-0.9.2.dist-info}/top_level.txt +0 -0
nettracer3d/nettracer_gui.py
CHANGED
|
@@ -5,7 +5,7 @@ from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QG
|
|
|
5
5
|
QFormLayout, QLineEdit, QPushButton, QFileDialog,
|
|
6
6
|
QLabel, QComboBox, QMessageBox, QTableView, QInputDialog,
|
|
7
7
|
QMenu, QTabWidget, QGroupBox)
|
|
8
|
-
from PyQt6.QtCore import (QPoint, Qt, QAbstractTableModel, QTimer, QThread, pyqtSignal, QObject, QCoreApplication)
|
|
8
|
+
from PyQt6.QtCore import (QPoint, Qt, QAbstractTableModel, QTimer, QThread, pyqtSignal, QObject, QCoreApplication, QEvent)
|
|
9
9
|
import numpy as np
|
|
10
10
|
import time
|
|
11
11
|
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
|
|
@@ -18,7 +18,7 @@ from nettracer3d import smart_dilate as sdl
|
|
|
18
18
|
from matplotlib.colors import LinearSegmentedColormap
|
|
19
19
|
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
|
|
20
20
|
import pandas as pd
|
|
21
|
-
from PyQt6.QtGui import (QFont, QCursor, QColor, QPixmap, QFontMetrics, QPainter, QPen)
|
|
21
|
+
from PyQt6.QtGui import (QFont, QCursor, QColor, QPixmap, QFontMetrics, QPainter, QPen, QShortcut, QKeySequence)
|
|
22
22
|
import tifffile
|
|
23
23
|
import copy
|
|
24
24
|
import multiprocessing as mp
|
|
@@ -137,7 +137,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
137
137
|
self.img_height = None
|
|
138
138
|
self.pre_pan_channel_state = None # Store which channels were visible before pan
|
|
139
139
|
self.is_pan_preview = False # Track if we're in pan preview mode
|
|
140
|
-
self.pre_pan_channel_state = None # Store which channels were visible before pan
|
|
141
140
|
self.pan_background_image = None # Store the rendered composite image
|
|
142
141
|
self.pan_zoom_state = None # Store zoom state when pan began
|
|
143
142
|
self.is_pan_preview = False # Track if we're in pan preview mode
|
|
@@ -309,18 +308,19 @@ class ImageViewerWindow(QMainWindow):
|
|
|
309
308
|
|
|
310
309
|
# Create left panel for image and controls
|
|
311
310
|
left_panel = QWidget()
|
|
312
|
-
left_layout = QVBoxLayout(left_panel)
|
|
311
|
+
self.left_layout = QVBoxLayout(left_panel)
|
|
312
|
+
|
|
313
313
|
|
|
314
314
|
# Create matplotlib canvas for image display
|
|
315
315
|
self.figure = Figure(figsize=(8, 8))
|
|
316
316
|
self.canvas = FigureCanvas(self.figure)
|
|
317
317
|
self.ax = self.figure.add_subplot(111)
|
|
318
|
-
left_layout.addWidget(self.canvas)
|
|
318
|
+
self.left_layout.addWidget(self.canvas)
|
|
319
319
|
|
|
320
320
|
self.canvas.mpl_connect('scroll_event', self.on_mpl_scroll)
|
|
321
321
|
|
|
322
322
|
|
|
323
|
-
left_layout.addWidget(control_panel)
|
|
323
|
+
self.left_layout.addWidget(control_panel)
|
|
324
324
|
|
|
325
325
|
# Add timer for debouncing slice updates
|
|
326
326
|
self._slice_update_timer = QTimer()
|
|
@@ -358,7 +358,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
358
358
|
self.continuous_scroll_timer.timeout.connect(self.continuous_scroll)
|
|
359
359
|
self.scroll_direction = 0 # 0: none, -1: left, 1: right
|
|
360
360
|
|
|
361
|
-
left_layout.addWidget(slider_container)
|
|
361
|
+
self.left_layout.addWidget(slider_container)
|
|
362
362
|
|
|
363
363
|
|
|
364
364
|
main_layout.addWidget(left_panel)
|
|
@@ -466,6 +466,299 @@ class ImageViewerWindow(QMainWindow):
|
|
|
466
466
|
|
|
467
467
|
self.resume = False
|
|
468
468
|
|
|
469
|
+
self.hold_update = False
|
|
470
|
+
self._first_pan_done = False
|
|
471
|
+
|
|
472
|
+
def popup_canvas(self):
|
|
473
|
+
"""Pop the canvas out into its own window"""
|
|
474
|
+
if hasattr(self, 'popup_window') and self.popup_window.isVisible():
|
|
475
|
+
# If popup already exists, just bring it to front
|
|
476
|
+
self.popup_window.raise_()
|
|
477
|
+
self.popup_window.activateWindow()
|
|
478
|
+
if hasattr(self, 'control_popup_window'):
|
|
479
|
+
self.control_popup_window.raise_()
|
|
480
|
+
self.control_popup_window.activateWindow()
|
|
481
|
+
# Also bring machine window to front if it exists
|
|
482
|
+
if self.machine_window is not None:
|
|
483
|
+
self.machine_window.raise_()
|
|
484
|
+
self.machine_window.activateWindow()
|
|
485
|
+
return
|
|
486
|
+
|
|
487
|
+
self.is_popped = True
|
|
488
|
+
|
|
489
|
+
# Store original figure size and canvas size policy before popping out
|
|
490
|
+
self.original_figure_size = self.figure.get_size_inches()
|
|
491
|
+
self.original_canvas_size_policy = self.canvas.sizePolicy()
|
|
492
|
+
|
|
493
|
+
# Create popup window for canvas
|
|
494
|
+
self.popup_window = QMainWindow()
|
|
495
|
+
self.popup_window.setWindowTitle("NetTracer3D - Canvas View")
|
|
496
|
+
self.popup_window.setGeometry(200, 200, 1000, 800) # Bigger size
|
|
497
|
+
|
|
498
|
+
# Install event filters for both window management and keyboard shortcuts
|
|
499
|
+
self.popup_window.installEventFilter(self)
|
|
500
|
+
|
|
501
|
+
# Create popup window for control panel and slider
|
|
502
|
+
self.control_popup_window = QMainWindow()
|
|
503
|
+
self.control_popup_window.setWindowTitle("NetTracer3D - Controls")
|
|
504
|
+
self.control_popup_window.setGeometry(1220, 200, 400, 200) # Bigger height for slider
|
|
505
|
+
|
|
506
|
+
# Install event filter on control window too
|
|
507
|
+
self.control_popup_window.installEventFilter(self)
|
|
508
|
+
|
|
509
|
+
# Make control window non-closeable while popped out
|
|
510
|
+
self.control_popup_window.setWindowFlags(
|
|
511
|
+
self.control_popup_window.windowFlags() & ~Qt.WindowType.WindowCloseButtonHint
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
# Set control panel as child of canvas popup for natural stacking
|
|
515
|
+
self.control_popup_window.setParent(self.popup_window, Qt.WindowType.Window)
|
|
516
|
+
|
|
517
|
+
# Remove canvas from left panel
|
|
518
|
+
self.canvas.setParent(None)
|
|
519
|
+
|
|
520
|
+
# Remove control panel from left panel
|
|
521
|
+
# First find the control_panel widget
|
|
522
|
+
control_panel = None
|
|
523
|
+
for i in range(self.left_layout.count()):
|
|
524
|
+
widget = self.left_layout.itemAt(i).widget()
|
|
525
|
+
if widget and hasattr(widget, 'findChild') and widget.findChild(QComboBox):
|
|
526
|
+
control_panel = widget
|
|
527
|
+
break
|
|
528
|
+
|
|
529
|
+
# Remove slider container from left panel
|
|
530
|
+
slider_container = None
|
|
531
|
+
for i in range(self.left_layout.count()):
|
|
532
|
+
widget = self.left_layout.itemAt(i).widget()
|
|
533
|
+
if widget and hasattr(widget, 'findChild') and widget.findChild(QSlider):
|
|
534
|
+
slider_container = widget
|
|
535
|
+
break
|
|
536
|
+
|
|
537
|
+
if control_panel:
|
|
538
|
+
control_panel.setParent(None)
|
|
539
|
+
self.popped_control_panel = control_panel
|
|
540
|
+
|
|
541
|
+
if slider_container:
|
|
542
|
+
slider_container.setParent(None)
|
|
543
|
+
self.popped_slider_container = slider_container
|
|
544
|
+
|
|
545
|
+
# Move the actual canvas to popup window
|
|
546
|
+
self.popup_window.setCentralWidget(self.canvas)
|
|
547
|
+
|
|
548
|
+
# Create a container widget for the control popup to hold both control panel and slider
|
|
549
|
+
control_container = QWidget()
|
|
550
|
+
control_container_layout = QVBoxLayout(control_container)
|
|
551
|
+
|
|
552
|
+
# Add control panel to container
|
|
553
|
+
if control_panel:
|
|
554
|
+
control_container_layout.addWidget(control_panel)
|
|
555
|
+
|
|
556
|
+
# Add slider container to container
|
|
557
|
+
if slider_container:
|
|
558
|
+
control_container_layout.addWidget(slider_container)
|
|
559
|
+
|
|
560
|
+
# Set the container as the central widget
|
|
561
|
+
self.control_popup_window.setCentralWidget(control_container)
|
|
562
|
+
|
|
563
|
+
# Create placeholder for left panel
|
|
564
|
+
placeholder = QLabel("Canvas and controls are popped out\nClick to return both")
|
|
565
|
+
placeholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
566
|
+
placeholder.setStyleSheet("""
|
|
567
|
+
QLabel {
|
|
568
|
+
background-color: #f0f0f0;
|
|
569
|
+
border: 2px dashed #ccc;
|
|
570
|
+
font-size: 14px;
|
|
571
|
+
color: #666;
|
|
572
|
+
}
|
|
573
|
+
""")
|
|
574
|
+
placeholder.mousePressEvent = lambda event: self.return_canvas()
|
|
575
|
+
|
|
576
|
+
# Add placeholder to left layout
|
|
577
|
+
self.left_layout.insertWidget(0, placeholder) # Insert at canvas position
|
|
578
|
+
self.canvas_placeholder = placeholder
|
|
579
|
+
|
|
580
|
+
# Create keyboard shortcuts for popup windows
|
|
581
|
+
self.create_popup_shortcuts()
|
|
582
|
+
|
|
583
|
+
# Show both popup windows
|
|
584
|
+
self.popup_window.show()
|
|
585
|
+
self.control_popup_window.show()
|
|
586
|
+
|
|
587
|
+
# Ensure proper initial window order
|
|
588
|
+
self.ensure_window_order()
|
|
589
|
+
|
|
590
|
+
# Connect close event to return canvas (only canvas window can be closed)
|
|
591
|
+
self.popup_window.closeEvent = self.on_popup_close
|
|
592
|
+
|
|
593
|
+
def eventFilter(self, obj, event):
|
|
594
|
+
"""Filter events to manage window stacking and keyboard shortcuts"""
|
|
595
|
+
# Handle keyboard events for popup windows
|
|
596
|
+
if (obj == self.popup_window or obj == self.control_popup_window) and event.type() == QEvent.Type.KeyPress:
|
|
597
|
+
# Forward key events to main window's keyPressEvent method
|
|
598
|
+
self.keyPressEvent(event)
|
|
599
|
+
return True # Event handled
|
|
600
|
+
|
|
601
|
+
# Handle scroll events for popup canvas
|
|
602
|
+
if obj == self.popup_window and event.type() == QEvent.Type.Wheel:
|
|
603
|
+
# Forward wheel events to the scroll handler
|
|
604
|
+
canvas_widget = self.popup_window.centralWidget()
|
|
605
|
+
if canvas_widget == self.canvas:
|
|
606
|
+
# Create a mock matplotlib event from the Qt wheel event
|
|
607
|
+
self.handle_popup_scroll(event)
|
|
608
|
+
return True
|
|
609
|
+
|
|
610
|
+
# Existing window stacking code
|
|
611
|
+
if obj == self.popup_window:
|
|
612
|
+
if event.type() == QEvent.Type.WindowActivate:
|
|
613
|
+
# Canvas popup was activated, raise our preferred windows
|
|
614
|
+
QTimer.singleShot(0, self.ensure_window_order)
|
|
615
|
+
elif event.type() == QEvent.Type.FocusIn:
|
|
616
|
+
# Canvas got focus, raise controls
|
|
617
|
+
QTimer.singleShot(0, self.ensure_window_order)
|
|
618
|
+
|
|
619
|
+
return super().eventFilter(obj, event)
|
|
620
|
+
|
|
621
|
+
def create_popup_shortcuts(self):
|
|
622
|
+
"""Create keyboard shortcuts for popup windows"""
|
|
623
|
+
if not hasattr(self, 'popup_shortcuts'):
|
|
624
|
+
self.popup_shortcuts = []
|
|
625
|
+
|
|
626
|
+
# Define shortcuts - using your existing keyPressEvent logic
|
|
627
|
+
shortcuts_config = [
|
|
628
|
+
('Z', lambda: self.zoom_button.click()),
|
|
629
|
+
('Ctrl+Z', self.handle_undo),
|
|
630
|
+
('X', lambda: self.high_button.click()),
|
|
631
|
+
('Shift+F', self.handle_find),
|
|
632
|
+
('Ctrl+S', self.handle_resave),
|
|
633
|
+
('Ctrl+L', lambda: self.load_from_network_obj(directory=self.last_load)),
|
|
634
|
+
('F', lambda: self.toggle_can() if self.brush_mode and self.machine_window is None else None),
|
|
635
|
+
('D', lambda: self.toggle_threed() if self.brush_mode and self.machine_window is None else None),
|
|
636
|
+
('A', lambda: self.machine_window.switch_foreground() if self.machine_window is not None else None),
|
|
637
|
+
]
|
|
638
|
+
|
|
639
|
+
# Create shortcuts for both popup windows
|
|
640
|
+
for key_seq, func in shortcuts_config:
|
|
641
|
+
# Canvas popup window shortcuts
|
|
642
|
+
shortcut1 = QShortcut(QKeySequence(key_seq), self.popup_window)
|
|
643
|
+
shortcut1.activated.connect(func)
|
|
644
|
+
self.popup_shortcuts.append(shortcut1)
|
|
645
|
+
|
|
646
|
+
# Control popup window shortcuts
|
|
647
|
+
shortcut2 = QShortcut(QKeySequence(key_seq), self.control_popup_window)
|
|
648
|
+
shortcut2.activated.connect(func)
|
|
649
|
+
self.popup_shortcuts.append(shortcut2)
|
|
650
|
+
|
|
651
|
+
def handle_undo(self):
|
|
652
|
+
"""Handle undo shortcut"""
|
|
653
|
+
try:
|
|
654
|
+
self.load_channel(self.last_change[1], self.last_change[0], True)
|
|
655
|
+
except:
|
|
656
|
+
pass
|
|
657
|
+
|
|
658
|
+
def handle_popup_scroll(self, qt_event):
|
|
659
|
+
"""Handle scroll events in popup window"""
|
|
660
|
+
# Get the matplotlib canvas from the popup window
|
|
661
|
+
canvas = self.popup_window.centralWidget()
|
|
662
|
+
if canvas == self.canvas:
|
|
663
|
+
# Create a mock matplotlib event to pass to your existing scroll handler
|
|
664
|
+
# You might need to adapt this based on how your on_mpl_scroll method works
|
|
665
|
+
# For now, we'll try to call it directly with the Qt event
|
|
666
|
+
try:
|
|
667
|
+
# If your scroll handler needs a matplotlib event, you may need to
|
|
668
|
+
# create a mock event or adapt the handler
|
|
669
|
+
pass # You'll need to implement the scroll forwarding here
|
|
670
|
+
except:
|
|
671
|
+
pass
|
|
672
|
+
|
|
673
|
+
def ensure_window_order(self):
|
|
674
|
+
"""Ensure control panel and machine window stay above canvas"""
|
|
675
|
+
if hasattr(self, 'control_popup_window') and self.control_popup_window.isVisible():
|
|
676
|
+
self.control_popup_window.raise_()
|
|
677
|
+
|
|
678
|
+
if self.machine_window is not None and self.machine_window.isVisible():
|
|
679
|
+
self.machine_window.raise_()
|
|
680
|
+
|
|
681
|
+
def return_canvas(self):
|
|
682
|
+
"""Return canvas and control panel to main window"""
|
|
683
|
+
if hasattr(self, 'popup_window'):
|
|
684
|
+
# Clean up popup shortcuts
|
|
685
|
+
if hasattr(self, 'popup_shortcuts'):
|
|
686
|
+
for shortcut in self.popup_shortcuts:
|
|
687
|
+
shortcut.deleteLater()
|
|
688
|
+
del self.popup_shortcuts
|
|
689
|
+
|
|
690
|
+
# Remove event filters when returning
|
|
691
|
+
self.popup_window.removeEventFilter(self)
|
|
692
|
+
if hasattr(self, 'control_popup_window'):
|
|
693
|
+
self.control_popup_window.removeEventFilter(self)
|
|
694
|
+
|
|
695
|
+
# Remove canvas from popup
|
|
696
|
+
self.canvas.setParent(None)
|
|
697
|
+
self.is_popped = False
|
|
698
|
+
|
|
699
|
+
# Remove control panel from popup
|
|
700
|
+
if hasattr(self, 'popped_control_panel') and hasattr(self, 'control_popup_window'):
|
|
701
|
+
self.popped_control_panel.setParent(None)
|
|
702
|
+
|
|
703
|
+
# Remove slider container from popup
|
|
704
|
+
if hasattr(self, 'popped_slider_container') and hasattr(self, 'control_popup_window'):
|
|
705
|
+
self.popped_slider_container.setParent(None)
|
|
706
|
+
|
|
707
|
+
# Remove placeholder
|
|
708
|
+
if hasattr(self, 'canvas_placeholder'):
|
|
709
|
+
self.canvas_placeholder.setParent(None)
|
|
710
|
+
del self.canvas_placeholder
|
|
711
|
+
|
|
712
|
+
# Reset canvas to original size and size policy
|
|
713
|
+
if hasattr(self, 'original_figure_size'):
|
|
714
|
+
self.figure.set_size_inches(self.original_figure_size)
|
|
715
|
+
if hasattr(self, 'original_canvas_size_policy'):
|
|
716
|
+
self.canvas.setSizePolicy(self.original_canvas_size_policy)
|
|
717
|
+
|
|
718
|
+
# Reset canvas minimum and maximum sizes to allow proper resizing
|
|
719
|
+
self.canvas.setMinimumSize(0, 0) # Remove any minimum size constraints
|
|
720
|
+
self.canvas.setMaximumSize(16777215, 16777215) # Reset to Qt's default maximum
|
|
721
|
+
|
|
722
|
+
# Return canvas to left panel
|
|
723
|
+
self.left_layout.insertWidget(0, self.canvas) # Insert at top
|
|
724
|
+
|
|
725
|
+
# Return control panel to left panel (after canvas)
|
|
726
|
+
if hasattr(self, 'popped_control_panel'):
|
|
727
|
+
self.left_layout.insertWidget(1, self.popped_control_panel) # Insert after canvas
|
|
728
|
+
del self.popped_control_panel
|
|
729
|
+
|
|
730
|
+
# Return slider container to left panel (after control panel)
|
|
731
|
+
if hasattr(self, 'popped_slider_container'):
|
|
732
|
+
self.left_layout.insertWidget(2, self.popped_slider_container) # Insert after control panel
|
|
733
|
+
del self.popped_slider_container
|
|
734
|
+
|
|
735
|
+
# Force the canvas to redraw with proper sizing
|
|
736
|
+
self.canvas.draw()
|
|
737
|
+
|
|
738
|
+
# Reset the main window layout to ensure proper proportions
|
|
739
|
+
# Get the main widget and force layout recalculation
|
|
740
|
+
main_widget = self.centralWidget()
|
|
741
|
+
if main_widget:
|
|
742
|
+
main_widget.updateGeometry()
|
|
743
|
+
main_widget.update()
|
|
744
|
+
|
|
745
|
+
# Close both popup windows
|
|
746
|
+
self.popup_window.close()
|
|
747
|
+
if hasattr(self, 'control_popup_window'):
|
|
748
|
+
self.control_popup_window.close()
|
|
749
|
+
del self.control_popup_window
|
|
750
|
+
|
|
751
|
+
# Clean up stored size references
|
|
752
|
+
if hasattr(self, 'original_figure_size'):
|
|
753
|
+
del self.original_figure_size
|
|
754
|
+
if hasattr(self, 'original_canvas_size_policy'):
|
|
755
|
+
del self.original_canvas_size_policy
|
|
756
|
+
|
|
757
|
+
def on_popup_close(self, event):
|
|
758
|
+
"""Return canvas when popup is closed"""
|
|
759
|
+
self.return_canvas()
|
|
760
|
+
event.accept()
|
|
761
|
+
|
|
469
762
|
def start_left_scroll(self):
|
|
470
763
|
"""Start scrolling left when left arrow is pressed."""
|
|
471
764
|
# Single increment first
|
|
@@ -500,7 +793,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
500
793
|
self.slice_slider.setValue(new_value)
|
|
501
794
|
elif self.scroll_direction > 0 and new_value <= self.slice_slider.maximum():
|
|
502
795
|
self.slice_slider.setValue(new_value)
|
|
503
|
-
|
|
504
796
|
|
|
505
797
|
def create_highlight_overlay(self, node_indices=None, edge_indices=None, overlay1_indices = None, overlay2_indices = None, bounds = False):
|
|
506
798
|
"""
|
|
@@ -514,6 +806,17 @@ class ImageViewerWindow(QMainWindow):
|
|
|
514
806
|
self.mini_overlay = False #If this method is ever being called, it means we are rendering the entire overlay so mini overlay needs to reset.
|
|
515
807
|
self.mini_overlay_data = None
|
|
516
808
|
|
|
809
|
+
|
|
810
|
+
if not self.high_button.isChecked():
|
|
811
|
+
|
|
812
|
+
if len(self.clicked_values['edges']) > 0:
|
|
813
|
+
self.format_for_upperright_table(self.clicked_values['edges'], title = 'Selected Edges')
|
|
814
|
+
if len(self.clicked_values['nodes']) > 0:
|
|
815
|
+
self.format_for_upperright_table(self.clicked_values['nodes'], title = 'Selected Nodes')
|
|
816
|
+
|
|
817
|
+
return
|
|
818
|
+
|
|
819
|
+
|
|
517
820
|
def process_chunk(chunk_data, indices_to_check):
|
|
518
821
|
"""Process a single chunk of the array to create highlight mask"""
|
|
519
822
|
mask = np.isin(chunk_data, indices_to_check)
|
|
@@ -550,7 +853,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
550
853
|
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
551
854
|
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
552
855
|
|
|
553
|
-
if not node_indices and not edge_indices and not overlay1_indices and not overlay2_indices:
|
|
856
|
+
if not node_indices and not edge_indices and not overlay1_indices and not overlay2_indices and self.machine_window is None:
|
|
554
857
|
self.highlight_overlay = None
|
|
555
858
|
self.highlight_bounds = None
|
|
556
859
|
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
@@ -623,7 +926,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
623
926
|
self.highlight_overlay = np.maximum(self.highlight_overlay, overlay1_overlay)
|
|
624
927
|
if overlay2_overlay is not None:
|
|
625
928
|
self.highlight_overlay = np.maximum(self.highlight_overlay, overlay2_overlay)
|
|
626
|
-
|
|
929
|
+
|
|
627
930
|
# Update display
|
|
628
931
|
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
629
932
|
|
|
@@ -822,9 +1125,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
822
1125
|
|
|
823
1126
|
|
|
824
1127
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
1128
|
|
|
829
1129
|
#METHODS RELATED TO RIGHT CLICK:
|
|
830
1130
|
|
|
@@ -1991,7 +2291,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1991
2291
|
del my_network.network_lists[0][i]
|
|
1992
2292
|
del my_network.network_lists[1][i]
|
|
1993
2293
|
del my_network.network_lists[2][i]
|
|
1994
|
-
|
|
2294
|
+
for node in self.clicked_values['nodes']:
|
|
2295
|
+
del my_network.node_centroids[node]
|
|
2296
|
+
del my_network.node_identities[node]
|
|
2297
|
+
del my_network.communities[node]
|
|
1995
2298
|
|
|
1996
2299
|
|
|
1997
2300
|
if len(self.clicked_values['edges']) > 0:
|
|
@@ -2006,6 +2309,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2006
2309
|
del my_network.network_lists[0][i]
|
|
2007
2310
|
del my_network.network_lists[1][i]
|
|
2008
2311
|
del my_network.network_lists[2][i]
|
|
2312
|
+
for node in self.clicked_values['edges']:
|
|
2313
|
+
del my_network.edge_centroids[edge]
|
|
2009
2314
|
|
|
2010
2315
|
my_network.network_lists = my_network.network_lists
|
|
2011
2316
|
|
|
@@ -2021,7 +2326,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2021
2326
|
for column in range(model.columnCount(None)):
|
|
2022
2327
|
self.network_table.resizeColumnToContents(column)
|
|
2023
2328
|
|
|
2024
|
-
self.show_centroid_dialog()
|
|
2329
|
+
#self.show_centroid_dialog()
|
|
2025
2330
|
except Exception as e:
|
|
2026
2331
|
print(f"Error: {e}")
|
|
2027
2332
|
|
|
@@ -2147,11 +2452,16 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2147
2452
|
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
2148
2453
|
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
2149
2454
|
|
|
2150
|
-
if self.high_button.isChecked():
|
|
2455
|
+
if self.high_button.isChecked() and self.machine_window is None:
|
|
2151
2456
|
if self.highlight_overlay is None and ((len(self.clicked_values['nodes']) + len(self.clicked_values['edges'])) > 0):
|
|
2152
2457
|
if self.needs_mini:
|
|
2153
2458
|
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
2154
2459
|
self.needs_mini = False
|
|
2460
|
+
else:
|
|
2461
|
+
self.create_highlight_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
2462
|
+
else:
|
|
2463
|
+
self.create_highlight_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
2464
|
+
|
|
2155
2465
|
|
|
2156
2466
|
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
2157
2467
|
|
|
@@ -2164,7 +2474,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2164
2474
|
self.pan_button.setChecked(False)
|
|
2165
2475
|
|
|
2166
2476
|
self.pen_button.setChecked(False)
|
|
2167
|
-
self.pan_mode = False
|
|
2168
2477
|
self.brush_mode = False
|
|
2169
2478
|
self.can = False
|
|
2170
2479
|
self.threed = False
|
|
@@ -2187,6 +2496,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2187
2496
|
current_xlim = self.ax.get_xlim()
|
|
2188
2497
|
current_ylim = self.ax.get_ylim()
|
|
2189
2498
|
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
2499
|
+
self.pan_mode = False
|
|
2190
2500
|
|
|
2191
2501
|
else:
|
|
2192
2502
|
if self.machine_window is None:
|
|
@@ -2234,15 +2544,31 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2234
2544
|
# Store current channel visibility state
|
|
2235
2545
|
self.pre_pan_channel_state = self.channel_visible.copy()
|
|
2236
2546
|
|
|
2237
|
-
|
|
2238
|
-
self.
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2547
|
+
current_xlim = self.ax.get_xlim()
|
|
2548
|
+
current_ylim = self.ax.get_ylim()
|
|
2549
|
+
|
|
2550
|
+
if (abs(current_xlim[1] - current_xlim[0]) * abs(current_ylim[0] - current_ylim[1]) > 400 * 400 and not self.shape[2] * self.shape[1] > 9000 * 9000 * 6) or self.shape[2] * self.shape[1] < 3000 * 3000:
|
|
2551
|
+
|
|
2552
|
+
# Create static background from currently visible channels
|
|
2553
|
+
self.create_pan_background()
|
|
2554
|
+
|
|
2555
|
+
# Hide all channels and show only the background
|
|
2556
|
+
self.channel_visible = [False] * 4
|
|
2557
|
+
self.is_pan_preview = True
|
|
2558
|
+
|
|
2559
|
+
# Get current downsample factor
|
|
2560
|
+
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
2561
|
+
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
2562
|
+
# Update display to show only background
|
|
2563
|
+
self.update_display_pan_mode(current_xlim, current_ylim)
|
|
2564
|
+
self.needs_update = False
|
|
2565
|
+
else:
|
|
2566
|
+
self.needs_update = True
|
|
2567
|
+
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
2568
|
+
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
2569
|
+
# Update display to show only background
|
|
2570
|
+
self._first_pan_done = False
|
|
2571
|
+
self.update_display(current_xlim, current_ylim)
|
|
2246
2572
|
|
|
2247
2573
|
else:
|
|
2248
2574
|
current_xlim = self.ax.get_xlim()
|
|
@@ -2714,7 +3040,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2714
3040
|
self.canvas.blit(self.ax.bbox)
|
|
2715
3041
|
|
|
2716
3042
|
elif self.panning and self.pan_start is not None:
|
|
2717
|
-
|
|
2718
3043
|
# Calculate the movement
|
|
2719
3044
|
dx = event.xdata - self.pan_start[0]
|
|
2720
3045
|
dy = event.ydata - self.pan_start[1]
|
|
@@ -2728,22 +3053,30 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2728
3053
|
new_ylim = [ylim[0] - dy, ylim[1] - dy]
|
|
2729
3054
|
|
|
2730
3055
|
# Get image bounds using cached dimensions
|
|
2731
|
-
if self.img_width is not None:
|
|
3056
|
+
if self.img_width is not None:
|
|
2732
3057
|
# Ensure new limits don't go beyond image bounds
|
|
2733
3058
|
if new_xlim[0] < 0:
|
|
2734
3059
|
new_xlim = [0, xlim[1] - xlim[0]]
|
|
2735
|
-
elif new_xlim[1] > self.img_width:
|
|
3060
|
+
elif new_xlim[1] > self.img_width:
|
|
2736
3061
|
new_xlim = [self.img_width - (xlim[1] - xlim[0]), self.img_width]
|
|
2737
3062
|
|
|
2738
3063
|
if new_ylim[0] < 0:
|
|
2739
3064
|
new_ylim = [0, ylim[1] - ylim[0]]
|
|
2740
|
-
elif new_ylim[1] > self.img_height:
|
|
3065
|
+
elif new_ylim[1] > self.img_height:
|
|
2741
3066
|
new_ylim = [self.img_height - (ylim[1] - ylim[0]), self.img_height]
|
|
2742
3067
|
|
|
2743
3068
|
# Apply new limits
|
|
2744
3069
|
self.ax.set_xlim(new_xlim)
|
|
2745
3070
|
self.ax.set_ylim(new_ylim)
|
|
2746
|
-
|
|
3071
|
+
|
|
3072
|
+
# Only call draw_idle if we have a pan background OR if this isn't the first pan
|
|
3073
|
+
if self.pan_background_image is not None or self._first_pan_done == True:
|
|
3074
|
+
self.canvas.draw_idle()
|
|
3075
|
+
else:
|
|
3076
|
+
# For the first pan without background, mark that we've done the first pan
|
|
3077
|
+
self._first_pan_done = True
|
|
3078
|
+
# Force a proper display update instead of draw_idle
|
|
3079
|
+
self.update_display(preserve_zoom=(new_xlim, new_ylim))
|
|
2747
3080
|
|
|
2748
3081
|
# Update pan start position
|
|
2749
3082
|
self.pan_start = (event.xdata, event.ydata)
|
|
@@ -2781,8 +3114,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2781
3114
|
self.pan_background_image = self.create_composite_for_pan()
|
|
2782
3115
|
self.pan_zoom_state = (current_xlim, current_ylim)
|
|
2783
3116
|
|
|
3117
|
+
|
|
2784
3118
|
def create_composite_for_pan(self):
|
|
2785
|
-
"""Create a properly rendered composite image for panning"""
|
|
3119
|
+
"""Create a properly rendered composite image for panning with downsample support"""
|
|
2786
3120
|
# Get active channels and dimensions (copied from update_display)
|
|
2787
3121
|
active_channels = [i for i in range(4) if self.channel_data[i] is not None]
|
|
2788
3122
|
if active_channels:
|
|
@@ -2793,8 +3127,58 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2793
3127
|
else:
|
|
2794
3128
|
return None
|
|
2795
3129
|
|
|
2796
|
-
#
|
|
2797
|
-
|
|
3130
|
+
# Store original dimensions for coordinate mapping
|
|
3131
|
+
self.original_dims = (min_height, min_width)
|
|
3132
|
+
|
|
3133
|
+
# Get current downsample factor
|
|
3134
|
+
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
3135
|
+
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
3136
|
+
|
|
3137
|
+
|
|
3138
|
+
# Calculate the visible region in pixel coordinates
|
|
3139
|
+
x_min = max(0, int(np.floor(current_xlim[0] + 0.5)))
|
|
3140
|
+
x_max = min(min_width, int(np.ceil(current_xlim[1] + 0.5)))
|
|
3141
|
+
y_min = max(0, int(np.floor(current_ylim[1] + 0.5))) # Note: y is flipped
|
|
3142
|
+
y_max = min(min_height, int(np.ceil(current_ylim[0] + 0.5)))
|
|
3143
|
+
|
|
3144
|
+
box_len = x_max - x_min
|
|
3145
|
+
box_height = y_max - y_min
|
|
3146
|
+
x_min = max(0, x_min - box_len)
|
|
3147
|
+
x_max = min(self.shape[2], x_max + box_len)
|
|
3148
|
+
y_min = max(0, y_min - box_height)
|
|
3149
|
+
y_max = min(self.shape[1], y_max + box_height)
|
|
3150
|
+
|
|
3151
|
+
# If using image pyramids
|
|
3152
|
+
size = (x_max - x_min) * (y_max - y_min)
|
|
3153
|
+
val = int(np.ceil(size/(3000 * 3000)))
|
|
3154
|
+
if self.shape[1] * self.shape[2] > 3000 * 3000 * val:
|
|
3155
|
+
val = 3
|
|
3156
|
+
|
|
3157
|
+
self.validate_downsample_input(text = val, update = False)
|
|
3158
|
+
|
|
3159
|
+
downsample_factor = self.downsample_factor
|
|
3160
|
+
|
|
3161
|
+
# Calculate display dimensions (downsampled)
|
|
3162
|
+
display_height = min_height // downsample_factor
|
|
3163
|
+
display_width = min_width // downsample_factor
|
|
3164
|
+
|
|
3165
|
+
# Helper function to downsample image (same as in update_display)
|
|
3166
|
+
def downsample_image(image, factor):
|
|
3167
|
+
if factor == 1:
|
|
3168
|
+
return image
|
|
3169
|
+
|
|
3170
|
+
# Handle different image types
|
|
3171
|
+
if len(image.shape) == 2:
|
|
3172
|
+
# Grayscale
|
|
3173
|
+
return image[::factor, ::factor]
|
|
3174
|
+
elif len(image.shape) == 3:
|
|
3175
|
+
# RGB/RGBA
|
|
3176
|
+
return image[::factor, ::factor, :]
|
|
3177
|
+
else:
|
|
3178
|
+
return image
|
|
3179
|
+
|
|
3180
|
+
# Create a blank RGBA composite to accumulate all channels (using display dimensions)
|
|
3181
|
+
composite = np.zeros((display_height, display_width, 4), dtype=np.float32)
|
|
2798
3182
|
|
|
2799
3183
|
# Process each visible channel exactly like update_display does
|
|
2800
3184
|
for channel in range(4):
|
|
@@ -2811,24 +3195,27 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2811
3195
|
else:
|
|
2812
3196
|
current_image = self.channel_data[channel]
|
|
2813
3197
|
|
|
3198
|
+
# Downsample the image for rendering
|
|
3199
|
+
display_image = downsample_image(current_image, downsample_factor)
|
|
3200
|
+
|
|
2814
3201
|
if is_rgb and self.channel_data[channel].shape[-1] == 3:
|
|
2815
3202
|
# RGB image - convert to RGBA and blend
|
|
2816
|
-
rgb_alpha = np.ones((*
|
|
2817
|
-
rgb_alpha[:, :, :3] =
|
|
3203
|
+
rgb_alpha = np.ones((*display_image.shape[:2], 4), dtype=np.float32)
|
|
3204
|
+
rgb_alpha[:, :, :3] = display_image.astype(np.float32) / 255.0
|
|
2818
3205
|
rgb_alpha[:, :, 3] = 0.7 # Same alpha as update_display
|
|
2819
3206
|
composite = self.blend_layers(composite, rgb_alpha)
|
|
2820
3207
|
|
|
2821
3208
|
elif is_rgb and self.channel_data[channel].shape[-1] == 4:
|
|
2822
3209
|
# RGBA image - blend directly
|
|
2823
|
-
rgba_image =
|
|
3210
|
+
rgba_image = display_image.astype(np.float32) / 255.0
|
|
2824
3211
|
composite = self.blend_layers(composite, rgba_image)
|
|
2825
3212
|
|
|
2826
3213
|
else:
|
|
2827
3214
|
# Regular channel processing (same logic as update_display)
|
|
2828
3215
|
if self.min_max[channel][0] == None:
|
|
2829
|
-
self.min_max[channel][0] = np.min(
|
|
3216
|
+
self.min_max[channel][0] = np.min(self.channel_data[channel])
|
|
2830
3217
|
if self.min_max[channel][1] == None:
|
|
2831
|
-
self.min_max[channel][1] = np.max(
|
|
3218
|
+
self.min_max[channel][1] = np.max(self.channel_data[channel])
|
|
2832
3219
|
|
|
2833
3220
|
img_min = self.min_max[channel][0]
|
|
2834
3221
|
img_max = self.min_max[channel][1]
|
|
@@ -2840,16 +3227,16 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2840
3227
|
vmin = img_min + (img_max - img_min) * self.channel_brightness[channel]['min']
|
|
2841
3228
|
vmax = img_min + (img_max - img_min) * self.channel_brightness[channel]['max']
|
|
2842
3229
|
|
|
2843
|
-
# Normalize the image
|
|
3230
|
+
# Normalize the downsampled image
|
|
2844
3231
|
if vmin == vmax:
|
|
2845
|
-
normalized_image = np.zeros_like(
|
|
3232
|
+
normalized_image = np.zeros_like(display_image)
|
|
2846
3233
|
else:
|
|
2847
|
-
normalized_image = np.clip((
|
|
3234
|
+
normalized_image = np.clip((display_image - vmin) / (vmax - vmin), 0, 1)
|
|
2848
3235
|
|
|
2849
3236
|
# Apply channel color and alpha
|
|
2850
3237
|
if channel == 2 and self.machine_window is not None:
|
|
2851
3238
|
# Special case for machine window channel 2
|
|
2852
|
-
channel_rgba = self.apply_machine_colormap(
|
|
3239
|
+
channel_rgba = self.apply_machine_colormap(display_image)
|
|
2853
3240
|
else:
|
|
2854
3241
|
# Regular channel with custom color
|
|
2855
3242
|
color = self.base_colors[channel]
|
|
@@ -2862,21 +3249,24 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2862
3249
|
# Blend this channel into the composite
|
|
2863
3250
|
composite = self.blend_layers(composite, channel_rgba)
|
|
2864
3251
|
|
|
2865
|
-
# Add highlight overlays if they exist (
|
|
3252
|
+
# Add highlight overlays if they exist (with downsampling)
|
|
2866
3253
|
if self.mini_overlay and self.highlight and self.machine_window is None:
|
|
2867
|
-
|
|
3254
|
+
display_overlay = downsample_image(self.mini_overlay_data, downsample_factor)
|
|
3255
|
+
highlight_rgba = self.create_highlight_rgba(display_overlay, yellow=True)
|
|
2868
3256
|
composite = self.blend_layers(composite, highlight_rgba)
|
|
2869
3257
|
elif self.highlight_overlay is not None and self.highlight:
|
|
2870
3258
|
highlight_slice = self.highlight_overlay[self.current_slice]
|
|
3259
|
+
display_highlight = downsample_image(highlight_slice, downsample_factor)
|
|
2871
3260
|
if self.machine_window is None:
|
|
2872
|
-
highlight_rgba = self.create_highlight_rgba(
|
|
3261
|
+
highlight_rgba = self.create_highlight_rgba(display_highlight, yellow=True)
|
|
2873
3262
|
else:
|
|
2874
|
-
highlight_rgba = self.create_highlight_rgba(
|
|
3263
|
+
highlight_rgba = self.create_highlight_rgba(display_highlight, yellow=False)
|
|
2875
3264
|
composite = self.blend_layers(composite, highlight_rgba)
|
|
2876
3265
|
|
|
2877
3266
|
# Convert to 0-255 range for display
|
|
2878
3267
|
return (composite * 255).astype(np.uint8)
|
|
2879
3268
|
|
|
3269
|
+
|
|
2880
3270
|
def apply_machine_colormap(self, image):
|
|
2881
3271
|
"""Apply the special machine window colormap for channel 2"""
|
|
2882
3272
|
rgba = np.zeros((*image.shape, 4), dtype=np.float32)
|
|
@@ -2902,7 +3292,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2902
3292
|
if yellow:
|
|
2903
3293
|
# Yellow highlight
|
|
2904
3294
|
mask = highlight_data > 0
|
|
2905
|
-
rgba[mask] = [1, 1, 0, 0.
|
|
3295
|
+
rgba[mask] = [1, 1, 0, 0.8] # Yellow with alpha 0.5
|
|
2906
3296
|
else:
|
|
2907
3297
|
# Multi-color highlight for machine window
|
|
2908
3298
|
mask_1 = (highlight_data == 1)
|
|
@@ -2914,7 +3304,31 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2914
3304
|
|
|
2915
3305
|
def blend_layers(self, base, overlay):
|
|
2916
3306
|
"""Alpha blend two RGBA layers"""
|
|
2917
|
-
|
|
3307
|
+
|
|
3308
|
+
def resize_overlay_to_base(overlay_arr, base_arr):
|
|
3309
|
+
base_height, base_width = base_arr.shape[:2]
|
|
3310
|
+
overlay_height, overlay_width = overlay_arr.shape[:2]
|
|
3311
|
+
|
|
3312
|
+
# First crop if overlay is larger
|
|
3313
|
+
cropped_overlay = overlay_arr[:base_height, :base_width]
|
|
3314
|
+
|
|
3315
|
+
# Then pad if still smaller after cropping
|
|
3316
|
+
current_height, current_width = cropped_overlay.shape[:2]
|
|
3317
|
+
pad_height = base_height - current_height
|
|
3318
|
+
pad_width = base_width - current_width
|
|
3319
|
+
|
|
3320
|
+
if pad_height > 0 or pad_width > 0:
|
|
3321
|
+
cropped_overlay = np.pad(cropped_overlay,
|
|
3322
|
+
((0, pad_height), (0, pad_width), (0, 0)),
|
|
3323
|
+
mode='constant', constant_values=0)
|
|
3324
|
+
|
|
3325
|
+
return cropped_overlay
|
|
3326
|
+
|
|
3327
|
+
# Resize the ENTIRE overlay array to match base dimensions
|
|
3328
|
+
if overlay.shape[:2] != base.shape[:2]:
|
|
3329
|
+
overlay = resize_overlay_to_base(overlay, base)
|
|
3330
|
+
|
|
3331
|
+
# Now extract alpha channels (they should be the same size)
|
|
2918
3332
|
alpha_overlay = overlay[:, :, 3:4]
|
|
2919
3333
|
alpha_base = base[:, :, 3:4]
|
|
2920
3334
|
|
|
@@ -2935,31 +3349,105 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2935
3349
|
|
|
2936
3350
|
return result
|
|
2937
3351
|
|
|
2938
|
-
def update_display_pan_mode(self):
|
|
2939
|
-
"""Lightweight display update for pan preview mode"""
|
|
3352
|
+
def update_display_pan_mode(self, current_xlim, current_ylim):
|
|
3353
|
+
"""Lightweight display update for pan preview mode with downsample support"""
|
|
2940
3354
|
|
|
2941
3355
|
if self.is_pan_preview and self.pan_background_image is not None:
|
|
2942
3356
|
# Clear and setup axes
|
|
2943
3357
|
self.ax.clear()
|
|
2944
3358
|
self.ax.set_facecolor('black')
|
|
2945
3359
|
|
|
2946
|
-
# Get dimensions
|
|
2947
|
-
|
|
2948
|
-
|
|
3360
|
+
# Get original dimensions (before downsampling)
|
|
3361
|
+
if hasattr(self, 'original_dims') and self.original_dims:
|
|
3362
|
+
height, width = self.original_dims
|
|
3363
|
+
else:
|
|
3364
|
+
# Fallback to pan background image dimensions
|
|
3365
|
+
height, width = self.pan_background_image.shape[:2]
|
|
3366
|
+
# If we have downsample factor, scale back up
|
|
3367
|
+
downsample_factor = getattr(self, 'downsample_factor', 1)
|
|
3368
|
+
height *= downsample_factor
|
|
3369
|
+
width *= downsample_factor
|
|
3370
|
+
|
|
3371
|
+
def crop_image(image, y_start, y_end, x_start, x_end):
|
|
3372
|
+
# Crop
|
|
3373
|
+
if len(image.shape) == 2:
|
|
3374
|
+
cropped = image[y_start:y_end, x_start:x_end]
|
|
3375
|
+
elif len(image.shape) == 3:
|
|
3376
|
+
cropped = image[y_start:y_end, x_start:x_end, :]
|
|
3377
|
+
else:
|
|
3378
|
+
cropped = image
|
|
3379
|
+
|
|
3380
|
+
return cropped
|
|
3381
|
+
|
|
3382
|
+
|
|
3383
|
+
downsample_factor = self.downsample_factor
|
|
3384
|
+
min_height, min_width = self.original_dims
|
|
3385
|
+
|
|
3386
|
+
# Calculate the visible region in pixel coordinates
|
|
3387
|
+
x_min = max(0, int(np.floor(current_xlim[0] + 0.5)))
|
|
3388
|
+
x_max = min(min_width, int(np.ceil(current_xlim[1] + 0.5)))
|
|
3389
|
+
y_min = max(0, int(np.floor(current_ylim[1] + 0.5))) # Note: y is flipped
|
|
3390
|
+
y_max = min(min_height, int(np.ceil(current_ylim[0] + 0.5)))
|
|
3391
|
+
|
|
3392
|
+
box_len = x_max - x_min
|
|
3393
|
+
box_height = y_max - y_min
|
|
3394
|
+
x_min = max(0, x_min - box_len)
|
|
3395
|
+
x_max = min(self.shape[2], x_max + box_len)
|
|
3396
|
+
y_min = max(0, y_min - box_height)
|
|
3397
|
+
y_max = min(self.shape[1], y_max + box_height)
|
|
3398
|
+
|
|
3399
|
+
# Add some padding to avoid edge artifacts during pan/zoom
|
|
3400
|
+
padding = max(10, downsample_factor * 2)
|
|
3401
|
+
x_min_padded = max(0, x_min - padding)
|
|
3402
|
+
x_max_padded = min(min_width, x_max + padding)
|
|
3403
|
+
y_min_padded = max(0, y_min - padding)
|
|
3404
|
+
y_max_padded = min(min_height, y_max + padding)
|
|
3405
|
+
|
|
3406
|
+
# Convert coordinates to downsampled space for cropping
|
|
3407
|
+
downsample_factor = self.downsample_factor
|
|
3408
|
+
|
|
3409
|
+
# Convert to downsampled coordinates for cropping
|
|
3410
|
+
x_min_ds = x_min // downsample_factor
|
|
3411
|
+
x_max_ds = x_max // downsample_factor
|
|
3412
|
+
y_min_ds = y_min // downsample_factor
|
|
3413
|
+
y_max_ds = y_max // downsample_factor
|
|
3414
|
+
|
|
3415
|
+
# Add padding in downsampled space
|
|
3416
|
+
padding_ds = max(10 // downsample_factor, 2)
|
|
3417
|
+
x_min_padded_ds = max(0, x_min_ds - padding_ds)
|
|
3418
|
+
x_max_padded_ds = min(self.pan_background_image.shape[1], x_max_ds + padding_ds)
|
|
3419
|
+
y_min_padded_ds = max(0, y_min_ds - padding_ds)
|
|
3420
|
+
y_max_padded_ds = min(self.pan_background_image.shape[0], y_max_ds + padding_ds)
|
|
3421
|
+
|
|
3422
|
+
# Crop using downsampled coordinates
|
|
3423
|
+
display_image = crop_image(
|
|
3424
|
+
self.pan_background_image, y_min_padded_ds, y_max_padded_ds,
|
|
3425
|
+
x_min_padded_ds, x_max_padded_ds)
|
|
3426
|
+
|
|
3427
|
+
# Calculate the extent for the cropped region (in original coordinates)
|
|
3428
|
+
crop_extent = (x_min_padded - 0.5, x_max_padded - 0.5,
|
|
3429
|
+
y_max_padded - 0.5, y_min_padded - 0.5)
|
|
3430
|
+
|
|
2949
3431
|
# Display the composite background with preserved zoom
|
|
2950
|
-
|
|
2951
|
-
|
|
3432
|
+
# Use extent to stretch downsampled image back to original coordinate space
|
|
3433
|
+
self.ax.imshow(display_image,
|
|
3434
|
+
extent=crop_extent,
|
|
2952
3435
|
aspect='equal')
|
|
2953
3436
|
|
|
2954
3437
|
# Restore the zoom state from when pan began
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
3438
|
+
self.ax.set_xlim(current_xlim)
|
|
3439
|
+
self.ax.set_ylim(current_ylim)
|
|
3440
|
+
|
|
3441
|
+
# Get downsample factor for title display
|
|
3442
|
+
downsample_factor = getattr(self, 'downsample_factor', 1)
|
|
2958
3443
|
|
|
2959
3444
|
# Style the axes (same as update_display)
|
|
2960
3445
|
self.ax.set_xlabel('X')
|
|
2961
|
-
self.ax.set_ylabel('Y')
|
|
2962
|
-
|
|
3446
|
+
self.ax.set_ylabel('Y')
|
|
3447
|
+
if downsample_factor > 1:
|
|
3448
|
+
self.ax.set_title(f'Slice {self.current_slice}')
|
|
3449
|
+
else:
|
|
3450
|
+
self.ax.set_title(f'Slice {self.current_slice}')
|
|
2963
3451
|
self.ax.xaxis.label.set_color('black')
|
|
2964
3452
|
self.ax.yaxis.label.set_color('black')
|
|
2965
3453
|
self.ax.title.set_color('black')
|
|
@@ -2967,7 +3455,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2967
3455
|
for spine in self.ax.spines.values():
|
|
2968
3456
|
spine.set_color('black')
|
|
2969
3457
|
|
|
2970
|
-
# Add measurement points if they exist (
|
|
3458
|
+
# Add measurement points if they exist (coordinates remain in original space)
|
|
2971
3459
|
for point in self.measurement_points:
|
|
2972
3460
|
x1, y1, z1 = point['point1']
|
|
2973
3461
|
x2, y2, z2 = point['point2']
|
|
@@ -2999,6 +3487,15 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2999
3487
|
|
|
3000
3488
|
if self.pan_mode:
|
|
3001
3489
|
|
|
3490
|
+
# Update display to show only background
|
|
3491
|
+
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
3492
|
+
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
3493
|
+
# Update display to show only background
|
|
3494
|
+
if self.pan_background_image is not None:
|
|
3495
|
+
self.update_display_pan_mode(current_xlim, current_ylim)
|
|
3496
|
+
else:
|
|
3497
|
+
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
3498
|
+
|
|
3002
3499
|
self.panning = False
|
|
3003
3500
|
self.pan_start = None
|
|
3004
3501
|
self.canvas.setCursor(Qt.CursorShape.OpenHandCursor)
|
|
@@ -3019,9 +3516,13 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3019
3516
|
self.show_crop_dialog(args)
|
|
3020
3517
|
|
|
3021
3518
|
elif self.zoom_mode: #Optional targeted zoom
|
|
3519
|
+
# Calculate aspect ratio to avoid zooming into very thin rectangles
|
|
3520
|
+
aspect_ratio = width / height if height > 0 else float('inf')
|
|
3022
3521
|
|
|
3023
|
-
|
|
3024
|
-
|
|
3522
|
+
# Skip zoom if the rectangle is too narrow/thin (adjust thresholds as needed)
|
|
3523
|
+
if width > 10 and height > 10 and 0.1 < aspect_ratio < 10:
|
|
3524
|
+
self.ax.set_xlim([x0, x0 + width])
|
|
3525
|
+
self.ax.set_ylim([y0 + height, y0])
|
|
3025
3526
|
|
|
3026
3527
|
self.zoom_changed = True # Flag that zoom has changed
|
|
3027
3528
|
|
|
@@ -3093,7 +3594,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3093
3594
|
except:
|
|
3094
3595
|
pass
|
|
3095
3596
|
self.selection_rect = None
|
|
3096
|
-
self.
|
|
3597
|
+
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
3598
|
+
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
3599
|
+
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
3600
|
+
#self.canvas.draw()
|
|
3097
3601
|
|
|
3098
3602
|
elif self.zoom_mode:
|
|
3099
3603
|
# Handle zoom mode press
|
|
@@ -3126,11 +3630,13 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3126
3630
|
|
|
3127
3631
|
new_xlim = [xdata - x_range, xdata + x_range]
|
|
3128
3632
|
new_ylim = [ydata - y_range, ydata + y_range]
|
|
3633
|
+
|
|
3634
|
+
shift_pressed = 'shift' in event.modifiers
|
|
3129
3635
|
|
|
3130
|
-
if (new_xlim[0] <=
|
|
3131
|
-
new_xlim[1] >= self.
|
|
3132
|
-
new_ylim[0] <=
|
|
3133
|
-
new_ylim[1] >= self.
|
|
3636
|
+
if (new_xlim[0] <= 0 or
|
|
3637
|
+
new_xlim[1] >= self.shape[2] or
|
|
3638
|
+
new_ylim[0] <= 0 or
|
|
3639
|
+
new_ylim[1] >= self.shape[1]) or shift_pressed:
|
|
3134
3640
|
self.ax.set_xlim(self.original_xlim)
|
|
3135
3641
|
self.ax.set_ylim(self.original_ylim)
|
|
3136
3642
|
else:
|
|
@@ -3142,7 +3648,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3142
3648
|
if not hasattr(self, 'zoom_changed'):
|
|
3143
3649
|
self.zoom_changed = False
|
|
3144
3650
|
|
|
3145
|
-
self.
|
|
3651
|
+
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
3652
|
+
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
3653
|
+
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
3654
|
+
#self.canvas.draw()
|
|
3146
3655
|
|
|
3147
3656
|
# Handle brush mode cleanup with paint session management
|
|
3148
3657
|
if self.brush_mode and hasattr(self, 'painting') and self.painting:
|
|
@@ -3277,7 +3786,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3277
3786
|
|
|
3278
3787
|
if not hasattr(self, 'zoom_changed'):
|
|
3279
3788
|
self.zoom_changed = False
|
|
3280
|
-
|
|
3789
|
+
|
|
3281
3790
|
elif event.button == 3: # Right click - zoom out
|
|
3282
3791
|
x_range = (current_xlim[1] - current_xlim[0])
|
|
3283
3792
|
y_range = (current_ylim[1] - current_ylim[0])
|
|
@@ -3296,10 +3805,11 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3296
3805
|
self.ax.set_ylim(new_ylim)
|
|
3297
3806
|
|
|
3298
3807
|
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3808
|
+
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
3809
|
+
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
3810
|
+
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
3811
|
+
|
|
3812
|
+
#self.canvas.draw()
|
|
3303
3813
|
|
|
3304
3814
|
elif event.button == 3: # Right click
|
|
3305
3815
|
self.create_context_menu(event)
|
|
@@ -3606,11 +4116,85 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3606
4116
|
help_button = menubar.addAction("Help")
|
|
3607
4117
|
help_button.triggered.connect(self.help_me)
|
|
3608
4118
|
|
|
4119
|
+
# Initialize downsample factor
|
|
4120
|
+
self.downsample_factor = 1
|
|
4121
|
+
|
|
4122
|
+
# Create container widget for corner controls
|
|
4123
|
+
corner_widget = QWidget()
|
|
4124
|
+
corner_layout = QHBoxLayout(corner_widget)
|
|
4125
|
+
corner_layout.setContentsMargins(5, 0, 5, 0)
|
|
4126
|
+
|
|
4127
|
+
|
|
4128
|
+
# Add after your other buttons
|
|
4129
|
+
self.popup_button = QPushButton("⤴") # or "🔗" or "⤴"
|
|
4130
|
+
self.popup_button.setFixedSize(40, 40)
|
|
4131
|
+
self.popup_button.setToolTip("Pop out canvas")
|
|
4132
|
+
self.popup_button.clicked.connect(self.popup_canvas)
|
|
4133
|
+
corner_layout.addWidget(self.popup_button)
|
|
4134
|
+
|
|
4135
|
+
# Add some spacing
|
|
4136
|
+
corner_layout.addSpacing(10)
|
|
4137
|
+
|
|
4138
|
+
# Add camera button
|
|
3609
4139
|
cam_button = QPushButton("📷")
|
|
3610
4140
|
cam_button.setFixedSize(40, 40)
|
|
3611
|
-
cam_button.setStyleSheet("font-size: 24px;")
|
|
4141
|
+
cam_button.setStyleSheet("font-size: 24px;")
|
|
3612
4142
|
cam_button.clicked.connect(self.snap)
|
|
3613
|
-
|
|
4143
|
+
corner_layout.addWidget(cam_button)
|
|
4144
|
+
|
|
4145
|
+
# Set as corner widget
|
|
4146
|
+
menubar.setCornerWidget(corner_widget, Qt.Corner.TopRightCorner)
|
|
4147
|
+
|
|
4148
|
+
def on_downsample_changed(self, text):
|
|
4149
|
+
"""Called whenever the text in the downsample input changes"""
|
|
4150
|
+
try:
|
|
4151
|
+
if text.strip() == "":
|
|
4152
|
+
self.downsample_factor = 1
|
|
4153
|
+
else:
|
|
4154
|
+
value = float(text)
|
|
4155
|
+
if value <= 0:
|
|
4156
|
+
self.downsample_factor = 1
|
|
4157
|
+
else:
|
|
4158
|
+
self.downsample_factor = int(value) if value == int(value) else value
|
|
4159
|
+
except (ValueError, TypeError):
|
|
4160
|
+
self.downsample_factor = 1
|
|
4161
|
+
|
|
4162
|
+
def validate_downsample_input(self, text = None, update = True):
|
|
4163
|
+
"""Called when user finishes editing (loses focus or presses Enter)"""
|
|
4164
|
+
if text:
|
|
4165
|
+
self.downsample_factor = text
|
|
4166
|
+
else:
|
|
4167
|
+
try: # If enabled for manual display downsampling
|
|
4168
|
+
text = self.downsample_input.text().strip()
|
|
4169
|
+
if text == "":
|
|
4170
|
+
# Empty input - set to default
|
|
4171
|
+
self.downsample_factor = 1
|
|
4172
|
+
self.downsample_input.setText("1")
|
|
4173
|
+
else:
|
|
4174
|
+
value = int(text)
|
|
4175
|
+
if value < 1:
|
|
4176
|
+
# Invalid value - reset to default
|
|
4177
|
+
self.downsample_factor = 1
|
|
4178
|
+
self.downsample_input.setText("1")
|
|
4179
|
+
else:
|
|
4180
|
+
# Valid value - use it (prefer int if possible)
|
|
4181
|
+
if value == int(value):
|
|
4182
|
+
self.downsample_factor = int(value)
|
|
4183
|
+
self.downsample_input.setText(str(int(value)))
|
|
4184
|
+
else:
|
|
4185
|
+
self.downsample_factor = value
|
|
4186
|
+
self.downsample_input.setText(f"{value:.1f}")
|
|
4187
|
+
except:
|
|
4188
|
+
# Invalid input - reset to default
|
|
4189
|
+
self.downsample_factor = 1
|
|
4190
|
+
|
|
4191
|
+
self.throttle = self.shape[1] * self.shape[2] > 3000 * 3000 * self.downsample_factor
|
|
4192
|
+
|
|
4193
|
+
# Optional: Trigger display update if you want immediate effect
|
|
4194
|
+
if update:
|
|
4195
|
+
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
4196
|
+
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
4197
|
+
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
3614
4198
|
|
|
3615
4199
|
def snap(self):
|
|
3616
4200
|
try:
|
|
@@ -3643,7 +4227,14 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3643
4227
|
filename += '.png'
|
|
3644
4228
|
format_type = 'png'
|
|
3645
4229
|
|
|
3646
|
-
|
|
4230
|
+
if self.downsample_factor > 1:
|
|
4231
|
+
self.pan_mode = True # Update display will ignore downsamples if this is true so we can just use it here
|
|
4232
|
+
self.downsample_factor = 1
|
|
4233
|
+
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
4234
|
+
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
4235
|
+
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
4236
|
+
|
|
4237
|
+
# Save with axes bbox
|
|
3647
4238
|
bbox = self.ax.get_window_extent().transformed(self.figure.dpi_scale_trans.inverted())
|
|
3648
4239
|
self.figure.savefig(filename,
|
|
3649
4240
|
dpi=300,
|
|
@@ -3654,6 +4245,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3654
4245
|
pad_inches=0)
|
|
3655
4246
|
|
|
3656
4247
|
print(f"Axes snapshot saved: {filename}")
|
|
4248
|
+
|
|
4249
|
+
self.toggle_pan_mode() # Assesses pan state since we messed with its vars potentially
|
|
3657
4250
|
|
|
3658
4251
|
except Exception as e:
|
|
3659
4252
|
print(f"Error saving snapshot: {e}")
|
|
@@ -4372,7 +4965,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4372
4965
|
|
|
4373
4966
|
if directory != "":
|
|
4374
4967
|
|
|
4375
|
-
self.reset(network = True, xy_scale = 1, z_scale = 1, edges = True, network_overlay = True, id_overlay = True)
|
|
4968
|
+
self.reset(network = True, xy_scale = 1, z_scale = 1, nodes = True, edges = True, network_overlay = True, id_overlay = True, update = False)
|
|
4969
|
+
|
|
4376
4970
|
|
|
4377
4971
|
my_network.assemble(directory)
|
|
4378
4972
|
|
|
@@ -4399,7 +4993,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4399
4993
|
if channel is not None:
|
|
4400
4994
|
self.slice_slider.setEnabled(True)
|
|
4401
4995
|
self.slice_slider.setMinimum(0)
|
|
4402
|
-
self.slice_slider.setMaximum(
|
|
4996
|
+
self.slice_slider.setMaximum(self.shape[0] - 1)
|
|
4403
4997
|
self.slice_slider.setValue(0)
|
|
4404
4998
|
self.current_slice = 0
|
|
4405
4999
|
break
|
|
@@ -4685,6 +5279,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4685
5279
|
"""Load a channel and enable active channel selection if needed."""
|
|
4686
5280
|
|
|
4687
5281
|
try:
|
|
5282
|
+
|
|
5283
|
+
self.hold_update = True
|
|
4688
5284
|
if not data: # For solo loading
|
|
4689
5285
|
filename, _ = QFileDialog.getOpenFileName(
|
|
4690
5286
|
self,
|
|
@@ -4752,9 +5348,13 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4752
5348
|
try:
|
|
4753
5349
|
if len(self.channel_data[channel_index].shape) == 3: # potentially 2D RGB
|
|
4754
5350
|
if self.channel_data[channel_index].shape[-1] in (3, 4): # last dim is 3 or 4
|
|
4755
|
-
if self.
|
|
4756
|
-
|
|
5351
|
+
if not data and self.shape is None:
|
|
5352
|
+
if self.confirm_rgb_dialog():
|
|
5353
|
+
# User confirmed it's 2D RGB, expand to 4D
|
|
5354
|
+
self.channel_data[channel_index] = np.expand_dims(self.channel_data[channel_index], axis=0)
|
|
5355
|
+
elif self.shape[0] == 1: # this can only be true if the user already loaded in a 2d image
|
|
4757
5356
|
self.channel_data[channel_index] = np.expand_dims(self.channel_data[channel_index], axis=0)
|
|
5357
|
+
|
|
4758
5358
|
except:
|
|
4759
5359
|
pass
|
|
4760
5360
|
|
|
@@ -4861,6 +5461,11 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4861
5461
|
pass
|
|
4862
5462
|
|
|
4863
5463
|
self.shape = self.channel_data[channel_index].shape
|
|
5464
|
+
if self.shape[1] * self.shape[2] > 3000 * 3000 * self.downsample_factor:
|
|
5465
|
+
self.throttle = True
|
|
5466
|
+
else:
|
|
5467
|
+
self.throttle = False
|
|
5468
|
+
|
|
4864
5469
|
|
|
4865
5470
|
self.img_height, self.img_width = self.shape[1], self.shape[2]
|
|
4866
5471
|
self.original_ylim, self.original_xlim = (self.shape[1] + 0.5, -0.5), (-0.5, self.shape[2] - 0.5)
|
|
@@ -4874,11 +5479,15 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4874
5479
|
self.current_operation = []
|
|
4875
5480
|
self.current_operation_type = None
|
|
4876
5481
|
|
|
4877
|
-
if
|
|
5482
|
+
if self.pan_mode:
|
|
5483
|
+
self.pan_button.click()
|
|
5484
|
+
if self.show_channels:
|
|
5485
|
+
self.channel_buttons[channel_index].click()
|
|
5486
|
+
self.channel_buttons[channel_index].click()
|
|
5487
|
+
elif not end_paint:
|
|
4878
5488
|
|
|
4879
5489
|
self.update_display(reset_resize = reset_resize, preserve_zoom = preserve_zoom)
|
|
4880
5490
|
|
|
4881
|
-
|
|
4882
5491
|
|
|
4883
5492
|
except Exception as e:
|
|
4884
5493
|
|
|
@@ -4890,7 +5499,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4890
5499
|
f"Failed to load tiff file: {str(e)}"
|
|
4891
5500
|
)
|
|
4892
5501
|
|
|
4893
|
-
def delete_channel(self, channel_index, called = True):
|
|
5502
|
+
def delete_channel(self, channel_index, called = True, update = True):
|
|
4894
5503
|
"""Delete the specified channel and update the display."""
|
|
4895
5504
|
if called:
|
|
4896
5505
|
# Confirm deletion
|
|
@@ -4936,11 +5545,13 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4936
5545
|
else:
|
|
4937
5546
|
# If no channels are available, disable active channel selector
|
|
4938
5547
|
self.active_channel_combo.setEnabled(False)
|
|
5548
|
+
self.shape = None # Also there is not an active shape anymore
|
|
4939
5549
|
|
|
4940
|
-
|
|
4941
|
-
|
|
5550
|
+
if update:
|
|
5551
|
+
# Update display
|
|
5552
|
+
self.update_display()
|
|
4942
5553
|
|
|
4943
|
-
def reset(self, nodes = False, network = False, xy_scale = 1, z_scale = 1, edges = False, search_region = False, network_overlay = False, id_overlay = False):
|
|
5554
|
+
def reset(self, nodes = False, network = False, xy_scale = 1, z_scale = 1, edges = False, search_region = False, network_overlay = False, id_overlay = False, update = True):
|
|
4944
5555
|
"""Method to flexibly reset certain fields to free up the RAM as desired"""
|
|
4945
5556
|
|
|
4946
5557
|
# Set scales first before any clearing operations
|
|
@@ -4961,10 +5572,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4961
5572
|
self.selection_table.setModel(PandasModel(empty_df))
|
|
4962
5573
|
|
|
4963
5574
|
if nodes:
|
|
4964
|
-
self.delete_channel(0, False)
|
|
5575
|
+
self.delete_channel(0, False, update = update)
|
|
4965
5576
|
|
|
4966
5577
|
if edges:
|
|
4967
|
-
self.delete_channel(1, False)
|
|
5578
|
+
self.delete_channel(1, False, update = update)
|
|
4968
5579
|
try:
|
|
4969
5580
|
if search_region:
|
|
4970
5581
|
my_network.search_region = None
|
|
@@ -4972,10 +5583,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4972
5583
|
pass
|
|
4973
5584
|
|
|
4974
5585
|
if network_overlay:
|
|
4975
|
-
self.delete_channel(2, False)
|
|
5586
|
+
self.delete_channel(2, False, update = update)
|
|
4976
5587
|
|
|
4977
5588
|
if id_overlay:
|
|
4978
|
-
self.delete_channel(3, False)
|
|
5589
|
+
self.delete_channel(3, False, update = update)
|
|
4979
5590
|
|
|
4980
5591
|
|
|
4981
5592
|
|
|
@@ -5109,9 +5720,14 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5109
5720
|
self.current_slice = slice_value
|
|
5110
5721
|
if self.mini_overlay == True: #If we are rendering the highlight overlay for selected values one at a time.
|
|
5111
5722
|
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
5112
|
-
self.
|
|
5723
|
+
if not self.hold_update:
|
|
5724
|
+
self.update_display(preserve_zoom=view_settings)
|
|
5725
|
+
else:
|
|
5726
|
+
self.hold_update = False
|
|
5113
5727
|
#if self.machine_window is not None:
|
|
5114
5728
|
#self.machine_window.poke_segmenter()
|
|
5729
|
+
if self.pan_mode:
|
|
5730
|
+
self.pan_button.click()
|
|
5115
5731
|
self.pending_slice = None
|
|
5116
5732
|
|
|
5117
5733
|
def update_brightness(self, channel_index, values):
|
|
@@ -5127,51 +5743,59 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5127
5743
|
self.update_display(preserve_zoom = (current_xlim, current_ylim))
|
|
5128
5744
|
|
|
5129
5745
|
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
def update_display(self, preserve_zoom=None, dims = None, called = False, reset_resize = False):
|
|
5133
|
-
"""Update the display with currently visible channels and highlight overlay."""
|
|
5746
|
+
def update_display(self, preserve_zoom=None, dims=None, called=False, reset_resize=False, skip=False):
|
|
5747
|
+
"""Optimized display update with view-based cropping for performance."""
|
|
5134
5748
|
try:
|
|
5135
|
-
|
|
5749
|
+
# Initialize reusable components if they don't exist
|
|
5750
|
+
if not hasattr(self, 'channel_images'):
|
|
5751
|
+
self.channel_images = {}
|
|
5752
|
+
self.highlight_image = None
|
|
5753
|
+
self.measurement_artists = []
|
|
5754
|
+
self.axes_initialized = False
|
|
5755
|
+
self.original_dims = None
|
|
5756
|
+
|
|
5757
|
+
# Handle special states (pan, static background)
|
|
5136
5758
|
if self.pan_background_image is not None:
|
|
5137
|
-
# Restore previously visible channels
|
|
5138
5759
|
self.channel_visible = self.pre_pan_channel_state.copy()
|
|
5139
5760
|
self.is_pan_preview = False
|
|
5140
5761
|
self.pan_background_image = None
|
|
5141
5762
|
if self.resume:
|
|
5142
5763
|
self.machine_window.segmentation_worker.resume()
|
|
5143
5764
|
self.resume = False
|
|
5765
|
+
if self.prev_down != self.downsample_factor:
|
|
5766
|
+
self.validate_downsample_input(text = self.prev_down)
|
|
5767
|
+
return
|
|
5768
|
+
|
|
5144
5769
|
if self.static_background is not None:
|
|
5145
|
-
#
|
|
5770
|
+
# Your existing virtual strokes conversion logic
|
|
5146
5771
|
if (hasattr(self, 'virtual_draw_operations') and self.virtual_draw_operations) or \
|
|
5147
5772
|
(hasattr(self, 'virtual_erase_operations') and self.virtual_erase_operations) or \
|
|
5148
5773
|
(hasattr(self, 'current_operation') and self.current_operation):
|
|
5149
|
-
# Finish current operation first
|
|
5150
5774
|
if hasattr(self, 'current_operation') and self.current_operation:
|
|
5151
5775
|
self.pm.finish_current_virtual_operation()
|
|
5152
|
-
# Now convert to real data
|
|
5153
5776
|
self.pm.convert_virtual_strokes_to_data()
|
|
5154
5777
|
|
|
5155
|
-
# Restore hidden channels
|
|
5156
5778
|
try:
|
|
5157
5779
|
for i in self.restore_channels:
|
|
5158
5780
|
self.channel_visible[i] = True
|
|
5159
5781
|
self.restore_channels = []
|
|
5160
5782
|
except:
|
|
5161
5783
|
pass
|
|
5162
|
-
|
|
5163
5784
|
self.static_background = None
|
|
5164
|
-
|
|
5785
|
+
|
|
5786
|
+
# Your existing machine_window logic
|
|
5165
5787
|
if self.machine_window is None:
|
|
5166
5788
|
try:
|
|
5167
|
-
self.channel_data[4][self.current_slice, :, :] = n3d.overlay_arrays_simple(
|
|
5168
|
-
|
|
5789
|
+
self.channel_data[4][self.current_slice, :, :] = n3d.overlay_arrays_simple(
|
|
5790
|
+
self.channel_data[self.temp_chan][self.current_slice, :, :],
|
|
5791
|
+
self.channel_data[4][self.current_slice, :, :])
|
|
5792
|
+
self.load_channel(self.temp_chan, self.channel_data[4], data=True, end_paint=True)
|
|
5169
5793
|
self.channel_data[4] = None
|
|
5170
5794
|
self.channel_visible[4] = False
|
|
5171
5795
|
except:
|
|
5172
5796
|
pass
|
|
5173
5797
|
|
|
5174
|
-
# Get
|
|
5798
|
+
# Get dimensions
|
|
5175
5799
|
active_channels = [i for i in range(4) if self.channel_data[i] is not None]
|
|
5176
5800
|
if dims is None:
|
|
5177
5801
|
if active_channels:
|
|
@@ -5180,247 +5804,260 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5180
5804
|
min_height = min(d[0] for d in dims)
|
|
5181
5805
|
min_width = min(d[1] for d in dims)
|
|
5182
5806
|
else:
|
|
5183
|
-
min_height = 1
|
|
5184
|
-
min_width = 1
|
|
5807
|
+
min_height = min_width = 1
|
|
5185
5808
|
else:
|
|
5186
|
-
min_height = dims[
|
|
5187
|
-
|
|
5809
|
+
min_height, min_width = dims[:2]
|
|
5810
|
+
|
|
5811
|
+
# Store original dimensions for pixel coordinate conversion
|
|
5812
|
+
self.original_dims = (min_height, min_width)
|
|
5813
|
+
|
|
5814
|
+
# Initialize axes only once or when needed
|
|
5815
|
+
if not self.axes_initialized or not hasattr(self, 'ax') or self.ax is None:
|
|
5816
|
+
self.figure.clear()
|
|
5817
|
+
self.figure.patch.set_facecolor('white')
|
|
5818
|
+
self.ax = self.figure.add_subplot(111)
|
|
5819
|
+
self.ax.set_facecolor('black')
|
|
5820
|
+
self.axes_initialized = True
|
|
5821
|
+
|
|
5822
|
+
# Style the axes once
|
|
5823
|
+
self.ax.set_xlabel('X')
|
|
5824
|
+
self.ax.set_ylabel('Y')
|
|
5825
|
+
self.ax.xaxis.label.set_color('black')
|
|
5826
|
+
self.ax.yaxis.label.set_color('black')
|
|
5827
|
+
self.ax.tick_params(colors='black')
|
|
5828
|
+
for spine in self.ax.spines.values():
|
|
5829
|
+
spine.set_color('black')
|
|
5830
|
+
else:
|
|
5831
|
+
# Clear only the image data, keep axes structure
|
|
5832
|
+
for img in list(self.ax.get_images()):
|
|
5833
|
+
img.remove()
|
|
5834
|
+
# Clear measurement points
|
|
5835
|
+
for artist in self.measurement_artists:
|
|
5836
|
+
artist.remove()
|
|
5837
|
+
self.measurement_artists.clear()
|
|
5838
|
+
|
|
5839
|
+
# Determine the current view bounds (either from preserve_zoom or current state)
|
|
5840
|
+
if preserve_zoom:
|
|
5841
|
+
current_xlim, current_ylim = preserve_zoom
|
|
5842
|
+
else:
|
|
5843
|
+
current_xlim = (-0.5, self.shape[2] - 0.5)
|
|
5844
|
+
current_ylim = (self.shape[1] - 0.5, -0.5)
|
|
5845
|
+
|
|
5846
|
+
# Calculate the visible region in pixel coordinates
|
|
5847
|
+
x_min = max(0, int(np.floor(current_xlim[0] + 0.5)))
|
|
5848
|
+
x_max = min(min_width, int(np.ceil(current_xlim[1] + 0.5)))
|
|
5849
|
+
y_min = max(0, int(np.floor(current_ylim[1] + 0.5))) # Note: y is flipped
|
|
5850
|
+
y_max = min(min_height, int(np.ceil(current_ylim[0] + 0.5)))
|
|
5188
5851
|
|
|
5189
|
-
|
|
5852
|
+
if self.pan_mode:
|
|
5853
|
+
box_len = (x_max - x_min)
|
|
5854
|
+
box_height = (y_max - y_min)
|
|
5855
|
+
x_min = max(0, x_min - box_len)
|
|
5856
|
+
x_max = min(self.shape[2], x_max + box_len)
|
|
5857
|
+
y_min = max(0, y_min - box_height)
|
|
5858
|
+
y_max = min(self.shape[1], y_max + box_height)
|
|
5859
|
+
|
|
5860
|
+
size = (x_max - x_min) * (y_max - y_min)
|
|
5861
|
+
val = int(np.ceil(size/(3000 * 3000)))
|
|
5862
|
+
self.validate_downsample_input(text = val, update = False)
|
|
5863
|
+
|
|
5864
|
+
downsample_factor = self.downsample_factor
|
|
5865
|
+
|
|
5866
|
+
# Add some padding to avoid edge artifacts during pan/zoom
|
|
5867
|
+
padding = max(10, downsample_factor * 2)
|
|
5868
|
+
x_min_padded = max(0, x_min - padding)
|
|
5869
|
+
x_max_padded = min(min_width, x_max + padding)
|
|
5870
|
+
y_min_padded = max(0, y_min - padding)
|
|
5871
|
+
y_max_padded = min(min_height, y_max + padding)
|
|
5872
|
+
|
|
5873
|
+
# Calculate the extent for the cropped region (in original coordinates)
|
|
5874
|
+
crop_extent = (x_min_padded - 0.5, x_max_padded - 0.5,
|
|
5875
|
+
y_max_padded - 0.5, y_min_padded - 0.5)
|
|
5876
|
+
|
|
5877
|
+
# Set limits to original dimensions (important for pixel queries)
|
|
5190
5878
|
self.ax.set_xlim(-0.5, min_width - 0.5)
|
|
5191
5879
|
self.ax.set_ylim(min_height - 0.5, -0.5)
|
|
5880
|
+
self.ax.set_title(f'Slice {self.current_slice}')
|
|
5881
|
+
self.ax.title.set_color('black')
|
|
5192
5882
|
|
|
5193
|
-
# Create subplot with tight layout and white figure background
|
|
5194
|
-
self.figure.patch.set_facecolor('white')
|
|
5195
|
-
self.ax = self.figure.add_subplot(111)
|
|
5196
|
-
|
|
5197
|
-
# Store current zoom limits if they exist and weren't provided
|
|
5198
|
-
|
|
5199
|
-
current_xlim, current_ylim = preserve_zoom if preserve_zoom else (None, None)
|
|
5200
|
-
|
|
5201
|
-
# Define base colors for each channel with increased intensity
|
|
5202
5883
|
base_colors = self.base_colors
|
|
5203
|
-
# Set only the axes (image area) background to black
|
|
5204
|
-
self.ax.set_facecolor('black')
|
|
5205
5884
|
|
|
5206
|
-
#
|
|
5885
|
+
# Helper function to crop and downsample image
|
|
5886
|
+
def crop_and_downsample_image(image, y_start, y_end, x_start, x_end, factor):
|
|
5887
|
+
# Crop first
|
|
5888
|
+
if len(image.shape) == 2:
|
|
5889
|
+
cropped = image[y_start:y_end, x_start:x_end]
|
|
5890
|
+
elif len(image.shape) == 3:
|
|
5891
|
+
cropped = image[y_start:y_end, x_start:x_end, :]
|
|
5892
|
+
else:
|
|
5893
|
+
cropped = image
|
|
5894
|
+
|
|
5895
|
+
# Then downsample if needed
|
|
5896
|
+
if factor == 1:
|
|
5897
|
+
return cropped
|
|
5898
|
+
|
|
5899
|
+
if len(cropped.shape) == 2:
|
|
5900
|
+
return cropped[::factor, ::factor]
|
|
5901
|
+
elif len(cropped.shape) == 3:
|
|
5902
|
+
return cropped[::factor, ::factor, :]
|
|
5903
|
+
else:
|
|
5904
|
+
return cropped
|
|
5905
|
+
|
|
5906
|
+
|
|
5907
|
+
# Update channel images efficiently with cropping and downsampling
|
|
5207
5908
|
for channel in range(4):
|
|
5208
|
-
if
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
is_rgb = len(self.channel_data[channel].shape) == 4 and (self.channel_data[channel].shape[-1] == 3 or self.channel_data[channel].shape[-1] == 4)
|
|
5909
|
+
if self.channel_visible[channel] and self.channel_data[channel] is not None:
|
|
5910
|
+
# Get current image data
|
|
5911
|
+
is_rgb = len(self.channel_data[channel].shape) == 4 and (
|
|
5912
|
+
self.channel_data[channel].shape[-1] in [3, 4])
|
|
5213
5913
|
|
|
5214
5914
|
if len(self.channel_data[channel].shape) == 3 and not is_rgb:
|
|
5215
5915
|
current_image = self.channel_data[channel][self.current_slice, :, :]
|
|
5216
5916
|
elif is_rgb:
|
|
5217
|
-
current_image = self.channel_data[channel][self.current_slice]
|
|
5917
|
+
current_image = self.channel_data[channel][self.current_slice]
|
|
5218
5918
|
else:
|
|
5219
5919
|
current_image = self.channel_data[channel]
|
|
5220
5920
|
|
|
5921
|
+
# Crop and downsample the image for rendering
|
|
5922
|
+
display_image = crop_and_downsample_image(
|
|
5923
|
+
current_image, y_min_padded, y_max_padded,
|
|
5924
|
+
x_min_padded, x_max_padded, downsample_factor)
|
|
5925
|
+
|
|
5221
5926
|
if is_rgb and self.channel_data[channel].shape[-1] in [3, 4]:
|
|
5222
|
-
#
|
|
5223
|
-
|
|
5224
|
-
# Calculate alpha based on brightness settings
|
|
5927
|
+
# RGB handling (keep your existing logic)
|
|
5225
5928
|
brightness_min = self.channel_brightness[channel]['min']
|
|
5226
5929
|
brightness_max = self.channel_brightness[channel]['max']
|
|
5227
|
-
|
|
5228
|
-
# Map brightness range to alpha range (0.0 to 1.0)
|
|
5229
|
-
# brightness_min controls minimum alpha, brightness_max controls maximum alpha
|
|
5230
5930
|
alpha_range = brightness_max - brightness_min
|
|
5231
|
-
base_alpha = brightness_min
|
|
5232
|
-
|
|
5233
|
-
final_alpha = base_alpha + alpha_range # Scale to reasonable alpha range
|
|
5234
|
-
final_alpha = np.clip(final_alpha, 0.0, 1.0) # Ensure valid alpha range
|
|
5931
|
+
base_alpha = brightness_min
|
|
5932
|
+
final_alpha = np.clip(base_alpha + alpha_range, 0.0, 1.0)
|
|
5235
5933
|
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
# For RGBA, multiply existing alpha by our brightness-controlled alpha
|
|
5239
|
-
img_with_alpha = current_image.copy()
|
|
5934
|
+
if display_image.shape[-1] == 4:
|
|
5935
|
+
img_with_alpha = display_image.copy()
|
|
5240
5936
|
img_with_alpha[..., 3] = img_with_alpha[..., 3] * final_alpha
|
|
5241
|
-
|
|
5937
|
+
# Use crop_extent to place in correct location
|
|
5938
|
+
im = self.ax.imshow(img_with_alpha, extent=crop_extent)
|
|
5242
5939
|
else:
|
|
5243
|
-
|
|
5244
|
-
self.ax.imshow(current_image, alpha=final_alpha)
|
|
5245
|
-
|
|
5940
|
+
im = self.ax.imshow(display_image, alpha=final_alpha, extent=crop_extent)
|
|
5246
5941
|
else:
|
|
5247
|
-
# Regular channel processing with
|
|
5248
|
-
# Calculate brightness/contrast limits from entire volume
|
|
5942
|
+
# Regular channel processing with optimized normalization
|
|
5249
5943
|
if self.min_max[channel][0] is None:
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5944
|
+
# For very large arrays, consider sampling for min/max
|
|
5945
|
+
if self.channel_data[channel].size > 1000000:
|
|
5946
|
+
sample = self.channel_data[channel][::max(1, self.channel_data[channel].shape[0]//100)]
|
|
5947
|
+
self.min_max[channel] = [np.min(sample), np.max(sample)]
|
|
5948
|
+
else:
|
|
5949
|
+
self.min_max[channel] = [np.min(self.channel_data[channel]),
|
|
5950
|
+
np.max(self.channel_data[channel])]
|
|
5951
|
+
|
|
5952
|
+
img_min, img_max = self.min_max[channel]
|
|
5255
5953
|
|
|
5256
|
-
# Calculate vmin and vmax, ensuring we don't get a zero range
|
|
5257
5954
|
if img_min == img_max:
|
|
5258
|
-
vmin = img_min
|
|
5259
|
-
|
|
5955
|
+
vmin, vmax = img_min, img_min + 1
|
|
5956
|
+
normalized_image = np.zeros_like(display_image)
|
|
5260
5957
|
else:
|
|
5261
5958
|
vmin = img_min + (img_max - img_min) * self.channel_brightness[channel]['min']
|
|
5262
5959
|
vmax = img_min + (img_max - img_min) * self.channel_brightness[channel]['max']
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
normalized_image = np.clip((current_image - vmin) / (vmax - vmin), 0, 1)
|
|
5960
|
+
|
|
5961
|
+
if vmin == vmax:
|
|
5962
|
+
normalized_image = np.zeros_like(display_image)
|
|
5963
|
+
else:
|
|
5964
|
+
normalized_image = np.clip((display_image - vmin) / (vmax - vmin), 0, 1)
|
|
5269
5965
|
|
|
5270
5966
|
if channel == 2 and self.machine_window is not None:
|
|
5271
5967
|
custom_cmap = LinearSegmentedColormap.from_list(
|
|
5272
5968
|
f'custom_{channel}',
|
|
5273
|
-
[(0, 0, 0, 0),
|
|
5274
|
-
(0.5, 1, 0.5, 1), # light green for 1
|
|
5275
|
-
(1, 0.5, 0.5, 1)] # light red for 2
|
|
5969
|
+
[(0, 0, 0, 0), (0.5, 1, 0.5, 1), (1, 0.5, 0.5, 1)]
|
|
5276
5970
|
)
|
|
5277
|
-
self.ax.imshow(
|
|
5278
|
-
|
|
5279
|
-
vmin=0,
|
|
5280
|
-
vmax=2,
|
|
5281
|
-
alpha=0.7,
|
|
5282
|
-
interpolation='nearest',
|
|
5283
|
-
extent=(-0.5, min_width-0.5, min_height-0.5, -0.5))
|
|
5971
|
+
im = self.ax.imshow(display_image, cmap=custom_cmap, vmin=0, vmax=2,
|
|
5972
|
+
alpha=0.7, interpolation='nearest', extent=crop_extent)
|
|
5284
5973
|
else:
|
|
5285
|
-
# Create custom colormap with higher intensity
|
|
5286
5974
|
color = base_colors[channel]
|
|
5287
5975
|
custom_cmap = LinearSegmentedColormap.from_list(
|
|
5288
|
-
f'custom_{channel}',
|
|
5289
|
-
[(0,0,0,0), (*color,1)]
|
|
5290
|
-
)
|
|
5976
|
+
f'custom_{channel}', [(0,0,0,0), (*color,1)])
|
|
5291
5977
|
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
alpha=0.7,
|
|
5295
|
-
cmap=custom_cmap,
|
|
5296
|
-
vmin=0,
|
|
5297
|
-
vmax=1,
|
|
5298
|
-
extent=(-0.5, min_width-0.5, min_height-0.5, -0.5))
|
|
5978
|
+
im = self.ax.imshow(normalized_image, alpha=0.7, cmap=custom_cmap,
|
|
5979
|
+
vmin=0, vmax=1, extent=crop_extent)
|
|
5299
5980
|
|
|
5981
|
+
# Handle preview, overlays, and measurements (apply cropping here too)
|
|
5300
5982
|
if self.preview and not called:
|
|
5301
|
-
self.create_highlight_overlay_slice(self.targs, bounds
|
|
5983
|
+
self.create_highlight_overlay_slice(self.targs, bounds=self.bounds)
|
|
5302
5984
|
|
|
5303
|
-
#
|
|
5985
|
+
# Overlay handling (optimized with cropping and downsampling)
|
|
5304
5986
|
if self.mini_overlay and self.highlight and self.machine_window is None:
|
|
5305
|
-
highlight_cmap = LinearSegmentedColormap.from_list(
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
self.ax.imshow(
|
|
5310
|
-
cmap=highlight_cmap,
|
|
5311
|
-
alpha=0.8)
|
|
5312
|
-
elif self.highlight_overlay is not None and self.highlight and self.machine_window is None:
|
|
5313
|
-
highlight_slice = self.highlight_overlay[self.current_slice]
|
|
5314
|
-
highlight_cmap = LinearSegmentedColormap.from_list(
|
|
5315
|
-
'highlight',
|
|
5316
|
-
[(0, 0, 0, 0), (1, 1, 0, 1)] # yellow
|
|
5317
|
-
)
|
|
5318
|
-
self.ax.imshow(highlight_slice,
|
|
5319
|
-
cmap=highlight_cmap,
|
|
5320
|
-
alpha=0.8)
|
|
5987
|
+
highlight_cmap = LinearSegmentedColormap.from_list('highlight', [(0, 0, 0, 0), (1, 1, 0, 1)])
|
|
5988
|
+
display_overlay = crop_and_downsample_image(
|
|
5989
|
+
self.mini_overlay_data, y_min_padded, y_max_padded,
|
|
5990
|
+
x_min_padded, x_max_padded, downsample_factor)
|
|
5991
|
+
self.ax.imshow(display_overlay, cmap=highlight_cmap, alpha=0.8, extent=crop_extent)
|
|
5321
5992
|
elif self.highlight_overlay is not None and self.highlight:
|
|
5322
5993
|
highlight_slice = self.highlight_overlay[self.current_slice]
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
self.ax.imshow(highlight_slice,
|
|
5330
|
-
cmap=highlight_cmap,
|
|
5331
|
-
vmin=0,
|
|
5332
|
-
vmax=2, # Important: set vmax to 2 to accommodate both values
|
|
5333
|
-
alpha=0.3)
|
|
5334
|
-
|
|
5335
|
-
if self.channel_data[4] is not None:
|
|
5336
|
-
|
|
5337
|
-
highlight_slice = self.channel_data[4][self.current_slice]
|
|
5338
|
-
img_min = self.min_max[4][0]
|
|
5339
|
-
img_max = self.min_max[4][1]
|
|
5340
|
-
|
|
5341
|
-
# Calculate vmin and vmax, ensuring we don't get a zero range
|
|
5342
|
-
if img_min == img_max:
|
|
5343
|
-
vmin = img_min
|
|
5344
|
-
vmax = img_min + 1
|
|
5345
|
-
else:
|
|
5346
|
-
vmin = img_min + (img_max - img_min) * self.channel_brightness[4]['min']
|
|
5347
|
-
vmax = img_min + (img_max - img_min) * self.channel_brightness[4]['max']
|
|
5348
|
-
|
|
5349
|
-
# Normalize the image safely
|
|
5350
|
-
if vmin == vmax:
|
|
5351
|
-
normalized_image = np.zeros_like(highlight_slice)
|
|
5994
|
+
display_highlight = crop_and_downsample_image(
|
|
5995
|
+
highlight_slice, y_min_padded, y_max_padded,
|
|
5996
|
+
x_min_padded, x_max_padded, downsample_factor)
|
|
5997
|
+
if self.machine_window is None:
|
|
5998
|
+
highlight_cmap = LinearSegmentedColormap.from_list('highlight', [(0, 0, 0, 0), (1, 1, 0, 1)])
|
|
5999
|
+
self.ax.imshow(display_highlight, cmap=highlight_cmap, alpha=0.8, extent=crop_extent)
|
|
5352
6000
|
else:
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
custom_cmap = LinearSegmentedColormap.from_list(
|
|
5357
|
-
f'custom_{4}',
|
|
5358
|
-
[(0,0,0,0), (*color,1)]
|
|
5359
|
-
)
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
self.ax.imshow(normalized_image,
|
|
5363
|
-
alpha=0.7,
|
|
5364
|
-
cmap=custom_cmap,
|
|
5365
|
-
vmin=0,
|
|
5366
|
-
vmax=1)
|
|
5367
|
-
|
|
5368
|
-
# Style the axes
|
|
5369
|
-
self.ax.set_xlabel('X')
|
|
5370
|
-
self.ax.set_ylabel('Y')
|
|
5371
|
-
self.ax.set_title(f'Slice {self.current_slice}')
|
|
5372
|
-
|
|
5373
|
-
# Make axis labels and ticks black for visibility against white background
|
|
5374
|
-
self.ax.xaxis.label.set_color('black')
|
|
5375
|
-
self.ax.yaxis.label.set_color('black')
|
|
5376
|
-
self.ax.title.set_color('black')
|
|
5377
|
-
self.ax.tick_params(colors='black')
|
|
5378
|
-
for spine in self.ax.spines.values():
|
|
5379
|
-
spine.set_color('black')
|
|
6001
|
+
highlight_cmap = LinearSegmentedColormap.from_list('highlight',
|
|
6002
|
+
[(0, 0, 0, 0), (1, 1, 0, 1), (0, 0.7, 1, 1)])
|
|
6003
|
+
self.ax.imshow(display_highlight, cmap=highlight_cmap, vmin=0, vmax=2, alpha=0.3, extent=crop_extent)
|
|
5380
6004
|
|
|
5381
|
-
#
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
# Redraw measurement points and their labels
|
|
6005
|
+
# Redraw measurement points efficiently (no cropping needed - these are vector graphics)
|
|
6006
|
+
# Only draw points that are within the visible region for additional performance
|
|
5385
6007
|
for point in self.measurement_points:
|
|
5386
6008
|
x1, y1, z1 = point['point1']
|
|
5387
6009
|
x2, y2, z2 = point['point2']
|
|
5388
6010
|
pair_idx = point['pair_index']
|
|
5389
6011
|
|
|
5390
|
-
#
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
6012
|
+
# Check if points are in visible region
|
|
6013
|
+
point1_visible = (z1 == self.current_slice and
|
|
6014
|
+
current_xlim[0] <= x1 <= current_xlim[1] and
|
|
6015
|
+
current_ylim[1] <= y1 <= current_ylim[0])
|
|
6016
|
+
point2_visible = (z2 == self.current_slice and
|
|
6017
|
+
current_xlim[0] <= x2 <= current_xlim[1] and
|
|
6018
|
+
current_ylim[1] <= y2 <= current_ylim[0])
|
|
6019
|
+
|
|
6020
|
+
if point1_visible:
|
|
6021
|
+
pt1 = self.ax.plot(x1, y1, 'yo', markersize=8)[0]
|
|
6022
|
+
txt1 = self.ax.text(x1, y1+5, str(pair_idx), color='white', ha='center', va='bottom')
|
|
6023
|
+
self.measurement_artists.extend([pt1, txt1])
|
|
5399
6024
|
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
self.ax.
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
self.
|
|
5406
|
-
|
|
6025
|
+
if point2_visible:
|
|
6026
|
+
pt2 = self.ax.plot(x2, y2, 'yo', markersize=8)[0]
|
|
6027
|
+
txt2 = self.ax.text(x2, y2+5, str(pair_idx), color='white', ha='center', va='bottom')
|
|
6028
|
+
self.measurement_artists.extend([pt2, txt2])
|
|
6029
|
+
|
|
6030
|
+
if z1 == z2 == self.current_slice and (point1_visible or point2_visible):
|
|
6031
|
+
line = self.ax.plot([x1, x2], [y1, y2], 'r--', alpha=0.5)[0]
|
|
6032
|
+
self.measurement_artists.append(line)
|
|
6033
|
+
|
|
6034
|
+
# Store current view limits for next update
|
|
6035
|
+
self.ax._current_xlim = current_xlim
|
|
6036
|
+
self.ax._current_ylim = current_ylim
|
|
5407
6037
|
|
|
6038
|
+
# Handle resizing
|
|
5408
6039
|
if self.resizing:
|
|
5409
6040
|
self.original_xlim = self.ax.get_xlim()
|
|
5410
6041
|
self.original_ylim = self.ax.get_ylim()
|
|
5411
|
-
|
|
6042
|
+
|
|
6043
|
+
# Restore zoom (this sets the final view, not the data extent)
|
|
5412
6044
|
if current_xlim is not None and current_ylim is not None:
|
|
5413
6045
|
self.ax.set_xlim(current_xlim)
|
|
5414
6046
|
self.ax.set_ylim(current_ylim)
|
|
6047
|
+
|
|
5415
6048
|
if reset_resize:
|
|
5416
6049
|
self.resizing = False
|
|
5417
6050
|
|
|
5418
|
-
|
|
6051
|
+
# Use draw_idle for better performance
|
|
6052
|
+
self.canvas.draw_idle()
|
|
6053
|
+
|
|
6054
|
+
except Exception as e:
|
|
6055
|
+
pass
|
|
6056
|
+
#import traceback
|
|
6057
|
+
#print(traceback.format_exc())
|
|
6058
|
+
|
|
5419
6059
|
|
|
5420
6060
|
|
|
5421
|
-
except:
|
|
5422
|
-
import traceback
|
|
5423
|
-
print(traceback.format_exc())
|
|
5424
6061
|
|
|
5425
6062
|
def get_channel_image(self, channel):
|
|
5426
6063
|
"""Find the matplotlib image object for a specific channel."""
|
|
@@ -5452,7 +6089,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5452
6089
|
stats['num_nodes'] = my_network.network.number_of_nodes()
|
|
5453
6090
|
stats['num_edges'] = my_network.network.number_of_edges()
|
|
5454
6091
|
except:
|
|
5455
|
-
|
|
6092
|
+
try:
|
|
6093
|
+
stats['num_nodes'] = len(np.unique(my_network.nodes)) - 1
|
|
6094
|
+
except:
|
|
6095
|
+
pass
|
|
5456
6096
|
|
|
5457
6097
|
try:
|
|
5458
6098
|
idens = invert_dict(my_network.node_identities)
|
|
@@ -6463,7 +7103,7 @@ class BrightnessContrastDialog(QDialog):
|
|
|
6463
7103
|
self.debounce_timer.setSingleShot(True)
|
|
6464
7104
|
self.debounce_timer.timeout.connect(self._apply_pending_updates)
|
|
6465
7105
|
self.pending_updates = {}
|
|
6466
|
-
self.debounce_delay =
|
|
7106
|
+
self.debounce_delay = 20 # 300ms delay
|
|
6467
7107
|
|
|
6468
7108
|
# Connect signals
|
|
6469
7109
|
slider.valueChanged.connect(lambda values, ch=i: self.on_slider_change(ch, values))
|
|
@@ -7378,11 +8018,10 @@ class ComIdDialog(QDialog):
|
|
|
7378
8018
|
self.umap.setChecked(True)
|
|
7379
8019
|
layout.addRow("Generate UMAP?:", self.umap)
|
|
7380
8020
|
|
|
7381
|
-
|
|
7382
|
-
self.label
|
|
7383
|
-
self.label.
|
|
7384
|
-
self.label
|
|
7385
|
-
layout.addRow("If using above - label UMAP points?:", self.label)
|
|
8021
|
+
self.label = QComboBox()
|
|
8022
|
+
self.label.addItems(["No Label", "By Community", "By Neighborhood (If already calculated via 'Analyze -> Network -> Convert Network Communities...')"])
|
|
8023
|
+
self.label.setCurrentIndex(0)
|
|
8024
|
+
layout.addRow("Label UMAP Points How?:", self.label)
|
|
7386
8025
|
|
|
7387
8026
|
self.limit = QLineEdit("")
|
|
7388
8027
|
layout.addRow("Min Community Size for UMAP (Smaller communities will be ignored in graph, does not apply if empty)", self.limit)
|
|
@@ -7402,6 +8041,12 @@ class ComIdDialog(QDialog):
|
|
|
7402
8041
|
|
|
7403
8042
|
try:
|
|
7404
8043
|
|
|
8044
|
+
if self.parent().prev_coms is not None:
|
|
8045
|
+
temp = my_network.communities
|
|
8046
|
+
my_network.communities = self.parent().prev_coms
|
|
8047
|
+
else:
|
|
8048
|
+
temp = None
|
|
8049
|
+
|
|
7405
8050
|
if my_network.node_identities is None:
|
|
7406
8051
|
print("Node identities must be set")
|
|
7407
8052
|
|
|
@@ -7414,7 +8059,9 @@ class ComIdDialog(QDialog):
|
|
|
7414
8059
|
mode = self.mode.currentIndex()
|
|
7415
8060
|
|
|
7416
8061
|
umap = self.umap.isChecked()
|
|
7417
|
-
|
|
8062
|
+
|
|
8063
|
+
label = self.label.currentIndex()
|
|
8064
|
+
|
|
7418
8065
|
proportional = self.proportional.isChecked()
|
|
7419
8066
|
limit = int(self.limit.text()) if self.limit.text().strip() else 0
|
|
7420
8067
|
|
|
@@ -7427,10 +8074,13 @@ class ComIdDialog(QDialog):
|
|
|
7427
8074
|
|
|
7428
8075
|
else:
|
|
7429
8076
|
|
|
7430
|
-
info, names = my_network.community_id_info_per_com(umap = umap, label = label, limit = limit, proportional = proportional)
|
|
8077
|
+
info, names = my_network.community_id_info_per_com(umap = umap, label = label, limit = limit, proportional = proportional, neighbors = temp)
|
|
7431
8078
|
|
|
7432
8079
|
self.parent().format_for_upperright_table(info, 'Community', names, 'Average of Community Makeup')
|
|
7433
8080
|
|
|
8081
|
+
if self.parent().prev_coms is not None:
|
|
8082
|
+
my_network.communities = temp
|
|
8083
|
+
|
|
7434
8084
|
self.accept()
|
|
7435
8085
|
|
|
7436
8086
|
except Exception as e:
|
|
@@ -7786,10 +8436,12 @@ class NearNeighDialog(QDialog):
|
|
|
7786
8436
|
title = f"Nearest {num} Neighbor(s) Distance of {targ} from {root}"
|
|
7787
8437
|
header = f"Shortest Distance to Closest {num} {targ}(s)"
|
|
7788
8438
|
header2 = f"{root} Node ID"
|
|
8439
|
+
header3 = f'Theoretical Uniform Distance to Closest {num} {targ}(s)'
|
|
7789
8440
|
else:
|
|
7790
8441
|
title = f"Nearest {num} Neighbor(s) Distance Between Nodes"
|
|
7791
8442
|
header = f"Shortest Distance to Closest {num} Nodes"
|
|
7792
8443
|
header2 = "Root Node ID"
|
|
8444
|
+
header3 = f'Simulated Theoretical Uniform Distance to Closest {num} Nodes'
|
|
7793
8445
|
|
|
7794
8446
|
if centroids and my_network.node_centroids is None:
|
|
7795
8447
|
self.parent().show_centroid_dialog()
|
|
@@ -7797,15 +8449,22 @@ class NearNeighDialog(QDialog):
|
|
|
7797
8449
|
return
|
|
7798
8450
|
|
|
7799
8451
|
if not numpy:
|
|
7800
|
-
avg, output, quant_overlay = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, heatmap = heatmap, threed = threed, quant = quant, centroids = centroids)
|
|
8452
|
+
avg, output, quant_overlay, pred = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, heatmap = heatmap, threed = threed, quant = quant, centroids = centroids)
|
|
7801
8453
|
else:
|
|
7802
|
-
avg, output, overlay, quant_overlay = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, heatmap = heatmap, threed = threed, numpy = True, quant = quant, centroids = centroids)
|
|
8454
|
+
avg, output, overlay, quant_overlay, pred = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, heatmap = heatmap, threed = threed, numpy = True, quant = quant, centroids = centroids)
|
|
7803
8455
|
self.parent().load_channel(3, overlay, data = True)
|
|
7804
8456
|
|
|
7805
8457
|
if quant_overlay is not None:
|
|
7806
8458
|
self.parent().load_channel(2, quant_overlay, data = True)
|
|
8459
|
+
|
|
8460
|
+
avg = {header:avg}
|
|
8461
|
+
|
|
8462
|
+
if pred is not None:
|
|
8463
|
+
|
|
8464
|
+
avg[header3] = pred
|
|
8465
|
+
|
|
7807
8466
|
|
|
7808
|
-
self.parent().format_for_upperright_table(
|
|
8467
|
+
self.parent().format_for_upperright_table(avg, 'Category', 'Value', title = f'Avg {title}')
|
|
7809
8468
|
self.parent().format_for_upperright_table(output, header2, header, title = title)
|
|
7810
8469
|
|
|
7811
8470
|
self.accept()
|
|
@@ -7830,7 +8489,7 @@ class NearNeighDialog(QDialog):
|
|
|
7830
8489
|
root = available[0]
|
|
7831
8490
|
|
|
7832
8491
|
for targ in available:
|
|
7833
|
-
avg, _, _ = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, centroids = centroids)
|
|
8492
|
+
avg, _, _, _ = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, centroids = centroids)
|
|
7834
8493
|
output_dict[f"{root} vs {targ}"] = avg
|
|
7835
8494
|
|
|
7836
8495
|
del available[0]
|
|
@@ -8742,15 +9401,6 @@ class ResizeDialog(QDialog):
|
|
|
8742
9401
|
undo_button.clicked.connect(lambda: self.run_resize(undo = True))
|
|
8743
9402
|
layout.addRow(undo_button)
|
|
8744
9403
|
|
|
8745
|
-
if my_network.xy_scale != my_network.z_scale:
|
|
8746
|
-
norm_button_upsize = QPushButton(f"Normalize Scaling with Upsample")
|
|
8747
|
-
norm_button_upsize.clicked.connect(lambda: self.run_resize(upsize = True, special = True))
|
|
8748
|
-
layout.addRow(norm_button_upsize)
|
|
8749
|
-
|
|
8750
|
-
norm_button_downsize = QPushButton("Normalize Scaling with Downsample")
|
|
8751
|
-
norm_button_downsize.clicked.connect(lambda: self.run_resize(upsize = False, special = True))
|
|
8752
|
-
layout.addRow(norm_button_downsize)
|
|
8753
|
-
|
|
8754
9404
|
run_button = QPushButton("Run Resize")
|
|
8755
9405
|
run_button.clicked.connect(self.run_resize)
|
|
8756
9406
|
layout.addRow(run_button)
|
|
@@ -8764,7 +9414,7 @@ class ResizeDialog(QDialog):
|
|
|
8764
9414
|
|
|
8765
9415
|
def run_resize(self, undo = False, upsize = True, special = False):
|
|
8766
9416
|
try:
|
|
8767
|
-
self.parent().resizing =
|
|
9417
|
+
self.parent().resizing = False
|
|
8768
9418
|
# Get parameters
|
|
8769
9419
|
try:
|
|
8770
9420
|
resize = float(self.resize.text()) if self.resize.text() else None
|
|
@@ -8778,6 +9428,12 @@ class ResizeDialog(QDialog):
|
|
|
8778
9428
|
|
|
8779
9429
|
resize = resize if resize is not None else (zsize, ysize, xsize)
|
|
8780
9430
|
|
|
9431
|
+
if (self.parent().shape[1] * resize) < 1 or (self.parent().shape[2] * resize) < 1:
|
|
9432
|
+
print("Incompatible x/y dimensions")
|
|
9433
|
+
return
|
|
9434
|
+
elif (self.parent().shape[0] * resize) < 1:
|
|
9435
|
+
resize = (1, resize, resize)
|
|
9436
|
+
|
|
8781
9437
|
if special:
|
|
8782
9438
|
if upsize:
|
|
8783
9439
|
if (my_network.z_scale > my_network.xy_scale):
|
|
@@ -8819,11 +9475,7 @@ class ResizeDialog(QDialog):
|
|
|
8819
9475
|
new_shape = tuple(int(dim * resize) for dim in array_shape)
|
|
8820
9476
|
else:
|
|
8821
9477
|
new_shape = tuple(int(dim * factor) for dim, factor in zip(array_shape, resize))
|
|
8822
|
-
|
|
8823
|
-
#if any(dim < 1 for dim in new_shape):
|
|
8824
|
-
#QMessageBox.critical(self, "Error", f"Resize would result in invalid dimensions: {new_shape}")
|
|
8825
|
-
#self.reset_fields()
|
|
8826
|
-
#return
|
|
9478
|
+
|
|
8827
9479
|
|
|
8828
9480
|
cubic = self.cubic.isChecked()
|
|
8829
9481
|
order = 3 if cubic else 0
|
|
@@ -8837,7 +9489,7 @@ class ResizeDialog(QDialog):
|
|
|
8837
9489
|
for channel in range(4):
|
|
8838
9490
|
if self.parent().channel_data[channel] is not None:
|
|
8839
9491
|
resized_data = n3d.resize(self.parent().channel_data[channel], resize, order)
|
|
8840
|
-
self.parent().load_channel(channel, channel_data=resized_data, data=True
|
|
9492
|
+
self.parent().load_channel(channel, channel_data=resized_data, data=True)
|
|
8841
9493
|
|
|
8842
9494
|
|
|
8843
9495
|
|
|
@@ -8858,7 +9510,7 @@ class ResizeDialog(QDialog):
|
|
|
8858
9510
|
for channel in range(4):
|
|
8859
9511
|
if self.parent().channel_data[channel] is not None:
|
|
8860
9512
|
resized_data = n3d.upsample_with_padding(self.parent().channel_data[channel], original_shape = self.parent().original_shape)
|
|
8861
|
-
self.parent().load_channel(channel, channel_data=resized_data, data=True
|
|
9513
|
+
self.parent().load_channel(channel, channel_data=resized_data, data=True)
|
|
8862
9514
|
|
|
8863
9515
|
if self.parent().mini_overlay_data is not None:
|
|
8864
9516
|
|
|
@@ -9664,6 +10316,7 @@ class MachineWindow(QMainWindow):
|
|
|
9664
10316
|
|
|
9665
10317
|
self.num_chunks = 0
|
|
9666
10318
|
|
|
10319
|
+
|
|
9667
10320
|
except:
|
|
9668
10321
|
return
|
|
9669
10322
|
|
|
@@ -10126,9 +10779,8 @@ class MachineWindow(QMainWindow):
|
|
|
10126
10779
|
print("Finished segmentation moved to Overlay 2. Use File -> Save(As) for disk saving.")
|
|
10127
10780
|
|
|
10128
10781
|
def closeEvent(self, event):
|
|
10129
|
-
|
|
10130
10782
|
try:
|
|
10131
|
-
if self.parent().isVisible():
|
|
10783
|
+
if self.parent() and self.parent().isVisible():
|
|
10132
10784
|
if self.confirm_close_dialog():
|
|
10133
10785
|
# Clean up resources before closing
|
|
10134
10786
|
if self.brush_button.isChecked():
|
|
@@ -10141,7 +10793,6 @@ class MachineWindow(QMainWindow):
|
|
|
10141
10793
|
# Kill the segmentation thread and wait for it to finish
|
|
10142
10794
|
self.kill_segmentation()
|
|
10143
10795
|
time.sleep(0.2) # Give additional time for cleanup
|
|
10144
|
-
|
|
10145
10796
|
try:
|
|
10146
10797
|
self.parent().channel_data[0] = self.parent().reduce_rgb_dimension(self.parent().channel_data[0], 'weight')
|
|
10147
10798
|
self.update_display()
|
|
@@ -10149,10 +10800,20 @@ class MachineWindow(QMainWindow):
|
|
|
10149
10800
|
pass
|
|
10150
10801
|
|
|
10151
10802
|
self.parent().machine_window = None
|
|
10803
|
+
event.accept() # IMPORTANT: Accept the close event
|
|
10152
10804
|
else:
|
|
10153
|
-
event.ignore()
|
|
10154
|
-
|
|
10155
|
-
|
|
10805
|
+
event.ignore() # User cancelled, ignore the close
|
|
10806
|
+
else:
|
|
10807
|
+
# Parent doesn't exist or isn't visible, just close
|
|
10808
|
+
if hasattr(self, 'parent') and self.parent():
|
|
10809
|
+
self.parent().machine_window = None
|
|
10810
|
+
event.accept()
|
|
10811
|
+
except Exception as e:
|
|
10812
|
+
print(f"Error in closeEvent: {e}")
|
|
10813
|
+
# Even if there's an error, allow the window to close
|
|
10814
|
+
if hasattr(self, 'parent') and self.parent():
|
|
10815
|
+
self.parent().machine_window = None
|
|
10816
|
+
event.accept()
|
|
10156
10817
|
|
|
10157
10818
|
|
|
10158
10819
|
|
|
@@ -10172,10 +10833,7 @@ class SegmentationWorker(QThread):
|
|
|
10172
10833
|
self.mem_lock = mem_lock
|
|
10173
10834
|
self._stop = False
|
|
10174
10835
|
self._paused = False # Add pause flag
|
|
10175
|
-
|
|
10176
|
-
self.update_interval = 10
|
|
10177
|
-
else:
|
|
10178
|
-
self.update_interval = 1 # Increased to 1s
|
|
10836
|
+
self.update_interval = 2 # Increased to 2s
|
|
10179
10837
|
self.chunks_since_update = 0
|
|
10180
10838
|
self.chunks_per_update = 5 # Only update every 5 chunks
|
|
10181
10839
|
self.poked = False # If it should wake up or not
|
|
@@ -10218,11 +10876,14 @@ class SegmentationWorker(QThread):
|
|
|
10218
10876
|
if self._stop:
|
|
10219
10877
|
break
|
|
10220
10878
|
|
|
10221
|
-
|
|
10222
|
-
|
|
10223
|
-
|
|
10224
|
-
|
|
10225
|
-
|
|
10879
|
+
if foreground_coords:
|
|
10880
|
+
fg_array = np.array(list(foreground_coords))
|
|
10881
|
+
self.overlay[fg_array[:, 0], fg_array[:, 1], fg_array[:, 2]] = 1
|
|
10882
|
+
|
|
10883
|
+
if background_coords:
|
|
10884
|
+
bg_array = np.array(list(background_coords))
|
|
10885
|
+
self.overlay[bg_array[:, 0], bg_array[:, 1], bg_array[:, 2]] = 2
|
|
10886
|
+
|
|
10226
10887
|
self.chunks_since_update += 1
|
|
10227
10888
|
current_time = time.time()
|
|
10228
10889
|
if (self.chunks_since_update >= self.chunks_per_update and
|
|
@@ -10247,27 +10908,6 @@ class SegmentationWorker(QThread):
|
|
|
10247
10908
|
import traceback
|
|
10248
10909
|
traceback.print_exc()
|
|
10249
10910
|
|
|
10250
|
-
def run_batch(self):
|
|
10251
|
-
try:
|
|
10252
|
-
foreground_coords, _ = self.segmenter.segment_volume()
|
|
10253
|
-
|
|
10254
|
-
# Modify the array directly
|
|
10255
|
-
self.overlay.fill(False)
|
|
10256
|
-
for z,y,x in foreground_coords:
|
|
10257
|
-
# Check for pause/stop during batch processing too
|
|
10258
|
-
self._check_pause()
|
|
10259
|
-
if self._stop:
|
|
10260
|
-
break
|
|
10261
|
-
self.overlay[z,y,x] = True
|
|
10262
|
-
|
|
10263
|
-
self.finished.emit()
|
|
10264
|
-
|
|
10265
|
-
except Exception as e:
|
|
10266
|
-
print(f"Error in segmentation: {e}")
|
|
10267
|
-
raise
|
|
10268
|
-
|
|
10269
|
-
|
|
10270
|
-
|
|
10271
10911
|
|
|
10272
10912
|
class ThresholdWindow(QMainWindow):
|
|
10273
10913
|
def __init__(self, parent=None, accepted_mode=0):
|