前后端分离的图像检测上位机软件知识点拆解

前后端分离的图像检测上位机软件知识点拆解

最近开发了一个前后端分离的图像检测上位机软件。下图是架构简易图。
CalculateServer使用python,其他模块是使用c++进行编码的
在这里插入图片描述
使用到的技术点有

  • 多线程,实现任务的并发执行
  • qt的信号与槽机制,实现事件响应机制
  • QWebSocketServer库和QWebSocket库,实现前后端的通讯
  • 共享内存,实现进程间通讯
  • opencv库,实现摄像头画面捕获
  • 图片相关的操作

多线程

qt框架下实现多线程有两种方式

继承QThread线程类成为线程类

第一种是常见的形式,通过继承QThread线程类成为线程类
在这里插入图片描述
在主线程中创建该类,然后调用start函数
在这里插入图片描述

moveToThread函数

第二种是不常见的形式,使用moveToThread函数,将普通的类移到创建的线程中去执行自己的成员方法、槽函数。

m_captureThread = new QThread();
m_video->moveToThread(m_captureThread);
m_captureThread->start();

这个m_video是一个捕获摄像头画面的类,它并不用继承QThread类。

qt的信号与槽机制

qt信号与槽机制中很有趣的地方是:信号与槽连接的方式,方式有2种

信号与槽的连接方式

  1. 直接连接 Qt::DirectConnection。

发送信号后,立刻执行槽函数。即便发送信号的emit代码所在代码块后续还有代码。

  1. 队列连接 Qt::QueuedConnection

发送信号后,槽函数放入到队列中,先执行完代码块的后续所有代码后,再从队列中取出槽函数执行

如果不显式添加连接类型,默认为Qt::AutoConnection。该类型是什么意思呢?

它表示,当信号发送所在线程与槽函数执行所在线程是同一个线程时,则等于Qt::DirectConnection。
当不在同一个线程时,则等于Qt::QueuedConnection。

注意:

  1. 即便当信号发送所在线程与槽函数执行所在线程不是同一线程,我们依旧可以使用Qt::DirectConnection。
  2. 即便当信号发送所在线程与槽函数执行所在线程是同一线程,我们依旧可以使用Qt::QueuedConnection。
  3. 信号发送者处在A线程,信号接收者处在B线程。
    当使用的是Qt::QueuedConnection连接时,槽函数的执行是在B线程中执行的。
  4. 当使用的是Qt::DirectConnection连接时,槽函数的执行是在A线程中执行的。

往往副线程不会添加事件循环,这会导致副线程的QTimer无法发挥作用,除非QTimer在如下情况被使用。1.QTimer所在的函数是一个槽函数。 2. 所在槽函数连接的信号在另一个有事件循环的线程中。3. 槽函数与信号的连接是直接连接。
满足上述三点条件后,槽函数会在信号发送方执行,QTimer就会被转移到信号发送方的事件循环之中。即可正常执行。
上述操作不推荐。

QTimer的timeout信号相关注意事项

  • QTimer 的工作机制:
    • QTimer 是基于 Qt 的事件循环机制实现的。它通过向线程的事件队列中插入定时器事件来触发定时操作。
    • 当你调用 QTimer::start() 时,Qt 会注册一个定时器事件到该线程的事件循环中。当定时器到期时,事件循环会将定时器事件分发给 QTimer,从而触发其超时信号(timeout())。
  • 线程与事件循环的关系:
    • 在 Qt 中,每个线程可以有一个事件循环,但需要显式启动(通过调用 QThread::exec())。
    • 主线程(GUI 线程)通常会自动运行事件循环(例如,在调用 QApplication::exec() 时启动)。
    • 如果你在非主线程中使用 QTimer,必须确保该线程已经启动了事件循环;否则,定时器事件不会被处理。
  • 如果没有事件循环会发生什么?
    • 如果 QTimer 所在线程没有运行事件循环,即使你调用了 QTimer::start(),定时器事件也不会被分发或处理。
    • 这不会导致程序崩溃或抛出错误,但 QTimer 的超时信号永远不会被触发,因此看起来像是“无效”的。

