C++ Qt 摄像头视频采集实战:V4L2 与多线程
文件概述
本文主要讲解如何使用 Qt 的 QThread 编写视频采集线程类,实现从 Linux 摄像头设备采集视频、转换图像并通过信号发送的功能。
头文件结构
#ifndef CAPTURE_THREAD_H
#define CAPTURE_THREAD_H
// 各种头文件
// 宏定义
// 结构体
// CaptureThread 类
#endif
这是标准的 C/C++ 头文件保护,防止重复包含。
依赖库分类
1. Linux 底层相关(摄像头 / 显存)
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>
#include <sys/mman.h>
| 头文件 | 用途 |
|---|---|
fcntl.h | 打开设备文件 |
unistd.h | read / write |
videodev2.h | V4L2 摄像头接口 |
sys/mman.h | 内存映射(mmap) |
结论:这是直接操作 Linux 摄像头设备,而非使用 OpenCV 等高级接口。
2. Qt 相关(线程 / 图片 / 网络)
#include <QThread>
#include <QImage>
#include <QUdpSocket>
| Qt 类 | 用途 |
|---|---|
QThread | 创建线程 |
QImage | 保存一帧图像 |
QUdpSocket | UDP 广播视频 |
宏定义
#define VIDEO_DEV "/dev/video1"
#define FB_DEV "/dev/fb0"
#define VIDEO_BUFFER_COUNT 3
/dev/video1:摄像头设备路径/dev/fb0:LCD 显存3:摄像头缓冲区数量(V4L2 常规做法,循环缓冲)
结构体 buffer_info
struct buffer_info {
void *start;
unsigned int length;
};
这是 V4L2 的缓冲区描述结构:
start:缓冲区首地址length:缓冲区大小
说明:一帧图像对应一个 buffer。
核心类 CaptureThread
继承关系
class CaptureThread : public QThread
这是一个线程类。在 Qt 中,QThread 代表独立执行的线程,实际线程代码写在 run() 函数中。
Q_OBJECT 宏
Q_OBJECT
启用 Qt 的信号 - 槽机制。没有它,signals 和 slots 关键字无法使用。
Signals(信号)
signals:
void imageReady(QImage);
void sendImage(QImage);
imageReady(QImage):通知界面有新图像数据sendImage(QImage):用于网络发送
含义:线程完成任务后,通过信号告知外部。
Private 成员变量
bool startFlag = false;
bool startBroadcast = false;
bool startLocalDisplay = false;
startFlag:是否开启采集线程startBroadcast:是否进行 UDP 广播startLocalDisplay:是否本地显示
构造函数
CaptureThread(QObject *parent = nullptr) {
Q_UNUSED(parent);
}
仅为了兼容 Qt 体系,未使用 parent 参数以避免警告。
Slots(槽函数)
1. 开启 / 关闭线程
void setThreadStart(bool start)
{
startFlag = start;
if (start) {
if (!this->isRunning())
this->start();
} else {
this->quit();
}
}
true:开始采集(若未运行则启动)false:停止采集(请求退出)
2. 开关广播
void setBroadcast(bool start)
{
// 修改标志位
}
3. 开关本地显示
void setLocalDisplay(bool start)
{
// 修改标志位
}
实现逻辑详解 (capture_thread.cpp)
run() 函数地位
void CaptureThread::run()
这是线程真正执行的入口函数。
start()调用后系统自动执行run()run()内部为子线程逻辑- 注意:不能在此处操作 UI,只能通过信号通信
平台判断
#ifdef linux
#ifndef __arm__
return;
#endif
#endif
- Linux + ARM:执行采集逻辑
- Linux + x86 / Windows:跳过
原因:该代码针对嵌入式 Linux 环境设计,PC 端通常无 /dev/video1 或驱动不同。
第一阶段:打开摄像头设备
video_fd = open(VIDEO_DEV, O_RDWR);
VIDEO_DEV即/dev/video1- 成功返回文件描述符,失败打印错误并退出
第二阶段:设置摄像头参数
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;
ioctl(video_fd, VIDIOC_S_FMT, &fmt);
配置摄像头输出 640×480、RGB565 格式的图像。
第三阶段:申请缓冲区
req_bufs.count = 3;
req_bufs.memory = V4L2_MEMORY_MMAP;
ioctl(video_fd, VIDIOC_REQBUFS, &req_bufs);
向驱动申请 3 个帧缓冲,使用 mmap 方式。
第四阶段:mmap 映射缓冲区
ioctl(video_fd, VIDIOC_QUERYBUF, &buf);
bufs_info[n_buf].start = mmap(...);
将摄像头内部缓冲区映射到用户空间。
第五阶段:提交缓冲区给驱动
ioctl(video_fd, VIDIOC_QBUF, &buf);
告诉驱动可以往该 buffer 写入数据。
第六阶段:启动采集
ioctl(video_fd, VIDIOC_STREAMON, &type);
摄像头开始工作。
第七阶段:视频循环采集
while (startFlag) {
// 1. 取出一帧
ioctl(video_fd, VIDIOC_DQBUF, &buf);
// 2. 转换为 QImage
QImage qImage((unsigned char*)bufs_info[n_buf].start, 640, 480, QImage::Format_RGB16);
// 3. 本地显示
if (startLocalDisplay)
emit imageReady(qImage);
// 4. UDP 广播
if (startBroadcast) {
QUdpSocket udpSocket;
QByteArray byte;
qImage.save(&byte, "JPEG");
QByteArray base64Byte = byte.toBase64();
udpSocket.writeDatagram(base64Byte.data(), base64Byte.size(), QHostAddress::Broadcast, 8888);
}
// 5. 归还缓冲区
ioctl(video_fd, VIDIOC_QBUF, &buf);
}
关键点:
- 摄像头数据为 RGB565,Qt 使用
QImage::Format_RGB16包装,无额外拷贝。 - 子线程不能直接操作 UI,需通过信号通知主线程。
- 原始 RGB 数据较大,先压缩为 JPEG 再 Base64 编码以防乱码。
资源清理
munmap(...);
close(video_fd);
释放内存并关闭设备。
线程与信号槽交互机制
核心原则
Qt 中:
run()里的代码在子线程,UI 永远只能在主线程。子线程 → UI 只能靠信号槽。
角色区分
- 主线程 (UI):负责界面控件(QLabel, QPushButton 等)
- CaptureThread 对象:属于主线程,但
run()运行在子线程 - 子线程执行体:负责摄像头采集逻辑
完整流程
- UI 点击按钮:触发
setThreadStart(true)(主线程) - 启动线程:调用
start()(主线程),操作系统分配新线程 - 执行 run():进入子线程,开始循环采集
- 发送信号:
emit imageReady(qImage)(子线程) - 接收信号:主线程事件队列收到消息,调用槽函数更新 UI
跨线程连接机制
当信号发出线程与槽函数对象线程不同时,Qt 自动使用 Qt::QueuedConnection。
- 子线程
emit不会阻塞,也不会直接调用槽函数 - 数据被放入主线程事件队列
- 主线程空闲时处理槽函数
常见错误与正确做法
❌ 错误:在子线程直接操作 UI
// ui->label->setPixmap(...); // 会导致崩溃
✅ 正确:通过信号传递数据
emit imageReady(qImage); // 安全
总结
CaptureThread对象在主线程,但run()在子线程emit只是投递消息,不是直接函数调用- 跨线程信号槽自动使用队列机制,保证 UI 不卡死

