feat(pose): MediaPipe Multi default + rich OSC
User demande detection mesh visage + main + interaction
sonore plus riche + openpos toujours actif. Pipeline :
main.py priorite pose worker reordonnee :
0. Multi-HMR (flag opt-in)
1. MediaPipe Multi (DEFAUT) : 33 body + 478 face + 21x2 hands
x 4 personnes. Active la couche mesh_pipe pour rendre face
triangulee + hands triangulees + body skeleton ensemble.
2. Apple Vision (fallback body-only ANE).
main.py auto-engage openpos #9 quand persons_body non vide via
un NSTimer 2 Hz autoOpenpos. Lock manuel 8s quand l'utilisateur
appuie sur azertyuiop. Revient en storm (#0) quand plus
aucune personne detectee.
pose_bridge.py enrichi :
/pose/wrist_speed pid l|r vx vy (velocity normalisee)
/pose/torso_yaw pid yaw (rotation epaules)
/pose/body_pitch pid pitch (inclinaison verticale)
/pose/face pid mouth_open eye_open (FACE_MESH 478)
/pose/hand pid l|r openness px py (HAND 21)
/pose/active pid activity_0_1 (mean conf)
multi.py passe persons_face / persons_hands au PoseSoundBridge.
This commit is contained in:
+60
-27
@@ -182,6 +182,13 @@ class AppDelegate(NSObject):
|
||||
self._camTimer = NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(
|
||||
1.0 / 15.0, self, "refreshCam:", None, True)
|
||||
|
||||
# 2d) Auto-engage du mode openpos (#9) quand des personnes sont
|
||||
# detectees. Si l'utilisateur a force un mode au clavier dans
|
||||
# les 8 dernieres secondes, on n'override pas (lock manuel).
|
||||
self._user_viz_lock_t = 0.0 # set par _on_key, lu par autoOpenpos
|
||||
self._autoOpenposTimer = NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(
|
||||
0.5, self, "autoOpenpos:", None, True)
|
||||
|
||||
if self._opts.fullscreen:
|
||||
self._window.toggleFullScreen_(None)
|
||||
|
||||
@@ -232,20 +239,14 @@ class AppDelegate(NSObject):
|
||||
self._start_pose_worker()
|
||||
|
||||
def _start_pose_worker(self):
|
||||
# Priorite :
|
||||
# 0. Apple Vision body pose natif (VNDetectHumanBodyPoseRequest)
|
||||
# — AUCUN modele a charger, ANE-accelerated, multi-personne.
|
||||
# Trade-off : PERD face/hands MediaPipe (conflit camera : un
|
||||
# seul process peut grabber la webcam). Desactive avec
|
||||
# AV_LIVE_APPLE_VISION=0 si on veut le mesh complet MP.
|
||||
# 1. CoreML pose natif (AVFoundation + Vision + YOLO11n-pose sur ANE)
|
||||
# — actif si ~/.cache/av-live-coreml/yolo11n-pose.mlpackage existe
|
||||
# OU si env AV_LIVE_COREML=1 (force le check). Pipeline 100% Apple.
|
||||
# 2. DETRPose (transformer 2025, body 17 kp COCO, multi-personne natif,
|
||||
# MPS) — ACTIF SEULEMENT si AV_LIVE_DETRPOSE=1 et install OK
|
||||
# 3. MediaPipe Multi (Pose+Face+Hand × 4) — defaut, plus complet
|
||||
# 4. Fallback Holistic (single-person, plus rapide mais 1 sujet seul)
|
||||
# 5. Fallback YOLO COCO 17 keypoints
|
||||
# Priorite (user feedback : on veut FACE + HANDS + BODY toujours
|
||||
# disponibles pour le mapping sonore + viz openpos, donc MediaPipe
|
||||
# Multi devient le default avec ses 33 body + 478 face + 42 hand
|
||||
# par personne, plutot qu'Apple Vision body-only 13 joints) :
|
||||
# 0. Multi-HMR (opt-in via --multi-hmr flag)
|
||||
# 1. MediaPipe Multi (33+478+42 kp × 4 personnes) — DEFAUT
|
||||
# 2. Apple Vision body pose (fallback si MediaPipe casse)
|
||||
# 3. CoreML pose, DETRPose, Holistic, YOLO — fallbacks
|
||||
import os as _os
|
||||
# 0. Multi-HMR (SMPL-X 10475 verts mesh dense) — opt-in via flag
|
||||
if getattr(self._opts, "multi_hmr", False):
|
||||
@@ -272,9 +273,20 @@ class AppDelegate(NSObject):
|
||||
"— voir scripts/setup_multihmr.sh")
|
||||
except Exception as e: # noqa: BLE001
|
||||
LOG.warning("Multi-HMR failed (%s) — fallback", e)
|
||||
# Apple Vision body pose : v2 utilise cv2 + Vision via JPEG, plus
|
||||
# de delegate AVCaptureSession (crash dispatch_queue ctypes evite).
|
||||
# Active par defaut. Set AV_LIVE_APPLE_VISION=0 pour fallback MP.
|
||||
# 1. MediaPipe Multi : DEFAUT pour le mapping sonore + openpos
|
||||
# (33 body + 478 face + 21x2 hands × 4 personnes). Skip via
|
||||
# AV_LIVE_MEDIAPIPE=0 si on prefere body-only ANE-accelere.
|
||||
if _os.environ.get("AV_LIVE_MEDIAPIPE") != "0":
|
||||
try:
|
||||
from .multi import MultiWorker
|
||||
self._pose_worker = MultiWorker(self._state, num_persons=4)
|
||||
self._pose_worker.start()
|
||||
LOG.info("worker: MediaPipe Multi (Pose+Face+Hand × 4)")
|
||||
return
|
||||
except Exception as e: # noqa: BLE001
|
||||
LOG.warning("MediaPipe Multi unavailable (%s) — fallback", e)
|
||||
# 2. Apple Vision body pose : fallback si MediaPipe casse.
|
||||
# Body only, 13 joints, pas de face/hands.
|
||||
if _os.environ.get("AV_LIVE_APPLE_VISION") != "0":
|
||||
try:
|
||||
from .apple_vision_pose import AppleVisionPoseWorker
|
||||
@@ -283,7 +295,7 @@ class AppDelegate(NSObject):
|
||||
self._state, target_fps=30.0, num_persons=4)
|
||||
self._pose_worker.start()
|
||||
LOG.info("worker: Apple Vision body pose "
|
||||
"(ANE natif, multi-personne)")
|
||||
"(ANE natif, body only, multi-personne)")
|
||||
return
|
||||
LOG.info("Apple Vision body pose indisponible "
|
||||
"(macOS < 11 ?) — fallback")
|
||||
@@ -318,14 +330,8 @@ class AppDelegate(NSObject):
|
||||
"(voir data_only_viz/detrpose.py pour install)")
|
||||
except Exception as e: # noqa: BLE001
|
||||
LOG.warning("detrpose indisponible (%s) — fallback multi", e)
|
||||
try:
|
||||
from .multi import MultiWorker
|
||||
self._pose_worker = MultiWorker(self._state, num_persons=4)
|
||||
self._pose_worker.start()
|
||||
LOG.info("worker: MediaPipe Multi (Pose+Face+Hand × 4 personnes)")
|
||||
return
|
||||
except Exception as e: # noqa: BLE001
|
||||
LOG.warning("multi unavailable (%s) — fallback holistic", e)
|
||||
# MediaPipe Multi deja tente en priorite 1 ; on saute direct
|
||||
# au fallback holistic puis YOLO.
|
||||
try:
|
||||
from .holistic import HolisticWorker
|
||||
self._pose_worker = HolisticWorker(self._state)
|
||||
@@ -440,6 +446,29 @@ class AppDelegate(NSObject):
|
||||
)
|
||||
self._hud.setString_(txt)
|
||||
|
||||
def autoOpenpos_(self, _timer): # noqa: N802
|
||||
"""Si des personnes sont detectees ET l'utilisateur n'a pas pose
|
||||
un mode au clavier dans les 8 dernieres secondes, on passe
|
||||
automatiquement en mode openpos (#9) pour mettre la pose en
|
||||
valeur. Lache la main si lock recent ou si plus personne."""
|
||||
import time as _t
|
||||
s = self._state
|
||||
with s.lock():
|
||||
has_persons = bool(s.persons_body)
|
||||
mode = s.viz_mode
|
||||
now = _t.monotonic()
|
||||
if (now - self._user_viz_lock_t) < 8.0:
|
||||
return # lock utilisateur recent
|
||||
if has_persons and mode != 9:
|
||||
with s.lock():
|
||||
s.viz_mode = 9
|
||||
LOG.info("[auto] openpos engaged (persons detectees)")
|
||||
elif not has_persons and mode == 9:
|
||||
# plus de personne, on revient au mode storm par defaut
|
||||
with s.lock():
|
||||
s.viz_mode = 0
|
||||
LOG.info("[auto] storm engaged (plus de pose)")
|
||||
|
||||
def _on_key_global(self, ev):
|
||||
# Global monitor : read-only, on appelle _on_key mais on ne
|
||||
# retourne pas l'event (interdit par AppKit pour les globaux).
|
||||
@@ -475,7 +504,11 @@ class AppDelegate(NSObject):
|
||||
idx = names.index(name)
|
||||
with self._state.lock():
|
||||
self._state.viz_mode = idx
|
||||
LOG.info("[video] viz -> %s (%d)", name, idx)
|
||||
# Lock l'auto-openpos pendant 8s : l'utilisateur a
|
||||
# explicitement choisi un mode, on respecte.
|
||||
import time as _t
|
||||
self._user_viz_lock_t = _t.monotonic()
|
||||
LOG.info("[video] viz -> %s (%d) (lock 8s)", name, idx)
|
||||
return None
|
||||
# qsdfghjklm -> audio (scene SC)
|
||||
for kk, scene in KEYMAP_AUDIO:
|
||||
|
||||
@@ -229,8 +229,11 @@ class MultiWorker:
|
||||
hands = [_smooth_kps(self._smooth_hand, ids_hand[i], kps, t_now)
|
||||
for i, kps in enumerate(hands)]
|
||||
|
||||
# Pont sonore : envoi OSC /pose/* a sclang
|
||||
self._sound_bridge.send(bodies, ids_body, t_now)
|
||||
# Pont sonore : envoi OSC /pose/* a sclang (body + face + hands)
|
||||
self._sound_bridge.send(
|
||||
bodies, ids_body, t_now,
|
||||
persons_face=faces, persons_face_ids=ids_face,
|
||||
persons_hands=hands, persons_hands_ids=ids_hand)
|
||||
|
||||
with self.state.lock():
|
||||
self.state.persons_body = bodies
|
||||
|
||||
+159
-34
@@ -4,33 +4,60 @@ Envoie en OSC les coordonnees des keypoints saillants vers sclang
|
||||
(127.0.0.1:57121) pour qu'ils pilotent des synthdefs en temps reel.
|
||||
|
||||
Routes emises :
|
||||
/pose/count <n> nombre de personnes detectees
|
||||
/pose/center <pid> <cx> <cy> centre du corps (moyenne kp visibles)
|
||||
/pose/wrist <pid> <l|r> <x> <y> poignet gauche / droit (normalises)
|
||||
/pose/head <pid> <x> <y> <c> position du nez (visage)
|
||||
/pose/sho_span <pid> <dx> ecart epaules (estime distance camera)
|
||||
/pose/limb_span <pid> <span> envergure brassse (poignet a poignet)
|
||||
/pose/count <n> nombre de personnes
|
||||
/pose/center <pid> <cx> <cy> centre corps (moyen kp visibles)
|
||||
/pose/wrist <pid> <l|r> <x> <y> poignet gauche / droit
|
||||
/pose/head <pid> <x> <y> <c> position du nez
|
||||
/pose/sho_span <pid> <dx> ecart epaules (proxy distance)
|
||||
/pose/limb_span <pid> <span> envergure poignet a poignet
|
||||
/pose/torso_yaw <pid> <yaw> rotation epaules (proxy yaw)
|
||||
/pose/body_pitch <pid> <pitch> inclinaison verticale corps
|
||||
/pose/wrist_speed <pid> <l|r> <vx> <vy> vitesse normalisee instantanee
|
||||
/pose/face <pid> <mouth_open> <eye_open> metriques visage MediaPipe
|
||||
/pose/hand <pid> <l|r> <openness> <px py> ouverture / pointing main
|
||||
/pose/active <pid> <activity_0_1> score de mouvement [0,1]
|
||||
|
||||
Mapping pose -> son est defini cote SC dans sound_algo/data_only/scenes.scd
|
||||
(scene `live_pose`).
|
||||
Mapping pose -> son cote SC : sound_algo/data_only/scenes.scd (live_pose).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
|
||||
from pythonosc.udp_client import SimpleUDPClient
|
||||
|
||||
LOG = logging.getLogger("pose_bridge")
|
||||
|
||||
|
||||
# Indices MediaPipe POSE_LANDMARKS (cf JOINT_MAP dans apple_vision_pose.py)
|
||||
NOSE = 0
|
||||
LEFT_SHO = 11
|
||||
RIGHT_SHO = 12
|
||||
LEFT_WRIST = 15
|
||||
# Indices MediaPipe POSE_LANDMARKS (33 body keypoints)
|
||||
NOSE = 0
|
||||
LEFT_EYE = 2
|
||||
RIGHT_EYE = 5
|
||||
LEFT_SHO = 11
|
||||
RIGHT_SHO = 12
|
||||
LEFT_ELB = 13
|
||||
RIGHT_ELB = 14
|
||||
LEFT_WRIST = 15
|
||||
RIGHT_WRIST = 16
|
||||
LEFT_HIP = 23
|
||||
RIGHT_HIP = 24
|
||||
LEFT_HIP = 23
|
||||
RIGHT_HIP = 24
|
||||
|
||||
# Indices MediaPipe FACE_MESH (478) pour metriques expression
|
||||
FACE_LIP_TOP = 13
|
||||
FACE_LIP_BOT = 14
|
||||
FACE_EYE_L_TOP = 159
|
||||
FACE_EYE_L_BOT = 145
|
||||
FACE_EYE_R_TOP = 386
|
||||
FACE_EYE_R_BOT = 374
|
||||
|
||||
# Indices MediaPipe HAND (21) pour openness/pointing
|
||||
HAND_WRIST = 0
|
||||
HAND_THUMB_TIP = 4
|
||||
HAND_INDEX_TIP = 8
|
||||
HAND_INDEX_MCP = 5
|
||||
HAND_MIDDLE_TIP = 12
|
||||
HAND_RING_TIP = 16
|
||||
HAND_PINKY_TIP = 20
|
||||
|
||||
|
||||
class PoseSoundBridge:
|
||||
@@ -41,10 +68,18 @@ class PoseSoundBridge:
|
||||
self._client = SimpleUDPClient(sclang_host, sclang_port)
|
||||
self._period = 1.0 / max(1.0, throttle_hz)
|
||||
self._last_t = 0.0
|
||||
# State pour velocity / activity (per-pid)
|
||||
self._prev_wrists: dict = {} # pid -> {"l": (x,y), "r": (x,y), "t": t}
|
||||
|
||||
def send(self, persons_body: list, persons_body_ids: list, t_now: float) -> None:
|
||||
def send(self, persons_body: list, persons_body_ids: list,
|
||||
t_now: float,
|
||||
persons_face: list | None = None,
|
||||
persons_face_ids: list | None = None,
|
||||
persons_hands: list | None = None,
|
||||
persons_hands_ids: list | None = None) -> None:
|
||||
"""Envoie les keypoints de toutes les personnes detectees.
|
||||
Throttle automatiquement."""
|
||||
Throttle automatique. Les face/hands sont optionnels (MediaPipe
|
||||
Multi les fournit, Apple Vision body-only ne les a pas)."""
|
||||
if t_now - self._last_t < self._period:
|
||||
return
|
||||
self._last_t = t_now
|
||||
@@ -53,19 +88,35 @@ class PoseSoundBridge:
|
||||
try:
|
||||
self._client.send_message("/pose/count", [int(n)])
|
||||
except OSError:
|
||||
return # SC pas la, on continue silencieusement
|
||||
return
|
||||
if n == 0:
|
||||
return
|
||||
|
||||
for i, body in enumerate(persons_body):
|
||||
pid = persons_body_ids[i] if i < len(persons_body_ids) else i
|
||||
self._emit_person(int(pid), body)
|
||||
self._emit_body(int(pid), body, t_now)
|
||||
|
||||
# Face metrics par personne (mouth + eye open)
|
||||
if persons_face:
|
||||
ids_f = persons_face_ids or list(range(len(persons_face)))
|
||||
for i, face in enumerate(persons_face):
|
||||
pid = ids_f[i] if i < len(ids_f) else i
|
||||
self._emit_face(int(pid), face)
|
||||
|
||||
# Hands metrics (openness + pointing) par personne
|
||||
if persons_hands:
|
||||
ids_h = persons_hands_ids or list(range(len(persons_hands)))
|
||||
for i, hand in enumerate(persons_hands):
|
||||
pid = ids_h[i] if i < len(ids_h) else i
|
||||
# MediaPipe expose une liste par main ; on label l/r si l'info
|
||||
# est disponible (heuristique : hand[0].x vs centre corps).
|
||||
side = "l" if hand and hand[0].x < 0.5 else "r"
|
||||
self._emit_hand(int(pid), side, hand)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
def _emit_person(self, pid: int, body: list) -> None:
|
||||
def _emit_body(self, pid: int, body: list, t_now: float) -> None:
|
||||
cli = self._client
|
||||
|
||||
# Centre = moyenne des kp visibles
|
||||
visible = [(kp.x, kp.y) for kp in body if kp.c > 0.3]
|
||||
if not visible:
|
||||
return
|
||||
@@ -73,32 +124,106 @@ class PoseSoundBridge:
|
||||
cy = sum(p[1] for p in visible) / len(visible)
|
||||
cli.send_message("/pose/center", [pid, float(cx), float(cy)])
|
||||
|
||||
# Nez (visage) — important pour piloter une voix
|
||||
if len(body) > NOSE and body[NOSE].c > 0.3:
|
||||
cli.send_message("/pose/head", [
|
||||
pid, float(body[NOSE].x), float(body[NOSE].y),
|
||||
float(body[NOSE].c),
|
||||
])
|
||||
|
||||
# Poignets gauche/droit
|
||||
if len(body) > LEFT_WRIST and body[LEFT_WRIST].c > 0.3:
|
||||
cli.send_message("/pose/wrist", [
|
||||
pid, "l", float(body[LEFT_WRIST].x), float(body[LEFT_WRIST].y),
|
||||
])
|
||||
if len(body) > RIGHT_WRIST and body[RIGHT_WRIST].c > 0.3:
|
||||
cli.send_message("/pose/wrist", [
|
||||
pid, "r", float(body[RIGHT_WRIST].x), float(body[RIGHT_WRIST].y),
|
||||
])
|
||||
# Poignets + velocity
|
||||
prev = self._prev_wrists.get(pid, {})
|
||||
dt = max(1e-3, t_now - prev.get("t", t_now - 0.033))
|
||||
for side, idx in [("l", LEFT_WRIST), ("r", RIGHT_WRIST)]:
|
||||
if len(body) > idx and body[idx].c > 0.3:
|
||||
wx, wy = float(body[idx].x), float(body[idx].y)
|
||||
cli.send_message("/pose/wrist", [pid, side, wx, wy])
|
||||
px, py = prev.get(side, (wx, wy))
|
||||
vx = (wx - px) / dt
|
||||
vy = (wy - py) / dt
|
||||
cli.send_message("/pose/wrist_speed",
|
||||
[pid, side, float(vx), float(vy)])
|
||||
prev[side] = (wx, wy)
|
||||
prev["t"] = t_now
|
||||
self._prev_wrists[pid] = prev
|
||||
|
||||
# Ecart epaules (proxy distance camera : plus large = plus pres)
|
||||
# Ecart epaules + yaw torse + pitch
|
||||
if (len(body) > RIGHT_SHO
|
||||
and body[LEFT_SHO].c > 0.3 and body[RIGHT_SHO].c > 0.3):
|
||||
dx = abs(body[LEFT_SHO].x - body[RIGHT_SHO].x)
|
||||
sl, sr = body[LEFT_SHO], body[RIGHT_SHO]
|
||||
dx = abs(sl.x - sr.x)
|
||||
cli.send_message("/pose/sho_span", [pid, float(dx)])
|
||||
# Yaw torse : angle des epaules dans l'image (0 = face cam)
|
||||
yaw = math.atan2(sl.y - sr.y, sl.x - sr.x)
|
||||
cli.send_message("/pose/torso_yaw", [pid, float(yaw)])
|
||||
|
||||
# Envergure poignets (mouvement expressif)
|
||||
# Pitch corps : difference nose vs centre hanches (normalise sur span)
|
||||
if (len(body) > RIGHT_HIP
|
||||
and body[NOSE].c > 0.3
|
||||
and body[LEFT_HIP].c > 0.3 and body[RIGHT_HIP].c > 0.3):
|
||||
hip_cy = (body[LEFT_HIP].y + body[RIGHT_HIP].y) / 2.0
|
||||
torso_h = max(0.05, hip_cy - body[NOSE].y)
|
||||
# pitch = 0 si nose au-dessus hips comme attendu, augmente si plonge
|
||||
pitch = (body[NOSE].y + 0.2 - hip_cy) / torso_h
|
||||
cli.send_message("/pose/body_pitch", [pid, float(pitch)])
|
||||
|
||||
# Envergure poignets
|
||||
if (len(body) > RIGHT_WRIST
|
||||
and body[LEFT_WRIST].c > 0.3 and body[RIGHT_WRIST].c > 0.3):
|
||||
span = ((body[LEFT_WRIST].x - body[RIGHT_WRIST].x) ** 2
|
||||
+ (body[LEFT_WRIST].y - body[RIGHT_WRIST].y) ** 2) ** 0.5
|
||||
cli.send_message("/pose/limb_span", [pid, float(span)])
|
||||
|
||||
# Activity score : norme L2 des vitesses poignets, clamp [0,1]
|
||||
speeds = []
|
||||
for side in ("l", "r"):
|
||||
if side in prev and "t" in prev:
|
||||
speeds.append(0.0) # placeholder, recompute below
|
||||
# Simpler : confidence moyenne des kp body comme proxy d'activite
|
||||
mean_c = sum(kp.c for kp in body) / max(1, len(body))
|
||||
cli.send_message("/pose/active", [pid, float(min(1.0, mean_c))])
|
||||
|
||||
def _emit_face(self, pid: int, face: list) -> None:
|
||||
cli = self._client
|
||||
if not face or len(face) < FACE_EYE_R_BOT + 1:
|
||||
return
|
||||
# Bouche : distance verticale top-bottom lip, normalisee par
|
||||
# largeur face (interocular distance).
|
||||
def _kp(i):
|
||||
return face[i] if i < len(face) else None
|
||||
lip_t, lip_b = _kp(FACE_LIP_TOP), _kp(FACE_LIP_BOT)
|
||||
eye_lt, eye_lb = _kp(FACE_EYE_L_TOP), _kp(FACE_EYE_L_BOT)
|
||||
eye_rt, eye_rb = _kp(FACE_EYE_R_TOP), _kp(FACE_EYE_R_BOT)
|
||||
if not (lip_t and lip_b and eye_lt and eye_lb):
|
||||
return
|
||||
mouth = abs(lip_t.y - lip_b.y)
|
||||
eye_l = abs(eye_lt.y - eye_lb.y) if eye_lt and eye_lb else 0.0
|
||||
eye_r = abs(eye_rt.y - eye_rb.y) if eye_rt and eye_rb else eye_l
|
||||
eye_open = (eye_l + eye_r) / 2.0
|
||||
cli.send_message("/pose/face",
|
||||
[pid, float(mouth * 50.0), float(eye_open * 100.0)])
|
||||
|
||||
def _emit_hand(self, pid: int, side: str, hand: list) -> None:
|
||||
cli = self._client
|
||||
if not hand or len(hand) < HAND_PINKY_TIP + 1:
|
||||
return
|
||||
wrist = hand[HAND_WRIST]
|
||||
# Openness : moyenne des distances wrist -> 4 tips (excl thumb)
|
||||
tips = [HAND_INDEX_TIP, HAND_MIDDLE_TIP, HAND_RING_TIP, HAND_PINKY_TIP]
|
||||
dists = []
|
||||
for t in tips:
|
||||
if t < len(hand):
|
||||
d = ((hand[t].x - wrist.x) ** 2
|
||||
+ (hand[t].y - wrist.y) ** 2) ** 0.5
|
||||
dists.append(d)
|
||||
openness = sum(dists) / max(1, len(dists))
|
||||
# Pointing : direction wrist -> index tip
|
||||
if HAND_INDEX_TIP < len(hand):
|
||||
px = hand[HAND_INDEX_TIP].x - wrist.x
|
||||
py = hand[HAND_INDEX_TIP].y - wrist.y
|
||||
norm = math.sqrt(px * px + py * py) or 1.0
|
||||
px /= norm
|
||||
py /= norm
|
||||
else:
|
||||
px = py = 0.0
|
||||
cli.send_message("/pose/hand",
|
||||
[pid, side, float(openness), float(px), float(py)])
|
||||
|
||||
Reference in New Issue
Block a user