机器人的camera入门基础知识

坐标系

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步:

  1. 用户空间发起请求

    应用程序通过 /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);  // 发送开启流命令
  2. 内核层接收请求

    内核通过 video_devicefops 成员(v4l2_file_operations)找到 unlocked_ioctl 函数(通常为V4L2核心层的 video_ioctl2),将请求转发给该函数。

  3. 核心层解析命令并匹配子设备
    video_ioctl2 函数根据命令码,从 video_deviceioctl_ops 中找到对应的处理函数,同时遍历 v4l2_devicesubdevs 链表,找到需要控制的 v4l2_subdev(如控制Sensor则找Sensor子设备)。

  4. 驱动层执行硬件控制

    调用目标 v4l2_subdevv4l2_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 兼容设备)。
  • 使用场景:打开设备后,必须先调用这个命令,确认设备支持视频采集,再进行后续操作。

    scss 复制代码
    struct 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 开始递增),驱动会返回对应的格式信息。
  • 使用场景:在设置格式前,先查询设备支持哪些像素格式,避免设置不支持的格式导致失败。

    ini 复制代码
    struct 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_format

    • type:流类型(采集 / 输出)
    • fmt.pix:像素格式信息(宽、高、像素格式、行字节数等)
  • 使用场景 :检查设备当前的格式,或确认 VIDIOC_S_FMT 后驱动是否接受了你设置的参数。


4. VIDIOC_S_FMT

  • 定义_IOWR('V', 5, struct v4l2_format)

  • 作用设置视频流的格式参数,是采集流程的核心步骤之一。

  • 结构体 :和 VIDIOC_G_FMT 一样用 struct v4l2_format

  • 使用场景:设置你想要的分辨率、像素格式(如 640x480 YUYV),驱动会返回实际生效的参数(如果硬件不支持,会自动调整为最接近的参数)。

    ini 复制代码
    struct 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_requestbuffers

    • type:流类型
    • memory:缓冲区类型(常用 V4L2_MEMORY_MMAP,用于内存映射)
    • count:申请的缓冲区数量(通常 3~5 个)
  • 使用场景 :格式设置完成后,必须先申请缓冲区,才能进行后续的 mmap 映射和流采集。


6. VIDIOC_QUERYBUF

  • 定义_IOWR('V', 9, struct v4l2_buffer)

  • 作用查询单个缓冲区的信息 (内核地址偏移、长度等),为 mmap 映射做准备。

  • 结构体struct v4l2_buffer

    • type:流类型
    • 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 摄像头采集程序,调用顺序是:

  1. open("/dev/video0")
  2. VIDIOC_QUERYCAP
  3. VIDIOC_ENUM_FMT(可选,查询支持的格式)
  4. VIDIOC_S_FMT(设置格式)
  5. VIDIOC_REQBUFS(申请缓冲区)
  6. VIDIOC_QUERYBUF + mmap(映射缓冲区)
  7. VIDIOC_QBUF(把所有缓冲区入队)
  8. VIDIOC_STREAMON(启动采集)
  9. 循环:VIDIOC_DQBUF → 处理数据 → VIDIOC_QBUF
  10. VIDIOC_STREAMOFF(停止采集)
  11. 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 功能包,提供了一系列标准化的消息类型,用于各种传感器数据的通信和交换。

  1. 在项目中,发布雷达话题消息时用到,订阅雷达话题消息时,也用到。
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 节点构建流程

  1. 创建包(依赖:rclcpp、sensor_msgs、cv_bridge、tf2)

  2. 写 V4L2 采集(open → set_fmt → reqbufs → mmap → streamon)

  3. 写 ROS 发布逻辑(定时器 → 读帧 → 坐标系变换 → 发布图像)

  4. 写 TF 坐标系发布(相机到 base_link)

  5. 创建launch管理camera参数

  6. 编写-CMakeLists.txt

  7. 编译运行

相关推荐
FeiHu 1145143 小时前
机器人运动学①
机器人
ROBOTGEEKER4 小时前
越疆CR全系列工业协作臂|从3kg轻载到30kg重载,覆盖重复、高精、高危全制造场景
人工智能·机器人·自动化·制造
观北海17 小时前
从 Sim2Sim 到 Sim2Real:以 ONNX 为核心的机器人策略实机落地全指南
python·机器人
米饭不加菜21 小时前
机器人矩阵运算MATLAB计算
matlab·矩阵·机器人
狐狐生风1 天前
LangChain实现简易版-----PDF 文档问答机器人
人工智能·langchain·机器人·pdf·prompt
2601_957964871 天前
水下机器人锂电池厂家解析(ROV/AUV/UUV电源系统解决方案)【浩博电池】
机器人
AI进化营-智能译站1 天前
ROS2 C++开发系列19-枚举定义机器人状态机|随机数生成仿真测试数据流
java·c++·ai·机器人
weixin_386212391 天前
20260502 杭州机器人仿真岗位整理
机器人
AI进化营-智能译站2 天前
ROS2 C++开发系列12-用多态与虚函数构建可扩展的ROS2机器人行为模块
开发语言·c++·ai·机器人