coralnet-toolbox 0.0.72__py2.py3-none-any.whl → 0.0.74__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 (57) hide show
  1. coralnet_toolbox/Annotations/QtAnnotation.py +28 -69
  2. coralnet_toolbox/Annotations/QtMaskAnnotation.py +408 -0
  3. coralnet_toolbox/Annotations/QtMultiPolygonAnnotation.py +72 -56
  4. coralnet_toolbox/Annotations/QtPatchAnnotation.py +165 -216
  5. coralnet_toolbox/Annotations/QtPolygonAnnotation.py +497 -353
  6. coralnet_toolbox/Annotations/QtRectangleAnnotation.py +126 -116
  7. coralnet_toolbox/AutoDistill/QtDeployModel.py +23 -12
  8. coralnet_toolbox/CoralNet/QtDownload.py +2 -1
  9. coralnet_toolbox/Explorer/QtDataItem.py +1 -1
  10. coralnet_toolbox/Explorer/QtExplorer.py +159 -17
  11. coralnet_toolbox/Explorer/QtSettingsWidgets.py +160 -86
  12. coralnet_toolbox/IO/QtExportTagLabAnnotations.py +30 -10
  13. coralnet_toolbox/IO/QtImportTagLabAnnotations.py +21 -15
  14. coralnet_toolbox/IO/QtOpenProject.py +46 -78
  15. coralnet_toolbox/IO/QtSaveProject.py +18 -43
  16. coralnet_toolbox/MachineLearning/DeployModel/QtDetect.py +22 -11
  17. coralnet_toolbox/MachineLearning/DeployModel/QtSegment.py +22 -10
  18. coralnet_toolbox/MachineLearning/ExportDataset/QtBase.py +61 -24
  19. coralnet_toolbox/MachineLearning/ExportDataset/QtClassify.py +5 -1
  20. coralnet_toolbox/MachineLearning/ExportDataset/QtDetect.py +19 -6
  21. coralnet_toolbox/MachineLearning/ExportDataset/QtSegment.py +21 -8
  22. coralnet_toolbox/MachineLearning/ImportDataset/QtBase.py +42 -22
  23. coralnet_toolbox/MachineLearning/VideoInference/QtBase.py +0 -4
  24. coralnet_toolbox/QtAnnotationWindow.py +42 -14
  25. coralnet_toolbox/QtEventFilter.py +19 -2
  26. coralnet_toolbox/QtImageWindow.py +134 -86
  27. coralnet_toolbox/QtLabelWindow.py +14 -2
  28. coralnet_toolbox/QtMainWindow.py +122 -9
  29. coralnet_toolbox/QtProgressBar.py +52 -27
  30. coralnet_toolbox/Rasters/QtRaster.py +59 -7
  31. coralnet_toolbox/Rasters/RasterTableModel.py +42 -14
  32. coralnet_toolbox/SAM/QtBatchInference.py +0 -2
  33. coralnet_toolbox/SAM/QtDeployGenerator.py +22 -11
  34. coralnet_toolbox/SAM/QtDeployPredictor.py +10 -0
  35. coralnet_toolbox/SeeAnything/QtBatchInference.py +19 -221
  36. coralnet_toolbox/SeeAnything/QtDeployGenerator.py +1634 -0
  37. coralnet_toolbox/SeeAnything/QtDeployPredictor.py +107 -154
  38. coralnet_toolbox/SeeAnything/QtTrainModel.py +115 -45
  39. coralnet_toolbox/SeeAnything/__init__.py +2 -0
  40. coralnet_toolbox/Tools/QtCutSubTool.py +18 -2
  41. coralnet_toolbox/Tools/QtResizeSubTool.py +19 -2
  42. coralnet_toolbox/Tools/QtSAMTool.py +222 -57
  43. coralnet_toolbox/Tools/QtSeeAnythingTool.py +223 -55
  44. coralnet_toolbox/Tools/QtSelectSubTool.py +6 -4
  45. coralnet_toolbox/Tools/QtSelectTool.py +27 -3
  46. coralnet_toolbox/Tools/QtSubtractSubTool.py +66 -0
  47. coralnet_toolbox/Tools/QtWorkAreaTool.py +25 -13
  48. coralnet_toolbox/Tools/__init__.py +2 -0
  49. coralnet_toolbox/__init__.py +1 -1
  50. coralnet_toolbox/utilities.py +137 -47
  51. coralnet_toolbox-0.0.74.dist-info/METADATA +375 -0
  52. {coralnet_toolbox-0.0.72.dist-info → coralnet_toolbox-0.0.74.dist-info}/RECORD +56 -53
  53. coralnet_toolbox-0.0.72.dist-info/METADATA +0 -341
  54. {coralnet_toolbox-0.0.72.dist-info → coralnet_toolbox-0.0.74.dist-info}/WHEEL +0 -0
  55. {coralnet_toolbox-0.0.72.dist-info → coralnet_toolbox-0.0.74.dist-info}/entry_points.txt +0 -0
  56. {coralnet_toolbox-0.0.72.dist-info → coralnet_toolbox-0.0.74.dist-info}/licenses/LICENSE.txt +0 -0
  57. {coralnet_toolbox-0.0.72.dist-info → coralnet_toolbox-0.0.74.dist-info}/top_level.txt +0 -0
