coralnet-toolbox 0.0.72__py2.py3-none-any.whl → 0.0.74__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. coralnet_toolbox/Annotations/QtAnnotation.py +28 -69
  2. coralnet_toolbox/Annotations/QtMaskAnnotation.py +408 -0
  3. coralnet_toolbox/Annotations/QtMultiPolygonAnnotation.py +72 -56
  4. coralnet_toolbox/Annotations/QtPatchAnnotation.py +165 -216
  5. coralnet_toolbox/Annotations/QtPolygonAnnotation.py +497 -353
  6. coralnet_toolbox/Annotations/QtRectangleAnnotation.py +126 -116
  7. coralnet_toolbox/AutoDistill/QtDeployModel.py +23 -12
  8. coralnet_toolbox/CoralNet/QtDownload.py +2 -1
  9. coralnet_toolbox/Explorer/QtDataItem.py +1 -1
  10. coralnet_toolbox/Explorer/QtExplorer.py +159 -17
  11. coralnet_toolbox/Explorer/QtSettingsWidgets.py +160 -86
  12. coralnet_toolbox/IO/QtExportTagLabAnnotations.py +30 -10
  13. coralnet_toolbox/IO/QtImportTagLabAnnotations.py +21 -15
  14. coralnet_toolbox/IO/QtOpenProject.py +46 -78
  15. coralnet_toolbox/IO/QtSaveProject.py +18 -43
  16. coralnet_toolbox/MachineLearning/DeployModel/QtDetect.py +22 -11
  17. coralnet_toolbox/MachineLearning/DeployModel/QtSegment.py +22 -10
  18. coralnet_toolbox/MachineLearning/ExportDataset/QtBase.py +61 -24
  19. coralnet_toolbox/MachineLearning/ExportDataset/QtClassify.py +5 -1
  20. coralnet_toolbox/MachineLearning/ExportDataset/QtDetect.py +19 -6
  21. coralnet_toolbox/MachineLearning/ExportDataset/QtSegment.py +21 -8
  22. coralnet_toolbox/MachineLearning/ImportDataset/QtBase.py +42 -22
  23. coralnet_toolbox/MachineLearning/VideoInference/QtBase.py +0 -4
  24. coralnet_toolbox/QtAnnotationWindow.py +42 -14
  25. coralnet_toolbox/QtEventFilter.py +19 -2
  26. coralnet_toolbox/QtImageWindow.py +134 -86
  27. coralnet_toolbox/QtLabelWindow.py +14 -2
  28. coralnet_toolbox/QtMainWindow.py +122 -9
  29. coralnet_toolbox/QtProgressBar.py +52 -27
  30. coralnet_toolbox/Rasters/QtRaster.py +59 -7
  31. coralnet_toolbox/Rasters/RasterTableModel.py +42 -14
  32. coralnet_toolbox/SAM/QtBatchInference.py +0 -2
  33. coralnet_toolbox/SAM/QtDeployGenerator.py +22 -11
  34. coralnet_toolbox/SAM/QtDeployPredictor.py +10 -0
  35. coralnet_toolbox/SeeAnything/QtBatchInference.py +19 -221
  36. coralnet_toolbox/SeeAnything/QtDeployGenerator.py +1634 -0
  37. coralnet_toolbox/SeeAnything/QtDeployPredictor.py +107 -154
  38. coralnet_toolbox/SeeAnything/QtTrainModel.py +115 -45
  39. coralnet_toolbox/SeeAnything/__init__.py +2 -0
  40. coralnet_toolbox/Tools/QtCutSubTool.py +18 -2
  41. coralnet_toolbox/Tools/QtResizeSubTool.py +19 -2
  42. coralnet_toolbox/Tools/QtSAMTool.py +222 -57
  43. coralnet_toolbox/Tools/QtSeeAnythingTool.py +223 -55
  44. coralnet_toolbox/Tools/QtSelectSubTool.py +6 -4
  45. coralnet_toolbox/Tools/QtSelectTool.py +27 -3
  46. coralnet_toolbox/Tools/QtSubtractSubTool.py +66 -0
  47. coralnet_toolbox/Tools/QtWorkAreaTool.py +25 -13
  48. coralnet_toolbox/Tools/__init__.py +2 -0
  49. coralnet_toolbox/__init__.py +1 -1
  50. coralnet_toolbox/utilities.py +137 -47
  51. coralnet_toolbox-0.0.74.dist-info/METADATA +375 -0
  52. {coralnet_toolbox-0.0.72.dist-info → coralnet_toolbox-0.0.74.dist-info}/RECORD +56 -53
  53. coralnet_toolbox-0.0.72.dist-info/METADATA +0 -341
  54. {coralnet_toolbox-0.0.72.dist-info → coralnet_toolbox-0.0.74.dist-info}/WHEEL +0 -0
  55. {coralnet_toolbox-0.0.72.dist-info → coralnet_toolbox-0.0.74.dist-info}/entry_points.txt +0 -0
  56. {coralnet_toolbox-0.0.72.dist-info → coralnet_toolbox-0.0.74.dist-info}/licenses/LICENSE.txt +0 -0
  57. {coralnet_toolbox-0.0.72.dist-info → coralnet_toolbox-0.0.74.dist-info}/top_level.txt +0 -0
