coralnet-toolbox 0.0.71__py2.py3-none-any.whl → 0.0.72__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.
@@ -149,6 +149,21 @@ class FeatureStore:
149
149
  index_path = f"{self.index_path_base}_{model_key}.faiss"
150
150
  print(f"Saving FAISS index for '{model_key}' to {index_path}")
151
151
  faiss.write_index(index_to_save, index_path)
152
+
153
+ def remove_features_for_annotation(self, annotation_id):
154
+ """
155
+ Removes an annotation's feature metadata from the SQLite database.
156
+ This effectively orphans the vector in the FAISS index, invalidating it.
157
+ """
158
+ try:
159
+ self.cursor.execute(
160
+ "DELETE FROM features WHERE annotation_id = ?",
161
+ (annotation_id,)
162
+ )
163
+ self.conn.commit()
164
+ print(f"Invalidated features for annotation_id: {annotation_id}")
165
+ except sqlite3.Error as e:
166
+ print(f"Error removing feature for annotation {annotation_id}: {e}")
152
167
 
153
168
  def close(self):
154
169
  """Closes the database connection."""
@@ -670,8 +670,8 @@ class EmbeddingSettingsWidget(QGroupBox):
670
670
 
671
671
  def apply_embedding(self):
672
672
  if self.explorer_window and hasattr(self.explorer_window, 'run_embedding_pipeline'):
673
- # Clear all selections before running embedding pipeline
674
- if hasattr(self.explorer_window, 'handle_selection_change'):
675
- self.explorer_window.handle_selection_change([])
673
+ # Clear all selections before running a new embedding pipeline.
674
+ if hasattr(self.explorer_window, '_clear_selections'):
675
+ self.explorer_window._clear_selections()
676
676
 
677
677
  self.explorer_window.run_embedding_pipeline()
@@ -48,6 +48,7 @@ class AnnotationWindow(QGraphicsView):
48
48
  annotationSelected = pyqtSignal(int) # Signal to emit when annotation is selected
49
49
  annotationDeleted = pyqtSignal(str) # Signal to emit when annotation is deleted
50
50
  annotationCreated = pyqtSignal(str) # Signal to emit when annotation is created
51
+ annotationModified = pyqtSignal(str) # Signal to emit when annotation is modified
51
52
 
52
53
  def __init__(self, main_window, parent=None):
53
54
  """Initialize the annotation window with the main window and parent widget."""
@@ -374,6 +375,9 @@ class AnnotationWindow(QGraphicsView):
374
375
 
375
376
  def set_image(self, image_path):
376
377
  """Set and display an image at the given path."""
378
+ # Calculate GDIs for Windows if needed
379
+ self.main_window.check_windows_gdi_count()
380
+
377
381
  # Clean up
378
382
  self.clear_scene()
379
383
 
@@ -543,7 +547,7 @@ class AnnotationWindow(QGraphicsView):
543
547
  return type(self.selected_annotations[0])
544
548
  return None
545
549
 
546
- def select_annotation(self, annotation, multi_select=False):
550
+ def select_annotation(self, annotation, multi_select=False, quiet_mode=False):
547
551
  """Select an annotation and update the UI accordingly."""
548
552
  # If the annotation is already selected and Ctrl is pressed, unselect it
549
553
  if annotation in self.selected_annotations and multi_select:
@@ -569,7 +573,11 @@ class AnnotationWindow(QGraphicsView):
569
573
 
570
574
  # If this is the only selected annotation, update label window and confidence window
571
575
  if len(self.selected_annotations) == 1:
572
- self.labelSelected.emit(annotation.label.id)
576
+
577
+ if not quiet_mode:
578
+ # Emit the label selected signal, unless in quiet mode.
579
+ # This is in Explorer to avoid overwriting preview label.
580
+ self.labelSelected.emit(annotation.label.id)
573
581
 
574
582
  # Make sure we have a cropped image
575
583
  if not annotation.cropped_image:
@@ -4,6 +4,7 @@ warnings.filterwarnings("ignore", category=DeprecationWarning)
4
4
 
5
5
  import os
6
6
  import re
7
+ import ctypes
7
8
  import requests
8
9
 
9
10
  from packaging import version
