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.
Files changed (41) hide show
  1. coralnet_toolbox/Annotations/QtAnnotation.py +28 -69
  2. coralnet_toolbox/Annotations/QtMaskAnnotation.py +408 -0
  3. coralnet_toolbox/Annotations/QtMultiPolygonAnnotation.py +72 -56
  4. coralnet_toolbox/Annotations/QtPatchAnnotation.py +165 -216
  5. coralnet_toolbox/Annotations/QtPolygonAnnotation.py +497 -353
  6. coralnet_toolbox/Annotations/QtRectangleAnnotation.py +126 -116
  7. coralnet_toolbox/CoralNet/QtDownload.py +2 -1
  8. coralnet_toolbox/Explorer/QtExplorer.py +16 -14
  9. coralnet_toolbox/Explorer/QtSettingsWidgets.py +114 -82
  10. coralnet_toolbox/IO/QtExportTagLabAnnotations.py +30 -10
  11. coralnet_toolbox/IO/QtImportTagLabAnnotations.py +21 -15
  12. coralnet_toolbox/IO/QtOpenProject.py +46 -78
  13. coralnet_toolbox/IO/QtSaveProject.py +18 -43
  14. coralnet_toolbox/MachineLearning/ExportDataset/QtBase.py +1 -1
  15. coralnet_toolbox/MachineLearning/ImportDataset/QtBase.py +42 -22
  16. coralnet_toolbox/MachineLearning/VideoInference/QtBase.py +0 -4
  17. coralnet_toolbox/QtEventFilter.py +11 -0
  18. coralnet_toolbox/QtImageWindow.py +117 -68
  19. coralnet_toolbox/QtLabelWindow.py +13 -1
  20. coralnet_toolbox/QtMainWindow.py +5 -27
  21. coralnet_toolbox/QtProgressBar.py +52 -27
  22. coralnet_toolbox/Rasters/RasterTableModel.py +8 -8
  23. coralnet_toolbox/SAM/QtDeployPredictor.py +10 -0
  24. coralnet_toolbox/SeeAnything/QtDeployGenerator.py +779 -161
  25. coralnet_toolbox/SeeAnything/QtDeployPredictor.py +86 -149
  26. coralnet_toolbox/Tools/QtCutSubTool.py +18 -2
  27. coralnet_toolbox/Tools/QtResizeSubTool.py +19 -2
  28. coralnet_toolbox/Tools/QtSAMTool.py +72 -50
  29. coralnet_toolbox/Tools/QtSeeAnythingTool.py +8 -5
  30. coralnet_toolbox/Tools/QtSelectTool.py +27 -3
  31. coralnet_toolbox/Tools/QtSubtractSubTool.py +66 -0
  32. coralnet_toolbox/Tools/__init__.py +2 -0
  33. coralnet_toolbox/__init__.py +1 -1
  34. coralnet_toolbox/utilities.py +137 -47
  35. coralnet_toolbox-0.0.74.dist-info/METADATA +375 -0
  36. {coralnet_toolbox-0.0.73.dist-info → coralnet_toolbox-0.0.74.dist-info}/RECORD +40 -38
  37. coralnet_toolbox-0.0.73.dist-info/METADATA +0 -341
  38. {coralnet_toolbox-0.0.73.dist-info → coralnet_toolbox-0.0.74.dist-info}/WHEEL +0 -0
  39. {coralnet_toolbox-0.0.73.dist-info → coralnet_toolbox-0.0.74.dist-info}/entry_points.txt +0 -0
  40. {coralnet_toolbox-0.0.73.dist-info → coralnet_toolbox-0.0.74.dist-info}/licenses/LICENSE.txt +0 -0
  41. {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, the stretch, and the bottom buttons
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.setMaximumHeight(50)
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.setMaximumHeight(50)
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.setMaximumHeight(50)
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, keeping it at the bottom
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 model selection with tabs for different model sources."""
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) # Add some top margin
458
-
459
- self.model_combo = QComboBox()
460
- self.model_combo.addItems(["Color Features"])
461
- self.model_combo.insertSeparator(1) # Add a separator
462
-
463
- standard_models = [
464
- 'yolov8n-cls.pt',
465
- 'yolov8s-cls.pt',
466
- 'yolov8m-cls.pt',
467
- 'yolov8l-cls.pt',
468
- 'yolov8x-cls.pt',
469
- 'yolo11n-cls.pt',
470
- 'yolo11s-cls.pt',
471
- 'yolo11m-cls.pt',
472
- 'yolo11l-cls.pt',
473
- 'yolo11x-cls.pt',
474
- 'yolo12n-cls.pt',
475
- 'yolo12s-cls.pt',
476
- 'yolo12m-cls.pt',
477
- 'yolo12l-cls.pt',
478
- 'yolo12x-cls.pt'
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 existing .pt model file...")
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
- # Connect all relevant widgets to a single slot that emits the new signal
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 in model selection and emit a single signal."""
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, *args):
548
- """Update the enabled state of the feature mode field based on the current model selection."""
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
- # Select Model tab - check if Color Features is selected
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 available for Color Features")
580
+ self.feature_mode_combo.setToolTip("Feature Mode is not applicable for Color Features.")
566
581
  else:
567
- self.feature_mode_combo.setToolTip("Select the feature extraction mode")
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
- model_name = self.model_combo.currentText()
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
- contour = self.taglabToPoints(np.array([[point.x(), point.y()] for point in points]))
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": [min_y, min_x, width, height],
214
- "centroid": [centroid_x, centroid_y],
233
+ "bbox": bbox,
234
+ "centroid": centroid,
215
235
  "area": area,
216
236
  "perimeter": perimeter,
217
237
  "contour": contour,
218
- "inner contours": [],
219
- "class name": annotation.label.short_label_code,
220
- "instance name": "coral0", # Placeholder, update as needed
221
- "blob name": f"c-0-{centroid_x}x-{centroid_y}y",
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
- # Unpack the annotation data
166
- bbox = annotation['bbox']
167
- centroid = annotation['centroid']
168
- area = annotation['area']
169
- perimeter = annotation['perimeter']
170
- contour = annotation['contour']
171
- inner_contours = annotation['inner contours']
172
- class_name = annotation['class name']
173
- instance_name = annotation['instance name']
174
- blob_name = annotation['blob name']
175
- idx = annotation['id']
176
- note = annotation['note']
177
- data = annotation['data']
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(project_data.get('image_paths'))
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, image_paths):
172
- """Import images from the given paths."""
173
- if not image_paths:
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."""