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.
Files changed (50) hide show
  1. coralnet_toolbox/Annotations/QtPolygonAnnotation.py +57 -12
  2. coralnet_toolbox/Annotations/QtRectangleAnnotation.py +44 -14
  3. coralnet_toolbox/Common/QtGraphicsUtility.py +18 -8
  4. coralnet_toolbox/Explorer/transformer_models.py +13 -2
  5. coralnet_toolbox/IO/QtExportMaskAnnotations.py +576 -402
  6. coralnet_toolbox/IO/QtImportImages.py +7 -15
  7. coralnet_toolbox/IO/QtOpenProject.py +15 -19
  8. coralnet_toolbox/Icons/system_monitor.png +0 -0
  9. coralnet_toolbox/MachineLearning/ImportDataset/QtBase.py +33 -8
  10. coralnet_toolbox/QtAnnotationWindow.py +4 -0
  11. coralnet_toolbox/QtEventFilter.py +5 -5
  12. coralnet_toolbox/QtImageWindow.py +4 -0
  13. coralnet_toolbox/QtMainWindow.py +104 -64
  14. coralnet_toolbox/QtProgressBar.py +1 -0
  15. coralnet_toolbox/QtSystemMonitor.py +370 -0
  16. coralnet_toolbox/Rasters/RasterManager.py +5 -2
  17. coralnet_toolbox/Results/ConvertResults.py +14 -8
  18. coralnet_toolbox/Results/ResultsProcessor.py +3 -2
  19. coralnet_toolbox/SAM/QtDeployGenerator.py +1 -1
  20. coralnet_toolbox/SAM/QtDeployPredictor.py +10 -0
  21. coralnet_toolbox/SeeAnything/QtDeployGenerator.py +324 -177
  22. coralnet_toolbox/SeeAnything/QtDeployPredictor.py +10 -6
  23. coralnet_toolbox/Tile/QtTileBatchInference.py +4 -4
  24. coralnet_toolbox/Tools/QtPatchTool.py +6 -2
  25. coralnet_toolbox/Tools/QtPolygonTool.py +5 -3
  26. coralnet_toolbox/Tools/QtRectangleTool.py +17 -9
  27. coralnet_toolbox/Tools/QtSAMTool.py +144 -91
  28. coralnet_toolbox/Tools/QtSeeAnythingTool.py +4 -0
  29. coralnet_toolbox/Tools/QtTool.py +79 -3
  30. coralnet_toolbox/Tools/QtWorkAreaTool.py +4 -0
  31. coralnet_toolbox/Transformers/Models/GroundingDINO.py +72 -0
  32. coralnet_toolbox/Transformers/Models/OWLViT.py +72 -0
  33. coralnet_toolbox/Transformers/Models/OmDetTurbo.py +68 -0
  34. coralnet_toolbox/Transformers/Models/QtBase.py +121 -0
  35. coralnet_toolbox/{AutoDistill → Transformers}/Models/__init__.py +1 -1
  36. coralnet_toolbox/{AutoDistill → Transformers}/QtBatchInference.py +15 -15
  37. coralnet_toolbox/{AutoDistill → Transformers}/QtDeployModel.py +18 -16
  38. coralnet_toolbox/{AutoDistill → Transformers}/__init__.py +1 -1
  39. coralnet_toolbox/__init__.py +1 -1
  40. coralnet_toolbox/utilities.py +0 -15
  41. {coralnet_toolbox-0.0.75.dist-info → coralnet_toolbox-0.0.77.dist-info}/METADATA +9 -9
  42. {coralnet_toolbox-0.0.75.dist-info → coralnet_toolbox-0.0.77.dist-info}/RECORD +46 -44
  43. coralnet_toolbox/AutoDistill/Models/GroundingDINO.py +0 -81
  44. coralnet_toolbox/AutoDistill/Models/OWLViT.py +0 -76
  45. coralnet_toolbox/AutoDistill/Models/OmDetTurbo.py +0 -75
  46. coralnet_toolbox/AutoDistill/Models/QtBase.py +0 -112
  47. {coralnet_toolbox-0.0.75.dist-info → coralnet_toolbox-0.0.77.dist-info}/WHEEL +0 -0
  48. {coralnet_toolbox-0.0.75.dist-info → coralnet_toolbox-0.0.77.dist-info}/entry_points.txt +0 -0
  49. {coralnet_toolbox-0.0.75.dist-info → coralnet_toolbox-0.0.77.dist-info}/licenses/LICENSE.txt +0 -0
  50. {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=YOLOEVPSegPredictor, # This also needs to be SegPredictor, no matter what
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=YOLOEVPSegPredictor,
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=YOLOEVPSegPredictor,
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.autodistill_dialog = main_window.auto_distill_deploy_model_dialog
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.autodistill_dialog and getattr(self.autodistill_dialog, "loaded_model", None):
171
- self.model_dialogs["Autodistill"] = self.autodistill_dialog
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
- # First clear any existing cursor annotation
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 from last point to cursor, do not modify self.points
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
- self.active = False
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
- if self.annotation_window.cursorInWindow(event.pos()) and self.annotation_window.selected_label:
89
- self.clear_cursor_annotation()
90
- # No cursor annotation in non-drawing mode
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 if holes are allowed from the SAM dialog
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
- # Polygonize the mask to get the exterior and holes
381
- exterior_coords, holes_coords_list = polygonize_mask_with_holes(mask_tensor)
382
-
383
- # Safety check: need at least 3 points for a valid polygon
384
- if len(exterior_coords) < 3:
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
- # Use existing temporary annotation
620
- final_annotation = PolygonAnnotation(
621
- self.points,
622
- self.temp_annotation.label.short_label_code,
623
- self.temp_annotation.label.long_label_code,
624
- self.temp_annotation.label.color,
625
- self.temp_annotation.image_path,
626
- self.temp_annotation.label.id,
627
- self.temp_annotation.label.transparency,
628
- holes=self.temp_annotation.holes
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 if holes are allowed from the SAM dialog
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
- # Polygonize the mask using the new method to get the exterior and holes
747
- exterior_coords, holes_coords_list = polygonize_mask_with_holes(mask_tensor)
748
-
749
- # Safety check for an empty result
750
- if not exterior_coords:
751
- QApplication.restoreOverrideCursor()
752
- return None
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
- # Get confidence score
776
- confidence = results.boxes.conf[top1_index].item()
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
 
@@ -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
- pass
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