Skip to content

Commit f9ff570

Browse files
committed
Persist processor and DLC camera selections
Restore and save processor choice, processor-control toggle, and inference camera preference through settings so user selections survive restarts. The camera handling now separates preferred vs active inference camera IDs, using temporary runtime fallback cameras without overwriting the saved preference, and updates overlay/inference routing accordingly. It also improves processor list refresh behavior by restoring the previous selection when available and syncing settings on close.
1 parent ade278f commit f9ff570

1 file changed

Lines changed: 150 additions & 41 deletions

File tree

‎dlclivegui/gui/main_window.py‎

Lines changed: 150 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,8 @@ def __init__(self, config: ApplicationSettings | None = None):
142142
)
143143

144144
self._config = config
145-
self._inference_camera_id: str | None = None # Camera ID used for inference
145+
self._inference_camera_id: str | None = self._settings_store.get_inference_camera_id()
146+
self._active_inference_camera_id: str | None = None
146147
self._running_cams_ids: set[str] = set()
147148
self._current_frame: np.ndarray | None = None
148149
self._raw_frame: np.ndarray | None = None
@@ -828,8 +829,8 @@ def _connect_signals(self) -> None:
828829
self._dlc.initialized.connect(self._on_dlc_initialised)
829830
self.dlc_camera_combo.currentIndexChanged.connect(self._on_dlc_camera_changed)
830831
self.dlc_camera_combo.currentTextChanged.connect(self.dlc_camera_combo.update_shrink_width)
831-
self.allow_processor_ctrl_checkbox.stateChanged.connect(lambda _s: self._update_dlc_controls_enabled())
832-
self.allow_processor_ctrl_checkbox.stateChanged.connect(lambda _s: self._update_processor_status())
832+
self.processor_combo.currentIndexChanged.connect(self._on_processor_changed)
833+
self.allow_processor_ctrl_checkbox.stateChanged.connect(self._on_processor_control_changed)
833834

834835
# Recording settings
835836
## Session name persistence + preview updates
@@ -898,6 +899,13 @@ def _apply_config(self, config: ApplicationSettings) -> None:
898899
if hasattr(self, "bbox_color_combo"):
899900
color_ui.set_bbox_combo_from_bgr(self.bbox_color_combo, self._bbox_color)
900901

902+
# Processor
903+
## Allow processor control checkbox state
904+
if hasattr(self, "allow_processor_ctrl_checkbox"):
905+
self.allow_processor_ctrl_checkbox.setChecked(
906+
self._settings_store.get_processor_control_enabled(default=False)
907+
)
908+
901909
# Update DLC camera list
902910
self._refresh_dlc_camera_list()
903911

@@ -1167,7 +1175,31 @@ def _processor_control_enabled(self) -> bool:
11671175
getattr(self, "allow_processor_ctrl_checkbox", None) and self.allow_processor_ctrl_checkbox.isChecked()
11681176
)
11691177

1178+
def _on_processor_changed(self, _index: int) -> None:
1179+
"""Persist selected processor key when the user changes the processor combo."""
1180+
processor_key = self.processor_combo.currentData()
1181+
self._settings_store.set_processor_key(processor_key)
1182+
1183+
if hasattr(self.processor_combo, "update_shrink_width"):
1184+
self.processor_combo.update_shrink_width()
1185+
1186+
def _on_processor_control_changed(self, _state: int) -> None:
1187+
"""Persist processor-control checkbox state and refresh dependent UI."""
1188+
enabled = self.allow_processor_ctrl_checkbox.isChecked()
1189+
self._settings_store.set_processor_control_enabled(enabled)
1190+
1191+
self._update_dlc_controls_enabled()
1192+
self._update_processor_status()
1193+
11701194
def _refresh_processors(self) -> None:
1195+
"""Scan processors and restore the last selected processor best-effort."""
1196+
previous_key = None
1197+
if hasattr(self, "processor_combo"):
1198+
previous_key = self.processor_combo.currentData()
1199+
1200+
preferred_key = previous_key or self._settings_store.get_processor_key()
1201+
1202+
self.processor_combo.blockSignals(True)
11711203
self.processor_combo.clear()
11721204
self.processor_combo.addItem("No Processor", None)
11731205