@@ -2,13 +2,14 @@ import warnings
2
2
  warnings.filterwarnings("ignore", category=DeprecationWarning)
3
3
 
4
4
  from rasterio.windows import Window
5
- from shapely.ops import split
6
- from shapely.geometry import Polygon, LineString, box
5
+
6
+ from shapely.ops import split, unary_union
7
+ from shapely.geometry import Point, LineString, box
7
8
 
8
9
  from PyQt5.QtCore import Qt, QPointF
9
- from PyQt5.QtWidgets import QGraphicsScene, QGraphicsPolygonItem
10
- from PyQt5.QtGui import (QPixmap, QColor, QPen, QBrush, QPolygonF, QPainter,
11
- QImage, QRegion)
10
+ from PyQt5.QtWidgets import QGraphicsScene, QGraphicsPathItem
11
+ from PyQt5.QtGui import (QPixmap, QColor, QPen, QBrush, QPolygonF,
12
+ QPainter, QImage, QRegion, QPainterPath)
12
13
 
13
14
  from coralnet_toolbox.Annotations.QtAnnotation import Annotation
14
15
 
@@ -67,21 +68,60 @@ class RectangleAnnotation(Annotation):
67
68
  self.annotation_size = int(max(width, height))
68
69
 
69
70
  def contains_point(self, point: QPointF) -> bool:
70
- """Check if the given point is within the rectangle."""
71
- return (self.top_left.x() <= point.x() <= self.bottom_right.x() and
72
- self.top_left.y() <= point.y() <= self.bottom_right.y())
71
+ """Check if the given point is within the rectangle using Shapely."""
72
+ try:
73
+ # Create a shapely box from the rectangle's corner coordinates
74
+ shapely_rect = box(self.top_left.x(),
75
+ self.top_left.y(),
76
+ self.bottom_right.x(),
77
+ self.bottom_right.y())
78
+
79
+ # Convert the input QPointF to a Shapely Point
80
+ shapely_point = Point(point.x(), point.y())
81
+
82
+ # Return Shapely's boolean result for the containment check
83
+ return shapely_rect.contains(shapely_point)
84
+
85
+ except Exception:
86
+ # Fallback to the original implementation if Shapely fails
87
+ return (self.top_left.x() <= point.x() <= self.bottom_right.x() and
88
+ self.top_left.y() <= point.y() <= self.bottom_right.y())
73
89
 
74
90
  def get_centroid(self):
75
91
  """Get the centroid of the annotation."""
76
92
  return (float(self.center_xy.x()), float(self.center_xy.y()))
77
93
 
78
94
  def get_area(self):
79
- """Calculate the area of the rectangle."""
80
- return (self.bottom_right.x() - self.top_left.x()) * (self.bottom_right.y() - self.top_left.y())
95
+ """Calculate the area of the rectangle using Shapely."""
96
+ try:
97
+ # Create a shapely box from the rectangle's corner coordinates
98
+ shapely_rect = box(self.top_left.x(),
99
+ self.top_left.y(),
100
+ self.bottom_right.x(),
101
+ self.bottom_right.y())
102
+ return shapely_rect.area
103
+
104
+ except Exception:
105
+ # Fallback to the original implementation if Shapely fails
106
+ width = self.bottom_right.x() - self.top_left.x()
107
+ height = self.bottom_right.y() - self.top_left.y()
108
+ return width * height
81
109
 
82
110
  def get_perimeter(self):
