基于V4L2的摄像头采集图像,HDMI转USB采集图像,并保存一帧图像

简单介绍

最近实习做了一个类似通过USB接口的摄像头显示图像的功能,不过手上有的是一个比较简单的HDMI转USB的小模块,也可以实现这样的功能原理一样的,只需要在HDMI接入一个输入源就可以了。此文章也可以作为要使用V4L2去做视频采集图像采集功能的小伙伴提供一个思路。流程也比较简单,主要是做一个记录,记录一下这个过程中遇到坑和一些心得体会,方便到时候回来复习,本人也是新手,菜的一批,也是刚开始写博客,希望大佬们多多指教,尽量少喷、少喷哈谢谢哈哈哈哈!不说废话了,这就开始吧!

开发环境

  1. 虚拟机Ubuntu 20.00
  2. 编辑器VsCode
  3. 交叉编译工具 aarch64-linux-gnu
  4. HDMI转usb图像模块或者usb摄像头淘宝随便买一个都可以
  5. 开发板是公司的板子,这里就不透露啦,基本的嵌入式开发板都可以的

什么是V4L2?

V4L2 Video for linux two 的简称,是 Linux 内核中视频类设备的一套驱动框架,为视频类设备驱动 ,为开发和应用层提供了一套统一的接口规范,那什么是视频类设备呢?一个非常典型的视频类设备就是视频采集设备,譬如各种摄像头;当然还包括其它类型视频类设备,这里就不再给介绍了。
使用 V4L2 设备驱动框架注册的设备会在 Linux 系统 /dev/ 目录下生成对应的设备节点文件,设备节点的名称通常为 videoX X 标准一个数字编号, 0 1 2 3…… ),每一个 videoX 设备文件就代表一个视频类设备。我自己的就是video0,大家可以通过插入usb后去通过“ls  /dev/video* ”指令去查看自己设备文件下的设备是什么。应用程序通过对 videoX 设备文件进行 I/O 操作来配置、使用设备类设备。

编程步骤

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

流程图

图片来自正点原子的MX6U嵌入式Linux C应用编程指南

1.打开设备

整个程序需要包含的头文件,最重要的是##include <linux/videodev2.h>,其他可根据个人情况选择

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>
#include <errno.h>
#include <sys/mman.h>

#include <linux/videodev2.h>
#include <linux/fb.h>

 打开设备,并以fd来保存返回的设备描述号,open成功返回设备号,不成功返回-1

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

 struct v4l2_capability结构体是linux/videodev2.h头文件已经有包含的了,我们调用就好,详解大家可以网上搜索进一步了解,这里就不过多介绍啦!

struct v4l2_capability {
 __u8 driver[16]; /* 驱动的名字 */
 __u8 card[32]; /* 设备的名字 */
 __u8 bus_info[32]; /* 总线的名字 */
 __u32 version; /* 版本信息 */
 __u32 capabilities; /* 设备拥有的能力 */
 __u32 device_caps;
 __u32 reserved[3]; /* 保留字段 */
};

 2. 查询设备的属性或功能

查询设备是否具备获取图像能力,同时顺便打印一下设备信息,便于观察(可忽略)

 ioctl(fd, VIDIOC_QUERYCAP, &cap);
    if (!(V4L2_CAP_VIDEO_CAPTURE & cap.capabilities)) 
    {
        perror("Error: No capture video device!\n");
        return -1;
    }
    printf("驱动名 : %s\n",cap.driver);
    printf("设备名字 : %s\n",cap.card);
    printf("总线信息 : %s\n",cap.bus_info);
    printf("驱动版本号 : %d\n",cap.version);

列举设备所支持的格式

这里ioctl(fd,VIDIOC_ENUM_FRAMESIZES,&frmsize),ioctl用法大家可以去搜索具体了解一下,这里的意思就是对fd指向的设备进行设备参数的列举,VIDIOC_ENUM_FRAMESIZES指令就是可以枚举出设备里面的参数的

列举MJEPG格式支持所有分辨率:

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

    printf("MJEPG格式支持所有分辨率如下:\n");
    frmsize.pixel_format = V4L2_PIX_FMT_MJEPG;
    while(ioctl(fd,VIDIOC_ENUM_FRAMESIZES,&frmsize) == 0)
    {
        printf("frame_size<%d*%d>\n",frmsize.discrete.width,frmsize.discrete.height);
        frmsize.index++;
    }
-index 表示编号,在枚举之前,需将其设置为 0 ,然后每次 ioctl() 调用之后将其值加 1
-type 字 段 需 要 在 调 用 ioctl() 之 前 设 置 它 的 值 , 对 于 摄 像 头 , 需 要 将 type 字 段 设 置 为 V4L2_BUF_TYPE_VIDEO_CAPTURE,指定我们将要获取的是视频采集的像素格式。
struct v4l2_frmsizeenum内容介绍(再次声明本文用到的V4l2-的结构体仅做介绍,不需要自己重新定义到程序里,关于v4l2_所定义的结构体感兴趣的可以网上搜索看看,这里由于篇幅问题就不过多列举出来了,下同)
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 格式下1280*720分辨率所支持的帧数:

  struct v4l2_frmivalenum frmival;
    frmival.index = 0;
    frmival.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    frmival.pixel_format = V4L2_PIX_FMT_MJEPG;
    frmival.width = 1280;
    frmival.height = 720;
    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_PIX_FMT_MJEPG表示图像格式, frmival.width 是图像宽度,frmival.height 是图像高度

