使用v4l2音、视频协议实现USB摄像头的图像、视频YUV格式采集功能(ubuntu16.04LTS)

第一感觉是首先得了解v4l2协议,它的功能、以及与之对应的实现逻辑,还有与硬件、操作系统的交互等内容。再试着根据功能逻辑和软硬件交互关系,借助硬件设备,实现基础的功能,如查询设备信息、帧类型等。然后,一步一步按照逻辑功能实现步骤,完成代码的编写。

一开始并不了解YUV格式文件,在代码中也仅仅是把硬件设备中采集的单帧数据写入到 file.yuv 文件中,并试着使用pYUV软件打开该文件,结果肯定打开失败。此处,考虑的问题是可能是打开的方式不对(格式选择、帧率等),或者是文件数据类型不对(并不是YUV格式),最后才发现,就是文件数据类型不对,原因是USB协议不支持也就是下面要求下载virtualbox扩展增强包的原因。而且,打开的方式也不对,要求VGA-YUV422-interleaved(后面补充)。

后面就可以实现USB摄像头采集YUV格式视频,保存并播放。下面将详细介绍整个流程。

一、软件环境搭建

1、系统环境

操作系统:ubuntu16.04LTS

编辑器:Vim 7.3.429

编译器:gcc 4.6.3

2、下载virtualbox扩展增强包(与USB摄像头有关)

说明:首先,要确认你的USB摄像头使用的什么版的USB协议,1.1或2.0或3.0;virtualbox16.04LTS仅支持USB1.1,所以如果你的USB摄像头是USB2.0或USB3.0就需要安装扩展增强包;当然,如果你的virtualbox一开始就支持USB2.0或USB3.0,也就无需执行这一步骤。

virtualbox管理器->设置->USB设备->USB控制器(确认是否支持USB2.0/3.0)

下载方式(https://jingyan.baidu.com/article/22a299b5e0198a9e19376a38.html)

3、驱动一般会自动安装,如果驱动未安装,也无法实现功能

二、硬件环境确认(确认步骤)

说明:这一步意在确认USB硬件是否正常,先在主机上确认(排除硬件设备出现的问题),再在ubuntu系统上使用相关软件确认(排除ubuntu上驱动的问题),若两步都能够成功显示图像,则表明在硬件、驱动、接口等方面都没有问题,最后才进行软件编程。

此处,考虑的较为繁杂,一般的设备都不会出现问题,最可能遇见的问题就是上面说的USB协议不一致的问题。所以,如果软件方面确认无误,此步可跳过。

1、所用到软件

主机(Windows):MiniVCap(USB检测)、pYUV(YUV格式文件打开)

使用v4l2音、视频协议实现USB摄像头的图像、视频YUV格式采集功能(ubuntu16.04LTS)_第1张图片

虚拟机(ubuntu):cheese/mplayer/camorama(USB检测)

2、确认USB摄像头在主机上可用

插入摄像头USB接口,打开MiniCap,若图像显示成功,则确认无误。

3、确认USB摄像头在ubuntu系统可用

安装相关的软件(任选其一)

sudo apt-get install cheese;
sudo apt-get install mplayer;
sudo apt-get install camorama;

 插入摄像头USB接口,打开执行任一软件,若图像显示成功,则确认无误。

cheese;
mplayer;
camorama;

三、v4l2协议了解

v4l2,全称video for Linux 2,是 linux 里一套标准的音频、视频数据采集的API接口,它支持 uvc 标准的摄像头。

UVC,全称为:USB video class 或USB video device class。是Microsoft与另外几家设备厂商联合推出的为USB视频捕获设备定义的协议标准。符合UVC规格的硬件设备在不需要安装任何的驱动程序下即可在主机中正常使用。

一般,在Linux系统下,一切皆文件,对设备的操作都是通过对设备文件的读写来实现的。一般,我们的USB摄像头的设备文件为 /dev/video0 ,对摄像头数据的采集也就是对设备文件进行读取。

由于对设备文件的读取是在内核的内存区域完成的,用户内存空间需要通过内核内存空间来获取相关采集数据,这就需要把内核内存空间的数据放到用户内存空间中,v4l2提供的方式有三种:内存映射(mmap)、直接读取(read)、用户指针,本文使用内存映射的方式。

1、ioctl函数的使用

对于设备文件的内核操作都是通过ioctl函数来实现,包括设备相关信息的读取和设置、设备缓冲区的申请和映射等操作。

#include 

int ioctl(int fd , int request , ...)

fd:文件指针
request:操作方式
...:与操作方式对应的参数

ioctl函数,在本次功能实现中扮演着重要的角色,其使用方式的具体实现将在下面的内容进行详细说明。

2、数据结构的声明

在实现功能的过程中,有着不同保存临时数据的数据结构体,它们都在 /usr/include/linux/videodev.h 内有声明

//device capabilities 设备能力(存放设备信息)
//使用:查看设备信息 driver、card、bus_info、version
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];
};
//video format 视频格式(存放视频信息,可设置)
//使用:type(V4L2_BUF_TYPE_VIDEO_CAPTURE)、fmt(如下)
struct v4l2_format {
	enum v4l2_buf_type 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 */
		__u8	raw_data[200];                   /* user-defined */
	} fmt;
};

