coralnet-toolbox 0.0.72__py2.py3-none-any.whl → 0.0.73__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 (35) hide show
  1. coralnet_toolbox/AutoDistill/QtDeployModel.py +23 -12
  2. coralnet_toolbox/Explorer/QtDataItem.py +1 -1
  3. coralnet_toolbox/Explorer/QtExplorer.py +143 -3
  4. coralnet_toolbox/Explorer/QtSettingsWidgets.py +46 -4
  5. coralnet_toolbox/MachineLearning/DeployModel/QtDetect.py +22 -11
  6. coralnet_toolbox/MachineLearning/DeployModel/QtSegment.py +22 -10
  7. coralnet_toolbox/MachineLearning/ExportDataset/QtBase.py +61 -24
  8. coralnet_toolbox/MachineLearning/ExportDataset/QtClassify.py +5 -1
  9. coralnet_toolbox/MachineLearning/ExportDataset/QtDetect.py +19 -6
  10. coralnet_toolbox/MachineLearning/ExportDataset/QtSegment.py +21 -8
  11. coralnet_toolbox/QtAnnotationWindow.py +42 -14
  12. coralnet_toolbox/QtEventFilter.py +8 -2
  13. coralnet_toolbox/QtImageWindow.py +17 -18
  14. coralnet_toolbox/QtLabelWindow.py +1 -1
  15. coralnet_toolbox/QtMainWindow.py +143 -8
  16. coralnet_toolbox/Rasters/QtRaster.py +59 -7
  17. coralnet_toolbox/Rasters/RasterTableModel.py +34 -6
  18. coralnet_toolbox/SAM/QtBatchInference.py +0 -2
  19. coralnet_toolbox/SAM/QtDeployGenerator.py +22 -11
  20. coralnet_toolbox/SeeAnything/QtBatchInference.py +19 -221
  21. coralnet_toolbox/SeeAnything/QtDeployGenerator.py +1016 -0
  22. coralnet_toolbox/SeeAnything/QtDeployPredictor.py +69 -53
  23. coralnet_toolbox/SeeAnything/QtTrainModel.py +115 -45
  24. coralnet_toolbox/SeeAnything/__init__.py +2 -0
  25. coralnet_toolbox/Tools/QtSAMTool.py +150 -7
  26. coralnet_toolbox/Tools/QtSeeAnythingTool.py +220 -55
  27. coralnet_toolbox/Tools/QtSelectSubTool.py +6 -4
  28. coralnet_toolbox/Tools/QtWorkAreaTool.py +25 -13
  29. coralnet_toolbox/__init__.py +1 -1
  30. {coralnet_toolbox-0.0.72.dist-info → coralnet_toolbox-0.0.73.dist-info}/METADATA +1 -1
  31. {coralnet_toolbox-0.0.72.dist-info → coralnet_toolbox-0.0.73.dist-info}/RECORD +35 -34
  32. {coralnet_toolbox-0.0.72.dist-info → coralnet_toolbox-0.0.73.dist-info}/WHEEL +0 -0
  33. {coralnet_toolbox-0.0.72.dist-info → coralnet_toolbox-0.0.73.dist-info}/entry_points.txt +0 -0
  34. {coralnet_toolbox-0.0.72.dist-info → coralnet_toolbox-0.0.73.dist-info}/licenses/LICENSE.txt +0 -0
  35. {coralnet_toolbox-0.0.72.dist-info → coralnet_toolbox-0.0.73.dist-info}/top_level.txt +0 -0
@@ -53,6 +53,10 @@ class Detect(Base):
53
53
  self.include_rectangles_checkbox.setEnabled(True) # Enable user to uncheck rectangles if desired
54
54
  self.include_polygons_checkbox.setChecked(True)
55
55
  self.include_polygons_checkbox.setEnabled(True) # Already enabled
56
+
57
+ # Explicitly enable negative sample options for detection
58
+ self.include_negatives_radio.setEnabled(True)
59
+ self.exclude_negatives_radio.setEnabled(True)
56
60
 
57
61
  def create_dataset(self, output_dir_path):
58
62
  """
@@ -106,12 +110,20 @@ class Detect(Base):
106
110
  Process and save detection annotations.
107
111
 
108
112
  Args:
109
- annotations (list): List of annotations.
113
+ annotations (list): List of annotations for this split.
110
114
  split_dir (str): Path to the split directory.
111
115
  split (str): Split name (e.g., "Training", "Validation", "Testing").
112
116
  """
113
- # Get unique image paths
114
- image_paths = list(set(a.image_path for a in annotations))
117
+ # Determine the full list of images for this split (including negatives)
118
+ if split == "Training":
119
+ image_paths = self.train_images
120
+ elif split == "Validation":
121
+ image_paths = self.val_images
122
+ elif split == "Testing":
123
+ image_paths = self.test_images
124
+ else:
125
+ image_paths = []
126
+
115
127
  if not image_paths:
116
128
  return
117
129
 
@@ -124,6 +136,7 @@ class Detect(Base):
124
136
  for image_path in image_paths:
125
137
  yolo_annotations = []
126
138
  image_height, image_width = rasterio_open(image_path).shape
139
+ # Filter the annotations passed to this function to get only those for the current image
127
140
  image_annotations = [a for a in annotations if a.image_path == image_path]
128
141
 
129
142
  for image_annotation in image_annotations:
@@ -132,11 +145,11 @@ class Detect(Base):
132
145
  yolo_annotations.append(f"{class_number} {annotation}")
133
146
 
134
147
  # Save the annotations to a text file
135
- file_ext = image_path.split(".")[-1]
136
- text_file = os.path.basename(image_path).replace(f".{file_ext}", ".txt")
148
+ file_ext = os.path.splitext(image_path)[1]
149
+ text_file = os.path.basename(image_path).replace(file_ext, ".txt")
137
150
  text_path = os.path.join(f"{split_dir}/labels", text_file)
138
151
 
139
- # Write the annotations to the text file
152
+ # Write the annotations to the text file (creates an empty file for negatives)
140
153
  with open(text_path, 'w') as f:
141
154
  for annotation in yolo_annotations:
142
155
  f.write(annotation + '\n')
@@ -53,6 +53,10 @@ class Segment(Base):
53
53
  self.include_rectangles_checkbox.setEnabled(True) # Enable rectangles for segmentation
54
54
  self.include_polygons_checkbox.setChecked(True)
55
55
  self.include_polygons_checkbox.setEnabled(True) # Enable user to uncheck polygons if desired
56
+
57
+ # Explicitly enable negative sample options for segmentation
58
+ self.include_negatives_radio.setEnabled(True)
59
+ self.exclude_negatives_radio.setEnabled(True)
56
60
 
57
61
  def create_dataset(self, output_dir_path):
58
62
  """
@@ -106,12 +110,20 @@ class Segment(Base):
106
110
  Process and save segmentation annotations.
107
111
 
108
112
  Args:
109
- annotations (list): List of annotations.
113
+ annotations (list): List of annotations for this split.
110
114
  split_dir (str): Path to the split directory.
111
115
  split (str): Split name (e.g., "Training", "Validation", "Testing").
112
116
  """
113
- # Get unique image paths
114
- image_paths = list(set(a.image_path for a in annotations))
117
+ # Determine the full list of images for this split (including negatives)
118
+ if split == "Training":
119
+ image_paths = self.train_images
120
+ elif split == "Validation":
121
+ image_paths = self.val_images
122
+ elif split == "Testing":
123
+ image_paths = self.test_images
124
+ else:
125
+ image_paths = []
126
+
115
127
  if not image_paths:
116
128
  return
117
129
 
@@ -124,6 +136,7 @@ class Segment(Base):
124
136
  for image_path in image_paths:
125
137
  yolo_annotations = []
126
138
  image_height, image_width = rasterio_open(image_path).shape
139
+ # Filter the annotations passed to this function to get only those for the current image
127
140
  image_annotations = [a for a in annotations if a.image_path == image_path]
128
141
 
129
142
  for image_annotation in image_annotations:
@@ -132,11 +145,11 @@ class Segment(Base):
132
145
  yolo_annotations.append(f"{class_number} {annotation}")
133
146
 
134
147
  # Save the annotations to a text file
135
- file_ext = image_path.split(".")[-1]
136
- text_file = os.path.basename(image_path).replace(f".{file_ext}", ".txt")
148
+ file_ext = os.path.splitext(image_path)[1]
149
+ text_file = os.path.basename(image_path).replace(file_ext, ".txt")
137
150
  text_path = os.path.join(f"{split_dir}/labels", text_file)
138
151
 
139
- # Write the annotations to the text file
152
+ # Write the annotations to the text file (creates an empty file for negatives)
140
153
  with open(text_path, 'w') as f:
141
154
  for annotation in yolo_annotations:
142
155
  f.write(annotation + '\n')
@@ -146,7 +159,7 @@ class Segment(Base):
146
159
 
147
160
  progress_bar.update_progress()
148
161
 
149
- # Make cursor normal
162
+ # Reset cursor
150
163
  QApplication.restoreOverrideCursor()
151
164
  progress_bar.stop_progress()
152
- progress_bar.close()
165
+ progress_bar.close()
@@ -408,10 +408,6 @@ class AnnotationWindow(QGraphicsView):
408
408
 
409
409
  self.toggle_cursor_annotation()
410
410
 
411
- # Set the image dimensions, and current view in status bar
412
- self.imageLoaded.emit(self.pixmap_image.width(), self.pixmap_image.height())
413
- self.viewChanged.emit(self.pixmap_image.width(), self.pixmap_image.height())
414
-
415
411
  # Load all associated annotations