@@ -1190,8 +1222,23 @@ def _refresh_processors(self) -> None:
11901222
display_name = f"{info['name']} ({info['file']})"
11911223
self.processor_combo.addItem(display_name, key)
11921224

1225+
# Restore selected processor best-effort.
1226+
if preferred_key is not None:
1227+
idx = self.processor_combo.findData(preferred_key)
1228+
if idx >= 0:
1229+
self.processor_combo.setCurrentIndex(idx)
1230+
else:
1231+
self.processor_combo.setCurrentIndex(0)
1232+
else:
1233+
self.processor_combo.setCurrentIndex(0)
1234+
1235+
self.processor_combo.blockSignals(False)
11931236
self.processor_combo.update_shrink_width()
1194-
self.statusBar().showMessage(f"Found {len(self._processor_keys)} processor(s) in {source_text}", 3000)
1237+
1238+
self.statusBar().showMessage(
1239+
f"Found {len(self._processor_keys)} processor(s) in {source_text}",
1240+
3000,
1241+
)
11951242

11961243
# ------------------------------------------------------------------
11971244
# Recording path preview and session name persistence
@@ -1385,34 +1432,56 @@ def _label_for_cam_id(self, cam_id: str) -> str:
13851432
return "Unknown camera"
13861433

13871434
def _refresh_dlc_camera_list_running(self) -> None:
1388-
"""Populate the inference camera dropdown from currently running cameras."""
1435+
"""Populate inference camera dropdown from currently running cameras.
1436+
1437+
- Keep the user's preferred camera if it is running
1438+
- Otherwise use a temporary runtime fallback
1439+
- Never persist fallback choices caused by preview/update events
1440+
"""
1441+
preferred_id = self._inference_camera_id or self._settings_store.get_inference_camera_id()
1442+
13891443
self.dlc_camera_combo.blockSignals(True)
13901444
self.dlc_camera_combo.clear()
1445+
13911446
for cam in self._config.multi_camera.get_active_cameras():
13921447
cam_id = get_camera_id(cam)
13931448
if cam_id in self._running_cams_ids:
13941449
self.dlc_camera_combo.addItem(self._label_for_cam_id(cam_id), cam_id)
13951450

1396-
# Keep current selection if still present, else select first running
1397-
if self._inference_camera_id in self._running_cams_ids:
1398-
idx = self.dlc_camera_combo.findData(self._inference_camera_id)
1451+
selected_id = None
1452+
1453+
if preferred_id in self._running_cams_ids:
1454+
idx = self.dlc_camera_combo.findData(preferred_id)
13991455
if idx >= 0:
14001456
self.dlc_camera_combo.setCurrentIndex(idx)
1401-
elif self.dlc_camera_combo.count() > 0:
1457+
selected_id = preferred_id
1458+
1459+
if selected_id is None and self.dlc_camera_combo.count() > 0:
14021460
self.dlc_camera_combo.setCurrentIndex(0)
1403-
self._inference_camera_id = self.dlc_camera_combo.currentData()
1461+
selected_id = self.dlc_camera_combo.currentData()
1462+
1463+
self._active_inference_camera_id = selected_id
1464+
14041465
self.dlc_camera_combo.blockSignals(False)
1466+
self.dlc_camera_combo.update_shrink_width()
14051467