@@ -130,6 +131,9 @@ class MainWindow(QMainWindow):
130
131
 
131
132
  def __init__(self, __version__):
132
133
  super().__init__()
134
+
135
+ # Get the process ID
136
+ self.pid = os.getpid()
133
137
 
134
138
  # Define icons
135
139
  self.coral_icon = get_icon("coral.png")
@@ -2329,6 +2333,62 @@ class MainWindow(QMainWindow):
2329
2333
  msg.exec_()
2330
2334
  except Exception as e:
2331
2335
  QMessageBox.critical(self, "Critical Error", f"{e}")
2336
+
2337
+ def check_windows_gdi_count(self):
2338
+ """Calculate and print the number of GDI objects for the current process on Windows."""
2339
+ # 1. Check if the OS is Windows. If not, return early.
2340
+ if os.name != 'nt':
2341
+ return
2342
+
2343
+ # Load necessary libraries
2344
+ kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
2345
+ user32 = ctypes.WinDLL('user32', use_last_error=True)
2346
+
2347
+ # Define constants
2348
+ PROCESS_QUERY_INFORMATION = 0x0400
2349
+ GR_GDIOBJECTS = 0
2350
+
2351
+ process_handle = None
2352
+ try:
2353
+ # 2. Get a handle to the process from its PID
2354
+ process_handle = kernel32.OpenProcess(PROCESS_QUERY_INFORMATION, False, self.pid)
2355
+
2356
+ if not process_handle:
2357
+ error_code = ctypes.get_last_error()
2358
+ raise RuntimeError(f"Failed to open PID {self.pid}. Error code: {error_code}")
2359
+
2360
+ # 3. Use the handle to get the GDI object count
2361
+ gdi_count = user32.GetGuiResources(process_handle, GR_GDIOBJECTS)
2362
+
2363
+ if gdi_count >= 9500: # GDI limit
2364
+ self.show_gdi_limit_warning()
2365
+
2366
+ except Exception as e:
2367
+ pass
2368
+
2369
+ finally:
2370
+ # 4. CRITICAL: Always close the handle when you're done
2371
+ if process_handle:
2372
+ kernel32.CloseHandle(process_handle)
2373
+
2374
+ return
2375
+
2376
+ def show_gdi_limit_warning(self):
2377
+ """
2378
+ Show a warning dialog if the GDI limit is reached.
2379
+ """
2380
+ try:
2381
+ self.untoggle_all_tools()
2382
+ msg = QMessageBox()
2383
+ msg.setWindowIcon(self.coral_icon)
2384
+ msg.setWindowTitle("GDI Limit Reached")
2385
+ msg.setText(
2386
+ "The GDI limit has been reached! Please immediately save your work, close, and reopen the application!"
2387
+ )
2388
+ msg.setStandardButtons(QMessageBox.Ok)
2389
+ msg.exec_()
2390
+ except Exception as e:
2391
+ QMessageBox.critical(self, "Critical Error", f"{e}")
2332
2392
 
2333
2393
  def open_snake_game_dialog(self):
