imagebaker 0.0.50__py3-none-any.whl → 0.0.53__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 +10 -0
- imagebaker/layers/annotable_layer.py +93 -16
- imagebaker/layers/base_layer.py +9 -1
- imagebaker/layers/canvas_layer.py +29 -6
- imagebaker/list_views/annotation_list.py +4 -2
- imagebaker/list_views/image_list.py +26 -8
- imagebaker/list_views/layer_settings.py +29 -0
- imagebaker/tabs/baker_tab.py +49 -1
- imagebaker/tabs/layerify_tab.py +275 -49
- imagebaker/utils/image.py +9 -6
- imagebaker/utils/state_utils.py +1 -0
- imagebaker/window/main_window.py +77 -6
- imagebaker/workers/baker_worker.py +22 -3
- {imagebaker-0.0.50.dist-info → imagebaker-0.0.53.dist-info}/METADATA +24 -11
- {imagebaker-0.0.50.dist-info → imagebaker-0.0.53.dist-info}/RECORD +20 -20
- {imagebaker-0.0.50.dist-info → imagebaker-0.0.53.dist-info}/LICENSE +0 -0
- {imagebaker-0.0.50.dist-info → imagebaker-0.0.53.dist-info}/WHEEL +0 -0
- {imagebaker-0.0.50.dist-info → imagebaker-0.0.53.dist-info}/entry_points.txt +0 -0
- {imagebaker-0.0.50.dist-info → imagebaker-0.0.53.dist-info}/top_level.txt +0 -0
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.")
|
172
|
+
current_label = self.layer.current_label
|
173
|
+
current_color = self.layer.current_color
|
165
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,11 +220,17 @@ 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
|
212
228
|
if i == 0:
|
213
229
|
self.layer = layer # Set the first layer as the current layer
|
230
|
+
# Select the first item in the image list panel's list widget
|
231
|
+
if self.image_list_panel.list_widget.count() > 0:
|
232
|
+
self.image_list_panel.list_widget.setCurrentRow(0)
|
233
|
+
|
214
234
|
else:
|
215
235
|
layer.setVisible(False)
|
216
236
|
|
@@ -224,12 +244,80 @@ class LayerifyTab(QWidget):
|
|
224
244
|
self.image_list_panel.update_image_list(self.image_entries)
|
225
245
|
self.update()
|
226
246
|
|
247
|
+
def save_layer_annotations(
|
248
|
+
self, layer: AnnotableLayer, save_dir: Path | None = None
|
249
|
+
):
|
250
|
+
"""Save annotations for a specific layer"""
|
251
|
+
if len(layer.annotations) > 0:
|
252
|
+
file_path = layer.file_path
|
253
|
+
file_name = file_path.name
|
254
|
+
if save_dir is None:
|
255
|
+
# Save to the cache directory
|
256
|
+
save_dir = self.config.cache_dir
|
257
|
+
save_dir = save_dir / f"{file_name}.json"
|
258
|
+
Annotation.save_as_json(layer.annotations, save_dir)
|
259
|
+
logger.info(f"Saved annotations for {layer.layer_name} to {save_dir}")
|
260
|
+
|
261
|
+
def load_layer_annotations(
|
262
|
+
self, layer: AnnotableLayer, load_dir: Path | None = None
|
263
|
+
):
|
264
|
+
"""Load annotations for a specific layer"""
|
265
|
+
if layer.file_path:
|
266
|
+
file_path = layer.file_path
|
267
|
+
file_name = file_path.name
|
268
|
+
if load_dir is None:
|
269
|
+
load_dir = self.config.cache_dir
|
270
|
+
load_dir = load_dir / f"{file_name}.json"
|
271
|
+
if load_dir.exists():
|
272
|
+
layer.annotations = Annotation.load_from_json(load_dir)
|
273
|
+
logger.info(
|
274
|
+
f"Loaded annotations for {layer.layer_name} from {load_dir}"
|
275
|
+
)
|
276
|
+
else:
|
277
|
+
logger.warning(f"No annotations found for {layer.layer_name}")
|
278
|
+
|
279
|
+
def update_active_entries(self, image_entries: list[ImageEntry]):
|
280
|
+
"""Update the active entries in the image list panel."""
|
281
|
+
self.curr_image_idx = 0
|
282
|
+
for i, layer in enumerate(self.annotable_layers):
|
283
|
+
self.save_layer_annotations(layer)
|
284
|
+
layer.annotations = []
|
285
|
+
|
286
|
+
if i < len(image_entries):
|
287
|
+
# get index on the self.image_entries
|
288
|
+
idx = self.image_entries.index(image_entries[i])
|
289
|
+
if self.image_entries[idx].is_baked_result:
|
290
|
+
# if the image is a baked result, set the layer to the baked result
|
291
|
+
layer = self.image_entries[idx].data
|
292
|
+
layer.file_path = layer.file_path
|
293
|
+
else:
|
294
|
+
layer.set_image(self.image_entries[idx].data)
|
295
|
+
self.load_layer_annotations(layer)
|
296
|
+
|
297
|
+
layer.layer_name = f"Layer_{idx + 1}"
|
298
|
+
layer.setVisible(i == 0)
|
299
|
+
if i == 0:
|
300
|
+
self.layer = layer
|
301
|
+
# update annotation list to the first layer
|
302
|
+
self.annotation_list.layer = self.layer
|
303
|
+
self.annotation_list.update_list()
|
304
|
+
else:
|
305
|
+
layer.setVisible(False)
|
306
|
+
logger.info("Updated active entries in image list panel.")
|
307
|
+
|
227
308
|
def clear_annotations(self):
|
228
309
|
"""Safely clear all annotations"""
|
229
310
|
try:
|
230
311
|
# Clear layer annotations
|
231
312
|
self.clearAnnotations.emit()
|
232
313
|
self.messageSignal.emit("Annotations cleared")
|
314
|
+
# clear cache annotation of layer
|
315
|
+
annotation_path = (
|
316
|
+
self.config.cache_dir / f"{self.layer.file_path.name}.json"
|
317
|
+
)
|
318
|
+
if annotation_path.exists():
|
319
|
+
os.remove(annotation_path)
|
320
|
+
logger.info(f"Cleared annotations from {annotation_path}")
|
233
321
|
|
234
322
|
except Exception as e:
|
235
323
|
logger.error(f"Clear error: {str(e)}")
|
@@ -241,14 +329,17 @@ class LayerifyTab(QWidget):
|
|
241
329
|
Args:
|
242
330
|
annotation (Annotation): The annotation that was added.
|
243
331
|
"""
|
244
|
-
|
332
|
+
|
333
|
+
# if annotation.label is not in the predefined labels, add it
|
334
|
+
if annotation.label not in [lbl.name for lbl in self.config.predefined_labels]:
|
335
|
+
logger.info(f"Label {annotation.label} created.")
|
245
336
|
self.config.predefined_labels.append(
|
246
337
|
Label(annotation.label, annotation.color)
|
247
338
|
)
|
248
339
|
self.update_label_combo()
|
249
340
|
logger.info(f"Added annotation: {annotation.label}")
|
250
341
|
self.messageSignal.emit(f"Added annotation: {annotation.label}")
|
251
|
-
|
342
|
+
self.save_layer_annotations(self.layer)
|
252
343
|
# Refresh the annotation list
|
253
344
|
self.annotation_list.update_list()
|
254
345
|
|
@@ -259,11 +350,12 @@ class LayerifyTab(QWidget):
|
|
259
350
|
Args:
|
260
351
|
annotation (Annotation): The updated annotation.
|
261
352
|
"""
|
262
|
-
logger.info(f"Updated annotation: {annotation
|
353
|
+
# logger.info(f"Updated annotation: {annotation}")
|
263
354
|
self.messageSignal.emit(f"Updated annotation: {annotation.label}")
|
264
355
|
|
265
356
|
# Refresh the annotation list
|
266
357
|
self.annotation_list.update_list()
|
358
|
+
self.save_layer_annotations(self.layer)
|
267
359
|
|
268
360
|
def update_label_combo(self):
|
269
361
|
"""
|
@@ -276,6 +368,27 @@ class LayerifyTab(QWidget):
|
|
276
368
|
pixmap = QPixmap(16, 16)
|
277
369
|
pixmap.fill(label.color)
|
278
370
|
self.label_combo.addItem(QIcon(pixmap), label.name)
|
371
|
+
logger.info("Updated label combo box with predefined labels.")
|
372
|
+
self.label_combo.setCurrentText(self.current_label)
|
373
|
+
|
374
|
+
def on_label_update(self, old_new_label: tuple[str, str]):
|
375
|
+
new_labels = []
|
376
|
+
index = 0
|
377
|
+
for i, label in enumerate(self.config.predefined_labels):
|
378
|
+
if label.name == old_new_label[0]:
|
379
|
+
label.name = old_new_label[1]
|
380
|
+
index = i
|
381
|
+
new_labels.append(label)
|
382
|
+
|
383
|
+
self.config.predefined_labels = new_labels
|
384
|
+
logger.info(f"Updated label from {old_new_label[0]} to {old_new_label[1]}")
|
385
|
+
self.messageSignal.emit(
|
386
|
+
f"Updated label from {old_new_label[0]} to {old_new_label[1]}."
|
387
|
+
)
|
388
|
+
|
389
|
+
self.update_label_combo()
|
390
|
+
self.handle_label_change(index=index)
|
391
|
+
self.label_combo.update()
|
279
392
|
|
280
393
|
def load_default_image(self):
|
281
394
|
"""
|
@@ -471,6 +584,34 @@ class LayerifyTab(QWidget):
|
|
471
584
|
QMessageBox.warning(self, "Warning", "No annotations to save!")
|
472
585
|
return
|
473
586
|
|
587
|
+
# an option to save all annotations (i.e. from all layers) or just the current layer
|
588
|
+
options = QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
589
|
+
# Change the "No" button text to "Just This Layer"
|
590
|
+
msg_box = QMessageBox(self)
|
591
|
+
msg_box.setWindowTitle("Save Annotations")
|
592
|
+
msg_box.setText("Do you want to save annotations from all layers?")
|
593
|
+
yes_button = msg_box.addButton("All Layers", QMessageBox.YesRole)
|
594
|
+
just_this_button = msg_box.addButton("Just This Layer", QMessageBox.NoRole)
|
595
|
+
msg_box.setDefaultButton(yes_button)
|
596
|
+
msg_box.exec()
|
597
|
+
save_all = msg_box.clickedButton() == yes_button
|
598
|
+
|
599
|
+
if save_all:
|
600
|
+
# folder select dialog
|
601
|
+
save_dir = QFileDialog.getExistingDirectory(
|
602
|
+
self, "Select Save Directory", str(self.config.cache_dir)
|
603
|
+
)
|
604
|
+
if not save_dir:
|
605
|
+
QMessageBox.warning(self, "Warning", "No directory selected!")
|
606
|
+
return
|
607
|
+
for layer in self.annotable_layers:
|
608
|
+
if layer.annotations:
|
609
|
+
self.save_layer_annotations(layer, Path(save_dir))
|
610
|
+
QMessageBox.information(
|
611
|
+
self, "Success", "All annotations saved successfully!"
|
612
|
+
)
|
613
|
+
return
|
614
|
+
|
474
615
|
options = QFileDialog.Options()
|
475
616
|
file_name, _ = QFileDialog.getSaveFileName(
|
476
617
|
self, "Save Annotations", "", "JSON Files (*.json)", options=options
|
@@ -493,6 +634,34 @@ class LayerifyTab(QWidget):
|
|
493
634
|
"""
|
494
635
|
Load annotations from a JSON file.
|
495
636
|
"""
|
637
|
+
# dialog box to load all annotations or just the current layer
|
638
|
+
options = QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
639
|
+
# Change the "No" button text to "Just This Layer"
|
640
|
+
msg_box = QMessageBox(self)
|
641
|
+
msg_box.setWindowTitle("Load Annotations")
|
642
|
+
msg_box.setText("Do you want to load annotations from all layers?")
|
643
|
+
yes_button = msg_box.addButton("All Layers", QMessageBox.YesRole)
|
644
|
+
just_this_button = msg_box.addButton("Just This Layer", QMessageBox.NoRole)
|
645
|
+
msg_box.setDefaultButton(yes_button)
|
646
|
+
msg_box.exec()
|
647
|
+
load_all = msg_box.clickedButton() == yes_button
|
648
|
+
if load_all:
|
649
|
+
# folder select dialog
|
650
|
+
load_dir = QFileDialog.getExistingDirectory(
|
651
|
+
self, "Select Load Directory", str(self.config.cache_dir)
|
652
|
+
)
|
653
|
+
if not load_dir:
|
654
|
+
QMessageBox.warning(self, "Warning", "No directory selected!")
|
655
|
+
return
|
656
|
+
for layer in self.annotable_layers:
|
657
|
+
layer.annotations = []
|
658
|
+
layer.update()
|
659
|
+
self.load_layer_annotations(layer, Path(load_dir))
|
660
|
+
self.update_annotation_list()
|
661
|
+
QMessageBox.information(
|
662
|
+
self, "Success", "All annotations loaded successfully!"
|
663
|
+
)
|
664
|
+
return
|
496
665
|
options = QFileDialog.Options()
|
497
666
|
file_name, _ = QFileDialog.getOpenFileName(
|
498
667
|
self, "Load Annotations", "", "JSON Files (*.json)", options=options
|
@@ -589,6 +758,24 @@ class LayerifyTab(QWidget):
|
|
589
758
|
)
|
590
759
|
msg = f"Label changed to {self.current_label}"
|
591
760
|
self.messageSignal.emit(msg)
|
761
|
+
self.layer.selected_annotation = self.layer._get_selected_annotation()
|
762
|
+
if self.layer.selected_annotation:
|
763
|
+
annotations = []
|
764
|
+
for ann in self.layer.annotations:
|
765
|
+
if ann == self.layer.selected_annotation:
|
766
|
+
ann.label = label_info.name
|
767
|
+
ann.color = label_info.color
|
768
|
+
annotations.append(ann)
|
769
|
+
|
770
|
+
self.layer.annotations = annotations
|
771
|
+
self.on_annotation_updated(self.layer.selected_annotation)
|
772
|
+
# disable label change callback
|
773
|
+
self.label_combo.currentIndexChanged.disconnect()
|
774
|
+
self.label_combo.currentIndexChanged.connect(lambda: None)
|
775
|
+
self.update_label_combo()
|
776
|
+
# set it back
|
777
|
+
self.label_combo.currentIndexChanged.connect(self.handle_label_change)
|
778
|
+
|
592
779
|
self.layer.update()
|
593
780
|
self.update()
|
594
781
|
|
@@ -628,7 +815,7 @@ class LayerifyTab(QWidget):
|
|
628
815
|
("🎨", "Color", self.choose_color),
|
629
816
|
("🧅", "Layerify All", self.layerify_all),
|
630
817
|
("🏷️", "Add Label", self.add_new_label),
|
631
|
-
("🗑️", "Clear",
|
818
|
+
("🗑️", "Clear", self.clear_annotations),
|
632
819
|
]
|
633
820
|
|
634
821
|
# Folder navigation buttons
|
@@ -636,18 +823,6 @@ class LayerifyTab(QWidget):
|
|
636
823
|
self.select_folder_btn.clicked.connect(self.select_folder)
|
637
824
|
toolbar_layout.addWidget(self.select_folder_btn)
|
638
825
|
|
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
826
|
# Add mode buttons
|
652
827
|
for icon, text, mode in modes:
|
653
828
|
btn_txt = icon + text
|
@@ -703,11 +878,25 @@ class LayerifyTab(QWidget):
|
|
703
878
|
ImageEntry(is_baked_result=False, data=img_path)
|
704
879
|
)
|
705
880
|
|
881
|
+
# load from bake folder if it exists
|
882
|
+
bake_folder = self.config.bake_dir
|
883
|
+
if bake_folder.exists() and bake_folder.is_dir():
|
884
|
+
for img_path in bake_folder.glob("*.*"):
|
885
|
+
if img_path.suffix.lower() in [
|
886
|
+
".jpg",
|
887
|
+
".jpeg",
|
888
|
+
".png",
|
889
|
+
".bmp",
|
890
|
+
".tiff",
|
891
|
+
]:
|
892
|
+
self.image_entries.append(
|
893
|
+
ImageEntry(is_baked_result=False, data=img_path)
|
894
|
+
)
|
895
|
+
|
706
896
|
def select_folder(self):
|
707
897
|
"""Allow the user to select a folder and load images from it."""
|
708
898
|
folder_path = QFileDialog.getExistingDirectory(self, "Select Folder")
|
709
899
|
if folder_path:
|
710
|
-
self.image_entries = [] # Clear the existing image paths
|
711
900
|
folder_path = Path(folder_path)
|
712
901
|
|
713
902
|
self._load_images_from_folder(folder_path)
|
@@ -726,9 +915,6 @@ class LayerifyTab(QWidget):
|
|
726
915
|
# Load the first set of images into the layers
|
727
916
|
self.load_default_images()
|
728
917
|
|
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
918
|
else:
|
733
919
|
QMessageBox.warning(
|
734
920
|
self,
|
@@ -736,28 +922,6 @@ class LayerifyTab(QWidget):
|
|
736
922
|
"No valid image files found in the selected folder.",
|
737
923
|
)
|
738
924
|
|
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
925
|
def __del__(self):
|
762
926
|
logger.warning(f"Tab {id(self)} deleted")
|
763
927
|
|
@@ -770,10 +934,20 @@ class LayerifyTab(QWidget):
|
|
770
934
|
config=self.config,
|
771
935
|
canvas_config=self.canvas_config,
|
772
936
|
)
|
773
|
-
|
937
|
+
# save it in cache
|
938
|
+
filename = baking_result.filename
|
939
|
+
filepath = self.config.bake_dir / filename.name
|
940
|
+
baking_result.image.save(str(filepath))
|
941
|
+
#
|
942
|
+
Annotation.save_as_json(
|
943
|
+
baking_result.annotations, self.config.cache_dir / f"{filename.name}.json"
|
944
|
+
)
|
774
945
|
|
775
|
-
layer.
|
776
|
-
|
946
|
+
layer.set_image(filepath)
|
947
|
+
|
948
|
+
layer.annotationAdded.connect(self.on_annotation_added)
|
949
|
+
layer.annotationUpdated.connect(self.on_annotation_updated)
|
950
|
+
layer.labelUpdated.connect(self.on_label_update)
|
777
951
|
layer.messageSignal.connect(self.messageSignal)
|
778
952
|
layer.layerSignal.connect(self.add_layer)
|
779
953
|
|
@@ -785,7 +959,7 @@ class LayerifyTab(QWidget):
|
|
785
959
|
self.annotable_layers.append(layer)
|
786
960
|
|
787
961
|
# Add baked result to image_entries
|
788
|
-
baked_result_entry = ImageEntry(is_baked_result=
|
962
|
+
baked_result_entry = ImageEntry(is_baked_result=False, data=filepath)
|
789
963
|
self.image_entries.append(baked_result_entry)
|
790
964
|
# baking_result.image.save(str(baking_result.filename))
|
791
965
|
layer.update()
|
@@ -793,6 +967,12 @@ class LayerifyTab(QWidget):
|
|
793
967
|
logger.info("A baked result has arrived, adding it to the image list.")
|
794
968
|
|
795
969
|
# Update the image list panel
|
970
|
+
# find the page index where this layer is
|
971
|
+
page_index = (
|
972
|
+
self.image_entries.index(baked_result_entry) // self.config.deque_maxlen
|
973
|
+
)
|
974
|
+
# set the current page to the page index
|
975
|
+
self.image_list_panel.current_page = page_index
|
796
976
|
self.image_list_panel.update_image_list(self.image_entries)
|
797
977
|
self.image_list_panel.imageSelected.emit(baked_result_entry)
|
798
978
|
|
@@ -831,6 +1011,52 @@ class LayerifyTab(QWidget):
|
|
831
1011
|
self.annotation_list.update_list()
|
832
1012
|
logger.info("Selected annotation deleted.")
|
833
1013
|
|
1014
|
+
# if clicked q, set the mode to point
|
1015
|
+
elif key == Qt.Key_Q:
|
1016
|
+
self.layer.set_mode(MouseMode.POINT)
|
1017
|
+
logger.info("Mouse mode set to POINT.")
|
1018
|
+
# if clicked w, set the mode to polygon
|
1019
|
+
elif key == Qt.Key_W:
|
1020
|
+
self.layer.set_mode(MouseMode.POLYGON)
|
1021
|
+
logger.info("Mouse mode set to POLYGON.")
|
1022
|
+
# if clicked e, set the mode to rectangle
|
1023
|
+
elif key == Qt.Key_E:
|
1024
|
+
self.layer.set_mode(MouseMode.RECTANGLE)
|
1025
|
+
logger.info("Mouse mode set to RECTANGLE.")
|
1026
|
+
# if pressed h key, then toggle visibility of annotations
|
1027
|
+
elif event.key() == Qt.Key_H:
|
1028
|
+
self.layer.toggle_annotation_visibility()
|
1029
|
+
|
1030
|
+
# if selected l, then run layerify the selected annotations
|
1031
|
+
elif event.key() == Qt.Key_L:
|
1032
|
+
if self.layer and self.layer.annotations:
|
1033
|
+
selected_annotations = [
|
1034
|
+
ann for ann in self.layer.annotations if ann.selected
|
1035
|
+
]
|
1036
|
+
if selected_annotations:
|
1037
|
+
logger.info(
|
1038
|
+
f"Layerifying {len(selected_annotations)} selected annotations."
|
1039
|
+
)
|
1040
|
+
self.layer.layerify_annotation(selected_annotations)
|
1041
|
+
else:
|
1042
|
+
# layerify all annotations in the current layer
|
1043
|
+
self.layer.layerify_annotation(self.layer.annotations)
|
1044
|
+
logger.info("Layerified all annotations in the current layer.")
|
1045
|
+
else:
|
1046
|
+
logger.warning("No annotations to layerify.")
|
1047
|
+
elif event.key() == Qt.Key_C:
|
1048
|
+
# if an annotation is selected, open a dialog box
|
1049
|
+
# in that dialog box, ask for a caption
|
1050
|
+
self.layer.selected_annotation = self.layer._get_selected_annotation()
|
1051
|
+
if self.layer.selected_annotation:
|
1052
|
+
current_caption = getattr(self.layer.selected_annotation, "caption", "")
|
1053
|
+
text, ok = QInputDialog.getMultiLineText(
|
1054
|
+
self, "Edit Caption", "Enter caption:", current_caption
|
1055
|
+
)
|
1056
|
+
if ok:
|
1057
|
+
self.layer.selected_annotation.caption = text
|
1058
|
+
self.layer.update()
|
1059
|
+
|
834
1060
|
# Pass the event to the annotation list if it needs to handle it
|
835
1061
|
if self.annotation_list.hasFocus():
|
836
1062
|
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/utils/state_utils.py
CHANGED
@@ -66,6 +66,7 @@ def calculate_intermediate_states(
|
|
66
66
|
+ (current_state.edge_opacity - previous_state.edge_opacity) * (i / steps),
|
67
67
|
edge_width=previous_state.edge_width
|
68
68
|
+ (current_state.edge_width - previous_state.edge_width) * (i / steps),
|
69
|
+
caption=previous_state.caption, # Assuming caption is the same in both states
|
69
70
|
)
|
70
71
|
|
71
72
|
# Deep copy the drawing_states from the previous_state
|
imagebaker/window/main_window.py
CHANGED
@@ -175,9 +175,80 @@ 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
|
+
)
|
190
|
+
|
191
|
+
def keyPressEvent(self, event):
|
192
|
+
# if pressed escape key, close the application
|
193
|
+
if event.key() == Qt.Key_Escape:
|
194
|
+
logger.info("Escape key pressed, closing the application.")
|
195
|
+
self.close()
|
196
|
+
|
197
|
+
# if ctrl+h, show help dialog
|
198
|
+
elif event.key() == Qt.Key_H and event.modifiers() == Qt.ControlModifier:
|
199
|
+
logger.info("Ctrl+H pressed, showing help dialog.")
|
200
|
+
current_tab = self.tab_widget.currentIndex()
|
201
|
+
if current_tab == 0:
|
202
|
+
info = (
|
203
|
+
"<b>Layerify Tab Shortcuts:</b><br>"
|
204
|
+
"<ul>"
|
205
|
+
"<li><b>Ctrl + C</b>: Copy selected annotation.</li>"
|
206
|
+
"<li><b>Ctrl + V</b>: Paste copied annotation in its parent image if it is currently open.</li>"
|
207
|
+
"<li><b>Delete</b>: Delete selected annotation.</li>"
|
208
|
+
"<li><b>Left Click</b>: Select an annotation on mouse position.</li>"
|
209
|
+
"<li><b>Left Click + Drag</b>: Drag a selected annotation.</li>"
|
210
|
+
"<li><b>Double Left Click</b>: When using polygon annotation, completes the polygon.</li>"
|
211
|
+
"<li><b>Right Click</b>: Unselect an annotation. While annotating the polygon, undo the last point.</li>"
|
212
|
+
"<li><b>Ctrl + Mouse Wheel</b>: Zoom In/Out on the mouse position, i.e., resize the viewport.</li>"
|
213
|
+
"<li><b>Ctrl + Drag</b>: If done on the background, the viewport is panned.</li>"
|
214
|
+
"<li><b>Q</b>: Point mode on annotation.</li>"
|
215
|
+
"<li><b>W</b>: Polygon mode on annotation.</li>"
|
216
|
+
"<li><b>E</b>: Rectangle mode on annotation.</li>"
|
217
|
+
"<li><b>H</b>: Hides/un-hides selected annotation.</li>"
|
218
|
+
"<li><b>L</b>: Creates layer from an annotation. If any annotation selected, creates only its, else creates layers from all visible annotations.</li>"
|
219
|
+
"<li><b>C</b>: If any annotation is selected, a input box for Caption is created.</li>"
|
220
|
+
"<li><b>Numerics</b>: Selecting number 1, 2, till 9 selects label. If not available, asks for a new label.</li>"
|
221
|
+
"<li><b>Escape</b>: Closes the application.</li>"
|
222
|
+
"</ul>"
|
223
|
+
)
|
224
|
+
elif current_tab == 1:
|
225
|
+
info = (
|
226
|
+
"<b>Baker Tab Shortcuts:</b><br>"
|
227
|
+
"<ul>"
|
228
|
+
"<li><b>Ctrl + C</b>: Copy selected layer.</li>"
|
229
|
+
"<li><b>Ctrl + V</b>: Paste copied layer in its parent if it is currently open.</li>"
|
230
|
+
"<li><b>Delete</b>: Delete selected layer.</li>"
|
231
|
+
"<li><b>Left Click</b>: Select a layer on mouse position.</li>"
|
232
|
+
"<li><b>Left Click + Drag</b>: Drag a selected layer.</li>"
|
233
|
+
"<li><b>Ctrl + S</b>: Save State on Baker Tab.</li>"
|
234
|
+
"<li><b>Ctrl + D</b>: Draw Mode on Baker Tab. Drawing can happen on a selected or main layer.</li>"
|
235
|
+
"<li><b>Ctrl + E</b>: Erase Mode on Baker Tab.</li>"
|
236
|
+
"<li><b>Wheel</b>: Change the size of the drawing pointer.</li>"
|
237
|
+
"<li><b>W</b>: Moves selected layer one step up in layer list in baker.</li>"
|
238
|
+
"<li><b>S</b>: Moves selected layer one step down in layer list in baker.</li>"
|
239
|
+
"<li><b>H</b>: Hides/un-hides selected layer.</li>"
|
240
|
+
"<li><b>C</b>: Edit Caption for selected layer.</li>"
|
241
|
+
"<li><b>Escape</b>: Closes the application.</li>"
|
242
|
+
"</ul>"
|
243
|
+
)
|
244
|
+
else:
|
245
|
+
info = "No shortcuts available for this tab."
|
246
|
+
|
247
|
+
QMessageBox.information(
|
248
|
+
self,
|
249
|
+
"Help",
|
250
|
+
info,
|
251
|
+
)
|
252
|
+
|
253
|
+
# pass event to other widgets
|
254
|
+
return super().keyPressEvent(event)
|