Skip to content

Commit 1ea3c3b

Browse files
committed
Refactor preview restart logic, add logging
Move camera probe options out of commented code and explicitly disable fast_start and apply_transforms during preview probing to avoid double transforms (Basler) and ensure a full open when probing. Replace _needs_preview_reopen with _should_restart_preview and implement a backend-agnostic policy: restart preview only for camera-side capture parameter changes (width, height, fps, exposure, gain) and do not restart for rotation/crop to provide a faster UX. Improve _apply_camera_settings by preventing applies while a loader is active, computing/logging a pre-apply diff, persisting the validated model, and then deciding whether to restart the preview; use a short QTimer delay for driver stability when restarting. Add debug/info logging around preview start/stop, loader success, backend close, and more robust error logging for loader failures. Small cleanup of exception handling and safer restart decision behavior.
1 parent 8efc257 commit 1ea3c3b

1 file changed

Lines changed: 115 additions & 49 deletions

File tree

dlclivegui/gui/camera_config_dialog.py

Lines changed: 115 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)