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
@@ -20,7 +20,14 @@ class LayerSettings(QDockWidget):
|
|
20
20
|
layerState = Signal(LayerState)
|
21
21
|
messageSignal = Signal(str)
|
22
22
|
|
23
|
-
def __init__(
|
23
|
+
def __init__(
|
24
|
+
self,
|
25
|
+
parent=None,
|
26
|
+
max_xpos=1000,
|
27
|
+
max_ypos=1000,
|
28
|
+
max_scale=100,
|
29
|
+
max_edge_width=10,
|
30
|
+
):
|
24
31
|
super().__init__("BaseLayer Settings", parent)
|
25
32
|
self.selected_layer: BaseLayer = None
|
26
33
|
|
@@ -29,6 +36,7 @@ class LayerSettings(QDockWidget):
|
|
29
36
|
self.max_xpos = max_xpos
|
30
37
|
self.max_ypos = max_ypos
|
31
38
|
self.max_scale = max_scale
|
39
|
+
self.max_edge_width = max_edge_width
|
32
40
|
self.init_ui()
|
33
41
|
self.setFeatures(
|
34
42
|
QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetFloatable
|
@@ -67,6 +75,12 @@ class LayerSettings(QDockWidget):
|
|
67
75
|
self.main_layout.addWidget(self.scale_y_slider["widget"])
|
68
76
|
self.rotation_slider = self.create_slider("Rotation:", 0, 360, 0, 1)
|
69
77
|
self.main_layout.addWidget(self.rotation_slider["widget"])
|
78
|
+
self.edge_opacity_slider = self.create_slider("Edge Opacity:", 0, 255, 255, 1)
|
79
|
+
self.main_layout.addWidget(self.edge_opacity_slider["widget"])
|
80
|
+
self.edge_width_slider = self.create_slider(
|
81
|
+
"Edge Width:", 0, self.max_edge_width, 5, 1
|
82
|
+
)
|
83
|
+
self.main_layout.addWidget(self.edge_width_slider["widget"])
|
70
84
|
|
71
85
|
# Add stretch to push content to the top
|
72
86
|
self.main_layout.addStretch()
|
@@ -125,7 +139,6 @@ class LayerSettings(QDockWidget):
|
|
125
139
|
|
126
140
|
try:
|
127
141
|
self._disable_updates = True
|
128
|
-
|
129
142
|
if sender == self.opacity_slider["slider"]:
|
130
143
|
self.selected_layer.opacity = value
|
131
144
|
elif sender == self.x_slider["slider"]:
|
@@ -138,6 +151,12 @@ class LayerSettings(QDockWidget):
|
|
138
151
|
self.selected_layer.scale_y = value / 100.0
|
139
152
|
elif sender == self.rotation_slider["slider"]:
|
140
153
|
self.selected_layer.rotation = value
|
154
|
+
elif sender == self.edge_opacity_slider["slider"]:
|
155
|
+
self.selected_layer.edge_opacity = value
|
156
|
+
self.selected_layer._apply_edge_opacity()
|
157
|
+
elif sender == self.edge_width_slider["slider"]:
|
158
|
+
self.selected_layer.edge_width = value
|
159
|
+
self.selected_layer._apply_edge_opacity()
|
141
160
|
|
142
161
|
self.selected_layer.update() # Trigger a repaint
|
143
162
|
|
@@ -154,6 +173,10 @@ class LayerSettings(QDockWidget):
|
|
154
173
|
rotation=self.selected_layer.rotation,
|
155
174
|
scale_x=self.selected_layer.scale_x,
|
156
175
|
scale_y=self.selected_layer.scale_y,
|
176
|
+
opacity=self.selected_layer.opacity,
|
177
|
+
edge_opacity=self.selected_layer.edge_opacity,
|
178
|
+
edge_width=self.selected_layer.edge_width,
|
179
|
+
visible=self.selected_layer.visible,
|
157
180
|
)
|
158
181
|
logger.info(f"Storing state {bake_settings}")
|
159
182
|
self.messageSignal.emit(f"Stored state for {bake_settings.layer_name}")
|
@@ -211,6 +234,12 @@ class LayerSettings(QDockWidget):
|
|
211
234
|
self.rotation_slider["slider"].setValue(
|
212
235
|
int(self.selected_layer.rotation)
|
213
236
|
)
|
237
|
+
self.edge_opacity_slider["slider"].setValue(
|
238
|
+
int(self.selected_layer.edge_opacity)
|
239
|
+
)
|
240
|
+
self.edge_width_slider["slider"].setValue(
|
241
|
+
int(self.selected_layer.edge_width)
|
242
|
+
)
|
214
243
|
else:
|
215
244
|
self.widget.setEnabled(False)
|
216
245
|
self.layer_name_label.setText("No BaseLayer")
|
imagebaker/models/base_model.py
CHANGED
imagebaker/tabs/baker_tab.py
CHANGED
@@ -66,6 +66,7 @@ class BakerTab(QWidget):
|
|
66
66
|
max_xpos=self.config.max_xpos,
|
67
67
|
max_ypos=self.config.max_ypos,
|
68
68
|
max_scale=self.config.max_scale,
|
69
|
+
max_edge_width=self.config.max_edge_width,
|
69
70
|
)
|
70
71
|
self.layer_list = LayerList(
|
71
72
|
canvas=self.current_canvas,
|
@@ -415,15 +416,7 @@ class BakerTab(QWidget):
|
|
415
416
|
"""Seek to a specific state using the timeline slider."""
|
416
417
|
self.messageSignal.emit(f"Seeking to step {step}")
|
417
418
|
logger.info(f"Seeking to step {step}")
|
418
|
-
|
419
|
-
# Get the states for the selected step
|
420
|
-
if step in self.current_canvas.states:
|
421
|
-
states = self.current_canvas.states[step]
|
422
|
-
for state in states:
|
423
|
-
layer = self.current_canvas.get_layer(state.layer_id)
|
424
|
-
if layer:
|
425
|
-
layer.layer_state = state
|
426
|
-
layer.update()
|
419
|
+
self.current_canvas.seek_state(step)
|
427
420
|
|
428
421
|
# Update the canvas
|
429
422
|
self.current_canvas.update()
|
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
|
"""
|
@@ -463,6 +556,7 @@ class LayerifyTab(QWidget):
|
|
463
556
|
def handle_model_error(self, error):
|
464
557
|
logger.error(f"Model error: {error}")
|
465
558
|
QMessageBox.critical(self, "Error", f"Model error: {error}")
|
559
|
+
self.loading_dialog.close()
|
466
560
|
|
467
561
|
def save_annotations(self):
|
468
562
|
"""Save annotations to a JSON file."""
|
@@ -588,6 +682,24 @@ class LayerifyTab(QWidget):
|
|
588
682
|
)
|
589
683
|
msg = f"Label changed to {self.current_label}"
|
590
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
|
+
|
591
703
|
self.layer.update()
|
592
704
|
self.update()
|
593
705
|
|
@@ -627,7 +739,7 @@ class LayerifyTab(QWidget):
|
|
627
739
|
("🎨", "Color", self.choose_color),
|
628
740
|
("🧅", "Layerify All", self.layerify_all),
|
629
741
|
("🏷️", "Add Label", self.add_new_label),
|
630
|
-
("🗑️", "Clear",
|
742
|
+
("🗑️", "Clear", self.clear_annotations),
|
631
743
|
]
|
632
744
|
|
633
745
|
# Folder navigation buttons
|
@@ -635,18 +747,6 @@ class LayerifyTab(QWidget):
|
|
635
747
|
self.select_folder_btn.clicked.connect(self.select_folder)
|
636
748
|
toolbar_layout.addWidget(self.select_folder_btn)
|
637
749
|
|
638
|
-
self.next_image_btn = QPushButton("Next")
|
639
|
-
self.next_image_btn.clicked.connect(self.show_next_image)
|
640
|
-
toolbar_layout.addWidget(self.next_image_btn)
|
641
|
-
|
642
|
-
self.prev_image_btn = QPushButton("Prev")
|
643
|
-
self.prev_image_btn.clicked.connect(self.show_prev_image)
|
644
|
-
toolbar_layout.addWidget(self.prev_image_btn)
|
645
|
-
|
646
|
-
# Initially hide next/prev buttons
|
647
|
-
self.next_image_btn.setVisible(False)
|
648
|
-
self.prev_image_btn.setVisible(False)
|
649
|
-
|
650
750
|
# Add mode buttons
|
651
751
|
for icon, text, mode in modes:
|
652
752
|
btn_txt = icon + text
|
@@ -702,11 +802,25 @@ class LayerifyTab(QWidget):
|
|
702
802
|
ImageEntry(is_baked_result=False, data=img_path)
|
703
803
|
)
|
704
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
|
+
|
705
820
|
def select_folder(self):
|
706
821
|
"""Allow the user to select a folder and load images from it."""
|
707
822
|
folder_path = QFileDialog.getExistingDirectory(self, "Select Folder")
|
708
823
|
if folder_path:
|
709
|
-
self.image_entries = [] # Clear the existing image paths
|
710
824
|
folder_path = Path(folder_path)
|
711
825
|
|
712
826
|
self._load_images_from_folder(folder_path)
|
@@ -725,9 +839,6 @@ class LayerifyTab(QWidget):
|
|
725
839
|
# Load the first set of images into the layers
|
726
840
|
self.load_default_images()
|
727
841
|
|
728
|
-
# Unhide the next/prev buttons if there are multiple images
|
729
|
-
self.next_image_btn.setVisible(len(self.image_entries) > 1)
|
730
|
-
self.prev_image_btn.setVisible(len(self.image_entries) > 1)
|
731
842
|
else:
|
732
843
|
QMessageBox.warning(
|
733
844
|
self,
|
@@ -735,28 +846,6 @@ class LayerifyTab(QWidget):
|
|
735
846
|
"No valid image files found in the selected folder.",
|
736
847
|
)
|
737
848
|
|
738
|
-
def show_next_image(self):
|
739
|
-
"""Show next image in the list. If at the end, show first image."""
|
740
|
-
if self.curr_image_idx < len(self.image_entries) - 1:
|
741
|
-
self.curr_image_idx += 1
|
742
|
-
else:
|
743
|
-
self.curr_image_idx = 0
|
744
|
-
self.layer.set_image(self.image_entries[self.curr_image_idx]["data"])
|
745
|
-
self.messageSignal.emit(
|
746
|
-
f"Showing image {self.curr_image_idx + 1}/{len(self.image_entries)}"
|
747
|
-
)
|
748
|
-
|
749
|
-
def show_prev_image(self):
|
750
|
-
"""Show previous image in the list. If at the start, show last image."""
|
751
|
-
if self.curr_image_idx > 0:
|
752
|
-
self.curr_image_idx -= 1
|
753
|
-
else:
|
754
|
-
self.curr_image_idx = len(self.image_entries) - 1
|
755
|
-
self.layer.set_image(self.image_entries[self.curr_image_idx]["data"])
|
756
|
-
self.messageSignal.emit(
|
757
|
-
f"Showing image {self.curr_image_idx + 1}/{len(self.image_entries)}"
|
758
|
-
)
|
759
|
-
|
760
849
|
def __del__(self):
|
761
850
|
logger.warning(f"Tab {id(self)} deleted")
|
762
851
|
|
@@ -769,10 +858,20 @@ class LayerifyTab(QWidget):
|
|
769
858
|
config=self.config,
|
770
859
|
canvas_config=self.canvas_config,
|
771
860
|
)
|
772
|
-
|
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
|
+
)
|
773
869
|
|
774
|
-
layer.
|
775
|
-
|
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)
|
776
875
|
layer.messageSignal.connect(self.messageSignal)
|
777
876
|
layer.layerSignal.connect(self.add_layer)
|
778
877
|
|
@@ -784,7 +883,7 @@ class LayerifyTab(QWidget):
|
|
784
883
|
self.annotable_layers.append(layer)
|
785
884
|
|
786
885
|
# Add baked result to image_entries
|
787
|
-
baked_result_entry = ImageEntry(is_baked_result=
|
886
|
+
baked_result_entry = ImageEntry(is_baked_result=False, data=filepath)
|
788
887
|
self.image_entries.append(baked_result_entry)
|
789
888
|
# baking_result.image.save(str(baking_result.filename))
|
790
889
|
layer.update()
|
@@ -792,6 +891,12 @@ class LayerifyTab(QWidget):
|
|
792
891
|
logger.info("A baked result has arrived, adding it to the image list.")
|
793
892
|
|
794
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
|
795
900
|
self.image_list_panel.update_image_list(self.image_entries)
|
796
901
|
self.image_list_panel.imageSelected.emit(baked_result_entry)
|
797
902
|
|
@@ -830,6 +935,19 @@ class LayerifyTab(QWidget):
|
|
830
935
|
self.annotation_list.update_list()
|
831
936
|
logger.info("Selected annotation deleted.")
|
832
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
|
+
|
833
951
|
# Pass the event to the annotation list if it needs to handle it
|
834
952
|
if self.annotation_list.hasFocus():
|
835
953
|
self.annotation_list.keyPressEvent(event)
|
imagebaker/utils/__init__.py
CHANGED
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/utils/state_utils.py
CHANGED
@@ -59,9 +59,13 @@ def calculate_intermediate_states(
|
|
59
59
|
visible=current_state.visible,
|
60
60
|
allow_annotation_export=current_state.allow_annotation_export,
|
61
61
|
playing=current_state.playing,
|
62
|
-
selected=
|
62
|
+
selected=False,
|
63
63
|
is_annotable=current_state.is_annotable,
|
64
64
|
status=current_state.status,
|
65
|
+
edge_opacity=previous_state.edge_opacity
|
66
|
+
+ (current_state.edge_opacity - previous_state.edge_opacity) * (i / steps),
|
67
|
+
edge_width=previous_state.edge_width
|
68
|
+
+ (current_state.edge_width - previous_state.edge_width) * (i / steps),
|
65
69
|
)
|
66
70
|
|
67
71
|
# Deep copy the drawing_states from the previous_state
|
@@ -0,0 +1,26 @@
|
|
1
|
+
import numpy as np
|
2
|
+
import cv2
|
3
|
+
|
4
|
+
|
5
|
+
def generate_color_map(num_colors: int = 20):
|
6
|
+
"""Generate a color map for the segmentation masks"""
|
7
|
+
np.random.seed(42) # For reproducible colors
|
8
|
+
|
9
|
+
colors = {}
|
10
|
+
for i in range(num_colors):
|
11
|
+
# Generate distinct colors with good visibility
|
12
|
+
# Using HSV color space for better distribution
|
13
|
+
hue = i / num_colors
|
14
|
+
saturation = 0.8 + np.random.random() * 0.2
|
15
|
+
value = 0.8 + np.random.random() * 0.2
|
16
|
+
|
17
|
+
# Convert HSV to BGR (OpenCV uses BGR)
|
18
|
+
hsv_color = np.array(
|
19
|
+
[[[hue * 180, saturation * 255, value * 255]]], dtype=np.uint8
|
20
|
+
)
|
21
|
+
bgr_color = cv2.cvtColor(hsv_color, cv2.COLOR_HSV2BGR)[0][0]
|
22
|
+
|
23
|
+
# Store as (B, G, R) tuple
|
24
|
+
colors[i] = (int(bgr_color[0]), int(bgr_color[1]), int(bgr_color[2]))
|
25
|
+
|
26
|
+
return colors
|