基于V4L2的摄像头捕获

V4L2(Video for Linux2)是Linux内核提供给应用程序访问音、视频驱动的统一接口,支持从摄像头采集数据到编码推流或保存文件等多种应用场景。它具有发现设备和操作设备的能力,通过一系列的回调函数来实现功能,比如设置摄像头的频率、帧频、视频压缩格式和图像参数等等。它也可以用于其他多媒体的开发,如音频开发等。

V4L2提供了一套通用的事件机制用于向用户空间异步传递事件。事件基于文件句柄(v4l2_fh)进行订阅,驱动通过函数将事件排队。用户空间通过系统调用来监听事件就绪,并通过ioctl函数调用相关命令来接收和处理事件。V4L2支持多种设备,提供了以下接口:

1. 视频采集接口(video capture interface):应用设备可以是高频头或者摄像头,V4L2的最初设计就是应用于该功能。
2. 视频输出接口(video output interface):可以驱动计算机的外围视频图像设备,用于输出视频信号。
3. 直接传输视频接口(video overlay interface):可以把视频采集设备采集过来的信号直接输出到输出设备上,不用经过系统CPU。
4. 视频间隔消隐信号接口(VBI interface):它可以使应用可以访问传输消隐期的视频信号。
5. 收音机接口(radio interface):可用来处理从AM或FM高频头设备接收来的音频流。

在Linux系统中,V4L2驱动的Video设备节点文件通常为/dev/videoX(比如/dev/video0)。V4L2驱动对用户空间提供字符设备,主设备号为81,对于视频设备,其次设备号为0-63。对于Radio收音机设备,其次设备号为64-127。对于Teletext广播设备,其次设备号为192-223。对于VBI视频消影设备,其次设备号为224-255。V4L2驱动的Video设备在用户空间可以通过各种ioctl函数调用进行控制,并且可以使用mmap进行内存映射。一般的编程流程如下图所示。

111

在进行V4L2开发中,一般会通过ioctl函数调用以下的命令标志符:
VIDIOC_REQBUFS:分配内存
VIDIOC_QUERYBUF:把VIDIOC_REQBUFS中分配的数据缓存转换成物理地址
VIDIOC_QUERYCAP:查询设备驱动功能
VIDIOC_ENUM_FMT:获取当前驱动支持的视频格式
VIDIOC_S_FMT:设置当前驱动的频捕获格式
VIDIOC_G_FMT:读取当前驱动的频捕获格式
VIDIOC_TRY_FMT:验证当前驱动的显示格式
VIDIOC_CROPCAP:查询驱动的修剪能力
VIDIOC_S_CROP:设置视频信号的边框
VIDIOC_G_CROP:读取视频信号的边框
VIDIOC_QBUF:把数据放回缓存队列
VIDIOC_DQBUF:把数据从缓存中读取出来
VIDIOC_STREAMON:开始视频显示函数
VIDIOC_STREAMOFF:结束视频显示函数
VIDIOC_QUERYSTD:检查当前视频设备支持的标准,例如PAL或NTSC。
以上这些IO调用,有些是必须的,有些是可选择的。

接下来通过野火STM32MP157开发板,运用V4L2实现一个摄像头动态显示和实时捕获照片的例子。所使用的摄像头型号为OV5640,外形如下图所示。

111

该摄像头有500万像素,最大分辨率为2592X1944,支持连续自动对焦,8位并口数据,支持RGB565、CCIR656、YUYV、YCbcR422和JPEG格式输出。也可以选择其他摄像头,但最好能输出RGB格式数据,因为这可以大大降低系统的格式转换运算。

