imagebaker 0.0.41__py3-none-any.whl → 0.0.48__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.
Files changed (42) hide show
  1. imagebaker/__init__.py +1 -1
  2. imagebaker/core/__init__.py +0 -0
  3. imagebaker/core/configs/__init__.py +1 -0
  4. imagebaker/core/configs/configs.py +156 -0
  5. imagebaker/core/defs/__init__.py +1 -0
  6. imagebaker/core/defs/defs.py +258 -0
  7. imagebaker/core/plugins/__init__.py +0 -0
  8. imagebaker/core/plugins/base_plugin.py +39 -0
  9. imagebaker/core/plugins/cosine_plugin.py +39 -0
  10. imagebaker/layers/__init__.py +3 -0
  11. imagebaker/layers/annotable_layer.py +847 -0
  12. imagebaker/layers/base_layer.py +724 -0
  13. imagebaker/layers/canvas_layer.py +1007 -0
  14. imagebaker/list_views/__init__.py +3 -0
  15. imagebaker/list_views/annotation_list.py +203 -0
  16. imagebaker/list_views/canvas_list.py +185 -0
  17. imagebaker/list_views/image_list.py +138 -0
  18. imagebaker/list_views/layer_list.py +390 -0
  19. imagebaker/list_views/layer_settings.py +219 -0
  20. imagebaker/models/__init__.py +0 -0
  21. imagebaker/models/base_model.py +150 -0
  22. imagebaker/tabs/__init__.py +2 -0
  23. imagebaker/tabs/baker_tab.py +496 -0
  24. imagebaker/tabs/layerify_tab.py +837 -0
  25. imagebaker/utils/__init__.py +0 -0
  26. imagebaker/utils/image.py +105 -0
  27. imagebaker/utils/state_utils.py +92 -0
  28. imagebaker/utils/transform_mask.py +107 -0
  29. imagebaker/window/__init__.py +1 -0
  30. imagebaker/window/app.py +136 -0
  31. imagebaker/window/main_window.py +181 -0
  32. imagebaker/workers/__init__.py +3 -0
  33. imagebaker/workers/baker_worker.py +247 -0
  34. imagebaker/workers/layerify_worker.py +91 -0
  35. imagebaker/workers/model_worker.py +54 -0
  36. {imagebaker-0.0.41.dist-info → imagebaker-0.0.48.dist-info}/METADATA +6 -6
  37. imagebaker-0.0.48.dist-info/RECORD +41 -0
  38. {imagebaker-0.0.41.dist-info → imagebaker-0.0.48.dist-info}/WHEEL +1 -1
  39. imagebaker-0.0.41.dist-info/RECORD +0 -7
  40. {imagebaker-0.0.41.dist-info/licenses → imagebaker-0.0.48.dist-info}/LICENSE +0 -0
  41. {imagebaker-0.0.41.dist-info → imagebaker-0.0.48.dist-info}/entry_points.txt +0 -0
  42. {imagebaker-0.0.41.dist-info → imagebaker-0.0.48.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,837 @@
1
+ from imagebaker.layers.annotable_layer import AnnotableLayer
2
+ from imagebaker.list_views import AnnotationList
3
+ from imagebaker.list_views.image_list import ImageListPanel
4
+ from imagebaker import logger
5
+ from imagebaker.workers import ModelPredictionWorker
6
+ from imagebaker.utils.image import qpixmap_to_numpy
7
+
8
+ from PySide6.QtCore import QPointF, Qt, QRectF, Signal
9
+ from PySide6.QtGui import (
10
+ QColor,
11
+ QPixmap,
12
+ QPolygonF,
13
+ QIcon,
14
+ )
15
+ from PySide6.QtWidgets import (
16
+ QApplication,
17
+ QWidget,
18
+ QVBoxLayout,
19
+ QHBoxLayout,
20
+ QInputDialog,
21
+ QPushButton,
22
+ QColorDialog,
23
+ QSizePolicy,
24
+ QFileDialog,
25
+ QComboBox,
26
+ QMessageBox,
27
+ QDockWidget,
28
+ )
29
+ from PySide6.QtCore import QThread
30
+ from PySide6.QtWidgets import QProgressDialog
31
+ from pathlib import Path
32
+ from imagebaker.core.configs import LayerConfig, CanvasConfig
33
+ from imagebaker.core.defs import (
34
+ Label,
35
+ MouseMode,
36
+ Annotation,
37
+ PredictionResult,
38
+ BakingResult,
39
+ )
40
+ from collections import deque
41
+ from typing import Deque
42
+ from dataclasses import dataclass
43
+
44
+
45
+ @dataclass
46
+ class ImageEntry:
47
+ is_baked_result: bool
48
+ data: AnnotableLayer | Path
49
+
50
+
51
+ class LayerifyTab(QWidget):
52
+ """Layerify Tab implementation"""
53
+
54
+ annotationRemoved = Signal(Annotation)
55
+ layerAdded = Signal(AnnotableLayer)
56
+ clearAnnotations = Signal()
57
+ messageSignal = Signal(str)
58
+ annotationAdded = Signal(Annotation)
59
+ annotationUpdated = Signal(Annotation)
60
+ gotToTab = Signal(int)
61
+
62
+ def __init__(
63
+ self,
64
+ main_window,
65
+ config: LayerConfig,
66
+ canvas_config: CanvasConfig,
67
+ loaded_models,
68
+ ):
69
+ """
70
+ A tab for layerifying annotations and managing multiple layers.
71
+
72
+ Args:
73
+ main_window: The main window instance.
74
+ config: LayerConfig instance with settings for the tab.
75
+ canvas_config: CanvasConfig instance with settings for the canvas.
76
+ loaded_models: Dictionary of loaded models.
77
+ """
78
+ super().__init__(parent=main_window)
79
+
80
+ self.setFocusPolicy(Qt.StrongFocus)
81
+ self.main_window = main_window
82
+ self.config = config
83
+ self.canvas_config = canvas_config
84
+ self.main_layout = QVBoxLayout(self)
85
+
86
+ self.all_models = loaded_models
87
+ self.current_model = list(self.all_models.values())[0]
88
+ self.current_label = self.config.default_label.name
89
+ self.image_entries = []
90
+ self.curr_image_idx = 0
91
+ self.processed_images = set()
92
+ self.annotable_layers: Deque[AnnotableLayer] = deque(
93
+ maxlen=self.config.deque_maxlen
94
+ )
95
+ self.baked_results: Deque[AnnotableLayer] = deque(
96
+ maxlen=self.config.deque_maxlen
97
+ )
98
+ self.layer = None
99
+ self.init_ui()
100
+ self._connect_signals()
101
+
102
+ def _connect_signals(self):
103
+ """Connect all necessary signals"""
104
+ # Connect all layers in the deque to annotation list
105
+ for layer in self.annotable_layers:
106
+ layer.annotationAdded.connect(self.annotation_list.update_list)
107
+ layer.annotationUpdated.connect(self.annotation_list.update_list)
108
+ layer.messageSignal.connect(self.messageSignal)
109
+ layer.layerSignal.connect(self.add_layer)
110
+
111
+ # Connect image list panel signals
112
+ self.image_list_panel.imageSelected.connect(self.on_image_selected)
113
+
114
+ def init_ui(self):
115
+ """Initialize the UI components"""
116
+ # Create annotation list and image list panel
117
+ self.annotation_list = AnnotationList(None, parent=self.main_window)
118
+ self.image_list_panel = ImageListPanel(
119
+ self.image_entries, self.processed_images
120
+ )
121
+
122
+ self.main_window.addDockWidget(Qt.LeftDockWidgetArea, self.image_list_panel)
123
+
124
+ # Add multiple layers (canvas) to the main layout
125
+ for _ in range(self.annotable_layers.maxlen):
126
+ layer = AnnotableLayer(
127
+ parent=self.main_window,
128
+ config=self.config,
129
+ canvas_config=self.canvas_config,
130
+ )
131
+ layer.setVisible(False) # Initially hide all layers
132
+ self.annotable_layers.append(layer)
133
+ self.main_layout.addWidget(layer)
134
+
135
+ # Set the annotation list to the first layer by default
136
+ if self.annotable_layers:
137
+ self.layer = self.annotable_layers[0]
138
+ self.layer.set_mode(MouseMode.RECTANGLE)
139
+ self.annotation_list.layer = self.layer
140
+
141
+ self.create_toolbar()
142
+
143
+ # Create a dock widget for the toolbar
144
+ self.toolbar_dock = QDockWidget("Tools", self)
145
+ self.toolbar_dock.setWidget(self.toolbar)
146
+ self.toolbar_dock.setFeatures(
147
+ QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetFloatable
148
+ )
149
+ self.main_window.addDockWidget(Qt.BottomDockWidgetArea, self.toolbar_dock)
150
+
151
+ # Add annotation list to main window's docks
152
+ self.main_window.addDockWidget(Qt.RightDockWidgetArea, self.annotation_list)
153
+ self.load_default_images()
154
+
155
+ def on_image_selected(self, image_entry: ImageEntry):
156
+ """Handle image selection from the image list panel."""
157
+ logger.info(f"Image selected: {image_entry}")
158
+
159
+ # Hide all layers first
160
+ for idx, layer in enumerate(self.annotable_layers):
161
+ layer.setVisible(False)
162
+ # logger.info(f"Layer {idx} hidden.")
163
+
164
+ if not image_entry.is_baked_result: # Regular image
165
+ image_path = image_entry.data
166
+ self.curr_image_idx = self.image_entries.index(image_entry)
167
+
168
+ # Make the corresponding layer visible and set the image
169
+ selected_layer = self.annotable_layers[self.curr_image_idx]
170
+ selected_layer.setVisible(True)
171
+ # logger.info(f"Layer {self.curr_image_idx} made visible for regular image.")
172
+ selected_layer.set_image(image_path) # Set the selected image
173
+ if self.layer:
174
+ selected_layer.set_mode(self.layer.mouse_mode)
175
+ self.layer = selected_layer # Update the currently selected layer
176
+
177
+ else: # Baked result
178
+ baked_result_layer = image_entry.data
179
+ self.curr_image_idx = self.image_entries.index(image_entry)
180
+
181
+ # Make the baked result layer visible
182
+ baked_result_layer.setVisible(True)
183
+ # logger.info(f"Layer {self.curr_image_idx} made visible for baked result.")
184
+ self.layer = baked_result_layer # Set the baked result as the current layer
185
+
186
+ self.annotation_list.layer = self.layer
187
+ self.annotation_list.update_list()
188
+
189
+ self.messageSignal.emit(
190
+ f"Showing image {self.curr_image_idx + 1}/{len(self.image_entries)}"
191
+ )
192
+ self.update()
193
+
194
+ def load_default_images(self):
195
+ """Load the first set of images as the default."""
196
+ # If no images are loaded, try to load from the assets folder
197
+ if not self.image_entries:
198
+ assets_folder = self.config.assets_folder
199
+ if assets_folder.exists() and assets_folder.is_dir():
200
+ for img_path in assets_folder.rglob("*.*"):
201
+ if img_path.suffix.lower() in [
202
+ ".jpg",
203
+ ".jpeg",
204
+ ".png",
205
+ ".bmp",
206
+ ".tiff",
207
+ ]:
208
+ # Add regular images as dictionaries with type and data
209
+ self.image_entries.append(
210
+ ImageEntry(is_baked_result=False, data=img_path)
211
+ )
212
+
213
+ # Load images into layers if any are found
214
+ if self.image_entries:
215
+ for i, layer in enumerate(self.annotable_layers):
216
+ if i < len(self.image_entries):
217
+ layer.set_image(self.image_entries[i].data)
218
+ layer.setVisible(
219
+ i == 0
220
+ ) # Only the first layer is visible by default
221
+ if i == 0:
222
+ self.layer = layer # Set the first layer as the current layer
223
+ else:
224
+ layer.setVisible(False)
225
+
226
+ self.messageSignal.emit(f"Showing image 1/{len(self.image_entries)}")
227
+ else:
228
+ # If no images are found, log a message
229
+ logger.warning("No images found in the assets folder.")
230
+ self.messageSignal.emit("No images found in the assets folder.")
231
+
232
+ # Update the image list panel
233
+ self.image_list_panel.update_image_list(self.image_entries)
234
+ self.update()
235
+
236
+ def clear_annotations(self):
237
+ """Safely clear all annotations"""
238
+ try:
239
+ # Clear layer annotations
240
+ self.clearAnnotations.emit()
241
+ self.messageSignal.emit("Annotations cleared")
242
+
243
+ except Exception as e:
244
+ logger.error(f"Clear error: {str(e)}")
245
+ self.messageSignal.emit(f"Error clearing: {str(e)}")
246
+
247
+ def on_annotation_added(self, annotation: Annotation):
248
+ """Handle annotation added event
249
+
250
+ Args:
251
+ annotation (Annotation): The annotation that was added.
252
+ """
253
+ if annotation.label not in self.config.predefined_labels:
254
+ self.config.predefined_labels.append(
255
+ Label(annotation.label, annotation.color)
256
+ )
257
+ self.update_label_combo()
258
+ logger.info(f"Added annotation: {annotation.label}")
259
+ self.messageSignal.emit(f"Added annotation: {annotation.label}")
260
+
261
+ # Refresh the annotation list
262
+ self.annotation_list.update_list()
263
+
264
+ def on_annotation_updated(self, annotation: Annotation):
265
+ """
266
+ A slot to handle the annotation updated signal.
267
+
268
+ Args:
269
+ annotation (Annotation): The updated annotation.
270
+ """
271
+ logger.info(f"Updated annotation: {annotation.label}")
272
+ self.messageSignal.emit(f"Updated annotation: {annotation.label}")
273
+
274
+ # Refresh the annotation list
275
+ self.annotation_list.update_list()
276
+
277
+ def update_label_combo(self):
278
+ """
279
+ Add predefined labels to the label combo box.
280
+
281
+ This method is called when a new label is added.
282
+ """
283
+ self.label_combo.clear()
284
+ for label in self.config.predefined_labels:
285
+ pixmap = QPixmap(16, 16)
286
+ pixmap.fill(label.color)
287
+ self.label_combo.addItem(QIcon(pixmap), label.name)
288
+
289
+ def load_default_image(self):
290
+ """
291
+ Load a default image from the assets folder.
292
+ """
293
+ default_path = self.config.assets_folder / "desk.png"
294
+ if not default_path.exists():
295
+ default_path, _ = QFileDialog.getOpenFileName()
296
+ default_path = Path(default_path)
297
+
298
+ if default_path.exists():
299
+ self.layer.set_image(default_path)
300
+
301
+ def handle_predict(self):
302
+ """
303
+ Handle the predict button click event.
304
+
305
+ """
306
+ if self.current_model is None:
307
+ logger.warning("No model selected to predict")
308
+ self.messageSignal.emit("No model selected/or loaded to predict")
309
+ return
310
+ # get image as an numpy array from canvas
311
+ image = qpixmap_to_numpy(self.layer.image)
312
+ if image is None:
313
+ return
314
+ # get annotations from canvas
315
+ annotations = [
316
+ ann
317
+ for ann in self.layer.annotations
318
+ if not ann.is_model_generated and ann.visible
319
+ ]
320
+
321
+ if len(annotations) == 0:
322
+ logger.warning("No annotations to predict passing image to model")
323
+ self.messageSignal.emit("No annotations to predict passing image to model")
324
+ # return
325
+
326
+ points = []
327
+ polygons = []
328
+ rectangles = []
329
+ label_hints = []
330
+ for ann in annotations:
331
+ if ann.points:
332
+ points.append([[p.x(), p.y()] for p in ann.points])
333
+ if ann.polygon:
334
+ polygons.append([[p.x(), p.y()] for p in ann.polygon])
335
+ if ann.rectangle:
336
+ rectangles.append(
337
+ [
338
+ ann.rectangle.x(),
339
+ ann.rectangle.y(),
340
+ ann.rectangle.x() + ann.rectangle.width(),
341
+ ann.rectangle.y() + ann.rectangle.height(),
342
+ ]
343
+ )
344
+ label_hints.append([0])
345
+ ann.visible = False
346
+
347
+ points = points if len(points) > 0 else None
348
+ polygons = polygons if len(polygons) > 0 else None
349
+ rectangles = [rectangles] if len(rectangles) > 0 else None
350
+ label_hints = label_hints if len(label_hints) > 0 else None
351
+
352
+ self.loading_dialog = QProgressDialog(
353
+ "Processing annotation...",
354
+ "Cancel", # Optional cancel button
355
+ 0,
356
+ 0,
357
+ self.parentWidget(), # Or your main window reference
358
+ )
359
+ self.loading_dialog.setWindowTitle("Please Wait")
360
+ self.loading_dialog.setWindowModality(Qt.WindowModal)
361
+ self.loading_dialog.setCancelButton(None) # Remove cancel button if not needed
362
+ self.loading_dialog.show()
363
+
364
+ # Force UI update
365
+ QApplication.processEvents()
366
+
367
+ # Setup worker thread
368
+ self.worker_thread = QThread()
369
+ self.worker = ModelPredictionWorker(
370
+ self.current_model, image, points, polygons, rectangles, label_hints
371
+ )
372
+ self.worker.moveToThread(self.worker_thread)
373
+
374
+ # Connect signals
375
+ self.worker_thread.started.connect(self.worker.process)
376
+ self.worker.finished.connect(self.handle_model_result)
377
+ self.worker.finished.connect(self.worker_thread.quit)
378
+ self.worker.error.connect(self.handle_model_error)
379
+
380
+ # Cleanup connections
381
+ self.worker.finished.connect(self.worker.deleteLater)
382
+ self.worker_thread.finished.connect(self.worker_thread.deleteLater)
383
+ self.worker_thread.finished.connect(self.loading_dialog.close)
384
+
385
+ # Start processing
386
+ self.worker_thread.start()
387
+
388
+ def handle_model_result(self, predictions: list[PredictionResult]):
389
+ """
390
+ A slot to handle the model prediction results.
391
+
392
+ Args:
393
+ predictions (list[PredictionResult]): The list of prediction results.
394
+ """
395
+ # update canvas with predictions
396
+ for prediction in predictions:
397
+ if prediction.class_name not in self.config.predefined_labels:
398
+ self.config.predefined_labels.append(Label(prediction.class_name))
399
+ self.update_label_combo()
400
+ if prediction.rectangle:
401
+ # make sure the returned rectangle is within the image
402
+
403
+ self.layer.annotations.append(
404
+ Annotation(
405
+ annotation_id=len(self.layer.annotations),
406
+ label=prediction.class_name,
407
+ color=self.config.get_label_color(prediction.class_name)
408
+ or QColor(255, 255, 255),
409
+ rectangle=QRectF(*prediction.rectangle),
410
+ is_complete=True,
411
+ score=prediction.score,
412
+ annotator=self.current_model.name,
413
+ annotation_time=str(
414
+ prediction.annotation_time
415
+ if prediction.annotation_time
416
+ else ""
417
+ ),
418
+ file_path=self.layer.file_path,
419
+ )
420
+ )
421
+ elif prediction.polygon is not None:
422
+
423
+ self.layer.annotations.append(
424
+ Annotation(
425
+ annotation_id=len(self.layer.annotations),
426
+ label=prediction.class_name,
427
+ color=self.config.get_label_color(prediction.class_name)
428
+ or QColor(255, 255, 255),
429
+ polygon=QPolygonF([QPointF(*p) for p in prediction.polygon]),
430
+ is_complete=True,
431
+ score=prediction.score,
432
+ annotator=self.current_model.name,
433
+ annotation_time=str(prediction.annotation_time),
434
+ file_path=self.layer.file_path,
435
+ )
436
+ )
437
+ else:
438
+ # points as center of canvas
439
+ x, y = self.layer.width() // 2, self.layer.height() // 2
440
+ self.layer.annotations.append(
441
+ Annotation(
442
+ annotation_id=len(self.layer.annotations),
443
+ label=prediction.class_name,
444
+ color=self.config.get_label_color(prediction.class_name)
445
+ or QColor(255, 255, 255),
446
+ points=[QPointF(x, y)],
447
+ is_complete=True,
448
+ score=prediction.score,
449
+ annotator=self.current_model.name,
450
+ annotation_time=str(prediction.annotation_time),
451
+ file_path=self.layer.file_path,
452
+ )
453
+ )
454
+
455
+ self.layer.update()
456
+ self.annotation_list.update_list()
457
+ self.update_annotation_list()
458
+
459
+ def handle_model_change(self, index):
460
+ """
461
+ Handle the model change event.
462
+
463
+ Args:
464
+ index (int): The index of the selected model.
465
+ """
466
+ model_name = self.model_combo.currentText()
467
+ self.current_model = self.all_models[model_name]
468
+ msg = f"Model changed to {model_name}"
469
+ logger.info(msg)
470
+ self.messageSignal.emit(msg)
471
+
472
+ def handle_model_error(self, error):
473
+ logger.error(f"Model error: {error}")
474
+ QMessageBox.critical(self, "Error", f"Model error: {error}")
475
+
476
+ def save_annotations(self):
477
+ """Save annotations to a JSON file."""
478
+ if not self.layer.annotations:
479
+ QMessageBox.warning(self, "Warning", "No annotations to save!")
480
+ return
481
+
482
+ options = QFileDialog.Options()
483
+ file_name, _ = QFileDialog.getSaveFileName(
484
+ self, "Save Annotations", "", "JSON Files (*.json)", options=options
485
+ )
486
+
487
+ if file_name:
488
+ try:
489
+ Annotation.save_as_json(self.layer.annotations, file_name)
490
+
491
+ QMessageBox.information(
492
+ self, "Success", "Annotations saved successfully!"
493
+ )
494
+
495
+ except Exception as e:
496
+ QMessageBox.critical(
497
+ self, "Error", f"Failed to save annotations: {str(e)}"
498
+ )
499
+
500
+ def load_annotations(self):
501
+ """
502
+ Load annotations from a JSON file.
503
+ """
504
+ options = QFileDialog.Options()
505
+ file_name, _ = QFileDialog.getOpenFileName(
506
+ self, "Load Annotations", "", "JSON Files (*.json)", options=options
507
+ )
508
+
509
+ if file_name:
510
+ try:
511
+ self.layer.annotations = Annotation.load_from_json(file_name)
512
+ self.layer.update()
513
+ self.update_annotation_list()
514
+ QMessageBox.information(
515
+ self, "Success", "Annotations loaded successfully!"
516
+ )
517
+
518
+ except Exception as e:
519
+ QMessageBox.critical(
520
+ self, "Error", f"Failed to load annotations: {str(e)}"
521
+ )
522
+ self.layer.annotations = []
523
+ self.layer.update()
524
+
525
+ def update_annotation_list(self):
526
+ """Update the annotation list with the current annotations."""
527
+ self.annotation_list.update_list()
528
+
529
+ def choose_color(self):
530
+ """Choose a color for the current label."""
531
+ current_label = self.label_combo.currentText()
532
+ label_info = next(
533
+ (
534
+ label
535
+ for label in self.config.predefined_labels
536
+ if label.name == current_label
537
+ ),
538
+ None,
539
+ )
540
+
541
+ if label_info:
542
+ color = QColorDialog.getColor(label_info.color)
543
+ if color.isValid():
544
+ # Update label color
545
+ label_info.color = color
546
+ # Update combo box display
547
+ index = self.label_combo.currentIndex()
548
+ pixmap = QPixmap(16, 16)
549
+ pixmap.fill(color)
550
+ self.label_combo.setItemIcon(index, QIcon(pixmap))
551
+ # Update canvas color
552
+ self.layer.current_color = color
553
+ self.layer.update()
554
+ for annotation in self.layer.annotations:
555
+ if annotation.label == current_label:
556
+ annotation.color = color
557
+ self.layer.update()
558
+
559
+ def add_new_label(self):
560
+ """Add a new label to the predefined labels."""
561
+ name, ok = QInputDialog.getText(self, "New Label", "Enter label name:")
562
+ if not ok or not name:
563
+ return
564
+
565
+ # Check for existing label
566
+ existing_names = [label.name for label in self.config.predefined_labels]
567
+ if name in existing_names:
568
+ QMessageBox.warning(self, "Duplicate", "Label name already exists!")
569
+ return
570
+
571
+ color = QColorDialog.getColor()
572
+ if not color.isValid():
573
+ return
574
+
575
+ # Add new predefined label
576
+ self.config.predefined_labels.append(Label(name=name, color=color))
577
+
578
+ # Update combo box
579
+ self.update_label_combo()
580
+
581
+ # Select the new label
582
+ index = self.label_combo.findText(name)
583
+ self.label_combo.setCurrentIndex(index)
584
+
585
+ def handle_label_change(self, index):
586
+ """Handle the label change event."""
587
+ label_info = self.config.predefined_labels[index]
588
+ self.current_label = label_info.name
589
+ # sort the labels by putting selected label on top
590
+ self.config.predefined_labels.remove(label_info)
591
+ self.config.predefined_labels.insert(0, label_info)
592
+ # self.update_label_combo()
593
+
594
+ self.layer.current_color = label_info.color
595
+ self.layer.current_label = (
596
+ self.current_label if self.current_label != "Custom" else None
597
+ )
598
+ msg = f"Label changed to {self.current_label}"
599
+ self.messageSignal.emit(msg)
600
+ self.layer.update()
601
+ self.update()
602
+
603
+ def add_layer(self, layer):
604
+ """Add a new layer to the tab."""
605
+ # this layer i.e. canvas will have only one annotation
606
+ logger.info(f"AnnotableLayer added: {layer.annotations[0].label}")
607
+ self.layerAdded.emit(layer)
608
+
609
+ self.layer.update()
610
+
611
+ def layerify_all(self):
612
+ """Layerify all annotations in the current layer."""
613
+ if len(self.layer.annotations) == 0:
614
+ logger.warning("No annotations to layerify")
615
+ self.messageSignal.emit("No annotations to layerify")
616
+
617
+ return
618
+ logger.info("Layerifying all annotations")
619
+
620
+ # else appends already added too
621
+ self.layer.layerify_annotation(self.layer.annotations)
622
+
623
+ def create_toolbar(self):
624
+ """Create Layerify-specific toolbar"""
625
+ self.toolbar = QWidget()
626
+ toolbar_layout = QHBoxLayout(self.toolbar)
627
+
628
+ modes = [
629
+ ("📍", "Point", lambda x: self.layer.set_mode(MouseMode.POINT)),
630
+ ("🔷", "Polygon", lambda x: self.layer.set_mode(MouseMode.POLYGON)),
631
+ ("🔳", "Rectangle", lambda x: self.layer.set_mode(MouseMode.RECTANGLE)),
632
+ ("⏳", "Idle", lambda x: self.layer.set_mode(MouseMode.IDLE)),
633
+ ("💾", "Annotations", self.save_annotations),
634
+ ("📂", "Annotations", self.load_annotations),
635
+ ("🔮", "Predict", self.handle_predict),
636
+ ("🎨", "Color", self.choose_color),
637
+ ("🧅", "Layerify All", self.layerify_all),
638
+ ("🏷️", "Add Label", self.add_new_label),
639
+ ("🗑️", "Clear", lambda x: self.clearAnnotations.emit()),
640
+ ]
641
+
642
+ # Folder navigation buttons
643
+ self.select_folder_btn = QPushButton("Select Folder")
644
+ self.select_folder_btn.clicked.connect(self.select_folder)
645
+ toolbar_layout.addWidget(self.select_folder_btn)
646
+
647
+ self.next_image_btn = QPushButton("Next")
648
+ self.next_image_btn.clicked.connect(self.show_next_image)
649
+ toolbar_layout.addWidget(self.next_image_btn)
650
+
651
+ self.prev_image_btn = QPushButton("Prev")
652
+ self.prev_image_btn.clicked.connect(self.show_prev_image)
653
+ toolbar_layout.addWidget(self.prev_image_btn)
654
+
655
+ # Initially hide next/prev buttons
656
+ self.next_image_btn.setVisible(False)
657
+ self.prev_image_btn.setVisible(False)
658
+
659
+ # Add mode buttons
660
+ for icon, text, mode in modes:
661
+ btn_txt = icon + text
662
+ btn = QPushButton(btn_txt)
663
+ btn.setToolTip(btn_txt)
664
+ btn.setMaximumWidth(80)
665
+ if isinstance(mode, MouseMode):
666
+ btn.clicked.connect(lambda _, m=mode: self.layer.set_mode(m))
667
+ else:
668
+ btn.clicked.connect(mode)
669
+ toolbar_layout.addWidget(btn)
670
+
671
+ # Add spacer
672
+ spacer = QWidget()
673
+ spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
674
+ toolbar_layout.addWidget(spacer)
675
+
676
+ # Label and Model dropdowns
677
+ self.label_combo = QComboBox()
678
+ self.label_combo.setStyleSheet("QComboBox { min-width: 120px; }")
679
+ for label in self.config.predefined_labels:
680
+ pixmap = QPixmap(16, 16)
681
+ pixmap.fill(label.color)
682
+ self.label_combo.addItem(QIcon(pixmap), label.name)
683
+ self.label_combo.currentIndexChanged.connect(self.handle_label_change)
684
+ toolbar_layout.addWidget(self.label_combo)
685
+
686
+ self.model_combo = QComboBox()
687
+ self.model_combo.setStyleSheet("QComboBox { min-width: 120px; }")
688
+ for model_name in self.all_models.keys():
689
+ self.model_combo.addItem(model_name)
690
+ self.model_combo.currentIndexChanged.connect(self.handle_model_change)
691
+ toolbar_layout.addWidget(self.model_combo)
692
+
693
+ def select_folder(self):
694
+ """Allow the user to select a folder and load images from it."""
695
+ folder_path = QFileDialog.getExistingDirectory(self, "Select Folder")
696
+ if folder_path:
697
+ self.image_entries = [] # Clear the existing image paths
698
+ folder_path = Path(folder_path)
699
+
700
+ # Use rglob to get all image files in the folder and subfolders
701
+ for img_path in folder_path.rglob("*.*"):
702
+ if img_path.suffix.lower() in [
703
+ ".jpg",
704
+ ".jpeg",
705
+ ".png",
706
+ ".bmp",
707
+ ".tiff",
708
+ ]:
709
+ self.image_entries.append(
710
+ ImageEntry(is_baked_result=False, data=img_path)
711
+ )
712
+
713
+ self.curr_image_idx = 0 # Reset the current image index
714
+
715
+ if len(self.image_entries) > 0:
716
+ msg = f"Loaded {len(self.image_entries)} images from {folder_path}"
717
+ logger.info(msg)
718
+ self.messageSignal.emit(msg)
719
+
720
+ # Update the image list panel with the new image paths
721
+ self.image_list_panel.image_entries = self.image_entries
722
+ self.image_list_panel.update_image_list(self.image_entries)
723
+
724
+ # Load the first set of images into the layers
725
+ self.load_default_images()
726
+
727
+ # Unhide the next/prev buttons if there are multiple images
728
+ self.next_image_btn.setVisible(len(self.image_entries) > 1)
729
+ self.prev_image_btn.setVisible(len(self.image_entries) > 1)
730
+ else:
731
+ QMessageBox.warning(
732
+ self,
733
+ "No Images Found",
734
+ "No valid image files found in the selected folder.",
735
+ )
736
+
737
+ def show_next_image(self):
738
+ """Show next image in the list. If at the end, show first image."""
739
+ if self.curr_image_idx < len(self.image_entries) - 1:
740
+ self.curr_image_idx += 1
741
+ else:
742
+ self.curr_image_idx = 0
743
+ self.layer.set_image(self.image_entries[self.curr_image_idx]["data"])
744
+ self.messageSignal.emit(
745
+ f"Showing image {self.curr_image_idx + 1}/{len(self.image_entries)}"
746
+ )
747
+
748
+ def show_prev_image(self):
749
+ """Show previous image in the list. If at the start, show last image."""
750
+ if self.curr_image_idx > 0:
751
+ self.curr_image_idx -= 1
752
+ else:
753
+ self.curr_image_idx = len(self.image_entries) - 1
754
+ self.layer.set_image(self.image_entries[self.curr_image_idx]["data"])
755
+ self.messageSignal.emit(
756
+ f"Showing image {self.curr_image_idx + 1}/{len(self.image_entries)}"
757
+ )
758
+
759
+ def __del__(self):
760
+ logger.warning(f"Tab {id(self)} deleted")
761
+
762
+ def add_baked_result(self, baking_result: BakingResult):
763
+ """Add a baked result to the baked results list and update the image list."""
764
+ # Create a new layer for the baked result
765
+ self.layer.setVisible(False) # Hide the current layer
766
+ layer = AnnotableLayer(
767
+ parent=self.main_window,
768
+ config=self.config,
769
+ canvas_config=self.canvas_config,
770
+ )
771
+ layer.annotations = baking_result.annotations
772
+
773
+ layer.annotationAdded.connect(self.annotation_list.update_list)
774
+ layer.annotationUpdated.connect(self.annotation_list.update_list)
775
+ layer.messageSignal.connect(self.messageSignal)
776
+ layer.layerSignal.connect(self.add_layer)
777
+
778
+ layer.set_image(baking_result.image) # Set the baked result's image
779
+ layer.setVisible(True) # Hide the layer initially
780
+ self.main_layout.addWidget(layer) # Add the layer to the layout
781
+
782
+ # Add the baked result layer to annotable_layers for proper visibility management
783
+ self.annotable_layers.append(layer)
784
+
785
+ # Add baked result to image_entries
786
+ baked_result_entry = ImageEntry(is_baked_result=True, data=layer)
787
+ self.image_entries.append(baked_result_entry)
788
+ # baking_result.image.save(str(baking_result.filename))
789
+ layer.update()
790
+
791
+ logger.info("A baked result has arrived, adding it to the image list.")
792
+
793
+ # Update the image list panel
794
+ self.image_list_panel.update_image_list(self.image_entries)
795
+ self.image_list_panel.imageSelected.emit(baked_result_entry)
796
+
797
+ self.messageSignal.emit("Baked result added")
798
+ self.gotToTab.emit(0)
799
+
800
+ def keyPressEvent(self, event):
801
+ """Handle key press events for setting labels and deleting annotations."""
802
+ key = event.key()
803
+
804
+ # Debugging: Log the key press
805
+ logger.info(f"Key pressed in LayerifyTab: {key}")
806
+
807
+ # Handle keys 0-9 for setting labels
808
+ if Qt.Key_0 <= key <= Qt.Key_9:
809
+ label_index = key - Qt.Key_0 # Convert key to index (0-9)
810
+ if label_index < len(self.config.predefined_labels):
811
+ # Set the current label to the corresponding predefined label
812
+ self.current_label = self.config.predefined_labels[label_index].name
813
+ self.label_combo.setCurrentIndex(label_index)
814
+ self.layer.current_label = self.current_label
815
+ self.layer.update()
816
+ logger.info(f"Label set to: {self.current_label}")
817
+ else:
818
+ # Show dialog to add a new label if the index is out of range
819
+ self.add_new_label()
820
+
821
+ # Handle Delete key for removing the selected annotation
822
+ elif key == Qt.Key_Delete:
823
+ self.layer.selected_annotation = self.layer._get_selected_annotation()
824
+ if self.layer and self.layer.selected_annotation:
825
+
826
+ self.layer.annotations.remove(self.layer.selected_annotation)
827
+ self.layer.selected_annotation = None # Clear the selection
828
+ self.layer.update()
829
+ self.annotation_list.update_list()
830
+ logger.info("Selected annotation deleted.")
831
+
832
+ # Pass the event to the annotation list if it needs to handle it
833
+ if self.annotation_list.hasFocus():
834
+ self.annotation_list.keyPressEvent(event)
835
+
836
+ # Pass unhandled events to the base class
837
+ super().keyPressEvent(event)