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
@@ -1,8 +1,6 @@
1
1
  import warnings
2
2
 
3
- import time
4
-
5
- from PyQt5.QtCore import pyqtSignal
3
+ from PyQt5.QtCore import pyqtSignal, QPropertyAnimation, QEventLoop
6
4
  from PyQt5.QtWidgets import QProgressBar, QVBoxLayout, QDialog, QPushButton, QApplication
7
5
 
8
6
  warnings.filterwarnings("ignore", category=DeprecationWarning)
@@ -112,16 +110,35 @@ class ProgressBar(QDialog):
112
110
  def update_progress(self, new_title=None):
113
111
  """
114
112
  Increment the progress by one step.
115
- Updates the UI and checks if progress is complete.
113
+ Updates the UI intermittently to improve performance and checks if progress is complete.
116
114
  """
117
115
  if new_title is not None:
118
116
  self.setWindowTitle(new_title)
119
117
 
120
- if not self.canceled:
121
- self.value += 1
118
+ if self.canceled:
119
+ return
120
+
121
+ self.value += 1
122
+
123
+ # --- Performance Improvement ---
124
+ # To avoid excessive UI repaints that slow down the process, we only update
125
+ # the visual progress bar periodically. This aims for about 100 updates
126
+ # over the entire range, ensuring a smooth look without bogging down the main task.
127
+ # 'max(1, ...)' ensures we always have an interval of at least 1.
128
+ update_interval = max(1, self.max_value // 100)
129
+
130
+ # We update the bar visually only under two conditions:
131
+ # 1. It's the very last step, to ensure it always finishes at 100%.
132
+ # 2. The current value is a multiple of our calculated interval.
133
+ is_last_step = self.value >= self.max_value
134
+ is_update_step = self.value % update_interval == 0
135
+
136
+ if is_update_step or is_last_step:
122
137
  self.progress_bar.setValue(self.value)
123
- if self.value >= self.max_value:
124
- self.stop_progress()
138
+
139
+ # This is crucial. It processes pending events, allowing the GUI to
140
+ # redraw with the new progress value and to respond to user input,
141
+ # like clicking the 'Cancel' button.
125
142
  QApplication.processEvents()
126
143
 
127
144
  def update_progress_percentage(self, percentage):
@@ -141,30 +158,38 @@ class ProgressBar(QDialog):
141
158
 
142
159
  def finish_progress(self, duration_ms=500):
143
160
  """
144
- Animate the progress bar to its maximum value regardless of current value.
145
- This creates a visual effect of the progress bar completing over a short duration.
161
+ Animate the progress bar to its maximum value using a non-blocking animation.
162
+ This creates a smooth visual effect of completion without freezing the UI.
146
163
 
147
164
  Args:
148
165
  duration_ms: The duration in milliseconds for the animation (default: 500)
149
166
  """
150
-
151
- # Calculate the steps and delay
152
- start_value = self.value
153
- steps_needed = self.max_value - start_value
154
- if steps_needed <= 0:
155
- self.progress_bar.setValue(self.max_value)
156
- QApplication.processEvents()
167
+ # If the progress is already complete, just set the final value and exit.
168
+ if self.value >= self.max_value:
169
+ self.stop_progress()
157
170
  return
158
-
159
- # Calculate delay between steps (minimum 1ms)
160
- delay = max(duration_ms / steps_needed / 1000, 0.001)
161
-
162
- # Animate the progress
163
- for current in range(start_value + 1, self.max_value + 1):
164
- self.value = current
165
- self.progress_bar.setValue(current)
166
- QApplication.processEvents()
167
- time.sleep(delay)
171
+
172
+ # --- Non-Blocking Animation using QPropertyAnimation ---
173
+ # QPropertyAnimation is the standard Qt way to animate widget properties.
174
+ # It runs on the main event loop, so it does not freeze the application
175
+ # like the previous time.sleep() implementation. The property name "value"
176
+ # is passed as a bytes object (b"value").
177
+ self.animation = QPropertyAnimation(self.progress_bar, b"value")
178
+ self.animation.setDuration(duration_ms)
179
+ self.animation.setStartValue(self.value)
180
+ self.animation.setEndValue(self.max_value)
181
+ self.animation.start()
182
+
183
+ # We run a local event loop that waits for the animation's 'finished'
184
+ # signal. This ensures that the animation completes visually before
185
+ # this method returns control to the calling code, which is often
186
+ # the desired behavior for a "finishing" step.
187
+ loop = QEventLoop()
188
+ self.animation.finished.connect(loop.quit)
189
+ loop.exec_()
190
+
191
+ # Finally, update our internal state variable to match the final progress.
192
+ self.value = self.max_value
168
193
 
169
194
  def stop_progress(self):
170
195
  """
@@ -2,6 +2,7 @@ import warnings
2
2
 
3
3
  import os
4
4
  import gc
5
+ from collections import defaultdict
5
6
  from typing import Optional, Set, List
6
7
 
7
8
  import cv2
@@ -13,7 +14,6 @@ from PyQt5.QtCore import QObject
13
14
 
14
15
  from coralnet_toolbox.utilities import rasterio_open
15
16
  from coralnet_toolbox.utilities import rasterio_to_qimage
16
- from coralnet_toolbox.utilities import rasterio_to_cropped_image
17
17
  from coralnet_toolbox.utilities import work_area_to_numpy
18
18
  from coralnet_toolbox.utilities import pixmap_to_numpy
19
19
 
@@ -67,6 +67,10 @@ class Raster(QObject):
67
67
  self.labels: Set = set()
68
68
  self.annotation_count = 0
69
69
  self.annotations: List = [] # Store the actual annotations
70
+ self.label_counts = {} # Store counts of annotations per label
71
+
72
+ self.label_set: Set[str] = set() # Add sets for efficient lookups
73
+ self.label_to_types_map = {} # This replaces annotation_types and annotation_type_set
70
74
 
71
75
  # Work Area state
72
76
  self.work_areas: List = [] # Store work area information
@@ -238,6 +242,7 @@ class Raster(QObject):
238
242
  def update_annotation_info(self, annotations: list):
239
243
  """
240
244
  Update annotation-related information for this raster.
245
+ This now builds a more powerful cache mapping labels to their annotation types.
241
246
 
242
247
  Args:
243
248
  annotations (list): List of annotation objects
@@ -246,13 +251,59 @@ class Raster(QObject):
246
251
  self.annotation_count = len(annotations)
247
252
  self.has_annotations = bool(annotations)
248
253
 
249
- # Check for predictions
250
- predictions = [a.machine_confidence for a in annotations if a.machine_confidence != {}]
254
+ predictions = [a.machine_confidence for a in annotations if a.machine_confidence]
251
255
  self.has_predictions = len(predictions) > 0
252
256
 
253
- # Update labels
254
- self.labels = {annotation.label for annotation in annotations if annotation.label}
257
+ # Clear previous data
258
+ self.label_counts.clear()
259
+ self.label_set.clear()
260
+ self.label_to_types_map.clear() # Clear the new map
261
+
262
+ # Use a defaultdict to simplify the aggregation logic
263
+ temp_map = defaultdict(set)
264
+
265
+ for annotation in annotations:
266
+ # Process label information
267
+ if annotation.label:
268
+ if hasattr(annotation.label, 'short_label_code'):
269
+ label_name = annotation.label.short_label_code
270
+ else:
271
+ label_name = str(annotation.label)
272
+
273
+ # Update label counts and the set of all labels
274
+ self.label_counts[label_name] = self.label_counts.get(label_name, 0) + 1
275
+ self.label_set.add(label_name)
276
+
277
+ # Process annotation type information and link it to the label
278
+ anno_type = annotation.__class__.__name__
279
+ temp_map[label_name].add(anno_type)
280
+
281
+ # Convert defaultdict back to a regular dict for the final attribute
282
+ self.label_to_types_map = dict(temp_map)
255
283
 
284
+ @property
285
+ def annotation_types(self) -> dict:
286
+ """
287
+ Computes a simple count of each annotation type on-the-fly.
288
+ This property provides backward compatibility for features like the tooltip
289
+ without needing to store this data permanently.
290
+
291
+ Returns:
292
+ dict: A dictionary mapping annotation type names to their counts.
293
+ e.g., {'PolygonAnnotation': 5, 'PointAnnotation': 2}
294
+ """
295
+ type_counts = defaultdict(int)
296
+ # The self.label_to_types_map structure is {'label': {'type1', 'type2'}}
297
+ # This is not ideal for counting total types. We need the original annotations list.
298
+ if not self.annotations:
299
+ return {}
300
+
301
+ for annotation in self.annotations:
302
+ anno_type = annotation.__class__.__name__
303
+ type_counts[anno_type] += 1
304
+
305
+ return dict(type_counts)
306
+
256
307
  def matches_filter(self,
257
308
  search_text="",
258
309
  search_label="",
@@ -283,8 +334,9 @@ class Raster(QObject):
283
334
  label_match = False
284
335
 
285
336
  # Check actual annotation labels (always consider these)
286
- for label in self.labels:
287
- if hasattr(label, 'short_label_code') and search_label in label.short_label_code:
337
+ # Look for the search label in the label_set instead of self.labels
338
+ for label_code in self.label_set:
339
+ if search_label in label_code:
288
340
  label_match = True
289
341
  break
290
342
 
@@ -20,8 +20,9 @@ class RasterTableModel(QAbstractTableModel):
20
20
  Custom table model for displaying a list of Raster objects.
21
21
  """
22
22
  # Column indices
23
- FILENAME_COL = 0
24
- ANNOTATION_COUNT_COL = 1
23
+ CHECKBOX_COL = 0
24
+ FILENAME_COL = 1
25
+ ANNOTATION_COUNT_COL = 2
25
26
 
26
27
  # Row colors
27
28
  HIGHLIGHTED_COLOR = QColor(173, 216, 230) # Light blue
@@ -39,13 +40,10 @@ class RasterTableModel(QAbstractTableModel):
39
40
  self.raster_manager = raster_manager
40
41
  self.filtered_paths: List[str] = []
41
42
 
42
- # We'll remove this separate tracking mechanism to avoid inconsistency
43
- # self.highlighted_paths: Set[str] = set()
44
-
45
- self.column_headers = ["Image Name", "Annotations"]
43
+ self.column_headers = ["\u2713", "Image Name", "Annotations"]
46
44
 
47
45
  # Column widths
48
- self.column_widths = [-1, 120] # -1 means stretch
46
+ self.column_widths = [30, -1, 120] # -1 means stretch
49
47
 
50
48
  # Connect to manager signals
51
49
  self.raster_manager.rasterAdded.connect(self.on_raster_added)
@@ -82,14 +80,16 @@ class RasterTableModel(QAbstractTableModel):
82
80
  raster.set_display_name(max_length=25)
83
81
 
84
82
  if role == Qt.DisplayRole:
85
- if index.column() == self.FILENAME_COL:
83
+ if index.column() == self.CHECKBOX_COL:
84
+ return "\u2713" if raster.checkbox_state else ""
85
+ elif index.column() == self.FILENAME_COL:
86
86
  return raster.display_name
87
87
  elif index.column() == self.ANNOTATION_COUNT_COL:
88
88
  return str(raster.annotation_count)
89
89
 
90
90
  elif role == Qt.TextAlignmentRole:
91
91
  return Qt.AlignCenter
92
-
92
+
93
93
  elif role == Qt.FontRole:
94
94
  # Bold the selected raster's text
95
95
  if raster.is_selected:
@@ -106,12 +106,40 @@ class RasterTableModel(QAbstractTableModel):
106
106
 
107
107
  elif role == Qt.ToolTipRole:
108
108
  if index.column() == self.FILENAME_COL:
109
- # Include full path and metadata in tooltip
110
109
  dimensions = raster.metadata.get('dimensions', f"{raster.width}x{raster.height}")
111
- return (f"Path: {path}\n"
112
- f"Dimensions: {dimensions}\n"
113
- f"Has Annotations: {'Yes' if raster.has_annotations else 'No'}\n"
114
- f"Has Predictions: {'Yes' if raster.has_predictions else 'No'}")
110
+
111
+ tooltip_parts = [
112
+ f"<b>Path:</b> {path}",
113
+ f"<b>Dimensions:</b> {dimensions}",
114
+ f"<b>Annotations:</b> {'Yes' if raster.has_annotations else 'No'}",
115
+ f"<b>Predictions:</b> {'Yes' if raster.has_predictions else 'No'}"
116
+ ]
117
+
118
+ if raster.has_work_areas():
119
+ tooltip_parts.append(f"<b>Work Areas:</b> {raster.count_work_items()}")
120
+
121
+ return "<br>".join(tooltip_parts)
122
+
123
+ elif index.column() == self.ANNOTATION_COUNT_COL and raster.annotation_count > 0:
124
+ tooltip_text = f"<b>Total annotations:</b> {raster.annotation_count}"
125
+
126
+ # Add annotation counts per label using a for loop
127
+ if hasattr(raster, 'label_counts') and raster.label_counts:
128
+ label_items = []
129
+ for label, count in raster.label_counts.items():
130
+ label_items.append(f"<li>{label}: {count}</li>")
131
+ label_counts_text = "".join(label_items)
132
+ tooltip_text += f"<br><br><b>Annotations by label:</b><ul>{label_counts_text}</ul>"
133
+
134
+ # Add annotation counts per type using a for loop
135
+ if hasattr(raster, 'annotation_types') and raster.annotation_types:
136
+ type_items = []
137
+ for type_name, count in raster.annotation_types.items():
138
+ type_items.append(f"<li>{type_name}: {count}</li>")
139
+ type_counts_text = "".join(type_items)
140
+ tooltip_text += f"<br><b>Annotations by type:</b><ul>{type_counts_text}</ul>"
141
+
142
+ return tooltip_text
115
143
 
116
144
  return None
117
145
 
@@ -35,8 +35,6 @@ class BatchInferenceDialog(QDialog):
35
35
  self.deploy_model_dialog = None
36
36
  self.loaded_model = None
37
37
 
38
- self.annotations = []
39
- self.prepared_patches = []
40
38
  self.image_paths = []
41
39
 
42
40
  self.layout = QVBoxLayout(self)
@@ -384,18 +384,29 @@ class DeployGeneratorDialog(QDialog):
384
384
 
385
385
  def update_sam_task_state(self):
386
386
  """
387
- Centralized method to check if SAM is loaded and update task and dropdown accordingly.
387
+ Centralized method to check if SAM is loaded and update task accordingly.
388
+ If the user has selected to use SAM, this function ensures the task is set to 'segment'.
389
+ Crucially, it does NOT alter the task if SAM is not selected, respecting the
390
+ user's choice from the 'Task' dropdown.
388
391
  """
389
- sam_active = (
390
- self.sam_dialog is not None and
391
- self.sam_dialog.loaded_model is not None and
392
- self.use_sam_dropdown.currentText() == "True"
393
- )
394
- if sam_active:
395
- self.task = 'segment'
396
- else:
397
- self.task = 'detect'
398
- self.use_sam_dropdown.setCurrentText("False")
392
+ # Check if the user wants to use the SAM model
393
+ if self.use_sam_dropdown.currentText() == "True":
394
+ # SAM is requested. Check if it's actually available.
395
+ sam_is_available = (
396
+ hasattr(self, 'sam_dialog') and
397
+ self.sam_dialog is not None and
398
+ self.sam_dialog.loaded_model is not None
399
+ )
400
+
401
+ if sam_is_available:
402
+ # If SAM is wanted and available, the task must be segmentation.
403
+ self.task = 'segment'
404
+ else:
405
+ # If SAM is wanted but not available, revert the dropdown and do nothing else.
406
+ # The 'is_sam_model_deployed' function already handles showing an error message.
407
+ self.use_sam_dropdown.setCurrentText("False")
408
+
409
+ # If use_sam_dropdown is "False", do nothing. Let self.task be whatever the user set.
399
410
 
400
411
  def load_model(self):
401
412
  """
@@ -142,6 +142,12 @@ class DeployPredictorDialog(QDialog):
142
142
  """
143
143
  group_box = QGroupBox("Parameters")
144
144
  layout = QFormLayout()
145
+
146
+ # Allow holes dropdown
147
+ self.allow_holes_dropdown = QComboBox()
148
+ self.allow_holes_dropdown.addItems(["True", "False"])
149
+ self.allow_holes_dropdown.setCurrentIndex(1) # Default to False
150
+ layout.addRow("Allow Holes:", self.allow_holes_dropdown)
145
151
 
146
152
  # Resize image dropdown
147
153
  self.resize_image_dropdown = QComboBox()
@@ -236,6 +242,10 @@ class DeployPredictorDialog(QDialog):
236
242
  group_box.setLayout(layout)
237
243
  self.layout.addWidget(group_box)
238
244
 
245
+ def get_allow_holes(self):
246
+ """Return the current setting for allowing holes."""
247
+ return self.allow_holes_dropdown.currentText() == "True"
248
+
239
249
  def initialize_uncertainty_threshold(self):
240
250
  """Initialize the uncertainty threshold slider with the current value"""
241
251
  current_value = self.main_window.get_uncertainty_thresh()