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.
@@ -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 = ['yolov8n-cls.pt',
247
- 'yolov8s-cls.pt',
248
- 'yolov8m-cls.pt',
249
- 'yolov8l-cls.pt',
250
- 'yolov8x-cls.pt',
251
- 'yolo11n-cls.pt',
252
- 'yolo11s-cls.pt',
253
- 'yolo11m-cls.pt',
254
- 'yolo11l-cls.pt',
255
- 'yolo11x-cls.pt',
256
- 'yolo12n-cls.pt',
257
- 'yolo12s-cls.pt',
258
- 'yolo12m-cls.pt',
259
- 'yolo12l-cls.pt',
260
- 'yolo12x-cls.pt']
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 tab change to update feature mode state
297
- self.tabs.currentChanged.connect(self._update_feature_mode_state)
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 based on current selection
307
- self._update_feature_mode_state()
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
- # Delete (backspace or delete key) selected annotations when select tool is active
76
- if event.key() == Qt.Key_Delete or event.key() == Qt.Key_Backspace:
77
- if self.main_window.select_tool_action.isChecked():
78
- if self.annotation_window.selected_annotations:
79
- self.annotation_window.delete_selected_annotations()
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 and get Explorer selections
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
- explorer_selected_count = len(self.main_window.explorer_window.annotation_viewer.selected_widgets)
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
- # Get annotation window selections
423
+ # --- ORIGINAL FALLBACK LOGIC (Unchanged) ---
406
424
  annotation_window_selected_count = len(self.annotation_window.selected_annotations)
407
425
 
408
- # Prioritize Explorer selections if Explorer is open
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}"