nettracer3d 0.9.1__py3-none-any.whl → 0.9.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nettracer3d/nettracer.py +4 -1
- nettracer3d/nettracer_gui.py +513 -95
- {nettracer3d-0.9.1.dist-info → nettracer3d-0.9.3.dist-info}/METADATA +4 -7
- {nettracer3d-0.9.1.dist-info → nettracer3d-0.9.3.dist-info}/RECORD +8 -8
- {nettracer3d-0.9.1.dist-info → nettracer3d-0.9.3.dist-info}/WHEEL +0 -0
- {nettracer3d-0.9.1.dist-info → nettracer3d-0.9.3.dist-info}/entry_points.txt +0 -0
- {nettracer3d-0.9.1.dist-info → nettracer3d-0.9.3.dist-info}/licenses/LICENSE +0 -0
- {nettracer3d-0.9.1.dist-info → nettracer3d-0.9.3.dist-info}/top_level.txt +0 -0
nettracer3d/nettracer.py
CHANGED
|
@@ -5585,7 +5585,10 @@ class Network_3D:
|
|
|
5585
5585
|
if self.communities is not None and label == 2:
|
|
5586
5586
|
neighbor_group = {}
|
|
5587
5587
|
for node, com in self.communities.items():
|
|
5588
|
-
|
|
5588
|
+
try:
|
|
5589
|
+
neighbor_group[com] = neighbors[node]
|
|
5590
|
+
except:
|
|
5591
|
+
neighbor_group[com] = 0
|
|
5589
5592
|
neighborhoods.visualize_cluster_composition_umap(umap_dict, id_set, neighborhoods = neighbor_group)
|
|
5590
5593
|
elif label == 1:
|
|
5591
5594
|
neighborhoods.visualize_cluster_composition_umap(umap_dict, id_set, label = True)
|
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
2546
|
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
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:
|
|
@@ -4157,6 +4542,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4157
4542
|
if not self.confirm_calcbranch_dialog("Use of this feature will require additional use of the Nodes and Overlay 2 channels. Please save any data and return, or proceed if you do not need those channels' data"):
|
|
4158
4543
|
return
|
|
4159
4544
|
|
|
4545
|
+
if my_network.edges is None and my_network.nodes is not None:
|
|
4546
|
+
self.load_channel(1, my_network.nodes, data = True)
|
|
4547
|
+
self.delete_channel(0, False)
|
|
4548
|
+
|
|
4160
4549
|
my_network.id_overlay = my_network.edges.copy()
|
|
4161
4550
|
|
|
4162
4551
|
self.show_gennodes_dialog()
|
|
@@ -4189,6 +4578,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4189
4578
|
if not self.confirm_calcbranch_dialog("Use of this feature will require additional use of the Nodes and Overlay 2 channels. Please save any data and return, or proceed if you do not need those channels' data"):
|
|
4190
4579
|
return
|
|
4191
4580
|
|
|
4581
|
+
if my_network.edges is None and my_network.nodes is not None:
|
|
4582
|
+
self.load_channel(1, my_network.nodes, data = True)
|
|
4583
|
+
self.delete_channel(0, False)
|
|
4584
|
+
|
|
4192
4585
|
self.show_branch_dialog(called = True)
|
|
4193
4586
|
|
|
4194
4587
|
self.load_channel(0, my_network.edges, data = True)
|
|
@@ -4580,7 +4973,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4580
4973
|
|
|
4581
4974
|
if directory != "":
|
|
4582
4975
|
|
|
4583
|
-
self.reset(network = True, xy_scale = 1, z_scale = 1, edges = True, network_overlay = True, id_overlay = True, update = False)
|
|
4976
|
+
self.reset(network = True, xy_scale = 1, z_scale = 1, nodes = True, edges = True, network_overlay = True, id_overlay = True, update = False)
|
|
4977
|
+
|
|
4584
4978
|
|
|
4585
4979
|
my_network.assemble(directory)
|
|
4586
4980
|
|
|
@@ -4607,7 +5001,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4607
5001
|
if channel is not None:
|
|
4608
5002
|
self.slice_slider.setEnabled(True)
|
|
4609
5003
|
self.slice_slider.setMinimum(0)
|
|
4610
|
-
self.slice_slider.setMaximum(
|
|
5004
|
+
self.slice_slider.setMaximum(self.shape[0] - 1)
|
|
4611
5005
|
self.slice_slider.setValue(0)
|
|
4612
5006
|
self.current_slice = 0
|
|
4613
5007
|
break
|
|
@@ -4893,6 +5287,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4893
5287
|
"""Load a channel and enable active channel selection if needed."""
|
|
4894
5288
|
|
|
4895
5289
|
try:
|
|
5290
|
+
|
|
4896
5291
|
self.hold_update = True
|
|
4897
5292
|
if not data: # For solo loading
|
|
4898
5293
|
filename, _ = QFileDialog.getOpenFileName(
|
|
@@ -5092,7 +5487,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5092
5487
|
self.current_operation = []
|
|
5093
5488
|
self.current_operation_type = None
|
|
5094
5489
|
|
|
5095
|
-
if
|
|
5490
|
+
if self.pan_mode:
|
|
5491
|
+
self.pan_button.click()
|
|
5492
|
+
if self.show_channels:
|
|
5493
|
+
self.channel_buttons[channel_index].click()
|
|
5494
|
+
self.channel_buttons[channel_index].click()
|
|
5495
|
+
elif not end_paint:
|
|
5096
5496
|
|
|
5097
5497
|
self.update_display(reset_resize = reset_resize, preserve_zoom = preserve_zoom)
|
|
5098
5498
|
|
|
@@ -5334,6 +5734,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5334
5734
|
self.hold_update = False
|
|
5335
5735
|
#if self.machine_window is not None:
|
|
5336
5736
|
#self.machine_window.poke_segmenter()
|
|
5737
|
+
if self.pan_mode:
|
|
5738
|
+
self.pan_button.click()
|
|
5337
5739
|
self.pending_slice = None
|
|
5338
5740
|
|
|
5339
5741
|
def update_brightness(self, channel_index, values):
|
|
@@ -5370,7 +5772,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5370
5772
|
self.resume = False
|
|
5371
5773
|
if self.prev_down != self.downsample_factor:
|
|
5372
5774
|
self.validate_downsample_input(text = self.prev_down)
|
|
5373
|
-
|
|
5775
|
+
return
|
|
5374
5776
|
|
|
5375
5777
|
if self.static_background is not None:
|
|
5376
5778
|
# Your existing virtual strokes conversion logic
|
|
@@ -5455,17 +5857,18 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5455
5857
|
y_min = max(0, int(np.floor(current_ylim[1] + 0.5))) # Note: y is flipped
|
|
5456
5858
|
y_max = min(min_height, int(np.ceil(current_ylim[0] + 0.5)))
|
|
5457
5859
|
|
|
5458
|
-
if
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5860
|
+
if self.pan_mode:
|
|
5861
|
+
box_len = (x_max - x_min)
|
|
5862
|
+
box_height = (y_max - y_min)
|
|
5863
|
+
x_min = max(0, x_min - box_len)
|
|
5864
|
+
x_max = min(self.shape[2], x_max + box_len)
|
|
5865
|
+
y_min = max(0, y_min - box_height)
|
|
5866
|
+
y_max = min(self.shape[1], y_max + box_height)
|
|
5867
|
+
|
|
5868
|
+
size = (x_max - x_min) * (y_max - y_min)
|
|
5869
|
+
val = int(np.ceil(size/(3000 * 3000)))
|
|
5870
|
+
self.validate_downsample_input(text = val, update = False)
|
|
5871
|
+
|
|
5469
5872
|
downsample_factor = self.downsample_factor
|
|
5470
5873
|
|
|
5471
5874
|
# Add some padding to avoid edge artifacts during pan/zoom
|
|
@@ -5507,6 +5910,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5507
5910
|
return cropped[::factor, ::factor, :]
|
|
5508
5911
|
else:
|
|
5509
5912
|
return cropped
|
|
5913
|
+
|
|
5510
5914
|
|
|
5511
5915
|
# Update channel images efficiently with cropping and downsampling
|
|
5512
5916
|
for channel in range(4):
|
|
@@ -6707,7 +7111,7 @@ class BrightnessContrastDialog(QDialog):
|
|
|
6707
7111
|
self.debounce_timer.setSingleShot(True)
|
|
6708
7112
|
self.debounce_timer.timeout.connect(self._apply_pending_updates)
|
|
6709
7113
|
self.pending_updates = {}
|
|
6710
|
-
self.debounce_delay =
|
|
7114
|
+
self.debounce_delay = 20 # 300ms delay
|
|
6711
7115
|
|
|
6712
7116
|
# Connect signals
|
|
6713
7117
|
slider.valueChanged.connect(lambda values, ch=i: self.on_slider_change(ch, values))
|
|
@@ -9920,6 +10324,7 @@ class MachineWindow(QMainWindow):
|
|
|
9920
10324
|
|
|
9921
10325
|
self.num_chunks = 0
|
|
9922
10326
|
|
|
10327
|
+
|
|
9923
10328
|
except:
|
|
9924
10329
|
return
|
|
9925
10330
|
|
|
@@ -10382,9 +10787,8 @@ class MachineWindow(QMainWindow):
|
|
|
10382
10787
|
print("Finished segmentation moved to Overlay 2. Use File -> Save(As) for disk saving.")
|
|
10383
10788
|
|
|
10384
10789
|
def closeEvent(self, event):
|
|
10385
|
-
|
|
10386
10790
|
try:
|
|
10387
|
-
if self.parent().isVisible():
|
|
10791
|
+
if self.parent() and self.parent().isVisible():
|
|
10388
10792
|
if self.confirm_close_dialog():
|
|
10389
10793
|
# Clean up resources before closing
|
|
10390
10794
|
if self.brush_button.isChecked():
|
|
@@ -10397,7 +10801,6 @@ class MachineWindow(QMainWindow):
|
|
|
10397
10801
|
# Kill the segmentation thread and wait for it to finish
|
|
10398
10802
|
self.kill_segmentation()
|
|
10399
10803
|
time.sleep(0.2) # Give additional time for cleanup
|
|
10400
|
-
|
|
10401
10804
|
try:
|
|
10402
10805
|
self.parent().channel_data[0] = self.parent().reduce_rgb_dimension(self.parent().channel_data[0], 'weight')
|
|
10403
10806
|
self.update_display()
|
|
@@ -10405,10 +10808,20 @@ class MachineWindow(QMainWindow):
|
|
|
10405
10808
|
pass
|
|
10406
10809
|
|
|
10407
10810
|
self.parent().machine_window = None
|
|
10811
|
+
event.accept() # IMPORTANT: Accept the close event
|
|
10408
10812
|
else:
|
|
10409
|
-
event.ignore()
|
|
10410
|
-
|
|
10411
|
-
|
|
10813
|
+
event.ignore() # User cancelled, ignore the close
|
|
10814
|
+
else:
|
|
10815
|
+
# Parent doesn't exist or isn't visible, just close
|
|
10816
|
+
if hasattr(self, 'parent') and self.parent():
|
|
10817
|
+
self.parent().machine_window = None
|
|
10818
|
+
event.accept()
|
|
10819
|
+
except Exception as e:
|
|
10820
|
+
print(f"Error in closeEvent: {e}")
|
|
10821
|
+
# Even if there's an error, allow the window to close
|
|
10822
|
+
if hasattr(self, 'parent') and self.parent():
|
|
10823
|
+
self.parent().machine_window = None
|
|
10824
|
+
event.accept()
|
|
10412
10825
|
|
|
10413
10826
|
|
|
10414
10827
|
|
|
@@ -10428,10 +10841,7 @@ class SegmentationWorker(QThread):
|
|
|
10428
10841
|
self.mem_lock = mem_lock
|
|
10429
10842
|
self._stop = False
|
|
10430
10843
|
self._paused = False # Add pause flag
|
|
10431
|
-
|
|
10432
|
-
self.update_interval = 10
|
|
10433
|
-
else:
|
|
10434
|
-
self.update_interval = 1 # Increased to 1s
|
|
10844
|
+
self.update_interval = 2 # Increased to 2s
|
|
10435
10845
|
self.chunks_since_update = 0
|
|
10436
10846
|
self.chunks_per_update = 5 # Only update every 5 chunks
|
|
10437
10847
|
self.poked = False # If it should wake up or not
|
|
@@ -12051,6 +12461,10 @@ class GenNodesDialog(QDialog):
|
|
|
12051
12461
|
def run_gennodes(self):
|
|
12052
12462
|
|
|
12053
12463
|
try:
|
|
12464
|
+
|
|
12465
|
+
if my_network.edges is None and my_network.nodes is not None:
|
|
12466
|
+
self.parent().load_channel(1, my_network.nodes, data = True)
|
|
12467
|
+
self.parent().delete_channel(0, True)
|
|
12054
12468
|
# Get directory (None if empty)
|
|
12055
12469
|
#directory = self.directory.text() if self.directory.text() else None
|
|
12056
12470
|
|
|
@@ -12280,6 +12694,10 @@ class BranchDialog(QDialog):
|
|
|
12280
12694
|
fix_val = float(self.fix_val.text()) if self.fix_val.text() else None
|
|
12281
12695
|
seed = int(self.seed.text()) if self.seed.text() else None
|
|
12282
12696
|
|
|
12697
|
+
if my_network.edges is None and my_network.nodes is not None:
|
|
12698
|
+
self.parent().load_channel(1, my_network.nodes, data = True)
|
|
12699
|
+
self.parent().delete_channel(0, True)
|
|
12700
|
+
|
|
12283
12701
|
original_shape = my_network.edges.shape
|
|
12284
12702
|
original_array = copy.deepcopy(my_network.edges)
|
|
12285
12703
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nettracer3d
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.3
|
|
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,6 @@ 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
|
-
* The image display window now uses image pyramids and cropping for zoom ins so it should run a lot faster on bigger images.
|
|
117
|
-
* The community UMAP can now color them by neighborhood.
|
|
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.3 Updates --
|
|
114
|
+
|
|
115
|
+
* Some minor bug fixes.
|
|
@@ -5,8 +5,8 @@ nettracer3d/excelotron.py,sha256=X9v_mte8gJBPNGdj6NJNUYja0Z6eorVoKAFx4nHiMnU,720
|
|
|
5
5
|
nettracer3d/modularity.py,sha256=pborVcDBvICB2-g8lNoSVZbIReIBlfeBmjFbPYmtq7Y,22443
|
|
6
6
|
nettracer3d/morphology.py,sha256=jyDjYzrZ4LvI5jOyw8DLsxmo-i5lpqHsejYpW7Tq7Mo,19786
|
|
7
7
|
nettracer3d/neighborhoods.py,sha256=iIaHU1COIdRtzRpAuIQKfLGLNKYFK3dL8Vb_EeJIlEA,46459
|
|
8
|
-
nettracer3d/nettracer.py,sha256=
|
|
9
|
-
nettracer3d/nettracer_gui.py,sha256=
|
|
8
|
+
nettracer3d/nettracer.py,sha256=KTVodpVpu2mfzdRR-ZucZlTQCZc1pqgH4jAI26vWAgY,264551
|
|
9
|
+
nettracer3d/nettracer_gui.py,sha256=eZuO9v9szm-pj_-Bl71Lyg0mw80u_HJragARR2muVLo,601133
|
|
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.3.dist-info/licenses/LICENSE,sha256=jnNT-yBeIAKAHpYthPvLeqCzJ6nSurgnKmloVnfsjCI,764
|
|
21
|
+
nettracer3d-0.9.3.dist-info/METADATA,sha256=lfHt7f_M_votMYdihCuZWBmL4jUCV6YXrN7PFu7iEkQ,6998
|
|
22
|
+
nettracer3d-0.9.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
23
|
+
nettracer3d-0.9.3.dist-info/entry_points.txt,sha256=Nx1rr_0QhJXDBHAQg2vcqCzLMKBzSHfwy3xwGkueVyc,53
|
|
24
|
+
nettracer3d-0.9.3.dist-info/top_level.txt,sha256=zsYy9rZwirfCEOubolhee4TyzqBAL5gSUeFMzhFTX8c,12
|
|
25
|
+
nettracer3d-0.9.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|