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.
- imagebaker/__init__.py +1 -1
- imagebaker/core/__init__.py +0 -0
- imagebaker/core/configs/__init__.py +1 -0
- imagebaker/core/configs/configs.py +156 -0
- imagebaker/core/defs/__init__.py +1 -0
- imagebaker/core/defs/defs.py +258 -0
- imagebaker/core/plugins/__init__.py +0 -0
- imagebaker/core/plugins/base_plugin.py +39 -0
- imagebaker/core/plugins/cosine_plugin.py +39 -0
- imagebaker/layers/__init__.py +3 -0
- imagebaker/layers/annotable_layer.py +847 -0
- imagebaker/layers/base_layer.py +724 -0
- imagebaker/layers/canvas_layer.py +1007 -0
- imagebaker/list_views/__init__.py +3 -0
- imagebaker/list_views/annotation_list.py +203 -0
- imagebaker/list_views/canvas_list.py +185 -0
- imagebaker/list_views/image_list.py +138 -0
- imagebaker/list_views/layer_list.py +390 -0
- imagebaker/list_views/layer_settings.py +219 -0
- imagebaker/models/__init__.py +0 -0
- imagebaker/models/base_model.py +150 -0
- imagebaker/tabs/__init__.py +2 -0
- imagebaker/tabs/baker_tab.py +496 -0
- imagebaker/tabs/layerify_tab.py +837 -0
- imagebaker/utils/__init__.py +0 -0
- imagebaker/utils/image.py +105 -0
- imagebaker/utils/state_utils.py +92 -0
- imagebaker/utils/transform_mask.py +107 -0
- imagebaker/window/__init__.py +1 -0
- imagebaker/window/app.py +136 -0
- imagebaker/window/main_window.py +181 -0
- imagebaker/workers/__init__.py +3 -0
- imagebaker/workers/baker_worker.py +247 -0
- imagebaker/workers/layerify_worker.py +91 -0
- imagebaker/workers/model_worker.py +54 -0
- {imagebaker-0.0.41.dist-info → imagebaker-0.0.48.dist-info}/METADATA +6 -6
- imagebaker-0.0.48.dist-info/RECORD +41 -0
- {imagebaker-0.0.41.dist-info → imagebaker-0.0.48.dist-info}/WHEEL +1 -1
- imagebaker-0.0.41.dist-info/RECORD +0 -7
- {imagebaker-0.0.41.dist-info/licenses → imagebaker-0.0.48.dist-info}/LICENSE +0 -0
- {imagebaker-0.0.41.dist-info → imagebaker-0.0.48.dist-info}/entry_points.txt +0 -0
- {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)
|