谈到相机在ROS系统中的位置与姿态,首先必须解决的就是坐标系的定义问题。
在ROS框架内,相机坐标系遵循一套独特的规则:X轴朝右,Y轴朝下,Z轴朝前。这一标准需要牢记。然而,大多数相机传感器(如CMOS或CCD)的物理像素排列方式决定了其自然坐标系——X轴沿像素列递增方向(从左至右),Y轴沿像素行递增方向(从上至下),Z轴沿光轴方向(从镜头指向场景)。这就带来了矛盾:两套坐标系不统一。因此,在ROS中,必须对相机坐标系执行一次tf坐标系变换。
具体实现分两步旋转:
- 首先,将坐标系绕Z轴旋转-90°。这一步用于调整X、Y轴的方向——原本朝前的X轴旋转后指向右方;原本朝左的Y轴旋转后指向前方。注意,此时Z轴未变动,仍指向上方。
- 接着,绕X轴再旋转-90°,目的是让Z轴从朝上变为朝前。旋转完成后,X轴保持朝右不变,Y轴由朝前变为朝下,Z轴则从朝上改为朝前。
获取相机数据信息(V4L2)
V4L2,全称为Video for Linux 2,是Linux内核专为视频设备提供的一套标准化驱动框架。其设计目标非常明确:为不同硬件的Camera设备——如Sensor、ISP、马达等——提供统一的用户空间接口,同时简化内核驱动的开发流程。整个框架分为三层:「用户空间」「内核空间」「硬件模块」,各层职责清晰,交互明确。
简单来说,在Linux环境下,无需繁琐地配置各类相机驱动和代码移植,只需调用对应的API接口即可操控相机。
关键声明与API接口解读
1. 结构体video_device:用户与内核的“交互桥梁”
该结构体抽象为一个用户空间可访问的视频设备实例——例如,你看到的/dev/video0背后就对应一个video_device。其核心作用是为应用层提供统一的文件操作接口,完全屏蔽底层硬件的差异。
让我们逐一解析几个关键成员:
fops:对接用户空间的各种文件操作,比如执行open设备时,实际调用的是其中挂载的my_video_open函数。capabilities:该字段直接告知应用层设备的能力——是否支持视频采集、是否支持流媒体等,一目了然。ioctl_ops:这是一组控制命令处理函数,专门负责处理应用层发来的指令,如调整分辨率、帧率等。
2. 结构体v4l2_device:视频设备的“大管家”
此结构体代表一个完整的视频设备集合,可能包含Sensor、ISP、马达等多个子设备。它如同一个大管家,负责管理所有子设备、协调资源分配、处理跨子设备的事件通知。
关键成员:
subdevs:通过链表将所有子设备串联起来,无论是Sensor子设备还是ISP子设备,均统一管理,便于遍历和调用。mutex:互斥锁,确保多线程或多进程同时访问设备时不会发生冲突。ctrl_handler:全局参数控制中心,负责管理分辨率、曝光、白平衡等参数。这样做的好处是每个子设备无需重复实现参数逻辑。
3. 结构体v4l2_subdev:硬件子设备的“抽象代表”
该结构体抽象化地代表了Camera系统中的单个硬件组件,例如Sensor、ISP或音圈马达。它使不同硬件的驱动逻辑模块化,每个子设备都能独立控制。
关键成员:
list:链表节点,用于将子设备挂载到v4l2_device的链表上,实现统一管理。ops:子设备的具体控制逻辑——启动视频流、调整亮度等操作均由它实现。v4l2_dev:指明该子设备所属的父设备,确保控制命令能正确传递到目标位置。
ioctl命令的“调用链路”
应用层通过ioctl发送控制命令——例如“开启视频流”或“设置对比度”——这一调用流程是V4L2框架的核心逻辑,总共分为4步:
- 用户空间发起请求。应用程序通过
/dev/videoX节点调用ioctl,传入命令码。比如VIDIOC_STREAMON,表示请求开启视频流。 - 内核层接收请求。内核通过
video_device的fops成员找到unlocked_ioctl函数——通常是V4L2核心层的video_ioctl2,然后将请求转发。 - 核心层解析命令并匹配子设备。
video_ioctl2根据命令码,从video_device的ioctl_ops中查找对应的处理函数。同时,它会遍历v4l2_device的subdevs链表,找到需要控制的v4l2_subdev——例如要控制Sensor,就去找Sensor子设备。 - 驱动层执行硬件控制。最后,调用目标
v4l2_subdev的v4l2_subdev_ops中的对应函数——比如s_stream负责开启视频流——最终通过硬件接口(如I2C)控制硬件完成操作。
V4L2的命令码:记住这几个核心
V4L2的命令码虽然较长,但核心的就那么几个,需要额外记忆。其实命名是有规律的:VIDIOC是Video Device IO Control的缩写,后面跟着的都是缩写组合:
QUERYCAP:Query Capability,查询能力。ENUM_FMT:Enumerate Format,列出支持的格式。G_FMT:Get Format,获取当前格式。S_FMT:Set Format,设置格式。REQBUFS:Request Buffers,申请缓冲区。QUERYBUF:Query Buffer,查看单个缓冲区信息。QBUF:Enqueue Buffer,将缓冲区放入队列。DQBUF:Dequeue Buffer,从队列取出缓冲区。STREAMON:Stream On,开启流。STREAMOFF:Stream Off,关闭流。
下面详细说明每一个:
1. VIDIOC_QUERYCAP
这是所有V4L2操作的第一步。打开设备后,必须先调用它来查询设备的基础能力——例如是否支持视频采集,是否为V4L2兼容设备。不查清楚,后续操作无法进行。
2. VIDIOC_ENUM_FMT
在设置格式之前,最好先询问设备支持哪些像素格式——YUYV、MJPEG、H.264等,做到心中有数,避免设置了不支持的格式导致失败。
3. VIDIOC_G_FMT / VIDIOC_S_FMT
一个用于获取当前格式,一个用于设置格式。设置格式是采集流程的核心步骤之一——你需要告诉设备所需的分辨率和像素格式。需要注意的是,如果设置的参数硬件不支持,驱动会自动调整成最接近的参数,并返回实际生效的值。
5. VIDIOC_REQBUFS
格式设置完成后,接下来需要向驱动申请内核缓冲区,用于存放采集的视频帧数据。一般申请3到5个缓冲区,类型常用MMAP(内存映射)。
6. VIDIOC_QUERYBUF
申请完缓冲区后,逐个查询每个缓冲区的信息——内核地址偏移、长度等,为后续的mmap映射做准备。
7. VIDIOC_QBUF
在采集循环中,处理完一帧数据后,必须调用此命令,将空闲的缓冲区重新放回队列。只有这样,驱动才能继续使用它接收下一帧数据。
8. VIDIOC_DQBUF
这是采集循环的核心步骤。从驱动队列中取出已经填入数据的缓冲区,获取数据后读取、处理,然后再通过QBUF放回去。
9. VIDIOC_STREAMON / VIDIOC_STREAMOFF
所有准备工作就绪后,调用STREAMON通知驱动开始工作。采集结束时,调用STREAMOFF停止流,然后释放缓冲区、关闭设备。
其他还有一些命令——G_PARM/S_PARM用于调整帧率,G_STD/S_STD处理视频标准,ENUMINPUT枚举输入源——在普通USB摄像头场景下使用频率不高,了解即可。
完整采集流程命令调用顺序
一个标准的USB摄像头采集程序,命令调用的顺序如下:
open("/dev/video0")VIDIOC_QUERYCAPVIDIOC_ENUM_FMT(可选,先查询支持的格式)VIDIOC_S_FMT(设置所需的格式)VIDIOC_REQBUFS(申请缓冲区)VIDIOC_QUERYBUF+mmap(将缓冲区映射到用户空间)VIDIOC_QBUF(将所有缓冲区入队)VIDIOC_STREAMON(启动采集)- 循环处理:
VIDIOC_DQBUF→ 处理数据 →VIDIOC_QBUF VIDIOC_STREAMOFF(停止采集)munmap解除映射 →close(fd)
V4L2应用视角
ROS中相机的图像采集处理
整个流程大致如下:物理摄像头输出MJPEG压缩数据 → V4L2内核缓冲区(内核DMA处理) → mmap映射到用户空间 → 分频处理 → 共享内存写入 → 订阅者读取并解码。
1. 硬件处理层面
需要强调的是,DMA是硬件层面的操作:相机数据通过DMA直接写入内核缓冲区,代码中无需显式操作。相机硬件直接输出MJPEG压缩数据,而非原始RGB数据。硬件压缩的优势在于数据量可缩减到原来的十分之一至二十分之一,大大降低带宽压力。而且这是帧内压缩,单帧独立解码,非常适合实时场景。
2. V4L2驱动与内核缓冲区
缓冲区的核心机制是队列。驱动会申请多个环形缓冲区,通常为3到4个。相机硬件通过DMA直接写入内核缓冲区——注意,这种设计实现了零拷贝,数据无需从内核到用户空间再拷贝一次。
3. mmap虚拟映射操作
mmap()的作用是创建内存映射。传统方式下,数据需要在内核和用户空间之间来回拷贝——内核缓冲区拷贝到用户缓冲区,再拷贝到目标位置,两次拷贝效率低下。而mmap通过将内核缓冲区的物理地址直接映射到用户空间的虚拟地址,用户程序可以直接访问,相当于一次拷贝即可——这才是真正的零拷贝。
4. 数据读取与缓冲区管理
实际读取时,使用select监测文件描述符的就绪状态。采用双缓冲队列机制:一边是驱动队列,存放等待硬件填充的空缓冲区;另一边是就绪队列,存放已填充数据、等待用户读取的缓冲区。用户程序从就绪队列取出数据,处理完后放回驱动队列,如此循环。
5. 分频处理
分频是在用户空间实现的——简单来说,就是跳过某些帧来降低有效帧率。例如,相机帧率为30帧,但后端算法(如YOLO)处理能力不足,可以选择每2帧或每3帧取一帧进行处理。
面试官可能会问:“分频在采集端做还是处理端做更好?为什么?”
- 采集端分频:优势在于减少数据传输量,降低带宽压力。
- 处理端分频:保留完整数据,灵活性更高。
我们在项目中选择了采集端分频,主要是为了节省共享内存的带宽。
6. 共享内存发布
最后,通过共享内存发布数据。共享内存是进程间通信的一种方式——发布者写入数据,订阅者直接读取,完全避免了ROS TCP带来的序列化和网络开销。整个数据流对比非常明显:传统ROS TCP需要序列化、网络传输、再反序列化,而共享内存只需直接写入、直接读取。当然,共享内存仅适用于同一台主机,跨主机仍需依靠TCP。
sensor_msgs 功能介绍
sensor_msgs是ROS的一个功能包,提供了一系列标准化的消息类型,用于各种传感器数据的通信和交换。
在项目中,它被用于多个场景:发布雷达话题消息时使用,订阅雷达话题消息时也使用;处理压缩图像信息时使用sensor_msgs::CompressedImage;传输原始图像信息时则使用sensor_msgs::Image。
相机ROS节点构建流程
最后,搭建整个节点的流程非常清晰:
- 创建包,依赖rclcpp、sensor_msgs、cv_bridge、tf2。
- 编写V4L2采集逻辑——open、set_fmt、reqbufs、mmap、streamon,走一遍标准流程。
- 编写ROS发布逻辑——定时器触发,读帧,执行坐标系变换,发布图像。
- 编写TF坐标系发布——将相机坐标变换到base_link。
- 创建launch文件,管理camera参数。
- 编写CMakeLists.txt。
- 编译,运行。
