V4L2编程之USB摄像头采集jpeg图像

V4L2编程实战

开发环境

  1. 虚拟机Ubuntu 16.04
  2. 编辑器VsCode
  3. 交叉编译工具 arm-linux-gnueabihf
  4. 已制作文件系统,已使能UVC相关驱动
  5. 正点原子ZYNQ7010启明星开发板
  6. USB摄像头淘宝随便买的一个

V4L2简介

V4L2,即 Video for linux two ,是 Linux 内核中视频类设备的一套驱动框架,为视频类设备驱动开发和应用层提供了一套统一的接口规范
使用 V4L2 设备驱动框架注册的设备会在 Linux 系统/dev/目录下生成对应的设备节点文件,设备节点的名称通常为 videoX(X为0、1、2…)

编程流程

  1. 首先是打开摄像头设备;
  2. 查询设备的属性或功能;
  3. 设置设备的参数,譬如像素格式、 帧大小、 帧率;
  4. 申请帧缓冲、 内存映射;
  5. 帧缓冲入队;
  6. 开启视频采集;
  7. 帧缓冲出队、对采集的数据进行处理;
  8. 处理完后,再次将帧缓冲入队,往复;
  9. 结束采集。

打开设备

视频类设备对应的设备节点为/dev/videoX(X为0、1、2…)
可以使用命令 ls /dev/video* 查看视频类设备对应的设备节点
笔者在Arm板上插上USB摄像头之后可以看到多了一个设备节点为 /dev/video1
在这里插入图片描述

	//定义一个设备描述符
    int fd;
    fd = open("/dev/videoX", O_RDWR);
    if(fd < 0){
        perror("video设备打开失败\n");
        return -1;
    }
    else{
        printf("video设备打开成功\n");
    }

查看设备的属性和能力

  1. 首先查看设备是否为视频采集设备
    ioctl(fd, VIDIOC_QUERYCAP, &vcap);
    if (!(V4L2_CAP_VIDEO_CAPTURE & vcap.capabilities)) {
        perror("Error: No capture video device!\n");
        return -1;
    }
  1. 查看摄像头所支持的所有像素格式,先看一下v4l2_fmtdesc结构体
struct v4l2_fmtdesc {
	__u32 index; /* index 就是一个编号 */
	__u32 type; /* enum v4l2_buf_type */
	__u32 flags;
	__u8 description[32]; /* description 字段是一个简单地描述性字符串 */
	__u32 pixelformat; /* pixelformat 字段则是对应的像素格式编号 */
	__u32 reserved[4];
};

代码实现,使用VIDIOC_ENUM_FMT

    struct v4l2_fmtdesc fmtdesc;
    fmtdesc.index = 0;
    fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    
    printf("摄像头支持所有格式如下:\n");
    while(ioctl(fd,VIDIOC_ENUM_FMT,&fmtdesc) == 0){
        printf("v4l2_format%d:%s\n",fmtdesc.index,fmtdesc.description);
        fmtdesc.index++;
    }
  1. 查看摄像头所支持的分辨率,先看一下v4l2_frmsizeenum结构体
struct v4l2_frmsizeenum {
	__u32 index; /* Frame size number */
	__u32 pixel_format; /* 像素格式 */
	__u32 type; /* type */
	union { /* Frame size */
		struct v4l2_frmsize_discrete discrete;
		struct v4l2_frmsize_stepwise stepwise;
	};
	__u32 reserved[2]; /* Reserved space for future use */
};

struct v4l2_frmsize_discrete {
	__u32 width; /* Frame width [pixel] */
	__u32 height; /* Frame height [pixel] */
};

比如我们要枚举出摄像头 MJPEG 像素格式所支持的所有分辨率:

struct v4l2_frmsizeenum frmsize;
frmsize.index = 0;
frmsize.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

printf("MJPEG格式支持所有分辨率如下:\n");
frmsize.pixel_format = V4L2_PIX_FMT_MJPEG;
while(ioctl(fd,VIDIOC_ENUM_FRAMESIZES,&frmsize) == 0){
    printf("frame_size<%d*%d>\n",frmsize.discrete.width,frmsize.discrete.height);
    frmsize.index++;
}
  1. 查看摄像头所支持的视频采集帧率,先看一下v4l2_frmivalenum结构体
struct v4l2_frmivalenum {
	__u32 index; /* Frame format index */
	__u32 pixel_format;/* Pixel format */
	__u32 width; /* Frame width */
	__u32 height; /* Frame height */
	__u32 type; /* type */
	union { /* Frame interval */
	struct v4l2_fract discrete;
	struct v4l2_frmival_stepwise stepwise;
	};
	__u32 reserved[2]; /* Reserved space for future use */
};

struct v4l2_fract {
	__u32 numerator; //分子
	__u32 denominator; //分母
};

