coralnet-toolbox 0.0.71__py2.py3-none-any.whl → 0.0.73__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 (39) hide show
  1. coralnet_toolbox/Annotations/QtRectangleAnnotation.py +31 -2
  2. coralnet_toolbox/AutoDistill/QtDeployModel.py +23 -12
  3. coralnet_toolbox/Explorer/QtDataItem.py +53 -21
  4. coralnet_toolbox/Explorer/QtExplorer.py +581 -276
  5. coralnet_toolbox/Explorer/QtFeatureStore.py +15 -0
  6. coralnet_toolbox/Explorer/QtSettingsWidgets.py +49 -7
  7. coralnet_toolbox/MachineLearning/DeployModel/QtDetect.py +22 -11
  8. coralnet_toolbox/MachineLearning/DeployModel/QtSegment.py +22 -10
  9. coralnet_toolbox/MachineLearning/ExportDataset/QtBase.py +61 -24
  10. coralnet_toolbox/MachineLearning/ExportDataset/QtClassify.py +5 -1
  11. coralnet_toolbox/MachineLearning/ExportDataset/QtDetect.py +19 -6
  12. coralnet_toolbox/MachineLearning/ExportDataset/QtSegment.py +21 -8
  13. coralnet_toolbox/QtAnnotationWindow.py +52 -16
  14. coralnet_toolbox/QtEventFilter.py +8 -2
  15. coralnet_toolbox/QtImageWindow.py +17 -18
  16. coralnet_toolbox/QtLabelWindow.py +1 -1
  17. coralnet_toolbox/QtMainWindow.py +203 -8
  18. coralnet_toolbox/Rasters/QtRaster.py +59 -7
  19. coralnet_toolbox/Rasters/RasterTableModel.py +34 -6
  20. coralnet_toolbox/SAM/QtBatchInference.py +0 -2
  21. coralnet_toolbox/SAM/QtDeployGenerator.py +22 -11
  22. coralnet_toolbox/SeeAnything/QtBatchInference.py +19 -221
  23. coralnet_toolbox/SeeAnything/QtDeployGenerator.py +1016 -0
  24. coralnet_toolbox/SeeAnything/QtDeployPredictor.py +69 -53
  25. coralnet_toolbox/SeeAnything/QtTrainModel.py +115 -45
  26. coralnet_toolbox/SeeAnything/__init__.py +2 -0
  27. coralnet_toolbox/Tools/QtResizeSubTool.py +6 -1
  28. coralnet_toolbox/Tools/QtSAMTool.py +150 -7
  29. coralnet_toolbox/Tools/QtSeeAnythingTool.py +220 -55
  30. coralnet_toolbox/Tools/QtSelectSubTool.py +6 -4
  31. coralnet_toolbox/Tools/QtSelectTool.py +48 -6
  32. coralnet_toolbox/Tools/QtWorkAreaTool.py +25 -13
  33. coralnet_toolbox/__init__.py +1 -1
  34. {coralnet_toolbox-0.0.71.dist-info → coralnet_toolbox-0.0.73.dist-info}/METADATA +1 -1
  35. {coralnet_toolbox-0.0.71.dist-info → coralnet_toolbox-0.0.73.dist-info}/RECORD +39 -38
  36. {coralnet_toolbox-0.0.71.dist-info → coralnet_toolbox-0.0.73.dist-info}/WHEEL +0 -0
  37. {coralnet_toolbox-0.0.71.dist-info → coralnet_toolbox-0.0.73.dist-info}/entry_points.txt +0 -0
  38. {coralnet_toolbox-0.0.71.dist-info → coralnet_toolbox-0.0.73.dist-info}/licenses/LICENSE.txt +0 -0
  39. {coralnet_toolbox-0.0.71.dist-info → coralnet_toolbox-0.0.73.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
@@ -165,6 +176,117 @@ class SeeAnythingTool(Tool):
165
176
 
166
177
  self.annotation_window.setCursor(Qt.CrossCursor)
167
178
  self.annotation_window.scene.update()
179
+
180
+ def set_custom_working_area(self, start_point, end_point):
181
+ """
182
+ Create a working area from custom points selected by the user.
183
+
184
+ Args:
185
+ start_point (QPointF): First corner of the working area
186
+ end_point (QPointF): Opposite corner of the working area
187
+ """
188
+ self.annotation_window.setCursor(Qt.WaitCursor)
189
+
190
+ # Cancel any existing working area
191
+ self.cancel_working_area()
192
+
193
+ # Calculate the rectangle bounds
194
+ left = max(0, int(min(start_point.x(), end_point.x())))
195
+ top = max(0, int(min(start_point.y(), end_point.y())))
196
+ right = min(int(self.annotation_window.pixmap_image.size().width()),
197
+ int(max(start_point.x(), end_point.x())))
198
+ bottom = min(int(self.annotation_window.pixmap_image.size().height()),
199
+ int(max(start_point.y(), end_point.y())))
200
+
201
+ # Ensure minimum size (at least 10x10 pixels)
202
+ if right - left < 10:
203
+ right = min(left + 10, int(self.annotation_window.pixmap_image.size().width()))
204
+ if bottom - top < 10:
205
+ bottom = min(top + 10, int(self.annotation_window.pixmap_image.size().height()))
206
+
207
+ # Original image information
208
+ self.image_path = self.annotation_window.current_image_path
209
+ self.original_image = pixmap_to_numpy(self.annotation_window.pixmap_image)
210
+ self.original_width = self.annotation_window.pixmap_image.size().width()
211
+ self.original_height = self.annotation_window.pixmap_image.size().height()
212
+
213
+ # Create the WorkArea instance
214
+ self.working_area = WorkArea(left, top, right - left, bottom - top, self.image_path)
215
+
216
+ # Get the thickness for the working area graphics
217
+ pen_width = self.graphics_utility.get_workarea_thickness(self.annotation_window)
218
+
219
+ # Create and add the working area graphics
220
+ self.working_area.create_graphics(self.annotation_window.scene, pen_width)
221
+ self.working_area.set_remove_button_visibility(False)
222
+ self.working_area.removed.connect(self.on_working_area_removed)
223
+
224
+ # Create shadow overlay
225
+ shadow_brush = QBrush(QColor(0, 0, 0, 150))
226
+ shadow_path = QPainterPath()
227
+ shadow_path.addRect(self.annotation_window.scene.sceneRect())
228
+ shadow_path.addRect(self.working_area.rect)
229
+ shadow_path = shadow_path.simplified()
230
+
231
+ self.shadow_area = QGraphicsPathItem(shadow_path)
232
+ self.shadow_area.setBrush(shadow_brush)
233
+ self.shadow_area.setPen(QPen(Qt.NoPen))
234
+ self.annotation_window.scene.addItem(self.shadow_area)
235
+
236
+ # Crop the image based on the working area
237
+ self.work_area_image = self.original_image[top:bottom, left:right]
238
+
239
+ # Set the image in the SeeAnything dialog
240
+ self.see_anything_dialog.set_image(self.work_area_image, self.image_path)
241
+
242
+ self.annotation_window.setCursor(Qt.CrossCursor)
243
+ self.annotation_window.scene.update()
244
+
245
+ def display_working_area_preview(self, current_pos):
246
+ """
247
+ Display a preview rectangle for the working area being created.
248
+
249
+ Args:
250
+ current_pos (QPointF): Current mouse position
251
+ """
252
+ if not self.working_area_start:
253
+ return
254
+
255
+ # Remove previous preview if it exists
256
+ if self.working_area_temp_graphics:
257
+ self.annotation_window.scene.removeItem(self.working_area_temp_graphics)
258
+ self.working_area_temp_graphics = None
259
+
260
+ # Create preview rectangle
261
+ rect = QRectF(
262
+ min(self.working_area_start.x(), current_pos.x()),
263
+ min(self.working_area_start.y(), current_pos.y()),
264
+ abs(current_pos.x() - self.working_area_start.x()),
265
+ abs(current_pos.y() - self.working_area_start.y())
266
+ )
267
+
268
+ # Create a dashed blue pen for the working area preview
269
+ pen = QPen(QColor(0, 120, 215))
270
+ pen.setStyle(Qt.DashLine)
271
+ pen.setWidth(2)
272
+
273
+ self.working_area_temp_graphics = QGraphicsRectItem(rect)
274
+ self.working_area_temp_graphics.setPen(pen)
275
+ self.working_area_temp_graphics.setBrush(QBrush(QColor(0, 120, 215, 30))) # Light blue transparent fill
276
+ self.annotation_window.scene.addItem(self.working_area_temp_graphics)
277
+
278
+ def cancel_working_area_creation(self):
279
+ """
280
+ Cancel the process of creating a working area.
281
+ """
282
+ self.creating_working_area = False
283
+ self.working_area_start = None
284
+
285
+ if self.working_area_temp_graphics:
286
+ self.annotation_window.scene.removeItem(self.working_area_temp_graphics)
287
+ self.working_area_temp_graphics = None
288
+
289
+ self.annotation_window.scene.update()
168
290
 
169
291
  def on_working_area_removed(self, work_area):
170
292
  """
@@ -277,12 +399,25 @@ class SeeAnythingTool(Tool):
277
399
  "A label must be selected before adding an annotation.")
278
400
  return None
279
401
 
402
+ # Get position in scene coordinates
403
+ scene_pos = self.annotation_window.mapToScene(event.pos())
404
+
405
+ # Handle working area creation mode
406
+ if not self.working_area and event.button() == Qt.LeftButton:
407
+ if not self.creating_working_area:
408
+ # Start working area creation
409
+ self.creating_working_area = True
410
+ self.working_area_start = scene_pos
411
+ return
412
+ elif self.creating_working_area and self.working_area_start:
413
+ # Finish working area creation
414
+ self.set_custom_working_area(self.working_area_start, scene_pos)
415
+ self.cancel_working_area_creation()
416
+ return
417
+
280
418
  if not self.working_area:
281
419
  return
282
420
 
283
- # Position in the scene
284
- scene_pos = self.annotation_window.mapToScene(event.pos())
285
-
286
421
  # Check if the position is within the working area
287
422
  if not self.working_area.contains_point(scene_pos):
288
423
  return
@@ -319,6 +454,14 @@ class SeeAnythingTool(Tool):
319
454
  Args:
320
455
  event (QMouseEvent): The mouse move event.
321
456
  """
457
+ scene_pos = self.annotation_window.mapToScene(event.pos())
458
+ self.hover_pos = scene_pos
459
+
460
+ # Update working area preview during creation
461
+ if self.creating_working_area and self.working_area_start:
462
+ self.display_working_area_preview(scene_pos)
463
+ return
464
+
322
465
  if self.working_area and self.drawing_rectangle:
323
466
  # Update the end point while drawing the rectangle
324
467
  self.end_point = self.annotation_window.mapToScene(event.pos())
@@ -334,6 +477,12 @@ class SeeAnythingTool(Tool):
334
477
  event (QKeyEvent): The key press event
335
478
  """
336
479
  if event.key() == Qt.Key_Space:
480
+ # If creating working area, confirm it
481
+ if self.creating_working_area and self.working_area_start and self.hover_pos:
482
+ self.set_custom_working_area(self.working_area_start, self.hover_pos)
483
+ self.cancel_working_area_creation()
484
+ return
485
+
337
486
  # If there is no working area, set it
338
487
  if not self.working_area:
339
488
  self.set_working_area()
@@ -359,6 +508,11 @@ class SeeAnythingTool(Tool):
359
508
  self.cancel_working_area()
360
509
 
361
510
  elif event.key() == Qt.Key_Backspace:
511
+ # If creating working area, cancel it
512
+ if self.creating_working_area:
513
+ self.cancel_working_area_creation()
514
+ return
515
+
362
516
  # Cancel current rectangle being drawn
363
517
  if self.drawing_rectangle:
364
518
  self.drawing_rectangle = False
@@ -398,8 +552,16 @@ class SeeAnythingTool(Tool):
398
552
  # Move the points back to the original image space
399
553
  working_area_top_left = self.working_area.rect.topLeft()
400
554
 
401
- # Predict the mask provided prompts (using only the current user-drawn rectangles)
402
- results = self.see_anything_dialog.predict_from_prompts(self.rectangles)[0]
555
+ task = self.see_anything_dialog.task_dropdown.currentText()
556
+ masks = None
557
+ if task == 'segment':
558
+ masks = []
559
+ for r in self.rectangles:
560
+ x1, y1, x2, y2 = r
561
+ masks.append(np.array([[x1, y1], [x2, y1], [x2, y2], [x1, y2]]))
562
+
563
+ # Predict from prompts, providing masks if the task is segmentation
564
+ results = self.see_anything_dialog.predict_from_prompts(self.rectangles, masks=masks)[0]
403
565
 
404
566
  if not results:
405
567
  # Make cursor normal
@@ -425,54 +587,58 @@ class SeeAnythingTool(Tool):
425
587
  # Clear previous annotations if any
426
588
  self.clear_annotations()
427
589
 
428
- # Loop through the results from the current prediction
429
- for result in self.results:
430
- # Extract values from result
431
- confidence = result.boxes.conf.item()
432
-
433
- if confidence < self.main_window.get_uncertainty_thresh():
434
- continue
435
-
436
- # Get the bounding box coordinates (x1, y1, x2, y2) in normalized format
437
- box = result.boxes.xyxyn.detach().cpu().numpy().squeeze()
438
-
439
- # Convert from normalized coordinates directly to absolute pixel coordinates in the whole image
440
- box_abs = box.copy() * np.array([self.work_area_image.shape[1],
441
- self.work_area_image.shape[0],
442
- self.work_area_image.shape[1],
443
- self.work_area_image.shape[0]])
444
-
445
- # Add working area offset to get coordinates in the whole image
446
- box_abs[0] += working_area_top_left.x()
447
- box_abs[1] += working_area_top_left.y()
448
- box_abs[2] += working_area_top_left.x()
449
- box_abs[3] += working_area_top_left.y()
450
-
451
- # Check box area relative to **work area view** area
452
- box_area = (box_abs[2] - box_abs[0]) * (box_abs[3] - box_abs[1])
453
-
454
- # self.main_window.get_area_thresh_min()
455
- if box_area < self.main_window.get_area_thresh_min() * image_area:
456
- continue
457
-
458
- if box_area > self.main_window.get_area_thresh_max() * image_area:
459
- continue
460
-
461
- if self.see_anything_dialog.task == "segment":
462
- # Use polygons from result.masks.data.xyn (list of polygons, each Nx2, normalized to crop)
463
- polygon = result.masks.xyn[0] # np.array of polygons, each as Nx2 array
464
-
465
- # Convert normalized polygon points directly to whole image coordinates
466
- polygon[:, 0] = polygon[:, 0] * self.work_area_image.shape[1] + working_area_top_left.x()
467
- polygon[:, 1] = polygon[:, 1] * self.work_area_image.shape[0] + working_area_top_left.y()
468
-
469
- polygon = simplify_polygon(polygon, 0.1)
470
-
471
- # Create the polygon annotation and add it to self.annotations
472
- self.create_polygon_annotation(polygon, confidence)
473
- else:
474
- # Create the rectangle annotation and add it to self.annotations
475
- self.create_rectangle_annotation(box_abs, confidence)
590
+ # Process results based on the task type
591
+ if self.see_anything_dialog.task == "segment":
592
+ if self.results.masks:
593
+ for i, polygon in enumerate(self.results.masks.xyn):
594
+ confidence = self.results.boxes.conf[i].item()
595
+ if confidence < self.main_window.get_uncertainty_thresh():
596
+ continue
597
+
598
+ # Get absolute bounding box for area check (relative to work area)
599
+ box_work_area = self.results.boxes.xyxy[i].detach().cpu().numpy()
600
+ box_area = (box_work_area[2] - box_work_area[0]) * (box_work_area[3] - box_work_area[1])
601
+
602
+ # Area filtering
603
+ min_area = self.main_window.get_area_thresh_min() * image_area
604
+ max_area = self.main_window.get_area_thresh_max() * image_area
605
+ if not (min_area <= box_area <= max_area):
606
+ continue
607
+
608
+ # Convert normalized polygon points to absolute coordinates in the whole image
609
+ polygon_abs = polygon.copy()
610
+ polygon_abs[:, 0] = polygon_abs[:, 0] * self.work_area_image.shape[1] + working_area_top_left.x()
611
+ polygon_abs[:, 1] = polygon_abs[:, 1] * self.work_area_image.shape[0] + working_area_top_left.y()
612
+
613
+ polygon_abs = simplify_polygon(polygon_abs, 0.1)
614
+ self.create_polygon_annotation(polygon_abs, confidence)
615
+
616
+ else: # Task is 'detect'
617
+ if self.results.boxes:
618
+ for i, box_norm in enumerate(self.results.boxes.xyxyn):
619
+ confidence = self.results.boxes.conf[i].item()
620
+ if confidence < self.main_window.get_uncertainty_thresh():
621
+ continue
622
+
623
+ # Convert normalized box to absolute coordinates in the work area
624
+ box_abs_work_area = box_norm.detach().cpu().numpy() * np.array(
625
+ [self.work_area_image.shape[1], self.work_area_image.shape[0],
626
+ self.work_area_image.shape[1], self.work_area_image.shape[0]])
627
+ box_area = (box_abs_work_area[2] - box_abs_work_area[0]) * (box_abs_work_area[3] - box_abs_work_area[1])
628
+
629
+ # Area filtering
630
+ min_area = self.main_window.get_area_thresh_min() * image_area
631
+ max_area = self.main_window.get_area_thresh_max() * image_area
632
+ if not (min_area <= box_area <= max_area):
633
+ continue
634
+
635
+ # Add working area offset to get coordinates in the whole image
636
+ box_abs_full = box_abs_work_area.copy()
637
+ box_abs_full[0] += working_area_top_left.x()
638
+ box_abs_full[1] += working_area_top_left.y()
639
+ box_abs_full[2] += working_area_top_left.x()
640
+ box_abs_full[3] += working_area_top_left.y()
641
+ self.create_rectangle_annotation(box_abs_full, confidence)
476
642
 
477
643
  self.annotation_window.scene.update()
478
644
 
@@ -750,4 +916,3 @@ class SeeAnythingTool(Tool):
750
916
 
751
917
  # Force update to ensure graphics are removed visually
752
918
  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(255, 255, 255), 2, Qt.DashLine)
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:
@@ -49,6 +49,7 @@ class SelectTool(Tool):
49
49
 
50
50
  # --- State for transient UI (like resize handles) ---
51
51
  self.resize_handles_visible = False
52
+ self.selection_locked = False
52
53
 
53
54
  self._connect_signals()
54
55
 
@@ -81,16 +82,36 @@ class SelectTool(Tool):
81
82
  self.deactivate_subtool()
82
83
  self._hide_resize_handles()
83
84
  self.annotation_window.viewport().setCursor(self.cursor)
85
+ self.selection_locked = False
84
86
 
85
87
  def deactivate(self):
86
88
  self.deactivate_subtool()
87
89
  self._hide_resize_handles()
88
90
  self.annotation_window.viewport().setCursor(self.default_cursor)
91
+ self.selection_locked = False
89
92
  super().deactivate()
90
93
 
91
94
  # --- Event Handlers (Dispatcher Logic) ---
92
95
 
93
96
  def mousePressEvent(self, event: QMouseEvent):
97
+ if self.selection_locked:
98
+ # If selection is locked, only allow interaction with resize handles.
99
+ # Check if a handle was clicked to start a resize operation.
100
+ position = self.annotation_window.mapToScene(event.pos())
101
+ items = self.annotation_window.scene.items(position)
102
+ if self.resize_handles_visible:
103
+ for item in items:
104
+ if item in self.resize_subtool.resize_handles_items:
105
+ handle_name = item.data(1)
106
+ if handle_name and len(self.selected_annotations) == 1:
107
+ self.set_active_subtool(
108
+ self.resize_subtool, event,
109
+ annotation=self.selected_annotations[0],
110
+ handle_name=handle_name
111
+ )
112
+ return # Exit after starting resize
113
+ return # Otherwise, ignore the click entirely
114
+
94
115
  # Ignore right mouse button events (used for panning)
95
116
  if event.button() == Qt.RightButton:
96
117
  return
@@ -215,14 +236,35 @@ class SelectTool(Tool):
215
236
  return self.annotation_window.annotations_dict.get(annotation_id) if annotation_id else None
216
237
 
217
238
  def _get_annotation_from_items(self, items, position):
218
- """Finds the first valid annotation at a position from a list of items."""
219
- for item in items:
220
- # We don't want to select by clicking a resize handle
221
- if item in self.resize_subtool.resize_handles_items:
222
- continue
239
+ """
240
+ Finds the first valid annotation at a position from a list of items.
241
+ First prioritizes annotations where the center graphic contains the point,
242
+ then falls back to any annotation that contains the point.
243
+ """
244
+ # Filter out resize handles
245
+ valid_items = [item for item in items if item not in self.resize_subtool.resize_handles_items]
246
+
247
+ center_threshold = 10.0 # Distance threshold in pixels to consider a click "on center"
248
+ center_candidates = []
249
+ general_candidates = []
250
+
251
+ # Gather all potential candidates
252
+ for item in valid_items:
223
253
  annotation = self._get_annotation_from_item(item)
224
254
  if annotation and annotation.contains_point(position):
225
- return annotation
255
+ # Calculate distance to center
256
+ center_distance = (position - annotation.center_xy).manhattanLength()
257
+ if center_distance <= center_threshold:
258
+ center_candidates.append(annotation)
259
+ else:
260
+ general_candidates.append(annotation)
261
+
262
+ # Return priority: center candidates first, then general candidates
263
+ if center_candidates:
264
+ return center_candidates[0]
265
+ elif general_candidates:
266
+ return general_candidates[0]
267
+
226
268
  return None
227
269
 
228
270
  def _handle_annotation_selection(self, position, items, modifiers):
@@ -1,7 +1,7 @@
1
1
  import warnings
2
2
 
3
- from PyQt5.QtCore import Qt, QPointF, QRectF
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(255, 255, 255), 2, Qt.DashLine)
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
- # Get the width
211
- width = self.graphics_utility.get_rectangle_graphic_thickness(self.annotation_window)
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
- # Set the pen properties
214
- self.work_area_pen.setWidth(width)
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."""
@@ -1,6 +1,6 @@
1
1
  """Top-level package for CoralNet-Toolbox."""
2
2
 
3
- __version__ = "0.0.71"
3
+ __version__ = "0.0.73"
4
4
  __author__ = "Jordan Pierce"
5
5
  __email__ = "jordan.pierce@noaa.gov"
6
6
  __credits__ = "National Center for Coastal and Ocean Sciences (NCCOS)"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coralnet-toolbox
3
- Version: 0.0.71
3
+ Version: 0.0.73
4
4
  Summary: Tools for annotating and developing ML models for benthic imagery
5
5
  Author-email: Jordan Pierce <jordan.pierce@noaa.gov>
6
6
  License: MIT License