【全志T113-S3_100ask】通过v4l2采集图像

文章目录

  • 【全志T113-S3_100ask】通过v4l2采集图像
  • 前言
  • 一、设置摄像头采集格式
  • 二、申请内核缓冲区队列
  • 三、将内核的缓冲区队列映射到用户空间
  • 四、开始采集
  • 五、停止采集
  • 六、释放映射和关闭设备
  • 七、执行文件
  • 八、通过tftp将图片传至PC上
  • 总结



前言

    在前一篇文章我已经介绍了v4l2这个linux关于视频设备的内核驱动以及如何获取我们的usb摄像头所支持的格式,若没有看过前一篇文章的可以阅读前一篇文章进行了解:
USB摄像头——v4l2打开设备、获取设备支持的格式【以t113-s3为例】     今天这篇文章要讲述的是如何通过v4l2采集一帧图像。


一、设置摄像头采集格式

    首先,需要将vmft结构体的type设置为摄像头采集模式:

//设置vfmt.type
    struct v4l2_format vfmt;
    vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;//摄像头采集

    或许有读者会疑问:如何查看这些标志位、结构体呢?可以通过查看/usr/include/linux/videodev2.h文件获取信息

vim /usr/include/linux/videodev2.h

    文件打开之后内容如下:

Android 采集摄像头每帧的图像和音频 摄像头采集模式用哪个_vim


    如果没有配置过vim的话是不会显示行号的,看起来可能不太方便,可以在编辑模式下输入":set nu"或": set number"来显示行号,也可以在/etc/vimrc配置文件最后加上"set number"

    要搜索你想要的只需要在编辑模式下按下"/"加你要搜索的字符串便可以了,如我们现在搜索v4l2_format:

Android 采集摄像头每帧的图像和音频 摄像头采集模式用哪个_vim_02


    可以看到,可以搜索到,按下快捷键ctrl+g键,可以跳转到下一个符合条件的位置。

    接着设置拍摄的宽、高:

//设置宽高(宽高有要求,不是说可以随便设置,宽和高的倍数固定)
    vfmt.fmt.pix.width = 1280;
    vfmt.fmt.pix.height = 720;

    需要注意的是,宽、高并不是可以随意设置的,宽和高的倍数是固定的,比如640×480、320×240等等。
    最后设置采集格式:

//设置采集格式
    // vfmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
    vfmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;

    在上篇文章中有提到,我的摄像头支持YUYV和Motion JPEG这两种格式,所以在设置采集格式的时候,我可以设置为YUYV,也可以设置为MJPEG。不过考虑到方便性,便先将pixelformat设置为V4L2_PIX_FMT_MJPEG了。
    最后判断一下格式是否设置错误

int ret = ioctl(fd,VIDIOC_S_FMT,&vfmt);
    if(ret < 0)
    {
        perror("格式设置错误!");
    }
    // else
    // {
    //     printf("设置成功!\n");
    // }
  
   if(vfmt.fmt.pix.width == 1280 && vfmt.fmt.pix.height == 720 && vfmt.fmt.pix.pixelformat == V4L2_PIX_FMT_MJPEG)
    {
        printf("设置成功!\n");
    }else 
    {
        printf("设置失败!\n");
    }

    刚开始我判断格式设置成功的条件是ret大于0,此时打印设置成功。后来了解到如果外接摄像头不支持你所设置的格式,会自动设置为其默认的参数,在这种情况下可能会导致没设置成功,却打印了“设置成功”。所以最后采用了if判断设置之后的宽、高、采集格式是否和我们所给的相同,如果相同才返回“设置成功”。

二、申请内核缓冲区队列

    申请内核缓冲区队列需要用到的是 VIDIOC_REQBUFS,其对应的结构体为 v4l2_requestbuffers

//申请内核缓冲区队列
    struct v4l2_requestbuffers reqbuffer;
    reqbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;//设置采集格式
    //申请4个缓冲区
    reqbuffer.count = 4;
    //指定映射方式
    reqbuffer.memory = V4L2_MEMORY_MMAP; 

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

三、将内核的缓冲区队列映射到用户空间

    代码如下:

