coralnet-toolbox 0.0.73__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 (41) 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/CoralNet/QtDownload.py +2 -1
  8. coralnet_toolbox/Explorer/QtExplorer.py +16 -14
  9. coralnet_toolbox/Explorer/QtSettingsWidgets.py +114 -82
  10. coralnet_toolbox/IO/QtExportTagLabAnnotations.py +30 -10
  11. coralnet_toolbox/IO/QtImportTagLabAnnotations.py +21 -15
  12. coralnet_toolbox/IO/QtOpenProject.py +46 -78
  13. coralnet_toolbox/IO/QtSaveProject.py +18 -43
  14. coralnet_toolbox/MachineLearning/ExportDataset/QtBase.py +1 -1
  15. coralnet_toolbox/MachineLearning/ImportDataset/QtBase.py +42 -22
  16. coralnet_toolbox/MachineLearning/VideoInference/QtBase.py +0 -4
  17. coralnet_toolbox/QtEventFilter.py +11 -0
  18. coralnet_toolbox/QtImageWindow.py +117 -68
  19. coralnet_toolbox/QtLabelWindow.py +13 -1
  20. coralnet_toolbox/QtMainWindow.py +5 -27
  21. coralnet_toolbox/QtProgressBar.py +52 -27
  22. coralnet_toolbox/Rasters/RasterTableModel.py +8 -8
  23. coralnet_toolbox/SAM/QtDeployPredictor.py +10 -0
  24. coralnet_toolbox/SeeAnything/QtDeployGenerator.py +779 -161
  25. coralnet_toolbox/SeeAnything/QtDeployPredictor.py +86 -149
  26. coralnet_toolbox/Tools/QtCutSubTool.py +18 -2
  27. coralnet_toolbox/Tools/QtResizeSubTool.py +19 -2
  28. coralnet_toolbox/Tools/QtSAMTool.py +72 -50
  29. coralnet_toolbox/Tools/QtSeeAnythingTool.py +8 -5
  30. coralnet_toolbox/Tools/QtSelectTool.py +27 -3
  31. coralnet_toolbox/Tools/QtSubtractSubTool.py +66 -0
  32. coralnet_toolbox/Tools/__init__.py +2 -0
  33. coralnet_toolbox/__init__.py +1 -1
  34. coralnet_toolbox/utilities.py +137 -47
  35. coralnet_toolbox-0.0.74.dist-info/METADATA +375 -0
  36. {coralnet_toolbox-0.0.73.dist-info → coralnet_toolbox-0.0.74.dist-info}/RECORD +40 -38
  37. coralnet_toolbox-0.0.73.dist-info/METADATA +0 -341
  38. {coralnet_toolbox-0.0.73.dist-info → coralnet_toolbox-0.0.74.dist-info}/WHEEL +0 -0
  39. {coralnet_toolbox-0.0.73.dist-info → coralnet_toolbox-0.0.74.dist-info}/entry_points.txt +0 -0
  40. {coralnet_toolbox-0.0.73.dist-info → coralnet_toolbox-0.0.74.dist-info}/licenses/LICENSE.txt +0 -0
  41. {coralnet_toolbox-0.0.73.dist-info → coralnet_toolbox-0.0.74.dist-info}/top_level.txt +0 -0
@@ -96,10 +96,9 @@ class SaveProject(QDialog):
96
96
 
97
97
  try:
98
98
  project_data = {
99
- 'image_paths': self.get_images(),
99
+ 'images': self.get_images(),
100
100
  'labels': self.get_labels(),
101
- 'annotations': self.get_annotations(),
102
- 'workareas': self.get_workareas()
101
+ 'annotations': self.get_annotations()
103
102
  }
104
103
 
105
104
  with open(file_path, 'w') as file:
@@ -125,10 +124,10 @@ class SaveProject(QDialog):
125
124
  self.accept()
126
125
 
127
126
  def get_images(self):
128
- """Get the list of image paths to export."""
127
+ """Get the list of image objects, including paths, states, and work areas."""
129
128
  # Start the progress bar
130
129
  total_images = len(self.image_window.raster_manager.image_paths)
131
- progress_bar = ProgressBar(self.label_window, "Exporting Images")
130
+ progress_bar = ProgressBar(self.label_window, "Exporting Image Data")
132
131
  progress_bar.show()
