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
@@ -16,7 +16,7 @@ from PyQt5.QtWidgets import (QVBoxLayout, QHBoxLayout, QGraphicsView, QScrollAre
|
|
16
16
|
QGraphicsScene, QPushButton, QComboBox, QLabel, QWidget,
|
17
17
|
QMainWindow, QSplitter, QGroupBox, QSlider, QMessageBox,
|
18
18
|
QApplication, QGraphicsRectItem, QRubberBand, QMenu,
|
19
|
-
QWidgetAction, QToolButton, QAction)
|
19
|
+
QWidgetAction, QToolButton, QAction, QDoubleSpinBox)
|
20
20
|
|
21
21
|
from coralnet_toolbox.Explorer.QtFeatureStore import FeatureStore
|
22
22
|
from coralnet_toolbox.Explorer.QtDataItem import AnnotationDataItem
|
@@ -28,6 +28,7 @@ from coralnet_toolbox.Explorer.QtSettingsWidgets import UncertaintySettingsWidge
|
|
28
28
|
from coralnet_toolbox.Explorer.QtSettingsWidgets import MislabelSettingsWidget
|
29
29
|
from coralnet_toolbox.Explorer.QtSettingsWidgets import EmbeddingSettingsWidget
|
30
30
|
from coralnet_toolbox.Explorer.QtSettingsWidgets import AnnotationSettingsWidget
|
31
|
+
from coralnet_toolbox.Explorer.QtSettingsWidgets import DuplicateSettingsWidget
|
31
32
|
|
32
33
|
from coralnet_toolbox.Annotations.QtRectangleAnnotation import RectangleAnnotation
|
33
34
|
|
@@ -68,6 +69,8 @@ class EmbeddingViewer(QWidget):
|
|
68
69
|
mislabel_parameters_changed = pyqtSignal(dict)
|
69
70
|
find_uncertain_requested = pyqtSignal()
|
70
71
|
uncertainty_parameters_changed = pyqtSignal(dict)
|
72
|
+
find_duplicates_requested = pyqtSignal()
|
73
|
+
duplicate_parameters_changed = pyqtSignal(dict)
|
71
74
|
|
72
75
|
def __init__(self, parent=None):
|
73
76
|
"""Initialize the EmbeddingViewer widget."""
|
@@ -193,8 +196,37 @@ class EmbeddingViewer(QWidget):
|
|
193
196
|
|
194
197
|
uncertainty_settings_widget.parameters_changed.connect(self.uncertainty_parameters_changed.emit)
|
195
198
|
toolbar_layout.addWidget(self.find_uncertain_button)
|
199
|
+
|
200
|
+
# Create a QToolButton for duplicate detection
|
201
|
+
self.find_duplicates_button = QToolButton()
|
202
|
+
self.find_duplicates_button.setText("Find Duplicates")
|
203
|
+
self.find_duplicates_button.setToolTip(
|
204
|
+
"Find annotations that are likely duplicates based on feature similarity."
|
205
|
+
)
|
206
|
+
self.find_duplicates_button.setPopupMode(QToolButton.MenuButtonPopup)
|
207
|
+
self.find_duplicates_button.setToolButtonStyle(Qt.ToolButtonTextOnly)
|
208
|
+
self.find_duplicates_button.setStyleSheet(
|
209
|
+
"QToolButton::menu-indicator { "
|
210
|
+
"subcontrol-position: right center; "
|
211
|
+
"subcontrol-origin: padding; "
|
212
|
+
"left: -4px; }"
|
213
|
+
)
|
214
|
+
|
215
|
+
run_duplicates_action = QAction("Find Duplicates", self)
|
216
|
+
run_duplicates_action.triggered.connect(self.find_duplicates_requested.emit)
|
217
|
+
self.find_duplicates_button.setDefaultAction(run_duplicates_action)
|
218
|
+
|
219
|
+
duplicate_settings_widget = DuplicateSettingsWidget()
|
220
|
+
duplicate_menu = QMenu(self)
|
221
|
+
duplicate_widget_action = QWidgetAction(duplicate_menu)
|
222
|
+
duplicate_widget_action.setDefaultWidget(duplicate_settings_widget)
|
223
|
+
duplicate_menu.addAction(duplicate_widget_action)
|
224
|
+
self.find_duplicates_button.setMenu(duplicate_menu)
|
225
|
+
|
226
|
+
duplicate_settings_widget.parameters_changed.connect(self.duplicate_parameters_changed.emit)
|
227
|
+
toolbar_layout.addWidget(self.find_duplicates_button)
|
196
228
|
|
197
|
-
# Add a
|
229
|
+
# Add a stretch and separator
|
198
230
|
toolbar_layout.addStretch()
|
199
231
|
toolbar_layout.addWidget(self._create_separator())
|
200
232
|
|
@@ -293,6 +325,7 @@ class EmbeddingViewer(QWidget):
|
|
293
325
|
|
294
326
|
self.find_mislabels_button.setEnabled(points_exist)
|
295
327
|
self.find_uncertain_button.setEnabled(points_exist and self.is_uncertainty_analysis_available)
|
328
|
+
self.find_duplicates_button.setEnabled(points_exist)
|
296
329
|
self.center_on_selection_button.setEnabled(points_exist and selection_exists)
|
297
330
|
|
298
331
|
if self.isolated_mode:
|
@@ -348,6 +381,7 @@ class EmbeddingViewer(QWidget):
|
|
348
381
|
self.center_on_selection_button.setEnabled(False) # Disable center button
|
349
382
|
self.find_mislabels_button.setEnabled(False)
|
350
383
|
self.find_uncertain_button.setEnabled(False)
|
384
|
+
self.find_duplicates_button.setEnabled(False)
|
351
385
|
|
352
386
|
self.isolate_button.show()
|
353
387
|
self.isolate_button.setEnabled(False)
|
@@ -851,7 +885,7 @@ class AnnotationViewer(QWidget):
|
|
851
885
|
|
852
886
|
# Show resize handles for Rectangle annotations
|
853
887
|
if isinstance(annotation_to_select, RectangleAnnotation):
|
854
|
-
explorer.annotation_window.set_selected_tool('select')
|
888
|
+
explorer.annotation_window.set_selected_tool('select') # Accidentally unselects in AnnotationWindow
|
855
889
|
explorer.annotation_window.select_annotation(annotation_to_select, quiet_mode=True)
|
856
890
|
select_tool = explorer.annotation_window.tools.get('select')
|
857
891
|
|
@@ -1584,6 +1618,7 @@ class ExplorerWindow(QMainWindow):
|
|
1584
1618
|
self.mislabel_params = {'k': 20, 'threshold': 0.6}
|
1585
1619
|
self.uncertainty_params = {'confidence': 0.6, 'margin': 0.1}
|
1586
1620
|
self.similarity_params = {'k': 30}
|
1621
|
+
self.duplicate_params = {'threshold': 0.05}
|
1587
1622
|
|
1588
1623
|
self.data_item_cache = {} # Cache for AnnotationDataItem objects
|
1589
1624
|
|
@@ -1670,37 +1705,27 @@ class ExplorerWindow(QMainWindow):
|
|
1670
1705
|
if child.widget():
|
1671
1706
|
child.widget().setParent(None)
|
1672
1707
|
|
1673
|
-
# Lazily initialize the settings and viewer widgets
|
1674
|
-
# This ensures that the widgets are only created once per ExplorerWindow instance.
|
1675
|
-
|
1676
|
-
# Annotation settings panel (filters by image, type, label)
|
1708
|
+
# Lazily initialize the settings and viewer widgets
|
1677
1709
|
if self.annotation_settings_widget is None:
|
1678
1710
|
self.annotation_settings_widget = AnnotationSettingsWidget(self.main_window, self)
|
1679
|
-
|
1680
|
-
# Model selection panel (choose feature extraction model)
|
1681
1711
|
if self.model_settings_widget is None:
|
1682
1712
|
self.model_settings_widget = ModelSettingsWidget(self.main_window, self)
|
1683
|
-
|
1684
|
-
# Embedding settings panel (choose dimensionality reduction method)
|
1685
1713
|
if self.embedding_settings_widget is None:
|
1686
1714
|
self.embedding_settings_widget = EmbeddingSettingsWidget(self.main_window, self)
|
1687
|
-
|
1688
|
-
# Annotation viewer (shows annotation image crops in a grid)
|
1689
1715
|
if self.annotation_viewer is None:
|
1690
1716
|
self.annotation_viewer = AnnotationViewer(self)
|
1691
|
-
|
1692
|
-
# Embedding viewer (shows 2D embedding scatter plot)
|
1693
1717
|
if self.embedding_viewer is None:
|
1694
1718
|
self.embedding_viewer = EmbeddingViewer(self)
|
1695
1719
|
|
1720
|
+
# Horizontal layout for the three settings panels (original horizontal layout)
|
1696
1721
|
top_layout = QHBoxLayout()
|
1697
1722
|
top_layout.addWidget(self.annotation_settings_widget, 2)
|
1698
1723
|
top_layout.addWidget(self.model_settings_widget, 1)
|
1699
1724
|
top_layout.addWidget(self.embedding_settings_widget, 1)
|
1700
1725
|
top_container = QWidget()
|
1701
1726
|
top_container.setLayout(top_layout)
|
1702
|
-
self.main_layout.addWidget(top_container)
|
1703
1727
|
|
1728
|
+
# Horizontal splitter for the two main viewer panels
|
1704
1729
|
middle_splitter = QSplitter(Qt.Horizontal)
|
1705
1730
|
annotation_group = QGroupBox("Annotation Viewer")
|
1706
1731
|
annotation_layout = QVBoxLayout(annotation_group)
|
@@ -1712,7 +1737,19 @@ class ExplorerWindow(QMainWindow):
|
|
1712
1737
|
embedding_layout.addWidget(self.embedding_viewer)
|
1713
1738
|
middle_splitter.addWidget(embedding_group)
|
1714
1739
|
middle_splitter.setSizes([500, 500])
|
1715
|
-
|
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
|
+
|
1716
1753
|
self.main_layout.addWidget(self.label_window)
|
1717
1754
|
|
1718
1755
|
self.buttons_layout = QHBoxLayout()
|
@@ -1744,6 +1781,8 @@ class ExplorerWindow(QMainWindow):
|
|
1744
1781
|
self.model_settings_widget.selection_changed.connect(self.on_model_selection_changed)
|
1745
1782
|
self.embedding_viewer.find_uncertain_requested.connect(self.find_uncertain_annotations)
|
1746
1783
|
self.embedding_viewer.uncertainty_parameters_changed.connect(self.on_uncertainty_params_changed)
|
1784
|
+
self.embedding_viewer.find_duplicates_requested.connect(self.find_duplicate_annotations)
|
1785
|
+
self.embedding_viewer.duplicate_parameters_changed.connect(self.on_duplicate_params_changed)
|
1747
1786
|
self.annotation_viewer.find_similar_requested.connect(self.find_similar_annotations)
|
1748
1787
|
self.annotation_viewer.similarity_settings_widget.parameters_changed.connect(self.on_similarity_params_changed)
|
1749
1788
|
|
@@ -1887,6 +1926,12 @@ class ExplorerWindow(QMainWindow):
|
|
1887
1926
|
"""Updates the stored parameters for uncertainty analysis."""
|
1888
1927
|
self.uncertainty_params = params
|
1889
1928
|
print(f"Uncertainty parameters updated: {self.uncertainty_params}")
|
1929
|
+
|
1930
|
+
@pyqtSlot(dict)
|
1931
|
+
def on_duplicate_params_changed(self, params):
|
1932
|
+
"""Updates the stored parameters for duplicate detection."""
|
1933
|
+
self.duplicate_params = params
|
1934
|
+
print(f"Duplicate detection parameters updated: {self.duplicate_params}")
|
1890
1935
|
|
1891
1936
|
@pyqtSlot(dict)
|
1892
1937
|
def on_similarity_params_changed(self, params):
|
@@ -2067,6 +2112,98 @@ class ExplorerWindow(QMainWindow):
|
|
2067
2112
|
|
2068
2113
|
finally:
|
2069
2114
|
QApplication.restoreOverrideCursor()
|
2115
|
+
|
2116
|
+
def find_duplicate_annotations(self):
|
2117
|
+
"""
|
2118
|
+
Identifies annotations that are likely duplicates based on feature similarity.
|
2119
|
+
It uses a nearest-neighbor approach in the high-dimensional feature space.
|
2120
|
+
For each group of duplicates found, it selects all but one "original".
|
2121
|
+
"""
|
2122
|
+
threshold = self.duplicate_params.get('threshold', 0.05)
|
2123
|
+
|
2124
|
+
if not self.embedding_viewer.points_by_id or len(self.embedding_viewer.points_by_id) < 2:
|
2125
|
+
QMessageBox.information(self,
|
2126
|
+
"Not Enough Data",
|
2127
|
+
"This feature requires at least 2 points in the embedding viewer.")
|
2128
|
+
return
|
2129
|
+
|
2130
|
+
items_in_view = list(self.embedding_viewer.points_by_id.values())
|
2131
|
+
data_items_in_view = [p.data_item for p in items_in_view]
|
2132
|
+
|
2133
|
+
model_info = self.model_settings_widget.get_selected_model()
|
2134
|
+
model_name, feature_mode = model_info if isinstance(model_info, tuple) else (model_info, "default")
|
2135
|
+
sanitized_model_name = os.path.basename(model_name).replace(' ', '_')
|
2136
|
+
sanitized_feature_mode = feature_mode.replace(' ', '_').replace('/', '_')
|
2137
|
+
model_key = f"{sanitized_model_name}_{sanitized_feature_mode}"
|
2138
|
+
|
2139
|
+
# Make cursor busy
|
2140
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
2141
|
+
try:
|
2142
|
+
index = self.feature_store._get_or_load_index(model_key)
|
2143
|
+
if index is None:
|
2144
|
+
QMessageBox.warning(self, "Error", "Could not find a valid feature index for the current model.")
|
2145
|
+
return
|
2146
|
+
|
2147
|
+
features_dict, _ = self.feature_store.get_features(data_items_in_view, model_key)
|
2148
|
+
if not features_dict:
|
2149
|
+
QMessageBox.warning(self, "Error", "Could not retrieve features for the items in view.")
|
2150
|
+
return
|
2151
|
+
|
2152
|
+
query_ann_ids = list(features_dict.keys())
|
2153
|
+
query_vectors = np.array([features_dict[ann_id] for ann_id in query_ann_ids]).astype('float32')
|
2154
|
+
|
2155
|
+
# Find the 2 nearest neighbors for each vector. D = squared L2 distances.
|
2156
|
+
D, I = index.search(query_vectors, 2)
|
2157
|
+
|
2158
|
+
# Use a Disjoint Set Union (DSU) data structure to group duplicates.
|
2159
|
+
parent = {ann_id: ann_id for ann_id in query_ann_ids}
|
2160
|
+
|
2161
|
+
# Helper functions for DSU
|
2162
|
+
def find_set(v):
|
2163
|
+
if v == parent[v]:
|
2164
|
+
return v
|
2165
|
+
parent[v] = find_set(parent[v])
|
2166
|
+
return parent[v]
|
2167
|
+
|
2168
|
+
def unite_sets(a, b):
|
2169
|
+
a = find_set(a)
|
2170
|
+
b = find_set(b)
|
2171
|
+
if a != b:
|
2172
|
+
parent[b] = a
|
2173
|
+
|
2174
|
+
id_map = self.feature_store.get_faiss_index_to_annotation_id_map(model_key)
|
2175
|
+
|
2176
|
+
for i, ann_id in enumerate(query_ann_ids):
|
2177
|
+
neighbor_faiss_idx = I[i, 1] # The second result is the nearest neighbor
|
2178
|
+
distance = D[i, 1]
|
2179
|
+
|
2180
|
+
if distance < threshold:
|
2181
|
+
neighbor_ann_id = id_map.get(neighbor_faiss_idx)
|
2182
|
+
if neighbor_ann_id and neighbor_ann_id in parent:
|
2183
|
+
unite_sets(ann_id, neighbor_ann_id)
|
2184
|
+
|
2185
|
+
# Group annotations by their set representative
|
2186
|
+
groups = {}
|
2187
|
+
for ann_id in query_ann_ids:
|
2188
|
+
root = find_set(ann_id)
|
2189
|
+
if root not in groups:
|
2190
|
+
groups[root] = []
|
2191
|
+
groups[root].append(ann_id)
|
2192
|
+
|
2193
|
+
copies_to_select = set()
|
2194
|
+
for root_id, group_ids in groups.items():
|
2195
|
+
if len(group_ids) > 1:
|
2196
|
+
# Sort IDs to consistently pick the same "original".
|
2197
|
+
# Sorting strings is reliable.
|
2198
|
+
sorted_ids = sorted(group_ids)
|
2199
|
+
# The first ID is the original, add the rest to the selection.
|
2200
|
+
copies_to_select.update(sorted_ids[1:])
|
2201
|
+
|
2202
|
+
print(f"Found {len(copies_to_select)} duplicate annotations.")
|
2203
|
+
self.embedding_viewer.render_selection_from_ids(copies_to_select)
|
2204
|
+
|
2205
|
+
finally:
|
2206
|
+
QApplication.restoreOverrideCursor()
|
2070
2207
|
|
2071
2208
|
def find_uncertain_annotations(self):
|
2072
2209
|
"""
|
@@ -2658,6 +2795,7 @@ class ExplorerWindow(QMainWindow):
|
|
2658
2795
|
norm_y = (embedded_features[i, 1] - min_vals[1]) / range_vals[1] if range_vals[1] > 0 else 0.5
|
2659
2796
|
item.embedding_x = (norm_x * scale_factor) - (scale_factor / 2)
|
2660
2797
|
item.embedding_y = (norm_y * scale_factor) - (scale_factor / 2)
|
2798
|
+
item.embedding_id = i
|
2661
2799
|
|
2662
2800
|
def run_embedding_pipeline(self):
|
2663
2801
|
"""
|
@@ -2813,6 +2951,10 @@ class ExplorerWindow(QMainWindow):
|
|
2813
2951
|
self.current_data_items = [
|
2814
2952
|
item for item in self.current_data_items if item.annotation.id not in deleted_ann_ids
|
2815
2953
|
]
|
2954
|
+
# Also update the annotation viewer's list to keep it in sync
|
2955
|
+
self.annotation_viewer.all_data_items = [
|
2956
|
+
item for item in self.annotation_viewer.all_data_items if item.annotation.id not in deleted_ann_ids
|
2957
|
+
]
|
2816
2958
|
for ann_id in deleted_ann_ids:
|
2817
2959
|
if ann_id in self.data_item_cache:
|
2818
2960
|
del self.data_item_cache[ann_id]
|
@@ -4,7 +4,7 @@ import warnings
|
|
4
4
|
from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot
|
5
5
|
from PyQt5.QtWidgets import (QVBoxLayout, QHBoxLayout, QPushButton, QComboBox, QLabel,
|
6
6
|
QWidget, QGroupBox, QSlider, QListWidget, QTabWidget,
|
7
|
-
QLineEdit, QFileDialog, QFormLayout, QSpinBox)
|
7
|
+
QLineEdit, QFileDialog, QFormLayout, QSpinBox, QDoubleSpinBox)
|
8
8
|
|
9
9
|
from coralnet_toolbox.MachineLearning.Community.cfg import get_available_configs
|
10
10
|
|
@@ -189,6 +189,48 @@ class SimilaritySettingsWidget(QWidget):
|
|
189
189
|
'k': self.k_spinbox.value()
|
190
190
|
}
|
191
191
|
|
192
|
+
|
193
|
+
class DuplicateSettingsWidget(QWidget):
|
194
|
+
"""Widget for configuring duplicate detection parameters."""
|
195
|
+
parameters_changed = pyqtSignal(dict)
|
196
|
+
|
197
|
+
def __init__(self, parent=None):
|
198
|
+
super(DuplicateSettingsWidget, self).__init__(parent)
|
199
|
+
layout = QVBoxLayout(self)
|
200
|
+
layout.setContentsMargins(10, 10, 10, 10)
|
201
|
+
|
202
|
+
# Using a DoubleSpinBox for the distance threshold
|
203
|
+
self.threshold_spinbox = QDoubleSpinBox()
|
204
|
+
self.threshold_spinbox.setDecimals(3)
|
205
|
+
self.threshold_spinbox.setRange(0.0, 10.0)
|
206
|
+
self.threshold_spinbox.setSingleStep(0.01)
|
207
|
+
self.threshold_spinbox.setValue(0.1) # Default value for squared L2 distance
|
208
|
+
self.threshold_spinbox.setToolTip(
|
209
|
+
"Similarity Threshold (Squared L2 Distance).\n"
|
210
|
+
"Lower values mean more similar.\n"
|
211
|
+
"A value of 0 means identical features."
|
212
|
+
)
|
213
|
+
|
214
|
+
self.threshold_spinbox.valueChanged.connect(self._emit_parameters)
|
215
|
+
|
216
|
+
form_layout = QHBoxLayout()
|
217
|
+
form_layout.addWidget(QLabel("Threshold:"))
|
218
|
+
form_layout.addWidget(self.threshold_spinbox)
|
219
|
+
layout.addLayout(form_layout)
|
220
|
+
|
221
|
+
def _emit_parameters(self):
|
222
|
+
"""Emits the current parameters."""
|
223
|
+
params = {
|
224
|
+
'threshold': self.threshold_spinbox.value()
|
225
|
+
}
|
226
|
+
self.parameters_changed.emit(params)
|
227
|
+
|
228
|
+
def get_parameters(self):
|
229
|
+
"""Returns the current parameters as a dictionary."""
|
230
|
+
return {
|
231
|
+
'threshold': self.threshold_spinbox.value()
|
232
|
+
}
|
233
|
+
|
192
234
|
|
193
235
|
class AnnotationSettingsWidget(QGroupBox):
|
194
236
|
"""Widget for filtering annotations by image, type, and label in a multi-column layout."""
|
@@ -200,7 +242,7 @@ class AnnotationSettingsWidget(QGroupBox):
|
|
200
242
|
self.setup_ui()
|
201
243
|
|
202
244
|
def setup_ui(self):
|
203
|
-
# The main layout is vertical, to hold the top columns
|
245
|
+
# The main layout is vertical, to hold the top columns and the bottom buttons
|
204
246
|
layout = QVBoxLayout(self)
|
205
247
|
|
206
248
|
# A horizontal layout to contain the filter columns
|
@@ -213,8 +255,8 @@ class AnnotationSettingsWidget(QGroupBox):
|
|
213
255
|
images_column.addWidget(images_label)
|
214
256
|
|
215
257
|
self.images_list = QListWidget()
|
216
|
-
self.images_list.setSelectionMode(QListWidget.
|
217
|
-
self.images_list.
|
258
|
+
self.images_list.setSelectionMode(QListWidget.ExtendedSelection)
|
259
|
+
self.images_list.setMinimumHeight(100)
|
218
260
|
|
219
261
|
if hasattr(self.main_window, 'image_window') and hasattr(self.main_window.image_window, 'raster_manager'):
|
220
262
|
for path in self.main_window.image_window.raster_manager.image_paths:
|
@@ -241,8 +283,8 @@ class AnnotationSettingsWidget(QGroupBox):
|
|
241
283
|
type_column.addWidget(type_label)
|
242
284
|
|
243
285
|
self.annotation_type_list = QListWidget()
|
244
|
-
self.annotation_type_list.setSelectionMode(QListWidget.
|
245
|
-
self.annotation_type_list.
|
286
|
+
self.annotation_type_list.setSelectionMode(QListWidget.ExtendedSelection)
|
287
|
+
self.annotation_type_list.setMinimumHeight(100)
|
246
288
|
self.annotation_type_list.addItems(["PatchAnnotation",
|
247
289
|
"RectangleAnnotation",
|
248
290
|
"PolygonAnnotation",
|
@@ -269,8 +311,8 @@ class AnnotationSettingsWidget(QGroupBox):
|
|
269
311
|
label_column.addWidget(label_label)
|
270
312
|
|
271
313
|
self.label_list = QListWidget()
|
272
|
-
self.label_list.setSelectionMode(QListWidget.
|
273
|
-
self.label_list.
|
314
|
+
self.label_list.setSelectionMode(QListWidget.ExtendedSelection)
|
315
|
+
self.label_list.setMinimumHeight(100)
|
274
316
|
|
275
317
|
if hasattr(self.main_window, 'label_window') and hasattr(self.main_window.label_window, 'labels'):
|
276
318
|
for label in self.main_window.label_window.labels:
|
@@ -293,9 +335,6 @@ class AnnotationSettingsWidget(QGroupBox):
|
|
293
335
|
# Add the horizontal layout of columns to the main vertical layout
|
294
336
|
layout.addLayout(conditions_layout)
|
295
337
|
|
296
|
-
# Add a stretch item to push the columns to the top
|
297
|
-
layout.addStretch(1)
|
298
|
-
|
299
338
|
# Bottom buttons layout with Apply and Clear buttons on the right
|
300
339
|
bottom_layout = QHBoxLayout()
|
301
340
|
bottom_layout.addStretch() # Push buttons to the right
|
@@ -308,9 +347,12 @@ class AnnotationSettingsWidget(QGroupBox):
|
|
308
347
|
self.clear_button.clicked.connect(self.clear_all_conditions)
|
309
348
|
bottom_layout.addWidget(self.clear_button)
|
310
349
|
|
311
|
-
# Add the bottom buttons layout to the main layout
|
350
|
+
# Add the bottom buttons layout to the main layout
|
312
351
|
layout.addLayout(bottom_layout)
|
313
352
|
|
353
|
+
# Add a stretch item to push all content to the top
|
354
|
+
layout.addStretch(1)
|
355
|
+
|
314
356
|
# Set defaults
|
315
357
|
self.set_defaults()
|
316
358
|
|
@@ -393,68 +435,66 @@ class AnnotationSettingsWidget(QGroupBox):
|
|
393
435
|
|
394
436
|
|
395
437
|
class ModelSettingsWidget(QGroupBox):
|
396
|
-
"""Widget containing
|
438
|
+
"""Widget containing a structured, hierarchical model selection system."""
|
397
439
|
selection_changed = pyqtSignal()
|
398
440
|
|
399
441
|
def __init__(self, main_window, parent=None):
|
400
442
|
super(ModelSettingsWidget, self).__init__("Model Settings", parent)
|
401
443
|
self.main_window = main_window
|
402
444
|
self.explorer_window = parent
|
445
|
+
|
446
|
+
# --- Data for hierarchical selection ---
|
447
|
+
self.standard_models_map = {
|
448
|
+
'YOLOv8': {'Nano': 'n', 'Small': 's', 'Medium': 'm', 'Large': 'l', 'X-Large': 'x'},
|
449
|
+
'YOLOv11': {'Nano': 'n', 'Small': 's', 'Medium': 'm', 'Large': 'l', 'X-Large': 'x'},
|
450
|
+
'YOLOv12': {'Nano': 'n', 'Small': 's', 'Medium': 'm', 'Large': 'l', 'X-Large': 'x'}
|
451
|
+
}
|
452
|
+
self.community_configs = get_available_configs(task='classify')
|
453
|
+
|
403
454
|
self.setup_ui()
|
404
455
|
|
405
456
|
def setup_ui(self):
|
406
457
|
"""Set up the UI with a tabbed interface for model selection."""
|
407
458
|
main_layout = QVBoxLayout(self)
|
408
|
-
|
409
|
-
# --- Tabbed Interface for Model Selection ---
|
410
459
|
self.tabs = QTabWidget()
|
411
460
|
|
412
|
-
# Tab 1: Select Model
|
461
|
+
# === Tab 1: Select Pre-defined Model ===
|
413
462
|
model_select_tab = QWidget()
|
414
|
-
model_select_layout = QFormLayout(model_select_tab)
|
415
|
-
model_select_layout.setContentsMargins(5, 10, 5, 5)
|
416
|
-
|
417
|
-
|
418
|
-
self.
|
419
|
-
self.
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
]
|
438
|
-
|
439
|
-
self.model_combo.addItems(standard_models)
|
440
|
-
|
441
|
-
community_configs = get_available_configs(task='classify')
|
442
|
-
if community_configs:
|
443
|
-
self.model_combo.insertSeparator(len(standard_models) + 2)
|
444
|
-
self.model_combo.addItems(list(community_configs.keys()))
|
445
|
-
|
446
|
-
self.model_combo.setCurrentText('Color Features')
|
447
|
-
model_select_layout.addRow("Model:", self.model_combo)
|
463
|
+
self.model_select_layout = QFormLayout(model_select_tab)
|
464
|
+
self.model_select_layout.setContentsMargins(5, 10, 5, 5)
|
465
|
+
|
466
|
+
# 1. Main Category ComboBox
|
467
|
+
self.category_combo = QComboBox()
|
468
|
+
self.category_combo.addItems(["Color Features", "Standard Model", "Community Model"])
|
469
|
+
self.model_select_layout.addRow("Category:", self.category_combo)
|
470
|
+
|
471
|
+
# 2. Standard Model Options (initially hidden)
|
472
|
+
self.family_combo = QComboBox()
|
473
|
+
self.family_combo.addItems(self.standard_models_map.keys())
|
474
|
+
self.size_combo = QComboBox()
|
475
|
+
self.family_combo.currentTextChanged.connect(self._update_size_combo)
|
476
|
+
self._update_size_combo(self.family_combo.currentText())
|
477
|
+
|
478
|
+
self.standard_model_widgets = [QLabel("Family:"), self.family_combo, QLabel("Size:"), self.size_combo]
|
479
|
+
self.model_select_layout.addRow(self.standard_model_widgets[0], self.standard_model_widgets[1])
|
480
|
+
self.model_select_layout.addRow(self.standard_model_widgets[2], self.standard_model_widgets[3])
|
481
|
+
|
482
|
+
# 3. Community Model Options (initially hidden)
|
483
|
+
self.community_combo = QComboBox()
|
484
|
+
if self.community_configs:
|
485
|
+
self.community_combo.addItems(list(self.community_configs.keys()))
|
486
|
+
self.community_model_widgets = [QLabel("Model:"), self.community_combo]
|
487
|
+
self.model_select_layout.addRow(self.community_model_widgets[0], self.community_model_widgets[1])
|
448
488
|
|
449
489
|
self.tabs.addTab(model_select_tab, "Select Model")
|
450
490
|
|
451
|
-
# Tab 2: Existing Model from File
|
491
|
+
# === Tab 2: Existing Model from File ===
|
452
492
|
model_existing_tab = QWidget()
|
453
493
|
model_existing_layout = QFormLayout(model_existing_tab)
|
454
494
|
model_existing_layout.setContentsMargins(5, 10, 5, 5)
|
455
495
|
|
456
496
|
self.model_path_edit = QLineEdit()
|
457
|
-
self.model_path_edit.setPlaceholderText("Path to a
|
497
|
+
self.model_path_edit.setPlaceholderText("Path to a compatible .pt model file...")
|
458
498
|
browse_button = QPushButton("Browse...")
|
459
499
|
browse_button.clicked.connect(self.browse_for_model)
|
460
500
|
|
@@ -463,82 +503,116 @@ class ModelSettingsWidget(QGroupBox):
|
|
463
503
|
path_layout.addWidget(browse_button)
|
464
504
|
model_existing_layout.addRow("Model Path:", path_layout)
|
465
505
|
|
506
|
+
help_label = QLabel("Note: Select an Ultralytics model (.pt).")
|
507
|
+
help_label.setStyleSheet("color: gray; font-style: italic;")
|
508
|
+
model_existing_layout.addRow("", help_label)
|
509
|
+
|
466
510
|
self.tabs.addTab(model_existing_tab, "Use Existing Model")
|
467
511
|
|
468
512
|
main_layout.addWidget(self.tabs)
|
469
|
-
|
470
|
-
#
|
471
|
-
self.model_combo.currentTextChanged.connect(self._on_selection_changed)
|
472
|
-
self.tabs.currentChanged.connect(self._on_selection_changed)
|
473
|
-
self.model_path_edit.textChanged.connect(self._on_selection_changed)
|
474
|
-
|
475
|
-
# Add feature extraction mode selection outside of tabs
|
513
|
+
|
514
|
+
# === Feature Extraction Mode (Reverted to bottom) ===
|
476
515
|
feature_mode_layout = QFormLayout()
|
477
516
|
self.feature_mode_combo = QComboBox()
|
478
517
|
self.feature_mode_combo.addItems(["Predictions", "Embed Features"])
|
479
|
-
self.feature_mode_combo.currentTextChanged.connect(self._on_selection_changed)
|
480
518
|
feature_mode_layout.addRow("Feature Mode:", self.feature_mode_combo)
|
481
519
|
main_layout.addLayout(feature_mode_layout)
|
520
|
+
|
521
|
+
# --- Connect Signals ---
|
522
|
+
self.category_combo.currentTextChanged.connect(self._on_category_changed)
|
523
|
+
self.tabs.currentChanged.connect(self._on_selection_changed)
|
524
|
+
for widget in [self.category_combo, self.family_combo, self.size_combo,
|
525
|
+
self.community_combo, self.model_path_edit, self.feature_mode_combo]:
|
526
|
+
if isinstance(widget, QComboBox):
|
527
|
+
widget.currentTextChanged.connect(self._on_selection_changed)
|
528
|
+
elif isinstance(widget, QLineEdit):
|
529
|
+
widget.textChanged.connect(self._on_selection_changed)
|
530
|
+
|
531
|
+
# --- Initial State ---
|
532
|
+
self._on_category_changed(self.category_combo.currentText())
|
533
|
+
self._on_selection_changed()
|
534
|
+
|
535
|
+
@pyqtSlot(str)
|
536
|
+
def _update_size_combo(self, family):
|
537
|
+
"""Populate the size combo box based on the selected family."""
|
538
|
+
self.size_combo.clear()
|
539
|
+
if family in self.standard_models_map:
|
540
|
+
self.size_combo.addItems(self.standard_models_map[family].keys())
|
541
|
+
|
542
|
+
@pyqtSlot(str)
|
543
|
+
def _on_category_changed(self, category):
|
544
|
+
"""Show or hide sub-option widgets based on the selected category."""
|
545
|
+
is_standard = (category == "Standard Model")
|
546
|
+
is_community = (category == "Community Model")
|
547
|
+
|
548
|
+
for widget in self.standard_model_widgets:
|
549
|
+
widget.setVisible(is_standard)
|
550
|
+
for widget in self.community_model_widgets:
|
551
|
+
widget.setVisible(is_community)
|
482
552
|
|
483
|
-
# Initialize the feature mode state and emit the first signal
|
484
553
|
self._on_selection_changed()
|
485
554
|
|
486
555
|
def browse_for_model(self):
|
487
556
|
"""Open a file dialog to browse for model files."""
|
488
557
|
options = QFileDialog.Options()
|
489
558
|
file_path, _ = QFileDialog.getOpenFileName(
|
490
|
-
self,
|
491
|
-
"Select Model File",
|
492
|
-
"",
|
493
|
-
"PyTorch Models (*.pt);;All Files (*)",
|
494
|
-
options=options
|
495
|
-
)
|
559
|
+
self, "Select Model File", "", "PyTorch Models (*.pt);;All Files (*)", options=options)
|
496
560
|
if file_path:
|
497
561
|
self.model_path_edit.setText(file_path)
|
498
562
|
|
499
563
|
@pyqtSlot()
|
500
564
|
def _on_selection_changed(self):
|
501
|
-
"""Central slot to handle any change
|
565
|
+
"""Central slot to handle any change and emit a single signal."""
|
502
566
|
self._update_feature_mode_state()
|
503
567
|
self.selection_changed.emit()
|
504
568
|
|
505
|
-
def _update_feature_mode_state(self
|
506
|
-
"""Update the enabled state of the feature mode field
|
507
|
-
current_tab_index = self.tabs.currentIndex()
|
569
|
+
def _update_feature_mode_state(self):
|
570
|
+
"""Update the enabled state and tooltip of the feature mode field."""
|
508
571
|
is_color_features = False
|
572
|
+
current_tab_index = self.tabs.currentIndex()
|
509
573
|
|
510
574
|
if current_tab_index == 0:
|
511
|
-
|
512
|
-
current_model = self.model_combo.currentText()
|
513
|
-
is_color_features = current_model == "Color Features"
|
514
|
-
elif current_tab_index == 1:
|
515
|
-
# Use Existing Model tab - feature mode should always be enabled
|
516
|
-
is_color_features = False
|
575
|
+
is_color_features = (self.category_combo.currentText() == "Color Features")
|
517
576
|
|
518
|
-
# Enable feature mode only if not Color Features
|
519
577
|
self.feature_mode_combo.setEnabled(not is_color_features)
|
520
578
|
|
521
|
-
# Update the tooltip based on state
|
522
579
|
if is_color_features:
|
523
|
-
self.feature_mode_combo.setToolTip("Feature Mode is not
|
580
|
+
self.feature_mode_combo.setToolTip("Feature Mode is not applicable for Color Features.")
|
524
581
|
else:
|
525
|
-
self.feature_mode_combo.setToolTip(
|
582
|
+
self.feature_mode_combo.setToolTip(
|
583
|
+
"Choose 'Predictions' for class probabilities (for uncertainty analysis)\n"
|
584
|
+
"or 'Embed Features' for a general-purpose feature vector."
|
585
|
+
)
|
526
586
|
|
527
587
|
def get_selected_model(self):
|
528
588
|
"""Get the currently selected model name/path and feature mode."""
|
529
589
|
current_tab_index = self.tabs.currentIndex()
|
590
|
+
model_name = ""
|
530
591
|
|
531
|
-
# Get model name/path and feature mode based on the active tab
|
532
592
|
if current_tab_index == 0:
|
533
|
-
|
593
|
+
category = self.category_combo.currentText()
|
594
|
+
if category == "Color Features":
|
595
|
+
model_name = "Color Features"
|
596
|
+
elif category == "Standard Model":
|
597
|
+
family_text = self.family_combo.currentText()
|
598
|
+
size_text = self.size_combo.currentText()
|
599
|
+
|
600
|
+
# Add a guard clause to prevent crashing if a combo is empty.
|
601
|
+
if not family_text or not size_text:
|
602
|
+
return "", "N/A" # Return a safe default
|
603
|
+
|
604
|
+
family_key = family_text.lower().replace('-', '')
|
605
|
+
size_key = self.standard_models_map[family_text][size_text]
|
606
|
+
model_name = f"{family_key}{size_key}-cls.pt"
|
607
|
+
|
608
|
+
elif category == "Community Model":
|
609
|
+
model_name = self.community_combo.currentText()
|
534
610
|
elif current_tab_index == 1:
|
535
611
|
model_name = self.model_path_edit.text()
|
536
|
-
else:
|
537
|
-
return "", None
|
538
612
|
|
539
613
|
feature_mode = self.feature_mode_combo.currentText() if self.feature_mode_combo.isEnabled() else "N/A"
|
540
614
|
return model_name, feature_mode
|
541
|
-
|
615
|
+
|
542
616
|
|
543
617
|
class EmbeddingSettingsWidget(QGroupBox):
|
544
618
|
"""Widget containing settings with tabs for models and embedding."""
|