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.
- 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/AutoDistill/QtDeployModel.py +23 -12
- coralnet_toolbox/CoralNet/QtDownload.py +2 -1
- coralnet_toolbox/Explorer/QtDataItem.py +1 -1
- coralnet_toolbox/Explorer/QtExplorer.py +159 -17
- coralnet_toolbox/Explorer/QtSettingsWidgets.py +160 -86
- 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/DeployModel/QtDetect.py +22 -11
- coralnet_toolbox/MachineLearning/DeployModel/QtSegment.py +22 -10
- coralnet_toolbox/MachineLearning/ExportDataset/QtBase.py +61 -24
- coralnet_toolbox/MachineLearning/ExportDataset/QtClassify.py +5 -1
- coralnet_toolbox/MachineLearning/ExportDataset/QtDetect.py +19 -6
- coralnet_toolbox/MachineLearning/ExportDataset/QtSegment.py +21 -8
- coralnet_toolbox/MachineLearning/ImportDataset/QtBase.py +42 -22
- coralnet_toolbox/MachineLearning/VideoInference/QtBase.py +0 -4
- coralnet_toolbox/QtAnnotationWindow.py +42 -14
- coralnet_toolbox/QtEventFilter.py +19 -2
- coralnet_toolbox/QtImageWindow.py +134 -86
- coralnet_toolbox/QtLabelWindow.py +14 -2
- coralnet_toolbox/QtMainWindow.py +122 -9
- coralnet_toolbox/QtProgressBar.py +52 -27
- coralnet_toolbox/Rasters/QtRaster.py +59 -7
- coralnet_toolbox/Rasters/RasterTableModel.py +42 -14
- coralnet_toolbox/SAM/QtBatchInference.py +0 -2
- coralnet_toolbox/SAM/QtDeployGenerator.py +22 -11
- coralnet_toolbox/SAM/QtDeployPredictor.py +10 -0
- coralnet_toolbox/SeeAnything/QtBatchInference.py +19 -221
- coralnet_toolbox/SeeAnything/QtDeployGenerator.py +1634 -0
- coralnet_toolbox/SeeAnything/QtDeployPredictor.py +107 -154
- coralnet_toolbox/SeeAnything/QtTrainModel.py +115 -45
- coralnet_toolbox/SeeAnything/__init__.py +2 -0
- coralnet_toolbox/Tools/QtCutSubTool.py +18 -2
- coralnet_toolbox/Tools/QtResizeSubTool.py +19 -2
- coralnet_toolbox/Tools/QtSAMTool.py +222 -57
- coralnet_toolbox/Tools/QtSeeAnythingTool.py +223 -55
- coralnet_toolbox/Tools/QtSelectSubTool.py +6 -4
- coralnet_toolbox/Tools/QtSelectTool.py +27 -3
- coralnet_toolbox/Tools/QtSubtractSubTool.py +66 -0
- coralnet_toolbox/Tools/QtWorkAreaTool.py +25 -13
- 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.72.dist-info → coralnet_toolbox-0.0.74.dist-info}/RECORD +56 -53
- coralnet_toolbox-0.0.72.dist-info/METADATA +0 -341
- {coralnet_toolbox-0.0.72.dist-info → coralnet_toolbox-0.0.74.dist-info}/WHEEL +0 -0
- {coralnet_toolbox-0.0.72.dist-info → coralnet_toolbox-0.0.74.dist-info}/entry_points.txt +0 -0
- {coralnet_toolbox-0.0.72.dist-info → coralnet_toolbox-0.0.74.dist-info}/licenses/LICENSE.txt +0 -0
- {coralnet_toolbox-0.0.72.dist-info → coralnet_toolbox-0.0.74.dist-info}/top_level.txt +0 -0
@@ -71,9 +71,17 @@ class SeeAnythingTool(Tool):
|
|
71
71
|
self.current_rect_graphics = None # For the rectangle currently being drawn
|
72
72
|
self.rectangles_processed = False # Track if rectangles have been processed
|
73
73
|
|
74
|
+
# Add state variables for custom working area creation
|
75
|
+
self.creating_working_area = False
|
76
|
+
self.working_area_start = None
|
77
|
+
self.working_area_temp_graphics = None
|
78
|
+
|
79
|
+
# Add hover position tracking
|
80
|
+
self.hover_pos = None
|
81
|
+
|
74
82
|
self.annotations = []
|
75
83
|
self.results = None
|
76
|
-
|
84
|
+
|
77
85
|
def activate(self):
|
78
86
|
"""
|
79
87
|
Activates the tool.
|
@@ -98,6 +106,9 @@ class SeeAnythingTool(Tool):
|
|
98
106
|
|
99
107
|
# Clean up working area and shadow
|
100
108
|
self.cancel_working_area()
|
109
|
+
|
110
|
+
# Cancel working area creation if in progress
|
111
|
+
self.cancel_working_area_creation()
|
101
112
|
|
102
113
|
# Clear detection data
|
103
114
|
self.results = None
|
@@ -162,9 +173,121 @@ class SeeAnythingTool(Tool):
|
|
162
173
|
|
163
174
|
# Set the image in the SeeAnything dialog
|
164
175
|
self.see_anything_dialog.set_image(self.work_area_image, self.image_path)
|
176
|
+
# self.see_anything_dialog.reload_model()
|
165
177
|
|
166
178
|
self.annotation_window.setCursor(Qt.CrossCursor)
|
167
179
|
self.annotation_window.scene.update()
|
180
|
+
|
181
|
+
def set_custom_working_area(self, start_point, end_point):
|
182
|
+
"""
|
183
|
+
Create a working area from custom points selected by the user.
|
184
|
+
|
185
|
+
Args:
|
186
|
+
start_point (QPointF): First corner of the working area
|
187
|
+
end_point (QPointF): Opposite corner of the working area
|
188
|
+
"""
|
189
|
+
self.annotation_window.setCursor(Qt.WaitCursor)
|
190
|
+
|
191
|
+
# Cancel any existing working area
|
192
|
+
self.cancel_working_area()
|
193
|
+
|
194
|
+
# Calculate the rectangle bounds
|
195
|
+
left = max(0, int(min(start_point.x(), end_point.x())))
|
196
|
+
top = max(0, int(min(start_point.y(), end_point.y())))
|
197
|
+
right = min(int(self.annotation_window.pixmap_image.size().width()),
|
198
|
+
int(max(start_point.x(), end_point.x())))
|
199
|
+
bottom = min(int(self.annotation_window.pixmap_image.size().height()),
|
200
|
+
int(max(start_point.y(), end_point.y())))
|
201
|
+
|
202
|
+
# Ensure minimum size (at least 10x10 pixels)
|
203
|
+
if right - left < 10:
|
204
|
+
right = min(left + 10, int(self.annotation_window.pixmap_image.size().width()))
|
205
|
+
if bottom - top < 10:
|
206
|
+
bottom = min(top + 10, int(self.annotation_window.pixmap_image.size().height()))
|
207
|
+
|
208
|
+
# Original image information
|
209
|
+
self.image_path = self.annotation_window.current_image_path
|
210
|
+
self.original_image = pixmap_to_numpy(self.annotation_window.pixmap_image)
|
211
|
+
self.original_width = self.annotation_window.pixmap_image.size().width()
|
212
|
+
self.original_height = self.annotation_window.pixmap_image.size().height()
|
213
|
+
|
214
|
+
# Create the WorkArea instance
|
215
|
+
self.working_area = WorkArea(left, top, right - left, bottom - top, self.image_path)
|
216
|
+
|
217
|
+
# Get the thickness for the working area graphics
|
218
|
+
pen_width = self.graphics_utility.get_workarea_thickness(self.annotation_window)
|
219
|
+
|
220
|
+
# Create and add the working area graphics
|
221
|
+
self.working_area.create_graphics(self.annotation_window.scene, pen_width)
|
222
|
+
self.working_area.set_remove_button_visibility(False)
|
223
|
+
self.working_area.removed.connect(self.on_working_area_removed)
|
224
|
+
|
225
|
+
# Create shadow overlay
|
226
|
+
shadow_brush = QBrush(QColor(0, 0, 0, 150))
|
227
|
+
shadow_path = QPainterPath()
|
228
|
+
shadow_path.addRect(self.annotation_window.scene.sceneRect())
|
229
|
+
shadow_path.addRect(self.working_area.rect)
|
230
|
+
shadow_path = shadow_path.simplified()
|
231
|
+
|
232
|
+
self.shadow_area = QGraphicsPathItem(shadow_path)
|
233
|
+
self.shadow_area.setBrush(shadow_brush)
|
234
|
+
self.shadow_area.setPen(QPen(Qt.NoPen))
|
235
|
+
self.annotation_window.scene.addItem(self.shadow_area)
|
236
|
+
|
237
|
+
# Crop the image based on the working area
|
238
|
+
self.work_area_image = self.original_image[top:bottom, left:right]
|
239
|
+
|
240
|
+
# Set the image in the SeeAnything dialog
|
241
|
+
self.see_anything_dialog.set_image(self.work_area_image, self.image_path)
|
242
|
+
|
243
|
+
self.annotation_window.setCursor(Qt.CrossCursor)
|
244
|
+
self.annotation_window.scene.update()
|
245
|
+
|
246
|
+
def display_working_area_preview(self, current_pos):
|
247
|
+
"""
|
248
|
+
Display a preview rectangle for the working area being created.
|
249
|
+
|
250
|
+
Args:
|
251
|
+
current_pos (QPointF): Current mouse position
|
252
|
+
"""
|
253
|
+
if not self.working_area_start:
|
254
|
+
return
|
255
|
+
|
256
|
+
# Remove previous preview if it exists
|
257
|
+
if self.working_area_temp_graphics:
|
258
|
+
self.annotation_window.scene.removeItem(self.working_area_temp_graphics)
|
259
|
+
self.working_area_temp_graphics = None
|
260
|
+
|
261
|
+
# Create preview rectangle
|
262
|
+
rect = QRectF(
|
263
|
+
min(self.working_area_start.x(), current_pos.x()),
|
264
|
+
min(self.working_area_start.y(), current_pos.y()),
|
265
|
+
abs(current_pos.x() - self.working_area_start.x()),
|
266
|
+
abs(current_pos.y() - self.working_area_start.y())
|
267
|
+
)
|
268
|
+
|
269
|
+
# Create a dashed blue pen for the working area preview
|
270
|
+
pen = QPen(QColor(0, 120, 215))
|
271
|
+
pen.setStyle(Qt.DashLine)
|
272
|
+
pen.setWidth(2)
|
273
|
+
|
274
|
+
self.working_area_temp_graphics = QGraphicsRectItem(rect)
|
275
|
+
self.working_area_temp_graphics.setPen(pen)
|
276
|
+
self.working_area_temp_graphics.setBrush(QBrush(QColor(0, 120, 215, 30))) # Light blue transparent fill
|
277
|
+
self.annotation_window.scene.addItem(self.working_area_temp_graphics)
|
278
|
+
|
279
|
+
def cancel_working_area_creation(self):
|
280
|
+
"""
|
281
|
+
Cancel the process of creating a working area.
|
282
|
+
"""
|
283
|
+
self.creating_working_area = False
|
284
|
+
self.working_area_start = None
|
285
|
+
|
286
|
+
if self.working_area_temp_graphics:
|
287
|
+
self.annotation_window.scene.removeItem(self.working_area_temp_graphics)
|
288
|
+
self.working_area_temp_graphics = None
|
289
|
+
|
290
|
+
self.annotation_window.scene.update()
|
168
291
|
|
169
292
|
def on_working_area_removed(self, work_area):
|
170
293
|
"""
|
@@ -277,12 +400,25 @@ class SeeAnythingTool(Tool):
|
|
277
400
|
"A label must be selected before adding an annotation.")
|
278
401
|
return None
|
279
402
|
|
403
|
+
# Get position in scene coordinates
|
404
|
+
scene_pos = self.annotation_window.mapToScene(event.pos())
|
405
|
+
|
406
|
+
# Handle working area creation mode
|
407
|
+
if not self.working_area and event.button() == Qt.LeftButton:
|
408
|
+
if not self.creating_working_area:
|
409
|
+
# Start working area creation
|
410
|
+
self.creating_working_area = True
|
411
|
+
self.working_area_start = scene_pos
|
412
|
+
return
|
413
|
+
elif self.creating_working_area and self.working_area_start:
|
414
|
+
# Finish working area creation
|
415
|
+
self.set_custom_working_area(self.working_area_start, scene_pos)
|
416
|
+
self.cancel_working_area_creation()
|
417
|
+
return
|
418
|
+
|
280
419
|
if not self.working_area:
|
281
420
|
return
|
282
421
|
|
283
|
-
# Position in the scene
|
284
|
-
scene_pos = self.annotation_window.mapToScene(event.pos())
|
285
|
-
|
286
422
|
# Check if the position is within the working area
|
287
423
|
if not self.working_area.contains_point(scene_pos):
|
288
424
|
return
|
@@ -319,6 +455,14 @@ class SeeAnythingTool(Tool):
|
|
319
455
|
Args:
|
320
456
|
event (QMouseEvent): The mouse move event.
|
321
457
|
"""
|
458
|
+
scene_pos = self.annotation_window.mapToScene(event.pos())
|
459
|
+
self.hover_pos = scene_pos
|
460
|
+
|
461
|
+
# Update working area preview during creation
|
462
|
+
if self.creating_working_area and self.working_area_start:
|
463
|
+
self.display_working_area_preview(scene_pos)
|
464
|
+
return
|
465
|
+
|
322
466
|
if self.working_area and self.drawing_rectangle:
|
323
467
|
# Update the end point while drawing the rectangle
|
324
468
|
self.end_point = self.annotation_window.mapToScene(event.pos())
|
@@ -334,6 +478,12 @@ class SeeAnythingTool(Tool):
|
|
334
478
|
event (QKeyEvent): The key press event
|
335
479
|
"""
|
336
480
|
if event.key() == Qt.Key_Space:
|
481
|
+
# If creating working area, confirm it
|
482
|
+
if self.creating_working_area and self.working_area_start and self.hover_pos:
|
483
|
+
self.set_custom_working_area(self.working_area_start, self.hover_pos)
|
484
|
+
self.cancel_working_area_creation()
|
485
|
+
return
|
486
|
+
|
337
487
|
# If there is no working area, set it
|
338
488
|
if not self.working_area:
|
339
489
|
self.set_working_area()
|
@@ -359,6 +509,11 @@ class SeeAnythingTool(Tool):
|
|
359
509
|
self.cancel_working_area()
|
360
510
|
|
361
511
|
elif event.key() == Qt.Key_Backspace:
|
512
|
+
# If creating working area, cancel it
|
513
|
+
if self.creating_working_area:
|
514
|
+
self.cancel_working_area_creation()
|
515
|
+
return
|
516
|
+
|
362
517
|
# Cancel current rectangle being drawn
|
363
518
|
if self.drawing_rectangle:
|
364
519
|
self.drawing_rectangle = False
|
@@ -398,8 +553,16 @@ class SeeAnythingTool(Tool):
|
|
398
553
|
# Move the points back to the original image space
|
399
554
|
working_area_top_left = self.working_area.rect.topLeft()
|
400
555
|
|
401
|
-
|
402
|
-
|
556
|
+
masks = None
|
557
|
+
# Create masks from the rectangles (these are not polygons)
|
558
|
+
if self.see_anything_dialog.task_dropdown.currentText() == 'segment':
|
559
|
+
masks = []
|
560
|
+
for r in self.rectangles:
|
561
|
+
x1, y1, x2, y2 = r
|
562
|
+
masks.append(np.array([[x1, y1], [x2, y1], [x2, y2], [x1, y2]]))
|
563
|
+
|
564
|
+
# Predict from prompts, providing masks if the task is segmentation
|
565
|
+
results = self.see_anything_dialog.predict_from_prompts(self.rectangles, masks=masks)[0]
|
403
566
|
|
404
567
|
if not results:
|
405
568
|
# Make cursor normal
|
@@ -425,54 +588,60 @@ class SeeAnythingTool(Tool):
|
|
425
588
|
# Clear previous annotations if any
|
426
589
|
self.clear_annotations()
|
427
590
|
|
428
|
-
#
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
if
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
591
|
+
# Process results based on the task type (creates polygons or rectangle annotations)
|
592
|
+
if self.see_anything_dialog.task_dropdown.currentText() == "segment":
|
593
|
+
if self.results.masks:
|
594
|
+
for i, polygon in enumerate(self.results.masks.xyn):
|
595
|
+
confidence = self.results.boxes.conf[i].item()
|
596
|
+
if confidence < self.main_window.get_uncertainty_thresh():
|
597
|
+
continue
|
598
|
+
|
599
|
+
# Get absolute bounding box for area check (relative to work area)
|
600
|
+
box_work_area = self.results.boxes.xyxy[i].detach().cpu().numpy()
|
601
|
+
box_area = (box_work_area[2] - box_work_area[0]) * (box_work_area[3] - box_work_area[1])
|
602
|
+
|
603
|
+
# Area filtering
|
604
|
+
min_area = self.main_window.get_area_thresh_min() * image_area
|
605
|
+
max_area = self.main_window.get_area_thresh_max() * image_area
|
606
|
+
if not (min_area <= box_area <= max_area):
|
607
|
+
continue
|
608
|
+
|
609
|
+
# Convert normalized polygon points to absolute coordinates in the whole image
|
610
|
+
polygon_abs = polygon.copy()
|
611
|
+
polygon_abs[:, 0] = polygon_abs[:, 0] * self.work_area_image.shape[1] + working_area_top_left.x()
|
612
|
+
polygon_abs[:, 1] = polygon_abs[:, 1] * self.work_area_image.shape[0] + working_area_top_left.y()
|
613
|
+
|
614
|
+
polygon_abs = simplify_polygon(polygon_abs, 0.1)
|
615
|
+
self.create_polygon_annotation(polygon_abs, confidence)
|
616
|
+
|
617
|
+
else: # Task is 'detect'
|
618
|
+
if self.results.boxes:
|
619
|
+
for i, box_norm in enumerate(self.results.boxes.xyxyn):
|
620
|
+
confidence = self.results.boxes.conf[i].item()
|
621
|
+
if confidence < self.main_window.get_uncertainty_thresh():
|
622
|
+
continue
|
623
|
+
|
624
|
+
# Convert normalized box to absolute coordinates in the work area
|
625
|
+
box_abs_work_area = box_norm.detach().cpu().numpy() * np.array(
|
626
|
+
[self.work_area_image.shape[1], self.work_area_image.shape[0],
|
627
|
+
self.work_area_image.shape[1], self.work_area_image.shape[0]])
|
628
|
+
# Calculate the area of the bounding box
|
629
|
+
box_area = (box_abs_work_area[2] - box_abs_work_area[0]) * \
|
630
|
+
(box_abs_work_area[3] - box_abs_work_area[1])
|
631
|
+
|
632
|
+
# Area filtering
|
633
|
+
min_area = self.main_window.get_area_thresh_min() * image_area
|
634
|
+
max_area = self.main_window.get_area_thresh_max() * image_area
|
635
|
+
if not (min_area <= box_area <= max_area):
|
636
|
+
continue
|
637
|
+
|
638
|
+
# Add working area offset to get coordinates in the whole image
|
639
|
+
box_abs_full = box_abs_work_area.copy()
|
640
|
+
box_abs_full[0] += working_area_top_left.x()
|
641
|
+
box_abs_full[1] += working_area_top_left.y()
|
642
|
+
box_abs_full[2] += working_area_top_left.x()
|
643
|
+
box_abs_full[3] += working_area_top_left.y()
|
644
|
+
self.create_rectangle_annotation(box_abs_full, confidence)
|
476
645
|
|
477
646
|
self.annotation_window.scene.update()
|
478
647
|
|
@@ -750,4 +919,3 @@ class SeeAnythingTool(Tool):
|
|
750
919
|
|
751
920
|
# Force update to ensure graphics are removed visually
|
752
921
|
self.annotation_window.scene.update()
|
753
|
-
|
@@ -1,5 +1,5 @@
|
|
1
1
|
from PyQt5.QtCore import Qt, QRectF
|
2
|
-
from PyQt5.QtGui import QPen, QColor
|
2
|
+
from PyQt5.QtGui import QPen, QColor, QBrush
|
3
3
|
from PyQt5.QtWidgets import QGraphicsRectItem
|
4
4
|
|
5
5
|
from coralnet_toolbox.Tools.QtSubTool import SubTool
|
@@ -22,15 +22,17 @@ class SelectSubTool(SubTool):
|
|
22
22
|
super().activate(event)
|
23
23
|
self.selection_start_pos = self.annotation_window.mapToScene(event.pos())
|
24
24
|
|
25
|
-
# Create and style the selection rectangle
|
25
|
+
# Create and style the selection rectangle (dashed blue, light blue fill)
|
26
26
|
self.selection_rectangle = QGraphicsRectItem()
|
27
27
|
width = self.parent_tool.graphics_utility.get_rectangle_graphic_thickness(self.annotation_window)
|
28
|
-
pen = QPen(QColor(
|
28
|
+
pen = QPen(QColor(0, 120, 215))
|
29
|
+
pen.setStyle(Qt.DashLine)
|
29
30
|
pen.setWidth(width)
|
30
31
|
self.selection_rectangle.setPen(pen)
|
32
|
+
self.selection_rectangle.setBrush(QBrush(QColor(0, 120, 215, 30))) # Light blue transparent fill
|
31
33
|
self.selection_rectangle.setRect(QRectF(self.selection_start_pos, self.selection_start_pos))
|
32
34
|
self.annotation_window.scene.addItem(self.selection_rectangle)
|
33
|
-
|
35
|
+
|
34
36
|
def deactivate(self):
|
35
37
|
super().deactivate()
|
36
38
|
if self.selection_rectangle:
|
@@ -11,6 +11,7 @@ from coralnet_toolbox.Tools.QtMoveSubTool import MoveSubTool
|
|
11
11
|
from coralnet_toolbox.Tools.QtResizeSubTool import ResizeSubTool
|
12
12
|
from coralnet_toolbox.Tools.QtSelectSubTool import SelectSubTool
|
13
13
|
from coralnet_toolbox.Tools.QtCutSubTool import CutSubTool
|
14
|
+
from coralnet_toolbox.Tools.QtSubtractSubTool import SubtractSubTool
|
14
15
|
|
15
16
|
from coralnet_toolbox.Annotations import (PatchAnnotation,
|
16
17
|
PolygonAnnotation,
|
@@ -45,6 +46,9 @@ class SelectTool(Tool):
|
|
45
46
|
self.resize_subtool = ResizeSubTool(self)
|
46
47
|
self.select_subtool = SelectSubTool(self)
|
47
48
|
self.cut_subtool = CutSubTool(self)
|
49
|
+
self.subtract_subtool = SubtractSubTool(self)
|
50
|
+
|
51
|
+
# --- State for the currently active sub-tool ---
|
48
52
|
self.active_subtool: SubTool | None = None
|
49
53
|
|
50
54
|
# --- State for transient UI (like resize handles) ---
|
@@ -175,9 +179,14 @@ class SelectTool(Tool):
|
|
175
179
|
if modifiers & Qt.ShiftModifier and len(self.selected_annotations) == 1:
|
176
180
|
self._show_resize_handles()
|
177
181
|
|
178
|
-
# Ctrl+X
|
179
|
-
if event.key() == Qt.Key_X
|
180
|
-
|
182
|
+
# --- Ctrl+X Hotkey Overload ---
|
183
|
+
if event.key() == Qt.Key_X:
|
184
|
+
if len(self.selected_annotations) > 1:
|
185
|
+
# If more than one annotation is selected, perform subtraction.
|
186
|
+
self.subtract_selected_annotations(event)
|
187
|
+
elif len(self.selected_annotations) == 1:
|
188
|
+
# If only one is selected, start cutting mode.
|
189
|
+
self.set_active_subtool(self.cut_subtool, event, annotation=self.selected_annotations[0])
|
181
190
|
|
182
191
|
# Ctrl+C: Combine selected annotations
|
183
192
|
elif event.key() == Qt.Key_C and len(self.selected_annotations) > 1:
|
@@ -315,6 +324,16 @@ class SelectTool(Tool):
|
|
315
324
|
annotation.update_user_confidence(top_label)
|
316
325
|
if len(self.selected_annotations) == 1:
|
317
326
|
self.annotation_window.main_window.confidence_window.refresh_display()
|
327
|
+
|
328
|
+
def subtract_selected_annotations(self, event):
|
329
|
+
"""
|
330
|
+
Initiates the subtraction operation by activating the SubtractSubTool.
|
331
|
+
"""
|
332
|
+
self.set_active_subtool(
|
333
|
+
self.subtract_subtool,
|
334
|
+
event,
|
335
|
+
selected_annotations=self.selected_annotations.copy()
|
336
|
+
)
|
318
337
|
|
319
338
|
def combine_selected_annotations(self):
|
320
339
|
"""Combine multiple selected annotations of the same type."""
|
@@ -437,6 +456,11 @@ class SelectTool(Tool):
|
|
437
456
|
# Add the newly created annotations from the cut.
|
438
457
|
for new_anno in new_annotations:
|
439
458
|
self.annotation_window.add_annotation_from_tool(new_anno)
|
459
|
+
|
460
|
+
def cancel_cutting_mode(self):
|
461
|
+
"""Safely cancels cutting mode."""
|
462
|
+
if self.active_subtool and isinstance(self.active_subtool, CutSubTool):
|
463
|
+
self.deactivate_subtool()
|
440
464
|
|
441
465
|
# --- Convenience Properties ---
|
442
466
|
@property
|
@@ -0,0 +1,66 @@
|
|
1
|
+
from coralnet_toolbox.Tools.QtSubTool import SubTool
|
2
|
+
from coralnet_toolbox.Annotations import PolygonAnnotation
|
3
|
+
from coralnet_toolbox.Annotations import MultiPolygonAnnotation
|
4
|
+
|
5
|
+
|
6
|
+
# ----------------------------------------------------------------------------------------------------------------------
|
7
|
+
# Classes
|
8
|
+
# ----------------------------------------------------------------------------------------------------------------------
|
9
|
+
|
10
|
+
class SubtractSubTool(SubTool):
|
11
|
+
"""
|
12
|
+
A SubTool to perform a "cookie cutter" subtraction operation on selected annotations.
|
13
|
+
This tool activates, performs its action, and immediately deactivates.
|
14
|
+
"""
|
15
|
+
|
16
|
+
def __init__(self, parent_tool):
|
17
|
+
super().__init__(parent_tool)
|
18
|
+
|
19
|
+
def activate(self, event, **kwargs):
|
20
|
+
"""
|
21
|
+
Activates the subtraction operation.
|
22
|
+
Expects 'selected_annotations' in kwargs.
|
23
|
+
"""
|
24
|
+
super().activate(event)
|
25
|
+
|
26
|
+
selected_annotations = kwargs.get('selected_annotations', [])
|
27
|
+
|
28
|
+
# --- 1. Perform Pre-activation Checks ---
|
29
|
+
if len(selected_annotations) < 2:
|
30
|
+
self.parent_tool.deactivate_subtool()
|
31
|
+
return
|
32
|
+
|
33
|
+
# Check if all annotations are verified Polygon or MultiPolygon Annotations
|
34
|
+
allowed_types = (PolygonAnnotation, MultiPolygonAnnotation)
|
35
|
+
if not all(isinstance(anno, allowed_types) and anno.verified for anno in selected_annotations):
|
36
|
+
self.parent_tool.deactivate_subtool()
|
37
|
+
return
|
38
|
+
|
39
|
+
# --- 2. Identify Base and Cutters ---
|
40
|
+
# The last selected annotation is the base, the rest are cutters.
|
41
|
+
base_annotation = selected_annotations[-1]
|
42
|
+
cutter_annotations = selected_annotations[:-1]
|
43
|
+
|
44
|
+
# --- 3. Perform the Subtraction ---
|
45
|
+
result_annotations = PolygonAnnotation.subtract(base_annotation, cutter_annotations)
|
46
|
+
|
47
|
+
if not result_annotations:
|
48
|
+
self.parent_tool.deactivate_subtool()
|
49
|
+
return
|
50
|
+
|
51
|
+
# --- 4. Update the Annotation Window ---
|
52
|
+
# Delete the original annotations that were used in the operation
|
53
|
+
for anno in selected_annotations:
|
54
|
+
self.annotation_window.delete_annotation(anno.id)
|
55
|
+
|
56
|
+
# Add the new resulting annotations to the scene
|
57
|
+
for new_anno in result_annotations:
|
58
|
+
self.annotation_window.add_annotation_from_tool(new_anno)
|
59
|
+
|
60
|
+
# Select the new annotations
|
61
|
+
self.annotation_window.unselect_annotations()
|
62
|
+
for new_anno in result_annotations:
|
63
|
+
self.annotation_window.select_annotation(new_anno, multi_select=True)
|
64
|
+
|
65
|
+
# --- 5. Deactivate Immediately ---
|
66
|
+
self.parent_tool.deactivate_subtool()
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import warnings
|
2
2
|
|
3
|
-
from PyQt5.QtCore import Qt,
|
4
|
-
from PyQt5.QtGui import QMouseEvent, QPen, QColor
|
3
|
+
from PyQt5.QtCore import Qt, QRectF
|
4
|
+
from PyQt5.QtGui import QMouseEvent, QPen, QColor, QBrush
|
5
5
|
from PyQt5.QtWidgets import (QGraphicsRectItem, QMessageBox, QGraphicsPixmapItem)
|
6
6
|
|
7
7
|
from coralnet_toolbox.Tools.QtTool import Tool
|
@@ -30,8 +30,8 @@ class WorkAreaTool(Tool):
|
|
30
30
|
self.current_rect = None
|
31
31
|
self.work_areas = [] # List to store WorkArea objects for the current image
|
32
32
|
|
33
|
-
# Style settings for drawing the work area rectangle
|
34
|
-
self.work_area_pen = QPen(QColor(
|
33
|
+
# Style settings for drawing the work area rectangle - update to use blue dashed line
|
34
|
+
self.work_area_pen = QPen(QColor(0, 120, 215), 2, Qt.DashLine)
|
35
35
|
|
36
36
|
# Track if Ctrl key is pressed
|
37
37
|
self.ctrl_pressed = False
|
@@ -43,6 +43,9 @@ class WorkAreaTool(Tool):
|
|
43
43
|
# Track current image path to detect image changes
|
44
44
|
self.current_image_path = None
|
45
45
|
|
46
|
+
# Add hover position tracking for preview
|
47
|
+
self.hover_pos = None
|
48
|
+
|
46
49
|
def activate(self):
|
47
50
|
"""Activate the work area tool and set the appropriate cursor."""
|
48
51
|
self.active = True
|
@@ -100,10 +103,12 @@ class WorkAreaTool(Tool):
|
|
100
103
|
|
101
104
|
def mouseMoveEvent(self, event: QMouseEvent):
|
102
105
|
"""Handle mouse move events to update the work area while drawing."""
|
106
|
+
scene_pos = self.annotation_window.mapToScene(event.pos())
|
107
|
+
self.hover_pos = scene_pos # Track hover position for spacebar confirmation
|
108
|
+
|
103
109
|
if not self.drawing or not self.annotation_window.active_image:
|
104
110
|
return
|
105
111
|
|
106
|
-
scene_pos = self.annotation_window.mapToScene(event.pos())
|
107
112
|
self.update_drawing(scene_pos)
|
108
113
|
|
109
114
|
def mouseReleaseEvent(self, event: QMouseEvent):
|
@@ -116,6 +121,11 @@ class WorkAreaTool(Tool):
|
|
116
121
|
modifiers = event.modifiers()
|
117
122
|
key = event.key()
|
118
123
|
|
124
|
+
# Confirm current drawing with spacebar
|
125
|
+
if key == Qt.Key_Space and self.drawing and self.hover_pos:
|
126
|
+
self.finish_drawing(self.hover_pos)
|
127
|
+
return
|
128
|
+
|
119
129
|
# Ctrl+Alt for temporary work area
|
120
130
|
if (modifiers & Qt.ControlModifier) and (modifiers & Qt.AltModifier):
|
121
131
|
if not self.temporary_work_area:
|
@@ -139,12 +149,12 @@ class WorkAreaTool(Tool):
|
|
139
149
|
return
|
140
150
|
|
141
151
|
# Ctrl+Space to create a work area from current view
|
142
|
-
if key == Qt.Key_Space and self.annotation_window.active_image:
|
152
|
+
if key == Qt.Key_Space and self.annotation_window.active_image and not self.drawing:
|
143
153
|
self.create_work_area_from_current_view()
|
144
154
|
return
|
145
155
|
|
146
|
-
# Cancel current drawing (Backspace - without modifiers)
|
147
|
-
if key == Qt.Key_Backspace and self.drawing and not (modifiers & Qt.ControlModifier):
|
156
|
+
# Cancel current drawing (Backspace or Escape - without modifiers)
|
157
|
+
if (key == Qt.Key_Backspace or key == Qt.Key_Escape) and self.drawing and not (modifiers & Qt.ControlModifier):
|
148
158
|
self.cancel_drawing()
|
149
159
|
return
|
150
160
|
|
@@ -207,12 +217,13 @@ class WorkAreaTool(Tool):
|
|
207
217
|
# Create an initial rectangle item for visual feedback
|
208
218
|
self.current_rect = QGraphicsRectItem(QRectF(pos.x(), pos.y(), 0, 0))
|
209
219
|
|
210
|
-
#
|
211
|
-
|
220
|
+
# Create a dashed blue pen for the working area preview
|
221
|
+
pen = QPen(QColor(0, 120, 215))
|
222
|
+
pen.setStyle(Qt.DashLine)
|
223
|
+
pen.setWidth(2)
|
212
224
|
|
213
|
-
|
214
|
-
self.
|
215
|
-
self.current_rect.setPen(self.work_area_pen)
|
225
|
+
self.current_rect.setPen(pen)
|
226
|
+
self.current_rect.setBrush(QBrush(QColor(0, 120, 215, 30))) # Light blue transparent fill
|
216
227
|
self.annotation_window.scene.addItem(self.current_rect)
|
217
228
|
|
218
229
|
def update_drawing(self, pos):
|
@@ -227,6 +238,7 @@ class WorkAreaTool(Tool):
|
|
227
238
|
constrained_rect = self.constrain_rect_to_image_bounds(rect)
|
228
239
|
|
229
240
|
self.current_rect.setRect(constrained_rect)
|
241
|
+
self.hover_pos = pos # Update hover position for key events
|
230
242
|
|
231
243
|
def finish_drawing(self, pos):
|
232
244
|
"""Finish drawing the work area and add it to the list."""
|
@@ -14,6 +14,7 @@ from .QtCutSubTool import CutSubTool
|
|
14
14
|
from .QtMoveSubTool import MoveSubTool
|
15
15
|
from .QtResizeSubTool import ResizeSubTool
|
16
16
|
from .QtSelectSubTool import SelectSubTool
|
17
|
+
from .QtSubtractSubTool import SubtractSubTool
|
17
18
|
|
18
19
|
__all__ = [
|
19
20
|
'PanTool',
|
@@ -29,4 +30,5 @@ __all__ = [
|
|
29
30
|
'MoveSubTool',
|
30
31
|
'ResizeSubTool',
|
31
32
|
'SelectSubTool',
|
33
|
+
'SubtractSubTool',
|
32
34
|
]
|
coralnet_toolbox/__init__.py
CHANGED