@@ -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