416
412
  self.load_annotations()
417
413
  # Update the image window's image annotations
@@ -421,6 +417,10 @@ class AnnotationWindow(QGraphicsView):
421
417
 
422
418
  QApplication.processEvents()
423
419
 
420
+ # Set the image dimensions, and current view in status bar
421
+ self.imageLoaded.emit(self.pixmap_image.width(), self.pixmap_image.height())
422
+ self.viewChanged.emit(self.pixmap_image.width(), self.pixmap_image.height())
423
+
424
424
  def update_current_image_path(self, image_path):
425
425
  """Update the current image path being displayed."""
426
426
  self.current_image_path = image_path
@@ -466,29 +466,57 @@ class AnnotationWindow(QGraphicsView):
466
466
  self.centerOn(annotation_center)
467
467
 
468
468
  def center_on_annotation(self, annotation):
469
- """Center and zoom in to focus on the specified annotation."""
469
+ """Center and zoom in to focus on the specified annotation with dynamic padding."""
470
470
  # Create graphics item if it doesn't exist
471
471
  if not annotation.graphics_item:
472
472
  annotation.create_graphics_item(self.scene)
473
473
 
474
474
  # Get the bounding rect of the annotation in scene coordinates
475
475
  annotation_rect = annotation.graphics_item.boundingRect()
476
-
477
- # Add some padding around the annotation (20% on each side)
478
- padding_x = annotation_rect.width() * 0.2
479
- padding_y = annotation_rect.height() * 0.2
476
+
477
+ # Step 1: Calculate annotation and image area
478
+ annotation_area = annotation_rect.width() * annotation_rect.height()
479
+ if self.pixmap_image:
480
+ image_width = self.pixmap_image.width()
481
+ image_height = self.pixmap_image.height()
482
+ else:
483
+ # Fallback to scene rect if image not loaded
484
+ image_width = self.scene.sceneRect().width()
485
+ image_height = self.scene.sceneRect().height()
486
+ image_area = image_width * image_height
487
+
488
+ # Step 2: Compute the relative area ratio (avoid division by zero)
489
+ if image_area > 0:
490
+ relative_area = annotation_area / image_area
491
+ else:
492
+ relative_area = 1.0 # fallback, treat as full image
493
+
494
+ # Step 3: Map ratio to padding factor (smaller annotation = more padding)
495
+ # Example: padding_factor = clamp(0.5 * (1/relative_area)**0.5, 0.1, 0.5)
496
+ # - For very small annotations, padding approaches 0.5 (50%)
497
+ # - For large annotations, padding approaches 0.1 (10%)
498
+ import math
499
+ min_padding = 0.1 # 10%
500
+ max_padding = 0.5 # 50%
501
+ if relative_area > 0:
502
+ padding_factor = max(min(0.5 * (1 / math.sqrt(relative_area)), max_padding), min_padding)
503
+ else:
504
+ padding_factor = min_padding
505
+
506
+ # Step 4: Apply dynamic padding
507
+ padding_x = annotation_rect.width() * padding_factor
508
+ padding_y = annotation_rect.height() * padding_factor
480
509
  padded_rect = annotation_rect.adjusted(-padding_x, -padding_y, padding_x, padding_y)
481
-
510
+
482
511
  # Fit the padded annotation rect in the view
483
512
  self.fitInView(padded_rect, Qt.KeepAspectRatio)
484
-
513
+
485
514
  # Update the zoom factor based on the new view transformation
486
- # We can calculate this by comparing the viewport size to the scene rect size
487
515
  view_rect = self.viewport().rect()
488
- zoom_x = view_rect.width() / padded_rect.width()
516
+ zoom_x = view_rect.width() / padded_rect.width()
489
517
  zoom_y = view_rect.height() / padded_rect.height()
490
518
  self.zoom_factor = min(zoom_x, zoom_y)
491
-
519
+
492
520
  # Signal that the view has changed
493
521
  self.viewChanged.emit(*self.get_image_dimensions())
494
522
 
@@ -23,6 +23,7 @@ class GlobalEventFilter(QObject):
23
23
  self.detect_deploy_model_dialog = main_window.detect_deploy_model_dialog
24
24
  self.segment_deploy_model_dialog = main_window.segment_deploy_model_dialog
25
25
  self.sam_deploy_generator_dialog = main_window.sam_deploy_generator_dialog
26
+ self.see_anything_deploy_generator_dialog = main_window.see_anything_deploy_generator_dialog
26
27
  self.auto_distill_deploy_model_dialog = main_window.auto_distill_deploy_model_dialog
27
28
 
28
29
  def eventFilter(self, obj, event):
