exoskeleton/code/test/plot_sew.py

437 lines
15 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.

import os
import sys
import numpy as np
import pinocchio as pin
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
from omegaconf import OmegaConf
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from core.sew_mapper import SEWMapper, fk_update, pose_of_frame, rot_error_deg
def safe_normalize(v, eps=1e-8):
"""Return v / ||v||, or None if the norm is too small."""
n = np.linalg.norm(v)
if n < eps:
return None
return v / n
def check_once(mapper, q_m, names, visualize=True):
import numpy as np
import open3d as o3d
from utils.drwa import o3d_frame, o3d_lineset, o3d_sphere
m_model, m_data = mapper.m_model, mapper.m_data
s_model, s_data = mapper.s_model, mapper.s_data
q_s, dbg = mapper.retargetting(q_m)
# 主端 FK
fk_update(m_model, m_data, q_m)
pS_m, RS_m = pose_of_frame(m_model, m_data, names["m_shoulder"])
pE_m, REf_m = pose_of_frame(m_model, m_data, names["m_elbow"])
pW_m, RW_m = pose_of_frame(m_model, m_data, names["m_wrist"])
p_m, R_m = pose_of_frame(m_model, m_data, names["m_ee"])
# 从端 FK
fk_update(s_model, s_data, q_s)
pS_s, RS_s = pose_of_frame(s_model, s_data, names["s_shoulder"])
pE_s, REf_s = pose_of_frame(s_model, s_data, names["s_elbow"])
pW_s, RW_s = pose_of_frame(s_model, s_data, names["s_wrist"])
p_s, R_s = pose_of_frame(s_model, s_data, names["s_ee"])
# 误差
pos_err = np.linalg.norm(p_m - p_s) * 1000.0 # mm
rot_err = rot_error_deg(R_m, R_s) # degree
print(f"rot_err: {rot_err}, rot_err: {rot_err}")
# 是否发生 reach 裁剪
clipped = dbg["d_s"] < np.linalg.norm(
m_data.oMf[m_model.getFrameId(names["m_wrist"])].translation
- m_data.oMf[m_model.getFrameId(names["m_shoulder"])].translation) - 1e-9 \
or dbg["d_s"] > mapper.L1 + mapper.L2 - mapper.eps_clip + 1e-9
if visualize:
geoms = []
geoms.append(o3d_frame(np.eye(3), np.zeros(3), size=0.1))
pts_m = np.vstack([pS_m, pE_m, pW_m, p_m])
geoms.append(o3d_lineset(pts_m, color=(1.0, 0.0, 0.0)))
geoms += [
o3d_sphere(pS_m, 0.016, (1.0,0.4,0.4)),
o3d_sphere(pE_m, 0.014, (1.0,0.3,0.3)),
o3d_sphere(pW_m, 0.012, (1.0,0.2,0.2)),
o3d_sphere(p_m, 0.012, (1.0,0.0,0.0)),
o3d_frame(R_m, p_m, size=0.07)
]
# 从臂(蓝)
pts_s = np.vstack([pS_s, pE_s, pW_s, p_s])
geoms.append(o3d_lineset(pts_s, color=(0.0, 0.4, 1.0)))
geoms += [
o3d_sphere(pS_s, 0.016, (0.4,0.6,1.0)),
o3d_sphere(pE_s, 0.014, (0.3,0.5,1.0)),
o3d_sphere(pW_s, 0.012, (0.2,0.4,1.0)),
o3d_sphere(p_s, 0.012, (0.0,0.2,1.0)),
o3d_frame(R_s, p_s, size=0.07)
]
geoms.append(o3d_sphere(dbg["pW_s_ref"], 0.008, (0.2, 1.0, 0.2)))
o3d.visualization.draw_geometries(geoms, window_name="SEW Retargeting (Master=Red, Slave=Blue)")
return dict(pos_mm=pos_err, rot_deg=rot_err, clipped=clipped, dbg=dbg, q_s=q_s)
def random_qm(mapper, N=50, margin=0.2):
lb = []; ub = []
for j in mapper.m_model.joints[1:]: # skip universe
if j.nq == 1: # 1-DoF revolute
jid = j.id
iq = j.idx_q
lb.append(mapper.m_model.lowerPositionLimit[iq])
ub.append(mapper.m_model.upperPositionLimit[iq])
lb = np.array(lb); ub = np.array(ub)
rng = (ub - lb)
lb2 = lb + margin * rng
ub2 = ub - margin * rng
qs = []
for _ in range(N):
v = lb2 + np.random.rand(len(lb2)) * (ub2 - lb2)
q_full = pin.neutral(mapper.m_model)
k = 0
for j in mapper.m_model.joints[1:]:
if j.nq == 1:
q_full[j.idx_q] = v[k]; k += 1
qs.append(q_full)
return qs
def validation(mapper):
q_m0 = pin.neutral(mapper.m_model)
names = {
"m_shoulder": "master_shoulder",
"m_elbow": "master_forearm",
"m_wrist": "master_wrist",
"m_ee": "master_ee",
"s_shoulder": "slave_shoulder",
"s_elbow": "slave_forearm",
"s_wrist": "slave_wrist",
"s_ee": "slave_ee"
}
res0 = check_once(mapper, q_m0, names)
print("[Neutral] pos_err = %.3f mm, rot_err = %.3f deg, clipped=%s" %
(res0["pos_mm"], res0["rot_deg"], res0["clipped"]))
# 2) 随机多组验证
N = 100
samples = random_qm(mapper, N=N, margin=0.1)
pos_errs = []; rot_errs = []; n_clip = 0
for q_m in samples:
r = check_once(mapper, q_m, names)
pos_errs.append(r["pos_mm"])
rot_errs.append(r["rot_deg"])
n_clip += int(r["clipped"])
pos_errs = np.array(pos_errs)
rot_errs = np.array(rot_errs)
# =======================================================
# 辅助函数:让 3D 轴等比例,图更“几何化”
# =======================================================
def set_equal_aspect_3d(ax, pts):
"""pts: (N,3) numpy array."""
pts = np.asarray(pts)
# 如果有 NaN 或 Inf直接返回不调坐标轴
if not np.all(np.isfinite(pts)):
return
x_min, y_min, z_min = pts.min(axis=0)
x_max, y_max, z_max = pts.max(axis=0)
max_range = max(x_max - x_min, y_max - y_min, z_max - z_min)
if max_range == 0:
max_range = 1.0 # 防止除零
x_mid = 0.5 * (x_max + x_min)
y_mid = 0.5 * (y_max + y_min)
z_mid = 0.5 * (z_max + z_min)
r = 0.6 * max_range
ax.set_xlim(x_mid - r, x_mid + r)
ax.set_ylim(y_mid - r, y_mid + r)
ax.set_zlim(z_mid - r, z_mid + r)
# =======================================================
# 绘图(论文插图专用)
# =======================================================
def plot_sew_geometry(pS_m, pE_m, pW_m,
pS_s, pE_s, pW_s_ref,
xhat_m, n_m, n_ref, phi_m):
"""
Fig. SEW Geometry
- Master/Slave S-E-W
- Shoulder-Wrist 方向 x̂_m
- 上臂平面法向 n_m
- 参考法向 n_ref
"""
fig = plt.figure(figsize=(6, 6))
ax = fig.add_subplot(111, projection='3d')
# Master skeleton (粗实体+虚线前臂)
ax.plot([pS_m[0], pE_m[0]], [pS_m[1], pE_m[1]], [pS_m[2], pE_m[2]],
'r-', lw=2.5, label='Master upper arm')
ax.plot([pE_m[0], pW_m[0]], [pE_m[1], pW_m[1]], [pE_m[2], pW_m[2]],
'r--', lw=2.0, label='Master forearm')
# Slave skeleton (蓝色)
ax.plot([pS_s[0], pE_s[0]], [pS_s[1], pE_s[1]], [pS_s[2], pE_s[2]],
'b-', lw=2.5, label='Slave upper arm')
ax.plot([pE_s[0], pW_s_ref[0]], [pE_s[1], pW_s_ref[1]], [pE_s[2], pW_s_ref[2]],
'b--', lw=2.0, label='Slave forearm')
# --- Master 上臂平面 patchS_m, E_m, W_m ---
tri = np.vstack([pS_m, pE_m, pW_m])
area = np.linalg.norm(np.cross(pE_m - pS_m, pW_m - pS_m))
if area > 1e-8:
# 使用 Poly3DCollection 画简单三角面片,避免 qhull
verts = [tri]
poly = Poly3DCollection(verts, alpha=0.08,
facecolor='r', edgecolor='none')
ax.add_collection3d(poly)
ax.plot_trisurf(tri[:, 0], tri[:, 1], tri[:, 2],
color='r', alpha=0.08, edgecolor='none')
# Vectors at S_m
arrow_len = 0.12
ax.quiver(*pS_m, *(arrow_len * xhat_m),
color='g', lw=2.0, normalize=False, label=r'$\hat{x}_m$')
ax.quiver(*pS_m, *(arrow_len * n_m),
color='m', lw=2.0, normalize=False, label=r'$n_m$')
ax.quiver(*pS_m, *(arrow_len * n_ref),
color='c', lw=2.0, normalize=False, label=r'$n_{\mathrm{ref}}$')
# Labels
ax.text(*pS_m, r'$S_m$', fontsize=10)
ax.text(*pE_m, r'$E_m$', fontsize=10)
ax.text(*pW_m, r'$W_m$', fontsize=10)
ax.text(*pS_s, r'$S_s$', fontsize=10)
ax.text(*pE_s, r'$E_s$', fontsize=10)
ax.text(*pW_s_ref, r'$W_s$', fontsize=10)
# 视角和轴
pts_all = np.vstack([pS_m, pE_m, pW_m, pS_s, pE_s, pW_s_ref])
set_equal_aspect_3d(ax, pts_all)
ax.view_init(elev=22, azim=52)
ax.set_xlabel("X"); ax.set_ylabel("Y"); ax.set_zlabel("Z")
title_deg = phi_m * 180.0 / np.pi
ax.set_title(r"SEW geometry (swivel $\phi_m = %.1f^\circ$)" % title_deg)
ax.legend(loc='upper right', fontsize=8)
plt.tight_layout()
plt.show()
def plot_swivel_angle(pS_m, n_ref, n_m, xhat_m):
"""
Fig. Swivel Angle:
可视化 n_ref → n_m 绕 x̂_m 的旋转,并画出圆弧 φ_m。
"""
fig = plt.figure(figsize=(6, 6))
ax = fig.add_subplot(111, projection='3d')
# 以 S_m 为圆心,在垂直于 xhat_m 的平面内画单位圆
# 构造平面正交基 b1, b2
tmp = np.array([1.0, 0.0, 0.0])
if abs(np.dot(tmp, xhat_m)) > 0.9:
tmp = np.array([0.0, 1.0, 0.0])
b1 = tmp - np.dot(tmp, xhat_m) * xhat_m
b1 = b1 / np.linalg.norm(b1)
b2 = np.cross(xhat_m, b1)
# 用 n_ref, n_m 在该平面上算出对应角度
# 这里假设 n_ref 已经在平面内
r = 0.12
# Swivel angle
cos_phi = np.dot(n_ref, n_m)
sin_phi = np.dot(xhat_m, np.cross(n_ref, n_m))
phi = np.arctan2(sin_phi, cos_phi)
# 圆弧
ts = np.linspace(0.0, phi, 80)
arc_pts = []
for t in ts:
v = np.cos(t) * n_ref + np.sin(t) * np.cross(xhat_m, n_ref)
v = v / np.linalg.norm(v)
arc_pts.append(pS_m + r * v)
arc_pts = np.asarray(arc_pts)
ax.plot(arc_pts[:, 0], arc_pts[:, 1], arc_pts[:, 2], 'k-', lw=2.0, label=r'$\phi_m$')
# 向量
ax.quiver(*pS_m, *(r * n_ref), color='c', lw=2.0, normalize=False, label=r'$n_{\mathrm{ref}}$')
ax.quiver(*pS_m, *(r * n_m), color='m', lw=2.0, normalize=False, label=r'$n_m$')
ax.quiver(*pS_m, *(r * xhat_m), color='g', lw=2.0, normalize=False, label=r'$\hat{x}_m$')
ax.text(*pS_m, r'$S_m$', fontsize=10)
pts_all = np.vstack([arc_pts, pS_m])
set_equal_aspect_3d(ax, pts_all)
ax.view_init(elev=20, azim=45)
ax.set_xlabel("X"); ax.set_ylabel("Y"); ax.set_zlabel("Z")
ax.set_title(r"Swivel angle definition")
ax.legend(loc='upper right', fontsize=8)
plt.tight_layout()
plt.show()
def plot_two_sphere_sew(pS_s, pW_s_ref, L1, L2, pE_s):
"""
Fig. Two-sphere intersection:
- 球心 S_s, W_s半径 L1, L2
- 交点 E_s
- 高亮三角形 S-E-W
"""
fig = plt.figure(figsize=(6, 6))
ax = fig.add_subplot(111, projection='3d')
# 球面
u, v = np.mgrid[0:2*np.pi:40j, 0:np.pi:20j]
sphere1 = pS_s.reshape(3,1,1) + L1 * np.array([
np.cos(u)*np.sin(v),
np.sin(u)*np.sin(v),
np.cos(v)
])
sphere2 = pW_s_ref.reshape(3,1,1) + L2 * np.array([
np.cos(u)*np.sin(v),
np.sin(u)*np.sin(v),
np.cos(v)
])
ax.plot_surface(sphere1[0], sphere1[1], sphere1[2],
alpha=0.15, color='r', edgecolor='none')
ax.plot_surface(sphere2[0], sphere2[1], sphere2[2],
alpha=0.15, color='b', edgecolor='none')
# S-E-W 三角形
tri = np.vstack([pS_s, pE_s, pW_s_ref])
ax.plot_trisurf(tri[:, 0], tri[:, 1], tri[:, 2],
color='k', alpha=0.08, edgecolor='none')
ax.plot([pS_s[0], pE_s[0], pW_s_ref[0], pS_s[0]],
[pS_s[1], pE_s[1], pW_s_ref[1], pS_s[1]],
[pS_s[2], pE_s[2], pW_s_ref[2], pS_s[2]],
'k-', lw=2.0)
# 关键点
ax.scatter(*pE_s, color='k', s=50, label=r'$E_s$')
ax.scatter(*pS_s, color='r', s=40, label=r'$S_s$')
ax.scatter(*pW_s_ref, color='b', s=40, label=r'$W_s$')
ax.set_xlabel("X"); ax.set_ylabel("Y"); ax.set_zlabel("Z")
ax.set_title("Two-sphere intersection for SEW elbow")
pts_all = np.vstack([pS_s, pE_s, pW_s_ref])
set_equal_aspect_3d(ax, pts_all)
ax.view_init(elev=20, azim=40)
ax.legend(loc='upper right', fontsize=8)
plt.tight_layout()
plt.show()
# =======================================================
# 主测试逻辑
# =======================================================
def main():
conf = OmegaConf.load("../config/config.yaml")
master_model, _, _ = pin.buildModelsFromUrdf(str(conf.master_urdf))
slave_model, _, _ = pin.buildModelsFromUrdf(str(conf.slave_urdf))
mapper = SEWMapper(
master_model=master_model,
slave_model=slave_model,
m_shoulder_frame=conf.m_shoulder_frame,
m_elbow_frame=conf.m_elbow_frame,
m_wrist_frame=conf.m_wrist_frame,
m_ee_frame=conf.m_ee_frame,
s_shoulder_frame=conf.s_shoulder_frame,
s_elbow_frame=conf.s_elbow_frame,
s_wrist_frame=conf.s_wrist_frame,
s_ee_frame=conf.s_ee_frame,
slave_joint_names=conf.sew_mapper.slave_joint_names,
up_dir=np.array(conf.sew_mapper.up_dir),
eps_clip=conf.sew_mapper.eps_clip,
)
# 选一组姿态示例(这里用 neutral
# q_m = pin.neutral(mapper.m_model)
samples = random_qm(mapper, N=500, margin=0.1)
for q_m in samples:
q_s, dbg = mapper.retargetting(q_m)
# 主端点
fk_update(mapper.m_model, mapper.m_data, q_m)
pS_m, _ = pose_of_frame(mapper.m_model, mapper.m_data, conf.m_shoulder_frame)
pE_m, _ = pose_of_frame(mapper.m_model, mapper.m_data, conf.m_elbow_frame)
pW_m, _ = pose_of_frame(mapper.m_model, mapper.m_data, conf.m_wrist_frame)
# 从端SEW 参考)
pS_s = mapper.pS_s_fixed
pE_s = dbg["pE_s"]
pW_s_ref = dbg["pW_s_ref"]
# 几何向量
r_m = pW_m - pS_m
xhat_m = safe_normalize(r_m)
if xhat_m is None:
raise RuntimeError("Shoulderwrist vector is zero; check FK / frames.")
# master 上臂平面法向
n_m_raw = np.cross(pE_m - pS_m, pW_m - pS_m)
n_m = safe_normalize(n_m_raw)
# 参考法向(胸部 up 向量投影到垂直于 xhat_m 的平面)
nref_tilde = mapper.up - (mapper.up @ xhat_m) * xhat_m
n_ref = safe_normalize(nref_tilde)
phi_m = dbg["phi_m"]
degenerate_plane = (n_m is None or n_ref is None)
if degenerate_plane:
print("[Warn] SEW nearly collinear, swivel angle undefined for this posture.")
# 用一个合理的默认法向,方便画 SEW overall 图swivel 角度取 0
if n_ref is None:
# 如果连 n_ref 都 degenerate就随便取一个与 xhat_m 垂直的向量
tmp = np.array([1.0, 0.0, 0.0])
if abs(np.dot(tmp, xhat_m)) > 0.9:
tmp = np.array([0.0, 1.0, 0.0])
n_ref = safe_normalize(tmp - np.dot(tmp, xhat_m) * xhat_m)
if n_m is None:
n_m = n_ref.copy()
phi_m = 0.0 # 退化情形下设成 0 仅用于示意
# 绘制三类图
print("Plotting SEW Geometry...")
plot_sew_geometry(pS_m, pE_m, pW_m,
pS_s, pE_s, pW_s_ref,
xhat_m, n_m, n_ref, phi_m)
if not degenerate_plane:
print("Plotting Swivel Angle Geometry...")
plot_swivel_angle(pS_m, n_ref, n_m, xhat_m)
else:
print("Skip swivel-angle figure for this degenerate posture (SEW nearly collinear).")
print("Plotting Two-Sphere Intersection...")
plot_two_sphere_sew(pS_s, pW_s_ref, mapper.L1, mapper.L2, pE_s)
if __name__ == '__main__':
main()