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.
@@ -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:
@@ -210,6 +210,10 @@ class Annotation:
210
210
  def load_from_json(path: str):
211
211
  import json
212
212
 
213
+ # if path does not exist, return empty list
214
+ if not Path(path).exists():
215
+ return []
216
+
213
217
  with open(path, "r") as f:
214
218
  data = json.load(f)
215
219
 
@@ -42,6 +42,7 @@ class AnnotableLayer(BaseLayer):
42
42
  annotationCleared = Signal()
43
43
  annotationMoved = Signal()
44
44
  layersChanged = Signal()
45
+ labelUpdated = Signal(tuple)
45
46
 
46
47
  def __init__(self, parent, config: LayerConfig, canvas_config: CanvasConfig):
47
48
  super().__init__(parent, config)
@@ -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
- self.update()
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
- self.current_annotation = None
831
- self.update()
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.COLOR_RGBA2BGR)
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
 
@@ -52,6 +52,7 @@ class AnnotationList(QDockWidget):
52
52
  if self.layer is None:
53
53
  return
54
54
  for idx, ann in enumerate(self.layer.annotations):
55
+
55
56
  item = QListWidgetItem(self.list_widget)
56
57
 
57
58
  # Create container widget
@@ -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 = 10
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 idx, image_entry in enumerate(image_entries):
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 + 1}"
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
- self.pagination_label.setText(
130
- f"Showing {self.current_page * self.images_per_page + 1} to "
131
- f"{min((self.current_page + 1) * self.images_per_page, len(image_entries))} "
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):
@@ -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.annotationUpdated.connect(self.annotation_list.update_list)
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, self.processed_images
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
- if annotation.label not in self.config.predefined_labels:
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.label}")
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", lambda x: self.clearAnnotations.emit()),
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
- layer.annotations = baking_result.annotations
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.annotationAdded.connect(self.annotation_list.update_list)
776
- layer.annotationUpdated.connect(self.annotation_list.update_list)
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=True, data=layer)
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
- (0, 255, 0),
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
- (0, 255, 0),
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
- (0, 255, 0),
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
- (0, 255, 0),
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, (0, 255, 0), -1)
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
- (0, 255, 0),
105
+ color,
103
106
  2,
104
107
  )
105
108
  return image
@@ -175,9 +175,15 @@ class MainWindow(QMainWindow):
175
175
  self.status_bar.showMessage(status_text)
176
176
 
177
177
  def closeEvent(self, event):
178
- # Clean up tabs first
179
- if hasattr(self, "layerify_tab"):
180
- self.layerify_tab.deleteLater()
181
- if hasattr(self, "baker_tab"):
182
- self.baker_tab.deleteLater()
183
- super().closeEvent(event)
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
- new_annotation.rectangle = QRectF(
252
- xywhs[0][0], xywhs[0][1], xywhs[0][2], xywhs[0][3]
253
- )
253
+ if len(xywhs) == 0:
254
+ logger.info("No rectangles found")
255
+ # return None
256
+ else:
257
+ logger.info(f"Found {len(xywhs)} rectangles")
258
+ new_annotation.rectangle = QRectF(
259
+ xywhs[0][0], xywhs[0][1], xywhs[0][2], xywhs[0][3]
260
+ )
254
261
  elif ann.polygon:
255
262
  polygon = mask_to_polygons(alpha_channel, merge_polygons=True)
256
263
  poly = QPolygonF([QPointF(p[0], p[1]) for p in polygon[0]])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: imagebaker
3
- Version: 0.0.50
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=-qy7vmYaaUk3bh49pwsBig8de_3Y2JWjTDeyWcGQods,5130
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=-ZItfJdWaK9yFSuFn2LmQ4ncqukAZ_hvYFgE44HUdIo,8394
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=ejBp6nooLtHs8c_G6lOsTJmSwBT404F_dmPsSkjvdEQ,37167
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=7eDo0UHXRRZ5BX3BciPX88JsumzmwVemtS6cBjU9qwM,42115
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=HGV6lGlkFjvJvvGnCcLuX1kkfXA0GL8wKo8jOXSBXec,7527
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=NInkc893FGU7L6oSxy8KrWql-i6RB0BqvkroPAASjVw,4912
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=Qsf9w81-43sB0LZy_vIB4wCzUhHHhoIiarZJvKRxeZ0,32340
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=fq7g3DqSdjF9okxZ3fe5kF4Hxn32rqhvVqxy8yI5bnI,3067
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=gpJ7DDuPmxhHh_6Rv3YH2J_1AqG7-NM8R3tKNYhFT3E,7030
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=EJTL4ln09NuntFpu0o-Hfk0vCtDxpKqJxJcmtgTnMwo,11297
35
+ imagebaker/workers/baker_worker.py,sha256=_jWeyYAGoO2mfxXDn7fBm9tIA69OITewDVN0hSAt3Jc,11532
36
36
  imagebaker/workers/layerify_worker.py,sha256=EOqKvhdACtf3y5Ljy6M7MvddAjlZW5DNfBFMtNPD-us,3223
37
37
  imagebaker/workers/model_worker.py,sha256=Tlg6_D977iK-kuGCNdQY4OnGiP8QqWY7adpRNXZw4rA,1636
38
- imagebaker-0.0.50.dist-info/LICENSE,sha256=1vkysFPOnT7y4LsoFTv9YsopIrQvBc2l6vUOfv4KKLc,1082
39
- imagebaker-0.0.50.dist-info/METADATA,sha256=Zsl37tIGiV9wrQ2zfiaubhmPbhOMucos2V_fF5mtbXU,6829
40
- imagebaker-0.0.50.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
41
- imagebaker-0.0.50.dist-info/entry_points.txt,sha256=IDjZHJCiiHpH5IUTByT2en0nMbnnnlrJZ5FPFehUvQM,61
42
- imagebaker-0.0.50.dist-info/top_level.txt,sha256=Gg-eILTlqJXwVQr0saSwsx3-H4SPdZ2agBZaufe194s,11
43
- imagebaker-0.0.50.dist-info/RECORD,,
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,,