diff --git a/source/Helpers/ChArUco.py b/source/Helpers/ChArUco.py index 11cda97..fbfd858 100644 --- a/source/Helpers/ChArUco.py +++ b/source/Helpers/ChArUco.py @@ -2,10 +2,10 @@ import cv2 import numpy as np # ---- Board parameters (CHANGE THESE ONLY IF YOU REPRINT) ---- -squares_x = 6 # number of chessboard squares in X -squares_y = 9 # number of chessboard squares in Y -square_length = 25 # mm -marker_length = 18 # mm (must be < square_length) +squares_x = 4 # number of chessboard squares in X +squares_y = 5 # number of chessboard squares in Y +square_length = 50 # mm +marker_length = 36 # mm (must be < square_length) dictionary_id = cv2.aruco.DICT_4X4_50 dpi = 300 # print DPI # ------------------------------------------------------------ diff --git a/source/Tbd/TrackingController.py b/source/Tbd/TrackingController.py index ec15bc9..066a4f9 100644 --- a/source/Tbd/TrackingController.py +++ b/source/Tbd/TrackingController.py @@ -32,18 +32,25 @@ class TrackingController(QObject): # MediaPipe's legacy solutions.hands API no longer exists on the # installed mediapipe version, so hand detection runs on the newer - # Tasks API instead (IMAGE mode: each camera frame is detected - # independently, since frames from different cameras aren't a single - # monotonic video stream that VIDEO mode requires). - options = HandLandmarkerOptions( - base_options=BaseOptions(model_asset_path=MODEL_PATH), - running_mode=RunningMode.IMAGE, - num_hands=4, - min_hand_detection_confidence=0.5, - min_hand_presence_confidence=0.5, - min_tracking_confidence=0.5, - ) - self.detector = HandLandmarker.create_from_options(options) + # Tasks API instead. Each camera gets its own HandLandmarker running + # in VIDEO mode, fed only that camera's own frames with a monotonically + # increasing per-camera timestamp - this lets MediaPipe track hands + # frame-to-frame per view (much less jitter than re-detecting blind + # every frame), which wouldn't be valid if frames from different + # camera viewpoints were interleaved through a single VIDEO-mode + # detector. + self._frame_counter = defaultdict(int) + self.detectors = {} + for camera in self.cameraList: + options = HandLandmarkerOptions( + base_options=BaseOptions(model_asset_path=MODEL_PATH), + running_mode=RunningMode.VIDEO, + num_hands=12, + min_hand_detection_confidence=0.5, + min_hand_presence_confidence=0.5, + min_tracking_confidence=0.5, + ) + self.detectors[camera.index] = HandLandmarker.create_from_options(options) def start(self): # Guard: need projections @@ -72,6 +79,9 @@ class TrackingController(QObject): for camera in self.cameraList: camera.close() + for detector in self.detectors.values(): + detector.close() + if self.logger: self.logger.log("[Tracking] Stopped") def _tick(self): @@ -102,6 +112,10 @@ class TrackingController(QObject): if camera.capture is None: continue + detector = self.detectors.get(camera.index) + if detector is None: + continue + ok, frameBGR = camera.capture.read() if not ok or frameBGR is None: continue @@ -110,7 +124,10 @@ class TrackingController(QObject): frameRGB = cv2.cvtColor(frameBGR, cv2.COLOR_BGR2RGB) mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=frameRGB) - result = self.detector.detect(mp_image) + + self._frame_counter[camera.index] += 1 + timestamp_ms = self._frame_counter[camera.index] * self.tick_ms + result = detector.detect_for_video(mp_image, timestamp_ms) if not result.hand_landmarks: continue diff --git a/source/Tbd/__pycache__/TrackingController.cpython-311.pyc b/source/Tbd/__pycache__/TrackingController.cpython-311.pyc index 1519ae1..b48fa33 100644 Binary files a/source/Tbd/__pycache__/TrackingController.cpython-311.pyc and b/source/Tbd/__pycache__/TrackingController.cpython-311.pyc differ diff --git a/source/Tbd/__pycache__/helper.cpython-311.pyc b/source/Tbd/__pycache__/helper.cpython-311.pyc index 99be3d9..f0cefbf 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/UI/SetupPageWidget.py b/source/UI/SetupPageWidget.py index 5d3d7d7..03acf6c 100644 --- a/source/UI/SetupPageWidget.py +++ b/source/UI/SetupPageWidget.py @@ -4,7 +4,7 @@ import os from Tbd.helper import CameraObject from UI.UILogger import UILogger -from PySide6.QtCore import Qt, QTimer +from PySide6.QtCore import Qt, QTimer, QThread, Signal from PySide6.QtGui import QImage, QPixmap from PySide6.QtWidgets import QLineEdit from PySide6.QtGui import QIntValidator @@ -12,6 +12,35 @@ from PySide6.QtWidgets import QWidget, QLabel, QVBoxLayout, QPushButton, QHBoxLa from typing import List +class LoadCamerasWorker(QThread): + """ + Opens/verifies/closes each camera off the UI thread - camera.open() does + MSMF format negotiation with retries+sleeps, which used to block the Qt + event loop (window shows as "Not Responding") for the whole duration. + """ + cameraOpened = Signal(object) + logMessage = Signal(str) + + def __init__(self, indexToAdd: List[int]): + super().__init__() + self.indexToAdd = indexToAdd + + def run(self): + for index in self.indexToAdd: + camera = CameraObject(capture=None, index=index) + + if not camera.open(): + self.logMessage.emit(f"cam{index}: failed to open, skipping.") + continue + + ret, frame = camera.capture.read() + if ret: + self.logMessage.emit(f"cam{index}: verified, frame shape={frame.shape}") + else: + self.logMessage.emit(f"cam{index}: opened but failed to read a frame.") + + camera.close() + self.cameraOpened.emit(camera) class SetupPage(QWidget): @@ -91,31 +120,22 @@ class SetupPage(QWidget): self.logger.log("Path not found") indexToAdd = [1, 2, 3, 4, 5, 6] - + #indexToAdd = [0, 3] # Cameras are verified one at a time and left closed afterwards - this # hardware can't sustain many concurrent capture graphs (confirmed via # both CAP_DSHOW and a raw DirectShow graph), so callers open a camera # right before they need it (CameraSetupWidget.Start, calibration capture) # and close it right after, instead of keeping all 6 open simultaneously. - for index in indexToAdd: - camera = CameraObject(capture=None, index=index) + self.loadBTN.setEnabled(False) + self._loadWorker = LoadCamerasWorker(indexToAdd) + self._loadWorker.logMessage.connect(self.logger.log) + self._loadWorker.cameraOpened.connect(self._onCameraLoaded) + self._loadWorker.finished.connect(lambda: self.loadBTN.setEnabled(True)) + self._loadWorker.start() - if not camera.open(): - self.logger.log(f"cam{index}: failed to open, skipping.") - continue - - ret, frame = camera.capture.read() - if ret: - self.logger.log(f"cam{index}: verified, frame shape={frame.shape}") - else: - self.logger.log(f"cam{index}: opened but failed to read a frame.") - - camera.close() - - self.cameraList.append(camera) - self._cameraList_changed() - - return + def _onCameraLoaded(self, camera: CameraObject): + self.cameraList.append(camera) + self._cameraList_changed() diff --git a/source/UI/__pycache__/CameraSetupWidget.cpython-311.pyc b/source/UI/__pycache__/CameraSetupWidget.cpython-311.pyc index 79c8714..274e272 100644 Binary files a/source/UI/__pycache__/CameraSetupWidget.cpython-311.pyc and b/source/UI/__pycache__/CameraSetupWidget.cpython-311.pyc differ diff --git a/source/UI/__pycache__/HomePageWidget.cpython-311.pyc b/source/UI/__pycache__/HomePageWidget.cpython-311.pyc index 276a63f..1329fb9 100644 Binary files a/source/UI/__pycache__/HomePageWidget.cpython-311.pyc and b/source/UI/__pycache__/HomePageWidget.cpython-311.pyc differ diff --git a/source/UI/__pycache__/SetupPageWidget.cpython-311.pyc b/source/UI/__pycache__/SetupPageWidget.cpython-311.pyc index cb626e3..a3a70e4 100644 Binary files a/source/UI/__pycache__/SetupPageWidget.cpython-311.pyc and b/source/UI/__pycache__/SetupPageWidget.cpython-311.pyc differ