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
@@ -57,9 +57,14 @@ class RectangleAnnotation(Annotation):
57
57
 
58
58
  def set_cropped_bbox(self):
59
59
  """Set the cropped bounding box for the annotation."""
60
+ # Store the raw coordinates, keeping the top_left and bottom_right as they are
61
+ # to maintain the user's dragging experience
60
62
  self.cropped_bbox = (self.top_left.x(), self.top_left.y(), self.bottom_right.x(), self.bottom_right.y())
61
- self.annotation_size = int(max(self.bottom_right.x() - self.top_left.x(),
62
- self.bottom_right.y() - self.top_left.y()))
63
+
64
+ # Calculate annotation_size using absolute differences to handle inverted coordinates
65
+ width = abs(self.bottom_right.x() - self.top_left.x())
66
+ height = abs(self.bottom_right.y() - self.top_left.y())
67
+ self.annotation_size = int(max(width, height))
63
68
 
64
69
  def contains_point(self, point: QPointF) -> bool:
65
70
  """Check if the given point is within the rectangle."""
@@ -172,6 +177,10 @@ class RectangleAnnotation(Annotation):
172
177
  self.set_cropped_bbox()
173
178
  # Get the bounding box of the rectangle
174
179
  min_x, min_y, max_x, max_y = self.cropped_bbox
180
+
181
+ # Ensure min/max values are correctly ordered to avoid negative width/height
182
+ min_x, max_x = min(min_x, max_x), max(min_x, max_x)
183
+ min_y, max_y = min(min_y, max_y), max(min_y, max_y)
175
184
 
176
185
  # Calculate the window for rasterio
