@@ -156,16 +156,21 @@ def __init__(self, cam: CameraSettings, parent: QWidget | None = None):
156156 super ().__init__ (parent )
157157 self ._cam = copy .deepcopy (cam )
158158
159- # Do not use fast_start here as we want to actually open the camera to probe capabilities
160- # If you want a quick probe without full open, use CameraProbeWorker instead which sets fast_start=True
161- # if isinstance(self._cam.properties, dict):
162- # ns = self._cam.properties.setdefault(self._cam.backend.lower(), {})
163- # if isinstance(ns, dict):
164- # ns.setdefault("fast_start", True)
165-
166159 self ._cancel = False
167160 self ._backend : CameraBackend | None = None
168161
162+ # Do not use fast_start here as we want to actually open the camera to probe capabilities
163+ # If you want a quick probe without full open, use CameraProbeWorker instead which sets fast_start=True
164+ # Ensure preview open never uses fast_start probe mode
165+ if isinstance (self ._cam .properties , dict ):
166+ ns = self ._cam .properties .setdefault (self ._cam .backend .lower (), {})
167+ if isinstance (ns , dict ):
168+ ns ["fast_start" ] = False
169+ # Basler implements transforms in the backend
170+ # but preview already takes care of rotation/crop
171+ # so we disable transform application in probe to avoid double transforms and speed up probe
172+ ns ["apply_transforms" ] = False
173+
169174 def request_cancel (self ):
170175 self ._cancel = True
171176
@@ -990,35 +995,23 @@ def _on_active_camera_selected(self, row: int) -> None:
990995 # UI helpers/actions
991996 # -------------------------------
992997
993- def _needs_preview_reopen (self , cam : CameraSettings ) -> bool :
994- if not (self ._preview_active and self ._preview_backend ):
995- return False
996-
997- # FPS: for OpenCV, treat FPS changes as requiring reopen.
998- if self ._is_backend_opencv (cam .backend ):
999- prev_w = getattr (self ._preview_backend .settings , "width" , None )
1000- prev_h = getattr (self ._preview_backend .settings , "height" , None )
1001- if isinstance (prev_w , int ) and isinstance (prev_h , int ):
1002- if (cam .width , cam .height ) != (prev_w , prev_h ):
998+ def _should_restart_preview (self , old : CameraSettings , new : CameraSettings ) -> bool :
999+ """
1000+ Fast UX policy:
1001+ - Do NOT restart for rotation/crop (preview applies those live).
1002+ - Restart for camera-side capture params: resolution/fps/exposure/gain.
1003+ Backend-agnostic for now (no OpenCV special casing).
1004+ """
1005+ # Restart on these changes
1006+ for key in ("width" , "height" , "fps" , "exposure" , "gain" ):
1007+ try :
1008+ if getattr (old , key , None ) != getattr (new , key , None ):
10031009 return True
1004- prev_fps = getattr (self ._preview_backend .settings , "fps" , None )
1005- if isinstance (prev_fps , (int , float )) and abs (cam .fps - float (prev_fps )) > 0.1 :
1006- return True
1010+ except Exception :
1011+ return True # safest: restart
10071012
1008- return any (
1009- [
1010- cam .exposure != getattr (self ._preview_backend .settings , "exposure" , cam .exposure ),
1011- cam .gain != getattr (self ._preview_backend .settings , "gain" , cam .gain ),
1012- cam .rotation != getattr (self ._preview_backend .settings , "rotation" , cam .rotation ),
1013- (cam .crop_x0 , cam .crop_y0 , cam .crop_x1 , cam .crop_y1 )
1014- != (
1015- getattr (self ._preview_backend .settings , "crop_x0" , cam .crop_x0 ),
1016- getattr (self ._preview_backend .settings , "crop_y0" , cam .crop_y0 ),
1017- getattr (self ._preview_backend .settings , "crop_x1" , cam .crop_x1 ),
1018- getattr (self ._preview_backend .settings , "crop_y1" , cam .crop_y1 ),
1019- ),
1020- ]
1021- )
1013+ # No restart needed if only rotation/crop/enabled changed
1014+ return False
10221015
10231016 def _backend_actual_fps (self ) -> float | None :
10241017 """Return backend's actual FPS if known; for OpenCV do NOT fall back to settings.fps."""
@@ -1397,6 +1390,9 @@ def _move_camera_down(self) -> None:
13971390 self ._refresh_camera_labels ()
13981391
13991392 def _apply_camera_settings (self ) -> None :
1393+ if self ._loading_active :
1394+ self ._append_status ("[Apply] Preview is loading; please wait or cancel loading first." )
1395+ return
14001396 try :
14011397 for sb in (
14021398 self .cam_fps ,
@@ -1424,24 +1420,72 @@ def _apply_camera_settings(self) -> None:
14241420 cam = self ._working_settings .cameras [row ]
14251421 self ._write_form_to_cam (cam )
14261422
1427- must_reopen = False
1428- if self ._preview_active and self ._preview_backend :
1429- prev_model = getattr (self ._preview_backend , "settings" , None )
1430- if prev_model :
1431- must_reopen = self ._needs_preview_reopen (new_model )
1423+ # --- Logging: compute diff before overwriting anything ---
1424+ def _cam_diff (old : CameraSettings , new : CameraSettings ) -> dict :
1425+ keys = (
1426+ "width" ,
1427+ "height" ,
1428+ "fps" ,
1429+ "exposure" ,
1430+ "gain" ,
1431+ "rotation" ,
1432+ "crop_x0" ,
1433+ "crop_y0" ,
1434+ "crop_x1" ,
1435+ "crop_y1" ,
1436+ "enabled" ,
1437+ )
1438+ out = {}
1439+ for k in keys :
1440+ try :
1441+ ov = getattr (old , k , None )
1442+ nv = getattr (new , k , None )
1443+ if ov != nv :
1444+ out [k ] = (ov , nv )
1445+ except Exception :
1446+ pass
1447+ return out
1448+
1449+ # We compare against the current preview backend settings if available, else against current_model
1450+ old_for_diff = getattr (self ._preview_backend , "settings" , None ) if self ._preview_backend else current_model
1451+ diff = _cam_diff (old_for_diff if isinstance (old_for_diff , CameraSettings ) else current_model , new_model )
1452+ LOGGER .info (
1453+ "[Apply] backend=%s idx=%s changes=%s" ,
1454+ getattr (new_model , "backend" , None ),
1455+ getattr (new_model , "index" , None ),
1456+ diff ,
1457+ )
1458+
1459+ # --- Persist validated model back BEFORE touching preview ---
1460+ self ._working_settings .cameras [row ] = new_model
1461+ self ._update_active_list_item (row , new_model )
1462+
1463+ # Decide whether we need to restart preview (fast UX)
1464+ old_settings = None
1465+ if self ._preview_backend and isinstance (getattr (self ._preview_backend , "settings" , None ), CameraSettings ):
1466+ old_settings = self ._preview_backend .settings
1467+ else :
1468+ old_settings = current_model
1469+
1470+ restart = False
1471+ if self ._preview_active and isinstance (old_settings , CameraSettings ):
1472+ restart = self ._should_restart_preview (old_settings , new_model )
1473+
1474+ LOGGER .info (
1475+ "[Apply] preview_active=%s restart=%s backend=%s idx=%s" ,
1476+ self ._preview_active ,
1477+ restart ,
1478+ new_model .backend ,
1479+ new_model .index ,
1480+ )
14321481
14331482 if self ._preview_active :
1434- if must_reopen :
1483+ if restart :
1484+ self ._append_status ("[Apply] Restarting preview to apply camera settings…" )
14351485 self ._stop_preview ()
1436- self ._start_preview ( )
1486+ QTimer . singleShot ( 100 , self ._start_preview ) # small delay for drivers (Basler/Pylon )
14371487 else :
1438- self ._reconcile_fps_from_backend (new_model )
1439- if not self ._backend_actual_fps ():
1440- self ._append_status ("[Info] FPS will reconcile automatically during preview." )
1441-
1442- # Persist validated model back
1443- self ._working_settings .cameras [row ] = new_model
1444- self ._update_active_list_item (row , new_model )
1488+ self ._append_status ("[Apply] Applied without restart (crop/rotation update is live)." )
14451489
14461490 except Exception as exc :
14471491 LOGGER .exception ("Apply camera settings failed" )
@@ -1510,6 +1554,15 @@ def _start_preview(self) -> None:
15101554 cam = item .data (Qt .ItemDataRole .UserRole )
15111555 if not cam :
15121556 return
1557+ LOGGER .info (
1558+ "[Preview] start requested row=%s backend=%s idx=%s name=%s loading=%s active=%s" ,
1559+ self ._current_edit_index ,
1560+ cam .backend ,
1561+ cam .index ,
1562+ cam .name ,
1563+ self ._loading_active ,
1564+ self ._preview_active ,
1565+ )
15131566
15141567 # Ensure any existing preview or loader is stopped/canceled
15151568 self ._stop_preview ()
@@ -1536,6 +1589,12 @@ def _start_preview(self) -> None:
15361589
15371590 def _stop_preview (self ) -> None :
15381591 """Stop camera preview and cancel any ongoing loading."""
1592+ LOGGER .info (
1593+ "[Preview] stop requested loading=%s active=%s backend=%s" ,
1594+ self ._loading_active ,
1595+ self ._preview_active ,
1596+ getattr (getattr (self ._preview_backend , "settings" , None ), "backend" , None ),
1597+ )
15391598 # Cancel loader if running
15401599 if self ._loader and self ._loader .isRunning ():
15411600 self ._loader .request_cancel ()
@@ -1548,6 +1607,7 @@ def _stop_preview(self) -> None:
15481607 # Close backend
15491608 if self ._preview_backend :
15501609 try :
1610+ LOGGER .debug ("[Preview] closing backend object=%r" , self ._preview_backend )
15511611 self ._preview_backend .close ()
15521612 except Exception :
15531613 pass
@@ -1610,6 +1670,12 @@ def _on_loader_success(self, payload) -> None:
16101670 if isinstance (payload , CameraSettings ):
16111671 cam_settings = payload
16121672 self ._append_status ("Opening camera…" )
1673+ LOGGER .debug (
1674+ "[Loader] success -> opening camera backend=%s idx=%s props_keys=%s" ,
1675+ cam_settings .backend ,
1676+ cam_settings .index ,
1677+ list (cam_settings .properties .keys ()) if isinstance (cam_settings .properties , dict ) else None ,
1678+ )
16131679 self ._preview_backend = CameraFactory .create (cam_settings )
16141680 self ._preview_backend .open ()
16151681
@@ -1666,7 +1732,7 @@ def _on_loader_success(self, payload) -> None:
16661732
16671733 def _on_loader_error (self , error : str ) -> None :
16681734 self ._append_status (f"Error: { error } " )
1669- LOGGER .exception ( "Failed to start preview" )
1735+ LOGGER .error ( "[Loader] error: %s" , error , exc_info = True )
16701736 self ._preview_active = False
16711737 self ._loading_active = False
16721738 self ._hide_loading_overlay ()
0 commit comments