目录
什么是V4L2?
V4L2编程流程
1.打开摄像头设备
2.查询设备的属性或功能
3.设置合适的采样方式
4.如果支持STREAM则设置缓冲队列属性
5.内存映射
6.开启视频采集
7.帧缓冲出队、对采集的数据进行处理(保存为图片,或者通过网络协议发送)
8.停止采集,释放映射,关闭设备
什么是V4L2?
V4L2,即 Video for linux two ,是 Linux 内核中视频类设备的一套驱动框架,为视频类设备驱动开发和应用层提供了一套统一的接口规范使用 V4L2 设备驱动框架注册的设备会在 Linux 系统/dev/目录下生成对应的设备节点文件,设备节点的名称通常为 videoX(X为0、1、2…)
V4L2是Linux视频处理模块的最新标准代码,包括对视频输入设备的处理,如高频(即、电视信号输入端子)或摄像头,还包括视频处理输出装置。一般来说,最常见的是使用V4L2来处理相机数据采集的问题。我们通常使用的相机实际上是一个图像传感器,将捕捉到的光线通过视频芯片处理后,编码成JPG/MJPG或YUV格式输出。我们可以很容易地通过V4L2与第一台摄像机设备“通信”,如设置或获取它们的工作参数。
V4L2编程流程
在内核中,摄像头捕捉到的视频数据,我们可以使用一个队列来存储。我们做的工作大致是这样的:首先配置摄像头的相关参数,可以正常工作,然后申请一个号码的内核视频缓存,送他们到队列,像三个空盘子在传送带上。然后我们还需要三个内核缓存区域通过mmap函数映射到用户空间,
这样我们就可以操作相机数据在用户层,然后我们可以启动相机开始数据采集,每一帧捕获数据我们可以做一个团队操作,读取数据,然后再阅读内核缓存的数据小组,依次循环。
V4L2接口编程步骤:
- 首先是打开摄像头设备;
- 查询设备的属性或功能;
- 设置设备的参数,譬如像素格式、 帧大小、 帧率;
- 申请帧缓冲、 内存映射(.把内核的缓冲区队列映射到用户空间);
- 帧缓冲入队;
- 开启视频采集;
- 帧缓冲出队、对采集的数据进行处理;
- 处理完后,再次将帧缓冲入队,往复;
- 结束采集,释放映射,关闭设备。
1.打开摄像头设备
视频类设备对应的设备节点为/dev/videoX(X为0、1、2…)可以使用命令 ls /dev/video*
查看视频类设备对应的设备节点,在ARM板上插上USB摄像头之后可以看到多了一个设备节点。
//1. 打开摄像头
int fd = open("/dev/video0", O_RDWR);
if(-1 == fd)
{
perror("open");//打开失败
return -1;
}
2.查询设备的属性或功能
1.提取摄像头的能力,查看设备是否为视频采集设备以及支持那些功能
#if 0
/***结构体里是摄像头支持的一些功能****/
struct v4l2_capability {
__u8 driver[16]; /* i.e. "bttv" */
__u8 card[32]; /* i.e. "Hauppauge WinTV" */
__u8 bus_info[32]; /* "PCI:" + pci_name(pci_dev) */
__u32 version; /* should use KERNEL_VERSION() */
__u32 capabilities; /* Device capabilities */
__u32 reserved[4];
};
#endif
struct v4l2_capability cap = {0};
//2. 提取摄像头的能力(linux v4l2规范)
int ret = ioctl(fd, VIDIOC_QUERYCAP, &cap);
if (-1 == ret)
{
perror("ioctl");
return -1;
}
printf("driver name: %s device name: %s device location: %s\n", cap.driver, cap.card, cap.bus_info);
printf("version: %u.%u.%u\n", (cap.version >> 16) & 0xff, (cap.version >> 8) & 0xff, cap.version & 0xff);
if(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)
printf("v4l2 dev support capture\n");
if(cap.capabilities & V4L2_CAP_VIDEO_OUTPUT)
printf("v4l2 dev support output\n");
if(cap.capabilities & V4L2_CAP_VIDEO_OVERLAY)
printf("v4l2 dev support overlay\n");
if(cap.capabilities & V4L2_CAP_STREAMING)
printf("v4l2 dev support streaming\n");
if(cap.capabilities & V4L2_CAP_READWRITE)
printf("v4l2 dev support read write\n");
2.查看摄像头所支持的所有像素格式,先看一下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];
};
代码查看设备支持那些格式如下:
struct v4l2_fmtdesc fmtdesc;
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmtdesc.index = 0;
//支持哪些图片格式
while (!ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc))
{
printf("fmt:%s\n", fmtdesc.description);
fmtdesc.index++;
}
3.查看摄像头所支持的分辨率,先看一下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++;
}
4.查看摄像头所支持的视频采集帧率,先看一下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++;
}
3.设置合适的采样方式
首先要定义结构体v4l2_format来保存采集格式信息,使用VIDIOC_S_FMT指令设置格式,最后用VIDIOC_G_FMT指令查看相关参数是否生效,我们先来看看结构体v4l2_format
struct v4l2_format {
__u32 type;
union {
struct v4l2_pix_format pix; /* V4L2_BUF_TYPE_VIDEO_CAPTURE */
struct v4l2_pix_format_mplane pix_mp; /* V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE */
struct v4l2_window win; /* V4L2_BUF_TYPE_VIDEO_OVERLAY */
struct v4l2_vbi_format vbi; /* V4L2_BUF_TYPE_VBI_CAPTURE */
struct v4l2_sliced_vbi_format sliced; /* V4L2_BUF_TYPE_SLICED_VBI_CAPTURE */
struct v4l2_sdr_format sdr; /* V4L2_BUF_TYPE_SDR_CAPTURE */
struct v4l2_meta_format meta; /* V4L2_BUF_TYPE_META_CAPTURE */
__u8 raw_data[200]; /* user-defined */
} fmt;
};
代码实现:
//3. 设置合适的采样方式
struct v4l2_format v4l2_fmt;
memset(&v4l2_fmt, 0, sizeof(struct v4l2_format));
v4l2_fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
v4l2_fmt.fmt.pix.width = W; //宽度
v4l2_fmt.fmt.pix.height = H; //高度
v4l2_fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG; //像素格式M-JPEG
v4l2_fmt.fmt.pix.field = V4L2_FIELD_ANY;
// 检查设置参数是否生效
if (ioctl(fd, VIDIOC_S_FMT, &v4l2_fmt) < 0)
{
printf("ERR(%s):VIDIOC_S_FMT failed\n", __func__);
return -1;
}
4.如果支持STREAM则设置缓冲队列属性
申请帧缓冲顾名思义就是申请用于存储一帧图像数据的缓冲区, 使 VIDIOC_REQBUFS 指令可申请帧缓冲其中struct v4l2_requestbuffers 结构体描述了申请帧缓冲的信息。我们先看看结构体v4l2_buffer
struct v4l2_buffer {
__u32 index; //缓冲区编号
__u32 type;
__u32 bytesused; //图像大小
__u32 flags;
__u32 field;
struct timeval timestamp;
struct v4l2_timecode timecode;
__u32 sequence; /* memory location */
__u32 memory;
union {
__u32 offset;
unsigned long userptr;
struct v4l2_plane *planes;
__s32 fd;
} m;
__u32 length;
__u32 reserved2;
union {
__s32 request_fd;
__u32 reserved;
};
};
代码实现:
void* addr[4]; //缓存地址, 假设定为系统缓存4帧
unsigned int size[4];
struct v4l2_requestbuffers req;
req.count = 4; //缓存数量
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0)
{
printf("ERR(%s):VIDIOC_REQBUFS failed\n", __func__);
return -1;
}
5.内存映射
把内核的缓冲区队列映射到用户空间
int i = 0;
for(;i<4; i++)
{
struct v4l2_buffer v4l2_buffer;
memset(&v4l2_buffer, 0, sizeof(struct v4l2_buffer));
v4l2_buffer.index = i; //想要查询的缓存
v4l2_buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
v4l2_buffer.memory = V4L2_MEMORY_MMAP;
/* 查询缓存信息 */
int ret = ioctl(fd, VIDIOC_QUERYBUF, &v4l2_buffer);
if(ret < 0)
{
printf("Unable to query buffer.\n");
return -1;
}
/* 得到图像缓冲位置 */
addr[i] = mmap(NULL /* start anywhere */ ,
v4l2_buffer.length, PROT_READ | PROT_WRITE, MAP_SHARED,
fd, v4l2_buffer.m.offset);
size[i]=v4l2_buffer.length;
if (ioctl(fd, VIDIOC_QBUF, &v4l2_buffer) < 0)
{
printf("ERR(%s):VIDIOC_QBUF failed\n", __func__);
return -1;
}
}
6.开启视频采集
//5. 开启采样流
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(fd, VIDIOC_STREAMON, &type) < 0)
{
printf("ERR(%s):VIDIOC_STREAMON failed\n", __func__);
return -1;
}
7.帧缓冲出队、对采集的数据进行处理(保存为图片,或者通过网络协议发送)
这里我们使用了IO多路复用,可以同时对多个设备进行操作。
//6. 等待采样成功并提取图像(select)
struct pollfd poll_fds[1];
poll_fds[0].fd = fd;
poll_fds[0].events = POLLIN; //关注可读
poll(poll_fds, 1, 10000);//等待有图像准备好
//6.2 提取采样内容
struct v4l2_buffer buffer;
buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buffer.memory = V4L2_MEMORY_MMAP;
if (ioctl(fd, VIDIOC_DQBUF, &buffer) < 0)
{
printf("ERR(%s):VIDIOC_DQBUF failed, dropped frame\n", __func__);
return -1;
}
//buffer.index: 成功拍摄出画面的下标
//buffer.bytesused: 成功拍下来图片的大小
//addr[buffer.index]: 图片存储的内存位置
printf("ready: %u %u\n", buffer.index, buffer.bytesused);
//假如实现视频监控: 你应该将内存图片通过网络发走!(这里使用了UDP协议)
#if 1
int ret=sendto(Socket_fd, addr[buffer.index], buffer.bytesused, 0, (struct sockaddr *)&Phone_ipaddr, sizeof(Phone_ipaddr));
if(ret<0)
{
perror("sendto");
}
printf("%d\n",ret);
#endif
//假设实现照相机: 你应该将内存图片保存到文件中!
#if 0
int fdjpg = open("1.jpg", O_CREAT|O_WRONLY, 0666);
if(-1 == fdjpg)
{
perror("open 1.jpg");
}
else
{
//内存图片保存到文件中
write(fdjpg, addr[buffer.index], buffer.bytesused);
}
close(fdjpg);
break;
#endif
//6.3 标识图像已经取走,读取数据并处理完之后要再次入队
if (ioctl(fd, VIDIOC_QBUF, &buffer) < 0)
{
printf("ERR(%s):VIDIOC_QBUF failed\n", __func__);//入队失败
return -1;
}
}
8.停止采集,释放映射,关闭设备
if (ioctl(fd, VIDIOC_STREAMOFF, &type) < 0)
{
printf("ERR(%s):VIDIOC_STREAMOFF failed\n", __func__);
return -1;
}
// 释放映射
for(int i=0;i<4;i++){
munmap(add[i],size[i]);
}
close(fd);