133
132
  progress_bar.start_progress(total_images)
134
133
 
@@ -137,7 +136,19 @@ class SaveProject(QDialog):
137
136
 
138
137
  # Loop through all of the image paths
139
138
  for image_path in self.image_window.raster_manager.image_paths:
140
- export_images.append(image_path)
139
+ raster = self.image_window.raster_manager.get_raster(image_path)
140
+ if raster:
141
+ # Get work areas for this raster
142
+ work_areas_list = [wa.to_dict() for wa in raster.get_work_areas()]
143
+
144
+ image_data = {
145
+ 'path': image_path,
146
+ 'state': {
147
+ 'checkbox_state': raster.checkbox_state
148
+ },
149
+ 'work_areas': work_areas_list
150
+ }
151
+ export_images.append(image_data)
141
152
  progress_bar.update_progress()
142
153
 
143
154
  except Exception as e:
@@ -234,42 +245,6 @@ class SaveProject(QDialog):
234
245
 
235
246
  return export_annotations
236
247
 
237
- def get_workareas(self):
238
- """Get the work areas to export."""
239
- # Start progress bar
240
- total_rasters = len(self.image_window.raster_manager.image_paths)
241
- progress_bar = ProgressBar(self.annotation_window, title="Exporting Work Areas")
242
- progress_bar.show()
243
- progress_bar.start_progress(total_rasters)
244
-
245
- try:
246
- export_workareas = {}
247
-
248
- # Loop through all rasters to get their work areas
249
- for image_path in self.image_window.raster_manager.image_paths:
250
- raster = self.image_window.raster_manager.get_raster(image_path)
251
- if raster and raster.has_work_areas():
252
- work_areas_list = []
253
- for work_area in raster.get_work_areas():
254
- work_areas_list.append(work_area.to_dict())
255
-
256
- if work_areas_list: # Only add if there are work areas
257
- export_workareas[image_path] = work_areas_list
258
-
259
- progress_bar.update_progress()
260
-
261
- except Exception as e:
262
- QMessageBox.warning(self.annotation_window,
263
- "Error Exporting Work Areas",
264
- f"An error occurred while exporting work areas: {str(e)}")
265
-
266
- finally:
267
- # Stop the progress bar
268
- progress_bar.stop_progress()
269
- progress_bar.close()
270
-
271
- return export_workareas
272
-
273
248
  def get_project_path(self):
274
249
  """Get the current project path."""
275
250
  return self.current_project_path
@@ -290,4 +265,4 @@ class SaveProject(QDialog):
290
265
  """Handle dialog rejection (Cancel or close)"""
291
266
  if self.current_project_path:
292
267
  self.file_path_edit.setText(self.current_project_path)
293
- super().reject()
268
+ super().reject()
@@ -42,7 +42,7 @@ class Base(QDialog):
42
42
  self.annotation_window = main_window.annotation_window
43
43
  self.image_window = main_window.image_window
44
44
 
45
- self.resize(1000, 800)
45
+ self.resize(800, 800)
46
46
  self.setWindowIcon(get_icon("coral.png"))
47
47
  self.setWindowTitle("Export Dataset")
48
48
 
