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.
- coralnet_toolbox/Annotations/QtAnnotation.py +28 -69
- coralnet_toolbox/Annotations/QtMaskAnnotation.py +408 -0
- coralnet_toolbox/Annotations/QtMultiPolygonAnnotation.py +72 -56
- coralnet_toolbox/Annotations/QtPatchAnnotation.py +165 -216
- coralnet_toolbox/Annotations/QtPolygonAnnotation.py +497 -353
- coralnet_toolbox/Annotations/QtRectangleAnnotation.py +126 -116
- coralnet_toolbox/AutoDistill/QtDeployModel.py +23 -12
- coralnet_toolbox/CoralNet/QtDownload.py +2 -1
- coralnet_toolbox/Explorer/QtDataItem.py +1 -1
- coralnet_toolbox/Explorer/QtExplorer.py +159 -17
- coralnet_toolbox/Explorer/QtSettingsWidgets.py +160 -86
- coralnet_toolbox/IO/QtExportTagLabAnnotations.py +30 -10
- coralnet_toolbox/IO/QtImportTagLabAnnotations.py +21 -15
- coralnet_toolbox/IO/QtOpenProject.py +46 -78
- coralnet_toolbox/IO/QtSaveProject.py +18 -43
- coralnet_toolbox/MachineLearning/DeployModel/QtDetect.py +22 -11
- coralnet_toolbox/MachineLearning/DeployModel/QtSegment.py +22 -10
- coralnet_toolbox/MachineLearning/ExportDataset/QtBase.py +61 -24
- coralnet_toolbox/MachineLearning/ExportDataset/QtClassify.py +5 -1
- coralnet_toolbox/MachineLearning/ExportDataset/QtDetect.py +19 -6
- coralnet_toolbox/MachineLearning/ExportDataset/QtSegment.py +21 -8
- coralnet_toolbox/MachineLearning/ImportDataset/QtBase.py +42 -22
- coralnet_toolbox/MachineLearning/VideoInference/QtBase.py +0 -4
- coralnet_toolbox/QtAnnotationWindow.py +42 -14
- coralnet_toolbox/QtEventFilter.py +19 -2
- coralnet_toolbox/QtImageWindow.py +134 -86
- coralnet_toolbox/QtLabelWindow.py +14 -2
- coralnet_toolbox/QtMainWindow.py +122 -9
- coralnet_toolbox/QtProgressBar.py +52 -27
- coralnet_toolbox/Rasters/QtRaster.py +59 -7
- coralnet_toolbox/Rasters/RasterTableModel.py +42 -14
- coralnet_toolbox/SAM/QtBatchInference.py +0 -2
- coralnet_toolbox/SAM/QtDeployGenerator.py +22 -11
- coralnet_toolbox/SAM/QtDeployPredictor.py +10 -0
- coralnet_toolbox/SeeAnything/QtBatchInference.py +19 -221
- coralnet_toolbox/SeeAnything/QtDeployGenerator.py +1634 -0
- coralnet_toolbox/SeeAnything/QtDeployPredictor.py +107 -154
- coralnet_toolbox/SeeAnything/QtTrainModel.py +115 -45
- coralnet_toolbox/SeeAnything/__init__.py +2 -0
- coralnet_toolbox/Tools/QtCutSubTool.py +18 -2
- coralnet_toolbox/Tools/QtResizeSubTool.py +19 -2
- coralnet_toolbox/Tools/QtSAMTool.py +222 -57
- coralnet_toolbox/Tools/QtSeeAnythingTool.py +223 -55
- coralnet_toolbox/Tools/QtSelectSubTool.py +6 -4
- coralnet_toolbox/Tools/QtSelectTool.py +27 -3
- coralnet_toolbox/Tools/QtSubtractSubTool.py +66 -0
- coralnet_toolbox/Tools/QtWorkAreaTool.py +25 -13
- coralnet_toolbox/Tools/__init__.py +2 -0
- coralnet_toolbox/__init__.py +1 -1
- coralnet_toolbox/utilities.py +137 -47
- coralnet_toolbox-0.0.74.dist-info/METADATA +375 -0
- {coralnet_toolbox-0.0.72.dist-info → coralnet_toolbox-0.0.74.dist-info}/RECORD +56 -53
- coralnet_toolbox-0.0.72.dist-info/METADATA +0 -341
- {coralnet_toolbox-0.0.72.dist-info → coralnet_toolbox-0.0.74.dist-info}/WHEEL +0 -0
- {coralnet_toolbox-0.0.72.dist-info → coralnet_toolbox-0.0.74.dist-info}/entry_points.txt +0 -0
- {coralnet_toolbox-0.0.72.dist-info → coralnet_toolbox-0.0.74.dist-info}/licenses/LICENSE.txt +0 -0
- {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
|
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
|
121
|
-
|
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
|
-
|
124
|
-
|
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
|
145
|
-
This creates a visual effect of
|
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
|
-
|
152
|
-
|
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
|
-
#
|
160
|
-
|
161
|
-
|
162
|
-
#
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
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
|
-
|
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
|
-
#
|
254
|
-
self.
|
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
|
-
|
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
|
-
|
24
|
-
|
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
|
-
|
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.
|
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
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
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
|
|
@@ -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
|
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
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
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()
|