@@ -1,22 +1,18 @@
1
1
  import warnings
2
2
 
3
3
  import os
4
- import gc
5
4
  from contextlib import contextmanager
6
5
 
7
6
  import rasterio
8
7
 
9
- from PyQt5.QtGui import QImage, QPixmap
10
- from PyQt5.QtCore import Qt, pyqtSignal, QTimer, QPoint, QThreadPool, QItemSelectionModel
8
+ from PyQt5.QtCore import Qt, pyqtSignal, QTimer, QPoint, QThreadPool, QItemSelectionModel, QModelIndex
11
9
  from PyQt5.QtWidgets import (QSizePolicy, QMessageBox, QCheckBox, QWidget, QVBoxLayout,
12
10
  QLabel, QComboBox, QHBoxLayout, QTableView, QHeaderView, QApplication,
13
- QMenu, QButtonGroup, QAbstractItemView, QGroupBox, QPushButton,
14
- QStyle, QFormLayout, QFrame)
15
-
16
- from coralnet_toolbox.Rasters import Raster, RasterManager, ImageFilter, RasterTableModel
11
+ QMenu, QButtonGroup, QGroupBox, QPushButton, QStyle,
12
+ QFormLayout, QFrame)
17
13
 
14
+ from coralnet_toolbox.Rasters import RasterManager, ImageFilter, RasterTableModel
18
15
  from coralnet_toolbox.QtProgressBar import ProgressBar
19
-
20
16
  from coralnet_toolbox.Icons import get_icon
21
17
 
22
18
 
@@ -30,11 +26,24 @@ warnings.filterwarnings("ignore", category=rasterio.errors.NotGeoreferencedWarni
30
26
 
31
27
 
32
28
  class NoArrowKeyTableView(QTableView):
29
+ # Custom signal to be emitted only on a left-click
30
+ leftClicked = pyqtSignal(QModelIndex)
31
+
33
32
  def keyPressEvent(self, event):
34
33
  if event.key() in (Qt.Key_Up, Qt.Key_Down):
35
34
  event.ignore()
36
35
  return
37
36
  super().keyPressEvent(event)
37
+
38
+ def mousePressEvent(self, event):
39
+ # On a left mouse press, emit our custom signal
40
+ if event.button() == Qt.LeftButton:
41
+ index = self.indexAt(event.pos())
42
+ if index.isValid():
43
+ self.leftClicked.emit(index)
44
+ # Call the base class implementation to handle standard behavior
45
+ # like row selection and context menu triggers.
46
+ super().mousePressEvent(event)
38
47
 
39
48
 
40
49
  class ImageWindow(QWidget):
@@ -256,10 +265,11 @@ class ImageWindow(QWidget):
256
265
  self.table_model = RasterTableModel(self.raster_manager, self)
257
266
  self.tableView.setModel(self.table_model)
258
267
 
259
- # Set column widths - removed checkbox column, adjust accordingly
260
- self.tableView.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
261
- self.tableView.setColumnWidth(1, 120)
262
- self.tableView.horizontalHeader().setSectionResizeMode(1, QHeaderView.Fixed)
268
+ # Set column widths
269
+ self.tableView.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents) # Checkmark column
270
+ self.tableView.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch) # Filename column
271
+ self.tableView.setColumnWidth(2, 120) # Annotation column
272
+ self.tableView.horizontalHeader().setSectionResizeMode(2, QHeaderView.Fixed)
263
273
 
