nettracer3d 0.9.1__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/nettracer_gui.py +497 -95
- {nettracer3d-0.9.1.dist-info → nettracer3d-0.9.2.dist-info}/METADATA +6 -7
- {nettracer3d-0.9.1.dist-info → nettracer3d-0.9.2.dist-info}/RECORD +7 -7
- {nettracer3d-0.9.1.dist-info → nettracer3d-0.9.2.dist-info}/WHEEL +0 -0
- {nettracer3d-0.9.1.dist-info → nettracer3d-0.9.2.dist-info}/entry_points.txt +0 -0
- {nettracer3d-0.9.1.dist-info → nettracer3d-0.9.2.dist-info}/licenses/LICENSE +0 -0
- {nettracer3d-0.9.1.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)
|
|
@@ -467,6 +467,297 @@ class ImageViewerWindow(QMainWindow):
|
|
|
467
467
|
self.resume = False
|
|
468
468
|
|
|
469
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()
|
|
470
761
|
|
|
471
762
|
def start_left_scroll(self):
|
|
472
763
|
"""Start scrolling left when left arrow is pressed."""
|
|
@@ -834,6 +1125,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
834
1125
|
|
|
835
1126
|
|
|
836
1127
|
|
|
1128
|
+
|
|
837
1129
|
#METHODS RELATED TO RIGHT CLICK:
|
|
838
1130
|
|
|
839
1131
|
def create_context_menu(self, event):
|
|
@@ -2182,7 +2474,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2182
2474
|
self.pan_button.setChecked(False)
|
|
2183
2475
|
|
|
2184
2476
|
self.pen_button.setChecked(False)
|
|
2185
|
-
self.pan_mode = False
|
|
2186
2477
|
self.brush_mode = False
|
|
2187
2478
|
self.can = False
|
|
2188
2479
|
self.threed = False
|
|
@@ -2205,6 +2496,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2205
2496
|
current_xlim = self.ax.get_xlim()
|
|
2206
2497
|
current_ylim = self.ax.get_ylim()
|
|
2207
2498
|
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
2499
|
+
self.pan_mode = False
|
|
2208
2500
|
|
|
2209
2501
|
else:
|
|
2210
2502
|
if self.machine_window is None:
|
|
@@ -2251,21 +2543,32 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2251
2543
|
|
|
2252
2544
|
# Store current channel visibility state
|
|
2253
2545
|
self.pre_pan_channel_state = self.channel_visible.copy()
|
|
2254
|
-
|
|
2255
|
-
self.prev_down = self.downsample_factor
|
|
2256
|
-
if self.throttle:
|
|
2257
|
-
if self.downsample_factor < 3:
|
|
2258
|
-
self.validate_downsample_input(text = 3)
|
|
2259
|
-
|
|
2260
|
-
# Create static background from currently visible channels
|
|
2261
|
-
self.create_pan_background()
|
|
2262
|
-
|
|
2263
|
-
# Hide all channels and show only the background
|
|
2264
|
-
self.channel_visible = [False] * 4
|
|
2265
|
-
self.is_pan_preview = True
|
|
2266
2546
|
|
|
2267
|
-
|
|
2268
|
-
self.
|
|
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)
|
|
2269
2572
|
|
|
2270
2573
|
else:
|
|
2271
2574
|
current_xlim = self.ax.get_xlim()
|
|
@@ -2737,7 +3040,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2737
3040
|
self.canvas.blit(self.ax.bbox)
|
|
2738
3041
|
|
|
2739
3042
|
elif self.panning and self.pan_start is not None:
|
|
2740
|
-
|
|
2741
3043
|
# Calculate the movement
|
|
2742
3044
|
dx = event.xdata - self.pan_start[0]
|
|
2743
3045
|
dy = event.ydata - self.pan_start[1]
|
|
@@ -2751,22 +3053,30 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2751
3053
|
new_ylim = [ylim[0] - dy, ylim[1] - dy]
|
|
2752
3054
|
|
|
2753
3055
|
# Get image bounds using cached dimensions
|
|
2754
|
-
if self.img_width is not None:
|
|
3056
|
+
if self.img_width is not None:
|
|
2755
3057
|
# Ensure new limits don't go beyond image bounds
|
|
2756
3058
|
if new_xlim[0] < 0:
|
|
2757
3059
|
new_xlim = [0, xlim[1] - xlim[0]]
|
|
2758
|
-
elif new_xlim[1] > self.img_width:
|
|
3060
|
+
elif new_xlim[1] > self.img_width:
|
|
2759
3061
|
new_xlim = [self.img_width - (xlim[1] - xlim[0]), self.img_width]
|
|
2760
3062
|
|
|
2761
3063
|
if new_ylim[0] < 0:
|
|
2762
3064
|
new_ylim = [0, ylim[1] - ylim[0]]
|
|
2763
|
-
elif new_ylim[1] > self.img_height:
|
|
3065
|
+
elif new_ylim[1] > self.img_height:
|
|
2764
3066
|
new_ylim = [self.img_height - (ylim[1] - ylim[0]), self.img_height]
|
|
2765
3067
|
|
|
2766
3068
|
# Apply new limits
|
|
2767
3069
|
self.ax.set_xlim(new_xlim)
|
|
2768
3070
|
self.ax.set_ylim(new_ylim)
|
|
2769
|
-
|
|
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))
|
|
2770
3080
|
|
|
2771
3081
|
# Update pan start position
|
|
2772
3082
|
self.pan_start = (event.xdata, event.ydata)
|
|
@@ -2804,6 +3114,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2804
3114
|
self.pan_background_image = self.create_composite_for_pan()
|
|
2805
3115
|
self.pan_zoom_state = (current_xlim, current_ylim)
|
|
2806
3116
|
|
|
3117
|
+
|
|
2807
3118
|
def create_composite_for_pan(self):
|
|
2808
3119
|
"""Create a properly rendered composite image for panning with downsample support"""
|
|
2809
3120
|
# Get active channels and dimensions (copied from update_display)
|
|
@@ -2820,8 +3131,33 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2820
3131
|
self.original_dims = (min_height, min_width)
|
|
2821
3132
|
|
|
2822
3133
|
# Get current downsample factor
|
|
2823
|
-
|
|
2824
|
-
|
|
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
|
+
|
|
2825
3161
|
# Calculate display dimensions (downsampled)
|
|
2826
3162
|
display_height = min_height // downsample_factor
|
|
2827
3163
|
display_width = min_width // downsample_factor
|
|
@@ -2930,6 +3266,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2930
3266
|
# Convert to 0-255 range for display
|
|
2931
3267
|
return (composite * 255).astype(np.uint8)
|
|
2932
3268
|
|
|
3269
|
+
|
|
2933
3270
|
def apply_machine_colormap(self, image):
|
|
2934
3271
|
"""Apply the special machine window colormap for channel 2"""
|
|
2935
3272
|
rgba = np.zeros((*image.shape, 4), dtype=np.float32)
|
|
@@ -3012,7 +3349,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3012
3349
|
|
|
3013
3350
|
return result
|
|
3014
3351
|
|
|
3015
|
-
def update_display_pan_mode(self):
|
|
3352
|
+
def update_display_pan_mode(self, current_xlim, current_ylim):
|
|
3016
3353
|
"""Lightweight display update for pan preview mode with downsample support"""
|
|
3017
3354
|
|
|
3018
3355
|
if self.is_pan_preview and self.pan_background_image is not None:
|
|
@@ -3030,17 +3367,76 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3030
3367
|
downsample_factor = getattr(self, 'downsample_factor', 1)
|
|
3031
3368
|
height *= downsample_factor
|
|
3032
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)
|
|
3033
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
|
+
|
|
3034
3431
|
# Display the composite background with preserved zoom
|
|
3035
3432
|
# Use extent to stretch downsampled image back to original coordinate space
|
|
3036
|
-
self.ax.imshow(
|
|
3037
|
-
extent=
|
|
3433
|
+
self.ax.imshow(display_image,
|
|
3434
|
+
extent=crop_extent,
|
|
3038
3435
|
aspect='equal')
|
|
3039
3436
|
|
|
3040
3437
|
# Restore the zoom state from when pan began
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
self.ax.set_ylim(self.pan_zoom_state[1])
|
|
3438
|
+
self.ax.set_xlim(current_xlim)
|
|
3439
|
+
self.ax.set_ylim(current_ylim)
|
|
3044
3440
|
|
|
3045
3441
|
# Get downsample factor for title display
|
|
3046
3442
|
downsample_factor = getattr(self, 'downsample_factor', 1)
|
|
@@ -3049,7 +3445,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3049
3445
|
self.ax.set_xlabel('X')
|
|
3050
3446
|
self.ax.set_ylabel('Y')
|
|
3051
3447
|
if downsample_factor > 1:
|
|
3052
|
-
self.ax.set_title(f'Slice {self.current_slice}
|
|
3448
|
+
self.ax.set_title(f'Slice {self.current_slice}')
|
|
3053
3449
|
else:
|
|
3054
3450
|
self.ax.set_title(f'Slice {self.current_slice}')
|
|
3055
3451
|
self.ax.xaxis.label.set_color('black')
|
|
@@ -3091,6 +3487,15 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3091
3487
|
|
|
3092
3488
|
if self.pan_mode:
|
|
3093
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
|
+
|
|
3094
3499
|
self.panning = False
|
|
3095
3500
|
self.pan_start = None
|
|
3096
3501
|
self.canvas.setCursor(Qt.CursorShape.OpenHandCursor)
|
|
@@ -3111,9 +3516,13 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3111
3516
|
self.show_crop_dialog(args)
|
|
3112
3517
|
|
|
3113
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')
|
|
3114
3521
|
|
|
3115
|
-
|
|
3116
|
-
|
|
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])
|
|
3117
3526
|
|
|
3118
3527
|
self.zoom_changed = True # Flag that zoom has changed
|
|
3119
3528
|
|
|
@@ -3710,32 +4119,19 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3710
4119
|
# Initialize downsample factor
|
|
3711
4120
|
self.downsample_factor = 1
|
|
3712
4121
|
|
|
3713
|
-
"""
|
|
3714
4122
|
# Create container widget for corner controls
|
|
3715
4123
|
corner_widget = QWidget()
|
|
3716
4124
|
corner_layout = QHBoxLayout(corner_widget)
|
|
3717
4125
|
corner_layout.setContentsMargins(5, 0, 5, 0)
|
|
3718
4126
|
|
|
3719
|
-
# Add downsample control
|
|
3720
|
-
downsample_label = QLabel("Downsample Display:")
|
|
3721
|
-
downsample_label.setStyleSheet("color: black; font-size: 11px;")
|
|
3722
|
-
corner_layout.addWidget(downsample_label)
|
|
3723
|
-
|
|
3724
|
-
self.downsample_input = QLineEdit("1")
|
|
3725
|
-
self.downsample_input.setFixedWidth(40)
|
|
3726
|
-
self.downsample_input.setFixedHeight(25)
|
|
3727
|
-
self.downsample_input.setStyleSheet("""
|
|
3728
|
-
#QLineEdit {
|
|
3729
|
-
#border: 1px solid gray;
|
|
3730
|
-
#border-radius: 2px;
|
|
3731
|
-
#padding: 1px;
|
|
3732
|
-
#font-size: 11px;
|
|
3733
|
-
#}
|
|
3734
|
-
""")
|
|
3735
|
-
self.downsample_input.textChanged.connect(self.on_downsample_changed)
|
|
3736
|
-
self.downsample_input.editingFinished.connect(self.validate_downsample_input)
|
|
3737
|
-
corner_layout.addWidget(self.downsample_input)
|
|
3738
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
|
+
|
|
3739
4135
|
# Add some spacing
|
|
3740
4136
|
corner_layout.addSpacing(10)
|
|
3741
4137
|
|
|
@@ -3748,12 +4144,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3748
4144
|
|
|
3749
4145
|
# Set as corner widget
|
|
3750
4146
|
menubar.setCornerWidget(corner_widget, Qt.Corner.TopRightCorner)
|
|
3751
|
-
"""
|
|
3752
|
-
cam_button = QPushButton("📷")
|
|
3753
|
-
cam_button.setFixedSize(40, 40)
|
|
3754
|
-
cam_button.setStyleSheet("font-size: 24px;") # Makes emoji larger
|
|
3755
|
-
cam_button.clicked.connect(self.snap)
|
|
3756
|
-
menubar.setCornerWidget(cam_button, Qt.Corner.TopRightCorner)
|
|
3757
4147
|
|
|
3758
4148
|
def on_downsample_changed(self, text):
|
|
3759
4149
|
"""Called whenever the text in the downsample input changes"""
|
|
@@ -3799,11 +4189,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3799
4189
|
self.downsample_factor = 1
|
|
3800
4190
|
|
|
3801
4191
|
self.throttle = self.shape[1] * self.shape[2] > 3000 * 3000 * self.downsample_factor
|
|
3802
|
-
if self.machine_window is not None:
|
|
3803
|
-
if self.throttle: #arbitrary throttle for large arrays.
|
|
3804
|
-
self.machine_window.update_interval = 10
|
|
3805
|
-
else:
|
|
3806
|
-
self.machine_window.update_interval = 1 # Increased to 1s
|
|
3807
4192
|
|
|
3808
4193
|
# Optional: Trigger display update if you want immediate effect
|
|
3809
4194
|
if update:
|
|
@@ -4580,7 +4965,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4580
4965
|
|
|
4581
4966
|
if directory != "":
|
|
4582
4967
|
|
|
4583
|
-
self.reset(network = True, xy_scale = 1, z_scale = 1, edges = True, network_overlay = True, id_overlay = True, update = False)
|
|
4968
|
+
self.reset(network = True, xy_scale = 1, z_scale = 1, nodes = True, edges = True, network_overlay = True, id_overlay = True, update = False)
|
|
4969
|
+
|
|
4584
4970
|
|
|
4585
4971
|
my_network.assemble(directory)
|
|
4586
4972
|
|
|
@@ -4607,7 +4993,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4607
4993
|
if channel is not None:
|
|
4608
4994
|
self.slice_slider.setEnabled(True)
|
|
4609
4995
|
self.slice_slider.setMinimum(0)
|
|
4610
|
-
self.slice_slider.setMaximum(
|
|
4996
|
+
self.slice_slider.setMaximum(self.shape[0] - 1)
|
|
4611
4997
|
self.slice_slider.setValue(0)
|
|
4612
4998
|
self.current_slice = 0
|
|
4613
4999
|
break
|
|
@@ -4893,6 +5279,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4893
5279
|
"""Load a channel and enable active channel selection if needed."""
|
|
4894
5280
|
|
|
4895
5281
|
try:
|
|
5282
|
+
|
|
4896
5283
|
self.hold_update = True
|
|
4897
5284
|
if not data: # For solo loading
|
|
4898
5285
|
filename, _ = QFileDialog.getOpenFileName(
|
|
@@ -5092,7 +5479,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5092
5479
|
self.current_operation = []
|
|
5093
5480
|
self.current_operation_type = None
|
|
5094
5481
|
|
|
5095
|
-
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:
|
|
5096
5488
|
|
|
5097
5489
|
self.update_display(reset_resize = reset_resize, preserve_zoom = preserve_zoom)
|
|
5098
5490
|
|
|
@@ -5334,6 +5726,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5334
5726
|
self.hold_update = False
|
|
5335
5727
|
#if self.machine_window is not None:
|
|
5336
5728
|
#self.machine_window.poke_segmenter()
|
|
5729
|
+
if self.pan_mode:
|
|
5730
|
+
self.pan_button.click()
|
|
5337
5731
|
self.pending_slice = None
|
|
5338
5732
|
|
|
5339
5733
|
def update_brightness(self, channel_index, values):
|
|
@@ -5370,7 +5764,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5370
5764
|
self.resume = False
|
|
5371
5765
|
if self.prev_down != self.downsample_factor:
|
|
5372
5766
|
self.validate_downsample_input(text = self.prev_down)
|
|
5373
|
-
|
|
5767
|
+
return
|
|
5374
5768
|
|
|
5375
5769
|
if self.static_background is not None:
|
|
5376
5770
|
# Your existing virtual strokes conversion logic
|
|
@@ -5455,17 +5849,18 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5455
5849
|
y_min = max(0, int(np.floor(current_ylim[1] + 0.5))) # Note: y is flipped
|
|
5456
5850
|
y_max = min(min_height, int(np.ceil(current_ylim[0] + 0.5)))
|
|
5457
5851
|
|
|
5458
|
-
if
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
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
|
+
|
|
5469
5864
|
downsample_factor = self.downsample_factor
|
|
5470
5865
|
|
|
5471
5866
|
# Add some padding to avoid edge artifacts during pan/zoom
|
|
@@ -5507,6 +5902,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5507
5902
|
return cropped[::factor, ::factor, :]
|
|
5508
5903
|
else:
|
|
5509
5904
|
return cropped
|
|
5905
|
+
|
|
5510
5906
|
|
|
5511
5907
|
# Update channel images efficiently with cropping and downsampling
|
|
5512
5908
|
for channel in range(4):
|
|
@@ -6707,7 +7103,7 @@ class BrightnessContrastDialog(QDialog):
|
|
|
6707
7103
|
self.debounce_timer.setSingleShot(True)
|
|
6708
7104
|
self.debounce_timer.timeout.connect(self._apply_pending_updates)
|
|
6709
7105
|
self.pending_updates = {}
|
|
6710
|
-
self.debounce_delay =
|
|
7106
|
+
self.debounce_delay = 20 # 300ms delay
|
|
6711
7107
|
|
|
6712
7108
|
# Connect signals
|
|
6713
7109
|
slider.valueChanged.connect(lambda values, ch=i: self.on_slider_change(ch, values))
|
|
@@ -9920,6 +10316,7 @@ class MachineWindow(QMainWindow):
|
|
|
9920
10316
|
|
|
9921
10317
|
self.num_chunks = 0
|
|
9922
10318
|
|
|
10319
|
+
|
|
9923
10320
|
except:
|
|
9924
10321
|
return
|
|
9925
10322
|
|
|
@@ -10382,9 +10779,8 @@ class MachineWindow(QMainWindow):
|
|
|
10382
10779
|
print("Finished segmentation moved to Overlay 2. Use File -> Save(As) for disk saving.")
|
|
10383
10780
|
|
|
10384
10781
|
def closeEvent(self, event):
|
|
10385
|
-
|
|
10386
10782
|
try:
|
|
10387
|
-
if self.parent().isVisible():
|
|
10783
|
+
if self.parent() and self.parent().isVisible():
|
|
10388
10784
|
if self.confirm_close_dialog():
|
|
10389
10785
|
# Clean up resources before closing
|
|
10390
10786
|
if self.brush_button.isChecked():
|
|
@@ -10397,7 +10793,6 @@ class MachineWindow(QMainWindow):
|
|
|
10397
10793
|
# Kill the segmentation thread and wait for it to finish
|
|
10398
10794
|
self.kill_segmentation()
|
|
10399
10795
|
time.sleep(0.2) # Give additional time for cleanup
|
|
10400
|
-
|
|
10401
10796
|
try:
|
|
10402
10797
|
self.parent().channel_data[0] = self.parent().reduce_rgb_dimension(self.parent().channel_data[0], 'weight')
|
|
10403
10798
|
self.update_display()
|
|
@@ -10405,10 +10800,20 @@ class MachineWindow(QMainWindow):
|
|
|
10405
10800
|
pass
|
|
10406
10801
|
|
|
10407
10802
|
self.parent().machine_window = None
|
|
10803
|
+
event.accept() # IMPORTANT: Accept the close event
|
|
10408
10804
|
else:
|
|
10409
|
-
event.ignore()
|
|
10410
|
-
|
|
10411
|
-
|
|
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()
|
|
10412
10817
|
|
|
10413
10818
|
|
|
10414
10819
|
|
|
@@ -10428,10 +10833,7 @@ class SegmentationWorker(QThread):
|
|
|
10428
10833
|
self.mem_lock = mem_lock
|
|
10429
10834
|
self._stop = False
|
|
10430
10835
|
self._paused = False # Add pause flag
|
|
10431
|
-
|
|
10432
|
-
self.update_interval = 10
|
|
10433
|
-
else:
|
|
10434
|
-
self.update_interval = 1 # Increased to 1s
|
|
10836
|
+
self.update_interval = 2 # Increased to 2s
|
|
10435
10837
|
self.chunks_since_update = 0
|
|
10436
10838
|
self.chunks_per_update = 5 # Only update every 5 chunks
|
|
10437
10839
|
self.poked = False # If it should wake up or not
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nettracer3d
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.2
|
|
4
4
|
Summary: Scripts for intializing and analyzing networks from segmentations of three dimensional images.
|
|
5
5
|
Author-email: Liam McLaughlin <liamm@wustl.edu>
|
|
6
6
|
Project-URL: Documentation, https://nettracer3d.readthedocs.io/en/latest/
|
|
@@ -110,9 +110,8 @@ McLaughlin, L., Zhang, B., Sharma, S. et al. Three dimensional multiscalar neuro
|
|
|
110
110
|
|
|
111
111
|
NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
|
|
112
112
|
|
|
113
|
-
-- Version 0.9.
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* No longer zooms all the way out by default with right click in zoom mode. Now user needs to Shift + Right Click.
|
|
113
|
+
-- Version 0.9.2 Updates --
|
|
114
|
+
* Image viewer canvas window can now be popped out into a separate window.
|
|
115
|
+
* Image pyramid calculation is more dynamic instead of using arbitrary size thresholds.
|
|
116
|
+
* Adjusted pan mode
|
|
117
|
+
* Some bug fixes.
|
|
@@ -6,7 +6,7 @@ nettracer3d/modularity.py,sha256=pborVcDBvICB2-g8lNoSVZbIReIBlfeBmjFbPYmtq7Y,224
|
|
|
6
6
|
nettracer3d/morphology.py,sha256=jyDjYzrZ4LvI5jOyw8DLsxmo-i5lpqHsejYpW7Tq7Mo,19786
|
|
7
7
|
nettracer3d/neighborhoods.py,sha256=iIaHU1COIdRtzRpAuIQKfLGLNKYFK3dL8Vb_EeJIlEA,46459
|
|
8
8
|
nettracer3d/nettracer.py,sha256=PBlvgCI65pwhMjTBuDl5CnHtv7unU8PMkyGt3Mz_a30,264443
|
|
9
|
-
nettracer3d/nettracer_gui.py,sha256=
|
|
9
|
+
nettracer3d/nettracer_gui.py,sha256=RBmt--IhiUWlHy0CI_DK6GEXEREWZyQcF7FNdrUK2WA,600327
|
|
10
10
|
nettracer3d/network_analysis.py,sha256=kBzsVaq4dZkMe0k-VGvQIUvM-tK0ZZ8bvb-wtsugZRQ,46150
|
|
11
11
|
nettracer3d/network_draw.py,sha256=F7fw6Pcf4qWOhdKwLmhwqWdschbDlHzwCVolQC9imeU,14117
|
|
12
12
|
nettracer3d/node_draw.py,sha256=kZcR1PekLg0riioNeGcALIXQyZ5PtHA_9MT6z7Zovdk,10401
|
|
@@ -17,9 +17,9 @@ nettracer3d/segmenter.py,sha256=-Llkhp3TlAIBXZNhcfMFQRdg0vec1xtlOm0c4_bSU9U,7576
|
|
|
17
17
|
nettracer3d/segmenter_GPU.py,sha256=optCZ_zLIfe99rgqmyKWUZlWW5TF5jEC_C3keu1m7VQ,77771
|
|
18
18
|
nettracer3d/simple_network.py,sha256=dkG4jpc4zzdeuoaQobgGfL3PNo6N8dGKQ5hEEubFIvA,9947
|
|
19
19
|
nettracer3d/smart_dilate.py,sha256=TvRUh6B4q4zIdCO1BWH-xgTdND5OUNmo99eyxG9oIAU,27145
|
|
20
|
-
nettracer3d-0.9.
|
|
21
|
-
nettracer3d-0.9.
|
|
22
|
-
nettracer3d-0.9.
|
|
23
|
-
nettracer3d-0.9.
|
|
24
|
-
nettracer3d-0.9.
|
|
25
|
-
nettracer3d-0.9.
|
|
20
|
+
nettracer3d-0.9.2.dist-info/licenses/LICENSE,sha256=jnNT-yBeIAKAHpYthPvLeqCzJ6nSurgnKmloVnfsjCI,764
|
|
21
|
+
nettracer3d-0.9.2.dist-info/METADATA,sha256=4lxrFb_E22_2wXMY7mW32K2VvG8OJS2l5s2kJeHOd6w,7179
|
|
22
|
+
nettracer3d-0.9.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
23
|
+
nettracer3d-0.9.2.dist-info/entry_points.txt,sha256=Nx1rr_0QhJXDBHAQg2vcqCzLMKBzSHfwy3xwGkueVyc,53
|
|
24
|
+
nettracer3d-0.9.2.dist-info/top_level.txt,sha256=zsYy9rZwirfCEOubolhee4TyzqBAL5gSUeFMzhFTX8c,12
|
|
25
|
+
nettracer3d-0.9.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|