@@ -35,7 +35,7 @@ class DatasetProcessor(QObject):
35
35
  """
36
36
  status_changed = pyqtSignal(str, int)
37
37
  progress_updated = pyqtSignal(int)
38
- processing_complete = pyqtSignal(list, list)
38
+ processing_complete = pyqtSignal(list, list, list)
39
39
  error = pyqtSignal(str)
40
40
  finished = pyqtSignal()
41
41
 
@@ -47,6 +47,7 @@ class DatasetProcessor(QObject):
47
47
  self.import_as = import_as # 'rectangle' or 'polygon' (target format)
48
48
  self.rename_on_conflict = rename_on_conflict
49
49
  self.is_running = True
50
+ self.parsing_errors = [] # To collect errors instead of printing
50
51
 
51
52
  def stop(self):
52
53
  self.is_running = False
@@ -81,7 +82,7 @@ class DatasetProcessor(QObject):
81
82
 
82
83
  # Step 4: Emit results for GUI to consume
83
84
  image_paths = list(image_label_paths.keys())
84
- self.processing_complete.emit(raw_annotations, image_paths)
85
+ self.processing_complete.emit(raw_annotations, image_paths, self.parsing_errors)
85
86
 
86
87
  except Exception as e:
87
88
  # Catch-all for any error during processing
@@ -146,7 +147,7 @@ class DatasetProcessor(QObject):
146
147
  with open(label_path, 'r') as file:
147
148
  lines = file.readlines()
148
149
 
149
- for line in lines:
150
+ for line_num, line in enumerate(lines):
150
151
  try:
151
152
  parts = list(map(float, line.split()))
152
153
  class_id = int(parts[0])
@@ -195,8 +196,11 @@ class DatasetProcessor(QObject):
195
196
 
196
197
  all_raw_annotations.append(raw_ann_data)
197
198
  except (ValueError, IndexError) as e:
198
- # Skip malformed lines and print a warning
199
- print(f"Skipping malformed line in {label_path}: {line.strip()} ({e})")
199
+ # Log the malformed line error instead of printing
200
+ error_msg = (f"In file '{os.path.basename(label_path)}' on line {line_num + 1}:\n"
201
+ f"Skipped malformed content: '{line.strip()}'\nReason: {e}\n")
202
+ self.parsing_errors.append(error_msg)
203
+
200
204
 
201
205
  # Update progress after each image
202
206
  self.progress_updated.emit(i + 1)
@@ -290,11 +294,14 @@ class Base(QDialog):
290
294
  )
291
295
  if file_path:
292
296
  self.yaml_path_label.setText(file_path)
293
- # Auto-fill output directory and folder name if not set
297
+ # Auto-fill output directory to be the PARENT of the yaml's directory
294
298
  if not self.output_dir_label.text():
295
- self.output_dir_label.setText(os.path.dirname(file_path))
299
+ parent_dir = os.path.dirname(os.path.dirname(file_path))
300
+ self.output_dir_label.setText(parent_dir)
296
301
  if not self.output_folder_name.text():
297
- self.output_folder_name.setText("project")
302
+ # Suggest a folder name based on the yaml file's parent folder
303
+ project_name = os.path.basename(os.path.dirname(file_path))
304
+ self.output_folder_name.setText(f"{project_name}_imported")
298
305
 
299
306
  def browse_output_dir(self):
300
307
  """Open a dialog to select the output directory."""
@@ -309,6 +316,16 @@ class Base(QDialog):
309
316
  if not all([self.yaml_path_label.text(), self.output_dir_label.text(), self.output_folder_name.text()]):
310
317
  QMessageBox.warning(self, "Error", "Please fill in all fields.")
311
318
  return
319
+
320
+ # This check for existing output is still relevant
321
+ self.output_folder = os.path.join(self.output_dir_label.text(), self.output_folder_name.text())
322
+ if os.path.exists(self.output_folder) and os.listdir(self.output_folder):
323
+ reply = QMessageBox.question(self,
324
+ 'Directory Not Empty',
325
+ f"The directory '{self.output_folder}' is not empty. Continue?",
326
+ QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
327
+ if reply == QMessageBox.No:
328
+ return
312
329
 
313
330
  # Pre-scan for duplicates
314
331
  yaml_path = self.yaml_path_label.text()
@@ -358,16 +375,7 @@ class Base(QDialog):
358
375
  rename_files = False
359
376
  else: # User closed the dialog
360
377
  return
361
-
362
- self.output_folder = os.path.join(self.output_dir_label.text(), self.output_folder_name.text())
363
- if os.path.exists(self.output_folder) and os.listdir(self.output_folder):
364
- reply = QMessageBox.question(self,
365
- 'Directory Not Empty',
366
- f"The directory '{self.output_folder}' is not empty. Continue?",
367
- QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
368
- if reply == QMessageBox.No:
369
- return
370
-
378
+
371
379
  self.button_box.setEnabled(False)
372
380
  QApplication.setOverrideCursor(Qt.WaitCursor)
373
381
 
@@ -393,6 +401,7 @@ class Base(QDialog):
393
401
  self.worker.error.connect(self.on_error)
394
402
  self.worker.status_changed.connect(self.on_status_changed)
395
403
  self.worker.progress_updated.connect(self.on_progress_update)
404
+ # Connect to the updated signal
396
405
  self.worker.processing_complete.connect(self.on_processing_complete)
397
406
  self.thread.start()
398
407
 
@@ -403,7 +412,7 @@ class Base(QDialog):
403
412
  def on_progress_update(self, value):
404
413
  self.progress_bar.set_value(value)
405
414
 
406
- def on_processing_complete(self, raw_annotations, image_paths):
415
+ def on_processing_complete(self, raw_annotations, image_paths, parsing_errors):
407
416
  added_paths = []
408
417
  for path in image_paths:
409
418
  if self.image_window.add_image(path):
@@ -447,9 +456,20 @@ class Base(QDialog):
447
456
  self.image_window.update_image_annotations(added_paths[-1])
448
457
  self.annotation_window.load_annotations()
449
458
 
450
- QMessageBox.information(self,
451
- "Dataset Imported",
452
- "Dataset has been successfully imported.")
459
+ # --- Display a summary message, including any parsing errors ---
460
+ summary_message = "Dataset has been successfully imported."
461
+ if parsing_errors:
462
+ # If there were errors, show a more detailed dialog
463
+ QMessageBox.warning(self,
464
+ "Import Complete with Warnings",
465
+ f"{summary_message}\n\nHowever, {len(parsing_errors)} issue(s) were found "
466
+ "in the label files. Please review them below.",
467
+ details='\n'.join(parsing_errors))
468
+ else:
469
+ # Otherwise, show a simple info box
470
+ QMessageBox.information(self,
471
+ "Dataset Imported",
472
+ summary_message)
453
473
 
454
474
  def export_annotations_to_json(self, annotations_list, output_dir):
455
475
  """
