nettracer3d 0.9.1__tar.gz → 0.9.2__tar.gz

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.

Files changed (30) hide show
  1. {nettracer3d-0.9.1/src/nettracer3d.egg-info → nettracer3d-0.9.2}/PKG-INFO +6 -7
  2. {nettracer3d-0.9.1 → nettracer3d-0.9.2}/README.md +5 -6
  3. {nettracer3d-0.9.1 → nettracer3d-0.9.2}/pyproject.toml +1 -1
  4. {nettracer3d-0.9.1 → nettracer3d-0.9.2}/src/nettracer3d/nettracer_gui.py +497 -95
  5. {nettracer3d-0.9.1 → nettracer3d-0.9.2/src/nettracer3d.egg-info}/PKG-INFO +6 -7
  6. {nettracer3d-0.9.1 → nettracer3d-0.9.2}/LICENSE +0 -0
  7. {nettracer3d-0.9.1 → nettracer3d-0.9.2}/setup.cfg +0 -0
  8. {nettracer3d-0.9.1 → nettracer3d-0.9.2}/src/nettracer3d/__init__.py +0 -0
  9. {nettracer3d-0.9.1 → nettracer3d-0.9.2}/src/nettracer3d/cellpose_manager.py +0 -0
  10. {nettracer3d-0.9.1 → nettracer3d-0.9.2}/src/nettracer3d/community_extractor.py +0 -0
  11. {nettracer3d-0.9.1 → nettracer3d-0.9.2}/src/nettracer3d/excelotron.py +0 -0
  12. {nettracer3d-0.9.1 → nettracer3d-0.9.2}/src/nettracer3d/modularity.py +0 -0
  13. {nettracer3d-0.9.1 → nettracer3d-0.9.2}/src/nettracer3d/morphology.py +0 -0
  14. {nettracer3d-0.9.1 → nettracer3d-0.9.2}/src/nettracer3d/neighborhoods.py +0 -0
  15. {nettracer3d-0.9.1 → nettracer3d-0.9.2}/src/nettracer3d/nettracer.py +0 -0
  16. {nettracer3d-0.9.1 → nettracer3d-0.9.2}/src/nettracer3d/network_analysis.py +0 -0
  17. {nettracer3d-0.9.1 → nettracer3d-0.9.2}/src/nettracer3d/network_draw.py +0 -0
  18. {nettracer3d-0.9.1 → nettracer3d-0.9.2}/src/nettracer3d/node_draw.py +0 -0
  19. {nettracer3d-0.9.1 → nettracer3d-0.9.2}/src/nettracer3d/painting.py +0 -0
  20. {nettracer3d-0.9.1 → nettracer3d-0.9.2}/src/nettracer3d/proximity.py +0 -0
  21. {nettracer3d-0.9.1 → nettracer3d-0.9.2}/src/nettracer3d/run.py +0 -0
  22. {nettracer3d-0.9.1 → nettracer3d-0.9.2}/src/nettracer3d/segmenter.py +0 -0
  23. {nettracer3d-0.9.1 → nettracer3d-0.9.2}/src/nettracer3d/segmenter_GPU.py +0 -0
  24. {nettracer3d-0.9.1 → nettracer3d-0.9.2}/src/nettracer3d/simple_network.py +0 -0
  25. {nettracer3d-0.9.1 → nettracer3d-0.9.2}/src/nettracer3d/smart_dilate.py +0 -0
  26. {nettracer3d-0.9.1 → nettracer3d-0.9.2}/src/nettracer3d.egg-info/SOURCES.txt +0 -0
  27. {nettracer3d-0.9.1 → nettracer3d-0.9.2}/src/nettracer3d.egg-info/dependency_links.txt +0 -0
  28. {nettracer3d-0.9.1 → nettracer3d-0.9.2}/src/nettracer3d.egg-info/entry_points.txt +0 -0
  29. {nettracer3d-0.9.1 → nettracer3d-0.9.2}/src/nettracer3d.egg-info/requires.txt +0 -0
  30. {nettracer3d-0.9.1 → nettracer3d-0.9.2}/src/nettracer3d.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nettracer3d
