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,1007 @@
1
+ from imagebaker.core.configs import CanvasConfig
2
+ from imagebaker.core.defs import Annotation, MouseMode, BakingResult, DrawingState
3
+ from imagebaker.layers import BaseLayer
4
+ from imagebaker.core.configs import CursorDef
5
+ from imagebaker import logger
6
+ from imagebaker.workers import BakerWorker
7
+ from imagebaker.utils.image import qpixmap_to_numpy, draw_annotations
8
+
9
+
10
+ from PySide6.QtCore import (
11
+ QPointF,
12
+ QPoint,
13
+ Qt,
14
+ Signal,
15
+ QRectF,
16
+ QLineF,
17
+ QThread,
18
+ QSizeF,
19
+ )
20
+ from PySide6.QtGui import (
21
+ QColor,
22
+ QPixmap,
23
+ QPainter,
24
+ QBrush,
25
+ QPen,
26
+ QWheelEvent,
27
+ QMouseEvent,
28
+ QKeyEvent,
29
+ QTransform,
30
+ )
31
+ from PySide6.QtWidgets import (
32
+ QApplication,
33
+ QSizePolicy,
34
+ QMessageBox,
35
+ QProgressDialog,
36
+ )
37
+
38
+ import math
39
+ import cv2
40
+ from datetime import datetime
41
+
42
+
43
+ class CanvasLayer(BaseLayer):
44
+ layersChanged = Signal()
45
+ layerSelected = Signal(BaseLayer)
46
+ annotationAdded = Signal(Annotation)
47
+ annotationUpdated = Signal(Annotation)
48
+ bakingResult = Signal(BakingResult)
49
+ thumbnailsAvailable = Signal(int)
50
+
51
+ def __init__(self, parent=None, config=CanvasConfig()):
52
+ """
53
+ Initialize the CanvasLayer with a parent widget and configuration.
54
+
55
+ Args:
56
+ parent (QWidget, optional): The parent widget for this layer. Defaults to None.
57
+ config (CanvasConfig): Configuration settings for the canvas layer.
58
+ """
59
+ super().__init__(parent, config)
60
+ self.image = QPixmap()
61
+ self.is_annotable = False
62
+ self.last_pan_point = None
63
+ self.state_thumbnail = dict()
64
+
65
+ self._last_draw_point = None # Track the last point for smooth drawing
66
+
67
+ def init_ui(self):
68
+ """
69
+ Initialize the user interface for the canvas layer, including size policies
70
+ and storing the original size of the layer.
71
+ """
72
+ logger.info(f"Initializing Layer UI of {self.layer_name}")
73
+ self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
74
+ self.original_size = QSizeF(self.image.size()) # Store original size
75
+
76
+ def handle_key_release(self, event: QKeyEvent):
77
+ """
78
+ Handle key release events, such as resetting the mouse mode when the Control key is released.
79
+
80
+ Args:
81
+ event (QKeyEvent): The key release event.
82
+ """
83
+ if event.key() == Qt.Key_Control:
84
+ if self.mouse_mode not in [MouseMode.DRAW, MouseMode.ERASE]:
85
+ self.mouse_mode = MouseMode.IDLE
86
+
87
+ def _update_back_buffer(self):
88
+ """
89
+ Update the back buffer for the canvas layer by rendering all visible layers
90
+ with their transformations and opacity settings.
91
+ """
92
+ # Initialize the back buffer
93
+ self._back_buffer = QPixmap(self.size())
94
+ self._back_buffer.fill(Qt.GlobalColor.transparent)
95
+
96
+ # Initialize the layer masks dictionary if it doesn't exist
97
+ if not hasattr(self, "layer_masks"):
98
+ self.layer_masks = {}
99
+
100
+ painter = QPainter(self._back_buffer)
101
+ try:
102
+ painter.setRenderHints(
103
+ QPainter.RenderHint.Antialiasing
104
+ | QPainter.RenderHint.SmoothPixmapTransform
105
+ )
106
+
107
+ for layer in self.layers:
108
+ if layer.visible and not layer.image.isNull():
109
+ # Save the painter state
110
+ painter.save()
111
+
112
+ # Apply layer transformations
113
+ painter.translate(layer.position)
114
+ painter.rotate(layer.rotation)
115
+ painter.scale(layer.scale, layer.scale)
116
+
117
+ # Draw the layer onto the back buffer
118
+ painter.setOpacity(layer.opacity)
119
+ painter.drawPixmap(QPoint(0, 0), layer.image)
120
+
121
+ # Restore the painter state
122
+ painter.restore()
123
+ finally:
124
+ painter.end()
125
+
126
+ self.image = self._back_buffer
127
+
128
+ ## Helper functions ##
129
+ def handle_key_press(self, event: QKeyEvent):
130
+
131
+ # Handle Delete key
132
+ if event.key() == Qt.Key_Delete:
133
+ self._delete_layer()
134
+ return # Important: return after handling
135
+
136
+ # Handle Ctrl key
137
+ if event.key() == Qt.Key_Control:
138
+ if self.mouse_mode not in [MouseMode.DRAW, MouseMode.ERASE]:
139
+ self.mouse_mode = MouseMode.PAN
140
+
141
+ return # Important: return after handling
142
+
143
+ # Handle Ctrl+C
144
+ if event.modifiers() & Qt.ControlModifier and event.key() == Qt.Key_C:
145
+ self._copy_layer()
146
+ return # Important: return after handling
147
+
148
+ # Handle Ctrl+V
149
+ if event.modifiers() & Qt.ControlModifier and event.key() == Qt.Key_V:
150
+ self._paste_layer()
151
+ return # Important: return after handling
152
+
153
+ def paint_layer(self, painter: QPainter):
154
+ """
155
+ Paint the canvas layer, including all visible layers, their transformations,
156
+ and any drawing states or selection indicators.
157
+
158
+ Args:
159
+ painter (QPainter): The painter object used for rendering.
160
+ """
161
+ painter.translate(self.pan_offset)
162
+ painter.scale(self.scale, self.scale)
163
+ for layer in self.layers:
164
+ if layer.visible and not layer.image.isNull():
165
+ painter.save()
166
+ painter.translate(layer.position)
167
+ painter.rotate(layer.rotation)
168
+ painter.scale(layer.scale_x, layer.scale_y)
169
+
170
+ # painter.drawPixmap(0, 0, layer.image)
171
+ # painter.setOpacity(layer.opacity / 255)
172
+ # Create a new pixmap with adjusted opacity
173
+ pixmap_with_alpha = QPixmap(layer.image.size())
174
+ pixmap_with_alpha.fill(Qt.transparent) # Ensure transparency
175
+
176
+ # Use QPainter to apply opacity to the pixmap
177
+ temp_painter = QPainter(pixmap_with_alpha)
178
+ opacity = layer.opacity / 255.0
179
+ temp_painter.setOpacity(opacity) # Scale opacity to 0.0-1.0
180
+ temp_painter.drawPixmap(0, 0, layer.image)
181
+ temp_painter.end()
182
+
183
+ # Draw the modified pixmap
184
+ painter.drawPixmap(0, 0, pixmap_with_alpha)
185
+
186
+ if layer.selected:
187
+ painter.setPen(
188
+ QPen(
189
+ self.config.selected_draw_config.color,
190
+ self.config.selected_draw_config.line_width,
191
+ )
192
+ )
193
+ painter.setBrush(
194
+ QBrush(
195
+ QColor(
196
+ self.config.selected_draw_config.color.red(),
197
+ self.config.selected_draw_config.color.green(),
198
+ self.config.selected_draw_config.color.blue(),
199
+ self.config.selected_draw_config.brush_alpha,
200
+ )
201
+ )
202
+ )
203
+ painter.drawRect(QRectF(QPointF(0, 0), layer.original_size))
204
+ painter.restore()
205
+
206
+ if layer.selected:
207
+ self._draw_transform_handles(painter, layer)
208
+ if layer.layer_state.drawing_states:
209
+ painter.save()
210
+ painter.translate(layer.position)
211
+ painter.rotate(layer.rotation)
212
+ painter.scale(layer.scale_x, layer.scale_y)
213
+
214
+ for state in layer.layer_state.drawing_states:
215
+ painter.setRenderHints(QPainter.Antialiasing)
216
+ painter.setPen(
217
+ QPen(
218
+ state.color,
219
+ state.size,
220
+ Qt.SolidLine,
221
+ Qt.RoundCap,
222
+ Qt.RoundJoin,
223
+ )
224
+ )
225
+ # Draw the point after applying transformations
226
+ painter.drawPoint(state.position)
227
+
228
+ painter.restore()
229
+ if self.layer_state.drawing_states:
230
+ painter.save()
231
+ painter.translate(self.position)
232
+ painter.rotate(self.rotation)
233
+ painter.scale(self.scale_x, self.scale_y)
234
+
235
+ for state in self.layer_state.drawing_states:
236
+ painter.setRenderHints(QPainter.Antialiasing)
237
+ painter.setPen(
238
+ QPen(
239
+ state.color,
240
+ state.size,
241
+ Qt.SolidLine,
242
+ Qt.RoundCap,
243
+ Qt.RoundJoin,
244
+ )
245
+ )
246
+ painter.drawPoint(state.position)
247
+
248
+ painter.restore()
249
+ painter.end()
250
+
251
+ def _draw_transform_handles(self, painter, layer):
252
+ """
253
+ Draw rotation and scaling handles for the selected layer.
254
+
255
+ Args:
256
+ painter (QPainter): The painter object used for rendering.
257
+ layer (BaseLayer): The layer for which the handles are drawn.
258
+ """
259
+ # Create transform including both scales
260
+ transform = QTransform()
261
+ transform.translate(layer.position.x(), layer.position.y())
262
+ transform.rotate(layer.rotation)
263
+ transform.scale(layer.scale_x, layer.scale_y)
264
+
265
+ # Get transformed rect
266
+ rect = transform.mapRect(QRectF(QPointF(0, 0), layer.original_size))
267
+
268
+ # Adjust handle positions to stay on edges
269
+ handle_size = 10 / self.scale
270
+ rotation_pos = rect.center()
271
+
272
+ # Scale handles (directly on corners/edges)
273
+ corners = [
274
+ rect.topLeft(),
275
+ rect.topRight(),
276
+ rect.bottomLeft(),
277
+ rect.bottomRight(),
278
+ ]
279
+ edges = [
280
+ QPointF(rect.center().x(), rect.top()),
281
+ QPointF(rect.center().x(), rect.bottom()),
282
+ QPointF(rect.left(), rect.center().y()),
283
+ QPointF(rect.right(), rect.center().y()),
284
+ ]
285
+
286
+ # Draw rotation handle at the center and fill it
287
+ painter.setPen(
288
+ QPen(
289
+ self.config.selected_draw_config.handle_color,
290
+ self.config.selected_draw_config.handle_width,
291
+ )
292
+ )
293
+ painter.setBrush(self.config.selected_draw_config.handle_color)
294
+ painter.drawEllipse(
295
+ rotation_pos,
296
+ self.config.selected_draw_config.handle_point_size * 2,
297
+ self.config.selected_draw_config.handle_point_size * 2,
298
+ )
299
+ # now draw rotation symbol
300
+ painter.setPen(
301
+ QPen(
302
+ self.config.selected_draw_config.handle_color,
303
+ self.config.selected_draw_config.handle_width,
304
+ )
305
+ )
306
+ painter.drawLine(
307
+ rotation_pos,
308
+ rotation_pos + QPointF(0, -handle_size),
309
+ )
310
+ painter.drawLine(
311
+ rotation_pos,
312
+ rotation_pos + QPointF(0, handle_size),
313
+ )
314
+ painter.drawLine(
315
+ rotation_pos,
316
+ rotation_pos + QPointF(-handle_size, 0),
317
+ )
318
+ painter.drawLine(
319
+ rotation_pos,
320
+ rotation_pos + QPointF(handle_size, 0),
321
+ )
322
+
323
+ # Draw scale handles
324
+ handle_color = self.config.selected_draw_config.handle_color
325
+ painter.setPen(
326
+ QPen(handle_color, self.config.selected_draw_config.handle_width)
327
+ )
328
+ painter.setBrush(self.config.selected_draw_config.handle_color)
329
+ for corner in corners:
330
+ painter.drawEllipse(
331
+ corner,
332
+ self.config.selected_draw_config.handle_point_size,
333
+ self.config.selected_draw_config.handle_point_size,
334
+ )
335
+ for edge in edges:
336
+ # draw small circles on the edges
337
+ painter.drawEllipse(
338
+ edge,
339
+ self.config.selected_draw_config.handle_edge_size,
340
+ self.config.selected_draw_config.handle_edge_size,
341
+ )
342
+ # draw sides
343
+ painter.drawLine(
344
+ edge + QPointF(-handle_size, 0),
345
+ edge + QPointF(handle_size, 0),
346
+ )
347
+ painter.drawLine(
348
+ edge + QPointF(0, -handle_size),
349
+ edge + QPointF(0, handle_size),
350
+ )
351
+
352
+ def _add_drawing_state(self, pos: QPointF):
353
+ """
354
+ Add a new drawing state to the selected layer or the canvas layer itself,
355
+ based on the current mouse mode (DRAW or ERASE).
356
+
357
+ Args:
358
+ pos (QPointF): The position where the drawing state is added.
359
+ """
360
+ """Add a new drawing state."""
361
+ self.selected_layer = self._get_selected_layer()
362
+ layer = self.selected_layer if self.selected_layer else self
363
+
364
+ # Convert the position to be relative to the layer
365
+ relative_pos = pos - layer.position
366
+
367
+ if self.mouse_mode == MouseMode.ERASE:
368
+ # Remove drawing states within the eraser's area
369
+ layer.layer_state.drawing_states = [
370
+ state
371
+ for state in layer.layer_state.drawing_states
372
+ if (state.position - relative_pos).manhattanLength() > self.brush_size
373
+ ]
374
+ elif self.mouse_mode == MouseMode.DRAW:
375
+ # Add a new drawing state only if the position has changed
376
+ # if self._last_draw_point is None or self._last_draw_point != relative_pos:
377
+ drawing_state = DrawingState(
378
+ position=relative_pos, # Store relative position
379
+ color=self.drawing_color,
380
+ size=self.brush_size,
381
+ )
382
+ layer.layer_state.drawing_states.append(drawing_state)
383
+ self._last_draw_point = relative_pos # Update the last draw point
384
+ # logger.debug(f"Added drawing state at position: {relative_pos}")
385
+
386
+ self.update() # Refresh the canvas to show the new drawing
387
+
388
+ def handle_wheel(self, event: QWheelEvent):
389
+ if self.mouse_mode == MouseMode.DRAW or self.mouse_mode == MouseMode.ERASE:
390
+ # Adjust the brush size using the mouse wheel
391
+ delta = event.angleDelta().y() / 120 # Each step is 120 units
392
+ self.brush_size = max(
393
+ 1, self.brush_size + int(delta)
394
+ ) # Ensure size is >= 1
395
+ self.messageSignal.emit(f"Brush size: {self.brush_size}")
396
+ self.update() # Refresh the canvas to show the updated brush cursor
397
+ return
398
+ if event.modifiers() & Qt.ControlModifier:
399
+ # Get mouse position in widget coordinates
400
+ mouse_pos = event.position()
401
+
402
+ # Calculate zoom factor
403
+ zoom_factor = 1.25 if event.angleDelta().y() > 0 else 0.8
404
+ old_scale = self.scale
405
+ new_scale = max(0.1, min(old_scale * zoom_factor, 10.0))
406
+
407
+ # Calculate the image point under the cursor before zooming
408
+ before_zoom_img_pos = (mouse_pos - self.pan_offset) / old_scale
409
+
410
+ # Update scale
411
+ self.scale = new_scale
412
+
413
+ # Calculate the new position of the same image point after zooming
414
+ after_zoom_widget_pos = before_zoom_img_pos * new_scale + self.pan_offset
415
+
416
+ # Adjust pan offset to keep the image point under the cursor fixed
417
+ self.pan_offset += mouse_pos - after_zoom_widget_pos
418
+
419
+ # Update mouse mode based on zoom direction
420
+ self.mouse_mode = (
421
+ MouseMode.ZOOM_IN if event.angleDelta().y() > 0 else MouseMode.ZOOM_OUT
422
+ )
423
+
424
+ self.zoomChanged.emit(self.scale)
425
+ self.update()
426
+
427
+ def handle_mouse_release(self, event: QMouseEvent):
428
+
429
+ if event.button() == Qt.LeftButton:
430
+ self._active_handle = None
431
+ self._dragging_layer = None
432
+
433
+ # Reset drawing state
434
+ if self.mouse_mode in [MouseMode.DRAW, MouseMode.ERASE]:
435
+ self._last_draw_point = None
436
+ self.update() # Refresh the canvas to show the updated brush cursor
437
+
438
+ def handle_mouse_move(self, event: QMouseEvent):
439
+ pos = (event.position() - self.pan_offset) / self.scale
440
+ # logger.info(f"Drawing states: {self.layer_state.drawing_states}")
441
+
442
+ # Update cursor position for the brush
443
+ self._cursor_position = event.position()
444
+
445
+ if event.buttons() & Qt.LeftButton:
446
+ # Handle drawing or erasing
447
+ if self.mouse_mode in [MouseMode.DRAW, MouseMode.ERASE]:
448
+ self._add_drawing_state(pos)
449
+ # self._last_draw_point = pos
450
+ return
451
+
452
+ if self.mouse_mode == MouseMode.PAN:
453
+ if (
454
+ event.modifiers() & Qt.ControlModifier
455
+ and event.buttons() & Qt.LeftButton
456
+ ):
457
+ if self.last_pan_point:
458
+ delta = event.position() - self.last_pan_point
459
+ self.pan_offset += delta
460
+ self.last_pan_point = event.position()
461
+ self.update()
462
+ return
463
+ else:
464
+ self.last_pan_point = None
465
+ self.mouse_mode = MouseMode.IDLE
466
+
467
+ if self._active_handle:
468
+ handle_type, layer = self._active_handle
469
+ start = self._drag_start
470
+ if "rotate" in handle_type:
471
+ start = self._drag_start
472
+ center = start["center"]
473
+
474
+ # Calculate rotation delta from the initial angle
475
+ current_vector = pos - center
476
+ current_angle = math.atan2(current_vector.y(), current_vector.x())
477
+ angle_delta = math.degrees(current_angle - start["initial_angle"])
478
+
479
+ new_transform = QTransform()
480
+ new_transform.translate(center.x(), center.y())
481
+ new_transform.rotate(angle_delta)
482
+ new_transform.translate(-center.x(), -center.y())
483
+
484
+ new_position = new_transform.map(start["position"])
485
+
486
+ # Update the layer using the original reference data
487
+ layer.rotation = (start["rotation"] + angle_delta) % 360
488
+ layer.position = new_position
489
+
490
+ logger.info(
491
+ f"Rotating layer {layer.layer_name} around center to {layer.rotation:.2f} degrees"
492
+ )
493
+ self.messageSignal.emit(
494
+ f"Rotating layer {layer.layer_name} to {layer.rotation:.2f} degrees"
495
+ )
496
+ layer.selected = True
497
+ self.layersChanged.emit()
498
+
499
+ return
500
+ elif "scale" in handle_type:
501
+ # Improved scaling logic
502
+ handle_index = int(handle_type.split("_")[-1])
503
+ original_size = layer.original_size
504
+ delta = pos - start["pos"]
505
+
506
+ # Calculate new scale factors
507
+ new_scale_x = layer.scale_x
508
+ new_scale_y = layer.scale_y
509
+
510
+ # Calculate position offset (for handles that move the layer)
511
+ pos_offset = QPointF(0, 0)
512
+
513
+ # Handle all 8 scale handles
514
+ if handle_index in [0]: # Top-left
515
+ new_scale_x = start["scale_x"] - delta.x() / original_size.width()
516
+ new_scale_y = start["scale_y"] - delta.y() / original_size.height()
517
+ pos_offset = delta
518
+ self.setCursor(CursorDef.TRANSFORM_ALL)
519
+
520
+ elif handle_index in [1]: # Top-right
521
+ new_scale_x = start["scale_x"] + delta.x() / original_size.width()
522
+ new_scale_y = start["scale_y"] - delta.y() / original_size.height()
523
+ pos_offset = QPointF(0, delta.y())
524
+ self.mouse_mode = MouseMode.RESIZE
525
+ elif handle_index in [2]: # Bottom-left
526
+ new_scale_x = start["scale_x"] - delta.x() / original_size.width()
527
+ new_scale_y = start["scale_y"] + delta.y() / original_size.height()
528
+ pos_offset = QPointF(delta.x(), 0)
529
+ self.mouse_mode = MouseMode.RESIZE
530
+ elif handle_index in [3]: # Bottom-right
531
+ new_scale_x = start["scale_x"] + delta.x() / original_size.width()
532
+ new_scale_y = start["scale_y"] + delta.y() / original_size.height()
533
+ self.mouse_mode = MouseMode.RESIZE
534
+ elif handle_index in [4]: # Top-center
535
+ new_scale_y = start["scale_y"] - delta.y() / original_size.height()
536
+ pos_offset = QPointF(0, delta.y())
537
+ self.mouse_mode = MouseMode.RESIZE_HEIGHT
538
+ elif handle_index in [5]: # Bottom-center
539
+ new_scale_y = start["scale_y"] + delta.y() / original_size.height()
540
+ self.mouse_mode = MouseMode.RESIZE_HEIGHT
541
+ elif handle_index in [6]: # Left-center
542
+ new_scale_x = start["scale_x"] - delta.x() / original_size.width()
543
+ self.mouse_mode = MouseMode.RESIZE_WIDTH
544
+ pos_offset = QPointF(delta.x(), 0)
545
+ elif handle_index in [7]: # Right-center
546
+ new_scale_x = start["scale_x"] + delta.x() / original_size.width()
547
+ self.mouse_mode = MouseMode.RESIZE_WIDTH
548
+
549
+ # Apply scale limits
550
+ new_scale_x = max(0.1, min(new_scale_x, 5.0))
551
+ new_scale_y = max(0.1, min(new_scale_y, 5.0))
552
+
553
+ # Update layer properties
554
+ layer.scale_x = new_scale_x
555
+ layer.scale_y = new_scale_y
556
+
557
+ # Adjust position for handles that move the layer
558
+ if handle_index in [0, 1, 2, 4, 6]:
559
+ layer.position = start["position"] + pos_offset
560
+
561
+ logger.info(
562
+ f"Scaling layer {layer.layer_name} to {layer.scale_x:.2f}, {layer.scale_y:.2f}"
563
+ )
564
+ self.messageSignal.emit(
565
+ f"Scaling layer {layer.layer_name} to {layer.scale_x:.2f}, {layer.scale_y:.2f}"
566
+ )
567
+
568
+ self.layersChanged.emit()
569
+ self.update()
570
+ elif self._dragging_layer:
571
+ self._dragging_layer.position = pos - self._drag_offset
572
+ self._dragging_layer.selected = True
573
+ self._dragging_layer.update()
574
+ # set all other layers to not selected
575
+ for layer in self.layers:
576
+ if layer != self._dragging_layer:
577
+ layer.selected = False
578
+
579
+ self.layersChanged.emit()
580
+ self.update()
581
+
582
+ def handle_mouse_press(self, event: QMouseEvent):
583
+ if event.button() == Qt.LeftButton:
584
+ pos = (event.position() - self.pan_offset) / self.scale
585
+ if self.mouse_mode in [MouseMode.DRAW, MouseMode.ERASE]:
586
+ logger.info(f"Drawing mode: {self.mouse_mode} at position: {pos}")
587
+ # Add a drawing state immediately on mouse press
588
+ self._last_draw_point = pos
589
+ self._add_drawing_state(pos) # Add the drawing state here
590
+ return
591
+ if event.modifiers() & Qt.ControlModifier:
592
+ self.mouse_mode = MouseMode.PAN
593
+ self.last_pan_point = event.position()
594
+ return
595
+ # Check handles first
596
+ for layer in reversed(self.layers):
597
+ if layer.selected and layer.visible:
598
+ # Compute visual center (ignoring rotation for the pivot)
599
+ handle_size = 10 / self.scale
600
+ transform = QTransform()
601
+ transform.translate(layer.position.x(), layer.position.y())
602
+ transform.rotate(layer.rotation) # now includes rotation!
603
+ transform.scale(layer.scale_x, layer.scale_y)
604
+ visual_rect = transform.mapRect(
605
+ QRectF(QPointF(0, 0), layer.original_size)
606
+ )
607
+ visual_center = visual_rect.center()
608
+
609
+ handle_size = 10 / self.scale
610
+ if QLineF(pos, visual_center).length() < handle_size:
611
+ vec = pos - visual_center
612
+ initial_angle = math.atan2(vec.y(), vec.x())
613
+ self._active_handle = ("rotate", layer)
614
+ self._drag_start = {
615
+ "pos": pos,
616
+ "rotation": layer.rotation,
617
+ "center": visual_center,
618
+ "initial_angle": initial_angle,
619
+ "position": layer.position, # Store the initial position
620
+ }
621
+ return
622
+
623
+ # Check scale handles (using fully transformed rect)
624
+ full_transform = QTransform()
625
+ full_transform.translate(layer.position.x(), layer.position.y())
626
+ full_transform.rotate(layer.rotation)
627
+ full_transform.scale(layer.scale_x, layer.scale_y)
628
+ full_rect = full_transform.mapRect(
629
+ QRectF(QPointF(0, 0), layer.original_size)
630
+ )
631
+ full_center = full_rect.center()
632
+
633
+ scale_handles = [
634
+ full_rect.topLeft(),
635
+ full_rect.topRight(),
636
+ full_rect.bottomLeft(),
637
+ full_rect.bottomRight(),
638
+ QPointF(full_center.x(), full_rect.top()),
639
+ QPointF(full_center.x(), full_rect.bottom()),
640
+ QPointF(full_rect.left(), full_center.y()),
641
+ QPointF(full_rect.right(), full_center.y()),
642
+ ]
643
+ for i, handle_pos in enumerate(scale_handles):
644
+ if QLineF(pos, handle_pos).length() < handle_size:
645
+ self._active_handle = (f"scale_{i}", layer)
646
+ self._drag_start = {
647
+ "pos": pos,
648
+ "scale_x": layer.scale_x,
649
+ "scale_y": layer.scale_y,
650
+ "position": layer.position,
651
+ }
652
+ return
653
+
654
+ # Check layer selection
655
+ for layer in reversed(self.layers):
656
+ if layer.visible:
657
+ transform = QTransform()
658
+ transform.translate(layer.position.x(), layer.position.y())
659
+ transform.rotate(layer.rotation)
660
+ transform.scale(layer.scale_x, layer.scale_y)
661
+ rect = transform.mapRect(QRectF(QPointF(0, 0), layer.original_size))
662
+
663
+ if rect.contains(pos):
664
+ self._dragging_layer = layer
665
+ self._drag_offset = pos - layer.position
666
+ layer.selected = True
667
+ # then set all other layers to not selected
668
+ for other_layer in self.layers:
669
+ if other_layer != layer:
670
+ other_layer.selected = False
671
+ layer.update()
672
+ self.layersChanged.emit()
673
+
674
+ break
675
+ # if right click, deselect all layers
676
+ elif event.button() == Qt.RightButton:
677
+ for layer in self.layers:
678
+ layer.selected = False
679
+ self.mouse_mode = MouseMode.IDLE
680
+ self.layersChanged.emit()
681
+ self.update()
682
+
683
+ def handle_mouse_double_click(self, event: QMouseEvent, pos: QPoint):
684
+ # was just trying to select/deselect layers with double click
685
+ # if left double click
686
+ # if event.button() == Qt.LeftButton:
687
+ # # did we click on a layer?
688
+ # pos = (event.position() - self.pan_offset) / self.scale
689
+ # selected_layer = None
690
+ # # Find clicked layer
691
+ # for layer in reversed(self.layers):
692
+ # if layer.visible:
693
+ # # Create transform including scale
694
+ # transform = QTransform()
695
+ # transform.translate(layer.position.x(), layer.position.y())
696
+ # transform.rotate(layer.rotation)
697
+ # transform.scale(layer.scale_x, layer.scale_y)
698
+ # rect = transform.mapRect(QRectF(QPointF(0, 0), layer.original_size))
699
+ # if rect.contains(pos):
700
+ # selected_layer = layer
701
+ # break
702
+
703
+ # if selected_layer:
704
+ # # toggle selection
705
+ # selected_layer.selected = not selected_layer.selected
706
+ # # make all other layers unselected
707
+ # for layer in self.layers:
708
+ # if layer != selected_layer:
709
+ # layer.selected = False
710
+ # else:
711
+ # # we clicked on the background
712
+ # # make all layers unselected
713
+ # for layer in self.layers:
714
+ # layer.selected = False
715
+ self.update()
716
+
717
+ def _get_selected_layer(self):
718
+ for layer in self.layers:
719
+ if layer.selected:
720
+ return layer
721
+ return None
722
+
723
+ def add_layer(self, layer: BaseLayer, index=-1):
724
+ """
725
+ This function adds a new layer to the canvas layer.
726
+
727
+ Args:
728
+ layer (BaseLayer): The layer to add.
729
+ index (int, optional): The index at which to add the layer. Defaults to -1.
730
+
731
+ Raises:
732
+ ValueError: If the layer is not a BaseLayer instance
733
+ """
734
+ layer.layer_name = f"{len(self.layers) + 1}_" + layer.layer_name
735
+ if index >= 0:
736
+ self.layers.append(layer)
737
+ else:
738
+ self.layers.insert(0, layer)
739
+
740
+ self._update_back_buffer()
741
+ self.update()
742
+ self.messageSignal.emit(f"Added layer {layer.layer_name}")
743
+
744
+ def clear_layers(self):
745
+ """
746
+ Clear all layers from the canvas layer.
747
+ """
748
+ self.layers.clear()
749
+ self._update_back_buffer()
750
+ self.update()
751
+ self.messageSignal.emit("Cleared all layers")
752
+
753
+ def _copy_layer(self):
754
+ """
755
+ Copy the selected layer to the clipboard.
756
+ """
757
+ self.selected_layer = self._get_selected_layer()
758
+ if self.selected_layer:
759
+ self.copied_layer = self.selected_layer.copy()
760
+ self.messageSignal.emit(f"Copied layer {self.selected_layer.layer_name}.")
761
+ else:
762
+ self.messageSignal.emit("No layer selected to copy.")
763
+
764
+ def _paste_layer(self):
765
+ """
766
+ Paste the copied layer to the canvas layer.
767
+ """
768
+ if self.copied_layer:
769
+ new_layer = self.copied_layer.copy()
770
+ new_layer.position += QPointF(10, 10)
771
+ self.add_layer(new_layer, index=0)
772
+ self.update()
773
+ self.layerSelected.emit(new_layer)
774
+ self.messageSignal.emit(f"Pasted layer {new_layer.layer_name}.")
775
+ else:
776
+ self.messageSignal.emit("No layer copied to paste.")
777
+
778
+ def _delete_layer(self):
779
+ self.selected_layer = self._get_selected_layer()
780
+ # now handled from bakertab
781
+ # if self.selected_layer:
782
+ # remaining_layers = []
783
+ # removed = False
784
+ # for layer in self.layers:
785
+ # if layer.selected:
786
+ # removed = True
787
+ # self.messageSignal.emit(f"Deleted {layer.layer_name} layer.")
788
+ # else:
789
+ # remaining_layers.append(layer)
790
+
791
+ # if removed:
792
+ # self.layers = remaining_layers
793
+ # self._update_back_buffer()
794
+ # self.layerRemoved.emit(self.selected_layer)
795
+ # self.update()
796
+
797
+ def export_current_state(self, export_to_annotation_tab=False):
798
+ """
799
+ Export the current state of the canvas layer to an image file or annotation tab.
800
+
801
+ Args:
802
+ export_to_annotation_tab (bool, optional): Whether to export the image to the annotation tab. Defaults to False.
803
+
804
+ Raises:
805
+ ValueError: If the layer is not a BaseLayer instance
806
+ """
807
+ if not self.layers:
808
+ QMessageBox.warning(
809
+ self,
810
+ "Operation Not Possible",
811
+ "No layers are available to export. Please add layers before exporting.",
812
+ QMessageBox.Ok,
813
+ )
814
+ return
815
+ filename = self.config.filename_format.format(
816
+ project_name=self.config.project_name,
817
+ timestamp=datetime.now().strftime("%Y%m%d_%H%M%S"),
818
+ )
819
+ filename = self.config.export_folder / f"{filename}.png"
820
+ logger.info(f"Exporting baked image to {filename}")
821
+ self.states = {0: [layer.layer_state for layer in self.layers]}
822
+
823
+ self.loading_dialog = QProgressDialog(
824
+ "Baking Please wait...", "Cancel", 0, 0, self.parentWidget()
825
+ )
826
+
827
+ self.loading_dialog.setWindowTitle("Please Wait")
828
+ self.loading_dialog.setWindowModality(Qt.WindowModal)
829
+ self.loading_dialog.setCancelButton(None) # Remove cancel button if not needed
830
+ self.loading_dialog.show()
831
+
832
+ # Force UI update
833
+ QApplication.processEvents()
834
+
835
+ # Setup worker thread
836
+ self.worker_thread = QThread()
837
+ self.worker = BakerWorker(
838
+ layers=self.layers,
839
+ states=self.states,
840
+ filename=filename,
841
+ )
842
+ self.worker.moveToThread(self.worker_thread)
843
+
844
+ # Connect signals
845
+ self.worker_thread.started.connect(self.worker.process)
846
+ self.worker.finished.connect(
847
+ lambda results, export_to_annotation_tab=export_to_annotation_tab: self.handle_baker_results(
848
+ results, export_to_annotation_tab=export_to_annotation_tab
849
+ )
850
+ )
851
+ self.worker.finished.connect(self.worker_thread.quit)
852
+ self.worker.error.connect(self.handle_baker_error)
853
+
854
+ # Cleanup connections
855
+ self.worker.finished.connect(self.worker.deleteLater)
856
+ self.worker_thread.finished.connect(self.worker_thread.deleteLater)
857
+ self.worker_thread.finished.connect(self.loading_dialog.close)
858
+
859
+ # Start processing
860
+ self.worker_thread.start()
861
+
862
+ def handle_baker_error(self, error_msg):
863
+ """
864
+ To handle any errors that occur during the baking process.
865
+ """
866
+ self.loading_dialog.close()
867
+ QMessageBox.critical(
868
+ self.parentWidget(), "Error", f"Processing failed: {error_msg}"
869
+ )
870
+
871
+ def predict_state(self):
872
+ """
873
+ To send the current state to the prediction tab.
874
+ """
875
+ self.export_current_state(export_to_annotation_tab=True)
876
+
877
+ def play_states(self):
878
+ """Play all the states stored in self.states."""
879
+ if len(self.states) == 0:
880
+ logger.warning("No states to play")
881
+ self.messageSignal.emit("No states to play")
882
+ return
883
+
884
+ for step, states in sorted(
885
+ self.states.items()
886
+ ): # Ensure states are played in order
887
+ self.messageSignal.emit(f"Playing step {step}")
888
+ logger.info(f"Playing step {step}")
889
+
890
+ # Update the slider position
891
+ self.parentWidget().timeline_slider.setValue(step)
892
+ # Clear the current drawing states
893
+
894
+ for state in states:
895
+ # Get the layer corresponding to the state
896
+ layer = self.get_layer(state.layer_id)
897
+ if layer:
898
+ # Update the layer's state
899
+ layer.layer_state = state
900
+ layer.update()
901
+
902
+ # Update the UI to reflect the changes
903
+ self.update() # Update the current widget
904
+
905
+ QApplication.processEvents() # Process pending events to refresh the UI
906
+
907
+ # Wait for the next frame
908
+ QThread.msleep(int(1000 / self.config.fps)) # Convert FPS to milliseconds
909
+
910
+ logger.info("Finished playing states")
911
+ self.messageSignal.emit("Finished playing states")
912
+
913
+ def export_baked_states(self, export_to_annotation_tab=False):
914
+ """Export all the states stored in self.states."""
915
+ if len(self.states) == 0:
916
+ msg = "No states to export. Creating a single image."
917
+ logger.warning(msg)
918
+ self.messageSignal.emit(msg)
919
+ self.states = {0: [layer.layer_state for layer in self.layers]}
920
+
921
+ filename = self.config.filename_format.format(
922
+ project_name=self.config.project_name,
923
+ timestamp=datetime.now().strftime("%Y%m%d_%H%M%S"),
924
+ )
925
+ filename = self.config.export_folder / f"{filename}.png"
926
+
927
+ self.loading_dialog = QProgressDialog(
928
+ "Exporting states, please wait...", "Cancel", 0, 0, self.parentWidget()
929
+ )
930
+ self.loading_dialog.setWindowTitle("Please Wait")
931
+ self.loading_dialog.setWindowModality(Qt.WindowModal)
932
+ self.loading_dialog.setCancelButton(None)
933
+ self.loading_dialog.show()
934
+
935
+ QApplication.processEvents()
936
+
937
+ # Setup worker thread
938
+ self.worker_thread = QThread()
939
+ self.worker = BakerWorker(
940
+ states=self.states, layers=self.layers, filename=filename
941
+ )
942
+ self.worker.moveToThread(self.worker_thread)
943
+
944
+ # Connect signals
945
+ self.worker_thread.started.connect(self.worker.process)
946
+ self.worker.finished.connect(
947
+ lambda results, export_to_annotation_tab=export_to_annotation_tab: self.handle_baker_results(
948
+ results, export_to_annotation_tab
949
+ )
950
+ ) # Handle multiple results
951
+ self.worker.finished.connect(self.worker_thread.quit)
952
+ self.worker.error.connect(self.handle_baker_error)
953
+
954
+ # Cleanup connections
955
+ self.worker.finished.connect(self.worker.deleteLater)
956
+ self.worker_thread.finished.connect(self.worker_thread.deleteLater)
957
+ self.worker_thread.finished.connect(self.loading_dialog.close)
958
+
959
+ # Start processing
960
+ self.worker_thread.start()
961
+
962
+ def handle_baker_results(
963
+ self,
964
+ baking_results: list[BakingResult],
965
+ export_to_annotation_tab=False,
966
+ ):
967
+ logger.info("Baking completed.")
968
+ for baking_result in baking_results:
969
+
970
+ filename, image = baking_result.filename, baking_result.image
971
+ masks = baking_result.masks
972
+ mask_names = baking_result.mask_names
973
+ annotations = baking_result.annotations
974
+
975
+ if not export_to_annotation_tab:
976
+ image.save(str(filename))
977
+ logger.info(f"Saved annotated image to annotated_{filename}")
978
+
979
+ if self.config.is_debug:
980
+ if self.config.write_masks:
981
+ for i, mask in enumerate(masks):
982
+ mask_name = mask_names[i]
983
+ write_to = filename.parent / f"{mask_name}_{filename.name}"
984
+
985
+ cv2.imwrite(write_to, mask)
986
+
987
+ logger.info(f"Saved mask for {mask_name}")
988
+ logger.info(f"Saved baked image to {filename}")
989
+ if self.config.write_annotations:
990
+ image = qpixmap_to_numpy(image.copy())
991
+ image = cv2.cvtColor(image, cv2.COLOR_RGBA2BGR)
992
+ drawn = draw_annotations(image, annotations)
993
+ write_to = filename.parent / f"annotated_{filename.name}"
994
+
995
+ cv2.imwrite(str(write_to), drawn)
996
+
997
+ logger.info(f"Saved annotated image to annotated_{filename}")
998
+
999
+ Annotation.save_as_json(
1000
+ annotations, f"{filename.parent/filename.stem}.json"
1001
+ )
1002
+ logger.info(f"Saved annotations to {filename}.json")
1003
+ else:
1004
+ self.bakingResult.emit(baking_result)
1005
+
1006
+ def export_states_to_predict(self):
1007
+ self.export_baked_states(export_to_annotation_tab=True)