296 lines
10 KiB
Python
296 lines
10 KiB
Python
# -*- 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=0,c 吸收残差(简化处理)
|
||
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 X–Z–Y 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
|