2334
2394
  """
@@ -59,9 +59,14 @@ class ResizeSubTool(SubTool):
59
59
  def mouseReleaseEvent(self, event):
60
60
  """Finalize the resize, update related windows, and deactivate."""
61
61
  if self.target_annotation:
62
+ # Normalize the coordinates after resize is complete
63
+ if hasattr(self.target_annotation, 'normalize_coordinates'):
64
+ self.target_annotation.normalize_coordinates()
65
+
62
66
  self.target_annotation.create_cropped_image(self.annotation_window.rasterio_image)
63
67
  self.parent_tool.main_window.confidence_window.display_cropped_image(self.target_annotation)
64
-
68
+ self.annotation_window.annotationModified.emit(self.target_annotation.id) # Emit modified signal
69
+
65
70
  self.parent_tool.deactivate_subtool()
66
71
 
67
72
  # --- Handle Management Logic (moved from original class) ---
@@ -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,6 +1,6 @@
1
1
  """Top-level package for CoralNet-Toolbox."""
2
2
 
3
- __version__ = "0.0.71"
3
+ __version__ = "0.0.72"
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.72
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
@@ -1,20 +1,20 @@
1
- coralnet_toolbox/QtAnnotationWindow.py,sha256=3pGy_81qaOYOrD_356DSiyXzmuoQnHl5GCp3o-9ay48,39096
1
+ coralnet_toolbox/QtAnnotationWindow.py,sha256=ZlaYYAMNqu95SJhduQxH1K0YP7mOkMv3BzGQwemfByg,39518
2
2
  coralnet_toolbox/QtConfidenceWindow.py,sha256=L5hR23uW91GpqnsNS9R1XF3zCTe2aU7w0iDoQMV0oyE,16190
3
3
  coralnet_toolbox/QtEventFilter.py,sha256=KKC9de3e66PvGVgiML8P7MZ9-r7vvHidPJJYpcbTwyM,6696
4
4
  coralnet_toolbox/QtImageWindow.py,sha256=vLziMSEWFfVRSBN0nUNkosgk3LiNxZDqPwbinz9ZivQ,49356
5
5
  coralnet_toolbox/QtLabelWindow.py,sha256=-4GCk4pTY9g4ADH1iE__4xwqT-7UR_7VCT8v-bJzerk,50869
6
- coralnet_toolbox/QtMainWindow.py,sha256=W86kc-ZFmZHcSKcNTWN5RFCvt7sysvTCYZrPWZPWd6E,112220
6
+ coralnet_toolbox/QtMainWindow.py,sha256=z_Erak0fJC4x962rsFgK3IwjLQYVUe6rayzQzfJtkhw,114372
7
7
  coralnet_toolbox/QtPatchSampling.py,sha256=Ehj06auBGfQwIruLNYQjF8eFOCpl8G72p42UXXb2mUo,29013
8
8
  coralnet_toolbox/QtProgressBar.py,sha256=pnozUOcVjfO_yTS9z8wOMPcrrrOtG_FeCknTcdI6eyk,6250
9
9
  coralnet_toolbox/QtWorkArea.py,sha256=YXRvHQKpWUtWyv_o9lZ8rmxfm28dUOG9pmMUeimDhQ4,13578
10
- coralnet_toolbox/__init__.py,sha256=K-BGYPTYU1ySXgZWS4vmpHfRksFJb3SAyX73veK1hdE,207
10
+ coralnet_toolbox/__init__.py,sha256=-OIPFY1gy8COaSsB29_2bWyVd3ro4BAdKgI2eIvHhPY,207
11
11
  coralnet_toolbox/main.py,sha256=6j2B_1reC_KDmqvq1C0fB-UeSEm8eeJOozp2f4XXMLQ,1573
12
12
  coralnet_toolbox/utilities.py,sha256=eUkxXuWaNFH83LSW-KniwujkXKJ2rK04czx3k3OPiAY,27115
13
13
  coralnet_toolbox/Annotations/QtAnnotation.py,sha256=-3ASbjl1dXw9U731vyCgwyiyZT9zOD5Mvp1jt-7bCnA,29242
14
14
  coralnet_toolbox/Annotations/QtMultiPolygonAnnotation.py,sha256=ErAT31gw-zhEVNxkPRpyB9uw-NSpPh-ShCBxpscXdRw,15579
15
15
  coralnet_toolbox/Annotations/QtPatchAnnotation.py,sha256=67fNnK_-muyhGZdGB0kBDx-JGuflv1TM6q5ikfW_zOk,20076
16
16
  coralnet_toolbox/Annotations/QtPolygonAnnotation.py,sha256=1EkZEJlO4VZ4so01Sat2T8LeO1LNs7HbGJLO-G2_73Q,26886
17
- coralnet_toolbox/Annotations/QtRectangleAnnotation.py,sha256=nLL_HIkcnuB6CeqE-v8LMAoFgTEhZGHwt5yeEnLaVWQ,20090
17
+ coralnet_toolbox/Annotations/QtRectangleAnnotation.py,sha256=TgeawekA3jNBlCmZBRqXYRHoZqch7pWM-NSBBPG6S60,21404
18
18
  coralnet_toolbox/Annotations/__init__.py,sha256=bpMldC70tT_lzMrOdBNDkEhG9dCX3tXEBd48IrcUg3E,419
19
19
  coralnet_toolbox/AutoDistill/QtBatchInference.py,sha256=k871aW3XRX8kc4BDaS1aipbPh9WOZxgmilF2c4KOdVA,5646
20
20
  coralnet_toolbox/AutoDistill/QtDeployModel.py,sha256=6alhzvA3KYEeLaQj-Qhs9GicjNQyVoQbnvgZ3lxGnCU,25162
@@ -35,10 +35,10 @@ coralnet_toolbox/Common/QtUpdateImagePaths.py,sha256=_hJYx6hXdAOfH_m77f75AQduQ0W
35
35
  coralnet_toolbox/CoralNet/QtAuthenticate.py,sha256=Y__iY0Kcosz6AOV7dlJBwiB6Hte40wHahHe-OmRngZA,13267
36
36
  coralnet_toolbox/CoralNet/QtDownload.py,sha256=HBb8TpZRIEFirGIaIAV1v8qg3fL4cP6Bf-hUiqXoiLE,48516
37
37
  coralnet_toolbox/CoralNet/__init__.py,sha256=ILkAZh6mlAK1UaCCZjCB9JZxd-oY4cIgfnIC8UgjjIU,188
38
- coralnet_toolbox/Explorer/QtDataItem.py,sha256=-O2Tneh9wYbAZarqwb_Cvy5cP1F_zQH2IAw9c-rHy1Y,13572
39
- coralnet_toolbox/Explorer/QtExplorer.py,sha256=IATLlZi57m5XU1KIG-oGNGqhcZEpjxQeilmKGrKMIFo,122968
40
- coralnet_toolbox/Explorer/QtFeatureStore.py,sha256=kMn--vuBed6wZS-BQhHt_KBA5z-tL1ydFgFkkIoGiB4,6742
41
- coralnet_toolbox/Explorer/QtSettingsWidgets.py,sha256=hIMj2lzqGKBoFWKYolH7bEPm5ePAIzYcGjYRQv2uWFE,27656
38
+ coralnet_toolbox/Explorer/QtDataItem.py,sha256=fNpCHJSxMzHL2XpBXtPwKchSbmY7H0HWzL1Kbs4W1Ts,14920
39
+ coralnet_toolbox/Explorer/QtExplorer.py,sha256=vAtGTNsEUcy14-_XJuOocH7xdAlesHm5faN9AINaEvg,131591
40
+ coralnet_toolbox/Explorer/QtFeatureStore.py,sha256=3VwGezs1stmu65Z4ZQpvY27rGEIJq_prERWkFwMATBo,7378
41
+ coralnet_toolbox/Explorer/QtSettingsWidgets.py,sha256=unm23yP329cVL84aOvy20DBt3YBHVLU85rnfN9VUF8A,27649
42
42
  coralnet_toolbox/Explorer/__init__.py,sha256=wZPhf2oaUUyIQ2WK48Aj-4q1ENIZG2dGl1HF_mjhI6w,116
43
43
  coralnet_toolbox/IO/QtExportAnnotations.py,sha256=xeaS0BukC3cpkBIGT9DXRqHmvHhp-vOU47h6EoANpNg,4474
44
44
  coralnet_toolbox/IO/QtExportCoralNetAnnotations.py,sha256=4royhF63EmeOlSIBX389EUjjvE-SF44_maW6qm52mdA,2778
@@ -206,19 +206,19 @@ coralnet_toolbox/Tools/QtPanTool.py,sha256=q0g5Ryse6mIZ_Ss4qJw5NNwgoLuQQBIyQTXNF
206
206
  coralnet_toolbox/Tools/QtPatchTool.py,sha256=57vFeR2jQ_VQRlMEIC_mH8NigUqOlVvmhaVkXDvd_Gw,5574
207
207
  coralnet_toolbox/Tools/QtPolygonTool.py,sha256=yxnkwK3rb52pWCq7a3iAABhHUSS_a3vkL7G7Ev0uLDA,9174
208
208
  coralnet_toolbox/Tools/QtRectangleTool.py,sha256=gYOOsn1WRHLG0YzkKmmM7OzLpuLNh8GWIZ4MloXoLDc,7218
209
- coralnet_toolbox/Tools/QtResizeSubTool.py,sha256=-knQTmwURVYItfmDUCMjITsdRFAOhvDVCGP-2l9AkTE,5496
209
+ coralnet_toolbox/Tools/QtResizeSubTool.py,sha256=derPy4adRj758-xYtjL-_35yGBOjoSe_DRE48HpdQpA,5836
210
210
  coralnet_toolbox/Tools/QtSAMTool.py,sha256=TQ--xcR76lymFS0YVo5Gi4ay_tIsIEecYpLDMRBPLWQ,26174
211
211
  coralnet_toolbox/Tools/QtSeeAnythingTool.py,sha256=2uxyX_chOIXcmW6oBlb6XlgbRmSwSaQXmkmgOtFrqI4,30606
212
212
  coralnet_toolbox/Tools/QtSelectSubTool.py,sha256=xKtXYCwezLq3YZQLsSTG3mxs_ZRjLiPrYl-0ebgq-GA,3125
213
- coralnet_toolbox/Tools/QtSelectTool.py,sha256=Es4h-YdUyrebPoeI4cGaM4rOEiJtIzK2PObFkBisHp8,17917
213
+ coralnet_toolbox/Tools/QtSelectTool.py,sha256=rSzM9s7pMxrLqvcWgIcEnpEQhYHU6TbGUna8ZaamakA,19957
214
214
  coralnet_toolbox/Tools/QtSubTool.py,sha256=H25FoFqywdi6Bl35MfpEXGrr48ZTgdRRvHMxUy1tqN4,1601
215
215
  coralnet_toolbox/Tools/QtTool.py,sha256=2MCjT151gYBN8KbsK0GX4WOrEg1uw3oeSkp7Elw1AUA,2531
216
216
  coralnet_toolbox/Tools/QtWorkAreaTool.py,sha256=-CDrEPenOdSI3sf5wn19Cip4alE1ef7WsRDxQFDkHlc,22162
217
217
  coralnet_toolbox/Tools/QtZoomTool.py,sha256=F9CAoABv1jxcUS7dyIh1FYjgjOXYRI1xtBPNIR1g62o,4041
218
218
  coralnet_toolbox/Tools/__init__.py,sha256=218iQ8IFXIkKXiUDVYtXk9e08UY9-LhHjcryaJAanQ0,797
219
- coralnet_toolbox-0.0.71.dist-info/licenses/LICENSE.txt,sha256=AURacZ_G_PZKqqPQ9VB9Sqegblk67RNgWSGAYKwXXMY,521
220
- coralnet_toolbox-0.0.71.dist-info/METADATA,sha256=LltT45S41MrUplIQGHLJ_1rjuFT8PccNnbRCUw2ODRQ,18152
221
- coralnet_toolbox-0.0.71.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
222
- coralnet_toolbox-0.0.71.dist-info/entry_points.txt,sha256=oEeMoDlJ_2lq95quOeDHIx9hZpubUlSo80OLtgbcrbM,63
223
- coralnet_toolbox-0.0.71.dist-info/top_level.txt,sha256=SMWPh4_9JfB8zVpPOOvjucV2_B_hvWW7bNWmMjG0LsY,17
224
- coralnet_toolbox-0.0.71.dist-info/RECORD,,
219
+ coralnet_toolbox-0.0.72.dist-info/licenses/LICENSE.txt,sha256=AURacZ_G_PZKqqPQ9VB9Sqegblk67RNgWSGAYKwXXMY,521
220
+ coralnet_toolbox-0.0.72.dist-info/METADATA,sha256=cOB6wtxUBZcPWkW0L9v3LCWIsI_NOaaiKo9RyQJl15M,18152
221
+ coralnet_toolbox-0.0.72.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
222
+ coralnet_toolbox-0.0.72.dist-info/entry_points.txt,sha256=oEeMoDlJ_2lq95quOeDHIx9hZpubUlSo80OLtgbcrbM,63
223
+ coralnet_toolbox-0.0.72.dist-info/top_level.txt,sha256=SMWPh4_9JfB8zVpPOOvjucV2_B_hvWW7bNWmMjG0LsY,17
224
+ coralnet_toolbox-0.0.72.dist-info/RECORD,,