一、概述
Video for Linuxtwo(Video4Linux2)简称V4L2,是V4L的改进版。V4L2是linux操作系统下用于采集图片、视频和音频数据的API接口,配合适当的视频采集设备和相应的驱动程序,可以实现图片、视频、音频等的采集。在远程会议、可视电话、视频监控系统和嵌入式多媒体终端中都有广泛的应用。
二、编程思路
在Linux下,所有外设都被看成一种特殊的文件,成为“设备文件”,可以象访问普通文件一样对其进行读写。一般来说,采用V4L2驱动的摄像头设备文件是/dev/video0。V4L2支持两种方式来采集图像:内存映射方式(mmap)和直接读取方式(read)。V4L2在include/linux/videodev.h文件中定义了一些重要的数据结构,在采集图像的过程中,就是通过对这些数据的操作来获得最终的图像数据。Linux系统V4L2的能力可在Linux内核编译阶段配置,默认情况下都有此开发接口。
而摄像头所用的主要是capature了,视频的捕捉,具体linux的调用可以参考下图。
三、应用程序通过V4L2进行视频采集的原理
V4L2支持内存映射方式(mmap)和直接读取方式(read)来采集数据,前者一般用于连续视频数据的采集,后者常用于静态图片数据的采集,本文重点讨论内存映射方式的视频采集。
应用程序通过V4L2接口采集视频数据分为五个步骤:
- 打开视频设备文件,进程视频采集的参数初始化,通过V4L2接口设置视频图像的采集窗口、采集的点阵大小和格式。
- 申请若干视频采集的帧缓冲区,并将这些帧缓冲区从内核空间映射到用户空间,便于应用程序读取/处理视频数据。
- 将申请到的帧缓冲区在视频采集输入队列排队,,并启动视频采集
- 驱动开始视频数据的采集,应用程序从视频采集输出队列取出帧缓冲区,将帧缓冲区重新放入视频采集输入队列,循环往复采集连续的视频数据
- 停止视频采集
具体的程序实现流程可以参考下面的流程图:
其实其他的都比较简单,就是通过ioctl这个接口去设置一些参数。最主要的就是buf管理。他有一个或者多个输入队列和输出队列。
启动视频采集后,驱动程序开始采集一帧数据,把采集的数据放入视频采集输入队列的第一个帧缓冲区,一帧数据采集完成,也就是第一个帧缓冲区存满一帧数据后,驱动程序将该帧缓冲区移至视频采集输出队列,等待应用程序从输出队列取出。驱动程序接下来采集下一帧数据,放入第二个帧缓冲区,同样帧缓冲区存满下一帧数据后,被放入视频采集输出队列。
应用程序从视频采集输出队列中取出含有视频塑化剂的帧缓冲区,处理帧缓冲区中的视频数据,如压缩或存储。
最后,应用程序将处理完数据的帧缓冲区重新放入视频采集输入队列,这样可以循环采集,如图所示:
每一个帧缓冲区都有一个对应的状态标志变量,其中每一个比特代表一个状态。
V4L2_BUF_FLAG_UNMAPPED 0B0000
V4L2_BUF_FLAG_MAPPED 0B0001
V4L2_BUF_FLAG_ENQUEUED 0B0010
V4L2_BUF_FLAG_DONE 0B0100
缓冲区的状态转化如图所示。
四、核心命令字和结构体(参见/usr/include/linux/videodev2.h)
一、VIDIOC_ENUM_FMT
含义:枚举出当前摄像头(驱动)所支持的所有数据格式
具体用法如下:
ioctl(fd,VIDIOC_ENUM_FMT,struct v4l2_fmtdesc *argp);
通过迭代结构体struct v4l2_fmtdesc
中的index 成员,来枚举罗列支持的所有格式,该结构体的详细信息如下:
struct v4l2_fmtdesc
{
__u32 index; // 数据格式的索引
__u32 type; // 一般设置为 V4L2_BUF_TYPE_VIDEO_CAPTURE
__u32 flags;
__u8 description[32];
__u32 pixelformat;
__u32 reserved[4];
};
其中 type
跟v4l2_format
中的type设置要一致。在成功调用ioctl之后,description
将保存对当前获取的数据格式的描述。
二、VIDIOC_G_FMT / VIDIOC_S_FMT / VIDIOC_TRY_FMT
含义:
1.获取当前摄像头驱动数据格式
2.设置摄像头驱动数据格式
3.尝试设置格式
具体用法:
ioctl(fd,VIDIOC_G_FMT ,struct v4l2_format *argp)
ioctl(fd,VIDIOC_S_FMT ,struct v4l2_format *argp)
ioctl(fd,VIDIOC_TRY_FMT,struct v4l2_format *argp)
涉及数据结构:
struct v4l2_format
{
__u32 type;
union
{
struct v4l2_pix_format pix;
struct v4l2_pix_format_mplane pix_mp;
struct v4l2_window win;
struct v4l2_vbi_format vbi;
struct v4l2_sliced_vbi_format sliced;
__u8 raw_data[200];
} fmt;
}
V4l2_format
中的fmt是一个union
,其中哪个成员有效取决于type的取值,一般较常用的是取类型type
为 V4L2_BUF_TYPE_VIDEO_CAPTURE
,此时pix
生效。该成员的详细内部细节如下:
struct v4l2_pix_format
{
__u32 width;
__u32 height;
__u32 pixelformat;
__u32 field;
__u32 bytesperline;
__u32 sizeimage;
__u32 colorspace;
__u32 priv;
};
该结构体中的成员 pixelformat
代表视频输入驱动所使用的像素格式,常见的有V4L2_PIX_FMT_JPEG
、V4L2_PIX_FMT_YUV
、V4L2_PIX_FMT_MJPG
等。而成员field
代表视频帧传输的方式,选择 V4L2_FIELD_INTERLACED
为交错式。
三、VIDIOC_REQBUFS
含义:向内核申请视频缓存
具体用法如下:
ioctl(fd,VIDIOC_REQBUFS,v4l2_requestbuffers *argp)
该命令字所申请的缓存就是如下图所示的内核中处理视频数据的队列缓存,这些缓存的具体配置参数用如下结构体来指定:
struct v4l2_requestbuffers
{
__u32 count; // 申请缓存总个数
__u32 type; // 与 struct v4l2_format 中的 type 一致
__u32 memory;
__u32 reserved[2];
};
其中 memory
的取值为 V4L2_MEMORY_MMAP
V4L2_MEMORY_USERPTR
,取决于,当该字段被设置为 V4L2_MEMORY_MMAP
时,count
字段才有效。
四、VIDIOC_QUERYBUF
含义:内核成功分配了缓存后,取得这些缓存的具体参数
具体用法如下:
ioctl(fd, VIDIOC_QUERYBUF, v4l2_buffer *argp);
之所以需要取得这些缓存的具体参数的一个目的是,这些缓存都是处在内核空间的,我们并不能直接操作他们,因此需要将他们通过mmap映射到用法空间,这就要求必须知道他们的大小、偏移等信息。这些信息统一被储存到如下结构体中:
struct v4l2_buffer
{
__u32 index; // 内核缓存索引号,由用户指定,范围是[0 ~ count-1]
__u32 type; // 与 v4l2_format 中的 type 一致
__u32 bytesused;
__u32 flags;
__u32 field;
struct timeval timestamp;
struct v4l2_timecode timecode;
__u32 sequence;
__u32 memory; // 与 v4l2_requestbuffers 中的 memory 一致
union
{
__u32 offset; // 缓存相对于设备内存的偏移
unsigned long userptr;
struct v4l2_plane *planes;
__s32 fd;
} m;
__u32 length; // 缓存大小
__u32 reserved2;
__u32 reserved;
};
五、VIDIOC_QBUF / VIDIOC_DQBUF
含义:
1.使一个空的(视频输入时)或者一个满的(视频输出时)缓存入队
2.使一个满的(视频输入时)或者一个空的(视频输出时)缓存出队
具体用法如下:
ioctl(fd, VIDIOC_QBUF, v4l2_buffer *argp);
ioctl(fd, VIDIOC_DQBUF, v4l2_buffer *argp);
这两个命令字是捕捉视频帧最常用的动作,通过 v4l2_buffer
中 index
字段,将指定缓存出队或者入队,这里需要澄清的几个要点是:
1、在尚未开启摄像头取像之前,需要将空的缓存一一入队
2、针对视频输入,出队的时候如果缓存没有数据,那么出队将阻塞
3、虽然内核对这些内存的定义时“队列”,但实际上不按顺序“插队”也是可以的,但一般不那么做
六、VIDIOC_STREAMON / VIDIOC_STREAMOFF
含义:
1、开启I/O流
2、关闭I/O流
具体用法如下:
ioctl(fd, VIDIOC_STREAMON, const int *argp);
ioctl(fd, VIDIOC_STREAMOFF, const int *argp);
不管 I/O 方式被设定为内存映射(MMAP)方式还是用户指针(USERPTR)方式,都可以使用 VIDIOC_STREAMON
和 VIDIOC_STREAMOFF
来启停 I/O 流。事实上,在使用 ioctl 调用 VIDIOC_STREAMON
之前,物理硬件将暂时被禁用且没有缓存被填充数据。
VIDIOC_STREAMOFF
除了终止进程的 DMA 操作(如果有的话)之外,还将解锁用户指针指向的物理内存,队列中的所有缓存都将被移除,这意味着如果是视频输入,那么那些没来得及读取的视频帧将被丢弃,如果是视频输出,那么那写没来及传输的视频帧也同样会被丢弃。
五、V4L2 API及数据结构
V4L2是V4L的升级版本,为linux下视频设备程序提供了一套接口规范。包括一套数据结构和底层V4L2驱动接口。
1.常用的结构体在内核目录include/linux/videodev2.h中定义
struct v4l2_requestbuffers //申请帧缓冲,对应命令VIDIOC_REQBUFS
struct v4l2_capability //视频设备的功能,对应命令VIDIOC_QUERYCAP
struct v4l2_input //视频输入信息,对应命令VIDIOC_ENUMINPUT
struct v4l2_standard //视频的制式,比如PAL,NTSC,对应命令VIDIOC_ENUMSTD
struct v4l2_format //帧的格式,对应命令VIDIOC_G_FMT、VIDIOC_S_FMT等
struct v4l2_buffer //驱动中的一帧图像缓存,对应命令VIDIOC_QUERYBUF
struct v4l2_crop //视频信号矩形边框
struct v4l2_std_id //视频制式
2、常用的IOCTL接口命令也在include/linux/videodev2.h中定义
VIDIOC_REQBUFS //分配内存
VIDIOC_QUERYBUF //把VIDIOC_REQBUFS中分配的数据缓存转换成物理地址
VIDIOC_QUERYCAP //查询驱动功能
VIDIOC_ENUM_FMT //获取当前驱动支持的视频格式
VIDIOC_S_FMT //设置当前驱动的频捕获格式
VIDIOC_G_FMT //读取当前驱动的频捕获格式
VIDIOC_TRY_FMT //验证当前驱动的显示格式
VIDIOC_CROPCAP //查询驱动的修剪能力
VIDIOC_S_CROP //设置视频信号的矩形边框
VIDIOC_G_CROP //读取视频信号的矩形边框
VIDIOC_QBUF //把数据从缓存中读取出来
VIDIOC_DQBUF //把数据放回缓存队列
VIDIOC_STREAMON //开始视频显示函数
VIDIOC_STREAMOFF //结束视频显示函数
VIDIOC_QUERYSTD //检查当前视频设备支持的标准,例如PAL或NTSC。
3、操作流程
V4L2提供了很多访问接口,你可以根据具体需要选择操作方法。需要注意的是,很少有驱动完全实现了所有的接口功能。所以在使用时需要参考驱动源码,或仔细阅读驱动提供者的使用说明。下面列举出一种操作的流程,供参考。
(1)打开设备文件
int fd = open(Devicename,mode);
Devicename:/dev/video0、/dev/video1 ……
Mode:O_RDWR | O_NONBLOCK
如果使用非阻塞模式调用视频设备,则当没有可用的视频数据时,不会阻塞,而立刻返回。
(2)获取摄像头设备的基本参数
struct v4l2_capability cap;
bzero(&cap, sizeof(cap));
if(ioctl(camfd, VIDIOC_QUERYCAP, &cap) == -1)
{
printf("获取摄像头基本信息失败: %s\n", strerror(errno));
exit(0);
}
printf("驱动:%s\n", cap.driver);
printf("显卡:%s\n", cap.card);
printf("总线:%s\n", cap.bus_info);
printf("版本:%d\n", cap.version);
if((cap.capabilities&V4L2_CAP_VIDEO_CAPTURE) == V4L2_CAP_VIDEO_CAPTURE)
{
printf("该设备为视频采集设备\n");
}
if((cap.capabilities&V4L2_CAP_STREAMING) == V4L2_CAP_STREAMING)
{
printf("该设备支持流IO操作\n\n");
}
(3)获取摄像头格式信息(固定)
struct v4l2_fmtdesc fmtdesc;
fmtdesc.index = 0;
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
int ret;
printf("像素格式: \n");
while((ret=ioctl(camfd, VIDIOC_ENUM_FMT, &fmtdesc)) == 0)
{
printf("[%d]", fmtdesc.index);
sprintf(formats[fmtdesc.index]+0, "%c", (fmtdesc.pixelformat>>8*0)&0xFF);
sprintf(formats[fmtdesc.index]+1, "%c", (fmtdesc.pixelformat>>8*1)&0xFF);
sprintf(formats[fmtdesc.index]+2, "%c", (fmtdesc.pixelformat>>8*2)&0xFF);
sprintf(formats[fmtdesc.index]+3, "%c", (fmtdesc.pixelformat>>8*3)&0xFF);
printf("\"%s\"", formats[fmtdesc.index]);
printf("(详细描述: %s)\n", fmtdesc.description);
fmtdesc.index++;
}
(4)获取摄像头格式信息(可调)
struct v4l2_format fmt;
bzero(&fmt, sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if(ioctl(camfd, VIDIOC_G_FMT, &fmt) == -1)
{
printf("获取摄像头格式信息失败: %s\n", strerror(errno));
exit(0);
}
printf("分辨率: %d×%d\n", fmt.fmt.pix.width, fmt.fmt.pix.height);
printf("像素格式: ");
switch(fmt.fmt.pix.pixelformat)
{
case V4L2_PIX_FMT_MJPEG:
printf("V4L2_PIX_FMT_MJPEG\n");
break;
case V4L2_PIX_FMT_JPEG:
printf("V4L2_PIX_FMT_JPEG\n");
break;
case V4L2_PIX_FMT_MPEG:
printf("V4L2_PIX_FMT_MPEG\n");
break;
case V4L2_PIX_FMT_MPEG1:
printf("V4L2_PIX_FMT_MPEG1\n");
break;
case V4L2_PIX_FMT_MPEG2:
printf("V4L2_PIX_FMT_MPEG2\n");
break;
case V4L2_PIX_FMT_MPEG4:
printf("V4L2_PIX_FMT_MPEG4\n");
break;
case V4L2_PIX_FMT_H264:
printf("V4L2_PIX_FMT_H264\n");
break;
case V4L2_PIX_FMT_XVID:
printf("V4L2_PIX_FMT_XVID\n");
break;
case V4L2_PIX_FMT_RGB24:
printf("V4L2_PIX_FMT_RGB24\n");
break;
case V4L2_PIX_FMT_BGR24:
printf("V4L2_PIX_FMT_BGR24\n");
break;
case V4L2_PIX_FMT_YUYV:
printf("V4L2_PIX_FMT_YUYV\n");
break;
case V4L2_PIX_FMT_YYUV:
printf("V4L2_PIX_FMT_YYUV\n");
break;
case V4L2_PIX_FMT_YVYU:
printf("V4L2_PIX_FMT_YVYU\n");
break;
case V4L2_PIX_FMT_YUV444:
printf("V4L2_PIX_FMT_YUV444\n");
break;
case V4L2_PIX_FMT_YUV410:
printf("V4L2_PIX_FMT_YUV410\n");
break;
case V4L2_PIX_FMT_YUV420:
printf("V4L2_PIX_FMT_YUV420\n");
break;
case V4L2_PIX_FMT_YVU420:
printf("V4L2_PIX_FMT_YVU420\n");
break;
case V4L2_PIX_FMT_YUV422P:
printf("V4L2_PIX_FMT_YUV422P\n");
break;
default:
printf("未知\n");
}
(5)配置摄像头像素格式
struct v4l2_format *tmp = calloc(1, sizeof(*tmp));
bzero(tmp, sizeof(*tmp));
tmp->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
tmp->fmt.pix.width = 800;
tmp->fmt.pix.height = 448;
printf("\n请选择要配置的像素格式:");
int n; scanf("%d", &n);
if(!strcmp(formats[n], "JPEG")) tmp->fmt.pix.pixelformat = V4L2_PIX_FMT_JPEG;
else if(!strcmp(formats[n], "MJPG")) tmp->fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
else if(!strcmp(formats[n], "MPEG")) tmp->fmt.pix.pixelformat = V4L2_PIX_FMT_MPEG;
else if(!strcmp(formats[n], "YUYV")) tmp->fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
else if(!strcmp(formats[n], "YVYU")) tmp->fmt.pix.pixelformat = V4L2_PIX_FMT_YVYU;
else if(!strcmp(formats[n], "H264")) tmp->fmt.pix.pixelformat = V4L2_PIX_FMT_H264;
else
{
printf("对不起,所选格式无法配置.\n");
exit(0);
}
tmp->fmt.pix.field = V4L2_FIELD_INTERLACED;
if(ioctl(camfd, VIDIOC_S_FMT, tmp) == -1)
{
printf("ioctl() VIDIOC_S_FMT 失败了: %s\n", strerror(errno));
}
(6)向驱动申请帧缓存
int nbuf = 3;
struct v4l2_requestbuffers reqbuf;
bzero(&reqbuf, sizeof (reqbuf));
reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuf.memory = V4L2_MEMORY_MMAP;
reqbuf.count = nbuf;
// 使用该参数reqbuf来申请缓存
ioctl(camfd, VIDIOC_REQBUFS, &reqbuf);
}
v4l2_requestbuffers结构中定义了缓存的数量,驱动会据此申请对应数量的视频缓存。多个缓存可以用于建立FIFO,来提高视频采集的效率。
(7)获取每个缓存的信息,并mmap到用户空间
// 根据刚刚设置的reqbuf.count的值,来定义相应数量的struct v4l2_buffer
// 每一个struct v4l2_buffer对应内核摄像头驱动中的一个缓存
struct v4l2_buffer buffer[nbuf];
int length[nbuf];
uint8_t *start[nbuf];
for(int i=0; i<nbuf; i++)
{
bzero(&buffer[i], sizeof(buffer[i]));
buffer[i].type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buffer[i].memory = V4L2_MEMORY_MMAP;
buffer[i].index = i;
ioctl(camfd, VIDIOC_QUERYBUF, &buffer[i]);
length[i] = buffer[i].length;
start[i] = mmap(NULL, buffer[i].length, PROT_READ | PROT_WRITE,
MAP_SHARED, camfd, buffer[i].m.offset);
ioctl(camfd , VIDIOC_QBUF, &buffer[i]);
}
(8)启动摄像头数据采集
enum v4l2_buf_type vtype= V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(camfd, VIDIOC_STREAMON, &vtype);
(9)取出FIFO缓存中已经采样的帧缓存
struct v4l2_buffer v4lbuf;
bzero(&v4lbuf, sizeof(v4lbuf));
v4lbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
v4lbuf.memory= V4L2_MEMORY_MMAP;
// 从队列中取出填满数据的缓存
v4lbuf.index = i%nbuf;
ioctl(camfd , VIDIOC_DQBUF, &v4lbuf);
display(start[i%nbuf]); //显示取出数据的缓存
根据返回的buf.index找到对应的mmap映射好的缓存,取出视频数据。
(10)将刚刚处理完的缓冲重新入队列尾,这样可以循环采集
// 将已经读取过数据的缓存块重新置入队列中
v4lbuf.index = i%nbuf;
ioctl(camfd , VIDIOC_QBUF, &v4lbuf);
}
(11)停止视频的采集
int ret = ioctl(fd, VIDIOC_STREAMOFF, &buf_type);
(12)关闭视频设备
close(fd);
六、YUV格式转RGB
七、摄像头快速编程
caminfo.c
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <errno.h>
#include <string.h>
#include <linux/fb.h>
#include <linux/videodev2.h>
#include <linux/input.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <setjmp.h>
char formats[5][16] = {0};
struct v4l2_fmtdesc fmtdesc;
struct v4l2_format fmt;
struct v4l2_capability cap;
// 获取摄像头格式信息(固定)
void get_caminfo(int camfd)
{
fmtdesc.index = 0;
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
int ret;
printf("像素格式: \n");
while((ret=ioctl(camfd, VIDIOC_ENUM_FMT, &fmtdesc)) == 0)
{
printf("[%d]", fmtdesc.index);
sprintf(formats[fmtdesc.index]+0, "%c", (fmtdesc.pixelformat>>8*0)&0xFF);
sprintf(formats[fmtdesc.index]+1, "%c", (fmtdesc.pixelformat>>8*1)&0xFF);
sprintf(formats[fmtdesc.index]+2, "%c", (fmtdesc.pixelformat>>8*2)&0xFF);
sprintf(formats[fmtdesc.index]+3, "%c", (fmtdesc.pixelformat>>8*3)&0xFF);
printf("\"%s\"", formats[fmtdesc.index]);
printf("(详细描述: %s)\n", fmtdesc.description);
fmtdesc.index++;
}
}
// 获取摄像头格式信息(可调)
void get_camfmt(int camfd)
{
bzero(&fmt, sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if(ioctl(camfd, VIDIOC_G_FMT, &fmt) == -1)
{
printf("获取摄像头格式信息失败: %s\n", strerror(errno));
exit(0);
}
printf("分辨率: %d×%d\n", fmt.fmt.pix.width, fmt.fmt.pix.height);
printf("像素格式: ");
switch(fmt.fmt.pix.pixelformat)
{
case V4L2_PIX_FMT_MJPEG:
printf("V4L2_PIX_FMT_MJPEG\n");
break;
case V4L2_PIX_FMT_JPEG:
printf("V4L2_PIX_FMT_JPEG\n");
break;
case V4L2_PIX_FMT_MPEG:
printf("V4L2_PIX_FMT_MPEG\n");
break;
case V4L2_PIX_FMT_MPEG1:
printf("V4L2_PIX_FMT_MPEG1\n");
break;
case V4L2_PIX_FMT_MPEG2:
printf("V4L2_PIX_FMT_MPEG2\n");
break;
case V4L2_PIX_FMT_MPEG4:
printf("V4L2_PIX_FMT_MPEG4\n");
break;
case V4L2_PIX_FMT_H264:
printf("V4L2_PIX_FMT_H264\n");
break;
case V4L2_PIX_FMT_XVID:
printf("V4L2_PIX_FMT_XVID\n");
break;
case V4L2_PIX_FMT_RGB24:
printf("V4L2_PIX_FMT_RGB24\n");
break;
case V4L2_PIX_FMT_BGR24:
printf("V4L2_PIX_FMT_BGR24\n");
break;
case V4L2_PIX_FMT_YUYV:
printf("V4L2_PIX_FMT_YUYV\n");
break;
case V4L2_PIX_FMT_YYUV:
printf("V4L2_PIX_FMT_YYUV\n");
break;
case V4L2_PIX_FMT_YVYU:
printf("V4L2_PIX_FMT_YVYU\n");
break;
case V4L2_PIX_FMT_YUV444:
printf("V4L2_PIX_FMT_YUV444\n");
break;
case V4L2_PIX_FMT_YUV410:
printf("V4L2_PIX_FMT_YUV410\n");
break;
case V4L2_PIX_FMT_YUV420:
printf("V4L2_PIX_FMT_YUV420\n");
break;
case V4L2_PIX_FMT_YVU420:
printf("V4L2_PIX_FMT_YVU420\n");
break;
case V4L2_PIX_FMT_YUV422P:
printf("V4L2_PIX_FMT_YUV422P\n");
break;
default:
printf("未知\n");
}
}
// 获取摄像头设备的基本参数
void get_camcap(int camfd)
{
bzero(&cap, sizeof(cap));
if(ioctl(camfd, VIDIOC_QUERYCAP, &cap) == -1)
{
printf("获取摄像头基本信息失败: %s\n", strerror(errno));
exit(0);
}
printf("驱动:%s\n", cap.driver);
printf("显卡:%s\n", cap.card);
printf("总线:%s\n", cap.bus_info);
printf("版本:%d\n", cap.version);
if((cap.capabilities&V4L2_CAP_VIDEO_CAPTURE) == V4L2_CAP_VIDEO_CAPTURE)
{
printf("该设备为视频采集设备\n");
}
if((cap.capabilities&V4L2_CAP_STREAMING) == V4L2_CAP_STREAMING)
{
printf("该设备支持流IO操作\n\n");
}
}
// 配置摄像头像素格式
void set_camfmt(int camfd)
{
struct v4l2_format *tmp = calloc(1, sizeof(*tmp));
bzero(tmp, sizeof(*tmp));
tmp->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
tmp->fmt.pix.width = 800;
tmp->fmt.pix.height = 448;
printf("\n请选择要配置的像素格式:");
int n; scanf("%d", &n);
if(!strcmp(formats[n], "JPEG")) tmp->fmt.pix.pixelformat = V4L2_PIX_FMT_JPEG;
else if(!strcmp(formats[n], "MJPG")) tmp->fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
else if(!strcmp(formats[n], "MPEG")) tmp->fmt.pix.pixelformat = V4L2_PIX_FMT_MPEG;
else if(!strcmp(formats[n], "YUYV")) tmp->fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
else if(!strcmp(formats[n], "YVYU")) tmp->fmt.pix.pixelformat = V4L2_PIX_FMT_YVYU;
else if(!strcmp(formats[n], "H264")) tmp->fmt.pix.pixelformat = V4L2_PIX_FMT_H264;
else
{
printf("对不起,所选格式无法配置.\n");
exit(0);
}
tmp->fmt.pix.field = V4L2_FIELD_INTERLACED;
if(ioctl(camfd, VIDIOC_S_FMT, tmp) == -1)
{
printf("ioctl() VIDIOC_S_FMT 失败了: %s\n", strerror(errno));
}
}
common.h
/
//
#ifndef __COMMON_H
#define __COMMON_H
#include <stdio.h>
#include <stdint.h>
#include <strings.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdbool.h>
#include <errno.h>
#include <string.h>
#include <linux/fb.h>
#include <linux/videodev2.h>
#include <linux/input.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <setjmp.h>
#include <pthread.h>
#include <semaphore.h>
extern char formats[5][16];
extern struct v4l2_fmtdesc fmtdesc;
extern struct v4l2_format fmt;
extern struct v4l2_capabilities cap;
// 获取摄像头格式信息(固定)
void get_caminfo(int camfd);
// 获取/设置摄像头格式信息(可调)
void get_camfmt(int camfd);
void set_camfmt(int camfd, char *pixfmt);
// 获取摄像头设备的基本参数
void get_camcap(int camfd);
#define MIN(a, b) \
({ \
typeof(a) _a = a; \
typeof(b) _b = b; \
(void)(&_a==&_b); \
_a < _b ? _a : _b;\
})
#endif
main.c
/
//
#include "common.h"
#define SCREENSIZE 800*480*4
#define MIN(a, b) \
({ \
typeof(a) _a = a; \
typeof(b) _b = b; \
(void)(&_a==&_b); \
_a < _b ? _a : _b; \
})
int redoffset ;
int greenoffset;
int blueoffset ;
int lcd;
struct fb_var_screeninfo lcdinfo;
uint8_t *fb;
int SCREEN_W, SCREEN_H;
int CAMERA_W, CAMERA_H;
int R[256][256];
int G[256][256][256];
int B[256][256];
sem_t s;
void *convert(void *arg)
{
/*******************************
R = Y + 1.042*(V-128);
G = Y - 0.344*(U-128)-0.714*(V-128);
B = Y + 1.772*(U-128);
*******************************/
pthread_detach(pthread_self());
for(int i=0; i<256; i++)
{
for(int j=0; j<256; j++)
{
R[i][j] = i + 1.042*(j-128);
R[i][j] = R[i][j]>255 ? 255 : R[i][j];
R[i][j] = R[i][j]<0 ? 0 : R[i][j];
B[i][j] = i + 1.772*(j-128);
B[i][j] = B[i][j]>255 ? 255 : B[i][j];
B[i][j] = B[i][j]<0 ? 0 : B[i][j];
for(int k=0; k<256; k++)
{
G[i][j][k] = i - 0.344*(j-128)-0.714*(k-128);
G[i][j][k] = G[i][j][k]>255 ? 255 : G[i][j][k];
G[i][j][k] = G[i][j][k]<0 ? 0 : G[i][j][k];
}
}
}
sem_post(&s);
}
void display(uint8_t *yuv)
{
static uint32_t shown = 0;
int R0, G0, B0;
int R1, G1, B1;
uint8_t Y0, U;
uint8_t Y1, V;
int w = MIN(SCREEN_W, CAMERA_W);
int h = MIN(SCREEN_H, CAMERA_H);
// 画显存之前,先把LCD移动到不可见区域
lcdinfo.xoffset = 0;
lcdinfo.yoffset = 480 * ((shown+1)%2);
ioctl(lcd, FBIOPAN_DISPLAY, &lcdinfo);
uint8_t *fbtmp = fb;
fbtmp += (shown%2) * SCREENSIZE;
int yuv_offset, lcd_offset;
for(int y=0; y<h; y++)
{
for(int x=0; x<w; x+=2)
{
yuv_offset = ( CAMERA_W*y + x ) * 2;
lcd_offset = ( SCREEN_W*y + x ) * 4;
Y0 = *(yuv + yuv_offset + 0);
U = *(yuv + yuv_offset + 1);
Y1 = *(yuv + yuv_offset + 2);
V = *(yuv + yuv_offset + 3);
*(fbtmp + lcd_offset + redoffset +0) = R[Y0][V];
*(fbtmp + lcd_offset + greenoffset+0) = G[Y0][U][V];
*(fbtmp + lcd_offset + blueoffset +0) = B[Y0][U];
*(fbtmp + lcd_offset + redoffset +4) = R[Y1][V];
*(fbtmp + lcd_offset + greenoffset+4) = G[Y1][U][V];
*(fbtmp + lcd_offset + blueoffset +4) = B[Y1][U];
}
}
shown++;
}
void usage(int argc, char *argv[])
{
if(argc != 2)
{
printf("Usage: %s </dev/videoX>\n", argv[0]);
exit(0);
}
}
int main(int argc, char *argv[])
{
usage(argc, argv);
sem_init(&s, 0, 0);
// 打开LCD设备
lcd = open("/dev/fb0", O_RDWR);
if(lcd == -1)
{
perror("open \"/dev/fb0\" failed");
exit(0);
}
// 获取LCD显示器的设备参数
ioctl(lcd, FBIOGET_VSCREENINFO, &lcdinfo);
SCREEN_W = lcdinfo.xres;
SCREEN_H = lcdinfo.yres;
fb = mmap(NULL, lcdinfo.xres* lcdinfo.yres_virtual* lcdinfo.bits_per_pixel/8,
PROT_READ | PROT_WRITE, MAP_SHARED, lcd, 0);
if(fb == MAP_FAILED)
{
perror("mmap failed");
exit(0);
}
// 清屏
bzero(fb, 2 * lcdinfo.xres * lcdinfo.yres * 4);
// 获取RGB偏移量
redoffset = lcdinfo.red.offset/8;
greenoffset= lcdinfo.green.offset/8;
blueoffset = lcdinfo.blue.offset/8;
lcdinfo.xoffset = 0;
lcdinfo.yoffset = 0;
ioctl(lcd, FBIOPAN_DISPLAY, &lcdinfo);
// ************************************************** //
// 准备好YUV-RGB映射表
pthread_t tid;
pthread_create(&tid, NULL, convert, NULL);
// 打开摄像头设备文件
int camfd = open(argv[1],O_RDWR);
if(camfd == -1)
{
printf("open %s faield: %s\n", argv[1], strerror(errno));
exit(0);
}
printf("\n摄像头的基本参数:\n");
get_camcap(camfd);
get_camfmt(camfd);
get_caminfo(camfd);
// 配置摄像头的采集格式
set_camfmt(camfd, "YUYV");
get_camfmt(camfd);
CAMERA_W = fmt.fmt.pix.width;
CAMERA_H = fmt.fmt.pix.height;
// 设置即将要申请的摄像头缓存的参数
int nbuf = 3;
struct v4l2_requestbuffers reqbuf;
bzero(&reqbuf, sizeof (reqbuf));
reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuf.memory = V4L2_MEMORY_MMAP;
reqbuf.count = nbuf;
// 使用该参数reqbuf来申请缓存
ioctl(camfd, VIDIOC_REQBUFS, &reqbuf);
// 根据刚刚设置的reqbuf.count的值,来定义相应数量的struct v4l2_buffer
// 每一个struct v4l2_buffer对应内核摄像头驱动中的一个缓存
struct v4l2_buffer buffer[nbuf];
int length[nbuf];
uint8_t *start[nbuf];
for(int i=0; i<nbuf; i++)
{
bzero(&buffer[i], sizeof(buffer[i]));
buffer[i].type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buffer[i].memory = V4L2_MEMORY_MMAP;
buffer[i].index = i;
ioctl(camfd, VIDIOC_QUERYBUF, &buffer[i]);
length[i] = buffer[i].length;
start[i] = mmap(NULL, buffer[i].length, PROT_READ | PROT_WRITE,
MAP_SHARED, camfd, buffer[i].m.offset);
ioctl(camfd , VIDIOC_QBUF, &buffer[i]);
}
// 启动摄像头数据采集
enum v4l2_buf_type vtype= V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(camfd, VIDIOC_STREAMON, &vtype);
struct v4l2_buffer v4lbuf;
bzero(&v4lbuf, sizeof(v4lbuf));
v4lbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
v4lbuf.memory= V4L2_MEMORY_MMAP;
// 开始抓取摄像头数据并在屏幕播放视频
sem_wait(&s);
int i=0;
while(1)
{
// 从队列中取出填满数据的缓存
v4lbuf.index = i%nbuf;
ioctl(camfd , VIDIOC_DQBUF, &v4lbuf);
display(start[i%nbuf]);
// 将已经读取过数据的缓存块重新置入队列中
v4lbuf.index = i%nbuf;
ioctl(camfd , VIDIOC_QBUF, &v4lbuf);
i++;
}
return 0;
}
通用Makefile
CROSS_COMPILE ?=arm-none-linux-gnueabi-
AS = $(CROSS_COMPILE)as
LD = $(CROSS_COMPILE)ld
CC = $(CROSS_COMPILE)gcc
CPP = $(CC) -E
AR = $(CROSS_COMPILE)ar
NM = $(CROSS_COMPILE)nm
STRIP = $(CROSS_COMPILE)strip
OBJCOPY = $(CROSS_COMPILE)objcopy
OBJDUMP = $(CROSS_COMPILE)objdump
export AS LD CC CPP AR NM
export STRIP OBJCOPY OBJDUMP
CFLAGS := -Wall -O2 -g
CFLAGS +=
LDFLAGS := -lpthread
export CFLAGS LDFLAGS
TOPDIR := $(shell pwd)
export TOPDIR
TARGET := test
obj-y += main.o
obj-y += caminfo.o
all : start_recursive_build $(TARGET)
@echo $(TARGET) has been built!
start_recursive_build:
make -C ./ -f $(TOPDIR)/Makefile.build
$(TARGET) : built-in.o
$(CC) -o $(TARGET) built-in.o $(LDFLAGS)
clean:
rm -f $(shell find -name "*.o")
rm -f $(TARGET)
distclean:
rm -f $(shell find -name "*.o")
rm -f $(shell find -name "*.d")
rm -f $(TARGET)
八、实验效果
[root@GEC6818 /mnt/v4l2]#./test /dev/video7
摄像头的基本参数:
驱动:uvcvideo
显卡:USB2.0 PC CAMERA
总线:usb-nxp-ehci-1.3
版本:197671
该设备为视频采集设备
该设备支持流IO操作
分辨率: 640×480
像素格式: V4L2_PIX_FMT_YUYV
像素格式:
[0]"YUYV"(详细描述: YUV 4:2:2 (YUYV))
请选择要配置的像素格式:0
分辨率: 640×480
像素格式: V4L2_PIX_FMT_YUYV
参考博文:
原文地址