@@ -69,9 +70,14 @@ class GlobalEventFilter(QObject):
69
70
  if event.key() == Qt.Key_4:
70
71
  self.sam_deploy_generator_dialog.predict()
71
72
  return True
72
-
73
- # Handle hotkey for auto distill prediction
73
+
74
+ # Handle hotkey for see anything (YOLOE) generator
74
75
  if event.key() == Qt.Key_5:
76
+ self.see_anything_deploy_generator_dialog.predict()
77
+ return True
78
+
79
+ # Handle hotkey for auto distill prediction
80
+ if event.key() == Qt.Key_6:
75
81
  self.auto_distill_deploy_model_dialog.predict()
76
82
  return True
77
83
 
@@ -562,7 +562,7 @@ class ImageWindow(QWidget):
562
562
 
563
563
  except Exception as e:
564
564
  self.show_error("Image Loading Error",
565
- f"Error loading image {os.path.basename(image_path)}:\n{str(e)}")
565
+ f"Error loading image {os.path.basename(image_path)}:\n{str(e)}")
566
566
  return False
567
567
 
568
568
  @property
@@ -1182,26 +1182,25 @@ class ImagePreviewTooltip(QFrame):
1182
1182
  self.hide()
1183
1183
 
1184
1184
  def show_at(self, global_pos):
1185
- """
1186
- Position and show the tooltip at the specified global position,
1187
- always placing it to the bottom-right of the cursor.
1188
-
1189
- Args:
1190
- global_pos (QPoint): Position to show the tooltip
1191
- """
1192
- # Always position to bottom-right of cursor with fixed offset
1193
- x, y = global_pos.x() + 25, global_pos.y() + 25
1194
-
1195
- # Ensure tooltip stays within screen boundaries
1196
- screen_rect = self.screen().geometry()
1185
+ """Position and show the tooltip at the specified global position."""
1186
+ # Position tooltip to bottom-right of cursor
1187
+ x, y = global_pos.x() + 15, global_pos.y() + 15
1188
+
1189
+ # Get the screen that contains the cursor position
1190
+ screen = QApplication.screenAt(global_pos)
1191
+ if not screen:
1192
+ screen = QApplication.primaryScreen()
1193
+
1194
+ # Get screen geometry and tooltip size
1195
+ screen_rect = screen.geometry()
1197
1196
  tooltip_size = self.sizeHint()
1198
-
1199
- # Adjust position if needed to stay on screen
1197
+
1198
+ # Adjust position to stay on screen
1200
1199
  if x + tooltip_size.width() > screen_rect.right():
1201
- x = screen_rect.right() - tooltip_size.width() - 10
1200
+ x = global_pos.x() - tooltip_size.width() - 15
1202
1201
  if y + tooltip_size.height() > screen_rect.bottom():
1203
- y = screen_rect.bottom() - tooltip_size.height() - 10
1204
-
1202
+ y = global_pos.y() - tooltip_size.height() - 15
1203
+
1205
1204
  # Set position and show
1206
1205
  self.move(x, y)
1207
1206
  self.show()
@@ -280,7 +280,7 @@ class LabelWindow(QWidget):
280
280
 
281
281
  self.edit_label_button = QPushButton()
282
282
  self.edit_label_button.setIcon(self.main_window.edit_icon)
283
- self.edit_label_button.setToolTip("Edit Label")
283
+ self.edit_label_button.setToolTip("Edit Label / Merge Labels")
284
284
  self.edit_label_button.setFixedSize(self.label_width, self.label_height)
285
285
  self.edit_label_button.setEnabled(False) # Initially disabled
286
286
  self.top_bar.addWidget(self.edit_label_button)
@@ -9,8 +9,8 @@ import requests
9
9
 
10
10
  from packaging import version
11
11
 
12
- from PyQt5.QtCore import Qt, pyqtSignal, QEvent, QSize, QPoint
13
12
  from PyQt5.QtGui import QIcon, QMouseEvent
13
+ from PyQt5.QtCore import Qt, pyqtSignal, QEvent, QSize, QPoint
14
14
  from PyQt5.QtWidgets import (QListWidget, QCheckBox, QFrame, QComboBox)
