简单介绍
最近实习做了一个类似通过USB接口的摄像头显示图像的功能,不过手上有的是一个比较简单的HDMI转USB的小模块,也可以实现这样的功能原理一样的,只需要在HDMI接入一个输入源就可以了。此文章也可以作为要使用V4L2去做视频采集图像采集功能的小伙伴提供一个思路。流程也比较简单,主要是做一个记录,记录一下这个过程中遇到坑和一些心得体会,方便到时候回来复习,本人也是新手,菜的一批,也是刚开始写博客,希望大佬们多多指教,尽量少喷、少喷哈谢谢哈哈哈哈!不说废话了,这就开始吧!
开发环境
- 虚拟机Ubuntu 20.00
- 编辑器VsCode
- 交叉编译工具 aarch64-linux-gnu
- HDMI转usb图像模块或者usb摄像头淘宝随便买一个都可以
- 开发板是公司的板子,这里就不透露啦,基本的嵌入式开发板都可以的
什么是V4L2?
编程步骤
流程图
图片来自正点原子的MX6U嵌入式Linux C应用编程指南
1.打开设备
整个程序需要包含的头文件,最重要的是##include <linux/videodev2.h>,其他可根据个人情况选择
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>
#include <errno.h>
#include <sys/mman.h>
#include <linux/videodev2.h>
#include <linux/fb.h>
打开设备,并以fd来保存返回的设备描述号,open成功返回设备号,不成功返回-1
//定义一个设备描述符
int fd;
struct v4l2_capability cap;//定义结构体类型
fd = open("/dev/video0", O_RDWR);
if(fd < 0)
{
perror("video设备打开失败\n");
return -1;
}
else
{
printf("video设备打开成功\n");
}
struct v4l2_capability结构体是linux/videodev2.h头文件已经有包含的了,我们调用就好,详解大家可以网上搜索进一步了解,这里就不过多介绍啦!
struct v4l2_capability {
__u8 driver[16]; /* 驱动的名字 */
__u8 card[32]; /* 设备的名字 */
__u8 bus_info[32]; /* 总线的名字 */
__u32 version; /* 版本信息 */
__u32 capabilities; /* 设备拥有的能力 */
__u32 device_caps;
__u32 reserved[3]; /* 保留字段 */
};
2. 查询设备的属性或功能
查询设备是否具备获取图像能力,同时顺便打印一下设备信息,便于观察(可忽略)
ioctl(fd, VIDIOC_QUERYCAP, &cap);
if (!(V4L2_CAP_VIDEO_CAPTURE & cap.capabilities))
{
perror("Error: No capture video device!\n");
return -1;
}
printf("驱动名 : %s\n",cap.driver);
printf("设备名字 : %s\n",cap.card);
printf("总线信息 : %s\n",cap.bus_info);
printf("驱动版本号 : %d\n",cap.version);
列举设备所支持的格式
这里ioctl(fd,VIDIOC_ENUM_FRAMESIZES,&frmsize),ioctl用法大家可以去搜索具体了解一下,这里的意思就是对fd指向的设备进行设备参数的列举,VIDIOC_ENUM_FRAMESIZES指令就是可以枚举出设备里面的参数的
列举MJEPG格式支持所有分辨率:
struct v4l2_frmsizeenum frmsize;
frmsize.index = 0;
frmsize.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
printf("MJEPG格式支持所有分辨率如下:\n");
frmsize.pixel_format = V4L2_PIX_FMT_MJEPG;
while(ioctl(fd,VIDIOC_ENUM_FRAMESIZES,&frmsize) == 0)
{
printf("frame_size<%d*%d>\n",frmsize.discrete.width,frmsize.discrete.height);
frmsize.index++;
}
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 格式下1280*720分辨率所支持的帧数:
struct v4l2_frmivalenum frmival;
frmival.index = 0;
frmival.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
frmival.pixel_format = V4L2_PIX_FMT_MJEPG;
frmival.width = 1280;
frmival.height = 720;
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_PIX_FMT_MJEPG表示图像格式, frmival.width 是图像宽度,frmival.height 是图像高度
-struct v4l2_fract 结构体中,numerator 表示分子、denominator 表示分 母,使用 numerator / denominator 来表示图像采集的周期(采集一幅图像需要多少秒),所以视频帧率便等于 denominator / numerator
3.设置设备的参数,譬如像素格式、帧大小、帧率等
struct v4l2_format vfmt; //头文件自带v4l2_format结构体,引用即可
vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
vfmt.fmt.pix.width = 1280;
vfmt.fmt.pix.height = 720;
vfmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJEPG;
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 == 1280 && vfmt.fmt.pix.height == 720 && vfmt.fmt.pix.pixelformat == V4L2_PIX_FMT_MJEPG )
{
printf("设置格式生效,实际分辨率大小<%d * %d>,图像格式:Motion-JPEG\n",vfmt.fmt.pix.width,vfmt.fmt.pix.height);
}
else
{
printf("设置格式未生效\n");
}
获取流streamparm
/* 获取 streamparm */
struct v4l2_streamparm streamparm = {0};
streamparm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(fd, VIDIOC_G_PARM, &streamparm);
if (V4L2_CAP_TIMEPERFRAME & streamparm.parm.capture.capability)
{
streamparm.parm.capture.timeperframe.numerator = 1;
streamparm.parm.capture.timeperframe.denominator = 60;//60fps
if (0 > ioctl(fd, VIDIOC_S_PARM, &streamparm))
{
fprintf(stderr, "ioctl error: VIDIOC_S_PARM: %s\n", strerror(errno));
return -1;
}
}
else
fprintf(stderr, "不支持帧率设置");
4. 申请帧缓冲、内存映射
申请帧缓冲
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;
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;
}