首先可以通过下面的代码来测试一下摄像头所支持的数据格式和分辨率,文件名为test.c。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>
int main(int argc, char *argv[])
{
const char *dev = argc > 1 ? argv[1] : "/dev/video0"; int fd = open(dev, O_RDWR | O_NONBLOCK); if (fd < 0) { perror("open"); return 1; } struct v4l2_fmtdesc fmtdesc; memset(&fmtdesc, 0, sizeof(fmtdesc)); fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; for (fmtdesc.index = 0; ; fmtdesc.index++) { if (ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc) != 0) break; printf("%c%c%c%c : ", fmtdesc.pixelformat & 0xff, (fmtdesc.pixelformat>>8) & 0xff, (fmtdesc.pixelformat>>16) & 0xff, (fmtdesc.pixelformat>>24) & 0xff); struct v4l2_frmsizeenum frmsize; memset(&frmsize, 0, sizeof(frmsize)); frmsize.pixel_format = fmtdesc.pixelformat; for (frmsize.index = 0; ; frmsize.index++) { if (ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &frmsize) != 0) break; if (frmsize.type != V4L2_FRMSIZE_TYPE_DISCRETE) continue; printf("%ux%u ", frmsize.discrete.width, frmsize.discrete.height); /* 帧率 */ struct v4l2_frmivalenum frmival; memset(&frmival, 0, sizeof(frmival)); frmival.pixel_format = fmtdesc.pixelformat; frmival.width = frmsize.discrete.width; frmival.height = frmsize.discrete.height; printf("["); for (frmival.index = 0; ; frmival.index++) { if (ioctl(fd, VIDIOC_ENUM_FRAMEINTERVALS, &frmival) != 0) break; if (frmival.type == V4L2_FRMIVAL_TYPE_DISCRETE) printf("%.2f ", (double)frmival.discrete.denominator / frmival.discrete.numerator); } printf("fps] "); } printf("\n"); } close(fd); return 0; }

把上述代码进行交叉编译,然后在开发板上执行,显示如下。

111

可见OV5640摄像头提供的输出数据格式和分辨率可以有很多种选择,特别是在640X480的分辨率下可以实现60Hz的帧频率。

