V4L2摄像头数据采集

970 阅读9分钟

1. 数据采集流程

Video for Linux two(Video4Linux2)简称V4L2,是V4L的改进版。V4L2支持三种方式来采集图像:内存映射方式(mmap)、直接读取方式(read)和用户指针

  • 内存映射的方式采集速度较快,一般用于连续视频数据的采集,实际工作中的应用概率更高;
  • 直接读取的方式相对速度慢一些,所以常用于静态图片数据的采集;
  • 用户指针使用较少,如有兴趣可自行研究。

1.1 buffer的管理

使用摄像头时,核心是"获得数据"。所以先讲如何获取数据,即如何得到buffer

摄像头采集数据时,是一帧又一帧地连续采集。所以需要申请若干个buffer,驱动程序把数据放入buffer,APP(应用程序)从buffer得到数据。这些buffer可以使用链表来管理。

驱动程序周而复始地做如下事情:

  • 从硬件采集到数据
  • 把"空闲链表"取出buffer,把数据存入buffer
  • 把含有数据的buffer放入"完成链表"

APP也会周而复始地做如下事情:

  • 监测"完成链表",等待它含有buffer
  • 从"完成链表"中取出buffer
  • 处理数据
  • 把buffer放入"空闲链表"

image.png

1.2 完整的使用流程

参考mjpg-streamer和video2lcd,总结了摄像头的使用流程,如下。

以下是根据流程对百问网编写的APP:video2lcd\video\v4l2.c进分析

  1. open:打开设备节点/dev/videoX

image.png

  1. ioctl VIDIOC_QUERYCAP:Query Capbility,查询能力,比如
  • 确认它是否是"捕获设备",因为有些节点是输出设备
  • 确认它是否支持mmap操作,还是仅支持read/write操作

1747537895384.jpg

  1. ioctl VIDIOC_ENUM_FMT:枚举它支持的格式

1747538658955.png

  1. ioctl VIDIOC_S_FMT:在上面枚举出来的格式里,选择一个来设置格式

1747538811422.png

  1. ioctl VIDIOC_REQBUFS:申请buffer,APP可以申请很多个buffer,但是驱动程序不一定能申请到

1747539022092.png

  1. ioctl VIDIOC_QUERYBUF和mmap:查询buffer信息、映射
  • 如果申请到了N个buffer,这个ioctl就应该执行N次
  • 执行mmap后,APP就可以直接读写这些buffer

1747539246648.png

  1. ioctl VIDIOC_QBUF:把buffer放入"空闲链表"
  • 如果申请到了N个buffer,这个ioctl就应该执行N次

1747539376122.png

  • ioctl VIDIOC_STREAMON:启动摄像头

1747541363199.png

  • 这里是一个循环:使用poll/select监测buffer,然后从"完成链表"中取出buffer,处理后再放入"空闲链表"
    • poll/select
    • ioctl VIDIOC_DQBUF:从"完成链表"中取出buffer
    • 处理:前面使用mmap映射了每个buffer的地址,处理时就可以直接使用地址来访问buffer
    • ioclt VIDIOC_QBUF:把buffer放入"空闲链表"

1747567843135.png

1747567896293.png

  • ioctl VIDIOC_STREAMOFF:停止摄像头

2. 控制流程

使用摄像头时,我们可以调整很多参数,比如:

  • 对于视频流本身:
    • 设置格式:比如V4L2_PIX_FMT_YUYV、V4L2_PIX_FMT_MJPEG、V4L2_PIX_FMT_RGB565
    • 设置分辨率:1024*768等
  • 对于控制部分:
    • 调节亮度
    • 调节对比度
    • 调节色度

2.1 APP接口

就APP而言,对于这些参数由3套接口:查询或枚举(Query/Enum)、获得(Get)、设置(Set)

2.1.1 数据格式

以设置数据格式为例,可以先枚举