15
15
  from PyQt5.QtWidgets import (QMainWindow, QApplication, QToolBar, QAction, QSizePolicy,
16
16
  QMessageBox, QWidget, QVBoxLayout, QLabel, QHBoxLayout,
@@ -95,6 +95,7 @@ from coralnet_toolbox.SAM import (
95
95
  from coralnet_toolbox.SeeAnything import (
96
96
  TrainModelDialog as SeeAnythingTrainModelDialog,
97
97
  DeployPredictorDialog as SeeAnythingDeployPredictorDialog,
98
+ DeployGeneratorDialog as SeeAnythingDeployGeneratorDialog,
98
99
  BatchInferenceDialog as SeeAnythingBatchInferenceDialog
99
100
  )
100
101
 
@@ -258,6 +259,7 @@ class MainWindow(QMainWindow):
258
259
  # Create dialogs (See Anything)
259
260
  self.see_anything_train_model_dialog = SeeAnythingTrainModelDialog(self)
260
261
  self.see_anything_deploy_predictor_dialog = SeeAnythingDeployPredictorDialog(self)
262
+ self.see_anything_deploy_generator_dialog = SeeAnythingDeployGeneratorDialog(self)
261
263
  self.see_anything_batch_inference_dialog = SeeAnythingBatchInferenceDialog(self)
262
264
 
263
265
  # Create dialogs (AutoDistill)
@@ -622,11 +624,17 @@ class MainWindow(QMainWindow):
622
624
  # Train Model
623
625
  self.see_anything_train_model_action = QAction("Train Model", self)
624
626
  self.see_anything_train_model_action.triggered.connect(self.open_see_anything_train_model_dialog)
625
- self.see_anything_menu.addAction(self.see_anything_train_model_action)
626
- # Deploy Model
627
+ # self.see_anything_menu.addAction(self.see_anything_train_model_action) TODO Doesn't work
628
+ # Deploy Model submenu
629
+ self.see_anything_deploy_model_menu = self.see_anything_menu.addMenu("Deploy Model")
630
+ # Deploy Predictor
627
631
  self.see_anything_deploy_predictor_action = QAction("Deploy Predictor", self)
628
632
  self.see_anything_deploy_predictor_action.triggered.connect(self.open_see_anything_deploy_predictor_dialog)
629
- self.see_anything_menu.addAction(self.see_anything_deploy_predictor_action)
633
+ self.see_anything_deploy_model_menu.addAction(self.see_anything_deploy_predictor_action)
634
+ # Deploy Generator
635
+ self.see_anything_deploy_generator_action = QAction("Deploy Generator", self)
636
+ self.see_anything_deploy_generator_action.triggered.connect(self.open_see_anything_deploy_generator_dialog)
637
+ self.see_anything_deploy_model_menu.addAction(self.see_anything_deploy_generator_action)
630
638
  # Batch Inference
631
639
  self.see_anything_batch_inference_action = QAction("Batch Inference", self)
632
640
  self.see_anything_batch_inference_action.triggered.connect(self.open_see_anything_batch_inference_dialog)
@@ -674,6 +682,76 @@ class MainWindow(QMainWindow):
674
682
  # ----------------------------------------
675
683
  # Create and add the toolbar
676
684
  # ----------------------------------------
685
+
686
+ # Define verbose tool descriptions
687
+ self.tool_descriptions = {
688
+ "select": ("Select Tool\n\n"
689
+ "Select, modify, and manage annotations.\n"
690
+ "• Left-click to select annotations; hold Ctrl+left-click to select multiple.\n"
691
+ "• Left-click and drag to move selected annotations.\n"
692
+ "• Ctrl+click and drag to create a selection rectangle.\n"
693
+ "• Ctrl+Shift to show resize handles for a selected Rectangle and Polygon annotations.\n"
694
+ "• Ctrl+X to cut a selected annotation along a drawn line.\n"
695
+ "• Ctrl+C to combine multiple selected annotations.\n"
696
+ "• Ctrl+Space to confirm selected annotations with top predictions.\n"
697
+ "• Ctrl+Shift+mouse wheel to adjust polygon complexity.\n"
698
+ "• Ctrl+Delete to remove selected annotations."),
699
+
700
+ "patch": ("Patch Tool\n\n"
701
+ "Create point (patch) annotations centered at the cursor.\n"
702
+ "• Left-click to place a patch at the mouse location.\n"
703
+ "• Hold Ctrl and use the mouse wheel or use the Patch Size box to adjust patch size.\n"
704
+ "• A semi-transparent preview shows the patch before placing it."),
705
+
706
+ "rectangle": ("Rectangle Tool\n\n"
707
+ "Create rectangular annotations by clicking and dragging.\n"
708
+ "• Left-click to set the first corner, then move the mouse to size the rectangle.\n"
709
+ "• Left-click again to place the rectangle.\n"
710
+ "• Press Backspace to cancel drawing the current rectangle.\n"
711
+ "• A semi-transparent preview shows the rectangle while drawing."),
712
+
713
+ "polygon": ("Polygon Tool\n\n"
714
+ "Create polygon annotations with multiple vertices.\n"
715
+ "• Left-click to set the first vertex, then move the mouse to draw the polygon\n"
716
+ "• Hold Ctrl while left-clicking to draw straight-line segments.\n"
717
+ "• Left-click again to complete the polygon.\n"
718
+ "• Press Backspace to cancel the current polygon.\n"
719
+ "• A semi-transparent preview shows the polygon while drawing."),
720
+
721
+ "sam": ("Segment Anything (SAM) Tool\n\n"
722
+ "Generates AI-powered segmentations.\n"
723
+ "• Left-click to create a working area, then left-click again to confirm.\n"
724
+ "\t• Or, press Spacebar to create a working area for the current view.\n"
725
+ "• Ctrl+Left-click to add positive points (foreground).\n"
726
+ "• Ctrl+Right-click to add negative points (background).\n"
727
+ "• Left-click and drag to create a bounding box for prompting.\n"
728
+ "• Press Spacebar to generate and confirm the segmentation.\n"
729
+ "• Press Backspace to cancel the current operation.\n"
730
+ "• Uncertainty can be adjusted in Parameters section.\n"
731
+ "• A SAM predictor must be deployed first."),
732
+
733
+ "see_anything": ("See Anything (YOLOE) Tool\n\n"
734
+ "Uses YOLOE to detect / segments objects of interest based on visual prompts.\n"
735
+ "• Left-click to create a working area, then click again to confirm.\n"
736
+ "\t• Or, press Spacebar to create a working area for the current view.\n"
737
+ "• Draw rectangles inside the working area to guide detection.\n"
738
+ "• Press Spacebar to generate detections using drawn rectangles.\n"
739
+ "• Press Spacebar again to confirm annotations or apply SAM refinement.\n"
740
+ "• Press Backspace to cancel current operation or clear annotations.\n"
741
+ "• Uncertainty can be adjusted in Parameters section.\n"
742
+ "• A See Anything (YOLOE) predictor must be deployed first."),
743
+
744
+ "work_area": ("Work Area Tool\n\n"
745
+ "Defines regions for detection and segmentation models to run predictions on.\n"
746
+ "• Left-click to create a working area, then left-click again to confirm.\n"
747
+ "\t• Or, press Spacebar to create a work area from the current view.\n"
748
+ "• Hold Ctrl+Shift to show delete buttons for existing work areas.\n"
749
+ "• Press Ctrl+Shift+Backspace to clear all work areas.\n"
750
+ "• Hold Ctrl+Alt to temporarily view a work area of the current view.\n"
751
+ "• Work areas can be used with Tile Batch Inference and other batch operations.\n"
752
+ "• All work areas are automatically saved with the image in a Project (JSON) file.")
753
+ }
754
+
677
755
  self.toolbar = QToolBar("Tools", self)
678
756
  self.toolbar.setOrientation(Qt.Vertical)
679
757
  self.toolbar.setFixedWidth(40)
@@ -696,6 +774,7 @@ class MainWindow(QMainWindow):
696
774
  # Add tools here with icons
697
775
  self.select_tool_action = QAction(self.select_icon, "Select", self)
698
776
  self.select_tool_action.setCheckable(True)
777
+ self.select_tool_action.setToolTip(self.tool_descriptions["select"])
699
778
  self.select_tool_action.triggered.connect(self.toggle_tool)
700
779
  self.toolbar.addAction(self.select_tool_action)
701
780
 
@@ -703,16 +782,19 @@ class MainWindow(QMainWindow):
703
782
 
704
783
  self.patch_tool_action = QAction(self.patch_icon, "Patch", self)
705
784
  self.patch_tool_action.setCheckable(True)
785
+ self.patch_tool_action.setToolTip(self.tool_descriptions["patch"])
706
786
  self.patch_tool_action.triggered.connect(self.toggle_tool)
707
787
  self.toolbar.addAction(self.patch_tool_action)
708
788
 
709
789
  self.rectangle_tool_action = QAction(self.rectangle_icon, "Rectangle", self)
710
790
  self.rectangle_tool_action.setCheckable(True)
791
+ self.rectangle_tool_action.setToolTip(self.tool_descriptions["rectangle"])
711
792
  self.rectangle_tool_action.triggered.connect(self.toggle_tool)
712
793
  self.toolbar.addAction(self.rectangle_tool_action)
713
794
 
714
795
  self.polygon_tool_action = QAction(self.polygon_icon, "Polygon", self)
715
796
  self.polygon_tool_action.setCheckable(True)
797
+ self.polygon_tool_action.setToolTip(self.tool_descriptions["polygon"])
716
798
  self.polygon_tool_action.triggered.connect(self.toggle_tool)
717
799
  self.toolbar.addAction(self.polygon_tool_action)
718
800
 
@@ -720,11 +802,13 @@ class MainWindow(QMainWindow):
720
802
 
721
803
  self.sam_tool_action = QAction(self.sam_icon, "SAM", self)
722
804
  self.sam_tool_action.setCheckable(True)
805
+ self.sam_tool_action.setToolTip(self.tool_descriptions["sam"])
723
806
  self.sam_tool_action.triggered.connect(self.toggle_tool)
724
807
  self.toolbar.addAction(self.sam_tool_action)
725
808
 
726
809
  self.see_anything_tool_action = QAction(self.see_anything_icon, "See Anything (YOLOE)", self)
727
810
  self.see_anything_tool_action.setCheckable(True)
811
+ self.see_anything_tool_action.setToolTip(self.tool_descriptions["see_anything"])
728
812
  self.see_anything_tool_action.triggered.connect(self.toggle_tool)
729
813
  self.toolbar.addAction(self.see_anything_tool_action)
730
814
 
@@ -732,6 +816,7 @@ class MainWindow(QMainWindow):
732
816
 
733
817
  self.work_area_tool_action = QAction(self.workarea_icon, "Work Area", self)
734
818
  self.work_area_tool_action.setCheckable(True)
819
+ self.work_area_tool_action.setToolTip(self.tool_descriptions["work_area"])
735
820
  self.work_area_tool_action.triggered.connect(self.toggle_tool)
736
821
  self.toolbar.addAction(self.work_area_tool_action)
737
822
 
@@ -2196,6 +2281,50 @@ class MainWindow(QMainWindow):
2196
2281
  self.see_anything_deploy_predictor_dialog.exec_()
2197
2282
  except Exception as e:
2198
2283
  QMessageBox.critical(self, "Critical Error", f"{e}")
2284
+
2285
+ def open_see_anything_deploy_generator_dialog(self):
2286
+ """Open the See Anything Deploy Generator dialog to deploy a See Anything generator."""
2287
+ if not self.image_window.raster_manager.image_paths:
2288
+ QMessageBox.warning(self,
2289
+ "See Anything (YOLOE)",
2290
+ "No images are present in the project.")
2291
+ return
2292
+
2293
+ if not self.annotation_window.annotations_dict:
2294
+ QMessageBox.warning(self,
2295
+ "See Anything (YOLOE)",
2296
+ "No annotations are present in the project.")
2297
+ return
2298
+
2299
+ valid_reference_types = {"PolygonAnnotation", "RectangleAnnotation"}
2300
+ has_valid_reference = False
2301
+
2302
+ # Iterate through the rasters in the main manager.
2303
+ for raster in self.image_window.raster_manager.rasters.values():
2304
+ # The values of our map are sets of annotation type names.
2305
+ # e.g., [{'PointAnnotation'}, {'PolygonAnnotation', 'RectangleAnnotation'}]
2306
+ for types_for_a_label in raster.label_to_types_map.values():
2307
+ # Check if the set of types for this specific label
2308
+ # has any overlap with our valid reference types.
2309
+ if not valid_reference_types.isdisjoint(types_for_a_label):
2310
+ # A valid reference type was found for at least one label on this raster.
2311
+ has_valid_reference = True
2312
+ break # Exit the inner loop (over types)
2313
+
2314
+ if has_valid_reference:
2315
+ break # Exit the outer loop (over rasters)
2316
+
2317
+ if not has_valid_reference:
2318
+ QMessageBox.warning(self,
2319
+ "No Valid Reference Annotations",
2320
+ "No images have polygon or rectangle annotations to use as a reference.")
2321
+ return
2322
+
2323
+ try:
2324
+ self.untoggle_all_tools()
2325
+ self.see_anything_deploy_generator_dialog.exec_()
2326
+ except Exception as e:
2327
+ QMessageBox.critical(self, "Critical Error", f"An error occurred: {e}")
2199
2328
 
2200
2329
  def open_see_anything_batch_inference_dialog(self):
2201
2330
  """Open the See Anything Batch Inference dialog to run batch inference with See Anything."""
@@ -2205,16 +2334,22 @@ class MainWindow(QMainWindow):
2205
2334
  "No images are present in the project.")
2206
2335
  return
2207
2336
 
2208
- if not self.see_anything_deploy_predictor_dialog.loaded_model:
2337
+ if not self.see_anything_deploy_generator_dialog.loaded_model:
2209
2338
  QMessageBox.warning(self,
2210
2339
  "See Anything (YOLOE) Batch Inference",
2211
- "Please deploy a model before running batch inference.")
2340
+ "Please deploy a generator before running batch inference.")
2341
+ return
2342
+
2343
+ # Check if there are any annotations
2344
+ if not self.annotation_window.annotations_dict:
2345
+ QMessageBox.warning(self,
2346
+ "See Anything (YOLOE)",
2347
+ "Cannot run See Anything (YOLOE) without reference annotations in the project.")
2212
2348
  return