下面给出本例的代码,文件名为camera.c。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <errno.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/videodev2.h>
#include <linux/fb.h>
#define CAM_DEV         "/dev/video0"        //设备节点文件
#define CAM_WIDTH       640            //视频高度(像素)
#define CAM_HEIGHT      480            //视频宽度(像素)
#define CAM_FORMAT      V4L2_PIX_FMT_RGB565    //视频格式
#define BUFFER_COUNT    4            //缓冲区数量
#define FB_DEV          "/dev/fb0"        //显示设备
static int cam_fd = -1;                //定义摄像头描述符
static int fb_fd = -1;                //定义LCD描述符
static uint8_t *fb_base = NULL;            //定义映射显示空间指针
static size_t fb_size = 0;            //定义Framebuffer显存大小变量
static void *cam_bufs[BUFFER_COUNT] = {NULL};    //定义缓冲区数组
static size_t cam_buf_len[BUFFER_COUNT] = {0};    //定义缓冲区长度数组
static struct fb_var_screeninfo vinfo;        //定义动态可变参数结构体
static struct fb_fix_screeninfo finfo;        //定义静态不可变参数结构体
static unsigned int fb_line_length = 0;        //定义行字节数变量
static volatile sig_atomic_t quit = 0;        //定义信号原子变量
//信号处理
static void sig_handler(int sig)
{
    quit = 1;
}
//直接拷贝RGB565数据到Framebuffer
static void rgb565_to_fb(const uint8_t *src)
{
    uint8_t *fb = fb_base;            //获取映射显示空间指针
    const int src_stride = CAM_WIDTH * 2;    //获取每行像素在内存中占用的字节数
    const int fb_stride = fb_line_length;    //获取每行像素在显存中所占的字节数
    //以下把一帧摄像头图像按行拷贝到Framebuffer的对应位置
    for(int y=0; y<CAM_HEIGHT; y++)
        memcpy(fb+y*fb_stride, src+y*src_stride, src_stride);
}
//V4L2初始化
static int v4l2_init(void)
{
    cam_fd = open(CAM_DEV, O_RDWR);        //打开摄像头设备节点
    if(cam_fd < 0)
    {
        perror("open camera");
        return -1;
    }
    struct v4l2_format fmt;            //定义v4l2的格式配置结构体fmt
    memset(&fmt, 0, sizeof(fmt));        //填充0,清除结构体fmt内容
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;    //配置v4l2为视频捕获方式
    fmt.fmt.pix.width = CAM_WIDTH;        //配置视频宽度
    fmt.fmt.pix.height = CAM_HEIGHT;    //配置视频高度
    fmt.fmt.pix.pixelformat = CAM_FORMAT;    //配置视频格式
    fmt.fmt.pix.field = V4L2_FIELD_ANY;    //自动配置场扫描方式
    if(ioctl(cam_fd, VIDIOC_S_FMT, &fmt) < 0)//使用配置好的fmt进行视频设置
    {
            perror("VIDIOC_S_FMT");
            return -1;
    }
    struct v4l2_requestbuffers req;        //定义v4l2的申请缓冲区结构体reg
    memset(&req, 0, sizeof(req));        //填充0,清除结构体reg内容
    req.count = BUFFER_COUNT;        //配置缓冲区数量
    req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;    //配置v4l2为视频捕获方式
    req.memory = V4L2_MEMORY_MMAP;        //配置缓冲区内存类型为内存映射方式
    if(ioctl(cam_fd, VIDIOC_REQBUFS, &req) < 0)//使用配置好的reg进行缓冲区申请
    {
            perror("VIDIOC_REQBUFS");
            return -1;
    }
    for (unsigned int i=0; i<req.count; i++)        //根据缓冲区数量循环配置各索引项内容
    {
        struct v4l2_buffer buf;                //定义v4l2的缓冲区结构体buf
        memset(&buf, 0, sizeof(buf));            //填充0,清除结构体buf内容
        buf.type   = V4L2_BUF_TYPE_VIDEO_CAPTURE;    //配置v4l2为视频捕获方式
        buf.memory = V4L2_MEMORY_MMAP;            //配置缓冲区内存类型为内存映射方式
        buf.index  = i;                    //配置索引
        if(ioctl(cam_fd, VIDIOC_QUERYBUF, &buf) < 0)//查询缓冲区地址和大小(转换成物理地址)并写入buf中
        {
                perror("VIDIOC_QUERYBUF");
                return -1;
        }
        cam_buf_len[i] = buf.length;            //获取缓冲区实际长度
        //将V4L2内核缓冲区映射到用户空间的虚拟地址
        cam_bufs[i] = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, cam_fd, buf.m.offset);
        if(cam_bufs[i] == MAP_FAILED)
        {
                perror("mmap camera buffer");
                return -1;
        }
        if(ioctl(cam_fd, VIDIOC_QBUF, &buf) < 0)//把配置好的buf数据放回到缓存队列
        {
                perror("VIDIOC_QBUF");
                return -1;
        }
    }                        //循环配置到此结束
    int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;        //定义v4l2类型为视频捕获方式
    if(ioctl(cam_fd, VIDIOC_STREAMON, &type) < 0)    //开启视频捕获
    {
    perror("VIDIOC_STREAMON");
    return -1;
    }
    printf("Camera streaming started!\n");
    return 0;
}
//Framebuffer(LCD)初始化
static int fb_init(void) {
    fb_fd = open(FB_DEV, O_RDWR);            //打开显示设备节点
    if(fb_fd < 0)
    { 
        perror("open framebuffer");
        return -1;
    }
    if(ioctl(fb_fd, FBIOGET_FSCREENINFO, &finfo) < 0)//获取硬件决定且不可更改的固定参数,并写入finfo中
    {
            perror("FBIOGET_FSCREENINFO");
            close(fb_fd);
            return -1;
    }
    if(ioctl(fb_fd, FBIOGET_VSCREENINFO, &vinfo) < 0)//获取可以动态修改的可变参数,并写入vinfo中
    {
            perror("FBIOGET_VSCREENINFO");
            close(fb_fd);
            return -1;
    }
    if(vinfo.bits_per_pixel != 16)            //判断色深是否为16位
    {
            fprintf(stderr, "Only 16bpp framebuffer supported\n");
            close(fb_fd);
            return -1;
    }
    fb_line_length = finfo.line_length;            //获取硬件决定固定参数中的行长度(行字节数)
    fb_size = finfo.smem_len;                //获取Framebuffer显存的总大小
    //若fb_size为0,则计算一个合理的映射大小:行字节数X虚拟高度,保证一定能够覆盖整个屏幕区域
    if(fb_size == 0)
        fb_size = fb_line_length * vinfo.yres_virtual;
    //将整个Framebuffer显存映射到用户进程的虚拟地址空间
    fb_base = mmap(NULL, fb_size, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0);
    if(fb_base == MAP_FAILED)
    {
        perror("mmap framebuffer");
        close(fb_fd);
        return -1;
    }
    return 0;
}
//退出时解除映射
static void cleanup(void) {
    if(cam_fd >= 0)                    //如果已经打开了摄像头设备节点
    {
            int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;    //定义v4l2类型为视频捕获方式
            ioctl(cam_fd, VIDIOC_STREAMOFF, &type);    //关闭视频捕获
    }
    for(int i=0; i<BUFFER_COUNT; i++)
    {
        if(cam_bufs[i] && (cam_bufs[i] != MAP_FAILED))
            munmap(cam_bufs[i], cam_buf_len[i]);//解除摄像头的内存映射
    }
    if(cam_fd >= 0)
        close(cam_fd);                //关闭摄像头设备节点
    if(fb_base && (fb_base != MAP_FAILED))
        munmap(fb_base, fb_size);        //解除Framebuffer的内存映射
    if(fb_fd >= 0)
        close(fb_fd);                //关闭显示设备节点
}
//主函数
int main(void)
{
    struct sigaction sa;                //声明一个结构体,用于描述对某个信号的处理方式。
    memset(&sa, 0, sizeof(sa));            //把结构体全部清零
    sa.sa_handler = sig_handler;            //设置信号处理函数的句柄                
    sigemptyset(&sa.sa_mask);            //清空信号处理期间的阻塞信号集
    //将上述设置应用到SIGINT信号(即终端按下Ctrl+C时产生的信号)
    if(sigaction(SIGINT, &sa, NULL) < 0)
    {
        perror("sigaction");
        return EXIT_FAILURE;
    }
    if (fb_init() < 0)                //调用显示初始化
    { 
        cleanup();
        return EXIT_FAILURE;
    }
    if (v4l2_init() < 0)                //调用v4l2初始化
    { 
        cleanup();
        return EXIT_FAILURE;
    }
    printf("RGB565 preview (Ctrl+C to exit)\n");
    while(!quit)
    {
        struct v4l2_buffer buf;            //定义v4l2的缓冲区结构体buf
        memset(&buf, 0, sizeof(buf));        //填充0,清除结构体buf内容
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;//配置v4l2为视频捕获方式
        buf.memory = V4L2_MEMORY_MMAP;        //配置缓冲区内存类型为内存映射方式
        //把数据从缓存中读取出来
        if(ioctl(cam_fd, VIDIOC_DQBUF, &buf) < 0)
        {
            if (errno == EINTR)
                continue;
            perror("DQBUF");
            break;
        }
        rgb565_to_fb(cam_bufs[buf.index]);        //调用显示
        //把数据放回缓存队列
        if (ioctl(cam_fd, VIDIOC_QBUF, &buf) < 0)
        {
              perror("QBUF");
              break;
        }
    }
    printf("Exiting...\n");
    cleanup();                        //调用解除映射
    return 0;
}

把上述程序交叉编译后在开发板上运行,就可以在LCD上看到摄像头的实时影像了。整个影像清晰流畅,通过top命令观察cup的占用率仅在34%左右, 执行效率较高。

从上述程序中可以看到,使用v4l2的形式基本上是固定的,一般都是先定义一个v4l2的结构体,然后对其进行参数填充,最后通过ioctl以命令(具体见前面的命令标志符)的方式进行设置。所引用的命令一般分为输入型、输出型和双向型三类,具体取决于所传入的结构体里哪些字段是输入、哪些是输出。简单来说,ioctl命令虽然分输入、输出和双向型,但在V4L2中它通常体现在同一个结构体里部分字段是输入、部分字段是输出,而不是整个结构体简单地归类为纯输入或纯输出。

下面总结一下v4l2摄像头捕获的使用步骤,如下。

1、打开设备。摄像头的设备文件一般是“/dev/video0”,打开之前需要确认一下具体名称。
2、查询摄像头支持的格式。这一步就是前面的test.c程序,用来获取摄像头的输出格式、分辨率及帧频等。如果对所使用的摄像头参数比较了解,可以跳过这步。本步聚在实际进行图像捕获时并不需要。
3、设置格式(命令标志为VIDIOC_S_FMT)。这一步需要使用结构体“v4l2_format”,通过它设置像素格式、分辨率、场模式等,驱动会返回实际支持的格式。若设置有问题,实际值可能会与设置值不同,必要时可以把设置好的值再打印出来检查。
4、请求缓冲区(命令标志为VIDIOC_REQBUFS)。这一步需要使用结构体“v4l2_requestbuffers”,通过它向驱动申请若干内存映射(V4L2_MEMORY_MMAP)缓冲区,数量一般 3~4 个。
5、查询缓冲区并映射到用户空间(命令标志为VIDIOC_QUERYBUF)。这一步需要使用结构体“v4l2_buffer”,通过它对每个缓冲区执行查询并获取物理偏移,然后通过mmap函数映射到进程地址空间。
6、将所有缓冲区入队(命令标志为VIDIOC_QBUF)。这一步仍然使用结构体“v4l2_buffer”,在开始视频流传输之前,通过它把全部缓冲区提交给驱动。
7、开启视频流(命令标志为VIDIOC_STREAMON)。这一步直接通过ioctl引用命令即可,不需要结构体,但需要传入一个值为V4L2_BUF_TYPE_VIDEO_CAPTURE的变量。
8、循环采集帧(VIDIOC_DQBUF → 处理 → VIDIOC_QBUF)。这一步循环采集,先出队,获取一帧数据(务必设置 type 和 memory)。再进行处理,直接使用buffers[buf.index]指向的帧数据。最后再入队,在处理完成后,把同一个缓冲区索引放回队列供驱动继续使用。这步仍然使用了结构体“v4l2_buffer”,还引用了出队(VIDIOC_DQBUF)和入队(VIDIOC_QBUF)两条命令。这里简要解释一下,出队就是在第4步中申请的缓冲区中拿出一个已经由驱动填充了视频数据的缓冲区来,处理就是读取这些数据,入队就是把已经读取过数据的缓冲区再放回去让驱动再填充视频数据。这里的队指的是队列,一般是由4个缓冲区组成,进行循环使用。
9、停止视频流(命令标志为VIDIOC_STREAMOFF)。这一步直接通过ioctl引用命令即可,不需要结构体,但需要传入一个值为V4L2_BUF_TYPE_VIDEO_CAPTURE的变量。
10、解除映射并关闭设备。这步需要先调用munmap函数把第5步映射的空间解除映射,然后关闭设备。

