1. 为什么我们需要在仿真中构建双目相机?

大家好,我是老张,在机器人仿真和视觉这块儿摸爬滚打了十来年。今天咱们不聊那些虚头巴脑的理论,直接上手干一件特别实用的事儿:在 Gazebo 里,从零开始给一个机器人(比如咱们常用的 XBot-U)装上一双“眼睛”——也就是双目相机,并且让 ROS 能顺畅地接收到它看到的图像。

你可能会问,我直接在真机上装两个摄像头不香吗?为啥非得在仿真里折腾?这事儿我深有体会。几年前我们团队做一个室内的视觉导航项目,真机调试那叫一个痛苦。硬件动不动就出问题,相机标定稍微动一下就得重来,更别提写算法时,想换个视角或者光照条件,那成本可就高了。仿真环境,特别是像 Gazebo 这样成熟的物理仿真器,就成了我们的“救命稻草”。它允许你在一个完全可控、可重复、零硬件成本的环境里,去设计、测试和验证你的视觉算法。比如,你可以轻松模拟不同天气(晴天、雾天)、不同光照(强光、弱光)、甚至是相机安装位置微调对算法的影响,这在真实世界里是不可想象的。

双目视觉的核心是立体感知,通过两个有一定距离(这个距离叫基线)的相机,从略有差异的视角观察同一场景,从而计算出物体的深度信息,也就是距离。这就像我们人的两只眼睛,能判断物体的远近。在机器人领域,这能力至关重要,无论是避障、导航、抓取,还是构建环境地图,都离不开它。

所以,掌握在 Gazebo 中构建双目相机仿真的全流程,就相当于给你手里的机器人项目加上了一个强大的、可任意“折腾”的视觉实验室。接下来,我会手把手带你走通从编写相机模型(SDF)、集成到机器人、再到用 ROS 节点读取图像的每一个步骤,过程中我踩过的坑、总结的技巧,都会毫无保留地分享给你。

2. 环境准备:搭建你的仿真舞台

工欲善其事,必先利其器。在开始编写双目相机之前,我们得先把“舞台”搭好。这里我推荐使用一个非常友好的入门级仿真环境:ROS-Academy-for-Beginners。它集成了 Gazebo、ROS 和一些常用的机器人模型(包括我们后面会用到的 XBot-U),省去了我们自己从零配置 Gazebo 模型世界的麻烦。

2.1 安装基础环境

首先,确保你的系统已经安装了 ROS(我以 ROS Noetic 为例,其他版本类似)和 Gazebo(通常随 ROS 桌面完整版安装)。然后,我们来获取这个仿真教程包。

打开终端,找一个你喜欢的工作空间,比如 ~/catkin_ws/src,然后执行:

cd ~/catkin_ws/src
git clone https://github.com/DroidAITech/ROS-Academy-for-Beginners.git
cd ~/catkin_ws
catkin_make

catkin_make 编译完成后,别忘了 source 一下 setup 文件,让 ROS 能找到我们的新包:

source ~/catkin_ws/devel/setup.bash

为了以后打开新终端不用每次都 source,你可以把这行命令加到你的 ~/.bashrc 文件末尾。

2.2 验证仿真环境

环境装好了,我们先跑起来看看效果。启动仿真场景的命令是:

roslaunch robot_sim_demo robot_spawn.launch

这个命令会启动 Gazebo,并加载一个包含 XBot-U 机器人的仿真世界。第一次启动可能会慢一些,因为 Gazebo 需要在线下载一些模型。如果遇到模型下载卡住的情况,别慌,这是 Gazebo 的老问题了。你可以按 Ctrl+C 终止,然后尝试预先下载所有模型:

# 在终端中执行,可能需要一些时间
curl -sSL http://osrf-distributions.s3.amazonaws.com/gazebo/models/gazebo_models.tar.gz | tar -xvz -C ~/.gazebo/

