坐标系
1.在ROS系统中,相机坐标系必须采用X轴向右、Y轴向下、Z轴向前的定义标准。
2.而一般相机传感器(CMOS/CCD)的物理像素排列决定了自然坐标系:
- X轴:沿像素列增加方向(从左到右)
- Y轴:沿像素行增加方向(从上到下)
- Z轴:沿光轴方向(从镜头指向场景) 故此,在ros中,需要对相机的坐标系进行一个tf坐标系转换:
rust
- 先绕Z轴旋转-90° (-π/2):调整X、Y轴方向
X由前->右, Y由左->前,Z轴不变向上
- 绕X轴旋转-90° (-π/2):使Z轴向前
X不变向右,Y轴由前->下,Z轴由上->前
获取相机的数据信息(V4L2)
-
V4L2(Video for Linux 2)是Linux 内核为视频设备提供的标准驱动框架 。V4L2框架的核心目标是:为不同硬件的Camera设备 (如Sensor、ISP、马达)提供统一的用户空间接口,同时简化内核驱动的开发流程。整体分为「用户空间」「内核空间」「硬件模块」三层,各层职责清晰、交互明确。
也就是说,在linux里面不需要做相机的各种驱动配置以及代码移植,只需要调用相应的api接口就能操控相机了。
现解读v4l2的关键声明以及api接口
1. 结构体 video_device:用户与内核的"交互桥梁"
抽象对象 :代表一个可被用户访问的视频设备实例(如 /dev/video0 对应一个 video_device)。
核心作用:为应用层提供统一的文件操作接口,屏蔽底层硬件差异。
arduino
struct video_device {
const struct v4l2_file_operations *fops; // 用户空间文件操作函数集(open/read/ioctl等)
struct device dev; // 设备模型节点,关联到Linux设备树
int minor; // 次设备号(主设备号固定为81,次设备号区分不同设备)
u32 capabilities; // 设备能力标识(如V4L2_CAP_VIDEO_CAPTURE表示支持视频采集)
const struct v4l2_ioctl_ops *ioctl_ops; // ioctl命令处理函数集(核心控制接口)
struct v4l2_device *v4l2_dev; // 关联的v4l2_device(所属的设备集合)
char name[32]; // 设备名称(如"my_camera")
// 其他辅助成员(如缓冲区管理、状态标记等)
};
关键成员说明:
fops:对接用户空间的文件操作(如open对应my_video_open函数);capabilities:告诉应用层设备支持的功能(如是否支持视频采集、流媒体);ioctl_ops:处理应用层的控制命令(如调整分辨率、帧率)。
2. 结构体 v4l2_device:视频设备的"大管家"
抽象对象 :代表一个完整的视频设备集合(可能包含多个子设备,如Sensor+ISP+马达)。
核心作用:管理所有子设备,协调资源分配,处理跨子设备的事件通知。
arduino
struct v4l2_device {
struct device *dev; // 关联的父设备(如平台设备)
struct list_head subdevs; // 子设备链表头(管理所有v4l2_subdev)
struct mutex mutex; // 互斥锁(保护子设备链表和资源访问)
struct list_head fds; // 打开该设备的文件描述符链表
struct v4l2_ctrl_handler *ctrl_handler; // 全局参数控制中心(如分辨率、曝光、白平衡)
const struct v4l2_device_ops *ops; // 设备级操作函数集(如子设备通知回调)
char name[V4L2_DEVICE_NAME_SIZE]; // 设备集合名称(如"camera_system")
// 其他辅助成员
};
关键成员说明:
subdevs:通过链表管理所有子设备(如Sensor子设备、ISP子设备),方便遍历和调用;mutex:保证多线程/多进程访问设备时的安全性;ctrl_handler:统一管理设备的控制参数(避免每个子设备重复实现参数逻辑)。
3. 结构体 v4l2_subdev:硬件子设备的"抽象代表"
抽象对象 :代表Camera系统中的单个硬件组件(如Sensor、ISP、音圈马达)。
核心作用:实现子设备的独立控制,让不同硬件的驱动逻辑模块化。
arduino
struct v4l2_subdev {
struct list_head list; // 链表节点(用于加入v4l2_device的subdevs链表)
struct device *dev; // 子设备的设备模型节点
struct v4l2_device *v4l2_dev; // 所属的v4l2_device(关联到设备集合)
const struct v4l2_subdev_ops *ops; // 子设备操作函数集(硬件控制核心)
const char *name; // 子设备名称(如"ov5640_sensor""isp_core")
struct v4l2_ctrl_handler *ctrl_handler; // 子设备私有参数控制(如Sensor的增益调节)
// 其他辅助成员(如子设备类型、状态标记)
};
关键成员说明:
list:将子设备挂载到v4l2_device的链表中,实现统一管理;ops:包含子设备的具体控制逻辑(如启动视频流、调整亮度);v4l2_dev:明确子设备的归属,确保控制命令能正确传递。
1.3 ioctl命令的"调用链路"
应用层通过 ioctl 发送控制命令(如"开启视频流""设置对比度"),其调用流程是V4L2框架的核心逻辑,具体分为4步:
-
用户空间发起请求
应用程序通过
/dev/videoX节点调用ioctl,传入命令码(如VIDIOC_STREAMON表示开启流):ini// 示例:用户空间开启视频流 int fd = open("/dev/video0", O_RDWR); enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE; ioctl(fd, VIDIOC_STREAMON, &type); // 发送开启流命令 -
内核层接收请求
内核通过
video_device的fops成员(v4l2_file_operations)找到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的命令码比较长,需要额外注意记忆
1.VIDIOC = Video Device IO Control 代码里一堆VIDIOC_XXX,需要如此记忆: - 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 -
定义 :
_IOR('V', 0, struct v4l2_capability) -
作用 :查询设备的基础能力集,是所有 V4L2 操作的第一步。
-
结构体 :
struct v4l2_capability- 包含设备名称、驱动名、支持的功能(如是否支持采集 / 输出、是否是 V4L2 兼容设备)。
-
使用场景:打开设备后,必须先调用这个命令,确认设备支持视频采集,再进行后续操作。
scssstruct v4l2_capability cap; ioctl(fd, VIDIOC_QUERYCAP, &cap); if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) { printf("设备不支持视频采集!\n"); exit(1); }
2. VIDIOC_ENUM_FMT
-
定义 :
_IOWR('V', 2, struct v4l2_fmtdesc) -
作用 :枚举设备支持的所有视频格式,比如 YUYV、MJPEG、H.264 等。
-
结构体 :
struct v4l2_fmtdesc- 你需要设置
type(流类型,如V4L2_BUF_TYPE_VIDEO_CAPTURE)和index(从 0 开始递增),驱动会返回对应的格式信息。
- 你需要设置
-
使用场景:在设置格式前,先查询设备支持哪些像素格式,避免设置不支持的格式导致失败。
inistruct v4l2_fmtdesc fmt_desc = {0}; fmt_desc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt_desc.index = 0; while (ioctl(fd, VIDIOC_ENUM_FMT, &fmt_desc) == 0) { printf("支持的格式: %c%c%c%c\n", fmt_desc.pixelformat & 0xff, (fmt_desc.pixelformat >> 8) & 0xff, (fmt_desc.pixelformat >> 16) & 0xff, (fmt_desc.pixelformat >> 24) & 0xff); fmt_desc.index++; }
3. VIDIOC_G_FMT
-
定义 :
_IOWR('V', 4, struct v4l2_format) -
作用 :获取设备当前生效的视频格式(分辨率、像素格式、帧率等)。
-
结构体 :
struct v4l2_formattype:流类型(采集 / 输出)fmt.pix:像素格式信息(宽、高、像素格式、行字节数等)
-
使用场景 :检查设备当前的格式,或确认
VIDIOC_S_FMT后驱动是否接受了你设置的参数。
4. VIDIOC_S_FMT
-
定义 :
_IOWR('V', 5, struct v4l2_format) -
作用 :设置视频流的格式参数,是采集流程的核心步骤之一。
-
结构体 :和
VIDIOC_G_FMT一样用struct v4l2_format -
使用场景:设置你想要的分辨率、像素格式(如 640x480 YUYV),驱动会返回实际生效的参数(如果硬件不支持,会自动调整为最接近的参数)。
inistruct v4l2_format fmt = {0}; fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt.fmt.pix.width = 640; fmt.fmt.pix.height = 480; fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; ioctl(fd, VIDIOC_S_FMT, &fmt); // 调用后 fmt 里会保存驱动实际生效的参数
5. VIDIOC_REQBUFS
-
定义 :
_IOWR('V', 8, struct v4l2_requestbuffers) -
作用 :向驱动申请内核缓冲区,用于存放采集的视频帧数据。
-
结构体 :
struct v4l2_requestbufferstype:流类型memory:缓冲区类型(常用V4L2_MEMORY_MMAP,用于内存映射)count:申请的缓冲区数量(通常 3~5 个)
-
使用场景 :格式设置完成后,必须先申请缓冲区,才能进行后续的
mmap映射和流采集。
6. VIDIOC_QUERYBUF
-
定义 :
_IOWR('V', 9, struct v4l2_buffer) -
作用 :查询单个缓冲区的信息 (内核地址偏移、长度等),为
mmap映射做准备。 -
结构体 :
struct v4l2_buffertype:流类型memory:缓冲区类型(和REQBUFS一致)index:要查询的缓冲区编号(从 0 开始)- 调用后,驱动会返回
m.offset(内核地址偏移)和length(缓冲区大小)。
-
使用场景 :申请缓冲区后,循环调用
VIDIOC_QUERYBUF拿到每个缓冲区的信息,再用mmap映射到用户态。
7. VIDIOC_G_FBUF / VIDIOC_S_FBUF
-
定义:
VIDIOC_G_FBUF:_IOR('V', 10, struct v4l2_framebuffer)VIDIOC_S_FBUF:_IOW('V', 11, struct v4l2_framebuffer)
-
作用:获取 / 设置帧缓冲区的参数(如叠加显示、帧缓冲地址)。
-
使用场景:主要用于视频叠加(overlay)场景,普通采集程序一般用不到。
8. VIDIOC_QBUF
- 定义 :
_IOWR('V', 15, struct v4l2_buffer) - 作用 :把用户态的空闲缓冲区「入队」交给驱动,等待硬件填充数据。
- 结构体 :
struct v4l2_buffer - 使用场景 :采集循环中,处理完一帧数据后,必须调用
QBUF把缓冲区重新放回队列,驱动才能继续使用它接收下一帧数据。
9. VIDIOC_EXPBUF
- 定义 :
_IOWR('V', 16, struct v4l2_exportbuffer) - 作用 :导出缓冲区的文件描述符,用于零拷贝场景(如 DMA-BUF)。
- 使用场景:高级用法,用于和其他硬件(如 GPU)共享缓冲区,普通采集程序一般不用。
10. VIDIOC_DQBUF
- 定义 :
_IOWR('V', 17, struct v4l2_buffer) - 作用 :从驱动队列中取出已经填好数据的缓冲区,用户态程序读取和处理数据。
- 结构体 :
struct v4l2_buffer - 使用场景 :采集循环的核心步骤,调用后你可以通过
buffer.index找到对应的mmap地址,读取视频帧数据。
11. VIDIOC_STREAMON / VIDIOC_STREAMOFF
-
定义:
VIDIOC_STREAMON:_IOW('V', 18, int)VIDIOC_STREAMOFF:_IOW('V', 19, int)
-
作用:启动 / 停止视频流采集。
-
使用场景:
- 所有准备工作完成后,调用
STREAMON通知驱动开始采集数据。 - 采集结束后,调用
STREAMOFF停止流,再释放缓冲区和关闭设备。
- 所有准备工作完成后,调用
12. VIDIOC_G_PARM / VIDIOC_S_PARM
-
定义:
VIDIOC_G_PARM:_IOWR('V', 21, struct v4l2_streamparm)VIDIOC_S_PARM:_IOWR('V', 22, struct v4l2_streamparm)
-
作用:获取 / 设置视频流的参数(如帧率、时间戳模式)。
-
结构体 :
struct v4l2_streamparm -
使用场景:调整采集帧率,或获取设备支持的帧率范围。
13. VIDIOC_G_STD / VIDIOC_S_STD / VIDIOC_ENUMSTD
-
定义:
VIDIOC_G_STD:_IOR('V', 23, v4l2_std_id)VIDIOC_S_STD:_IOW('V', 24, v4l2_std_id)VIDIOC_ENUMSTD:_IOWR('V', 25, struct v4l2_standard)
-
作用:获取 / 设置 / 枚举视频标准(如 PAL、NTSC)。
-
使用场景:主要用于模拟视频采集设备(如电视卡),USB 摄像头一般不用。
14. VIDIOC_ENUMINPUT
- 定义 :
_IOWR('V', 26, struct v4l2_input) - 作用 :枚举设备支持的视频输入源(如摄像头 1、摄像头 2)。
- 使用场景:多输入设备(如带多个摄像头的设备),可以用这个命令选择要采集的输入源。
完整采集流程命令调用顺序
一个标准的 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直接写入内核缓冲区,代码中无需显式操作
ini
fmt.fmt.pix.pixelformat = VIDEO_FORMAT; // 通常为MJPEG
相机硬件 直接输出MJPEG压缩数据 ,而非原始RGB;硬件压缩减少了数据量(约10-20倍),降低带宽需求;帧内压缩,单帧独立解码,适合实时场景
2. V4L2驱动与内核缓冲区
ini
struct v4l2_requestbuffers reqbuf;
reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuf.memory = V4L2_MEMORY_MMAP; // 使用mmap方式
reqbuf.count = BUFFER_COUNT; // 缓冲区数量
ret = ioctl(fd, VIDIOC_REQBUFS, &reqbuf);
缓冲区队列机制 :申请多个环形缓冲区(通常3-4个) DMA直接访问 :相机硬件通过DMA直接写入内核缓冲区 零拷贝设计 :避免数据从内核到用户空间的拷贝
3. mmap虚拟映射操作
js
mmap_buffer[i].start = (unsigned char *)mmap(0, buf.length,
PROT_READ|PROT_WRITE, MAP_SHARED, fd, buf.m.offset);
-
mmap(): 用于创建内存映射。其原型是void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); -
addr: 映射的起始地址,通常设置为NULL让内核自动分配。length: 映射的长度。prot: 映射区域的保护方式,如PROT_READ(可读)、PROT_WRITE(可写)等。flags: 映射的特性,如共享或私有等。fd: 目标文件的文件描述符。offset: 文件中的偏移量。
-
返回值:mmap()返回被映射区的指针,该指针就是需要映射的内核空间在用户空间的虚拟地址

