lazylabel-gui 1.1.7__py3-none-any.whl → 1.1.8__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.
- lazylabel/core/file_manager.py +44 -1
- lazylabel/core/model_manager.py +12 -0
- lazylabel/ui/control_panel.py +676 -98
- lazylabel/ui/main_window.py +995 -8
- lazylabel/ui/widgets/__init__.py +13 -2
- lazylabel/ui/widgets/adjustments_widget.py +23 -40
- lazylabel/ui/widgets/border_crop_widget.py +210 -0
- lazylabel/ui/widgets/channel_threshold_widget.py +500 -0
- lazylabel/ui/widgets/fragment_threshold_widget.py +97 -0
- lazylabel/ui/widgets/model_selection_widget.py +26 -0
- {lazylabel_gui-1.1.7.dist-info → lazylabel_gui-1.1.8.dist-info}/METADATA +1 -1
- {lazylabel_gui-1.1.7.dist-info → lazylabel_gui-1.1.8.dist-info}/RECORD +16 -13
- {lazylabel_gui-1.1.7.dist-info → lazylabel_gui-1.1.8.dist-info}/WHEEL +0 -0
- {lazylabel_gui-1.1.7.dist-info → lazylabel_gui-1.1.8.dist-info}/entry_points.txt +0 -0
- {lazylabel_gui-1.1.7.dist-info → lazylabel_gui-1.1.8.dist-info}/licenses/LICENSE +0 -0
- {lazylabel_gui-1.1.7.dist-info → lazylabel_gui-1.1.8.dist-info}/top_level.txt +0 -0
lazylabel/ui/control_panel.py
CHANGED
@@ -6,11 +6,155 @@ from PyQt6.QtWidgets import (
|
|
6
6
|
QHBoxLayout,
|
7
7
|
QLabel,
|
8
8
|
QPushButton,
|
9
|
+
QScrollArea,
|
10
|
+
QTabWidget,
|
9
11
|
QVBoxLayout,
|
10
12
|
QWidget,
|
11
13
|
)
|
12
14
|
|
13
|
-
from .widgets import
|
15
|
+
from .widgets import (
|
16
|
+
AdjustmentsWidget,
|
17
|
+
BorderCropWidget,
|
18
|
+
ChannelThresholdWidget,
|
19
|
+
FragmentThresholdWidget,
|
20
|
+
ModelSelectionWidget,
|
21
|
+
SettingsWidget,
|
22
|
+
)
|
23
|
+
|
24
|
+
|
25
|
+
class SimpleCollapsible(QWidget):
|
26
|
+
"""A simple collapsible widget for use within tabs."""
|
27
|
+
|
28
|
+
def __init__(self, title: str, content_widget: QWidget, parent=None):
|
29
|
+
super().__init__(parent)
|
30
|
+
self.content_widget = content_widget
|
31
|
+
self.is_collapsed = False
|
32
|
+
|
33
|
+
layout = QVBoxLayout(self)
|
34
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
35
|
+
layout.setSpacing(2)
|
36
|
+
|
37
|
+
# Header with toggle button
|
38
|
+
header_layout = QHBoxLayout()
|
39
|
+
header_layout.setContentsMargins(2, 2, 2, 2)
|
40
|
+
|
41
|
+
self.toggle_button = QPushButton("▼")
|
42
|
+
self.toggle_button.setMaximumWidth(16)
|
43
|
+
self.toggle_button.setMaximumHeight(16)
|
44
|
+
self.toggle_button.setStyleSheet(
|
45
|
+
"""
|
46
|
+
QPushButton {
|
47
|
+
border: 1px solid rgba(120, 120, 120, 0.5);
|
48
|
+
background: rgba(70, 70, 70, 0.6);
|
49
|
+
color: #E0E0E0;
|
50
|
+
font-size: 10px;
|
51
|
+
font-weight: bold;
|
52
|
+
border-radius: 2px;
|
53
|
+
}
|
54
|
+
QPushButton:hover {
|
55
|
+
background: rgba(90, 90, 90, 0.8);
|
56
|
+
border: 1px solid rgba(140, 140, 140, 0.8);
|
57
|
+
color: #FFF;
|
58
|
+
}
|
59
|
+
QPushButton:pressed {
|
60
|
+
background: rgba(50, 50, 50, 0.8);
|
61
|
+
border: 1px solid rgba(100, 100, 100, 0.6);
|
62
|
+
}
|
63
|
+
"""
|
64
|
+
)
|
65
|
+
self.toggle_button.clicked.connect(self.toggle_collapse)
|
66
|
+
|
67
|
+
title_label = QLabel(title)
|
68
|
+
title_label.setStyleSheet(
|
69
|
+
"""
|
70
|
+
QLabel {
|
71
|
+
color: #E0E0E0;
|
72
|
+
font-weight: bold;
|
73
|
+
font-size: 11px;
|
74
|
+
background: transparent;
|
75
|
+
border: none;
|
76
|
+
padding: 2px;
|
77
|
+
}
|
78
|
+
"""
|
79
|
+
)
|
80
|
+
|
81
|
+
header_layout.addWidget(self.toggle_button)
|
82
|
+
header_layout.addWidget(title_label)
|
83
|
+
header_layout.addStretch()
|
84
|
+
|
85
|
+
self.header_widget = QWidget()
|
86
|
+
self.header_widget.setLayout(header_layout)
|
87
|
+
self.header_widget.setStyleSheet(
|
88
|
+
"""
|
89
|
+
QWidget {
|
90
|
+
background-color: rgba(60, 60, 60, 0.3);
|
91
|
+
border-radius: 3px;
|
92
|
+
border: 1px solid rgba(80, 80, 80, 0.4);
|
93
|
+
}
|
94
|
+
"""
|
95
|
+
)
|
96
|
+
self.header_widget.setFixedHeight(20)
|
97
|
+
|
98
|
+
layout.addWidget(self.header_widget)
|
99
|
+
layout.addWidget(content_widget)
|
100
|
+
|
101
|
+
# Add some spacing below content
|
102
|
+
layout.addSpacing(4)
|
103
|
+
|
104
|
+
def toggle_collapse(self):
|
105
|
+
"""Toggle the collapsed state."""
|
106
|
+
self.is_collapsed = not self.is_collapsed
|
107
|
+
self.content_widget.setVisible(not self.is_collapsed)
|
108
|
+
self.toggle_button.setText("▶" if self.is_collapsed else "▼")
|
109
|
+
|
110
|
+
|
111
|
+
class ProfessionalCard(QFrame):
|
112
|
+
"""A professional-looking card widget for containing controls."""
|
113
|
+
|
114
|
+
def __init__(self, title: str = "", parent=None):
|
115
|
+
super().__init__(parent)
|
116
|
+
self.setFrameStyle(QFrame.Shape.Box)
|
117
|
+
self.setStyleSheet(
|
118
|
+
"""
|
119
|
+
QFrame {
|
120
|
+
background-color: rgba(40, 40, 40, 0.8);
|
121
|
+
border: 1px solid rgba(80, 80, 80, 0.6);
|
122
|
+
border-radius: 8px;
|
123
|
+
margin: 2px;
|
124
|
+
}
|
125
|
+
"""
|
126
|
+
)
|
127
|
+
|
128
|
+
layout = QVBoxLayout(self)
|
129
|
+
layout.setContentsMargins(8, 8, 8, 8)
|
130
|
+
layout.setSpacing(6)
|
131
|
+
|
132
|
+
if title:
|
133
|
+
title_label = QLabel(title)
|
134
|
+
title_label.setStyleSheet(
|
135
|
+
"""
|
136
|
+
QLabel {
|
137
|
+
color: #E0E0E0;
|
138
|
+
font-weight: bold;
|
139
|
+
font-size: 11px;
|
140
|
+
border: none;
|
141
|
+
background: transparent;
|
142
|
+
padding: 0px;
|
143
|
+
margin-bottom: 4px;
|
144
|
+
}
|
145
|
+
"""
|
146
|
+
)
|
147
|
+
layout.addWidget(title_label)
|
148
|
+
|
149
|
+
self.content_layout = layout
|
150
|
+
|
151
|
+
def addWidget(self, widget):
|
152
|
+
"""Add a widget to the card."""
|
153
|
+
self.content_layout.addWidget(widget)
|
154
|
+
|
155
|
+
def addLayout(self, layout):
|
156
|
+
"""Add a layout to the card."""
|
157
|
+
self.content_layout.addLayout(layout)
|
14
158
|
|
15
159
|
|
16
160
|
class ControlPanel(QWidget):
|
@@ -19,8 +163,9 @@ class ControlPanel(QWidget):
|
|
19
163
|
# Signals
|
20
164
|
sam_mode_requested = pyqtSignal()
|
21
165
|
polygon_mode_requested = pyqtSignal()
|
22
|
-
bbox_mode_requested = pyqtSignal()
|
166
|
+
bbox_mode_requested = pyqtSignal()
|
23
167
|
selection_mode_requested = pyqtSignal()
|
168
|
+
edit_mode_requested = pyqtSignal()
|
24
169
|
clear_points_requested = pyqtSignal()
|
25
170
|
fit_view_requested = pyqtSignal()
|
26
171
|
browse_models_requested = pyqtSignal()
|
@@ -37,133 +182,453 @@ class ControlPanel(QWidget):
|
|
37
182
|
image_adjustment_changed = pyqtSignal()
|
38
183
|
hotkeys_requested = pyqtSignal()
|
39
184
|
pop_out_requested = pyqtSignal()
|
185
|
+
settings_changed = pyqtSignal()
|
186
|
+
# Border crop signals
|
187
|
+
crop_draw_requested = pyqtSignal()
|
188
|
+
crop_clear_requested = pyqtSignal()
|
189
|
+
crop_applied = pyqtSignal(int, int, int, int) # x1, y1, x2, y2
|
190
|
+
# Channel threshold signals
|
191
|
+
channel_threshold_changed = pyqtSignal()
|
40
192
|
|
41
193
|
def __init__(self, parent=None):
|
42
194
|
super().__init__(parent)
|
43
|
-
self.setMinimumWidth(
|
44
|
-
self.preferred_width =
|
195
|
+
self.setMinimumWidth(260) # Slightly wider for better layout
|
196
|
+
self.preferred_width = 280
|
45
197
|
self._setup_ui()
|
46
198
|
self._connect_signals()
|
47
199
|
|
48
200
|
def _setup_ui(self):
|
49
|
-
"""Setup the UI layout."""
|
201
|
+
"""Setup the professional UI layout."""
|
50
202
|
layout = QVBoxLayout(self)
|
51
|
-
layout.
|
203
|
+
layout.setContentsMargins(8, 8, 8, 8)
|
204
|
+
layout.setSpacing(8)
|
205
|
+
|
206
|
+
# Create widgets first
|
207
|
+
self.model_widget = ModelSelectionWidget()
|
208
|
+
self.crop_widget = BorderCropWidget()
|
209
|
+
self.channel_threshold_widget = ChannelThresholdWidget()
|
210
|
+
self.settings_widget = SettingsWidget()
|
211
|
+
self.adjustments_widget = AdjustmentsWidget()
|
212
|
+
self.fragment_widget = FragmentThresholdWidget()
|
52
213
|
|
53
|
-
# Top button
|
54
|
-
|
55
|
-
|
214
|
+
# Top header with pop-out button
|
215
|
+
header_layout = QHBoxLayout()
|
216
|
+
header_layout.addStretch()
|
56
217
|
|
57
218
|
self.btn_popout = QPushButton("⋯")
|
58
219
|
self.btn_popout.setToolTip("Pop out panel to separate window")
|
59
220
|
self.btn_popout.setMaximumWidth(30)
|
60
|
-
|
221
|
+
self.btn_popout.setMaximumHeight(25)
|
222
|
+
self.btn_popout.setStyleSheet(
|
223
|
+
"""
|
224
|
+
QPushButton {
|
225
|
+
background-color: rgba(60, 60, 60, 0.8);
|
226
|
+
border: 1px solid rgba(80, 80, 80, 0.6);
|
227
|
+
border-radius: 4px;
|
228
|
+
color: #E0E0E0;
|
229
|
+
}
|
230
|
+
QPushButton:hover {
|
231
|
+
background-color: rgba(80, 80, 80, 0.9);
|
232
|
+
}
|
233
|
+
QPushButton:pressed {
|
234
|
+
background-color: rgba(40, 40, 40, 0.9);
|
235
|
+
}
|
236
|
+
"""
|
237
|
+
)
|
238
|
+
header_layout.addWidget(self.btn_popout)
|
239
|
+
layout.addLayout(header_layout)
|
240
|
+
|
241
|
+
# Fixed Mode Controls Section (Always Visible)
|
242
|
+
mode_card = self._create_mode_card()
|
243
|
+
layout.addWidget(mode_card)
|
244
|
+
|
245
|
+
# Tabbed Interface for Everything Else
|
246
|
+
self.tab_widget = QTabWidget()
|
247
|
+
self.tab_widget.setStyleSheet(
|
248
|
+
"""
|
249
|
+
QTabWidget::pane {
|
250
|
+
border: 1px solid rgba(80, 80, 80, 0.6);
|
251
|
+
border-radius: 6px;
|
252
|
+
background-color: rgba(35, 35, 35, 0.9);
|
253
|
+
margin-top: 2px;
|
254
|
+
}
|
255
|
+
QTabWidget::tab-bar {
|
256
|
+
alignment: center;
|
257
|
+
}
|
258
|
+
QTabBar::tab {
|
259
|
+
background-color: rgba(50, 50, 50, 0.8);
|
260
|
+
border: 1px solid rgba(70, 70, 70, 0.6);
|
261
|
+
border-bottom: none;
|
262
|
+
border-radius: 4px 4px 0 0;
|
263
|
+
padding: 4px 8px;
|
264
|
+
margin-right: 1px;
|
265
|
+
color: #B0B0B0;
|
266
|
+
font-size: 9px;
|
267
|
+
min-width: 45px;
|
268
|
+
max-width: 80px;
|
269
|
+
}
|
270
|
+
QTabBar::tab:selected {
|
271
|
+
background-color: rgba(70, 70, 70, 0.9);
|
272
|
+
color: #E0E0E0;
|
273
|
+
font-weight: bold;
|
274
|
+
}
|
275
|
+
QTabBar::tab:hover:!selected {
|
276
|
+
background-color: rgba(60, 60, 60, 0.8);
|
277
|
+
color: #D0D0D0;
|
278
|
+
}
|
279
|
+
"""
|
280
|
+
)
|
61
281
|
|
62
|
-
|
282
|
+
# AI & Settings Tab
|
283
|
+
ai_tab = self._create_ai_tab()
|
284
|
+
self.tab_widget.addTab(ai_tab, "🤖 AI")
|
63
285
|
|
64
|
-
#
|
65
|
-
|
66
|
-
|
67
|
-
main_layout.setContentsMargins(0, 0, 0, 0)
|
286
|
+
# Processing & Adjustments Tab
|
287
|
+
processing_tab = self._create_processing_tab()
|
288
|
+
self.tab_widget.addTab(processing_tab, "🛠️ Tools")
|
68
289
|
|
69
|
-
|
70
|
-
self.mode_label = QLabel("Mode: Points")
|
71
|
-
font = self.mode_label.font()
|
72
|
-
font.setPointSize(14)
|
73
|
-
font.setBold(True)
|
74
|
-
self.mode_label.setFont(font)
|
75
|
-
main_layout.addWidget(self.mode_label)
|
290
|
+
layout.addWidget(self.tab_widget, 1)
|
76
291
|
|
77
|
-
#
|
78
|
-
self.
|
292
|
+
# Status label at bottom
|
293
|
+
self.notification_label = QLabel("")
|
294
|
+
self.notification_label.setStyleSheet(
|
295
|
+
"""
|
296
|
+
QLabel {
|
297
|
+
color: #FFA500;
|
298
|
+
font-style: italic;
|
299
|
+
font-size: 9px;
|
300
|
+
background: transparent;
|
301
|
+
border: none;
|
302
|
+
padding: 4px;
|
303
|
+
}
|
304
|
+
"""
|
305
|
+
)
|
306
|
+
self.notification_label.setWordWrap(True)
|
307
|
+
layout.addWidget(self.notification_label)
|
79
308
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
main_layout.addSpacing(10)
|
309
|
+
def _create_mode_card(self):
|
310
|
+
"""Create the fixed mode controls card."""
|
311
|
+
mode_card = ProfessionalCard("Mode Controls")
|
84
312
|
|
85
|
-
#
|
86
|
-
|
87
|
-
|
313
|
+
# Mode buttons in a clean grid
|
314
|
+
buttons_layout = QVBoxLayout()
|
315
|
+
buttons_layout.setSpacing(4)
|
88
316
|
|
89
|
-
#
|
90
|
-
|
91
|
-
|
92
|
-
main_layout.addSpacing(10)
|
317
|
+
# First row: AI and Polygon
|
318
|
+
row1_layout = QHBoxLayout()
|
319
|
+
row1_layout.setSpacing(4)
|
93
320
|
|
94
|
-
|
95
|
-
|
96
|
-
|
321
|
+
self.btn_sam_mode = self._create_mode_button(
|
322
|
+
"AI", "1", "Switch to AI Mode for AI segmentation"
|
323
|
+
)
|
324
|
+
self.btn_sam_mode.setCheckable(True)
|
325
|
+
self.btn_sam_mode.setChecked(True) # Default mode
|
97
326
|
|
98
|
-
|
99
|
-
|
100
|
-
|
327
|
+
self.btn_polygon_mode = self._create_mode_button(
|
328
|
+
"Poly", "2", "Switch to Polygon Drawing Mode"
|
329
|
+
)
|
330
|
+
self.btn_polygon_mode.setCheckable(True)
|
101
331
|
|
102
|
-
|
103
|
-
self.
|
104
|
-
|
332
|
+
row1_layout.addWidget(self.btn_sam_mode)
|
333
|
+
row1_layout.addWidget(self.btn_polygon_mode)
|
334
|
+
buttons_layout.addLayout(row1_layout)
|
105
335
|
|
106
|
-
|
336
|
+
# Second row: BBox and Selection
|
337
|
+
row2_layout = QHBoxLayout()
|
338
|
+
row2_layout.setSpacing(4)
|
107
339
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
self.notification_label.setFont(font)
|
113
|
-
self.notification_label.setStyleSheet("color: #ffa500;")
|
114
|
-
self.notification_label.setWordWrap(True)
|
115
|
-
main_layout.addWidget(self.notification_label)
|
340
|
+
self.btn_bbox_mode = self._create_mode_button(
|
341
|
+
"Box", "3", "Switch to Bounding Box Drawing Mode"
|
342
|
+
)
|
343
|
+
self.btn_bbox_mode.setCheckable(True)
|
116
344
|
|
117
|
-
|
345
|
+
self.btn_selection_mode = self._create_mode_button(
|
346
|
+
"Select", "E", "Toggle segment selection"
|
347
|
+
)
|
348
|
+
self.btn_selection_mode.setCheckable(True)
|
118
349
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
self.btn_sam_mode.setToolTip("Switch to Point Mode for AI segmentation (1)")
|
350
|
+
row2_layout.addWidget(self.btn_bbox_mode)
|
351
|
+
row2_layout.addWidget(self.btn_selection_mode)
|
352
|
+
buttons_layout.addLayout(row2_layout)
|
123
353
|
|
124
|
-
|
125
|
-
self.btn_polygon_mode.setToolTip("Switch to Polygon Drawing Mode (2)")
|
354
|
+
mode_card.addLayout(buttons_layout)
|
126
355
|
|
127
|
-
|
128
|
-
|
356
|
+
# Bottom utility row: Edit and Hotkeys
|
357
|
+
utility_layout = QHBoxLayout()
|
358
|
+
utility_layout.setSpacing(4)
|
129
359
|
|
130
|
-
self.
|
131
|
-
|
360
|
+
self.btn_edit_mode = self._create_utility_button(
|
361
|
+
"Edit", "R", "Edit segments and polygons"
|
362
|
+
)
|
363
|
+
self.btn_edit_mode.setCheckable(True) # Make edit button checkable
|
364
|
+
|
365
|
+
self.btn_hotkeys = self._create_utility_button(
|
366
|
+
"⌨️ Hotkeys", "", "Configure keyboard shortcuts"
|
367
|
+
)
|
368
|
+
|
369
|
+
utility_layout.addWidget(self.btn_edit_mode)
|
370
|
+
utility_layout.addWidget(self.btn_hotkeys)
|
371
|
+
|
372
|
+
mode_card.addLayout(utility_layout)
|
373
|
+
|
374
|
+
return mode_card
|
375
|
+
|
376
|
+
def _create_mode_button(self, text, key, tooltip):
|
377
|
+
"""Create a professional mode button."""
|
378
|
+
button = QPushButton(f"{text} ({key})")
|
379
|
+
button.setToolTip(f"{tooltip} ({key})")
|
380
|
+
button.setFixedHeight(28)
|
381
|
+
button.setFixedWidth(75) # Fixed width for consistency
|
382
|
+
button.setStyleSheet(
|
383
|
+
"""
|
384
|
+
QPushButton {
|
385
|
+
background-color: rgba(60, 90, 120, 0.8);
|
386
|
+
border: 1px solid rgba(80, 110, 140, 0.8);
|
387
|
+
border-radius: 6px;
|
388
|
+
color: #E0E0E0;
|
389
|
+
font-weight: bold;
|
390
|
+
font-size: 10px;
|
391
|
+
padding: 4px 8px;
|
392
|
+
}
|
393
|
+
QPushButton:hover {
|
394
|
+
background-color: rgba(80, 110, 140, 0.9);
|
395
|
+
border-color: rgba(100, 130, 160, 0.9);
|
396
|
+
}
|
397
|
+
QPushButton:pressed {
|
398
|
+
background-color: rgba(40, 70, 100, 0.9);
|
399
|
+
}
|
400
|
+
QPushButton:checked {
|
401
|
+
background-color: rgba(120, 170, 220, 1.0);
|
402
|
+
border: 2px solid rgba(150, 200, 250, 1.0);
|
403
|
+
color: #FFFFFF;
|
404
|
+
font-weight: bold;
|
405
|
+
}
|
406
|
+
QPushButton:checked:hover {
|
407
|
+
background-color: rgba(140, 190, 240, 1.0);
|
408
|
+
border: 2px solid rgba(170, 220, 255, 1.0);
|
409
|
+
}
|
410
|
+
"""
|
411
|
+
)
|
412
|
+
return button
|
413
|
+
|
414
|
+
def _create_utility_button(self, text, key, tooltip):
|
415
|
+
"""Create a utility button with consistent styling."""
|
416
|
+
if key:
|
417
|
+
button_text = f"{text} ({key})"
|
418
|
+
tooltip_text = f"{tooltip} ({key})"
|
419
|
+
else:
|
420
|
+
button_text = text
|
421
|
+
tooltip_text = tooltip
|
422
|
+
|
423
|
+
button = QPushButton(button_text)
|
424
|
+
button.setToolTip(tooltip_text)
|
425
|
+
button.setFixedHeight(28)
|
426
|
+
button.setFixedWidth(75) # Fixed width for consistency
|
427
|
+
button.setStyleSheet(
|
428
|
+
"""
|
429
|
+
QPushButton {
|
430
|
+
background-color: rgba(70, 100, 130, 0.8);
|
431
|
+
border: 1px solid rgba(90, 120, 150, 0.8);
|
432
|
+
border-radius: 6px;
|
433
|
+
color: #E0E0E0;
|
434
|
+
font-weight: bold;
|
435
|
+
font-size: 10px;
|
436
|
+
padding: 4px 8px;
|
437
|
+
}
|
438
|
+
QPushButton:hover {
|
439
|
+
background-color: rgba(90, 120, 150, 0.9);
|
440
|
+
border-color: rgba(110, 140, 170, 0.9);
|
441
|
+
}
|
442
|
+
QPushButton:pressed {
|
443
|
+
background-color: rgba(50, 80, 110, 0.9);
|
444
|
+
}
|
445
|
+
QPushButton:checked {
|
446
|
+
background-color: rgba(120, 170, 220, 1.0);
|
447
|
+
border: 2px solid rgba(150, 200, 250, 1.0);
|
448
|
+
color: #FFFFFF;
|
449
|
+
font-weight: bold;
|
450
|
+
}
|
451
|
+
QPushButton:checked:hover {
|
452
|
+
background-color: rgba(140, 190, 240, 1.0);
|
453
|
+
border: 2px solid rgba(170, 220, 255, 1.0);
|
454
|
+
}
|
455
|
+
"""
|
456
|
+
)
|
457
|
+
return button
|
458
|
+
|
459
|
+
def _get_mode_sized_button_style(self):
|
460
|
+
"""Get styling for utility buttons that matches mode button size."""
|
461
|
+
return """
|
462
|
+
QPushButton {
|
463
|
+
background-color: rgba(80, 80, 80, 0.8);
|
464
|
+
border: 1px solid rgba(100, 100, 100, 0.6);
|
465
|
+
border-radius: 6px;
|
466
|
+
color: #E0E0E0;
|
467
|
+
font-weight: bold;
|
468
|
+
font-size: 10px;
|
469
|
+
padding: 4px 8px;
|
470
|
+
min-height: 22px;
|
471
|
+
}
|
472
|
+
QPushButton:hover {
|
473
|
+
background-color: rgba(100, 100, 100, 0.9);
|
474
|
+
border-color: rgba(120, 120, 120, 0.8);
|
475
|
+
}
|
476
|
+
QPushButton:pressed {
|
477
|
+
background-color: rgba(60, 60, 60, 0.9);
|
478
|
+
}
|
479
|
+
"""
|
480
|
+
|
481
|
+
def _get_utility_button_style(self):
|
482
|
+
"""Get styling for utility buttons."""
|
483
|
+
return """
|
484
|
+
QPushButton {
|
485
|
+
background-color: rgba(80, 80, 80, 0.8);
|
486
|
+
border: 1px solid rgba(100, 100, 100, 0.6);
|
487
|
+
border-radius: 5px;
|
488
|
+
color: #E0E0E0;
|
489
|
+
font-size: 10px;
|
490
|
+
padding: 4px 8px;
|
491
|
+
min-height: 22px;
|
492
|
+
}
|
493
|
+
QPushButton:hover {
|
494
|
+
background-color: rgba(100, 100, 100, 0.9);
|
495
|
+
}
|
496
|
+
QPushButton:pressed {
|
497
|
+
background-color: rgba(60, 60, 60, 0.9);
|
498
|
+
}
|
499
|
+
"""
|
500
|
+
|
501
|
+
def _create_ai_tab(self):
|
502
|
+
"""Create the AI & Settings tab."""
|
503
|
+
tab_widget = QWidget()
|
504
|
+
|
505
|
+
# Create scroll area for AI and settings
|
506
|
+
scroll = QScrollArea()
|
507
|
+
scroll.setWidgetResizable(True)
|
508
|
+
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
509
|
+
scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
510
|
+
scroll.setStyleSheet(
|
511
|
+
"""
|
512
|
+
QScrollArea {
|
513
|
+
border: none;
|
514
|
+
background: transparent;
|
515
|
+
}
|
516
|
+
QScrollBar:vertical {
|
517
|
+
background-color: rgba(60, 60, 60, 0.5);
|
518
|
+
width: 8px;
|
519
|
+
border-radius: 4px;
|
520
|
+
}
|
521
|
+
QScrollBar::handle:vertical {
|
522
|
+
background-color: rgba(120, 120, 120, 0.7);
|
523
|
+
border-radius: 4px;
|
524
|
+
min-height: 20px;
|
525
|
+
}
|
526
|
+
QScrollBar::handle:vertical:hover {
|
527
|
+
background-color: rgba(140, 140, 140, 0.8);
|
528
|
+
}
|
529
|
+
"""
|
530
|
+
)
|
531
|
+
|
532
|
+
scroll_content = QWidget()
|
533
|
+
layout = QVBoxLayout(scroll_content)
|
534
|
+
layout.setContentsMargins(6, 6, 6, 6)
|
535
|
+
layout.setSpacing(8)
|
132
536
|
|
133
|
-
|
134
|
-
|
135
|
-
layout.addWidget(
|
136
|
-
|
537
|
+
# AI Model Selection - collapsible
|
538
|
+
model_collapsible = SimpleCollapsible("AI Model Selection", self.model_widget)
|
539
|
+
layout.addWidget(model_collapsible)
|
540
|
+
|
541
|
+
# AI Fragment Filter - collapsible
|
542
|
+
fragment_collapsible = SimpleCollapsible(
|
543
|
+
"AI Fragment Filter", self.fragment_widget
|
544
|
+
)
|
545
|
+
layout.addWidget(fragment_collapsible)
|
546
|
+
|
547
|
+
# Application Settings - collapsible
|
548
|
+
settings_collapsible = SimpleCollapsible(
|
549
|
+
"Application Settings", self.settings_widget
|
550
|
+
)
|
551
|
+
layout.addWidget(settings_collapsible)
|
552
|
+
|
553
|
+
layout.addStretch()
|
554
|
+
|
555
|
+
scroll.setWidget(scroll_content)
|
556
|
+
|
557
|
+
tab_layout = QVBoxLayout(tab_widget)
|
558
|
+
tab_layout.setContentsMargins(0, 0, 0, 0)
|
559
|
+
tab_layout.addWidget(scroll)
|
560
|
+
|
561
|
+
return tab_widget
|
562
|
+
|
563
|
+
def _create_processing_tab(self):
|
564
|
+
"""Create the Processing & Tools tab."""
|
565
|
+
tab_widget = QWidget()
|
566
|
+
|
567
|
+
# Create scroll area for processing controls
|
568
|
+
scroll = QScrollArea()
|
569
|
+
scroll.setWidgetResizable(True)
|
570
|
+
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
571
|
+
scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
572
|
+
scroll.setStyleSheet(
|
573
|
+
"""
|
574
|
+
QScrollArea {
|
575
|
+
border: none;
|
576
|
+
background: transparent;
|
577
|
+
}
|
578
|
+
QScrollBar:vertical {
|
579
|
+
background-color: rgba(60, 60, 60, 0.5);
|
580
|
+
width: 8px;
|
581
|
+
border-radius: 4px;
|
582
|
+
}
|
583
|
+
QScrollBar::handle:vertical {
|
584
|
+
background-color: rgba(120, 120, 120, 0.7);
|
585
|
+
border-radius: 4px;
|
586
|
+
min-height: 20px;
|
587
|
+
}
|
588
|
+
QScrollBar::handle:vertical:hover {
|
589
|
+
background-color: rgba(140, 140, 140, 0.8);
|
590
|
+
}
|
591
|
+
"""
|
592
|
+
)
|
593
|
+
|
594
|
+
scroll_content = QWidget()
|
595
|
+
layout = QVBoxLayout(scroll_content)
|
596
|
+
layout.setContentsMargins(6, 6, 6, 6)
|
597
|
+
layout.setSpacing(8)
|
598
|
+
|
599
|
+
# Border Crop - collapsible
|
600
|
+
crop_collapsible = SimpleCollapsible("Border Crop", self.crop_widget)
|
601
|
+
layout.addWidget(crop_collapsible)
|
602
|
+
|
603
|
+
# Channel Threshold - collapsible
|
604
|
+
threshold_collapsible = SimpleCollapsible(
|
605
|
+
"Channel Threshold", self.channel_threshold_widget
|
606
|
+
)
|
607
|
+
layout.addWidget(threshold_collapsible)
|
137
608
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
609
|
+
# Image Adjustments - collapsible
|
610
|
+
adjustments_collapsible = SimpleCollapsible(
|
611
|
+
"Image Adjustments", self.adjustments_widget
|
612
|
+
)
|
613
|
+
layout.addWidget(adjustments_collapsible)
|
142
614
|
|
143
|
-
|
144
|
-
self.btn_clear_points.setToolTip("Clear current temporary points/vertices (C)")
|
615
|
+
layout.addStretch()
|
145
616
|
|
146
|
-
|
147
|
-
self.btn_hotkeys.setToolTip("Configure keyboard shortcuts")
|
617
|
+
scroll.setWidget(scroll_content)
|
148
618
|
|
149
|
-
|
150
|
-
|
151
|
-
|
619
|
+
tab_layout = QVBoxLayout(tab_widget)
|
620
|
+
tab_layout.setContentsMargins(0, 0, 0, 0)
|
621
|
+
tab_layout.addWidget(scroll)
|
152
622
|
|
153
|
-
|
154
|
-
"""Create a horizontal separator line."""
|
155
|
-
line = QFrame()
|
156
|
-
line.setFrameShape(QFrame.Shape.HLine)
|
157
|
-
return line
|
623
|
+
return tab_widget
|
158
624
|
|
159
625
|
def _connect_signals(self):
|
160
626
|
"""Connect internal signals."""
|
161
|
-
self.btn_sam_mode.clicked.connect(self.
|
162
|
-
self.btn_polygon_mode.clicked.connect(self.
|
163
|
-
self.btn_bbox_mode.clicked.connect(self.
|
164
|
-
self.btn_selection_mode.clicked.connect(self.
|
165
|
-
self.
|
166
|
-
self.btn_fit_view.clicked.connect(self.fit_view_requested)
|
627
|
+
self.btn_sam_mode.clicked.connect(self._on_sam_mode_clicked)
|
628
|
+
self.btn_polygon_mode.clicked.connect(self._on_polygon_mode_clicked)
|
629
|
+
self.btn_bbox_mode.clicked.connect(self._on_bbox_mode_clicked)
|
630
|
+
self.btn_selection_mode.clicked.connect(self._on_selection_mode_clicked)
|
631
|
+
self.btn_edit_mode.clicked.connect(self._on_edit_mode_clicked)
|
167
632
|
self.btn_hotkeys.clicked.connect(self.hotkeys_requested)
|
168
633
|
self.btn_popout.clicked.connect(self.pop_out_requested)
|
169
634
|
|
@@ -172,6 +637,9 @@ class ControlPanel(QWidget):
|
|
172
637
|
self.model_widget.refresh_requested.connect(self.refresh_models_requested)
|
173
638
|
self.model_widget.model_selected.connect(self.model_selected)
|
174
639
|
|
640
|
+
# Settings widget signals
|
641
|
+
self.settings_widget.settings_changed.connect(self.settings_changed)
|
642
|
+
|
175
643
|
# Adjustments widget signals
|
176
644
|
self.adjustments_widget.annotation_size_changed.connect(
|
177
645
|
self.annotation_size_changed
|
@@ -180,9 +648,6 @@ class ControlPanel(QWidget):
|
|
180
648
|
self.adjustments_widget.join_threshold_changed.connect(
|
181
649
|
self.join_threshold_changed
|
182
650
|
)
|
183
|
-
self.adjustments_widget.fragment_threshold_changed.connect(
|
184
|
-
self.fragment_threshold_changed
|
185
|
-
)
|
186
651
|
self.adjustments_widget.brightness_changed.connect(self.brightness_changed)
|
187
652
|
self.adjustments_widget.contrast_changed.connect(self.contrast_changed)
|
188
653
|
self.adjustments_widget.gamma_changed.connect(self.gamma_changed)
|
@@ -193,6 +658,63 @@ class ControlPanel(QWidget):
|
|
193
658
|
self.image_adjustment_changed
|
194
659
|
)
|
195
660
|
|
661
|
+
# Fragment threshold widget signals
|
662
|
+
self.fragment_widget.fragment_threshold_changed.connect(
|
663
|
+
self.fragment_threshold_changed
|
664
|
+
)
|
665
|
+
|
666
|
+
# Border crop signals
|
667
|
+
self.crop_widget.crop_draw_requested.connect(self.crop_draw_requested)
|
668
|
+
self.crop_widget.crop_clear_requested.connect(self.crop_clear_requested)
|
669
|
+
self.crop_widget.crop_applied.connect(self.crop_applied)
|
670
|
+
|
671
|
+
# Channel threshold signals
|
672
|
+
self.channel_threshold_widget.thresholdChanged.connect(
|
673
|
+
self.channel_threshold_changed
|
674
|
+
)
|
675
|
+
|
676
|
+
def _on_sam_mode_clicked(self):
|
677
|
+
"""Handle AI mode button click."""
|
678
|
+
self._set_active_mode_button(self.btn_sam_mode)
|
679
|
+
self.sam_mode_requested.emit()
|
680
|
+
|
681
|
+
def _on_polygon_mode_clicked(self):
|
682
|
+
"""Handle polygon mode button click."""
|
683
|
+
self._set_active_mode_button(self.btn_polygon_mode)
|
684
|
+
self.polygon_mode_requested.emit()
|
685
|
+
|
686
|
+
def _on_bbox_mode_clicked(self):
|
687
|
+
"""Handle bbox mode button click."""
|
688
|
+
self._set_active_mode_button(self.btn_bbox_mode)
|
689
|
+
self.bbox_mode_requested.emit()
|
690
|
+
|
691
|
+
def _on_selection_mode_clicked(self):
|
692
|
+
"""Handle selection mode button click."""
|
693
|
+
self._set_active_mode_button(self.btn_selection_mode)
|
694
|
+
self.selection_mode_requested.emit()
|
695
|
+
|
696
|
+
def _on_edit_mode_clicked(self):
|
697
|
+
"""Handle edit mode button click."""
|
698
|
+
# For now, emit the signal - the main window will handle polygon checking
|
699
|
+
self.edit_mode_requested.emit()
|
700
|
+
|
701
|
+
def _set_active_mode_button(self, active_button):
|
702
|
+
"""Set the active mode button and deactivate others."""
|
703
|
+
mode_buttons = [
|
704
|
+
self.btn_sam_mode,
|
705
|
+
self.btn_polygon_mode,
|
706
|
+
self.btn_bbox_mode,
|
707
|
+
self.btn_selection_mode,
|
708
|
+
]
|
709
|
+
|
710
|
+
# Clear all mode buttons
|
711
|
+
for button in mode_buttons:
|
712
|
+
button.setChecked(button == active_button if active_button else False)
|
713
|
+
|
714
|
+
# Clear edit button when setting mode buttons
|
715
|
+
if active_button and active_button != self.btn_edit_mode:
|
716
|
+
self.btn_edit_mode.setChecked(False)
|
717
|
+
|
196
718
|
def mouseDoubleClickEvent(self, event):
|
197
719
|
"""Handle double-click to expand collapsed panel."""
|
198
720
|
if (
|
@@ -213,8 +735,34 @@ class ControlPanel(QWidget):
|
|
213
735
|
self.notification_label.clear()
|
214
736
|
|
215
737
|
def set_mode_text(self, mode: str):
|
216
|
-
"""Set the mode
|
217
|
-
|
738
|
+
"""Set the active mode by highlighting the corresponding button."""
|
739
|
+
# Map internal mode names to buttons
|
740
|
+
mode_buttons = {
|
741
|
+
"sam_points": self.btn_sam_mode,
|
742
|
+
"polygon": self.btn_polygon_mode,
|
743
|
+
"bbox": self.btn_bbox_mode,
|
744
|
+
"selection": self.btn_selection_mode,
|
745
|
+
"edit": self.btn_edit_mode,
|
746
|
+
}
|
747
|
+
|
748
|
+
active_button = mode_buttons.get(mode)
|
749
|
+
if active_button:
|
750
|
+
# Clear all mode buttons first
|
751
|
+
self._set_active_mode_button(None)
|
752
|
+
# Set edit button separately if it's edit mode
|
753
|
+
if mode == "edit":
|
754
|
+
self.btn_edit_mode.setChecked(True)
|
755
|
+
else:
|
756
|
+
self._set_active_mode_button(active_button)
|
757
|
+
|
758
|
+
def set_edit_mode_active(self, active: bool):
|
759
|
+
"""Set edit mode button as active or inactive."""
|
760
|
+
if active:
|
761
|
+
# Clear all mode buttons and set edit as active
|
762
|
+
self._set_active_mode_button(None)
|
763
|
+
self.btn_edit_mode.setChecked(True)
|
764
|
+
else:
|
765
|
+
self.btn_edit_mode.setChecked(False)
|
218
766
|
|
219
767
|
# Delegate methods for sub-widgets
|
220
768
|
def populate_models(self, models):
|
@@ -251,7 +799,7 @@ class ControlPanel(QWidget):
|
|
251
799
|
|
252
800
|
def set_fragment_threshold(self, value):
|
253
801
|
"""Set fragment threshold."""
|
254
|
-
self.
|
802
|
+
self.fragment_widget.set_fragment_threshold(value)
|
255
803
|
|
256
804
|
def set_brightness(self, value):
|
257
805
|
"""Set brightness."""
|
@@ -269,9 +817,9 @@ class ControlPanel(QWidget):
|
|
269
817
|
"""Enable or disable the SAM mode button."""
|
270
818
|
self.btn_sam_mode.setEnabled(enabled)
|
271
819
|
if not enabled:
|
272
|
-
self.btn_sam_mode.setToolTip("
|
820
|
+
self.btn_sam_mode.setToolTip("AI Mode (SAM model not available)")
|
273
821
|
else:
|
274
|
-
self.btn_sam_mode.setToolTip("Switch to
|
822
|
+
self.btn_sam_mode.setToolTip("Switch to AI Mode for AI segmentation (1)")
|
275
823
|
|
276
824
|
def set_popout_mode(self, is_popped_out: bool):
|
277
825
|
"""Update the pop-out button based on panel state."""
|
@@ -281,3 +829,33 @@ class ControlPanel(QWidget):
|
|
281
829
|
else:
|
282
830
|
self.btn_popout.setText("⋯")
|
283
831
|
self.btn_popout.setToolTip("Pop out panel to separate window")
|
832
|
+
|
833
|
+
# Border crop delegate methods
|
834
|
+
def set_crop_coordinates(self, x1, y1, x2, y2):
|
835
|
+
"""Set crop coordinates in the crop widget."""
|
836
|
+
self.crop_widget.set_crop_coordinates(x1, y1, x2, y2)
|
837
|
+
|
838
|
+
def clear_crop_coordinates(self):
|
839
|
+
"""Clear crop coordinates."""
|
840
|
+
self.crop_widget.clear_crop_coordinates()
|
841
|
+
|
842
|
+
def get_crop_coordinates(self):
|
843
|
+
"""Get current crop coordinates."""
|
844
|
+
return self.crop_widget.get_crop_coordinates()
|
845
|
+
|
846
|
+
def has_crop(self):
|
847
|
+
"""Check if crop coordinates are set."""
|
848
|
+
return self.crop_widget.has_crop()
|
849
|
+
|
850
|
+
def set_crop_status(self, message):
|
851
|
+
"""Set crop status message."""
|
852
|
+
self.crop_widget.set_status(message)
|
853
|
+
|
854
|
+
# Channel threshold delegate methods
|
855
|
+
def update_channel_threshold_for_image(self, image_array):
|
856
|
+
"""Update channel threshold widget for new image."""
|
857
|
+
self.channel_threshold_widget.update_for_image(image_array)
|
858
|
+
|
859
|
+
def get_channel_threshold_widget(self):
|
860
|
+
"""Get the channel threshold widget."""
|
861
|
+
return self.channel_threshold_widget
|