Added continous hand tracking
This commit is contained in:
@@ -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
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
|
|||||||
@@ -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.
Binary file not shown.
@@ -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.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user