手把手教你使用linux摄像头(V4L2框架)

前言

本系统教程分为3篇,由浅至深地教你在Linux上应用摄像头,为以后在视觉等领域的应用打下基础:
1.本篇为基础学习篇,使用摄像头拍照获取.jpg格式图片
2.在ubuntu上获取动态读取摄像头的视频流并显示在pc上
3.在linux嵌入式开发板lcd上显示摄像头拍摄的视频

什么是V4L2框架

市场上出售着种类繁多的摄像头,他们的型号不同,厂商不同。这样子的话,每一次换一个摄像头难道就要我们自己去写一个驱动吗?这样子太麻烦了而且没有必要,于是就出现了v4l2框架,现在的摄像头都适配这个主流框架(就算不适配厂商也会想尽办法去适配以此来增加自己产品的畅销性)。这个v4l2适配了多种接口的摄像头。我们只需要学习一个简单的V4L2编程就可以通用所有摄像头了!我们知道linux的摄像头编程应用在人工智能嵌入式很热门,下面我来手把手教你linux是怎样驱动摄像头的。

驱动流程

本次教程在pc的虚拟机中实战,想要在嵌入式设备运行只要使用交叉编译器编译出可执行文件再运行即可。新手建议先再pc端使用usb摄像头实验,等学会了再调试到嵌入式设备中。

1.打开设备

当我们摄像头插入到电脑后,再/dev目录下会出现相应的摄像头video节点如图:
在这里插入图片描述
我们需要找到摄像头对应的节点,可以每个都试一试。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>

int main(void) 
{
    int fd = open("/dev/video0",O_RDWR);
    if (fd < 0)
    {
        perror("打开设备失败");
        return -1;
    }



    close(fd);
    return 0;
}

2.获取支持格式

在这里需要用到ioctl(文件描述符,命令,与命令对应的结构体)函数来获取摄像头格式,对应的参数在如下图头文件中查找,具体命令作用请复制到百度搜索,这里直接告诉大家VIDIOC_ENUM_FMT命令是获取摄像头格式。
在这里插入图片描述
以上文档以及要写的程序建议大家在vscode打开,方便查找以及便于使用智能补全插件检查代码和写代码。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>

#include <sys/ioctl.h>
#include <linux/videodev2.h>

int main(void) 
{
    int fd = open("/dev/video1",O_RDWR);
    if (fd < 0)
    {
        perror("打开设备失败");
        return -1;
    }

    //获取摄像头支持格式 ioctl(文件描述符,命令,与命令对应的结构体)
    struct v4l2_fmtdesc vfmt;
    vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;//摄像头采集

    int i = 0;
    while(1)         //使用while循环从0开始测试index索引值
    {
        vfmt.index = i;
        i++;
        int ret = ioctl(fd,VIDIOC_ENUM_FMT,&vfmt);
        if (ret < 0)
        {
            perror("获取失败");
            break;
        }
        printf("index=%d\n",vfmt.index);
        printf("flags=%d\n",vfmt.flags);
        printf("discription=%s\n",vfmt.description);
        unsigned char *p = (unsigned char *)&vfmt.pixelformat;
        printf("pixelformat=%c%c%c%c\n",p[0],p[1],p[2],p[3]);
        printf("reserved=%d\n",vfmt.reserved[0]);
    }
    close(fd);
    return 0;
}

运行结果
在这里插入图片描述

说明我的摄像头支持三种格式。

3.配置摄像头