3
- Version: 0.9.1
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.1 Updates --
114
- * Adjusted the segment by 3D function to now show the 3D chunks in the preview mode. Previously it showed 2D segmentations in the preview which finished the current plane faster but didn't show accurate training data.
115
- * Adjusted the neighborhood heatmap predicted range value to now just simulate a uniform distribution rather than trying to use a mathematical algorithm.
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.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.
@@ -65,9 +65,8 @@ McLaughlin, L., Zhang, B., Sharma, S. et al. Three dimensional multiscalar neuro
65
65
 
66
66
  NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
67
67
 
68
- -- Version 0.9.1 Updates --
69
- * Adjusted the segment by 3D function to now show the 3D chunks in the preview mode. Previously it showed 2D segmentations in the preview which finished the current plane faster but didn't show accurate training data.
70
- * Adjusted the neighborhood heatmap predicted range value to now just simulate a uniform distribution rather than trying to use a mathematical algorithm.
71
- * The image display window now uses image pyramids and cropping for zoom ins so it should run a lot faster on bigger images.
72
- * The community UMAP can now color them by neighborhood.
73
- * No longer zooms all the way out by default with right click in zoom mode. Now user needs to Shift + Right Click.
68
+ -- Version 0.9.2 Updates --
69
+ * Image viewer canvas window can now be popped out into a separate window.
70
+ * Image pyramid calculation is more dynamic instead of using arbitrary size thresholds.
71
+ * Adjusted pan mode
72
+ * Some bug fixes.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nettracer3d"
3
- version = "0.9.1"
3
+ version = "0.9.2"
4
4
  authors = [
5
5
  { name="Liam McLaughlin", email="liamm@wustl.edu" },
6
6
  ]
@@ -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
- # Update display to show only background
2268
- self.update_display_pan_mode()
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: # Changed from self.channel_data[0] check
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: # Changed from img_width variable lookup
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: # Changed from img_height variable lookup
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
- self.canvas.draw_idle() # Changed from draw() to draw_idle()
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
- downsample_factor = getattr(self, 'downsample_factor', 1)
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(self.pan_background_image,
3037
- extent=(-0.5, width-0.5, height-0.5, -0.5),
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
- if hasattr(self, 'pan_zoom_state'):
3042
- self.ax.set_xlim(self.pan_zoom_state[0])
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} (DS: {downsample_factor}x)')
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
- self.ax.set_xlim([x0, x0 + width])
3116
- self.ax.set_ylim([y0 + height, y0])
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(channel.shape[0] - 1)
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 not end_paint:
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
- return
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 not self.pan_mode: # If using image pyramids
5459
- size = (x_max - x_min) * (y_max - y_min)
5460
- if size < (3000 * 3000): # Smaller window
5461
- val = 1
5462
- elif size > (3000 * 3000) and size < (6000 * 6000): # Med window
5463
- val = 2
5464
- elif size > (6000 * 6000) and size < (9000 * 9000): # Large window
5465
- val = 3
5466
- elif size > (9000 * 9000): # Very large window
5467
- val = 3
5468
- self.validate_downsample_input(text = val, update = False)
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 = 300 # 300ms 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
- except:
10411
- pass
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
- if self.machine_window.parent().shape[1] * self.machine_window.parent().shape[2] > 3000 * 3000 * self.machine_window.parent().downsample_factor: #arbitrary throttle for large arrays.
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.1
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.1 Updates --
114
- * Adjusted the segment by 3D function to now show the 3D chunks in the preview mode. Previously it showed 2D segmentations in the preview which finished the current plane faster but didn't show accurate training data.
115
- * Adjusted the neighborhood heatmap predicted range value to now just simulate a uniform distribution rather than trying to use a mathematical algorithm.
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.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.
File without changes
File without changes