imagebaker 0.0.50__py3-none-any.whl → 0.52__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 +15 -8
- 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 +171 -49
- 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.52.dist-info}/METADATA +6 -1
- {imagebaker-0.0.50.dist-info → imagebaker-0.52.dist-info}/RECORD +16 -16
- {imagebaker-0.0.50.dist-info → imagebaker-0.52.dist-info}/LICENSE +0 -0
- {imagebaker-0.0.50.dist-info → imagebaker-0.52.dist-info}/WHEEL +0 -0
- {imagebaker-0.0.50.dist-info → imagebaker-0.52.dist-info}/entry_points.txt +0 -0
- {imagebaker-0.0.50.dist-info → imagebaker-0.52.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:
|
@@ -603,6 +602,15 @@ class AnnotableLayer(BaseLayer):
|
|
603
602
|
elif self.mouse_mode == MouseMode.POLYGON:
|
604
603
|
# If not double-click
|
605
604
|
if not self.current_annotation:
|
605
|
+
# if this point is equal to the last point of the previous polygon, then ignore it
|
606
|
+
if len(self.annotations) > 0:
|
607
|
+
last_polygon = self.annotations[-1].polygon
|
608
|
+
if last_polygon:
|
609
|
+
last_point = last_polygon[-1]
|
610
|
+
if last_point== clamped_pos:
|
611
|
+
logger.info("Ignoring point, same as last polygon point")
|
612
|
+
return
|
613
|
+
|
606
614
|
self.current_annotation = Annotation(
|
607
615
|
file_path=self.file_path,
|
608
616
|
annotation_id=len(self.annotations),
|
@@ -751,8 +759,8 @@ class AnnotableLayer(BaseLayer):
|
|
751
759
|
and len(self.current_annotation.polygon) >= 3
|
752
760
|
):
|
753
761
|
self.current_annotation.is_complete = True
|
762
|
+
|
754
763
|
self.finalize_annotation()
|
755
|
-
self.annotationAdded.emit(self.current_annotation)
|
756
764
|
self.current_annotation = None
|
757
765
|
|
758
766
|
return
|
@@ -794,6 +802,7 @@ class AnnotableLayer(BaseLayer):
|
|
794
802
|
self, "Edit Label", "Enter new label:", text=annotation.label
|
795
803
|
)
|
796
804
|
if ok and new_label:
|
805
|
+
self.labelUpdated.emit((annotation.label, new_label))
|
797
806
|
annotation.label = new_label
|
798
807
|
self.annotationUpdated.emit(annotation) # Emit signal
|
799
808
|
self.update()
|
@@ -811,8 +820,6 @@ class AnnotableLayer(BaseLayer):
|
|
811
820
|
self.current_annotation
|
812
821
|
)
|
813
822
|
self.annotationAdded.emit(self.current_annotation)
|
814
|
-
self.current_annotation = None
|
815
|
-
self.update()
|
816
823
|
else:
|
817
824
|
# Show custom label dialog
|
818
825
|
label, ok = QInputDialog.getText(self, "Label", "Enter label name:")
|
@@ -827,8 +834,8 @@ class AnnotableLayer(BaseLayer):
|
|
827
834
|
)
|
828
835
|
self.annotationAdded.emit(self.current_annotation)
|
829
836
|
self.current_annotation.annotation_id = len(self.annotations)
|
830
|
-
|
831
|
-
|
837
|
+
self.current_annotation = None
|
838
|
+
self.update()
|
832
839
|
|
833
840
|
# in update, update cursor
|
834
841
|
|
@@ -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)
|
@@ -162,10 +169,14 @@ class LayerifyTab(QWidget):
|
|
162
169
|
for idx, layer in enumerate(self.annotable_layers):
|
163
170
|
layer.setVisible(False)
|
164
171
|
# logger.info(f"Layer {idx} hidden.")
|
165
|
-
|
172
|
+
current_label = self.layer.current_label
|
173
|
+
current_color = self.layer.current_color
|
174
|
+
|
166
175
|
if not image_entry.is_baked_result: # Regular image
|
167
176
|
image_path = image_entry.data
|
168
177
|
self.curr_image_idx = self.image_entries.index(image_entry)
|
178
|
+
# convert curr_image_idx to correct index
|
179
|
+
self.curr_image_idx = self.curr_image_idx % len(self.annotable_layers)
|
169
180
|
|
170
181
|
# Make the corresponding layer visible and set the image
|
171
182
|
selected_layer = self.annotable_layers[self.curr_image_idx]
|
@@ -185,6 +196,9 @@ class LayerifyTab(QWidget):
|
|
185
196
|
# logger.info(f"Layer {self.curr_image_idx} made visible for baked result.")
|
186
197
|
self.layer = baked_result_layer # Set the baked result as the current layer
|
187
198
|
|
199
|
+
# Set the current label and color
|
200
|
+
self.layer.current_label = current_label
|
201
|
+
self.layer.current_color = current_color
|
188
202
|
self.annotation_list.layer = self.layer
|
189
203
|
self.annotation_list.update_list()
|
190
204
|
|
@@ -206,6 +220,8 @@ class LayerifyTab(QWidget):
|
|
206
220
|
for i, layer in enumerate(self.annotable_layers):
|
207
221
|
if i < len(self.image_entries):
|
208
222
|
layer.set_image(self.image_entries[i].data)
|
223
|
+
self.load_layer_annotations(layer)
|
224
|
+
layer.layer_name = f"Layer_{i + 1}"
|
209
225
|
layer.setVisible(
|
210
226
|
i == 0
|
211
227
|
) # Only the first layer is visible by default
|
@@ -224,12 +240,68 @@ class LayerifyTab(QWidget):
|
|
224
240
|
self.image_list_panel.update_image_list(self.image_entries)
|
225
241
|
self.update()
|
226
242
|
|
243
|
+
def save_layer_annotations(self, layer: AnnotableLayer):
|
244
|
+
"""Save annotations for a specific layer"""
|
245
|
+
if len(layer.annotations) > 0:
|
246
|
+
file_path = layer.file_path
|
247
|
+
file_name = file_path.name
|
248
|
+
save_dir = self.config.cache_dir / f"{file_name}.json"
|
249
|
+
Annotation.save_as_json(layer.annotations, save_dir)
|
250
|
+
logger.info(f"Saved annotations for {layer.layer_name} to {save_dir}")
|
251
|
+
|
252
|
+
def load_layer_annotations(self, layer: AnnotableLayer):
|
253
|
+
"""Load annotations for a specific layer"""
|
254
|
+
if layer.file_path:
|
255
|
+
file_path = layer.file_path
|
256
|
+
file_name = file_path.name
|
257
|
+
load_dir = self.config.cache_dir / f"{file_name}.json"
|
258
|
+
if load_dir.exists():
|
259
|
+
layer.annotations = Annotation.load_from_json(load_dir)
|
260
|
+
logger.info(
|
261
|
+
f"Loaded annotations for {layer.layer_name} from {load_dir}"
|
262
|
+
)
|
263
|
+
else:
|
264
|
+
logger.warning(f"No annotations found for {layer.layer_name}")
|
265
|
+
|
266
|
+
def update_active_entries(self, image_entries: list[ImageEntry]):
|
267
|
+
"""Update the active entries in the image list panel."""
|
268
|
+
self.curr_image_idx = 0
|
269
|
+
for i, layer in enumerate(self.annotable_layers):
|
270
|
+
self.save_layer_annotations(layer)
|
271
|
+
layer.annotations = []
|
272
|
+
|
273
|
+
if i < len(image_entries):
|
274
|
+
# get index on the self.image_entries
|
275
|
+
idx = self.image_entries.index(image_entries[i])
|
276
|
+
if self.image_entries[idx].is_baked_result:
|
277
|
+
# if the image is a baked result, set the layer to the baked result
|
278
|
+
layer = self.image_entries[idx].data
|
279
|
+
layer.file_path = layer.file_path
|
280
|
+
else:
|
281
|
+
layer.set_image(self.image_entries[idx].data)
|
282
|
+
self.load_layer_annotations(layer)
|
283
|
+
|
284
|
+
layer.layer_name = f"Layer_{idx + 1}"
|
285
|
+
layer.setVisible(i == 0)
|
286
|
+
if i == 0:
|
287
|
+
self.layer = layer
|
288
|
+
else:
|
289
|
+
layer.setVisible(False)
|
290
|
+
logger.info("Updated active entries in image list panel.")
|
291
|
+
|
227
292
|
def clear_annotations(self):
|
228
293
|
"""Safely clear all annotations"""
|
229
294
|
try:
|
230
295
|
# Clear layer annotations
|
231
296
|
self.clearAnnotations.emit()
|
232
297
|
self.messageSignal.emit("Annotations cleared")
|
298
|
+
# clear cache annotation of layer
|
299
|
+
annotation_path = (
|
300
|
+
self.config.cache_dir / f"{self.layer.file_path.name}.json"
|
301
|
+
)
|
302
|
+
if annotation_path.exists():
|
303
|
+
os.remove(annotation_path)
|
304
|
+
logger.info(f"Cleared annotations from {annotation_path}")
|
233
305
|
|
234
306
|
except Exception as e:
|
235
307
|
logger.error(f"Clear error: {str(e)}")
|
@@ -241,13 +313,17 @@ class LayerifyTab(QWidget):
|
|
241
313
|
Args:
|
242
314
|
annotation (Annotation): The annotation that was added.
|
243
315
|
"""
|
244
|
-
|
316
|
+
|
317
|
+
# if annotation.label is not in the predefined labels, add it
|
318
|
+
if annotation.label not in [lbl.name for lbl in self.config.predefined_labels]:
|
319
|
+
logger.info(f"Label {annotation.label} created.")
|
245
320
|
self.config.predefined_labels.append(
|
246
321
|
Label(annotation.label, annotation.color)
|
247
322
|
)
|
248
323
|
self.update_label_combo()
|
249
324
|
logger.info(f"Added annotation: {annotation.label}")
|
250
325
|
self.messageSignal.emit(f"Added annotation: {annotation.label}")
|
326
|
+
self.save_layer_annotations(self.layer)
|
251
327
|
|
252
328
|
# Refresh the annotation list
|
253
329
|
self.annotation_list.update_list()
|
@@ -259,11 +335,12 @@ class LayerifyTab(QWidget):
|
|
259
335
|
Args:
|
260
336
|
annotation (Annotation): The updated annotation.
|
261
337
|
"""
|
262
|
-
logger.info(f"Updated annotation: {annotation
|
338
|
+
# logger.info(f"Updated annotation: {annotation}")
|
263
339
|
self.messageSignal.emit(f"Updated annotation: {annotation.label}")
|
264
340
|
|
265
341
|
# Refresh the annotation list
|
266
342
|
self.annotation_list.update_list()
|
343
|
+
self.save_layer_annotations(self.layer)
|
267
344
|
|
268
345
|
def update_label_combo(self):
|
269
346
|
"""
|
@@ -276,6 +353,27 @@ class LayerifyTab(QWidget):
|
|
276
353
|
pixmap = QPixmap(16, 16)
|
277
354
|
pixmap.fill(label.color)
|
278
355
|
self.label_combo.addItem(QIcon(pixmap), label.name)
|
356
|
+
logger.info("Updated label combo box with predefined labels.")
|
357
|
+
self.label_combo.setCurrentText(self.current_label)
|
358
|
+
|
359
|
+
def on_label_update(self, old_new_label: tuple[str, str]):
|
360
|
+
new_labels = []
|
361
|
+
index = 0
|
362
|
+
for i, label in enumerate(self.config.predefined_labels):
|
363
|
+
if label.name == old_new_label[0]:
|
364
|
+
label.name = old_new_label[1]
|
365
|
+
index = i
|
366
|
+
new_labels.append(label)
|
367
|
+
|
368
|
+
self.config.predefined_labels = new_labels
|
369
|
+
logger.info(f"Updated label from {old_new_label[0]} to {old_new_label[1]}")
|
370
|
+
self.messageSignal.emit(
|
371
|
+
f"Updated label from {old_new_label[0]} to {old_new_label[1]}."
|
372
|
+
)
|
373
|
+
|
374
|
+
self.update_label_combo()
|
375
|
+
self.handle_label_change(index=index)
|
376
|
+
self.label_combo.update()
|
279
377
|
|
280
378
|
def load_default_image(self):
|
281
379
|
"""
|
@@ -589,6 +687,24 @@ class LayerifyTab(QWidget):
|
|
589
687
|
)
|
590
688
|
msg = f"Label changed to {self.current_label}"
|
591
689
|
self.messageSignal.emit(msg)
|
690
|
+
self.layer.selected_annotation = self.layer._get_selected_annotation()
|
691
|
+
if self.layer.selected_annotation:
|
692
|
+
annotations = []
|
693
|
+
for ann in self.layer.annotations:
|
694
|
+
if ann == self.layer.selected_annotation:
|
695
|
+
ann.label = label_info.name
|
696
|
+
ann.color = label_info.color
|
697
|
+
annotations.append(ann)
|
698
|
+
|
699
|
+
self.layer.annotations = annotations
|
700
|
+
self.on_annotation_updated(self.layer.selected_annotation)
|
701
|
+
# disable label change callback
|
702
|
+
self.label_combo.currentIndexChanged.disconnect()
|
703
|
+
self.label_combo.currentIndexChanged.connect(lambda: None)
|
704
|
+
self.update_label_combo()
|
705
|
+
# set it back
|
706
|
+
self.label_combo.currentIndexChanged.connect(self.handle_label_change)
|
707
|
+
|
592
708
|
self.layer.update()
|
593
709
|
self.update()
|
594
710
|
|
@@ -628,7 +744,7 @@ class LayerifyTab(QWidget):
|
|
628
744
|
("🎨", "Color", self.choose_color),
|
629
745
|
("🧅", "Layerify All", self.layerify_all),
|
630
746
|
("🏷️", "Add Label", self.add_new_label),
|
631
|
-
("🗑️", "Clear",
|
747
|
+
("🗑️", "Clear", self.clear_annotations),
|
632
748
|
]
|
633
749
|
|
634
750
|
# Folder navigation buttons
|
@@ -636,18 +752,6 @@ class LayerifyTab(QWidget):
|
|
636
752
|
self.select_folder_btn.clicked.connect(self.select_folder)
|
637
753
|
toolbar_layout.addWidget(self.select_folder_btn)
|
638
754
|
|
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
755
|
# Add mode buttons
|
652
756
|
for icon, text, mode in modes:
|
653
757
|
btn_txt = icon + text
|
@@ -703,11 +807,25 @@ class LayerifyTab(QWidget):
|
|
703
807
|
ImageEntry(is_baked_result=False, data=img_path)
|
704
808
|
)
|
705
809
|
|
810
|
+
# load from bake folder if it exists
|
811
|
+
bake_folder = self.config.bake_dir
|
812
|
+
if bake_folder.exists() and bake_folder.is_dir():
|
813
|
+
for img_path in bake_folder.glob("*.*"):
|
814
|
+
if img_path.suffix.lower() in [
|
815
|
+
".jpg",
|
816
|
+
".jpeg",
|
817
|
+
".png",
|
818
|
+
".bmp",
|
819
|
+
".tiff",
|
820
|
+
]:
|
821
|
+
self.image_entries.append(
|
822
|
+
ImageEntry(is_baked_result=False, data=img_path)
|
823
|
+
)
|
824
|
+
|
706
825
|
def select_folder(self):
|
707
826
|
"""Allow the user to select a folder and load images from it."""
|
708
827
|
folder_path = QFileDialog.getExistingDirectory(self, "Select Folder")
|
709
828
|
if folder_path:
|
710
|
-
self.image_entries = [] # Clear the existing image paths
|
711
829
|
folder_path = Path(folder_path)
|
712
830
|
|
713
831
|
self._load_images_from_folder(folder_path)
|
@@ -726,9 +844,6 @@ class LayerifyTab(QWidget):
|
|
726
844
|
# Load the first set of images into the layers
|
727
845
|
self.load_default_images()
|
728
846
|
|
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
847
|
else:
|
733
848
|
QMessageBox.warning(
|
734
849
|
self,
|
@@ -736,28 +851,6 @@ class LayerifyTab(QWidget):
|
|
736
851
|
"No valid image files found in the selected folder.",
|
737
852
|
)
|
738
853
|
|
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
854
|
def __del__(self):
|
762
855
|
logger.warning(f"Tab {id(self)} deleted")
|
763
856
|
|
@@ -770,10 +863,20 @@ class LayerifyTab(QWidget):
|
|
770
863
|
config=self.config,
|
771
864
|
canvas_config=self.canvas_config,
|
772
865
|
)
|
773
|
-
|
866
|
+
# save it in cache
|
867
|
+
filename = baking_result.filename
|
868
|
+
filepath = self.config.bake_dir / filename.name
|
869
|
+
baking_result.image.save(str(filepath))
|
870
|
+
#
|
871
|
+
Annotation.save_as_json(
|
872
|
+
baking_result.annotations, self.config.cache_dir / f"{filename.name}.json"
|
873
|
+
)
|
774
874
|
|
775
|
-
layer.
|
776
|
-
|
875
|
+
layer.set_image(filepath)
|
876
|
+
|
877
|
+
layer.annotationAdded.connect(self.on_annotation_added)
|
878
|
+
layer.annotationUpdated.connect(self.on_annotation_updated)
|
879
|
+
layer.labelUpdated.connect(self.on_label_update)
|
777
880
|
layer.messageSignal.connect(self.messageSignal)
|
778
881
|
layer.layerSignal.connect(self.add_layer)
|
779
882
|
|
@@ -785,7 +888,7 @@ class LayerifyTab(QWidget):
|
|
785
888
|
self.annotable_layers.append(layer)
|
786
889
|
|
787
890
|
# Add baked result to image_entries
|
788
|
-
baked_result_entry = ImageEntry(is_baked_result=
|
891
|
+
baked_result_entry = ImageEntry(is_baked_result=False, data=filepath)
|
789
892
|
self.image_entries.append(baked_result_entry)
|
790
893
|
# baking_result.image.save(str(baking_result.filename))
|
791
894
|
layer.update()
|
@@ -793,6 +896,12 @@ class LayerifyTab(QWidget):
|
|
793
896
|
logger.info("A baked result has arrived, adding it to the image list.")
|
794
897
|
|
795
898
|
# Update the image list panel
|
899
|
+
# find the page index where this layer is
|
900
|
+
page_index = (
|
901
|
+
self.image_entries.index(baked_result_entry) // self.config.deque_maxlen
|
902
|
+
)
|
903
|
+
# set the current page to the page index
|
904
|
+
self.image_list_panel.current_page = page_index
|
796
905
|
self.image_list_panel.update_image_list(self.image_entries)
|
797
906
|
self.image_list_panel.imageSelected.emit(baked_result_entry)
|
798
907
|
|
@@ -831,6 +940,19 @@ class LayerifyTab(QWidget):
|
|
831
940
|
self.annotation_list.update_list()
|
832
941
|
logger.info("Selected annotation deleted.")
|
833
942
|
|
943
|
+
# if clicked q, set the mode to point
|
944
|
+
elif key == Qt.Key_Q:
|
945
|
+
self.layer.set_mode(MouseMode.POINT)
|
946
|
+
logger.info("Mouse mode set to POINT.")
|
947
|
+
# if clicked w, set the mode to polygon
|
948
|
+
elif key == Qt.Key_W:
|
949
|
+
self.layer.set_mode(MouseMode.POLYGON)
|
950
|
+
logger.info("Mouse mode set to POLYGON.")
|
951
|
+
# if clicked e, set the mode to rectangle
|
952
|
+
elif key == Qt.Key_E:
|
953
|
+
self.layer.set_mode(MouseMode.RECTANGLE)
|
954
|
+
logger.info("Mouse mode set to RECTANGLE.")
|
955
|
+
|
834
956
|
# Pass the event to the annotation list if it needs to handle it
|
835
957
|
if self.annotation_list.hasFocus():
|
836
958
|
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.
|
3
|
+
Version: 0.52
|
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
|
@@ -151,3 +154,5 @@ Click on the image above to play the video on YouTube.
|
|
151
154
|
Contributions are welcome!
|
152
155
|
|
153
156
|
Do you find this project to be useful and are you looking for some features that are not implemented yet? Feel free to open issues or submit pull requests to improve the project.
|
157
|
+
|
158
|
+
For more please visit [CONTRIBUTING](CONTRIBUTING).
|
@@ -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=_jR6SX14A4BB-kzH0vnx8phgJwq7pJj_fwdpbsq7rVo,37542
|
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=UEEPxC-_TK01nk-pqJ96JEnSN5h3gpK1YVxIA9lsFu4,37661
|
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.
|
39
|
-
imagebaker-0.
|
40
|
-
imagebaker-0.
|
41
|
-
imagebaker-0.
|
42
|
-
imagebaker-0.
|
43
|
-
imagebaker-0.
|
38
|
+
imagebaker-0.52.dist-info/LICENSE,sha256=1vkysFPOnT7y4LsoFTv9YsopIrQvBc2l6vUOfv4KKLc,1082
|
39
|
+
imagebaker-0.52.dist-info/METADATA,sha256=TvkbuBIRmuwTIOHHcMAR6fmgB3TQs5GJ5c738antwpg,6991
|
40
|
+
imagebaker-0.52.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
41
|
+
imagebaker-0.52.dist-info/entry_points.txt,sha256=IDjZHJCiiHpH5IUTByT2en0nMbnnnlrJZ5FPFehUvQM,61
|
42
|
+
imagebaker-0.52.dist-info/top_level.txt,sha256=Gg-eILTlqJXwVQr0saSwsx3-H4SPdZ2agBZaufe194s,11
|
43
|
+
imagebaker-0.52.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|