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,500 @@
|
|
1
|
+
"""
|
2
|
+
Channel Threshold Widget for LazyLabel.
|
3
|
+
|
4
|
+
This widget provides channel-based thresholding with multi-indicator sliders.
|
5
|
+
Users can add multiple threshold points by double-clicking on sliders,
|
6
|
+
creating pixel remapping with multiple value ranges.
|
7
|
+
"""
|
8
|
+
|
9
|
+
import numpy as np
|
10
|
+
from PyQt6.QtCore import QRect, Qt, pyqtSignal
|
11
|
+
from PyQt6.QtGui import QBrush, QColor, QFont, QPainter, QPen
|
12
|
+
from PyQt6.QtWidgets import (
|
13
|
+
QCheckBox,
|
14
|
+
QHBoxLayout,
|
15
|
+
QLabel,
|
16
|
+
QSizePolicy,
|
17
|
+
QVBoxLayout,
|
18
|
+
QWidget,
|
19
|
+
)
|
20
|
+
|
21
|
+
|
22
|
+
class MultiIndicatorSlider(QWidget):
|
23
|
+
"""Custom slider widget with multiple draggable indicators."""
|
24
|
+
|
25
|
+
valueChanged = pyqtSignal(list) # Emits list of indicator positions
|
26
|
+
dragStarted = pyqtSignal() # Emitted when user starts dragging
|
27
|
+
dragFinished = pyqtSignal() # Emitted when user finishes dragging
|
28
|
+
|
29
|
+
def __init__(self, channel_name="Channel", minimum=0, maximum=256, parent=None):
|
30
|
+
super().__init__(parent)
|
31
|
+
self.channel_name = channel_name
|
32
|
+
self.minimum = minimum
|
33
|
+
self.maximum = maximum
|
34
|
+
self.indicators = [] # Start with no indicators
|
35
|
+
self.dragging_index = -1
|
36
|
+
self.drag_offset = 0
|
37
|
+
self.is_dragging = False # Track if currently dragging
|
38
|
+
|
39
|
+
self.setMinimumHeight(60)
|
40
|
+
self.setFixedHeight(60)
|
41
|
+
self.setMinimumWidth(200)
|
42
|
+
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
43
|
+
|
44
|
+
# Colors for the channel
|
45
|
+
self.channel_colors = {
|
46
|
+
"Red": QColor(255, 100, 100),
|
47
|
+
"Green": QColor(100, 255, 100),
|
48
|
+
"Blue": QColor(100, 100, 255),
|
49
|
+
"Gray": QColor(200, 200, 200),
|
50
|
+
"Channel": QColor(150, 150, 150),
|
51
|
+
}
|
52
|
+
|
53
|
+
def get_channel_color(self):
|
54
|
+
"""Get color for this channel."""
|
55
|
+
return self.channel_colors.get(self.channel_name, QColor(150, 150, 150))
|
56
|
+
|
57
|
+
def get_slider_rect(self):
|
58
|
+
"""Get the slider track rectangle."""
|
59
|
+
margin = 20
|
60
|
+
return QRect(margin, 25, self.width() - 2 * margin, 10)
|
61
|
+
|
62
|
+
def value_to_x(self, value):
|
63
|
+
"""Convert value to x coordinate."""
|
64
|
+
slider_rect = self.get_slider_rect()
|
65
|
+
ratio = (value - self.minimum) / (self.maximum - self.minimum)
|
66
|
+
return slider_rect.left() + int(ratio * slider_rect.width())
|
67
|
+
|
68
|
+
def x_to_value(self, x):
|
69
|
+
"""Convert x coordinate to value."""
|
70
|
+
slider_rect = self.get_slider_rect()
|
71
|
+
ratio = (x - slider_rect.left()) / slider_rect.width()
|
72
|
+
ratio = max(0, min(1, ratio)) # Clamp to [0, 1]
|
73
|
+
return int(self.minimum + ratio * (self.maximum - self.minimum))
|
74
|
+
|
75
|
+
def paintEvent(self, event):
|
76
|
+
"""Paint the slider."""
|
77
|
+
painter = QPainter(self)
|
78
|
+
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
79
|
+
|
80
|
+
# Draw channel label
|
81
|
+
font = QFont()
|
82
|
+
font.setPointSize(9)
|
83
|
+
painter.setFont(font)
|
84
|
+
painter.setPen(QPen(QColor(255, 255, 255)))
|
85
|
+
painter.drawText(5, 15, f"{self.channel_name}")
|
86
|
+
|
87
|
+
# Draw slider track
|
88
|
+
slider_rect = self.get_slider_rect()
|
89
|
+
painter.setPen(QPen(QColor(100, 100, 100), 2))
|
90
|
+
painter.setBrush(QBrush(QColor(50, 50, 50)))
|
91
|
+
painter.drawRoundedRect(slider_rect, 5, 5)
|
92
|
+
|
93
|
+
# Draw value segments
|
94
|
+
channel_color = self.get_channel_color()
|
95
|
+
sorted_indicators = sorted(self.indicators)
|
96
|
+
|
97
|
+
# Handle case with no indicators - draw single segment
|
98
|
+
if not sorted_indicators:
|
99
|
+
# Single segment covering entire slider
|
100
|
+
segment_rect = QRect(
|
101
|
+
slider_rect.left(),
|
102
|
+
slider_rect.top(),
|
103
|
+
slider_rect.width(),
|
104
|
+
slider_rect.height(),
|
105
|
+
)
|
106
|
+
# Use low alpha for inactive state
|
107
|
+
segment_color = QColor(channel_color)
|
108
|
+
segment_color.setAlpha(50)
|
109
|
+
painter.setBrush(QBrush(segment_color))
|
110
|
+
painter.setPen(QPen(Qt.GlobalColor.transparent))
|
111
|
+
painter.drawRoundedRect(segment_rect, 5, 5)
|
112
|
+
else:
|
113
|
+
# Draw segments between indicators
|
114
|
+
for i in range(len(sorted_indicators) + 1):
|
115
|
+
start_val = self.minimum if i == 0 else sorted_indicators[i - 1]
|
116
|
+
end_val = (
|
117
|
+
self.maximum
|
118
|
+
if i == len(sorted_indicators)
|
119
|
+
else sorted_indicators[i]
|
120
|
+
)
|
121
|
+
|
122
|
+
start_x = self.value_to_x(start_val)
|
123
|
+
end_x = self.value_to_x(end_val)
|
124
|
+
|
125
|
+
# Calculate segment value (evenly distributed)
|
126
|
+
segment_value = (
|
127
|
+
i / len(sorted_indicators) if len(sorted_indicators) > 0 else 0
|
128
|
+
)
|
129
|
+
alpha = int(50 + segment_value * 150) # 50-200 alpha range
|
130
|
+
|
131
|
+
segment_color = QColor(channel_color)
|
132
|
+
segment_color.setAlpha(alpha)
|
133
|
+
|
134
|
+
segment_rect = QRect(
|
135
|
+
start_x, slider_rect.top(), end_x - start_x, slider_rect.height()
|
136
|
+
)
|
137
|
+
painter.setBrush(QBrush(segment_color))
|
138
|
+
painter.setPen(QPen(Qt.GlobalColor.transparent))
|
139
|
+
painter.drawRoundedRect(segment_rect, 5, 5)
|
140
|
+
|
141
|
+
# Draw indicators
|
142
|
+
for i, value in enumerate(self.indicators):
|
143
|
+
x = self.value_to_x(value)
|
144
|
+
|
145
|
+
# Indicator handle
|
146
|
+
handle_rect = QRect(
|
147
|
+
x - 6, slider_rect.top() - 3, 12, slider_rect.height() + 6
|
148
|
+
)
|
149
|
+
|
150
|
+
# Highlight if dragging
|
151
|
+
if i == self.dragging_index:
|
152
|
+
painter.setBrush(QBrush(QColor(255, 255, 100)))
|
153
|
+
painter.setPen(QPen(QColor(200, 200, 50), 2))
|
154
|
+
else:
|
155
|
+
painter.setBrush(QBrush(QColor(255, 255, 255)))
|
156
|
+
painter.setPen(QPen(QColor(150, 150, 150), 1))
|
157
|
+
|
158
|
+
painter.drawRoundedRect(handle_rect, 3, 3)
|
159
|
+
|
160
|
+
# Value label
|
161
|
+
painter.setPen(QPen(QColor(255, 255, 255)))
|
162
|
+
painter.drawText(x - 15, slider_rect.bottom() + 15, f"{value}")
|
163
|
+
|
164
|
+
def mousePressEvent(self, event):
|
165
|
+
"""Handle mouse press events."""
|
166
|
+
if event.button() == Qt.MouseButton.LeftButton:
|
167
|
+
slider_rect = self.get_slider_rect()
|
168
|
+
|
169
|
+
# Check if clicking on an existing indicator
|
170
|
+
for i, value in enumerate(self.indicators):
|
171
|
+
x = self.value_to_x(value)
|
172
|
+
handle_rect = QRect(
|
173
|
+
x - 6, slider_rect.top() - 3, 12, slider_rect.height() + 6
|
174
|
+
)
|
175
|
+
|
176
|
+
if handle_rect.contains(event.pos()):
|
177
|
+
self.dragging_index = i
|
178
|
+
self.drag_offset = event.pos().x() - x
|
179
|
+
self.setCursor(Qt.CursorShape.ClosedHandCursor)
|
180
|
+
self.is_dragging = True
|
181
|
+
self.dragStarted.emit()
|
182
|
+
return
|
183
|
+
|
184
|
+
def mouseDoubleClickEvent(self, event):
|
185
|
+
"""Handle double-click to add new indicator."""
|
186
|
+
if event.button() == Qt.MouseButton.LeftButton:
|
187
|
+
slider_rect = self.get_slider_rect()
|
188
|
+
|
189
|
+
if slider_rect.contains(event.pos()):
|
190
|
+
new_value = self.x_to_value(event.pos().x())
|
191
|
+
|
192
|
+
# Don't add if too close to existing indicator
|
193
|
+
min_distance = 10
|
194
|
+
for existing_value in self.indicators:
|
195
|
+
if abs(new_value - existing_value) < min_distance:
|
196
|
+
return
|
197
|
+
|
198
|
+
self.indicators.append(new_value)
|
199
|
+
self.indicators.sort()
|
200
|
+
self.valueChanged.emit(self.indicators[:])
|
201
|
+
self.update()
|
202
|
+
|
203
|
+
def mouseMoveEvent(self, event):
|
204
|
+
"""Handle mouse move events for dragging."""
|
205
|
+
if self.dragging_index >= 0:
|
206
|
+
new_x = event.pos().x() - self.drag_offset
|
207
|
+
new_value = self.x_to_value(new_x)
|
208
|
+
|
209
|
+
# Clamp value
|
210
|
+
new_value = max(self.minimum, min(self.maximum, new_value))
|
211
|
+
|
212
|
+
# Update indicator
|
213
|
+
self.indicators[self.dragging_index] = new_value
|
214
|
+
self.valueChanged.emit(self.indicators[:])
|
215
|
+
self.update()
|
216
|
+
|
217
|
+
def mouseReleaseEvent(self, event):
|
218
|
+
"""Handle mouse release events."""
|
219
|
+
if event.button() == Qt.MouseButton.LeftButton:
|
220
|
+
self.dragging_index = -1
|
221
|
+
self.setCursor(Qt.CursorShape.ArrowCursor)
|
222
|
+
self.is_dragging = False
|
223
|
+
self.dragFinished.emit()
|
224
|
+
|
225
|
+
def contextMenuEvent(self, event):
|
226
|
+
"""Handle right-click to remove indicator."""
|
227
|
+
slider_rect = self.get_slider_rect()
|
228
|
+
|
229
|
+
# Only allow removal if more than 1 indicator
|
230
|
+
if len(self.indicators) <= 1:
|
231
|
+
return
|
232
|
+
|
233
|
+
# Check if right-clicking on an indicator
|
234
|
+
for i, value in enumerate(self.indicators):
|
235
|
+
x = self.value_to_x(value)
|
236
|
+
handle_rect = QRect(
|
237
|
+
x - 6, slider_rect.top() - 3, 12, slider_rect.height() + 6
|
238
|
+
)
|
239
|
+
|
240
|
+
if handle_rect.contains(event.pos()):
|
241
|
+
self.indicators.pop(i)
|
242
|
+
self.valueChanged.emit(self.indicators[:])
|
243
|
+
self.update()
|
244
|
+
return
|
245
|
+
|
246
|
+
def reset(self):
|
247
|
+
"""Reset to no indicators."""
|
248
|
+
self.indicators = []
|
249
|
+
self.valueChanged.emit(self.indicators[:])
|
250
|
+
self.update()
|
251
|
+
|
252
|
+
def get_indicators(self):
|
253
|
+
"""Get current indicator values."""
|
254
|
+
return self.indicators[:]
|
255
|
+
|
256
|
+
def set_indicators(self, indicators):
|
257
|
+
"""Set indicator values."""
|
258
|
+
self.indicators = indicators[:]
|
259
|
+
self.valueChanged.emit(self.indicators[:])
|
260
|
+
self.update()
|
261
|
+
|
262
|
+
|
263
|
+
class ChannelSliderWidget(QWidget):
|
264
|
+
"""Combined widget with checkbox and slider for a single channel."""
|
265
|
+
|
266
|
+
valueChanged = pyqtSignal() # Emitted when checkbox or slider changes
|
267
|
+
dragStarted = pyqtSignal() # Emitted when slider drag starts
|
268
|
+
dragFinished = pyqtSignal() # Emitted when slider drag finishes
|
269
|
+
|
270
|
+
def __init__(self, channel_name, parent=None):
|
271
|
+
super().__init__(parent)
|
272
|
+
self.channel_name = channel_name
|
273
|
+
self.setupUI()
|
274
|
+
|
275
|
+
def setupUI(self):
|
276
|
+
"""Set up the UI for the combined widget."""
|
277
|
+
layout = QHBoxLayout(self)
|
278
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
279
|
+
layout.setSpacing(5)
|
280
|
+
|
281
|
+
# Checkbox to enable/disable
|
282
|
+
self.checkbox = QCheckBox()
|
283
|
+
self.checkbox.setChecked(False)
|
284
|
+
self.checkbox.toggled.connect(self._on_checkbox_toggled)
|
285
|
+
layout.addWidget(self.checkbox)
|
286
|
+
|
287
|
+
# Slider
|
288
|
+
self.slider = MultiIndicatorSlider(self.channel_name, 0, 256, self)
|
289
|
+
self.slider.valueChanged.connect(self._on_slider_changed)
|
290
|
+
self.slider.dragStarted.connect(self.dragStarted.emit) # Forward drag signals
|
291
|
+
self.slider.dragFinished.connect(self.dragFinished.emit)
|
292
|
+
self.slider.setEnabled(False) # Start disabled
|
293
|
+
layout.addWidget(self.slider)
|
294
|
+
|
295
|
+
def _on_checkbox_toggled(self, checked):
|
296
|
+
"""Handle checkbox toggle."""
|
297
|
+
self.slider.setEnabled(checked)
|
298
|
+
if not checked:
|
299
|
+
# Reset slider when unchecked
|
300
|
+
self.slider.reset()
|
301
|
+
self.valueChanged.emit()
|
302
|
+
|
303
|
+
def _on_slider_changed(self):
|
304
|
+
"""Handle slider value change."""
|
305
|
+
if self.checkbox.isChecked():
|
306
|
+
self.valueChanged.emit()
|
307
|
+
|
308
|
+
def is_enabled(self):
|
309
|
+
"""Check if this channel is enabled."""
|
310
|
+
return self.checkbox.isChecked()
|
311
|
+
|
312
|
+
def get_indicators(self):
|
313
|
+
"""Get current indicator values if enabled."""
|
314
|
+
if self.is_enabled():
|
315
|
+
return self.slider.get_indicators()
|
316
|
+
return []
|
317
|
+
|
318
|
+
def reset(self):
|
319
|
+
"""Reset this channel."""
|
320
|
+
self.checkbox.setChecked(False)
|
321
|
+
self.slider.reset()
|
322
|
+
|
323
|
+
|
324
|
+
class ChannelThresholdWidget(QWidget):
|
325
|
+
"""Widget for channel-based thresholding with multi-indicator sliders."""
|
326
|
+
|
327
|
+
thresholdChanged = pyqtSignal() # Emitted when any threshold changes
|
328
|
+
dragStarted = pyqtSignal() # Emitted when any slider drag starts
|
329
|
+
dragFinished = pyqtSignal() # Emitted when any slider drag finishes
|
330
|
+
|
331
|
+
def __init__(self, parent=None):
|
332
|
+
super().__init__(parent)
|
333
|
+
self.current_image_channels = 0 # 0 = no image, 1 = grayscale, 3 = RGB
|
334
|
+
self.sliders = {} # Dictionary of channel name -> slider
|
335
|
+
|
336
|
+
self.setupUI()
|
337
|
+
|
338
|
+
def setupUI(self):
|
339
|
+
"""Set up the user interface."""
|
340
|
+
layout = QVBoxLayout(self)
|
341
|
+
layout.setContentsMargins(5, 5, 5, 5)
|
342
|
+
layout.setSpacing(5)
|
343
|
+
|
344
|
+
# Title
|
345
|
+
title_label = QLabel("Channel Thresholding")
|
346
|
+
title_label.setStyleSheet("font-weight: bold; font-size: 11px;")
|
347
|
+
layout.addWidget(title_label)
|
348
|
+
|
349
|
+
# Sliders container
|
350
|
+
self.sliders_layout = QVBoxLayout()
|
351
|
+
layout.addLayout(self.sliders_layout)
|
352
|
+
|
353
|
+
# Instructions
|
354
|
+
instructions = QLabel(
|
355
|
+
"✓ Check to enable • Double-click to add threshold • Right-click to remove"
|
356
|
+
)
|
357
|
+
instructions.setStyleSheet("color: #888; font-size: 9px;")
|
358
|
+
instructions.setWordWrap(True)
|
359
|
+
layout.addWidget(instructions)
|
360
|
+
|
361
|
+
layout.addStretch()
|
362
|
+
|
363
|
+
def update_for_image(self, image_array):
|
364
|
+
"""Update widget based on loaded image."""
|
365
|
+
if image_array is None:
|
366
|
+
self._clear_sliders()
|
367
|
+
self.current_image_channels = 0
|
368
|
+
return
|
369
|
+
|
370
|
+
# Determine number of channels
|
371
|
+
if len(image_array.shape) == 2:
|
372
|
+
# Grayscale
|
373
|
+
self.current_image_channels = 1
|
374
|
+
channels = ["Gray"]
|
375
|
+
elif len(image_array.shape) == 3 and image_array.shape[2] == 3:
|
376
|
+
# RGB
|
377
|
+
self.current_image_channels = 3
|
378
|
+
channels = ["Red", "Green", "Blue"]
|
379
|
+
else:
|
380
|
+
# Unsupported format
|
381
|
+
self._clear_sliders()
|
382
|
+
self.current_image_channels = 0
|
383
|
+
return
|
384
|
+
|
385
|
+
# Create sliders for each channel
|
386
|
+
self._create_sliders(channels)
|
387
|
+
|
388
|
+
def _create_sliders(self, channel_names):
|
389
|
+
"""Create sliders for the specified channels."""
|
390
|
+
# Clear existing sliders
|
391
|
+
self._clear_sliders()
|
392
|
+
|
393
|
+
# Create new combined slider widgets
|
394
|
+
for channel_name in channel_names:
|
395
|
+
slider_widget = ChannelSliderWidget(channel_name, self)
|
396
|
+
slider_widget.valueChanged.connect(self._on_slider_changed)
|
397
|
+
slider_widget.dragStarted.connect(
|
398
|
+
self.dragStarted.emit
|
399
|
+
) # Forward drag signals
|
400
|
+
slider_widget.dragFinished.connect(self.dragFinished.emit)
|
401
|
+
self.sliders[channel_name] = slider_widget
|
402
|
+
self.sliders_layout.addWidget(slider_widget)
|
403
|
+
|
404
|
+
def _clear_sliders(self):
|
405
|
+
"""Clear all sliders."""
|
406
|
+
for slider in self.sliders.values():
|
407
|
+
self.sliders_layout.removeWidget(slider)
|
408
|
+
slider.deleteLater()
|
409
|
+
self.sliders.clear()
|
410
|
+
|
411
|
+
def _on_slider_changed(self):
|
412
|
+
"""Handle slider value change."""
|
413
|
+
self.thresholdChanged.emit()
|
414
|
+
|
415
|
+
def get_threshold_settings(self):
|
416
|
+
"""Get current threshold settings for all channels."""
|
417
|
+
settings = {}
|
418
|
+
for channel_name, slider_widget in self.sliders.items():
|
419
|
+
settings[channel_name] = slider_widget.get_indicators()
|
420
|
+
return settings
|
421
|
+
|
422
|
+
def apply_thresholding(self, image_array):
|
423
|
+
"""Apply thresholding to image array and return modified array."""
|
424
|
+
if image_array is None or not self.sliders:
|
425
|
+
return image_array
|
426
|
+
|
427
|
+
# Create a copy to modify
|
428
|
+
result = image_array.copy().astype(np.float32)
|
429
|
+
|
430
|
+
if self.current_image_channels == 1:
|
431
|
+
# Grayscale image
|
432
|
+
if "Gray" in self.sliders:
|
433
|
+
result = self._apply_channel_thresholding(
|
434
|
+
result, self.sliders["Gray"].get_indicators()
|
435
|
+
)
|
436
|
+
elif self.current_image_channels == 3:
|
437
|
+
# RGB image
|
438
|
+
channel_names = ["Red", "Green", "Blue"]
|
439
|
+
for i, channel_name in enumerate(channel_names):
|
440
|
+
if channel_name in self.sliders:
|
441
|
+
result[:, :, i] = self._apply_channel_thresholding(
|
442
|
+
result[:, :, i], self.sliders[channel_name].get_indicators()
|
443
|
+
)
|
444
|
+
|
445
|
+
# Convert back to uint8
|
446
|
+
return np.clip(result, 0, 255).astype(np.uint8)
|
447
|
+
|
448
|
+
def _apply_channel_thresholding(self, channel_data, indicators):
|
449
|
+
"""Apply thresholding to a single channel."""
|
450
|
+
if not indicators:
|
451
|
+
return channel_data
|
452
|
+
|
453
|
+
# Sort indicators
|
454
|
+
sorted_indicators = sorted(indicators)
|
455
|
+
|
456
|
+
# Create output array
|
457
|
+
result = np.zeros_like(channel_data)
|
458
|
+
|
459
|
+
# Number of segments = number of indicators + 1
|
460
|
+
num_segments = len(sorted_indicators) + 1
|
461
|
+
|
462
|
+
# Apply thresholding for each segment
|
463
|
+
for i in range(num_segments):
|
464
|
+
# Determine segment bounds
|
465
|
+
if i == 0:
|
466
|
+
# First segment: 0 to first indicator
|
467
|
+
mask = channel_data < sorted_indicators[0]
|
468
|
+
segment_value = 0
|
469
|
+
elif i == num_segments - 1:
|
470
|
+
# Last segment: last indicator to max
|
471
|
+
mask = channel_data >= sorted_indicators[-1]
|
472
|
+
segment_value = 255
|
473
|
+
else:
|
474
|
+
# Middle segments
|
475
|
+
mask = (channel_data >= sorted_indicators[i - 1]) & (
|
476
|
+
channel_data < sorted_indicators[i]
|
477
|
+
)
|
478
|
+
# Evenly distribute values between segments
|
479
|
+
segment_value = int((i / (num_segments - 1)) * 255)
|
480
|
+
|
481
|
+
result[mask] = segment_value
|
482
|
+
|
483
|
+
return result
|
484
|
+
|
485
|
+
def has_active_thresholding(self):
|
486
|
+
"""Check if any channel has active thresholding (indicators present)."""
|
487
|
+
for slider_widget in self.sliders.values():
|
488
|
+
if slider_widget.get_indicators():
|
489
|
+
return True
|
490
|
+
return False
|
491
|
+
|
492
|
+
def get_threshold_params(self):
|
493
|
+
"""Get current threshold parameters for caching."""
|
494
|
+
params = {}
|
495
|
+
for channel_name, slider_widget in self.sliders.items():
|
496
|
+
params[channel_name] = {
|
497
|
+
"indicators": sorted(slider_widget.get_indicators()),
|
498
|
+
"enabled": slider_widget.is_enabled(),
|
499
|
+
}
|
500
|
+
return params
|