Linux V4L2驱动开发USB摄像头

发布于:2025-09-02 ⋅ 阅读:(17) ⋅ 点赞:(0)

CameraThread.h

#ifndef CAMERA_THREAD_H  // 如果没有定义 CAMERA_THREAD_H 宏
#define CAMERA_THREAD_H  // 定义 CAMERA_THREAD_H 宏,防止头文件重复包含

#include <QThread>          // 包含Qt线程类
#include <QImage>           // 包含Qt图像处理类
#include <linux/videodev2.h> // 包含Linux视频设备V4L2接口

class CameraThread : public QThread  // 定义CameraThread类,继承自QThread
{
    Q_OBJECT  // Qt宏,启用这个类的信号槽机制和元对象系统

public:
    explicit CameraThread(QObject *parent = nullptr);  // 显式构造函数,防止隐式转换
    ~CameraThread();  // 析构函数

    void stopCapture();  // 停止摄像头采集的公共方法
    void setDevice(const QString &device);  // 设置摄像头设备路径的方法

signals:  // 信号声明区域(用于线程间通信)
    void frameReady(const QImage &image);  // 当一帧图像准备好时发射的信号
    void errorOccurred(const QString &message);  // 当发生错误时发射的信号

protected:
    void run() override;  // 重写QThread的run方法,线程的执行入口点

private:
    struct Buffer {  // 定义缓冲区结构体
        void *start;   // 缓冲区的起始地址指针
        size_t length; // 缓冲区的长度
    };

    bool initCamera();  // 初始化摄像头设备的私有方法
    void cleanup();     // 清理资源的私有方法
    QImage convertYUYVToRGB(const unsigned char *yuyv, int width, int height);  // YUYV转RGB的转换方法

    bool m_running = false;  // 线程运行标志,false表示停止
    QString m_device;        // 摄像头设备路径(如"/dev/video0")
    int m_v4l2Fd = -1;       // V4L2设备的文件描述符,-1表示未打开
    int m_width = 640;       // 图像宽度,默认640像素
    int m_height = 480;      // 图像高度,默认480像素
    Buffer *m_buffers = nullptr;  // 缓冲区数组指针,初始化为空
    unsigned int m_nBuffers = 0;  // 缓冲区数量,初始为0
    unsigned int m_pixelFormat = V4L2_PIX_FMT_YUYV;  // 像素格式,默认YUYV
};

#endif // CAMERA_THREAD_H  // 结束头文件保护

camera_thread.cpp

#include "camera_thread.h"  // 包含自定义头文件
#include <fcntl.h>          // 包含文件控制相关函数(如open)
#include <unistd.h>         // 包含Unix标准函数(如close)
#include <sys/ioctl.h>      // 包含设备I/O控制函数
#include <sys/mman.h>       // 包含内存映射相关函数
#include <string.h>         // 包含字符串处理函数
#include <errno.h>          // 包含错误号定义
#include <QDebug>           // 包含Qt调试输出功能

#define CLEAR(x) memset(&(x), 0, sizeof(x))  // 定义宏,用于清空结构体

CameraThread::CameraThread(QObject *parent) : QThread(parent) {}  // 构造函数实现,调用父类构造函数

CameraThread::~CameraThread() {  // 析构函数实现
    stopCapture();  // 确保线程停止运行
}

void CameraThread::setDevice(const QString &device) {  // 设置设备路径方法实现
    m_device = device;  // 将传入的设备路径保存到成员变量
}

void CameraThread::stopCapture() {  // 停止采集方法实现
    m_running = false;  // 设置运行标志为false,让线程循环退出
    wait();  // 等待线程执行完毕(阻塞调用)
}