或者,更一劳永逸的方法是,找到并编辑 ~/.ignition/fuel/config.yaml 文件(对于 Gazebo 11+),将 url: https://fuel.ignitionrobotics.org 替换为 url: https://fuel.gazebosim.org。这个源在国内访问通常会更稳定。

当 Gazebo 界面成功弹出,你看到一个蓝色的 XBot-U 小车站在一个简单的室内场景中时,恭喜你,舞台已经就绪!你可以用键盘(ikjl 等键)控制小车前后左右移动一下,感受一下仿真的交互。接下来,我们就要给这个小车装上“眼睛”了。

3. 核心:编写双目相机的 SDF 模型文件

SDF(Simulation Description Format)是 Gazebo 世界的“通用语言”,它用 XML 格式描述机器人、传感器、灯光等所有模型及其物理属性。给机器人添加传感器,本质上就是在它的 SDF 描述里,加入对应的传感器标签。对于双目相机,Gazebo 提供了一个非常方便的 multicamera 传感器类型,它可以让我们在一个传感器标签下定义多个相机,并自动处理它们之间的位姿关系。

3.1 理解 SDF 结构

我们先来看一个完整的双目相机 SDF 代码块,我会逐段拆解,告诉你每个参数是干嘛的,以及我调试时总结的经验值。

<!-- stereo camera -->
<gazebo reference="camera_link">
  <sensor type="multicamera" name="stereocamera">
    <always_on>true</always_on>
    <update_rate>10</update_rate>
    <visualize>false</visualize>
  • <gazebo reference="camera_link">:这是关键!它指定了这个传感器插件要“附着”在机器人的哪个连杆(link)上。camera_link 需要是你机器人 URDF/Xacro 文件中定义好的一个连杆名。通常,我们会在机器人的头部或前方定义一个专门用来挂相机的连杆。
  • <sensor type="multicamera" ...>:声明这是一个多相机传感器。
  • <always_on>true</always_on>:让相机在仿真中持续工作。
  • <update_rate>10</update_rate>:相机的发布频率,单位是 Hz。这里设为 10,意味着每秒发布 10 帧图像。这个值不是越高越好,要兼顾仿真性能和你的算法需求。对于测试,10Hz 足够了。
  • <visualize>false</visualize>:是否在 Gazebo 的 GUI 里显示相机的视锥体(一个锥形的线框)。通常我们设为 false,避免界面过于杂乱。

3.2 配置左右相机参数

接下来,在 <sensor> 标签内部,我们定义两个子相机:左目和右目。

    <camera name="left">
      <pose>0 0 0 0 0 0</pose>
      <horizontal_fov>1.047</horizontal_fov>
      <image>
        <width>320</width>
        <height>240</height>
      </image>
      <clip>
        <near>0.1</near>
        <far>100</far>
      </clip>
    </camera>
    <camera name="right">
      <pose>0 -0.07 0 0 0 0</pose>
      <horizontal_fov>1.047</horizontal_fov>
      <image>
        <width>320</width>
        <height>240</height>
      </image>
      <clip>
        <near>0.1</near>
        <far>100</far>
      </clip>
    </camera>
  • <camera name="left/right">:给相机起个名字,这个名字会体现在后续 ROS 的话题名里。
  • <pose>:这是**相对于父连杆(camera_link)**的位姿。格式是 x y z roll pitch yaw(单位:米和弧度)。
    • 左目相机:0 0 0 0 0 0,表示它就在 camera_link 的原点,姿态一致。
    • 右目相机:0 -0.07 0 0 0 0。这里 y 坐标是 -0.07 米。在 Gazebo/ROS 的坐标系里(遵循右手定则,X 向前,Y 向左,Z 向上),这意味着右目相机在左目相机的左侧 7 厘米处。这个 0.07 米就是双目相机的基线(Baseline),是后续计算深度的关键参数。这里有个经典大坑:很多人以为 y 负值是往右,结果基线算反了。记住,Y 轴负方向是机器人的左侧。
  • <horizontal_fov>:水平视场角,单位弧度。1.047 弧度约等于 60 度,是一个比较常见的广角视角。
  • <image>:定义图像分辨率。为了仿真速度,我们先用小分辨率 320x240 测试。没问题后再可以调高,比如 640x480。
  • <clip>:定义相机的可视范围。<near> 是最近能看到物体的距离,小于这个距离的物体不会被渲染;<far> 是最远距离。设置合理的范围可以提升渲染效率。

