coralnet-toolbox 0.0.73__py2.py3-none-any.whl → 0.0.75__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 (50) 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/QtDataItem.py +52 -22
  9. coralnet_toolbox/Explorer/QtExplorer.py +293 -1614
  10. coralnet_toolbox/Explorer/QtSettingsWidgets.py +203 -85
  11. coralnet_toolbox/Explorer/QtViewers.py +1568 -0
  12. coralnet_toolbox/Explorer/transformer_models.py +59 -0
  13. coralnet_toolbox/Explorer/yolo_models.py +112 -0
  14. coralnet_toolbox/IO/QtExportTagLabAnnotations.py +30 -10
  15. coralnet_toolbox/IO/QtImportTagLabAnnotations.py +21 -15
  16. coralnet_toolbox/IO/QtOpenProject.py +46 -78
  17. coralnet_toolbox/IO/QtSaveProject.py +18 -43
  18. coralnet_toolbox/MachineLearning/ExportDataset/QtBase.py +1 -1
  19. coralnet_toolbox/MachineLearning/ImportDataset/QtBase.py +253 -141
  20. coralnet_toolbox/MachineLearning/VideoInference/QtBase.py +0 -4
  21. coralnet_toolbox/MachineLearning/VideoInference/YOLO3D/run.py +102 -16
  22. coralnet_toolbox/QtAnnotationWindow.py +16 -10
  23. coralnet_toolbox/QtEventFilter.py +11 -0
  24. coralnet_toolbox/QtImageWindow.py +120 -75
  25. coralnet_toolbox/QtLabelWindow.py +13 -1
  26. coralnet_toolbox/QtMainWindow.py +5 -27
  27. coralnet_toolbox/QtProgressBar.py +52 -27
  28. coralnet_toolbox/Rasters/RasterTableModel.py +28 -8
  29. coralnet_toolbox/SAM/QtDeployGenerator.py +1 -4
  30. coralnet_toolbox/SAM/QtDeployPredictor.py +11 -3
  31. coralnet_toolbox/SeeAnything/QtDeployGenerator.py +805 -162
  32. coralnet_toolbox/SeeAnything/QtDeployPredictor.py +130 -151
  33. coralnet_toolbox/Tools/QtCutSubTool.py +18 -2
  34. coralnet_toolbox/Tools/QtPolygonTool.py +42 -3
  35. coralnet_toolbox/Tools/QtRectangleTool.py +30 -0
  36. coralnet_toolbox/Tools/QtResizeSubTool.py +19 -2
  37. coralnet_toolbox/Tools/QtSAMTool.py +72 -50
  38. coralnet_toolbox/Tools/QtSeeAnythingTool.py +8 -5
  39. coralnet_toolbox/Tools/QtSelectTool.py +27 -3
  40. coralnet_toolbox/Tools/QtSubtractSubTool.py +66 -0
  41. coralnet_toolbox/Tools/__init__.py +2 -0
  42. coralnet_toolbox/__init__.py +1 -1
  43. coralnet_toolbox/utilities.py +158 -47
  44. coralnet_toolbox-0.0.75.dist-info/METADATA +378 -0
  45. {coralnet_toolbox-0.0.73.dist-info → coralnet_toolbox-0.0.75.dist-info}/RECORD +49 -44
  46. coralnet_toolbox-0.0.73.dist-info/METADATA +0 -341
  47. {coralnet_toolbox-0.0.73.dist-info → coralnet_toolbox-0.0.75.dist-info}/WHEEL +0 -0
  48. {coralnet_toolbox-0.0.73.dist-info → coralnet_toolbox-0.0.75.dist-info}/entry_points.txt +0 -0
  49. {coralnet_toolbox-0.0.73.dist-info → coralnet_toolbox-0.0.75.dist-info}/licenses/LICENSE.txt +0 -0
  50. {coralnet_toolbox-0.0.73.dist-info → coralnet_toolbox-0.0.75.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)
@@ -121,24 +121,45 @@ class AnnotationImageWidget(QWidget):
121
121
  self.setToolTip(self.data_item.get_tooltip_text())
122
122
 
123
123
  def recalculate_aspect_ratio(self):
124
- """Calculate aspect ratio from annotation geometry without loading image."""
124
+ """Calculate and store the annotation's aspect ratio."""
125
+ annotation = self.data_item.annotation
126
+
127
+ # Try to use the cropped_bbox attribute first
128
+ if hasattr(annotation, 'cropped_bbox'):
129
+ min_x, min_y, max_x, max_y = annotation.cropped_bbox
130
+ width = max_x - min_x
131
+ height = max_y - min_y
132
+
133
+ if height > 0:
134
+ self.aspect_ratio = width / height
135
+ return
136
+
137
+ # Fallback to bounding box methods
125
138
  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
139
+ top_left = annotation.get_bounding_box_top_left()
140
+ bottom_right = annotation.get_bounding_box_bottom_right()
141
+
142
+ if top_left and bottom_right:
143
+ width = bottom_right.x() - top_left.x()
144
+ height = bottom_right.y() - top_left.y()
145
+
146
+ if height > 0:
147
+ self.aspect_ratio = width / height
148
+ return
149
+ except (AttributeError, TypeError):
150
+ pass
151
+
152
+ # Last resort: try to get aspect ratio from the cropped image
153
+ try:
154
+ pixmap = annotation.get_cropped_image()
155
+ if pixmap and not pixmap.isNull() and pixmap.height() > 0:
156
+ self.aspect_ratio = pixmap.width() / pixmap.height()
157
+ return
158
+ except (AttributeError, TypeError):
159
+ pass
160
+
161
+ # Default to square if we can't determine aspect ratio
162
+ self.aspect_ratio = 1.0
142
163
 
143
164
  def load_image(self):
144
165
  """Loads the image pixmap if it hasn't been loaded yet."""
@@ -356,16 +377,25 @@ class AnnotationDataItem:
356
377
  return "<br>".join(tooltip_parts)
357
378
 
358
379
  def get_effective_confidence(self):
359
- """Get the effective confidence value."""
380
+ """
381
+ Get the effective confidence value, handling scalar, array, and vector predictions.
382
+ """
360
383
  # First check if prediction probabilities are available from model predictions
361
384
  if hasattr(self, 'prediction_probabilities') and self.prediction_probabilities is not None:
362
- if len(self.prediction_probabilities) > 0:
363
- # Use the maximum probability for confidence sorting
364
- return float(np.max(self.prediction_probabilities))
365
-
385
+ probs = self.prediction_probabilities
386
+ try:
387
+ # This will succeed for lists and multi-element numpy arrays
388
+ if len(probs) > 0:
389
+ return float(np.max(probs))
390
+ except TypeError:
391
+ # This will catch the error if `len()` is called on a scalar or 0-D array.
392
+ # In this case, the value of `probs` itself is the confidence score.
393
+ return float(probs)
394
+
366
395
  # Fallback to existing confidence values
367
396
  if self.annotation.verified and hasattr(self.annotation, 'user_confidence') and self.annotation.user_confidence:
368
397
  return list(self.annotation.user_confidence.values())[0]
369
398
  elif hasattr(self.annotation, 'machine_confidence') and self.annotation.machine_confidence:
370
399
  return list(self.annotation.machine_confidence.values())[0]
400
+
371
401
  return 0.0