bool CameraThread::initCamera() {  // 初始化摄像头方法实现
    // 打开设备
    m_v4l2Fd = open(m_device.toUtf8().constData(), O_RDWR | O_NONBLOCK);  // 以读写和非阻塞模式打开设备
    if (m_v4l2Fd < 0) {  // 如果打开失败
        emit errorOccurred(QString("Cannot open camera: %1").arg(strerror(errno)));  // 发射错误信号
        return false;  // 返回初始化失败
    }

    // 查询设备能力
    struct v4l2_capability cap;  // 定义设备能力结构体
    CLEAR(cap);  // 清空结构体
    if (ioctl(m_v4l2Fd, VIDIOC_QUERYCAP, &cap) < 0) {  // 查询设备能力
        emit errorOccurred(QString("VIDIOC_QUERYCAP failed: %1").arg(strerror(errno)));  // 发射错误信号
        return false;  // 返回初始化失败
    }

    if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {  // 检查设备是否支持视频采集
        emit errorOccurred("Device does not support video capture");  // 发射错误信号
        return false;  // 返回初始化失败
    }

    // 设置格式
    struct v4l2_format fmt;  // 定义视频格式结构体
    CLEAR(fmt);  // 清空结构体
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;  // 设置缓冲区类型为视频采集
    fmt.fmt.pix.width = m_width;  // 设置图像宽度
    fmt.fmt.pix.height = m_height;  // 设置图像高度
    fmt.fmt.pix.pixelformat = m_pixelFormat;  // 设置像素格式
    fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;  // 设置场模式为隔行扫描

    if (ioctl(m_v4l2Fd, VIDIOC_S_FMT, &fmt) < 0) {  // 设置视频格式
        emit errorOccurred(QString("VIDIOC_S_FMT failed: %1").arg(strerror(errno)));  // 发射错误信号
        return false;  // 返回初始化失败
    }

    // 获取实际设置
    m_width = fmt.fmt.pix.width;  // 从设备获取实际设置的宽度
    m_height = fmt.fmt.pix.height;  // 从设备获取实际设置的高度
    m_pixelFormat = fmt.fmt.pix.pixelformat;  // 从设备获取实际设置的像素格式

    // 申请缓冲区
    struct v4l2_requestbuffers req;  // 定义缓冲区请求结构体
    CLEAR(req);  // 清空结构体
    req.count = 4;  // 请求4个缓冲区
    req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;  // 设置缓冲区类型
    req.memory = V4L2_MEMORY_MMAP;  // 设置内存模式为内存映射

    if (ioctl(m_v4l2Fd, VIDIOC_REQBUFS, &req) < 0) {  // 申请缓冲区
        emit errorOccurred(QString("VIDIOC_REQBUFS failed: %1").arg(strerror(errno)));  // 发射错误信号
        return false;  // 返回初始化失败
    }

    m_buffers = new Buffer[req.count];  // 动态分配缓冲区数组
    m_nBuffers = req.count;  // 保存缓冲区数量

    // 内存映射
    for (unsigned int i = 0; i < req.count; ++i) {  // 遍历所有缓冲区
        struct v4l2_buffer buf;  // 定义缓冲区信息结构体
        CLEAR(buf);  // 清空结构体
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;  // 设置缓冲区类型
        buf.memory = V4L2_MEMORY_MMAP;  // 设置内存模式
        buf.index = i;  // 设置缓冲区索引

        if (ioctl(m_v4l2Fd, VIDIOC_QUERYBUF, &buf) < 0) {  // 查询缓冲区信息
            emit errorOccurred(QString("VIDIOC_QUERYBUF failed: %1").arg(strerror(errno)));  // 发射错误信号
            return false;  // 返回初始化失败
        }

        m_buffers[i].length = buf.length;  // 保存缓冲区长度
        m_buffers[i].start = mmap(NULL, buf.length, PROT_READ | PROT_WRITE,  // 内存映射
                                 MAP_SHARED, m_v4l2Fd, buf.m.offset);  // 创建共享内存映射

        if (m_buffers[i].start == MAP_FAILED) {  // 如果内存映射失败
            emit errorOccurred(QString("mmap failed: %1").arg(strerror(errno)));  // 发射错误信号
            return false;  // 返回初始化失败
        }

        if (ioctl(m_v4l2Fd, VIDIOC_QBUF, &buf) < 0) {  // 将缓冲区加入队列
            emit errorOccurred(QString("VIDIOC_QBUF failed: %1").arg(strerror(errno)));  // 发射错误信号
            return false;  // 返回初始化失败
        }
    }

    // 开始采集
    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;  // 定义缓冲区类型枚举
    if (ioctl(m_v4l2Fd, VIDIOC_STREAMON, &type) < 0) {  // 开始视频流采集
        emit errorOccurred(QString("VIDIOC_STREAMON failed: %1").arg(strerror(errno)));  // 发射错误信号
        return false;  // 返回初始化失败
    }

    return true;  // 初始化成功
}

void CameraThread::cleanup() {  // 清理资源方法实现
    if (m_v4l2Fd != -1) {  // 如果文件描述符有效
        enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;  // 定义缓冲区类型
        ioctl(m_v4l2Fd, VIDIOC_STREAMOFF, &type);  // 停止视频流采集

        for (unsigned int i = 0; i < m_nBuffers; ++i) {  // 遍历所有缓冲区
            if (m_buffers[i].start != MAP_FAILED) {  // 如果内存映射有效
                munmap(m_buffers[i].start, m_buffers[i].length);  // 解除内存映射
            }
        }

        close(m_v4l2Fd);  // 关闭设备文件
        m_v4l2Fd = -1;  // 重置文件描述符
    }

    if (m_buffers) {  // 如果缓冲区数组存在
        delete[] m_buffers;  // 释放内存
        m_buffers = nullptr;  // 重置指针
    }
}