//video image format 单帧格式(存放单帧信息,可设置)
//使用:width(680)、height(480)、pixelformat(V4L2_PIX_FMT_YUYV)、field(V4L2_FIELD_INTERLACED)
struct v4l2_pix_format {
	__u32         		width;
	__u32			height;
	__u32			pixelformat;
	enum v4l2_field  	field;
	__u32            	bytesperline;	/* for padding, zero if unused */
	__u32          		sizeimage;
	enum v4l2_colorspace	colorspace;
	__u32			priv;		/* private data, depends on pixelformat */
};
//format enumeration 帧格式枚举(存放设备支持的格式,用于查看设备支持的格式)
//使用:type(V4L2_BUF_TYPE_VIDEO_CAPTURE)
struct v4l2_fmtdesc {
	__u32		    index;             /* Format number      */
	enum v4l2_buf_type  type;              /* buffer type        */
	__u32               flags;
	__u8		    description[32];   /* Description string */
	__u32		    pixelformat;       /* Format fourcc      */
	__u32		    reserved[4];
};
//memeory-mapping buffers 请求内存映射的缓冲结构体(请求内核分配缓冲区)
//使用:count(4)、type(V4L2_BUF_TYPE_VIDEO_CAPTURE)、memory(V4L2_MEMORY_MMAP)
struct v4l2_requestbuffers {
	__u32			count;
	enum v4l2_buf_type      type;
	enum v4l2_memory        memory;
	__u32			reserved[2];
};
//memeory buffer information 内核空间帧信息(用于获取内核帧的长度和地址,便于映射)
//使用:type(V4L2_BUF_TYPE_VIDEO_CAPTURE)、memory(V4L2_MEMORY_MMAP)
struct v4l2_buffer {
	__u32			index;
	enum v4l2_buf_type      type;
	__u32			bytesused;
	__u32			flags;
	enum v4l2_field		field;
	struct timeval		timestamp;
	struct v4l2_timecode	timecode;
	__u32			sequence;

	/* memory location */
	enum v4l2_memory        memory;
	union {
		__u32           offset;
		unsigned long   userptr;
		struct v4l2_plane *planes;
	} m;
	__u32			length;
	__u32			input;
	__u32			reserved;
};

3、请求类型request与相关参数的实现

合理使用ioctl函数的request参数就可以实现对设备I/O通道进行管理,具体在 /usr/include/linux/videodev.h 有声明。

//ioctl codes for video devices 函数ioctl的request设置说明

#define VIDIOC_QUERYCAP		 _IOR('V',  0, struct v4l2_capability)//获取设备能力
#define VIDIOC_ENUM_FMT         _IOWR('V',  2, struct v4l2_fmtdesc)//获取设备支持的帧格式
#define VIDIOC_G_FMT		_IOWR('V',  4, struct v4l2_format)//获取当前设备帧格式
#define VIDIOC_S_FMT		_IOWR('V',  5, struct v4l2_format)//设置当前设备帧格式
#define VIDIOC_REQBUFS		_IOWR('V',  8, struct v4l2_requestbuffers)//申请内核空间缓冲区
#define VIDIOC_QUERYBUF		_IOWR('V',  9, struct v4l2_buffer)//获取内核空间单帧缓冲区信息
#define VIDIOC_QBUF		_IOWR('V', 15, struct v4l2_buffer)//将单帧缓冲区放入采集入队队列
#define VIDIOC_DQBUF		_IOWR('V', 17, struct v4l2_buffer)//从采集出队队列取出单帧缓冲区
#define VIDIOC_STREAMON		 _IOW('V', 18, int)//开始视频流采集
#define VIDIOC_STREAMOFF	 _IOW('V', 19, int)//关闭视频流采集

后面的结构体数据类型(struct v4l2_*)、基本数据类型(int)就是ioctl函数参数的 ...,与request对应。

4、映射函数(mmap、munmap)和自定义用户内存空间缓冲区(VideoBuffer)

#include 

//将内核内存映射到用户内存
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
//addr:一般为NULL,让系统自动选择地址
//length:映射到内存中的内容大小
//prot:映射区域的保护方式。包括PROT_EXEC(执行)、PROT_READ(读)、PROT_WRITE(写)、PROT_NONE(无操作)
//flags:映射区域的特性。此处用到了MAP_SHARED
//fd:映射文件的文件描述符
//offeset:文件映射的偏移量。