264
274
  # Style the header
265
275
  self.tableView.horizontalHeader().setStyleSheet("""
@@ -271,7 +281,7 @@ class ImageWindow(QWidget):
271
281
  """)
272
282
 
273
283
  # Connect signals for clicking
274
- self.tableView.pressed.connect(self.on_table_pressed)
284
+ self.tableView.leftClicked.connect(self.on_table_pressed)
275
285
  self.tableView.doubleClicked.connect(self.on_table_double_clicked)
276
286
 
277
287
  # Add table view to the layout
@@ -413,59 +423,62 @@ class ImageWindow(QWidget):
413
423
  #
414
424
 
415
425
  def on_table_pressed(self, index):
416
- """Handle a single click on the table view."""
426
+ """Handle a single left-click on the table view with complex modifier support."""
417
427
  if not index.isValid():
418
428
  return
419
-
420
- # Get the path at the clicked row
429
+
421
430
  path = self.table_model.get_path_at_row(index.row())
422
431
  if not path:
423
432
  return
424
433
 
425
- # Get keyboard modifiers
426
434
  modifiers = QApplication.keyboardModifiers()
427
-
428
- # Handle highlighting logic
429
- if modifiers & Qt.ControlModifier:
430
- # Ctrl+Click: Toggle highlight for the clicked row
435
+ current_row = index.row()
436
+
437
+ # Define conditions for modifiers
438
+ has_ctrl = bool(modifiers & Qt.ControlModifier)
439
+ has_shift = bool(modifiers & Qt.ShiftModifier)
440
+
441
+ if has_shift:
442
+ # This block handles both Shift+Click and Ctrl+Shift+Click.
443
+ # First, determine the paths in the selection range.
444
+ range_paths = []
445
+ if self.last_highlighted_row >= 0:
446
+ start = min(self.last_highlighted_row, current_row)
447
+ end = max(self.last_highlighted_row, current_row)
448
+ for r in range(start, end + 1):
449
+ p = self.table_model.get_path_at_row(r)
450
+ if p:
451
+ range_paths.append(p)
452
+ else:
453
+ # If there's no anchor, the range is just the clicked item.
454
+ range_paths.append(path)
455
+
456
+ if not has_ctrl:
457
+ # Case 1: Simple Shift+Click. Clears previous highlights
458
+ # and selects only the new range.
459
+ self.table_model.set_highlighted_paths(range_paths)
460
+ else:
461
+ # Case 2: Ctrl+Shift+Click. Adds the new range to the
462
+ # existing highlighted rows without clearing them.
463
+ for p in range_paths:
464
+ self.table_model.highlight_path(p, True)
465
+
466
+ elif has_ctrl:
467
+ # Case 3: Ctrl+Click. Toggles a single row's highlight state
468
+ # and sets it as the new anchor for future shift-clicks.
431
469
  raster = self.raster_manager.get_raster(path)
432
470
  if raster:
433
471
  self.table_model.highlight_path(path, not raster.is_highlighted)
434
- # Update highlighted count
435
- self.update_highlighted_count_label()
436
-
437
- elif modifiers & Qt.ShiftModifier:
438
- # Shift+Click: Highlight range from last highlighted to current
439
- if self.last_highlighted_row >= 0:
440
- # Get the current row and last highlighted row
441
- current_row = index.row()
442
-
443
- # Calculate range (handle both directions)
444
- start_row = min(self.last_highlighted_row, current_row)
445
- end_row = max(self.last_highlighted_row, current_row)
446
-
447
- # Highlight the range
448
- for row in range(start_row, end_row + 1):
449
- path_to_highlight = self.table_model.get_path_at_row(row)
450
- if path_to_highlight:
451
- self.table_model.highlight_path(path_to_highlight, True)
452
- else:
453
- # No previous selection, just highlight the current row
454
- self.table_model.highlight_path(path, True)
455
-
456
- # Update the last highlighted row
457
- self.last_highlighted_row = index.row()
458
-
459
- # Update highlighted count
460
- self.update_highlighted_count_label()
472
+ self.last_highlighted_row = current_row
473
+
461
474
  else:
462
- # Regular click: Clear all highlights and highlight only this row
463
- self.table_model.clear_highlights()
464
- self.table_model.highlight_path(path, True)
465
- self.last_highlighted_row = index.row()
466
-
467
- # Update highlighted count
468
- self.update_highlighted_count_label()
475
+ # Case 4: Plain Click. Clears everything and highlights only
476
+ # the clicked row, setting it as the new anchor.
477
+ self.table_model.set_highlighted_paths([path])
478
+ self.last_highlighted_row = current_row
479
+
480
+ # Finally, update the count label after any changes.
481
+ self.update_highlighted_count_label()
469
482
 
470
483
  def on_table_double_clicked(self, index):
471
484
  """Handle double click on table view (selects image and loads it)."""
@@ -521,6 +534,24 @@ class ImageWindow(QWidget):
521
534
  """Handler for when an image is loaded."""
522
535
  self.selected_image_path = path
523
536
 
537
+ def on_toggle(self, new_state: bool):
538
+ """
539
+ Sets the checked state for all currently highlighted rows.
540
+
541
+ Args:
542
+ new_state (bool): The new state to set (True for checked, False for unchecked).
543
+ """
544
+ highlighted_paths = self.table_model.get_highlighted_paths()
545
+ if not highlighted_paths:
546
+ return
547
+
548
+ for path in highlighted_paths:
549
+ raster = self.raster_manager.get_raster(path)
550
+ if raster:
551
+ raster.checkbox_state = new_state
552
+ # Notify the model to update the view for this specific raster
553
+ self.table_model.update_raster_data(path)
554
+
524
555
  #
525
556
  # Public methods
526
557
  #
@@ -562,7 +593,7 @@ class ImageWindow(QWidget):
562
593
 
563
594
  except Exception as e:
564
595
  self.show_error("Image Loading Error",
565
- f"Error loading image {os.path.basename(image_path)}:\n{str(e)}")
596
+ f"Error loading image {os.path.basename(image_path)}:\n{str(e)}")
566
597
  return False
567
598
 
568
599
  @property
@@ -960,28 +991,46 @@ class ImageWindow(QWidget):
960
991
 
961
992
  def show_context_menu(self, position):
962
993
  """
963
- Show the context menu for the table.
994
+ Show the context menu for the table, including the toggle check state action.
964
995
 
965
996
  Args:
966
997
  position (QPoint): Position to show the menu
967
998
  """
999
+ # Get the path corresponding to the right-clicked row
1000
+ index = self.tableView.indexAt(position)
1001
+ path_at_cursor = self.table_model.get_path_at_row(index.row()) if index.isValid() else None
1002
+
1003
+ # Get the currently highlighted paths from the model
968
1004
  highlighted_paths = self.table_model.get_highlighted_paths()
969
- if not highlighted_paths:
970
- # If no highlights, highlight the row under the cursor only
971
- index = self.tableView.indexAt(position)
972
- if index.isValid():
973
- path = self.table_model.get_path_at_row(index.row())
974
- if path:
975
- self.table_model.set_highlighted_paths([path])
976
- self.last_highlighted_row = index.row()
977
- highlighted_paths = [path]
978
- else:
979
- # If any highlights, ensure all highlighted rows are used (no change needed)
980
- self.table_model.set_highlighted_paths(highlighted_paths)
1005
+
1006
+ # If the user right-clicked on a row that wasn't already highlighted,
1007
+ # then we assume they want to act on this row alone.
1008
+ if path_at_cursor and path_at_cursor not in highlighted_paths:
1009
+ self.table_model.set_highlighted_paths([path_at_cursor])
1010
+ self.last_highlighted_row = index.row()
1011
+ highlighted_paths = [path_at_cursor]
1012
+
1013
+ # If no rows are highlighted, do nothing.
981
1014
  if not highlighted_paths:
982
1015
  return
1016
+
983
1017
  context_menu = QMenu(self)
984
1018
  count = len(highlighted_paths)
1019
+
1020
+ # Add the check/uncheck action
1021
+ raster_under_cursor = self.raster_manager.get_raster(path_at_cursor)
1022
+ if raster_under_cursor:
1023
+ is_checked = raster_under_cursor.checkbox_state
1024
+ if is_checked:
1025
+ action_text = f"Uncheck {count} Highlighted Image{'s' if count > 1 else ''}"
1026
+ else:
1027
+ action_text = f"Check {count} Highlighted Image{'s' if count > 1 else ''}"
1028
+ toggle_check_action = context_menu.addAction(action_text)
1029
+ toggle_check_action.triggered.connect(lambda: self.on_toggle(not is_checked))
1030
+
1031
+ context_menu.addSeparator()
1032
+
1033
+ # Add existing delete actions
985
1034
  delete_images_action = context_menu.addAction(f"Delete {count} Highlighted Image{'s' if count > 1 else ''}")