在这里插入图片描述

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>
#include <string.h>
int main(void) 
{
    int fd = open("/dev/video0",O_RDWR);//根据自己的摄像头设备节点打开
    if (fd < 0)
    {
        perror("打开设备失败");
        return -1;
    }

    //获取摄像头支持格式 ioctl(文件描述符,命令,与命令对应的结构体)
    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_YYUV; //设置视频采集格式 ,根据上一步测得,注意格式有yyuv和yuyv不要搞混
    
    int ret = ioctl(fd,VIDIOC_S_FMT,&vfmt); //设置格式命令
    if (ret < 0)
    {
        perror("设置格式失败1");
    }

    memset(&vfmt,0,sizeof(vfmt));
    vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    ret = ioctl(fd,VIDIOC_G_FMT,&vfmt);
    if (ret < 0)
    {
        perror("设置格式失败2");
    }

    if(vfmt.fmt.pix.width == 640 && vfmt.fmt.pix.height == 480 && vfmt.fmt.pix.pixelformat == V4L2_PIX_FMT_YUYV)
    {
        printf("设置成功!");
    }else
    {
        printf("设置失败3");
    }
    close(fd);
    return 0;
}

打印出:设置成功 即可。
在这里插入图片描述

4.申请内核缓冲区队列

这里也是一样,直接告诉大家VIDIOC_REQBUFS操作命令是申请缓冲区队列。
在这里插入图片描述

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>
#include <string.h>
int main(void) 
{
    int fd = open("/dev/video0",O_RDWR);
    if (fd < 0)
    {
        perror("打开设备失败");
        return -1;
    }

    //获取摄像头支持格式 ioctl(文件描述符,命令,与命令对应的结构体)
    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_YYUV; //设置视频采集格式 ,根据上一布测得
    
    int ret = ioctl(fd,VIDIOC_S_FMT,&vfmt);
    if (ret < 0)
    {
        perror("设置格式失败1");
    }

    //申请内核空间
    struct v4l2_requestbuffers reqbuffer;
    reqbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    reqbuffer.count = 4; //申请4个缓冲区
    reqbuffer.memory = V4L2_MEMORY_MMAP;  //映射方式

    ret = ioctl(fd,VIDIOC_REQBUFS,&reqbuffer);
    if (ret < 0)
    {
        perror("申请空间失败");
    }
   
    close(fd);
    return 0;
}

在这里插入图片描述
执行结果如图,表示正常(在linux中,没有输出是最好的输出)

5.内核缓冲区队列映射到用户空间

映射步骤用到了两个命令:VIDIOC_QUERYBUF和VIDIOC_QBUF,他们两个使用相同的结构体

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>
#include <string.h>
#include <sys/mman.h>
int main(void) 
{
    int fd = open("/dev/video0",O_RDWR);
    if (fd < 0)
    {
        perror("打开设备失败");
        return -1;
    }

    //获取摄像头支持格式 ioctl(文件描述符,命令,与命令对应的结构体)
    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_YYUV; //设置视频采集格式 ,根据上一布测得
    
    int ret = ioctl(fd,VIDIOC_S_FMT,&vfmt);
    if (ret < 0)
    {
        perror("设置格式失败1");
    }

    //申请内核空间
    struct v4l2_requestbuffers reqbuffer;
    reqbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    reqbuffer.count = 4; //申请4个缓冲区
    reqbuffer.memory = V4L2_MEMORY_MMAP;  //映射方式

    ret = ioctl(fd,VIDIOC_REQBUFS,&reqbuffer);
    if (ret < 0)
    {
        perror("申请空间失败");
    }
   
    //映射
    unsigned char *mptr[4];//保存映射后用户空间的首地址
    struct v4l2_buffer mapbuffer;
    //初始化type和index
    mapbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

    for(int i = 0; i <4;i++) {  //使用for对四个申请的空间进行轮询
        mapbuffer.index = i;
        ret = ioctl(fd,VIDIOC_QUERYBUF,&mapbuffer); //从内核空间中查询一个空间作映射
        if (ret < 0)
        {
            perror("查询内核空间失败");
        }
        //映射到用户空间
        mptr[i] = (unsigned char *)mmap(NULL,mapbuffer.length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,mapbuffer.m.offset);
        //查询后通知内核已经放回
        ret = ioctl(fd,VIDIOC_QBUF,&mapbuffer); 
        if (ret < 0)
        {
            perror("放回失败");
        }
    }


    close(fd);
    return 0;
}