3.3 集成 ROS 插件:让数据流动起来

传感器在 Gazebo 里“看到”了东西,怎么让 ROS 知道呢?这就需要 plugin。Gazebo 提供了 libgazebo_ros_multicamera.so 这个插件,专门用于将多相机传感器的数据发布到 ROS 话题。

    <plugin name="stereo_camera_controller" filename="libgazebo_ros_multicamera.so">
      <cameraName>stereocamera</cameraName>
      <alwaysOn>true</alwaysOn>
      <updateRate>10</updateRate>
      <imageTopicName>image_raw</imageTopicName>
      <cameraInfoTopicName>camera_info</cameraInfoTopicName>
      <frameName>camera_link_optical</frameName>
      <baseline>0.07</baseline>
      <distortion_k1>0.0</distortion_k1>
      <distortion_k2>0.0</distortion_k2>
      <distortion_k3>0.0</distortion_k3>
      <distortion_t1>0.0</distortion_t1>
      <distortion_t2>0.0</distortion_t2>
    </plugin>
  </sensor>
</gazebo>
  • <cameraName>:插件发布的 ROS 话题的命名空间前缀。这里设为 stereocamera,那么左图话题就会是 /stereocamera/left/image_raw
  • <frameName>:这是另一个超级重要的参数,它指定了相机数据发布的坐标系。注意,我们这里写的是 camera_link_optical,而不是 camera_link。为什么?因为 ROS 里的视觉算法(如 OpenCV, image_pipeline)遵循一套光学坐标系标准:Z 轴向前(光轴方向),X 轴向右,Y 轴向下。这和我们机器人常用的 camera_link(Z 轴向上)是不同的。你必须在 URDF 里定义一个 camera_link_optical 连杆,并通过 <joint> 将其与 camera_link 连接,并设置一个旋转关系(通常是将 camera_link 绕 X 轴旋转 -90°,再绕 Z 轴旋转 -90°),来满足这个转换。如果这个错了,后续所有视觉处理都会出问题。
  • <baseline>:再次指定基线长度,单位米。这个值应该和右目相机 <pose> 中的 Y 轴偏移量的绝对值一致(这里是 0.07)。
  • <distortion_k1...t2>:相机的畸变参数。在仿真中,我们通常假设是理想针孔模型,所以都设为 0。如果你需要模拟真实相机的畸变,可以在这里填入标定得到的参数。

把上面所有这些代码块组合起来,就是一个完整的、可用的双目相机 SDF 模型描述了。你可以先把它保存为一个独立的 .sdf 文件,用 Gazebo 打开看看效果,但更常见的做法是把它集成到机器人的描述文件中。

4. 集成:将双目相机“安装”到 XBot-U 机器人

现在我们的“眼睛”零件造好了,得把它“焊”到机器人身上。对于 XBot-U 机器人,在 ROS-Academy-for-Beginners 包里,它的 Gazebo 相关配置通常放在一个叫 xbot-u.gazebo 的文件里(路径可能是 robot_sim_demo/urdf/)。这个文件就是用来补充 URDF 模型在 Gazebo 中仿真所需的属性的,比如传感器、材质、摩擦系数等。

4.1 定位并修改描述文件

首先,找到这个文件:

cd ~/catkin_ws/src/ROS-Academy-for-Beginners/robot_sim_demo/urdf
ls -la | grep xbot-u

你应该能看到 xbot-u.gazeboxbot-u.urdfxbot-u.xacro 文件。我们用文本编辑器打开 xbot-u.gazebo