传统方式: 内核缓冲区 ──copy──> 用户缓冲区 ──copy──> 目标
arduino
传统read需要两次拷贝:内核→用户→目标
- 绕过用户空间与内核空间拷贝: 传统的文件操作需要将数据从磁盘复制到内核空间,再从内核空间复制到用户空间,而
mmap则通过将文件页直接映射到用户空间的页缓存,减少了数据拷贝次数,只需一次从磁盘到用户内存的拷贝,从而提高效率
mmap方式: 内核缓冲区 <──映射──> 用户空间指针 (无拷贝)
mmap建立虚拟地址到物理地址的映射,用户空间直接访问内核缓冲区
4. 数据读取与缓冲区管理
ini
// 1. 等待数据就绪(select机制)
r = select(fd + 1, &fds, NULL, NULL, &tv);
// 2. 出队缓冲区(从驱动获取填充好的缓冲区)
ioctl(fd, VIDIOC_DQBUF, &buf);
frame_buf->start = mmap_buffer[buf.index].start;
frame_buf->length = buf.bytesused;
// 3. 重新入队缓冲区(供硬件再次填充)
ioctl(fd, VIDIOC_QBUF, &buf);
select用于监测文件描述符就绪状态
使用双缓冲队列机制 :
- 驱动队列 :等待硬件填充的空缓冲区
- 就绪队列 :已填充数据等待读取的缓冲区
5. 分频处理
分频在用户空间实现 :跳过某些帧以降低有效帧率
js
if(frame_cnt % div != 0)
continue; // 跳过帧,实现分频
应用场景 :
- 降低后端处理压力(如YOLO推理速度跟不上)
- 减少带宽占用(远程传输时)
- 降低功耗(电池供电场景)
面试官可能问 : "分频是在采集端还是处理端做更好?为什么?"
- 采集端分频:减少数据传输量,降低带宽
- 处理端分频:保留完整数据,灵活选择
- 本项目在采集端分频,节省共享内存带宽
6. 共享内存发布
js
shm_transport::Topic shm_topic(nh);
shm_transport::Publisher shm_pub = shm_topic.advertise<sensor_msgs::CompressedImage>(
pub_image_topic, 1, 10 * 1024 * 1024); // 10MB共享内存
共享内存是进程间通信 :发布者写入,订阅者直接读取,避免ROS TCP开销 共享内存设计 :
- 大小计算 :1280×720 MJPEG约100-300KB/帧,10MB可缓存30+帧
- 进程间通信 :避免ROS TCP序列化开销
- 零拷贝传输 :发布者写入共享内存,订阅者直接读取 数据流对比 :
makefile
传统ROS TCP: 序列化 ──网络传输──> 反序列化
共享内存: 直接写入 <──共享内存──> 直接读取
面试官可能问 : "共享内存和ROS TCP各有什么优缺点?如何选择?"
- 共享内存:低延迟、高带宽,但只能同一主机
- ROS TCP:跨主机,但有序列化和网络开销
- 本项目相机和算法在同一主机,适合共享内存
sensor_msgs 功能介绍
sensor_msgs 是一个 ROS 功能包,提供了一系列标准化的消息类型,用于各种传感器数据的通信和交换。