struct v4l2_fmtdesc fmtdesc;
fmtdesc.index = 0;  // 比如从0开始
fmtdesc.type  = V4L2_BUF_TYPE_VIDEO_CAPTURE;  // 指定type为"捕获"
ioctl(vd->fd, VIDIOC_ENUM_FMT, &fmtdesc);

#if 0
/*
 *	F O R M A T   E N U M E R A T I O N
 */
struct v4l2_fmtdesc {
	__u32		    index;             /* Format number      */
	__u32		    type;              /* enum v4l2_buf_type */
	__u32               flags;
	__u8		    description[32];   /* Description string */
	__u32		    pixelformat;       /* Format fourcc      */
	__u32		    reserved[4];
};
#endif

还可以获得当前的格式:

struct v4l2_format currentFormat;
memset(&currentFormat, 0, sizeof(struct v4l2_format));
currentFormat.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(vd->fd, VIDIOC_G_FMT, &currentFormat);

#if 0
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;
};

/*
 *	V I D E O   I M A G E   F O R M A T
 */
struct v4l2_pix_format {v4l2_format
	__u32         		width;
	__u32			height;
	__u32			pixelformat;
	__u32			field;		/* enum v4l2_field */
	__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 */
};
#endif

也可以设置当前的格式:

struct v4l2_format fmt;
memset(&fmt, 0, sizeof(struct v4l2_format));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 1024;
fmt.fmt.pix.height = 768;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
fmt.fmt.pix.field = V4L2_FIELD_ANY;
int ret = ioctl(vd->fd, VIDIOC_S_FMT, &fmt);

2.1.2 选择输入源

可以获得当期输入源、设置当前输入源:

int value;
ioctl(h->fd,VIDIOC_G_INPUT,&value);  // 读到的value从0开始, 0表示第1个input源

int value = 0;  // 0表示第1个input源
ioctl(h->fd,VIDIOC_S_INPUT,&value)

2.1.3 其他参数

如果每一参数都提供一系列的ioctl cmd,那使用起来很不方便。对于这些参数,APP使用对应ID来选中它,然后使用VIDIOC_QUERYCTRL、VIDIOC_G_CTRL、VIDIOC_S_CTRL来操作它。 不同参数的ID值不同。以亮度Brightness为例,有如下调用方法:

  • 查询:
struct v4l2_queryctrl   qctrl;
memset(&qctrl, 0, sizeof(qctrl));
qctrl.id = V4L2_CID_BRIGHTNESS; // V4L2_CID_BASE+0;
ioctl(fd, VIDIOC_QUERYCTRL, &qctrl);

/*  Used in the VIDIOC_QUERYCTRL ioctl for querying controls */
struct v4l2_queryctrl {
	__u32		     id;
	__u32		     type;	/* enum v4l2_ctrl_type */
	__u8		     name[32];	/* Whatever */
	__s32		     minimum;	/* Note signedness */
	__s32		     maximum;
	__s32		     step;
	__s32		     default_value;
	__u32                flags;
	__u32		     reserved[2];
};
  • 获得当前值
struct v4l2_control c;
c.id = V4L2_CID_BRIGHTNESS; // V4L2_CID_BASE+0;
ioctl(h->fd, VIDIOC_G_CTRL, &c);


/*
 *	C O N T R O L S
 */
struct v4l2_control {
	__u32		     id;
	__s32		     value;
};
  • 设置
struct v4l2_control c;
c.id = V4L2_CID_BRIGHTNESS; // V4L2_CID_BASE+0;
c.value = 99;
ioctl(h->fd, VIDIOC_S_CTRL, &c);

2.2 理解接口

2.2.1 概念

2.2.2 操作方法

3. 编写APP

3.1 完整流程

完整流程: 打开设备、查询能力、枚举格式、设置格式、申请缓冲区、映射内存、缓冲队列管理、启动流(摄像头)和数据处理循环。

v4l2摄像头数据采集.png

编译:arm-buildroot-linux-gnueabihf-gcc -o video_test video_test.c -lpthread

3.2 运行结果

1747901010654.png

3.3 完整代码:

