Linux v4l2子系统(10):基于opencv的v4l2应用
关键词:v4l2、OpenCV、mmap、ioctl。
Ubuntu下cheese非常简洁易用的拍照/录像工具,通过apt-get install cheese安装。相关的源码也可以在cheese.git下载。
如果想要更深入的了解v4l2的相关使用方法,就需要对v4l2设备进行编程。
首先通过编写程序打开/dev/video0,获取数据;通过OpenCV进行预览/拍摄/录像。
1. /dev/video0编程
对v4l2设备进行视频采集遵循着一些基本的流程,v4l2支持内存映射方式(mmap),read/write方式,用户指针模式。
对v4l2接口进行视频采集一般分为如下几个步骤:
- 打开视频设备文件。
- 进行视频采集参数初始化,通过v4l2接口获取设备能力,设置图像格式/采集窗口/采集点阵大小等等。
- 申请若干视频采集缓冲区,并将这些缓冲区从内核映射到用户空间,便于应用程序读取/处理视频数据。
- 将申请到的帧缓冲区加入视频采集的输入队列排队,并启动视频采集。
- 驱动程序开始采集,采集完之后帧缓冲放入输出队列;应用程序从输出队列中取出帧缓冲区,处理完之后再将帧缓冲区重新翻入视频采集输入队列,循环往复采集连续的视频数据。
- 停止视频采集,释放申请的相关资源。
流程如下:
1.1 视频输入输出队列
v4l2驱动维护两个队列,一个输入队列一个输出队列。
启动视频数据采集后,驱动程序采集一帧数据,把采集的数据放入视频采集输入队列的第一个帧缓冲区,一帧数据采集完成,也即第一个帧缓冲区存满之后,驱动程序将该帧缓冲区移至视频采集输出队列。
应用程序从输出队列取出帧缓冲区后,处理缓冲区中的视频,并将对应的帧缓冲区移至输入队列。
如下图,驱动不停将输入队列帧缓冲区填满,移至输出队列;应用程序不停从输出队列取数据,处理,然后移至输入队列。
每一个帧缓冲区都有一个对应的状态标志变量,其中每一个比特代表一个状态
V4L2_BUF_FLAG_UNMAPPED 0B0000
V4L2_BUF_FLAG_MAPPED 0B0001
V4L2_BUF_FLAG_ENQUEUED 0B0010
V4L2_BUF_FLAG_DONE 0B0100
缓冲区的状态转化如图所示。
1.2 编程实例
下面是一个v4l2编程实例,基本上和上面提到的流程吻合。
详细分析在代码中。
#include <errno.h> #include <fcntl.h> #include <linux/videodev2.h> #include <stdint.h> #include <stdio.h> #include <string.h> #include <sys/ioctl.h> #include <sys/mman.h> #include <unistd.h> #include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #define IMAGEWIDTH 1280 #define IMAGEHEIGHT 720 #define TRUE 1 #define FALSE 0 #define FILE_VIDEO1 "/dev/video0" static int fd; //设备描述符 struct v4l2_streamparm setfps; //结构体v4l2_streamparm来描述视频流的属性 struct v4l2_capability cap; //取得设备的capability,看看设备具有什么功能,比如是否具有视频输入,或者音频输入输出等 struct v4l2_fmtdesc fmtdesc; //枚举设备所支持的image format: VIDIOC_ENUM_FMT struct v4l2_format fmt,fmtack; //子结构体struct v4l2_pix_format设置摄像头采集视频的宽高和类型:V4L2_PIX_FMT_YYUV V4L2_PIX_FMT_YUYV struct v4l2_requestbuffers req; //向驱动申请帧缓冲的请求,里面包含申请的个数 struct v4l2_buffer buf; //代表驱动中的一帧 enum v4l2_buf_type type; //帧类型 typedef struct { void *start; int length; }buftype; buftype *usr_buf; int main() { char image_name[20]; unsigned int n_buffers; //1. open video file if ((fd = open(FILE_VIDEO1, O_RDWR)) == -1){//打开设备采用阻塞式,等到设备捕获信息之后才会返回给应用;非阻塞式即使尚未捕捉到信息,驱动仍然会把缓存东西返回给应用程序。 printf("Opening video device error\n"); return FALSE; } //2. set capabilities if (ioctl(fd, VIDIOC_QUERYCAP, &cap) == -1){ //查询视频设备的功能 printf("unable Querying Capabilities\n"); return FALSE; } else { printf( "Driver Caps:\n" " Driver: \"%s\"\n" " Card: \"%s\"\n" " Bus: \"%s\"\n" " Version: %d\n" " Capabilities: %x\n", cap.driver, cap.card, cap.bus_info, cap.version, cap.capabilities); } if((cap.capabilities & V4L2_CAP_VIDEO_CAPTURE) == V4L2_CAP_VIDEO_CAPTURE){ printf("Camera device %s: support capture\n",FILE_VIDEO1); } if((cap.capabilities & V4L2_CAP_STREAMING) == V4L2_CAP_STREAMING){ printf("Camera device %s: support streaming.\n",FILE_VIDEO1); } //3. set fmt fmtdesc.index = 0; fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; printf("Support format: \n"); while(ioctl(fd,VIDIOC_ENUM_FMT, &fmtdesc) != -1){ // 获取当前视频设备支持的视频格式 printf("\t%d. %s\n",fmtdesc.index+1,fmtdesc.description); fmtdesc.index++; } fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt.fmt.pix.width = IMAGEWIDTH; fmt.fmt.pix.height = IMAGEHEIGHT; fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG; //V4L2_PIX_FMT_MJPEG, V4L2_PIX_FMT_YUYV fmt.fmt.pix.field = V4L2_FIELD_NONE; if (ioctl(fd, VIDIOC_S_FMT, &fmt) == -1){ // 设置视频设备的视频数据格式,例如设置视频图像数据的长、宽,图像格式(MJPEG、YUYV格式) printf("Setting Pixel Format error\n"); return FALSE; } if(ioctl(fd,VIDIOC_G_FMT,&fmt) == -1){ //获取当前视频设备捕获图像格式 printf("Unable to get format\n"); return FALSE; } else { printf("fmt.type:\t%d\n",fmt.type); //可以输出图像的格式 printf("pix.pixelformat:\t%c%c%c%c\n",fmt.fmt.pix.pixelformat & 0xFF,(fmt.fmt.pix.pixelformat >> 8) & 0xFF,\ (fmt.fmt.pix.pixelformat >> 16) & 0xFF, (fmt.fmt.pix.pixelformat >> 24) & 0xFF); printf("pix.width:\t%d\n", fmt.fmt.pix.width); printf("pix.height:\t%d\n",fmt.fmt.pix.height); printf("pix.field:\t%d\n",fmt.fmt.pix.field); } setfps.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; setfps.parm.capture.timeperframe.numerator = 100; setfps.parm.capture.timeperframe.denominator = 100; if (ioctl(fd, VIDIOC_S_PARM, &setfps) == -1) //请求若干个帧缓冲区,开启内存映射或用户指针I/O { printf("Set fps error\n"); return FALSE; } //4. request for 4 buffers req.count = 4; req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory = V4L2_MEMORY_MMAP; if (ioctl(fd, VIDIOC_REQBUFS, &req) == -1) //请求若干个帧缓冲区,开启内存映射或用户指针I/O { printf("Requesting Buffer error\n"); return FALSE; } //5. mmap for buffers usr_buf = (buftype*)calloc(req.count, sizeof(buftype)); if(!usr_buf){ printf("Out of memory\n"); return FALSE; } buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; for(n_buffers = 0; n_buffers < req.count; n_buffers++) { buf.index = n_buffers; if(ioctl(fd, VIDIOC_QUERYBUF, &buf) == -1){ //查询已经分配的V4L2的视频缓冲区的相关信息,包括视频缓冲区的使用状态、在内核空间的偏移地址、缓冲区长度等。 printf("Querying Buffer error\n"); return FALSE; } usr_buf[n_buffers].length = buf.length; usr_buf[n_buffers].start = (uchar*)mmap (NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset); //将申请到的缓冲区映射到用户空间 if(usr_buf[n_buffers].start == MAP_FAILED) { printf("buffer map error\n"); return FALSE; } printf("Length: %d\nAddress: %p\n", usr_buf[n_buffers].length, usr_buf[n_buffers].start); printf("Image Length: %d\n", buf.bytesused); } //6. queue //在 driver 内部管理着两个 buffer queues ,一个输入队列,一个输出队列。 //对于 capture device 来说,当输入队列中的 buffer 被塞满数据以后会自动变为输出队列, //等待调用 VIDIOC_DQBUF 将数据进行处理以后重新调用 VIDIOC_QBUF 将 buffer 重新放进输入队列. for(n_buffers = 0; n_buffers <req.count; n_buffers++){ buf.index = n_buffers; if(ioctl(fd, VIDIOC_QBUF, &buf)){ //将一个空的视频缓冲区到视频缓冲区输入队列中,在启动数据流之后,自动填充缓冲区。 printf("query buffer error\n"); return FALSE; } } //7. start streaming type = V4L2_BUF_TYPE_VIDEO_CAPTURE; if(ioctl(fd, VIDIOC_STREAMON, &type) == -1){ //启动数据流 printf("stream on error\n"); return FALSE; } buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //Stream 或者Buffer的类型。此处肯定为V4L2_BUF_TYPE_VIDEO_CAPTURE buf.memory = V4L2_MEMORY_MMAP; //既然是Memory Mapping模式,则此处设置为:V4L2_MEMORY_MMAP //cvNamedWindow("one",CV_WINDOW_AUTOSIZE); IplImage* img; CvMat cvmat; int i, count = 20; double t; while(true) { for(n_buffers = 0; n_buffers <req.count; n_buffers++) { t = (double)cvGetTickCount(); //调用时钟测时间 buf.index = n_buffers; ioctl(fd,VIDIOC_DQBUF,&buf); //8. dequeue 以阻塞方式,从缓冲区输出队列中取出一帧,将数据放入usr_buf[n_buffers].start中 cvmat = cvMat(IMAGEHEIGHT, IMAGEWIDTH, CV_8UC3, (void*)usr_buf[n_buffers].start);//CV_8UC3 img = cvDecodeImage(&cvmat,1); if(!img) printf("No img\n"); cvShowImage("one",img); //sprintf(image_name, "out/test-%d-%d.jpg", i++, n_buffers+1); //cvSaveImage(image_name,img); cvReleaseImage(&img); ioctl(fd,VIDIOC_QBUF,&buf); //重新调用VIDIOC_BUF将缓冲帧放入到输入队列中。 if((cvWaitKey(1)&255) == 27) exit(0); t=(double)cvGetTickCount()-t; printf("FPS %.3g\n", (cvGetTickFrequency()*1000000)/t); } } //9. stop streaming ioctl(fd, VIDIOC_STREAMOFF, &type); // 停止视频采集命令,应用程序调用VIDIOC_ STREAMOFF停止视频采集命令后,视频设备驱动程序不在采集视频数据。 //10. munmap for(n_buffers = 0;n_buffers <req.count;n_buffers++) { if(-1 == munmap(usr_buf[i].start,usr_buf[i].length)) { printf("Unmap error\n"); return FALSE; } } //11. close fd close(fd); return 0; }
1.3 相关数据结构详解
例中设置ioctl用到的相关数据结构,对于理解和设置参数很有必要。
struct v4l2_fmtdesc { __u32 index; /* Format number */ __u32 type; /* enum v4l2_buf_type */-------V4L2_BUF_TYPE_VIDEO_CAPTURE等类型 __u32 flags; __u8 description[32]; /* Description string */ __u32 pixelformat; /* Format fourcc */ __u32 reserved[4]; }; 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 */ __u8 raw_data[200]; /* user-defined */ } fmt; }; struct v4l2_pix_format { __u32 width;----------------------------------帧宽度 __u32 height;--------------------------------------帧高度 __u32 pixelformat;---------------------------------帧格式,V4L2_PIX_FMT_MJPEG, V4L2_PIX_FMT_YUYV等。 __u32 field; /* enum v4l2_field */----------V4L2_FIELD_NONE等 __u32 bytesperline; /* for padding, zero if unused */ __u32 sizeimage; __u32 colorspace; /* enum v4l2_colorspace */ __u32 priv; /* private data, depends on pixelformat */ __u32 flags; /* format flags (V4L2_PIX_FMT_FLAG_*) */ __u32 ycbcr_enc; /* enum v4l2_ycbcr_encoding */ __u32 quantization; /* enum v4l2_quantization */ __u32 xfer_func; /* enum v4l2_xfer_func */ }; struct v4l2_streamparm { __u32 type; /* enum v4l2_buf_type */ union { struct v4l2_captureparm capture; struct v4l2_outputparm output; __u8 raw_data[200]; /* user-defined */ } parm; }; enum v4l2_buf_type { V4L2_BUF_TYPE_VIDEO_CAPTURE = 1, V4L2_BUF_TYPE_VIDEO_OUTPUT = 2, V4L2_BUF_TYPE_VIDEO_OVERLAY = 3, V4L2_BUF_TYPE_VBI_CAPTURE = 4, V4L2_BUF_TYPE_VBI_OUTPUT = 5, V4L2_BUF_TYPE_SLICED_VBI_CAPTURE = 6, V4L2_BUF_TYPE_SLICED_VBI_OUTPUT = 7, #if 1 /* Experimental */ V4L2_BUF_TYPE_VIDEO_OUTPUT_OVERLAY = 8, #endif V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE = 9, V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE = 10, V4L2_BUF_TYPE_SDR_CAPTURE = 11, V4L2_BUF_TYPE_SDR_OUTPUT = 12, /* Deprecated, do not use */ V4L2_BUF_TYPE_PRIVATE = 0x80, };
1.4 相关ioctl命令
视频设备的纷繁的设置通过ioctl来实现,这也导致ioctl命令相当的多。
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。
VIDIOC_G_PARM :得到Stream信息。如帧数等。
VIDIOC_S_PARM:设置Stream信息。如帧数等。
1.5 实例测试
编译如下:
g++ v4l2_my.c -o v4l2_my `pkg-config --cflags --libs opencv`
使用4个帧缓冲区的时候,帧率能达到30+。
Driver Caps: Driver: "uvcvideo" Card: "HD WebCam" Bus: "usb-0000:00:14.0-9" Version: 263306 Capabilities: 84200001 Camera device /dev/video0: support capture Camera device /dev/video0: support streaming. Support format: 1. YUYV 4:2:2 2. Motion-JPEG fmt.type: 1 pix.pixelformat: MJPG pix.width: 1280 pix.height: 720 pix.field: 1 Length: 1843200 Address: 0x7faf4bb66000 Image Length: 0 ... Length: 1843200 Address: 0x7faf4b620000 Image Length: 0 FPS 8.47 FPS 99.7 FPS 56 FPS 20.5 FPS 42.4 FPS 32.4 FPS 29.9 FPS 28 FPS 31.6 FPS 31.4 FPS 33 FPS 27.9 FPS 23.5 FPS 37.1 FPS 28.6 FPS 34.5 FPS 39.2
当只使用一个帧缓冲区的时候,帧率只有13左右;2个帧缓冲区帧率和4个基本差不多。
2.基于OpenCV预览/拍照/录像
OpenCV即Open Source Computer Vision Library,其官网是opencv.org。
它夸平台,提供多种语言接口,实现了图像处理和计算机视觉方面的很多通用算法。
下面先看看和v4l2结合的视频预览/拍照/录像三个简单功能。
2.1 OpenCV预览和拍照
预览和拍照只是对帧缓冲区数据的处理方式不一样,预览是将缓冲区数据编码直接显示,拍照是将帧缓冲区数据编码然后保存成jpeg文件。
cvMat使用已有的数据初始化一个矩阵;然后cvDecodeImage对数据进行编码;cvShowImage()直接显示编码后的数据;cvSaveImage()保存编码后的数据到文件系统;cvReleaseImage()释放相关资源。
...
#include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> ...int main() { char image_name[20]; ...//cvNamedWindow("one",CV_WINDOW_AUTOSIZE); IplImage* img; CvMat cvmat; int i, count = 20; double t; while(true) { for(n_buffers = 0; n_buffers <req.count; n_buffers++) { t = (double)cvGetTickCount(); //调用时钟测时间 buf.index = n_buffers; ioctl(fd,VIDIOC_DQBUF,&buf); //8. dequeue 以阻塞方式,从缓冲区输出队列中取出一帧,将数据放入usr_buf[n_buffers].start中 cvmat = cvMat(IMAGEHEIGHT, IMAGEWIDTH, CV_8UC3, (void*)usr_buf[n_buffers].start);//CV_8UC3 img = cvDecodeImage(&cvmat,1); if(!img) printf("No img\n"); cvShowImage("one",img); //sprintf(image_name, "out/test-%d-%d.jpg", i++, n_buffers+1); //cvSaveImage(image_name,img); cvReleaseImage(&img); ioctl(fd,VIDIOC_QBUF,&buf); //重新调用VIDIOC_BUF将缓冲帧放入到输入队列中。 if((cvWaitKey(1)&255) == 27) exit(0); t=(double)cvGetTickCount()-t; printf("FPS %.3g\n", (cvGetTickFrequency()*1000000)/t); } } ...return 0; }
2.2 OpenCV录像
下面直接使用OpenCV库录制avi文件。
#include "opencv2/opencv.hpp" #include <iostream> using namespace std; using namespace cv; int main(){ // Create a VideoCapture object and use camera to capture the video VideoCapture cap(0); // Check if camera opened successfully if(!cap.isOpened()) { cout << "Error opening video stream" << endl; return -1; } // Default resolution of the frame is obtained.The default resolution is system dependent. int frame_width = cap.get(CV_CAP_PROP_FRAME_WIDTH); int frame_height = cap.get(CV_CAP_PROP_FRAME_HEIGHT); // Define the codec and create VideoWriter object.The output is stored in 'outcpp.avi' file. VideoWriter video("outcpp.avi",CV_FOURCC('M','J','P','G'),60, Size(frame_width,frame_height)); while(1) { Mat frame; // Capture frame-by-frame cap >> frame; // If the frame is empty, break immediately if (frame.empty()) break; // Write the frame into the file 'outcpp.avi' video.write(frame); // Display the resulting frame imshow( "Frame", frame ); // Press ESC on keyboard to exit char c = (char)waitKey(1); if( c == 27 ) break; } // When everything done, release the video capture and write object cap.release(); video.release(); // Closes all the windows destroyAllWindows(); return 0; }
3. OpenCV安装
安装OpenCV参照《ubuntu 16.04 OpenCV3.2.0完全编译安装》,最简单的方式是:
sudo apt-get install libopencv-dev python-opencv
检查一下安装成果:
pkg-config --cflags --libs opencv
最后是编译:
g++ v4l2_my.c -o v4l2_my `pkg-config --cflags --libs opencv`
参考文档
《和菜鸟一起学linux之V4L2摄像头应用流程》:v4l2编程步骤、流程图;以及输入输出队列的详细讲解;最后代码以及相关数据结构做了介绍。
《Jetson TX1开发笔记(六):V4L2+OpenCV3.1以MJPG格式读取USB摄像头图像并实时显示》:以类函数的形式实现了对v4l2的功能封装。
《V4L2采集图像基本流程》:扼要介绍流程和代码。
《V4L2视频采集与H264编码1—V4L2采集JPEG数据》