-
Notifications
You must be signed in to change notification settings - Fork 27
Upgrade & fix GenTL multi-camera mode #76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
f360b73
971f629
60b7607
4032402
b2e53dc
ab2598e
95c8647
f2d0cc7
02bcbb9
92dd624
5f56a5f
9b77c42
90241f6
760825f
2e54469
0af5615
ed1a9f5
0d5e1dc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
|
|
||
| from __future__ import annotations | ||
|
|
||
| import copy | ||
| import logging | ||
| import time | ||
| from dataclasses import dataclass | ||
|
|
@@ -14,6 +15,7 @@ | |
|
|
||
| from dlclivegui.cameras import CameraFactory | ||
| from dlclivegui.cameras.base import CameraBackend | ||
| from dlclivegui.cameras.factory import camera_identity_key | ||
|
|
||
| # from dlclivegui.config import CameraSettings | ||
| from dlclivegui.config import CameraSettings | ||
|
|
@@ -29,6 +31,7 @@ class MultiFrameData: | |
| timestamps: dict[str, float] # camera_id -> timestamp | ||
| source_camera_id: str = "" # ID of camera that triggered this emission | ||
| tiled_frame: np.ndarray | None = None # Combined tiled frame (deprecated, done in GUI) | ||
| display_ids: dict[str, str] = None # camera_id -> display_id (for labeling) | ||
|
|
||
|
|
||
| class SingleCameraWorker(QObject): | ||
|
|
@@ -42,7 +45,7 @@ class SingleCameraWorker(QObject): | |
| def __init__(self, camera_id: str, settings: CameraSettings): | ||
| super().__init__() | ||
| self._camera_id = camera_id | ||
| self._settings = settings | ||
| self._settings = copy.deepcopy(settings) | ||
| self._stop_event = Event() | ||
| self._backend: CameraBackend | None = None | ||
| self._max_consecutive_errors = 5 | ||
|
|
@@ -53,7 +56,24 @@ def run(self) -> None: | |
| self._stop_event.clear() | ||
|
|
||
| try: | ||
| LOGGER.debug( | ||
| "[Worker %s] before create: backend=%s index=%s properties=%s", | ||
| self._camera_id, | ||
| self._settings.backend, | ||
| self._settings.index, | ||
| self._settings.properties, | ||
| ) | ||
|
|
||
| self._backend = CameraFactory.create(self._settings) | ||
|
|
||
| LOGGER.debug( | ||
| "[Worker %s] after create: backend=%s index=%s properties=%s", | ||
| self._camera_id, | ||
| self._backend.settings.backend, | ||
| self._backend.settings.index, | ||
| self._backend.settings.properties, | ||
| ) | ||
|
|
||
| self._backend.open() | ||
| except Exception as exc: | ||
| LOGGER.exception(f"Failed to initialize camera {self._camera_id}", exc_info=exc) | ||
|
|
@@ -102,11 +122,27 @@ def stop(self) -> None: | |
| self._stop_event.set() | ||
|
|
||
|
|
||
| def get_camera_id(settings: CameraSettings) -> str: | ||
| """Generate a unique camera ID from settings.""" | ||
| def get_display_id(settings: CameraSettings) -> str: | ||
| return f"{settings.backend}:{settings.index}" | ||
|
|
||
|
|
||
| def get_camera_id(settings: CameraSettings) -> str: | ||
| """Generate a unique camera ID from stable backend identity.""" | ||
| backend = (settings.backend or "").lower() | ||
| props = settings.properties if isinstance(settings.properties, dict) else {} | ||
| ns = props.get(backend, {}) if isinstance(props.get(backend), dict) else {} | ||
|
|
||
| device_id = ns.get("device_id") | ||
| if device_id: | ||
| return f"{backend}:{device_id}" | ||
|
|
||
| serial = ns.get("serial_number") or ns.get("device_serial_number") or ns.get("serial") | ||
| if serial: | ||
| return f"{backend}:serial:{serial}" | ||
|
|
||
| return f"{backend}:index:{int(settings.index)}" | ||
|
|
||
|
|
||
| class MultiCameraController(QObject): | ||
| """Controller for managing multiple cameras simultaneously.""" | ||
|
|
||
|
|
@@ -131,6 +167,7 @@ def __init__(self): | |
| self._frame_lock = Lock() | ||
| self._running = False | ||
| self._started_cameras: set = set() | ||
| self._display_ids: dict[str, str] = {} # camera_id -> display_id (for labeling) | ||
| self._failed_cameras: dict[str, str] = {} # camera_id -> error message | ||
| self._expected_cameras: int = 0 # Number of cameras we're trying to start | ||
|
|
||
|
|
@@ -153,25 +190,58 @@ def start(self, camera_settings: list[CameraSettings]) -> None: | |
| LOGGER.warning("No active cameras to start") | ||
| return | ||
|
|
||
| # Check for dupes | ||
| seen = {} | ||
| for s in active_settings: | ||
| camera_id = get_camera_id(s) | ||
| try: | ||
| key = camera_identity_key(s) | ||
| except Exception: | ||
| LOGGER.exception( | ||
| "Failed to compute camera identity key for %s; falling back to camera_id", | ||
| camera_id, | ||
| ) | ||
| key = camera_id | ||
|
|
||
| if key in seen: | ||
| self.initialization_failed.emit( | ||
| [ | ||
| ( | ||
| camera_id, | ||
| f"Duplicate camera configuration. Conflicts with {seen[key]}", | ||
| ) | ||
| ] | ||
| ) | ||
| return | ||
|
|
||
| seen[key] = camera_id | ||
|
|
||
|
C-Achard marked this conversation as resolved.
|
||
| self._running = True | ||
| self._frames.clear() | ||
| self._timestamps.clear() | ||
| self._started_cameras.clear() | ||
| self._failed_cameras.clear() | ||
| self._display_ids.clear() | ||
| self._expected_cameras = len(active_settings) | ||
|
|
||
| for settings in active_settings: | ||
| self._start_camera(settings) | ||
|
|
||
| def _start_camera(self, settings: CameraSettings) -> None: | ||
| """Start a single camera.""" | ||
| cam_id = get_camera_id(settings) | ||
| settings_copy = copy.deepcopy(settings) | ||
| cam_id = get_camera_id(settings_copy) | ||
| display_id = get_display_id(settings_copy) | ||
|
|
||
| if cam_id in self._workers: | ||
| LOGGER.warning(f"Camera {cam_id} already has a worker") | ||
| return | ||
|
Comment on lines
230
to
238
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same affects
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, indeed it seems I sort of did this halfway, but the true intent was really only to use display_id for the GUI and keep everything internal with the more stable ID. |
||
|
|
||
| LOGGER.info(f"[MultiCameraController] Starting {cam_id} with settings: {settings_copy}") | ||
|
|
||
| # Normalize and store the dataclass once | ||
| self._settings[cam_id] = settings | ||
| self._settings[cam_id] = settings_copy | ||
| self._display_ids[cam_id] = display_id | ||
| dc = self._settings[cam_id] | ||
| worker = SingleCameraWorker(cam_id, dc) | ||
| thread = QThread() | ||
|
|
@@ -211,6 +281,7 @@ def stop(self, wait: bool = True) -> None: | |
| self._settings.clear() | ||
| self._started_cameras.clear() | ||
| self._failed_cameras.clear() | ||
| self._display_ids.clear() | ||
| self._expected_cameras = 0 | ||
| self.all_stopped.emit() | ||
|
|
||
|
|
@@ -238,6 +309,7 @@ def _on_frame_captured(self, camera_id: str, frame: np.ndarray, timestamp: float | |
| timestamps=dict(self._timestamps), | ||
| source_camera_id=camera_id, # Track which camera triggered this | ||
| tiled_frame=None, | ||
| display_ids=dict(self._display_ids), | ||
| ) | ||
| self.frame_ready.emit(frame_data) | ||
|
|
||
|
|
@@ -425,6 +497,7 @@ def _on_camera_stopped(self, camera_id: str) -> None: | |
| # Check if this camera never started (initialization failure) | ||
| was_started = camera_id in self._started_cameras | ||
| self._started_cameras.discard(camera_id) | ||
| self._display_ids.pop(camera_id, None) | ||
| self.camera_stopped.emit(camera_id) | ||
| LOGGER.info(f"Camera {camera_id} stopped (was_started={was_started})") | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.