参考:mjpg-streamer,github.com/jacksonliam…

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <linux/types.h>          /* for videodev2.h */
#include <linux/videodev2.h>
#include <poll.h>
#include <sys/mman.h>
#include <pthread.h>

/* ./video_test </dev/video0> */

static void *thread_brightness_control (void *args)
{
    int fd = (int)args;

    unsigned char c;
    int brightness;
    int delta;
    
    struct v4l2_queryctrl   qctrl;
    memset(&qctrl, 0, sizeof(qctrl));
    qctrl.id = V4L2_CID_BRIGHTNESS; // V4L2_CID_BASE+0;
    if (0 != ioctl(fd, VIDIOC_QUERYCTRL, &qctrl))
    {
        printf("can not query brightness\n");
        return NULL;
    }

    printf("brightness min = %d, max = %d\n", qctrl.minimum, qctrl.maximum);
    delta = (qctrl.maximum - qctrl.minimum) / 10;
        
    struct v4l2_control ctl;
    ctl.id = V4L2_CID_BRIGHTNESS; // V4L2_CID_BASE+0;
    ioctl(fd, VIDIOC_G_CTRL, &ctl);

    while (1)
    {
        c = getchar();
        if (c == 'u' || c == 'U')
        {
            ctl.value += delta;
        }
        else if (c == 'd' || c == 'D')
        {
            ctl.value -= delta;
        }
        if (ctl.value > qctrl.maximum)
            ctl.value = qctrl.maximum;
        if (ctl.value < qctrl.minimum)
            ctl.value = qctrl.minimum;

        ioctl(fd, VIDIOC_S_CTRL, &ctl);
    }
    return NULL;
}