986
1035
  delete_images_action.triggered.connect(lambda: self.delete_highlighted_images())
987
1036
  delete_annotations_action = context_menu.addAction(
@@ -1182,26 +1231,25 @@ class ImagePreviewTooltip(QFrame):
1182
1231
  self.hide()
1183
1232
 
1184
1233
  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()
1234
+ """Position and show the tooltip at the specified global position."""
1235
+ # Position tooltip to bottom-right of cursor
1236
+ x, y = global_pos.x() + 15, global_pos.y() + 15
1237
+
1238
+ # Get the screen that contains the cursor position
1239
+ screen = QApplication.screenAt(global_pos)
1240
+ if not screen:
1241
+ screen = QApplication.primaryScreen()
1242
+
1243
+ # Get screen geometry and tooltip size
1244
+ screen_rect = screen.geometry()
1197
1245
  tooltip_size = self.sizeHint()
1198
-
1199
- # Adjust position if needed to stay on screen
1246
+
1247
+ # Adjust position to stay on screen
1200
1248
  if x + tooltip_size.width() > screen_rect.right():
1201
- x = screen_rect.right() - tooltip_size.width() - 10
1249
+ x = global_pos.x() - tooltip_size.width() - 15
1202
1250
  if y + tooltip_size.height() > screen_rect.bottom():
1203
- y = screen_rect.bottom() - tooltip_size.height() - 10
1204
-
1251
+ y = global_pos.y() - tooltip_size.height() - 15
1252
+
1205
1253
  # Set position and show
1206
1254
  self.move(x, y)
1207
1255
  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)
@@ -485,7 +485,19 @@ class LabelWindow(QWidget):
485
485
  self.scroll_content.setFixedWidth(self.labels_per_row * self.label_width)
486
486
 
487
487
  def reorganize_labels(self):
488
- """Rearrange labels in the grid layout based on the current order and labels_per_row."""
488
+ """
489
+ Rearrange labels in the grid layout based on the current order and labels_per_row.
490
+ """
491
+ # First, clear the existing layout to remove any lingering widgets.
492
+ # This prevents references to deleted widgets from persisting in the layout.
493
+ while self.grid_layout.count():
494
+ item = self.grid_layout.takeAt(0)
495
+ widget = item.widget()
496
+ if widget:
497
+ # Setting the parent to None removes the widget from the layout's control.
498
+ widget.setParent(None)
499
+
500
+ # Now, add the current labels from the model back into the clean layout.
489
501
  for i, label in enumerate(self.labels):
490
502
  row = i // self.labels_per_row
491
503
  col = i % self.labels_per_row
@@ -9,8 +9,8 @@ import requests
9
9
 
10
10
  from packaging import version
11
11
 
12
- from PyQt5.QtCore import Qt, pyqtSignal, QEvent, QSize, QPoint
13
12
  from PyQt5.QtGui import QIcon, QMouseEvent
13
+ from PyQt5.QtCore import Qt, pyqtSignal, QEvent, QSize, QPoint
14
14
  from PyQt5.QtWidgets import (QListWidget, QCheckBox, QFrame, QComboBox)