在上面的代码中,在main函数中的前10行代码是用来捕获Ctrl+C事件的,如果在实际工程中使用本例代码,这部分可以不要(包括前面定义的sig_handler函数)。

最后通过Qt来实现一个可以照相的实例,在QtCreator中实现,工程名称为camera。

1、界面设计。新建一个带窗体的工程,把窗体设置为800X480大小,去除标题栏、菜单栏和状态栏,并在其中放入一个Widget控件实现图像显示功能(不要使用Lable,效率太低),大小为640X480。再依次放入三个按钮,分别做为“开启”、“关闭”和“照相”功能。设计好的界面如下图所示。

111

2、新建线程头文件,名称为camerathread.h,内容如下。

#ifndef CAMERATHREAD_H
#define CAMERATHREAD_H

#include <QThread>
#include <QImage>
#include <QMutex>
#include <linux/videodev2.h>

class cameraThread : public QThread
{
    Q_OBJECT

public:
    cameraThread();
    void stop();                //外部调用,安全停止线程

signals:
    void frameReady(const QImage &image);//帧捕获就绪信号
    void error(const QString &msg);      //发生错误信号

protected:
    void run() override;

private:
    int camFd = -1;             //摄像头设备描述符
    void* buffers[4];           //缓冲区数组
    size_t bufLengths[4];       //缓冲区长度数组
    bool running = false;       //控制循环标志
};