5. 帧缓冲入队
简单介绍一下出队入队

// 入队操作
if(ioctl(fd,VIDIOC_QBUF,&buf) < 0)
{
perror("入队失败\n");
return -1;
}
}
6. 开启视频采集
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(fd, VIDIOC_STREAMON, &type) < 0)
{
perror("开始采集失败\n");
return -1;
}
7. 帧缓冲出队、对采集的数据进行处理
出队操作:
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格式保存下来
// 保存这一帧,格式为jpg
FILE *file = fopen("qqq","w+");
fwrite(frm_base[readbuffer.index],buf.length,1,file);
fclose(file);
将用完的数据又放回队列中,以便下一次取出
if(ioctl(fd,VIDIOC_QBUF,&readbuffer) < 0)
{
perror("入队失败\n");
}
注意:本文出队的操作是对一帧的数据图像进行保存,如果小伙伴们发现保存的图片是黑色的,这是正常的,因为有时候信号源刚开机的时候会有延迟,是黑屏的,你可能就刚好采集到这一帧,所以不奇怪,我刚开始也遇到这种问题。只需要对出队,开启采集,入队操作进行一个for循环,采集后面多几帧的数据就可以看到了
8.结束采集工作,关闭采集,释放映射
// 停止采集
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);
}
9.采集效果
好了,到这里基本的采集流程就差不多了,接下来就是进行交叉编译,把程序烧录到板子,运行程序就可以看到效果啦,如图:
看到这里的小伙伴也快点去试试吧,如果觉得这篇文章对你有所帮助,不妨碍点赞收藏+关注再走呗!编写不易,谢谢大家的支持哦,谢谢!谢谢!谢谢! 重要的事情说三遍!!!同时非常感谢
爱学习的诸葛铁锤这位大佬的文章“V4L2编程之USB摄像头采集jpeg图像”!
参考文献:正点原子的MX6U嵌入式Linux C应用编程指南,爱学习的诸葛铁锤—“V4L2编程之USB摄像头采集jpeg图像”