capture_thread.h
一、这个文件是干嘛的?(一句话先懂)
👉 这是一个用 Qt 的 QThread 写的"视频采集线程类"
作用大致是:
-
从 Linux 摄像头设备
/dev/video1采集视频 -
把采集到的图像转换成
QImage -
通过 Qt 信号 发给界面显示 / 通过 UDP 广播发送
二、头文件结构总览(先有整体)
#ifndef CAPTURE_THREAD_H
#define CAPTURE_THREAD_H
// 各种头文件
// 宏定义
// 结构体
// CaptureThread 类
#endif
这是标准 C/C++ 头文件保护 ,防止重复包含,不用纠结。
三、头文件为什么这么多?(分三类)
① Linux 底层相关(摄像头 / 显存)
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>
👉 用来干这些事:
| 头文件 | 用途 |
|---|---|
fcntl.h |
打开设备文件 |
unistd.h |
read / write |
videodev2.h |
V4L2 摄像头接口 |
sys/mman.h |
内存映射(mmap) |
📌 结论:
这是直接操作 Linux 摄像头设备,不是 OpenCV 那种高级接口。
② 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 常规做法:用 3 个 buffer 做循环缓冲
五、结构体:buffer_info
struct buffer_info {
void *start;
unsigned int length;
};
👉 这是 V4L2 的缓冲区描述结构
| 成员 | 含义 |
|---|---|
start |
缓冲区首地址 |
length |
缓冲区大小 |
📌 一帧图像 = 一个 buffer
六、重点来了:CaptureThread 类
1️⃣ 继承关系
class CaptureThread : public QThread
👉 这是一个线程类
📌 在 Qt 中:
-
QThread= 一个独立执行的线程 -
真正线程代码写在
run()里
2️⃣ Q_OBJECT 是干嘛的?
Q_OBJECT
👉 启用 Qt 的"信号-槽机制"
没有它:
-
signals -
slots全部 不能用
七、signals(信号)------线程往外"喊话"
signals:
void imageReady(QImage);
void sendImage(QImage);
怎么理解?
👉 线程干完活 → 告诉 UI
| 信号 | 用途 |
|---|---|
imageReady(QImage) |
通知界面:有新图像 |
sendImage(QImage) |
用于网络发送 |
📌 信号 ≈ "我这里准备好数据了!"
八、private 成员变量(控制线程行为)
bool startFlag = false;
bool startBroadcast = false;
bool startLocalDisplay = false;
含义一看就懂版:
| 变量 | 作用 |
|---|---|
startFlag |
是否开启采集线程 |
startBroadcast |
是否 UDP 广播 |
startLocalDisplay |
是否本地显示 |
👉 线程内部会根据这些 flag 决定做不做事
线程函数
void run() override;
📌 重点重点重点
这里面一般是:
while(startFlag) {
采集一帧;
转 QImage;
emit imageReady(img);
}
九、public 构造函数(很简单)
CaptureThread(QObject *parent = nullptr) {
Q_UNUSED(parent);
}
👉 只是为了:
-
和 Qt 体系兼容
-
没用
parent,防止警告
十、slots(槽函数)------外部"控制线程"
1️⃣ 开启 / 关闭线程
void setThreadStart(bool start)
逻辑拆开看:
startFlag = start;
-
true→ 开始采集 -
false→ 停止采集if (start) {
if (!this->isRunning())
this->start();
}
👉 只有线程没跑,才启动
else {
this->quit();
}
👉 请求线程退出
2️⃣ 开关广播
void setBroadcast(bool start)
👉 只是 改标志位
3️⃣ 开关本地显示
void setLocalDisplay(bool start)
👉 同样只是 改标志位
十一、整张图一句话理解(非常重要)
UI 线程
👉 调用
setThreadStart(true)👉 采集线程
run()开始跑👉 摄像头采集一帧
👉 转成
QImage👉
emit imageReady(img)👉 UI 显示图像
capture_thread.cpp
好,这个 capture_thread.cpp 我给你按"一步一步跑起来"的顺序讲 ,你把它当成
👉 "Linux 摄像头 + Qt 线程 + 信号槽 + UDP 广播"的完整实战模板 来看。
你不用一次全懂,看完这一遍,至少知道每一段在干嘛。
一、run() 是什么地位?(先定性)
void CaptureThread::run()
👉 这是线程真正执行的函数
在 Qt 里:
-
start()→ 系统自动调用run() -
run()里面就是 子线程逻辑 -
不能写 UI,只能发信号
二、最外层的宏判断(为什么有)
#ifdef linux
#ifndef __arm__
return;
#endif
含义:
| 条件 | 结果 |
|---|---|
| Linux + ARM | ✅ 真正执行 |
| Linux + x86 | ❌ 直接 return |
| Windows | ❌ 不编译 |
📌 原因 :
这是给 正点原子 ARM 开发板 用的,PC 上没有 /dev/video1
三、第一阶段:打开摄像头设备
video_fd = open(VIDEO_DEV, O_RDWR);
-
VIDEO_DEV=/dev/video1 -
成功 → 返回文件描述符
-
失败 → 打印错误并退出线程
👉 Linux 里,摄像头 = 文件
四、第二阶段:设置摄像头参数(格式)
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;
意思一句话:
我要一个 640×480、RGB565 格式的摄像头图像
然后:
ioctl(video_fd, VIDIOC_S_FMT, &fmt);
👉 把参数"塞进"摄像头驱动
五、第三阶段:申请缓冲区(V4L2 核心)
req_bufs.count = 3;
req_bufs.memory = V4L2_MEMORY_MMAP;
👉 告诉驱动:我要 3 个帧缓冲,用 mmap 方式
ioctl(video_fd, VIDIOC_REQBUFS, &req_bufs);
📌 这一步只是"申请",还没拿到地址
六、第四阶段:mmap 映射缓冲区(重点)
ioctl(video_fd, VIDIOC_QUERYBUF, &buf);
👉 查询第 n_buf 个 buffer 的信息
bufs_info[n_buf].start = mmap(...);
👉 把"摄像头内部缓冲区"映射到用户空间
📌 结果是:
摄像头DMA → 内核buffer → mmap → 用户指针
七、第五阶段:把缓冲区"还给驱动"
ioctl(video_fd, VIDIOC_QBUF, &buf);
👉 意思是:
"我准备好了,你可以往这个 buffer 里放数据了"
八、第六阶段:启动摄像头采集
ioctl(video_fd, VIDIOC_STREAMON, &type);
📌 摄像头开始工作
九、第七阶段:真正的"视频循环"(核心)
while (startFlag)
👉 只要外部没关线程,就一直采集
1️⃣ 取出一帧(DQBUF)
ioctl(video_fd, VIDIOC_DQBUF, &buf);
👉 驱动说:这一帧你可以用了
2️⃣ 把 raw 数据 → Qt 的 QImage(关键桥梁)
QImage qImage(
(unsigned char*)bufs_info[n_buf].start,
640,
480,
QImage::Format_RGB16
);
📌 这一步非常重要:
-
摄像头给的是 RGB565
-
Qt 用
QImage::Format_RGB16 -
没有拷贝数据,只是"包装"
3️⃣ 本地显示(信号槽)
if (startLocalDisplay)
emit imageReady(qImage);
👉 子线程 不能直接操作 UI
👉 用 信号通知主线程
emit imageReady → QLabel::setPixmap
4️⃣ UDP 广播(图像发网络)
if (startBroadcast)
步骤拆解:
① 创建 UDP 套接字
QUdpSocket udpSocket;
② QImage → JPEG → QByteArray
qImage.save(&buff, "JPEG");
👉 原始 RGB 太大 → 压缩成 JPEG
③ Base64(防止乱码)
QByteArray base64Byte = byte.toBase64();
④ 广播发送
udpSocket.writeDatagram(
base64Byte.data(),
base64Byte.size(),
QHostAddress::Broadcast,
8888
);
📌 这就是"视频服务器"的核心
5️⃣ 用完后,再放回驱动(QBUF)
ioctl(video_fd, VIDIOC_QBUF, &buf);
👉 形成 循环缓冲
十、线程结束后的清理
munmap(...)
close(video_fd);
👉 释放内存 + 关闭设备
十一、整段代码一句话总结(非常重要)
这个线程做的事就是:
👉 摄像头取一帧
👉 包装成 QImage
👉 要么发给 UI 显示
👉 要么压缩后 UDP 广播
👉 再把 buffer 还给驱动
信号槽 + 线程调用关系图
太好了 👍
这一块一旦想通,Qt 线程 + 信号槽你就算"过关"了。
下面我给你 不用 Qt 术语、用工程思维 来讲,再配一张 "脑中必须有的调用关系图"。
一、先给你一句"定理级结论"
Qt 中:
run()里的代码在 子线程UI 永远只能在主线程
子线程 → UI = 只能靠信号槽
二、三种"角色",先分清(非常重要)
┌────────────┐
│ 主线程(UI) │ ← QLabel / QPushButton / 界面
└─────┬──────┘
│ 信号槽
┌─────▼──────┐
│ CaptureThread │ ← QThread 对象(壳)
└─────┬──────┘
│ run()
┌─────▼──────┐
│ 子线程执行体 │ ← 摄像头采集
└────────────┘
📌 重点 :
CaptureThread 对象 属于 主线程
run() 里的代码运行在 子线程
三、从"按钮点击"开始看完整流程
① 你在 UI 点了「开始采集」
connect(button, &QPushButton::clicked,
captureThread, &CaptureThread::setThreadStart);
👉 发生在:主线程
② 调用槽函数 setThreadStart(true)
void setThreadStart(bool start)
{
startFlag = start;
if (start) {
if (!isRunning())
start(); // 关键!
}
}
📌 重点来了:
| 函数 | 在哪个线程 |
|---|---|
setThreadStart() |
主线程 |
start() |
主线程 |
run() |
子线程 |
③ start() 干了什么?(很多人卡在这)
this->start();
👉 它不会执行 run()
👉 它只是:
向操作系统申请一个新线程
成功后,OS 自动调用 run()
四、run() 才是真正的"子线程世界"
void CaptureThread::run()
{
while (startFlag) {
采集一帧;
emit imageReady(qImage);
}
}
📌 从这里开始:
-
不在 UI 线程
-
不能碰 QLabel / QPushButton
-
只能
emit
五、emit imageReady(qImage) 到底发生了什么?
这是最关键的一步。
1️⃣ 你在 main.cpp / widget.cpp 里一般会这样连
connect(captureThread, &CaptureThread::imageReady,
this, &Widget::updateImage);
📌 连接时:
-
captureThread→ 子线程发信号 -
this (Widget)→ 主线程对象
2️⃣ Qt 自动判断"跨线程连接"
信号发出线程 ≠ 槽函数对象线程
Qt 自动使用:
Qt::QueuedConnection
3️⃣ 这意味着什么?(非常重要)
子线程 emit imageReady
↓
Qt 把 QImage 放进 主线程事件队列
↓
主线程空闲时
↓
调用 updateImage(QImage)
👉 子线程不会等
👉 UI 不会卡死
六、用一张"时间线图"彻底看懂
时间 →
────────────────────────────────────────────
主线程:
[按钮点击]
↓
setThreadStart(true)
↓
start()
↓
(继续跑UI)
子线程:
run()开始
↓
摄像头采集
↓
emit imageReady
↓
(立刻继续采集)
主线程:
Qt事件循环
↓
updateImage()
↓
QLabel显示
📌 关键理解:
emit ≠ 直接调用
emit = "发消息"
七、为什么 UI 不会崩?(考试必问)
❌ 错误做法(会崩):
// 子线程里
ui->label->setPixmap(...); ❌
✅ 正确做法(你现在这个):
emit imageReady(qImage); ✅
👉 UI 操作 100% 在主线程
八、你代码里的三种"信号控制关系"
| 信号 / 槽 | 控制方向 |
|---|---|
setThreadStart() |
UI → 子线程 |
setLocalDisplay() |
UI → 子线程 |
imageReady() |
子线程 → UI |
📌 非常标准的生产级写法
九、用"单片机思维"类比(给你兜底)
如果你把 Qt 当成 MCU:
| Qt | MCU |
|---|---|
| 主线程 | main() |
| 子线程 | 定时器中断 |
| signal | 中断置位 |
| slot | 中断回调 |
👉 中断不能直接改 LCD,只能发标志位
十、你现在应该"通"的 3 个点
如果你点头,说明真的懂了:
1️⃣ CaptureThread 对象在主线程,但 run 在子线程
2️⃣ emit 只是投递消息,不是函数调用
3️⃣ 跨线程信号槽 = 自动队列