1406-
def _set_dlc_combo_to_id(self, cam_id: str) -> None:
1407-
"""Update combo selection to a given ID without firing signals."""
1468+
def _set_dlc_combo_to_id(self, cam_id: str) -> bool:
1469+
"""Update combo selection to a given camera ID without firing signals."""
14081470
self.dlc_camera_combo.blockSignals(True)
1409-
idx = self.dlc_camera_combo.findData(cam_id)
1410-
if idx >= 0:
1411-
self.dlc_camera_combo.setCurrentIndex(idx)
1412-
self.dlc_camera_combo.blockSignals(False)
1471+
try:
1472+
idx = self.dlc_camera_combo.findData(cam_id)
1473+
if idx >= 0:
1474+
self.dlc_camera_combo.setCurrentIndex(idx)
1475+
return True
1476+
return False
1477+
finally:
1478+
self.dlc_camera_combo.blockSignals(False)
1479+
self.dlc_camera_combo.update_shrink_width()
14131480

14141481
def _refresh_dlc_camera_list(self) -> None:
1415-
"""Populate the inference camera dropdown from active cameras."""
1482+
"""Populate inference camera dropdown from configured active cameras."""
1483+
preferred_id = self._inference_camera_id or self._settings_store.get_inference_camera_id()
1484+
14161485
self.dlc_camera_combo.blockSignals(True)
14171486
self.dlc_camera_combo.clear()
14181487

@@ -1423,27 +1492,41 @@ def _refresh_dlc_camera_list(self) -> None:
14231492
label = f"{display_id} [{cam.backend}:{cam.index}]"
14241493
self.dlc_camera_combo.addItem(label, cam_id)
14251494

1426-
# Keep previous selection if still present, else default to first
1427-
if self._inference_camera_id is not None:
1428-
idx = self.dlc_camera_combo.findData(self._inference_camera_id)
1495+
selected_id = None
1496+
1497+
if preferred_id is not None:
1498+
idx = self.dlc_camera_combo.findData(preferred_id)
14291499
if idx >= 0:
14301500
self.dlc_camera_combo.setCurrentIndex(idx)
1431-
elif self.dlc_camera_combo.count() > 0:
1432-
self.dlc_camera_combo.setCurrentIndex(0)
1433-
self._inference_camera_id = self.dlc_camera_combo.currentData()
1434-
else:
1435-
if self.dlc_camera_combo.count() > 0:
1436-
self.dlc_camera_combo.setCurrentIndex(0)
1437-
self._inference_camera_id = self.dlc_camera_combo.currentData()
1501+
selected_id = preferred_id
1502+
1503+
if selected_id is None and self.dlc_camera_combo.count() > 0:
1504+
self.dlc_camera_combo.setCurrentIndex(0)
1505+
selected_id = self.dlc_camera_combo.currentData()
1506+
1507+
# First-run convenience only.
1508+
# If there is no previous preference, initialize one.
1509+
if self._inference_camera_id is None and preferred_id is None:
1510+
self._inference_camera_id = selected_id
1511+
self._settings_store.set_inference_camera_id(selected_id)
1512+
1513+
self._active_inference_camera_id = selected_id
14381514

14391515
self.dlc_camera_combo.blockSignals(False)
14401516
self.dlc_camera_combo.update_shrink_width()
14411517

14421518
def _on_dlc_camera_changed(self, _index: int) -> None:
1443-
"""Track user selection of the inference camera."""
1444-
self._inference_camera_id = self.dlc_camera_combo.currentData()
1519+
"""Track explicit user selection of the inference camera."""
1520+
cam_id = self.dlc_camera_combo.currentData()
1521+
1522+
self._inference_camera_id = cam_id
1523+
self._active_inference_camera_id = cam_id
1524+
1525+
self._settings_store.set_inference_camera_id(cam_id)
1526+
14451527
self.dlc_camera_combo.update_shrink_width()
1446-
# Force redraw so bbox/pose overlays switch to the new tile immediately
1528+
1529+
# Force redraw so bbox/pose overlays switch to the new tile immediately.
14471530
if self._current_frame is not None:
14481531
self._display_frame(self._current_frame, force=True)
14491532

@@ -1455,7 +1538,7 @@ def _render_overlays_for_recording(self, cam_id, frame):
14551538
offset, scale = (0, 0), (1.0, 1.0)
14561539

