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()
|