#endif

3、新建线程程序文件,名称为camerathread.cpp,内容如下。

#include "camerathread.h"
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <cstring>
#include <QDebug>
#include <errno.h>

cameraThread::cameraThread()
{

}

void cameraThread::stop()
{
    running = false;   //通知循环退出
    //唤醒可能阻塞在DQBUF的线程:发送STREAMOFF
    if (camFd >= 0)
    {
        int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        ioctl(camFd, VIDIOC_STREAMOFF, &type);
    }
    wait();  //等待run()完全退出
}

void cameraThread::run()
{
    running = true;
    camFd = open("/dev/video0", O_RDWR);    //打开设备(阻塞模式)
    if (camFd < 0)
    {
        emit error("Cannot open /dev/video0");
        return;
    }
    //设置格式:RGB565, 640x480
    struct v4l2_format fmt;
    std::memset(&fmt, 0, sizeof(fmt));
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt.fmt.pix.width = 640;
    fmt.fmt.pix.height = 480;
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB565;
    fmt.fmt.pix.field = V4L2_FIELD_ANY;
    if (ioctl(camFd, VIDIOC_S_FMT, &fmt) < 0)
    {
        emit error("VIDIOC_S_FMT failed");
        running = false;
        close(camFd);
        return;
    }
    //请求缓冲区
    struct v4l2_requestbuffers req;
    std::memset(&req, 0, sizeof(req));
    req.count = 4;
    req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    req.memory = V4L2_MEMORY_MMAP;
    if (ioctl(camFd, VIDIOC_REQBUFS, &req) < 0 || req.count < 2)
    {
        emit error("VIDIOC_REQBUFS failed");
        running = false;
        close(camFd);
        return;
    }
    //映射并入队
    for (unsigned int i = 0; i < req.count; ++i)
    {
        struct v4l2_buffer buf;
        std::memset(&buf, 0, sizeof(buf));
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        buf.index = i;
        if (ioctl(camFd, VIDIOC_QUERYBUF, &buf) < 0)
        {
            emit error("VIDIOC_QUERYBUF failed");
            running = false;
            close(camFd);
            return;
        }
        bufLengths[i] = buf.length;
        buffers[i] = mmap(nullptr, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, camFd, buf.m.offset);
        if (buffers[i] == MAP_FAILED)
        {
            emit error("mmap failed");
            running = false;
            close(camFd);
            return;
        }
        if (ioctl(camFd, VIDIOC_QBUF, &buf) < 0)
        {
            emit error("VIDIOC_QBUF failed");
            running = false;
            close(camFd);
            return;
        }
    }
    //启动视频流
    int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    if (ioctl(camFd, VIDIOC_STREAMON, &type) < 0)
    {
        emit error("VIDIOC_STREAMON failed");
        running = false;
        close(camFd);
        return;
    }
    //主循环:依据running标志运行
    while (running)
    {
        struct v4l2_buffer buf;
        std::memset(&buf, 0, sizeof(buf));
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        if (ioctl(camFd, VIDIOC_DQBUF, &buf) < 0)
        {
            if (errno == EINTR) continue;   //被信号打断,继续尝试
            if (!running) break;            //如果是停止引起的错误,退出
            emit error("DQBUF failed");
            break;
        }
        QImage img(static_cast<const uchar *>(buffers[buf.index]), 640, 480, QImage::Format_RGB16);
        emit frameReady(img.copy());   //发出独立副本
        if (ioctl(camFd, VIDIOC_QBUF, &buf) < 0)
        {
            emit error("QBUF failed");
            break;
        }
    }
    //清理资源
    type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    ioctl(camFd, VIDIOC_STREAMOFF, &type);
    for (unsigned int i = 0; i < req.count; ++i)
    {
        if (buffers[i] && buffers[i] != MAP_FAILED)
            munmap(buffers[i], bufLengths[i]);
    }
    close(camFd);
    camFd = -1;
    running = false;
}

3、编辑mainwindow.h头文件,完成后的内容如下。

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QImage>
#include "camerathread.h"
#include "imagewidget.h"

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private:
    Ui::MainWindow *ui;
    cameraThread *CameraThread;         //新建摄像头捕获线程
    QImage m_currentImage;              //存储最新一帧(主线程访问,线程安全)
    bool m_cameraRunning = false;       //摄像头是否已开启
    void updateButtonStates();          //更新按钮启用状态

private slots:
    void onFrameReady(const QImage &image); //帧捕获就绪槽函数
    void on_pushButton_clicked();
    void on_pushButton_2_clicked();
    void on_pushButton_3_clicked();
};

#endif // MAINWINDOW_H

4、编辑mainwindow.cpp文件,完成后的内容如下。

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QVBoxLayout>
#include <QFileDialog>
#include <QDateTime>
#include <QMessageBox>

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    CameraThread = new cameraThread;     //创建摄像头线程
    //处理frameReady信号
    connect(CameraThread, &cameraThread::frameReady, this, &MainWindow::onFrameReady);
    //处理error信号
    connect(CameraThread, &cameraThread::error, this, [](const QString &err){qWarning() << "Camera error:" << err;});
    updateButtonStates();   //初始状态"关闭按钮和拍照按钮"禁用
}

MainWindow::~MainWindow()
{
    CameraThread->stop();  //确保线程退出
    delete ui;
}

void MainWindow::onFrameReady(const QImage &image)
{
    m_currentImage = image;              //保存最新帧(拍照用)
    ui->imageWidget->setImage(image);    //显示到控件
}

void MainWindow::on_pushButton_clicked()    //开始按钮
{
    if (m_cameraRunning)
        return;                         //已开启,忽略重复点击
    CameraThread->start();              //开启线程
    m_cameraRunning = true;             //改变状态
    updateButtonStates();               //更新按钮
}

void MainWindow::on_pushButton_2_clicked()  //停止按钮
{
    if (!m_cameraRunning)
        return;                         //未开启,忽略
    CameraThread->stop();               //停止线程
    m_cameraRunning = false;            //改变状态
    updateButtonStates();               //更新按钮
}

void MainWindow::on_pushButton_3_clicked()  //拍照按钮
{
    if (!m_cameraRunning || m_currentImage.isNull())
    {
        QMessageBox::information(this, "提示", "摄像头未开启或无画面");
        return;
    }
    //设置默认文件名
    QString defaultName = "capture_" + QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss") + ".png";
    //调用保存文件对话框
    QString fileName = QFileDialog::getSaveFileName(this, tr("Save Image"), defaultName, tr("Images (*.png *.jpg *.bmp)"));
    if (!fileName.isEmpty())
        m_currentImage.save(fileName);      //保存图片文件
}

void MainWindow::updateButtonStates()
{
    ui->pushButton->setEnabled(!m_cameraRunning);    //摄像头未开启时才能点“开启”
    ui->pushButton_2->setEnabled(m_cameraRunning);   //已开启时才能点“关闭”
    ui->pushButton_3->setEnabled(m_cameraRunning);   //已开启时才能拍照
}

全部完成后对工程进行交叉编译,完成后会生成camera可执行文件,通过NFS挂载到开发板。确认开发板上已接好了7寸液晶屏和OV5640摄像头。执行程序,并点击屏上的“开启”按钮,就可以看到摄像头捕获的实时影像了。点击“拍照”按钮,会弹出保存对话框,使用默认的名称和路径,点击“Save”按钮可保存捕获的照片。

把上面的Qt程序放在后台运行,通过top命令查看,cpu的占用率仅在60%左右,执行效率还不错。若启用STM32MP157的硬件加速功能,还能够大幅降低cpu占用率,可自行研究。v4l2的主要研究在于获取到视频数据后的处理方法,因为数据量巨大,所以对数据的处理比较苛刻,要尽可能的优化。比如本例若选取Lable来显示图像,则cpu的占用率会高达180%,是现在版本的3倍,但运行效果完全一样,由此可以看出优化的必要性。对于后续的应用,可以加入OpenCV,让v4l2获取的数据通过OpneCV进行处理,以解决图像应用的各种需求问题。

posted @ 2026-07-26 22:01  fxzq  阅读(19)  评论(0)    收藏  举报