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.
Files changed (42) hide show
  1. imagebaker/__init__.py +1 -1
  2. imagebaker/core/__init__.py +0 -0
  3. imagebaker/core/configs/__init__.py +1 -0
  4. imagebaker/core/configs/configs.py +156 -0
  5. imagebaker/core/defs/__init__.py +1 -0
  6. imagebaker/core/defs/defs.py +258 -0
  7. imagebaker/core/plugins/__init__.py +0 -0
  8. imagebaker/core/plugins/base_plugin.py +39 -0
  9. imagebaker/core/plugins/cosine_plugin.py +39 -0
  10. imagebaker/layers/__init__.py +3 -0
  11. imagebaker/layers/annotable_layer.py +847 -0
  12. imagebaker/layers/base_layer.py +724 -0
  13. imagebaker/layers/canvas_layer.py +1007 -0
  14. imagebaker/list_views/__init__.py +3 -0
  15. imagebaker/list_views/annotation_list.py +203 -0
  16. imagebaker/list_views/canvas_list.py +185 -0
  17. imagebaker/list_views/image_list.py +138 -0
  18. imagebaker/list_views/layer_list.py +390 -0
  19. imagebaker/list_views/layer_settings.py +219 -0
  20. imagebaker/models/__init__.py +0 -0
  21. imagebaker/models/base_model.py +150 -0
  22. imagebaker/tabs/__init__.py +2 -0
  23. imagebaker/tabs/baker_tab.py +496 -0
  24. imagebaker/tabs/layerify_tab.py +837 -0
  25. imagebaker/utils/__init__.py +0 -0
  26. imagebaker/utils/image.py +105 -0
  27. imagebaker/utils/state_utils.py +92 -0
  28. imagebaker/utils/transform_mask.py +107 -0
  29. imagebaker/window/__init__.py +1 -0
  30. imagebaker/window/app.py +136 -0
  31. imagebaker/window/main_window.py +181 -0
  32. imagebaker/workers/__init__.py +3 -0
  33. imagebaker/workers/baker_worker.py +247 -0
  34. imagebaker/workers/layerify_worker.py +91 -0
  35. imagebaker/workers/model_worker.py +54 -0
  36. {imagebaker-0.0.41.dist-info → imagebaker-0.0.48.dist-info}/METADATA +6 -6
  37. imagebaker-0.0.48.dist-info/RECORD +41 -0
  38. {imagebaker-0.0.41.dist-info → imagebaker-0.0.48.dist-info}/WHEEL +1 -1
  39. imagebaker-0.0.41.dist-info/RECORD +0 -7
  40. {imagebaker-0.0.41.dist-info/licenses → imagebaker-0.0.48.dist-info}/LICENSE +0 -0
  41. {imagebaker-0.0.41.dist-info → imagebaker-0.0.48.dist-info}/entry_points.txt +0 -0
  42. {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