diff --git a/Test/Gestures/gesture_config.json b/Test/Gestures/gesture_config.json index 6f0967b..1191e17 100644 --- a/Test/Gestures/gesture_config.json +++ b/Test/Gestures/gesture_config.json @@ -5,6 +5,12 @@ "left": "PointIndex", "right": "", "extensions": [] + }, + { + "action": "Pointer", + "left": "", + "right": "PointIndex", + "extensions": [] } ] } \ No newline at end of file diff --git a/source/Tbd/__pycache__/helper.cpython-311.pyc b/source/Tbd/__pycache__/helper.cpython-311.pyc index 8f81ba7..99be3d9 100644 Binary files a/source/Tbd/__pycache__/helper.cpython-311.pyc and b/source/Tbd/__pycache__/helper.cpython-311.pyc differ diff --git a/source/Tbd/helper.py b/source/Tbd/helper.py index 8140839..9cce31a 100644 --- a/source/Tbd/helper.py +++ b/source/Tbd/helper.py @@ -1,10 +1,92 @@ import numpy as np import json import cv2 +import threading from dataclasses import dataclass, field, asdict from typing import List, Optional, Tuple, Dict +from pygrabber.dshow_graph import FilterGraph + + +class DShowMJPGCapture: + """ + cv2.VideoCapture-like wrapper (read/release/isOpened) that selects an exact + DirectShow media type (e.g. MJPG at a given resolution) before capturing. + + This exists because cv2.VideoCapture(..., cv2.CAP_DSHOW).set(CAP_PROP_FOURCC, ...) + is unreliable on some webcams: OpenCV opens its own separate DirectShow filter + graph, which renegotiates its own format independently of anything set through + pygrabber beforehand. Doing format selection AND frame grabbing in the same + graph (via pygrabber's sample grabber) avoids that. + """ + + def __init__(self, index: int, width: int, height: int, + fourcc_substr: str = "MJPG", timeout: float = 2.0): + self.index = index + self.width = width + self.height = height + self.selected_format = None + self._opened = False + self._latest_frame = None + self._frame_event = threading.Event() + self._timeout = timeout + + self._graph = FilterGraph() + self._graph.add_video_input_device(index) + + device = self._graph.get_input_device() + formats = device.get_formats() + match = next( + (f for f in formats + if f["width"] == width and f["height"] == height + and fourcc_substr.upper() in f["media_type_str"].upper()), + None, + ) + if match is None: + raise RuntimeError( + f"No {fourcc_substr} format at {width}x{height} available on camera {index}. " + f"Available formats: {formats}" + ) + device.set_format(match["index"]) + self.selected_format = match + + self._graph.add_sample_grabber(self._on_frame) + self._graph.add_null_render() + self._graph.prepare_preview_graph() + self._graph.run() + self._opened = True + + def _on_frame(self, frame): + self._latest_frame = frame + self._frame_event.set() + + def isOpened(self) -> bool: + return self._opened + + def read(self): + if not self._opened: + return False, None + self._frame_event.clear() + self._graph.grab_frame() + if not self._frame_event.wait(self._timeout): + return False, None + return True, self._latest_frame + + def release(self): + if self._opened: + self._graph.stop() + self._graph.remove_filters() + self._opened = False + + # No-ops for compatibility with code paths that still call .set()/.get() + # on the capture object (real config happens via device.set_format above). + def set(self, prop_id, value): + return False + + def get(self, prop_id): + return 0.0 + # ----- Helper Functions ----- def compute_distance(p1, p2): diff --git a/source/UI/HomePageWidget.py b/source/UI/HomePageWidget.py index 2709dfb..72565e9 100644 --- a/source/UI/HomePageWidget.py +++ b/source/UI/HomePageWidget.py @@ -436,25 +436,25 @@ class HomeWindow(QWidget): for camera in self.cameraList: # Loop all Cameras ok, frame = camera.capture.read() #Capture a frame - if not ok: # Check if captured frame is valid, if even one frame is invalide the whole capture failed + if not ok: # A single failed grab shouldn't discard what other cameras saw self.logger.log(f"Could not grab frame for calibration sample, from camera {camera.index}.") - return - - h, w = frame.shape[:2] + continue + + h, w = frame.shape[:2] self.logger.log(f"cam{camera.index} capture size = {w}x{h}") gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # Turn the frame black and white gray = 255 - gray # Invert the color of the frame, as the ChArCuo board I use is inverted (to save printer Ink) - h, w = gray.shape[:2] + h, w = gray.shape[:2] self.logger.log(f"cam{camera.index} capture size = {w}x{h}") #This is apparently a fast check, bevor i detect the actuall board corner, ids, _ = self.arcuo_detector.detectMarkers(gray) #Detect ChArCuo Markers - if ids is None or len(ids) == 0: #Check if Markers where found, if none where found the sample is invalid + if ids is None or len(ids) == 0: #Board not visible from this camera this time, skip it, not the whole sample self.logger.log(f"Not enought markers found for calibration sample, from camera {camera.index}.") - return - + continue + # Detecting the Charcuo board and evaluating if its good as a sample result = self.charuco_detector.detectBoard(gray) @@ -463,12 +463,12 @@ class HomeWindow(QWidget): if charuco_ids is None: self.logger.log(f"Not enought markers found for calibration sample, from camera {camera.index}.") - return - + continue + if len(charuco_ids) < MIN_CHARUCO_CORNERS: self.logger.log(f"Not enought markers found for calibration sample, from camera {camera.index}.") - return - + continue + # Adding the sample for the current camera to detections detections[camera.index] = CharucoDetection( camera_id = camera.index, @@ -477,12 +477,18 @@ class HomeWindow(QWidget): gray_frame = gray ) - # All cameras found valide markers so the sample capture was succesfull + # Need at least two cameras' worth of detections for this sample to be + # useful for stereo pairing (a single-camera detection can still help + # that camera's intrinsics, but can't contribute to any camera pair). + if len(detections) < 2: + self.logger.log("Not enough cameras saw the board in this sample, discarding.") + return + self.calibration_samples.append( CalibrationSample(detections=detections) ) - self.logger.log("Added calibration sample.") + self.logger.log(f"Added calibration sample ({len(detections)}/{len(self.cameraList)} cameras saw the board).") def updateCameras(self): self._clearLayout() diff --git a/source/UI/SetupPageWidget.py b/source/UI/SetupPageWidget.py index 0de989e..065cb5a 100644 --- a/source/UI/SetupPageWidget.py +++ b/source/UI/SetupPageWidget.py @@ -1,8 +1,7 @@ import cv2, numpy as np import os - -from Tbd.helper import CameraObject +from Tbd.helper import CameraObject, DShowMJPGCapture from UI.UILogger import UILogger from PySide6.QtCore import Qt, QTimer @@ -98,31 +97,24 @@ class SetupPage(QWidget): # index=None #) for index in indexToAdd: - capture = cv2.VideoCapture(index, cv2.CAP_MSMF) - #capture = cv2.VideoCapture(index, cv2.CAP_DSHOW) + try: + capture = DShowMJPGCapture(index, 2592, 1944, fourcc_substr="MJPG") + except RuntimeError as e: + self.logger.log(str(e)) + print(str(e)) + continue - capture.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*'MJPG')) - capture.set(cv2.CAP_PROP_FPS, 30) - capture.set(cv2.CAP_PROP_FRAME_WIDTH, 2592) - capture.set(cv2.CAP_PROP_FRAME_HEIGHT, 1944) + self.logger.log(f"Selected format: {capture.selected_format}") + print(f"Selected format: {capture.selected_format}") - #capture.set(cv2.CAP_PROP_FRAME_WIDTH, 2048.0) - #capture.set(cv2.CAP_PROP_FRAME_HEIGHT, 1536.0) - width = capture.get(cv2.CAP_PROP_FRAME_WIDTH) - height = capture.get(cv2.CAP_PROP_FRAME_HEIGHT) - fps = capture.get(cv2.CAP_PROP_FPS) + ret, frame = capture.read() + if ret: + self.logger.log(f"Actual frame shape: {frame.shape}") + print(f"Actual frame shape: {frame.shape}") + else: + self.logger.log("Failed to read a frame after configuring camera") + print("Failed to read a frame after configuring camera") - fourcc = int(capture.get(cv2.CAP_PROP_FOURCC)) - fourcc_str = "".join([chr((fourcc >> 8*i) & 0xFF) for i in range(4)]) - - print(f"Resolution: {width}x{height}") - print(f"FPS: {fps}") - print(f"Format (FOURCC): {fourcc_str}") - self.logger.log(f"Resolution: {width}x{height}") - self.logger.log(f"FPS: {fps}") - self.logger.log(f"Format (FOURCC): {fourcc_str}") - #currentcapture = cv2.VideoCapture(index, cv2.CAP_MSMF) - #self.logger.log("Initial frame:", w, "x", h) self.cameraList.append(CameraObject( capture=capture, @@ -185,7 +177,7 @@ class SetupPage(QWidget): index = int(self.cameraIndex.text()) self.release_camera() - self.cameraObject.capture = cv2.VideoCapture(index) + self.cameraObject.capture = cv2.VideoCapture(index, cv2.CAP_DSHOW) self.cameraObject.index = index if not self.cameraObject.capture.isOpened(): diff --git a/source/UI/__pycache__/SetupPageWidget.cpython-311.pyc b/source/UI/__pycache__/SetupPageWidget.cpython-311.pyc index e55ab62..cb626e3 100644 Binary files a/source/UI/__pycache__/SetupPageWidget.cpython-311.pyc and b/source/UI/__pycache__/SetupPageWidget.cpython-311.pyc differ