V4L2 视频采集笔记

V4L2 视频采集


本文用于记录学习 V4L2 采集视频,写得比较难粗略。完整代码在文末给出。

一、 V4L2简介

Video for Linux two(Video4Linux2) 简称 V4L2,是 V4L 的改进版。V4L2 是 linux 操作系统下一套用于采集图片、视频和音频数据的通用API接口,配合适当的视频采集设备和相应的驱动程序,可以实现图片、视频、音频等的采集。V4L2 像一个优秀的快递员,将视频采集设备的图像数据安全、高效的传递给不同需求的用户。

​ 在 Linux 中,一切皆文件,所有外设都被看成一种特殊的文件,称为“设备文件”。视频设备也不例外,也可以可以看成是设备文件,可以像访问普通文件一样对其进行读写。V4L2驱动的摄像头的设备文件一般是/dev/videoX(X为任意数字,要与自己的设备相对应)。

​ V4L2支持三种方式来采集图像:内存映射方式(mmap)、直接读取方式(read)和用户指针。内存映射的方式采集速度较快,一般用于连续视频数据的采集,实际工作中的应用概率更高;直接读取的方式相对速度慢一些,所以常用于静态图片数据的采集;用户指针使用较少,如有兴趣可自行研究。由于内存映射方式的应用更广泛,所以本文重点讨论内存映射方式的视频采集。

二、 V4L2 视频采集流程

使用V4L2进行视频采集,一般分为5个步骤:

(1)打开设备,进行初始化参数设置,通过V4L2接口设置视频图像的采集窗口、采集的点阵大小和格式;

(2)申请图像帧缓冲,并进行内存映射,将这些帧缓冲区从内核空间映射到用户空间,便于应用程序读取、处理图像数据;

(3)将帧缓冲进行入队操作,启动视频采集;

(4)驱动开始视频数据的采集,应用程序从视频采集输出队列取出帧缓冲区,处理完后,将帧缓冲区重新放入视频采集输入队列,循环往复采集连续的视频数据;

(5)释放资源,停止采集工作。

三、V4L2 程序实例

3.1 头文件和宏定义

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

#define VIDEO_DEVICE "/dev/video0"  // 摄像头设备文件路径
#define OUTPUT_FOLDER "./captured_frames/"  // 图像保存文件夹路径
#define NUM_FRAMES 100  // 要捕获的帧数

3.2 打开摄像头设备

查询设备属性需要使用struct v4l2_capability结构体,该结构体描述了视频采集设备的driver信息。

struct v4l2_capability
{
    
    
     __u8 driver[16];       // 驱动名字
     __u8 card[32];         // 设备名字
     __u8 bus_info[32];     // 设备在系统中的位置
     __u32 version;         // 驱动版本号
     __u32 capabilities;    // 设备支持的操作
     __u32 reserved[4];     // 保留字段
};
// 打开摄像头设备
fd = open(VIDEO_DEVICE, O_RDWR);
if (fd == -1) 
{
    
    
     perror("Unable to open device");
     return -1;
}

// 查询摄像头能力
if (ioctl(fd, VIDIOC_QUERYCAP, &cap) == -1) 
{
    
    
     perror("Unable to query device");
     close(fd);
     return -1;
}

3.3 设置图像帧格式

设置图像格式需要用到struct v4l2_format结构体,该结构体描述每帧图像的具体格式,包括帧类型以及图像的长、宽等信息。

// 设置图像格式
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 640;  // 设置图像宽度
fmt.fmt.pix.height = 480; // 设置图像高度
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG; // 设置图像格式为MJPEG
if (ioctl(fd, VIDIOC_S_FMT, &fmt) == -1) 
{
    
    
     perror("Unable to set format");
     close(fd);
     return -1;
}

这里我们可以先用如下方式查询摄像头支持的格式,代码运行后会输出所以摄像头支持的格式。

//1.打开设备
int fd = open("/dev/video0", O_RDWR);
if(fd < 0)
{
    
    
     perror("打开设备失败");
     return -1;
}
//2.获取摄像头支持的格式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]);
}
//9.关闭设备
close(fd);
return 0;

3.4 申请图像帧缓存,映射到用户空间

// 请求缓冲区
req.count = 4; // 请求4个缓冲区(可以根据需要调整)
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
if (ioctl(fd, VIDIOC_REQBUFS, &req) == -1) 
{
    
    
     perror("Unable to request buffers");
     close(fd);
     return -1;
}

// 映射缓冲区
for (int i = 0; i < req.count; ++i) 
{
    
    
     buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
     buf.memory = V4L2_MEMORY_MMAP;
     buf.index = i;
     if (ioctl(fd, VIDIOC_QUERYBUF, &buf) == -1) 
     {
    
    
          perror("Unable to query buffer");
          close(fd);
          return -1;
     }
     buffer = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset);

     // 启用缓冲区
     if (ioctl(fd, VIDIOC_QBUF, &buf) == -1) 
     {
    
    
          perror("Unable to queue buffer");
          close(fd);
          return -1;
     }
}

3.5 开始捕获和停止捕获

// 启动捕获
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(fd, VIDIOC_STREAMON, &type) == -1) 
{
    
    
     perror("Unable to start capture");
     close(fd);
     return -1;
}

// 创建输出文件夹
if (mkdir(OUTPUT_FOLDER, 0755) == -1) 
{
    
    
     perror("Unable to create output folder");
     close(fd);
     return -1;
}

while (frame_count < NUM_FRAMES) 
{
    
    
     // 从缓冲区中获取图像数据
     buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
     if (ioctl(fd, VIDIOC_DQBUF, &buf) == -1) 
     {
    
    
          perror("Unable to dequeue buffer");
          continue;
     }

     // 生成文件名
     snprintf(filename, sizeof(filename), "%sframe%d.jpg", OUTPUT_FOLDER, frame_count);

     // 创建并保存图像文件
     output = fopen(filename, "wb");
     if (!output) 
     {
    
    
          perror("Unable to open output file");
     } 
     else 
     {
    
    
          fwrite(buffer, buf.bytesused, 1, output);
          fclose(output);
          printf("Frame %d captured and saved to %s\n", frame_count, filename);
          frame_count++;
     }

     // 将缓冲区重新排队以继续捕获
     if (ioctl(fd, VIDIOC_QBUF, &buf) == -1) 
     {
    
    
          perror("Unable to queue buffer");
          break;
     }
}

// 停止捕获
if (ioctl(fd, VIDIOC_STREAMOFF, &type) == -1) 
{
    
    
     perror("Unable to stop capture");
}

// 关闭文件和设备
for (int i = 0; i < req.count; ++i) 
{
    
    
     munmap(buffer, buf.length);
}
close(fd);

完整代码如下,代码实现了利用 V4L2 采集图像并创建一个文件夹将图像储存在其中。

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

#define VIDEO_DEVICE "/dev/video0"  // 摄像头设备文件路径
#define OUTPUT_FOLDER "./captured_frames/"  // 图像保存文件夹路径
#define NUM_FRAMES 100  // 要捕获的帧数

int main() 
{
    
    
    int fd;
    struct v4l2_capability cap;
    struct v4l2_format fmt;
    struct v4l2_requestbuffers req;
    struct v4l2_buffer buf;
    void *buffer;
    char filename[256];
    FILE *output;
    int frame_count = 0;

    // 打开摄像头设备
    fd = open(VIDEO_DEVICE, O_RDWR);
    if (fd == -1) 
    {
    
    
        perror("Unable to open device");
        return -1;
    }

    // 查询摄像头能力
    if (ioctl(fd, VIDIOC_QUERYCAP, &cap) == -1) 
    {
    
    
        perror("Unable to query device");
        close(fd);
        return -1;
    }

    // 设置图像格式
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt.fmt.pix.width = 640;  // 设置图像宽度
    fmt.fmt.pix.height = 480; // 设置图像高度
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG; // 设置图像格式为MJPEG
    if (ioctl(fd, VIDIOC_S_FMT, &fmt) == -1) 
    {
    
    
        perror("Unable to set format");
        close(fd);
        return -1;
    }

    // 请求缓冲区
    req.count = 4; // 请求4个缓冲区(可以根据需要调整)
    req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    req.memory = V4L2_MEMORY_MMAP;
    if (ioctl(fd, VIDIOC_REQBUFS, &req) == -1) 
    {
    
    
        perror("Unable to request buffers");
        close(fd);
        return -1;
    }

    // 映射缓冲区
    for (int i = 0; i < req.count; ++i) 
    {
    
    
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        buf.index = i;
        if (ioctl(fd, VIDIOC_QUERYBUF, &buf) == -1) 
        {
    
    
            perror("Unable to query buffer");
            close(fd);
            return -1;
        }
        buffer = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset);
        
        // 启用缓冲区
        if (ioctl(fd, VIDIOC_QBUF, &buf) == -1) 
        {
    
    
            perror("Unable to queue buffer");
            close(fd);
            return -1;
        }
    }

    // 启动捕获
    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    if (ioctl(fd, VIDIOC_STREAMON, &type) == -1) 
    {
    
    
        perror("Unable to start capture");
        close(fd);
        return -1;
    }

    // 创建输出文件夹
    if (mkdir(OUTPUT_FOLDER, 0755) == -1) 
    {
    
    
        perror("Unable to create output folder");
        close(fd);
        return -1;
    }

    while (frame_count < NUM_FRAMES) 
    {
    
    
        // 从缓冲区中获取图像数据
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        if (ioctl(fd, VIDIOC_DQBUF, &buf) == -1) 
        {
    
    
            perror("Unable to dequeue buffer");
            continue;
        }

        // 生成文件名
        snprintf(filename, sizeof(filename), "%sframe%d.jpg", OUTPUT_FOLDER, frame_count);

        // 创建并保存图像文件
        output = fopen(filename, "wb");
        if (!output) 
        {
    
    
            perror("Unable to open output file");
        } 
        else 
        {
    
    
            fwrite(buffer, buf.bytesused, 1, output);
            fclose(output);
            printf("Frame %d captured and saved to %s\n", frame_count, filename);
            frame_count++;
        }

        // 将缓冲区重新排队以继续捕获
        if (ioctl(fd, VIDIOC_QBUF, &buf) == -1) 
        {
    
    
            perror("Unable to queue buffer");
            break;
        }
    }

    // 停止捕获
    if (ioctl(fd, VIDIOC_STREAMOFF, &type) == -1) 
    {
    
    
        perror("Unable to stop capture");
    }

    // 关闭文件和设备
    for (int i = 0; i < req.count; ++i) 
    {
    
    
        munmap(buffer, buf.length);
    }
    close(fd);

    return 0;
}

参考

  1. Linux应用开发【第七章】摄像头V4L2编程应用开发 - 知乎 (zhihu.com)
  2. 和小白一起学习V4L2采集视频_v4l2视频采集详解_小袁OVO的博客-CSDN博客
  3. 有道云笔记 (youdao.com)

猜你喜欢

转载自blog.csdn.net/weixin_69035671/article/details/132699680