QWebSocketServer库和QWebSocket库

这两个库在传统的TcpSocket库和TcpSocketServer库的基础上封装了新的特性,比如,通讯协议被提前定义好,无需自己定义。

QWebSocket进行通讯的两种协议:

  1. 基于text类型(其实就是string类型)的消息,传递消息的函数为QWebSocket::sendTextMessage
  2. 基于二进制类型的消息,传递消息的函数为QWebSocket::sendBinaryMessage

客户端如何与服务端构建通讯

服务端:

  1. 初始化QWebSocketServer对象
m_server = new QWebSocketServer("Camera Service", QWebSocketServer::NonSecureMode, nullptr);
  1. 进行监听,这里需要指定在哪个端口监听,监听哪些ip地址传来的连接请求
quint16 port = 8888;
if(!m_server->listen(QHostAddress::Any, port)) {
    qDebug() << "Server could not start!";
} else {
    qDebug() << "Server started";
}
  1. 记录构建连接的众多客户端套接字对象
connect(m_server, &QWebSocketServer::newConnection, this, &CameraService::handleNewConnection);
void CameraService::handleNewConnection()
{
    QWebSocket *clientSocket = m_server->nextPendingConnection(); // 获取即将与服务端连接的客户端网络套接字
    connect(clientSocket, &QWebSocket::disconnected, clientSocket, &QWebSocket::deleteLater);

    // 构建消息发送和消息响应的信号槽机制
    connect(clientSocket, &QWebSocket::textMessageReceived, this, &CameraService::onTextMessageReceived);
    m_clients.append(clientSocket);
    qDebug() << "new client connected.";
}

客户端:

  1. 初始化QWebSocket对象
m_videoSocket = new QWebSocket();
m_calcuSocket = new QWebSocket();
  1. 进行连接,设置ip地址,端口号
QString serverIP = this->m_LE_serverIP->text();
QString videoPort = this->m_LE_videoPort->text();
QString calcuPort = this->m_LE_calcuPort->text();

QString s1 = "ws://" + serverIP + ":" + videoPort;
QString s2 = "ws://" + serverIP + ":" + calcuPort;

m_videoSocket->open(QUrl(s1));
m_calcuSocket->open(QUrl(s2));

共享内存,实现进程间通讯

进程间通讯,有7种

  1. 共享内存
  2. 匿名管道通讯
  3. 命名管道通讯
  4. 消息队列
  5. 套接字通讯
  6. 信号量通讯
  7. 信号通讯

本程序的录像模块与计算模块的通讯是通过共享内存实现的,它的特性是:进程通讯中效率最高的

以下是创建共享内存,访问共享内存的方式。

创建和使用的具体逻辑如下:

先由录像模块创建一个共享内存,将一张图片数据保存到共享内存中,再由计算模块去访问该共享内存,取出图片。

录像模块如何创建共享内存

#include <windows.h>
#define ShareMemoryRoom 140000

// 创建共享内存
m_hMapFile = CreateFileMapping(
        INVALID_HANDLE_VALUE,    // 使用系统分页文件
        NULL,                    // 默认安全性
        PAGE_READWRITE,          // 可读写权限
        0,                       // 最大对象大小(高位)
        ShareMemoryRoom,          // 最大对象大小(低位)
        L"Local\\MySharedMemory"); // 名称

if (m_hMapFile == NULL) {
    qDebug() << "Could not create file mapping object: " << GetLastError();
    return 1;
}
// 映射到当前进程的内存空间
// 映射缓冲区视图
char* m_pBuf = (char*)MapViewOfFile(m_hMapFile, // 映射对象句柄
                                    FILE_MAP_ALL_ACCESS,  // 可读写许可
                                    0,
                                    0,
                                    ShareMemoryRoom);      // 映射大小

if (m_pBuf == NULL) {
    qDebug() << "Could not map view of file: " << GetLastError();
    CloseHandle(m_hMapFile);
    return 1;
}

计算模块如何使用这个已经创建好的共享内存呢?

from multiprocessing import shared_memory

try:
    # 尝试打开已存在的共享内存
    self.existing_shm = shared_memory.SharedMemory(name=self.shm_name[6:], create=False)
