Skip to content

Commit 48faa41

Browse files
authored
Refactor camera config dialog and reduce code duplication (#46)
* Extract camera loaders; refactor preview UI state Move camera worker and preview state logic into a new module (dlclivegui/gui/camera_loaders.py) and refactor the CameraConfigDialog to use it. Added DetectCamerasWorker, CameraProbeWorker, CameraLoadWorker, PreviewState enum and PreviewSession dataclass to centralize loader/backend/timer intent. Removed the embedded worker classes from camera_config_dialog.py and replaced multiple booleans/flags with a single PreviewSession, an epoch-based signal invalidation mechanism, coalesced preview restarts, and unified scan/preview UI syncing. Also adjusted loader signal handlers to be epoch-aware, simplified start/stop flows, and made small docstring/comment tweaks in basler_backend.py regarding fast_start. * Refactor camera config into package Move camera configuration code into a dedicated gui/camera_config package and extract UI construction into a new ui_blocks module. Renamed camera_config_dialog.py and camera_loaders.py to camera_config/camera_config_dialog.py and camera_config/loaders.py respectively, replaced the large _setup_ui implementation with setup_camera_config_dialog_ui(dlg) from ui_blocks.py, and added ui_blocks.py to build the dialog UI. Updated relative imports in __init__.py, main_window.py, and affected modules/tests to the new paths. This refactor improves modularity and keeps the dialog file focused on logic rather than bulky widget construction. * Refactor camera config dialog & add identity utils Move camera identity utilities into cameras.factory (apply_detected_identity, camera_identity_key) and add CameraSettings.check_diff for concise settings diffs. Major refactor of CameraConfigDialog: reorganize UI/state helpers, probe/preview lifecycle, auto-apply pending edits, improved scan/probe cancellation and dialog reject handling, duplicate-camera checks, add reorder/add/remove safety, and split preview helpers into a new preview module; also remove unused cv2 import. These changes centralize identity handling, improve preview & probe UX, and add safer settings application and logging. Tests updated to match the new contracts. * Centralize camera dialog cleanup and fixes Add a unified _on_close_cleanup and closeEvent to ensure preview/worker shutdown and UI reset on dialog close/cancel. Make cleanup idempotent with a _cleanup_done guard, shorten worker wait times to reduce UI freeze, and defensively reset scan UI widgets. Connect scan cancel button handler. Initialize _multi_camera_settings fallback, tighten type annotations, and note eventFilter UI assignments. Refactor form/apply flow to build a new CameraSettings model, compute diffs, replace the working camera entry and update the active list item (apply now returns a truthy value). Call _reconcile_fps_from_backend when loading settings, remove exc_info from a loader error log, and invoke cleanup before accepting the dialog. These changes improve robustness during shutdown and reduce UI hangs. * Refactor preview helpers into controller Move preview state and session datatypes into preview.py and refactor low-level image ops into MultiCameraController static methods. preview.py now delegates rotation, crop, resize and pixmap conversion to MultiCameraController (with proper type hints and QTimer usage). loaders.py removes duplicate PreviewState/PreviewSession definitions and cleans imports. camera_config_dialog uses getattr to safely read backend.actual_fps and updates imports to match the refactor. MultiCameraController gains apply_rotation/apply_crop/apply_resize/ensure_color_* and to_display_pixmap utilities (default: no upscale when resizing). * Update tests to use PreviewState and dialog._preview Adapt camera config GUI tests to the refactored preview API: import PreviewState and replace checks against legacy dialog attributes (_loader, _preview_active, _preview_backend, _preview_timer) with dialog._preview.loader, dialog._preview.state (using PreviewState.*), dialog._preview.backend, and dialog._preview.timer. Update waitUntil conditions and assertions across test_cam_dialog_e2e.py to reflect the new preview state machine and object structure.
1 parent be27cc4 commit 48faa41

File tree

13 files changed

+1703
-1376
lines changed

13 files changed

+1703
-1376
lines changed

dlclivegui/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
MultiCameraSettings,
88
RecordingSettings,
99
)
10-
from .gui.camera_config_dialog import CameraConfigDialog
10+
from .gui.camera_config.camera_config_dialog import CameraConfigDialog
1111
from .gui.main_window import DLCLiveMainWindow
1212
from .main import main
1313
from .services.multi_camera_controller import MultiCameraController, MultiFrameData