//取消内存映射
int munmap(void *addr, size_t length);
//addr:需要取消映射的用户内存首地址
//length:取消映射的大小
//自定义的用户内存缓冲区,用于和内核内存映射
typedef struct VideoBuffer
{
	void * start;    //首地址指针
	size_t length;   //内容长度
}VideoBuffer;

 说明:在本次功能实现过程中,我们使用到三种内存缓冲区的结构体,分别是 struct v4l2_requestbuffers、struct v4l2_buffer、struct VideoBuffer,此处做一个辨析。

使用v4l2音、视频协议实现USB摄像头的图像、视频YUV格式采集功能(ubuntu16.04LTS)_第2张图片

四、功能实现编程

1、打开设备文件

#include 
int fd = open("/dev/video0",O_RDWR,0);

2、查看设备信息

void query_cap(int fd)
{
    struct v4l2_capability cap;
    ioctl(fd,VIDIOC_QUERYCAP,&cap);
    printf(“DriverName:%s\nCard Name:%s\nBus info:%s\nDriverVersion:%u.%u.%u\n”,cap.driver,cap.card,cap.bus_info,(cap.version>>16)&0xff,(cap.version>>8)&0xff,cap.version&0xff);
}

 3、查看设备支持的帧结构

void enum_fmt(int fd)
{
    struct v4l2_fmtdesc fmtdesc;
    fmtdesc.index=0;
    fmtdesc.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;
    printf("Supportformat:/n");
    while(ioctl(fd,VIDIOC_ENUM_FMT,&fmtdesc)!=-1)
    {
        printf("/t%d.%s/n",fmtdesc.index+1,fmtdesc.description);
        fmtdesc.index++;
    }
}

4、查看或设置当前格式

void get_fmt(int fd)
{
    struct v4l2_format fmt;
    fmt.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;
    ioctl(fd,VIDIOC_G_FMT,&fmt);
    printf(“Currentdata format information:\n\twidth:%d\n\theight:%d\n”,fmt.fmt.width,fmt.fmt.height);
    struct v4l2_fmtdesc fmtdesc;
    fmtdesc.index=0;
    fmtdesc.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;
    while(ioctl(fd,VIDIOC_ENUM_FMT,&fmtdesc)!=-1)
    {
        if(fmtdesc.pixelformat& fmt.fmt.pixelformat)
        {
            printf(“\tformat:%s\n”,fmtdesc.description);
            break;
        }
        fmtdesc.index++;
    }
}

void set_fmt(int fd)
{
    struct v4l2_format fmt;
    memset(&fmt,0,sizeof(fmt));
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt.fmt.pix.width = 640;
    fmt.fmt.pix.height = 480;
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
    fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;
    if(ioctl(fd,VIDIOC_S_FMT,&fmt) == -1) return -1;
}

5、申请内核内存缓冲区(4帧)

struct v4l2_requestbuffers req;
req.count=4;
req.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory=V4L2_MEMORY_MMAP;
ioctl(fd,VIDIOC_REQBUFS,&req);

6、获取内存内核的地址、长度,并映射到用户内存空间 ,将其放入采集入队队列

VideoBuffer * buffers = calloc(req.count,sizeof(VideoBuffer));
struct v4l2_buffer buf;
for(int numBufs = 0;numBufs < req.count;numBufs++)
{
    memset(&buf,0,sizeof(buf));
    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP;
    buf.index = numBufs;
    // 读取缓存
    if(ioctl(fd,VIDIOC_QUERYBUF,&buf) == -1) return -1;
    buffers[numBufs].length = buf.length;
    // 转换成相对地址
    buffers[numBufs].start = mmap(NULL,buf.length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,buf.m.offset);
    if(buffers[numBufs].start == MAP_FAILED) return -1;
    // 放入缓存队列
    if(ioctl(fd,VIDIOC_QBUF,&buf) == -1) return -1;
}

7、开启视频流采集

enum v4l2_buf_type type;
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl (fd,VIDIOC_STREAMON, &type);

8、将帧缓冲区,取出队列,读取数据(数据处理),放入队列

while(1)
{
    struct v4l2_buffer buf;
    memset(&buf,0,sizeof(buf));
    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP;
    // 从缓冲区取出一个缓冲帧
    ioctl (fd,VIDIOC_DQBUF, &buf);
    // 图像处理
    process_image(buffers[buf.index].start,buffers[buf.index].length);
    // 将取出的缓冲帧放回缓冲区
    ioctl (fd, VIDIOC_QBUF,&buf);
}

9、图像处理函数的实现

void process_image(void* start,size_t length)
{
    int fd = open("video.yuv",O_RDWR|O_CREAT|O_APPEND,0777);
    write(fd,(char*)start,length);
    close(fd);
}

 10、关闭视频流

ioctl (fd,VIDIOC_STREAMOFF, &type);

 11、关闭映射

for(int i=0;i

12、关闭文件

close(fd);

五、总结

源代码:等代码

扩展:YUV格式数据量较大,格式可以转RGB格式,便于对数据进行压缩,另外还可编码H.264。

 

 

 

 

 

 

 

 

你可能感兴趣的:(v4l2)