imagebaker 0.0.50__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/core/configs/configs.py +17 -0
- imagebaker/core/defs/defs.py +4 -0
- imagebaker/layers/annotable_layer.py +4 -4
- imagebaker/layers/canvas_layer.py +1 -1
- imagebaker/list_views/annotation_list.py +1 -0
- imagebaker/list_views/image_list.py +21 -8
- imagebaker/tabs/layerify_tab.py +165 -48
- imagebaker/utils/image.py +9 -6
- imagebaker/window/main_window.py +12 -6
- imagebaker/workers/baker_worker.py +10 -3
- {imagebaker-0.0.50.dist-info → imagebaker-0.0.51.dist-info}/METADATA +4 -1
- {imagebaker-0.0.50.dist-info → imagebaker-0.0.51.dist-info}/RECORD +16 -16
- {imagebaker-0.0.50.dist-info → imagebaker-0.0.51.dist-info}/LICENSE +0 -0
- {imagebaker-0.0.50.dist-info → imagebaker-0.0.51.dist-info}/WHEEL +0 -0
- {imagebaker-0.0.50.dist-info → imagebaker-0.0.51.dist-info}/entry_points.txt +0 -0
- {imagebaker-0.0.50.dist-info → imagebaker-0.0.51.dist-info}/top_level.txt +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:
|
imagebaker/core/defs/defs.py
CHANGED
@@ -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)
|
@@ -534,13 +535,11 @@ class AnnotableLayer(BaseLayer):
|
|
534
535
|
self.current_annotation.polygon = QPolygonF(
|
535
536
|
[p for p in self.current_annotation.polygon][:-1]
|
536
537
|
)
|
537
|
-
self.update()
|
538
538
|
|
539
539
|
# If the polygon is now empty, reset to idle mode
|
540
540
|
if len(self.current_annotation.polygon) == 0:
|
541
541
|
self.current_annotation = None
|
542
542
|
self.mouse_mode = MouseMode.IDLE
|
543
|
-
self.update()
|
544
543
|
|
545
544
|
# If not drawing a polygon, go to idle mode
|
546
545
|
if not self.current_annotation:
|
@@ -548,7 +547,7 @@ class AnnotableLayer(BaseLayer):
|
|
548
547
|
for ann in self.annotations:
|
549
548
|
ann.selected = False
|
550
549
|
self.annotationUpdated.emit(ann)
|
551
|
-
|
550
|
+
self.update()
|
552
551
|
|
553
552
|
# If left-clicked
|
554
553
|
if event.button() == Qt.LeftButton:
|
@@ -751,8 +750,8 @@ class AnnotableLayer(BaseLayer):
|
|
751
750
|
and len(self.current_annotation.polygon) >= 3
|
752
751
|
):
|
753
752
|
self.current_annotation.is_complete = True
|
753
|
+
|
754
754
|
self.finalize_annotation()
|
755
|
-
self.annotationAdded.emit(self.current_annotation)
|
756
755
|
self.current_annotation = None
|
757
756
|
|
758
757
|
return
|
@@ -794,6 +793,7 @@ class AnnotableLayer(BaseLayer):
|
|
794
793
|
self, "Edit Label", "Enter new label:", text=annotation.label
|
795
794
|
)
|
796
795
|
if ok and new_label:
|
796
|
+
self.labelUpdated.emit((annotation.label, new_label))
|
797
797
|
annotation.label = new_label
|
798
798
|
self.annotationUpdated.emit(annotation) # Emit signal
|
799
799
|
self.update()
|
@@ -1030,7 +1030,7 @@ class CanvasLayer(BaseLayer):
|
|
1030
1030
|
logger.info(f"Saved baked image to {filename}")
|
1031
1031
|
if self.config.write_annotations:
|
1032
1032
|
image = qpixmap_to_numpy(image.copy())
|
1033
|
-
image = cv2.cvtColor(image, cv2.
|
1033
|
+
image = cv2.cvtColor(image, cv2.COLOR_RGBA2BGRA)
|
1034
1034
|
drawn = draw_annotations(image, annotations)
|
1035
1035
|
write_to = filename.parent / f"annotated_{filename.name}"
|
1036
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):
|
imagebaker/tabs/layerify_tab.py
CHANGED
@@ -40,6 +40,7 @@ from imagebaker.core.defs import (
|
|
40
40
|
from collections import deque
|
41
41
|
from typing import Deque
|
42
42
|
from dataclasses import dataclass
|
43
|
+
import os
|
43
44
|
|
44
45
|
|
45
46
|
@dataclass
|
@@ -103,13 +104,17 @@ class LayerifyTab(QWidget):
|
|
103
104
|
"""Connect all necessary signals"""
|
104
105
|
# Connect all layers in the deque to annotation list
|
105
106
|
for layer in self.annotable_layers:
|
106
|
-
layer.annotationAdded.connect(self.annotation_list.update_list)
|
107
|
-
layer.
|
107
|
+
# layer.annotationAdded.connect(self.annotation_list.update_list)
|
108
|
+
layer.annotationAdded.connect(self.on_annotation_added)
|
109
|
+
# layer.annotationUpdated.connect(self.annotation_list.update_list)
|
110
|
+
layer.annotationUpdated.connect(self.on_annotation_updated)
|
108
111
|
layer.messageSignal.connect(self.messageSignal)
|
109
112
|
layer.layerSignal.connect(self.add_layer)
|
113
|
+
layer.labelUpdated.connect(self.on_label_update)
|
110
114
|
|
111
115
|
# Connect image list panel signals
|
112
116
|
self.image_list_panel.imageSelected.connect(self.on_image_selected)
|
117
|
+
self.image_list_panel.activeImageEntries.connect(self.update_active_entries)
|
113
118
|
|
114
119
|
def init_ui(self):
|
115
120
|
"""Initialize the UI components"""
|
@@ -118,7 +123,9 @@ class LayerifyTab(QWidget):
|
|
118
123
|
None, parent=self.main_window, max_name_length=self.config.max_name_length
|
119
124
|
)
|
120
125
|
self.image_list_panel = ImageListPanel(
|
121
|
-
self.image_entries,
|
126
|
+
self.image_entries,
|
127
|
+
self.processed_images,
|
128
|
+
images_per_page=self.config.deque_maxlen,
|
122
129
|
)
|
123
130
|
|
124
131
|
self.main_window.addDockWidget(Qt.LeftDockWidgetArea, self.image_list_panel)
|
@@ -166,6 +173,8 @@ class LayerifyTab(QWidget):
|
|
166
173
|
if not image_entry.is_baked_result: # Regular image
|
167
174
|
image_path = image_entry.data
|
168
175
|
self.curr_image_idx = self.image_entries.index(image_entry)
|
176
|
+
# convert curr_image_idx to correct index
|
177
|
+
self.curr_image_idx = self.curr_image_idx % len(self.annotable_layers)
|
169
178
|
|
170
179
|
# Make the corresponding layer visible and set the image
|
171
180
|
selected_layer = self.annotable_layers[self.curr_image_idx]
|
@@ -206,6 +215,8 @@ class LayerifyTab(QWidget):
|
|
206
215
|
for i, layer in enumerate(self.annotable_layers):
|
207
216
|
if i < len(self.image_entries):
|
208
217
|
layer.set_image(self.image_entries[i].data)
|
218
|
+
self.load_layer_annotations(layer)
|
219
|
+
layer.layer_name = f"Layer_{i + 1}"
|
209
220
|
layer.setVisible(
|
210
221
|
i == 0
|
211
222
|
) # Only the first layer is visible by default
|
@@ -224,12 +235,68 @@ class LayerifyTab(QWidget):
|
|
224
235
|
self.image_list_panel.update_image_list(self.image_entries)
|
225
236
|
self.update()
|
226
237
|
|
238
|
+
def save_layer_annotations(self, layer: AnnotableLayer):
|
239
|
+
"""Save annotations for a specific layer"""
|
240
|
+
if len(layer.annotations) > 0:
|
241
|
+
file_path = layer.file_path
|
242
|
+
file_name = file_path.name
|
243
|
+
save_dir = self.config.cache_dir / f"{file_name}.json"
|
244
|
+
Annotation.save_as_json(layer.annotations, save_dir)
|
245
|
+
logger.info(f"Saved annotations for {layer.layer_name} to {save_dir}")
|
246
|
+
|
247
|
+
def load_layer_annotations(self, layer: AnnotableLayer):
|
248
|
+
"""Load annotations for a specific layer"""
|
249
|
+
if layer.file_path:
|
250
|
+
file_path = layer.file_path
|
251
|
+
file_name = file_path.name
|
252
|
+
load_dir = self.config.cache_dir / f"{file_name}.json"
|
253
|
+
if load_dir.exists():
|
254
|
+
layer.annotations = Annotation.load_from_json(load_dir)
|
255
|
+
logger.info(
|
256
|
+
f"Loaded annotations for {layer.layer_name} from {load_dir}"
|
257
|
+
)
|
258
|
+
else:
|
259
|
+
logger.warning(f"No annotations found for {layer.layer_name}")
|
260
|
+
|
261
|
+
def update_active_entries(self, image_entries: list[ImageEntry]):
|
262
|
+
"""Update the active entries in the image list panel."""
|
263
|
+
self.curr_image_idx = 0
|
264
|
+
for i, layer in enumerate(self.annotable_layers):
|
265
|
+
self.save_layer_annotations(layer)
|
266
|
+
layer.annotations = []
|
267
|
+
|
268
|
+
if i < len(image_entries):
|
269
|
+
# get index on the self.image_entries
|
270
|
+
idx = self.image_entries.index(image_entries[i])
|
271
|
+
if self.image_entries[idx].is_baked_result:
|
272
|
+
# if the image is a baked result, set the layer to the baked result
|
273
|
+
layer = self.image_entries[idx].data
|
274
|
+
layer.file_path = layer.file_path
|
275
|
+
else:
|
276
|
+
layer.set_image(self.image_entries[idx].data)
|
277
|
+
self.load_layer_annotations(layer)
|
278
|
+
|
279
|
+
layer.layer_name = f"Layer_{idx + 1}"
|
280
|
+
layer.setVisible(i == 0)
|
281
|
+
if i == 0:
|
282
|
+
self.layer = layer
|
283
|
+
else:
|
284
|
+
layer.setVisible(False)
|
285
|
+
logger.info("Updated active entries in image list panel.")
|
286
|
+
|
227
287
|
def clear_annotations(self):
|
228
288
|
"""Safely clear all annotations"""
|
229
289
|
try:
|
230
290
|
# Clear layer annotations
|
231
291
|
self.clearAnnotations.emit()
|
232
292
|
self.messageSignal.emit("Annotations cleared")
|
293
|
+
# clear cache annotation of layer
|
294
|
+
annotation_path = (
|
295
|
+
self.config.cache_dir / f"{self.layer.file_path.name}.json"
|
296
|
+
)
|
297
|
+
if annotation_path.exists():
|
298
|
+
os.remove(annotation_path)
|
299
|
+
logger.info(f"Cleared annotations from {annotation_path}")
|
233
300
|
|
234
301
|
except Exception as e:
|
235
302
|
logger.error(f"Clear error: {str(e)}")
|
@@ -241,13 +308,17 @@ class LayerifyTab(QWidget):
|
|
241
308
|
Args:
|
242
309
|
annotation (Annotation): The annotation that was added.
|
243
310
|
"""
|
244
|
-
|
311
|
+
|
312
|
+
# if annotation.label is not in the predefined labels, add it
|
313
|
+
if annotation.label not in [lbl.name for lbl in self.config.predefined_labels]:
|
314
|
+
logger.info(f"Label {annotation.label} created.")
|
245
315
|
self.config.predefined_labels.append(
|
246
316
|
Label(annotation.label, annotation.color)
|
247
317
|
)
|
248
318
|
self.update_label_combo()
|
249
319
|
logger.info(f"Added annotation: {annotation.label}")
|
250
320
|
self.messageSignal.emit(f"Added annotation: {annotation.label}")
|
321
|
+
self.save_layer_annotations(self.layer)
|
251
322
|
|
252
323
|
# Refresh the annotation list
|
253
324
|
self.annotation_list.update_list()
|
@@ -259,11 +330,12 @@ class LayerifyTab(QWidget):
|
|
259
330
|
Args:
|
260
331
|
annotation (Annotation): The updated annotation.
|
261
332
|
"""
|
262
|
-
logger.info(f"Updated annotation: {annotation
|
333
|
+
# logger.info(f"Updated annotation: {annotation}")
|
263
334
|
self.messageSignal.emit(f"Updated annotation: {annotation.label}")
|
264
335
|
|
265
336
|
# Refresh the annotation list
|
266
337
|
self.annotation_list.update_list()
|
338
|
+
self.save_layer_annotations(self.layer)
|
267
339
|
|
268
340
|
def update_label_combo(self):
|
269
341
|
"""
|
@@ -276,6 +348,27 @@ class LayerifyTab(QWidget):
|
|
276
348
|
pixmap = QPixmap(16, 16)
|
277
349
|
pixmap.fill(label.color)
|
278
350
|
self.label_combo.addItem(QIcon(pixmap), label.name)
|
351
|
+
logger.info("Updated label combo box with predefined labels.")
|
352
|
+
self.label_combo.setCurrentText(self.current_label)
|
353
|
+
|
354
|
+
def on_label_update(self, old_new_label: tuple[str, str]):
|
355
|
+
new_labels = []
|
356
|
+
index = 0
|
357
|
+
for i, label in enumerate(self.config.predefined_labels):
|
358
|
+
if label.name == old_new_label[0]:
|
359
|
+
label.name = old_new_label[1]
|
360
|
+
index = i
|
361
|
+
new_labels.append(label)
|
362
|
+
|
363
|
+
self.config.predefined_labels = new_labels
|
364
|
+
logger.info(f"Updated label from {old_new_label[0]} to {old_new_label[1]}")
|
365
|
+
self.messageSignal.emit(
|
366
|
+
f"Updated label from {old_new_label[0]} to {old_new_label[1]}."
|
367
|
+
)
|
368
|
+
|
369
|
+
self.update_label_combo()
|
370
|
+
self.handle_label_change(index=index)
|
371
|
+
self.label_combo.update()
|
279
372
|
|
280
373
|
def load_default_image(self):
|
281
374
|
"""
|
@@ -589,6 +682,24 @@ class LayerifyTab(QWidget):
|
|
589
682
|
)
|
590
683
|
msg = f"Label changed to {self.current_label}"
|
591
684
|
self.messageSignal.emit(msg)
|
685
|
+
self.layer.selected_annotation = self.layer._get_selected_annotation()
|
686
|
+
if self.layer.selected_annotation:
|
687
|
+
annotations = []
|
688
|
+
for ann in self.layer.annotations:
|
689
|
+
if ann == self.layer.selected_annotation:
|
690
|
+
ann.label = label_info.name
|
691
|
+
ann.color = label_info.color
|
692
|
+
annotations.append(ann)
|
693
|
+
|
694
|
+
self.layer.annotations = annotations
|
695
|
+
self.on_annotation_updated(self.layer.selected_annotation)
|
696
|
+
# disable label change callback
|
697
|
+
self.label_combo.currentIndexChanged.disconnect()
|
698
|
+
self.label_combo.currentIndexChanged.connect(lambda: None)
|
699
|
+
self.update_label_combo()
|
700
|
+
# set it back
|
701
|
+
self.label_combo.currentIndexChanged.connect(self.handle_label_change)
|
702
|
+
|
592
703
|
self.layer.update()
|
593
704
|
self.update()
|
594
705
|
|
@@ -628,7 +739,7 @@ class LayerifyTab(QWidget):
|
|
628
739
|
("🎨", "Color", self.choose_color),
|
629
740
|
("🧅", "Layerify All", self.layerify_all),
|
630
741
|
("🏷️", "Add Label", self.add_new_label),
|
631
|
-
("🗑️", "Clear",
|
742
|
+
("🗑️", "Clear", self.clear_annotations),
|
632
743
|
]
|
633
744
|
|
634
745
|
# Folder navigation buttons
|
@@ -636,18 +747,6 @@ class LayerifyTab(QWidget):
|
|
636
747
|
self.select_folder_btn.clicked.connect(self.select_folder)
|
637
748
|
toolbar_layout.addWidget(self.select_folder_btn)
|
638
749
|
|
639
|
-
self.next_image_btn = QPushButton("Next")
|
640
|
-
self.next_image_btn.clicked.connect(self.show_next_image)
|
641
|
-
toolbar_layout.addWidget(self.next_image_btn)
|
642
|
-
|
643
|
-
self.prev_image_btn = QPushButton("Prev")
|
644
|
-
self.prev_image_btn.clicked.connect(self.show_prev_image)
|
645
|
-
toolbar_layout.addWidget(self.prev_image_btn)
|
646
|
-
|
647
|
-
# Initially hide next/prev buttons
|
648
|
-
self.next_image_btn.setVisible(False)
|
649
|
-
self.prev_image_btn.setVisible(False)
|
650
|
-
|
651
750
|
# Add mode buttons
|
652
751
|
for icon, text, mode in modes:
|
653
752
|
btn_txt = icon + text
|
@@ -703,11 +802,25 @@ class LayerifyTab(QWidget):
|
|
703
802
|
ImageEntry(is_baked_result=False, data=img_path)
|
704
803
|
)
|
705
804
|
|
805
|
+
# load from bake folder if it exists
|
806
|
+
bake_folder = self.config.bake_dir
|
807
|
+
if bake_folder.exists() and bake_folder.is_dir():
|
808
|
+
for img_path in bake_folder.glob("*.*"):
|
809
|
+
if img_path.suffix.lower() in [
|
810
|
+
".jpg",
|
811
|
+
".jpeg",
|
812
|
+
".png",
|
813
|
+
".bmp",
|
814
|
+
".tiff",
|
815
|
+
]:
|
816
|
+
self.image_entries.append(
|
817
|
+
ImageEntry(is_baked_result=False, data=img_path)
|
818
|
+
)
|
819
|
+
|
706
820
|
def select_folder(self):
|
707
821
|
"""Allow the user to select a folder and load images from it."""
|
708
822
|
folder_path = QFileDialog.getExistingDirectory(self, "Select Folder")
|
709
823
|
if folder_path:
|
710
|
-
self.image_entries = [] # Clear the existing image paths
|
711
824
|
folder_path = Path(folder_path)
|
712
825
|
|
713
826
|
self._load_images_from_folder(folder_path)
|
@@ -726,9 +839,6 @@ class LayerifyTab(QWidget):
|
|
726
839
|
# Load the first set of images into the layers
|
727
840
|
self.load_default_images()
|
728
841
|
|
729
|
-
# Unhide the next/prev buttons if there are multiple images
|
730
|
-
self.next_image_btn.setVisible(len(self.image_entries) > 1)
|
731
|
-
self.prev_image_btn.setVisible(len(self.image_entries) > 1)
|
732
842
|
else:
|
733
843
|
QMessageBox.warning(
|
734
844
|
self,
|
@@ -736,28 +846,6 @@ class LayerifyTab(QWidget):
|
|
736
846
|
"No valid image files found in the selected folder.",
|
737
847
|
)
|
738
848
|
|
739
|
-
def show_next_image(self):
|
740
|
-
"""Show next image in the list. If at the end, show first image."""
|
741
|
-
if self.curr_image_idx < len(self.image_entries) - 1:
|
742
|
-
self.curr_image_idx += 1
|
743
|
-
else:
|
744
|
-
self.curr_image_idx = 0
|
745
|
-
self.layer.set_image(self.image_entries[self.curr_image_idx]["data"])
|
746
|
-
self.messageSignal.emit(
|
747
|
-
f"Showing image {self.curr_image_idx + 1}/{len(self.image_entries)}"
|
748
|
-
)
|
749
|
-
|
750
|
-
def show_prev_image(self):
|
751
|
-
"""Show previous image in the list. If at the start, show last image."""
|
752
|
-
if self.curr_image_idx > 0:
|
753
|
-
self.curr_image_idx -= 1
|
754
|
-
else:
|
755
|
-
self.curr_image_idx = len(self.image_entries) - 1
|
756
|
-
self.layer.set_image(self.image_entries[self.curr_image_idx]["data"])
|
757
|
-
self.messageSignal.emit(
|
758
|
-
f"Showing image {self.curr_image_idx + 1}/{len(self.image_entries)}"
|
759
|
-
)
|
760
|
-
|
761
849
|
def __del__(self):
|
762
850
|
logger.warning(f"Tab {id(self)} deleted")
|
763
851
|
|
@@ -770,10 +858,20 @@ class LayerifyTab(QWidget):
|
|
770
858
|
config=self.config,
|
771
859
|
canvas_config=self.canvas_config,
|
772
860
|
)
|
773
|
-
|
861
|
+
# save it in cache
|
862
|
+
filename = baking_result.filename
|
863
|
+
filepath = self.config.bake_dir / filename.name
|
864
|
+
baking_result.image.save(str(filepath))
|
865
|
+
#
|
866
|
+
Annotation.save_as_json(
|
867
|
+
baking_result.annotations, self.config.cache_dir / f"{filename.name}.json"
|
868
|
+
)
|
774
869
|
|
775
|
-
layer.
|
776
|
-
|
870
|
+
layer.set_image(filepath)
|
871
|
+
|
872
|
+
layer.annotationAdded.connect(self.on_annotation_added)
|
873
|
+
layer.annotationUpdated.connect(self.on_annotation_updated)
|
874
|
+
layer.labelUpdated.connect(self.on_label_update)
|
777
875
|
layer.messageSignal.connect(self.messageSignal)
|
778
876
|
layer.layerSignal.connect(self.add_layer)
|
779
877
|
|
@@ -785,7 +883,7 @@ class LayerifyTab(QWidget):
|
|
785
883
|
self.annotable_layers.append(layer)
|
786
884
|
|
787
885
|
# Add baked result to image_entries
|
788
|
-
baked_result_entry = ImageEntry(is_baked_result=
|
886
|
+
baked_result_entry = ImageEntry(is_baked_result=False, data=filepath)
|
789
887
|
self.image_entries.append(baked_result_entry)
|
790
888
|
# baking_result.image.save(str(baking_result.filename))
|
791
889
|
layer.update()
|
@@ -793,6 +891,12 @@ class LayerifyTab(QWidget):
|
|
793
891
|
logger.info("A baked result has arrived, adding it to the image list.")
|
794
892
|
|
795
893
|
# Update the image list panel
|
894
|
+
# find the page index where this layer is
|
895
|
+
page_index = (
|
896
|
+
self.image_entries.index(baked_result_entry) // self.config.deque_maxlen
|
897
|
+
)
|
898
|
+
# set the current page to the page index
|
899
|
+
self.image_list_panel.current_page = page_index
|
796
900
|
self.image_list_panel.update_image_list(self.image_entries)
|
797
901
|
self.image_list_panel.imageSelected.emit(baked_result_entry)
|
798
902
|
|
@@ -831,6 +935,19 @@ class LayerifyTab(QWidget):
|
|
831
935
|
self.annotation_list.update_list()
|
832
936
|
logger.info("Selected annotation deleted.")
|
833
937
|
|
938
|
+
# if clicked q, set the mode to point
|
939
|
+
elif key == Qt.Key_Q:
|
940
|
+
self.layer.set_mode(MouseMode.POINT)
|
941
|
+
logger.info("Mouse mode set to POINT.")
|
942
|
+
# if clicked w, set the mode to polygon
|
943
|
+
elif key == Qt.Key_W:
|
944
|
+
self.layer.set_mode(MouseMode.POLYGON)
|
945
|
+
logger.info("Mouse mode set to POLYGON.")
|
946
|
+
# if clicked e, set the mode to rectangle
|
947
|
+
elif key == Qt.Key_E:
|
948
|
+
self.layer.set_mode(MouseMode.RECTANGLE)
|
949
|
+
logger.info("Mouse mode set to RECTANGLE.")
|
950
|
+
|
834
951
|
# Pass the event to the annotation list if it needs to handle it
|
835
952
|
if self.annotation_list.hasFocus():
|
836
953
|
self.annotation_list.keyPressEvent(event)
|
imagebaker/utils/image.py
CHANGED
@@ -49,8 +49,11 @@ def draw_annotations(image: np.ndarray, annotations: list[Annotation]) -> np.nda
|
|
49
49
|
Returns:
|
50
50
|
np.ndarray: Image with annotations drawn.
|
51
51
|
"""
|
52
|
+
color = (0, 255, 0, 255) if image.shape[2] == 4 else (0, 255, 0)
|
53
|
+
|
52
54
|
for i, ann in enumerate(annotations):
|
53
55
|
if ann.rectangle:
|
56
|
+
# if image has alpha channel, make color full alpha
|
54
57
|
cv2.rectangle(
|
55
58
|
image,
|
56
59
|
(int(ann.rectangle.x()), int(ann.rectangle.y())),
|
@@ -58,7 +61,7 @@ def draw_annotations(image: np.ndarray, annotations: list[Annotation]) -> np.nda
|
|
58
61
|
int(ann.rectangle.x() + ann.rectangle.width()),
|
59
62
|
int(ann.rectangle.y() + ann.rectangle.height()),
|
60
63
|
),
|
61
|
-
|
64
|
+
color,
|
62
65
|
2,
|
63
66
|
)
|
64
67
|
rect_center = ann.rectangle.center()
|
@@ -69,7 +72,7 @@ def draw_annotations(image: np.ndarray, annotations: list[Annotation]) -> np.nda
|
|
69
72
|
(int(rect_center.x()), int(rect_center.y())),
|
70
73
|
cv2.FONT_HERSHEY_SIMPLEX,
|
71
74
|
1,
|
72
|
-
|
75
|
+
color,
|
73
76
|
2,
|
74
77
|
)
|
75
78
|
elif ann.polygon:
|
@@ -77,7 +80,7 @@ def draw_annotations(image: np.ndarray, annotations: list[Annotation]) -> np.nda
|
|
77
80
|
image,
|
78
81
|
[np.array([[int(p.x()), int(p.y())] for p in ann.polygon])],
|
79
82
|
True,
|
80
|
-
|
83
|
+
color,
|
81
84
|
2,
|
82
85
|
)
|
83
86
|
polygon_center = ann.polygon.boundingRect().center()
|
@@ -87,19 +90,19 @@ def draw_annotations(image: np.ndarray, annotations: list[Annotation]) -> np.nda
|
|
87
90
|
(int(polygon_center.x()), int(polygon_center.y())),
|
88
91
|
cv2.FONT_HERSHEY_SIMPLEX,
|
89
92
|
1,
|
90
|
-
|
93
|
+
color,
|
91
94
|
2,
|
92
95
|
)
|
93
96
|
elif ann.points:
|
94
97
|
for p in ann.points:
|
95
|
-
cv2.circle(image, (int(p.x()), int(p.y())), 5,
|
98
|
+
cv2.circle(image, (int(p.x()), int(p.y())), 5, color, -1)
|
96
99
|
cv2.putText(
|
97
100
|
image,
|
98
101
|
ann.label,
|
99
102
|
(int(ann.points[0].x()), int(ann.points[0].y())),
|
100
103
|
cv2.FONT_HERSHEY_SIMPLEX,
|
101
104
|
1,
|
102
|
-
|
105
|
+
color,
|
103
106
|
2,
|
104
107
|
)
|
105
108
|
return image
|
imagebaker/window/main_window.py
CHANGED
@@ -175,9 +175,15 @@ class MainWindow(QMainWindow):
|
|
175
175
|
self.status_bar.showMessage(status_text)
|
176
176
|
|
177
177
|
def closeEvent(self, event):
|
178
|
-
|
179
|
-
if
|
180
|
-
|
181
|
-
|
182
|
-
self.
|
183
|
-
|
178
|
+
logger.info("Closing the application.")
|
179
|
+
if self.layerify_config.cleanup_on_exit:
|
180
|
+
import shutil
|
181
|
+
|
182
|
+
if self.layerify_config.bake_dir.exists():
|
183
|
+
shutil.rmtree(self.layerify_config.bake_dir)
|
184
|
+
logger.info(f"Deleted bake directory: {self.layerify_config.bake_dir}")
|
185
|
+
if self.layerify_config.cache_dir.exists():
|
186
|
+
shutil.rmtree(self.layerify_config.cache_dir)
|
187
|
+
logger.info(
|
188
|
+
f"Deleted cache directory: {self.layerify_config.cache_dir}"
|
189
|
+
)
|
@@ -224,6 +224,8 @@ class BakerWorker(QObject):
|
|
224
224
|
self.finished.emit(results)
|
225
225
|
|
226
226
|
except Exception as e:
|
227
|
+
import traceback
|
228
|
+
|
227
229
|
logger.error(f"Error in BakerWorker: {e}")
|
228
230
|
self.error.emit(str(e))
|
229
231
|
traceback.print_exc()
|
@@ -248,9 +250,14 @@ class BakerWorker(QObject):
|
|
248
250
|
new_annotation.points = ann.points
|
249
251
|
elif ann.rectangle:
|
250
252
|
xywhs = mask_to_rectangles(alpha_channel, merge_rectangles=True)
|
251
|
-
|
252
|
-
|
253
|
-
|
253
|
+
if len(xywhs) == 0:
|
254
|
+
logger.info("No rectangles found")
|
255
|
+
# return None
|
256
|
+
else:
|
257
|
+
logger.info(f"Found {len(xywhs)} rectangles")
|
258
|
+
new_annotation.rectangle = QRectF(
|
259
|
+
xywhs[0][0], xywhs[0][1], xywhs[0][2], xywhs[0][3]
|
260
|
+
)
|
254
261
|
elif ann.polygon:
|
255
262
|
polygon = mask_to_polygons(alpha_channel, merge_polygons=True)
|
256
263
|
poly = QPolygonF([QPointF(p[0], p[1]) for p in polygon[0]])
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: imagebaker
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.51
|
4
4
|
Summary: A package for baking images.
|
5
5
|
Home-page: https://github.com/q-viper/Image-Baker
|
6
6
|
Author: Ramkrishna Acharya
|
@@ -112,6 +112,9 @@ After cloning and going to the project directory, the following code should work
|
|
112
112
|
* **Ctrl + D**: Draw Mode on Baker Tab. Drawing can happen on a selected or main layer.
|
113
113
|
* **Ctrl + E**: Erase Mode on Baker Tab.
|
114
114
|
* **Wheel**: Change the size of the drawing pointer.
|
115
|
+
* **Q**: Point mode on annotation.
|
116
|
+
* **W**: Polygon mode on annotation.
|
117
|
+
* **E**: Rectangle mode on annotation.
|
115
118
|
|
116
119
|
## Demo
|
117
120
|
### Annotation Page
|
@@ -1,43 +1,43 @@
|
|
1
1
|
imagebaker/__init__.py,sha256=zrrxwyzuqVNeIu3rVPrOGYf6SCd5kWsoGcdqqUfsYX4,258
|
2
2
|
imagebaker/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
3
|
imagebaker/core/configs/__init__.py,sha256=iyR_GOVMFw3XJSm7293YfyTnaLZa7pLQMfn5tGxVofI,31
|
4
|
-
imagebaker/core/configs/configs.py,sha256
|
4
|
+
imagebaker/core/configs/configs.py,sha256=hm0CHfMVTD9353wB61Df9bVUb898zQVIFjdoXELdJcc,5694
|
5
5
|
imagebaker/core/defs/__init__.py,sha256=NqV7gYIlRkaS7nx_UTNPSNZbdPrx4w-VurKOKyRLbKY,28
|
6
|
-
imagebaker/core/defs/defs.py,sha256
|
6
|
+
imagebaker/core/defs/defs.py,sha256=oZCkgqHgvE_yupUzD5IB_ZMoRhyTh0EdCR8Gmh6TMsI,8505
|
7
7
|
imagebaker/core/plugins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
8
|
imagebaker/core/plugins/base_plugin.py,sha256=ROa1HTwV5LgGL-40CHKk_5MZYI5QAT1MzpYO7Fx-9P0,1084
|
9
9
|
imagebaker/core/plugins/cosine_plugin.py,sha256=IXBfvaoxrvf-hytg_dT1zFmOfDbcWXBZ7NvIFPJ2tWQ,1251
|
10
10
|
imagebaker/layers/__init__.py,sha256=q1kUDHhUXEGBOdu6CHDfqCnE2mraLHRqh0DFHYTbnRY,158
|
11
|
-
imagebaker/layers/annotable_layer.py,sha256=
|
11
|
+
imagebaker/layers/annotable_layer.py,sha256=xkV6KTXN8-N6Wu3QAj5nQDMPZ4bRr673_fu5QuRKBj8,37128
|
12
12
|
imagebaker/layers/base_layer.py,sha256=1K7Nt6OPITrILj-p4I6Jf0eaesCpdeecOXGj_8oAQb8,30650
|
13
|
-
imagebaker/layers/canvas_layer.py,sha256=
|
13
|
+
imagebaker/layers/canvas_layer.py,sha256=6quVe_Ieyz14zPL92xTfrinAT7X4yb9iyEl-f9SnQZU,42116
|
14
14
|
imagebaker/list_views/__init__.py,sha256=Aa9slE6do8eYgZp77wrofpd_mlBDwxgF3adMyHYFanE,144
|
15
|
-
imagebaker/list_views/annotation_list.py,sha256=
|
15
|
+
imagebaker/list_views/annotation_list.py,sha256=w6jNMyo3nxhQKZx8MyusNvxs_R6Pj-b-YBI5e0APZVM,7528
|
16
16
|
imagebaker/list_views/canvas_list.py,sha256=JYSYR0peGyJFJ6amL1894KsUHETPUkR3qAWdGL50Lbc,6717
|
17
|
-
imagebaker/list_views/image_list.py,sha256=
|
17
|
+
imagebaker/list_views/image_list.py,sha256=EX2YmoTRffBXTbbVnuflbdsxZQD8fgM2StSxg1a9ANA,5389
|
18
18
|
imagebaker/list_views/layer_list.py,sha256=fLx3Ry72fas1W5y_V84hSp41ARneogQN3qjfYTOcpxY,14476
|
19
19
|
imagebaker/list_views/layer_settings.py,sha256=0WVSCm_RSBKo4pCkYU5c2OYjb_sW8x0UUfFC4So26jQ,9752
|
20
20
|
imagebaker/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
21
21
|
imagebaker/models/base_model.py,sha256=4RyS4vShqWFHhdQDwYluwTPnRPEXjpZl9UjYY_w8NL0,4203
|
22
22
|
imagebaker/tabs/__init__.py,sha256=ijg7MA17RvcHA2AuZE4OgRJXWxjecaUAlfASKAoCQ6Q,86
|
23
23
|
imagebaker/tabs/baker_tab.py,sha256=yFQRiNmLIua5BvgW7Ysj5FrNbTLhGxqGRTRCABxeTtw,20493
|
24
|
-
imagebaker/tabs/layerify_tab.py,sha256=
|
24
|
+
imagebaker/tabs/layerify_tab.py,sha256=F3BK5XsVCteB-2zyFBKqeWyHgIif5cU3TBEMbuDH3QM,37415
|
25
25
|
imagebaker/utils/__init__.py,sha256=I1z5VVEf6QPOMvVkgVHDauQ9ew7tcTVguV4Kdi3Lk4Y,130
|
26
|
-
imagebaker/utils/image.py,sha256=
|
26
|
+
imagebaker/utils/image.py,sha256=2-wbwD3PMwefgswge0drFM1XfXE7yQ64lqZZ5PwyCWs,3165
|
27
27
|
imagebaker/utils/state_utils.py,sha256=Y2JVRGVfsoffwfA2lsCcqHwIxH_jOrEJAdH-oWfe2XE,3841
|
28
28
|
imagebaker/utils/transform_mask.py,sha256=k8MfTgM5-_U2TvDHQHRelz-leGFX6OcsllV6-J4BKfw,3651
|
29
29
|
imagebaker/utils/utils.py,sha256=MnJ4flxxwZbjROWJ5iKHnJxPSSMbfWRbF9GKfVcKutA,840
|
30
30
|
imagebaker/utils/vis.py,sha256=f7c44gm6g9ja5hgVeXKfOhHzxHdzXcIUwKiA1RZU_F8,4736
|
31
31
|
imagebaker/window/__init__.py,sha256=FIxtUR1qnbQMYzppQv7tEfv1-ueHhpu0Z7xuWZR794w,44
|
32
32
|
imagebaker/window/app.py,sha256=e6FGO_BnvkiQC9JN3AmqkgbF72zzZS0hc7PFc43QiVc,4725
|
33
|
-
imagebaker/window/main_window.py,sha256
|
33
|
+
imagebaker/window/main_window.py,sha256=-CP7q6xzkhv9Cl5RwUft1Rv8nIAuj_SLOiZDS2x6QJ4,7374
|
34
34
|
imagebaker/workers/__init__.py,sha256=XfXENwAYyNg9q_zR-gOsYJGjzwg_iIb_gING8ydnp9c,154
|
35
|
-
imagebaker/workers/baker_worker.py,sha256=
|
35
|
+
imagebaker/workers/baker_worker.py,sha256=_jWeyYAGoO2mfxXDn7fBm9tIA69OITewDVN0hSAt3Jc,11532
|
36
36
|
imagebaker/workers/layerify_worker.py,sha256=EOqKvhdACtf3y5Ljy6M7MvddAjlZW5DNfBFMtNPD-us,3223
|
37
37
|
imagebaker/workers/model_worker.py,sha256=Tlg6_D977iK-kuGCNdQY4OnGiP8QqWY7adpRNXZw4rA,1636
|
38
|
-
imagebaker-0.0.
|
39
|
-
imagebaker-0.0.
|
40
|
-
imagebaker-0.0.
|
41
|
-
imagebaker-0.0.
|
42
|
-
imagebaker-0.0.
|
43
|
-
imagebaker-0.0.
|
38
|
+
imagebaker-0.0.51.dist-info/LICENSE,sha256=1vkysFPOnT7y4LsoFTv9YsopIrQvBc2l6vUOfv4KKLc,1082
|
39
|
+
imagebaker-0.0.51.dist-info/METADATA,sha256=1DumEPL4LyKVeuV_tDwyepZ9PsreraeKSTEJBernVmw,6940
|
40
|
+
imagebaker-0.0.51.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
41
|
+
imagebaker-0.0.51.dist-info/entry_points.txt,sha256=IDjZHJCiiHpH5IUTByT2en0nMbnnnlrJZ5FPFehUvQM,61
|
42
|
+
imagebaker-0.0.51.dist-info/top_level.txt,sha256=Gg-eILTlqJXwVQr0saSwsx3-H4SPdZ2agBZaufe194s,11
|
43
|
+
imagebaker-0.0.51.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|