Added continous hand tracking

This commit is contained in:
DuOtto
2026-08-02 16:37:29 +02:00
parent db5d09e156
commit 936d8e0531
8 changed files with 74 additions and 37 deletions
+4 -4
View File
@@ -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
# ------------------------------------------------------------
+30 -13
View File
@@ -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
Binary file not shown.
+40 -20
View File
@@ -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()
Binary file not shown.