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放入"空闲链表"
1.2 完整的使用流程
参考mjpg-streamer和video2lcd,总结了摄像头的使用流程,如下。
以下是根据流程对百问网编写的APP:video2lcd\video\v4l2.c进分析
- open:打开设备节点/dev/videoX
- ioctl VIDIOC_QUERYCAP:Query Capbility,查询能力,比如
- 确认它是否是"捕获设备",因为有些节点是输出设备确认它是否支持mmap操作,还是仅支持read/write操作
- ioctl VIDIOC_ENUM_FMT:枚举它支持的格式
- ioctl VIDIOC_S_FMT:在上面枚举出来的格式里,选择一个来设置格式
- ioctl VIDIOC_REQBUFS:申请buffer,APP可以申请很多个buffer,但是驱动程序不一定能申请到
- ioctl VIDIOC_QUERYBUF和mmap:查询buffer信息、映射
- 如果申请到了N个buffer,这个ioctl就应该执行N次执行mmap后,APP就可以直接读写这些buffer
- ioctl VIDIOC_QBUF:把buffer放入"空闲链表"
- 如果申请到了N个buffer,这个ioctl就应该执行N次
- ioctl VIDIOC_STREAMON:启动摄像头
- 这里是一个循环:使用poll/select监测buffer,然后从"完成链表"中取出buffer,处理后再放入"空闲链表"
- poll/selectioctl VIDIOC_DQBUF:从"完成链表"中取出buffer处理:前面使用mmap映射了每个buffer的地址,处理时就可以直接使用地址来访问bufferioclt VIDIOC_QBUF:把buffer放入"空闲链表"
- 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(¤tFormat, 0, sizeof(struct v4l2_format));currentFormat.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;ioctl(vd->fd, VIDIOC_G_FMT, ¤tFormat);#if 0struct v4l2_format {__u32 type;union {struct v4l2_pix_formatpix; /* V4L2_BUF_TYPE_VIDEO_CAPTURE */struct v4l2_pix_format_mplanepix_mp; /* V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE */struct v4l2_windowwin; /* V4L2_BUF_TYPE_VIDEO_OVERLAY */struct v4l2_vbi_formatvbi; /* V4L2_BUF_TYPE_VBI_CAPTURE */struct v4l2_sliced_vbi_formatsliced; /* V4L2_BUF_TYPE_SLICED_VBI_CAPTURE */struct v4l2_sdr_formatsdr; /* V4L2_BUF_TYPE_SDR_CAPTURE */__u8raw_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;__u32height;__u32pixelformat;__u32field;/* enum v4l2_field */__u32 bytesperline;/* for padding, zero if unused */__u32 sizeimage;__u32colorspace;/* enum v4l2_colorspace */__u32priv;/* private data, depends on pixelformat */__u32flags;/* format flags (V4L2_PIX_FMT_FLAG_*) */__u32ycbcr_enc;/* enum v4l2_ycbcr_encoding */__u32quantization;/* enum v4l2_quantization */__u32xfer_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 完整流程
完整流程:打开设备、查询能力、枚举格式、设置格式、申请缓冲区、映射内存、缓冲队列管理、启动流(摄像头)和数据处理循环。
编译:arm-buildroot-linux-gnueabihf-gcc -o video_test video_test.c -lpthread
3.2 运行结果
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;}