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
@@ -0,0 +1,299 @@
|
|
1
|
+
"""Single view mode handler."""
|
2
|
+
|
3
|
+
from PyQt6.QtCore import QPointF, Qt
|
4
|
+
from PyQt6.QtGui import QBrush, QColor, QPen, QPolygonF
|
5
|
+
from PyQt6.QtWidgets import QGraphicsEllipseItem
|
6
|
+
|
7
|
+
from ...utils import mask_to_pixmap
|
8
|
+
from ..hoverable_pixelmap_item import HoverablePixmapItem
|
9
|
+
from ..hoverable_polygon_item import HoverablePolygonItem
|
10
|
+
from .base_mode import BaseModeHandler
|
11
|
+
|
12
|
+
|
13
|
+
class SingleViewModeHandler(BaseModeHandler):
|
14
|
+
"""Handler for single view mode operations."""
|
15
|
+
|
16
|
+
def handle_ai_click(self, pos, event):
|
17
|
+
"""Handle AI mode click in single view."""
|
18
|
+
# Implementation moved from main_window._add_point
|
19
|
+
positive = event.button() == Qt.MouseButton.LeftButton
|
20
|
+
|
21
|
+
# Check if SAM model is updating
|
22
|
+
if self.main_window.sam_is_updating:
|
23
|
+
self.main_window._show_warning_notification(
|
24
|
+
"AI model is updating, please wait..."
|
25
|
+
)
|
26
|
+
return
|
27
|
+
|
28
|
+
# Ensure SAM model is updated
|
29
|
+
self.main_window._ensure_sam_updated()
|
30
|
+
|
31
|
+
# Check again if model is now updating
|
32
|
+
if self.main_window.sam_is_updating:
|
33
|
+
self.main_window._show_warning_notification(
|
34
|
+
"AI model is loading, please wait..."
|
35
|
+
)
|
36
|
+
return
|
37
|
+
|
38
|
+
# Transform coordinates and add point
|
39
|
+
sam_x, sam_y = self.main_window._transform_display_coords_to_sam_coords(pos)
|
40
|
+
|
41
|
+
point_list = (
|
42
|
+
self.main_window.positive_points
|
43
|
+
if positive
|
44
|
+
else self.main_window.negative_points
|
45
|
+
)
|
46
|
+
point_list.append([sam_x, sam_y])
|
47
|
+
|
48
|
+
# Add visual point
|
49
|
+
point_color = (
|
50
|
+
QColor(Qt.GlobalColor.green) if positive else QColor(Qt.GlobalColor.red)
|
51
|
+
)
|
52
|
+
point_color.setAlpha(150)
|
53
|
+
point_diameter = self.main_window.point_radius * 2
|
54
|
+
|
55
|
+
point_item = QGraphicsEllipseItem(
|
56
|
+
pos.x() - self.main_window.point_radius,
|
57
|
+
pos.y() - self.main_window.point_radius,
|
58
|
+
point_diameter,
|
59
|
+
point_diameter,
|
60
|
+
)
|
61
|
+
point_item.setBrush(QBrush(point_color))
|
62
|
+
point_item.setPen(QPen(Qt.PenStyle.NoPen))
|
63
|
+
self.main_window.viewer.scene().addItem(point_item)
|
64
|
+
self.main_window.point_items.append(point_item)
|
65
|
+
|
66
|
+
# Record the action for undo
|
67
|
+
self.main_window.action_history.append(
|
68
|
+
{
|
69
|
+
"type": "add_point",
|
70
|
+
"point_type": "positive" if positive else "negative",
|
71
|
+
"point_coords": [int(pos.x()), int(pos.y())],
|
72
|
+
"sam_coords": [sam_x, sam_y],
|
73
|
+
"point_item": point_item,
|
74
|
+
"viewer_mode": "single",
|
75
|
+
}
|
76
|
+
)
|
77
|
+
# Clear redo history when a new action is performed
|
78
|
+
self.main_window.redo_history.clear()
|
79
|
+
|
80
|
+
# Generate prediction
|
81
|
+
self.main_window._update_segmentation()
|
82
|
+
|
83
|
+
def handle_polygon_click(self, pos):
|
84
|
+
"""Handle polygon mode click in single view."""
|
85
|
+
# Check if clicking near first point to close polygon
|
86
|
+
if self.main_window.polygon_points and len(self.main_window.polygon_points) > 2:
|
87
|
+
first_point = self.main_window.polygon_points[0]
|
88
|
+
distance_squared = (pos.x() - first_point.x()) ** 2 + (
|
89
|
+
pos.y() - first_point.y()
|
90
|
+
) ** 2
|
91
|
+
if distance_squared < self.main_window.polygon_join_threshold**2:
|
92
|
+
self._finalize_polygon()
|
93
|
+
return
|
94
|
+
|
95
|
+
# Add point to polygon
|
96
|
+
self.main_window.polygon_points.append(pos)
|
97
|
+
|
98
|
+
# Add visual point
|
99
|
+
point_item = QGraphicsEllipseItem(pos.x() - 3, pos.y() - 3, 6, 6)
|
100
|
+
point_item.setBrush(QBrush(QColor(0, 255, 255))) # Cyan
|
101
|
+
point_item.setPen(QPen(Qt.PenStyle.NoPen))
|
102
|
+
self.main_window.viewer.scene().addItem(point_item)
|
103
|
+
self.main_window.polygon_preview_items.append(point_item)
|
104
|
+
|
105
|
+
def handle_bbox_start(self, pos):
|
106
|
+
"""Handle bbox mode start in single view."""
|
107
|
+
from PyQt6.QtWidgets import QGraphicsRectItem
|
108
|
+
|
109
|
+
self.main_window.drag_start_pos = pos
|
110
|
+
|
111
|
+
# Create rubber band rectangle
|
112
|
+
self.main_window.rubber_band_rect = QGraphicsRectItem()
|
113
|
+
self.main_window.rubber_band_rect.setPen(
|
114
|
+
QPen(QColor(255, 0, 0), 2, Qt.PenStyle.DashLine)
|
115
|
+
)
|
116
|
+
self.main_window.viewer.scene().addItem(self.main_window.rubber_band_rect)
|
117
|
+
|
118
|
+
def handle_bbox_drag(self, pos):
|
119
|
+
"""Handle bbox mode drag in single view."""
|
120
|
+
if (
|
121
|
+
hasattr(self.main_window, "drag_start_pos")
|
122
|
+
and self.main_window.drag_start_pos
|
123
|
+
and hasattr(self.main_window, "rubber_band_rect")
|
124
|
+
and self.main_window.rubber_band_rect
|
125
|
+
):
|
126
|
+
from PyQt6.QtCore import QRectF
|
127
|
+
|
128
|
+
# Update rubber band rectangle
|
129
|
+
rect = QRectF(self.main_window.drag_start_pos, pos).normalized()
|
130
|
+
self.main_window.rubber_band_rect.setRect(rect)
|
131
|
+
|
132
|
+
def handle_bbox_complete(self, pos):
|
133
|
+
"""Handle bbox mode completion in single view."""
|
134
|
+
# Implementation from main_window._scene_mouse_release bbox handling
|
135
|
+
if (
|
136
|
+
hasattr(self.main_window, "rubber_band_rect")
|
137
|
+
and self.main_window.rubber_band_rect
|
138
|
+
):
|
139
|
+
# Remove rubber band
|
140
|
+
self.main_window.viewer.scene().removeItem(
|
141
|
+
self.main_window.rubber_band_rect
|
142
|
+
)
|
143
|
+
self.main_window.rubber_band_rect = None
|
144
|
+
|
145
|
+
# Create polygon from bbox
|
146
|
+
start_pos = self.main_window.drag_start_pos
|
147
|
+
from PyQt6.QtCore import QRectF
|
148
|
+
|
149
|
+
rect = QRectF(start_pos, pos).normalized()
|
150
|
+
if rect.width() > 10 and rect.height() > 10:
|
151
|
+
# Convert to polygon
|
152
|
+
vertices = [
|
153
|
+
[rect.left(), rect.top()],
|
154
|
+
[rect.right(), rect.top()],
|
155
|
+
[rect.right(), rect.bottom()],
|
156
|
+
[rect.left(), rect.bottom()],
|
157
|
+
]
|
158
|
+
|
159
|
+
new_segment = {
|
160
|
+
"vertices": vertices,
|
161
|
+
"type": "Polygon",
|
162
|
+
"mask": None,
|
163
|
+
}
|
164
|
+
|
165
|
+
self.segment_manager.add_segment(new_segment)
|
166
|
+
|
167
|
+
# Record action for undo
|
168
|
+
self.main_window.action_history.append(
|
169
|
+
{
|
170
|
+
"type": "add_segment",
|
171
|
+
"segment_index": len(self.segment_manager.segments) - 1,
|
172
|
+
}
|
173
|
+
)
|
174
|
+
self.main_window.redo_history.clear()
|
175
|
+
|
176
|
+
self.main_window._update_all_lists()
|
177
|
+
|
178
|
+
def display_all_segments(self):
|
179
|
+
"""Display all segments in single view."""
|
180
|
+
# Clear existing segment items
|
181
|
+
for _i, items in self.main_window.segment_items.items():
|
182
|
+
for item in items:
|
183
|
+
if item.scene():
|
184
|
+
self.main_window.viewer.scene().removeItem(item)
|
185
|
+
self.main_window.segment_items.clear()
|
186
|
+
self.main_window._clear_edit_handles()
|
187
|
+
|
188
|
+
# Display segments from segment manager
|
189
|
+
for i, segment in enumerate(self.segment_manager.segments):
|
190
|
+
self.main_window.segment_items[i] = []
|
191
|
+
class_id = segment.get("class_id")
|
192
|
+
base_color = self.main_window._get_color_for_class(class_id)
|
193
|
+
|
194
|
+
if segment["type"] == "Polygon" and segment.get("vertices"):
|
195
|
+
# Convert stored list of lists back to QPointF objects
|
196
|
+
qpoints = [QPointF(p[0], p[1]) for p in segment["vertices"]]
|
197
|
+
|
198
|
+
poly_item = HoverablePolygonItem(QPolygonF(qpoints))
|
199
|
+
default_brush = QBrush(
|
200
|
+
QColor(base_color.red(), base_color.green(), base_color.blue(), 70)
|
201
|
+
)
|
202
|
+
hover_brush = QBrush(
|
203
|
+
QColor(base_color.red(), base_color.green(), base_color.blue(), 170)
|
204
|
+
)
|
205
|
+
poly_item.set_brushes(default_brush, hover_brush)
|
206
|
+
poly_item.setPen(QPen(Qt.GlobalColor.transparent))
|
207
|
+
self.main_window.viewer.scene().addItem(poly_item)
|
208
|
+
self.main_window.segment_items[i].append(poly_item)
|
209
|
+
|
210
|
+
elif segment.get("mask") is not None:
|
211
|
+
default_pixmap = mask_to_pixmap(
|
212
|
+
segment["mask"], base_color.getRgb()[:3], alpha=70
|
213
|
+
)
|
214
|
+
hover_pixmap = mask_to_pixmap(
|
215
|
+
segment["mask"], base_color.getRgb()[:3], alpha=170
|
216
|
+
)
|
217
|
+
pixmap_item = HoverablePixmapItem()
|
218
|
+
pixmap_item.set_pixmaps(default_pixmap, hover_pixmap)
|
219
|
+
self.main_window.viewer.scene().addItem(pixmap_item)
|
220
|
+
pixmap_item.setZValue(i + 1)
|
221
|
+
self.main_window.segment_items[i].append(pixmap_item)
|
222
|
+
|
223
|
+
def clear_all_points(self):
|
224
|
+
"""Clear all temporary points in single view."""
|
225
|
+
if (
|
226
|
+
hasattr(self.main_window, "rubber_band_line")
|
227
|
+
and self.main_window.rubber_band_line
|
228
|
+
):
|
229
|
+
self.main_window.viewer.scene().removeItem(
|
230
|
+
self.main_window.rubber_band_line
|
231
|
+
)
|
232
|
+
self.main_window.rubber_band_line = None
|
233
|
+
|
234
|
+
self.main_window.positive_points.clear()
|
235
|
+
self.main_window.negative_points.clear()
|
236
|
+
|
237
|
+
for item in self.main_window.point_items:
|
238
|
+
self.main_window.viewer.scene().removeItem(item)
|
239
|
+
self.main_window.point_items.clear()
|
240
|
+
|
241
|
+
self.main_window.polygon_points.clear()
|
242
|
+
for item in self.main_window.polygon_preview_items:
|
243
|
+
self.main_window.viewer.scene().removeItem(item)
|
244
|
+
self.main_window.polygon_preview_items.clear()
|
245
|
+
|
246
|
+
# Clear polygon lasso lines
|
247
|
+
if (
|
248
|
+
hasattr(self.main_window, "polygon_lasso_lines")
|
249
|
+
and self.main_window.polygon_lasso_lines
|
250
|
+
):
|
251
|
+
for line in self.main_window.polygon_lasso_lines:
|
252
|
+
if line.scene():
|
253
|
+
self.main_window.viewer.scene().removeItem(line)
|
254
|
+
self.main_window.polygon_lasso_lines.clear()
|
255
|
+
|
256
|
+
if (
|
257
|
+
hasattr(self.main_window, "preview_mask_item")
|
258
|
+
and self.main_window.preview_mask_item
|
259
|
+
):
|
260
|
+
self.main_window.viewer.scene().removeItem(
|
261
|
+
self.main_window.preview_mask_item
|
262
|
+
)
|
263
|
+
self.main_window.preview_mask_item = None
|
264
|
+
|
265
|
+
def _finalize_polygon(self):
|
266
|
+
"""Finalize polygon drawing in single view."""
|
267
|
+
if len(self.main_window.polygon_points) < 3:
|
268
|
+
return
|
269
|
+
|
270
|
+
# Clear lasso lines when finalizing
|
271
|
+
if (
|
272
|
+
hasattr(self.main_window, "polygon_lasso_lines")
|
273
|
+
and self.main_window.polygon_lasso_lines
|
274
|
+
):
|
275
|
+
for line in self.main_window.polygon_lasso_lines:
|
276
|
+
if line.scene():
|
277
|
+
self.main_window.viewer.scene().removeItem(line)
|
278
|
+
self.main_window.polygon_lasso_lines.clear()
|
279
|
+
|
280
|
+
new_segment = {
|
281
|
+
"vertices": [[p.x(), p.y()] for p in self.main_window.polygon_points],
|
282
|
+
"type": "Polygon",
|
283
|
+
"mask": None,
|
284
|
+
}
|
285
|
+
|
286
|
+
self.segment_manager.add_segment(new_segment)
|
287
|
+
|
288
|
+
# Record action for undo
|
289
|
+
self.main_window.action_history.append(
|
290
|
+
{
|
291
|
+
"type": "add_segment",
|
292
|
+
"segment_index": len(self.segment_manager.segments) - 1,
|
293
|
+
}
|
294
|
+
)
|
295
|
+
self.main_window.redo_history.clear()
|
296
|
+
|
297
|
+
self.main_window.polygon_points.clear()
|
298
|
+
self.clear_all_points()
|
299
|
+
self.main_window._update_all_lists()
|
lazylabel/ui/photo_viewer.py
CHANGED
@@ -1,11 +1,15 @@
|
|
1
1
|
import cv2
|
2
2
|
import numpy as np
|
3
|
-
from PyQt6.QtCore import QRectF, Qt
|
3
|
+
from PyQt6.QtCore import QRectF, Qt, pyqtSignal
|
4
4
|
from PyQt6.QtGui import QCursor, QImage, QPixmap
|
5
5
|
from PyQt6.QtWidgets import QGraphicsPixmapItem, QGraphicsScene, QGraphicsView
|
6
6
|
|
7
7
|
|
8
8
|
class PhotoViewer(QGraphicsView):
|
9
|
+
# Signals for multi-view synchronization
|
10
|
+
zoom_changed = pyqtSignal(float) # Emits zoom factor
|
11
|
+
view_changed = pyqtSignal() # Emits when view (pan/zoom) changes
|
12
|
+
|
9
13
|
def __init__(self, parent=None):
|
10
14
|
super().__init__(parent)
|
11
15
|
self._scene = QGraphicsScene(self)
|
@@ -18,6 +22,7 @@ class PhotoViewer(QGraphicsView):
|
|
18
22
|
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
19
23
|
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
20
24
|
self.setDragMode(QGraphicsView.DragMode.NoDrag)
|
25
|
+
self.setMouseTracking(True) # Enable mouse tracking for hover events
|
21
26
|
|
22
27
|
self._original_image = None
|
23
28
|
self._adjusted_pixmap = None
|
@@ -41,6 +46,10 @@ class PhotoViewer(QGraphicsView):
|
|
41
46
|
if pixmap and not pixmap.isNull():
|
42
47
|
self._original_image = pixmap.toImage()
|
43
48
|
self._adjusted_pixmap = pixmap
|
49
|
+
# Check if _pixmap_item still exists, recreate if deleted
|
50
|
+
if self._pixmap_item not in self._scene.items():
|
51
|
+
self._pixmap_item = QGraphicsPixmapItem()
|
52
|
+
self._scene.addItem(self._pixmap_item)
|
44
53
|
self._pixmap_item.setPixmap(pixmap)
|
45
54
|
|
46
55
|
# Convert QImage to ARGB32 for consistent processing
|
@@ -61,12 +70,21 @@ class PhotoViewer(QGraphicsView):
|
|
61
70
|
self._original_image = None
|
62
71
|
self._adjusted_pixmap = None
|
63
72
|
self._original_image_bgr = None
|
73
|
+
# Check if _pixmap_item still exists, recreate if deleted
|
74
|
+
if self._pixmap_item not in self._scene.items():
|
75
|
+
self._pixmap_item = QGraphicsPixmapItem()
|
76
|
+
self._scene.addItem(self._pixmap_item)
|
64
77
|
self._pixmap_item.setPixmap(QPixmap())
|
65
78
|
|
66
79
|
def set_image_adjustments(self, brightness: float, contrast: float, gamma: float):
|
67
|
-
if self._original_image_bgr is None:
|
80
|
+
if self._original_image_bgr is None or self._original_image is None:
|
68
81
|
return
|
69
82
|
|
83
|
+
# Ensure _pixmap_item exists and is valid
|
84
|
+
if self._pixmap_item not in self._scene.items():
|
85
|
+
self._pixmap_item = QGraphicsPixmapItem()
|
86
|
+
self._scene.addItem(self._pixmap_item)
|
87
|
+
|
70
88
|
img_bgr = self._original_image_bgr.copy()
|
71
89
|
|
72
90
|
# Apply brightness and contrast
|
@@ -90,7 +108,10 @@ class PhotoViewer(QGraphicsView):
|
|
90
108
|
adjusted_img.data, w, h, bytes_per_line, QImage.Format.Format_BGR888
|
91
109
|
)
|
92
110
|
self._adjusted_pixmap = QPixmap.fromImage(adjusted_qimage)
|
93
|
-
|
111
|
+
|
112
|
+
# Ensure the pixmap is valid before setting it
|
113
|
+
if not self._adjusted_pixmap.isNull():
|
114
|
+
self._pixmap_item.setPixmap(self._adjusted_pixmap)
|
94
115
|
|
95
116
|
def set_cursor(self, cursor_shape):
|
96
117
|
self.viewport().setCursor(QCursor(cursor_shape))
|
@@ -103,3 +124,10 @@ class PhotoViewer(QGraphicsView):
|
|
103
124
|
if not self._pixmap_item.pixmap().isNull():
|
104
125
|
factor = 1.25 if event.angleDelta().y() > 0 else 0.8
|
105
126
|
self.scale(factor, factor)
|
127
|
+
# Emit zoom signal for multi-view synchronization
|
128
|
+
self.zoom_changed.emit(factor)
|
129
|
+
|
130
|
+
def sync_zoom(self, factor):
|
131
|
+
"""Synchronize zoom from another viewer."""
|
132
|
+
if not self._pixmap_item.pixmap().isNull():
|
133
|
+
self.scale(factor, factor)
|
@@ -0,0 +1,48 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Test script to check hover functionality.
|
4
|
+
Run this before starting LazyLabel to enable debug logging.
|
5
|
+
"""
|
6
|
+
|
7
|
+
# First, enable debug logging
|
8
|
+
import logging
|
9
|
+
import os
|
10
|
+
import sys
|
11
|
+
|
12
|
+
# Add the src directory to path so we can import LazyLabel modules
|
13
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
14
|
+
|
15
|
+
from lazylabel.utils.logger import logger
|
16
|
+
|
17
|
+
# Set logger to DEBUG level
|
18
|
+
logger.setLevel(logging.DEBUG)
|
19
|
+
|
20
|
+
# Add console handler if not already present
|
21
|
+
if not logger.handlers:
|
22
|
+
console_handler = logging.StreamHandler()
|
23
|
+
console_handler.setLevel(logging.DEBUG)
|
24
|
+
formatter = logging.Formatter(
|
25
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
26
|
+
)
|
27
|
+
console_handler.setFormatter(formatter)
|
28
|
+
logger.addHandler(console_handler)
|
29
|
+
|
30
|
+
print("=" * 50)
|
31
|
+
print("HOVER DEBUG MODE ENABLED")
|
32
|
+
print("=" * 50)
|
33
|
+
print("Logger level:", logger.level)
|
34
|
+
print("Logger handlers:", len(logger.handlers))
|
35
|
+
print()
|
36
|
+
print("Now run LazyLabel and test hover functionality:")
|
37
|
+
print("1. Load images in multi-view mode")
|
38
|
+
print("2. Create some segments (AI or polygon)")
|
39
|
+
print("3. Try hovering over segments")
|
40
|
+
print("4. Watch the console for debug messages")
|
41
|
+
print()
|
42
|
+
print("Expected debug messages:")
|
43
|
+
print("- HoverablePolygonItem.set_segment_info")
|
44
|
+
print("- HoverablePixmapItem.set_segment_info")
|
45
|
+
print("- HoverablePolygonItem.hoverEnterEvent")
|
46
|
+
print("- HoverablePixmapItem.hoverEnterEvent")
|
47
|
+
print("- _trigger_segment_hover called")
|
48
|
+
print("=" * 50)
|
@@ -50,9 +50,9 @@ class AdjustmentsWidget(QWidget):
|
|
50
50
|
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
|
51
51
|
)
|
52
52
|
|
53
|
-
# Text edit with
|
53
|
+
# Text edit with width to show numbers like 1.00 and -1.00
|
54
54
|
text_edit = QLineEdit(str(default_value))
|
55
|
-
text_edit.setFixedWidth(35
|
55
|
+
text_edit.setFixedWidth(45) # Increased from 35 to 45
|
56
56
|
|
57
57
|
# Slider takes remaining space
|
58
58
|
slider = QSlider(Qt.Orientation.Horizontal)
|
@@ -188,6 +188,17 @@ class BorderCropWidget(QWidget):
|
|
188
188
|
"""Check if crop coordinates are set."""
|
189
189
|
return self.get_crop_coordinates() is not None
|
190
190
|
|
191
|
+
def disable_thresholding_for_multi_view(self):
|
192
|
+
"""Disable thresholding controls for multi-view mode."""
|
193
|
+
# This method is called when entering multi-view mode
|
194
|
+
# to handle mixed BW/RGB images
|
195
|
+
pass
|
196
|
+
|
197
|
+
def enable_thresholding(self):
|
198
|
+
"""Re-enable thresholding controls when exiting multi-view mode."""
|
199
|
+
# This method is called when exiting multi-view mode
|
200
|
+
pass
|
201
|
+
|
191
202
|
def _get_button_style(self):
|
192
203
|
"""Get consistent button styling."""
|
193
204
|
return """
|
@@ -70,7 +70,12 @@ class MultiIndicatorSlider(QWidget):
|
|
70
70
|
slider_rect = self.get_slider_rect()
|
71
71
|
ratio = (x - slider_rect.left()) / slider_rect.width()
|
72
72
|
ratio = max(0, min(1, ratio)) # Clamp to [0, 1]
|
73
|
-
|
73
|
+
value = self.minimum + ratio * (self.maximum - self.minimum)
|
74
|
+
# Use integer values for channel thresholds (0-255) and intensity sliders, float for frequency sliders (0-10000)
|
75
|
+
if self.maximum <= 255 or self.maximum == 256:
|
76
|
+
return int(value)
|
77
|
+
else:
|
78
|
+
return value
|
74
79
|
|
75
80
|
def paintEvent(self, event):
|
76
81
|
"""Paint the slider."""
|
@@ -138,7 +143,8 @@ class MultiIndicatorSlider(QWidget):
|
|
138
143
|
painter.setPen(QPen(Qt.GlobalColor.transparent))
|
139
144
|
painter.drawRoundedRect(segment_rect, 5, 5)
|
140
145
|
|
141
|
-
# Draw indicators
|
146
|
+
# Draw indicators and collect label positions to avoid overlaps
|
147
|
+
label_positions = []
|
142
148
|
for i, value in enumerate(self.indicators):
|
143
149
|
x = self.value_to_x(value)
|
144
150
|
|
@@ -157,9 +163,47 @@ class MultiIndicatorSlider(QWidget):
|
|
157
163
|
|
158
164
|
painter.drawRoundedRect(handle_rect, 3, 3)
|
159
165
|
|
160
|
-
#
|
161
|
-
|
162
|
-
|
166
|
+
# Store label info for non-overlapping positioning
|
167
|
+
label_text = f"{int(value)}"
|
168
|
+
label_positions.append((x, label_text))
|
169
|
+
|
170
|
+
# Draw labels with overlap prevention
|
171
|
+
self._draw_non_overlapping_labels(painter, slider_rect, label_positions)
|
172
|
+
|
173
|
+
def _draw_non_overlapping_labels(self, painter, slider_rect, label_positions):
|
174
|
+
"""Draw labels with spacing to prevent overlaps."""
|
175
|
+
if not label_positions:
|
176
|
+
return
|
177
|
+
|
178
|
+
painter.setPen(QPen(QColor(255, 255, 255)))
|
179
|
+
|
180
|
+
# Sort by x position
|
181
|
+
sorted_labels = sorted(label_positions, key=lambda item: item[0])
|
182
|
+
|
183
|
+
# Minimum spacing between labels (in pixels)
|
184
|
+
min_spacing = 30
|
185
|
+
|
186
|
+
# Adjust positions to prevent overlaps
|
187
|
+
adjusted_positions = []
|
188
|
+
for i, (x, text) in enumerate(sorted_labels):
|
189
|
+
if i == 0:
|
190
|
+
adjusted_positions.append((x, text))
|
191
|
+
else:
|
192
|
+
prev_x = adjusted_positions[-1][0]
|
193
|
+
if x - prev_x < min_spacing:
|
194
|
+
# Move this label to maintain minimum spacing
|
195
|
+
new_x = prev_x + min_spacing
|
196
|
+
# But don't go beyond the slider bounds
|
197
|
+
slider_right = slider_rect.right() - 15
|
198
|
+
if new_x > slider_right:
|
199
|
+
new_x = slider_right
|
200
|
+
adjusted_positions.append((new_x, text))
|
201
|
+
else:
|
202
|
+
adjusted_positions.append((x, text))
|
203
|
+
|
204
|
+
# Draw the adjusted labels
|
205
|
+
for x, text in adjusted_positions:
|
206
|
+
painter.drawText(int(x - 15), slider_rect.bottom() + 15, text)
|
163
207
|
|
164
208
|
def mousePressEvent(self, event):
|
165
209
|
"""Handle mouse press events."""
|
@@ -352,7 +396,7 @@ class ChannelThresholdWidget(QWidget):
|
|
352
396
|
|
353
397
|
# Instructions
|
354
398
|
instructions = QLabel(
|
355
|
-
"✓ Check to enable
|
399
|
+
"✓ Check to enable\n• Double-click to add threshold\n• Right-click to remove"
|
356
400
|
)
|
357
401
|
instructions.setStyleSheet("color: #888; font-size: 9px;")
|
358
402
|
instructions.setWordWrap(True)
|