14571540
# If this is the inference camera, apply pose overlays
1458-
if cam_id == self._inference_camera_id and self._last_pose and self._last_pose.pose is not None:
1541+
if cam_id == self._active_inference_camera_id and self._last_pose and self._last_pose.pose is not None:
14591542
output = draw_pose(
14601543
output,
14611544
self._last_pose.pose,
@@ -1512,25 +1595,30 @@ def _on_multi_frame_processing_ready(self, frame_data: MultiFrameData) -> None:
15121595
self._running_cams_ids = new_running
15131596
self._refresh_dlc_camera_list_running()
15141597

1515-
# Determine DLC camera (first active camera)
1516-
selected_id = self._inference_camera_id
1598+
preferred_id = self._inference_camera_id
15171599
available_ids = list(frame_data.frames.keys())
1518-
if selected_id in frame_data.frames:
1519-
dlc_cam_id = selected_id
1600+
1601+
if preferred_id in frame_data.frames:
1602+
dlc_cam_id = preferred_id
15201603
else:
15211604
dlc_cam_id = available_ids[0] if available_ids else ""
1605+
15221606
if dlc_cam_id:
1523-
self._inference_camera_id = dlc_cam_id
1524-
self._set_dlc_combo_to_id(dlc_cam_id)
1525-
self.statusBar().showMessage(
1526-
f"DLC inference camera changed to {self._label_for_cam_id(dlc_cam_id)}", 3000
1527-
)
1528-
else: # No more cameras available
1607+
if self._active_inference_camera_id != dlc_cam_id:
1608+
self._active_inference_camera_id = dlc_cam_id
1609+
self._set_dlc_combo_to_id(dlc_cam_id)
1610+
self.statusBar().showMessage(
1611+
f"Using temporary DLC inference camera: {self._label_for_cam_id(dlc_cam_id)}",
1612+
3000,
1613+
)
1614+
else:
15291615
if self._dlc_active:
15301616
self._stop_inference(show_message=True)
15311617
self._display_dirty = True
15321618
return
15331619

1620+
self._active_inference_camera_id = dlc_cam_id
1621+
15341622
# Check if this frame is from the DLC camera
15351623
is_dlc_camera_frame = frame_data.source_camera_id == dlc_cam_id
15361624

@@ -1816,6 +1904,8 @@ def _configure_dlc(self) -> bool:
18161904
processor = None
18171905
if self._processor_control_enabled():
18181906
selected_key = self.processor_combo.currentData()
1907+
self._settings_store.set_processor_key(selected_key)
1908+
18191909
if selected_key is not None and self._scanned_processors:
18201910
try:
18211911
# For now, instantiate with no parameters
@@ -2284,9 +2374,28 @@ def closeEvent(self, event: QCloseEvent) -> None: # pragma: no cover - GUI beha
22842374

22852375
# Remember model path on exit
22862376
self._model_path_store.save_if_valid(self.model_path_edit.text().strip())
2377+
22872378
# Remember processor folder on exit
22882379
if hasattr(self, "processor_folder_edit"):
22892380
self._settings_store.set_processor_folder(self.processor_folder_edit.text().strip())
22902381

2382+
# Remember user-preferred inference camera on exit.
2383+
if hasattr(self, "_inference_camera_id"):
2384+
self._settings_store.set_inference_camera_id(self._inference_camera_id)
2385+
2386+
# Remember selected processor on exit
2387+
if hasattr(self, "processor_combo"):
2388+
self._settings_store.set_processor_key(self.processor_combo.currentData())
2389+
2390+
# Remember processor-control checkbox state on exit
2391+
if hasattr(self, "allow_processor_ctrl_checkbox"):
2392+
self._settings_store.set_processor_control_enabled(self.allow_processor_ctrl_checkbox.isChecked())
2393+
2394+
# Flush QSettings best-effort
2395+
try:
2396+
self.settings.sync()
2397+
except Exception:
2398+
logger.exception("Failed to sync QSettings on close", exc_info=True)
2399+
22912400
# Close the window
22922401
super().closeEvent(event)

0 commit comments

Comments
 (0)