lazylabel-gui 1.2.1__py3-none-any.whl → 1.3.1__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/config/settings.py +3 -0
- lazylabel/main.py +1 -0
- lazylabel/models/sam2_model.py +166 -18
- lazylabel/ui/control_panel.py +17 -15
- lazylabel/ui/editable_vertex.py +72 -0
- lazylabel/ui/hoverable_pixelmap_item.py +25 -0
- lazylabel/ui/hoverable_polygon_item.py +26 -0
- lazylabel/ui/main_window.py +5632 -232
- lazylabel/ui/modes/__init__.py +6 -0
- lazylabel/ui/modes/base_mode.py +52 -0
- lazylabel/ui/modes/multi_view_mode.py +1173 -0
- lazylabel/ui/modes/single_view_mode.py +299 -0
- lazylabel/ui/photo_viewer.py +31 -3
- lazylabel/ui/test_hover.py +48 -0
- lazylabel/ui/widgets/adjustments_widget.py +2 -2
- lazylabel/ui/widgets/border_crop_widget.py +11 -0
- lazylabel/ui/widgets/channel_threshold_widget.py +50 -6
- lazylabel/ui/widgets/fft_threshold_widget.py +116 -22
- lazylabel/ui/widgets/model_selection_widget.py +117 -4
- {lazylabel_gui-1.2.1.dist-info → lazylabel_gui-1.3.1.dist-info}/METADATA +194 -200
- {lazylabel_gui-1.2.1.dist-info → lazylabel_gui-1.3.1.dist-info}/RECORD +25 -20
- {lazylabel_gui-1.2.1.dist-info → lazylabel_gui-1.3.1.dist-info}/WHEEL +0 -0
- {lazylabel_gui-1.2.1.dist-info → lazylabel_gui-1.3.1.dist-info}/entry_points.txt +0 -0
- {lazylabel_gui-1.2.1.dist-info → lazylabel_gui-1.3.1.dist-info}/licenses/LICENSE +0 -0
- {lazylabel_gui-1.2.1.dist-info → lazylabel_gui-1.3.1.dist-info}/top_level.txt +0 -0
@@ -24,6 +24,100 @@ from .channel_threshold_widget import MultiIndicatorSlider
|
|
24
24
|
class FFTThresholdSlider(MultiIndicatorSlider):
|
25
25
|
"""Custom slider for FFT thresholds that allows removing all indicators."""
|
26
26
|
|
27
|
+
def paintEvent(self, event):
|
28
|
+
"""Paint the slider with appropriate labels."""
|
29
|
+
# Call parent paint method but skip its label drawing
|
30
|
+
from PyQt6.QtCore import QRect, Qt
|
31
|
+
from PyQt6.QtGui import QBrush, QColor, QFont, QPainter, QPen
|
32
|
+
|
33
|
+
painter = QPainter(self)
|
34
|
+
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
35
|
+
|
36
|
+
# Draw channel label
|
37
|
+
font = QFont()
|
38
|
+
font.setPointSize(9)
|
39
|
+
painter.setFont(font)
|
40
|
+
painter.setPen(QPen(QColor(255, 255, 255)))
|
41
|
+
painter.drawText(5, 15, f"{self.channel_name}")
|
42
|
+
|
43
|
+
# Draw slider track
|
44
|
+
slider_rect = self.get_slider_rect()
|
45
|
+
painter.setPen(QPen(QColor(100, 100, 100), 2))
|
46
|
+
painter.setBrush(QBrush(QColor(50, 50, 50)))
|
47
|
+
painter.drawRoundedRect(slider_rect, 5, 5)
|
48
|
+
|
49
|
+
# Draw value segments (copied from parent)
|
50
|
+
channel_color = self.get_channel_color()
|
51
|
+
sorted_indicators = sorted(self.indicators)
|
52
|
+
|
53
|
+
# Handle case with no indicators - draw single segment
|
54
|
+
if not sorted_indicators:
|
55
|
+
segment_rect = QRect(
|
56
|
+
slider_rect.left(),
|
57
|
+
slider_rect.top(),
|
58
|
+
slider_rect.width(),
|
59
|
+
slider_rect.height(),
|
60
|
+
)
|
61
|
+
segment_color = QColor(channel_color)
|
62
|
+
segment_color.setAlpha(50)
|
63
|
+
painter.setBrush(QBrush(segment_color))
|
64
|
+
painter.setPen(QPen(Qt.GlobalColor.transparent))
|
65
|
+
painter.drawRoundedRect(segment_rect, 5, 5)
|
66
|
+
else:
|
67
|
+
# Draw segments between indicators
|
68
|
+
for i in range(len(sorted_indicators) + 1):
|
69
|
+
start_val = self.minimum if i == 0 else sorted_indicators[i - 1]
|
70
|
+
end_val = (
|
71
|
+
self.maximum
|
72
|
+
if i == len(sorted_indicators)
|
73
|
+
else sorted_indicators[i]
|
74
|
+
)
|
75
|
+
|
76
|
+
start_x = self.value_to_x(start_val)
|
77
|
+
end_x = self.value_to_x(end_val)
|
78
|
+
|
79
|
+
segment_value = (
|
80
|
+
i / len(sorted_indicators) if len(sorted_indicators) > 0 else 0
|
81
|
+
)
|
82
|
+
alpha = int(50 + segment_value * 150)
|
83
|
+
|
84
|
+
segment_color = QColor(channel_color)
|
85
|
+
segment_color.setAlpha(alpha)
|
86
|
+
|
87
|
+
segment_rect = QRect(
|
88
|
+
start_x, slider_rect.top(), end_x - start_x, slider_rect.height()
|
89
|
+
)
|
90
|
+
painter.setBrush(QBrush(segment_color))
|
91
|
+
painter.setPen(QPen(Qt.GlobalColor.transparent))
|
92
|
+
painter.drawRoundedRect(segment_rect, 5, 5)
|
93
|
+
|
94
|
+
# Draw indicators without labels
|
95
|
+
for i, value in enumerate(self.indicators):
|
96
|
+
x = self.value_to_x(value)
|
97
|
+
handle_rect = QRect(
|
98
|
+
x - 6, slider_rect.top() - 3, 12, slider_rect.height() + 6
|
99
|
+
)
|
100
|
+
|
101
|
+
if i == self.dragging_index:
|
102
|
+
painter.setBrush(QBrush(QColor(255, 255, 100)))
|
103
|
+
painter.setPen(QPen(QColor(200, 200, 50), 2))
|
104
|
+
else:
|
105
|
+
painter.setBrush(QBrush(QColor(255, 255, 255)))
|
106
|
+
painter.setPen(QPen(QColor(150, 150, 150), 1))
|
107
|
+
|
108
|
+
painter.drawRoundedRect(handle_rect, 3, 3)
|
109
|
+
|
110
|
+
# Now draw our custom labels
|
111
|
+
painter.setPen(QPen(QColor(255, 255, 255)))
|
112
|
+
for value in self.indicators:
|
113
|
+
x = self.value_to_x(value)
|
114
|
+
if self.maximum == 10000: # Frequency slider (percentage)
|
115
|
+
percentage = round(value / 100.0)
|
116
|
+
label = f"{percentage}%"
|
117
|
+
else: # Intensity slider (integer pixel values)
|
118
|
+
label = f"{int(value)}"
|
119
|
+
painter.drawText(x - 15, slider_rect.bottom() + 15, label)
|
120
|
+
|
27
121
|
def contextMenuEvent(self, event):
|
28
122
|
"""Handle right-click to remove indicator (allows removing all indicators)."""
|
29
123
|
from PyQt6.QtCore import QRect
|
@@ -58,7 +152,7 @@ class FFTThresholdWidget(QWidget):
|
|
58
152
|
0 # 0 = no image, 1 = grayscale, 3+ = not supported
|
59
153
|
)
|
60
154
|
self.frequency_thresholds = [] # List of frequency threshold percentages (0-100)
|
61
|
-
self.intensity_thresholds = [] # List of intensity threshold
|
155
|
+
self.intensity_thresholds = [] # List of intensity threshold pixel values (0-255)
|
62
156
|
self._setup_ui()
|
63
157
|
self._connect_signals()
|
64
158
|
|
@@ -66,7 +160,7 @@ class FFTThresholdWidget(QWidget):
|
|
66
160
|
"""Setup the UI layout."""
|
67
161
|
group = QGroupBox("FFT Frequency Band Thresholding")
|
68
162
|
layout = QVBoxLayout(group)
|
69
|
-
layout.setSpacing(
|
163
|
+
layout.setSpacing(4) # Reduce spacing between widgets
|
70
164
|
|
71
165
|
# Enable checkbox
|
72
166
|
self.enable_checkbox = QCheckBox("Enable FFT Frequency Thresholding")
|
@@ -81,45 +175,45 @@ class FFTThresholdWidget(QWidget):
|
|
81
175
|
layout.addWidget(self.status_label)
|
82
176
|
|
83
177
|
# Frequency threshold slider (percentage-based)
|
84
|
-
freq_label = QLabel("Frequency Thresholds
|
85
|
-
freq_label.setStyleSheet("font-weight: bold; margin-top:
|
178
|
+
freq_label = QLabel("Frequency Thresholds\n(Double-click to add):")
|
179
|
+
freq_label.setStyleSheet("font-weight: bold; margin-top: 2px; font-size: 10px;")
|
86
180
|
layout.addWidget(freq_label)
|
87
181
|
|
88
182
|
self.frequency_slider = FFTThresholdSlider(
|
89
|
-
channel_name="Frequency Bands", minimum=0, maximum=
|
183
|
+
channel_name="Frequency Bands", minimum=0, maximum=10000, parent=self
|
90
184
|
)
|
91
185
|
self.frequency_slider.setEnabled(False)
|
92
186
|
self.frequency_slider.setToolTip(
|
93
|
-
"Double-click to add frequency cutoff points
|
187
|
+
"Double-click to add frequency cutoff points.\nEach band gets mapped to different intensity."
|
94
188
|
)
|
95
189
|
layout.addWidget(self.frequency_slider)
|
96
190
|
|
97
191
|
# Intensity threshold slider (percentage-based)
|
98
|
-
intensity_label = QLabel("Intensity Thresholds
|
99
|
-
intensity_label.setStyleSheet(
|
192
|
+
intensity_label = QLabel("Intensity Thresholds\n(Double-click to add):")
|
193
|
+
intensity_label.setStyleSheet(
|
194
|
+
"font-weight: bold; margin-top: 5px; font-size: 10px;"
|
195
|
+
)
|
100
196
|
layout.addWidget(intensity_label)
|
101
197
|
|
102
198
|
self.intensity_slider = FFTThresholdSlider(
|
103
|
-
channel_name="Intensity Levels", minimum=0, maximum=
|
199
|
+
channel_name="Intensity Levels", minimum=0, maximum=255, parent=self
|
104
200
|
)
|
105
201
|
self.intensity_slider.setEnabled(False)
|
106
202
|
self.intensity_slider.setToolTip(
|
107
|
-
"Double-click to add intensity threshold points
|
203
|
+
"Double-click to add intensity threshold points.\nApplied after frequency band processing."
|
108
204
|
)
|
109
205
|
layout.addWidget(self.intensity_slider)
|
110
206
|
|
111
|
-
#
|
112
|
-
instructions = QLabel(
|
113
|
-
|
114
|
-
"2. Add intensity thresholds to further process the result with quantization levels."
|
115
|
-
)
|
116
|
-
instructions.setStyleSheet("color: #888; font-size: 9px;")
|
207
|
+
# Compact instructions
|
208
|
+
instructions = QLabel("Freq: low→dark, high→bright | Intensity: quantization")
|
209
|
+
instructions.setStyleSheet("color: #888; font-size: 8px; margin-top: 2px;")
|
117
210
|
instructions.setWordWrap(True)
|
118
211
|
layout.addWidget(instructions)
|
119
212
|
|
120
|
-
# Main layout
|
213
|
+
# Main layout with reduced spacing
|
121
214
|
main_layout = QVBoxLayout(self)
|
122
215
|
main_layout.setContentsMargins(0, 0, 0, 0)
|
216
|
+
main_layout.setSpacing(2) # Reduce spacing between elements
|
123
217
|
main_layout.addWidget(group)
|
124
218
|
|
125
219
|
def _connect_signals(self):
|
@@ -162,7 +256,7 @@ class FFTThresholdWidget(QWidget):
|
|
162
256
|
|
163
257
|
def _on_intensity_slider_changed(self, indicators):
|
164
258
|
"""Handle intensity threshold slider change (receives list of threshold indicators)."""
|
165
|
-
# Store the intensity threshold indicators (
|
259
|
+
# Store the intensity threshold indicators (pixel values 0-255)
|
166
260
|
self.intensity_thresholds = indicators[:] # Copy the list
|
167
261
|
self._emit_change_if_active()
|
168
262
|
|
@@ -295,8 +389,8 @@ class FFTThresholdWidget(QWidget):
|
|
295
389
|
# Create frequency bands based on thresholds
|
296
390
|
sorted_thresholds = sorted(self.frequency_thresholds)
|
297
391
|
freq_thresholds_normalized = [
|
298
|
-
t /
|
299
|
-
] # Convert to 0-1
|
392
|
+
t / 10000.0 for t in sorted_thresholds
|
393
|
+
] # Convert to 0-1 (from 0-10000 range, giving 0.01% precision)
|
300
394
|
|
301
395
|
# Number of bands = number of thresholds + 1
|
302
396
|
num_bands = len(freq_thresholds_normalized) + 1
|
@@ -347,8 +441,8 @@ class FFTThresholdWidget(QWidget):
|
|
347
441
|
if not sorted_thresholds:
|
348
442
|
return image_array
|
349
443
|
|
350
|
-
#
|
351
|
-
intensity_thresholds =
|
444
|
+
# Thresholds are already in pixel values (0-255), no conversion needed
|
445
|
+
intensity_thresholds = sorted_thresholds
|
352
446
|
|
353
447
|
# Number of levels = number of thresholds + 1
|
354
448
|
num_levels = len(intensity_thresholds) + 1
|
@@ -1,17 +1,121 @@
|
|
1
1
|
"""Model selection widget."""
|
2
2
|
|
3
|
-
from PyQt6.QtCore import pyqtSignal
|
3
|
+
from PyQt6.QtCore import Qt, pyqtSignal
|
4
4
|
from PyQt6.QtWidgets import (
|
5
|
-
QComboBox,
|
6
5
|
QGroupBox,
|
7
6
|
QHBoxLayout,
|
8
7
|
QLabel,
|
8
|
+
QMenu,
|
9
9
|
QPushButton,
|
10
|
+
QToolButton,
|
10
11
|
QVBoxLayout,
|
11
12
|
QWidget,
|
12
13
|
)
|
13
14
|
|
14
15
|
|
16
|
+
class CustomDropdown(QToolButton):
|
17
|
+
"""Custom dropdown using QToolButton + QMenu for reliable closing behavior."""
|
18
|
+
|
19
|
+
activated = pyqtSignal(int)
|
20
|
+
|
21
|
+
def __init__(self, parent=None):
|
22
|
+
super().__init__(parent)
|
23
|
+
self.setText("Default (vit_h)")
|
24
|
+
self.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
|
25
|
+
self.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextOnly)
|
26
|
+
|
27
|
+
# Create the menu
|
28
|
+
self.menu = QMenu(self)
|
29
|
+
self.setMenu(self.menu)
|
30
|
+
|
31
|
+
# Store items for access
|
32
|
+
self.items = []
|
33
|
+
|
34
|
+
# Style to match app theme (dark theme with consistent colors)
|
35
|
+
self.setStyleSheet("""
|
36
|
+
QToolButton {
|
37
|
+
background-color: rgba(40, 40, 40, 0.8);
|
38
|
+
border: 1px solid rgba(80, 80, 80, 0.6);
|
39
|
+
border-radius: 6px;
|
40
|
+
color: #E0E0E0;
|
41
|
+
font-size: 10px;
|
42
|
+
padding: 5px 8px;
|
43
|
+
text-align: left;
|
44
|
+
min-width: 150px;
|
45
|
+
}
|
46
|
+
QToolButton:hover {
|
47
|
+
background-color: rgba(60, 60, 60, 0.8);
|
48
|
+
border-color: rgba(90, 120, 150, 0.8);
|
49
|
+
}
|
50
|
+
QToolButton:pressed {
|
51
|
+
background-color: rgba(70, 100, 130, 0.8);
|
52
|
+
}
|
53
|
+
QToolButton::menu-indicator {
|
54
|
+
subcontrol-origin: padding;
|
55
|
+
subcontrol-position: top right;
|
56
|
+
width: 16px;
|
57
|
+
border-left: 1px solid rgba(80, 80, 80, 0.6);
|
58
|
+
}
|
59
|
+
""")
|
60
|
+
|
61
|
+
def addItem(self, text, data=None):
|
62
|
+
"""Add an item to the dropdown."""
|
63
|
+
action = self.menu.addAction(text)
|
64
|
+
action.setData(data)
|
65
|
+
self.items.append((text, data))
|
66
|
+
|
67
|
+
# Connect to selection handler
|
68
|
+
action.triggered.connect(
|
69
|
+
lambda checked, idx=len(self.items) - 1: self._on_item_selected(idx)
|
70
|
+
)
|
71
|
+
|
72
|
+
# Set first item as current
|
73
|
+
if len(self.items) == 1:
|
74
|
+
self.setText(text)
|
75
|
+
|
76
|
+
def clear(self):
|
77
|
+
"""Clear all items."""
|
78
|
+
self.menu.clear()
|
79
|
+
self.items.clear()
|
80
|
+
|
81
|
+
def _on_item_selected(self, index):
|
82
|
+
"""Handle item selection."""
|
83
|
+
if 0 <= index < len(self.items):
|
84
|
+
text, data = self.items[index]
|
85
|
+
self.setText(text)
|
86
|
+
self.activated.emit(index)
|
87
|
+
|
88
|
+
def itemText(self, index):
|
89
|
+
"""Get text of item at index."""
|
90
|
+
if 0 <= index < len(self.items):
|
91
|
+
return self.items[index][0]
|
92
|
+
return ""
|
93
|
+
|
94
|
+
def itemData(self, index):
|
95
|
+
"""Get data of item at index."""
|
96
|
+
if 0 <= index < len(self.items):
|
97
|
+
return self.items[index][1]
|
98
|
+
return None
|
99
|
+
|
100
|
+
def currentIndex(self):
|
101
|
+
"""Get current selected index."""
|
102
|
+
current_text = self.text()
|
103
|
+
for i, (text, _) in enumerate(self.items):
|
104
|
+
if text == current_text:
|
105
|
+
return i
|
106
|
+
return 0
|
107
|
+
|
108
|
+
def setCurrentIndex(self, index):
|
109
|
+
"""Set current selected index."""
|
110
|
+
if 0 <= index < len(self.items):
|
111
|
+
text, _ = self.items[index]
|
112
|
+
self.setText(text)
|
113
|
+
|
114
|
+
def blockSignals(self, block):
|
115
|
+
"""Block/unblock signals."""
|
116
|
+
super().blockSignals(block)
|
117
|
+
|
118
|
+
|
15
119
|
class ModelSelectionWidget(QWidget):
|
16
120
|
"""Widget for model selection and management."""
|
17
121
|
|
@@ -47,7 +151,7 @@ class ModelSelectionWidget(QWidget):
|
|
47
151
|
|
48
152
|
# Model combo
|
49
153
|
layout.addWidget(QLabel("Available Models:"))
|
50
|
-
self.model_combo =
|
154
|
+
self.model_combo = CustomDropdown()
|
51
155
|
self.model_combo.setToolTip("Select a .pth model file to use")
|
52
156
|
self.model_combo.addItem("Default (vit_h)")
|
53
157
|
layout.addWidget(self.model_combo)
|
@@ -67,7 +171,16 @@ class ModelSelectionWidget(QWidget):
|
|
67
171
|
"""Connect internal signals."""
|
68
172
|
self.btn_browse.clicked.connect(self.browse_requested)
|
69
173
|
self.btn_refresh.clicked.connect(self.refresh_requested)
|
70
|
-
|
174
|
+
# Use activated signal which fires when user actually selects an item
|
175
|
+
self.model_combo.activated.connect(self._on_model_activated)
|
176
|
+
|
177
|
+
def _on_model_activated(self, index):
|
178
|
+
"""Handle model selection when user clicks on an item."""
|
179
|
+
# Get the selected text
|
180
|
+
selected_text = self.model_combo.itemText(index)
|
181
|
+
|
182
|
+
# Emit the signal immediately
|
183
|
+
self.model_selected.emit(selected_text)
|
71
184
|
|
72
185
|
def populate_models(self, models: list[tuple[str, str]]):
|
73
186
|
"""Populate the models combo box.
|