V4L2读取摄像头资源

1.V4L2

它是Linux内核中标准的关于视频驱动程序,Video for Linux 2,简称V4L2。

它为Linux下的视频驱动提供了统一的接口,使得应用程序可以使用统一的API操作不同的视频设备。

V4L2支持三类设备:视频输入输出设备VBI设备radio设备 ,分别会在/dev目录下产生video*、radio*和vbi设备节点。应用程序通过对 videoX 设备文件进行 I/O 操作来配置、使用设备类设备。

2.整体流程

3.V4L2读取摄像头流程


从流程图中可以看到,几乎对摄像头的所有操作都是通过 ioctl() 来完成,搭配不同的 V4L2 指令( request 参数)请求不同的操作,这些指令定义在头文件 linux/videodev2.h 中,在摄像头应用程序代码中,需要包含 头文件 linux/videodev2.h,它 提供了很多ioctl宏来和应用层交互。

每一个不同的指令宏就表示向设备请求不同的操作,调用 ioctl()时需要传入的第三个参数的类型;调用 ioctl()前,定义一个该类型变量,调用 ioctl()时、将变量的指针作为 ioctl()的第三个参数传入

cpp 复制代码
###define VIDIOC_QUERYCAP _IOR('V', 0, struct v4l2_capability)

struct v4l2_capability cap;
......
ioctl(fd, VIDIOC_QUERYCAP, &cap);

下面列出来一些比较常用的:

复制代码
在进行V4L2开发中,常用的命令标识符如下:
(1) VIDIOC_REQBUFS:  分配内存;
(2) VIDIOC_QUERYBUF: 把VIDIOC_REQBUFS中分配的数据缓存转换成物理地址;
(3) VIDIOC_QUERYCAP: 查询驱动功能;
(4) VIDIOC_ENUM_FMT: 获取当前驱动支持的视频格式;
(5) VIDIOC_S_FMT:    设置当前驱动的视频捕获格式;
(6) VIDIOC_G_FMT:    读取当前驱动的视频捕获格式;
(7) VIDIOC_TRY_FMT:  验证当前驱动的显示格式;
(8) VIDIOC_CROPCAP:  查询驱动的修剪功能;
(9) VIDIOC_S_CROP:   设置视频信号的边框;
(10)VIDIOC_G_CROP:   读取视频信号的边框;
(11)VIDIOC_QBUF:     把数据从缓存中读取出来;
(12)VIDIOC_DQBUF:    把数据放回缓存队列;
(13)VIDIOC_STREAMON: 开始视频显示函数;
(14)VIDIOC_STREAMOFF:结束视频显示函数;
(15)VIDIOC_QUERYSTD: 检查当前视频设备支持的标准,例如PAL或NTSC;

3.1 打开视频文件设备

  • 视频类设备对应的设备节点为/dev/videoX, X 为数字编号,通常从 0 开始 ,使用open打开节点, 应用程序能够使用阻塞模式非阻塞模式打开视频设备,如果使用非阻塞模式调用视频设备,即使尚未捕获到信息,驱动依旧会把缓存(DQBUFF)里的东西返回给应用程序。
cpp 复制代码
//阻塞模式
fd = open("/dev/video0", O_RDWR);
//非阻塞模式
fd = open("/dev/video0", O_RDWR | O_NOBLOCK);

if (0 > fd) {
fprintf(stderr, "open error: %s: %s\n", "/dev/video0", strerror(errno));
return -1;
}

3.2 查询属性、功能

  • 查询设备的属性, 使用的指令为 VIDIOC_QUERYCAP,需要传入一个表示属性的结构体
cpp 复制代码
/* 查询设备功能 */
struct v4l2_capability vcap;
ioctl(fd, VIDIOC_QUERYCAP, &vcap);

/* 判断是否是视频采集设备 */
/*capabilities字段必须包含 V4L2_CAP_VIDEO_CAPTURE,表示它支持视频采集功能*/
if (!(V4L2_CAP_VIDEO_CAPTURE & vcap.capabilities)) {
fprintf(stderr, "Error: No capture video device!\n");
return -1;
}