6.采集帧数据

采集有四个命令:
VIDIOC_STREAMON(开始采集写数据到队列中)
VIDIOC_DQBUF(告诉内核我要某一个数据,内核不可以修改我正在采集的地方的数据)
VIDIOC_QBUF(告诉内核我已经使用完毕,内核可以写入了)
VIDIOC_STREAMOFF(停止采集-不在向队列中写数据)

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>
#include <string.h>
#include <sys/mman.h>
int main(void) 
{
    int fd = open("/dev/video0",O_RDWR);
    if (fd < 0)
    {
        perror("打开设备失败");
        return -1;
    }

    //获取摄像头支持格式 ioctl(文件描述符,命令,与命令对应的结构体)
    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; //设置为mjpg格式,则我可以直接写入文件保存,YUYV格式保存后需要转换格式才能查看
    
    int ret = ioctl(fd,VIDIOC_S_FMT,&vfmt);
    if (ret < 0)
    {
        perror("设置格式失败1");
    }

    //申请内核空间
    struct v4l2_requestbuffers reqbuffer;
    reqbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    reqbuffer.count = 4; //申请4个缓冲区
    reqbuffer.memory = V4L2_MEMORY_MMAP;  //映射方式

    ret = ioctl(fd,VIDIOC_REQBUFS,&reqbuffer);
    if (ret < 0)
    {
        perror("申请空间失败");
    }
   
    //映射
    unsigned char *mptr[4];//保存映射后用户空间的首地址
    unsigned int size[4];
    struct v4l2_buffer mapbuffer;
    //初始化type和index
    mapbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

    for(int i = 0; i <4;i++) {
        mapbuffer.index = i;
        ret = ioctl(fd,VIDIOC_QUERYBUF,&mapbuffer); //从内核空间中查询一个空间作映射
        if (ret < 0)
        {
            perror("查询内核空间失败");
        }
        //映射到用户空间
        mptr[i] = (unsigned char *)mmap(NULL,mapbuffer.length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,mapbuffer.m.offset);
        size[i] = mapbuffer.length; //保存映射长度用于后期释放
        //查询后通知内核已经放回
        ret = ioctl(fd,VIDIOC_QBUF,&mapbuffer); 
        if (ret < 0)
        {
            perror("放回失败");
        }
    }
    //开始采集
    int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    ret = ioctl(fd,VIDIOC_STREAMON,&type); 
    if (ret < 0)
        {
            perror("开启失败");
        }
    //从队列中提取一帧数据
    struct v4l2_buffer readbuffer;
    readbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //每个结构体都需要设置type为这个参赛要记住
    ret = ioctl(fd,VIDIOC_DQBUF,&readbuffer); 
    if (ret < 0)
        {
            perror("读取数据失败");
        }
    
    FILE *file=fopen("my.jpg", "w+");  //打开一个文件
    fwrite( mptr[readbuffer.index],readbuffer.length,1,file);//写入文件
    fclose(file);    //写入完成,关闭文件

    //通知内核使用完毕
   ret = ioctl(fd, VIDIOC_QBUF, &readbuffer);
   if(ret < 0)
	{
		perror("放回队列失败");
	}
	
    //停止采集
    ret = ioctl(fd,VIDIOC_STREAMOFF,&type);
    
	//释放映射
    for(int i=0; i<4; i)munmap(mptr[i], size[i]);

    close(fd); //关闭文件
    
    return 0;
}

到了这一步我们就已经是可以采集到帧数据了。
在这里插入图片描述

进阶学习

在我们成功驱动摄像头拍取照片之后,下一步当然是连续读取做成视频啦!接下来我将教大家将生成的jpg格式视频流转化成rgb格式显示在屏幕上:动态显示摄像头数据

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
xiaoxingxing的头像xiaoxingxing管理团队
上一篇 2023年3月4日 下午1:19
下一篇 2023年3月4日 下午1:21

相关推荐