2、单目相机建模:相机link与joint定义、光学中心坐标系、相机参数在URDF中的表示

单目相机,说白了就是咱们手机上最常见的那个摄像头。在机器人世界里,它是最基础也最常用的视觉传感器。今天我就带你一步步把它装进URDF里。

我记得刚入行那会儿,总觉得相机建模不就是加个盒子嘛,结果在标定环节吃了大亏——相机坐标系没对齐,整个视觉定位全偏了。嗯,咱们今天就把这事彻底理清楚。

2.1 相机link与joint的定义

相机在URDF里,本质上就是一个link加上一个joint。link描述相机的外形和物理属性,joint把它固定在机器人本体上。

2.1.1 相机link

一个典型的单目相机link长这样:

<link name="camera_link">
  <visual>
    <geometry>
      <box size="0.03 0.05 0.02"/>
    </geometry>
    <origin xyz="0 0 0" rpy="0 0 0"/>
    <material name="camera_black">
      <color rgba="0.1 0.1 0.1 1.0"/>
    </material>
  </visual>
  <collision>
    <geometry>
      <box size="0.03 0.05 0.02"/>
    </geometry>
  </collision>
  <inertial>
    <mass value="0.02"/>
    <inertia ixx="1e-6" iyy="1e-6" izz="1e-6" ixy="0" ixz="0" iyz="0"/>
  </inertial>
</link>

这里我习惯把相机外形做成一个扁长方体,尺寸根据实际硬件来。比如常见的USB摄像头,大概是30mm×50mm×20mm。质量设个20克,惯性矩算个近似值就行——又不是做有限元分析,差不多得了。

我的经验:visual和collision用同样的几何体,省得后面碰撞检测出幺蛾子。如果你用的是仿真环境,collision可以简化成更大的包围盒,能提升计算速度。

2.1.2 相机joint

相机怎么装到机器人上?用joint。我一般用fixed joint,因为相机通常是刚性固定的:

<joint name="camera_joint" type="fixed">
  <parent link="base_link"/>
  <child link="camera_link"/>
  <origin xyz="0.1 0 0.05" rpy="0 0 0"/>
</joint>

xyz表示相机相对于base_link的位置偏移,rpy是旋转角度。这里我把相机放在机器人前方10cm、上方5cm处,水平安装。

注意:joint的origin是子link坐标系在父link坐标系中的位姿。我曾经搞反过一次,结果相机跑到机器人肚子里去了,排查了半天才发现是坐标变换方向搞错了。

2.2 光学中心坐标系

这才是相机建模的核心。相机link的坐标系原点在物理中心,但真正成像的光学中心并不在那里。

光学中心坐标系,也叫camera_optical_frame,它的规矩是:

  • Z轴:指向相机前方(镜头朝向)
  • Y轴:指向下方(图像的行方向)
  • X轴:指向右方(图像的列方向)

这和ROS里图像消息的坐标系约定完全一致。为什么这么定?因为OpenCV的图像坐标系是左上角为原点,行向下、列向右。ROS为了对齐,就把相机光轴指向Z正方向,Y向下,X向右。

咱们得在URDF里再加一个link来表示光学中心:

<link name="camera_optical_frame"/>

<joint name="camera_optical_joint" type="fixed">
  <parent link="camera_link"/>
  <child link="camera_optical_frame"/>
  <origin xyz="0 0 0.015" rpy="-1.5708 0 -1.5708"/>
</joint>

这里rpy的旋转很关键。从camera_link到camera_optical_frame,需要做两次旋转:

  1. 绕X轴转-90度(让Y轴指向下)
  2. 绕Z轴转-90度(让Z轴指向前)

合起来就是rpy="-1.5708 0 -1.5708"。xyz的偏移量0.015m,是因为我用的相机镜头到外壳表面有15mm的距离,光学中心在镜头处。

避坑指南:我曾经在一个项目里忘了加optical_frame,直接用camera_link的坐标系去算投影矩阵,结果图像全反了。后来花了整整两天才找到原因——坐标系旋转没做。所以,一定要单独定义optical_frame,别偷懒。

2.3 相机参数在URDF中的表示

URDF本身不直接存储相机内参(焦距、畸变系数等),它只描述几何和运动学。那相机参数放哪儿?

ROS的惯例是:

  • URDF里只放相机link和joint,以及一个指向参数文件的标签
  • 内参、分辨率、帧率等放在YAML配置文件里
  • 通过robot_state_publisher发布tf树

但URDF里可以加一个<gazebo>标签来传递相机参数(如果你用Gazebo仿真的话):

<gazebo reference="camera_link">
  <sensor type="camera" name="camera_sensor">
    <update_rate>30.0</update_rate>
    <camera>
      <horizontal_fov>1.0472</horizontal_fov>
      <image>
        <width>640</width>
        <height>480</height>
        <format>R8G8B8</format>
      </image>
      <clip>
        <near>0.1</near>
        <far>100.0</far>
      </clip>
    </camera>
  </sensor>
