lazylabel-gui 1.0.8__py3-none-any.whl → 1.1.0__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.
Files changed (40) hide show
  1. lazylabel/__init__.py +9 -0
  2. lazylabel/config/__init__.py +7 -0
  3. lazylabel/config/hotkeys.py +169 -0
  4. lazylabel/config/paths.py +41 -0
  5. lazylabel/config/settings.py +66 -0
  6. lazylabel/core/__init__.py +7 -0
  7. lazylabel/core/file_manager.py +106 -0
  8. lazylabel/core/model_manager.py +94 -0
  9. lazylabel/core/segment_manager.py +140 -0
  10. lazylabel/main.py +10 -1246
  11. lazylabel/models/__init__.py +5 -0
  12. lazylabel/models/sam_model.py +154 -0
  13. lazylabel/ui/__init__.py +8 -0
  14. lazylabel/ui/control_panel.py +220 -0
  15. lazylabel/{editable_vertex.py → ui/editable_vertex.py} +25 -3
  16. lazylabel/ui/hotkey_dialog.py +384 -0
  17. lazylabel/{hoverable_polygon_item.py → ui/hoverable_polygon_item.py} +17 -1
  18. lazylabel/ui/main_window.py +1264 -0
  19. lazylabel/ui/right_panel.py +239 -0
  20. lazylabel/ui/widgets/__init__.py +7 -0
  21. lazylabel/ui/widgets/adjustments_widget.py +107 -0
  22. lazylabel/ui/widgets/model_selection_widget.py +94 -0
  23. lazylabel/ui/widgets/settings_widget.py +106 -0
  24. lazylabel/utils/__init__.py +6 -0
  25. lazylabel/utils/custom_file_system_model.py +132 -0
  26. {lazylabel_gui-1.0.8.dist-info → lazylabel_gui-1.1.0.dist-info}/METADATA +62 -12
  27. lazylabel_gui-1.1.0.dist-info/RECORD +36 -0
  28. lazylabel/controls.py +0 -261
  29. lazylabel/custom_file_system_model.py +0 -72
  30. lazylabel/sam_model.py +0 -70
  31. lazylabel_gui-1.0.8.dist-info/RECORD +0 -17
  32. /lazylabel/{hoverable_pixelmap_item.py → ui/hoverable_pixelmap_item.py} +0 -0
  33. /lazylabel/{numeric_table_widget_item.py → ui/numeric_table_widget_item.py} +0 -0
  34. /lazylabel/{photo_viewer.py → ui/photo_viewer.py} +0 -0
  35. /lazylabel/{reorderable_class_table.py → ui/reorderable_class_table.py} +0 -0
  36. /lazylabel/{utils.py → utils/utils.py} +0 -0
  37. {lazylabel_gui-1.0.8.dist-info → lazylabel_gui-1.1.0.dist-info}/WHEEL +0 -0
  38. {lazylabel_gui-1.0.8.dist-info → lazylabel_gui-1.1.0.dist-info}/entry_points.txt +0 -0
  39. {lazylabel_gui-1.0.8.dist-info → lazylabel_gui-1.1.0.dist-info}/licenses/LICENSE +0 -0
  40. {lazylabel_gui-1.0.8.dist-info → lazylabel_gui-1.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,239 @@
1
+ """Right panel with file explorer and segment management."""
2
+
3
+ from PyQt6.QtWidgets import (
4
+ QWidget, QVBoxLayout, QPushButton, QLabel, QHBoxLayout,
5
+ QTableWidget, QTreeView, QComboBox, QSplitter, QSpacerItem, QHeaderView
6
+ )
7
+ from PyQt6.QtCore import Qt, pyqtSignal
8
+ from PyQt6.QtGui import QBrush, QColor
9
+
10
+ from .reorderable_class_table import ReorderableClassTable
11
+ from .numeric_table_widget_item import NumericTableWidgetItem
12
+
13
+
14
+ class RightPanel(QWidget):
15
+ """Right panel with file explorer and segment management."""
16
+
17
+ # Signals
18
+ open_folder_requested = pyqtSignal()
19
+ image_selected = pyqtSignal('QModelIndex')
20
+ merge_selection_requested = pyqtSignal()
21
+ delete_selection_requested = pyqtSignal()
22
+ segments_selection_changed = pyqtSignal()
23
+ class_alias_changed = pyqtSignal(int, str) # class_id, alias
24
+ reassign_classes_requested = pyqtSignal()
25
+ class_filter_changed = pyqtSignal()
26
+
27
+ def __init__(self, parent=None):
28
+ super().__init__(parent)
29
+ self.setFixedWidth(350)
30
+ self._setup_ui()
31
+ self._connect_signals()
32
+
33
+ def _setup_ui(self):
34
+ """Setup the UI layout."""
35
+ self.v_layout = QVBoxLayout(self)
36
+
37
+ # Toggle button
38
+ toggle_layout = QHBoxLayout()
39
+ toggle_layout.addStretch()
40
+ self.btn_toggle_visibility = QPushButton("Hide >")
41
+ self.btn_toggle_visibility.setToolTip("Hide this panel")
42
+ toggle_layout.addWidget(self.btn_toggle_visibility)
43
+ self.v_layout.addLayout(toggle_layout)
44
+
45
+ # Main controls widget
46
+ self.main_controls_widget = QWidget()
47
+ main_layout = QVBoxLayout(self.main_controls_widget)
48
+ main_layout.setContentsMargins(0, 0, 0, 0)
49
+
50
+ # Vertical splitter for sections
51
+ v_splitter = QSplitter(Qt.Orientation.Vertical)
52
+
53
+ # File explorer section
54
+ self._setup_file_explorer(v_splitter)
55
+
56
+ # Segment management section
57
+ self._setup_segment_management(v_splitter)
58
+
59
+ # Class management section
60
+ self._setup_class_management(v_splitter)
61
+
62
+ main_layout.addWidget(v_splitter)
63
+
64
+ # Status label
65
+ self.status_label = QLabel("")
66
+ self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
67
+ main_layout.addWidget(self.status_label)
68
+
69
+ self.v_layout.addWidget(self.main_controls_widget)
70
+
71
+ def _setup_file_explorer(self, splitter):
72
+ """Setup file explorer section."""
73
+ file_explorer_widget = QWidget()
74
+ layout = QVBoxLayout(file_explorer_widget)
75
+ layout.setContentsMargins(0, 0, 0, 0)
76
+
77
+ self.btn_open_folder = QPushButton("Open Image Folder")
78
+ self.btn_open_folder.setToolTip("Open a directory of images")
79
+ layout.addWidget(self.btn_open_folder)
80
+
81
+ self.file_tree = QTreeView()
82
+ layout.addWidget(self.file_tree)
83
+
84
+ splitter.addWidget(file_explorer_widget)
85
+
86
+ def _setup_segment_management(self, splitter):
87
+ """Setup segment management section."""
88
+ segment_widget = QWidget()
89
+ layout = QVBoxLayout(segment_widget)
90
+ layout.setContentsMargins(0, 0, 0, 0)
91
+
92
+ # Class filter
93
+ filter_layout = QHBoxLayout()
94
+ filter_layout.addWidget(QLabel("Filter Class:"))
95
+ self.class_filter_combo = QComboBox()
96
+ self.class_filter_combo.setToolTip("Filter segments list by class")
97
+ filter_layout.addWidget(self.class_filter_combo)
98
+ layout.addLayout(filter_layout)
99
+
100
+ # Segment table
101
+ self.segment_table = QTableWidget()
102
+ self.segment_table.setColumnCount(3)
103
+ self.segment_table.setHorizontalHeaderLabels(["Segment ID", "Class ID", "Alias"])
104
+ self.segment_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
105
+ self.segment_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
106
+ self.segment_table.setSortingEnabled(True)
107
+ self.segment_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
108
+ layout.addWidget(self.segment_table)
109
+
110
+ # Action buttons
111
+ action_layout = QHBoxLayout()
112
+ self.btn_merge_selection = QPushButton("Merge to Class")
113
+ self.btn_merge_selection.setToolTip("Merge selected segments into a single class (M)")
114
+ self.btn_delete_selection = QPushButton("Delete")
115
+ self.btn_delete_selection.setToolTip("Delete selected segments (Delete/Backspace)")
116
+ action_layout.addWidget(self.btn_merge_selection)
117
+ action_layout.addWidget(self.btn_delete_selection)
118
+ layout.addLayout(action_layout)
119
+
120
+ splitter.addWidget(segment_widget)
121
+
122
+ def _setup_class_management(self, splitter):
123
+ """Setup class management section."""
124
+ class_widget = QWidget()
125
+ layout = QVBoxLayout(class_widget)
126
+ layout.setContentsMargins(0, 0, 0, 0)
127
+
128
+ layout.addWidget(QLabel("Class Order:"))
129
+
130
+ self.class_table = ReorderableClassTable()
131
+ self.class_table.setToolTip(
132
+ "Double-click to set class aliases and drag to reorder channels for saving."
133
+ )
134
+ self.class_table.setColumnCount(2)
135
+ self.class_table.setHorizontalHeaderLabels(["Alias", "Class ID"])
136
+ self.class_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
137
+ self.class_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
138
+ self.class_table.setEditTriggers(QTableWidget.EditTrigger.DoubleClicked)
139
+ layout.addWidget(self.class_table)
140
+
141
+ self.btn_reassign_classes = QPushButton("Reassign Class IDs")
142
+ self.btn_reassign_classes.setToolTip(
143
+ "Re-index class channels based on the current order in this table"
144
+ )
145
+ layout.addWidget(self.btn_reassign_classes)
146
+
147
+ splitter.addWidget(class_widget)
148
+
149
+ def _connect_signals(self):
150
+ """Connect internal signals."""
151
+ self.btn_open_folder.clicked.connect(self.open_folder_requested)
152
+ self.file_tree.doubleClicked.connect(self.image_selected)
153
+ self.btn_merge_selection.clicked.connect(self.merge_selection_requested)
154
+ self.btn_delete_selection.clicked.connect(self.delete_selection_requested)
155
+ self.segment_table.itemSelectionChanged.connect(self.segments_selection_changed)
156
+ self.class_table.itemChanged.connect(self._handle_class_alias_change)
157
+ self.btn_reassign_classes.clicked.connect(self.reassign_classes_requested)
158
+ self.class_filter_combo.currentIndexChanged.connect(self.class_filter_changed)
159
+
160
+ def _handle_class_alias_change(self, item):
161
+ """Handle class alias change in table."""
162
+ if item.column() != 0: # Only handle alias column
163
+ return
164
+
165
+ class_table = self.class_table
166
+ id_item = class_table.item(item.row(), 1)
167
+ if id_item:
168
+ try:
169
+ class_id = int(id_item.text())
170
+ self.class_alias_changed.emit(class_id, item.text())
171
+ except (ValueError, AttributeError):
172
+ pass
173
+
174
+ def setup_file_model(self, file_model):
175
+ """Setup the file model for the tree view."""
176
+ self.file_tree.setModel(file_model)
177
+ self.file_tree.setColumnWidth(0, 200)
178
+
179
+ def set_folder(self, folder_path, file_model):
180
+ """Set the folder for file browsing."""
181
+ self.file_tree.setRootIndex(file_model.setRootPath(folder_path))
182
+
183
+ def toggle_visibility(self):
184
+ """Toggle panel visibility."""
185
+ is_visible = self.main_controls_widget.isVisible()
186
+ self.main_controls_widget.setVisible(not is_visible)
187
+
188
+ if is_visible: # Content is now hidden
189
+ self.v_layout.addStretch(1)
190
+ self.btn_toggle_visibility.setText("< Show")
191
+ self.setFixedWidth(self.btn_toggle_visibility.sizeHint().width() + 20)
192
+ else: # Content is now visible
193
+ # Remove the stretch
194
+ for i in range(self.v_layout.count()):
195
+ item = self.v_layout.itemAt(i)
196
+ if isinstance(item, QSpacerItem):
197
+ self.v_layout.removeItem(item)
198
+ break
199
+ self.btn_toggle_visibility.setText("Hide >")
200
+ self.setFixedWidth(350)
201
+
202
+ def get_selected_segment_indices(self):
203
+ """Get indices of selected segments."""
204
+ selected_items = self.segment_table.selectedItems()
205
+ selected_rows = sorted(list({item.row() for item in selected_items}))
206
+ return [
207
+ self.segment_table.item(row, 0).data(Qt.ItemDataRole.UserRole)
208
+ for row in selected_rows
209
+ if self.segment_table.item(row, 0)
210
+ ]
211
+
212
+ def get_class_order(self):
213
+ """Get the current class order from the class table."""
214
+ ordered_ids = []
215
+ for row in range(self.class_table.rowCount()):
216
+ id_item = self.class_table.item(row, 1)
217
+ if id_item and id_item.text():
218
+ try:
219
+ ordered_ids.append(int(id_item.text()))
220
+ except ValueError:
221
+ continue
222
+ return ordered_ids
223
+
224
+ def clear_selections(self):
225
+ """Clear all selections."""
226
+ self.segment_table.clearSelection()
227
+ self.class_table.clearSelection()
228
+
229
+ def select_all_segments(self):
230
+ """Select all segments."""
231
+ self.segment_table.selectAll()
232
+
233
+ def set_status(self, message):
234
+ """Set status message."""
235
+ self.status_label.setText(message)
236
+
237
+ def clear_status(self):
238
+ """Clear status message."""
239
+ self.status_label.clear()
@@ -0,0 +1,7 @@
1
+ """UI widgets for LazyLabel."""
2
+
3
+ from .model_selection_widget import ModelSelectionWidget
4
+ from .settings_widget import SettingsWidget
5
+ from .adjustments_widget import AdjustmentsWidget
6
+
7
+ __all__ = ['ModelSelectionWidget', 'SettingsWidget', 'AdjustmentsWidget']
@@ -0,0 +1,107 @@
1
+ """Adjustments widget for sliders and controls."""
2
+
3
+ from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel, QSlider, QGroupBox
4
+ from PyQt6.QtCore import Qt, pyqtSignal
5
+
6
+
7
+ class AdjustmentsWidget(QWidget):
8
+ """Widget for adjustment controls."""
9
+
10
+ annotation_size_changed = pyqtSignal(int)
11
+ pan_speed_changed = pyqtSignal(int)
12
+ join_threshold_changed = pyqtSignal(int)
13
+
14
+ def __init__(self, parent=None):
15
+ super().__init__(parent)
16
+ self._setup_ui()
17
+ self._connect_signals()
18
+
19
+ def _setup_ui(self):
20
+ """Setup the UI layout."""
21
+ group = QGroupBox("Adjustments")
22
+ layout = QVBoxLayout(group)
23
+
24
+ # Annotation size
25
+ self.size_label = QLabel("Annotation Size: 1.0x")
26
+ self.size_slider = QSlider(Qt.Orientation.Horizontal)
27
+ self.size_slider.setRange(1, 50)
28
+ self.size_slider.setValue(10)
29
+ self.size_slider.setToolTip("Adjusts the size of points and lines (Ctrl +/-)")
30
+ layout.addWidget(self.size_label)
31
+ layout.addWidget(self.size_slider)
32
+
33
+ layout.addSpacing(10)
34
+
35
+ # Pan speed
36
+ self.pan_label = QLabel("Pan Speed: 1.0x")
37
+ self.pan_slider = QSlider(Qt.Orientation.Horizontal)
38
+ self.pan_slider.setRange(1, 100)
39
+ self.pan_slider.setValue(10)
40
+ self.pan_slider.setToolTip(
41
+ "Adjusts the speed of WASD panning. Hold Shift for 5x boost."
42
+ )
43
+ layout.addWidget(self.pan_label)
44
+ layout.addWidget(self.pan_slider)
45
+
46
+ layout.addSpacing(10)
47
+
48
+ # Polygon join threshold
49
+ self.join_label = QLabel("Polygon Join Distance: 2px")
50
+ self.join_slider = QSlider(Qt.Orientation.Horizontal)
51
+ self.join_slider.setRange(1, 10)
52
+ self.join_slider.setValue(2)
53
+ self.join_slider.setToolTip("The pixel distance to 'snap' a polygon closed.")
54
+ layout.addWidget(self.join_label)
55
+ layout.addWidget(self.join_slider)
56
+
57
+ # Main layout
58
+ main_layout = QVBoxLayout(self)
59
+ main_layout.setContentsMargins(0, 0, 0, 0)
60
+ main_layout.addWidget(group)
61
+
62
+ def _connect_signals(self):
63
+ """Connect internal signals."""
64
+ self.size_slider.valueChanged.connect(self._on_size_changed)
65
+ self.pan_slider.valueChanged.connect(self._on_pan_changed)
66
+ self.join_slider.valueChanged.connect(self._on_join_changed)
67
+
68
+ def _on_size_changed(self, value):
69
+ """Handle annotation size change."""
70
+ multiplier = value / 10.0
71
+ self.size_label.setText(f"Annotation Size: {multiplier:.1f}x")
72
+ self.annotation_size_changed.emit(value)
73
+
74
+ def _on_pan_changed(self, value):
75
+ """Handle pan speed change."""
76
+ multiplier = value / 10.0
77
+ self.pan_label.setText(f"Pan Speed: {multiplier:.1f}x")
78
+ self.pan_speed_changed.emit(value)
79
+
80
+ def _on_join_changed(self, value):
81
+ """Handle join threshold change."""
82
+ self.join_label.setText(f"Polygon Join Distance: {value}px")
83
+ self.join_threshold_changed.emit(value)
84
+
85
+ def get_annotation_size(self):
86
+ """Get current annotation size value."""
87
+ return self.size_slider.value()
88
+
89
+ def set_annotation_size(self, value):
90
+ """Set annotation size value."""
91
+ self.size_slider.setValue(value)
92
+
93
+ def get_pan_speed(self):
94
+ """Get current pan speed value."""
95
+ return self.pan_slider.value()
96
+
97
+ def set_pan_speed(self, value):
98
+ """Set pan speed value."""
99
+ self.pan_slider.setValue(value)
100
+
101
+ def get_join_threshold(self):
102
+ """Get current join threshold value."""
103
+ return self.join_slider.value()
104
+
105
+ def set_join_threshold(self, value):
106
+ """Set join threshold value."""
107
+ self.join_slider.setValue(value)
@@ -0,0 +1,94 @@
1
+ """Model selection widget."""
2
+
3
+ from PyQt6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QComboBox, QGroupBox
4
+ from PyQt6.QtCore import pyqtSignal
5
+ from typing import List, Tuple
6
+
7
+
8
+ class ModelSelectionWidget(QWidget):
9
+ """Widget for model selection and management."""
10
+
11
+ browse_requested = pyqtSignal()
12
+ refresh_requested = pyqtSignal()
13
+ model_selected = pyqtSignal(str)
14
+
15
+ def __init__(self, parent=None):
16
+ super().__init__(parent)
17
+ self._setup_ui()
18
+ self._connect_signals()
19
+
20
+ def _setup_ui(self):
21
+ """Setup the UI layout."""
22
+ group = QGroupBox("Model Selection")
23
+ layout = QVBoxLayout(group)
24
+
25
+ # Buttons
26
+ button_layout = QHBoxLayout()
27
+ self.btn_browse = QPushButton("Browse Models")
28
+ self.btn_browse.setToolTip("Browse for a folder containing .pth model files")
29
+ self.btn_refresh = QPushButton("Refresh")
30
+ self.btn_refresh.setToolTip("Refresh the list of available models")
31
+
32
+ button_layout.addWidget(self.btn_browse)
33
+ button_layout.addWidget(self.btn_refresh)
34
+ layout.addLayout(button_layout)
35
+
36
+ # Model combo
37
+ layout.addWidget(QLabel("Available Models:"))
38
+ self.model_combo = QComboBox()
39
+ self.model_combo.setToolTip("Select a .pth model file to use")
40
+ self.model_combo.addItem("Default (vit_h)")
41
+ layout.addWidget(self.model_combo)
42
+
43
+ # Current model label
44
+ self.current_model_label = QLabel("Current: Default SAM Model")
45
+ self.current_model_label.setWordWrap(True)
46
+ self.current_model_label.setStyleSheet("color: #90EE90; font-style: italic;")
47
+ layout.addWidget(self.current_model_label)
48
+
49
+ # Main layout
50
+ main_layout = QVBoxLayout(self)
51
+ main_layout.setContentsMargins(0, 0, 0, 0)
52
+ main_layout.addWidget(group)
53
+
54
+ def _connect_signals(self):
55
+ """Connect internal signals."""
56
+ self.btn_browse.clicked.connect(self.browse_requested)
57
+ self.btn_refresh.clicked.connect(self.refresh_requested)
58
+ self.model_combo.currentTextChanged.connect(self.model_selected)
59
+
60
+ def populate_models(self, models: List[Tuple[str, str]]):
61
+ """Populate the models combo box.
62
+
63
+ Args:
64
+ models: List of (display_name, full_path) tuples
65
+ """
66
+ self.model_combo.blockSignals(True)
67
+ self.model_combo.clear()
68
+
69
+ # Add default option
70
+ self.model_combo.addItem("Default (vit_h)")
71
+
72
+ # Add custom models
73
+ for display_name, full_path in models:
74
+ self.model_combo.addItem(display_name, full_path)
75
+
76
+ self.model_combo.blockSignals(False)
77
+
78
+ def set_current_model(self, model_name: str):
79
+ """Set the current model display."""
80
+ self.current_model_label.setText(model_name)
81
+
82
+ def get_selected_model_path(self) -> str:
83
+ """Get the path of the currently selected model."""
84
+ current_index = self.model_combo.currentIndex()
85
+ if current_index <= 0: # Default option
86
+ return ""
87
+ return self.model_combo.itemData(current_index) or ""
88
+
89
+ def reset_to_default(self):
90
+ """Reset selection to default model."""
91
+ self.model_combo.blockSignals(True)
92
+ self.model_combo.setCurrentIndex(0)
93
+ self.model_combo.blockSignals(False)
94
+ self.set_current_model("Current: Default SAM Model")
@@ -0,0 +1,106 @@
1
+ """Settings widget for save options."""
2
+
3
+ from PyQt6.QtWidgets import QWidget, QVBoxLayout, QCheckBox, QGroupBox
4
+ from PyQt6.QtCore import pyqtSignal
5
+
6
+
7
+ class SettingsWidget(QWidget):
8
+ """Widget for application settings."""
9
+
10
+ settings_changed = pyqtSignal()
11
+
12
+ def __init__(self, parent=None):
13
+ super().__init__(parent)
14
+ self._setup_ui()
15
+ self._connect_signals()
16
+
17
+ def _setup_ui(self):
18
+ """Setup the UI layout."""
19
+ group = QGroupBox("Settings")
20
+ layout = QVBoxLayout(group)
21
+
22
+ # Auto-save
23
+ self.chk_auto_save = QCheckBox("Auto-Save on Navigate")
24
+ self.chk_auto_save.setToolTip(
25
+ "Automatically save work when using arrow keys to change images."
26
+ )
27
+ self.chk_auto_save.setChecked(True)
28
+ layout.addWidget(self.chk_auto_save)
29
+
30
+ # Save NPZ
31
+ self.chk_save_npz = QCheckBox("Save .npz")
32
+ self.chk_save_npz.setChecked(True)
33
+ self.chk_save_npz.setToolTip(
34
+ "Save the final mask as a compressed NumPy NPZ file."
35
+ )
36
+ layout.addWidget(self.chk_save_npz)
37
+
38
+ # Save TXT
39
+ self.chk_save_txt = QCheckBox("Save .txt")
40
+ self.chk_save_txt.setChecked(True)
41
+ self.chk_save_txt.setToolTip(
42
+ "Save bounding box annotations in YOLO TXT format."
43
+ )
44
+ layout.addWidget(self.chk_save_txt)
45
+
46
+ # YOLO with aliases
47
+ self.chk_yolo_use_alias = QCheckBox("Save YOLO with Class Aliases")
48
+ self.chk_yolo_use_alias.setToolTip(
49
+ "If checked, saves YOLO .txt files using class alias names instead of numeric IDs.\n"
50
+ "This is useful when a separate .yaml or .names file defines the classes."
51
+ )
52
+ self.chk_yolo_use_alias.setChecked(True)
53
+ layout.addWidget(self.chk_yolo_use_alias)
54
+
55
+ # Save class aliases
56
+ self.chk_save_class_aliases = QCheckBox("Save Class Aliases (.json)")
57
+ self.chk_save_class_aliases.setToolTip(
58
+ "Save class aliases to a companion JSON file."
59
+ )
60
+ self.chk_save_class_aliases.setChecked(False)
61
+ layout.addWidget(self.chk_save_class_aliases)
62
+
63
+ # Main layout
64
+ main_layout = QVBoxLayout(self)
65
+ main_layout.setContentsMargins(0, 0, 0, 0)
66
+ main_layout.addWidget(group)
67
+
68
+ def _connect_signals(self):
69
+ """Connect internal signals."""
70
+ self.chk_save_npz.stateChanged.connect(self._handle_save_checkbox_change)
71
+ self.chk_save_txt.stateChanged.connect(self._handle_save_checkbox_change)
72
+
73
+ # Connect all checkboxes to settings changed signal
74
+ for checkbox in [self.chk_auto_save, self.chk_save_npz, self.chk_save_txt,
75
+ self.chk_yolo_use_alias, self.chk_save_class_aliases]:
76
+ checkbox.stateChanged.connect(self.settings_changed)
77
+
78
+ def _handle_save_checkbox_change(self):
79
+ """Ensure at least one save format is selected."""
80
+ is_npz_checked = self.chk_save_npz.isChecked()
81
+ is_txt_checked = self.chk_save_txt.isChecked()
82
+
83
+ if not is_npz_checked and not is_txt_checked:
84
+ sender = self.sender()
85
+ if sender == self.chk_save_npz:
86
+ self.chk_save_txt.setChecked(True)
87
+ else:
88
+ self.chk_save_npz.setChecked(True)
89
+
90
+ def get_settings(self):
91
+ """Get current settings as dictionary."""
92
+ return {
93
+ 'auto_save': self.chk_auto_save.isChecked(),
94
+ 'save_npz': self.chk_save_npz.isChecked(),
95
+ 'save_txt': self.chk_save_txt.isChecked(),
96
+ 'yolo_use_alias': self.chk_yolo_use_alias.isChecked(),
97
+ 'save_class_aliases': self.chk_save_class_aliases.isChecked(),
98
+ }
99
+
100
+ def set_settings(self, settings):
101
+ """Set settings from dictionary."""
102
+ self.chk_auto_save.setChecked(settings.get('auto_save', True))
103
+ self.chk_save_npz.setChecked(settings.get('save_npz', True))
104
+ self.chk_save_txt.setChecked(settings.get('save_txt', True))
105
+ self.chk_yolo_use_alias.setChecked(settings.get('yolo_use_alias', True))
106
+ self.chk_save_class_aliases.setChecked(settings.get('save_class_aliases', False))
@@ -0,0 +1,6 @@
1
+ """Utility modules."""
2
+
3
+ from .utils import mask_to_pixmap
4
+ from .custom_file_system_model import CustomFileSystemModel
5
+
6
+ __all__ = ['mask_to_pixmap', 'CustomFileSystemModel']