imagebaker 0.0.49__py3-none-any.whl → 0.0.51__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 +5 -1
- imagebaker/core/configs/configs.py +18 -0
- imagebaker/core/defs/defs.py +8 -0
- imagebaker/layers/annotable_layer.py +36 -24
- imagebaker/layers/base_layer.py +132 -1
- imagebaker/layers/canvas_layer.py +53 -11
- imagebaker/list_views/annotation_list.py +1 -0
- imagebaker/list_views/image_list.py +21 -8
- imagebaker/list_views/layer_settings.py +31 -2
- imagebaker/models/base_model.py +1 -1
- imagebaker/tabs/baker_tab.py +2 -9
- imagebaker/tabs/layerify_tab.py +166 -48
- imagebaker/utils/__init__.py +3 -0
- imagebaker/utils/image.py +9 -6
- imagebaker/utils/state_utils.py +5 -1
- imagebaker/utils/utils.py +26 -0
- imagebaker/utils/vis.py +174 -0
- imagebaker/window/main_window.py +12 -6
- imagebaker/workers/baker_worker.py +23 -3
- {imagebaker-0.0.49.dist-info → imagebaker-0.0.51.dist-info}/METADATA +5 -2
- imagebaker-0.0.51.dist-info/RECORD +43 -0
- imagebaker-0.0.49.dist-info/RECORD +0 -41
- {imagebaker-0.0.49.dist-info → imagebaker-0.0.51.dist-info}/LICENSE +0 -0
- {imagebaker-0.0.49.dist-info → imagebaker-0.0.51.dist-info}/WHEEL +0 -0
- {imagebaker-0.0.49.dist-info → imagebaker-0.0.51.dist-info}/entry_points.txt +0 -0
- {imagebaker-0.0.49.dist-info → imagebaker-0.0.51.dist-info}/top_level.txt +0 -0
imagebaker/__init__.py
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
from loguru import logger # noqa
|
2
|
+
from importlib.metadata import version, PackageNotFoundError
|
2
3
|
|
3
4
|
logger.info("imagebaker package loaded with loguru logger.")
|
4
5
|
|
5
|
-
|
6
|
+
try:
|
7
|
+
__version__ = version("imagebaker")
|
8
|
+
except PackageNotFoundError:
|
9
|
+
__version__ = "0.0.0"
|
@@ -74,6 +74,22 @@ class BaseConfig(BaseModel):
|
|
74
74
|
logger.info(f"Created assets folder at {asset_dir}")
|
75
75
|
return asset_dir
|
76
76
|
|
77
|
+
@property
|
78
|
+
def cache_dir(self):
|
79
|
+
cache_dir = self.project_dir / ".imagebaker" / "cache"
|
80
|
+
if not cache_dir.exists():
|
81
|
+
cache_dir.mkdir(parents=True)
|
82
|
+
logger.info(f"Created cache folder at {cache_dir}")
|
83
|
+
return cache_dir
|
84
|
+
|
85
|
+
@property
|
86
|
+
def bake_dir(self):
|
87
|
+
bake_dir = self.project_dir / ".imagebaker" / "bake"
|
88
|
+
if not bake_dir.exists():
|
89
|
+
bake_dir.mkdir(parents=True)
|
90
|
+
logger.info(f"Created bake folder at {bake_dir}")
|
91
|
+
return bake_dir
|
92
|
+
|
77
93
|
class Config:
|
78
94
|
arbitrary_types_allowed = True
|
79
95
|
|
@@ -96,6 +112,7 @@ class LayerConfig(BaseConfig):
|
|
96
112
|
)
|
97
113
|
# whether to search image in subfolders as well
|
98
114
|
full_search: bool = False
|
115
|
+
cleanup_on_exit: bool = False
|
99
116
|
|
100
117
|
def get_label_color(self, label):
|
101
118
|
for lbl in self.predefined_labels:
|
@@ -119,6 +136,7 @@ class CanvasConfig(BaseConfig):
|
|
119
136
|
write_labels: bool = True
|
120
137
|
write_masks: bool = True
|
121
138
|
fps: int = 5
|
139
|
+
max_edge_width: int = 100
|
122
140
|
|
123
141
|
@property
|
124
142
|
def export_folder(self):
|
imagebaker/core/defs/defs.py
CHANGED
@@ -76,6 +76,8 @@ class LayerState:
|
|
76
76
|
is_annotable: bool = True
|
77
77
|
status: str = "Ready"
|
78
78
|
drawing_states: list[DrawingState] = field(default_factory=list)
|
79
|
+
edge_opacity: int = 100
|
80
|
+
edge_width: int = 10
|
79
81
|
|
80
82
|
def copy(self):
|
81
83
|
return LayerState(
|
@@ -105,6 +107,8 @@ class LayerState:
|
|
105
107
|
)
|
106
108
|
for d in self.drawing_states
|
107
109
|
],
|
110
|
+
edge_opacity=self.edge_opacity,
|
111
|
+
edge_width=self.edge_width,
|
108
112
|
)
|
109
113
|
|
110
114
|
|
@@ -206,6 +210,10 @@ class Annotation:
|
|
206
210
|
def load_from_json(path: str):
|
207
211
|
import json
|
208
212
|
|
213
|
+
# if path does not exist, return empty list
|
214
|
+
if not Path(path).exists():
|
215
|
+
return []
|
216
|
+
|
209
217
|
with open(path, "r") as f:
|
210
218
|
data = json.load(f)
|
211
219
|
|
@@ -42,6 +42,7 @@ class AnnotableLayer(BaseLayer):
|
|
42
42
|
annotationCleared = Signal()
|
43
43
|
annotationMoved = Signal()
|
44
44
|
layersChanged = Signal()
|
45
|
+
labelUpdated = Signal(tuple)
|
45
46
|
|
46
47
|
def __init__(self, parent, config: LayerConfig, canvas_config: CanvasConfig):
|
47
48
|
super().__init__(parent, config)
|
@@ -54,6 +55,7 @@ class AnnotableLayer(BaseLayer):
|
|
54
55
|
self.file_path: Path = Path("Runtime")
|
55
56
|
self.layers: list[BaseLayer] = []
|
56
57
|
self.is_annotable = True
|
58
|
+
self.handle_zoom: float = 1
|
57
59
|
|
58
60
|
def init_ui(self):
|
59
61
|
logger.info(f"Initializing Layer UI of {self.layer_name}")
|
@@ -157,14 +159,14 @@ class AnnotableLayer(BaseLayer):
|
|
157
159
|
self.config.normal_draw_config.brush_alpha,
|
158
160
|
)
|
159
161
|
|
160
|
-
pen = QPen(pen_color, self.config.normal_draw_config.line_width)
|
162
|
+
pen = QPen(pen_color, self.config.normal_draw_config.line_width / self.scale)
|
161
163
|
brush = QBrush(brush_color, Qt.DiagCrossPattern)
|
162
164
|
|
163
165
|
if annotation.selected:
|
164
166
|
painter.setPen(
|
165
167
|
QPen(
|
166
168
|
self.config.selected_draw_config.color,
|
167
|
-
self.config.selected_draw_config.line_width,
|
169
|
+
self.config.selected_draw_config.line_width / self.scale,
|
168
170
|
)
|
169
171
|
)
|
170
172
|
painter.setBrush(
|
@@ -184,8 +186,8 @@ class AnnotableLayer(BaseLayer):
|
|
184
186
|
elif annotation.points:
|
185
187
|
painter.drawEllipse(
|
186
188
|
annotation.points[0],
|
187
|
-
self.config.selected_draw_config.ellipse_size,
|
188
|
-
self.config.selected_draw_config.ellipse_size,
|
189
|
+
self.config.selected_draw_config.ellipse_size / self.scale,
|
190
|
+
self.config.selected_draw_config.ellipse_size / self.scale,
|
189
191
|
)
|
190
192
|
|
191
193
|
if is_temp:
|
@@ -200,8 +202,8 @@ class AnnotableLayer(BaseLayer):
|
|
200
202
|
for point in annotation.points:
|
201
203
|
painter.drawEllipse(
|
202
204
|
point,
|
203
|
-
self.config.normal_draw_config.point_size,
|
204
|
-
self.config.normal_draw_config.point_size,
|
205
|
+
self.config.normal_draw_config.point_size / self.scale,
|
206
|
+
self.config.normal_draw_config.point_size / self.scale,
|
205
207
|
)
|
206
208
|
elif annotation.rectangle:
|
207
209
|
painter.drawRect(annotation.rectangle)
|
@@ -223,28 +225,34 @@ class AnnotableLayer(BaseLayer):
|
|
223
225
|
]
|
224
226
|
painter.save()
|
225
227
|
painter.setPen(
|
226
|
-
QPen(
|
228
|
+
QPen(
|
229
|
+
Qt.black,
|
230
|
+
self.config.normal_draw_config.control_point_size / self.scale,
|
231
|
+
)
|
227
232
|
)
|
228
233
|
painter.setBrush(QBrush(Qt.white))
|
229
234
|
for corner in corners:
|
230
235
|
painter.drawEllipse(
|
231
236
|
corner,
|
232
|
-
self.config.normal_draw_config.point_size,
|
233
|
-
self.config.normal_draw_config.point_size,
|
237
|
+
self.config.normal_draw_config.point_size / self.scale,
|
238
|
+
self.config.normal_draw_config.point_size / self.scale,
|
234
239
|
)
|
235
240
|
painter.restore()
|
236
241
|
|
237
242
|
if annotation.polygon and len(annotation.polygon) > 0:
|
238
243
|
painter.save()
|
239
244
|
painter.setPen(
|
240
|
-
QPen(
|
245
|
+
QPen(
|
246
|
+
Qt.white,
|
247
|
+
self.config.normal_draw_config.control_point_size / self.scale,
|
248
|
+
)
|
241
249
|
)
|
242
250
|
painter.setBrush(QBrush(Qt.darkGray))
|
243
251
|
for point in annotation.polygon:
|
244
252
|
painter.drawEllipse(
|
245
253
|
point,
|
246
|
-
self.config.normal_draw_config.point_size,
|
247
|
-
self.config.normal_draw_config.point_size,
|
254
|
+
self.config.normal_draw_config.point_size / self.scale,
|
255
|
+
self.config.normal_draw_config.point_size / self.scale,
|
248
256
|
)
|
249
257
|
painter.restore()
|
250
258
|
|
@@ -266,7 +274,7 @@ class AnnotableLayer(BaseLayer):
|
|
266
274
|
# Set up font
|
267
275
|
font = painter.font()
|
268
276
|
font.setPixelSize(
|
269
|
-
self.config.normal_draw_config.label_font_size
|
277
|
+
self.config.normal_draw_config.label_font_size * self.scale
|
270
278
|
) # Fixed screen size
|
271
279
|
painter.setFont(font)
|
272
280
|
|
@@ -300,7 +308,10 @@ class AnnotableLayer(BaseLayer):
|
|
300
308
|
painter.save()
|
301
309
|
handle_color = self.config.selected_draw_config.handle_color
|
302
310
|
painter.setPen(
|
303
|
-
QPen(
|
311
|
+
QPen(
|
312
|
+
handle_color,
|
313
|
+
self.config.selected_draw_config.handle_width / self.scale,
|
314
|
+
)
|
304
315
|
)
|
305
316
|
painter.setBrush(QBrush(handle_color))
|
306
317
|
|
@@ -315,8 +326,8 @@ class AnnotableLayer(BaseLayer):
|
|
315
326
|
]:
|
316
327
|
painter.drawEllipse(
|
317
328
|
corner,
|
318
|
-
self.config.selected_draw_config.handle_point_size,
|
319
|
-
self.config.selected_draw_config.handle_point_size,
|
329
|
+
self.config.selected_draw_config.handle_point_size / self.scale,
|
330
|
+
self.config.selected_draw_config.handle_point_size / self.scale,
|
320
331
|
)
|
321
332
|
# Draw edge handles
|
322
333
|
for edge in [
|
@@ -327,8 +338,8 @@ class AnnotableLayer(BaseLayer):
|
|
327
338
|
]:
|
328
339
|
painter.drawEllipse(
|
329
340
|
edge,
|
330
|
-
self.config.selected_draw_config.handle_edge_size,
|
331
|
-
self.config.selected_draw_config.handle_edge_size,
|
341
|
+
self.config.selected_draw_config.handle_edge_size / self.scale,
|
342
|
+
self.config.selected_draw_config.handle_edge_size / self.scale,
|
332
343
|
)
|
333
344
|
|
334
345
|
elif annotation.polygon:
|
@@ -336,8 +347,8 @@ class AnnotableLayer(BaseLayer):
|
|
336
347
|
for point in annotation.polygon:
|
337
348
|
painter.drawEllipse(
|
338
349
|
point,
|
339
|
-
self.config.selected_draw_config.handle_point_size,
|
340
|
-
self.config.selected_draw_config.handle_point_size,
|
350
|
+
self.config.selected_draw_config.handle_point_size / self.scale,
|
351
|
+
self.config.selected_draw_config.handle_point_size / self.scale,
|
341
352
|
)
|
342
353
|
|
343
354
|
painter.restore()
|
@@ -524,13 +535,11 @@ class AnnotableLayer(BaseLayer):
|
|
524
535
|
self.current_annotation.polygon = QPolygonF(
|
525
536
|
[p for p in self.current_annotation.polygon][:-1]
|
526
537
|
)
|
527
|
-
self.update()
|
528
538
|
|
529
539
|
# If the polygon is now empty, reset to idle mode
|
530
540
|
if len(self.current_annotation.polygon) == 0:
|
531
541
|
self.current_annotation = None
|
532
542
|
self.mouse_mode = MouseMode.IDLE
|
533
|
-
self.update()
|
534
543
|
|
535
544
|
# If not drawing a polygon, go to idle mode
|
536
545
|
if not self.current_annotation:
|
@@ -538,7 +547,7 @@ class AnnotableLayer(BaseLayer):
|
|
538
547
|
for ann in self.annotations:
|
539
548
|
ann.selected = False
|
540
549
|
self.annotationUpdated.emit(ann)
|
541
|
-
|
550
|
+
self.update()
|
542
551
|
|
543
552
|
# If left-clicked
|
544
553
|
if event.button() == Qt.LeftButton:
|
@@ -741,8 +750,8 @@ class AnnotableLayer(BaseLayer):
|
|
741
750
|
and len(self.current_annotation.polygon) >= 3
|
742
751
|
):
|
743
752
|
self.current_annotation.is_complete = True
|
753
|
+
|
744
754
|
self.finalize_annotation()
|
745
|
-
self.annotationAdded.emit(self.current_annotation)
|
746
755
|
self.current_annotation = None
|
747
756
|
|
748
757
|
return
|
@@ -784,6 +793,7 @@ class AnnotableLayer(BaseLayer):
|
|
784
793
|
self, "Edit Label", "Enter new label:", text=annotation.label
|
785
794
|
)
|
786
795
|
if ok and new_label:
|
796
|
+
self.labelUpdated.emit((annotation.label, new_label))
|
787
797
|
annotation.label = new_label
|
788
798
|
self.annotationUpdated.emit(annotation) # Emit signal
|
789
799
|
self.update()
|
@@ -906,6 +916,8 @@ class AnnotableLayer(BaseLayer):
|
|
906
916
|
f"{annotation.label} {annotation.annotation_id} {annotation.annotator}"
|
907
917
|
)
|
908
918
|
|
919
|
+
new_layer._apply_edge_opacity()
|
920
|
+
new_layer.update()
|
909
921
|
self.messageSignal.emit(f"Layerified: {new_layer.layer_name}")
|
910
922
|
logger.info(f"Num annotations: {len(self.annotations)}")
|
911
923
|
|
imagebaker/layers/base_layer.py
CHANGED
@@ -18,6 +18,8 @@ from PySide6.QtWidgets import QWidget
|
|
18
18
|
|
19
19
|
from typing import Optional
|
20
20
|
from pathlib import Path
|
21
|
+
import cv2
|
22
|
+
import numpy as np
|
21
23
|
|
22
24
|
|
23
25
|
class BaseLayer(QWidget):
|
@@ -157,7 +159,8 @@ class BaseLayer(QWidget):
|
|
157
159
|
self.offset: QPointF = QPointF(0, 0)
|
158
160
|
self.pan_start: QPointF = None
|
159
161
|
self.pan_offset: QPointF = None
|
160
|
-
self.
|
162
|
+
self._image = QPixmap()
|
163
|
+
self._original_image = QPixmap()
|
161
164
|
self.annotations: list[Annotation] = []
|
162
165
|
self.current_annotation: Optional[Annotation] = None
|
163
166
|
self.copied_annotation: Optional[Annotation] = None
|
@@ -365,8 +368,120 @@ class BaseLayer(QWidget):
|
|
365
368
|
self.reset_view()
|
366
369
|
self.update()
|
367
370
|
|
371
|
+
self._original_image = self.image.copy() # Store a copy of the original image
|
368
372
|
self.original_size = QSizeF(self.image.size()) # Store original size
|
369
373
|
|
374
|
+
@property
|
375
|
+
def image(self):
|
376
|
+
"""
|
377
|
+
Get the current image of the canvas layer.
|
378
|
+
|
379
|
+
Returns:
|
380
|
+
QPixmap: The current image of the canvas layer.
|
381
|
+
"""
|
382
|
+
return self._image
|
383
|
+
|
384
|
+
@image.setter
|
385
|
+
def image(self, value: QPixmap):
|
386
|
+
"""
|
387
|
+
Set the image of the canvas layer.
|
388
|
+
|
389
|
+
Args:
|
390
|
+
value (QPixmap): The new image for the canvas layer.
|
391
|
+
"""
|
392
|
+
self._image = value
|
393
|
+
|
394
|
+
def _apply_edge_opacity(self):
|
395
|
+
"""
|
396
|
+
Apply edge opacity to the image. This function modifies the edges of the image
|
397
|
+
to have reduced opacity based on the configuration.
|
398
|
+
"""
|
399
|
+
logger.debug("Applying edge opacity to the image.")
|
400
|
+
edge_width = self.edge_width
|
401
|
+
edge_opacity = self.edge_opacity
|
402
|
+
|
403
|
+
# Convert QPixmap to QImage for pixel manipulation
|
404
|
+
image = self._original_image.toImage()
|
405
|
+
image = image.convertToFormat(
|
406
|
+
QImage.Format_ARGB32
|
407
|
+
) # Ensure format supports alpha
|
408
|
+
|
409
|
+
width = image.width()
|
410
|
+
height = image.height()
|
411
|
+
annotation = self.annotations[0] if self.annotations else None
|
412
|
+
if annotation is None:
|
413
|
+
return
|
414
|
+
|
415
|
+
if annotation.rectangle:
|
416
|
+
for x in range(width):
|
417
|
+
for y in range(height):
|
418
|
+
color = image.pixelColor(x, y)
|
419
|
+
if color.alpha() != 0: # If the pixel is not fully transparent
|
420
|
+
# Calculate horizontal and vertical distances to the edges
|
421
|
+
horizontal_distance = min(x, width - x - 1)
|
422
|
+
vertical_distance = min(y, height - y - 1)
|
423
|
+
|
424
|
+
# If the pixel is within the edge region
|
425
|
+
if (
|
426
|
+
horizontal_distance < edge_width
|
427
|
+
or vertical_distance < edge_width
|
428
|
+
):
|
429
|
+
distance_to_edge = min(
|
430
|
+
horizontal_distance, vertical_distance
|
431
|
+
)
|
432
|
+
# Calculate the new alpha based on the distance to the edge
|
433
|
+
factor = (edge_width - distance_to_edge) / edge_width
|
434
|
+
new_alpha = int(
|
435
|
+
color.alpha()
|
436
|
+
* ((1 - factor) + (factor * (edge_opacity / 255.0)))
|
437
|
+
)
|
438
|
+
color.setAlpha(new_alpha)
|
439
|
+
image.setPixelColor(x, y, color)
|
440
|
+
|
441
|
+
elif annotation.polygon:
|
442
|
+
# Extract alpha channel and find contours
|
443
|
+
alpha_image = image.convertToFormat(QImage.Format_Alpha8)
|
444
|
+
bytes_per_line = (
|
445
|
+
alpha_image.bytesPerLine()
|
446
|
+
) # Get the stride (bytes per line)
|
447
|
+
alpha_data = alpha_image.bits().tobytes()
|
448
|
+
|
449
|
+
# Extract only the valid data (remove padding)
|
450
|
+
alpha_array = np.frombuffer(alpha_data, dtype=np.uint8).reshape(
|
451
|
+
(alpha_image.height(), bytes_per_line)
|
452
|
+
)[
|
453
|
+
:, : alpha_image.width()
|
454
|
+
] # Remove padding to match the actual width
|
455
|
+
|
456
|
+
# Use OpenCV to find contours
|
457
|
+
contours, _ = cv2.findContours(
|
458
|
+
alpha_array, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
|
459
|
+
)
|
460
|
+
|
461
|
+
# Iterate over each pixel and apply edge opacity
|
462
|
+
for x in range(width):
|
463
|
+
for y in range(height):
|
464
|
+
color = image.pixelColor(x, y)
|
465
|
+
if color.alpha() != 0: # If the pixel is not fully transparent
|
466
|
+
# Calculate distance to the nearest contour
|
467
|
+
distance_to_edge = cv2.pointPolygonTest(
|
468
|
+
contours[0], (x, y), True
|
469
|
+
) # True for distance calculation
|
470
|
+
|
471
|
+
# If the pixel is within the edge region
|
472
|
+
if 0 <= distance_to_edge < edge_width:
|
473
|
+
# Calculate the new alpha based on the distance to the edge
|
474
|
+
factor = (edge_width - distance_to_edge) / edge_width
|
475
|
+
new_alpha = int(
|
476
|
+
color.alpha()
|
477
|
+
* ((1 - factor) + (factor * (edge_opacity / 255.0)))
|
478
|
+
)
|
479
|
+
color.setAlpha(new_alpha)
|
480
|
+
image.setPixelColor(x, y, color)
|
481
|
+
|
482
|
+
# Convert the modified QImage back to QPixmap
|
483
|
+
self.image = QPixmap.fromImage(image)
|
484
|
+
|
370
485
|
def get_thumbnail(self, annotation: Annotation = None):
|
371
486
|
"""
|
372
487
|
Generate a thumbnail for the layer or a specific annotation.
|
@@ -722,3 +837,19 @@ class BaseLayer(QWidget):
|
|
722
837
|
@drawing_states.setter
|
723
838
|
def drawing_states(self, value: list[DrawingState]):
|
724
839
|
self.layer_state.drawing_states = value
|
840
|
+
|
841
|
+
@property
|
842
|
+
def edge_opacity(self) -> int:
|
843
|
+
return self.layer_state.edge_opacity
|
844
|
+
|
845
|
+
@edge_opacity.setter
|
846
|
+
def edge_opacity(self, value: int):
|
847
|
+
self.layer_state.edge_opacity = value
|
848
|
+
|
849
|
+
@property
|
850
|
+
def edge_width(self) -> int:
|
851
|
+
return self.layer_state.edge_width
|
852
|
+
|
853
|
+
@edge_width.setter
|
854
|
+
def edge_width(self, value: int):
|
855
|
+
self.layer_state.edge_width = value
|
@@ -27,6 +27,7 @@ from PySide6.QtGui import (
|
|
27
27
|
QMouseEvent,
|
28
28
|
QKeyEvent,
|
29
29
|
QTransform,
|
30
|
+
QImage,
|
30
31
|
)
|
31
32
|
from PySide6.QtWidgets import (
|
32
33
|
QApplication,
|
@@ -57,7 +58,6 @@ class CanvasLayer(BaseLayer):
|
|
57
58
|
config (CanvasConfig): Configuration settings for the canvas layer.
|
58
59
|
"""
|
59
60
|
super().__init__(parent, config)
|
60
|
-
self.image = QPixmap()
|
61
61
|
self.is_annotable = False
|
62
62
|
self.last_pan_point = None
|
63
63
|
self.state_thumbnail = dict()
|
@@ -178,6 +178,7 @@ class CanvasLayer(BaseLayer):
|
|
178
178
|
opacity = layer.opacity / 255.0
|
179
179
|
temp_painter.setOpacity(opacity) # Scale opacity to 0.0-1.0
|
180
180
|
temp_painter.drawPixmap(0, 0, layer.image)
|
181
|
+
|
181
182
|
temp_painter.end()
|
182
183
|
|
183
184
|
# Draw the modified pixmap
|
@@ -287,20 +288,20 @@ class CanvasLayer(BaseLayer):
|
|
287
288
|
painter.setPen(
|
288
289
|
QPen(
|
289
290
|
self.config.selected_draw_config.handle_color,
|
290
|
-
self.config.selected_draw_config.handle_width,
|
291
|
+
self.config.selected_draw_config.handle_width / self.scale,
|
291
292
|
)
|
292
293
|
)
|
293
294
|
painter.setBrush(self.config.selected_draw_config.handle_color)
|
294
295
|
painter.drawEllipse(
|
295
296
|
rotation_pos,
|
296
|
-
self.config.selected_draw_config.handle_point_size *
|
297
|
-
self.config.selected_draw_config.handle_point_size *
|
297
|
+
self.config.selected_draw_config.handle_point_size * 1.1 / self.scale,
|
298
|
+
self.config.selected_draw_config.handle_point_size * 1.1 / self.scale,
|
298
299
|
)
|
299
300
|
# now draw rotation symbol
|
300
301
|
painter.setPen(
|
301
302
|
QPen(
|
302
303
|
self.config.selected_draw_config.handle_color,
|
303
|
-
self.config.selected_draw_config.handle_width,
|
304
|
+
self.config.selected_draw_config.handle_width / self.scale,
|
304
305
|
)
|
305
306
|
)
|
306
307
|
painter.drawLine(
|
@@ -323,21 +324,23 @@ class CanvasLayer(BaseLayer):
|
|
323
324
|
# Draw scale handles
|
324
325
|
handle_color = self.config.selected_draw_config.handle_color
|
325
326
|
painter.setPen(
|
326
|
-
QPen(
|
327
|
+
QPen(
|
328
|
+
handle_color, self.config.selected_draw_config.handle_width / self.scale
|
329
|
+
)
|
327
330
|
)
|
328
331
|
painter.setBrush(self.config.selected_draw_config.handle_color)
|
329
332
|
for corner in corners:
|
330
333
|
painter.drawEllipse(
|
331
334
|
corner,
|
332
|
-
self.config.selected_draw_config.handle_point_size,
|
333
|
-
self.config.selected_draw_config.handle_point_size,
|
335
|
+
self.config.selected_draw_config.handle_point_size / self.scale,
|
336
|
+
self.config.selected_draw_config.handle_point_size / self.scale,
|
334
337
|
)
|
335
338
|
for edge in edges:
|
336
339
|
# draw small circles on the edges
|
337
340
|
painter.drawEllipse(
|
338
341
|
edge,
|
339
|
-
self.config.selected_draw_config.handle_edge_size,
|
340
|
-
self.config.selected_draw_config.handle_edge_size,
|
342
|
+
self.config.selected_draw_config.handle_edge_size / self.scale,
|
343
|
+
self.config.selected_draw_config.handle_edge_size / self.scale,
|
341
344
|
)
|
342
345
|
# draw sides
|
343
346
|
painter.drawLine(
|
@@ -874,6 +877,33 @@ class CanvasLayer(BaseLayer):
|
|
874
877
|
"""
|
875
878
|
self.export_current_state(export_to_annotation_tab=True)
|
876
879
|
|
880
|
+
def seek_state(self, step):
|
881
|
+
"""Seek to a specific state using the timeline slider."""
|
882
|
+
self.messageSignal.emit(f"Seeking to step {step}")
|
883
|
+
logger.info(f"Seeking to step {step}")
|
884
|
+
|
885
|
+
# Get the states for the selected step
|
886
|
+
if step in self.states:
|
887
|
+
states = self.states[step]
|
888
|
+
for state in states:
|
889
|
+
layer = self.get_layer(state.layer_id)
|
890
|
+
if layer:
|
891
|
+
# Update the layer's state
|
892
|
+
update_opacities = False
|
893
|
+
logger.debug(
|
894
|
+
f"Updating layer {layer.layer_name} with state: {state}"
|
895
|
+
)
|
896
|
+
|
897
|
+
if (
|
898
|
+
layer.edge_width != state.edge_width
|
899
|
+
or layer.edge_opacity != state.edge_opacity
|
900
|
+
):
|
901
|
+
update_opacities = True
|
902
|
+
layer.layer_state = state
|
903
|
+
if update_opacities:
|
904
|
+
layer._apply_edge_opacity()
|
905
|
+
layer.update()
|
906
|
+
|
877
907
|
def play_states(self):
|
878
908
|
"""Play all the states stored in self.states."""
|
879
909
|
if len(self.states) == 0:
|
@@ -896,7 +926,19 @@ class CanvasLayer(BaseLayer):
|
|
896
926
|
layer = self.get_layer(state.layer_id)
|
897
927
|
if layer:
|
898
928
|
# Update the layer's state
|
929
|
+
update_opacities = False
|
930
|
+
logger.debug(
|
931
|
+
f"Updating layer {layer.layer_name} with state: {state}"
|
932
|
+
)
|
933
|
+
|
934
|
+
if (
|
935
|
+
layer.edge_width != state.edge_width
|
936
|
+
or layer.edge_opacity != state.edge_opacity
|
937
|
+
):
|
938
|
+
update_opacities = True
|
899
939
|
layer.layer_state = state
|
940
|
+
if update_opacities:
|
941
|
+
layer._apply_edge_opacity()
|
900
942
|
layer.update()
|
901
943
|
|
902
944
|
# Update the UI to reflect the changes
|
@@ -988,7 +1030,7 @@ class CanvasLayer(BaseLayer):
|
|
988
1030
|
logger.info(f"Saved baked image to {filename}")
|
989
1031
|
if self.config.write_annotations:
|
990
1032
|
image = qpixmap_to_numpy(image.copy())
|
991
|
-
image = cv2.cvtColor(image, cv2.
|
1033
|
+
image = cv2.cvtColor(image, cv2.COLOR_RGBA2BGRA)
|
992
1034
|
drawn = draw_annotations(image, annotations)
|
993
1035
|
write_to = filename.parent / f"annotated_{filename.name}"
|
994
1036
|
|
@@ -19,6 +19,7 @@ from pathlib import Path
|
|
19
19
|
|
20
20
|
class ImageListPanel(QDockWidget):
|
21
21
|
imageSelected = Signal(Path)
|
22
|
+
activeImageEntries = Signal(list)
|
22
23
|
|
23
24
|
def __init__(
|
24
25
|
self,
|
@@ -26,6 +27,7 @@ class ImageListPanel(QDockWidget):
|
|
26
27
|
processed_images: set[Path],
|
27
28
|
parent=None,
|
28
29
|
max_name_length=15,
|
30
|
+
images_per_page=10,
|
29
31
|
):
|
30
32
|
"""
|
31
33
|
:param image_entries: List of image paths to display.
|
@@ -35,7 +37,7 @@ class ImageListPanel(QDockWidget):
|
|
35
37
|
self.image_entries: list["ImageEntry"] = image_entries
|
36
38
|
self.processed_images = processed_images
|
37
39
|
self.current_page = 0
|
38
|
-
self.images_per_page =
|
40
|
+
self.images_per_page = images_per_page
|
39
41
|
self.max_name_length = max_name_length
|
40
42
|
self.init_ui()
|
41
43
|
|
@@ -90,7 +92,20 @@ class ImageListPanel(QDockWidget):
|
|
90
92
|
"""Update the image list with image paths and baked results."""
|
91
93
|
self.list_widget.clear()
|
92
94
|
|
93
|
-
for
|
95
|
+
# Calculate the range of images to display for the current page
|
96
|
+
start_index = self.current_page * self.images_per_page
|
97
|
+
end_index = min(start_index + self.images_per_page, len(image_entries))
|
98
|
+
|
99
|
+
# Update the pagination label
|
100
|
+
self.pagination_label.setText(
|
101
|
+
f"Showing {start_index + 1} to {end_index} of {len(image_entries)}"
|
102
|
+
)
|
103
|
+
|
104
|
+
# Display only the images for the current page
|
105
|
+
active_image_entries = []
|
106
|
+
for idx, image_entry in enumerate(
|
107
|
+
image_entries[start_index:end_index], start=start_index + 1
|
108
|
+
):
|
94
109
|
item_widget = QWidget()
|
95
110
|
item_layout = QHBoxLayout(item_widget)
|
96
111
|
item_layout.setContentsMargins(5, 5, 5, 5)
|
@@ -101,7 +116,7 @@ class ImageListPanel(QDockWidget):
|
|
101
116
|
thumbnail_pixmap = (
|
102
117
|
image_entry.data.get_thumbnail()
|
103
118
|
) # Baked result thumbnail
|
104
|
-
name_label_text = f"Baked Result {idx
|
119
|
+
name_label_text = f"Baked Result {idx}"
|
105
120
|
else:
|
106
121
|
thumbnail_pixmap = QPixmap(str(image_entry.data)).scaled(
|
107
122
|
50, 50, Qt.KeepAspectRatio, Qt.SmoothTransformation
|
@@ -126,11 +141,9 @@ class ImageListPanel(QDockWidget):
|
|
126
141
|
|
127
142
|
# Store metadata for the image
|
128
143
|
list_item.setData(Qt.UserRole, image_entry)
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
f"of {len(image_entries)}"
|
133
|
-
)
|
144
|
+
active_image_entries.append(image_entry)
|
145
|
+
|
146
|
+
self.activeImageEntries.emit(active_image_entries)
|
134
147
|
self.update()
|
135
148
|
|
136
149
|
def handle_item_clicked(self, item: QListWidgetItem):
|