177
186
  window = Window(
@@ -275,10 +284,30 @@ class RectangleAnnotation(Annotation):
275
284
  elif handle == "bottom_right":
276
285
  self.bottom_right = new_pos
277
286
 
287
+ # Normalize coordinates to ensure top_left has smaller values than bottom_right
288
+ self.normalize_coordinates()
289
+
278
290
  self.set_precision(self.top_left, self.bottom_right)
279
291
  self.set_centroid()
280
292
  self.update_graphics_item()
281
293
  self.annotationUpdated.emit(self)
294
+
295
+ def normalize_coordinates(self):
296
+ """Ensure that top_left has smaller coordinates than bottom_right."""
297
+ # Create temporary points to store the normalized coordinates
298
+ x_min = min(self.top_left.x(), self.bottom_right.x())
299
+ y_min = min(self.top_left.y(), self.bottom_right.y())
300
+ x_max = max(self.top_left.x(), self.bottom_right.x())
301
+ y_max = max(self.top_left.y(), self.bottom_right.y())
302
+
303
+ # Update the points
304
+ self.top_left.setX(x_min)
305
+ self.top_left.setY(y_min)
306
+ self.bottom_right.setX(x_max)
307
+ self.bottom_right.setY(y_max)
308
+
309
+ # Update centroid after normalization
310
+ self.set_centroid()
282
311
 
283
312
  @classmethod
284
313
  def combine(cls, annotations: list):
@@ -52,7 +52,7 @@ class DeployModelDialog(QDialog):
52
52
  self.annotation_window = main_window.annotation_window
53
53
 
54
54
  self.setWindowIcon(get_icon("coral.png"))
55
- self.setWindowTitle("AutoDistill Deploy Model (Ctrl + 5)")
55
+ self.setWindowTitle("AutoDistill Deploy Model (Ctrl + 6)")
56
56
  self.resize(400, 325)
57
57
 
58
58
  # Initialize variables
@@ -350,18 +350,29 @@ class DeployModelDialog(QDialog):
350
350
 
351
351
  def update_sam_task_state(self):
352
352
  """
353
- Centralized method to check if SAM is loaded and update task and dropdown accordingly.
353
+ Centralized method to check if SAM is loaded and update task accordingly.
354
+ If the user has selected to use SAM, this function ensures the task is set to 'segment'.
355
+ Crucially, it does NOT alter the task if SAM is not selected, respecting the
356
+ user's choice from the 'Task' dropdown.
354
357
  """
355
- sam_active = (
356
- self.sam_dialog is not None and
357
- self.sam_dialog.loaded_model is not None and
358
- self.use_sam_dropdown.currentText() == "True"
359
- )
360
- if sam_active:
361
- self.task = 'segment'
362
- else:
363
- self.task = 'detect'
364
- self.use_sam_dropdown.setCurrentText("False")
358
+ # Check if the user wants to use the SAM model
359
+ if self.use_sam_dropdown.currentText() == "True":
360
+ # SAM is requested. Check if it's actually available.
361
+ sam_is_available = (
362
+ hasattr(self, 'sam_dialog') and
363
+ self.sam_dialog is not None and
364
+ self.sam_dialog.loaded_model is not None
365
+ )
366
+
367
+ if sam_is_available:
368
+ # If SAM is wanted and available, the task must be segmentation.
369
+ self.task = 'segment'
370
+ else:
371
+ # If SAM is wanted but not available, revert the dropdown and do nothing else.
372
+ # The 'is_sam_model_deployed' function already handles showing an error message.
373
+ self.use_sam_dropdown.setCurrentText("False")
374
+
375
+ # If use_sam_dropdown is "False", do nothing. Let self.task be whatever the user set.
365
376
 
366
377
  def load_model(self):
367
378
  """
@@ -94,6 +94,7 @@ class AnnotationImageWidget(QWidget):
94
94
  self.widget_height = widget_height
95
95
  self.aspect_ratio = 1.0
96
96
  self.pixmap = None
97
+ self.is_loaded = False # Flag for lazy loading
97
98
 
98
99
  self.animation_offset = 0
99
100
  self.animation_timer = QTimer(self)
@@ -101,7 +102,9 @@ class AnnotationImageWidget(QWidget):
101
102
  self.animation_timer.setInterval(75)
102
103
 
103
104
  self.setup_ui()
104
- self.load_and_set_image()
105
+ self.recalculate_aspect_ratio() # Calculate aspect ratio from geometry
106
+ self.update_height(self.widget_height) # Set initial size
107
+ self.update_tooltip()
105
108
 
106
109
  def setup_ui(self):
107
110
  """Set up the basic UI with a label for the image."""
@@ -117,43 +120,72 @@ class AnnotationImageWidget(QWidget):
117
120
  """Updates the tooltip by fetching the latest text from the data item."""
118
121
  self.setToolTip(self.data_item.get_tooltip_text())
119
122
 
120
- def load_and_set_image(self):
121
- """Load image, calculate its aspect ratio, and set the widget's initial size."""
123
+ def recalculate_aspect_ratio(self):
124
+ """Calculate aspect ratio from annotation geometry without loading image."""
125
+ try:
126
+ if hasattr(self.annotation, 'rect'): # RectangleAnnotation
127
+ rect = self.annotation.rect
128
+ if rect.height() > 0:
129
+ self.aspect_ratio = rect.width() / rect.height()
130
+ elif hasattr(self.annotation, 'size'): # PatchAnnotation
131
+ self.aspect_ratio = 1.0
132
+ elif hasattr(self.annotation, 'polygon'): # PolygonAnnotation
133
+ rect = self.annotation.polygon.boundingRect()
134
+ if rect.height() > 0:
135
+ self.aspect_ratio = rect.width() / rect.height()
136
+ else:
137
+ # Fallback for other types or if geometry is not available
138
+ self.aspect_ratio = 1.0
139
+ except Exception as e:
140
+ print(f"Could not determine aspect ratio for {self.annotation.id}: {e}")
141
+ self.aspect_ratio = 1.0
142
+
143
+ def load_image(self):
144
+ """Loads the image pixmap if it hasn't been loaded yet."""
145
+ if self.is_loaded:
146
+ return
147
+
122
148
  try:
123
149
  cropped_pixmap = self.annotation.get_cropped_image_graphic()
124
150
  if cropped_pixmap and not cropped_pixmap.isNull():
125
151
  self.pixmap = cropped_pixmap
126
- if self.pixmap.height() > 0:
127
- self.aspect_ratio = self.pixmap.width() / self.pixmap.height()
128
- else:
129
- self.aspect_ratio = 1.0
152
+ self.is_loaded = True
153
+ self._display_pixmap()
130
154
  else:
131
155
  self.image_label.setText("No Image\nAvailable")
132
156
  self.pixmap = None
133
- self.aspect_ratio = 1.0
134
157
  except Exception as e:
135
158
  print(f"Error loading annotation image: {e}")
136
159
  self.image_label.setText("Error\nLoading Image")
137
160
  self.pixmap = None
138
- self.aspect_ratio = 1.0
139
-
140
- self.update_height(self.widget_height)
141
- # Set the initial tooltip
142
- self.update_tooltip()
143
161
 
144
- def update_height(self, new_height):
145
- """Updates the widget's height and rescales its width and content accordingly."""
146
- self.widget_height = new_height
147
- new_width = int(self.widget_height * self.aspect_ratio)
148
- self.setFixedSize(new_width, new_height)
162
+ def unload_image(self):
163
+ """Unloads the pixmap to free memory."""
164
+ if not self.is_loaded:
165
+ return
166
+ self.pixmap = None
167
+ self.image_label.clear()
168
+ self.is_loaded = False
169
+
170
+ def _display_pixmap(self):
171
+ """Scales and displays the currently loaded pixmap."""
149
172
  if self.pixmap:
173
+ new_width = int(self.widget_height * self.aspect_ratio)
150
174
  scaled_pixmap = self.pixmap.scaled(
151
175
  new_width - 8,
152
- new_height - 8,
176
+ self.widget_height - 8,
153
177
  Qt.KeepAspectRatio,
154
178
  Qt.SmoothTransformation
155
179
  )
156
180
  self.image_label.setPixmap(scaled_pixmap)
181
+
182
+ def update_height(self, new_height):
183
+ """Updates the widget's height and rescales its width and content accordingly."""
184
+ self.widget_height = new_height
185
+ new_width = int(self.widget_height * self.aspect_ratio)
186
+ self.setFixedSize(new_width, new_height)
187
+ if self.pixmap:
188
+ self._display_pixmap()
157
189
  self.update()
158
190
 
159
191
  def update_selection_visuals(self):
@@ -240,7 +272,7 @@ class AnnotationDataItem:
240
272
 
241
273
  self.embedding_x = embedding_x if embedding_x is not None else 0.0
242
274
  self.embedding_y = embedding_y if embedding_y is not None else 0.0
243
- self.embedding_id = embedding_id if embedding_id is not None else 0
275
+ self.embedding_id = embedding_id
244
276
 
245
277
  self._is_selected = False
246
278
  self._preview_label = None
@@ -336,4 +368,4 @@ class AnnotationDataItem:
336
368
  return list(self.annotation.user_confidence.values())[0]
337
369
  elif hasattr(self.annotation, 'machine_confidence') and self.annotation.machine_confidence:
338
370
  return list(self.annotation.machine_confidence.values())[0]
339
- return 0.0
371
+ return 0.0