机械臂逆运动学求解器:CCD与Jacobian对比实战
导读:给定机械臂末端要到达的三维坐标,如何反算每个关节该转多少度?本文从零实现两种数值IK算法——CCD(循环坐标下降)利用几何投影快速迭代,Jacobian(阻尼最小二乘)用梯度下降稳定收敛。零外部依赖,300行C++代码覆盖原理推导与工程实现。
一、逆运动学问题建模
逆运动学(IK)的核心问题是:已知末端要到达的目标位置,反算每个关节该转多少度。这就像你知道手要碰到桌子上的杯子,反推肩膀和肘部该怎么弯——给定结果,求过程。
数值IK的本质是迭代优化——从初始猜测出发,逐步调整关节角直到末端到达目标位置。每次迭代就像闭着眼睛用手去摸索目标:偏差太大就往回缩一点,方向不对就转一点,反复几次总能碰到。不同于解析IK(如6轴机械臂的三角函数闭式解),数值IK适用于任意结构的机器人。
1.1 正运动学(FK)基础
FK是IK的反向:已知关节角,求末端位置。本项目用标准DH参数建模:
| DH参数 | 含义 |
|---|---|
| a | 连杆长度 |
| alpha | 连杆扭角 |
| theta | 关节转角 |
| d | 连杆偏移 |
单关节的齐次变换矩阵:
// DHChain.cpp · 两连杆FK
common::Mat4 DHChain::dhTransform(const DHParameter& dh, double theta) const {
double ct = std::cos(theta);
double st = std::sin(theta);
double ca = std::cos(dh.alpha);
double sa = std::sin(dh.alpha);
common::Mat4 m;
m(0, 0) = ct; m(0, 1) = -st * ca; m(0, 2) = st * sa; m(0, 3) = dh.a * ct;
m(1, 0) = st; m(1, 1) = ct * ca; m(1, 2) = -ct * sa; m(1, 3) = dh.a * st;
m(2, 0) = 0; m(2, 1) = sa; m(2, 2) = ca; m(2, 3) = dh.d;
m(3, 0) = 0; m(3, 1) = 0; m(3, 2) = 0; m(3, 3) = 1;
return m;
}
串联n个关节的FK就是矩阵连乘:
common::Mat4 DHChain::forwardKinematics(const std::vector<double>& jointAngles, int endLink) const {
common::Mat4 result = common::Mat4::identity();
int lastLink = (endLink < 0) ? static_cast<int>(links_.size()) : endLink;
for (int i = 0; i < lastLink; ++i) {
common::Mat4 linkTransform = dhTransform(links_[i], jointAngles[i]);
result = result * linkTransform; // 链式乘积
}
return result;
}
1.2 DH参数建模实例:两连杆机械臂
最简单的2D两连杆模型:
θ2
↖
┌─── joint2
│
θ1│
↙
joint1 (原点)
// 创建两连杆DH链
DHChain chain;
chain.addLink({1.0, 0.0, 0.0, 0.0}, {-1.5, 1.5}); // link1: a=1.0, θ1可变
chain.addLink({1.0, 0.0, 0.0, 0.0}, {-1.5, 1.5}); // link2: a=1.0, θ2可变
std::vector<double> angles = {PI/2, PI/2};
Mat4 result = chain.forwardKinematics(angles);
Vec3 pos = result.translation(); // 得到末端位置
二、CCD循环坐标下降
2.1 几何原理
CCD的核心思想是从末端向根部依次调整每个关节,让末端逐步逼近目标:
关键:如何计算关节转角?
投影法——把末端和目标都投影到当前关节的旋转平面,计算夹角:
// IKSolver.cpp · CCD核心计算
// 计算当前关节位置
common::Mat4 baseTransform = chain.forwardKinematics(angles, i);
common::Vec3 jointPos = baseTransform.translation();
// 计算末端和目标相对关节的向量
common::Vec3 toEnd = (endPos - jointPos).normalized(); // 指向当前末端
common::Vec3 toTarget = (targetPosition - jointPos).normalized(); // 指向目标
// 两向量夹角的余弦值
double cosAngle = toEnd.dot(toTarget);
// 防止acos越界
if (cosAngle > 0.9999) continue;
if (cosAngle < -0.9999) cosAngle = -0.9999;
// arccos得到旋转角度
double angle = std::acos(cosAngle);
// DH链中关节绕自身Z轴,无需额外方向判断
angles[i] += angle; // 全步长
2.2 完整CCD实现
// IKSolver.cpp · CCD循环坐标下降求解器
std::vector<double> IKSolver::solveCCD(
const DHChain& chain,
const common::Vec3& targetPosition,
const std::vector<double>& initialGuess,
int endLink) const
{
size_t n = chain.dof();
int lastLink = (endLink < 0) ? static_cast<int>(n) : endLink;
if (lastLink <= 0) return initialGuess;
std::vector<double> angles = initialGuess;
if (angles.size() < n) angles.resize(n, 0.0);
for (int iter = 0; iter < maxIterations_; ++iter) {
for (int i = lastLink - 1; i >= 0; --i) {
// 1. 计算当前末端位置
common::Mat4 baseTransform = chain.forwardKinematics(angles, i);
common::Mat4 fullTransform = chain.forwardKinematics(angles, lastLink);
common::Vec3 endPos = fullTransform.translation();
common::Vec3 jointPos = baseTransform.translation();
// 2. 投影到关节旋转平面
common::Vec3 toEnd = (endPos - jointPos).normalized();
common::Vec3 toTarget = (targetPosition - jointPos).normalized();
double cosAngle = toEnd.dot(toTarget);
if (cosAngle > 0.9999) continue;
if (cosAngle < -0.9999) cosAngle = -0.9999;
// 3. 计算旋转角并应用
double angle = std::acos(cosAngle);
angles[i] += angle;
// 4. 关节限位钳制
angles[i] = std::max(chain.jointMin(i), std::min(chain.jointMax(i), angles[i]));
// 5. 检查收敛
fullTransform = chain.forwardKinematics(angles, lastLink);
double error = (fullTransform.translation() - targetPosition).length();
if (error < tolerance_) {
return angles;
}
}
}
return angles;
}
2.3 CCD特性分析
| 特性 | 说明 |
|---|---|
| 收敛速度 | 快,通常5-10轮迭代收敛 |
| 收敛质量 | 可能陷入局部最优,非全局最优 |
| 关节限位 | 自然处理(钳制后仍继续调整其他关节) |
| 奇异位姿 | 关节接近180°时,旋转轴退化 |
三、Jacobian阻尼最小二乘
3.1 数学原理
Jacobian IK的核心思想是**"关节怎么转,末端就怎么动"**——把关节到末端的关系写成一个表(雅可比矩阵),表里的每一列告诉你"单独转这个关节,末端会往哪个方向移动"。有了这张表,就可以反查:要末端往目标方向移动,每个关节该转多少。
但这张表在机器人完全伸直(奇异位姿)时会失效,就像门轴和门把手在一条线上时你推不动门。加入阻尼项(给表加一个很小的缓冲),可以有效抑制这种数值爆炸。
工程上用梯度下降+线搜索实现——梯度指出"最陡的下坡方向",线搜索决定"这一步迈多大":
工程实现中用梯度下降+线搜索代替矩阵求逆:
// 梯度 = J^T · Δp
for (size_t i = 0; i < n; ++i) {
gradient[i] = jacobianCols[i].dot(posError);
}
// 步长 = 1 / (max(dot(J,J)) + λ²)
double alpha = 1.0 / (jtjMax + lambda2 + 1e-12);
3.2 差分法计算雅可比
不需要手写雅可比解析式,用数值差分自动计算:
// IKSolver.cpp · 差分法计算雅可比列
for (size_t i = 0; i < n; ++i) {
std::vector<double> anglesPerturbed = angles;
anglesPerturbed[i] += epsilon; // 对第i个关节加扰动
common::Mat4 perturbedPose = chain.forwardKinematics(anglesPerturbed, lastLink);
// 位置变化量 / 扰动量 = 雅可比第i列
jacobianCols[i] = (perturbedPose.translation() - currentPose.translation()) / epsilon;
}
3.3 带线搜索的梯度下降
直接用梯度下降可能过冲导致误差增大,用线搜索找最优步长:
// IKSolver.cpp · 线搜索优化步长
double alpha = 1.0 / (jtjMax + lambda2 + 1e-12); // 初始步长
double prevError = errorLen;
std::vector<double> bestAngles = angles;
double bestError = errorLen;
// 最多尝试8次倍增/减半
for (int ls = 0; ls < 8; ++ls) {
std::vector<double> trialAngles = angles;
for (size_t i = 0; i < n; ++i) {
trialAngles[i] += alpha * gradient[i];
trialAngles[i] = std::max(chain.jointMin(i), std::min(chain.jointMax(i), trialAngles[i]));
}
common::Mat4 trialPose = chain.forwardKinematics(trialAngles, lastLink);
double trialError = (trialPose.translation() - targetPose.translation()).length();
if (trialError < bestError) {
bestAngles = trialAngles;
bestError = trialError;
alpha *= 2.0; // 上次有效,倍增加速
} else {
alpha *= 0.5; // 过冲了,减半
}
}
angles = bestAngles; // 用误差最小的步长
3.4 完整Jacobian实现
// IKSolver.cpp · Jacobian阻尼最小二乘求解器
std::vector<double> IKSolver::solveJacobian(
const DHChain& chain,
const common::Mat4& targetPose,
const std::vector<double>& initialGuess,
int endLink) const
{
size_t n = chain.dof();
int lastLink = (endLink < 0) ? static_cast<int>(n) : endLink;
std::vector<double> angles = initialGuess;
if (angles.size() < n) angles.resize(n, 0.0);
double epsilon = 1e-6;
double lambda2 = damping_ * damping_;
for (int iter = 0; iter < maxIterations_; ++iter) {
common::Mat4 currentPose = chain.forwardKinematics(angles, lastLink);
// 1. 计算位置误差
common::Vec3 posError = targetPose.translation() - currentPose.translation();
double errorLen = posError.length();
if (errorLen < tolerance_) {
return angles; // 收敛
}
// 2. 数值法计算雅可比矩阵
std::vector<common::Vec3> jacobianCols(n);
for (size_t i = 0; i < n; ++i) {
std::vector<double> anglesPerturbed = angles;
anglesPerturbed[i] += epsilon;
common::Mat4 perturbedPose = chain.forwardKinematics(anglesPerturbed, lastLink);
jacobianCols[i] = (perturbedPose.translation() - currentPose.translation()) / epsilon;
}
// 3. 计算梯度向量
std::vector<double> gradient(n, 0.0);
double jtjMax = 0.0;
for (size_t i = 0; i < n; ++i) {
gradient[i] = jacobianCols[i].dot(posError);
double jtj = jacobianCols[i].dot(jacobianCols[i]);
if (jtj > jtjMax) jtjMax = jtj;
}
// 4. 线搜索梯度下降
double alpha = 1.0 / (jtjMax + lambda2 + 1e-12);
std::vector<double> bestAngles = angles;
double bestError = errorLen;
for (int ls = 0; ls < 8; ++ls) {
std::vector<double> trialAngles = angles;
for (size_t i = 0; i < n; ++i) {
trialAngles[i] += alpha * gradient[i];
trialAngles[i] = std::max(chain.jointMin(i), std::min(chain.jointMax(i), trialAngles[i]));
}
common::Mat4 trialPose = chain.forwardKinematics(trialAngles, lastLink);
double trialError = (trialPose.translation() - targetPose.translation()).length();
if (trialError < bestError) {
bestAngles = trialAngles;
bestError = trialError;
alpha *= 2.0;
} else {
alpha *= 0.5;
}
}
angles = bestAngles;
}
return angles;
}
3.5 Jacobian特性分析
| 特性 | 说明 |
|---|---|
| 收敛速度 | 慢,通常20-100轮迭代 |
| 收敛质量 | 稳定逼近局部最优 |
| 计算量 | O(n²×iter),需计算n列雅可比 |
| 奇异鲁棒 | 阻尼项λ²抑制奇异震荡 |
| 关节限位 | 需要钳制(影响梯度方向) |
四、算法对比与选型
| 维度 | CCD | Jacobian DLS | 解析IK |
|---|---|---|---|
| 收敛速度 | 快(5-10轮) | 慢(20-100轮) | 微秒级 |
| 收敛质量 | 局部最优 | 稳定逼近 | 全局最优(若有解) |
| 计算量 | O(n×iter) | O(n²×iter) | O(1) |
| 通用性 | 任意结构 | 任意结构 | 特定结构 |
| 奇异处理 | 一般 | 阻尼项保护 | 需特殊处理 |
| 关节限位 | 自然处理 | 需钳制 | 需后处理 |
工程建议:
- 实时控制(毫秒级延迟要求):选CCD
- 离线规划(稳定性要求高):选Jacobian DLS
- 已知结构的6轴机械臂:选解析IK + 数值法兜底
五、统一调用接口
两种算法封装为统一接口,便于切换和对比:
// IKSolver.h · 统一solve接口
std::vector<double> solve(
common::IKSolverType type, // CCD 或 JACOBIAN
const DHChain& chain,
const common::Mat4& targetPose,
const std::vector<double>& initialGuess,
int endLink = -1) const;
std::vector<double> solveCCD(...) const;
std::vector<double> solveJacobian(...) const;
// 使用示例
IKSolver solver;
solver.setMaxIterations(100);
solver.setTolerance(1e-6);
solver.setDamping(0.01); // Jacobian专用
// 选CCD或Jacobian
std::vector<double> result = solver.solve(
common::IKSolverType::CCD,
chain, targetPose, initialGuess);
六、测试验证
// test_kinematics.cpp · CCD收敛性测试
TEST(ik_ccd_simple_reach) {
DHChain chain;
chain.addLink({1.0, 0.0, 0.0, 0.0}, {-1.5, 1.5});
chain.addLink({1.0, 0.0, 0.0, 0.0}, {-1.5, 1.5});
std::vector<double> initial = {0.0, 0.0};
Vec3 target(1.5, 0.0, 0.0); // 目标在伸展方向
IKSolver solver;
solver.setMaxIterations(100);
solver.setTolerance(1e-4);
std::vector<double> result = solver.solveCCD(chain, target, initial);
Mat4 fk = chain.forwardKinematics(result);
Vec3 pos = fk.translation();
ASSERT_NEAR(pos.x, 1.5, 1e-3);
ASSERT_NEAR(pos.y, 0.0, 1e-3);
return true;
}
// test_kinematics.cpp · Jacobian收敛性测试
TEST(ik_jacobian_simple_reach) {
DHChain chain;
chain.addLink({1.0, 0.0, 0.0, 0.0}, {-1.5, 1.5});
chain.addLink({1.0, 0.0, 0.0, 0.0}, {-1.5, 1.5});
std::vector<double> initial = {0.0, 0.0};
Mat4 target;
target(0,3) = 1.5; target(1,3) = 0.0; target(2,3) = 0.0;
IKSolver solver;
solver.setMaxIterations(100);
solver.setDamping(0.01);
std::vector<double> result = solver.solveJacobian(chain, target, initial);
Mat4 fk = chain.forwardKinematics(result);
double error = (fk.translation() - target.translation()).length();
ASSERT_TRUE(error < 0.1);
return true;
}
测试结果:
========================================
[Test Summary]
Total: 34 | Passed: 34 | Failed: 0
========================================
七、总结
数值IK的核心是迭代优化——CCD用几何投影快速调整关节,Jacobian用梯度下降稳定收敛。两种方法各有优劣:
- CCD:速度快,适合实时控制,但可能陷入局部最优
- Jacobian DLS:收敛稳定,带线搜索和阻尼保护,适合离线规划
实际工程中,通常组合使用:先用CCD快速逼近,再用Jacobian精细调整,或在解析IK失败时做兜底。
下期预告:机器人动作的数学基础——零依赖C++自研Vec3/Quaternion/Mat4数学库,聊聊为什么不用Eigen。
代码仓库:D:\code\test\motion_retargeting
源码路径:src/kinematics/IKSolver.cpp, src/kinematics/DHChain.cpp