QImage CameraThread::convertYUYVToRGB(const unsigned char *yuyv, int width, int height) {  // YUYV转RGB方法实现
    QImage rgb(width, height, QImage::Format_RGB32);  // 创建RGB32格式的QImage

    for (int y = 0; y < height; ++y) {  // 遍历每一行
        for (int x = 0; x < width; x += 2) {  // 每两个像素处理一次(YUYV格式的特性)
            int index = y * width * 2 + x * 2;  // 计算在YUYV数据中的索引位置
            unsigned char y0 = yuyv[index];  // 获取第一个Y分量
            unsigned char u = yuyv[index + 1];  // 获取U分量
            unsigned char y1 = yuyv[index + 2];  // 获取第二个Y分量
            unsigned char v = yuyv[index + 3];  // 获取V分量

            // 转换YUV到RGB的lambda函数
            auto convert = [](unsigned char y, unsigned char u, unsigned char v) {
                int r = y + 1.402 * (v - 128);  // 计算红色分量
                int g = y - 0.344 * (u - 128) - 0.714 * (v - 128);  // 计算绿色分量
                int b = y + 1.772 * (u - 128);  // 计算蓝色分量

                r = qBound(0, r, 255);  // 限制红色在0-255范围内
                g = qBound(0, g, 255);  // 限制绿色在0-255范围内
                b = qBound(0, b, 255);  // 限制蓝色在0-255范围内

                return qRgb(r, g, b);  // 返回RGB颜色值
            };

            rgb.setPixel(x, y, convert(y0, u, v));  // 设置第一个像素的RGB值
            rgb.setPixel(x + 1, y, convert(y1, u, v));  // 设置第二个像素的RGB值
        }
    }

    return rgb;  // 返回转换后的RGB图像
}

void CameraThread::run() {  // 线程运行方法实现
    if (!initCamera()) {  // 初始化摄像头,如果失败
        emit frameReady(QImage());  // 发射空图像信号
        return;  // 退出线程
    }

    m_running = true;  // 设置运行标志为true
    struct v4l2_buffer buf;  // 定义缓冲区结构体
    CLEAR(buf);  // 清空结构体
    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;  // 设置缓冲区类型
    buf.memory = V4L2_MEMORY_MMAP;  // 设置内存模式

    while (m_running) {  // 主循环,直到运行标志为false
        fd_set fds;  // 定义文件描述符集合
        FD_ZERO(&fds);  // 清空文件描述符集合
        FD_SET(m_v4l2Fd, &fds);  // 将摄像头文件描述符加入集合

        struct timeval tv = {0};  // 定义超时时间结构体
        tv.tv_sec = 2;  // 设置超时时间为2秒

        int r = select(m_v4l2Fd + 1, &fds, NULL, NULL, &tv);  // 监听文件描述符状态
        if (r == -1) {  // 如果select出错
            if (errno == EINTR) continue;  // 如果是中断信号,继续循环
            emit errorOccurred(QString("select error: %1").arg(strerror(errno)));  // 发射错误信号
            break;  // 退出循环
        }

        if (r == 0) {  // 如果超时
            emit errorOccurred("Camera timeout");  // 发射超时错误信号
            continue;  // 继续循环
        }

        if (ioctl(m_v4l2Fd, VIDIOC_DQBUF, &buf) < 0) {  // 从缓冲区队列取出一个缓冲区
            emit errorOccurred(QString("VIDIOC_DQBUF failed: %1").arg(strerror(errno)));  // 发射错误信号
            continue;  // 继续循环
        }

        QImage image;  // 定义QImage对象
        if (m_pixelFormat == V4L2_PIX_FMT_YUYV) {  // 如果是YUYV格式
            image = convertYUYVToRGB(static_cast<unsigned char*>(m_buffers[buf.index].start),  // 转换YUYV到RGB
                                    m_width, m_height);  // 传入宽度和高度
        } else if (m_pixelFormat == V4L2_PIX_FMT_RGB565) {  // 如果是RGB565格式
            image = QImage(static_cast<unsigned char*>(m_buffers[buf.index].start),  // 直接创建QImage
                          m_width, m_height, QImage::Format_RGB16);  // 使用RGB16格式
        }

        if (!image.isNull()) {  // 如果图像有效
            emit frameReady(image);  // 发射帧就绪信号
        }

        if (ioctl(m_v4l2Fd, VIDIOC_QBUF, &buf) < 0) {  // 将缓冲区重新放回队列
            emit errorOccurred(QString("VIDIOC_QBUF failed: %1").arg(strerror(errno)));  // 发射错误信号
            break;  // 退出循环
        }
    }

    cleanup();  // 清理资源
}

camera_widget.h

#ifndef CAMERA_WIDGET_H  // 如果没有定义 CAMERA_WIDGET_H 宏
#define CAMERA_WIDGET_H  // 定义 CAMERA_WIDGET_H 宏,防止头文件重复包含

