c++qt开发第三天 摄像头采集视频

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️⃣ 跨线程信号槽 = 自动队列


相关推荐
Dev7z5 分钟前
基于MATLAB图像处理的苹果品质自动分级系统设计与实现
开发语言·图像处理·matlab
源代码•宸10 分钟前
Golang基础语法(go语言指针、go语言方法、go语言接口、go语言断言)
开发语言·经验分享·后端·golang·接口·指针·方法
Bony-10 分钟前
Golang 常用工具
开发语言·后端·golang
Paul_092011 分钟前
golang编程题
开发语言·算法·golang
csbysj202011 分钟前
Go 语言变量作用域
开发语言
牛奔14 分钟前
GVM:Go 版本管理器安装与使用指南
开发语言·后端·golang
百***787516 分钟前
2026 优化版 GPT-5.2 国内稳定调用指南:API 中转实操与成本优化
开发语言·人工智能·python
ChoSeitaku23 分钟前
16.C++入门:list|手撕list|反向迭代器|与vector对比
c++·windows·list
腥臭腐朽的日子熠熠生辉25 分钟前
nest js docker 化全流程
开发语言·javascript·docker
奔跑的web.25 分钟前
Vue 事件系统核心:createInvoker 函数深度解析
开发语言·前端·javascript·vue.js