机器人的camera入门基础知识
说到相机在ROS中的位置和姿态,第一个绕不开的问题就是坐标系怎么定义。
在ROS系统里,相机坐标系走得是一套自己的规矩:X轴向右,Y轴向下,Z轴向前。这个标准必须记清楚。但有意思的是,大多数相机传感器(CMOS或者CCD)本身,人家物理像素的排列方式决定了一套自然的坐标系——X轴沿像素列增加方向(从左到右),Y轴沿像素行增加方向(从上到下),Z轴沿光轴方向(从镜头指向场景)。这就麻烦了,两套坐标系不统一,所以在ROS里,必须给相机的坐标系做一次tf坐标系的转换。
具体怎么做?两步旋转搞定:
- 先把坐标系绕Z轴旋转-90°。这一步是为了调整X、Y轴的方向——原来X轴是朝前的,转完之后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。
- 编译,运行。