LXR | KVM | PM | Time | Interrupt | Systems Performance | Bootup Optimization

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数据

V4L2视频采集与H264编码2—v4l2采集YUV数据

V4L2视频采集与H264编码3—X264移植

posted on 2024-04-15 23:59  ArnoldLu  阅读(659)  评论(0编辑  收藏  举报

导航