coralnet-toolbox 0.0.75__py2.py3-none-any.whl → 0.0.77__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/QtPolygonAnnotation.py +57 -12
- coralnet_toolbox/Annotations/QtRectangleAnnotation.py +44 -14
- coralnet_toolbox/Common/QtGraphicsUtility.py +18 -8
- coralnet_toolbox/Explorer/transformer_models.py +13 -2
- coralnet_toolbox/IO/QtExportMaskAnnotations.py +576 -402
- coralnet_toolbox/IO/QtImportImages.py +7 -15
- coralnet_toolbox/IO/QtOpenProject.py +15 -19
- coralnet_toolbox/Icons/system_monitor.png +0 -0
- coralnet_toolbox/MachineLearning/ImportDataset/QtBase.py +33 -8
- coralnet_toolbox/QtAnnotationWindow.py +4 -0
- coralnet_toolbox/QtEventFilter.py +5 -5
- coralnet_toolbox/QtImageWindow.py +4 -0
- coralnet_toolbox/QtMainWindow.py +104 -64
- coralnet_toolbox/QtProgressBar.py +1 -0
- coralnet_toolbox/QtSystemMonitor.py +370 -0
- coralnet_toolbox/Rasters/RasterManager.py +5 -2
- coralnet_toolbox/Results/ConvertResults.py +14 -8
- coralnet_toolbox/Results/ResultsProcessor.py +3 -2
- coralnet_toolbox/SAM/QtDeployGenerator.py +1 -1
- coralnet_toolbox/SAM/QtDeployPredictor.py +10 -0
- coralnet_toolbox/SeeAnything/QtDeployGenerator.py +324 -177
- coralnet_toolbox/SeeAnything/QtDeployPredictor.py +10 -6
- coralnet_toolbox/Tile/QtTileBatchInference.py +4 -4
- coralnet_toolbox/Tools/QtPatchTool.py +6 -2
- coralnet_toolbox/Tools/QtPolygonTool.py +5 -3
- coralnet_toolbox/Tools/QtRectangleTool.py +17 -9
- coralnet_toolbox/Tools/QtSAMTool.py +144 -91
- coralnet_toolbox/Tools/QtSeeAnythingTool.py +4 -0
- coralnet_toolbox/Tools/QtTool.py +79 -3
- coralnet_toolbox/Tools/QtWorkAreaTool.py +4 -0
- coralnet_toolbox/Transformers/Models/GroundingDINO.py +72 -0
- coralnet_toolbox/Transformers/Models/OWLViT.py +72 -0
- coralnet_toolbox/Transformers/Models/OmDetTurbo.py +68 -0
- coralnet_toolbox/Transformers/Models/QtBase.py +121 -0
- coralnet_toolbox/{AutoDistill → Transformers}/Models/__init__.py +1 -1
- coralnet_toolbox/{AutoDistill → Transformers}/QtBatchInference.py +15 -15
- coralnet_toolbox/{AutoDistill → Transformers}/QtDeployModel.py +18 -16
- coralnet_toolbox/{AutoDistill → Transformers}/__init__.py +1 -1
- coralnet_toolbox/__init__.py +1 -1
- coralnet_toolbox/utilities.py +0 -15
- {coralnet_toolbox-0.0.75.dist-info → coralnet_toolbox-0.0.77.dist-info}/METADATA +9 -9
- {coralnet_toolbox-0.0.75.dist-info → coralnet_toolbox-0.0.77.dist-info}/RECORD +46 -44
- coralnet_toolbox/AutoDistill/Models/GroundingDINO.py +0 -81
- coralnet_toolbox/AutoDistill/Models/OWLViT.py +0 -76
- coralnet_toolbox/AutoDistill/Models/OmDetTurbo.py +0 -75
- coralnet_toolbox/AutoDistill/Models/QtBase.py +0 -112
- {coralnet_toolbox-0.0.75.dist-info → coralnet_toolbox-0.0.77.dist-info}/WHEEL +0 -0
- {coralnet_toolbox-0.0.75.dist-info → coralnet_toolbox-0.0.77.dist-info}/entry_points.txt +0 -0
- {coralnet_toolbox-0.0.75.dist-info → coralnet_toolbox-0.0.77.dist-info}/licenses/LICENSE.txt +0 -0
- {coralnet_toolbox-0.0.75.dist-info → coralnet_toolbox-0.0.77.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,5 @@
|
|
1
1
|
import warnings
|
2
2
|
|
3
|
-
import os
|
4
3
|
import gc
|
5
4
|
|
6
5
|
import numpy as np
|
@@ -14,7 +13,6 @@ from ultralytics.models.yolo.yoloe import YOLOEVPSegPredictor
|
|
14
13
|
from ultralytics.models.yolo.yoloe import YOLOEVPDetectPredictor
|
15
14
|
|
16
15
|
from PyQt5.QtCore import Qt
|
17
|
-
from PyQt5.QtGui import QColor
|
18
16
|
from PyQt5.QtWidgets import (QApplication, QComboBox, QDialog, QFormLayout,
|
19
17
|
QHBoxLayout, QLabel, QMessageBox, QPushButton,
|
20
18
|
QSlider, QSpinBox, QVBoxLayout, QGroupBox,
|
@@ -408,7 +406,7 @@ class DeployPredictorDialog(QDialog):
|
|
408
406
|
self.loaded_model.predict(
|
409
407
|
np.zeros((640, 640, 3), dtype=np.uint8),
|
410
408
|
visual_prompts=visuals.copy(), # This needs to happen to properly initialize the predictor
|
411
|
-
predictor=
|
409
|
+
predictor=YOLOEVPDetectPredictor if self.task == 'detect' else YOLOEVPSegPredictor,
|
412
410
|
imgsz=640,
|
413
411
|
conf=0.99,
|
414
412
|
)
|
@@ -545,12 +543,15 @@ class DeployPredictorDialog(QDialog):
|
|
545
543
|
|
546
544
|
# Get the scaled visual prompts
|
547
545
|
visual_prompts = self.scale_prompts(bboxes, masks)
|
546
|
+
|
547
|
+
# Set the predictor
|
548
|
+
predictor=YOLOEVPDetectPredictor if self.task == 'detect' else YOLOEVPSegPredictor
|
548
549
|
|
549
550
|
try:
|
550
551
|
# Make predictions
|
551
552
|
results = self.loaded_model.predict(self.resized_image,
|
552
|
-
visual_prompts=visual_prompts.copy(),
|
553
|
-
predictor=
|
553
|
+
visual_prompts=visual_prompts.copy(),
|
554
|
+
predictor=predictor,
|
554
555
|
imgsz=max(self.resized_image.shape[:2]),
|
555
556
|
conf=self.main_window.get_uncertainty_thresh(),
|
556
557
|
iou=self.main_window.get_iou_thresh(),
|
@@ -615,6 +616,9 @@ class DeployPredictorDialog(QDialog):
|
|
615
616
|
progress_bar = ProgressBar(self.annotation_window, title="Making Predictions")
|
616
617
|
progress_bar.show()
|
617
618
|
progress_bar.start_progress(len(target_images))
|
619
|
+
|
620
|
+
# Set the predictor
|
621
|
+
predictor = YOLOEVPDetectPredictor if self.task == 'detect' else YOLOEVPSegPredictor
|
618
622
|
|
619
623
|
for target_image in target_images:
|
620
624
|
|
@@ -623,7 +627,7 @@ class DeployPredictorDialog(QDialog):
|
|
623
627
|
results = self.loaded_model.predict(target_image,
|
624
628
|
refer_image=refer_image,
|
625
629
|
visual_prompts=visual_prompts.copy(),
|
626
|
-
predictor=
|
630
|
+
predictor=predictor,
|
627
631
|
imgsz=self.imgsz_spinbox.value(),
|
628
632
|
conf=self.main_window.get_uncertainty_thresh(),
|
629
633
|
iou=self.main_window.get_iou_thresh(),
|
@@ -37,8 +37,8 @@ class TileBatchInference(QDialog):
|
|
37
37
|
self.detect_dialog = main_window.detect_deploy_model_dialog
|
38
38
|
self.segment_dialog = main_window.segment_deploy_model_dialog
|
39
39
|
self.sam_dialog = main_window.sam_deploy_generator_dialog
|
40
|
-
self.
|
41
|
-
|
40
|
+
self.transformers_dialog = main_window.transformers_deploy_model_dialog
|
41
|
+
|
42
42
|
# Create a dictionary of the different model dialogs and their loaded models
|
43
43
|
self.model_dialogs = {}
|
44
44
|
|
@@ -167,8 +167,8 @@ class TileBatchInference(QDialog):
|
|
167
167
|
self.model_dialogs["Segment"] = self.segment_dialog
|
168
168
|
if self.sam_dialog and getattr(self.sam_dialog, "loaded_model", None):
|
169
169
|
self.model_dialogs["SAM Generator"] = self.sam_dialog
|
170
|
-
if self.
|
171
|
-
self.model_dialogs["
|
170
|
+
if self.transformers_dialog and getattr(self.transformers_dialog, "loaded_model", None):
|
171
|
+
self.model_dialogs["Transformers"] = self.transformers_dialog
|
172
172
|
|
173
173
|
# Update the model combo box with the available models
|
174
174
|
self.update_model_combo()
|
@@ -56,9 +56,13 @@ class PatchTool(Tool):
|
|
56
56
|
self.update_cursor_annotation(scene_pos)
|
57
57
|
|
58
58
|
def mouseMoveEvent(self, event: QMouseEvent):
|
59
|
-
#
|
59
|
+
# Call parent implementation to handle crosshair
|
60
|
+
super().mouseMoveEvent(event)
|
61
|
+
|
62
|
+
# Then clear any existing cursor annotation
|
60
63
|
self.clear_cursor_annotation()
|
61
|
-
|
64
|
+
|
65
|
+
# Continue with tool-specific behavior for cursor annotation
|
62
66
|
if self.annotation_window.active_image and self.annotation_window.selected_label:
|
63
67
|
scene_pos = self.annotation_window.mapToScene(event.pos())
|
64
68
|
if self.annotation_window.cursorInWindow(event.pos()):
|
@@ -78,15 +78,17 @@ class PolygonTool(Tool):
|
|
78
78
|
else:
|
79
79
|
self.cancel_annotation()
|
80
80
|
|
81
|
-
def mouseMoveEvent(self, event: QMouseEvent):
|
81
|
+
def mouseMoveEvent(self, event: QMouseEvent):
|
82
|
+
# Tool-specific behavior (non-crosshair related) for mouse move events
|
82
83
|
if self.drawing_continuous:
|
83
|
-
scene_pos = self.annotation_window.mapToScene(event.pos())
|
84
84
|
active_image = self.annotation_window.active_image
|
85
85
|
pixmap_image = self.annotation_window.pixmap_image
|
86
86
|
cursor_in_window = self.annotation_window.cursorInWindow(event.pos())
|
87
|
+
scene_pos = self.annotation_window.mapToScene(event.pos())
|
88
|
+
|
87
89
|
if active_image and pixmap_image and cursor_in_window and self.points:
|
88
90
|
if self.ctrl_pressed and self.last_click_point:
|
89
|
-
# Show a straight line preview
|
91
|
+
# Show a straight line preview
|
90
92
|
self.update_cursor_annotation(scene_pos)
|
91
93
|
else:
|
92
94
|
# Free-hand: add points as the mouse moves
|
@@ -2,7 +2,7 @@ import warnings
|
|
2
2
|
|
3
3
|
from PyQt5.QtCore import Qt, QPointF
|
4
4
|
from PyQt5.QtGui import QMouseEvent, QKeyEvent
|
5
|
-
from PyQt5.QtWidgets import QMessageBox
|
5
|
+
from PyQt5.QtWidgets import QMessageBox, QGraphicsPixmapItem
|
6
6
|
|
7
7
|
from coralnet_toolbox.Tools.QtTool import Tool
|
8
8
|
from coralnet_toolbox.Annotations.QtRectangleAnnotation import RectangleAnnotation
|
@@ -29,9 +29,7 @@ class RectangleTool(Tool):
|
|
29
29
|
self.annotation_window.setCursor(self.cursor)
|
30
30
|
|
31
31
|
def deactivate(self):
|
32
|
-
|
33
|
-
self.annotation_window.setCursor(self.default_cursor)
|
34
|
-
self.clear_cursor_annotation()
|
32
|
+
super().deactivate()
|
35
33
|
self.start_point = None
|
36
34
|
self.end_point = None
|
37
35
|
self.drawing_continuous = False
|
@@ -72,8 +70,11 @@ class RectangleTool(Tool):
|
|
72
70
|
self.cancel_annotation()
|
73
71
|
|
74
72
|
def mouseMoveEvent(self, event: QMouseEvent):
|
73
|
+
# Call parent implementation to handle crosshair
|
74
|
+
super().mouseMoveEvent(event)
|
75
|
+
|
76
|
+
# Continue with tool-specific behavior
|
75
77
|
if self.drawing_continuous:
|
76
|
-
# Update the end point while drawing the rectangle
|
77
78
|
self.end_point = self.annotation_window.mapToScene(event.pos())
|
78
79
|
|
79
80
|
# Update the cursor annotation if we're in the window
|
@@ -82,12 +83,19 @@ class RectangleTool(Tool):
|
|
82
83
|
cursor_in_window = self.annotation_window.cursorInWindow(event.pos())
|
83
84
|
if active_image and pixmap_image and cursor_in_window and self.start_point:
|
84
85
|
self.update_cursor_annotation(self.end_point)
|
86
|
+
|
87
|
+
# Show crosshair at current cursor position during drawing
|
88
|
+
self.update_crosshair(self.end_point)
|
85
89
|
else:
|
86
90
|
# Show a preview rectangle at the cursor position when not drawing
|
87
91
|
scene_pos = self.annotation_window.mapToScene(event.pos())
|
88
|
-
|
89
|
-
|
90
|
-
|
92
|
+
cursor_in_window = self.annotation_window.cursorInWindow(event.pos())
|
93
|
+
|
94
|
+
# Show crosshair guides when cursor is over the image
|
95
|
+
if cursor_in_window and self.active and self.annotation_window.selected_label:
|
96
|
+
self.update_crosshair(scene_pos)
|
97
|
+
else:
|
98
|
+
self.clear_crosshair()
|
91
99
|
|
92
100
|
def keyPressEvent(self, event: QKeyEvent):
|
93
101
|
if event.key() == Qt.Key_Backspace:
|
@@ -183,4 +191,4 @@ class RectangleTool(Tool):
|
|
183
191
|
def update_cursor_annotation(self, scene_pos: QPointF = None):
|
184
192
|
"""Update the rectangle cursor annotation."""
|
185
193
|
self.clear_cursor_annotation()
|
186
|
-
self.create_cursor_annotation(scene_pos)
|
194
|
+
self.create_cursor_annotation(scene_pos)
|
@@ -6,6 +6,8 @@ from PyQt5.QtGui import QMouseEvent, QKeyEvent, QPen, QColor, QBrush, QPainterPa
|
|
6
6
|
from PyQt5.QtWidgets import QMessageBox, QGraphicsEllipseItem, QGraphicsRectItem, QGraphicsPathItem, QApplication
|
7
7
|
|
8
8
|
from coralnet_toolbox.Tools.QtTool import Tool
|
9
|
+
|
10
|
+
from coralnet_toolbox.Annotations.QtRectangleAnnotation import RectangleAnnotation
|
9
11
|
from coralnet_toolbox.Annotations.QtPolygonAnnotation import PolygonAnnotation
|
10
12
|
|
11
13
|
from coralnet_toolbox.QtWorkArea import WorkArea
|
@@ -374,45 +376,21 @@ class SAMTool(Tool):
|
|
374
376
|
top1_index = np.argmax(results.boxes.conf)
|
375
377
|
mask_tensor = results[top1_index].masks.data
|
376
378
|
|
377
|
-
# Check
|
379
|
+
# Check which output type is selected and get allow_holes settings
|
380
|
+
output_type = self.sam_dialog.get_output_type()
|
378
381
|
allow_holes = self.sam_dialog.get_allow_holes()
|
379
|
-
|
380
|
-
#
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
382
|
+
|
383
|
+
# Create annotation using the helper method
|
384
|
+
self.temp_annotation = self.create_annotation_from_mask(
|
385
|
+
mask_tensor,
|
386
|
+
output_type,
|
387
|
+
allow_holes
|
388
|
+
)
|
389
|
+
|
390
|
+
if not self.temp_annotation:
|
385
391
|
QApplication.restoreOverrideCursor()
|
386
392
|
return
|
387
|
-
|
388
|
-
# --- Process and Clean the Polygon Points ---
|
389
|
-
working_area_top_left = self.working_area.rect.topLeft()
|
390
|
-
offset_x, offset_y = working_area_top_left.x(), working_area_top_left.y()
|
391
|
-
|
392
|
-
# Simplify, offset, and convert the exterior points
|
393
|
-
simplified_exterior = simplify_polygon(exterior_coords, 0.1)
|
394
|
-
self.points = [QPointF(p[0] + offset_x, p[1] + offset_y) for p in simplified_exterior]
|
395
|
-
|
396
|
-
# Simplify, offset, and convert each hole only if allowed
|
397
|
-
final_holes = []
|
398
|
-
if allow_holes:
|
399
|
-
for hole_coords in holes_coords_list:
|
400
|
-
if len(hole_coords) >= 3: # Ensure holes are also valid polygons
|
401
|
-
simplified_hole = simplify_polygon(hole_coords, 0.1)
|
402
|
-
final_holes.append([QPointF(p[0] + offset_x, p[1] + offset_y) for p in simplified_hole])
|
403
|
-
|
404
|
-
# Create the temporary annotation, now with holes (or not)
|
405
|
-
self.temp_annotation = PolygonAnnotation(
|
406
|
-
points=self.points,
|
407
|
-
holes=final_holes,
|
408
|
-
short_label_code=self.annotation_window.selected_label.short_label_code,
|
409
|
-
long_label_code=self.annotation_window.selected_label.long_label_code,
|
410
|
-
color=self.annotation_window.selected_label.color,
|
411
|
-
image_path=self.annotation_window.current_image_path,
|
412
|
-
label_id=self.annotation_window.selected_label.id,
|
413
|
-
transparency=self.main_window.label_window.active_label.transparency
|
414
|
-
)
|
415
|
-
|
393
|
+
|
416
394
|
# Create the graphics item for the temporary annotation
|
417
395
|
self.temp_annotation.create_graphics_item(self.annotation_window.scene)
|
418
396
|
|
@@ -566,6 +544,10 @@ class SAMTool(Tool):
|
|
566
544
|
"""
|
567
545
|
Handle mouse move events.
|
568
546
|
"""
|
547
|
+
# Call parent implementation to handle crosshair
|
548
|
+
super().mouseMoveEvent(event)
|
549
|
+
|
550
|
+
# Continue with tool-specific behavior
|
569
551
|
scene_pos = self.annotation_window.mapToScene(event.pos())
|
570
552
|
self.hover_pos = scene_pos
|
571
553
|
|
@@ -616,17 +598,31 @@ class SAMTool(Tool):
|
|
616
598
|
elif self.has_active_prompts:
|
617
599
|
# Create the final annotation
|
618
600
|
if self.temp_annotation:
|
619
|
-
#
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
601
|
+
# Check if temp_annotation is a PolygonAnnotation or RectangleAnnotation
|
602
|
+
if isinstance(self.temp_annotation, PolygonAnnotation):
|
603
|
+
# For polygon annotations, use the points and holes
|
604
|
+
final_annotation = PolygonAnnotation(
|
605
|
+
self.points,
|
606
|
+
self.temp_annotation.label.short_label_code,
|
607
|
+
self.temp_annotation.label.long_label_code,
|
608
|
+
self.temp_annotation.label.color,
|
609
|
+
self.temp_annotation.image_path,
|
610
|
+
self.temp_annotation.label.id,
|
611
|
+
self.temp_annotation.label.transparency,
|
612
|
+
holes=self.temp_annotation.holes
|
613
|
+
)
|
614
|
+
elif isinstance(self.temp_annotation, RectangleAnnotation):
|
615
|
+
# For rectangle annotations, use the top_left and bottom_right
|
616
|
+
final_annotation = RectangleAnnotation(
|
617
|
+
top_left=self.temp_annotation.top_left,
|
618
|
+
bottom_right=self.temp_annotation.bottom_right,
|
619
|
+
short_label_code=self.temp_annotation.label.short_label_code,
|
620
|
+
long_label_code=self.temp_annotation.label.long_label_code,
|
621
|
+
color=self.temp_annotation.label.color,
|
622
|
+
image_path=self.temp_annotation.image_path,
|
623
|
+
label_id=self.temp_annotation.label.id,
|
624
|
+
transparency=self.temp_annotation.label.transparency
|
625
|
+
)
|
630
626
|
|
631
627
|
# Copy confidence data
|
632
628
|
final_annotation.update_machine_confidence(
|
@@ -740,54 +736,23 @@ class SAMTool(Tool):
|
|
740
736
|
top1_index = np.argmax(results.boxes.conf)
|
741
737
|
mask_tensor = results[top1_index].masks.data
|
742
738
|
|
743
|
-
# Check
|
739
|
+
# Check which output type is selected and get allow_holes settings
|
740
|
+
output_type = self.sam_dialog.get_output_type()
|
744
741
|
allow_holes = self.sam_dialog.get_allow_holes()
|
745
|
-
|
746
|
-
#
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
# --- Process and Clean the Polygon Points ---
|
755
|
-
working_area_top_left = self.working_area.rect.topLeft()
|
756
|
-
offset_x, offset_y = working_area_top_left.x(), working_area_top_left.y()
|
757
|
-
|
758
|
-
# Simplify, offset, and convert the exterior points
|
759
|
-
simplified_exterior = simplify_polygon(exterior_coords, 0.1)
|
760
|
-
self.points = [QPointF(p[0] + offset_x, p[1] + offset_y) for p in simplified_exterior]
|
761
|
-
|
762
|
-
# Simplify, offset, and convert each hole only if allowed
|
763
|
-
final_holes = []
|
764
|
-
if allow_holes:
|
765
|
-
for hole_coords in holes_coords_list:
|
766
|
-
if len(hole_coords) >= 3:
|
767
|
-
simplified_hole = simplify_polygon(hole_coords, 0.1)
|
768
|
-
final_holes.append([QPointF(p[0] + offset_x, p[1] + offset_y) for p in simplified_hole])
|
769
|
-
|
770
|
-
# Require at least 3 points for valid polygon
|
771
|
-
if len(self.points) < 3:
|
742
|
+
|
743
|
+
# Create annotation using the helper method
|
744
|
+
annotation = self.create_annotation_from_mask(
|
745
|
+
mask_tensor,
|
746
|
+
output_type,
|
747
|
+
allow_holes
|
748
|
+
)
|
749
|
+
|
750
|
+
if not annotation:
|
772
751
|
QApplication.restoreOverrideCursor()
|
773
752
|
return None
|
774
753
|
|
775
|
-
#
|
776
|
-
confidence = results.boxes.conf[top1_index]
|
777
|
-
|
778
|
-
# Create final annotation, now passing the holes argument
|
779
|
-
annotation = PolygonAnnotation(
|
780
|
-
points=self.points,
|
781
|
-
holes=final_holes,
|
782
|
-
short_label_code=self.annotation_window.selected_label.short_label_code,
|
783
|
-
long_label_code=self.annotation_window.selected_label.long_label_code,
|
784
|
-
color=self.annotation_window.selected_label.color,
|
785
|
-
image_path=self.annotation_window.current_image_path,
|
786
|
-
label_id=self.annotation_window.selected_label.id,
|
787
|
-
transparency=self.main_window.label_window.active_label.transparency
|
788
|
-
)
|
789
|
-
|
790
|
-
# Update confidence
|
754
|
+
# Update confidence - make sure to extract confidence from results
|
755
|
+
confidence = float(results.boxes.conf[top1_index])
|
791
756
|
annotation.update_machine_confidence({self.annotation_window.selected_label: confidence})
|
792
757
|
|
793
758
|
# Create cropped image
|
@@ -799,6 +764,94 @@ class SAMTool(Tool):
|
|
799
764
|
|
800
765
|
return annotation
|
801
766
|
|
767
|
+
def create_annotation_from_mask(self, mask_tensor, output_type, allow_holes=True):
|
768
|
+
"""
|
769
|
+
Create annotation (Rectangle or Polygon) from a mask tensor.
|
770
|
+
|
771
|
+
Args:
|
772
|
+
mask_tensor: The tensor containing the mask data
|
773
|
+
output_type (str): "Rectangle" or "Polygon"
|
774
|
+
allow_holes (bool): Whether to include holes in polygon annotations
|
775
|
+
|
776
|
+
Returns:
|
777
|
+
Annotation object or None if creation fails
|
778
|
+
"""
|
779
|
+
if not self.working_area:
|
780
|
+
return None
|
781
|
+
|
782
|
+
if output_type == "Rectangle":
|
783
|
+
# For rectangle output, just get the bounding box of the mask
|
784
|
+
# Find the bounding rectangle of the mask
|
785
|
+
y_indices, x_indices = np.where(mask_tensor.cpu().numpy()[0] > 0)
|
786
|
+
if len(y_indices) == 0 or len(x_indices) == 0:
|
787
|
+
return None
|
788
|
+
|
789
|
+
# Get the min/max coordinates
|
790
|
+
min_x, max_x = np.min(x_indices), np.max(x_indices)
|
791
|
+
min_y, max_y = np.min(y_indices), np.max(y_indices)
|
792
|
+
|
793
|
+
# Apply the offset from working area
|
794
|
+
working_area_top_left = self.working_area.rect.topLeft()
|
795
|
+
offset_x, offset_y = working_area_top_left.x(), working_area_top_left.y()
|
796
|
+
|
797
|
+
top_left = QPointF(min_x + offset_x, min_y + offset_y)
|
798
|
+
bottom_right = QPointF(max_x + offset_x, max_y + offset_y)
|
799
|
+
|
800
|
+
# Create a rectangle annotation
|
801
|
+
annotation = RectangleAnnotation(
|
802
|
+
top_left=top_left,
|
803
|
+
bottom_right=bottom_right,
|
804
|
+
short_label_code=self.annotation_window.selected_label.short_label_code,
|
805
|
+
long_label_code=self.annotation_window.selected_label.long_label_code,
|
806
|
+
color=self.annotation_window.selected_label.color,
|
807
|
+
image_path=self.annotation_window.current_image_path,
|
808
|
+
label_id=self.annotation_window.selected_label.id,
|
809
|
+
transparency=self.main_window.label_window.active_label.transparency
|
810
|
+
)
|
811
|
+
else:
|
812
|
+
# Original polygon code
|
813
|
+
# Polygonize the mask using the new method to get the exterior and holes
|
814
|
+
exterior_coords, holes_coords_list = polygonize_mask_with_holes(mask_tensor)
|
815
|
+
|
816
|
+
# Safety check for an empty result
|
817
|
+
if not exterior_coords:
|
818
|
+
return None
|
819
|
+
|
820
|
+
# --- Process and Clean the Polygon Points ---
|
821
|
+
working_area_top_left = self.working_area.rect.topLeft()
|
822
|
+
offset_x, offset_y = working_area_top_left.x(), working_area_top_left.y()
|
823
|
+
|
824
|
+
# Simplify, offset, and convert the exterior points
|
825
|
+
simplified_exterior = simplify_polygon(exterior_coords, 0.1)
|
826
|
+
self.points = [QPointF(p[0] + offset_x, p[1] + offset_y) for p in simplified_exterior]
|
827
|
+
|
828
|
+
# Simplify, offset, and convert each hole only if allowed
|
829
|
+
final_holes = []
|
830
|
+
if allow_holes:
|
831
|
+
for hole_coords in holes_coords_list:
|
832
|
+
simplified_hole = simplify_polygon(hole_coords, 0.1)
|
833
|
+
if len(simplified_hole) >= 3:
|
834
|
+
hole_points = [QPointF(p[0] + offset_x, p[1] + offset_y) for p in simplified_hole]
|
835
|
+
final_holes.append(hole_points)
|
836
|
+
|
837
|
+
# Require at least 3 points for valid polygon
|
838
|
+
if len(self.points) < 3:
|
839
|
+
return None
|
840
|
+
|
841
|
+
# Create final annotation, now passing the holes argument
|
842
|
+
annotation = PolygonAnnotation(
|
843
|
+
points=self.points,
|
844
|
+
holes=final_holes,
|
845
|
+
short_label_code=self.annotation_window.selected_label.short_label_code,
|
846
|
+
long_label_code=self.annotation_window.selected_label.long_label_code,
|
847
|
+
color=self.annotation_window.selected_label.color,
|
848
|
+
image_path=self.annotation_window.current_image_path,
|
849
|
+
label_id=self.annotation_window.selected_label.id,
|
850
|
+
transparency=self.main_window.label_window.active_label.transparency
|
851
|
+
)
|
852
|
+
|
853
|
+
return annotation
|
854
|
+
|
802
855
|
def cancel_working_area(self):
|
803
856
|
"""
|
804
857
|
Cancel the working area and clean up all associated resources.
|
@@ -455,6 +455,10 @@ class SeeAnythingTool(Tool):
|
|
455
455
|
Args:
|
456
456
|
event (QMouseEvent): The mouse move event.
|
457
457
|
"""
|
458
|
+
# Call parent implementation to handle crosshair
|
459
|
+
super().mouseMoveEvent(event)
|
460
|
+
|
461
|
+
# Continue with tool-specific behavior
|
458
462
|
scene_pos = self.annotation_window.mapToScene(event.pos())
|
459
463
|
self.hover_pos = scene_pos
|
460
464
|
|
coralnet_toolbox/Tools/QtTool.py
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
import warnings
|
2
2
|
|
3
3
|
from PyQt5.QtCore import Qt, QPointF
|
4
|
-
from PyQt5.QtGui import QMouseEvent
|
4
|
+
from PyQt5.QtGui import QMouseEvent, QColor
|
5
|
+
from PyQt5.QtWidgets import QGraphicsPixmapItem
|
5
6
|
|
6
7
|
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
7
8
|
|
@@ -21,6 +22,11 @@ class Tool:
|
|
21
22
|
self.cursor = Qt.ArrowCursor
|
22
23
|
self.default_cursor = Qt.ArrowCursor
|
23
24
|
self.cursor_annotation = None
|
25
|
+
|
26
|
+
# Crosshair settings
|
27
|
+
self.show_crosshair = True # Flag to toggle crosshair visibility for this tool
|
28
|
+
self.h_crosshair_line = None
|
29
|
+
self.v_crosshair_line = None
|
24
30
|
|
25
31
|
def activate(self):
|
26
32
|
self.active = True
|
@@ -30,12 +36,26 @@ class Tool:
|
|
30
36
|
self.active = False
|
31
37
|
self.annotation_window.setCursor(self.default_cursor)
|
32
38
|
self.clear_cursor_annotation()
|
39
|
+
self.clear_crosshair() # Clear any crosshair when tool is deactivated
|
33
40
|
|
34
41
|
def mousePressEvent(self, event: QMouseEvent):
|
35
42
|
pass
|
36
43
|
|
37
44
|
def mouseMoveEvent(self, event: QMouseEvent):
|
38
|
-
|
45
|
+
"""
|
46
|
+
Base implementation of mouseMoveEvent that handles crosshair display.
|
47
|
+
Child classes should call super().mouseMoveEvent(event) in their implementation.
|
48
|
+
"""
|
49
|
+
# Handle crosshair display
|
50
|
+
scene_pos = self.annotation_window.mapToScene(event.pos())
|
51
|
+
cursor_in_window = self.annotation_window.cursorInWindow(event.pos())
|
52
|
+
|
53
|
+
if (cursor_in_window and self.active and
|
54
|
+
self.annotation_window.selected_label and
|
55
|
+
self.show_crosshair):
|
56
|
+
self.update_crosshair(scene_pos)
|
57
|
+
else:
|
58
|
+
self.clear_crosshair()
|
39
59
|
|
40
60
|
def mouseReleaseEvent(self, event: QMouseEvent):
|
41
61
|
pass
|
@@ -77,4 +97,60 @@ class Tool:
|
|
77
97
|
"""
|
78
98
|
if self.cursor_annotation:
|
79
99
|
self.cursor_annotation.delete()
|
80
|
-
self.cursor_annotation = None
|
100
|
+
self.cursor_annotation = None
|
101
|
+
|
102
|
+
def draw_crosshair(self, scene_pos):
|
103
|
+
"""
|
104
|
+
Draw crosshair guides at the current cursor position.
|
105
|
+
|
106
|
+
Args:
|
107
|
+
scene_pos: Position in scene coordinates where to draw the crosshair
|
108
|
+
"""
|
109
|
+
# Only draw if we have an active image and scene position
|
110
|
+
if (
|
111
|
+
not self.show_crosshair
|
112
|
+
or not self.annotation_window.active_image
|
113
|
+
or not scene_pos
|
114
|
+
or not self.annotation_window.pixmap_image
|
115
|
+
):
|
116
|
+
return
|
117
|
+
|
118
|
+
# Remove any existing crosshair lines
|
119
|
+
self.clear_crosshair()
|
120
|
+
|
121
|
+
# Get image bounds
|
122
|
+
image_rect = QGraphicsPixmapItem(self.annotation_window.pixmap_image).boundingRect()
|
123
|
+
|
124
|
+
# Create horizontal line across the full width of the image
|
125
|
+
self.h_crosshair_line = self.graphics_utility.create_guide_line(
|
126
|
+
QPointF(image_rect.left(), scene_pos.y()),
|
127
|
+
QPointF(image_rect.right(), scene_pos.y())
|
128
|
+
)
|
129
|
+
self.annotation_window.scene.addItem(self.h_crosshair_line)
|
130
|
+
|
131
|
+
# Create vertical line across the full height of the image
|
132
|
+
self.v_crosshair_line = self.graphics_utility.create_guide_line(
|
133
|
+
QPointF(scene_pos.x(), image_rect.top()),
|
134
|
+
QPointF(scene_pos.x(), image_rect.bottom())
|
135
|
+
)
|
136
|
+
self.annotation_window.scene.addItem(self.v_crosshair_line)
|
137
|
+
|
138
|
+
def clear_crosshair(self):
|
139
|
+
"""Remove any crosshair guide lines from the scene."""
|
140
|
+
if self.h_crosshair_line and self.h_crosshair_line.scene():
|
141
|
+
self.annotation_window.scene.removeItem(self.h_crosshair_line)
|
142
|
+
self.h_crosshair_line = None
|
143
|
+
if self.v_crosshair_line and self.v_crosshair_line.scene():
|
144
|
+
self.annotation_window.scene.removeItem(self.v_crosshair_line)
|
145
|
+
self.v_crosshair_line = None
|
146
|
+
|
147
|
+
def update_crosshair(self, scene_pos):
|
148
|
+
"""
|
149
|
+
Update the crosshair position. This is a convenience method that
|
150
|
+
clears and redraws the crosshair.
|
151
|
+
|
152
|
+
Args:
|
153
|
+
scene_pos: New position for the crosshair
|
154
|
+
"""
|
155
|
+
self.clear_crosshair()
|
156
|
+
self.draw_crosshair(scene_pos)
|
@@ -103,6 +103,10 @@ class WorkAreaTool(Tool):
|
|
103
103
|
|
104
104
|
def mouseMoveEvent(self, event: QMouseEvent):
|
105
105
|
"""Handle mouse move events to update the work area while drawing."""
|
106
|
+
# Call parent implementation to handle crosshair
|
107
|
+
super().mouseMoveEvent(event)
|
108
|
+
|
109
|
+
# Continue with tool-specific behavior
|
106
110
|
scene_pos = self.annotation_window.mapToScene(event.pos())
|
107
111
|
self.hover_pos = scene_pos # Track hover position for spacebar confirmation
|
108
112
|
|