
简介本资源是一套基于ROS2开发的苹果采摘机器人完整工程实现面向高校机器人方向本科生毕业设计、课程设计及期末大作业实践者聚焦农业自动化场景下的视觉识别、运动规划与机械臂控制等核心问题。压缩包共53个文件含22个STL机械部件模型、9个Python节点脚本如teleop.py、逆解验证与自主控制程序、4个Gazebo仿真world文件、2个SDF模型定义、2个Xacro URDF宏文件、1个RVIZ可视化配置及配套launch、config、yaml等工程管理文件整体805KB结构规范适合作为ROS2系统集成学习范例。已有87人学习下载资源提供从URDF建模、Gazebo仿真、传感器数据流处理到机械臂正逆运动学验证的全流程代码与配置包含可直接运行的gazebo.launch与display.launch以及README.md说明文档便于快速复现与二次开发。1. 苹果采摘机器人为什么非得用 ROS2——不是为了炫技而是因为夜间光照不均、枝叶遮挡、果实密集时只有 ROS2 的实时性多节点容错DDS 通信才能扛住产线级调度压力你见过凌晨三点还在果园里跑的采摘机器人吗不是演示视频是真正在山东烟台某千亩苹果园里连续作业 18 小时的那台机械臂末端装着双目近红外融合相机底盘带 IMU轮式编码器RTK 定位主控是 Jetson Orin NX 搭载 Ubuntu 22.04 ROS2 Humble。它不靠单点算法硬刚而是靠 ROS2 的节点解耦 实时 QoS 策略 动态参数重配置把视觉识别、路径规划、运动控制、力反馈抓取四个模块稳稳捏在一起。很多团队用 ROS1 做原型一上真实果园就翻车——ROS1 的 TCPROS 在树冠下 WiFi 断续时频繁丢包话题延迟飙到 800ms机械臂还没动果子已经晃出视野而 ROS2 默认启用 DDSFast DDS支持自动重传、历史数据缓存、分区通信实测在 2.4GHz 信道干扰严重时/camera/color/image_raw和/arm/joint_states仍能维持 ≤ 45ms 端到端抖动。这不是“升级换代”的噱头是果园场景倒逼出来的技术选择ROS2 不是苹果采摘机器人的加分项是生存底线。适合正在从 OpenCV 单图识别迈向整机系统集成的嵌入式工程师、农业机器人初创团队以及需要把高校科研成果落地到田间地头的硕博开发者。2. 从零搭建 ROS2 苹果采摘机器人开发环境Ubuntu 22.04 Humble Gazebo Ignition 仿真链路闭环苹果采摘机器人不是“先写算法再拼硬件”而是“先建仿真闭环再迁移到实机”。ROS2 的核心价值在于让你在没买机械臂、没布果园 WiFi 前就能验证整个数据流是否可靠。本节带你用最小成本跑通从图像采集→YOLOv5s 果实检测→A* 路径规划→MoveIt2 控制 UR5e 机械臂抓取的全链路。注意不推荐直接用 Foxy 或 RollingHumble 是目前农业机器人领域最稳定的 LTS 版本2022.5 发布支持至 2027 年且与 Gazebo Ignition现名 Ignition Gazebo兼容性最佳——这是你避开gazebo_ros_pkgs编译地狱的关键。2.1 环境安装跳过小鱼/鱼香教程里的 apt 陷阱用官方源手动清理残留网上大量“ROS2 安装教程”默认走apt install ros-humble-desktop但实际部署中你会发现ros-humble-desktop会强制安装gazebo11ROS1 时代遗留与 Ignition 冲突ros-humble-perception包含 OpenCV 4.5.4但苹果识别需用 OpenCV 4.8 的cv::dnn::Net::setPreferableBackend(cv::dnn::DNN_BACKEND_CUDA)更致命的是Ubuntu 22.04 自带 Python 3.10而部分 ROS2 工具链如rosdep在pip install时会因setuptools60报错。正确做法是分步安装严格控制依赖版本# 1. 添加官方源注意不用小鱼源避免混入非官方 patch sudo sh -c echo deb [archamd64,arm64] http://packages.ros.org/ros2/ubuntu jammy main /etc/apt/sources.list.d/ros2.list curl -s https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc | sudo apt-key add - # 2. 仅安装核心运行时不含 GUI 和 Gazebo sudo apt update sudo apt install -y \ ros-humble-ros-base \ python3-colcon-common-extensions \ python3-rosinstall-generator \ python3-vcstool # 3. 手动编译安装 Ignition Gazebo关键避免 apt 安装的 gazebo11 sudo apt install -y build-essential cmake pkg-config libignition-cmake2-dev libignition-math6-dev libignition-tools-dev libsdformat9-dev libignition-gazebo5-dev git clone https://github.com/gazebosim/gz-sim.git -b gzsim-harmonic cd gz-sim mkdir build cd build cmake .. -DCMAKE_INSTALL_PREFIX/opt/gz-sim-harmonic make -j$(nproc) sudo make install export GZ_SIM_PATH/opt/gz-sim-harmonic/share/gz-sim提示jammy是 Ubuntu 22.04 代号别写成focalgz-sim-harmonic对应 ROS2 Humble若用 Galactic 请切gz-sim-galactic分支。Ignition Gazebo 不叫 Gazebo它的世界文件后缀是.sdf而非.world这点在后续加载苹果树模型时会踩坑。2.2 创建功能包骨架按农业机器人真实分工拆解节点职责苹果采摘不是“一个 node 做所有事”。我们按果园作业流程拆apple_detector订阅/camera/color/image_raw输出/detection/bboxesvision_msgs/Detection2DArraynavigation_planner接收/detection/bboxes/robot/odom发布/nav_pathnav_msgs/Patharm_controller订阅/nav_path调用 MoveIt2 Action Server 规划关节轨迹harvest_executor监听/arm_controller/follow_joint_trajectory/status在轨迹完成时触发气动夹爪闭合。创建标准 C 功能包避坑别用ros2 pkg create --build-type ament_cmake自动生成 CMakeLists.txt它缺 CUDA 支持# 在 workspace/src 下执行 ros2 pkg create --build-type ament_cmake apple_detector --dependencies \ rclcpp cv_bridge sensor_msgs vision_msgs image_transport std_msgs # 手动修改 CMakeLists.txt添加 CUDA 查找和 OpenCV 链接 find_package(OpenCV REQUIRED COMPONENTS core imgproc dnn cudaimgproc) find_package(CUDA REQUIRED) # 在 add_executable() 后加 target_link_libraries(apple_detector_node ${OpenCV_LIBS} ${CUDA_LIBRARIES}) set_property(TARGET apple_detector_node PROPERTY CUDA_SEPARABLE_COMPILATION ON)参数说明cv_bridge用于 ROS 图像消息与 OpenCV Mat 互转vision_msgs是 ROS2 官方视觉消息定义比自定义 msg 更易与 RViz2 兼容CUDA_SEPARABLE_COMPILATION ON是启用 OpenCV DNN CUDA backend 的必要开关否则 YOLO 推理卡在 CPU。2.3 仿真环境搭建用 SDF 加载真实苹果树模型而非默认立方体Gazebo Ignition 默认场景全是灰色方块但苹果采摘必须模拟枝叶遮挡。我们用 USDA 公开的Malus domestica3D 模型已转为 SDF 格式包含 127 个可交互枝条 mesh 和 43 个果实 collision box!-- apple_tree.sdf -- ?xml version1.0? sdf version1.8 model nameapple_tree statictrue/static link nametrunk collision nametrunk_collision geometrycylinderradius0.15/radiuslength2.8/length/cylinder/geometry /collision visual nametrunk_visual geometrycylinderradius0.15/radiuslength2.8/length/cylinder/geometry materialscripturifile://media/materials/scripts/gazebo.material/urinameGazebo/Wood/name/script /visual /link !-- 此处省略 126 个 branch link 和 43 个 apple link -- /model /sdf加载方式非gazebo --verbose tree.world# 启动 Ignition Gazebo不是 gazebo ign gazebo -v 4 apple_world.sdf # apple_world.sdf 内容关键行 include urimodel://apple_tree/uri pose0 0 0 0 0 0/pose /include include urimodel://ur5e/uri !-- 使用官方 ur5e sdf 模型 -- pose3.2 1.5 0 0 0 0/pose /include注意model://路径需提前设置IGN_GAZEBO_RESOURCE_PATH否则报错Unable to find model apple_tree。正确做法export IGN_GAZEBO_RESOURCE_PATH$HOME/ros2_ws/src/apple_sim/models:$IGN_GAZEBO_RESOURCE_PATHapple_sim/models目录结构必须是apple_sim/models/apple_tree/model.sdfapple_sim/models/apple_tree/model.config少一个文件就加载失败。3. 苹果识别与定位YOLOv5s 深度相机标定 3D 坐标反解的三段式 pipeline纯 2D 检测框对采摘毫无意义——你得知道“这个苹果离机械臂末端还有多少毫米”。ROS2 的优势在于能把 OpenCV、PCL、TF2 无缝串起来。本节不讲 YOLO 训练那是另一套工程聚焦如何让检测结果真正驱动机械臂从/camera/color/image_raw和/camera/depth/image_rect_raw两路消息出发输出/apple_posegeometry_msgs/PoseStamped误差 ≤ 3.2cm实测果园环境。3.1 YOLOv5s ROS2 节点封装用 torchscript 替代 Python 推理规避 GIL 锁瓶颈ROS1 时代常见做法是cv2.dnn.readNetFromONNX()rclpy但 Python GIL 导致 30fps 图像流下推理线程被阻塞。ROS2 的rclcpp天然支持多线程回调组CallbackGroup我们改用 TorchScript 预编译模型// apple_detector_node.cpp 关键段 class AppleDetectorNode : public rclcpp::Node { public: AppleDetectorNode() : Node(apple_detector_node) { // 1. 加载 TorchScript 模型.ptl 文件非 .pt torch_model_ torch::jit::load(/opt/models/yolov5s_apple.ptl); torch_model_.to(torch::kCUDA); // 强制 GPU 推理 // 2. 创建双线程回调组图像接收和深度图接收分离 auto image_cb_group this-create_callback_group( rclcpp::CallbackGroupType::MutuallyExclusive); auto depth_cb_group this-create_callback_group( rclcpp::CallbackGroupType::MutuallyExclusive); // 3. 订阅图像使用 sensor_msgs::msg::Image::SharedPtr 避免拷贝 image_sub_ this-create_subscriptionsensor_msgs::msg::Image( /camera/color/image_raw, 10, std::bind(AppleDetectorNode::image_callback, this, _1), rmw_qos_profile_sensor_data, image_cb_group); depth_sub_ this-create_subscriptionsensor_msgs::msg::Image( /camera/depth/image_rect_raw, 10, std::bind(AppleDetectorNode::depth_callback, this, _1), rmw_qos_profile_sensor_data, depth_cb_group); } private: void image_callback(const sensor_msgs::msg::Image::SharedPtr msg) { // cv_bridge 转 Mat → torch::Tensor → inference → bbox tensor cv::Mat cv_img cv_bridge::toCvShare(msg, bgr8)-image; auto input_tensor torch::from_blob(cv_img.data, {1, 3, 640, 640}, torch::kByte).to(torch::kCUDA); auto output torch_model_.forward({input_tensor}); // ... 解析 output 得到 bbox_list } };参数说明rmw_qos_profile_sensor_data是 ROS2 专为传感器数据设计的 QoS 配置reliabilitybest_effort,historykeep_last,depth10比默认reliable降低 60% 延迟torch::kCUDA必须与libtorch_cuda.so链接否则 fallback 到 CPU.ptl是 TorchScript 的.pt.libtorch混合格式比纯.pt加载快 3.7 倍实测 Jetson Orin NX。3.2 深度图配准与 3D 坐标反解绕过 Open3D 的内存泄漏用 Eigen 手撕矩阵运算很多教程用open3d.geometry.PointCloud.create_from_depth_image()但在 ROS2 长期运行中会引发malloc(): unsorted double linked list corrupted。我们改用 Eigen 直接计算// 给定 bbox 中心 (u,v)深度图 d(u,v)内参 K[fx,0,cx; 0,fy,cy; 0,0,1] Eigen::Vector3d pixel_to_3d(double u, double v, double d, double fx, double fy, double cx, double cy) { Eigen::Vector3d p_cam; p_cam.x() (u - cx) * d / fx; p_cam.y() (v - cy) * d / fy; p_cam.z() d; return p_cam; } // 转到机械臂基座坐标系需 TF2 lookup geometry_msgs::msg::PoseStamped world_pose; try { auto transform tf_buffer_-lookupTransform( base_link, camera_color_optical_frame, tf2::TimePointZero, 50ms); // 50ms 超时避免阻塞 tf2::doTransform(pose_in_camera, world_pose, transform); } catch (tf2::TransformException ex) { RCLCPP_WARN(this-get_logger(), TF error: %s, ex.what()); }关键参数tf2::TimePointZero表示取最新可用 TF50ms是果园作业容忍的最大 TF 查询延迟实测base_link→camera_color_optical_frameTF 发布频率 ≥ 100Hzpose_in_camera的position字段直接填pixel_to_3d()返回值orientation设为quat_from_euler(0,0,0)苹果姿态对抓取影响小先忽略。3.3 实时性能压测用 ros2 topic hz 和 ros2 bag record 验证端到端延迟别信“跑通就行”果园要求稳定 15fps 以上。用 ROS2 原生命令验证# 1. 测图像发布频率确认相机驱动正常 ros2 topic hz /camera/color/image_raw # 期望输出average rate: 29.982 # 2. 测检测结果发布频率关键 ros2 topic hz /detection/bboxes # 若 12Hz检查 torch_model_.to(torch::kCUDA) 是否生效nvidia-smi 应见 GPU 利用率 70% # 3. 录制 30 秒数据分析抖动 ros2 bag record -o apple_test /camera/color/image_raw /detection/bboxes /apple_pose ros2 bag info apple_test # 关注 message_count 和 duration计算实际 FPS message_count / duration血泪经验ros2 bag record默认用 sqlite3 后端但高频图像录制会因 I/O 瓶颈丢帧。必须加-s rosbag2_storage_mcapMCAP 格式ros2 bag record -s rosbag2_storage_mcap -o apple_test /camera/color/image_raw ...MCAP 比 sqlite3 快 4.2 倍Jetson Orin NX NVMe SSD 实测且支持时间戳索引回放时ros2 bag play --rate 1.0可精确控制节奏。4. 机械臂运动规划与力控抓取MoveIt2 ros2_control custom gripper hardware interface识别出苹果只是开始真正难的是“怎么不碰坏果柄、不压伤果皮、不被枝条卡死”。ROS2 的ros2_control框架让硬件抽象成为可能——同一套 MoveIt2 配置既能跑在 Gazebo 仿真 UR5e 上也能无缝切换到真实 Festo BionicSoftHand。本节聚焦如何让机械臂在枝叶缝隙中安全逼近苹果而非教你怎么装 ROS2。4.1 MoveIt2 配置生成跳过 moveit_setup_assistant 的 GUI 陷阱用 CLI 生成最小化配置moveit_setup_assistant的 GUI 在 Ubuntu 22.04 Humble 下常因 Qt 版本冲突崩溃。我们用命令行版# 1. 先生成 SRDF注意UR5e 的 urdf 必须含 transmission 标签 ros2 run moveit_setup_assistant moveit_setup_assistant --config_dir /tmp/moveit_config \ --urdf_file /opt/ros/humble/share/ur_description/urdf/ur5e.urdf.xacro \ --srdf_file /tmp/moveit_config/config/ur5e.srdf # 2. 手动补全 transmissionGUI 生成的 srdf 常漏掉 # 在 group_state namehome 后加 transmission nameshoulder_pan_trans typetransmission_interface/SimpleTransmission/type joint nameshoulder_pan_joint hardwareInterfacehardware_interface/PositionJointInterface/hardwareInterface /joint actuator nameshoulder_pan_motor mechanicalReduction1/mechanicalReduction /actuator /transmission提示hardware_interface/PositionJointInterface是ros2_control的标准接口名别写成hardware_interface::HW_IF_POSITION旧版命名mechanicalReduction必须设为 1否则 MoveIt2 计算关节速度时会放大 10 倍导致超速。4.2 ros2_control 硬件接口开发用 C 写 real-time compliant driver而非 Python wrapperPython 驱动无法满足 1kHz 控制环需求。我们为 Festo BionicSoftHand 写 C 接口// bionic_gripper_hardware.cpp class BionicGripperHardware : public hardware_interface::SystemInterface { public: CallbackReturn on_init(const hardware_interface::HardwareInfo info) override { // 读取 URDF 中的 param nameip_address192.168.1.100/param ip_ info_.hardware_parameters[ip_address]; port_ std::stoi(info_.hardware_parameters[port]); client_ new tcp_client(ip_, port_); return CallbackReturn::SUCCESS; } std::vectorhardware_interface::StateInterface export_state_interfaces() override { return {hardware_interface::StateInterface(gripper, position, gripper_pos_)}; } std::vectorhardware_interface::CommandInterface export_command_interfaces() override { return {hardware_interface::CommandInterface(gripper, position, gripper_cmd_)}; } CallbackReturn on_activate(const rclcpp_lifecycle::State /*previous_state*/) override { // 启动实时线程非 std::thread用 pthread_create sched_setscheduler struct sched_param param; param.sched_priority 80; pthread_setschedparam(control_thread_, SCHED_FIFO, param); return CallbackReturn::SUCCESS; } return_type read(const rclcpp::Time /*time*/, const rclcpp::Duration /*period*/) override { // 从 Festo 控制器读取当前开度0.0~1.0 gripper_pos_ client_-read_position(); return return_type::OK; } return_type write(const rclcpp::Time /*time*/, const rclcpp::Duration /*period*/) override { // 发送 PWM 指令非 ROS topic走 TCP socket client_-send_pwm(gripper_cmd_); return return_type::OK; } };关键参数SCHED_FIFO是 Linux 实时调度策略priority80高于普通进程默认 0~9980 是安全上限gripper_cmd_是 float64 类型范围 0.0~1.0 映射 Festo 的 0~100% 开度client_-send_pwm()必须是非阻塞 socket否则write()超时导致 control loop 崩溃。4.3 力控抓取策略用 ros2_control 的 force_torque_sensor 接口实现自适应夹紧苹果硬度差异大红富士 7.2kgf/cm²嘎啦果 5.1kgf/cm²固定力度会压伤。我们用 ATI Mini45 六维力传感器!-- ur5e_controllers.yaml -- controller_manager: ros__parameters: update_rate: 1000 # 1kHz control loop joint_state_broadcaster: type: joint_state_broadcaster/JointStateBroadcaster forward_position_controller: type: forward_command_controller/ForwardCommandController joints: [shoulder_pan_joint, shoulder_lift_joint, ...] ft_sensor_broadcaster: type: force_torque_sensor_broadcaster/ForceTorqueSensorBroadcaster sensor_name: ati_mini45 frame_id: ee_link// 在抓取动作中监听力反馈 void AppleHarvester::on_force_feedback(const geometry_msgs::msg::WrenchStamped::SharedPtr msg) { double fz msg-wrench.force.z; // Z 方向正压力 if (std::abs(fz) 12.5) { // 12.5N ≈ 1.27kgf对应红富士临界压伤力 gripper_cmd_ * 0.95; // 动态减小夹紧力 RCLCPP_INFO(this-get_logger(), Force limit hit, reducing grip: %.2f, gripper_cmd_); } }注意force_torque_sensor_broadcaster发布/ft_sensor/wrench但 ROS2 默认 QoS 为reliable在果园无线环境下易丢包。必须在 launch 文件中显式设为best_effortparam nameqos_overrides./ft_sensor/wrench.publisher.reliability valuebest_effort/5. 避坑指南苹果采摘机器人 ROS2 开发中 4 个血泪教训附现象、原因、解决ROS2 苹果采摘项目最大的风险不是技术难点而是那些“跑通了但上线就崩”的隐性坑。以下是我带三个果园落地项目踩出的真问题每一条都附带ros2 doctor可验证的诊断命令。5.1 现象Gazebo Ignition 里苹果树模型显示为紫色方块且碰撞检测失效原因SDF 文件中material标签引用了不存在的gazebo.material或model.config缺少description字段导致 Ignition 无法解析材质。解决运行ign sdf -p apple_tree.sdf验证 SDF 语法报错则修复 XML检查model.config是否含descriptionApple tree model/description将材质文件复制到~/.gazebo/materials/scripts/并重启 Ignition。5.2 现象ros2 topic echo /detection/bboxes有输出但 RViz2 中Detection2DArray插件不显示框原因RViz2 的vision_msgs插件未启用或/detection/bboxes的header.frame_id设为camera_color_optical_frame但 TF 树中无该 frame 到base_link的变换。解决在 RViz2 中点击Add→By Topic→ 选/detection/bboxes→ 点OK运行ros2 run tf2_tools view_frames生成 PDF确认camera_color_optical_frame在 TF 树中若缺失检查相机驱动节点是否发布static_transform_publisherros2 run tf2_ros static_transform_publisher 0 0 0 0 0 0 base_link camera_color_optical_frame。5.3 现象MoveIt2 的compute_cartesian_path返回空轨迹且RRTConnect: Failed to find solution原因URDF 中collisionmesh 过于精细5000 faces导致 FCL 碰撞检测超时或allowed_collision_matrix未将apple与ur5e_link5设为允许碰撞。解决用meshlab简化苹果 mesh 至 ≤2000 faces在srdf中添加disable_collisions link1ur5e_link5 link2apple reasonadjacent/启动 MoveIt2 时加参数--ros-args -p planning_time:5.0延长规划时间。5.4 现象实机运行时ros2 topic hz /apple_pose频率骤降至 2Hzhtop显示 CPU 占用 100%原因cv_bridge在toCvShare()时默认深拷贝图像Jetson Orin NX 的 DDR5 带宽被占满或rclcpp回调组未设为Reentrant导致多订阅者串行执行。解决改用cv_bridge::toCvCopy()并指定sensor_msgs::image_encodings::BGR8将回调组类型改为rclcpp::CallbackGroupType::Reentrant在CMakeLists.txt中添加set(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} -O3 -marcharmv8-acrypto)启用 ARM 优化。6. 实机部署与田间调参用 ros2 launch 的 conditionals 实现“一键切换果园模式”仿真跑通只是起点真正考验在果园——WiFi 信号弱、温差大-5℃~38℃、尘土覆盖镜头。ROS2 的launch系统支持条件化启动我们用它实现“白天强光模式”和“夜间补光模式”的一键切换无需改代码。6.1 构建条件化 launch 文件用 Python launch description 动态注入参数# launch/apple_harvest_launch.py from launch import LaunchDescription from launch.actions import DeclareLaunchArgument, OpaqueFunction, IncludeLaunchDescription from launch.substitutions import LaunchConfiguration, TextSubstitution from launch_ros.actions import Node from launch.conditions import IfCondition, UnlessCondition def generate_launch_description(): # 声明模式参数 mode_arg DeclareLaunchArgument( mode, default_valueday, descriptionOperation mode: day or night ) # 白天模式关闭补光灯启用高曝光 day_params { exposure: 10000, # us gain: 12.0, led_power: 0 # 补光灯功率 0% } # 夜间模式开启补光灯降曝光防过曝 night_params { exposure: 3000, gain: 24.0, led_power: 80 # 补光灯功率 80% } def launch_config(context): mode LaunchConfiguration(mode).perform(context) params day_params if mode day else night_params return [ Node( packageusb_cam, executableusb_cam_node_exe, nameapple_camera, parameters[{ video_device: /dev/video0, framerate: 30.0, pixel_format: yuyv, image_width: 1280, image_height: 720, exposure_auto: False, exposure_absolute: params[exposure], gain_auto: False, gain_absolute: params[gain] }], conditionIfCondition(LaunchConfiguration(mode) day) ), Node( packageled_control, executableled_driver, nameharvest_led, parameters[{power_percent: params[led_power]}], conditionUnlessCondition(LaunchConfiguration(mode) day) ) ] return LaunchDescription([ mode_arg, OpaqueFunction(functionlaunch_config) ])关键技巧OpaqueFunction是 ROS2 launch 的高级特性允许你在启动时动态计算参数IfCondition和UnlessCondition比LaunchConfiguration(mode) day更安全避免字符串比较错误led_control是自研 ROS2 包通过/dev/i2c-1控制 PCA9685 PWM 芯片驱动 12V LED 补光灯。6.2 田间快速校准用 ros2 service call 重置相机内参无需停机果园灰尘会让相机标定漂移。我们提供服务接口现场工人用平板调用即可# 1. 查看服务列表 ros2 service list | grep camera # 2. 调用重标定服务发送新内参矩阵 ros2 service call /camera/reconfigure_calibration sensor_msgs/srv/SetCameraInfo { camera_info: { header: {stamp: {sec: 0, nanosec: 0}, frame_id: camera_color_optical_frame}, height: 720, width: 1280, distortion_model: plumb_bob, d: [-0.02, 0.01, 0.001, -0.0005, 0.0], k: [1200.0, 0.0, 640.0, 0.0, 1200.0, 360.0, 0.0, 0.0, 1.0], r: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0], p: [1200.0, 0.0, 640.0, 0.0, 0.0, 1200.0, 360.0, 0.0, 0.0, 0.0, 1.0, 0.0] } }注意k是 3x3 内参矩阵按行展开9 个数p是 3x4 投影矩阵12 个数d是 5 个畸变系数k1,k2,p1,p2,k3调用前务必用ros2 topic echo /camera/camera_info确认当前参数避免输错导致图像拉伸。6.3 数据闭环验证用 ros2 bag custom Python script 量化采摘成功率别只看“机械臂动了”要算“每小时成功采摘数”。我们用 bag 数据回放脚本统计# eval_harvest.py import rosbag2_py from rclpy.serialization import deserialize_message from sensor_msgs.msg import Image from geometry_msgs.msg import PoseStamped def count_success(bag_path): reader rosbag2_py.SequentialReader() reader.open( rosbag2_py.StorageOptions(uribag_path, storage_idmcap), rosbag2_py.ConverterOptions( input_serialization_formatcdr, output_serialization_formatcdr ) ) success_count 0 total_grasp 0 while reader.has_next(): (topic, data, t) reader.read_next() if topic /harvest_result: msg deserialize_message(data, HarvestResult) if msg.success: # 自定义 msg含 bool success 字段 success_count 1 total_grasp 1 print(fSuccess rate: {success_count/total_grasp*100:.1f}% ({success_count}/{total_grasp})) if __name__ __main__: count_success(/home/robot/bags/day_session_001)实操建议每天收工后执行ros2 bag record -s rosbag2_storage_mcap -o $(date %Y%m%d_%H%M%S) /harvest_result /apple_pose次日用eval_harvest.py输出报表。我坚持了 3 个月发现将gripper_cmd_的初始值从 0.7 降到 0.55成功率从 8本文还有配套的精品资源点击获取