3.3设置设备参数

  • 在设置格式之前,要先枚举出所有的格式,看一看是否支持要设置的格式,然后再进一步设置 。
  • **a)****枚举出摄像头支持的所有像素格式:**VIDIOC_ENUM_FMT

ioctl(int fd, VIDIOC_ENUM_FMT, struct v4l2_fmtdesc *fmtdesc);

cpp 复制代码
/* 枚举出摄像头所支持的所有像素格式以及描述信息 */
fmtdesc.index = 0;
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
while (0 == ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc)) {
printf("fmt: %s <0x%x>\n", fmtdesc.description, fmtdesc.pixelformat);
fmtdesc.index++;
}
  • **b)****枚举出摄像头支持的所有像素格式:**VIDIOC_ENUM_FMT

ioctl(int fd, VIDIOC_ENUM_FRAMESIZES, struct v4l2_frmsizeenum *frmsize);

cpp 复制代码
struct v4l2_frmsizeenum frmsize;
frmsize.index = 0;
frmsize.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
frmsize.pixel_format = V4L2_PIX_FMT_RGB565;
while (0 == ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &frmsize)) {
printf("frame_size<%d*%d>\n", frmsize.discrete.width, frmsize.discrete.height);
frmsize.index++;
}
  • **c)****枚举摄像头所支持的所有视频采集帧率:**VIDIOC_ENUM_FRAMEINTERVALS

同理,略
d)设置当前的格式:VIDIOC_S_FMT 像素格式、视频帧大小以及视频采集帧率
int ioctl(int fd, VIDIOC_G_FMT, struct v4l2_format *fmt);//查看get,像素格式、视频帧大小
int ioctl(int fd, VIDIOC_S_FMT, struct v4l2_format *fmt);//设置set,像素格式、视频帧大小

cpp 复制代码
/*使用摄像头设备时,type设置成V4L2_BUF_TYPE_VIDEO_CAPTURE ,使用struct v4l2_pix_format来描述摄像头属性*/
struct v4l2_format fmt
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 800;
fmt.fmt.pix.height = 480;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB565;
if (0 > ioctl(fd, VIDIOC_S_FMT, &fmt)) { //设置格式
	perror("ioctl error");
	return -1;
}



ioctl(int fd, VIDIOC_G_PARM, struct v4l2_streamparm *streamparm);//设置帧率

cpp 复制代码
struct v4l2_streamparm streamparm;

streamparm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(v4l2_fd, VIDIOC_G_PARM, &streamparm);

/** 判断是否支持帧率设置 **/
if (V4L2_CAP_TIMEPERFRAME & streamparm.parm.capture.capability) {
    streamparm.parm.capture.timeperframe.numerator = 1;
    streamparm.parm.capture.timeperframe.denominator = 30;//30fps
    
    if (0 > ioctl(v4l2_fd, VIDIOC_S_PARM, &streamparm))
    {    //设置参数
        fprintf(stderr, "ioctl error: VIDIOC_S_PARM: %s\n", strerror(errno));
        return -1;
    }
}
else
    fprintf(stderr, "不支持帧率设置");

3.4申请帧缓存

读取摄像头数据的方式有两种,一种是 read 方式(对应设备功能返回的 V4L2_CAP_READWRITE );另一种则是 streaming 方式 (使用mmap,对应设备功能返回的 V4L2_CAP_STREAMING )

我们需要将设备申请帧缓冲,并将帧缓冲映射到应用程序的进程地址空间中。
ioctl(int fd, VIDIOC_REQBUFS, struct v4l2_requestbuffers *reqbuf);//申请缓冲帧

cpp 复制代码
struct v4l2_requestbuffers reqbuf;
reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuf.count = 3; // 申请 3 个帧缓冲
reqbuf.memory = V4L2_MEMORY_MMAP;
if (0 > ioctl(fd, VIDIOC_REQBUFS, &reqbuf)) {
fprintf(stderr, "ioctl error: VIDIOC_REQBUFS: %s\n", strerror(errno));
return -1;
}