int main(int argc, char **argv)
{
    int fd;
    struct v4l2_fmtdesc fmtdesc;
    struct v4l2_frmsizeenum fsenum;
    int fmt_index = 0;
    int frame_index = 0;
    int i;
    void *bufs[32];
    int buf_cnt;
    int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    struct pollfd fds[1];
    char filename[32];
    int file_cnt = 0;

    if (argc != 2)
    {
        printf("Usage: %s </dev/videoX>, print format detail for video device\n", argv[0]);
        return -1;
    }

    /* open */
    fd = open(argv[1], O_RDWR);
    if (fd < 0)
    {
        printf("can not open %s\n", argv[1]);
        return -1;
    }

    /* 查询能力 */
    struct v4l2_capability cap;
    memset(&cap, 0, sizeof(struct v4l2_capability));
    
    if (0 == ioctl(fd, VIDIOC_QUERYCAP, &cap))
    {        
        if((cap.capabilities & V4L2_CAP_VIDEO_CAPTURE) == 0) {
            fprintf(stderr, "Error opening device %s: video capture not supported.\n",
                    argv[1]);
            return -1;
        }
        
        if(!(cap.capabilities & V4L2_CAP_STREAMING)) {
            fprintf(stderr, "%s does not support streaming i/o\n", argv[1]);
            return -1;
        }
    }
    else
    {
        printf("can not get capability\n");
        return -1;
    }

    while (1)
    {
        /* 枚举格式 */
        fmtdesc.index = fmt_index;  // 比如从0开始
        fmtdesc.type  = V4L2_BUF_TYPE_VIDEO_CAPTURE;  // 指定type为"捕获"
        if (0 != ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc))
            break;

        frame_index = 0;
        while (1)
        {
            /* 枚举这种格式所支持的帧大小 */
            memset(&fsenum, 0, sizeof(struct v4l2_frmsizeenum));
            fsenum.pixel_format = fmtdesc.pixelformat;
            fsenum.index = frame_index;

            if (ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &fsenum) == 0)
            {
                printf("format %s,%d, framesize %d: %d x %d\n", fmtdesc.description, fmtdesc.pixelformat, frame_index, fsenum.discrete.width, fsenum.discrete.height);
            }
            else
            {
                break;
            }

            frame_index++;
        }

        fmt_index++;
    }


    /* 设置格式 */
    struct v4l2_format fmt;
    memset(&fmt, 0, sizeof(struct v4l2_format));
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt.fmt.pix.width = 1024;
    fmt.fmt.pix.height = 768;
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
    fmt.fmt.pix.field = V4L2_FIELD_ANY;
    if (0 == ioctl(fd, VIDIOC_S_FMT, &fmt))
    {
        printf("set format ok: %d x %d\n", fmt.fmt.pix.width, fmt.fmt.pix.height);
    }
    else
    {
        printf("can not set format\n");
        return -1;
    }

    /*
     * 申请buffer
     */
    struct v4l2_requestbuffers rb;
    memset(&rb, 0, sizeof(struct v4l2_requestbuffers));
    rb.count = 32;
    rb.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    rb.memory = V4L2_MEMORY_MMAP;

    if (0 == ioctl(fd, VIDIOC_REQBUFS, &rb))
    {
        /* 申请成功后, mmap这些buffer */
        buf_cnt = rb.count;
        for(i = 0; i < rb.count; i++) {
            struct v4l2_buffer buf;
            memset(&buf, 0, sizeof(struct v4l2_buffer));
            buf.index = i;
            buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
            buf.memory = V4L2_MEMORY_MMAP;
            if (0 == ioctl(fd, VIDIOC_QUERYBUF, &buf))
            {
                /* mmap */
                bufs[i] = mmap(0 /* start anywhere */ ,
                                  buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd,
                                  buf.m.offset);
                if(bufs[i] == MAP_FAILED) {
                    perror("Unable to map buffer");
                    return -1;
                }
            }
            else
            {
                printf("can not query buffer\n");
                return -1;
            }            
        }

        printf("map %d buffers ok\n", buf_cnt);
        
    }
    else
    {
        printf("can not request buffers\n");
        return -1;
    }

    /* 把所有buffer放入"空闲链表" */
    for(i = 0; i < buf_cnt; ++i) {
        struct v4l2_buffer buf;
        memset(&buf, 0, sizeof(struct v4l2_buffer));
        buf.index = i;
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        if (0 != ioctl(fd, VIDIOC_QBUF, &buf))
        {
            perror("Unable to queue buffer");
            return -1;
        }
    }
    printf("queue buffers ok\n");

    /* 启动摄像头 */
    if (0 != ioctl(fd, VIDIOC_STREAMON, &type))
    {
        perror("Unable to start capture");
        return -1;
    }
    printf("start capture ok\n");


    /* 创建线程用来控制亮度 */
    pthread_t thread;
    pthread_create(&thread, NULL, thread_brightness_control, (void *)fd);
   

    while (1)
    {
        /* poll */
        memset(fds, 0, sizeof(fds));
        fds[0].fd = fd;
        fds[0].events = POLLIN;
        if (1 == poll(fds, 1, -1))
        {
            /* 把buffer取出队列 */
            struct v4l2_buffer buf;
            memset(&buf, 0, sizeof(struct v4l2_buffer));
            buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
            buf.memory = V4L2_MEMORY_MMAP;
            
            if (0 != ioctl(fd, VIDIOC_DQBUF, &buf))
            {
                perror("Unable to dequeue buffer");
                return -1;
            }
            
            /* 把buffer的数据存为文件 */
            sprintf(filename, "video_raw_data_%04d.jpg", file_cnt++);
            int fd_file = open(filename, O_RDWR | O_CREAT, 0666);
            if (fd_file < 0)
            {
                printf("can not create file : %s\n", filename);
            }
            printf("capture to %s\n", filename);
            write(fd_file, bufs[buf.index], buf.bytesused);
            close(fd_file);

            /* 把buffer放入队列 */
            if (0 != ioctl(fd, VIDIOC_QBUF, &buf))
            {
                perror("Unable to queue buffer");
                return -1;
            }
        }
    }

    if (0 != ioctl(fd, VIDIOC_STREAMOFF, &type))
    {
        perror("Unable to stop capture");
        return -1;
    }
    printf("stop capture ok\n");
    close(fd);

    return 0;
}