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.
@@ -20,7 +20,14 @@ class LayerSettings(QDockWidget):
20
20
  layerState = Signal(LayerState)
21
21
  messageSignal = Signal(str)
22
22
 
23
- def __init__(self, parent=None, max_xpos=1000, max_ypos=1000, max_scale=100):
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")
@@ -39,7 +39,7 @@ class BaseModel(ABC):
39
39
  return image
40
40
 
41
41
  # @abstractmethod
42
- def postprocess(self, output) -> PredictionResult:
42
+ def postprocess(self, output) -> list[PredictionResult]:
43
43
  return output
44
44
 
45
45
  def predict(
@@ -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()
@@ -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)
@@ -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
- if annotation.label not in self.config.predefined_labels:
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.label}")
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", lambda x: self.clearAnnotations.emit()),
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
- layer.annotations = baking_result.annotations
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.annotationAdded.connect(self.annotation_list.update_list)
775
- layer.annotationUpdated.connect(self.annotation_list.update_list)
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=True, data=layer)
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)
@@ -0,0 +1,3 @@
1
+ from .utils import * # noqa: F401, F403
2
+ from .transform_mask import * # noqa: F401, F403
3
+ from .vis import * # noqa: F401, F403
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
@@ -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=current_state.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