coralnet-toolbox 0.0.67__py2.py3-none-any.whl → 0.0.69__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/Explorer/QtDataItem.py +339 -0
- coralnet_toolbox/Explorer/QtExplorer.py +1579 -1006
- coralnet_toolbox/Explorer/QtFeatureStore.py +176 -0
- coralnet_toolbox/Explorer/QtSettingsWidgets.py +212 -25
- coralnet_toolbox/QtEventFilter.py +24 -12
- coralnet_toolbox/QtLabelWindow.py +23 -11
- coralnet_toolbox/QtMainWindow.py +59 -7
- coralnet_toolbox/__init__.py +1 -1
- {coralnet_toolbox-0.0.67.dist-info → coralnet_toolbox-0.0.69.dist-info}/METADATA +14 -7
- {coralnet_toolbox-0.0.67.dist-info → coralnet_toolbox-0.0.69.dist-info}/RECORD +14 -15
- coralnet_toolbox/Explorer/QtAnnotationDataItem.py +0 -97
- coralnet_toolbox/Explorer/QtAnnotationImageWidget.py +0 -183
- coralnet_toolbox/Explorer/QtEmbeddingPointItem.py +0 -30
- {coralnet_toolbox-0.0.67.dist-info → coralnet_toolbox-0.0.69.dist-info}/WHEEL +0 -0
- {coralnet_toolbox-0.0.67.dist-info → coralnet_toolbox-0.0.69.dist-info}/entry_points.txt +0 -0
- {coralnet_toolbox-0.0.67.dist-info → coralnet_toolbox-0.0.69.dist-info}/licenses/LICENSE.txt +0 -0
- {coralnet_toolbox-0.0.67.dist-info → coralnet_toolbox-0.0.69.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,176 @@
|
|
1
|
+
import os
|
2
|
+
import glob
|
3
|
+
import sqlite3
|
4
|
+
import warnings
|
5
|
+
|
6
|
+
import faiss
|
7
|
+
|
8
|
+
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
9
|
+
|
10
|
+
|
11
|
+
# ----------------------------------------------------------------------------------------------------------------------
|
12
|
+
# Viewers
|
13
|
+
# ----------------------------------------------------------------------------------------------------------------------
|
14
|
+
|
15
|
+
|
16
|
+
class FeatureStore:
|
17
|
+
"""
|
18
|
+
Manages storing and retrieving annotation features for MULTIPLE models
|
19
|
+
using a single SQLite database and multiple, model-specific FAISS indexes.
|
20
|
+
"""
|
21
|
+
def __init__(self, db_path='feature_store.db', index_path_base='features'):
|
22
|
+
self.db_path = db_path
|
23
|
+
self.index_path_base = index_path_base # Base name for index files, e.g., 'features'
|
24
|
+
self.conn = sqlite3.connect(self.db_path)
|
25
|
+
self.cursor = self.conn.cursor()
|
26
|
+
self._create_table()
|
27
|
+
|
28
|
+
# A dictionary to hold multiple FAISS indexes, keyed by model_key
|
29
|
+
self.faiss_indexes = {}
|
30
|
+
|
31
|
+
def _create_table(self):
|
32
|
+
"""Create the metadata table if it doesn't exist."""
|
33
|
+
self.cursor.execute('''
|
34
|
+
CREATE TABLE IF NOT EXISTS features (
|
35
|
+
annotation_id TEXT NOT NULL,
|
36
|
+
model_key TEXT NOT NULL,
|
37
|
+
faiss_index INTEGER NOT NULL,
|
38
|
+
PRIMARY KEY (annotation_id, model_key)
|
39
|
+
)
|
40
|
+
''')
|
41
|
+
self.conn.commit()
|
42
|
+
|
43
|
+
def _get_or_load_index(self, model_key):
|
44
|
+
"""
|
45
|
+
Retrieves an index from memory or loads it from disk if it exists.
|
46
|
+
Returns the index object or None if not found in memory or on disk.
|
47
|
+
"""
|
48
|
+
# 1. Check if the index is already loaded in memory
|
49
|
+
if model_key in self.faiss_indexes:
|
50
|
+
return self.faiss_indexes[model_key]
|
51
|
+
|
52
|
+
# 2. If not in memory, check for a corresponding file on disk
|
53
|
+
index_path = f"{self.index_path_base}_{model_key}.faiss"
|
54
|
+
if os.path.exists(index_path):
|
55
|
+
print(f"Loading existing FAISS index from {index_path}")
|
56
|
+
index = faiss.read_index(index_path)
|
57
|
+
self.faiss_indexes[model_key] = index # Cache it in memory
|
58
|
+
return index
|
59
|
+
|
60
|
+
# 3. If not in memory or on disk, return None
|
61
|
+
return None
|
62
|
+
|
63
|
+
def add_features(self, data_items, features, model_key):
|
64
|
+
"""
|
65
|
+
Adds new features to the store for a specific model.
|
66
|
+
"""
|
67
|
+
if not len(features):
|
68
|
+
return
|
69
|
+
|
70
|
+
# Get the specific index for this model, loading it if necessary
|
71
|
+
index = self._get_or_load_index(model_key)
|
72
|
+
|
73
|
+
# If no index exists yet, create one
|
74
|
+
if index is None:
|
75
|
+
feature_dim = features.shape[1]
|
76
|
+
print(f"Creating new FAISS index for model '{model_key}' with dimension {feature_dim}.")
|
77
|
+
index = faiss.IndexFlatL2(feature_dim)
|
78
|
+
self.faiss_indexes[model_key] = index
|
79
|
+
|
80
|
+
# Add vectors to the specific FAISS index
|
81
|
+
start_index = index.ntotal
|
82
|
+
index.add(features.astype('float32'))
|
83
|
+
|
84
|
+
# Add metadata to SQLite. The table already supports multiple models.
|
85
|
+
for i, item in enumerate(data_items):
|
86
|
+
faiss_row_index = start_index + i
|
87
|
+
self.cursor.execute(
|
88
|
+
"INSERT OR REPLACE INTO features (annotation_id, model_key, faiss_index) VALUES (?, ?, ?)",
|
89
|
+
(item.annotation.id, model_key, faiss_row_index)
|
90
|
+
)
|
91
|
+
self.conn.commit()
|
92
|
+
self.save_faiss_index(model_key) # Save the specific index that was modified
|
93
|
+
|
94
|
+
def get_features(self, data_items, model_key):
|
95
|
+
"""
|
96
|
+
Retrieves features for given data items and a specific model.
|
97
|
+
"""
|
98
|
+
# Get the specific index for this model
|
99
|
+
index = self._get_or_load_index(model_key)
|
100
|
+
|
101
|
+
if index is None:
|
102
|
+
# No features have ever been stored for this model
|
103
|
+
return {}, data_items
|
104
|
+
|
105
|
+
found_features = {}
|
106
|
+
not_found_items = []
|
107
|
+
|
108
|
+
ids_to_query = [item.annotation.id for item in data_items]
|
109
|
+
|
110
|
+
# Query SQLite for the given model_key
|
111
|
+
placeholders = ','.join('?' for _ in ids_to_query)
|
112
|
+
query = (f"SELECT annotation_id, faiss_index FROM features "
|
113
|
+
f"WHERE model_key=? AND annotation_id IN ({placeholders})")
|
114
|
+
params = [model_key] + ids_to_query
|
115
|
+
self.cursor.execute(query, params)
|
116
|
+
|
117
|
+
faiss_map = {ann_id: faiss_idx for ann_id, faiss_idx in self.cursor.fetchall()}
|
118
|
+
|
119
|
+
if not faiss_map:
|
120
|
+
return {}, data_items
|
121
|
+
|
122
|
+
# Reconstruct vectors from the correct FAISS index
|
123
|
+
faiss_indices = list(faiss_map.values())
|
124
|
+
retrieved_vectors = index.reconstruct_batch(faiss_indices)
|
125
|
+
|
126
|
+
id_to_vector = {ann_id: retrieved_vectors[i] for i, ann_id in enumerate(faiss_map.keys())}
|
127
|
+
|
128
|
+
for item in data_items:
|
129
|
+
ann_id = item.annotation.id
|
130
|
+
if ann_id in id_to_vector:
|
131
|
+
found_features[ann_id] = id_to_vector[ann_id]
|
132
|
+
else:
|
133
|
+
not_found_items.append(item)
|
134
|
+
|
135
|
+
return found_features, not_found_items
|
136
|
+
|
137
|
+
def get_faiss_index_to_annotation_id_map(self, model_key):
|
138
|
+
"""
|
139
|
+
Retrieves a mapping from FAISS row index to annotation_id for a given model.
|
140
|
+
"""
|
141
|
+
query = "SELECT faiss_index, annotation_id FROM features WHERE model_key = ?"
|
142
|
+
self.cursor.execute(query, (model_key,))
|
143
|
+
return {faiss_idx: ann_id for faiss_idx, ann_id in self.cursor.fetchall()}
|
144
|
+
|
145
|
+
def save_faiss_index(self, model_key):
|
146
|
+
"""Saves a specific FAISS index to disk."""
|
147
|
+
if model_key in self.faiss_indexes:
|
148
|
+
index_to_save = self.faiss_indexes[model_key]
|
149
|
+
index_path = f"{self.index_path_base}_{model_key}.faiss"
|
150
|
+
print(f"Saving FAISS index for '{model_key}' to {index_path}")
|
151
|
+
faiss.write_index(index_to_save, index_path)
|
152
|
+
|
153
|
+
def close(self):
|
154
|
+
"""Closes the database connection."""
|
155
|
+
self.conn.close()
|
156
|
+
|
157
|
+
def delete_storage(self):
|
158
|
+
"""
|
159
|
+
Closes connection and deletes the DB and ALL FAISS index files.
|
160
|
+
"""
|
161
|
+
self.close()
|
162
|
+
|
163
|
+
if os.path.exists(self.db_path):
|
164
|
+
try:
|
165
|
+
os.remove(self.db_path)
|
166
|
+
print(f"Deleted feature database: {self.db_path}")
|
167
|
+
except OSError as e:
|
168
|
+
print(f"Error removing database file {self.db_path}: {e}")
|
169
|
+
|
170
|
+
# Use glob to find and delete all matching index files
|
171
|
+
for index_file in glob.glob(f"{self.index_path_base}_*.faiss"):
|
172
|
+
try:
|
173
|
+
os.remove(index_file)
|
174
|
+
print(f"Deleted FAISS index: {index_file}")
|
175
|
+
except OSError as e:
|
176
|
+
print(f"Error removing index file {index_file}: {e}")
|
@@ -1,10 +1,10 @@
|
|
1
1
|
import os
|
2
2
|
import warnings
|
3
3
|
|
4
|
-
from PyQt5.QtCore import Qt
|
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)
|
7
|
+
QLineEdit, QFileDialog, QFormLayout, QSpinBox)
|
8
8
|
|
9
9
|
from coralnet_toolbox.MachineLearning.Community.cfg import get_available_configs
|
10
10
|
|
@@ -16,6 +16,180 @@ warnings.filterwarnings("ignore", category=DeprecationWarning)
|
|
16
16
|
# ----------------------------------------------------------------------------------------------------------------------
|
17
17
|
|
18
18
|
|
19
|
+
class UncertaintySettingsWidget(QWidget):
|
20
|
+
"""A widget for configuring uncertainty sampling parameters."""
|
21
|
+
parameters_changed = pyqtSignal(dict)
|
22
|
+
|
23
|
+
def __init__(self, parent=None):
|
24
|
+
super().__init__(parent)
|
25
|
+
self.setup_ui()
|
26
|
+
# Set a strong focus policy to prevent the menu from closing on interaction
|
27
|
+
self.confidence_slider.setFocusPolicy(Qt.StrongFocus)
|
28
|
+
self.margin_slider.setFocusPolicy(Qt.StrongFocus)
|
29
|
+
|
30
|
+
def setup_ui(self):
|
31
|
+
"""Creates the UI controls for the parameters."""
|
32
|
+
main_layout = QFormLayout(self)
|
33
|
+
main_layout.setContentsMargins(10, 10, 10, 10)
|
34
|
+
main_layout.setSpacing(15)
|
35
|
+
|
36
|
+
# 1. Confidence Threshold
|
37
|
+
confidence_layout = QHBoxLayout()
|
38
|
+
self.confidence_slider = QSlider(Qt.Horizontal)
|
39
|
+
self.confidence_slider.setMinimum(0)
|
40
|
+
self.confidence_slider.setMaximum(100)
|
41
|
+
self.confidence_slider.setValue(60)
|
42
|
+
self.confidence_slider.setToolTip(
|
43
|
+
"Find annotations where the model's top guess\n"
|
44
|
+
"has a confidence BELOW this threshold."
|
45
|
+
)
|
46
|
+
self.confidence_label = QLabel("60%")
|
47
|
+
self.confidence_label.setMinimumWidth(40)
|
48
|
+
confidence_layout.addWidget(self.confidence_slider)
|
49
|
+
confidence_layout.addWidget(self.confidence_label)
|
50
|
+
main_layout.addRow("Max Confidence:", confidence_layout)
|
51
|
+
|
52
|
+
# 2. Margin Threshold
|
53
|
+
margin_layout = QHBoxLayout()
|
54
|
+
self.margin_slider = QSlider(Qt.Horizontal)
|
55
|
+
self.margin_slider.setMinimum(0)
|
56
|
+
self.margin_slider.setMaximum(50)
|
57
|
+
self.margin_slider.setValue(10)
|
58
|
+
self.margin_slider.setToolTip(
|
59
|
+
"Find annotations where the confidence difference\n"
|
60
|
+
"between the top two guesses is BELOW this threshold."
|
61
|
+
)
|
62
|
+
self.margin_label = QLabel("10%")
|
63
|
+
self.margin_label.setMinimumWidth(40)
|
64
|
+
margin_layout.addWidget(self.margin_slider)
|
65
|
+
margin_layout.addWidget(self.margin_label)
|
66
|
+
main_layout.addRow("Min Margin:", margin_layout)
|
67
|
+
|
68
|
+
# Connect signals
|
69
|
+
self.confidence_slider.valueChanged.connect(self._emit_parameters)
|
70
|
+
self.margin_slider.valueChanged.connect(self._emit_parameters)
|
71
|
+
self.confidence_slider.valueChanged.connect(
|
72
|
+
lambda v: self.confidence_label.setText(f"{v}%")
|
73
|
+
)
|
74
|
+
self.margin_slider.valueChanged.connect(
|
75
|
+
lambda v: self.margin_label.setText(f"{v}%")
|
76
|
+
)
|
77
|
+
|
78
|
+
@pyqtSlot()
|
79
|
+
def _emit_parameters(self):
|
80
|
+
"""Gathers current values and emits them in a dictionary."""
|
81
|
+
params = self.get_parameters()
|
82
|
+
self.parameters_changed.emit(params)
|
83
|
+
|
84
|
+
def get_parameters(self):
|
85
|
+
"""Returns the current parameters as a dictionary."""
|
86
|
+
return {
|
87
|
+
'confidence': self.confidence_slider.value() / 100.0,
|
88
|
+
'margin': self.margin_slider.value() / 100.0
|
89
|
+
}
|
90
|
+
|
91
|
+
|
92
|
+
class MislabelSettingsWidget(QWidget):
|
93
|
+
"""A widget for configuring mislabel detection parameters."""
|
94
|
+
parameters_changed = pyqtSignal(dict)
|
95
|
+
|
96
|
+
def __init__(self, parent=None):
|
97
|
+
super().__init__(parent)
|
98
|
+
self.setup_ui()
|
99
|
+
# Set a default value to prevent the menu from closing on interaction
|
100
|
+
self.k_spinbox.setFocusPolicy(Qt.StrongFocus)
|
101
|
+
self.threshold_slider.setFocusPolicy(Qt.StrongFocus)
|
102
|
+
|
103
|
+
def setup_ui(self):
|
104
|
+
"""Creates the UI controls for the parameters."""
|
105
|
+
main_layout = QFormLayout(self)
|
106
|
+
main_layout.setContentsMargins(10, 10, 10, 10)
|
107
|
+
main_layout.setSpacing(15)
|
108
|
+
|
109
|
+
# 1. K (Number of Neighbors)
|
110
|
+
self.k_spinbox = QSpinBox()
|
111
|
+
self.k_spinbox.setMinimum(2)
|
112
|
+
self.k_spinbox.setMaximum(50)
|
113
|
+
self.k_spinbox.setValue(5)
|
114
|
+
self.k_spinbox.setToolTip("Number of neighbors to check for each point (K).")
|
115
|
+
main_layout.addRow("Neighbors (K):", self.k_spinbox)
|
116
|
+
|
117
|
+
# 2. Agreement Threshold
|
118
|
+
threshold_layout = QHBoxLayout()
|
119
|
+
self.threshold_slider = QSlider(Qt.Horizontal)
|
120
|
+
self.threshold_slider.setMinimum(0)
|
121
|
+
self.threshold_slider.setMaximum(100)
|
122
|
+
self.threshold_slider.setValue(60)
|
123
|
+
self.threshold_slider.setToolTip(
|
124
|
+
"A point is flagged if the percentage of neighbors\n"
|
125
|
+
"with the same label is BELOW this threshold."
|
126
|
+
)
|
127
|
+
|
128
|
+
self.threshold_label = QLabel("60%")
|
129
|
+
self.threshold_label.setMinimumWidth(40)
|
130
|
+
|
131
|
+
threshold_layout.addWidget(self.threshold_slider)
|
132
|
+
threshold_layout.addWidget(self.threshold_label)
|
133
|
+
main_layout.addRow("Agreement:", threshold_layout)
|
134
|
+
|
135
|
+
# Connect signals
|
136
|
+
self.k_spinbox.valueChanged.connect(self._emit_parameters)
|
137
|
+
self.threshold_slider.valueChanged.connect(self._emit_parameters)
|
138
|
+
self.threshold_slider.valueChanged.connect(
|
139
|
+
lambda v: self.threshold_label.setText(f"{v}%")
|
140
|
+
)
|
141
|
+
|
142
|
+
@pyqtSlot()
|
143
|
+
def _emit_parameters(self):
|
144
|
+
"""Gathers current values and emits them in a dictionary."""
|
145
|
+
params = self.get_parameters()
|
146
|
+
self.parameters_changed.emit(params)
|
147
|
+
|
148
|
+
def get_parameters(self):
|
149
|
+
"""Returns the current parameters as a dictionary."""
|
150
|
+
return {
|
151
|
+
'k': self.k_spinbox.value(),
|
152
|
+
'threshold': self.threshold_slider.value() / 100.0
|
153
|
+
}
|
154
|
+
|
155
|
+
|
156
|
+
class SimilaritySettingsWidget(QWidget):
|
157
|
+
"""A widget for configuring similarity search parameters (number of neighbors)."""
|
158
|
+
parameters_changed = pyqtSignal(dict)
|
159
|
+
|
160
|
+
def __init__(self, parent=None):
|
161
|
+
super().__init__(parent)
|
162
|
+
self.setup_ui()
|
163
|
+
self.k_spinbox.setFocusPolicy(Qt.StrongFocus)
|
164
|
+
|
165
|
+
def setup_ui(self):
|
166
|
+
"""Creates the UI controls for the parameters."""
|
167
|
+
main_layout = QFormLayout(self)
|
168
|
+
main_layout.setContentsMargins(10, 10, 10, 10)
|
169
|
+
main_layout.setSpacing(15)
|
170
|
+
|
171
|
+
# K (Number of Neighbors)
|
172
|
+
self.k_spinbox = QSpinBox()
|
173
|
+
self.k_spinbox.setMinimum(1)
|
174
|
+
self.k_spinbox.setMaximum(200)
|
175
|
+
self.k_spinbox.setValue(10)
|
176
|
+
self.k_spinbox.setToolTip("Number of similar items to find (K).")
|
177
|
+
main_layout.addRow("Neighbors (K):", self.k_spinbox)
|
178
|
+
|
179
|
+
# Connect signals
|
180
|
+
self.k_spinbox.valueChanged.connect(self._emit_parameters)
|
181
|
+
|
182
|
+
@pyqtSlot()
|
183
|
+
def _emit_parameters(self):
|
184
|
+
params = self.get_parameters()
|
185
|
+
self.parameters_changed.emit(params)
|
186
|
+
|
187
|
+
def get_parameters(self):
|
188
|
+
return {
|
189
|
+
'k': self.k_spinbox.value()
|
190
|
+
}
|
191
|
+
|
192
|
+
|
19
193
|
class AnnotationSettingsWidget(QGroupBox):
|
20
194
|
"""Widget for filtering annotations by image, type, and label in a multi-column layout."""
|
21
195
|
|
@@ -220,6 +394,7 @@ class AnnotationSettingsWidget(QGroupBox):
|
|
220
394
|
|
221
395
|
class ModelSettingsWidget(QGroupBox):
|
222
396
|
"""Widget containing model selection with tabs for different model sources."""
|
397
|
+
selection_changed = pyqtSignal()
|
223
398
|
|
224
399
|
def __init__(self, main_window, parent=None):
|
225
400
|
super(ModelSettingsWidget, self).__init__("Model Settings", parent)
|
@@ -243,21 +418,23 @@ class ModelSettingsWidget(QGroupBox):
|
|
243
418
|
self.model_combo.addItems(["Color Features"])
|
244
419
|
self.model_combo.insertSeparator(1) # Add a separator
|
245
420
|
|
246
|
-
standard_models = [
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
421
|
+
standard_models = [
|
422
|
+
'yolov8n-cls.pt',
|
423
|
+
'yolov8s-cls.pt',
|
424
|
+
'yolov8m-cls.pt',
|
425
|
+
'yolov8l-cls.pt',
|
426
|
+
'yolov8x-cls.pt',
|
427
|
+
'yolo11n-cls.pt',
|
428
|
+
'yolo11s-cls.pt',
|
429
|
+
'yolo11m-cls.pt',
|
430
|
+
'yolo11l-cls.pt',
|
431
|
+
'yolo11x-cls.pt',
|
432
|
+
'yolo12n-cls.pt',
|
433
|
+
'yolo12s-cls.pt',
|
434
|
+
'yolo12m-cls.pt',
|
435
|
+
'yolo12l-cls.pt',
|
436
|
+
'yolo12x-cls.pt'
|
437
|
+
]
|
261
438
|
|
262
439
|
self.model_combo.addItems(standard_models)
|
263
440
|
|
@@ -267,9 +444,6 @@ class ModelSettingsWidget(QGroupBox):
|
|
267
444
|
self.model_combo.addItems(list(community_configs.keys()))
|
268
445
|
|
269
446
|
self.model_combo.setCurrentText('Color Features')
|
270
|
-
# Connect selection change to update feature mode field state
|
271
|
-
self.model_combo.currentTextChanged.connect(self._update_feature_mode_state)
|
272
|
-
|
273
447
|
model_select_layout.addRow("Model:", self.model_combo)
|
274
448
|
|
275
449
|
self.tabs.addTab(model_select_tab, "Select Model")
|
@@ -293,18 +467,21 @@ class ModelSettingsWidget(QGroupBox):
|
|
293
467
|
|
294
468
|
main_layout.addWidget(self.tabs)
|
295
469
|
|
296
|
-
# Connect
|
297
|
-
self.
|
470
|
+
# Connect all relevant widgets to a single slot that emits the new signal
|
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)
|
298
474
|
|
299
475
|
# Add feature extraction mode selection outside of tabs
|
300
476
|
feature_mode_layout = QFormLayout()
|
301
477
|
self.feature_mode_combo = QComboBox()
|
302
478
|
self.feature_mode_combo.addItems(["Predictions", "Embed Features"])
|
479
|
+
self.feature_mode_combo.currentTextChanged.connect(self._on_selection_changed)
|
303
480
|
feature_mode_layout.addRow("Feature Mode:", self.feature_mode_combo)
|
304
481
|
main_layout.addLayout(feature_mode_layout)
|
305
482
|
|
306
|
-
# Initialize the feature mode state
|
307
|
-
self.
|
483
|
+
# Initialize the feature mode state and emit the first signal
|
484
|
+
self._on_selection_changed()
|
308
485
|
|
309
486
|
def browse_for_model(self):
|
310
487
|
"""Open a file dialog to browse for model files."""
|
@@ -318,6 +495,12 @@ class ModelSettingsWidget(QGroupBox):
|
|
318
495
|
)
|
319
496
|
if file_path:
|
320
497
|
self.model_path_edit.setText(file_path)
|
498
|
+
|
499
|
+
@pyqtSlot()
|
500
|
+
def _on_selection_changed(self):
|
501
|
+
"""Central slot to handle any change in model selection and emit a single signal."""
|
502
|
+
self._update_feature_mode_state()
|
503
|
+
self.selection_changed.emit()
|
321
504
|
|
322
505
|
def _update_feature_mode_state(self, *args):
|
323
506
|
"""Update the enabled state of the feature mode field based on the current model selection."""
|
@@ -457,16 +640,20 @@ class EmbeddingSettingsWidget(QGroupBox):
|
|
457
640
|
self.param2_slider.valueChanged.connect(lambda v: self.param2_value_label.setText(f"{v/10.0:.1f}"))
|
458
641
|
|
459
642
|
elif technique == "PCA":
|
460
|
-
# Disable both rows for PCA
|
643
|
+
# Disable both rows for PCA and reset to minimum values
|
461
644
|
self.param1_label.setEnabled(False)
|
462
645
|
self.param1_slider.setEnabled(False)
|
463
646
|
self.param1_value_label.setEnabled(False)
|
464
647
|
self.param1_label.setText(" ")
|
648
|
+
self.param1_slider.setValue(self.param1_slider.minimum())
|
649
|
+
self.param1_value_label.setText(str(self.param1_slider.minimum()))
|
465
650
|
|
466
651
|
self.param2_label.setEnabled(False)
|
467
652
|
self.param2_slider.setEnabled(False)
|
468
653
|
self.param2_value_label.setEnabled(False)
|
469
654
|
self.param2_label.setText(" ")
|
655
|
+
self.param2_slider.setValue(self.param2_slider.minimum())
|
656
|
+
self.param2_value_label.setText(str(self.param2_slider.minimum()))
|
470
657
|
|
471
658
|
def get_embedding_parameters(self):
|
472
659
|
"""Returns a dictionary of the current embedding parameters."""
|
@@ -26,18 +26,29 @@ class GlobalEventFilter(QObject):
|
|
26
26
|
self.auto_distill_deploy_model_dialog = main_window.auto_distill_deploy_model_dialog
|
27
27
|
|
28
28
|
def eventFilter(self, obj, event):
|
29
|
+
# Check for explorer window first - this applies to all event types
|
30
|
+
if hasattr(self.main_window, 'explorer_window') and self.main_window.explorer_window:
|
31
|
+
# Special exception for WASD keys which should always work
|
32
|
+
if event.type() == QEvent.KeyPress and event.key() in [Qt.Key_W, Qt.Key_A, Qt.Key_S, Qt.Key_D] and \
|
33
|
+
event.modifiers() & Qt.ControlModifier:
|
34
|
+
self.label_window.handle_wasd_key(event.key())
|
35
|
+
return True
|
36
|
+
|
37
|
+
# For all other events when explorer is visible, pass them through
|
38
|
+
return False
|
39
|
+
|
40
|
+
# Now handle keyboard events
|
29
41
|
if event.type() == QEvent.KeyPress:
|
30
42
|
if event.modifiers() & Qt.ControlModifier and not (event.modifiers() & Qt.ShiftModifier):
|
31
|
-
|
32
|
-
# Handle Tab key for switching between Select and Annotation tools
|
33
|
-
if event.key() == Qt.Key_Alt:
|
34
|
-
self.main_window.switch_back_to_tool()
|
35
|
-
return True
|
36
|
-
|
37
43
|
# Handle WASD keys for selecting Label
|
38
44
|
if event.key() in [Qt.Key_W, Qt.Key_A, Qt.Key_S, Qt.Key_D]:
|
39
45
|
self.label_window.handle_wasd_key(event.key())
|
40
46
|
return True
|
47
|
+
|
48
|
+
# Handle Alt key for switching between Select and Annotation tools
|
49
|
+
if event.key() == Qt.Key_Alt:
|
50
|
+
self.main_window.switch_back_to_tool()
|
51
|
+
return True
|
41
52
|
|
42
53
|
# Handle hotkey for image classification prediction
|
43
54
|
if event.key() == Qt.Key_1:
|
@@ -72,13 +83,14 @@ class GlobalEventFilter(QObject):
|
|
72
83
|
self.annotation_window.cycle_annotations(1)
|
73
84
|
return True
|
74
85
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
86
|
+
# Delete (backspace or delete key) selected annotations when select tool is active
|
87
|
+
if event.key() == Qt.Key_Delete or event.key() == Qt.Key_Backspace:
|
88
|
+
if self.main_window.select_tool_action.isChecked():
|
89
|
+
if self.annotation_window.selected_annotations:
|
90
|
+
self.annotation_window.delete_selected_annotations()
|
91
|
+
# Consume the event so it doesn't do anything else
|
80
92
|
return True
|
81
|
-
|
93
|
+
|
82
94
|
# Handle image cycling hotkeys
|
83
95
|
if event.key() == Qt.Key_Up and event.modifiers() == (Qt.AltModifier):
|
84
96
|
self.image_window.cycle_previous_image()
|
@@ -395,23 +395,35 @@ class LabelWindow(QWidget):
|
|
395
395
|
"""Update the annotation count display with current selection and total count."""
|
396
396
|
annotations = self.annotation_window.get_image_annotations()
|
397
397
|
|
398
|
-
# Check if we're in Explorer mode
|
399
|
-
explorer_selected_count = 0
|
398
|
+
# Check if we're in Explorer mode
|
400
399
|
if (hasattr(self.main_window, 'explorer_window') and
|
401
400
|
self.main_window.explorer_window and
|
402
401
|
hasattr(self.main_window.explorer_window, 'annotation_viewer')):
|
403
|
-
|
402
|
+
|
403
|
+
annotation_viewer = self.main_window.explorer_window.annotation_viewer
|
404
|
+
|
405
|
+
# --- REORDERED LOGIC ---
|
406
|
+
# Priority 1: Always check for a selection in Explorer first.
|
407
|
+
explorer_selected_count = len(annotation_viewer.selected_widgets)
|
408
|
+
if explorer_selected_count > 0:
|
409
|
+
if explorer_selected_count == 1:
|
410
|
+
text = "Annotation: 1"
|
411
|
+
else:
|
412
|
+
text = f"Annotations: {explorer_selected_count}"
|
413
|
+
self.annotation_count_display.setText(text)
|
414
|
+
return # Exit early, selection count is most important.
|
415
|
+
|
416
|
+
# Priority 2: If no selection, THEN check for isolation mode.
|
417
|
+
if annotation_viewer.isolated_mode:
|
418
|
+
count = len(annotation_viewer.isolated_widgets)
|
419
|
+
text = f"Annotations: {count}"
|
420
|
+
self.annotation_count_display.setText(text)
|
421
|
+
return # Exit early
|
404
422
|
|
405
|
-
#
|
423
|
+
# --- ORIGINAL FALLBACK LOGIC (Unchanged) ---
|
406
424
|
annotation_window_selected_count = len(self.annotation_window.selected_annotations)
|
407
425
|
|
408
|
-
|
409
|
-
if explorer_selected_count > 0:
|
410
|
-
if explorer_selected_count == 1:
|
411
|
-
text = f"Annotation: 1"
|
412
|
-
else:
|
413
|
-
text = f"Annotations: {explorer_selected_count}"
|
414
|
-
elif annotation_window_selected_count == 0:
|
426
|
+
if annotation_window_selected_count == 0:
|
415
427
|
text = f"Annotations: {len(annotations)}"
|
416
428
|
elif annotation_window_selected_count > 1:
|
417
429
|
text = f"Annotations: {annotation_window_selected_count}"
|