比如我们要枚举出摄像头 MJPEG 格式下640*480分辨率所支持的帧数:

    struct v4l2_frmivalenum frmival;
    frmival.index = 0;
    frmival.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    frmival.pixel_format = V4L2_PIX_FMT_MJPEG;
    frmival.width = 640;
    frmival.height = 480;
    while(ioctl(fd,VIDIOC_ENUM_FRAMEINTERVALS,&frmival) == 0){
        printf("frame_interval under frame_size <%d*%d> support %dfps\n",frmival.width,frmival.height,frmival.discrete.denominator / frmival.discrete.numerator);
        frmival.index++;
    }

设置采集格式

首先要定义结构体v4l2_format来保存采集格式信息,使用VIDIOC_S_FMT指令设置格式,最后用VIDIOC_G_FMT指令查看相关参数是否生效

    struct v4l2_format vfmt;
    vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    vfmt.fmt.pix.width = 640;
    vfmt.fmt.pix.height = 480;
    vfmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
    if(ioctl(fd,VIDIOC_S_FMT,&vfmt) < 0){
        perror("设置格式失败\n");
        return -1;
    }
    // 检查设置参数是否生效
    if(ioctl(fd,VIDIOC_G_FMT,&vfmt) < 0){
        perror("获取设置格式失败\n");
        return -1;
    }
    else if(vfmt.fmt.pix.width == 640 && vfmt.fmt.pix.height == 480 && vfmt.fmt.pix.pixelformat == V4L2_PIX_FMT_MJPEG){
        printf("设置格式生效,实际分辨率大小<%d * %d>,图像格式:Motion-JPEG\n",vfmt.fmt.pix.width,vfmt.fmt.pix.height);
    }
    else{
        printf("设置格式未生效\n");
    }

申请缓冲区空间

申请帧缓冲顾名思义就是申请用于存储一帧图像数据的缓冲区, 使 VIDIOC_REQBUFS 指令可申请帧缓冲
其中struct v4l2_requestbuffers 结构体描述了申请帧缓冲的信息,代码实现如下:

    struct v4l2_requestbuffers reqbuf;
    reqbuf.type  = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    reqbuf.count = 3;   //3个帧缓冲
    reqbuf.memory = V4L2_MEMORY_MMAP;
    if(ioctl(fd,VIDIOC_REQBUFS,&reqbuf) < 0){
        perror("申请缓冲区失败\n");
        return -1;
    }

内存映射

    // 将帧缓冲映射到进程地址空间
    void *frm_base[3];  //映射后的用户空间的首地址
    unsigned int frm_size[3];

    struct v4l2_buffer buf;
    buf.type   = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP;
    // 将每一帧对应的缓冲区的起始地址保存在frm_base数组中,读取采集数据时,只需直接读取映射区即可
    for(buf.index=0;buf.index<3;buf.index++){
        ioctl(fd, VIDIOC_QUERYBUF, &buf);
        frm_base[buf.index] = mmap(NULL,buf.length,PROT_READ | PROT_WRITE,MAP_SHARED,fd,buf.m.offset);
        frm_size[buf.index] = buf.length;

        if(frm_base[buf.index] == MAP_FAILED){
            perror("mmap failed\n");
            return -1;
        }

        // 入队操作
        if(ioctl(fd,VIDIOC_QBUF,&buf) < 0){
            perror("入队失败\n");
            return -1;
        }
    }

开启视频采集

    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    if (ioctl(fd, VIDIOC_STREAMON, &type) < 0){
        perror("开始采集失败\n");
        return -1;
    }

读取帧并保存为.jpg格式的图片

    struct v4l2_buffer  readbuffer;
    readbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    readbuffer.memory = V4L2_MEMORY_MMAP;
    if(ioctl(fd, VIDIOC_DQBUF, &readbuffer) < 0){
        perror("读取帧失败\n");
    }

    // 保存这一帧,格式为jpg
    FILE *file = fopen("v4l2_cap.jpg","w+");
    fwrite(frm_base[readbuffer.index],buf.length,1,file);
    fclose(file);

读取数据并处理完之后要再次入队

    if(ioctl(fd,VIDIOC_QBUF,&readbuffer) < 0){
        perror("入队失败\n");
    }

停止采集,释放映射

    // 停止采集
    if (ioctl(fd, VIDIOC_STREAMOFF, &type) < 0){
        perror("停止采集失败\n");
        return -1;
    }

    // 释放映射
    for(int i=0;i<3;i++){
        munmap(frm_base[i],frm_size[i]);
    }
    
	close(fd);

交叉编译,移植到ARM开发板运行

笔者的开发板插上USB摄像头之后生成的采集设备为/dev/video1
在Linux虚拟机也可以运行,用gcc编译即可生成的采集设备为/dev/video0
执行命令 ./v4l2_first_test /dev/video1 输出结果如下

在这里插入图片描述

然后可以在同级目录下找到保存的.jpg文件,可以在Linux虚拟机内使用scp命令从开发板下载该图片查看
scp使用方法可以看我的这篇博客: 开发板和虚拟机Linux使用scp命令互传文件

在这里插入图片描述

参考资料

【正点原子】I.MX6U 嵌入式 Linux C 应用编程指南 V1.4

  • 11
    点赞
  • 79
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值