lazylabel-gui 1.1.7__py3-none-any.whl → 1.1.9__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 +885 -283
- lazylabel/ui/main_window.py +3085 -2020
- lazylabel/ui/widgets/__init__.py +15 -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/fft_threshold_widget.py +392 -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.9.dist-info}/METADATA +1 -1
- {lazylabel_gui-1.1.7.dist-info → lazylabel_gui-1.1.9.dist-info}/RECORD +17 -13
- {lazylabel_gui-1.1.7.dist-info → lazylabel_gui-1.1.9.dist-info}/WHEEL +0 -0
- {lazylabel_gui-1.1.7.dist-info → lazylabel_gui-1.1.9.dist-info}/entry_points.txt +0 -0
- {lazylabel_gui-1.1.7.dist-info → lazylabel_gui-1.1.9.dist-info}/licenses/LICENSE +0 -0
- {lazylabel_gui-1.1.7.dist-info → lazylabel_gui-1.1.9.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,392 @@
|
|
1
|
+
"""
|
2
|
+
FFT Threshold Widget for LazyLabel.
|
3
|
+
|
4
|
+
This widget provides FFT-based thresholding for single channel images.
|
5
|
+
It includes frequency band thresholding and intensity thresholding.
|
6
|
+
Users can double-click to add threshold points for both frequency and intensity processing.
|
7
|
+
"""
|
8
|
+
|
9
|
+
import numpy as np
|
10
|
+
from PyQt6.QtCore import pyqtSignal
|
11
|
+
from PyQt6.QtWidgets import (
|
12
|
+
QCheckBox,
|
13
|
+
QGroupBox,
|
14
|
+
QLabel,
|
15
|
+
QVBoxLayout,
|
16
|
+
QWidget,
|
17
|
+
)
|
18
|
+
from scipy.fft import fft2, fftshift, ifft2
|
19
|
+
|
20
|
+
# Import MultiIndicatorSlider from channel threshold widget
|
21
|
+
from .channel_threshold_widget import MultiIndicatorSlider
|
22
|
+
|
23
|
+
|
24
|
+
class FFTThresholdSlider(MultiIndicatorSlider):
|
25
|
+
"""Custom slider for FFT thresholds that allows removing all indicators."""
|
26
|
+
|
27
|
+
def contextMenuEvent(self, event):
|
28
|
+
"""Handle right-click to remove indicator (allows removing all indicators)."""
|
29
|
+
from PyQt6.QtCore import QRect
|
30
|
+
|
31
|
+
slider_rect = self.get_slider_rect()
|
32
|
+
|
33
|
+
# Allow removal of any indicator (no minimum constraint)
|
34
|
+
# Check if right-clicking on an indicator
|
35
|
+
for i, value in enumerate(self.indicators):
|
36
|
+
x = self.value_to_x(value)
|
37
|
+
handle_rect = QRect(
|
38
|
+
x - 6, slider_rect.top() - 3, 12, slider_rect.height() + 6
|
39
|
+
)
|
40
|
+
|
41
|
+
if handle_rect.contains(event.pos()):
|
42
|
+
self.indicators.pop(i)
|
43
|
+
self.valueChanged.emit(self.indicators[:])
|
44
|
+
self.update()
|
45
|
+
return
|
46
|
+
|
47
|
+
|
48
|
+
class FFTThresholdWidget(QWidget):
|
49
|
+
"""Widget for FFT-based thresholding of single channel images."""
|
50
|
+
|
51
|
+
fft_threshold_changed = pyqtSignal() # Emitted when FFT threshold changes
|
52
|
+
dragStarted = pyqtSignal() # Emitted when slider drag starts
|
53
|
+
dragFinished = pyqtSignal() # Emitted when slider drag finishes
|
54
|
+
|
55
|
+
def __init__(self, parent=None):
|
56
|
+
super().__init__(parent)
|
57
|
+
self.current_image_channels = (
|
58
|
+
0 # 0 = no image, 1 = grayscale, 3+ = not supported
|
59
|
+
)
|
60
|
+
self.frequency_thresholds = [] # List of frequency threshold percentages (0-100)
|
61
|
+
self.intensity_thresholds = [] # List of intensity threshold percentages (0-100)
|
62
|
+
self._setup_ui()
|
63
|
+
self._connect_signals()
|
64
|
+
|
65
|
+
def _setup_ui(self):
|
66
|
+
"""Setup the UI layout."""
|
67
|
+
group = QGroupBox("FFT Frequency Band Thresholding")
|
68
|
+
layout = QVBoxLayout(group)
|
69
|
+
layout.setSpacing(8)
|
70
|
+
|
71
|
+
# Enable checkbox
|
72
|
+
self.enable_checkbox = QCheckBox("Enable FFT Frequency Thresholding")
|
73
|
+
self.enable_checkbox.setChecked(False)
|
74
|
+
layout.addWidget(self.enable_checkbox)
|
75
|
+
|
76
|
+
# Status label
|
77
|
+
self.status_label = QLabel("Load a single channel (grayscale) image")
|
78
|
+
self.status_label.setStyleSheet(
|
79
|
+
"color: #888; font-size: 9px; font-style: italic;"
|
80
|
+
)
|
81
|
+
layout.addWidget(self.status_label)
|
82
|
+
|
83
|
+
# Frequency threshold slider (percentage-based)
|
84
|
+
freq_label = QLabel("Frequency Thresholds (Double-click to add):")
|
85
|
+
freq_label.setStyleSheet("font-weight: bold; margin-top: 5px;")
|
86
|
+
layout.addWidget(freq_label)
|
87
|
+
|
88
|
+
self.frequency_slider = FFTThresholdSlider(
|
89
|
+
channel_name="Frequency Bands", minimum=0, maximum=100, parent=self
|
90
|
+
)
|
91
|
+
self.frequency_slider.setEnabled(False)
|
92
|
+
self.frequency_slider.setToolTip(
|
93
|
+
"Double-click to add frequency cutoff points. Each frequency band gets mapped to a different intensity level."
|
94
|
+
)
|
95
|
+
layout.addWidget(self.frequency_slider)
|
96
|
+
|
97
|
+
# Intensity threshold slider (percentage-based)
|
98
|
+
intensity_label = QLabel("Intensity Thresholds (Double-click to add):")
|
99
|
+
intensity_label.setStyleSheet("font-weight: bold; margin-top: 10px;")
|
100
|
+
layout.addWidget(intensity_label)
|
101
|
+
|
102
|
+
self.intensity_slider = FFTThresholdSlider(
|
103
|
+
channel_name="Intensity Levels", minimum=0, maximum=100, parent=self
|
104
|
+
)
|
105
|
+
self.intensity_slider.setEnabled(False)
|
106
|
+
self.intensity_slider.setToolTip(
|
107
|
+
"Double-click to add intensity threshold points. Applied after frequency band processing."
|
108
|
+
)
|
109
|
+
layout.addWidget(self.intensity_slider)
|
110
|
+
|
111
|
+
# Instructions
|
112
|
+
instructions = QLabel(
|
113
|
+
"1. Add frequency thresholds to create bands: low freq → dark, high freq → bright.\n"
|
114
|
+
"2. Add intensity thresholds to further process the result with quantization levels."
|
115
|
+
)
|
116
|
+
instructions.setStyleSheet("color: #888; font-size: 9px;")
|
117
|
+
instructions.setWordWrap(True)
|
118
|
+
layout.addWidget(instructions)
|
119
|
+
|
120
|
+
# Main layout
|
121
|
+
main_layout = QVBoxLayout(self)
|
122
|
+
main_layout.setContentsMargins(0, 0, 0, 0)
|
123
|
+
main_layout.addWidget(group)
|
124
|
+
|
125
|
+
def _connect_signals(self):
|
126
|
+
"""Connect internal signals."""
|
127
|
+
# Enable checkbox connection
|
128
|
+
self.enable_checkbox.toggled.connect(self._on_enable_checkbox_toggled)
|
129
|
+
|
130
|
+
# Frequency threshold connections
|
131
|
+
self.frequency_slider.valueChanged.connect(self._on_frequency_slider_changed)
|
132
|
+
self.frequency_slider.dragStarted.connect(self.dragStarted.emit)
|
133
|
+
self.frequency_slider.dragFinished.connect(self.dragFinished.emit)
|
134
|
+
|
135
|
+
# Intensity threshold connections
|
136
|
+
self.intensity_slider.valueChanged.connect(self._on_intensity_slider_changed)
|
137
|
+
self.intensity_slider.dragStarted.connect(self.dragStarted.emit)
|
138
|
+
self.intensity_slider.dragFinished.connect(self.dragFinished.emit)
|
139
|
+
|
140
|
+
def _on_enable_checkbox_toggled(self, checked):
|
141
|
+
"""Handle enable checkbox toggle."""
|
142
|
+
# Enable/disable controls based on checkbox state
|
143
|
+
self.frequency_slider.setEnabled(checked)
|
144
|
+
self.intensity_slider.setEnabled(checked)
|
145
|
+
|
146
|
+
# If unchecking, optionally reset the thresholds
|
147
|
+
if not checked:
|
148
|
+
self.frequency_slider.reset() # Clear frequency threshold indicators
|
149
|
+
self.intensity_slider.reset() # Clear intensity threshold indicators
|
150
|
+
self.frequency_thresholds = [] # Clear stored thresholds
|
151
|
+
self.intensity_thresholds = [] # Clear stored thresholds
|
152
|
+
|
153
|
+
# Always emit change signal when checkbox is toggled (both check and uncheck)
|
154
|
+
# This ensures the image refreshes to show/remove thresholding
|
155
|
+
self.fft_threshold_changed.emit()
|
156
|
+
|
157
|
+
def _on_frequency_slider_changed(self, indicators):
|
158
|
+
"""Handle frequency threshold slider change (receives list of threshold indicators)."""
|
159
|
+
# Store the frequency threshold indicators (percentages 0-100)
|
160
|
+
self.frequency_thresholds = indicators[:] # Copy the list
|
161
|
+
self._emit_change_if_active()
|
162
|
+
|
163
|
+
def _on_intensity_slider_changed(self, indicators):
|
164
|
+
"""Handle intensity threshold slider change (receives list of threshold indicators)."""
|
165
|
+
# Store the intensity threshold indicators (percentages 0-100)
|
166
|
+
self.intensity_thresholds = indicators[:] # Copy the list
|
167
|
+
self._emit_change_if_active()
|
168
|
+
|
169
|
+
def _emit_change_if_active(self):
|
170
|
+
"""Emit change signal if FFT processing is active."""
|
171
|
+
if self.is_active():
|
172
|
+
self.fft_threshold_changed.emit()
|
173
|
+
|
174
|
+
def update_fft_threshold_for_image(self, image_array):
|
175
|
+
"""Update widget based on loaded image."""
|
176
|
+
if image_array is None:
|
177
|
+
self.current_image_channels = 0
|
178
|
+
self.status_label.setText("Load a single channel (grayscale) image")
|
179
|
+
self.status_label.setStyleSheet(
|
180
|
+
"color: #888; font-size: 9px; font-style: italic;"
|
181
|
+
)
|
182
|
+
return
|
183
|
+
|
184
|
+
# Determine if image is grayscale (single channel or 3-channel with identical values)
|
185
|
+
if len(image_array.shape) == 2:
|
186
|
+
# True grayscale - supported
|
187
|
+
self.current_image_channels = 1
|
188
|
+
self.status_label.setText("✓ Grayscale image - FFT processing available")
|
189
|
+
self.status_label.setStyleSheet(
|
190
|
+
"color: #4CAF50; font-size: 9px; font-style: italic;"
|
191
|
+
)
|
192
|
+
elif len(image_array.shape) == 3 and image_array.shape[2] == 3:
|
193
|
+
# Check if all three channels are identical (grayscale stored as RGB)
|
194
|
+
r_channel = image_array[:, :, 0]
|
195
|
+
g_channel = image_array[:, :, 1]
|
196
|
+
b_channel = image_array[:, :, 2]
|
197
|
+
if np.array_equal(r_channel, g_channel) and np.array_equal(
|
198
|
+
g_channel, b_channel
|
199
|
+
):
|
200
|
+
# Grayscale stored as RGB - supported
|
201
|
+
self.current_image_channels = 1
|
202
|
+
self.status_label.setText(
|
203
|
+
"✓ Grayscale image (RGB format) - FFT processing available"
|
204
|
+
)
|
205
|
+
self.status_label.setStyleSheet(
|
206
|
+
"color: #4CAF50; font-size: 9px; font-style: italic;"
|
207
|
+
)
|
208
|
+
else:
|
209
|
+
# True multi-channel - not supported
|
210
|
+
self.current_image_channels = 3
|
211
|
+
self.status_label.setText(
|
212
|
+
"❌ Multi-channel color image - not supported"
|
213
|
+
)
|
214
|
+
self.status_label.setStyleSheet(
|
215
|
+
"color: #F44336; font-size: 9px; font-style: italic;"
|
216
|
+
)
|
217
|
+
else:
|
218
|
+
# Unknown format
|
219
|
+
self.current_image_channels = 0
|
220
|
+
self.status_label.setText("❌ Unsupported image format")
|
221
|
+
self.status_label.setStyleSheet(
|
222
|
+
"color: #F44336; font-size: 9px; font-style: italic;"
|
223
|
+
)
|
224
|
+
|
225
|
+
def is_active(self):
|
226
|
+
"""Check if FFT processing is active (checkbox enabled and image is grayscale)."""
|
227
|
+
return self.enable_checkbox.isChecked() and self.current_image_channels == 1
|
228
|
+
|
229
|
+
def apply_fft_thresholding(self, image_array):
|
230
|
+
"""Apply frequency band thresholding to image array and return modified array."""
|
231
|
+
if not self.is_active() or image_array is None:
|
232
|
+
return image_array
|
233
|
+
|
234
|
+
# Handle both 2D grayscale and 3D grayscale (stored as RGB) images
|
235
|
+
if len(image_array.shape) == 2:
|
236
|
+
# True grayscale
|
237
|
+
processing_image = image_array
|
238
|
+
is_3channel = False
|
239
|
+
elif len(image_array.shape) == 3 and image_array.shape[2] == 3:
|
240
|
+
# Check if it's grayscale stored as RGB
|
241
|
+
r_channel = image_array[:, :, 0]
|
242
|
+
g_channel = image_array[:, :, 1]
|
243
|
+
b_channel = image_array[:, :, 2]
|
244
|
+
if np.array_equal(r_channel, g_channel) and np.array_equal(
|
245
|
+
g_channel, b_channel
|
246
|
+
):
|
247
|
+
# Convert to 2D for processing
|
248
|
+
processing_image = image_array[:, :, 0]
|
249
|
+
is_3channel = True
|
250
|
+
else:
|
251
|
+
return image_array
|
252
|
+
else:
|
253
|
+
return image_array
|
254
|
+
|
255
|
+
try:
|
256
|
+
result_image = self._apply_frequency_band_thresholding(processing_image)
|
257
|
+
|
258
|
+
# Convert back to original format if needed
|
259
|
+
if is_3channel:
|
260
|
+
result = np.stack([result_image, result_image, result_image], axis=2)
|
261
|
+
else:
|
262
|
+
result = result_image
|
263
|
+
|
264
|
+
return result
|
265
|
+
|
266
|
+
except Exception:
|
267
|
+
# If FFT processing fails, return original image
|
268
|
+
return image_array
|
269
|
+
|
270
|
+
def _apply_frequency_band_thresholding(self, image_array):
|
271
|
+
"""Apply frequency band thresholding with multiple frequency cutoffs."""
|
272
|
+
# Convert to float for processing
|
273
|
+
image_float = image_array.astype(np.float64)
|
274
|
+
height, width = image_float.shape
|
275
|
+
|
276
|
+
# Apply FFT
|
277
|
+
fft_image = fft2(image_float)
|
278
|
+
fft_shifted = fftshift(fft_image)
|
279
|
+
|
280
|
+
# Create frequency coordinate arrays (normalized 0-1)
|
281
|
+
y_coords, x_coords = np.ogrid[:height, :width]
|
282
|
+
center_y, center_x = height // 2, width // 2
|
283
|
+
|
284
|
+
# Calculate distance from center (frequency magnitude)
|
285
|
+
max_freq = np.sqrt((height / 2) ** 2 + (width / 2) ** 2)
|
286
|
+
freq_distance = (
|
287
|
+
np.sqrt((y_coords - center_y) ** 2 + (x_coords - center_x) ** 2) / max_freq
|
288
|
+
)
|
289
|
+
freq_distance = np.clip(freq_distance, 0, 1) # Normalize to 0-1
|
290
|
+
|
291
|
+
if not self.frequency_thresholds:
|
292
|
+
# No frequency thresholds - use original FFT
|
293
|
+
result_fft = fft_shifted
|
294
|
+
else:
|
295
|
+
# Create frequency bands based on thresholds
|
296
|
+
sorted_thresholds = sorted(self.frequency_thresholds)
|
297
|
+
freq_thresholds_normalized = [
|
298
|
+
t / 100.0 for t in sorted_thresholds
|
299
|
+
] # Convert to 0-1
|
300
|
+
|
301
|
+
# Number of bands = number of thresholds + 1
|
302
|
+
num_bands = len(freq_thresholds_normalized) + 1
|
303
|
+
result_fft = np.zeros_like(fft_shifted, dtype=complex)
|
304
|
+
|
305
|
+
for band_idx in range(num_bands):
|
306
|
+
# Define frequency band
|
307
|
+
if band_idx == 0:
|
308
|
+
# First band: 0 to first threshold
|
309
|
+
band_mask = freq_distance <= freq_thresholds_normalized[0]
|
310
|
+
elif band_idx == num_bands - 1:
|
311
|
+
# Last band: last threshold to 1
|
312
|
+
band_mask = freq_distance > freq_thresholds_normalized[band_idx - 1]
|
313
|
+
else:
|
314
|
+
# Middle bands: between two thresholds
|
315
|
+
band_mask = (
|
316
|
+
freq_distance > freq_thresholds_normalized[band_idx - 1]
|
317
|
+
) & (freq_distance <= freq_thresholds_normalized[band_idx])
|
318
|
+
|
319
|
+
# Band intensity (evenly distributed)
|
320
|
+
band_intensity = (band_idx / (num_bands - 1)) if num_bands > 1 else 1.0
|
321
|
+
|
322
|
+
# Apply band contribution
|
323
|
+
result_fft += fft_shifted * band_mask * band_intensity
|
324
|
+
|
325
|
+
# Inverse FFT
|
326
|
+
filtered_fft_unshifted = fftshift(result_fft)
|
327
|
+
filtered_image = np.real(ifft2(filtered_fft_unshifted))
|
328
|
+
|
329
|
+
# Normalize to 0-255 range
|
330
|
+
filtered_image = filtered_image - np.min(filtered_image)
|
331
|
+
if np.max(filtered_image) > 0:
|
332
|
+
filtered_image = filtered_image / np.max(filtered_image) * 255
|
333
|
+
|
334
|
+
result_image = filtered_image.astype(np.uint8)
|
335
|
+
|
336
|
+
# Apply intensity thresholding if specified
|
337
|
+
if self.intensity_thresholds:
|
338
|
+
result_image = self._apply_intensity_thresholding(result_image)
|
339
|
+
|
340
|
+
return result_image
|
341
|
+
|
342
|
+
def _apply_intensity_thresholding(self, image_array):
|
343
|
+
"""Apply intensity thresholding to the image array."""
|
344
|
+
sorted_thresholds = sorted(self.intensity_thresholds)
|
345
|
+
|
346
|
+
# If no thresholds, return original
|
347
|
+
if not sorted_thresholds:
|
348
|
+
return image_array
|
349
|
+
|
350
|
+
# Convert thresholds from percentages to intensity values (0-255)
|
351
|
+
intensity_thresholds = [t * 255 / 100.0 for t in sorted_thresholds]
|
352
|
+
|
353
|
+
# Number of levels = number of thresholds + 1
|
354
|
+
num_levels = len(intensity_thresholds) + 1
|
355
|
+
result_image = np.copy(image_array)
|
356
|
+
|
357
|
+
for level_idx in range(num_levels):
|
358
|
+
# Define intensity range for this level
|
359
|
+
if level_idx == 0:
|
360
|
+
# First level: 0 to first threshold
|
361
|
+
mask = image_array <= intensity_thresholds[0]
|
362
|
+
elif level_idx == num_levels - 1:
|
363
|
+
# Last level: last threshold to 255
|
364
|
+
mask = image_array > intensity_thresholds[level_idx - 1]
|
365
|
+
else:
|
366
|
+
# Middle levels: between two thresholds
|
367
|
+
mask = (image_array > intensity_thresholds[level_idx - 1]) & (
|
368
|
+
image_array <= intensity_thresholds[level_idx]
|
369
|
+
)
|
370
|
+
|
371
|
+
# Map to quantized level (evenly distributed)
|
372
|
+
level_value = (
|
373
|
+
(level_idx / (num_levels - 1)) * 255 if num_levels > 1 else 255
|
374
|
+
)
|
375
|
+
result_image[mask] = level_value
|
376
|
+
|
377
|
+
return result_image.astype(np.uint8)
|
378
|
+
|
379
|
+
def get_settings(self):
|
380
|
+
"""Get current FFT threshold settings."""
|
381
|
+
return {
|
382
|
+
"frequency_thresholds": self.frequency_thresholds,
|
383
|
+
"intensity_thresholds": self.intensity_thresholds,
|
384
|
+
"is_active": self.is_active(),
|
385
|
+
}
|
386
|
+
|
387
|
+
def reset(self):
|
388
|
+
"""Reset to default values."""
|
389
|
+
self.frequency_slider.reset() # Reset the frequency slider
|
390
|
+
self.intensity_slider.reset() # Reset the intensity slider
|
391
|
+
self.frequency_thresholds = []
|
392
|
+
self.intensity_thresholds = []
|
@@ -0,0 +1,97 @@
|
|
1
|
+
"""Fragment threshold widget for AI segmentation filtering."""
|
2
|
+
|
3
|
+
from PyQt6.QtCore import Qt, pyqtSignal
|
4
|
+
from PyQt6.QtWidgets import (
|
5
|
+
QGroupBox,
|
6
|
+
QHBoxLayout,
|
7
|
+
QLabel,
|
8
|
+
QLineEdit,
|
9
|
+
QSlider,
|
10
|
+
QVBoxLayout,
|
11
|
+
QWidget,
|
12
|
+
)
|
13
|
+
|
14
|
+
|
15
|
+
class FragmentThresholdWidget(QWidget):
|
16
|
+
"""Widget for fragment threshold control specific to AI segmentation."""
|
17
|
+
|
18
|
+
fragment_threshold_changed = pyqtSignal(int)
|
19
|
+
|
20
|
+
def __init__(self, parent=None):
|
21
|
+
super().__init__(parent)
|
22
|
+
self._setup_ui()
|
23
|
+
self._connect_signals()
|
24
|
+
|
25
|
+
def _setup_ui(self):
|
26
|
+
"""Setup the UI layout."""
|
27
|
+
group = QGroupBox("AI Fragment Filter")
|
28
|
+
layout = QVBoxLayout(group)
|
29
|
+
layout.setSpacing(8)
|
30
|
+
|
31
|
+
# Fragment threshold row
|
32
|
+
row_layout = QHBoxLayout()
|
33
|
+
row_layout.setSpacing(8)
|
34
|
+
|
35
|
+
# Label with fixed width for alignment
|
36
|
+
label = QLabel("Filter:")
|
37
|
+
label.setFixedWidth(40)
|
38
|
+
label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
39
|
+
|
40
|
+
# Text edit with smaller width
|
41
|
+
self.fragment_edit = QLineEdit("0")
|
42
|
+
self.fragment_edit.setFixedWidth(35)
|
43
|
+
self.fragment_edit.setToolTip("Fragment threshold value (0-100)")
|
44
|
+
|
45
|
+
# Slider takes remaining space
|
46
|
+
self.fragment_slider = QSlider(Qt.Orientation.Horizontal)
|
47
|
+
self.fragment_slider.setRange(0, 100)
|
48
|
+
self.fragment_slider.setValue(0)
|
49
|
+
self.fragment_slider.setToolTip(
|
50
|
+
"Filter out small AI segments. 0=no filtering, 50=drop <50% of largest, 100=only keep largest"
|
51
|
+
)
|
52
|
+
|
53
|
+
row_layout.addWidget(label)
|
54
|
+
row_layout.addWidget(self.fragment_edit)
|
55
|
+
row_layout.addWidget(self.fragment_slider, 1) # Stretch factor of 1
|
56
|
+
|
57
|
+
layout.addLayout(row_layout)
|
58
|
+
|
59
|
+
# Description label
|
60
|
+
desc_label = QLabel("Filters small AI segments relative to the largest segment")
|
61
|
+
desc_label.setStyleSheet("color: #888; font-size: 9px; font-style: italic;")
|
62
|
+
desc_label.setWordWrap(True)
|
63
|
+
layout.addWidget(desc_label)
|
64
|
+
|
65
|
+
# Main layout
|
66
|
+
main_layout = QVBoxLayout(self)
|
67
|
+
main_layout.setContentsMargins(0, 0, 0, 0)
|
68
|
+
main_layout.addWidget(group)
|
69
|
+
|
70
|
+
def _connect_signals(self):
|
71
|
+
"""Connect internal signals."""
|
72
|
+
self.fragment_slider.valueChanged.connect(self._on_fragment_slider_changed)
|
73
|
+
self.fragment_edit.editingFinished.connect(self._on_fragment_edit_finished)
|
74
|
+
|
75
|
+
def _on_fragment_slider_changed(self, value):
|
76
|
+
"""Handle fragment threshold slider change."""
|
77
|
+
self.fragment_edit.setText(f"{value}")
|
78
|
+
self.fragment_threshold_changed.emit(value)
|
79
|
+
|
80
|
+
def _on_fragment_edit_finished(self):
|
81
|
+
"""Handle fragment threshold text edit change."""
|
82
|
+
try:
|
83
|
+
value = int(self.fragment_edit.text())
|
84
|
+
slider_value = max(0, min(100, value))
|
85
|
+
self.fragment_slider.setValue(slider_value)
|
86
|
+
self.fragment_threshold_changed.emit(slider_value)
|
87
|
+
except ValueError:
|
88
|
+
self.fragment_edit.setText(f"{self.fragment_slider.value()}")
|
89
|
+
|
90
|
+
def get_fragment_threshold(self):
|
91
|
+
"""Get current fragment threshold value."""
|
92
|
+
return self.fragment_slider.value()
|
93
|
+
|
94
|
+
def set_fragment_threshold(self, value):
|
95
|
+
"""Set fragment threshold value."""
|
96
|
+
self.fragment_slider.setValue(value)
|
97
|
+
self.fragment_edit.setText(f"{value}")
|
@@ -33,8 +33,13 @@ class ModelSelectionWidget(QWidget):
|
|
33
33
|
button_layout = QHBoxLayout()
|
34
34
|
self.btn_browse = QPushButton("Browse Models")
|
35
35
|
self.btn_browse.setToolTip("Browse for a folder containing .pth model files")
|
36
|
+
self.btn_browse.setMinimumHeight(28)
|
37
|
+
self.btn_browse.setStyleSheet(self._get_button_style())
|
38
|
+
|
36
39
|
self.btn_refresh = QPushButton("Refresh")
|
37
40
|
self.btn_refresh.setToolTip("Refresh the list of available models")
|
41
|
+
self.btn_refresh.setMinimumHeight(28)
|
42
|
+
self.btn_refresh.setStyleSheet(self._get_button_style())
|
38
43
|
|
39
44
|
button_layout.addWidget(self.btn_browse)
|
40
45
|
button_layout.addWidget(self.btn_refresh)
|
@@ -99,3 +104,24 @@ class ModelSelectionWidget(QWidget):
|
|
99
104
|
self.model_combo.setCurrentIndex(0)
|
100
105
|
self.model_combo.blockSignals(False)
|
101
106
|
self.set_current_model("Current: Default SAM Model")
|
107
|
+
|
108
|
+
def _get_button_style(self):
|
109
|
+
"""Get consistent button styling."""
|
110
|
+
return """
|
111
|
+
QPushButton {
|
112
|
+
background-color: rgba(70, 100, 130, 0.8);
|
113
|
+
border: 1px solid rgba(90, 120, 150, 0.8);
|
114
|
+
border-radius: 6px;
|
115
|
+
color: #E0E0E0;
|
116
|
+
font-weight: bold;
|
117
|
+
font-size: 10px;
|
118
|
+
padding: 4px 8px;
|
119
|
+
}
|
120
|
+
QPushButton:hover {
|
121
|
+
background-color: rgba(90, 120, 150, 0.9);
|
122
|
+
border-color: rgba(110, 140, 170, 0.9);
|
123
|
+
}
|
124
|
+
QPushButton:pressed {
|
125
|
+
background-color: rgba(50, 80, 110, 0.9);
|
126
|
+
}
|
127
|
+
"""
|
@@ -6,34 +6,38 @@ lazylabel/config/hotkeys.py,sha256=bbHPpCNokwgH0EX74vGcNdn3RoyVZNVr-is6us-7J3M,7
|
|
6
6
|
lazylabel/config/paths.py,sha256=ZVKbtaNOxmYO4l6JgsY-8DXaE_jaJfDg2RQJJn3-5nw,1275
|
7
7
|
lazylabel/config/settings.py,sha256=0muOT64zBr9Tn-JbyYRPf9xX760-X84ov93m56SpSHA,1898
|
8
8
|
lazylabel/core/__init__.py,sha256=FmRjop_uIBSJwKMGhaZ-3Iwu34LkoxTuD-hnq5vbTSY,232
|
9
|
-
lazylabel/core/file_manager.py,sha256=
|
10
|
-
lazylabel/core/model_manager.py,sha256=
|
9
|
+
lazylabel/core/file_manager.py,sha256=CmRLd3FtOa64mt9Wz-eYErDerFOm1gt3roGlibviqwo,6013
|
10
|
+
lazylabel/core/model_manager.py,sha256=GYwAVCJVsKrgVXD54XS6BxkFxQEcrNJyt_IBfwOA0AI,3815
|
11
11
|
lazylabel/core/segment_manager.py,sha256=M6kHcYeiub3WqL01NElCvKOc2GNmf72LUM1W8XwSaxc,6465
|
12
12
|
lazylabel/models/__init__.py,sha256=fIlk_0DuZfiClcm0XlZdimeHzunQwBmTMI4PcGsaymw,91
|
13
13
|
lazylabel/models/sam_model.py,sha256=anh4XMkoX8U5MCZHV5HqN0x8iVnRYi50rcXFt6LdFCQ,8020
|
14
14
|
lazylabel/ui/__init__.py,sha256=4qDIh9y6tABPmD8MAMGZn_G7oSRyrcHt2HkjoWgbGH4,268
|
15
|
-
lazylabel/ui/control_panel.py,sha256=
|
15
|
+
lazylabel/ui/control_panel.py,sha256=IvGtL-Z-WOpo9q-owhwBSB-DSwXaDubec2ajLRMoyFk,31322
|
16
16
|
lazylabel/ui/editable_vertex.py,sha256=nAFC2UuFfbvMbGBbAiLWA77cS5-Hn3a08xe1_QLz2yk,2449
|
17
17
|
lazylabel/ui/hotkey_dialog.py,sha256=U_B76HLOxWdWkfA4d2XgRUaZTJPAAE_m5fmwf7Rh-5Y,14743
|
18
18
|
lazylabel/ui/hoverable_pixelmap_item.py,sha256=kJFOp7WXiyHpNf7l73TZjiob85jgP30b5MZvu_z5L3c,728
|
19
19
|
lazylabel/ui/hoverable_polygon_item.py,sha256=aclUwd0P8H8xbcep6GwhnfaVs1zSkqeZKAL-xeDyMiU,1222
|
20
|
-
lazylabel/ui/main_window.py,sha256=
|
20
|
+
lazylabel/ui/main_window.py,sha256=R7QpEY3kEhxTm_bYYJDpmF1zUgvEwU9UxLwPkduCgkU,128160
|
21
21
|
lazylabel/ui/numeric_table_widget_item.py,sha256=dQUlIFu9syCxTGAHVIlmbgkI7aJ3f3wmDPBz1AGK9Bg,283
|
22
22
|
lazylabel/ui/photo_viewer.py,sha256=f93Mn9ajR2CYakJbbhhHvD5blKrwiGq3ZYgro-k2npc,4217
|
23
23
|
lazylabel/ui/reorderable_class_table.py,sha256=sxHhQre5O_MXLDFgKnw43QnvXXoqn5xRKMGitgO7muI,2371
|
24
24
|
lazylabel/ui/right_panel.py,sha256=-PeXcu7Lr-xhZniBMvWLDPiFb_RAHYAcILyw8fPJs6I,13139
|
25
|
-
lazylabel/ui/widgets/__init__.py,sha256=
|
26
|
-
lazylabel/ui/widgets/adjustments_widget.py,sha256=
|
27
|
-
lazylabel/ui/widgets/
|
25
|
+
lazylabel/ui/widgets/__init__.py,sha256=6VDoMnanqVm3yjOovxie3WggPODuhUsIO75RtxOhQhI,688
|
26
|
+
lazylabel/ui/widgets/adjustments_widget.py,sha256=hQt0LC3hOM0qJn0KKLM5zMcnn8cSl1sxNv7Mo6ON8Ow,12695
|
27
|
+
lazylabel/ui/widgets/border_crop_widget.py,sha256=VdgSxQQOREnXsCKBOBF36UbbAtwPO7fw5M7LpQADMGs,7100
|
28
|
+
lazylabel/ui/widgets/channel_threshold_widget.py,sha256=PNz1S8a283F_lkMi5o5leoHp99IP8Htdj_mmJZCLIYc,18726
|
29
|
+
lazylabel/ui/widgets/fft_threshold_widget.py,sha256=F1NfRVJiimuat5Wbt6NNOlBuObNC1BQPsipb86RGX6w,16730
|
30
|
+
lazylabel/ui/widgets/fragment_threshold_widget.py,sha256=YtToua1eAUtEuJ3EwdCMvI-39TRrFnshkty4tnR4OMU,3492
|
31
|
+
lazylabel/ui/widgets/model_selection_widget.py,sha256=7MA9L1VKjzEAA7BJl2BAbZRSCRFJeGX5xQb-_LO9_eQ,4449
|
28
32
|
lazylabel/ui/widgets/settings_widget.py,sha256=ShTaLJeXxwrSuTV4kmtV2JiWjfREil2D1nvPUIfAgDs,4859
|
29
33
|
lazylabel/ui/widgets/status_bar.py,sha256=wTbMQNEOBfmtNj8EVFZS_lxgaemu-CbRXeZzEQDaVz8,4014
|
30
34
|
lazylabel/utils/__init__.py,sha256=V6IR5Gim-39HgM2NyTVT-n8gy3mjilCSFW9y0owN5nc,179
|
31
35
|
lazylabel/utils/custom_file_system_model.py,sha256=-3EimlybvevH6bvqBE0qdFnLADVtayylmkntxPXK0Bk,4869
|
32
36
|
lazylabel/utils/logger.py,sha256=R7z6ifgA-NY-9ZbLlNH0i19zzwXndJ_gkG2J1zpVEhg,1306
|
33
37
|
lazylabel/utils/utils.py,sha256=sYSCoXL27OaLgOZaUkCAhgmKZ7YfhR3Cc5F8nDIa3Ig,414
|
34
|
-
lazylabel_gui-1.1.
|
35
|
-
lazylabel_gui-1.1.
|
36
|
-
lazylabel_gui-1.1.
|
37
|
-
lazylabel_gui-1.1.
|
38
|
-
lazylabel_gui-1.1.
|
39
|
-
lazylabel_gui-1.1.
|
38
|
+
lazylabel_gui-1.1.9.dist-info/licenses/LICENSE,sha256=kSDEIgrWAPd1u2UFGGpC9X71dhzrlzBFs8hbDlENnGE,1092
|
39
|
+
lazylabel_gui-1.1.9.dist-info/METADATA,sha256=MjH3jrF3QDAOKiGUpuA3vIBJPTjAM4jax88hJdojKlw,8064
|
40
|
+
lazylabel_gui-1.1.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
41
|
+
lazylabel_gui-1.1.9.dist-info/entry_points.txt,sha256=Hd0WwEG9OPTa_ziYjiD0aRh7R6Fupt-wdQ3sspdc1mM,54
|
42
|
+
lazylabel_gui-1.1.9.dist-info/top_level.txt,sha256=YN4uIyrpDBq1wiJaBuZLDipIzyZY0jqJOmmXiPIOUkU,10
|
43
|
+
lazylabel_gui-1.1.9.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|