在这个文件里,你会看到很多 <gazebo> 标签,它们对应着机器人 URDF 中的各个连杆(link)。我们需要做两件事:

  1. 确保有相机安装连杆:在 URDF 文件(xbot-u.urdfxbot-u.xacro)中,找到一个合适的位置(比如小车顶部前方),添加一个用于安装相机的连杆(link)和关节(joint)。如果原模型没有,我们就需要自己加。例如,在 xbot-u.xacro 中增加:

    <!-- 定义相机安装连杆 -->
    <link name="camera_mount">
      <visual>
        <geometry>
          <box size="0.02 0.02 0.02"/>
        </geometry>
      </visual>
      <inertial>
        <mass value="0.01"/>
        <inertia ixx="1e-6" ixy="0" ixz="0" iyy="1e-6" iyz="0" izz="1e-6"/>
      </inertial>
    </link>
    <!-- 将相机安装连杆连接到机器人底盘 -->
    <joint name="camera_mount_joint" type="fixed">
      <parent link="base_link"/> <!-- 假设连接到底盘 -->
      <child link="camera_mount"/>
      <origin xyz="0.2 0 0.1" rpy="0 0 0"/> <!-- 安装在底盘前方上方 -->
    </joint>
    <!-- 定义相机连杆(物理位置) -->
    <link name="camera_link">
      <visual>
        <geometry>
          <box size="0.05 0.1 0.03"/>
        </geometry>
      </visual>
      <inertial>
        <mass value="0.05"/>
        <inertia ixx="1e-5" ixy="0" ixz="0" iyy="1e-5" iyz="0" izz="1e-5"/>
      </inertial>
    </link>
    <!-- 将相机连杆安装到安装架上 -->
    <joint name="camera_joint" type="fixed">
      <parent link="camera_mount"/>
      <child link="camera_link"/>
      <origin xyz="0 0 0" rpy="0 0 0"/>
    </joint>
    <!-- 定义光学坐标系连杆(这是一个虚拟连杆,用于满足ROS光学坐标系) -->
    <link name="camera_link_optical"/>
    <!-- 连接物理坐标系和光学坐标系:这个变换是关键! -->
    <joint name="camera_link_optical_joint" type="fixed">
      <parent link="camera_link"/>
      <child link="camera_link_optical"/>
      <origin xyz="0 0 0" rpy="-1.57079632679 0 -1.57079632679"/> <!-- 绕X转-90°,绕Z转-90° -->
    </joint>
    

    这段代码创建了一个完整的相机安装结构。camera_link 是相机在物理世界中的位置,camera_link_optical 是满足 ROS 视觉标准的光学坐标系。那个 rpy="-1.57079632679 0 -1.57079632679"(即 -π/2, 0, -π/2)的变换是核心,它完成了从 Z-up 到 Z-forward 的转换。

  2. 插入 SDF 代码:在 xbot-u.gazebo 文件中,找到对应 camera_link<gazebo> 标签部分,或者直接在文件末尾的 </robot> 标签之前,插入我们在第三章编写的整个双目相机 SDF 代码块。确保 <gazebo reference="camera_link"> 中的 reference 属性值,就是你 URDF 中定义的相机物理连杆名 camera_link

4.2 验证集成效果

保存所有修改后,我们重新启动仿真,来验证相机是否成功加载。

# 确保在 catkin_ws 下已 source
roslaunch robot_sim_demo robot_spawn.launch

Gazebo 启动后,仔细观察你的 XBot-U 机器人模型。如果一切顺利,你应该能在你定义的 camera_link 位置看到相机模型(一个盒子,如果你在 visual 中定义了的话)。更直接的验证方法是查看 ROS 话题列表。

打开一个新的终端,输入:

rostopic list | grep stereo

你应该能看到类似以下的话题:

/stereocamera/left/camera_info
/stereocamera/left/image_raw
/stereocamera/right/camera_info
/stereocamera/right/image_raw

如果看到了这些话题,恭喜!你的双目相机已经成功“安装”到机器人上,并且 Gazebo 正在通过 ROS 插件发布图像数据。你可以用 rostopic echo /stereocamera/left/camera_info 快速查看一下相机信息,确认帧率、分辨率等参数是否正确。现在,这双“眼睛”已经开始“看”东西了,接下来我们要做的就是写个程序,去“理解”它看到的是什么。

5. 订阅与可视化:用 ROS 节点读取双目图像

传感器数据已经通过话题流出来了,下一步就是写一个 ROS 节点来订阅这些话题,把图像数据拿到手,并显示出来。这是我们验证整个流程是否成功的最后一步,也是后续进行视觉算法开发的基础。

5.1 创建 ROS 功能包与节点

首先,我们在 catkin_ws/src 目录下创建一个新的功能包,专门用来放我们的视觉测试代码。这个包依赖 roscpp, sensor_msgs, cv_bridge, image_transportOpenCV

cd ~/catkin_ws/src
catkin_create_pkg my_stereo_vision roscpp sensor_msgs cv_bridge image_transport
cd my_stereo_vision

然后,在 src 目录下创建一个 C++ 源文件,比如 stereo_viewer.cpp。下面是我写的一个增强版的订阅节点,加了一些注释和错误处理:

#include <ros/ros.h>
#include <sensor_msgs/Image.h>
#include <sensor_msgs/image_encodings.h>
#include <image_transport/image_transport.h> // 方便地订阅和发布图像
#include <cv_bridge/cv_bridge.h> // 在ROS的sensor_msgs/Image和OpenCV的cv::Mat之间转换
#include <opencv2/highgui/highgui.hpp> // OpenCV的GUI模块,用于显示图像
#include <iostream>

// 全局变量,用于存储左右图像的窗口名称
const std::string LEFT_WINDOW = "Left Camera View";
const std::string RIGHT_WINDOW = "Right Camera View";

// 左图像的回调函数
void leftImageCallback(const sensor_msgs::ImageConstPtr& msg)
{
  try
  {
    // 将ROS图像消息转换为OpenCV的BGR8格式图像
    cv_bridge::CvImagePtr cv_ptr = cv_bridge::toCvCopy(msg, sensor_msgs::image_encodings::BGR8);
    
    // 在指定窗口显示图像
    cv::imshow(LEFT_WINDOW, cv_ptr->image);
    // waitKey必须被调用,否则imshow无法更新窗口。参数1表示等待1ms,保持UI响应。
    cv::waitKey(1);
    
    // 可选:在终端打印一些信息,确认收到数据(不要太频繁)
    static int count = 0;
    if (++count % 100 == 0) {
      ROS_INFO_STREAM_ONCE("Left image stream is active. Resolution: " << msg->width << "x" << msg->height);
    }
  }
  catch (cv_bridge::Exception& e)
  {
    // 如果转换失败,打印错误信息
    ROS_ERROR("cv_bridge exception in left callback: %s", e.what());
  }
}

// 右图像的回调函数(与左图类似)
void rightImageCallback(const sensor_msgs::ImageConstPtr& msg)
{
  try
  {
    cv_bridge::CvImagePtr cv_ptr = cv_bridge::toCvCopy(msg, sensor_msgs::image_encodings::BGR8);
    cv::imshow(RIGHT_WINDOW, cv_ptr->image);
    cv::waitKey(1);
    
    static int count = 0;
    if (++count % 100 == 0) {
      ROS_INFO_STREAM_ONCE("Right image stream is active.");
    }
  }
  catch (cv_bridge::Exception& e)
  {
    ROS_ERROR("cv_bridge exception in right callback: %s", e.what());
  }
}