except FileNotFoundError:
    print("Shared memory not found.")
    exit(1)

录像模块如何将图片存入该共享内存呢?

  1. 对空间进行定义
// 对共享内存进行定义,一部分空间用来记录图片大小,剩下的空间用来记录图片数据
m_length = (int*)m_pBuf;
*m_length = 0;
m_data = m_pBuf + sizeof(int);
  1. 对空间进行操作
*m_length = imgBase64.size();
memcpy(m_data, imgBase64.data(), imgBase64.size());
  1. 为了保证录像模块在对共享内存进行操作时,不被其他线程操作打断,需要添加锁
#include <mutex>

std::mutex mtx;
std::unique_lock<std::mutex> lock(mtx);
*m_length = imgBase64.size();
memcpy(m_data, imgBase64.data(), imgBase64.size());
lock.unlock(); // 手动解锁
  1. 为了保证录像模块与计算模块的操作同步,需要使用window事件对象以实现同步机制
static int count = 0;
DWORD result;
count++;

// if (count == 100) count = 2;
if (count != 1) {
    result = WaitForSingleObject(m_REvent, 30); // 最多等待读取共享内存数据事件完成信号30ms
    if (result == WAIT_TIMEOUT) {
        qDebug() << "write delay timeout";
    }
}
// 将数据保存到共享内存中
std::mutex mtx;
std::unique_lock<std::mutex> lock(mtx);
*m_length = imgBase64.size();
memcpy(m_data, imgBase64.data(), imgBase64.size());
lock.unlock(); // 手动解锁
// 通知处理图片数据的进程,可以进行读取
SetEvent(m_WEvent); // 发送写操作完成信号

具体操作如下,录像模块创建同步事件对象,计算模块使用同步事件对象。

如下是录像模块创建同步事件对象的代码。

// 创建windows写事件对象用于同步
m_WEvent = CreateEvent(NULL, FALSE, FALSE, L"Local\\MySharedWEvent");
if (m_WEvent == NULL) {
    qDebug() << "Could not create event write object: " << GetLastError();
    UnmapViewOfFile(m_pBuf);
    CloseHandle(m_hMapFile);
    return 1;
}
// 创建windows读事件对象用于同步
m_REvent = CreateEvent(NULL, FALSE, FALSE, L"Local\\MySharedREvent");
if (m_REvent == NULL) {
    qDebug() << "Could not create event read object: " << GetLastError();
    UnmapViewOfFile(m_pBuf);
    CloseHandle(m_hMapFile);
    return 1;
}

计算模块那边如何使用这些事件对象呢?

import win32event

self.r_event = win32event.OpenEvent(0x1F0003, False, self.r_event_name) # 0x1F0003意味着请求对该事件对象具有完全的控制权。
self.w_event = win32event.OpenEvent(0x1F0003, False, self.w_event_name)

接下来介绍计算模块如何对共享内存中的图片数据进行读取?

# 等待写操作事件完成信号
result = win32event.WaitForSingleObject(self.w_event, 100)
if result == win32con.WAIT_TIMEOUT:
    logger.info("等待超时,已经超过100毫秒")
    break
# 读取数据长度
length_b = self.existing_shm.buf[:4].tobytes() # 返回的是bytes类型
# 将bytes类型数据解包为元组,其元素为int类型
length = struct.unpack("i", length_b)[0] # 由于length_b只有四个字节,存储一个int类型的数据,故元组元素个数为1
data = self.existing_shm.buf[4:4+length].tobytes()  # 获取共享内存中的原始数据
# 设置读取完成信号,允许C++程序再次写入
win32event.SetEvent(self.r_event)

这里我忘记对共享内存的读取设置锁操作了

opencv库,实现摄像头画面捕获

摄像头初始化

