V4L2编程实战
开发环境
- 虚拟机Ubuntu 16.04
- 编辑器VsCode
- 交叉编译工具 arm-linux-gnueabihf
- 已制作文件系统,已使能UVC相关驱动
- 正点原子ZYNQ7010启明星开发板
- USB摄像头淘宝随便买的一个
V4L2简介
V4L2,即 Video for linux two ,是 Linux 内核中视频类设备的一套驱动框架,为视频类设备驱动开发和应用层提供了一套统一的接口规范
使用 V4L2 设备驱动框架注册的设备会在 Linux 系统/dev/目录下生成对应的设备节点文件,设备节点的名称通常为 videoX(X为0、1、2…)
编程流程
- 首先是打开摄像头设备;
- 查询设备的属性或功能;
- 设置设备的参数,譬如像素格式、 帧大小、 帧率;
- 申请帧缓冲、 内存映射;
- 帧缓冲入队;
- 开启视频采集;
- 帧缓冲出队、对采集的数据进行处理;
- 处理完后,再次将帧缓冲入队,往复;
- 结束采集。
打开设备
视频类设备对应的设备节点为/dev/videoX(X为0、1、2…)
可以使用命令 ls /dev/video*
查看视频类设备对应的设备节点
笔者在Arm板上插上USB摄像头之后可以看到多了一个设备节点为 /dev/video1
//定义一个设备描述符
int fd;
fd = open("/dev/videoX", O_RDWR);
if(fd < 0){
perror("video设备打开失败\n");
return -1;
}
else{
printf("video设备打开成功\n");
}
查看设备的属性和能力
- 首先查看设备是否为视频采集设备
ioctl(fd, VIDIOC_QUERYCAP, &vcap);
if (!(V4L2_CAP_VIDEO_CAPTURE & vcap.capabilities)) {
perror("Error: No capture video device!\n");
return -1;
}
- 查看摄像头所支持的所有像素格式,先看一下v4l2_fmtdesc结构体
struct v4l2_fmtdesc {
__u32 index; /* index 就是一个编号 */
__u32 type; /* enum v4l2_buf_type */
__u32 flags;
__u8 description[32]; /* description 字段是一个简单地描述性字符串 */
__u32 pixelformat; /* pixelformat 字段则是对应的像素格式编号 */
__u32 reserved[4];
};
代码实现,使用VIDIOC_ENUM_FMT
struct v4l2_fmtdesc fmtdesc;
fmtdesc.index = 0;
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
printf("摄像头支持所有格式如下:\n");
while(ioctl(fd,VIDIOC_ENUM_FMT,&fmtdesc) == 0){
printf("v4l2_format%d:%s\n",fmtdesc.index,fmtdesc.description);
fmtdesc.index++;
}
- 查看摄像头所支持的分辨率,先看一下v4l2_frmsizeenum结构体
struct v4l2_frmsizeenum {
__u32 index; /* Frame size number */
__u32 pixel_format; /* 像素格式 */
__u32 type; /* type */
union { /* Frame size */
struct v4l2_frmsize_discrete discrete;
struct v4l2_frmsize_stepwise stepwise;
};
__u32 reserved[2]; /* Reserved space for future use */
};
struct v4l2_frmsize_discrete {
__u32 width; /* Frame width [pixel] */
__u32 height; /* Frame height [pixel] */
};
比如我们要枚举出摄像头 MJPEG 像素格式所支持的所有分辨率:
struct v4l2_frmsizeenum frmsize;
frmsize.index = 0;
frmsize.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
printf("MJPEG格式支持所有分辨率如下:\n");
frmsize.pixel_format = V4L2_PIX_FMT_MJPEG;
while(ioctl(fd,VIDIOC_ENUM_FRAMESIZES,&frmsize) == 0){
printf("frame_size<%d*%d>\n",frmsize.discrete.width,frmsize.discrete.height);
frmsize.index++;
}
- 查看摄像头所支持的视频采集帧率,先看一下v4l2_frmivalenum结构体
struct v4l2_frmivalenum {
__u32 index; /* Frame format index */
__u32 pixel_format;/* Pixel format */
__u32 width; /* Frame width */
__u32 height; /* Frame height */
__u32 type; /* type */
union { /* Frame interval */
struct v4l2_fract discrete;
struct v4l2_frmival_stepwise stepwise;
};
__u32 reserved[2]; /* Reserved space for future use */
};
struct v4l2_fract {
__u32 numerator; //分子
__u32 denominator; //分母
};
比如我们要枚举出摄像头 MJPEG 格式下640*480分辨率所支持的帧数:
struct v4l2_frmivalenum frmival;
frmival.index = 0;
frmival.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
frmival.pixel_format = V4L2_PIX_FMT_MJPEG;
frmival.width = 640;
frmival.height = 480;
while(ioctl(fd,VIDIOC_ENUM_FRAMEINTERVALS,&frmival) == 0){
printf("frame_interval under frame_size <%d*%d> support %dfps\n",frmival.width,frmival.height,frmival.discrete.denominator / frmival.discrete.numerator);
frmival.index++;
}
设置采集格式
首先要定义结构体v4l2_format来保存采集格式信息,使用VIDIOC_S_FMT指令设置格式,最后用VIDIOC_G_FMT指令查看相关参数是否生效
struct v4l2_format vfmt;
vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
vfmt.fmt.pix.width = 640;
vfmt.fmt.pix.height = 480;
vfmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
if(ioctl(fd,VIDIOC_S_FMT,&vfmt) < 0){
perror("设置格式失败\n");
return -1;
}
// 检查设置参数是否生效
if(ioctl(fd,VIDIOC_G_FMT,&vfmt) < 0){
perror("获取设置格式失败\n");
return -1;
}
else if(vfmt.fmt.pix.width == 640 && vfmt.fmt.pix.height == 480 && vfmt.fmt.pix.pixelformat == V4L2_PIX_FMT_MJPEG){
printf("设置格式生效,实际分辨率大小<%d * %d>,图像格式:Motion-JPEG\n",vfmt.fmt.pix.width,vfmt.fmt.pix.height);
}
else{
printf("设置格式未生效\n");
}
申请缓冲区空间
申请帧缓冲顾名思义就是申请用于存储一帧图像数据的缓冲区, 使 VIDIOC_REQBUFS 指令可申请帧缓冲
其中struct v4l2_requestbuffers 结构体描述了申请帧缓冲的信息,代码实现如下:
struct v4l2_requestbuffers reqbuf;
reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuf.count = 3; //3个帧缓冲
reqbuf.memory = V4L2_MEMORY_MMAP;
if(ioctl(fd,VIDIOC_REQBUFS,&reqbuf) < 0){
perror("申请缓冲区失败\n");
return -1;
}
内存映射
// 将帧缓冲映射到进程地址空间
void *frm_base[3]; //映射后的用户空间的首地址
unsigned int frm_size[3];
struct v4l2_buffer buf;
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
// 将每一帧对应的缓冲区的起始地址保存在frm_base数组中,读取采集数据时,只需直接读取映射区即可
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);
frm_size[buf.index] = buf.length;
if(frm_base[buf.index] == MAP_FAILED){
perror("mmap failed\n");
return -1;
}
// 入队操作
if(ioctl(fd,VIDIOC_QBUF,&buf) < 0){
perror("入队失败\n");
return -1;
}
}
开启视频采集
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(fd, VIDIOC_STREAMON, &type) < 0){
perror("开始采集失败\n");
return -1;
}
读取帧并保存为.jpg格式的图片
struct v4l2_buffer readbuffer;
readbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
readbuffer.memory = V4L2_MEMORY_MMAP;
if(ioctl(fd, VIDIOC_DQBUF, &readbuffer) < 0){
perror("读取帧失败\n");
}
// 保存这一帧,格式为jpg
FILE *file = fopen("v4l2_cap.jpg","w+");
fwrite(frm_base[readbuffer.index],buf.length,1,file);
fclose(file);
读取数据并处理完之后要再次入队
if(ioctl(fd,VIDIOC_QBUF,&readbuffer) < 0){
perror("入队失败\n");
}
停止采集,释放映射
// 停止采集
if (ioctl(fd, VIDIOC_STREAMOFF, &type) < 0){
perror("停止采集失败\n");
return -1;
}
// 释放映射
for(int i=0;i<3;i++){
munmap(frm_base[i],frm_size[i]);
}
close(fd);
交叉编译,移植到ARM开发板运行
笔者的开发板插上USB摄像头之后生成的采集设备为/dev/video1
在Linux虚拟机也可以运行,用gcc编译即可生成的采集设备为/dev/video0
执行命令 ./v4l2_first_test /dev/video1
输出结果如下
然后可以在同级目录下找到保存的.jpg文件,可以在Linux虚拟机内使用scp命令从开发板下载该图片查看
scp使用方法可以看我的这篇博客: 开发板和虚拟机Linux使用scp命令互传文件
参考资料
【正点原子】I.MX6U 嵌入式 Linux C 应用编程指南 V1.4