15
15
  from PyQt5.QtWidgets import (QMainWindow, QApplication, QToolBar, QAction, QSizePolicy,
16
16
  QMessageBox, QWidget, QVBoxLayout, QLabel, QHBoxLayout,
@@ -95,6 +95,7 @@ from coralnet_toolbox.SAM import (
95
95
  from coralnet_toolbox.SeeAnything import (
96
96
  TrainModelDialog as SeeAnythingTrainModelDialog,
97
97
  DeployPredictorDialog as SeeAnythingDeployPredictorDialog,
98
+ DeployGeneratorDialog as SeeAnythingDeployGeneratorDialog,
98
99
  BatchInferenceDialog as SeeAnythingBatchInferenceDialog
99
100
  )
100
101
 
@@ -258,6 +259,7 @@ class MainWindow(QMainWindow):
258
259
  # Create dialogs (See Anything)
259
260
  self.see_anything_train_model_dialog = SeeAnythingTrainModelDialog(self)
260
261
  self.see_anything_deploy_predictor_dialog = SeeAnythingDeployPredictorDialog(self)
262
+ self.see_anything_deploy_generator_dialog = SeeAnythingDeployGeneratorDialog(self)
261
263
  self.see_anything_batch_inference_dialog = SeeAnythingBatchInferenceDialog(self)
262
264
 
263
265
  # Create dialogs (AutoDistill)
@@ -622,11 +624,17 @@ class MainWindow(QMainWindow):
622
624
  # Train Model
623
625
  self.see_anything_train_model_action = QAction("Train Model", self)
624
626
  self.see_anything_train_model_action.triggered.connect(self.open_see_anything_train_model_dialog)
625
- self.see_anything_menu.addAction(self.see_anything_train_model_action)
626
- # Deploy Model
627
+ # self.see_anything_menu.addAction(self.see_anything_train_model_action) TODO Doesn't work
628
+ # Deploy Model submenu
629
+ self.see_anything_deploy_model_menu = self.see_anything_menu.addMenu("Deploy Model")
630
+ # Deploy Predictor
627
631
  self.see_anything_deploy_predictor_action = QAction("Deploy Predictor", self)
628
632
  self.see_anything_deploy_predictor_action.triggered.connect(self.open_see_anything_deploy_predictor_dialog)
629
- self.see_anything_menu.addAction(self.see_anything_deploy_predictor_action)
633
+ self.see_anything_deploy_model_menu.addAction(self.see_anything_deploy_predictor_action)
634
+ # Deploy Generator
635
+ self.see_anything_deploy_generator_action = QAction("Deploy Generator", self)
636
+ self.see_anything_deploy_generator_action.triggered.connect(self.open_see_anything_deploy_generator_dialog)
637
+ self.see_anything_deploy_model_menu.addAction(self.see_anything_deploy_generator_action)
630
638
  # Batch Inference
631
639
  self.see_anything_batch_inference_action = QAction("Batch Inference", self)
632
640
  self.see_anything_batch_inference_action.triggered.connect(self.open_see_anything_batch_inference_dialog)
@@ -674,6 +682,76 @@ class MainWindow(QMainWindow):
674
682
  # ----------------------------------------
675
683
  # Create and add the toolbar
676
684
  # ----------------------------------------
685
+
686
+ # Define verbose tool descriptions
687
+ self.tool_descriptions = {
688
+ "select": ("Select Tool\n\n"
689
+ "Select, modify, and manage annotations.\n"
690
+ "• Left-click to select annotations; hold Ctrl+left-click to select multiple.\n"
691
+ "• Left-click and drag to move selected annotations.\n"
692
+ "• Ctrl+click and drag to create a selection rectangle.\n"
693
+ "• Ctrl+Shift to show resize handles for a selected Rectangle and Polygon annotations.\n"
694
+ "• Ctrl+X to cut a selected annotation along a drawn line.\n"
695
+ "• Ctrl+C to combine multiple selected annotations.\n"
696
+ "• Ctrl+Space to confirm selected annotations with top predictions.\n"
697
+ "• Ctrl+Shift+mouse wheel to adjust polygon complexity.\n"
698
+ "• Ctrl+Delete to remove selected annotations."),
699
+
700
+ "patch": ("Patch Tool\n\n"
701
+ "Create point (patch) annotations centered at the cursor.\n"
702
+ "• Left-click to place a patch at the mouse location.\n"
703
+ "• Hold Ctrl and use the mouse wheel or use the Patch Size box to adjust patch size.\n"
704
+ "• A semi-transparent preview shows the patch before placing it."),
705
+
706
+ "rectangle": ("Rectangle Tool\n\n"
707
+ "Create rectangular annotations by clicking and dragging.\n"
708
+ "• Left-click to set the first corner, then move the mouse to size the rectangle.\n"
709
+ "• Left-click again to place the rectangle.\n"
710
+ "• Press Backspace to cancel drawing the current rectangle.\n"
711
+ "• A semi-transparent preview shows the rectangle while drawing."),
712
+
713
+ "polygon": ("Polygon Tool\n\n"
714
+ "Create polygon annotations with multiple vertices.\n"
715
+ "• Left-click to set the first vertex, then move the mouse to draw the polygon\n"
716
+ "• Hold Ctrl while left-clicking to draw straight-line segments.\n"
717
+ "• Left-click again to complete the polygon.\n"
718
+ "• Press Backspace to cancel the current polygon.\n"
719
+ "• A semi-transparent preview shows the polygon while drawing."),
720
+
721
+ "sam": ("Segment Anything (SAM) Tool\n\n"
722
+ "Generates AI-powered segmentations.\n"
723
+ "• Left-click to create a working area, then left-click again to confirm.\n"
724
+ "\t• Or, press Spacebar to create a working area for the current view.\n"
725
+ "• Ctrl+Left-click to add positive points (foreground).\n"
726
+ "• Ctrl+Right-click to add negative points (background).\n"
727
+ "• Left-click and drag to create a bounding box for prompting.\n"
728
+ "• Press Spacebar to generate and confirm the segmentation.\n"
729
+ "• Press Backspace to cancel the current operation.\n"
730
+ "• Uncertainty can be adjusted in Parameters section.\n"
731
+ "• A SAM predictor must be deployed first."),
732
+
733
+ "see_anything": ("See Anything (YOLOE) Tool\n\n"
734
+ "Uses YOLOE to detect / segments objects of interest based on visual prompts.\n"
735
+ "• Left-click to create a working area, then click again to confirm.\n"
736
+ "\t• Or, press Spacebar to create a working area for the current view.\n"
737
+ "• Draw rectangles inside the working area to guide detection.\n"
738
+ "• Press Spacebar to generate detections using drawn rectangles.\n"
739
+ "• Press Spacebar again to confirm annotations or apply SAM refinement.\n"
740
+ "• Press Backspace to cancel current operation or clear annotations.\n"
741
+ "• Uncertainty can be adjusted in Parameters section.\n"
742
+ "• A See Anything (YOLOE) predictor must be deployed first."),
743
+
744
+ "work_area": ("Work Area Tool\n\n"
745
+ "Defines regions for detection and segmentation models to run predictions on.\n"
746
+ "• Left-click to create a working area, then left-click again to confirm.\n"
747
+ "\t• Or, press Spacebar to create a work area from the current view.\n"
748
+ "• Hold Ctrl+Shift to show delete buttons for existing work areas.\n"
749
+ "• Press Ctrl+Shift+Backspace to clear all work areas.\n"
750
+ "• Hold Ctrl+Alt to temporarily view a work area of the current view.\n"
751
+ "• Work areas can be used with Tile Batch Inference and other batch operations.\n"
752
+ "• All work areas are automatically saved with the image in a Project (JSON) file.")
753
+ }
754
+
677
755
  self.toolbar = QToolBar("Tools", self)
678
756
  self.toolbar.setOrientation(Qt.Vertical)
679
757
  self.toolbar.setFixedWidth(40)
@@ -696,6 +774,7 @@ class MainWindow(QMainWindow):
696
774
  # Add tools here with icons
697
775
  self.select_tool_action = QAction(self.select_icon, "Select", self)
698
776
  self.select_tool_action.setCheckable(True)
777
+ self.select_tool_action.setToolTip(self.tool_descriptions["select"])
699
778
  self.select_tool_action.triggered.connect(self.toggle_tool)
700
779
  self.toolbar.addAction(self.select_tool_action)
701
780
 
@@ -703,16 +782,19 @@ class MainWindow(QMainWindow):
703
782
 
704
783
  self.patch_tool_action = QAction(self.patch_icon, "Patch", self)
705
784
  self.patch_tool_action.setCheckable(True)
785
+ self.patch_tool_action.setToolTip(self.tool_descriptions["patch"])
706
786
  self.patch_tool_action.triggered.connect(self.toggle_tool)
707
787
  self.toolbar.addAction(self.patch_tool_action)
708
788
 
709
789
  self.rectangle_tool_action = QAction(self.rectangle_icon, "Rectangle", self)
710
790
  self.rectangle_tool_action.setCheckable(True)
791
+ self.rectangle_tool_action.setToolTip(self.tool_descriptions["rectangle"])
711
792
  self.rectangle_tool_action.triggered.connect(self.toggle_tool)
712
793
  self.toolbar.addAction(self.rectangle_tool_action)
713
794
 
714
795
  self.polygon_tool_action = QAction(self.polygon_icon, "Polygon", self)
715
796
  self.polygon_tool_action.setCheckable(True)
797
+ self.polygon_tool_action.setToolTip(self.tool_descriptions["polygon"])
716
798
  self.polygon_tool_action.triggered.connect(self.toggle_tool)
717
799
  self.toolbar.addAction(self.polygon_tool_action)
718
800
 
@@ -720,11 +802,13 @@ class MainWindow(QMainWindow):
720
802
 
721
803
  self.sam_tool_action = QAction(self.sam_icon, "SAM", self)
722
804
  self.sam_tool_action.setCheckable(True)
805
+ self.sam_tool_action.setToolTip(self.tool_descriptions["sam"])
723
806
  self.sam_tool_action.triggered.connect(self.toggle_tool)
724
807
  self.toolbar.addAction(self.sam_tool_action)
725
808
 
726
809
  self.see_anything_tool_action = QAction(self.see_anything_icon, "See Anything (YOLOE)", self)
727
810
  self.see_anything_tool_action.setCheckable(True)
811
+ self.see_anything_tool_action.setToolTip(self.tool_descriptions["see_anything"])
728
812
  self.see_anything_tool_action.triggered.connect(self.toggle_tool)
729
813
  self.toolbar.addAction(self.see_anything_tool_action)
730
814
 
@@ -732,6 +816,7 @@ class MainWindow(QMainWindow):
732
816
 
733
817
  self.work_area_tool_action = QAction(self.workarea_icon, "Work Area", self)
734
818
  self.work_area_tool_action.setCheckable(True)
819
+ self.work_area_tool_action.setToolTip(self.tool_descriptions["work_area"])
735
820
  self.work_area_tool_action.triggered.connect(self.toggle_tool)
736
821
  self.toolbar.addAction(self.work_area_tool_action)
737
822
 
@@ -2196,6 +2281,26 @@ 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 len(self.label_window.labels) <= 1:
2294
+ QMessageBox.warning(self,
2295
+ "See Anything (YOLOE)",
2296
+ "At least one reference label is required for reference.")
2297
+ return
2298
+
2299
+ try:
2300
+ self.untoggle_all_tools()
2301
+ self.see_anything_deploy_generator_dialog.exec_()
2302
+ except Exception as e:
2303
+ QMessageBox.critical(self, "Critical Error", f"An error occurred: {e}")
2199
2304
 
2200
2305
  def open_see_anything_batch_inference_dialog(self):
2201
2306
  """Open the See Anything Batch Inference dialog to run batch inference with See Anything."""
@@ -2205,16 +2310,22 @@ class MainWindow(QMainWindow):
2205
2310
  "No images are present in the project.")
2206
2311
  return
2207
2312
 
2208
- if not self.see_anything_deploy_predictor_dialog.loaded_model:
2313
+ if not self.see_anything_deploy_generator_dialog.loaded_model:
2209
2314
  QMessageBox.warning(self,
2210
2315
  "See Anything (YOLOE) Batch Inference",
2211
- "Please deploy a model before running batch inference.")
2316
+ "Please deploy a generator before running batch inference.")
2317
+ return
2318
+
2319
+ # Check if there are any annotations
2320
+ if not self.annotation_window.annotations_dict:
2321
+ QMessageBox.warning(self,
2322
+ "See Anything (YOLOE)",
2323
+ "Cannot run See Anything (YOLOE) without reference annotations in the project.")
2212
2324
  return
2213
2325
 
2214
2326
  try:
2215
2327
  self.untoggle_all_tools()
2216
- if self.see_anything_batch_inference_dialog.has_valid_sources():
2217
- self.see_anything_batch_inference_dialog.exec_()
2328
+ self.see_anything_batch_inference_dialog.exec_()
2218
2329
  except Exception as e:
2219
2330
  QMessageBox.critical(self, "Critical Error", f"{e}")
2220
2331
 
@@ -2383,7 +2494,9 @@ class MainWindow(QMainWindow):
2383
2494
  msg.setWindowIcon(self.coral_icon)
2384
2495
  msg.setWindowTitle("GDI Limit Reached")
2385
2496
  msg.setText(
2386
- "The GDI limit has been reached! Please immediately save your work, close, and reopen the application!"
2497
+ "The GDI limit is getting dangerously close to being reached (this is a known issue). "
2498
+ "Please immediately save your progress, close, and re-open the application. Failure to do so may "
2499
+ "result in data loss."
2387
2500
  )
2388
2501
  msg.setStandardButtons(QMessageBox.Ok)
2389
2502
  msg.exec_()