coralnet-toolbox 0.0.73__py2.py3-none-any.whl → 0.0.75__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/QtDataItem.py +52 -22
- coralnet_toolbox/Explorer/QtExplorer.py +293 -1614
- coralnet_toolbox/Explorer/QtSettingsWidgets.py +203 -85
- coralnet_toolbox/Explorer/QtViewers.py +1568 -0
- coralnet_toolbox/Explorer/transformer_models.py +59 -0
- coralnet_toolbox/Explorer/yolo_models.py +112 -0
- 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 +253 -141
- coralnet_toolbox/MachineLearning/VideoInference/QtBase.py +0 -4
- coralnet_toolbox/MachineLearning/VideoInference/YOLO3D/run.py +102 -16
- coralnet_toolbox/QtAnnotationWindow.py +16 -10
- coralnet_toolbox/QtEventFilter.py +11 -0
- coralnet_toolbox/QtImageWindow.py +120 -75
- coralnet_toolbox/QtLabelWindow.py +13 -1
- coralnet_toolbox/QtMainWindow.py +5 -27
- coralnet_toolbox/QtProgressBar.py +52 -27
- coralnet_toolbox/Rasters/RasterTableModel.py +28 -8
- coralnet_toolbox/SAM/QtDeployGenerator.py +1 -4
- coralnet_toolbox/SAM/QtDeployPredictor.py +11 -3
- coralnet_toolbox/SeeAnything/QtDeployGenerator.py +805 -162
- coralnet_toolbox/SeeAnything/QtDeployPredictor.py +130 -151
- coralnet_toolbox/Tools/QtCutSubTool.py +18 -2
- coralnet_toolbox/Tools/QtPolygonTool.py +42 -3
- coralnet_toolbox/Tools/QtRectangleTool.py +30 -0
- 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 +158 -47
- coralnet_toolbox-0.0.75.dist-info/METADATA +378 -0
- {coralnet_toolbox-0.0.73.dist-info → coralnet_toolbox-0.0.75.dist-info}/RECORD +49 -44
- coralnet_toolbox-0.0.73.dist-info/METADATA +0 -341
- {coralnet_toolbox-0.0.73.dist-info → coralnet_toolbox-0.0.75.dist-info}/WHEEL +0 -0
- {coralnet_toolbox-0.0.73.dist-info → coralnet_toolbox-0.0.75.dist-info}/entry_points.txt +0 -0
- {coralnet_toolbox-0.0.73.dist-info → coralnet_toolbox-0.0.75.dist-info}/licenses/LICENSE.txt +0 -0
- {coralnet_toolbox-0.0.73.dist-info → coralnet_toolbox-0.0.75.dist-info}/top_level.txt +0 -0
@@ -189,6 +189,21 @@ Examples:
|
|
189
189
|
help='Filter by specific class IDs (e.g., --classes 0 1 2 for persons, bicycles, cars)'
|
190
190
|
)
|
191
191
|
|
192
|
+
# Frame range parameters
|
193
|
+
parser.add_argument(
|
194
|
+
'--start_at',
|
195
|
+
type=int,
|
196
|
+
default=0,
|
197
|
+
help='Start processing at this frame number (0-based)'
|
198
|
+
)
|
199
|
+
parser.add_argument(
|
200
|
+
'--end_at',
|
201
|
+
type=int,
|
202
|
+
default=None,
|
203
|
+
help='End processing at this frame number (inclusive, 0-based). If not provided, '
|
204
|
+
'process until the end of the video.'
|
205
|
+
)
|
206
|
+
|
192
207
|
# Device settings
|
193
208
|
parser.add_argument(
|
194
209
|
'--device',
|
@@ -222,6 +237,8 @@ Examples:
|
|
222
237
|
action='store_true',
|
223
238
|
help='Show all visualization windows (result, depth, detection). By default, only the result frame is shown.'
|
224
239
|
)
|
240
|
+
|
241
|
+
|
225
242
|
return parser.parse_args()
|
226
243
|
|
227
244
|
|
@@ -271,19 +288,25 @@ def main():
|
|
271
288
|
enable_bev = not args.no_bev
|
272
289
|
enable_display = not args.no_display
|
273
290
|
show_all_frames = args.yes_display
|
291
|
+
|
292
|
+
# Frame range parameters
|
293
|
+
start_frame = args.start_at
|
294
|
+
end_frame = args.end_at
|
295
|
+
|
274
296
|
# Camera parameters - simplified approach
|
275
297
|
camera_params_file = None # Path to camera parameters file (None to use default parameters)
|
276
298
|
# ===============================================
|
277
|
-
print(
|
299
|
+
print("\nConfiguration:")
|
278
300
|
print(f"Input source: {source}")
|
279
301
|
print(f"Output path: {output_path}")
|
280
302
|
if target_size is not None:
|
281
303
|
print(f"Target size: {target_size}px (longest edge)")
|
282
304
|
else:
|
283
|
-
print(
|
305
|
+
print("Using original resolution (no scaling)")
|
284
306
|
print(f"YOLO model: {'Custom path: ' + yolo_model_path if yolo_model_path else 'Size: ' + yolo_model_size}")
|
285
307
|
print(f"Depth model size: {depth_model_size}")
|
286
308
|
print(f"Device: {device}")
|
309
|
+
print(f"Frame range: {start_frame} to {end_frame if end_frame is not None else 'end'}")
|
287
310
|
print(f"Tracking: {'enabled' if enable_tracking else 'disabled'}")
|
288
311
|
print(f"Bird's Eye View: {'enabled' if enable_bev else 'disabled'}")
|
289
312
|
print(f"Display: {'enabled' if enable_display else 'disabled'}")
|
@@ -382,9 +405,52 @@ def main():
|
|
382
405
|
# Use a scale that works well for the 1-5 meter range
|
383
406
|
bev = BirdEyeView(image_shape=(width, height), scale=100) # Increased scale to spread objects out
|
384
407
|
|
385
|
-
# Initialize video writer
|
386
|
-
|
387
|
-
|
408
|
+
# Initialize video writer - use a more reliable codec and check output path
|
409
|
+
output_dir = os.path.dirname(output_path)
|
410
|
+
if output_dir and not os.path.exists(output_dir):
|
411
|
+
os.makedirs(output_dir)
|
412
|
+
|
413
|
+
# On Windows, try different codec options
|
414
|
+
if sys.platform == 'win32':
|
415
|
+
try:
|
416
|
+
# First try H264 codec
|
417
|
+
fourcc = cv2.VideoWriter_fourcc(*'H264')
|
418
|
+
out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
|
419
|
+
|
420
|
+
# Check if writer was successfully initialized
|
421
|
+
if not out.isOpened():
|
422
|
+
# Fallback to XVID codec
|
423
|
+
fourcc = cv2.VideoWriter_fourcc(*'XVID')
|
424
|
+
out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
|
425
|
+
|
426
|
+
if not out.isOpened():
|
427
|
+
# Last resort, try MJPG
|
428
|
+
fourcc = cv2.VideoWriter_fourcc(*'MJPG')
|
429
|
+
out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
|
430
|
+
|
431
|
+
if not out.isOpened():
|
432
|
+
print(f"Warning: Could not create output video with standard codecs. Trying AVI format.")
|
433
|
+
# Try changing extension to .avi
|
434
|
+
output_path = os.path.splitext(output_path)[0] + '.avi'
|
435
|
+
fourcc = cv2.VideoWriter_fourcc(*'XVID')
|
436
|
+
out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
|
437
|
+
except Exception as e:
|
438
|
+
print(f"Error initializing video writer: {e}")
|
439
|
+
# Last fallback to MP4V
|
440
|
+
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
441
|
+
out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
|
442
|
+
else:
|
443
|
+
# For other platforms, use mp4v codec
|
444
|
+
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
445
|
+
out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
|
446
|
+
|
447
|
+
# Check if writer was successfully initialized
|
448
|
+
if not out.isOpened():
|
449
|
+
print(f"Error: Could not create output video file at {output_path}")
|
450
|
+
print("Continuing without saving output video.")
|
451
|
+
out = None
|
452
|
+
else:
|
453
|
+
print(f"Successfully opened output video file: {output_path}")
|
388
454
|
|
389
455
|
# Initialize variables for FPS calculation
|
390
456
|
frame_count = 0
|
@@ -394,6 +460,7 @@ def main():
|
|
394
460
|
print("Starting processing...")
|
395
461
|
|
396
462
|
# Main loop
|
463
|
+
current_frame = 0
|
397
464
|
while True:
|
398
465
|
# Check for key press at the beginning of each loop
|
399
466
|
if enable_display:
|
@@ -401,11 +468,22 @@ def main():
|
|
401
468
|
if key == ord('q') or key == 27 or (key & 0xFF) == ord('q') or (key & 0xFF) == 27:
|
402
469
|
print("Exiting program...")
|
403
470
|
break
|
404
|
-
try:
|
471
|
+
try:
|
472
|
+
# Read frame
|
405
473
|
ret, frame = cap.read()
|
406
474
|
if not ret:
|
407
475
|
break
|
408
476
|
|
477
|
+
# Skip frames before start_frame
|
478
|
+
if current_frame < start_frame:
|
479
|
+
current_frame += 1
|
480
|
+
continue
|
481
|
+
|
482
|
+
# Stop if we've processed the end_frame
|
483
|
+
if end_frame is not None and current_frame > end_frame:
|
484
|
+
print(f"Reached end frame {end_frame}, stopping processing.")
|
485
|
+
break
|
486
|
+
|
409
487
|
# Apply resizing if needed
|
410
488
|
if target_size is not None:
|
411
489
|
frame = cv2.resize(frame, (width, height))
|
@@ -543,7 +621,7 @@ def main():
|
|
543
621
|
fps_display = f"FPS: {fps_value:.1f}"
|
544
622
|
|
545
623
|
# Add FPS and device info to the result frame (top-right corner)
|
546
|
-
text = f"{fps_display} | Device: {device}"
|
624
|
+
text = f"{fps_display} | Device: {device} | Frame: {current_frame}"
|
547
625
|
|
548
626
|
# Calculate text size for right alignment
|
549
627
|
(text_width, text_height), _ = cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, 0.7, 2)
|
@@ -584,8 +662,9 @@ def main():
|
|
584
662
|
except Exception as e:
|
585
663
|
print(f"Error adding depth map to result: {e}")
|
586
664
|
|
587
|
-
# Write frame to output video
|
588
|
-
out.
|
665
|
+
# Write frame to output video (only if writer is valid)
|
666
|
+
if out is not None and out.isOpened():
|
667
|
+
out.write(result_frame)
|
589
668
|
|
590
669
|
# Display frames only if display is enabled
|
591
670
|
if enable_display:
|
@@ -594,12 +673,15 @@ def main():
|
|
594
673
|
cv2.imshow("Depth Map", depth_colored)
|
595
674
|
cv2.imshow("Object Detection", detection_frame)
|
596
675
|
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
676
|
+
# Check for key press again at the end of the loop
|
677
|
+
key = cv2.waitKey(1)
|
678
|
+
if key == ord('q') or key == 27 or (key & 0xFF) == ord('q') or (key & 0xFF) == 27:
|
679
|
+
print("Exiting program...")
|
680
|
+
break
|
681
|
+
|
682
|
+
# Increment frame counter
|
683
|
+
current_frame += 1
|
684
|
+
|
603
685
|
except Exception as e:
|
604
686
|
print(f"Error processing frame: {e}")
|
605
687
|
# Also check for key press during exception handling
|
@@ -608,12 +690,16 @@ def main():
|
|
608
690
|
if key == ord('q') or key == 27 or (key & 0xFF) == ord('q') or (key & 0xFF) == 27:
|
609
691
|
print("Exiting program...")
|
610
692
|
break
|
693
|
+
|
694
|
+
# Still increment frame counter even if there was an error
|
695
|
+
current_frame += 1
|
611
696
|
continue
|
612
697
|
|
613
698
|
# Clean up
|
614
699
|
print("Cleaning up resources...")
|
615
700
|
cap.release()
|
616
|
-
out
|
701
|
+
if out is not None:
|
702
|
+
out.release()
|
617
703
|
cv2.destroyAllWindows()
|
618
704
|
|
619
705
|
print(f"Processing complete. Output saved to {output_path}")
|
@@ -492,9 +492,6 @@ class AnnotationWindow(QGraphicsView):
|
|
492
492
|
relative_area = 1.0 # fallback, treat as full image
|
493
493
|
|
494
494
|
# Step 3: Map ratio to padding factor (smaller annotation = more padding)
|
495
|
-
# Example: padding_factor = clamp(0.5 * (1/relative_area)**0.5, 0.1, 0.5)
|
496
|
-
# - For very small annotations, padding approaches 0.5 (50%)
|
497
|
-
# - For large annotations, padding approaches 0.1 (10%)
|
498
495
|
import math
|
499
496
|
min_padding = 0.1 # 10%
|
500
497
|
max_padding = 0.5 # 50%
|
@@ -503,23 +500,32 @@ class AnnotationWindow(QGraphicsView):
|
|
503
500
|
else:
|
504
501
|
padding_factor = min_padding
|
505
502
|
|
506
|
-
# Step 4: Apply dynamic padding
|
507
|
-
|
508
|
-
|
503
|
+
# Step 4: Apply dynamic padding with minimum values to prevent zero width/height
|
504
|
+
min_padding_absolute = 1.0 # Minimum padding in pixels
|
505
|
+
padding_x = max(annotation_rect.width() * padding_factor, min_padding_absolute)
|
506
|
+
padding_y = max(annotation_rect.height() * padding_factor, min_padding_absolute)
|
509
507
|
padded_rect = annotation_rect.adjusted(-padding_x, -padding_y, padding_x, padding_y)
|
510
508
|
|
511
509
|
# Fit the padded annotation rect in the view
|
512
510
|
self.fitInView(padded_rect, Qt.KeepAspectRatio)
|
513
511
|
|
514
|
-
# Update the zoom factor based on the new view transformation
|
512
|
+
# Update the zoom factor based on the new view transformation with safety checks
|
515
513
|
view_rect = self.viewport().rect()
|
516
|
-
|
517
|
-
|
514
|
+
if padded_rect.width() > 0:
|
515
|
+
zoom_x = view_rect.width() / padded_rect.width()
|
516
|
+
else:
|
517
|
+
zoom_x = 1.0 # Default zoom if width is zero
|
518
|
+
|
519
|
+
if padded_rect.height() > 0:
|
520
|
+
zoom_y = view_rect.height() / padded_rect.height()
|
521
|
+
else:
|
522
|
+
zoom_y = 1.0 # Default zoom if height is zero
|
523
|
+
|
518
524
|
self.zoom_factor = min(zoom_x, zoom_y)
|
519
525
|
|
520
526
|
# Signal that the view has changed
|
521
527
|
self.viewChanged.emit(*self.get_image_dimensions())
|
522
|
-
|
528
|
+
|
523
529
|
def cycle_annotations(self, direction):
|
524
530
|
"""Cycle through annotations in the specified direction."""
|
525
531
|
# Get the annotations for the current image
|
@@ -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
|
#
|
@@ -547,13 +578,9 @@ class ImageWindow(QWidget):
|
|
547
578
|
|
548
579
|
# Immediately update filtered paths to include the new image
|
549
580
|
# This ensures the image will be visible in the table right away
|
550
|
-
if self.table_model.filtered_paths == self.raster_manager.image_paths
|
551
|
-
# No filters are active, so
|
552
|
-
self.table_model.
|
553
|
-
self.table_model.dataChanged.emit(
|
554
|
-
self.table_model.index(0, 0),
|
555
|
-
self.table_model.index(len(self.table_model.filtered_paths) - 1,
|
556
|
-
self.table_model.columnCount() - 1))
|
581
|
+
if len(self.table_model.filtered_paths) == len(self.raster_manager.image_paths) - 1:
|
582
|
+
# No filters are active, so efficiently add the new path to the model
|
583
|
+
self.table_model.add_path(image_path)
|
557
584
|
else:
|
558
585
|
# Filters are active, so run filtering again
|
559
586
|
self.filter_images()
|
@@ -960,28 +987,46 @@ class ImageWindow(QWidget):
|
|
960
987
|
|
961
988
|
def show_context_menu(self, position):
|
962
989
|
"""
|
963
|
-
Show the context menu for the table.
|
990
|
+
Show the context menu for the table, including the toggle check state action.
|
964
991
|
|
965
992
|
Args:
|
966
993
|
position (QPoint): Position to show the menu
|
967
994
|
"""
|
995
|
+
# Get the path corresponding to the right-clicked row
|
996
|
+
index = self.tableView.indexAt(position)
|
997
|
+
path_at_cursor = self.table_model.get_path_at_row(index.row()) if index.isValid() else None
|
998
|
+
|
999
|
+
# Get the currently highlighted paths from the model
|
968
1000
|
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)
|
1001
|
+
|
1002
|
+
# If the user right-clicked on a row that wasn't already highlighted,
|
1003
|
+
# then we assume they want to act on this row alone.
|
1004
|
+
if path_at_cursor and path_at_cursor not in highlighted_paths:
|
1005
|
+
self.table_model.set_highlighted_paths([path_at_cursor])
|
1006
|
+
self.last_highlighted_row = index.row()
|
1007
|
+
highlighted_paths = [path_at_cursor]
|
1008
|
+
|
1009
|
+
# If no rows are highlighted, do nothing.
|
981
1010
|
if not highlighted_paths:
|
982
1011
|
return
|
1012
|
+
|
983
1013
|
context_menu = QMenu(self)
|
984
1014
|
count = len(highlighted_paths)
|
1015
|
+
|
1016
|
+
# Add the check/uncheck action
|
1017
|
+
raster_under_cursor = self.raster_manager.get_raster(path_at_cursor)
|
1018
|
+
if raster_under_cursor:
|
1019
|
+
is_checked = raster_under_cursor.checkbox_state
|
1020
|
+
if is_checked:
|
1021
|
+
action_text = f"Uncheck {count} Highlighted Image{'s' if count > 1 else ''}"
|
1022
|
+
else:
|
1023
|
+
action_text = f"Check {count} Highlighted Image{'s' if count > 1 else ''}"
|
1024
|
+
toggle_check_action = context_menu.addAction(action_text)
|
1025
|
+
toggle_check_action.triggered.connect(lambda: self.on_toggle(not is_checked))
|
1026
|
+
|
1027
|
+
context_menu.addSeparator()
|
1028
|
+
|
1029
|
+
# Add existing delete actions
|
985
1030
|
delete_images_action = context_menu.addAction(f"Delete {count} Highlighted Image{'s' if count > 1 else ''}")
|
986
1031
|
delete_images_action.triggered.connect(lambda: self.delete_highlighted_images())
|
987
1032
|
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_()
|