imagebaker 0.0.41__py3-none-any.whl → 0.0.48__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.
- imagebaker/__init__.py +1 -1
- imagebaker/core/__init__.py +0 -0
- imagebaker/core/configs/__init__.py +1 -0
- imagebaker/core/configs/configs.py +156 -0
- imagebaker/core/defs/__init__.py +1 -0
- imagebaker/core/defs/defs.py +258 -0
- imagebaker/core/plugins/__init__.py +0 -0
- imagebaker/core/plugins/base_plugin.py +39 -0
- imagebaker/core/plugins/cosine_plugin.py +39 -0
- imagebaker/layers/__init__.py +3 -0
- imagebaker/layers/annotable_layer.py +847 -0
- imagebaker/layers/base_layer.py +724 -0
- imagebaker/layers/canvas_layer.py +1007 -0
- imagebaker/list_views/__init__.py +3 -0
- imagebaker/list_views/annotation_list.py +203 -0
- imagebaker/list_views/canvas_list.py +185 -0
- imagebaker/list_views/image_list.py +138 -0
- imagebaker/list_views/layer_list.py +390 -0
- imagebaker/list_views/layer_settings.py +219 -0
- imagebaker/models/__init__.py +0 -0
- imagebaker/models/base_model.py +150 -0
- imagebaker/tabs/__init__.py +2 -0
- imagebaker/tabs/baker_tab.py +496 -0
- imagebaker/tabs/layerify_tab.py +837 -0
- imagebaker/utils/__init__.py +0 -0
- imagebaker/utils/image.py +105 -0
- imagebaker/utils/state_utils.py +92 -0
- imagebaker/utils/transform_mask.py +107 -0
- imagebaker/window/__init__.py +1 -0
- imagebaker/window/app.py +136 -0
- imagebaker/window/main_window.py +181 -0
- imagebaker/workers/__init__.py +3 -0
- imagebaker/workers/baker_worker.py +247 -0
- imagebaker/workers/layerify_worker.py +91 -0
- imagebaker/workers/model_worker.py +54 -0
- {imagebaker-0.0.41.dist-info → imagebaker-0.0.48.dist-info}/METADATA +6 -6
- imagebaker-0.0.48.dist-info/RECORD +41 -0
- {imagebaker-0.0.41.dist-info → imagebaker-0.0.48.dist-info}/WHEEL +1 -1
- imagebaker-0.0.41.dist-info/RECORD +0 -7
- {imagebaker-0.0.41.dist-info/licenses → imagebaker-0.0.48.dist-info}/LICENSE +0 -0
- {imagebaker-0.0.41.dist-info → imagebaker-0.0.48.dist-info}/entry_points.txt +0 -0
- {imagebaker-0.0.41.dist-info → imagebaker-0.0.48.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,847 @@
|
|
1
|
+
from imagebaker.core.configs import LayerConfig, CursorDef, CanvasConfig
|
2
|
+
from imagebaker.core.defs import Annotation, MouseMode
|
3
|
+
from imagebaker.layers import BaseLayer
|
4
|
+
from imagebaker.layers.canvas_layer import CanvasLayer
|
5
|
+
from imagebaker import logger
|
6
|
+
from imagebaker.workers import LayerifyWorker
|
7
|
+
|
8
|
+
from PySide6.QtCore import (
|
9
|
+
QPointF,
|
10
|
+
QPoint,
|
11
|
+
Qt,
|
12
|
+
Signal,
|
13
|
+
QRectF,
|
14
|
+
QLineF,
|
15
|
+
QThread,
|
16
|
+
)
|
17
|
+
from PySide6.QtGui import (
|
18
|
+
QColor,
|
19
|
+
QPixmap,
|
20
|
+
QPainter,
|
21
|
+
QBrush,
|
22
|
+
QPen,
|
23
|
+
QPolygonF,
|
24
|
+
QWheelEvent,
|
25
|
+
QMouseEvent,
|
26
|
+
QKeyEvent,
|
27
|
+
)
|
28
|
+
from PySide6.QtWidgets import (
|
29
|
+
QApplication,
|
30
|
+
QInputDialog,
|
31
|
+
QSizePolicy,
|
32
|
+
QMessageBox,
|
33
|
+
QProgressDialog,
|
34
|
+
)
|
35
|
+
from pathlib import Path
|
36
|
+
|
37
|
+
|
38
|
+
class AnnotableLayer(BaseLayer):
|
39
|
+
annotationAdded = Signal(Annotation)
|
40
|
+
annotationRemoved = Signal()
|
41
|
+
annotationUpdated = Signal(Annotation)
|
42
|
+
annotationCleared = Signal()
|
43
|
+
annotationMoved = Signal()
|
44
|
+
layersChanged = Signal()
|
45
|
+
|
46
|
+
def __init__(self, parent, config: LayerConfig, canvas_config: CanvasConfig):
|
47
|
+
super().__init__(parent, config)
|
48
|
+
self.canvas_config = canvas_config
|
49
|
+
|
50
|
+
self.image = QPixmap()
|
51
|
+
self.mouse_mode = MouseMode.POINT
|
52
|
+
|
53
|
+
self.label_rects = []
|
54
|
+
self.file_path: Path = Path("Runtime")
|
55
|
+
self.layers: list[BaseLayer] = []
|
56
|
+
self.is_annotable = True
|
57
|
+
|
58
|
+
def init_ui(self):
|
59
|
+
logger.info(f"Initializing Layer UI of {self.layer_name}")
|
60
|
+
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
61
|
+
|
62
|
+
def clear_annotations(self):
|
63
|
+
self.annotations.clear()
|
64
|
+
self.selected_annotation = None
|
65
|
+
self.current_annotation = None
|
66
|
+
self.annotationCleared.emit()
|
67
|
+
self.update()
|
68
|
+
|
69
|
+
def handle_key_press(self, event: QKeyEvent):
|
70
|
+
# Handle Ctrl key for panning
|
71
|
+
if event.key() == Qt.Key_Control:
|
72
|
+
if (
|
73
|
+
self.mouse_mode != MouseMode.POLYGON
|
74
|
+
): # Only activate pan mode when not drawing polygons
|
75
|
+
|
76
|
+
self.mouse_mode = MouseMode.PAN
|
77
|
+
|
78
|
+
# Handle Ctrl+C for copy
|
79
|
+
if event.modifiers() & Qt.ControlModifier and event.key() == Qt.Key_C:
|
80
|
+
self._copy_annotation()
|
81
|
+
|
82
|
+
# Handle Ctrl+V for paste
|
83
|
+
if event.modifiers() & Qt.ControlModifier and event.key() == Qt.Key_V:
|
84
|
+
self._paste_annotation()
|
85
|
+
|
86
|
+
def handle_key_release(self, event):
|
87
|
+
if event.key() == Qt.Key_Control:
|
88
|
+
if self.mouse_mode == MouseMode.PAN:
|
89
|
+
self.mouse_mode = MouseMode.IDLE
|
90
|
+
|
91
|
+
def apply_opacity(self):
|
92
|
+
"""Apply opacity to the QPixmap image."""
|
93
|
+
if self.image and self.opacity < 255:
|
94
|
+
# Create a new transparent pixmap with the same size
|
95
|
+
transparent_pixmap = QPixmap(self.image.size())
|
96
|
+
transparent_pixmap.fill(Qt.transparent)
|
97
|
+
|
98
|
+
# Create a painter to draw on the new pixmap
|
99
|
+
painter = QPainter(transparent_pixmap)
|
100
|
+
try:
|
101
|
+
# Set the opacity
|
102
|
+
painter.setOpacity(self.opacity / 255.0)
|
103
|
+
|
104
|
+
# Draw the original image onto the new pixmap
|
105
|
+
painter.drawPixmap(0, 0, self.image)
|
106
|
+
finally:
|
107
|
+
# Ensure the painter is properly ended
|
108
|
+
painter.end()
|
109
|
+
|
110
|
+
# Replace the original image with the transparent version
|
111
|
+
self.image = transparent_pixmap
|
112
|
+
|
113
|
+
def paint_layer(self, painter: QPainter):
|
114
|
+
with QPainter(self) as painter:
|
115
|
+
painter.fillRect(
|
116
|
+
self.rect(),
|
117
|
+
self.config.normal_draw_config.background_color,
|
118
|
+
)
|
119
|
+
painter.setRenderHints(
|
120
|
+
QPainter.Antialiasing | QPainter.SmoothPixmapTransform
|
121
|
+
)
|
122
|
+
|
123
|
+
if not self.image.isNull():
|
124
|
+
painter.save()
|
125
|
+
painter.translate(self.offset)
|
126
|
+
painter.scale(self.scale, self.scale)
|
127
|
+
painter.drawPixmap(0, 0, self.image)
|
128
|
+
|
129
|
+
# Draw all annotations
|
130
|
+
for annotation in self.annotations:
|
131
|
+
self.draw_annotation(painter, annotation)
|
132
|
+
|
133
|
+
# Draw current annotation
|
134
|
+
if self.current_annotation:
|
135
|
+
self.draw_annotation(painter, self.current_annotation, is_temp=True)
|
136
|
+
|
137
|
+
painter.restore()
|
138
|
+
|
139
|
+
def draw_annotation(self, painter, annotation: Annotation, is_temp=False):
|
140
|
+
"""
|
141
|
+
Draw annotation on the image.
|
142
|
+
"""
|
143
|
+
if not annotation.visible:
|
144
|
+
return
|
145
|
+
painter.save()
|
146
|
+
base_color = annotation.color
|
147
|
+
pen_color = QColor(
|
148
|
+
base_color.red(),
|
149
|
+
base_color.green(),
|
150
|
+
base_color.blue(),
|
151
|
+
self.config.normal_draw_config.pen_alpha,
|
152
|
+
)
|
153
|
+
brush_color = QColor(
|
154
|
+
base_color.red(),
|
155
|
+
base_color.green(),
|
156
|
+
base_color.blue(),
|
157
|
+
self.config.normal_draw_config.brush_alpha,
|
158
|
+
)
|
159
|
+
|
160
|
+
pen = QPen(pen_color, self.config.normal_draw_config.line_width)
|
161
|
+
brush = QBrush(brush_color, Qt.DiagCrossPattern)
|
162
|
+
|
163
|
+
if annotation.selected:
|
164
|
+
painter.setPen(
|
165
|
+
QPen(
|
166
|
+
self.config.selected_draw_config.color,
|
167
|
+
self.config.selected_draw_config.line_width,
|
168
|
+
)
|
169
|
+
)
|
170
|
+
painter.setBrush(
|
171
|
+
QBrush(
|
172
|
+
QColor(
|
173
|
+
self.config.selected_draw_config.color.red(),
|
174
|
+
self.config.selected_draw_config.color.green(),
|
175
|
+
self.config.selected_draw_config.color.blue(),
|
176
|
+
self.config.selected_draw_config.brush_alpha,
|
177
|
+
)
|
178
|
+
)
|
179
|
+
)
|
180
|
+
if annotation.rectangle:
|
181
|
+
painter.drawRect(annotation.rectangle)
|
182
|
+
elif annotation.polygon:
|
183
|
+
painter.drawPolygon(annotation.polygon)
|
184
|
+
elif annotation.points:
|
185
|
+
painter.drawEllipse(
|
186
|
+
annotation.points[0],
|
187
|
+
self.config.selected_draw_config.ellipse_size,
|
188
|
+
self.config.selected_draw_config.ellipse_size,
|
189
|
+
)
|
190
|
+
|
191
|
+
if is_temp:
|
192
|
+
pen.setStyle(Qt.DashLine)
|
193
|
+
brush.setStyle(Qt.Dense4Pattern)
|
194
|
+
|
195
|
+
painter.setPen(pen)
|
196
|
+
painter.setBrush(brush)
|
197
|
+
|
198
|
+
# Draw main shape
|
199
|
+
if annotation.points:
|
200
|
+
for point in annotation.points:
|
201
|
+
painter.drawEllipse(
|
202
|
+
point,
|
203
|
+
self.config.normal_draw_config.point_size,
|
204
|
+
self.config.normal_draw_config.point_size,
|
205
|
+
)
|
206
|
+
elif annotation.rectangle:
|
207
|
+
painter.drawRect(annotation.rectangle)
|
208
|
+
elif annotation.polygon:
|
209
|
+
if len(annotation.polygon) > 1:
|
210
|
+
if annotation.is_complete:
|
211
|
+
painter.drawPolygon(annotation.polygon)
|
212
|
+
else:
|
213
|
+
painter.drawPolyline(annotation.polygon)
|
214
|
+
|
215
|
+
# Draw control points
|
216
|
+
if annotation.rectangle:
|
217
|
+
rect = annotation.rectangle
|
218
|
+
corners = [
|
219
|
+
rect.topLeft(),
|
220
|
+
rect.topRight(),
|
221
|
+
rect.bottomLeft(),
|
222
|
+
rect.bottomRight(),
|
223
|
+
]
|
224
|
+
painter.save()
|
225
|
+
painter.setPen(
|
226
|
+
QPen(Qt.black, self.config.normal_draw_config.control_point_size)
|
227
|
+
)
|
228
|
+
painter.setBrush(QBrush(Qt.white))
|
229
|
+
for corner in corners:
|
230
|
+
painter.drawEllipse(
|
231
|
+
corner,
|
232
|
+
self.config.normal_draw_config.point_size,
|
233
|
+
self.config.normal_draw_config.point_size,
|
234
|
+
)
|
235
|
+
painter.restore()
|
236
|
+
|
237
|
+
if annotation.polygon and len(annotation.polygon) > 0:
|
238
|
+
painter.save()
|
239
|
+
painter.setPen(
|
240
|
+
QPen(Qt.white, self.config.normal_draw_config.control_point_size)
|
241
|
+
)
|
242
|
+
painter.setBrush(QBrush(Qt.darkGray))
|
243
|
+
for point in annotation.polygon:
|
244
|
+
painter.drawEllipse(
|
245
|
+
point,
|
246
|
+
self.config.normal_draw_config.point_size,
|
247
|
+
self.config.normal_draw_config.point_size,
|
248
|
+
)
|
249
|
+
painter.restore()
|
250
|
+
|
251
|
+
# Draw labels
|
252
|
+
if annotation.is_complete and annotation.label:
|
253
|
+
painter.save()
|
254
|
+
label_pos = self.get_label_position(annotation)
|
255
|
+
text = annotation.label
|
256
|
+
|
257
|
+
# Convert to widget coordinates
|
258
|
+
widget_pos = QPointF(
|
259
|
+
label_pos.x() * self.scale + self.offset.x(),
|
260
|
+
label_pos.y() * self.scale + self.offset.y(),
|
261
|
+
)
|
262
|
+
|
263
|
+
if annotation.points:
|
264
|
+
widget_pos += QPointF(10, 10)
|
265
|
+
|
266
|
+
# Set up font
|
267
|
+
font = painter.font()
|
268
|
+
font.setPixelSize(
|
269
|
+
self.config.normal_draw_config.label_font_size
|
270
|
+
) # Fixed screen size
|
271
|
+
painter.setFont(font)
|
272
|
+
|
273
|
+
# Calculate text size
|
274
|
+
metrics = painter.fontMetrics()
|
275
|
+
text_width = metrics.horizontalAdvance(text)
|
276
|
+
text_height = metrics.height()
|
277
|
+
|
278
|
+
# Draw background
|
279
|
+
bg_rect = QRectF(
|
280
|
+
widget_pos.x() - text_width / 2 - 2,
|
281
|
+
widget_pos.y() - text_height / 2 - 2,
|
282
|
+
text_width + 4,
|
283
|
+
text_height + 4,
|
284
|
+
)
|
285
|
+
painter.resetTransform()
|
286
|
+
painter.setBrush(self.config.normal_draw_config.label_font_background_color)
|
287
|
+
painter.setPen(Qt.NoPen)
|
288
|
+
painter.drawRect(bg_rect)
|
289
|
+
|
290
|
+
# Draw text
|
291
|
+
painter.setPen(Qt.white)
|
292
|
+
painter.drawText(bg_rect, Qt.AlignCenter, text)
|
293
|
+
painter.restore()
|
294
|
+
self.label_rects.append((bg_rect, annotation))
|
295
|
+
|
296
|
+
painter.restore()
|
297
|
+
|
298
|
+
# Draw transformation handles for selected annotations
|
299
|
+
if annotation.selected and annotation.is_complete:
|
300
|
+
painter.save()
|
301
|
+
handle_color = self.config.selected_draw_config.handle_color
|
302
|
+
painter.setPen(
|
303
|
+
QPen(handle_color, self.config.selected_draw_config.handle_width)
|
304
|
+
)
|
305
|
+
painter.setBrush(QBrush(handle_color))
|
306
|
+
|
307
|
+
if annotation.rectangle:
|
308
|
+
rect = annotation.rectangle
|
309
|
+
# Draw corner handles
|
310
|
+
for corner in [
|
311
|
+
rect.topLeft(),
|
312
|
+
rect.topRight(),
|
313
|
+
rect.bottomLeft(),
|
314
|
+
rect.bottomRight(),
|
315
|
+
]:
|
316
|
+
painter.drawEllipse(
|
317
|
+
corner,
|
318
|
+
self.config.selected_draw_config.handle_point_size,
|
319
|
+
self.config.selected_draw_config.handle_point_size,
|
320
|
+
)
|
321
|
+
# Draw edge handles
|
322
|
+
for edge in [
|
323
|
+
QPointF(rect.center().x(), rect.top()),
|
324
|
+
QPointF(rect.center().x(), rect.bottom()),
|
325
|
+
QPointF(rect.left(), rect.center().y()),
|
326
|
+
QPointF(rect.right(), rect.center().y()),
|
327
|
+
]:
|
328
|
+
painter.drawEllipse(
|
329
|
+
edge,
|
330
|
+
self.config.selected_draw_config.handle_edge_size,
|
331
|
+
self.config.selected_draw_config.handle_edge_size,
|
332
|
+
)
|
333
|
+
|
334
|
+
elif annotation.polygon:
|
335
|
+
# Draw vertex handles
|
336
|
+
for point in annotation.polygon:
|
337
|
+
painter.drawEllipse(
|
338
|
+
point,
|
339
|
+
self.config.selected_draw_config.handle_point_size,
|
340
|
+
self.config.selected_draw_config.handle_point_size,
|
341
|
+
)
|
342
|
+
|
343
|
+
painter.restore()
|
344
|
+
|
345
|
+
def get_label_position(self, annotation: Annotation):
|
346
|
+
if annotation.points:
|
347
|
+
return annotation.points[0]
|
348
|
+
if annotation.rectangle:
|
349
|
+
return annotation.rectangle.center()
|
350
|
+
if annotation.polygon:
|
351
|
+
return annotation.polygon.boundingRect().center()
|
352
|
+
return QPointF()
|
353
|
+
|
354
|
+
def handle_wheel(self, event: QWheelEvent):
|
355
|
+
if event.modifiers() & Qt.ControlModifier:
|
356
|
+
# Get mouse position before zoom
|
357
|
+
old_pos = self.widget_to_image_pos(event.position())
|
358
|
+
|
359
|
+
# Calculate zoom factor
|
360
|
+
zoom_factor = (
|
361
|
+
self.config.zoom_in_factor
|
362
|
+
if event.angleDelta().y() > 0
|
363
|
+
else self.config.zoom_out_factor
|
364
|
+
)
|
365
|
+
new_scale = max(0.1, min(self.scale * zoom_factor, 10.0))
|
366
|
+
|
367
|
+
# Calculate position shift to keep cursor over same image point
|
368
|
+
self.offset += old_pos * self.scale - old_pos * new_scale
|
369
|
+
self.scale = new_scale
|
370
|
+
|
371
|
+
# is wheel going forward or backward
|
372
|
+
if event.angleDelta().y() > 0:
|
373
|
+
self.mouse_mode = MouseMode.ZOOM_IN
|
374
|
+
else:
|
375
|
+
self.mouse_mode = MouseMode.ZOOM_OUT
|
376
|
+
|
377
|
+
self.zoomChanged.emit(self.scale)
|
378
|
+
|
379
|
+
def handle_mouse_release(self, event: QMouseEvent):
|
380
|
+
if event.button() == Qt.LeftButton:
|
381
|
+
if self.mouse_mode == MouseMode.RECTANGLE and self.current_annotation:
|
382
|
+
self.finalize_annotation()
|
383
|
+
elif self.mouse_mode == MouseMode.POLYGON and self.current_annotation:
|
384
|
+
pass
|
385
|
+
elif self.mouse_mode in [
|
386
|
+
MouseMode.PAN,
|
387
|
+
MouseMode.ZOOM_IN,
|
388
|
+
MouseMode.ZOOM_OUT,
|
389
|
+
]:
|
390
|
+
self.mouse_mode = MouseMode.IDLE
|
391
|
+
|
392
|
+
# Clean up transformation state
|
393
|
+
if hasattr(self, "selected_annotation"):
|
394
|
+
self.selected_annotation = None
|
395
|
+
if hasattr(self, "active_handle"):
|
396
|
+
del self.active_handle
|
397
|
+
if hasattr(self, "active_point_index"):
|
398
|
+
del self.active_point_index
|
399
|
+
if hasattr(self, "initial_rect"):
|
400
|
+
del self.initial_rect
|
401
|
+
if hasattr(self, "initial_polygon"):
|
402
|
+
del self.initial_polygon
|
403
|
+
|
404
|
+
self.pan_start = None
|
405
|
+
self.drag_start = None
|
406
|
+
|
407
|
+
def handle_mouse_move(self, event: QMouseEvent):
|
408
|
+
# logger.info(f"Mouse move event: {event.position()} with {self.mouse_mode}")
|
409
|
+
img_pos = self.widget_to_image_pos(event.position())
|
410
|
+
clamped_pos = QPointF(
|
411
|
+
max(0, min(self.image.width(), img_pos.x())),
|
412
|
+
max(0, min(self.image.height(), img_pos.y())),
|
413
|
+
)
|
414
|
+
self.mouseMoved.emit(img_pos)
|
415
|
+
self.messageSignal.emit(f"X: {img_pos.x()}, Y: {img_pos.y()}")
|
416
|
+
|
417
|
+
# if we are not clicking
|
418
|
+
if not event.buttons():
|
419
|
+
annotation, handle = self.find_annotation_and_handle_at(img_pos)
|
420
|
+
if annotation and handle and self.mouse_mode == MouseMode.IDLE:
|
421
|
+
if "point_" in handle or handle in [
|
422
|
+
"top_left",
|
423
|
+
"top_right",
|
424
|
+
"bottom_left",
|
425
|
+
"bottom_right",
|
426
|
+
]:
|
427
|
+
self.mouse_mode = MouseMode.RESIZE
|
428
|
+
elif "center" in handle:
|
429
|
+
if "top" in handle or "bottom" in handle:
|
430
|
+
self.mouse_mode = MouseMode.RESIZE_HEIGHT
|
431
|
+
else:
|
432
|
+
self.mouse_mode = MouseMode.RESIZE_WIDTH
|
433
|
+
elif handle == "move":
|
434
|
+
self.mouse_mode = MouseMode.GRAB
|
435
|
+
|
436
|
+
elif not handle and self.mouse_mode in [
|
437
|
+
MouseMode.RESIZE,
|
438
|
+
MouseMode.RESIZE_HEIGHT,
|
439
|
+
MouseMode.RESIZE_WIDTH,
|
440
|
+
MouseMode.GRAB,
|
441
|
+
]:
|
442
|
+
self.mouse_mode = MouseMode.IDLE
|
443
|
+
# self.mouse_mode = MouseMode.IDLE
|
444
|
+
pass
|
445
|
+
self.update_cursor()
|
446
|
+
else:
|
447
|
+
if (
|
448
|
+
event.buttons() & Qt.LeftButton
|
449
|
+
and self.selected_annotation
|
450
|
+
and self.active_handle
|
451
|
+
):
|
452
|
+
if self.active_handle == "move":
|
453
|
+
self.setCursor(CursorDef.GRABBING_CURSOR)
|
454
|
+
new_pos = img_pos - self.drag_offset
|
455
|
+
self.move_annotation(self.selected_annotation, new_pos)
|
456
|
+
elif self.selected_annotation.rectangle:
|
457
|
+
rect = QRectF(self.initial_rect)
|
458
|
+
|
459
|
+
if "top" in self.active_handle:
|
460
|
+
rect.setTop(img_pos.y())
|
461
|
+
if "bottom" in self.active_handle:
|
462
|
+
rect.setBottom(img_pos.y())
|
463
|
+
if "left" in self.active_handle:
|
464
|
+
rect.setLeft(img_pos.x())
|
465
|
+
if "right" in self.active_handle:
|
466
|
+
rect.setRight(img_pos.x())
|
467
|
+
|
468
|
+
self.selected_annotation.rectangle = rect.normalized()
|
469
|
+
elif self.selected_annotation.polygon and hasattr(
|
470
|
+
self, "active_point_index"
|
471
|
+
):
|
472
|
+
self.selected_annotation.polygon[self.active_point_index] = (
|
473
|
+
clamped_pos
|
474
|
+
)
|
475
|
+
self.annotationMoved.emit()
|
476
|
+
self.annotationUpdated.emit(self.selected_annotation)
|
477
|
+
self.update()
|
478
|
+
return
|
479
|
+
if self.mouse_mode == MouseMode.PAN and event.buttons() & Qt.LeftButton:
|
480
|
+
if self.pan_start:
|
481
|
+
delta = event.position() - self.pan_start
|
482
|
+
self.offset += delta
|
483
|
+
self.pan_start = event.position()
|
484
|
+
self.update()
|
485
|
+
elif self.mouse_mode == MouseMode.RECTANGLE and self.drag_start:
|
486
|
+
self.current_annotation.rectangle = QRectF(
|
487
|
+
self.drag_start, clamped_pos
|
488
|
+
).normalized()
|
489
|
+
self.update()
|
490
|
+
elif self.mouse_mode == MouseMode.POLYGON and self.current_annotation:
|
491
|
+
if self.current_annotation.polygon:
|
492
|
+
temp_points = QPolygonF(self.current_annotation.polygon)
|
493
|
+
if temp_points:
|
494
|
+
temp_points[-1] = clamped_pos
|
495
|
+
self.current_annotation.polygon = temp_points
|
496
|
+
self.update()
|
497
|
+
|
498
|
+
def move_annotation(self, annotation, new_pos: QPointF):
|
499
|
+
delta = new_pos - self.get_annotation_position(annotation)
|
500
|
+
|
501
|
+
if annotation.rectangle:
|
502
|
+
annotation.rectangle.translate(delta)
|
503
|
+
elif annotation.polygon:
|
504
|
+
annotation.polygon.translate(delta)
|
505
|
+
elif annotation.points:
|
506
|
+
annotation.points = [p + delta for p in annotation.points]
|
507
|
+
|
508
|
+
def handle_mouse_press(self, event: QMouseEvent):
|
509
|
+
# logger.info(f"Mouse press event: {event.position()} with {self.mouse_mode}")
|
510
|
+
img_pos = self.widget_to_image_pos(event.position())
|
511
|
+
clamped_pos = QPointF(
|
512
|
+
max(0, min(self.image.width(), img_pos.x())),
|
513
|
+
max(0, min(self.image.height(), img_pos.y())),
|
514
|
+
)
|
515
|
+
|
516
|
+
# If right-clicked
|
517
|
+
if event.button() == Qt.RightButton:
|
518
|
+
# If polygon drawing, remove the last point
|
519
|
+
if self.current_annotation and self.mouse_mode == MouseMode.POLYGON:
|
520
|
+
if len(self.current_annotation.polygon) > 0:
|
521
|
+
self.current_annotation.polygon = QPolygonF(
|
522
|
+
[p for p in self.current_annotation.polygon][:-1]
|
523
|
+
)
|
524
|
+
self.update()
|
525
|
+
|
526
|
+
# If the polygon is now empty, reset to idle mode
|
527
|
+
if len(self.current_annotation.polygon) == 0:
|
528
|
+
self.current_annotation = None
|
529
|
+
self.mouse_mode = MouseMode.IDLE
|
530
|
+
self.update()
|
531
|
+
|
532
|
+
# If not drawing a polygon, go to idle mode
|
533
|
+
if not self.current_annotation:
|
534
|
+
self.mouse_mode = MouseMode.IDLE
|
535
|
+
for ann in self.annotations:
|
536
|
+
ann.selected = False
|
537
|
+
self.update()
|
538
|
+
|
539
|
+
# If left-clicked
|
540
|
+
if event.button() == Qt.LeftButton:
|
541
|
+
self.selected_annotation, self.active_handle = (
|
542
|
+
self.find_annotation_and_handle_at(img_pos)
|
543
|
+
)
|
544
|
+
# Handle dragging later on
|
545
|
+
if self.selected_annotation:
|
546
|
+
self.drag_offset = img_pos - self.get_annotation_position(
|
547
|
+
self.selected_annotation
|
548
|
+
)
|
549
|
+
self.selected_annotation.selected = True
|
550
|
+
|
551
|
+
# Make all other annotations unselected
|
552
|
+
for ann in self.annotations:
|
553
|
+
if ann != self.selected_annotation:
|
554
|
+
ann.selected = False
|
555
|
+
self.annotationUpdated.emit(ann)
|
556
|
+
|
557
|
+
if self.selected_annotation.rectangle:
|
558
|
+
self.initial_rect = QRectF(self.selected_annotation.rectangle)
|
559
|
+
elif self.selected_annotation.polygon:
|
560
|
+
self.initial_polygon = QPolygonF(self.selected_annotation.polygon)
|
561
|
+
if "point_" in self.active_handle:
|
562
|
+
self.active_point = int(self.active_handle.split("_")[1])
|
563
|
+
|
564
|
+
# If pan mode
|
565
|
+
if self.mouse_mode == MouseMode.PAN:
|
566
|
+
self.pan_start = event.position()
|
567
|
+
return
|
568
|
+
|
569
|
+
# If drawing mode
|
570
|
+
if self.mouse_mode == MouseMode.POINT:
|
571
|
+
self.current_annotation = Annotation(
|
572
|
+
label=self.current_label,
|
573
|
+
annotation_id=len(self.annotations),
|
574
|
+
points=[clamped_pos],
|
575
|
+
)
|
576
|
+
self.finalize_annotation()
|
577
|
+
elif self.mouse_mode == MouseMode.RECTANGLE:
|
578
|
+
# The incomplete annotation
|
579
|
+
self.current_annotation = Annotation(
|
580
|
+
file_path=self.file_path,
|
581
|
+
annotation_id=len(self.annotations),
|
582
|
+
label="Incomplete",
|
583
|
+
color=self.current_color,
|
584
|
+
rectangle=QRectF(clamped_pos, clamped_pos),
|
585
|
+
)
|
586
|
+
self.drag_start = clamped_pos
|
587
|
+
elif self.mouse_mode == MouseMode.POLYGON:
|
588
|
+
# If not double-click
|
589
|
+
if not self.current_annotation:
|
590
|
+
self.current_annotation = Annotation(
|
591
|
+
file_path=self.file_path,
|
592
|
+
annotation_id=len(self.annotations),
|
593
|
+
label="Incomplete",
|
594
|
+
color=self.current_color,
|
595
|
+
polygon=QPolygonF([clamped_pos]),
|
596
|
+
)
|
597
|
+
else:
|
598
|
+
logger.info(f"Adding point to polygon: {clamped_pos}")
|
599
|
+
# Add point to polygon
|
600
|
+
self.current_annotation.polygon.append(clamped_pos)
|
601
|
+
|
602
|
+
self.update()
|
603
|
+
|
604
|
+
def get_annotation_position(self, annotation: Annotation):
|
605
|
+
if annotation.rectangle:
|
606
|
+
return annotation.rectangle.center()
|
607
|
+
elif annotation.polygon:
|
608
|
+
return annotation.polygon.boundingRect().center()
|
609
|
+
elif annotation.points:
|
610
|
+
return annotation.points[0]
|
611
|
+
return QPointF()
|
612
|
+
|
613
|
+
def find_annotation_and_handle_at(self, pos: QPointF, margin=10.0):
|
614
|
+
"""Find annotation and specific handle at given position"""
|
615
|
+
for annotation in reversed(self.annotations):
|
616
|
+
if not annotation.visible or not annotation.is_complete:
|
617
|
+
continue
|
618
|
+
|
619
|
+
# Check rectangle handles
|
620
|
+
if annotation.rectangle:
|
621
|
+
rect = annotation.rectangle
|
622
|
+
handles = {
|
623
|
+
"top_left": rect.topLeft(),
|
624
|
+
"top_right": rect.topRight(),
|
625
|
+
"bottom_left": rect.bottomLeft(),
|
626
|
+
"bottom_right": rect.bottomRight(),
|
627
|
+
"top_center": QPointF(rect.center().x(), rect.top()),
|
628
|
+
"bottom_center": QPointF(rect.center().x(), rect.bottom()),
|
629
|
+
"left_center": QPointF(rect.left(), rect.center().y()),
|
630
|
+
"right_center": QPointF(rect.right(), rect.center().y()),
|
631
|
+
}
|
632
|
+
|
633
|
+
for handle_name, handle_pos in handles.items():
|
634
|
+
if (handle_pos - pos).manhattanLength() < margin:
|
635
|
+
return annotation, handle_name
|
636
|
+
|
637
|
+
if rect.contains(pos):
|
638
|
+
return annotation, "move"
|
639
|
+
|
640
|
+
# Check polygon points
|
641
|
+
elif annotation.polygon:
|
642
|
+
for i, point in enumerate(annotation.polygon):
|
643
|
+
if (point - pos).manhattanLength() < margin:
|
644
|
+
return annotation, f"point_{i}"
|
645
|
+
|
646
|
+
if annotation.polygon.containsPoint(pos, Qt.OddEvenFill):
|
647
|
+
return annotation, "move"
|
648
|
+
|
649
|
+
return None, None
|
650
|
+
|
651
|
+
def handle_mouse_double_click(self, event: QMouseEvent, pos: QPoint):
|
652
|
+
pos = event.position()
|
653
|
+
for rect, annotation in self.label_rects:
|
654
|
+
if rect.contains(pos):
|
655
|
+
self.edit_annotation_label(annotation)
|
656
|
+
break
|
657
|
+
# if left double click
|
658
|
+
if event.button() == Qt.LeftButton:
|
659
|
+
# if drawing a polygon, close the polygon
|
660
|
+
if (
|
661
|
+
self.current_annotation
|
662
|
+
and self.mouse_mode == MouseMode.POLYGON
|
663
|
+
and len(self.current_annotation.polygon) >= 3
|
664
|
+
):
|
665
|
+
self.current_annotation.is_complete = True
|
666
|
+
self.finalize_annotation()
|
667
|
+
self.annotationAdded.emit(self.current_annotation)
|
668
|
+
self.current_annotation = None
|
669
|
+
|
670
|
+
return
|
671
|
+
|
672
|
+
# did we click on an annotation?
|
673
|
+
annotation = self.find_annotation_at(self.widget_to_image_pos(pos))
|
674
|
+
if annotation:
|
675
|
+
# toggle selection
|
676
|
+
annotation.selected = not annotation.selected
|
677
|
+
|
678
|
+
# make all other annotations unselected
|
679
|
+
for ann in self.annotations:
|
680
|
+
if ann != annotation:
|
681
|
+
ann.selected = False
|
682
|
+
else:
|
683
|
+
# we clicked on the background
|
684
|
+
# make all annotations unselected
|
685
|
+
for ann in self.annotations:
|
686
|
+
ann.selected = False
|
687
|
+
# update the view
|
688
|
+
for ann in self.annotations:
|
689
|
+
self.annotationUpdated.emit(ann)
|
690
|
+
self.update()
|
691
|
+
|
692
|
+
def find_annotation_at(self, pos: QPointF):
|
693
|
+
for ann in reversed(self.annotations):
|
694
|
+
if ann.rectangle and ann.rectangle.contains(pos):
|
695
|
+
return ann
|
696
|
+
elif ann.polygon and ann.polygon.containsPoint(pos, Qt.OddEvenFill):
|
697
|
+
return ann
|
698
|
+
elif ann.points:
|
699
|
+
for p in ann.points:
|
700
|
+
if QLineF(pos, p).length() < 5:
|
701
|
+
return ann
|
702
|
+
return None
|
703
|
+
|
704
|
+
def edit_annotation_label(self, annotation: Annotation):
|
705
|
+
new_label, ok = QInputDialog.getText(
|
706
|
+
self, "Edit Label", "Enter new label:", text=annotation.label
|
707
|
+
)
|
708
|
+
if ok and new_label:
|
709
|
+
annotation.label = new_label
|
710
|
+
self.annotationUpdated.emit(annotation) # Emit signal
|
711
|
+
self.update()
|
712
|
+
|
713
|
+
def finalize_annotation(self):
|
714
|
+
if self.current_label:
|
715
|
+
# Use predefined label
|
716
|
+
self.current_annotation.annotation_id = len(self.annotations)
|
717
|
+
self.current_annotation.label = self.current_label
|
718
|
+
self.current_annotation.color = self.current_color
|
719
|
+
self.current_annotation.is_complete = True
|
720
|
+
self.annotations.append(self.current_annotation)
|
721
|
+
|
722
|
+
self.thumbnails[self.current_annotation.annotation_id] = self.get_thumbnail(
|
723
|
+
self.current_annotation
|
724
|
+
)
|
725
|
+
self.annotationAdded.emit(self.current_annotation)
|
726
|
+
self.current_annotation = None
|
727
|
+
self.update()
|
728
|
+
else:
|
729
|
+
# Show custom label dialog
|
730
|
+
label, ok = QInputDialog.getText(self, "Label", "Enter label name:")
|
731
|
+
if ok:
|
732
|
+
if self.current_annotation:
|
733
|
+
self.current_annotation.annotation_id = len(self.annotations)
|
734
|
+
self.current_annotation.label = label or "Unlabeled"
|
735
|
+
self.current_annotation.is_complete = True
|
736
|
+
self.annotations.append(self.current_annotation)
|
737
|
+
self.thumbnails[self.current_annotation.annotation_id] = (
|
738
|
+
self.get_thumbnail(self.current_annotation)
|
739
|
+
)
|
740
|
+
self.annotationAdded.emit(self.current_annotation)
|
741
|
+
self.current_annotation.annotation_id = len(self.annotations)
|
742
|
+
self.current_annotation = None
|
743
|
+
self.update()
|
744
|
+
|
745
|
+
# in update, update cursor
|
746
|
+
|
747
|
+
def _copy_annotation(self):
|
748
|
+
self.selected_annotation = self._get_selected_annotation()
|
749
|
+
if self.selected_annotation:
|
750
|
+
self.copied_annotation = self.selected_annotation
|
751
|
+
self.messageSignal.emit(
|
752
|
+
f"Copied annotation: {self.selected_annotation.label}"
|
753
|
+
)
|
754
|
+
self.mouse_mode = MouseMode.IDLE
|
755
|
+
else:
|
756
|
+
self.messageSignal.emit("No annotation selected to copy.")
|
757
|
+
|
758
|
+
def _paste_annotation(self):
|
759
|
+
if self.copied_annotation:
|
760
|
+
new_annotation = self.copied_annotation.copy()
|
761
|
+
new_annotation.annotation_id = len(self.annotations)
|
762
|
+
self.annotations.append(new_annotation)
|
763
|
+
self.annotationAdded.emit(new_annotation)
|
764
|
+
self.thumbnails[new_annotation.annotation_id] = self.get_thumbnail(
|
765
|
+
new_annotation
|
766
|
+
)
|
767
|
+
self.messageSignal.emit(f"Annotation {new_annotation.label} pasted")
|
768
|
+
self.update()
|
769
|
+
else:
|
770
|
+
self.messageSignal.emit("No annotation copied to paste.")
|
771
|
+
|
772
|
+
def _get_selected_annotation(self):
|
773
|
+
for annotation in self.annotations:
|
774
|
+
if annotation.selected:
|
775
|
+
return annotation
|
776
|
+
|
777
|
+
def layerify_annotation(self, annotations: list[Annotation]):
|
778
|
+
annotations = [ann for ann in annotations if ann.visible]
|
779
|
+
|
780
|
+
if len(annotations) == 0:
|
781
|
+
QMessageBox.information(
|
782
|
+
self.parentWidget(), "Info", "No visible annotations to layerify"
|
783
|
+
)
|
784
|
+
return
|
785
|
+
# Create and configure loading dialog
|
786
|
+
self.loading_dialog = QProgressDialog(
|
787
|
+
"Processing annotation...",
|
788
|
+
"Cancel", # Optional cancel button
|
789
|
+
0,
|
790
|
+
0,
|
791
|
+
self.parentWidget(),
|
792
|
+
)
|
793
|
+
self.loading_dialog.setWindowTitle("Please Wait")
|
794
|
+
self.loading_dialog.setWindowModality(Qt.WindowModal)
|
795
|
+
# self.loading_dialog.setCancelButton()
|
796
|
+
self.loading_dialog.show()
|
797
|
+
|
798
|
+
# Force UI update
|
799
|
+
QApplication.processEvents()
|
800
|
+
|
801
|
+
# Setup worker thread
|
802
|
+
self.worker_thread = QThread()
|
803
|
+
self.worker = LayerifyWorker(self.image, annotations, self.config)
|
804
|
+
self.worker.moveToThread(self.worker_thread)
|
805
|
+
|
806
|
+
# Connect signals
|
807
|
+
self.worker_thread.started.connect(self.worker.process)
|
808
|
+
self.worker.finished.connect(self.handle_layerify_result)
|
809
|
+
self.worker.finished.connect(self.worker_thread.quit)
|
810
|
+
self.worker.error.connect(self.handle_layerify_error)
|
811
|
+
|
812
|
+
# Cleanup connections
|
813
|
+
self.worker.finished.connect(self.worker.deleteLater)
|
814
|
+
self.worker_thread.finished.connect(self.worker_thread.deleteLater)
|
815
|
+
self.worker_thread.finished.connect(self.loading_dialog.close)
|
816
|
+
|
817
|
+
# Start processing
|
818
|
+
self.worker_thread.start()
|
819
|
+
|
820
|
+
def handle_layerify_result(self, annotation: Annotation, cropped_image: QPixmap):
|
821
|
+
# Create new canvas with results
|
822
|
+
new_layer = CanvasLayer(parent=self.parent_obj, config=self.canvas_config)
|
823
|
+
# get top left corner of the annotation
|
824
|
+
|
825
|
+
new_layer.set_image(cropped_image)
|
826
|
+
new_layer.annotations = [annotation]
|
827
|
+
new_layer.layer_name = (
|
828
|
+
f"{annotation.label} {annotation.annotation_id} {annotation.annotator}"
|
829
|
+
)
|
830
|
+
|
831
|
+
self.messageSignal.emit(f"Layerified: {new_layer.layer_name}")
|
832
|
+
logger.info(f"Num annotations: {len(self.annotations)}")
|
833
|
+
|
834
|
+
self.layerSignal.emit(new_layer)
|
835
|
+
|
836
|
+
def handle_layerify_error(self, error_msg: str):
|
837
|
+
self.loading_dialog.close()
|
838
|
+
QMessageBox.critical(
|
839
|
+
self.parentWidget(), "Error", f"Processing failed: {error_msg}"
|
840
|
+
)
|
841
|
+
|
842
|
+
@property
|
843
|
+
def selected_annotation_index(self):
|
844
|
+
for idx, annotation in enumerate(self.annotations):
|
845
|
+
if annotation.selected:
|
846
|
+
return idx
|
847
|
+
return -1
|