int main(int argc, char** argv)
{
  // 初始化ROS节点,节点名为"stereo_viewer"
  ros::init(argc, argv, "stereo_viewer");
  ros::NodeHandle nh;
  
  // 创建image_transport对象,它提供了更高效的图像传输机制(如压缩)
  image_transport::ImageTransport it(nh);
  
  // 创建OpenCV显示窗口
  cv::namedWindow(LEFT_WINDOW, cv::WINDOW_AUTOSIZE);
  cv::namedWindow(RIGHT_WINDOW, cv::WINDOW_AUTOSIZE);
  
  // 使用image_transport订阅左右相机的图像话题
  // 话题名称必须与Gazebo插件发布的一致
  image_transport::Subscriber sub_left = it.subscribe("/stereocamera/left/image_raw", 1, leftImageCallback);
  image_transport::Subscriber sub_right = it.subscribe("/stereocamera/right/image_raw", 1, rightImageCallback);
  
  ROS_INFO("Stereo image viewer node started. Waiting for images...");
  
  // ros::spin()让程序进入循环,持续监听话题消息,直到节点被关闭
  ros::spin();
  
  // 节点关闭时,销毁OpenCV窗口
  cv::destroyAllWindows();
  return 0;
}

这个节点做了几件比基础版本更稳妥的事:1) 使用 cv_bridge::toCvCopy 安全地转换图像;2) 使用 image_transport 订阅,为将来可能的压缩传输留有余地;3) 加入了初始化的信息打印和错误捕获;4) 规范地创建和销毁 OpenCV 窗口。

5.2 编译与运行测试

接下来,我们需要编辑功能包的 CMakeLists.txt 文件,告诉编译系统如何构建我们的节点。

打开 my_stereo_vision/CMakeLists.txt,在文件末尾添加:

find_package(OpenCV REQUIRED)
include_directories(${OpenCV_INCLUDE_DIRS})

add_executable(stereo_viewer src/stereo_viewer.cpp)
target_link_libraries(stereo_viewer ${catkin_LIBRARIES} ${OpenCV_LIBRARIES})
add_dependencies(stereo_viewer ${${PROJECT_NAME}_EXPORTED_TARGETS})

保存后,回到工作空间根目录进行编译:

cd ~/catkin_ws
catkin_make

编译成功后,就可以进行激动人心的测试了。请确保你的 Gazebo 仿真已经按照第四章的步骤启动并运行着。

  1. 打开一个新终端,启动我们刚写的节点:
    source ~/catkin_ws/devel/setup.bash
    rosrun my_stereo_vision stereo_viewer
    
  2. 如果一切配置正确,你应该会弹出两个 OpenCV 窗口,分别显示左目和右目相机看到的 Gazebo 仿真世界画面。
  3. 在 Gazebo 界面里,用键盘控制 XBot-U 机器人移动或旋转,观察两个窗口的图像变化。你会发现左右两个视图是有细微差异的,这个差异就是视差,是计算深度信息的根源。

踩坑提醒:如果窗口弹出但图像是黑的,或者节点启动后没反应,请按顺序检查:

  • 话题名是否正确:用 rostopic list 再次确认 /stereocamera/left/image_raw 等话题是否存在。
  • 图像编码:Gazebo 默认发布的可能是 rgb8 编码,而我们的代码期望 bgr8cv_bridgetoCvCopy 会自动处理一些常见转换,但如果不确定,可以在回调函数里打印 msg->encoding 看看。如果真是 rgb8,把代码中的 BGR8 改成 RGB8 即可,但注意 OpenCV 默认使用 BGR 顺序,显示颜色会不对,可能需要额外转换。
  • 坐标系和基线:如果左右图像看起来完全一样,没有视差,那很可能是右目相机的 <pose> 设置错了,或者基线 <baseline> 设成了 0。回去仔细检查 SDF 文件中右目相机的 Y 轴偏移量。

当两个窗口清晰地显示出带有视差的仿真场景时,你就成功完成了从 SDF 建模到 ROS 数据流的双目相机仿真全流程!这双仿真的“眼睛”将成为你开发 SLAM、三维重建、目标检测等高级视觉算法的强大沙盒。你可以随意修改相机参数、环境光照、物体纹理,反复测试你的算法,而不用担心摔坏任何一个真实的摄像头。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