diff --git a/modules/platform_info.py b/modules/platform_info.py index 64235cf..212db05 100644 --- a/modules/platform_info.py +++ b/modules/platform_info.py @@ -59,7 +59,7 @@ def camera_backends() -> List[Tuple[int, int]]: def accelerator_label() -> str: - if HAS_TORCH_CUDA and HAS_CUDA_PROVIDER: + if HAS_CUDA_PROVIDER: return "CUDA (NVIDIA)" if IS_APPLE_SILICON and HAS_COREML_PROVIDER: return "CoreML (Apple Neural Engine)" diff --git a/modules/ui.py b/modules/ui.py index a7cacf8..c3c672b 100644 --- a/modules/ui.py +++ b/modules/ui.py @@ -1240,7 +1240,11 @@ def create_webcam_preview(camera_index: int): global preview_label, PREVIEW cap = VideoCapturer(camera_index) - if not cap.start(1920, 1080, 60): + # Capture at preview window size — drops USB bandwidth and processing cost + # vs. capturing at 1080p and downscaling. The camera negotiates the closest + # mode it supports, which is usually a clean 16:9 resolution at least this + # size; cv2 downscales the rest on the fly. + if not cap.start(PREVIEW_DEFAULT_WIDTH, PREVIEW_DEFAULT_HEIGHT, 60): update_status("Failed to start camera") return diff --git a/modules/video_capture.py b/modules/video_capture.py index e0e830d..65148ad 100644 --- a/modules/video_capture.py +++ b/modules/video_capture.py @@ -38,19 +38,35 @@ class VideoCapturer: """Initialize and start video capture""" try: if platform.system() == "Windows": - # Windows-specific capture methods. - # MSMF (Media Foundation) is preferred — DirectShow often - # caps at 30fps even when the camera supports 60fps. + # device_index comes from pygrabber.FilterGraph (DirectShow + # enumeration), so open with DSHOW first to preserve mapping. + # MSMF and DirectShow enumerate cameras in different orders, so + # opening MSMF with a DSHOW index silently selects the wrong + # camera. MSMF/ANY remain as fallbacks for cameras DSHOW can't + # open. + # + # Pass codec + resolution + fps as construction params (OpenCV + # 4.6+). DSHOW locks the pixel format at open time and ignores + # later cap.set(CAP_PROP_FOURCC, ...) — without this, DSHOW + # falls back to uncompressed YUYV at 1080p, which is USB- + # bandwidth-limited to ~5 fps. Setting MJPG at construction + # negotiates compressed frames from the first read. + mjpg = cv2.VideoWriter_fourcc(*'MJPG') + open_params = [ + cv2.CAP_PROP_FOURCC, mjpg, + cv2.CAP_PROP_FRAME_WIDTH, width, + cv2.CAP_PROP_FRAME_HEIGHT, height, + cv2.CAP_PROP_FPS, fps, + ] capture_methods = [ - (self.device_index, cv2.CAP_MSMF), # Media Foundation first - (self.device_index, cv2.CAP_DSHOW), # DirectShow fallback + (self.device_index, cv2.CAP_DSHOW), + (self.device_index, cv2.CAP_MSMF), (self.device_index, cv2.CAP_ANY), - (0, cv2.CAP_ANY), ] for dev_id, backend in capture_methods: try: - self.cap = cv2.VideoCapture(dev_id, backend) + self.cap = cv2.VideoCapture(dev_id, backend, open_params) if self.cap.isOpened(): break self.cap.release() @@ -63,14 +79,14 @@ class VideoCapturer: if not self.cap or not self.cap.isOpened(): raise RuntimeError("Failed to open camera") - # Try MJPEG first — avoids USB bandwidth limits with - # uncompressed YUV at high resolutions. Falls back silently - # if the camera/backend doesn't support it. - self.cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*'MJPG')) - # Request desired resolution and frame rate - self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, width) - self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height) - self.cap.set(cv2.CAP_PROP_FPS, fps) + # Belt-and-braces: also set via cap.set() for backends that honor + # post-open changes (MSMF, V4L2). DSHOW ignores these, but the + # construction params above already handled it. + if platform.system() != "Windows": + self.cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*'MJPG')) + self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, width) + self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height) + self.cap.set(cv2.CAP_PROP_FPS, fps) # Read back resolution (usually reliable) self.actual_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))