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.
- coralnet_toolbox/Annotations/QtAnnotation.py +28 -69
- coralnet_toolbox/Annotations/QtMaskAnnotation.py +408 -0
- coralnet_toolbox/Annotations/QtMultiPolygonAnnotation.py +72 -56
- coralnet_toolbox/Annotations/QtPatchAnnotation.py +165 -216
- coralnet_toolbox/Annotations/QtPolygonAnnotation.py +497 -353
- coralnet_toolbox/Annotations/QtRectangleAnnotation.py +126 -116
- coralnet_toolbox/CoralNet/QtDownload.py +2 -1
- coralnet_toolbox/Explorer/QtExplorer.py +16 -14
- coralnet_toolbox/Explorer/QtSettingsWidgets.py +114 -82
- coralnet_toolbox/IO/QtExportTagLabAnnotations.py +30 -10
- coralnet_toolbox/IO/QtImportTagLabAnnotations.py +21 -15
- coralnet_toolbox/IO/QtOpenProject.py +46 -78
- coralnet_toolbox/IO/QtSaveProject.py +18 -43
- coralnet_toolbox/MachineLearning/ExportDataset/QtBase.py +1 -1
- coralnet_toolbox/MachineLearning/ImportDataset/QtBase.py +42 -22
- coralnet_toolbox/MachineLearning/VideoInference/QtBase.py +0 -4
- coralnet_toolbox/QtEventFilter.py +11 -0
- coralnet_toolbox/QtImageWindow.py +117 -68
- coralnet_toolbox/QtLabelWindow.py +13 -1
- coralnet_toolbox/QtMainWindow.py +5 -27
- coralnet_toolbox/QtProgressBar.py +52 -27
- coralnet_toolbox/Rasters/RasterTableModel.py +8 -8
- coralnet_toolbox/SAM/QtDeployPredictor.py +10 -0
- coralnet_toolbox/SeeAnything/QtDeployGenerator.py +779 -161
- coralnet_toolbox/SeeAnything/QtDeployPredictor.py +86 -149
- coralnet_toolbox/Tools/QtCutSubTool.py +18 -2
- coralnet_toolbox/Tools/QtResizeSubTool.py +19 -2
- coralnet_toolbox/Tools/QtSAMTool.py +72 -50
- coralnet_toolbox/Tools/QtSeeAnythingTool.py +8 -5
- coralnet_toolbox/Tools/QtSelectTool.py +27 -3
- coralnet_toolbox/Tools/QtSubtractSubTool.py +66 -0
- coralnet_toolbox/Tools/__init__.py +2 -0
- coralnet_toolbox/__init__.py +1 -1
- coralnet_toolbox/utilities.py +137 -47
- coralnet_toolbox-0.0.74.dist-info/METADATA +375 -0
- {coralnet_toolbox-0.0.73.dist-info → coralnet_toolbox-0.0.74.dist-info}/RECORD +40 -38
- coralnet_toolbox-0.0.73.dist-info/METADATA +0 -341
- {coralnet_toolbox-0.0.73.dist-info → coralnet_toolbox-0.0.74.dist-info}/WHEEL +0 -0
- {coralnet_toolbox-0.0.73.dist-info → coralnet_toolbox-0.0.74.dist-info}/entry_points.txt +0 -0
- {coralnet_toolbox-0.0.73.dist-info → coralnet_toolbox-0.0.74.dist-info}/licenses/LICENSE.txt +0 -0
- {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
|
-
'
|
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
|
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
|
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
|
-
|
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(
|
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
|
-
#
|
199
|
-
|
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
|
297
|
+
# Auto-fill output directory to be the PARENT of the yaml's directory
|
294
298
|
if not self.output_dir_label.text():
|
295
|
-
|
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
|
-
|
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
|
-
|
451
|
-
|
452
|
-
|
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.
|
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,
|
14
|
-
|
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
|
260
|
-
self.tableView.horizontalHeader().setSectionResizeMode(0, QHeaderView.
|
261
|
-
self.tableView.
|
262
|
-
self.tableView.
|
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.
|
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
|
-
|
429
|
-
|
430
|
-
|
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
|
-
|
435
|
-
|
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
|
-
#
|
463
|
-
|
464
|
-
self.table_model.
|
465
|
-
self.last_highlighted_row =
|
466
|
-
|
467
|
-
|
468
|
-
|
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
|
-
|
970
|
-
|
971
|
-
|
972
|
-
|
973
|
-
|
974
|
-
|
975
|
-
|
976
|
-
|
977
|
-
|
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
|
-
"""
|
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
|
coralnet_toolbox/QtMainWindow.py
CHANGED
@@ -2290,34 +2290,10 @@ class MainWindow(QMainWindow):
|
|
2290
2290
|
"No images are present in the project.")
|
2291
2291
|
return
|
2292
2292
|
|
2293
|
-
if
|
2293
|
+
if len(self.label_window.labels) <= 1:
|
2294
2294
|
QMessageBox.warning(self,
|
2295
2295
|
"See Anything (YOLOE)",
|
2296
|
-
"
|
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
|
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_()
|