</gazebo>

这里horizontal_fov是水平视场角,单位弧度。1.0472弧度约等于60度,是常见的相机视场角。near和far是近远裁剪面,决定了相机能看到的最远最近距离。

对于真实机器人,相机参数通常放在一个单独的YAML文件里,比如camera_params.yaml:

camera_name: "my_camera"
image_width: 640
image_height: 480
camera_matrix:
  rows: 3
  cols: 3
  data: [530.0, 0.0, 320.0, 0.0, 530.0, 240.0, 0.0, 0.0, 1.0]
distortion_coefficients:
  rows: 1
  cols: 5
  data: [0.1, -0.05, 0.0, 0.0, 0.0]
rectification_matrix:
  rows: 3
  cols: 3
  data: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]
projection_matrix:
  rows: 3
  cols: 4
  data: [530.0, 0.0, 320.0, 0.0, 0.0, 530.0, 240.0, 0.0, 0.0, 0.0, 1.0, 0.0]

这些参数怎么来的?用相机标定工具(比如ROS的camera_calibration包)对着一张棋盘格拍几张照片就出来了。我一般标定完会把参数存到这个YAML里,然后在launch文件里加载。

2.4 完整示例:一个可用的单目相机URDF

把上面所有东西拼起来,就是一个完整的单目相机URDF片段:

<robot name="camera_robot" xmlns:xacro="http://www.ros.org/wiki/xacro">

  <!-- 相机本体 -->
  <link name="camera_link">
    <visual>
      <geometry>
        <box size="0.03 0.05 0.02"/>
      </geometry>
      <origin xyz="0 0 0" rpy="0 0 0"/>
      <material name="black">
        <color rgba="0.1 0.1 0.1 1.0"/>
      </material>
    </visual>
    <collision>
      <geometry>
        <box size="0.03 0.05 0.02"/>
      </geometry>
    </collision>
    <inertial>
      <mass value="0.02"/>
      <inertia ixx="1e-6" iyy="1e-6" izz="1e-6" ixy="0" ixz="0" iyz="0"/>
    </inertial>
  </link>

  <!-- 光学中心坐标系 -->
  <link name="camera_optical_frame"/>

  <!-- 相机到机器人本体的joint -->
  <joint name="camera_joint" type="fixed">
    <parent link="base_link"/>
    <child link="camera_link"/>
    <origin xyz="0.1 0 0.05" rpy="0 0 0"/>
  </joint>

  <!-- 光学中心到相机本体的joint -->
  <joint name="camera_optical_joint" type="fixed">
    <parent link="camera_link"/>
    <child link="camera_optical_frame"/>
    <origin xyz="0 0 0.015" rpy="-1.5708 0 -1.5708"/>
  </joint>

  <!-- Gazebo仿真参数 -->
  <gazebo reference="camera_link">
    <sensor type="camera" name="camera_sensor">
      <update_rate>30.0</update_rate>
      <camera>
        <horizontal_fov>1.0472</horizontal_fov>
        <image>
          <width>640</width>
          <height>480</height>
          <format>R8G8B8</format>
        </image>
        <clip>
          <near>0.1</near>
          <far>100.0</far>
        </clip>
      </camera>
    </sensor>
  </gazebo>

</robot>

这个URDF文件可以直接被robot_state_publisher解析,发布出完整的tf树:base_link → camera_link → camera_optical_frame。

2.5 知识结构总览

下面这张图把单目相机建模的核心逻辑串起来了:

单目相机URDF建模核心结构 base_link camera_joint (fixed) camera_link camera_optical_joint camera_optical_frame 关键参数 • 位置偏移: xyz • 旋转角度: rpy • 光学旋转: -90°绕X -90°绕Z Gazebo参数 • 水平视场角 • 图像分辨率 • 近远裁剪面

从这张图可以清楚看到:base_link → camera_joint → camera_link → camera_optical_joint → camera_optical_frame,一条完整的tf链。每个环节都有对应的参数要填,缺一不可。

2.6 常见问题与避坑

问题 原因 解决方案
图像上下颠倒 optical_frame的rpy旋转不对 检查rpy是否为-1.5708 0 -1.5708
相机位置不对 joint的xyz偏移量错误 用尺子量实际安装位置
Gazebo里看不到图像 缺少<gazebo>标签或参数不全 补全camera参数块
tf树缺少optical_frame 忘了定义camera_optical_joint 加上对应的link和joint
血的教训:有一次我在项目里把camera_link和camera_optical_frame合并成一个link,想着省事。结果所有视觉算法都跑偏了,因为tf树里少了一个坐标系变换。后来老老实实加回去,问题立刻解决。别在这上面省事,该有的坐标系一个都不能少。