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 import numpy as np
# ---- Board parameters (CHANGE THESE ONLY IF YOU REPRINT) ---- # ---- Board parameters (CHANGE THESE ONLY IF YOU REPRINT) ----
squares_x = 6 # number of chessboard squares in X squares_x = 4 # number of chessboard squares in X
squares_y = 9 # number of chessboard squares in Y squares_y = 5 # number of chessboard squares in Y
square_length = 25 # mm square_length = 50 # mm
marker_length = 18 # mm (must be < square_length) marker_length = 36 # mm (must be < square_length)
dictionary_id = cv2.aruco.DICT_4X4_50 dictionary_id = cv2.aruco.DICT_4X4_50
dpi = 300 # print DPI dpi = 300 # print DPI
# ------------------------------------------------------------ # ------------------------------------------------------------
+24 -7
View File
@@ -32,18 +32,25 @@ class TrackingController(QObject):
# MediaPipe's legacy solutions.hands API no longer exists on the # MediaPipe's legacy solutions.hands API no longer exists on the
# installed mediapipe version, so hand detection runs on the newer # installed mediapipe version, so hand detection runs on the newer
# Tasks API instead (IMAGE mode: each camera frame is detected # Tasks API instead. Each camera gets its own HandLandmarker running
# independently, since frames from different cameras aren't a single # in VIDEO mode, fed only that camera's own frames with a monotonically
# monotonic video stream that VIDEO mode requires). # 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( options = HandLandmarkerOptions(
base_options=BaseOptions(model_asset_path=MODEL_PATH), base_options=BaseOptions(model_asset_path=MODEL_PATH),
running_mode=RunningMode.IMAGE, running_mode=RunningMode.VIDEO,
num_hands=4, num_hands=12,
min_hand_detection_confidence=0.5, min_hand_detection_confidence=0.5,
min_hand_presence_confidence=0.5, min_hand_presence_confidence=0.5,
min_tracking_confidence=0.5, min_tracking_confidence=0.5,
) )
self.detector = HandLandmarker.create_from_options(options) self.detectors[camera.index] = HandLandmarker.create_from_options(options)
def start(self): def start(self):
# Guard: need projections # Guard: need projections
@@ -72,6 +79,9 @@ class TrackingController(QObject):
for camera in self.cameraList: for camera in self.cameraList:
camera.close() camera.close()
for detector in self.detectors.values():
detector.close()
if self.logger: self.logger.log("[Tracking] Stopped") if self.logger: self.logger.log("[Tracking] Stopped")
def _tick(self): def _tick(self):
@@ -102,6 +112,10 @@ class TrackingController(QObject):
if camera.capture is None: if camera.capture is None:
continue continue
detector = self.detectors.get(camera.index)
if detector is None:
continue
ok, frameBGR = camera.capture.read() ok, frameBGR = camera.capture.read()
if not ok or frameBGR is None: if not ok or frameBGR is None:
continue continue
@@ -110,7 +124,10 @@ class TrackingController(QObject):
frameRGB = cv2.cvtColor(frameBGR, cv2.COLOR_BGR2RGB) frameRGB = cv2.cvtColor(frameBGR, cv2.COLOR_BGR2RGB)
mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=frameRGB) 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: if not result.hand_landmarks:
continue continue
Binary file not shown.
+38 -18
View File
@@ -4,7 +4,7 @@ import os
from Tbd.helper import CameraObject from Tbd.helper import CameraObject
from UI.UILogger import UILogger 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.QtGui import QImage, QPixmap
from PySide6.QtWidgets import QLineEdit from PySide6.QtWidgets import QLineEdit
from PySide6.QtGui import QIntValidator from PySide6.QtGui import QIntValidator
@@ -12,6 +12,35 @@ from PySide6.QtWidgets import QWidget, QLabel, QVBoxLayout, QPushButton, QHBoxLa
from typing import List 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): class SetupPage(QWidget):
@@ -91,32 +120,23 @@ class SetupPage(QWidget):
self.logger.log("Path not found") self.logger.log("Path not found")
indexToAdd = [1, 2, 3, 4, 5, 6] indexToAdd = [1, 2, 3, 4, 5, 6]
#indexToAdd = [0, 3]
# Cameras are verified one at a time and left closed afterwards - this # Cameras are verified one at a time and left closed afterwards - this
# hardware can't sustain many concurrent capture graphs (confirmed via # hardware can't sustain many concurrent capture graphs (confirmed via
# both CAP_DSHOW and a raw DirectShow graph), so callers open a camera # both CAP_DSHOW and a raw DirectShow graph), so callers open a camera
# right before they need it (CameraSetupWidget.Start, calibration capture) # right before they need it (CameraSetupWidget.Start, calibration capture)
# and close it right after, instead of keeping all 6 open simultaneously. # and close it right after, instead of keeping all 6 open simultaneously.
for index in indexToAdd: self.loadBTN.setEnabled(False)
camera = CameraObject(capture=None, index=index) self._loadWorker = LoadCamerasWorker(indexToAdd)
self._loadWorker.logMessage.connect(self.logger.log)
if not camera.open(): self._loadWorker.cameraOpened.connect(self._onCameraLoaded)
self.logger.log(f"cam{index}: failed to open, skipping.") self._loadWorker.finished.connect(lambda: self.loadBTN.setEnabled(True))
continue self._loadWorker.start()
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()
def _onCameraLoaded(self, camera: CameraObject):
self.cameraList.append(camera) self.cameraList.append(camera)
self._cameraList_changed() self._cameraList_changed()
return
def save_setup(self): def save_setup(self):
Binary file not shown.