coralnet-toolbox 0.0.73__py2.py3-none-any.whl → 0.0.74__py2.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.
- coralnet_toolbox/Annotations/QtAnnotation.py +28 -69
- coralnet_toolbox/Annotations/QtMaskAnnotation.py +408 -0
- coralnet_toolbox/Annotations/QtMultiPolygonAnnotation.py +72 -56
- coralnet_toolbox/Annotations/QtPatchAnnotation.py +165 -216
- coralnet_toolbox/Annotations/QtPolygonAnnotation.py +497 -353
- coralnet_toolbox/Annotations/QtRectangleAnnotation.py +126 -116
- coralnet_toolbox/CoralNet/QtDownload.py +2 -1
- coralnet_toolbox/Explorer/QtExplorer.py +16 -14
- coralnet_toolbox/Explorer/QtSettingsWidgets.py +114 -82
- coralnet_toolbox/IO/QtExportTagLabAnnotations.py +30 -10
- coralnet_toolbox/IO/QtImportTagLabAnnotations.py +21 -15
- coralnet_toolbox/IO/QtOpenProject.py +46 -78
- coralnet_toolbox/IO/QtSaveProject.py +18 -43
- coralnet_toolbox/MachineLearning/ExportDataset/QtBase.py +1 -1
- coralnet_toolbox/MachineLearning/ImportDataset/QtBase.py +42 -22
- coralnet_toolbox/MachineLearning/VideoInference/QtBase.py +0 -4
- coralnet_toolbox/QtEventFilter.py +11 -0
- coralnet_toolbox/QtImageWindow.py +117 -68
- coralnet_toolbox/QtLabelWindow.py +13 -1
- coralnet_toolbox/QtMainWindow.py +5 -27
- coralnet_toolbox/QtProgressBar.py +52 -27
- coralnet_toolbox/Rasters/RasterTableModel.py +8 -8
- coralnet_toolbox/SAM/QtDeployPredictor.py +10 -0
- coralnet_toolbox/SeeAnything/QtDeployGenerator.py +779 -161
- coralnet_toolbox/SeeAnything/QtDeployPredictor.py +86 -149
- coralnet_toolbox/Tools/QtCutSubTool.py +18 -2
- coralnet_toolbox/Tools/QtResizeSubTool.py +19 -2
- coralnet_toolbox/Tools/QtSAMTool.py +72 -50
- coralnet_toolbox/Tools/QtSeeAnythingTool.py +8 -5
- coralnet_toolbox/Tools/QtSelectTool.py +27 -3
- coralnet_toolbox/Tools/QtSubtractSubTool.py +66 -0
- coralnet_toolbox/Tools/__init__.py +2 -0
- coralnet_toolbox/__init__.py +1 -1
- coralnet_toolbox/utilities.py +137 -47
- coralnet_toolbox-0.0.74.dist-info/METADATA +375 -0
- {coralnet_toolbox-0.0.73.dist-info → coralnet_toolbox-0.0.74.dist-info}/RECORD +40 -38
- coralnet_toolbox-0.0.73.dist-info/METADATA +0 -341
- {coralnet_toolbox-0.0.73.dist-info → coralnet_toolbox-0.0.74.dist-info}/WHEEL +0 -0
- {coralnet_toolbox-0.0.73.dist-info → coralnet_toolbox-0.0.74.dist-info}/entry_points.txt +0 -0
- {coralnet_toolbox-0.0.73.dist-info → coralnet_toolbox-0.0.74.dist-info}/licenses/LICENSE.txt +0 -0
- {coralnet_toolbox-0.0.73.dist-info → coralnet_toolbox-0.0.74.dist-info}/top_level.txt +0 -0
@@ -242,7 +242,7 @@ class AnnotationSettingsWidget(QGroupBox):
|
|
242
242
|
self.setup_ui()
|
243
243
|
|
244
244
|
def setup_ui(self):
|
245
|
-
# The main layout is vertical, to hold the top columns
|
245
|
+
# The main layout is vertical, to hold the top columns and the bottom buttons
|
246
246
|
layout = QVBoxLayout(self)
|
247
247
|
|
248
248
|
# A horizontal layout to contain the filter columns
|
@@ -256,7 +256,7 @@ class AnnotationSettingsWidget(QGroupBox):
|
|
256
256
|
|
257
257
|
self.images_list = QListWidget()
|
258
258
|
self.images_list.setSelectionMode(QListWidget.ExtendedSelection)
|
259
|
-
self.images_list.
|
259
|
+
self.images_list.setMinimumHeight(100)
|
260
260
|
|
261
261
|
if hasattr(self.main_window, 'image_window') and hasattr(self.main_window.image_window, 'raster_manager'):
|
262
262
|
for path in self.main_window.image_window.raster_manager.image_paths:
|
@@ -284,7 +284,7 @@ class AnnotationSettingsWidget(QGroupBox):
|
|
284
284
|
|
285
285
|
self.annotation_type_list = QListWidget()
|
286
286
|
self.annotation_type_list.setSelectionMode(QListWidget.ExtendedSelection)
|
287
|
-
self.annotation_type_list.
|
287
|
+
self.annotation_type_list.setMinimumHeight(100)
|
288
288
|
self.annotation_type_list.addItems(["PatchAnnotation",
|
289
289
|
"RectangleAnnotation",
|
290
290
|
"PolygonAnnotation",
|
@@ -312,7 +312,7 @@ class AnnotationSettingsWidget(QGroupBox):
|
|
312
312
|
|
313
313
|
self.label_list = QListWidget()
|
314
314
|
self.label_list.setSelectionMode(QListWidget.ExtendedSelection)
|
315
|
-
self.label_list.
|
315
|
+
self.label_list.setMinimumHeight(100)
|
316
316
|
|
317
317
|
if hasattr(self.main_window, 'label_window') and hasattr(self.main_window.label_window, 'labels'):
|
318
318
|
for label in self.main_window.label_window.labels:
|
@@ -335,9 +335,6 @@ class AnnotationSettingsWidget(QGroupBox):
|
|
335
335
|
# Add the horizontal layout of columns to the main vertical layout
|
336
336
|
layout.addLayout(conditions_layout)
|
337
337
|
|
338
|
-
# Add a stretch item to push the columns to the top
|
339
|
-
layout.addStretch(1)
|
340
|
-
|
341
338
|
# Bottom buttons layout with Apply and Clear buttons on the right
|
342
339
|
bottom_layout = QHBoxLayout()
|
343
340
|
bottom_layout.addStretch() # Push buttons to the right
|
@@ -350,9 +347,12 @@ class AnnotationSettingsWidget(QGroupBox):
|
|
350
347
|
self.clear_button.clicked.connect(self.clear_all_conditions)
|
351
348
|
bottom_layout.addWidget(self.clear_button)
|
352
349
|
|
353
|
-
# Add the bottom buttons layout to the main layout
|
350
|
+
# Add the bottom buttons layout to the main layout
|
354
351
|
layout.addLayout(bottom_layout)
|
355
352
|
|
353
|
+
# Add a stretch item to push all content to the top
|
354
|
+
layout.addStretch(1)
|
355
|
+
|
356
356
|
# Set defaults
|
357
357
|
self.set_defaults()
|
358
358
|
|
@@ -435,68 +435,66 @@ class AnnotationSettingsWidget(QGroupBox):
|
|
435
435
|
|
436
436
|
|
437
437
|
class ModelSettingsWidget(QGroupBox):
|
438
|
-
"""Widget containing
|
438
|
+
"""Widget containing a structured, hierarchical model selection system."""
|
439
439
|
selection_changed = pyqtSignal()
|
440
440
|
|
441
441
|
def __init__(self, main_window, parent=None):
|
442
442
|
super(ModelSettingsWidget, self).__init__("Model Settings", parent)
|
443
443
|
self.main_window = main_window
|
444
444
|
self.explorer_window = parent
|
445
|
+
|
446
|
+
# --- Data for hierarchical selection ---
|
447
|
+
self.standard_models_map = {
|
448
|
+
'YOLOv8': {'Nano': 'n', 'Small': 's', 'Medium': 'm', 'Large': 'l', 'X-Large': 'x'},
|
449
|
+
'YOLOv11': {'Nano': 'n', 'Small': 's', 'Medium': 'm', 'Large': 'l', 'X-Large': 'x'},
|
450
|
+
'YOLOv12': {'Nano': 'n', 'Small': 's', 'Medium': 'm', 'Large': 'l', 'X-Large': 'x'}
|
451
|
+
}
|
452
|
+
self.community_configs = get_available_configs(task='classify')
|
453
|
+
|
445
454
|
self.setup_ui()
|
446
455
|
|
447
456
|
def setup_ui(self):
|
448
457
|
"""Set up the UI with a tabbed interface for model selection."""
|
449
458
|
main_layout = QVBoxLayout(self)
|
450
|
-
|
451
|
-
# --- Tabbed Interface for Model Selection ---
|
452
459
|
self.tabs = QTabWidget()
|
453
460
|
|
454
|
-
# Tab 1: Select Model
|
461
|
+
# === Tab 1: Select Pre-defined Model ===
|
455
462
|
model_select_tab = QWidget()
|
456
|
-
model_select_layout = QFormLayout(model_select_tab)
|
457
|
-
model_select_layout.setContentsMargins(5, 10, 5, 5)
|
458
|
-
|
459
|
-
|
460
|
-
self.
|
461
|
-
self.
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
]
|
480
|
-
|
481
|
-
self.model_combo.addItems(standard_models)
|
482
|
-
|
483
|
-
community_configs = get_available_configs(task='classify')
|
484
|
-
if community_configs:
|
485
|
-
self.model_combo.insertSeparator(len(standard_models) + 2)
|
486
|
-
self.model_combo.addItems(list(community_configs.keys()))
|
487
|
-
|
488
|
-
self.model_combo.setCurrentText('Color Features')
|
489
|
-
model_select_layout.addRow("Model:", self.model_combo)
|
463
|
+
self.model_select_layout = QFormLayout(model_select_tab)
|
464
|
+
self.model_select_layout.setContentsMargins(5, 10, 5, 5)
|
465
|
+
|
466
|
+
# 1. Main Category ComboBox
|
467
|
+
self.category_combo = QComboBox()
|
468
|
+
self.category_combo.addItems(["Color Features", "Standard Model", "Community Model"])
|
469
|
+
self.model_select_layout.addRow("Category:", self.category_combo)
|
470
|
+
|
471
|
+
# 2. Standard Model Options (initially hidden)
|
472
|
+
self.family_combo = QComboBox()
|
473
|
+
self.family_combo.addItems(self.standard_models_map.keys())
|
474
|
+
self.size_combo = QComboBox()
|
475
|
+
self.family_combo.currentTextChanged.connect(self._update_size_combo)
|
476
|
+
self._update_size_combo(self.family_combo.currentText())
|
477
|
+
|
478
|
+
self.standard_model_widgets = [QLabel("Family:"), self.family_combo, QLabel("Size:"), self.size_combo]
|
479
|
+
self.model_select_layout.addRow(self.standard_model_widgets[0], self.standard_model_widgets[1])
|
480
|
+
self.model_select_layout.addRow(self.standard_model_widgets[2], self.standard_model_widgets[3])
|
481
|
+
|
482
|
+
# 3. Community Model Options (initially hidden)
|
483
|
+
self.community_combo = QComboBox()
|
484
|
+
if self.community_configs:
|
485
|
+
self.community_combo.addItems(list(self.community_configs.keys()))
|
486
|
+
self.community_model_widgets = [QLabel("Model:"), self.community_combo]
|
487
|
+
self.model_select_layout.addRow(self.community_model_widgets[0], self.community_model_widgets[1])
|
490
488
|
|
491
489
|
self.tabs.addTab(model_select_tab, "Select Model")
|
492
490
|
|
493
|
-
# Tab 2: Existing Model from File
|
491
|
+
# === Tab 2: Existing Model from File ===
|
494
492
|
model_existing_tab = QWidget()
|
495
493
|
model_existing_layout = QFormLayout(model_existing_tab)
|
496
494
|
model_existing_layout.setContentsMargins(5, 10, 5, 5)
|
497
495
|
|
498
496
|
self.model_path_edit = QLineEdit()
|
499
|
-
self.model_path_edit.setPlaceholderText("Path to a
|
497
|
+
self.model_path_edit.setPlaceholderText("Path to a compatible .pt model file...")
|
500
498
|
browse_button = QPushButton("Browse...")
|
501
499
|
browse_button.clicked.connect(self.browse_for_model)
|
502
500
|
|
@@ -505,82 +503,116 @@ class ModelSettingsWidget(QGroupBox):
|
|
505
503
|
path_layout.addWidget(browse_button)
|
506
504
|
model_existing_layout.addRow("Model Path:", path_layout)
|
507
505
|
|
506
|
+
help_label = QLabel("Note: Select an Ultralytics model (.pt).")
|
507
|
+
help_label.setStyleSheet("color: gray; font-style: italic;")
|
508
|
+
model_existing_layout.addRow("", help_label)
|
509
|
+
|
508
510
|
self.tabs.addTab(model_existing_tab, "Use Existing Model")
|
509
511
|
|
510
512
|
main_layout.addWidget(self.tabs)
|
511
|
-
|
512
|
-
#
|
513
|
-
self.model_combo.currentTextChanged.connect(self._on_selection_changed)
|
514
|
-
self.tabs.currentChanged.connect(self._on_selection_changed)
|
515
|
-
self.model_path_edit.textChanged.connect(self._on_selection_changed)
|
516
|
-
|
517
|
-
# Add feature extraction mode selection outside of tabs
|
513
|
+
|
514
|
+
# === Feature Extraction Mode (Reverted to bottom) ===
|
518
515
|
feature_mode_layout = QFormLayout()
|
519
516
|
self.feature_mode_combo = QComboBox()
|
520
517
|
self.feature_mode_combo.addItems(["Predictions", "Embed Features"])
|
521
|
-
self.feature_mode_combo.currentTextChanged.connect(self._on_selection_changed)
|
522
518
|
feature_mode_layout.addRow("Feature Mode:", self.feature_mode_combo)
|
523
519
|
main_layout.addLayout(feature_mode_layout)
|
520
|
+
|
521
|
+
# --- Connect Signals ---
|
522
|
+
self.category_combo.currentTextChanged.connect(self._on_category_changed)
|
523
|
+
self.tabs.currentChanged.connect(self._on_selection_changed)
|
524
|
+
for widget in [self.category_combo, self.family_combo, self.size_combo,
|
525
|
+
self.community_combo, self.model_path_edit, self.feature_mode_combo]:
|
526
|
+
if isinstance(widget, QComboBox):
|
527
|
+
widget.currentTextChanged.connect(self._on_selection_changed)
|
528
|
+
elif isinstance(widget, QLineEdit):
|
529
|
+
widget.textChanged.connect(self._on_selection_changed)
|
530
|
+
|
531
|
+
# --- Initial State ---
|
532
|
+
self._on_category_changed(self.category_combo.currentText())
|
533
|
+
self._on_selection_changed()
|
534
|
+
|
535
|
+
@pyqtSlot(str)
|
536
|
+
def _update_size_combo(self, family):
|
537
|
+
"""Populate the size combo box based on the selected family."""
|
538
|
+
self.size_combo.clear()
|
539
|
+
if family in self.standard_models_map:
|
540
|
+
self.size_combo.addItems(self.standard_models_map[family].keys())
|
541
|
+
|
542
|
+
@pyqtSlot(str)
|
543
|
+
def _on_category_changed(self, category):
|
544
|
+
"""Show or hide sub-option widgets based on the selected category."""
|
545
|
+
is_standard = (category == "Standard Model")
|
546
|
+
is_community = (category == "Community Model")
|
547
|
+
|
548
|
+
for widget in self.standard_model_widgets:
|
549
|
+
widget.setVisible(is_standard)
|
550
|
+
for widget in self.community_model_widgets:
|
551
|
+
widget.setVisible(is_community)
|
524
552
|
|
525
|
-
# Initialize the feature mode state and emit the first signal
|
526
553
|
self._on_selection_changed()
|
527
554
|
|
528
555
|
def browse_for_model(self):
|
529
556
|
"""Open a file dialog to browse for model files."""
|
530
557
|
options = QFileDialog.Options()
|
531
558
|
file_path, _ = QFileDialog.getOpenFileName(
|
532
|
-
self,
|
533
|
-
"Select Model File",
|
534
|
-
"",
|
535
|
-
"PyTorch Models (*.pt);;All Files (*)",
|
536
|
-
options=options
|
537
|
-
)
|
559
|
+
self, "Select Model File", "", "PyTorch Models (*.pt);;All Files (*)", options=options)
|
538
560
|
if file_path:
|
539
561
|
self.model_path_edit.setText(file_path)
|
540
562
|
|
541
563
|
@pyqtSlot()
|
542
564
|
def _on_selection_changed(self):
|
543
|
-
"""Central slot to handle any change
|
565
|
+
"""Central slot to handle any change and emit a single signal."""
|
544
566
|
self._update_feature_mode_state()
|
545
567
|
self.selection_changed.emit()
|
546
568
|
|
547
|
-
def _update_feature_mode_state(self
|
548
|
-
"""Update the enabled state of the feature mode field
|
549
|
-
current_tab_index = self.tabs.currentIndex()
|
569
|
+
def _update_feature_mode_state(self):
|
570
|
+
"""Update the enabled state and tooltip of the feature mode field."""
|
550
571
|
is_color_features = False
|
572
|
+
current_tab_index = self.tabs.currentIndex()
|
551
573
|
|
552
574
|
if current_tab_index == 0:
|
553
|
-
|
554
|
-
current_model = self.model_combo.currentText()
|
555
|
-
is_color_features = current_model == "Color Features"
|
556
|
-
elif current_tab_index == 1:
|
557
|
-
# Use Existing Model tab - feature mode should always be enabled
|
558
|
-
is_color_features = False
|
575
|
+
is_color_features = (self.category_combo.currentText() == "Color Features")
|
559
576
|
|
560
|
-
# Enable feature mode only if not Color Features
|
561
577
|
self.feature_mode_combo.setEnabled(not is_color_features)
|
562
578
|
|
563
|
-
# Update the tooltip based on state
|
564
579
|
if is_color_features:
|
565
|
-
self.feature_mode_combo.setToolTip("Feature Mode is not
|
580
|
+
self.feature_mode_combo.setToolTip("Feature Mode is not applicable for Color Features.")
|
566
581
|
else:
|
567
|
-
self.feature_mode_combo.setToolTip(
|
582
|
+
self.feature_mode_combo.setToolTip(
|
583
|
+
"Choose 'Predictions' for class probabilities (for uncertainty analysis)\n"
|
584
|
+
"or 'Embed Features' for a general-purpose feature vector."
|
585
|
+
)
|
568
586
|
|
569
587
|
def get_selected_model(self):
|
570
588
|
"""Get the currently selected model name/path and feature mode."""
|
571
589
|
current_tab_index = self.tabs.currentIndex()
|
590
|
+
model_name = ""
|
572
591
|
|
573
|
-
# Get model name/path and feature mode based on the active tab
|
574
592
|
if current_tab_index == 0:
|
575
|
-
|
593
|
+
category = self.category_combo.currentText()
|
594
|
+
if category == "Color Features":
|
595
|
+
model_name = "Color Features"
|
596
|
+
elif category == "Standard Model":
|
597
|
+
family_text = self.family_combo.currentText()
|
598
|
+
size_text = self.size_combo.currentText()
|
599
|
+
|
600
|
+
# Add a guard clause to prevent crashing if a combo is empty.
|
601
|
+
if not family_text or not size_text:
|
602
|
+
return "", "N/A" # Return a safe default
|
603
|
+
|
604
|
+
family_key = family_text.lower().replace('-', '')
|
605
|
+
size_key = self.standard_models_map[family_text][size_text]
|
606
|
+
model_name = f"{family_key}{size_key}-cls.pt"
|
607
|
+
|
608
|
+
elif category == "Community Model":
|
609
|
+
model_name = self.community_combo.currentText()
|
576
610
|
elif current_tab_index == 1:
|
577
611
|
model_name = self.model_path_edit.text()
|
578
|
-
else:
|
579
|
-
return "", None
|
580
612
|
|
581
613
|
feature_mode = self.feature_mode_combo.currentText() if self.feature_mode_combo.isEnabled() else "N/A"
|
582
614
|
return model_name, feature_mode
|
583
|
-
|
615
|
+
|
584
616
|
|
585
617
|
class EmbeddingSettingsWidget(QGroupBox):
|
586
618
|
"""Widget containing settings with tabs for models and embedding."""
|
@@ -197,30 +197,50 @@ class ExportTagLabAnnotations:
|
|
197
197
|
"""
|
198
198
|
# Calculate bounding box, centroid, area, perimeter, and contour
|
199
199
|
points = annotation.points
|
200
|
+
# Convert points to TagLab contour format
|
201
|
+
contour = self.taglabToPoints(np.array([[point.x(), point.y()] for point in points]))
|
202
|
+
inner_contours = []
|
203
|
+
if hasattr(annotation, 'holes') and annotation.holes:
|
204
|
+
# Convert holes to TagLab format
|
205
|
+
for hole in annotation.holes:
|
206
|
+
inner_contours.append(self.taglabToPoints(np.array([[point.x(), point.y()] for point in hole])))
|
207
|
+
|
208
|
+
# Calculate bounding box
|
200
209
|
min_x = int(min(point.x() for point in points))
|
201
210
|
min_y = int(min(point.y() for point in points))
|
202
211
|
max_x = int(max(point.x() for point in points))
|
203
|
-
max_y = int(max(point.y() for point in points))
|
212
|
+
max_y = int(max(point.y() for point in points))
|
204
213
|
width = max_x - min_x
|
205
214
|
height = max_y - min_y
|
215
|
+
bbox = [min_x, min_y, width, height]
|
216
|
+
|
217
|
+
# Calculate centroid
|
206
218
|
centroid_x = float(f"{sum(point.x() for point in points) / len(points):.1f}")
|
207
219
|
centroid_y = float(f"{sum(point.y() for point in points) / len(points):.1f}")
|
220
|
+
centroid = [centroid_x, centroid_y]
|
221
|
+
|
208
222
|
area = float(f"{annotation.get_area():.1f}")
|
209
223
|
perimeter = float(f"{annotation.get_perimeter():.1f}")
|
210
|
-
|
224
|
+
data = annotation.data if hasattr(annotation, 'data') else {}
|
225
|
+
|
226
|
+
# Pop these keys from data if they exist
|
227
|
+
class_name = data.pop('class_name', annotation.label.short_label_code)
|
228
|
+
instance_name = data.pop('instance_name', "coral0")
|
229
|
+
blob_name = data.pop('blob_name', f"c-0-{centroid_x}x-{centroid_y}y")
|
230
|
+
note = data.pop('note', "")
|
211
231
|
|
212
232
|
annotation_dict = {
|
213
|
-
"bbox":
|
214
|
-
"centroid":
|
233
|
+
"bbox": bbox,
|
234
|
+
"centroid": centroid,
|
215
235
|
"area": area,
|
216
236
|
"perimeter": perimeter,
|
217
237
|
"contour": contour,
|
218
|
-
"inner contours":
|
219
|
-
"class name":
|
220
|
-
"instance name":
|
221
|
-
"blob name":
|
222
|
-
"note":
|
223
|
-
"data":
|
238
|
+
"inner contours": inner_contours,
|
239
|
+
"class name": class_name,
|
240
|
+
"instance name": instance_name,
|
241
|
+
"blob name": blob_name,
|
242
|
+
"note": note,
|
243
|
+
"data": data
|
224
244
|
}
|
225
245
|
|
226
246
|
return annotation_dict
|
@@ -161,23 +161,25 @@ class ImportTagLabAnnotations:
|
|
161
161
|
short_label_code = label_info['name'].strip()
|
162
162
|
long_label_code = label_info['name'].strip()
|
163
163
|
color = QColor(*label_info['fill'])
|
164
|
-
|
165
|
-
#
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
164
|
+
|
165
|
+
# Pack all other data into a dict
|
166
|
+
imported_data = {
|
167
|
+
'bbox': annotation.get('bbox'),
|
168
|
+
'centroid': annotation.get('centroid'),
|
169
|
+
'area': annotation.get('area'),
|
170
|
+
'perimeter': annotation.get('perimeter'),
|
171
|
+
'class_name': annotation.get('class name'),
|
172
|
+
'instance_name': annotation.get('instance name'),
|
173
|
+
'blob_name': annotation.get('blob name'),
|
174
|
+
'id': annotation.get('id'),
|
175
|
+
'note': annotation.get('note'),
|
176
|
+
'data': annotation.get('data'),
|
177
|
+
}
|
178
178
|
|
179
179
|
# Convert contour string to points
|
180
180
|
points = self.parse_contour(annotation['contour'])
|
181
|
+
# Convert inner contours to a list of lists of points (holes)
|
182
|
+
holes = [self.parse_contour(inner) for inner in annotation.get('inner contours', [])]
|
181
183
|
|
182
184
|
# Create the label if it doesn't exist
|
183
185
|
label = self.label_window.add_label_if_not_exists(short_label_code,
|
@@ -191,8 +193,12 @@ class ImportTagLabAnnotations:
|
|
191
193
|
long_label_code=long_label_code,
|
192
194
|
color=color,
|
193
195
|
image_path=image_full_path,
|
194
|
-
label_id=label_id
|
196
|
+
label_id=label_id,
|
197
|
+
holes=holes,
|
195
198
|
)
|
199
|
+
# Add additional data to the annotation
|
200
|
+
polygon_annotation.data = imported_data
|
201
|
+
|
196
202
|
# Add annotation to the dict
|
197
203
|
self.annotation_window.add_annotation_to_dict(polygon_annotation)
|
198
204
|
|
@@ -12,16 +12,12 @@ from PyQt5.QtWidgets import (QDialog, QFileDialog, QVBoxLayout, QPushButton, QLa
|
|
12
12
|
QLineEdit)
|
13
13
|
|
14
14
|
from coralnet_toolbox.QtLabelWindow import Label
|
15
|
-
|
16
15
|
from coralnet_toolbox.QtWorkArea import WorkArea
|
17
|
-
|
18
16
|
from coralnet_toolbox.Annotations.QtPatchAnnotation import PatchAnnotation
|
19
17
|
from coralnet_toolbox.Annotations.QtPolygonAnnotation import PolygonAnnotation
|
20
18
|
from coralnet_toolbox.Annotations.QtRectangleAnnotation import RectangleAnnotation
|
21
19
|
from coralnet_toolbox.Annotations.QtMultiPolygonAnnotation import MultiPolygonAnnotation
|
22
|
-
|
23
20
|
from coralnet_toolbox.Common.QtUpdateImagePaths import UpdateImagePaths
|
24
|
-
|
25
21
|
from coralnet_toolbox.QtProgressBar import ProgressBar
|
26
22
|
|
27
23
|
|
@@ -143,19 +139,18 @@ class OpenProject(QDialog):
|
|
143
139
|
with open(file_path, 'r') as file:
|
144
140
|
project_data = json.load(file)
|
145
141
|
|
142
|
+
# Handle both new and old project formats for images and work areas
|
143
|
+
images_data = project_data.get('images', project_data.get('image_paths'))
|
144
|
+
legacy_workareas = project_data.get('workareas') # For backward compatibility
|
145
|
+
|
146
146
|
# Update main window with loaded project data
|
147
|
-
self.import_images(
|
148
|
-
self.import_workareas(project_data.get('workareas'))
|
147
|
+
self.import_images(images_data, legacy_workareas)
|
149
148
|
self.import_labels(project_data.get('labels'))
|
150
149
|
self.import_annotations(project_data.get('annotations'))
|
151
150
|
|
152
151
|
# Update current project path
|
153
152
|
self.current_project_path = file_path
|
154
153
|
|
155
|
-
QMessageBox.information(self.annotation_window,
|
156
|
-
"Project Loaded",
|
157
|
-
"Project has been successfully loaded.")
|
158
|
-
|
159
154
|
except Exception as e:
|
160
155
|
QMessageBox.warning(self.annotation_window,
|
161
156
|
"Error Loading Project",
|
@@ -168,10 +163,15 @@ class OpenProject(QDialog):
|
|
168
163
|
# Exit
|
169
164
|
self.accept()
|
170
165
|
|
171
|
-
def import_images(self,
|
172
|
-
"""Import images from the given
|
173
|
-
if not
|
166
|
+
def import_images(self, images_data, legacy_workareas=None):
|
167
|
+
"""Import images, states, and work areas from the given data."""
|
168
|
+
if not images_data:
|
174
169
|
return
|
170
|
+
|
171
|
+
# Determine if the format is old (list of strings) or new (list of dicts)
|
172
|
+
is_new_format = isinstance(images_data[0], dict)
|
173
|
+
|
174
|
+
image_paths = [img['path'] for img in images_data] if is_new_format else images_data
|
175
175
|
|
176
176
|
if not all([os.path.exists(path) for path in image_paths]):
|
177
177
|
image_paths, self.updated_paths = UpdateImagePaths.update_paths(image_paths)
|
@@ -183,15 +183,46 @@ class OpenProject(QDialog):
|
|
183
183
|
progress_bar.start_progress(total_images)
|
184
184
|
|
185
185
|
try:
|
186
|
+
# Create a map for quick data lookup if using the new format
|
187
|
+
image_data_map = {img['path']: img for img in images_data} if is_new_format else {}
|
188
|
+
|
186
189
|
# Add images to the image window's raster manager one by one
|
187
190
|
for path in image_paths:
|
188
|
-
# Use the improved add_image method which handles both
|
189
|
-
# adding to raster_manager and updating filtered_paths
|
190
191
|
self.image_window.add_image(path)
|
192
|
+
raster = self.image_window.raster_manager.get_raster(path)
|
193
|
+
if not raster:
|
194
|
+
continue
|
195
|
+
|
196
|
+
# If using the new format, apply saved state and work areas
|
197
|
+
if is_new_format and path in image_data_map:
|
198
|
+
data = image_data_map[path]
|
199
|
+
state = data.get('state', {})
|
200
|
+
work_areas_list = data.get('work_areas', [])
|
201
|
+
|
202
|
+
# Apply raster state
|
203
|
+
raster.checkbox_state = state.get('checkbox_state', False)
|
204
|
+
|
205
|
+
# Import work areas for this image
|
206
|
+
for work_area_data in work_areas_list:
|
207
|
+
try:
|
208
|
+
work_area = WorkArea.from_dict(work_area_data, path)
|
209
|
+
raster.add_work_area(work_area)
|
210
|
+
except Exception as e:
|
211
|
+
print(f"Warning: Could not import work area {work_area_data}: {str(e)}")
|
191
212
|
|
192
213
|
# Update the progress bar
|
193
214
|
progress_bar.update_progress()
|
194
215
|
|
216
|
+
# Handle backward compatibility for old, top-level work areas
|
217
|
+
if legacy_workareas:
|
218
|
+
for image_path, work_areas_list in legacy_workareas.items():
|
219
|
+
current_path = self.updated_paths.get(image_path, image_path)
|
220
|
+
raster = self.image_window.raster_manager.get_raster(current_path)
|
221
|
+
if raster:
|
222
|
+
for work_area_data in work_areas_list:
|
223
|
+
work_area = WorkArea.from_dict(work_area_data, current_path)
|
224
|
+
raster.add_work_area(work_area)
|
225
|
+
|
195
226
|
# Show the last image if any were imported
|
196
227
|
if self.image_window.raster_manager.image_paths:
|
197
228
|
self.image_window.load_image_by_path(self.image_window.raster_manager.image_paths[-1])
|
@@ -204,69 +235,6 @@ class OpenProject(QDialog):
|
|
204
235
|
# Close progress bar
|
205
236
|
progress_bar.stop_progress()
|
206
237
|
progress_bar.close()
|
207
|
-
|
208
|
-
def import_workareas(self, workareas):
|
209
|
-
"""Import work areas for each image."""
|
210
|
-
if not workareas:
|
211
|
-
return
|
212
|
-
|
213
|
-
# Start the progress bar
|
214
|
-
total_images = len(workareas)
|
215
|
-
progress_bar = ProgressBar(self.annotation_window, title="Importing Work Areas")
|
216
|
-
progress_bar.show()
|
217
|
-
progress_bar.start_progress(total_images)
|
218
|
-
|
219
|
-
try:
|
220
|
-
# Loop through each image's work areas
|
221
|
-
for image_path, work_areas_list in workareas.items():
|
222
|
-
|
223
|
-
# Check if the image path was updated (moved)
|
224
|
-
updated_path = False
|
225
|
-
|
226
|
-
if image_path not in self.image_window.raster_manager.image_paths:
|
227
|
-
# Check if the path was updated
|
228
|
-
if image_path in self.updated_paths:
|
229
|
-
image_path = self.updated_paths[image_path]
|
230
|
-
updated_path = True
|
231
|
-
else:
|
232
|
-
print(f"Warning: Image not found for work areas: {image_path}")
|
233
|
-
continue
|
234
|
-
|
235
|
-
# Get the raster for this image
|
236
|
-
raster = self.image_window.raster_manager.get_raster(image_path)
|
237
|
-
if not raster:
|
238
|
-
print(f"Warning: Could not get raster for image: {image_path}")
|
239
|
-
continue
|
240
|
-
|
241
|
-
# Import each work area for this image
|
242
|
-
for work_area_data in work_areas_list:
|
243
|
-
try:
|
244
|
-
# Update image path if it was changed
|
245
|
-
if updated_path:
|
246
|
-
work_area_data['image_path'] = image_path
|
247
|
-
|
248
|
-
# Create WorkArea from dictionary
|
249
|
-
work_area = WorkArea.from_dict(work_area_data, image_path)
|
250
|
-
|
251
|
-
# Add work area to the raster
|
252
|
-
raster.add_work_area(work_area)
|
253
|
-
|
254
|
-
except Exception as e:
|
255
|
-
print(f"Warning: Could not import work area {work_area_data}: {str(e)}")
|
256
|
-
continue
|
257
|
-
|
258
|
-
# Update the progress bar
|
259
|
-
progress_bar.update_progress()
|
260
|
-
|
261
|
-
except Exception as e:
|
262
|
-
QMessageBox.warning(self.annotation_window,
|
263
|
-
"Error Importing Work Areas",
|
264
|
-
f"An error occurred while importing work areas: {str(e)}")
|
265
|
-
|
266
|
-
finally:
|
267
|
-
# Close progress bar
|
268
|
-
progress_bar.stop_progress()
|
269
|
-
progress_bar.close()
|
270
238
|
|
271
239
|
def import_labels(self, labels):
|
272
240
|
"""Import labels from the given list."""
|