lazylabel-gui 1.3.3__tar.gz → 1.3.4__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.
Files changed (54) hide show
  1. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/PKG-INFO +1 -1
  2. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/pyproject.toml +1 -1
  3. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/core/file_manager.py +1 -1
  4. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/ui/control_panel.py +7 -2
  5. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/ui/main_window.py +164 -63
  6. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/ui/widgets/channel_threshold_widget.py +8 -9
  7. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/ui/widgets/fft_threshold_widget.py +4 -0
  8. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/ui/widgets/model_selection_widget.py +9 -0
  9. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/utils/fast_file_manager.py +422 -78
  10. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel_gui.egg-info/PKG-INFO +1 -1
  11. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/LICENSE +0 -0
  12. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/README.md +0 -0
  13. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/setup.cfg +0 -0
  14. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/__init__.py +0 -0
  15. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/__main__.py +0 -0
  16. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/config/__init__.py +0 -0
  17. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/config/hotkeys.py +0 -0
  18. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/config/paths.py +0 -0
  19. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/config/settings.py +0 -0
  20. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/core/__init__.py +0 -0
  21. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/core/model_manager.py +0 -0
  22. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/core/segment_manager.py +0 -0
  23. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/main.py +0 -0
  24. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/models/__init__.py +0 -0
  25. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/models/sam2_model.py +0 -0
  26. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/models/sam_model.py +0 -0
  27. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/ui/__init__.py +0 -0
  28. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/ui/editable_vertex.py +0 -0
  29. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/ui/hotkey_dialog.py +0 -0
  30. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/ui/hoverable_pixelmap_item.py +0 -0
  31. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/ui/hoverable_polygon_item.py +0 -0
  32. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/ui/modes/__init__.py +0 -0
  33. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/ui/modes/base_mode.py +0 -0
  34. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/ui/modes/multi_view_mode.py +0 -0
  35. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/ui/modes/single_view_mode.py +0 -0
  36. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/ui/numeric_table_widget_item.py +0 -0
  37. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/ui/photo_viewer.py +0 -0
  38. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/ui/reorderable_class_table.py +0 -0
  39. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/ui/right_panel.py +0 -0
  40. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/ui/widgets/__init__.py +0 -0
  41. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/ui/widgets/adjustments_widget.py +0 -0
  42. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/ui/widgets/border_crop_widget.py +0 -0
  43. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/ui/widgets/fragment_threshold_widget.py +0 -0
  44. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/ui/widgets/settings_widget.py +0 -0
  45. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/ui/widgets/status_bar.py +0 -0
  46. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/utils/__init__.py +0 -0
  47. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/utils/custom_file_system_model.py +0 -0
  48. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/utils/logger.py +0 -0
  49. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel/utils/utils.py +0 -0
  50. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel_gui.egg-info/SOURCES.txt +0 -0
  51. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel_gui.egg-info/dependency_links.txt +0 -0
  52. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel_gui.egg-info/entry_points.txt +0 -0
  53. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel_gui.egg-info/requires.txt +0 -0
  54. {lazylabel_gui-1.3.3 → lazylabel_gui-1.3.4}/src/lazylabel_gui.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lazylabel-gui
3
- Version: 1.3.3
3
+ Version: 1.3.4
4
4
  Summary: An image segmentation GUI for generating ML ready mask tensors and annotations.
5
5
  Author-email: "Deniz N. Cakan" <deniz.n.cakan@gmail.com>
6
6
  License: MIT License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lazylabel-gui"
7
- version = "1.3.3"
7
+ version = "1.3.4"
8
8
  authors = [
9
9
  { name="Deniz N. Cakan", email="deniz.n.cakan@gmail.com" },
10
10
  ]
@@ -65,7 +65,7 @@ class FileManager:
65
65
  if not os.path.exists(npz_path):
66
66
  raise OSError(f"NPZ file was not created: {npz_path}")
67
67
 
68
- logger.info(f"Successfully saved NPZ: {os.path.basename(npz_path)}")
68
+ logger.debug(f"Successfully saved NPZ: {os.path.basename(npz_path)}")
69
69
  return npz_path