@@ -320,10 +320,6 @@ class Base(QDialog):
320
320
  # If video already loaded, update output dir for widget
321
321
  if self.video_path:
322
322
  self.video_region_widget.load_video(self.video_path, dir_name)
323
- else:
324
- self.update_record_buttons()
325
- else:
326
- self.update_record_buttons()
327
323
 
328
324
  def browse_model(self):
329
325
  """Open file dialog to select model file (filtered to .pt, .pth)."""
@@ -91,9 +91,20 @@ class GlobalEventFilter(QObject):
91
91
 
92
92
  # Delete (backspace or delete key) selected annotations when select tool is active
93
93
  if event.key() == Qt.Key_Delete or event.key() == Qt.Key_Backspace:
94
+ # First check if the select tool is active
94
95
  if self.main_window.select_tool_action.isChecked():
96
+ selected_tool = self.annotation_window.selected_tool
97
+ select_tool = self.annotation_window.tools[selected_tool]
98
+ # Get the active subtool if it exists, pass to its keyPressEvent
99
+ if hasattr(select_tool, 'active_subtool') and select_tool.active_subtool:
100
+ select_tool.active_subtool.keyPressEvent(event)
101
+ return True
102
+
103
+ # Otherwise, proceed with deletion if there are selected annotations
95
104
  if self.annotation_window.selected_annotations:
96
105
  self.annotation_window.delete_selected_annotations()
106
+ return True
107
+
97
108
  # Consume the event so it doesn't do anything else
98
109
  return True
99
110
 
@@ -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
  #
@@ -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(
@@ -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
@@ -2290,34 +2290,10 @@ class MainWindow(QMainWindow):
2290
2290
  "No images are present in the project.")
2291
2291
  return
2292
2292
 
2293
- if not self.annotation_window.annotations_dict:
2293
+ if len(self.label_window.labels) <= 1:
2294
2294
  QMessageBox.warning(self,
2295
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.")
2296
+ "At least one reference label is required for reference.")
2321
2297
  return
2322
2298
 
2323
2299
  try:
@@ -2518,7 +2494,9 @@ class MainWindow(QMainWindow):
2518
2494
  msg.setWindowIcon(self.coral_icon)
2519
2495
  msg.setWindowTitle("GDI Limit Reached")
2520
2496
  msg.setText(
2521
- "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."
2522
2500
  )
2523
2501
  msg.setStandardButtons(QMessageBox.Ok)
2524
2502
  msg.exec_()