83
- """Calculate the perimeter of the rectangle."""
84
- return 2 * (self.bottom_right.x() - self.top_left.x()) + 2 * (self.bottom_right.y() - self.top_left.y())
111
+ """Calculate the perimeter of the rectangle using Shapely."""
112
+ try:
113
+ # Create a shapely box from the rectangle's corner coordinates
114
+ shapely_rect = box(self.top_left.x(),
115
+ self.top_left.y(),
116
+ self.bottom_right.x(),
117
+ self.bottom_right.y())
118
+ return shapely_rect.length
119
+
120
+ except Exception:
121
+ # Fallback to the original implementation if Shapely fails
122
+ width = self.bottom_right.x() - self.top_left.x()
123
+ height = self.bottom_right.y() - self.top_left.y()
124
+ return 2 * width + 2 * height
85
125
 
86
126
  def get_polygon(self):
87
127
  """Get the polygon representation of this rectangle."""
@@ -92,6 +132,15 @@ class RectangleAnnotation(Annotation):
92
132
  QPointF(self.top_left.x(), self.bottom_right.y())
93
133
  ]
94
134
  return QPolygonF(points)
135
+
136
+ def get_painter_path(self) -> QPainterPath:
137
+ """
138
+ Get a QPainterPath representation of the annotation.
139
+ """
140
+ path = QPainterPath()
141
+ polygon = self.get_polygon()
142
+ path.addPolygon(polygon)
143
+ return path
95
144
 
96
145
  def get_bounding_box_top_left(self):
97
146
  """Get the top-left corner of the bounding box."""
@@ -102,69 +151,55 @@ class RectangleAnnotation(Annotation):
102
151
  return self.bottom_right
103
152
 
104
153
  def get_cropped_image_graphic(self):
105
- """Create a cropped image with a mask and dotted outline."""
154
+ """Create a cropped image with a mask and solid outline."""
106
155
  if self.cropped_image is None:
107
156
  return None
108
157
 
109
158
  # Create a QImage with transparent background for the mask
110
159
  masked_image = QImage(self.cropped_image.size(), QImage.Format_ARGB32)
111
- masked_image.fill(Qt.transparent) # Transparent background
112
-
113
- # Create a QPainter to draw the polygon onto the mask
114
- painter = QPainter(masked_image)
115
- painter.setRenderHint(QPainter.Antialiasing)
116
- painter.setBrush(QBrush(Qt.white)) # White fill for the mask area
117
- painter.setPen(Qt.NoPen)
160
+ masked_image.fill(Qt.transparent)
118
161
 
119
- # Define the rectangle points based on top_left and bottom_right
120
- rectangle_points = [
121
- self.top_left,
122
- QPointF(self.bottom_right.x(), self.top_left.y()),
123
- self.bottom_right,
124
- QPointF(self.top_left.x(), self.bottom_right.y())
125
- ]
126
-
127
- # Create a copy of the points that are transformed to be relative to the cropped_image
128
- cropped_points = [QPointF(point.x() - self.cropped_bbox[0],
129
- point.y() - self.cropped_bbox[1]) for point in rectangle_points]
162
+ # Create a painter to draw the path onto the mask
163
+ mask_painter = QPainter(masked_image)
164
+ mask_painter.setRenderHint(QPainter.Antialiasing)
165
+ mask_painter.setBrush(QBrush(Qt.white))
166
+ mask_painter.setPen(Qt.NoPen)
130
167
 
131
- # Create a polygon from the cropped points
132
- polygon = QPolygonF(cropped_points)
168
+ # Create a path from the points relative to the cropped image
169
+ path = QPainterPath()
170
+ # The cropped image is the same size as the rectangle, so the path is from (0,0)
171
+ path.addRect(0, 0, self.cropped_image.width(), self.cropped_image.height())
133
172
 
134
- # Draw the polygon onto the mask
135
- painter.drawPolygon(polygon)
136
- painter.end()
173
+ # Draw the path onto the mask
174
+ mask_painter.drawPath(path)
175
+ mask_painter.end()
137
176
 
138
177
  # Convert the mask QImage to QPixmap and create a bitmap mask
139
- # We want the inside of the polygon to show the image, so we DON'T use MaskInColor
140
178
  mask_pixmap = QPixmap.fromImage(masked_image)
141
179
  mask_bitmap = mask_pixmap.createMaskFromColor(Qt.white, Qt.MaskOutColor)
142
-
143
- # Convert bitmap to region for clipping
144
180
  mask_region = QRegion(mask_bitmap)
145
181
 
146
182
  # Create the result image
147
183
  cropped_image_graphic = QPixmap(self.cropped_image.size())
148
-
149
- # First draw the entire original image at 50% opacity (for area outside polygon)
150
184
  result_painter = QPainter(cropped_image_graphic)
151
185
  result_painter.setRenderHint(QPainter.Antialiasing)