#include <QWidget>          // 包含Qt窗口基础类
#include <QLabel>           // 包含Qt标签控件类
#include "camera_thread.h"  // 包含自定义的摄像头线程类

namespace Ui {  // 命名空间Ui(Qt Designer自动生成)
class camera_Widget;  // 前向声明camera_Widget类
}

class camera_Widget : public QWidget  // 定义camera_Widget类,继承自QWidget
{
    Q_OBJECT  // Qt宏,启用这个类的信号槽机制和元对象系统

public:
    explicit camera_Widget(QWidget *parent = nullptr);  // 显式构造函数,可指定父窗口
    ~camera_Widget();  // 析构函数

signals:  // 信号声明区域
    void showMenu();  // 显示菜单的信号,用于返回主菜单

private slots:  // 私有槽函数区域(用于界面交互)
    void on_pushButton_clicked();  // 打开摄像头按钮的点击槽函数
    void on_pushButton_2_clicked();  // 退出按钮的点击槽函数

private slots:  // 私有槽函数区域(用于摄像头线程通信)
    void updateFrame(const QImage &image);  // 更新图像帧的槽函数
    void handleError(const QString &message);  // 处理错误的槽函数

private:
    Ui::camera_Widget *ui;  // UI界面指针(Qt Designer生成)

private:  // 私有成员变量
    CameraThread *m_cameraThread;  // 摄像头线程对象指针
};

#endif // CAMERA_WIDGET_H  // 结束头文件保护

camera_widget.cpp

#include "camera_widget.h"  // 包含自定义头文件
#include "ui_camera_widget.h"  // 包含Qt Designer生成的UI头文件
#include<QMessageBox>  // 包含Qt消息框类
#include<QDebug>  // 包含Qt调试输出功能

camera_Widget::camera_Widget(QWidget *parent) :  // 构造函数实现
    QWidget(parent),  // 调用父类QWidget的构造函数
    ui(new Ui::camera_Widget)  // 初始化UI指针,创建UI对象
{
    ui->setupUi(this);  // 设置UI界面(Qt Designer生成的界面布局)
    
    m_cameraThread = new CameraThread(this);  // 创建摄像头线程对象,指定父对象为当前窗口
    m_cameraThread->setDevice("/dev/video2");  // 设置摄像头设备节点(根据实际情况修改)

    // 连接信号槽 - 摄像头线程到界面更新
    connect(m_cameraThread, &CameraThread::frameReady,  // 连接frameReady信号
            this, &camera_Widget::updateFrame);  // 到updateFrame槽函数
    
    // 连接信号槽 - 摄像头线程错误处理
    connect(m_cameraThread, &CameraThread::errorOccurred,  // 连接errorOccurred信号
            this, &camera_Widget::handleError);  // 到handleError槽函数

    // 注意:构造函数中没有启动摄像头线程,等待用户点击按钮
}

camera_Widget::~camera_Widget()  // 析构函数实现
{
    delete ui;  // 删除UI对象,释放内存
}

void camera_Widget::updateFrame(const QImage &image) {  // 更新图像帧槽函数实现
    // 缩放图像保持比例
    QPixmap pix = QPixmap::fromImage(image)  // 将QImage转换为QPixmap
                  .scaled(ui->label->size(),  // 缩放到label的大小
                         Qt::KeepAspectRatio,  // 保持宽高比
                         Qt::SmoothTransformation);  // 使用平滑的变换算法

    ui->label->setPixmap(pix);  // 在label上显示缩放后的图像
}

void camera_Widget::handleError(const QString &message) {  // 错误处理槽函数实现
    QMessageBox::critical(this, "Error", message);  // 显示错误消息框(critical级别)
    close();  // 关闭当前窗口
}

void camera_Widget::on_pushButton_clicked()  // 打开摄像头按钮点击槽函数
{
    m_cameraThread->start();  // 启动摄像头线程(开始采集视频)
}

void camera_Widget::on_pushButton_2_clicked()  // 退出按钮点击槽函数
{
    emit showMenu();  // 发射showMenu信号,通知主窗口显示菜单
}

本系统基于Qt框架和Linux V4L2驱动开发了一套完整的USB摄像头实时采集与显示解决方案。采用多线程架构将摄像头采集线程与UI显示线程分离,通过信号槽机制实现线程间安全通信。底层使用V4L2内存映射技术实现零拷贝数据采集,显著提升性能并降低CPU占用。系统支持YUYV到RGB的实时色彩空间转换,具备自适应图像缩放功能确保显示质量。集成了完善的错误处理机制,包含设备检测、ioctl操作监控和异常恢复功能,保证系统稳定运行。提供设备无关接口设计,支持多种摄像头设备的热插拔和动态配置,通过标准化信号槽接口实现采集数据与界面显示的高效协同,最终实现了低延迟、高效率的实时视频采集显示系统。需要自取