70
70
 
71
71
  def save_yolo_txt(
@@ -612,10 +612,10 @@ class ControlPanel(QWidget):
612
612
  layout.addWidget(crop_collapsible)
613
613
 
614
614
  # Channel Threshold - collapsible
615
- threshold_collapsible = SimpleCollapsible(
615
+ self.channel_threshold_collapsible = SimpleCollapsible(
616
616
  "Channel Threshold", self.channel_threshold_widget
617
617
  )
618
- layout.addWidget(threshold_collapsible)
618
+ layout.addWidget(self.channel_threshold_collapsible)
619
619
 
620
620
  # FFT Threshold - collapsible (default collapsed)
621
621
  self.fft_threshold_collapsible = SimpleCollapsible(
@@ -880,6 +880,11 @@ class ControlPanel(QWidget):
880
880
  """Update channel threshold widget for new image."""
881
881
  self.channel_threshold_widget.update_for_image(image_array)
882
882
 
883
+ # Auto-expand channel threshold panel when any image is loaded
884
+ if image_array is not None and hasattr(self, "channel_threshold_collapsible"):
885
+ # Find and expand the Channel Threshold panel
886
+ self.channel_threshold_collapsible.set_collapsed(False)
887
+
883
888
  def get_channel_threshold_widget(self):
884
889
  """Get the channel threshold widget."""
885
890
  return self.channel_threshold_widget
@@ -19,12 +19,14 @@ from PyQt6.QtGui import (
19
19
  QShortcut,
20
20
  )
21
21
  from PyQt6.QtWidgets import (
22
+ QApplication,
22
23
  QDialog,
23
24
  QFileDialog,
24
25
  QGraphicsEllipseItem,
25
26
  QGraphicsLineItem,
26
27
  QGraphicsPolygonItem,
27
28
  QGraphicsRectItem,
29
+ QGridLayout,
28
30
  QHBoxLayout,
29
31
  QLabel,
30
32
  QMainWindow,
@@ -772,6 +774,7 @@ class MainWindow(QMainWindow):
772
774
  logger.info("Step 5/8: Discovering available models...")
773
775
  self._setup_model_manager() # Just setup manager, don't load model
774
776
  self._setup_connections()
777
+ self._fix_fft_connection() # Workaround for FFT signal connection issue
775
778
  self._setup_shortcuts()
776
779
  self._load_settings()
777
780
 
@@ -877,12 +880,7 @@ class MainWindow(QMainWindow):
877
880
  grid_widget = QWidget()
878
881
 
879
882
  # Use grid layout for 4-view, horizontal layout for 2-view
880
- if use_grid:
881
- from PyQt6.QtWidgets import QGridLayout
882
-
883
- grid_layout = QGridLayout(grid_widget)
884
- else:
885
- grid_layout = QHBoxLayout(grid_widget)
883
+ grid_layout = QGridLayout(grid_widget) if use_grid else QHBoxLayout(grid_widget)
886
884
  grid_layout.setSpacing(5)
887
885
 
888
886
  self.multi_view_viewers = []
@@ -951,29 +949,30 @@ class MainWindow(QMainWindow):
951
949
  grid_mode_label = QLabel("View Mode:")
952
950
  controls_layout.addWidget(grid_mode_label)
953
951
 
954
- from PyQt6.QtWidgets import QComboBox
952
+ from lazylabel.ui.widgets.model_selection_widget import CustomDropdown
955
953
 
956
- self.grid_mode_combo = QComboBox()
954
+ self.grid_mode_combo = CustomDropdown()
955
+ self.grid_mode_combo.setText("View Mode") # Default text
957
956
  self.grid_mode_combo.addItem("2 Views (1x2)", "2_view")
958
957
  self.grid_mode_combo.addItem("4 Views (2x2)", "4_view")
959
958
 
960
959
  # Set current selection based on settings
961
960
  current_mode = self.settings.multi_view_grid_mode
962
- for i in range(self.grid_mode_combo.count()):
961
+ for i in range(len(self.grid_mode_combo.items)):
963
962
  if self.grid_mode_combo.itemData(i) == current_mode:
964
963
  self.grid_mode_combo.setCurrentIndex(i)
965
964
  break
966
965
 
967
- self.grid_mode_combo.currentTextChanged.connect(self._on_grid_mode_changed)
966
+ self.grid_mode_combo.activated.connect(self._on_grid_mode_changed)
968
967
  controls_layout.addWidget(self.grid_mode_combo)
969
968
 
970
969
  controls_layout.addStretch()
971
970
 
972
971
  layout.addWidget(controls_widget)
973
972
 
974
- def _on_grid_mode_changed(self):
973
+ def _on_grid_mode_changed(self, index):
975
974
  """Handle grid mode change from combo box."""
976
- current_data = self.grid_mode_combo.currentData()
975
+ current_data = self.grid_mode_combo.itemData(index)
977
976
  if current_data and current_data != self.settings.multi_view_grid_mode:
978
977
  # Update settings
979
978
  self.settings.multi_view_grid_mode = current_data
@@ -1062,8 +1061,7 @@ class MainWindow(QMainWindow):
1062
1061
  layout.deleteLater()
1063
1062
  except Exception as e:
1064
1063
  # If layout clearing fails, just reset the viewer lists
1065
- print(f"Layout clearing failed: {e}")
1066
- pass
1064
+ logger.error(f"Layout clearing failed: {e}")
1067
1065
 
1068
1066
  def _clear_multi_view_layout(self):
1069
1067
  """Clear the existing multi-view layout."""
@@ -1121,6 +1119,48 @@ class MainWindow(QMainWindow):
1121
1119
  # Switch to polygon mode if SAM is disabled and we're in SAM/AI mode
1122
1120
  self.set_polygon_mode()
1123
1121
 
1122
+ def _fix_fft_connection(self):
1123
+ """Fix FFT signal connection issue - workaround for connection timing problem."""
1124
+ try:
1125
+ # Get the FFT widget directly and connect to its signal
1126
+ fft_widget = self.control_panel.get_fft_threshold_widget()
1127
+ if fft_widget:
1128
+ # Direct connection bypass - connect FFT widget directly to main window handler
1129
+ # This bypasses the control panel signal forwarding which has timing issues
1130
+ # Use a wrapper to ensure the connection works reliably
1131
+ def fft_signal_wrapper():
1132
+ self._handle_fft_threshold_changed()
1133
+
1134
+ fft_widget.fft_threshold_changed.connect(fft_signal_wrapper)
1135
+
1136
+ logger.info("FFT signal connection bypass established successfully")
1137
+ else:
1138
+ logger.warning("FFT widget not found during connection fix")
1139
+ except Exception as e:
1140
+ logger.warning(f"Failed to establish FFT connection bypass: {e}")
1141
+
1142
+ # Also fix channel threshold connection for RGB images
1143
+ try:
1144
+ channel_widget = self.control_panel.get_channel_threshold_widget()
1145
+ if channel_widget:
1146
+ # Direct connection bypass for channel threshold widget too
1147
+ def channel_signal_wrapper():
1148
+ self._handle_channel_threshold_changed()
1149
+
1150
+ channel_widget.thresholdChanged.connect(channel_signal_wrapper)
1151
+
1152
+ logger.info(
1153
+ "Channel threshold signal connection bypass established successfully"
1154
+ )
1155
+ else:
1156
+ logger.warning(
1157
+ "Channel threshold widget not found during connection fix"
1158
+ )
1159
+ except Exception as e:
1160
+ logger.warning(
1161
+ f"Failed to establish channel threshold connection bypass: {e}"
1162
+ )
1163
+
1124
1164
  def _setup_connections(self):
1125
1165
  """Setup signal connections."""
1126
1166
  # Control panel connections
@@ -1169,9 +1209,13 @@ class MainWindow(QMainWindow):
1169
1209
  )
1170
1210
 
1171
1211
  # FFT threshold connections
1172
- self.control_panel.fft_threshold_changed.connect(
1173
- self._handle_fft_threshold_changed
1174
- )
1212
+ try:
1213
+ self.control_panel.fft_threshold_changed.connect(
1214
+ self._handle_fft_threshold_changed
1215
+ )
1216
+ logger.debug("FFT threshold connection established in _setup_connections")
1217
+ except Exception as e:
1218
+ logger.error(f"Failed to establish FFT threshold connection: {e}")
1175
1219
 
1176
1220
  # Right panel connections
1177
1221
  self.right_panel.open_folder_requested.connect(self._open_folder_dialog)
@@ -1774,6 +1818,9 @@ class MainWindow(QMainWindow):
1774
1818
  # Update file selection in the file manager
1775
1819
  self.right_panel.select_file(Path(path))
1776
1820
 
1821
+ # Update threshold widgets for new image (this was missing!)
1822
+ self._update_channel_threshold_for_image(pixmap)
1823
+
1777
1824
  def _load_multi_view_from_path(self, path: str):
1778
1825
  """Load multi-view starting from a specific path using FastFileManager."""
1779
1826
  config = self._get_multi_view_config()
@@ -1948,6 +1995,9 @@ class MainWindow(QMainWindow):
1948
1995
  if changed_indices:
1949
1996
  self._fast_update_multi_view_images(changed_indices)
1950
1997
 
1998
+ # Update threshold widgets for the loaded images
1999
+ self._update_multi_view_channel_threshold_for_images()
2000
+
1951
2001
  # Load existing segments for all loaded images
1952
2002
  valid_image_paths = [path for path in image_paths if path is not None]
1953
2003
  if valid_image_paths:
@@ -2005,7 +2055,9 @@ class MainWindow(QMainWindow):
2005
2055
  all_segments.extend(viewer_segments)
2006
2056
  self.segment_manager.segments.clear()
2007
2057
  except Exception as e:
2008
- print(f"Error loading segments for viewer {viewer_index}: {e}")
2058
+ logger.error(
2059
+ f"Error loading segments for viewer {viewer_index}: {e}"
2060
+ )
2009
2061
 
2010
2062
  # Set all segments at once
2011
2063
  self.segment_manager.segments = all_segments
@@ -2025,7 +2077,7 @@ class MainWindow(QMainWindow):
2025
2077
  self._update_all_lists()
2026
2078
 
2027
2079
  except Exception as e:
2028
- print(f"Error in _load_multi_view_segments: {e}")
2080
+ logger.error(f"Error in _load_multi_view_segments: {e}")
2029
2081
  self.segment_manager.segments.clear()
2030
2082
 
2031
2083
  def _cancel_multi_view_sam_loading(self):
@@ -3353,6 +3405,8 @@ class MainWindow(QMainWindow):
3353
3405
  valid_paths = [Path(img) for img in self.multi_view_images if img]
3354
3406
  if valid_paths:
3355
3407
  self.right_panel.file_manager.batchUpdateFileStatus(valid_paths)
3408
+ # Force immediate GUI update
3409
+ QApplication.processEvents()
3356
3410
  # Clear the tracking list for next save
3357
3411
  self._saved_file_paths = []
3358
3412
 
@@ -3401,6 +3455,8 @@ class MainWindow(QMainWindow):
3401
3455
  )
3402
3456
  # Update UI immediately when files are deleted
3403
3457
  self._update_all_lists()
3458
+ # Force immediate GUI update
3459
+ QApplication.processEvents()
3404
3460
  else:
3405
3461
  self._show_warning_notification("No segments to save.")
3406
3462
  return
@@ -3478,15 +3534,16 @@ class MainWindow(QMainWindow):
3478
3534
  )
3479
3535
 
3480
3536
  # Update FastFileManager to show NPZ/TXT checkmarks
3481
- if (
3482
- (npz_path or txt_path)
3483
- and hasattr(self, "right_panel")
3484
- and hasattr(self.right_panel, "file_manager")
3537
+ # Always update file status after save attempt (regardless of what was saved)
3538
+ if hasattr(self, "right_panel") and hasattr(
3539
+ self.right_panel, "file_manager"
3485
3540
  ):
3486
3541
  # Update the file status in the FastFileManager
3487
3542
  self.right_panel.file_manager.updateFileStatus(
3488
3543
  Path(self.current_image_path)
3489
3544
  )
3545
+ # Force immediate GUI update
3546
+ QApplication.processEvents()
3490
3547
  except Exception as e:
3491
3548
  logger.error(f"Error saving file: {str(e)}", exc_info=True)
3492
3549
  self._show_error_notification(f"Error saving: {str(e)}")
@@ -5714,27 +5771,50 @@ class MainWindow(QMainWindow):
5714
5771
 
5715
5772
  def _update_channel_threshold_for_image(self, pixmap):
5716
5773
  """Update channel threshold widget for the given image pixmap."""
5717
- if pixmap.isNull():
5774
+ if pixmap.isNull() or not self.current_image_path:
5718
5775
  self.control_panel.update_channel_threshold_for_image(None)
5719
5776
  return
5720
5777
 
5721
- # Convert pixmap to numpy array
5722
- qimage = pixmap.toImage()
5723
- ptr = qimage.constBits()
5724
- ptr.setsize(qimage.bytesPerLine() * qimage.height())
5725
- image_np = np.array(ptr).reshape(qimage.height(), qimage.width(), 4)
5726
- # Convert from BGRA to RGB, ignore alpha
5727
- image_rgb = image_np[:, :, [2, 1, 0]]
5778
+ # Use cv2.imread for more robust loading instead of QPixmap conversion
5779
+ try:
5780
+ import cv2
5728
5781
 
5729
- # Check if image is grayscale (all channels are the same)
5730
- if np.array_equal(image_rgb[:, :, 0], image_rgb[:, :, 1]) and np.array_equal(
5731
- image_rgb[:, :, 1], image_rgb[:, :, 2]
5732
- ):
5733
- # Convert to single channel grayscale
5734
- image_array = image_rgb[:, :, 0]
5735
- else:
5736
- # Keep as RGB
5737
- image_array = image_rgb
5782
+ image_array = cv2.imread(self.current_image_path)
5783
+ if image_array is None:
5784
+ self.control_panel.update_channel_threshold_for_image(None)
5785
+ return
5786
+
5787
+ # Convert from BGR to RGB
5788
+ if len(image_array.shape) == 3 and image_array.shape[2] == 3:
5789
+ image_array = cv2.cvtColor(image_array, cv2.COLOR_BGR2RGB)
5790
+
5791
+ # Check if image is grayscale (all channels are the same)
5792
+ if (
5793
+ len(image_array.shape) == 3
5794
+ and np.array_equal(image_array[:, :, 0], image_array[:, :, 1])
5795
+ and np.array_equal(image_array[:, :, 1], image_array[:, :, 2])
5796
+ ):
5797
+ # Convert to single channel grayscale
5798
+ image_array = image_array[:, :, 0]
5799
+
5800
+ except Exception:
5801
+ # Fallback to QPixmap conversion if cv2 fails
5802
+ qimage = pixmap.toImage()
5803
+ ptr = qimage.constBits()
5804
+ ptr.setsize(qimage.bytesPerLine() * qimage.height())
5805
+ image_np = np.array(ptr).reshape(qimage.height(), qimage.width(), 4)
5806
+ # Convert from BGRA to RGB, ignore alpha
5807
+ image_rgb = image_np[:, :, [2, 1, 0]]
5808
+
5809
+ # Check if image is grayscale (all channels are the same)
5810
+ if np.array_equal(
5811
+ image_rgb[:, :, 0], image_rgb[:, :, 1]
5812
+ ) and np.array_equal(image_rgb[:, :, 1], image_rgb[:, :, 2]):
5813
+ # Convert to single channel grayscale
5814
+ image_array = image_rgb[:, :, 0]
5815
+ else:
5816
+ # Keep as RGB
5817
+ image_array = image_rgb
5738
5818
 
5739
5819
  # Update the channel threshold widget
5740
5820
  self.control_panel.update_channel_threshold_for_image(image_array)
@@ -5762,29 +5842,51 @@ class MainWindow(QMainWindow):
5762
5842
  self.control_panel.update_channel_threshold_for_image(None)
5763
5843
  return
5764
5844
 
5765
- # Load and convert the first image to update the widget
5766
- pixmap = QPixmap(first_image_path)
5767
- if pixmap.isNull():
5768
- self.control_panel.update_channel_threshold_for_image(None)
5769
- return
5845
+ # Use cv2.imread for more robust loading instead of QPixmap conversion
5846
+ try:
5847
+ import cv2
5770
5848
 
5771
- # Convert pixmap to numpy array
5772
- qimage = pixmap.toImage()
5773
- ptr = qimage.constBits()
5774
- ptr.setsize(qimage.bytesPerLine() * qimage.height())
5775
- image_np = np.array(ptr).reshape(qimage.height(), qimage.width(), 4)
5776
- # Convert from BGRA to RGB, ignore alpha
5777
- image_rgb = image_np[:, :, [2, 1, 0]]
5849
+ image_array = cv2.imread(first_image_path)
5850
+ if image_array is None:
5851
+ self.control_panel.update_channel_threshold_for_image(None)
5852
+ return
5778
5853
 
5779
- # Check if image is grayscale (all channels are the same)
5780
- if np.array_equal(image_rgb[:, :, 0], image_rgb[:, :, 1]) and np.array_equal(
5781
- image_rgb[:, :, 1], image_rgb[:, :, 2]
5782
- ):
5783
- # Convert to single channel grayscale
5784
- image_array = image_rgb[:, :, 0]
5785
- else:
5786
- # Keep as RGB
5787
- image_array = image_rgb
5854
+ # Convert from BGR to RGB
5855
+ if len(image_array.shape) == 3 and image_array.shape[2] == 3:
5856
+ image_array = cv2.cvtColor(image_array, cv2.COLOR_BGR2RGB)
5857
+
5858
+ # Check if image is grayscale (all channels are the same)
5859
+ if (
5860
+ len(image_array.shape) == 3
5861
+ and np.array_equal(image_array[:, :, 0], image_array[:, :, 1])
5862
+ and np.array_equal(image_array[:, :, 1], image_array[:, :, 2])
5863
+ ):
5864
+ # Convert to single channel grayscale
5865
+ image_array = image_array[:, :, 0]
5866
+
5867
+ except Exception:
5868
+ # Fallback to QPixmap conversion if cv2 fails
5869
+ pixmap = QPixmap(first_image_path)
5870
+ if pixmap.isNull():
5871
+ self.control_panel.update_channel_threshold_for_image(None)
5872
+ return
5873
+
5874
+ qimage = pixmap.toImage()
5875
+ ptr = qimage.constBits()
5876
+ ptr.setsize(qimage.bytesPerLine() * qimage.height())
5877
+ image_np = np.array(ptr).reshape(qimage.height(), qimage.width(), 4)
5878
+ # Convert from BGRA to RGB, ignore alpha
5879
+ image_rgb = image_np[:, :, [2, 1, 0]]
5880
+
5881
+ # Check if image is grayscale (all channels are the same)
5882
+ if np.array_equal(
5883
+ image_rgb[:, :, 0], image_rgb[:, :, 1]
5884
+ ) and np.array_equal(image_rgb[:, :, 1], image_rgb[:, :, 2]):
5885
+ # Convert to single channel grayscale
5886
+ image_array = image_rgb[:, :, 0]
5887
+ else:
5888
+ # Keep as RGB
5889
+ image_array = image_rgb
5788
5890
 
5789
5891
  # Update the channel threshold widget
5790
5892
  self.control_panel.update_channel_threshold_for_image(image_array)
@@ -7097,7 +7199,6 @@ class MainWindow(QMainWindow):
7097
7199
  )
7098
7200
 
7099
7201
  # Load existing segments for the current image
7100
- print(f"Loading segments for single-view: {self.current_image_path}")
7101
7202
  try:
7102
7203
  # Clear any leftover multi-view segments first
7103
7204
  self.segment_manager.clear()
@@ -7114,7 +7215,7 @@ class MainWindow(QMainWindow):
7114
7215
  self._update_all_lists()
7115
7216
 
7116
7217
  except Exception as e:
7117
- print(f"Error loading segments for single-view: {e}")
7218
+ logger.error(f"Error loading segments for single-view: {e}")
7118
7219
 
7119
7220
  # Redisplay segments for single view
7120
7221
  if hasattr(self, "single_view_mode_handler"):
@@ -270,10 +270,6 @@ class MultiIndicatorSlider(QWidget):
270
270
  """Handle right-click to remove indicator."""
271
271
  slider_rect = self.get_slider_rect()
272
272
 
273
- # Only allow removal if more than 1 indicator
274
- if len(self.indicators) <= 1:
275
- return
276
-
277
273
  # Check if right-clicking on an indicator
278
274
  for i, value in enumerate(self.indicators):
279
275
  x = self.value_to_x(value)
@@ -473,7 +469,7 @@ class ChannelThresholdWidget(QWidget):
473
469
 
474
470
  if self.current_image_channels == 1:
475
471
  # Grayscale image
476
- if "Gray" in self.sliders:
472
+ if "Gray" in self.sliders and self.sliders["Gray"].is_enabled():
477
473
  result = self._apply_channel_thresholding(
478
474
  result, self.sliders["Gray"].get_indicators()
479
475
  )
@@ -481,7 +477,10 @@ class ChannelThresholdWidget(QWidget):
481
477
  # RGB image
482
478
  channel_names = ["Red", "Green", "Blue"]
483
479
  for i, channel_name in enumerate(channel_names):
484
- if channel_name in self.sliders:
480
+ if (
481
+ channel_name in self.sliders
482
+ and self.sliders[channel_name].is_enabled()
483
+ ):
485
484
  result[:, :, i] = self._apply_channel_thresholding(
486
485
  result[:, :, i], self.sliders[channel_name].get_indicators()
487
486
  )
@@ -494,7 +493,7 @@ class ChannelThresholdWidget(QWidget):
494
493
  if not indicators:
495
494
  return channel_data
496
495
 
497
- # Sort indicators
496
+ # Sort indicators
498
497
  sorted_indicators = sorted(indicators)
499
498
 
500
499
  # Create output array
@@ -527,9 +526,9 @@ class ChannelThresholdWidget(QWidget):
527
526
  return result
528
527
 
529
528
  def has_active_thresholding(self):
530
- """Check if any channel has active thresholding (indicators present)."""
529
+ """Check if any channel has active thresholding (enabled and indicators present)."""
531
530
  for slider_widget in self.sliders.values():
532
- if slider_widget.get_indicators():
531
+ if slider_widget.is_enabled() and slider_widget.get_indicators():
533
532
  return True
534
533
  return False
535
534
 
@@ -308,6 +308,8 @@ class FFTThresholdWidget(QWidget):
308
308
  self.status_label.setStyleSheet(
309
309
  "color: #F44336; font-size: 9px; font-style: italic;"
310
310
  )
311
+ # Disable FFT processing for color images
312
+ self.enable_checkbox.setChecked(False)
311
313
  else:
312
314
  # Unknown format
313
315
  self.current_image_channels = 0
@@ -315,6 +317,8 @@ class FFTThresholdWidget(QWidget):
315
317
  self.status_label.setStyleSheet(
316
318
  "color: #F44336; font-size: 9px; font-style: italic;"
317
319
  )
320
+ # Disable FFT processing for unsupported formats
321
+ self.enable_checkbox.setChecked(False)
318
322
 
319
323
  def is_active(self):
320
324
  """Check if FFT processing is active (checkbox enabled and image is grayscale)."""
@@ -111,6 +111,15 @@ class CustomDropdown(QToolButton):
111
111
  text, _ = self.items[index]
112
112
  self.setText(text)
113
113
 
114
+ def count(self):
115
+ """Get number of items."""
116
+ return len(self.items)
117
+
118
+ def currentData(self):
119
+ """Get data of currently selected item."""
120
+ current_idx = self.currentIndex()
121
+ return self.itemData(current_idx)
122
+
114
123
  def blockSignals(self, block):
115
124
  """Block/unblock signals."""
116
125
  super().blockSignals(block)