2213
2349
 
2214
2350
  try:
2215
2351
  self.untoggle_all_tools()
2216
- if self.see_anything_batch_inference_dialog.has_valid_sources():
2217
- self.see_anything_batch_inference_dialog.exec_()
2352
+ self.see_anything_batch_inference_dialog.exec_()
2218
2353
  except Exception as e:
2219
2354
  QMessageBox.critical(self, "Critical Error", f"{e}")
2220
2355
 
@@ -2,6 +2,7 @@ import warnings
2
2
 
3
3
  import os
4
4
  import gc
5
+ from collections import defaultdict
5
6
  from typing import Optional, Set, List
6
7
 
7
8
  import cv2
@@ -13,7 +14,6 @@ from PyQt5.QtCore import QObject
13
14
 
14
15
  from coralnet_toolbox.utilities import rasterio_open
15
16
  from coralnet_toolbox.utilities import rasterio_to_qimage
16
- from coralnet_toolbox.utilities import rasterio_to_cropped_image
17
17
  from coralnet_toolbox.utilities import work_area_to_numpy
18
18
  from coralnet_toolbox.utilities import pixmap_to_numpy
19
19
 
@@ -67,6 +67,10 @@ class Raster(QObject):
67
67
  self.labels: Set = set()
68
68
  self.annotation_count = 0