dlclivegui/cameras/backends/basler_backend.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ def __init__(self, settings):
3030

3131
self._props: dict = settings.properties if isinstance(settings.properties, dict) else {}
3232

33-
# Optional fast-start hint for probe workers (best-effort; doesn't change behavior yet)
33+
# Optional fast-start hint for probe workers
34+
# (may skip StartGrabbing and converter setup for faster capability probing; not suitable for normal capture)
3435
self._fast_start: bool = bool(self.ns.get("fast_start", False))
3536

3637
# Stable identity (serial-based). Prefer new namespace; fall back to legacy keys read-only.

dlclivegui/cameras/factory.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,3 +415,49 @@ def _resolve_backend(name: str) -> type[CameraBackend]:
415415
"Tip: enable strict import failures with DLC_CAMERA_BACKENDS_STRICT_IMPORT=1"
416416
)
417417
raise RuntimeError(msg) from exc
418+
419+
420+
# -------------------------------
421+
# Camera identity utilities
422+
# -------------------------------
423+
424+
425+
def apply_detected_identity(cam: CameraSettings, detected: DetectedCamera, backend: str) -> None:
426+
"""Persist stable identity from a detected camera into cam.properties under backend namespace."""
427+
if not isinstance(cam.properties, dict):
428+
cam.properties = {}
429+
430+
ns = cam.properties.get(backend.lower())
431+
if not isinstance(ns, dict):
432+
ns = {}
433+
cam.properties[backend.lower()] = ns
434+
435+
# Store whatever we have (backend-specific but written generically)
436+
if getattr(detected, "device_id", None):
437+
ns["device_id"] = detected.device_id
438+
if getattr(detected, "vid", None) is not None:
439+
ns["device_vid"] = int(detected.vid)
440+
if getattr(detected, "pid", None) is not None:
441+
ns["device_pid"] = int(detected.pid)
442+
if getattr(detected, "path", None):
443+
ns["device_path"] = detected.path
444+
445+
# Optional: store human name for matching fallback
446+
if getattr(detected, "label", None):
447+
ns["device_name"] = detected.label
448+
449+
# Optional: store backend_hint if you expose it (e.g., CAP_DSHOW)
450+
if getattr(detected, "backend_hint", None) is not None:
451+
ns["backend_hint"] = int(detected.backend_hint)
452+
453+
454+
def camera_identity_key(cam: CameraSettings) -> tuple:
455+
backend = (cam.backend or "").lower()
456+
props = cam.properties if isinstance(cam.properties, dict) else {}
457+
ns = props.get(backend, {}) if isinstance(props, dict) else {}
458+
device_id = ns.get("device_id")
459+
460+
# Prefer stable identity if present, otherwise fallback
461+
if device_id:
462+
return (backend, "device_id", device_id)
463+
return (backend, "index", int(cam.index))

dlclivegui/config.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,32 @@ def apply_defaults(self) -> CameraSettings:
140140

141141
return self
142142

143+
@staticmethod
144+
def check_diff(old: CameraSettings, new: CameraSettings) -> dict:
145+
keys = (
146+
"width",
147+
"height",
148+
"fps",
149+
"exposure",
150+
"gain",
151+
"rotation",
152+
"crop_x0",
153+
"crop_y0",
154+
"crop_x1",
155+
"crop_y1",
156+
"enabled",
157+
)
158+
out = {}
159+
for k in keys:
160+
try:
161+
ov = getattr(old, k, None)
162+
nv = getattr(new, k, None)
163+
if ov != nv:
164+
out[k] = (ov, nv)
165+
except Exception:
166+
pass
167+
return out
168+
143169

144170
class MultiCameraSettings(BaseModel):
145171
cameras: list[CameraSettings] = Field(default_factory=list)

0 commit comments

Comments
 (0)