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.
Files changed (39) hide show
  1. coralnet_toolbox/Annotations/QtRectangleAnnotation.py +31 -2
  2. coralnet_toolbox/AutoDistill/QtDeployModel.py +23 -12
  3. coralnet_toolbox/Explorer/QtDataItem.py +53 -21
  4. coralnet_toolbox/Explorer/QtExplorer.py +581 -276
  5. coralnet_toolbox/Explorer/QtFeatureStore.py +15 -0
  6. coralnet_toolbox/Explorer/QtSettingsWidgets.py +49 -7
  7. coralnet_toolbox/MachineLearning/DeployModel/QtDetect.py +22 -11
  8. coralnet_toolbox/MachineLearning/DeployModel/QtSegment.py +22 -10
  9. coralnet_toolbox/MachineLearning/ExportDataset/QtBase.py +61 -24
  10. coralnet_toolbox/MachineLearning/ExportDataset/QtClassify.py +5 -1
  11. coralnet_toolbox/MachineLearning/ExportDataset/QtDetect.py +19 -6
  12. coralnet_toolbox/MachineLearning/ExportDataset/QtSegment.py +21 -8
  13. coralnet_toolbox/QtAnnotationWindow.py +52 -16
  14. coralnet_toolbox/QtEventFilter.py +8 -2
  15. coralnet_toolbox/QtImageWindow.py +17 -18
  16. coralnet_toolbox/QtLabelWindow.py +1 -1
  17. coralnet_toolbox/QtMainWindow.py +203 -8
  18. coralnet_toolbox/Rasters/QtRaster.py +59 -7
  19. coralnet_toolbox/Rasters/RasterTableModel.py +34 -6
  20. coralnet_toolbox/SAM/QtBatchInference.py +0 -2
  21. coralnet_toolbox/SAM/QtDeployGenerator.py +22 -11
  22. coralnet_toolbox/SeeAnything/QtBatchInference.py +19 -221
  23. coralnet_toolbox/SeeAnything/QtDeployGenerator.py +1016 -0
  24. coralnet_toolbox/SeeAnything/QtDeployPredictor.py +69 -53
  25. coralnet_toolbox/SeeAnything/QtTrainModel.py +115 -45
  26. coralnet_toolbox/SeeAnything/__init__.py +2 -0
  27. coralnet_toolbox/Tools/QtResizeSubTool.py +6 -1
  28. coralnet_toolbox/Tools/QtSAMTool.py +150 -7
  29. coralnet_toolbox/Tools/QtSeeAnythingTool.py +220 -55
  30. coralnet_toolbox/Tools/QtSelectSubTool.py +6 -4
  31. coralnet_toolbox/Tools/QtSelectTool.py +48 -6
  32. coralnet_toolbox/Tools/QtWorkAreaTool.py +25 -13
  33. coralnet_toolbox/__init__.py +1 -1
  34. {coralnet_toolbox-0.0.71.dist-info → coralnet_toolbox-0.0.73.dist-info}/METADATA +1 -1
  35. {coralnet_toolbox-0.0.71.dist-info → coralnet_toolbox-0.0.73.dist-info}/RECORD +39 -38
  36. {coralnet_toolbox-0.0.71.dist-info → coralnet_toolbox-0.0.73.dist-info}/WHEEL +0 -0
  37. {coralnet_toolbox-0.0.71.dist-info → coralnet_toolbox-0.0.73.dist-info}/entry_points.txt +0 -0
  38. {coralnet_toolbox-0.0.71.dist-info → coralnet_toolbox-0.0.73.dist-info}/licenses/LICENSE.txt +0 -0
  39. {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
- 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)
@@ -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.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)
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.see_anything_deploy_predictor_dialog.loaded_model:
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 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.")
2208
2348
  return
2209
2349
 
2210
2350
  try:
2211
2351
  self.untoggle_all_tools()
2212
- if self.see_anything_batch_inference_dialog.has_valid_sources():
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
- # 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
 
@@ -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
- return (f"Path: {path}\n"
112
- f"Dimensions: {dimensions}\n"
113
- f"Has Annotations: {'Yes' if raster.has_annotations else 'No'}\n"
114
- f"Has Predictions: {'Yes' if raster.has_predictions else 'No'}")
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
 
@@ -35,8 +35,6 @@ class BatchInferenceDialog(QDialog):
35
35
  self.deploy_model_dialog = None
36
36
  self.loaded_model = None
37
37
 
38
- self.annotations = []
39
- self.prepared_patches = []
40
38
  self.image_paths = []
41
39
 
42
40
  self.layout = QVBoxLayout(self)
@@ -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 and dropdown accordingly.
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
- sam_active = (
390
- self.sam_dialog is not None and
391
- self.sam_dialog.loaded_model is not None and
392
- self.use_sam_dropdown.currentText() == "True"
393
- )
394
- if sam_active:
395
- self.task = 'segment'
396
- else:
397
- self.task = 'detect'
398
- self.use_sam_dropdown.setCurrentText("False")
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
  """