//将内核的缓冲区队列映射到用户空间
    unsigned char *mptr[4];//保存映射后用户空间首地址

    struct v4l2_buffer mapbuffer;
    mapbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;//指定type
    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;
	ret = ioctl(fd, VIDIOC_DQBUF, &readbuffer);
	if(ret < 0)
	{
		perror("提取数据失败");
	}

	FILE *file=fopen("test.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);

六、释放映射和关闭设备

    代码如下:

//释放映射
    unsigned int size[4];
    for(int i=0; i<4; i++)
    {
        munmap(mptr[i], size[i]);
    }

    //关闭设备
    close(fd);

七、执行文件

    上面已经完成了采集图片的功能了,将文件编译后传至开发板上运行,其结果如下:

Android 采集摄像头每帧的图像和音频 摄像头采集模式用哪个_#include_03


    可以看到,原来在目录下是没有test.jpg文件的,运行完之后出现了test.jpg文件,这就是摄像头采集到的图片。

八、通过tftp将图片传至PC上

    在开发板上没办法直接查看图片的,所以我们通过tftp将文件传输至电脑上打开查看。

    首先,在MobaXterm1_CHS1这款工具上是集成了tftp服务器功能的,点击左上角“服务器”(如果不是中文版的就点击左上角第二个选项),在弹出的窗口中点击tftp服务器的设置,如图所示:

Android 采集摄像头每帧的图像和音频 摄像头采集模式用哪个_#include_04


    在设置中选择你电脑与开发板“交互”的文件夹,然后点击“好的”完成设置,如图所示:

Android 采集摄像头每帧的图像和音频 摄像头采集模式用哪个_c语言_05


    配置完成之后记得点击启动按钮:

Android 采集摄像头每帧的图像和音频 摄像头采集模式用哪个_vim_06


    启动之后还需要确保开发板与电脑连接的是同一个局域网,t113-s3开发板连接wifi的方法是运行/etc/wlan-connect.sh,其格式如下:

/etc/wlan-connect.sh  WIFI名字   WIFI密码  1

    连接了同一个WiFi之后,便可以通过tftp从开发板将图片传过来(也可以从PC端传文件去开发板,我的执行文件就是这么传去开发板的)。

将图片文件传输至PC端的命令:
tftp  -r  ./文件名  -p  电脑IP

Android 采集摄像头每帧的图像和音频 摄像头采集模式用哪个_linux_07


    完成传输后,在文件夹中便会出现我们刚刚拍摄的图片:

Android 采集摄像头每帧的图像和音频 摄像头采集模式用哪个_linux_08


    打开图片查看:

Android 采集摄像头每帧的图像和音频 摄像头采集模式用哪个_开发板_09


    拍摄图片成功(因为我的摄像头没有自动对焦,所以图片较糊)。


总结

下面给出完整的代码,希望能够给文章点个赞+收藏

#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()
{
    //打开设备
    int fd = open("/dev/video0",O_RDWR);
    if(fd < 0)
    {
        perror("打开设备失败!");
        return -1;
    }

    // //获取摄像头支持的格式 
    // //ioctl(文件描述符,命令,与命令对应结构体)
    // struct v4l2_fmtdesc v4fmt;
    // v4fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    // int i = 0;
    // while(1)
    // {
    //     v4fmt.index = i++;
    //     int ret = ioctl(fd,VIDIOC_ENUM_FMT,&v4fmt);
    //     if(ret < 0)
    //     {
    //         perror("获取失败");
    //         break;
            
    //     }
    //     printf("index=%d\n",v4fmt.index);
    //     printf("flags=%d\n",v4fmt.flags);
    //     printf("description=%s\n",v4fmt.description);
    //     unsigned char *p =(unsigned char *) &v4fmt.pixelformat;
    //     printf("pixelformat=%c%c%c%c\n",p[0],p[1],p[2],p[3]);
    //     printf("reserved=%d\n",v4fmt.reserved[0]);
    // }
    
    //设置vfmt.type
    struct v4l2_format vfmt;
    vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;//摄像头采集
    
    //设置宽高(宽高有要求,不是说可以随便设置,宽和高的倍数固定)
    vfmt.fmt.pix.width = 1280;
    vfmt.fmt.pix.height = 720;
    //设置采集格式
    // vfmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
    vfmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;

    int ret = ioctl(fd,VIDIOC_S_FMT,&vfmt);
    if(ret < 0)
    {
        perror("格式设置错误!");
    }
    // else
    // {
    //     printf("设置成功!\n");
    // }

    // memset(&vfmt,0,sizeof(vfmt));   //memset是一个初始化函数,作用是将某一块内存中的全部设置为指定的值
    // vfmt.type =V4L2_BUF_TYPE_VIDEO_CAPTURE; //初始化之后需要告知采集格式,否则会报错

    // ret = ioctl(fd,VIDIOC_G_FMT,&vfmt);
    // if(ret < 0)
    // {
    //     perror("获取格式失败!");
    // }

    if(vfmt.fmt.pix.width == 1280 && vfmt.fmt.pix.height == 720 && vfmt.fmt.pix.pixelformat == V4L2_PIX_FMT_MJPEG)
    {
        printf("设置成功!\n");
    }else 
    {
        printf("设置失败!\n");
    }

    //申请内核缓冲区队列
    struct v4l2_requestbuffers reqbuffer;
    reqbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;//设置采集格式
    //申请4个缓冲区
    reqbuffer.count = 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;
    mapbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;//指定type
    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;
	ret = ioctl(fd, VIDIOC_DQBUF, &readbuffer);
	if(ret < 0)
	{
		perror("提取数据失败");
	}

	FILE *file=fopen("test.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;
}

    可能有些读者的摄像头并不支持MJPEG格式,采集到的图片需要解码才能查看,这个等下期再讲。