exoskeleton/code/core/sew_mapper.py

296 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
import numpy as np
import pinocchio as pin
from pathlib import Path
def hat(v):
x, y, z = v
return np.array([[0, -z, y],
[z, 0,-x],
[-y, x, 0]], dtype=float)
def normalize(v, eps=1e-12):
n = np.linalg.norm(v)
if n < eps:
return v * 0.0
return v / n
def rodrigues(u, phi):
u = normalize(u)
K = hat(u)
return np.eye(3) + np.sin(phi)*K + (1-np.cos(phi))*(K@K)
def minimal_rotation_align(a, b):
a = normalize(a); b = normalize(b)
v = np.cross(a, b)
s = np.linalg.norm(v)
c = float(np.dot(a, b))
if s < 1e-12:
# parallel or anti-parallel
if c > 0.0:
return np.eye(3)
# 180°: choose any axis orthogonal to a
axis = normalize(np.array([1.0,0,0]) if abs(a[0])<0.9 else np.array([0,1.0,0]))
axis = normalize(np.cross(a, axis))
K = hat(axis)
return np.eye(3) + 2*(K@K) # R = I + 2[K]^2 for 180°
K = hat(v/s)
return np.eye(3) + K*s + (1-c)/(s*s) * (K@K)
def euler_zyx_from_R(R):
"""Return Z-Y-X Euler angles (about z,y,x of the shoulder frame)."""
sy = -R[2,0]
cy = np.sqrt(max(0.0, 1.0 - sy*sy))
if cy > 1e-9:
z = np.arctan2(R[1,0], R[0,0])
y = np.arctan2(sy, cy)
x = np.arctan2(R[2,1], R[2,2])
else:
# Gimbal case: cy ~ 0
z = np.arctan2(-R[0,1], R[1,1])
y = np.arctan2(sy, cy)
x = 0.0
return np.array([z, y, x], dtype=float)
def euler_xzy_from_R(R):
"""
Decompose R ≈ Rx(a) * Rz(b) * Ry(c)
Return (a, b, c)
"""
# from derivation:
# sb = -R[0,1]; cb = sqrt(R[0,0]^2 + R[0,2]^2)
sb = -R[0,1]
cb = np.sqrt(max(0.0, R[0,0]**2 + R[0,2]**2))
b = np.arctan2(sb, cb)
# c = atan2(R[0,2], R[0,0])
c = np.arctan2(R[0,2], R[0,0])
# a from R[1,1] = ca*cb and R[2,1] = sa*cb
if cb < 1e-9:
# singular: cb≈0 => b≈±pi/2退化时把 a=0c 吸收残差(简化处理)
a = 0.0
else:
ca = np.clip(R[1,1] / cb, -1.0, 1.0)
sa = np.clip(R[2,1] / cb, -1.0, 1.0)
a = np.arctan2(sa, ca)
return np.array([a, b, c], dtype=float)
def euler_zy_from_R(R):
"""Return Z-Y Euler angles for wrist (about z then y)."""
# R ≈ Rz(z) * Ry(y)
# y = asin(R[0,2])? We'll use standard decomposition.
# From R = Rz*Ry:
# R[2,0] = -sin(y)
y = np.arcsin(np.clip(R[0,2], -1.0, 1.0))
cy = np.cos(y)
if abs(cy) < 1e-9:
z = 0.0
else:
z = np.arctan2(-R[0,1]/cy, R[0,0]/cy)
return np.array([z, y], dtype=float)
def rot_error_deg(RA, RB):
# 旋转误差:以李群对数映射的范数(弧度),再转度
R = RA.T @ RB
w = pin.log3(R)
return np.linalg.norm(w) * 180.0/np.pi
def pose_of_frame(model, data, frame_name):
fid = model.getFrameId(frame_name)
oMf = data.oMf[fid]
return oMf.translation.copy(), oMf.rotation.copy()
def fk_update(model, data, q):
pin.forwardKinematics(model, data, q)
pin.updateFramePlacements(model, data)
class SEWMapper:
"""
SEW Mapper (closed-form, no numeric IK):
- Loads master & slave URDFs
- Computes heteromorphic retargeting from master EE pose to slave q_s using SEW geometry
Assumptions:
* Slave arm is 7-DoF in the order: 3 shoulder + 1 elbow + 3 wrist (axes orthogonal at shoulder/wrist frames).
* Provide correct link/frame names and joint name order for your model.
"""
def __init__(
self,
master_model: pin.Model,
slave_model: pin.Model,
# Frame/link names
m_shoulder_frame: str,
m_elbow_frame: str,
m_wrist_frame: str,
m_ee_frame: str,
s_shoulder_frame: str,
s_elbow_frame: str,
s_wrist_frame: str,
s_ee_frame: str,
# Joint name order for slave 7-DoF: [S1,S2,S3, EL, W1, W2, W3]
slave_joint_names: list,
# world/up direction and safety margins
up_dir=np.array([0,0,1.0]),
eps_clip=1e-3,
):
# Load master
self.m_model = master_model
self.m_data = self.m_model.createData()
# Load slave
self.s_model = slave_model
self.s_data = self.s_model.createData()
# Frame IDs
self.fid_mS = self._get_frame_id(self.m_model, m_shoulder_frame)
self.fid_mE = self._get_frame_id(self.m_model, m_elbow_frame)
self.fid_mW = self._get_frame_id(self.m_model, m_wrist_frame)
self.fid_mEE= self._get_frame_id(self.m_model, m_ee_frame)
self.fid_sS = self._get_frame_id(self.s_model, s_shoulder_frame)
self.fid_sE = self._get_frame_id(self.s_model, s_elbow_frame)
self.fid_sW = self._get_frame_id(self.s_model, s_wrist_frame)
self.fid_sEE= self._get_frame_id(self.s_model, s_ee_frame)
# Up direction and epsilon
self.up = normalize(up_dir)
self.eps_clip = float(eps_clip)
# Query slave joint indices (order critical)
self.s_joint_ids = [self.s_model.getJointId(n) for n in slave_joint_names]
self.s_qidx = [self.s_model.joints[jid].idx_q for jid in self.s_joint_ids]
assert len(self.s_qidx) == 7, "Provide 7 slave joints in order [S1,S2,S3, EL, W1, W2, W3]"
# Pre-compute slave segment lengths L1, L2 in reference (zero) config
self.qs0 = pin.neutral(self.s_model)
self._update_fk_slave(self.qs0)
pS = self._frame_pos(self.s_data, self.fid_sS)
pE = self._frame_pos(self.s_data, self.fid_sE)
pW = self._frame_pos(self.s_data, self.fid_sW)
self.L1 = float(np.linalg.norm(pE - pS))
self.L2 = float(np.linalg.norm(pW - pE))
# Fixed slave shoulder location in world
self.pS_s_fixed = pS.copy()
# ------------------ Utilities ------------------ #
def _get_frame_id(self, model, name):
try:
return model.getFrameId(name)
except:
# fallback: also try link->frame mapping via joint name
return model.getFrameId(name)
def _update_fk_master(self, q_m):
pin.forwardKinematics(self.m_model, self.m_data, q_m)
pin.updateFramePlacements(self.m_model, self.m_data)
def _update_fk_slave(self, q_s):
pin.forwardKinematics(self.s_model, self.s_data, q_s)
pin.updateFramePlacements(self.s_model, self.s_data)
def _frame_pos(self, data, fid):
return data.oMf[fid].translation.copy()
def _frame_rot(self, data, fid):
return data.oMf[fid].rotation.copy()
# ------------------ Core Retarget ------------------ #
def retargetting(self, q_m, q_s_init=None):
"""
Input:
q_m : master joint configuration (np.ndarray, size = master nq)
q_s_init: optional slave initial seed (ignored for geometry, used only to keep continuity of angle unwrap if desired)
Output:
q_s : slave joint angles (np.ndarray, size = slave nq)
debug : dict with intermediate targets (pE_s, pW_s_ref, phi_m, theta)
"""
# 1) Master FK and SEW quantities
self._update_fk_master(q_m)
pS_m = self._frame_pos(self.m_data, self.fid_mS)
pE_m = self._frame_pos(self.m_data, self.fid_mE)
pW_m = self._frame_pos(self.m_data, self.fid_mW)
# RW_m = self._frame_rot(self.m_data, self.fid_mW)
RE_m = self._frame_rot(self.m_data, self.fid_mEE)
r_m = pW_m - pS_m
d_m = float(np.linalg.norm(r_m))
xhat_m = normalize(r_m)
# Swivel (master)
nm_raw = np.cross(pE_m - pS_m, pW_m - pS_m)
nm = normalize(nm_raw)
nref_tilde = self.up - np.dot(self.up, xhat_m) * xhat_m
if np.linalg.norm(nref_tilde) < 1e-6:
ey = np.array([0,1.0,0])
nref_tilde = ey - np.dot(ey, xhat_m) * xhat_m
nref = normalize(nref_tilde)
num = np.dot(xhat_m, np.cross(nref, nm))
den = float(np.dot(nref, nm))
phi_m = np.arctan2(num, den)
# 2) Wrist pose and reach clipping on slave
d_min = abs(self.L1 - self.L2) + self.eps_clip
d_max = (self.L1 + self.L2) - self.eps_clip
d_s = np.clip(d_m, d_min, d_max)
xhat_s = xhat_m.copy()
pS_s = self.pS_s_fixed
pW_s_ref = pS_s + d_s * xhat_s
R_ee_ref = RE_m.copy() # preserve orientation
# 3) Two-sphere elbow construction on slave
d = float(np.linalg.norm(pW_s_ref - pS_s))
e3 = rodrigues(xhat_s, phi_m) @ nref
e3 = normalize(e3)
e2 = normalize(np.cross(xhat_s, e3))
cos_th = (self.L1**2 + d**2 - self.L2**2) / (2*self.L1*d)
cos_th = float(np.clip(cos_th, -1.0, 1.0))
th = np.arccos(cos_th)
sin_th = np.sqrt(max(0.0, 1.0 - cos_th*cos_th))
pE_s = pS_s + self.L1*(cos_th * xhat_s + sin_th * e2)
# 4) Joint reconstruction (assumes 3-1-3 structure with Z-Y-X at shoulder and Z-Y at wrist)
q_s = pin.neutral(self.s_model) if q_s_init is None else q_s_init.copy()
# (a) Set shoulder (first 3 joints): align upper-arm vector
u = normalize(pE_s - pS_s)
y_axis = e3 # 让“肘轴=局部Y”严格对齐 SEW 的平面法向
x_axis = u
z_axis = normalize(np.cross(x_axis, y_axis))
RS_des = np.column_stack([x_axis, y_axis, z_axis])
dz, dy, dx = euler_zyx_from_R(RS_des) # R ≈ Rz(dz)*Ry(dy)*Rx(dx)
q_s[self.s_qidx[0]] = dz
q_s[self.s_qidx[1]] = dy
q_s[self.s_qidx[2]] = dx
# (b) Elbow flex = pi - theta
fhat = normalize(pW_s_ref - pE_s)
u = normalize(pE_s - pS_s)# 上臂方向(从肩指向肘)
q_elbow = np.arctan2(np.dot(e3, np.cross(u, fhat)), np.dot(u, fhat))# 计算把 u 绕 e3 旋到 fhat 的有符号角atan2( 轴·(u×f), u·f )
q_s[self.s_qidx[3]] = q_elbow
self._update_fk_slave(q_s)
# (c) Wrist orientation match with XZY on wrist frame
RW_cur = self._frame_rot(self.s_data, self.fid_sW)
R_needed = RW_cur.T @ R_ee_ref
a_b_c = euler_xzy_from_R(R_needed) # (roll-X, yaw-Z, pitch-Y)
q_s[self.s_qidx[4]] = a_b_c[0]
q_s[self.s_qidx[5]] = a_b_c[1]
q_s[self.s_qidx[6]] = a_b_c[2]
debug = dict(
phi_m=phi_m,
d_s=d_s,
pW_s_ref=pW_s_ref,
pE_s=pE_s,
theta=th
)
return q_s, debug