152
- result_painter.setOpacity(0.5) # 50% opacity for outside the polygon
186
+
187
+ # Draw the background at 50% opacity
188
+ result_painter.setOpacity(0.5)
153
189
  result_painter.drawPixmap(0, 0, self.cropped_image)
154
190
 
155
- # Then draw the full opacity image only in the masked area (inside the polygon)
156
- result_painter.setOpacity(1.0) # Reset to full opacity
191
+ # Draw the full-opacity image inside the masked region
192
+ result_painter.setOpacity(1.0)
157
193
  result_painter.setClipRegion(mask_region)
158
194
  result_painter.drawPixmap(0, 0, self.cropped_image)
159
195
 
160
- # Draw the dotted line outline on top
196
+ # Draw the solid line outline on top
161
197
  pen = QPen(Qt.black)
162
- pen.setStyle(Qt.SolidLine) # Solid line
163
- pen.setWidth(1) # Line width
198
+ pen.setStyle(Qt.SolidLine)
199
+ pen.setWidth(1)
164
200
  result_painter.setPen(pen)
165
- result_painter.setClipping(False) # Disable clipping for the outline
166
- result_painter.drawPolygon(polygon)
167
-
201
+ result_painter.setClipping(False)
202
+ result_painter.drawPath(path)
168
203
  result_painter.end()
169
204
 
170
205
  return cropped_image_graphic
@@ -198,29 +233,25 @@ class RectangleAnnotation(Annotation):
198
233
  self.annotationUpdated.emit(self) # Notify update
199
234
 
200
235
  def create_graphics_item(self, scene: QGraphicsScene):
201
- """Create all graphics items for the rectangle annotation and add them to the scene as a group."""
202
- # Use a polygon (rectangle) as the main graphics item for consistency
203
- points = [
204
- self.top_left,
205
- QPointF(self.bottom_right.x(), self.top_left.y()),
206
- self.bottom_right,
207
- QPointF(self.top_left.x(), self.bottom_right.y())
208
- ]
209
- self.graphics_item = QGraphicsPolygonItem(QPolygonF(points))
210
- # Call parent to handle group and helpers
236
+ """Create all graphics items for the annotation and add them to the scene."""
237
+ # Get the complete shape as a QPainterPath.
238
+ path = self.get_painter_path()
239
+
240
+ # Use a QGraphicsPathItem for rendering.
241
+ self.graphics_item = QGraphicsPathItem(path)
242
+
243
+ # Call the parent class method to handle grouping, styling, and adding to the scene.
211
244
  super().create_graphics_item(scene)
212
245
 
213
246
  def update_graphics_item(self):
214
247
  """Update the graphical representation of the rectangle annotation."""
215
- # Use a polygon (rectangle) as the main graphics item for consistency
216
- points = [
217
- self.top_left,
218
- QPointF(self.bottom_right.x(), self.top_left.y()),
219
- self.bottom_right,
220
- QPointF(self.top_left.x(), self.bottom_right.y())
221
- ]
222
- self.graphics_item = QGraphicsPolygonItem(QPolygonF(points))
223
- # Call parent to handle group and helpers
248
+ # Get the complete shape as a QPainterPath.
249
+ path = self.get_painter_path()
250
+
251
+ # Use a QGraphicsPathItem to correctly represent the shape.
252
+ self.graphics_item = QGraphicsPathItem(path)
253
+
254
+ # Call the parent class method to handle rebuilding the graphics group.
224
255
  super().update_graphics_item()
225
256
 
226
257
  def update_polygon(self, delta):
@@ -311,75 +342,54 @@ class RectangleAnnotation(Annotation):
311
342
 
312
343
  @classmethod
313
344
  def combine(cls, annotations: list):