使用 VIDIOC_REQBUFS 指令申请帧缓冲,该缓冲区实质上是由内核所维护的, 应用程序不能直接读取该缓冲区的数据 ,我们需要将其映射到用户空间中,这样,应用程序读取映射区的数据实际上就是读取内核维护的帧缓冲中的数据。

cpp 复制代码
/* 建立内存映射 */
struct v4l2_buffer buf
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
for (buf.index = 0; buf.index < 3; buf.index++) {

    ioctl(fd, VIDIOC_QUERYBUF, &buf);//查看帧缓冲信息
    frm_base[buf.index] = mmap(NULL, buf.length,PROT_READ | PROT_WRITE, MAP_SHARED,fd, buf.m.offset);
    if (MAP_FAILED == frm_base[buf.index]) {
        perror("mmap error");
        return -1;
    }
}

3.5入队,开始采集

使用 VIDIOC_QBUF 指令将帧缓冲放入到内核的帧缓冲队列,将三个帧缓冲放入内核的帧缓冲队列(入队操作)中:

cpp 复制代码
struct v4l2_buffer buf;
/* 入队操作 */
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;/*入队前需先设置 struct v4l2_buffer 类型对象的 memory、type 字段*/
for (buf.index = 0; buf.index < 3; buf.index++) {
    
    if (0 > ioctl(fd, VIDIOC_QBUF, &buf)) {
        perror("ioctl error");
        return -1;
    }
}

开始采集:

cpp 复制代码
ioctl(int fd, VIDIOC_STREAMON, int *type); //开启视频采集
ioctl(int fd, VIDIOC_STREAMOFF, int *type); //停止视频采集

enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (0 > ioctl(fd, VIDIOC_STREAMON, &type)) {
    perror("ioctl error");
    return -1;
}

3.6出队

边采集边出队

cpp 复制代码
struct v4l2_buffer buf;
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
for ( ; ; ) {
    for(buf.index = 0; buf.index < 3; buf.index++) {
        ioctl(fd, VIDIOC_DQBUF, &buf); //出队
        // 读取帧缓冲的映射区、获取一帧数据
        // 处理这一帧数据
        do_something();
        // 数据处理完之后、将当前帧缓冲入队、接着读取下一帧数据
        ioctl(fd, VIDIOC_QBUF, &buf);
    }
}

帧缓冲出队之后,接下来便可读取数据了,然后对数据进行处理, 比如对数据进行转化,如RGB888转RGB565,将转换后的数据刷到FrameBuffer上进行显示。处理完后再入队继续采集。

3.7关闭采集

如果要结束视频采集,使用 VIDIOC_STREAMOFF 指令:

cpp 复制代码
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (0 > ioctl(fd, VIDIOC_STREAMOFF, &type)) {
    perror("ioctl error");
    return -1;
}

3.8 摄像头采集到的图像数据显示到开发板 LCD 屏上

相关推荐
心随_风动3 分钟前
RHEL与CentOS:从同源到分流的开源操作系统演进
linux·开源·centos
ALex_zry11 分钟前
跨Linux发行版CPU指令集兼容性深度解析与实践指南
linux·运维·服务器
特立独行的猫a27 分钟前
redis客户端库redis++在嵌入式Linux下的交叉编译及使用
linux·数据库·c++·redis·redis客户端库
无敌小茶1 小时前
Linux学习笔记之环境变量
linux·笔记
Harbor Lau1 小时前
Linux常用中间件命令大全
linux·运维·中间件
漫谈网络1 小时前
基于 Netmiko 的网络设备自动化操作
运维·自动化·netdevops·netmiko
꧁坚持很酷꧂2 小时前
Linux Ubuntu18.04下安装Qt Craeator 5.12.9(图文详解)
linux·运维·qt
凉、介2 小时前
PCI 总线学习笔记(五)
android·linux·笔记·学习·pcie·pci
电鱼智能的电小鱼3 小时前
EFISH-SBC-RK3588无人机地面基准站项目
linux·网络·嵌入式硬件·机器人·无人机·边缘计算
电鱼智能的电小鱼3 小时前
基于 EFISH-SBC-RK3588 的无人机环境感知与数据采集方案
linux·网络·嵌入式硬件·数码相机·无人机·边缘计算