-struct v4l2_fract 结构体中,numerator 表示分子、denominator 表示分 母,使用 numerator / denominator 来表示图像采集的周期(采集一幅图像需要多少秒),所以视频帧率便等于 denominator / numerator

3.设置设备的参数,譬如像素格式、帧大小、帧率等

  struct v4l2_format vfmt;    //头文件自带v4l2_format结构体,引用即可
    vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    vfmt.fmt.pix.width = 1280;
    vfmt.fmt.pix.height = 720;
    vfmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJEPG;
    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 == 1280 && vfmt.fmt.pix.height == 720 && vfmt.fmt.pix.pixelformat == V4L2_PIX_FMT_MJEPG )
    {
        printf("设置格式生效,实际分辨率大小<%d * %d>,图像格式:Motion-JPEG\n",vfmt.fmt.pix.width,vfmt.fmt.pix.height);
    }
    else
    {
        printf("设置格式未生效\n");
    }

获取流streamparm

 /* 获取 streamparm */
     struct v4l2_streamparm streamparm = {0};
     streamparm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
     ioctl(fd, VIDIOC_G_PARM, &streamparm);
     if (V4L2_CAP_TIMEPERFRAME & streamparm.parm.capture.capability) 
     {
         streamparm.parm.capture.timeperframe.numerator = 1;
         streamparm.parm.capture.timeperframe.denominator = 60;//60fps
         if (0 > ioctl(fd, VIDIOC_S_PARM, &streamparm)) 
         {
             fprintf(stderr, "ioctl error: VIDIOC_S_PARM: %s\n", strerror(errno));
             return -1;
         }
     }
     else
         fprintf(stderr, "不支持帧率设置");

4. 申请帧缓冲、内存映射

申请帧缓冲

-绝大部分设备都支持 streaming I/O 方式读取数据,使用 streaming I/O 方式,我们需要向设备申请帧缓冲,并将帧缓冲映射到应用程序进程地址空间中。帧缓冲顾名思义就是用于存储一帧图像数据的缓冲区,使用 VIDIOC_REQBUFS 指令可申请帧缓冲
    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;
    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;
        }
-因为在Linux系统下是不可以直接读取内核的文件内容的,如果想要读取其信息必须将其映射到内存区域上面,那么通过内存映射,指针指向的地址便可以找到其内容,此时读取的内存数据就是内核里面所呈现的数据(具体解释可以网上搜索,这里只做个人的简单理解)
-存储映射 I/O memory-mapped I/O )是一种基于内存区域的高级 I/O 操作,它能将一个文件映射到进程 地址空间中的一块内存区域中,当从这段内存中读数据时,就相当于读文件中的数据(对文件进行 read 操 作),将数据写入这段内存时,则相当于将数据直接写入文件中(对文件进行 write 操作)。这样就可以在不使用基本 I/O 操作函数 read() write() 的情况下执行 I/O 操作。

 5. 帧缓冲入队

简单介绍一下出队入队

streaming I/O 方式会在内核空间中维护一个帧缓冲队列,驱动程序会将从摄像头读取的一帧数据写入到队列中的一个帧缓冲,接着将下一帧数据写入到队列中的下一个帧缓冲;当应用程序需要读取一帧数据时,需要从队列中取出一个装满一帧数据的帧缓冲,这个取出过程就叫做出队;当应用程序处理完这一帧数据后,需要再把这个帧缓冲加入到内核的帧缓冲队列中,这个过程叫做入队,如图所示:
先将数据放入队中,以便后面取出,程序如下:
// 入队操作
        if(ioctl(fd,VIDIOC_QBUF,&buf) < 0)
        {
            perror("入队失败\n");
            return -1;
        }
    }

6. 开启视频采集

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

7. 帧缓冲出队、对采集的数据进行处理

出队操作:

 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格式保存下来

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

将用完的数据又放回队列中,以便下一次取出

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

注意:本文出队的操作是对一帧的数据图像进行保存,如果小伙伴们发现保存的图片是黑色的,这是正常的,因为有时候信号源刚开机的时候会有延迟,是黑屏的,你可能就刚好采集到这一帧,所以不奇怪,我刚开始也遇到这种问题。只需要对出队,开启采集,入队操作进行一个for循环,采集后面多几帧的数据就可以看到了

8.结束采集工作,关闭采集,释放映射

// 停止采集
    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);


}

9.采集效果

好了,到这里基本的采集流程就差不多了,接下来就是进行交叉编译,把程序烧录到板子,运行程序就可以看到效果啦,如图:

看到这里的小伙伴也快点去试试吧,如果觉得这篇文章对你有所帮助,不妨碍点赞收藏+关注再走呗!编写不易,谢谢大家的支持哦,谢谢!谢谢!谢谢! 重要的事情说三遍!!!同时非常感谢

爱学习的诸葛铁锤这位大佬的文章“V4L2编程之USB摄像头采集jpeg图像”!

参考文献:正点原子的MX6U嵌入式Linux C应用编程指南,爱学习的诸葛铁锤—“V4L2编程之USB摄像头采集jpeg图像”

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值