437 lines
15 KiB
Python
437 lines
15 KiB
Python
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 上臂平面 patch(S_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("Shoulder–wrist 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] S–E–W 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 (S–E–W 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()
|