diff --git a/config/default.yaml b/config/default.yaml index df2f235..34f19ea 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -14,12 +14,13 @@ input: type: camera camera_id: 0 video_path: null - width: 1280 - height: 720 + width: 1920 + height: 1080 fps: 30 backend: auto horizontal_flip: false max_consecutive_read_failures: 5 + fourcc: MJPG # camera pixel format; "" = driver default. MJPG is needed for 1080p@30 on many webcams detector: model_path: yolo11n.pt device: auto diff --git a/config/experiment.yaml b/config/experiment.yaml index 69a0634..dc51c6f 100644 --- a/config/experiment.yaml +++ b/config/experiment.yaml @@ -6,8 +6,8 @@ experiment: input: type: camera camera_id: 0 - width: 1280 - height: 720 + width: 1920 + height: 1080 fps: 30 backend: auto horizontal_flip: false diff --git a/src/camera.py b/src/camera.py index 3347191..cd00989 100644 --- a/src/camera.py +++ b/src/camera.py @@ -1,5 +1,6 @@ """OpenCV camera and video input abstraction.""" +import logging from pathlib import Path import cv2 @@ -8,6 +9,8 @@ import numpy as np from .config_loader import InputSettings from .exceptions import CameraOpenError, FrameReadError +logger = logging.getLogger(__name__) + class Camera: """Read frames from a camera device or video file.""" @@ -32,9 +35,24 @@ class Camera: self.release() raise CameraOpenError(f"Could not open {self.config.type} input: {source}") if self.config.type == "camera": + # The pixel format must be set before the resolution: many webcams only offer + # Full HD at a usable frame rate in MJPG, and fall back to a lower mode otherwise. + if self.config.fourcc: + self._capture.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*self.config.fourcc)) self._capture.set(cv2.CAP_PROP_FRAME_WIDTH, self.config.width) self._capture.set(cv2.CAP_PROP_FRAME_HEIGHT, self.config.height) self._capture.set(cv2.CAP_PROP_FPS, self.config.fps) + actual_w, actual_h = self.get_actual_width(), self.get_actual_height() + logger.info( + "Camera opened: requested %dx%d@%.1f fourcc=%s, actual %dx%d@%.1f", + self.config.width, self.config.height, self.config.fps, self.config.fourcc or "default", + actual_w, actual_h, self.get_actual_fps(), + ) + if (actual_w, actual_h) != (self.config.width, self.config.height): + logger.warning( + "Camera resolution differs from the request: requested %dx%d, got %dx%d", + self.config.width, self.config.height, actual_w, actual_h, + ) def read(self) -> np.ndarray | None: if self._capture is None or not self._capture.isOpened(): diff --git a/src/config_loader.py b/src/config_loader.py index d954cdc..78ab96f 100644 --- a/src/config_loader.py +++ b/src/config_loader.py @@ -42,6 +42,8 @@ class InputSettings: backend: str horizontal_flip: bool max_consecutive_read_failures: int + # Four-character camera pixel format (e.g. "MJPG"); empty string leaves the driver default. + fourcc: str = "" @dataclass(frozen=True, slots=True) @@ -539,6 +541,8 @@ def _validate(config: AppConfig) -> None: raise ConfigurationError("input dimensions, fps, and read failure limit must be positive") if i.backend not in {"auto", "dshow", "msmf", "v4l2"}: raise ConfigurationError("input.backend must be auto, dshow, msmf, or v4l2") + if not isinstance(i.fourcc, str) or (i.fourcc and len(i.fourcc) != 4): + raise ConfigurationError("input.fourcc must be empty or exactly 4 characters (e.g. MJPG)") if not 0.0 <= d.confidence_threshold <= 1.0 or not 0.0 <= d.iou_threshold <= 1.0: raise ConfigurationError("detector confidence and IoU thresholds must be in [0, 1]") if d.image_size <= 0 or d.max_detections <= 0 or d.person_class_id < 0: