Pinocchio刚体动力学库实战:从CMake配置到人形机器人雅可比矩阵计算

📅 发布时间:2026/7/6 20:44:55 👁️ 浏览次数:
Pinocchio刚体动力学库实战:从CMake配置到人形机器人雅可比矩阵计算
1. 环境准备从零搭建你的Pinocchio工作空间如果你刚接触机器人动力学听到“Pinocchio”这个名字可能会觉得有点可爱但千万别小看它。Pinocchio是一个功能强大的C刚体动力学库由法国国家信息与自动化研究所INRIA的团队开发维护。它最大的特点就是快计算效率极高特别适合用在实时性要求高的场景比如人形机器人的实时控制、轨迹规划甚至是强化学习中的物理仿真。我自己在做人形机器人项目时从最初自己手写雅可比矩阵到后来用上Pinocchio那种从“吭哧吭哧”推导公式到“一键调用”的爽快感至今记忆犹新。那么我们怎么开始呢第一步也是最关键的一步就是搭建一个正确、干净、可复现的编译环境。很多新手朋友卡在这一步要么是库没装对要么是CMake配置写错了导致后面代码怎么调都不对。今天我就带你走一遍我踩过坑之后总结出来的“最佳实践”保证你能一次成功。首先你需要一个Linux系统Ubuntu 20.04或22.04是最佳选择社区支持最好。Pinocchio的安装方式有很多但我强烈推荐使用APT包管理器来安装官方预编译版本这是最省心、最不容易出错的方法。打开你的终端依次执行下面几条命令# 1. 添加Pinocchio的软件源 sudo apt-get update -qq sudo apt-get install -qq curl lsb-release curl -s https://raw.githubusercontent.com/stack-of-tasks/pinocchio/master/.github/install.sh | bash # 2. 安装Pinocchio及其核心依赖 sudo apt-get install -qq libpinocchio-dev执行完这两步Pinocchio库的头文件和库文件就已经被安装到你的系统目录了通常是/opt/openrobots/。为什么推荐这种方式因为它会自动帮你处理好所有复杂的依赖关系比如Eigen3、urdfdom、hpp-fcl等你自己手动编译这些依赖可能会遇到版本冲突非常头疼。安装完成后我们来验证一下。创建一个新的文件夹作为你的项目目录比如叫做pinocchio_tutorial。在这个文件夹里我们就要开始写那个至关重要的CMakeLists.txt文件了。你可以把它理解为一个“项目构建说明书”告诉CMake工具我的代码在哪里、需要链接哪些库、最终要生成什么可执行文件。下面这个版本是我经过多个项目锤炼后的模板你可以直接拿去用。2. 手把手教你写CMakeLists.txt很多教程里的CMakeLists.txt写得过于简单或者过于复杂让人摸不着头脑。我这里给你一个既完整又清晰的版本并且我会逐行解释每一句是干什么的让你知其然也知其所以然。cmake_minimum_required(VERSION 3.16) project(my_humanoid_robot VERSION 0.1.0 LANGUAGES CXX) # 设置C标准为C11或更高Pinocchio需要 set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 最关键的一步寻找Pinocchio包 find_package(pinocchio REQUIRED) # 寻找Eigen库Pinocchio重度依赖它进行矩阵运算 find_package(Eigen3 REQUIRED) # 一个非常实用的调试技巧打印所有CMake变量用于排查路径问题 # 初次配置时可以取消下面几行的注释看看Pinocchio_DIR等关键变量是否正确设置 # get_cmake_property(_variableNames VARIABLES) # foreach (_variableName ${_variableNames}) # message(STATUS DEBUG: ${_variableName}${${_variableName}}) # endforeach() # 将找到的头文件目录添加到编译搜索路径中 # 这里演示了两种方式直接指定路径和使用CMake变量 include_directories(/opt/openrobots/include) # 通常安装后的路径 include_directories(${EIGEN3_INCLUDE_DIRS}) # 使用find_package找到的Eigen路径 # 将库文件目录添加到链接搜索路径 link_directories(/opt/openrobots/lib) # 添加你的可执行文件 # 假设你的源代码文件是 main_jacobian.cpp add_executable(compute_jacobian main_jacobian.cpp) # 将你的可执行文件与必要的库链接起来 # 这一行是灵魂它告诉链接器你的程序需要Pinocchio库 target_link_libraries(compute_jacobian pinocchio::pinocchio) # 更优雅的现代CMake写法推荐 # target_include_directories(compute_jacobian PRIVATE ${PINOCCHIO_INCLUDE_DIRS}) # target_link_libraries(compute_jacobian PRIVATE pinocchio::pinocchio)我来解释几个容易出错的点。第一find_package(pinocchio REQUIRED)这一行REQUIRED关键字意味着“必须找到”如果找不到CMake配置阶段就会直接报错这能让你第一时间发现问题而不是等到编译链接时才出一堆看不懂的错误。第二关于include_directories和link_directories这是一种比较传统的写法。现在更推荐使用target_include_directories和target_link_libraries这种针对特定目标target的设置方式这样管理更清晰尤其是在项目有多个可执行文件时不会造成全局污染。写好了CMakeLists.txt怎么用呢在你的项目根目录也就是和CMakeLists.txt同一级的位置打开终端执行经典的“三板斧”mkdir build cd build # 创建一个独立的构建目录保持源码干净 cmake .. # 运行CMake生成Makefile make -j4 # 开始编译-j4表示用4个CPU核心并行编译更快如果一切顺利你会在build目录下看到一个名为compute_jacobian的可执行文件。恭喜你最难的环境关已经过了接下来我们就可以进入激动人心的编码环节了。3. 加载机器人模型你的数字孪生环境搭好了我们得有个机器人来“玩”吧。在Pinocchio的世界里我们操作的不是实体机器人而是它的数字模型。这个模型通常用一个.urdf文件来描述它定义了机器人的连杆、关节、质量、惯性等所有物理属性。你可以从机器人厂商那里获取或者用SolidWorks、Fusion 360等软件设计导出。我这里假设你已经有了一个名为humanoid.urdf的模型文件把它放在项目根目录下的robots文件夹里。那么怎么用C代码把这个文件“喂”给Pinocchio并让它理解这个模型呢看下面这段代码我加了非常详细的注释#include pinocchio/parsers/urdf.hpp // 用于解析URDF文件 #include pinocchio/algorithm/model.hpp #include pinocchio/algorithm/joint-configuration.hpp #include iostream #include string int main() { // 1. 指定你的URDF模型文件路径 // 这里假设文件放在项目根目录的 robots 子文件夹下 std::string urdf_filename ../robots/humanoid.urdf; // 2. 创建模型容器和数据容器 // pinocchio::Model 是模型的“蓝图”存储了所有固定不变的结构信息 // pinocchio::Data 是模型的“运行时内存”存储计算过程中的临时数据和结果 pinocchio::Model model; pinocchio::Data data; // 3. 从URDF文件构建模型 // 这个函数会读取URDF解析所有关节和连杆并自动构建一个运动学树 // 第二个参数 false 表示不自动计算重力项我们稍后手动处理 try { pinocchio::loadURDF(model, urdf_filename, false); } catch (const std::exception e) { std::cerr Failed to load URDF model: e.what() std::endl; return -1; } // 4. 为上面创建的模型初始化对应的数据容器 // 这一步是必须的它根据model的维度为data分配内存 data pinocchio::Data(model); // 5. 打印一些模型基本信息验证加载是否成功 std::cout 模型名称: model.name std::endl; std::cout 关节数量 (njoints): model.njoints std::endl; std::cout 自由度数量 (nv): model.nv std::endl; std::cout 模型加载成功 std::endl; // 现在model和data就准备好了它们是你后续所有计算的基础 // 你可以把它们想象成游戏里的角色和它的状态栏 return 0; }把这段代码保存为main_jacobian.cpp然后编译运行。如果终端打印出了关节数量和自由度数量比如nv: 12对于一个简化的人形机器人12个自由度很常见每条腿6个那就说明模型加载完全正确。这里有个小坑需要注意URDF文件中的路径问题。如果你的URDF里通过mesh标签引用了三维模型文件比如STL或DAE文件你需要确保这些网格文件的路径是正确的或者使用URDF中的package://协议并正确设置ROS_PACKAGE_PATH环境变量。最简单的办法是在URDF里使用绝对路径或者把网格文件和URDF放在同一个目录。4. 正向运动学让机器人“动”起来模型加载好了但它还是个“雕塑”一动不动。正向运动学Forward Kinematics, FK就是给这个雕塑注入生命的第一步。简单说正向运动学就是根据每个关节的角度关节空间计算出机器人末端比如手或脚在三维空间中的位置和姿态笛卡尔空间。这就像你知道你的肩膀、肘部、手腕每个关节转了多少度就能算出你的手掌心现在在哪里掌心朝哪个方向。在Pinocchio里做正向运动学计算非常直观。我们需要做两件事第一定义一个关节角度向量q第二调用库函数。让我们扩展之前的main函数// ... 前面加载模型的代码 ... // 6. 定义机器人的关节配置关节角度 // 创建一个向量长度等于模型的自由度数量 (nv) // 这里我们假设机器人有12个自由度并给它们赋一组初始值单位弧度 Eigen::VectorXd q Eigen::VectorXd::Zero(model.nv); // 举个例子让机器人稍微“蹲”一点。假设索引0-5是右腿6-11是左腿 // 髋关节俯仰角第二个关节弯曲-0.2弧度约-11.5度 q[1] -0.2; // 右髋俯仰 q[7] -0.2; // 左髋俯仰 // 膝关节第四个关节弯曲0.4弧度约23度 q[3] 0.4; q[9] 0.4; std::cout 初始关节角度 q: q.transpose() std::endl; // 7. 执行正向运动学计算 // 这个函数会更新data结构中所有关节的位置和姿态 pinocchio::forwardKinematics(model, data, q); // 8. 更新“帧”Frame的位姿 // “帧”是附着在机器人特定部位如脚心、手掌的坐标系。 // 在URDF中我们通常已经定义好了这些帧例如 l_sole, r_sole。 // 这一步会根据上一步计算出的关节位姿计算出这些帧的位姿。 pinocchio::updateFramePlacements(model, data); // 9. 获取特定帧比如右脚的位姿并打印 // 首先通过帧的名字找到它在模型中的索引ID pinocchio::FrameIndex right_foot_frame_id model.getFrameId(r_sole); // 检查是否找到 if (right_foot_frame_id model.frames.size()) { std::cerr 错误未找到名为 r_sole 的帧 std::endl; // 你可以打印所有帧的名字来排查 for (const auto frame : model.frames) { std::cout 帧名: frame.name std::endl; } return -1; } // 获取该帧的位姿一个SE3对象包含旋转矩阵和平移向量 const pinocchio::SE3 right_foot_pose data.oMf[right_foot_frame_id]; std::cout \n右脚r_sole的位置 (x, y, z): right_foot_pose.translation().transpose() 米 std::endl; // 姿态可以用旋转矩阵或四元数表示这里打印旋转矩阵3x3 std::cout 右脚姿态旋转矩阵:\n right_foot_pose.rotation() std::endl; // 同样可以获取左脚 pinocchio::FrameIndex left_foot_frame_id model.getFrameId(l_sole); if (left_foot_frame_id model.frames.size()) { const pinocchio::SE3 left_foot_pose data.oMf[left_foot_frame_id]; std::cout \n左脚l_sole的位置: left_foot_pose.translation().transpose() 米 std::endl; }运行这段代码你就能看到在给定的关节角度下机器人的双脚在三维空间中的具体坐标。这就是正向运动学它是后续一切高级计算如逆运动学、动力学、雅可比矩阵的基石。我建议你多尝试几组不同的q值观察末端位置的变化这能帮你建立对机器人构型的直观感受。5. 雅可比矩阵连接关节速度与末端速度的桥梁好了现在机器人能摆姿势了。但机器人是要运动的我们不仅关心它脚在哪更关心它的脚能以多快的速度、向哪个方向移动。这就引出了机器人学中一个极其核心的概念——雅可比矩阵Jacobian。你可以把雅可比矩阵想象成一个**“速度映射器”**。它建立了关节空间速度每个关节转动的角速度与笛卡尔空间速度末端执行器沿X、Y、Z轴的线速度和绕X、Y、Z轴的角速度之间的线性关系。数学上表示为v J(q) * dq。其中v是一个6维向量3维线速度 3维角速度dq是关节速度向量J(q)就是那个神奇的雅可比矩阵它的大小是6 x nvnv是自由度。关键是雅可比矩阵不是固定的它随着机器人当前的姿势q的变化而变化。所以我们需要实时计算它。在Pinocchio中计算一个特定帧的雅可比矩阵只需要一行函数调用。让我们继续在代码中添加// ... 接续正向运动学之后的代码 ... // 10. 定义关节速度假设当前时刻各关节的角速度单位弧度/秒 Eigen::VectorXd dq Eigen::VectorXd::Zero(model.nv); // 假设右腿髋关节正在以0.1 rad/s的速度转动 dq[0] 0.1; // 假设左腿膝关节正在以-0.05 rad/s的速度转动 dq[9] -0.05; std::cout \n关节速度 dq: dq.transpose() rad/s std::endl; // 11. 为雅可比矩阵分配内存 // 雅可比矩阵是 6行 x nv列 的矩阵 pinocchio::Data::Matrix6x J_right_foot(6, model.nv); J_right_foot.setZero(); // 初始化为零 // 12. 计算右脚帧的雅可比矩阵 // 注意这个函数计算的是“世界坐标系”下的雅可比矩阵。 // 如果你想计算相对于局部坐标系或其它坐标系的雅可比Pinocchio也提供了其他函数。 pinocchio::computeFrameJacobian(model, data, q, right_foot_frame_id, J_right_foot); std::cout \n右脚雅可比矩阵 J (6 x model.nv ):\n J_right_foot std::endl; // 13. 利用雅可比矩阵计算末端速度 // 这就是那个核心公式v J * dq Eigen::VectorXd v_right_foot J_right_foot * dq; // 一个6维向量 std::cout \n右脚末端速度 v (线速度 角速度): std::endl; std::cout 线速度 [vx, vy, vz]: v_right_foot.head3().transpose() m/s std::endl; std::cout 角速度 [wx, wy, wz]: v_right_foot.tail3().transpose() rad/s std::endl;看计算雅可比矩阵并得到末端速度就是这么简单直接。pinocchio::computeFrameJacobian这个函数帮你处理了所有复杂的求导和链式法则。输出结果中雅可比矩阵的每一列对应一个关节速度对末端速度的“贡献度”。你可以试着改变q姿势和dq速度观察雅可比矩阵和末端速度的变化。比如当腿完全伸直时某些方向的运动可能会变得奇异雅可比矩阵秩亏这在后续的轨迹规划和控制中是需要特别注意的。6. 进阶实战完整的人形机器人双足速度计算在实际的人形机器人控制中我们往往需要同时知道双脚的速度并且可能只关心脚掌中心点CoP的线速度或者需要处理更复杂的模型比如包含浮动基的模型。下面我给出一个更贴近真实项目的完整函数示例它封装了从模型加载到计算双足速度的全过程并处理了一些工程细节。#include pinocchio/parsers/urdf.hpp #include pinocchio/algorithm/frames.hpp #include pinocchio/algorithm/jacobian.hpp #include pinocchio/algorithm/kinematics.hpp #include Eigen/Dense #include iostream #include memory class HumanoidKinematics { public: // 构造函数加载模型 HumanoidKinematics(const std::string urdf_path) { pinocchio::urdf::buildModel(urdf_path, model_); data_ std::make_uniquepinocchio::Data(model_); // 预查找关键帧的ID避免每次计算时都进行字符串查找 right_foot_id_ model_.getFrameId(r_ankle); // 根据你的URDF中帧的名字修改 left_foot_id_ model_.getFrameId(l_ankle); if (right_foot_id_ model_.frames.size() || left_foot_id_ model_.frames.size()) { std::cerr 警告未找到指定的脚部帧请检查URDF中帧的名称。 std::endl; // 列出所有帧供参考 for (size_t i 0; i model_.frames.size(); i) { std::cout 帧索引 i : model_.frames[i].name std::endl; } } std::cout 初始化完成。模型自由度: model_.nv std::endl; } // 核心计算函数输入关节角度和速度输出双脚的线速度和角速度 void computeFootVelocities(const Eigen::VectorXd q, const Eigen::VectorXd dq, Eigen::Vector3d v_right_linear, Eigen::Vector3d v_right_angular, Eigen::Vector3d v_left_linear, Eigen::Vector3d v_left_angular) { // 输入检查 assert(q.size() model_.nq 关节角度向量q维度错误); assert(dq.size() model_.nv 关节速度向量dq维度错误); // 步骤1: 正向运动学 pinocchio::forwardKinematics(model_, *data_, q, dq); // 注意这里传入了dq用于更新速度相关的数据 pinocchio::updateFramePlacements(model_, *data_); // 步骤2: 计算雅可比矩阵 pinocchio::Data::Matrix6x J_right(6, model_.nv); pinocchio::Data::Matrix6x J_left(6, model_.nv); J_right.setZero(); J_left.setZero(); // 计算世界坐标系下的雅可比矩阵 pinocchio::computeFrameJacobian(model_, *data_, q, right_foot_id_, J_right); pinocchio::computeFrameJacobian(model_, *data_, q, left_foot_id_, J_left); // 步骤3: 计算末端速度 (v J * dq) Eigen::VectorXd v_right_full J_right * dq; // 6维向量 [线速度; 角速度] Eigen::VectorXd v_left_full J_left * dq; // 步骤4: 提取线速度和角速度部分 v_right_linear v_right_full.head3(); v_right_angular v_right_full.tail3(); v_left_linear v_left_full.head3(); v_left_angular v_left_full.tail3(); } // 一个便利函数只获取线速度 void computeFootLinearVelocities(const Eigen::VectorXd q, const Eigen::VectorXd dq, Eigen::Vector3d v_right, Eigen::Vector3d v_left) { Eigen::Vector3d v_right_ang, v_left_ang; // 临时变量不需要 computeFootVelocities(q, dq, v_right, v_right_ang, v_left, v_left_ang); } private: pinocchio::Model model_; std::unique_ptrpinocchio::Data data_; pinocchio::FrameIndex right_foot_id_; pinocchio::FrameIndex left_foot_id_; }; // 主函数中使用这个类 int main() { std::string urdf_file ../robots/humanoid_simple.urdf; HumanoidKinematics robot(urdf_file); // 设置一个示例姿势和速度 int nv robot.getDof(); // 假设类里有这个getter方法 Eigen::VectorXd q Eigen::VectorXd::Zero(nv); Eigen::VectorXd dq Eigen::VectorXd::Zero(nv); // 设置一个行走中的姿势示例值 q 0.0, 0.1, -0.2, 0.4, -0.2, 0.0, // 右腿: 髋横滚髋俯仰膝踝... 0.0, -0.1, -0.2, 0.35, -0.15, 0.0; // 左腿 // 设置关节速度示例值 dq 0.05, 0.02, 0.1, -0.05, 0.01, 0.0, -0.03, 0.01, 0.08, -0.04, 0.005, 0.0; Eigen::Vector3d v_right_lin, v_right_ang, v_left_lin, v_left_ang; robot.computeFootVelocities(q, dq, v_right_lin, v_right_ang, v_left_lin, v_left_ang); std::cout \n 双足速度计算结果 std::endl; std::cout 右脚线速度: v_right_lin.transpose() m/s std::endl; std::cout 右脚角速度: v_right_ang.transpose() rad/s std::endl; std::cout 左脚线速度: v_left_lin.transpose() m/s std::endl; std::cout 左脚角速度: v_left_ang.transpose() rad/s std::endl; return 0; }这个类把功能封装了起来更清晰也更易复用。在实际项目中你可能会把这个类集成到更大的控制循环里。这里有几个我踩过坑后总结的要点第一帧Frame的名字一定要和URDF里定义的完全一致包括大小写。最好在初始化时打印所有帧名确认一下。第二注意pinocchio::forwardKinematics函数的重载版本当传入dq时它会同时计算速度级的信息为后续计算做准备。第三雅可比矩阵的计算依赖于当前的q所以如果你的机器人姿势在实时变化务必在每次控制循环中都重新计算。7. 调试技巧与常见问题排查即使按照步骤来第一次运行也难免会遇到问题。别慌这是学习的必经之路。我把我遇到过的典型问题和排查方法列出来希望能帮你快速定位。问题一编译时找不到Pinocchio头文件或链接失败。现象fatal error: pinocchio/xxx.hpp: No such file or directory或者undefined reference topinocchio::xxx。排查确保你通过APT正确安装了libpinocchio-dev。可以尝试在终端输入pkg-config --cflags pinocchio看看是否能输出正确的包含路径。检查你的CMakeLists.txt中find_package(pinocchio REQUIRED)是否成功。可以在find_package前后加message打印信息或者像我之前给的模板里那样临时启用变量打印查看PINOCCHIO_INCLUDE_DIRS和PINOCCHIO_LIBRARIES这两个关键变量是否被正确设置。确保target_link_libraries中链接的目标名称正确。现代Pinocchio提供的导入目标通常是pinocchio::pinocchio。问题二运行时加载URDF失败。现象程序崩溃提示Failed to parse URDF file或std::runtime_error。排查绝对路径 vs 相对路径最最常见的问题确保你传给loadURDF或buildModel的字符串路径是正确的。使用std::filesystem::exists函数检查文件是否存在。在CMake中有时需要将资源文件如URDF复制到构建目录build下或者使用configure_file命令。URDF语法错误URDF是XML格式一个标签不闭合或属性错误就会导致解析失败。可以用check_urdf工具sudo apt install liburdfdom-tools检查check_urdf your_robot.urdf。网格文件缺失如果URDF里引用了mesh filenamepackage://my_robot/meshes/leg.stl/你需要确保ROS_PACKAGE_PATH环境变量包含了my_robot包所在的路径或者将mesh路径改为绝对路径。问题三计算出的雅可比矩阵或速度全是零或者数值明显不对。现象程序能跑但结果不符合物理直觉。排查检查关节角度q和速度dq的单位Pinocchio默认使用弧度rad和弧度/秒rad/s。如果你从度°转换而来别忘了乘以M_PI/180.0。检查帧ID用model.getFrameId(“frame_name”)获取ID后判断一下返回值是否小于model.frames.size()。如果等于说明没找到函数返回了默认值通常是frames的大小。这时你需要核对URDF中帧的名字。验证正向运动学在计算雅可比之前先打印末端执行器的位置data.oMf[frame_id].translation()。看看这个位置随q变化是否合理。如果位置都不对那雅可比肯定不对。检查模型自由度打印model.nv确保你创建的q和dq向量长度与之匹配。一个常见的错误是混淆了nq关节配置向量维度对于旋转关节通常是nv但对于四元数表示的浮动基会不同和nv关节速度向量维度即自由度。问题四程序运行缓慢。现象计算一次雅可比矩阵耗时很长。排查确保是Release编译在Debug模式下Eigen和Pinocchio会有很多断言检查速度慢很多。使用cmake -DCMAKE_BUILD_TYPERelease ..来配置Release构建。避免重复计算pinocchio::forwardKinematics和pinocchio::updateFramePlacements是计算雅可比的前提。如果你在同一姿势q下需要计算多个帧的雅可比只需调用一次正向运动学然后分别计算各帧雅可比即可。使用更高效的算法Pinocchio提供了computeJointJacobians函数一次性计算所有关节的雅可比然后再用getFrameJacobian提取特定帧的雅可比。在需要多个帧雅可比时这比多次调用computeFrameJacobian更高效。调试时养成使用std::cout或日志库打印关键中间变量如qdq 帧ID 末端位置的习惯。把复杂问题分解先确保模型加载和正向运动学正确再攻破雅可比矩阵计算。