Video::Video(int index, QObject *parent): QObject(parent), m_isRecording(true)
{
    m_camera.open(index); // 打开第一个摄像头
    m_isRecordingTimer = new QTimer();
    connect(m_isRecordingTimer, &QTimer::timeout, this, &Video::onisRecordingTimerSignal);
    if (!m_camera.isOpened()) {
        this->setCreatVideoCaptureSucc(false);
    }
    else {
        this->setCreatVideoCaptureSucc(true);
        m_camera.set(cv::CAP_PROP_FRAME_WIDTH, 640);   // 设置帧宽为640像素
        m_camera.set(cv::CAP_PROP_FRAME_HEIGHT, 480);  // 设置帧高为480像素
        m_camera.set(cv::CAP_PROP_FPS, 30);            // 设置帧率为30fps

        // 检查是否成功设置了参数
        double width = m_camera.get(cv::CAP_PROP_FRAME_WIDTH);
        double height = m_camera.get(cv::CAP_PROP_FRAME_HEIGHT);
        double brightness = m_camera.get(cv::CAP_PROP_BRIGHTNESS);
        double fps = m_camera.get(cv::CAP_PROP_FPS);
        double focus = m_camera.get(cv::CAP_PROP_FOCUS);
        double exposure = m_camera.get(cv::CAP_PROP_EXPOSURE);
        double saturation = m_camera.get(cv::CAP_PROP_SATURATION);

        std::cout << "Width: " << width << ", Height: " << height
                  << ", Brightness: " << brightness << ", FPS: " << fps
                  << ", Focus: " << focus << ", Exposure: " << exposure
                  << ", Saturation: " << saturation << std::endl;
    }
}

使用cv::CAP_DSHOW作为后端可以更快打开摄像头

m_camera.open(index, cv::CAP_DSHOW);

读取画面,将画面展示在窗口上

void Video::playVideo()
{
    // 创建窗口
    cv::namedWindow("video");
    m_isRecordingTimer->start(60000);
    while(m_isRecording){
        m_camera.read(m_frame);
        cv::imshow("video", m_frame);
        m_videoWriter.write(m_frame);
        int key = cv::waitKey(30); // 等待30毫秒,获取键值
        std::cout << "KeyPressed: " << static_cast<int>(key) << std::endl;
        if (key == 27) break; // 按下Esc键退出
    }
    cv::destroyWindow("video");
    this->onCloseCameraSignal();
}

将图片写入视频文件

# 定义视频文件的输出参数
path = 'video/output_video.mp4'
fps = 30  # 视频帧率(每秒帧数)
frame_size = (myConfig["save_video_width"], myConfig["save_video_height"])  # 帧尺寸 (宽度, 高度)
# 创建 VideoWriter 对象
fourcc = cv2.VideoWriter_fourcc(*'mp4v')  # 编码格式,这里使用 MP4
self.video_writer = cv2.VideoWriter(path, fourcc, fps, frame_size)

self.video_writer.write(self.image) # self.image 是cv2.Mat格式

图片相关的操作

将图片数据转为base64编码的2进制数据

base64编码的好处:

  1. 跨系统兼容性,不同系统对二进制数据的处理可能不一致(如字节序、编码)。Base64作为中间格式,确保数据在不同系统间正确解析。
  2. 避免数据损坏,二进制数据可能包含特殊字符(如\0、换行符),在传输或存储时可能被截断或篡改(如邮件系统处理换行符)。Base64编码后的数据仅包含安全字符,避免此类问题。

如下为代码

QByteArray imgEncoded;

if (!m_camera.read(m_frame))
{
    qDebug() << "lose a frame";
}
if (m_frame.empty()) return;
// 将图片转为jpeg格式图片,并且按照base64编码
std::vector<uchar> buffer;
cv::imencode(".jpg", m_frame, buffer); // 将frame转为jgep格式的数据,存储到buffer中
imgEncoded = QByteArray((const char*)buffer.data(), buffer.size());
QByteArray imgBase64 = imgEncoded.toBase64(); // 按照base64进行编码
emit sentPicSignal(imgBase64);

将base64编码的2进制数据进行解码

m_imageData = QByteArray::fromBase64(imgBase64);

将base64编码的2进制数据变为Mat格式的数据

image_data = base64.b64decode(data)
self.image = cv2.imdecode(np.frombuffer(image_data, np.uint8), cv2.IMREAD_COLOR)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值