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,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)
|