314
- """Combine multiple rectangle annotations into a single encompassing rectangle,
315
- but only if every annotation overlaps with at least one other annotation.
316
-
317
- Args:
318
- annotations: List of RectangleAnnotations objects to combine.
319
-
320
- Returns:
321
- A new RectangleAnnotations that encompasses all input rectangles if every
322
- annotation overlaps with at least one other, otherwise None.
345
+ """Combine multiple rectangle annotations into a single encompassing rectangle
346
+ using Shapely, but only if every annotation overlaps with at least one other.
323
347
  """
324
348
  if not annotations:
325
349
  return None
326
-
327
350
  if len(annotations) == 1:
328
351
  return annotations[0]
329
352
 
330
- # Check if each annotation overlaps with at least one other annotation
331
- for i, anno_i in enumerate(annotations):
353
+ # Convert all annotations to Shapely boxes
354
+ shapely_rects = []
355
+ for anno in annotations:
356
+ shaped_rect = box(anno.top_left.x(),
357
+ anno.top_left.y(),
358
+ anno.bottom_right.x(),
359
+ anno.bottom_right.y())
360
+
361
+ shapely_rects.append(shaped_rect)
362
+
363
+ # 1. Perform the overlap check using Shapely's `intersects`
364
+ for i, rect_i in enumerate(shapely_rects):
332
365
  has_overlap = False
333
- for j, anno_j in enumerate(annotations):
334
- if i == j:
335
- continue
336
-
337
- # Check if these two rectangles overlap
338
- if (anno_i.top_left.x() < anno_j.bottom_right.x() and
339
- anno_i.bottom_right.x() > anno_j.top_left.x() and
340
- anno_i.top_left.y() < anno_j.bottom_right.y() and
341
- anno_i.bottom_right.y() > anno_j.top_left.y()):
366
+ for j, rect_j in enumerate(shapely_rects):
367
+ if i != j and rect_i.intersects(rect_j):
342
368
  has_overlap = True
343
369
  break
344
-
345
- # If any annotation doesn't overlap with any other, return None
346
370
  if not has_overlap:
347
- return None
371
+ return None # An annotation is isolated, cancel the combine
348
372
 
349
- # Find the minimum top-left and maximum bottom-right coordinates
350
- min_x = min(anno.top_left.x() for anno in annotations)
351
- min_y = min(anno.top_left.y() for anno in annotations)
352
- max_x = max(anno.bottom_right.x() for anno in annotations)
353
- max_y = max(anno.bottom_right.y() for anno in annotations)
373
+ # 2. Get the encompassing bounding box using Shapely's union and bounds
374
+ merged_geom = unary_union(shapely_rects)
375
+ min_x, min_y, max_x, max_y = merged_geom.bounds
354
376
 
355
377
  # Create new rectangle with these bounds
356
378
  top_left = QPointF(min_x, min_y)
357
379
  bottom_right = QPointF(max_x, max_y)
358
380
 
359
381
  # Extract info from the first annotation
360
- short_label_code = annotations[0].label.short_label_code
361
- long_label_code = annotations[0].label.long_label_code
362
- color = annotations[0].label.color
363
- image_path = annotations[0].image_path
364
- label_id = annotations[0].label.id
365
-
366
- # Create a new annotation with the merged points
382
+ first_anno = annotations[0]
367
383
  new_annotation = cls(
368
384
  top_left=top_left,
369
385
  bottom_right=bottom_right,
370
- short_label_code=short_label_code,
371
- long_label_code=long_label_code,
372
- color=color,
373
- image_path=image_path,
374
- label_id=label_id
386
+ short_label_code=first_anno.label.short_label_code,
387
+ long_label_code=first_anno.label.long_label_code,
388
+ color=first_anno.label.color,
389
+ image_path=first_anno.image_path,
390
+ label_id=first_anno.label.id
375
391
  )
376
392
 
377
- # All input annotations have the same rasterio source, use it for the new one
378
- if all(hasattr(anno, 'rasterio_src') and anno.rasterio_src is not None for anno in annotations):
379
- if len(set(id(anno.rasterio_src) for anno in annotations)) == 1:
380
- new_annotation.rasterio_src = annotations[0].rasterio_src
381
- new_annotation.create_cropped_image(new_annotation.rasterio_src)
382
-
383
393
  return new_annotation
384
394
 
385
395
  @classmethod
@@ -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
  """
@@ -946,7 +946,8 @@ class DownloadDialog(QDialog):
946
946
 
947
947
  try:
948
948
  # Check if there is a next page button and it's enabled
949
- element_text = 'form.no-padding [type="submit"][value=">"]'
949
+ # ---- THIS IS THE MODIFIED LINE ----
950
+ element_text = 'a[title="Next page"]'
950
951
 
951
952
  try:
952
953
  next_button = self.driver.find_element(By.CSS_SELECTOR, element_text)
@@ -272,7 +272,7 @@ class AnnotationDataItem:
272
272
 
273
273
  self.embedding_x = embedding_x if embedding_x is not None else 0.0
274
274
  self.embedding_y = embedding_y if embedding_y is not None else 0.0
275
- self.embedding_id = embedding_id if embedding_id is not None else 0
275
+ self.embedding_id = embedding_id
276
276
 
277
277
  self._is_selected = False
278
278
  self._preview_label = None