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.
- coralnet_toolbox/AutoDistill/QtDeployModel.py +23 -12
- coralnet_toolbox/Explorer/QtDataItem.py +1 -1
- coralnet_toolbox/Explorer/QtExplorer.py +143 -3
- coralnet_toolbox/Explorer/QtSettingsWidgets.py +46 -4
- coralnet_toolbox/MachineLearning/DeployModel/QtDetect.py +22 -11
- coralnet_toolbox/MachineLearning/DeployModel/QtSegment.py +22 -10
- coralnet_toolbox/MachineLearning/ExportDataset/QtBase.py +61 -24
- coralnet_toolbox/MachineLearning/ExportDataset/QtClassify.py +5 -1
- coralnet_toolbox/MachineLearning/ExportDataset/QtDetect.py +19 -6
- coralnet_toolbox/MachineLearning/ExportDataset/QtSegment.py +21 -8
- coralnet_toolbox/QtAnnotationWindow.py +42 -14
- coralnet_toolbox/QtEventFilter.py +8 -2
- coralnet_toolbox/QtImageWindow.py +17 -18
- coralnet_toolbox/QtLabelWindow.py +1 -1
- coralnet_toolbox/QtMainWindow.py +143 -8
- coralnet_toolbox/Rasters/QtRaster.py +59 -7
- coralnet_toolbox/Rasters/RasterTableModel.py +34 -6
- coralnet_toolbox/SAM/QtBatchInference.py +0 -2
- coralnet_toolbox/SAM/QtDeployGenerator.py +22 -11
- coralnet_toolbox/SeeAnything/QtBatchInference.py +19 -221
- coralnet_toolbox/SeeAnything/QtDeployGenerator.py +1016 -0
- coralnet_toolbox/SeeAnything/QtDeployPredictor.py +69 -53
- coralnet_toolbox/SeeAnything/QtTrainModel.py +115 -45
- coralnet_toolbox/SeeAnything/__init__.py +2 -0
- coralnet_toolbox/Tools/QtSAMTool.py +150 -7
- coralnet_toolbox/Tools/QtSeeAnythingTool.py +220 -55
- coralnet_toolbox/Tools/QtSelectSubTool.py +6 -4
- coralnet_toolbox/Tools/QtWorkAreaTool.py +25 -13
- coralnet_toolbox/__init__.py +1 -1
- {coralnet_toolbox-0.0.72.dist-info → coralnet_toolbox-0.0.73.dist-info}/METADATA +1 -1
- {coralnet_toolbox-0.0.72.dist-info → coralnet_toolbox-0.0.73.dist-info}/RECORD +35 -34
- {coralnet_toolbox-0.0.72.dist-info → coralnet_toolbox-0.0.73.dist-info}/WHEEL +0 -0
- {coralnet_toolbox-0.0.72.dist-info → coralnet_toolbox-0.0.73.dist-info}/entry_points.txt +0 -0
- {coralnet_toolbox-0.0.72.dist-info → coralnet_toolbox-0.0.73.dist-info}/licenses/LICENSE.txt +0 -0
- {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
|
-
#
|
114
|
-
|
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 =
|
136
|
-
text_file = os.path.basename(image_path).replace(
|
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
|
-
#
|
114
|
-
|
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 =
|
136
|
-
text_file = os.path.basename(image_path).replace(
|
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
|
-
#
|
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
|
-
#
|
478
|
-
|
479
|
-
|
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
|
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
|
-
|
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
|
1187
|
-
|
1188
|
-
|
1189
|
-
|
1190
|
-
|
1191
|
-
|
1192
|
-
|
1193
|
-
|
1194
|
-
|
1195
|
-
|
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
|
1197
|
+
|
1198
|
+
# Adjust position to stay on screen
|
1200
1199
|
if x + tooltip_size.width() > screen_rect.right():
|
1201
|
-
x =
|
1200
|
+
x = global_pos.x() - tooltip_size.width() - 15
|
1202
1201
|
if y + tooltip_size.height() > screen_rect.bottom():
|
1203
|
-
y =
|
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)
|
coralnet_toolbox/QtMainWindow.py
CHANGED
@@ -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.
|
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.
|
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
|
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
|
-
|
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
|
-
|
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
|
-
#
|
254
|
-
self.
|
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
|
-
|
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
|
|