69
69
  self.annotations: List = [] # Store the actual annotations
70
+ self.label_counts = {} # Store counts of annotations per label
71
+
72
+ self.label_set: Set[str] = set() # Add sets for efficient lookups
73
+ self.label_to_types_map = {} # This replaces annotation_types and annotation_type_set
70
74
 
71
75
  # Work Area state
72
76
  self.work_areas: List = [] # Store work area information
@@ -238,6 +242,7 @@ class Raster(QObject):
238
242
  def update_annotation_info(self, annotations: list):
239
243
  """
240
244
  Update annotation-related information for this raster.
245
+ This now builds a more powerful cache mapping labels to their annotation types.
241
246
 
242
247
  Args:
243
248
  annotations (list): List of annotation objects
@@ -246,13 +251,59 @@ class Raster(QObject):
246
251
  self.annotation_count = len(annotations)
247
252
  self.has_annotations = bool(annotations)
248
253
 
249
- # Check for predictions
250
- predictions = [a.machine_confidence for a in annotations if a.machine_confidence != {}]
254
+ predictions = [a.machine_confidence for a in annotations if a.machine_confidence]
251
255
  self.has_predictions = len(predictions) > 0
252
256
 
253
- # Update labels
254
- self.labels = {annotation.label for annotation in annotations if annotation.label}
257
+ # Clear previous data
258
+ self.label_counts.clear()
259
+ self.label_set.clear()
260
+ self.label_to_types_map.clear() # Clear the new map
261
+
262
+ # Use a defaultdict to simplify the aggregation logic
263
+ temp_map = defaultdict(set)
264
+
265
+ for annotation in annotations:
266
+ # Process label information
267
+ if annotation.label:
268
+ if hasattr(annotation.label, 'short_label_code'):
269
+ label_name = annotation.label.short_label_code
270
+ else:
271
+ label_name = str(annotation.label)
272
+
273
+ # Update label counts and the set of all labels
274
+ self.label_counts[label_name] = self.label_counts.get(label_name, 0) + 1
275
+ self.label_set.add(label_name)
276
+
277
+ # Process annotation type information and link it to the label
278
+ anno_type = annotation.__class__.__name__
279
+ temp_map[label_name].add(anno_type)
280
+
281
+ # Convert defaultdict back to a regular dict for the final attribute
282
+ self.label_to_types_map = dict(temp_map)
255
283
 
284
+ @property
285
+ def annotation_types(self) -> dict:
286
+ """
287
+ Computes a simple count of each annotation type on-the-fly.
288
+ This property provides backward compatibility for features like the tooltip
289
+ without needing to store this data permanently.
290
+
291
+ Returns:
292
+ dict: A dictionary mapping annotation type names to their counts.
293
+ e.g., {'PolygonAnnotation': 5, 'PointAnnotation': 2}
294
+ """
295
+ type_counts = defaultdict(int)
296
+ # The self.label_to_types_map structure is {'label': {'type1', 'type2'}}
297
+ # This is not ideal for counting total types. We need the original annotations list.
298
+ if not self.annotations:
299
+ return {}
300
+
301
+ for annotation in self.annotations:
302
+ anno_type = annotation.__class__.__name__
303
+ type_counts[anno_type] += 1
304
+
305
+ return dict(type_counts)
306
+
256
307
  def matches_filter(self,
257
308
  search_text="",
258
309
  search_label="",
@@ -283,8 +334,9 @@ class Raster(QObject):
283
334
  label_match = False
284
335
 
285
336
  # Check actual annotation labels (always consider these)
286
- for label in self.labels:
287
- if hasattr(label, 'short_label_code') and search_label in label.short_label_code:
337
+ # Look for the search label in the label_set instead of self.labels
338
+ for label_code in self.label_set:
339
+ if search_label in label_code:
288
340
  label_match = True
289
341
  break
290
342