coralnet-toolbox 0.0.71__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/Annotations/QtRectangleAnnotation.py +31 -2
- coralnet_toolbox/AutoDistill/QtDeployModel.py +23 -12
- coralnet_toolbox/Explorer/QtDataItem.py +53 -21
- coralnet_toolbox/Explorer/QtExplorer.py +581 -276
- coralnet_toolbox/Explorer/QtFeatureStore.py +15 -0
- coralnet_toolbox/Explorer/QtSettingsWidgets.py +49 -7
- 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 +52 -16
- coralnet_toolbox/QtEventFilter.py +8 -2
- coralnet_toolbox/QtImageWindow.py +17 -18
- coralnet_toolbox/QtLabelWindow.py +1 -1
- coralnet_toolbox/QtMainWindow.py +203 -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/QtResizeSubTool.py +6 -1
- coralnet_toolbox/Tools/QtSAMTool.py +150 -7
- coralnet_toolbox/Tools/QtSeeAnythingTool.py +220 -55
- coralnet_toolbox/Tools/QtSelectSubTool.py +6 -4
- coralnet_toolbox/Tools/QtSelectTool.py +48 -6
- coralnet_toolbox/Tools/QtWorkAreaTool.py +25 -13
- coralnet_toolbox/__init__.py +1 -1
- {coralnet_toolbox-0.0.71.dist-info → coralnet_toolbox-0.0.73.dist-info}/METADATA +1 -1
- {coralnet_toolbox-0.0.71.dist-info → coralnet_toolbox-0.0.73.dist-info}/RECORD +39 -38
- {coralnet_toolbox-0.0.71.dist-info → coralnet_toolbox-0.0.73.dist-info}/WHEEL +0 -0
- {coralnet_toolbox-0.0.71.dist-info → coralnet_toolbox-0.0.73.dist-info}/entry_points.txt +0 -0
- {coralnet_toolbox-0.0.71.dist-info → coralnet_toolbox-0.0.73.dist-info}/licenses/LICENSE.txt +0 -0
- {coralnet_toolbox-0.0.71.dist-info → coralnet_toolbox-0.0.73.dist-info}/top_level.txt +0 -0
@@ -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
@@ -4,12 +4,13 @@ warnings.filterwarnings("ignore", category=DeprecationWarning)
|
|
4
4
|
|
5
5
|
import os
|
6
6
|
import re
|
7
|
+
import ctypes
|
7
8
|
import requests
|
8
9
|
|
9
10
|
from packaging import version
|
10
11
|
|
11
|
-
from PyQt5.QtCore import Qt, pyqtSignal, QEvent, QSize, QPoint
|
12
12
|
from PyQt5.QtGui import QIcon, QMouseEvent
|
13
|
+
from PyQt5.QtCore import Qt, pyqtSignal, QEvent, QSize, QPoint
|
13
14
|
from PyQt5.QtWidgets import (QListWidget, QCheckBox, QFrame, QComboBox)
|
14
15
|
from PyQt5.QtWidgets import (QMainWindow, QApplication, QToolBar, QAction, QSizePolicy,
|
15
16
|
QMessageBox, QWidget, QVBoxLayout, QLabel, QHBoxLayout,
|
@@ -94,6 +95,7 @@ from coralnet_toolbox.SAM import (
|
|
94
95
|
from coralnet_toolbox.SeeAnything import (
|
95
96
|
TrainModelDialog as SeeAnythingTrainModelDialog,
|
96
97
|
DeployPredictorDialog as SeeAnythingDeployPredictorDialog,
|
98
|
+
DeployGeneratorDialog as SeeAnythingDeployGeneratorDialog,
|
97
99
|
BatchInferenceDialog as SeeAnythingBatchInferenceDialog
|
98
100
|
)
|
99
101
|
|
@@ -130,6 +132,9 @@ class MainWindow(QMainWindow):
|
|
130
132
|
|
131
133
|
def __init__(self, __version__):
|
132
134
|
super().__init__()
|
135
|
+
|
136
|
+
# Get the process ID
|
137
|
+
self.pid = os.getpid()
|
133
138
|
|
134
139
|
# Define icons
|
135
140
|
self.coral_icon = get_icon("coral.png")
|
@@ -254,6 +259,7 @@ class MainWindow(QMainWindow):
|
|
254
259
|
# Create dialogs (See Anything)
|
255
260
|
self.see_anything_train_model_dialog = SeeAnythingTrainModelDialog(self)
|
256
261
|
self.see_anything_deploy_predictor_dialog = SeeAnythingDeployPredictorDialog(self)
|
262
|
+
self.see_anything_deploy_generator_dialog = SeeAnythingDeployGeneratorDialog(self)
|
257
263
|
self.see_anything_batch_inference_dialog = SeeAnythingBatchInferenceDialog(self)
|
258
264
|
|
259
265
|
# Create dialogs (AutoDistill)
|
@@ -618,11 +624,17 @@ class MainWindow(QMainWindow):
|
|
618
624
|
# Train Model
|
619
625
|
self.see_anything_train_model_action = QAction("Train Model", self)
|
620
626
|
self.see_anything_train_model_action.triggered.connect(self.open_see_anything_train_model_dialog)
|
621
|
-
self.see_anything_menu.addAction(self.see_anything_train_model_action)
|
622
|
-
# 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
|
623
631
|
self.see_anything_deploy_predictor_action = QAction("Deploy Predictor", self)
|
624
632
|
self.see_anything_deploy_predictor_action.triggered.connect(self.open_see_anything_deploy_predictor_dialog)
|
625
|
-
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)
|
626
638
|
# Batch Inference
|
627
639
|
self.see_anything_batch_inference_action = QAction("Batch Inference", self)
|
628
640
|
self.see_anything_batch_inference_action.triggered.connect(self.open_see_anything_batch_inference_dialog)
|
@@ -670,6 +682,76 @@ class MainWindow(QMainWindow):
|
|
670
682
|
# ----------------------------------------
|
671
683
|
# Create and add the toolbar
|
672
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
|
+
|
673
755
|
self.toolbar = QToolBar("Tools", self)
|
674
756
|
self.toolbar.setOrientation(Qt.Vertical)
|
675
757
|
self.toolbar.setFixedWidth(40)
|
@@ -692,6 +774,7 @@ class MainWindow(QMainWindow):
|
|
692
774
|
# Add tools here with icons
|
693
775
|
self.select_tool_action = QAction(self.select_icon, "Select", self)
|
694
776
|
self.select_tool_action.setCheckable(True)
|
777
|
+
self.select_tool_action.setToolTip(self.tool_descriptions["select"])
|
695
778
|
self.select_tool_action.triggered.connect(self.toggle_tool)
|
696
779
|
self.toolbar.addAction(self.select_tool_action)
|
697
780
|
|
@@ -699,16 +782,19 @@ class MainWindow(QMainWindow):
|
|
699
782
|
|
700
783
|
self.patch_tool_action = QAction(self.patch_icon, "Patch", self)
|
701
784
|
self.patch_tool_action.setCheckable(True)
|
785
|
+
self.patch_tool_action.setToolTip(self.tool_descriptions["patch"])
|
702
786
|
self.patch_tool_action.triggered.connect(self.toggle_tool)
|
703
787
|
self.toolbar.addAction(self.patch_tool_action)
|
704
788
|
|
705
789
|
self.rectangle_tool_action = QAction(self.rectangle_icon, "Rectangle", self)
|
706
790
|
self.rectangle_tool_action.setCheckable(True)
|
791
|
+
self.rectangle_tool_action.setToolTip(self.tool_descriptions["rectangle"])
|
707
792
|
self.rectangle_tool_action.triggered.connect(self.toggle_tool)
|
708
793
|
self.toolbar.addAction(self.rectangle_tool_action)
|
709
794
|
|
710
795
|
self.polygon_tool_action = QAction(self.polygon_icon, "Polygon", self)
|
711
796
|
self.polygon_tool_action.setCheckable(True)
|
797
|
+
self.polygon_tool_action.setToolTip(self.tool_descriptions["polygon"])
|
712
798
|
self.polygon_tool_action.triggered.connect(self.toggle_tool)
|
713
799
|
self.toolbar.addAction(self.polygon_tool_action)
|
714
800
|
|
@@ -716,11 +802,13 @@ class MainWindow(QMainWindow):
|
|
716
802
|
|
717
803
|
self.sam_tool_action = QAction(self.sam_icon, "SAM", self)
|
718
804
|
self.sam_tool_action.setCheckable(True)
|
805
|
+
self.sam_tool_action.setToolTip(self.tool_descriptions["sam"])
|
719
806
|
self.sam_tool_action.triggered.connect(self.toggle_tool)
|
720
807
|
self.toolbar.addAction(self.sam_tool_action)
|
721
808
|
|
722
809
|
self.see_anything_tool_action = QAction(self.see_anything_icon, "See Anything (YOLOE)", self)
|
723
810
|
self.see_anything_tool_action.setCheckable(True)
|
811
|
+
self.see_anything_tool_action.setToolTip(self.tool_descriptions["see_anything"])
|
724
812
|
self.see_anything_tool_action.triggered.connect(self.toggle_tool)
|
725
813
|
self.toolbar.addAction(self.see_anything_tool_action)
|
726
814
|
|
@@ -728,6 +816,7 @@ class MainWindow(QMainWindow):
|
|
728
816
|
|
729
817
|
self.work_area_tool_action = QAction(self.workarea_icon, "Work Area", self)
|
730
818
|
self.work_area_tool_action.setCheckable(True)
|
819
|
+
self.work_area_tool_action.setToolTip(self.tool_descriptions["work_area"])
|
731
820
|
self.work_area_tool_action.triggered.connect(self.toggle_tool)
|
732
821
|
self.toolbar.addAction(self.work_area_tool_action)
|
733
822
|
|
@@ -2192,6 +2281,50 @@ class MainWindow(QMainWindow):
|
|
2192
2281
|
self.see_anything_deploy_predictor_dialog.exec_()
|
2193
2282
|
except Exception as e:
|
2194
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}")
|
2195
2328
|
|
2196
2329
|
def open_see_anything_batch_inference_dialog(self):
|
2197
2330
|
"""Open the See Anything Batch Inference dialog to run batch inference with See Anything."""
|
@@ -2201,16 +2334,22 @@ class MainWindow(QMainWindow):
|
|
2201
2334
|
"No images are present in the project.")
|
2202
2335
|
return
|
2203
2336
|
|
2204
|
-
if not self.
|
2337
|
+
if not self.see_anything_deploy_generator_dialog.loaded_model:
|
2205
2338
|
QMessageBox.warning(self,
|
2206
2339
|
"See Anything (YOLOE) Batch Inference",
|
2207
|
-
"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.")
|
2208
2348
|
return
|
2209
2349
|
|
2210
2350
|
try:
|
2211
2351
|
self.untoggle_all_tools()
|
2212
|
-
|
2213
|
-
self.see_anything_batch_inference_dialog.exec_()
|
2352
|
+
self.see_anything_batch_inference_dialog.exec_()
|
2214
2353
|
except Exception as e:
|
2215
2354
|
QMessageBox.critical(self, "Critical Error", f"{e}")
|
2216
2355
|
|
@@ -2329,6 +2468,62 @@ class MainWindow(QMainWindow):
|
|
2329
2468
|
msg.exec_()
|
2330
2469
|
except Exception as e:
|
2331
2470
|
QMessageBox.critical(self, "Critical Error", f"{e}")
|
2471
|
+
|
2472
|
+
def check_windows_gdi_count(self):
|
2473
|
+
"""Calculate and print the number of GDI objects for the current process on Windows."""
|
2474
|
+
# 1. Check if the OS is Windows. If not, return early.
|
2475
|
+
if os.name != 'nt':
|
2476
|
+
return
|
2477
|
+
|
2478
|
+
# Load necessary libraries
|
2479
|
+
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
|
2480
|
+
user32 = ctypes.WinDLL('user32', use_last_error=True)
|
2481
|
+
|
2482
|
+
# Define constants
|
2483
|
+
PROCESS_QUERY_INFORMATION = 0x0400
|
2484
|
+
GR_GDIOBJECTS = 0
|
2485
|
+
|
2486
|
+
process_handle = None
|
2487
|
+
try:
|
2488
|
+
# 2. Get a handle to the process from its PID
|
2489
|
+
process_handle = kernel32.OpenProcess(PROCESS_QUERY_INFORMATION, False, self.pid)
|
2490
|
+
|
2491
|
+
if not process_handle:
|
2492
|
+
error_code = ctypes.get_last_error()
|
2493
|
+
raise RuntimeError(f"Failed to open PID {self.pid}. Error code: {error_code}")
|
2494
|
+
|
2495
|
+
# 3. Use the handle to get the GDI object count
|
2496
|
+
gdi_count = user32.GetGuiResources(process_handle, GR_GDIOBJECTS)
|
2497
|
+
|
2498
|
+
if gdi_count >= 9500: # GDI limit
|
2499
|
+
self.show_gdi_limit_warning()
|
2500
|
+
|
2501
|
+
except Exception as e:
|
2502
|
+
pass
|
2503
|
+
|
2504
|
+
finally:
|
2505
|
+
# 4. CRITICAL: Always close the handle when you're done
|
2506
|
+
if process_handle:
|
2507
|
+
kernel32.CloseHandle(process_handle)
|
2508
|
+
|
2509
|
+
return
|
2510
|
+
|
2511
|
+
def show_gdi_limit_warning(self):
|
2512
|
+
"""
|
2513
|
+
Show a warning dialog if the GDI limit is reached.
|
2514
|
+
"""
|
2515
|
+
try:
|
2516
|
+
self.untoggle_all_tools()
|
2517
|
+
msg = QMessageBox()
|
2518
|
+
msg.setWindowIcon(self.coral_icon)
|
2519
|
+
msg.setWindowTitle("GDI Limit Reached")
|
2520
|
+
msg.setText(
|
2521
|
+
"The GDI limit has been reached! Please immediately save your work, close, and reopen the application!"
|
2522
|
+
)
|
2523
|
+
msg.setStandardButtons(QMessageBox.Ok)
|
2524
|
+
msg.exec_()
|
2525
|
+
except Exception as e:
|
2526
|
+
QMessageBox.critical(self, "Critical Error", f"{e}")
|
2332
2527
|
|
2333
2528
|
def open_snake_game_dialog(self):
|
2334
2529
|
"""
|
@@ -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
|
|
@@ -89,7 +89,7 @@ class RasterTableModel(QAbstractTableModel):
|
|
89
89
|
|
90
90
|
elif role == Qt.TextAlignmentRole:
|
91
91
|
return Qt.AlignCenter
|
92
|
-
|
92
|
+
|
93
93
|
elif role == Qt.FontRole:
|
94
94
|
# Bold the selected raster's text
|
95
95
|
if raster.is_selected:
|
@@ -106,12 +106,40 @@ class RasterTableModel(QAbstractTableModel):
|
|
106
106
|
|
107
107
|
elif role == Qt.ToolTipRole:
|
108
108
|
if index.column() == self.FILENAME_COL:
|
109
|
-
# Include full path and metadata in tooltip
|
110
109
|
dimensions = raster.metadata.get('dimensions', f"{raster.width}x{raster.height}")
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
110
|
+
|
111
|
+
tooltip_parts = [
|
112
|
+
f"<b>Path:</b> {path}",
|
113
|
+
f"<b>Dimensions:</b> {dimensions}",
|
114
|
+
f"<b>Annotations:</b> {'Yes' if raster.has_annotations else 'No'}",
|
115
|
+
f"<b>Predictions:</b> {'Yes' if raster.has_predictions else 'No'}"
|
116
|
+
]
|
117
|
+
|
118
|
+
if raster.has_work_areas():
|
119
|
+
tooltip_parts.append(f"<b>Work Areas:</b> {raster.count_work_items()}")
|
120
|
+
|
121
|
+
return "<br>".join(tooltip_parts)
|
122
|
+
|
123
|
+
elif index.column() == self.ANNOTATION_COUNT_COL and raster.annotation_count > 0:
|
124
|
+
tooltip_text = f"<b>Total annotations:</b> {raster.annotation_count}"
|
125
|
+
|
126
|
+
# Add annotation counts per label using a for loop
|
127
|
+
if hasattr(raster, 'label_counts') and raster.label_counts:
|
128
|
+
label_items = []
|
129
|
+
for label, count in raster.label_counts.items():
|
130
|
+
label_items.append(f"<li>{label}: {count}</li>")
|
131
|
+
label_counts_text = "".join(label_items)
|
132
|
+
tooltip_text += f"<br><br><b>Annotations by label:</b><ul>{label_counts_text}</ul>"
|
133
|
+
|
134
|
+
# Add annotation counts per type using a for loop
|
135
|
+
if hasattr(raster, 'annotation_types') and raster.annotation_types:
|
136
|
+
type_items = []
|
137
|
+
for type_name, count in raster.annotation_types.items():
|
138
|
+
type_items.append(f"<li>{type_name}: {count}</li>")
|
139
|
+
type_counts_text = "".join(type_items)
|
140
|
+
tooltip_text += f"<br><b>Annotations by type:</b><ul>{type_counts_text}</ul>"
|
141
|
+
|
142
|
+
return tooltip_text
|
115
143
|
|
116
144
|
return None
|
117
145
|
|
@@ -384,18 +384,29 @@ class DeployGeneratorDialog(QDialog):
|
|
384
384
|
|
385
385
|
def update_sam_task_state(self):
|
386
386
|
"""
|
387
|
-
Centralized method to check if SAM is loaded and update task
|
387
|
+
Centralized method to check if SAM is loaded and update task accordingly.
|
388
|
+
If the user has selected to use SAM, this function ensures the task is set to 'segment'.
|
389
|
+
Crucially, it does NOT alter the task if SAM is not selected, respecting the
|
390
|
+
user's choice from the 'Task' dropdown.
|
388
391
|
"""
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
392
|
+
# Check if the user wants to use the SAM model
|
393
|
+
if self.use_sam_dropdown.currentText() == "True":
|
394
|
+
# SAM is requested. Check if it's actually available.
|
395
|
+
sam_is_available = (
|
396
|
+
hasattr(self, 'sam_dialog') and
|
397
|
+
self.sam_dialog is not None and
|
398
|
+
self.sam_dialog.loaded_model is not None
|
399
|
+
)
|
400
|
+
|
401
|
+
if sam_is_available:
|
402
|
+
# If SAM is wanted and available, the task must be segmentation.
|
403
|
+
self.task = 'segment'
|
404
|
+
else:
|
405
|
+
# If SAM is wanted but not available, revert the dropdown and do nothing else.
|
406
|
+
# The 'is_sam_model_deployed' function already handles showing an error message.
|
407
|
+
self.use_sam_dropdown.setCurrentText("False")
|
408
|
+
|
409
|
+
# If use_sam_dropdown is "False", do nothing. Let self.task be whatever the user set.
|
399
410
|
|
400
411
|
def load_model(self):
|
401
412
|
"""
|