- 在项目中,发布雷达话题消息时用到,订阅雷达话题消息时,也用到。
arduino
#include "sensor_msgs/LaserScan.h"
// 第32行 - 发布激光雷达数据
ros::Publisher scan_pub = nh.advertise<sensor_msgs::LaserScan>("scan", 1);
// 第156行 - 填充并发布消息
sensor_msgs::LaserScan scan_msg;
pub.publish(scan_msg);
2.压缩图像信息时也用到
ini
ros::Publisher pub = nh.advertise<sensor_msgs::CompressedImage>(pub_image_topic, 1);
sensor_msgs::CompressedImage msg;
// 第156-159行 - 填充消息
msg.header.stamp = ros::Time::now();
msg.header.frame_id = frame_id;
msg.format = "jpeg";
msg.data.assign(frame_buf.start, frame_buf.start+frame_buf.length);
pub.publish(msg);
3.传输图像信息时也用到
ini
raw_image_pub = nh.advertise<sensor_msgs::Image>(pub_raw_image_topic, 10);
// 第72-77行 - 填充原始图像消息
msg_pub.header = msg->header;
msg_pub.height = image.rows * scale;
msg_pub.width = image.cols * scale;
msg_pub.encoding = "rgb8";
msg_pub.step = msg_pub.width * 3;
raw_image_pub.publish(msg_pub);
相机 ROS 节点构建流程
-
创建包(依赖:rclcpp、sensor_msgs、cv_bridge、tf2)
-
写 V4L2 采集(open → set_fmt → reqbufs → mmap → streamon)
-
写 ROS 发布逻辑(定时器 → 读帧 → 坐标系变换 → 发布图像)
-
写 TF 坐标系发布(相机到 base_link)
-
创建launch管理camera参数
-
编写-CMakeLists.txt
-
编译运行