coralnet-toolbox 0.0.73__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 (41) 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/CoralNet/QtDownload.py +2 -1
  8. coralnet_toolbox/Explorer/QtExplorer.py +16 -14
  9. coralnet_toolbox/Explorer/QtSettingsWidgets.py +114 -82
  10. coralnet_toolbox/IO/QtExportTagLabAnnotations.py +30 -10
  11. coralnet_toolbox/IO/QtImportTagLabAnnotations.py +21 -15
  12. coralnet_toolbox/IO/QtOpenProject.py +46 -78
  13. coralnet_toolbox/IO/QtSaveProject.py +18 -43
  14. coralnet_toolbox/MachineLearning/ExportDataset/QtBase.py +1 -1
  15. coralnet_toolbox/MachineLearning/ImportDataset/QtBase.py +42 -22
  16. coralnet_toolbox/MachineLearning/VideoInference/QtBase.py +0 -4
  17. coralnet_toolbox/QtEventFilter.py +11 -0
  18. coralnet_toolbox/QtImageWindow.py +117 -68
  19. coralnet_toolbox/QtLabelWindow.py +13 -1
  20. coralnet_toolbox/QtMainWindow.py +5 -27
  21. coralnet_toolbox/QtProgressBar.py +52 -27
  22. coralnet_toolbox/Rasters/RasterTableModel.py +8 -8
  23. coralnet_toolbox/SAM/QtDeployPredictor.py +10 -0
  24. coralnet_toolbox/SeeAnything/QtDeployGenerator.py +779 -161
  25. coralnet_toolbox/SeeAnything/QtDeployPredictor.py +86 -149
  26. coralnet_toolbox/Tools/QtCutSubTool.py +18 -2
  27. coralnet_toolbox/Tools/QtResizeSubTool.py +19 -2
  28. coralnet_toolbox/Tools/QtSAMTool.py +72 -50
  29. coralnet_toolbox/Tools/QtSeeAnythingTool.py +8 -5
  30. coralnet_toolbox/Tools/QtSelectTool.py +27 -3
  31. coralnet_toolbox/Tools/QtSubtractSubTool.py +66 -0
  32. coralnet_toolbox/Tools/__init__.py +2 -0
  33. coralnet_toolbox/__init__.py +1 -1
  34. coralnet_toolbox/utilities.py +137 -47
  35. coralnet_toolbox-0.0.74.dist-info/METADATA +375 -0
  36. {coralnet_toolbox-0.0.73.dist-info → coralnet_toolbox-0.0.74.dist-info}/RECORD +40 -38
  37. coralnet_toolbox-0.0.73.dist-info/METADATA +0 -341
  38. {coralnet_toolbox-0.0.73.dist-info → coralnet_toolbox-0.0.74.dist-info}/WHEEL +0 -0
  39. {coralnet_toolbox-0.0.73.dist-info → coralnet_toolbox-0.0.74.dist-info}/entry_points.txt +0 -0
  40. {coralnet_toolbox-0.0.73.dist-info → coralnet_toolbox-0.0.74.dist-info}/licenses/LICENSE.txt +0 -0
  41. {coralnet_toolbox-0.0.73.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
@@ -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)
@@ -1705,37 +1705,27 @@ class ExplorerWindow(QMainWindow):
1705
1705
  if child.widget():
1706
1706
  child.widget().setParent(None)
1707
1707
 
1708
- # Lazily initialize the settings and viewer widgets if they haven't been created yet.
1709
- # This ensures that the widgets are only created once per ExplorerWindow instance.
1710
-
1711
- # Annotation settings panel (filters by image, type, label)
1708
+ # Lazily initialize the settings and viewer widgets
1712
1709
  if self.annotation_settings_widget is None:
1713
1710
  self.annotation_settings_widget = AnnotationSettingsWidget(self.main_window, self)
1714
-
1715
- # Model selection panel (choose feature extraction model)
1716
1711
  if self.model_settings_widget is None:
1717
1712
  self.model_settings_widget = ModelSettingsWidget(self.main_window, self)
1718
-
1719
- # Embedding settings panel (choose dimensionality reduction method)
1720
1713
  if self.embedding_settings_widget is None:
1721
1714
  self.embedding_settings_widget = EmbeddingSettingsWidget(self.main_window, self)
1722
-
1723
- # Annotation viewer (shows annotation image crops in a grid)
1724
1715
  if self.annotation_viewer is None:
1725
1716
  self.annotation_viewer = AnnotationViewer(self)
1726
-
1727
- # Embedding viewer (shows 2D embedding scatter plot)
1728
1717
  if self.embedding_viewer is None:
1729
1718
  self.embedding_viewer = EmbeddingViewer(self)
1730
1719
 
1720
+ # Horizontal layout for the three settings panels (original horizontal layout)
1731
1721
  top_layout = QHBoxLayout()
1732
1722
  top_layout.addWidget(self.annotation_settings_widget, 2)
1733
1723
  top_layout.addWidget(self.model_settings_widget, 1)
1734
1724
  top_layout.addWidget(self.embedding_settings_widget, 1)
1735
1725
  top_container = QWidget()
1736
1726
  top_container.setLayout(top_layout)
1737
- self.main_layout.addWidget(top_container)
1738
1727
 
1728
+ # Horizontal splitter for the two main viewer panels
1739
1729
  middle_splitter = QSplitter(Qt.Horizontal)
1740
1730
  annotation_group = QGroupBox("Annotation Viewer")
1741
1731
  annotation_layout = QVBoxLayout(annotation_group)
@@ -1747,7 +1737,19 @@ class ExplorerWindow(QMainWindow):
1747
1737
  embedding_layout.addWidget(self.embedding_viewer)
1748
1738
  middle_splitter.addWidget(embedding_group)
1749
1739
  middle_splitter.setSizes([500, 500])
1750
- self.main_layout.addWidget(middle_splitter, 1)
1740
+
1741
+ # Create a VERTICAL splitter to manage the height between the settings and viewers.
1742
+ # This makes the top settings panel vertically resizable.
1743
+ main_splitter = QSplitter(Qt.Vertical)
1744
+ main_splitter.addWidget(top_container)
1745
+ main_splitter.addWidget(middle_splitter)
1746
+
1747
+ # Set initial heights to give the settings panel a bit more space by default
1748
+ main_splitter.setSizes([250, 750])
1749
+
1750
+ # Add the new main splitter to the layout instead of the individual components
1751
+ self.main_layout.addWidget(main_splitter, 1)
1752
+
1751
1753
  self.main_layout.addWidget(self.label_window)
1752
1754
 
1753
1755
  self.buttons_layout = QHBoxLayout()