项目实践2—全球证件智能识别系统(Qt客户端开发+FastAPI后端人工智能服务开发)
文章目录
- 一、任务概述
- 二、开发“国家代码”快速匹配模块
- 2.1 功能概述与设计思路
- 2.2 数据模型的独立封装
- 2.3 主界面集成与实现
- 2.3.1 更新`mainwindow.h`
- 2.3.2 修改`createToolBars()`方法
- 三、开发图像上传和接收模块
- 3.1 设计前后端交互接口
- 3.1.1 通信协议与数据格式
- 3.1.2 请求(Request)数据结构
- 3.1.3 响应(Response)数据结构
- 3.2 FastAPI后端服务模拟实现
- 3.3 客户端功能集成与实现
- 3.3.1 更新`mainwindow.h`
- 3.3.2 更新 `mainwindow.cpp` 构造函数
- 3.3.3 更新 `createActions()` 方法
- 3.3.4 更新 `createToolBars()` 方法
- 3.3.5 实现图像上传 (`onRecognize` 槽函数)
- 3.3.6 实现异步结果处理 (`onRecognitionFinished` 槽函数)
- 3.3.7 实现图像比对与切换显示
一、任务概述
在上一篇博客《项目实践1—全球证件智能识别系统(Qt客户端开发+FastAPI后端人工智能服务开发)》中,详细介绍了系统的任务背景与开发环境配置,并完成了客户端图像采集模块的基础框架搭建。 具体包括创建Qt主程序、设计并美化用户界面、集成多光谱采集设备SDK,最终实现了高质量的证件图像采集、处理与保存功能。
(上一篇博客网址:https://blog.csdn.net/qianbin3200896/article/details/152807676)
本篇博客将在此基础上,继续推进客户端的功能开发,主要完成以下两个核心模块:
- 开发“国家代码”模块:该模块旨在集成一个国家代码选择功能,允许用户在进行证件识别前,预先指定证件的所属国家。这不仅能优化用户交互,还能为后端识别服务提供关键的先验信息,从而缩小检索范围,显著提升识别的效率和准确性。
- 开发图像上传和接收模块:这是连接客户端与后端人工智能服务的桥梁。将详细阐述如何通过HTTP/HTTPS协议,将客户端采集并预处理好的多光谱图像数据安全、高效地上传至FastAPI后端服务器。同时,还将实现对服务器返回结果的异步接收与解析,并将识别出的证件信息实时呈现在客户端界面上,完成整个识别流程的闭环。
通过本篇内容的学习,将构建起一个功能更加完备、前后端联动的智能证件识别客户端。
二、开发“国家代码”快速匹配模块
2.1 功能概述与设计思路
在实际的证件识别业务场景中,窗口工作人员通常能预先获知待处理证件的所属国家。若能在客户端将这一关键信息(即国家代码)连同证件图像一并提交给后端识别服务,将极大地优化处理流程。后端服务可以利用此先验信息,将识别范围限定在指定国家的证件模板库内,从而有效避免在庞大的全球样本数据库中进行全局检索,其优势体现在:
- 提升效率:显著缩短后端服务的检索与匹配时间。
- 提高准确性:降低因版式相似而导致的跨国误识别概率。
为实现这一目标,需要在客户端主界面集成一个国家代码选择功能。考虑到便捷性与交互效率,该功能将设计为一个位于主工具栏的可编辑下拉列表(QComboBox
)。用户可通过输入国家名称的关键词(如“美”、“中”)进行实时模糊匹配,系统会自动筛选并展示相关的国家条目,供用户快速选择。
国家代码体系将遵循国际标准化组织(ISO)发布的ISO 3166-1标准。具体而言,将采用其“中文名称”和“三位数字代码”的组合,形成如“中国_156”、“美国_840”格式的标准化词条列表。
2.2 数据模型的独立封装
ISO 3166-1所涵盖的国家和地区数量众多,若将其数据列表直接硬编码于mainwindow.cpp
中,将导致该文件代码冗长,不利于后续的阅读与维护。遵循关注点分离的设计原则,更好的做法是将国家代码数据的加载逻辑封装在一个独立的模块中。
为此,创建一个新的C++类 CountryData
,专门负责提供国家代码列表。
首先,在Qt Creator中通过“添加新文件”功能创建一个新的C++类,命名为CountryData
,基类选择QObject
。
代码清单: countrydata.h
#ifndef COUNTRYDATA_H
#define COUNTRYDATA_H#include <QObject>
#include <QStringList>class CountryData : public QObject
{Q_OBJECT
public:explicit CountryData(QObject *parent = nullptr);// 提供一个静态方法,外部可直接调用以获取格式化的国家代码列表static QStringList getCountryList();signals:};#endif // COUNTRYDATA_H
代码清单: countrydata.cpp
#include "countrydata.h"CountryData::CountryData(QObject *parent) : QObject(parent)
{}// 定义并返回包含所有国家及其数字代码的QStringList
// 数据格式为 "中文名称_三位数字代码"
QStringList CountryData::getCountryList()
{// 列表基于ISO 3166-1标准编制// 为保持代码简洁,此处仅列举部分条目作为示例// 实际项目中应包含完整的国家/地区列表return QStringList()<< QStringLiteral("阿富汗_004")<< QStringLiteral("奥兰_248")<< QStringLiteral("阿尔巴尼亚_008")<< QStringLiteral("阿尔及利亚_012")<< QStringLiteral("美属萨摩亚_016")
}
通过将数据获取逻辑封装在CountryData::getCountryList()
静态方法中,MainWindow
或其他任何需要此数据的模块,都可以通过一次简单的函数调用来获取,实现了数据与界面的解耦。
2.3 主界面集成与实现
完成数据模块的封装后,下一步是在主窗口MainWindow
的工具栏中添加QComboBox
控件,并为其配置QCompleter
以实现动态模糊匹配功能。
2.3.1 更新mainwindow.h
首先,在mainwindow.h
头文件中添加QComboBox
和QCompleter
的类前置声明,并定义相应的成员变量。
代码清单: mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H#include <QMainWindow>
// ... (其他原有头文件)
// 新增头文件
#include <QComboBox>
#include <QCompleter>// --- 新增前置声明 ---
QT_BEGIN_NAMESPACE
class QComboBox;
class QCompleter;
QT_END_NAMESPACE
// ... (其他原有内容)class MainWindow : public QMainWindow
{// ... (Q_OBJECT等)
public:// ... (构造与析构函数)
private:// ... (原有私有方法)// ... (原有成员变量)// --- 新增国家代码选择相关控件 ---QComboBox *countryComboBox; // 国家代码选择框
};
#endif // MAINWINDOW_H
2.3.2 修改createToolBars()
方法
接着,在mainwindow.cpp
中对createToolBars()
方法进行扩展。在该方法中,将实例化QComboBox
和QCompleter
,并完成数据加载、匹配器配置以及控件添加到工具栏的全部逻辑。
代码清单: mainwindow.cpp
// 在文件顶部添加必要的头文件
#include <QComboBox>
#include <QCompleter>
#include <QLabel>
#include "countrydata.h" // 引入新创建的数据模块// ... (其他方法保持不变)// 修改createToolBars方法以集成国家代码选择框
void MainWindow::createToolBars()
{// 实例化主工具栏mainToolBar = new QToolBar("主工具栏", this);mainToolBar->setMovable(false);mainToolBar->setIconSize(QSize(48, 48));mainToolBar->setToolButtonStyle(Qt::ToolButtonTextUnderIcon);// 将Actions添加到工具栏mainToolBar->addAction(captureFrontAct);mainToolBar->addAction(captureBackAct);mainToolBar->addSeparator();mainToolBar->addAction(recognizeAct);mainTool-Bar->addAction(saveAct);mainToolBar->addSeparator();mainToolBar->addAction(settingsAct);mainToolBar->addSeparator();// --- 新增:创建并配置国家代码选择模块 ---// 1. 添加一个提示标签mainToolBar->addWidget(new QLabel(QStringLiteral(" 国家/地区: "), this));// 2. 实例化QComboBoxcountryComboBox = new QComboBox(this);countryComboBox->setEditable(true); // 设置为可编辑,允许用户输入countryComboBox->setFixedWidth(200); // 设置一个合适的固定宽度countryComboBox->setPlaceholderText(QStringLiteral("输入关键词匹配..."));// 3. 从CountryData模块获取国家代码列表QStringList countryList = CountryData::getCountryList();countryComboBox->addItems(countryList);// 4. 创建并配置QCompleter实现模糊匹配QCompleter *completer = new QCompleter(countryList, this);completer->setFilterMode(Qt::MatchContains); // 设置过滤模式为“包含”completer->setCaseSensitivity(Qt::CaseInsensitive); // 设置大小写不敏感// 5. 将Completer应用到ComboBoxcountryComboBox->setCompleter(completer);// 6. 将ComboBox添加到工具栏mainToolBar->addWidget(countryComboBox);// 将工具栏添加到主窗口this->addToolBar(mainToolBar);
}
上述代码执行流程如下:
- 在工具栏右侧添加了一个静态文本标签
QLabel
作为引导提示。 - 创建
QComboBox
实例,并将其关键属性editable
设置为true
,这是实现输入匹配的前提。 - 通过调用
CountryData::getCountryList()
静态方法,获取预先定义好的国家代码字符串列表,并将其加载到QComboBox
中。 - 创建一个
QCompleter
实例,并将国家列表作为其数据模型。setFilterMode(Qt::MatchContains)
是实现“模糊匹配”的核心配置,它使得匹配器能够查找包含用户输入子串的所有条目,而非仅仅匹配开头。 - 最后,通过
setCompleter()
方法将配置好的QCompleter
与QComboBox
关联,并把QComboBox
添加到工具栏中。
完成所有代码修改后,重新编译并运行项目。此时,主界面的工具栏右侧将出现一个国家/地区选择框。当焦点在此控件上并开始输入时(例如,输入“国”),下拉列表中会自动筛选并高亮显示所有包含该字符的条目。用户可以继续输入以缩小范围,或直接从列表中选择目标国家。
至此,一个高效、便捷的国家代码快速匹配模块已成功集成到客户端中。后续在开发图像上传功能时,便可以从countryComboBox->currentText()
获取用户选定的国家代码字符串,并将其与图像数据一同发送至后端服务。
三、开发图像上传和接收模块
在完成客户端的图像采集与国家代码选择功能后,接下来需要构建连接客户端与后端人工智能服务的核心桥梁——图像上传与结果接收模块。本章将详细阐述如何利用Qt的网络模块,通过HTTP协议将采集到的多光谱图像及国家代码数据,安全、高效地发送至FastAPI后端服务器。同时,还将实现对服务器返回的识别结果进行异步接收、解析与界面呈现,最终完成整个证件识别流程的闭环。
3.1 设计前后端交互接口
在着手编码之前,首先要明确客户端与服务器之间的通信契约,即API接口规范。清晰、统一的接口设计是保障前后端协同开发顺利进行的关键。
3.1.1 通信协议与数据格式
- 通信协议:采用HTTP/HTTPS协议。客户端作为请求方,向服务器发起POST请求。
- 数据格式:请求体(Request Body)和响应体(Response Body)均采用JSON格式。
- 图像编码:为便于在JSON中传输,所有图像数据均需进行Base64编码。在客户端,将内存中的图像(如
QPixmap
对象)以JPG格式保存到字节数组,再转换为Base64字符串。
3.1.2 请求(Request)数据结构
客户端向服务器发送识别请求时,JSON请求体需包含以下字段:
country_code
(string): 用户选择的国家三位数字代码。image_front_white
(string): 证件正面白光图像的Base64编码字符串。image_front_uv
(string): 证件正面紫外图像的Base64编码字符串。image_front_ir
(string): 证件正面红外图像的Base64编码字符串。image_back_white
(string): 证件反面白光图像的Base64编码字符串。image_back_uv
(string): 证件反面紫外图像的Base64编码字符串。image_back_ir
(string): 证件反面红外图像的Base64编码字符串。
3.1.3 响应(Response)数据结构
服务器处理完毕后,向客户端返回一个JSON响应体,其结构如下:
code
(integer): 状态码。1
表示识别成功,-1
表示识别失败(如未在模板库中找到对应证件)。message
(string): 识别出的文本信息。成功时为证件信息,失败时可为空或错误提示。result_front_white
(string): 识别后返回的正面白光图像(Base64编码)。result_front_uv
(string): 识别后返回的正面紫外图像(Base64编码)。result_back_white
(string): 识别后返回的反面白光图像(Base64编码)。result_back_uv
(string): 识别后返回的反面紫外图像(Base64编码)。
3.2 FastAPI后端服务模拟实现
接下来,搭建一个FastAPI服务器来接收客户端的请求。在本阶段,服务器将不执行实际的AI识别,而是模拟一个耗时3秒的处理流程,并将接收到的四张关键图像(正反面白光、紫外)直接返回,同时附带固定的测试文本。这足以验证整个前后端通信链路的正确性。
首先,确保已安装FastAPI和Uvicorn:
pip install fastapi "uvicorn[standard]" pydantic
代码清单: main.py
import base64
import time
from typing import Optionalimport uvicorn
from fastapi import FastAPI
from pydantic import BaseModel, Field# --- 定义请求体的数据模型 ---
class RecognitionRequest(BaseModel):country_code: str = Field(..., description="国家三位数字代码")image_front_white: str = Field(..., description="正面白光图像 (Base64)")image_front_uv: str = Field(..., description="正面紫外图像 (Base64)")image_front_ir: str = Field(..., description="正面红外图像 (Base64)")image_back_white: str = Field(..., description="反面白光图像 (Base64)")image_back_uv: str = Field(..., description="反面紫外图像 (Base64)")image_back_ir: str = Field(..., description="反面红外图像 (Base64)")# --- 定义响应体的数据模型 ---
class RecognitionResponse(BaseModel):code: intmessage: strresult_front_white: Optional[str] = Noneresult_front_uv: Optional[str] = Noneresult_back_white: Optional[str] = Noneresult_back_uv: Optional[str] = None# --- 创建FastAPI应用实例 ---
app = FastAPI(title="交管证照智能识别系统 - 后端服务",description="用于接收多光谱证照图像并返回识别结果的API",version="1.0.0",
)# --- 定义识别API端点 ---
@app.post("/api/recognize", response_model=RecognitionResponse, summary="证照智能识别接口")
async def recognize_document(request: RecognitionRequest):"""接收证件图像和国家代码,执行识别并返回结果。- **request**: 包含图像和国家代码的请求体。- **return**: 包含识别结果的响应体。"""print(f"接收到来自客户端的请求,国家代码: {request.country_code}")# --- 模拟AI模型处理延迟 ---print("正在进行智能识别...")time.sleep(3)print("识别完成。")# --- 模拟识别成功场景 ---# 在本章节,直接返回接收到的四张关键图像,以验证通信链路# 后续章节将替换为真实的模型处理与图像生成逻辑response_data = {"code": 1,"message": "证照测试","result_front_white": request.image_front_white,"result_front_uv": request.image_front_uv,"result_back_white": request.image_back_white,"result_back_uv": request.image_back_uv,}return RecognitionResponse(**response_data)
将以上代码保存为main.py
,然后在终端中运行 如下命令:
uvicorn main:app --host 0.0.0.0 --port 5000
后端服务即启动。
3.3 客户端功能集成与实现
现在回到Qt客户端,实现数据发送、异步等待、结果接收与界面更新的完整逻辑。
3.3.1 更新mainwindow.h
首先,需要在头文件中添加网络请求、JSON处理和新UI控件所需的相关声明。
代码清单: mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H#include <QMainWindow>
// ... (其他原有头文件)
#include <QComboBox>
#include <QCompleter>// --- 新增头文件 ---
#include <QNetworkAccessManager>
#include <QNetworkReply>// --- 新增前置声明 ---
QT_BEGIN_NAMESPACE
class QComboBox;
class QCompleter;
class QNetworkAccessManager;
class QNetworkReply;
class QProgressDialog;
QT_END_NAMESPACE// ... (其他原有内容)class MainWindow : public QMainWindow
{Q_OBJECTpublic:MainWindow(QWidget *parent = nullptr);~MainWindow();private slots:// ... (原有槽函数)// --- 新增槽函数 ---void onRecognize(); // "智能识别"按钮触发的槽函数void onRecognitionFinished(QNetworkReply *reply); // 网络请求完成后的处理槽函数void onCompareFront(); // "正面比对"按钮触发的槽函数void onCompareBack(); // "反面比对"按钮触发的槽函数void onShowOriginals(); // "原始图像"按钮触发的槽函数private:void createActions();void createToolBars();void createStatusBar();// ... (原有私有方法)// ... (原有成员变量)// 新增图像转换函数QString pixmapToBase64(const QPixmap& pixmap);QPixmap base64ToPixmap(const QString& base64);// --- 新增网络相关成员变量 ---QNetworkAccessManager *networkManager;QProgressDialog *progressDialog;// --- 新增识别与比对相关Action ---QAction *compareFrontAct;QAction *compareBackAct;QAction *showOriginalsAct;// 新增存储服务器返回的4张结果图像QPixmap receivedFrontWhitePixmap;QPixmap receivedFrontUvPixmap;QPixmap receivedBackWhitePixmap;QPixmap receivedBackUvPixmap;
};
#endif // MAINWINDOW_H
这里增加了网络管理器QNetworkAccessManager
、进度对话框QProgressDialog
、新的QAction
以及一系列QPixmap
成员变量,用于在内存中缓存服务器返回的图像。
3.3.2 更新 mainwindow.cpp
构造函数
在MainWindow
的构造函数中,需要对新增的成员变量进行初始化,特别是网络管理器QNetworkAccessManager
和进度对话框QProgressDialog
。
代码清单: mainwindow.cpp
// 在文件顶部添加必要的头文件
#include <QJsonDocument>
#include <QJsonObject>
#include <QByteArray>
#include <QBuffer>
#include <QMessageBox>
#include <QProgressDialog>
#include <QRegularExpression>// MainWindow构造函数
MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow), m_pSamapi(nullptr), m_pCamera(nullptr), m_pCap(nullptr), m_isCameraReady(false), m_pCaptureTimer(nullptr), m_captureStep(0), m_isCapturingFront(true), progressDialog(nullptr)//新增:进度对话框初始化
{// ... (原有UI初始化代码)// 初始化网络访问管理器networkManager = new QNetworkAccessManager(this);// 将网络应答的finished信号连接到处理槽函数connect(networkManager, &QNetworkAccessManager::finished, this, &MainWindow::onRecognitionFinished);connect(recognizeAct, &QAction::triggered, this, &MainWindow::onRecognize);connect(captureFrontAct, &QAction::triggered, this, &MainWindow::onCaptureFrontClicked);connect(captureBackAct, &QAction::triggered, this, &MainWindow::onCaptureBackClicked);connect(showOriginalsAct, &QAction::triggered, this, &MainWindow::onShowOriginals);// ... (其他原有构造函数代码)
}
此部分代码完成了两项关键初始化:
- 创建
QNetworkAccessManager
实例,它是执行所有网络操作的中心。同时,将其finished
信号连接到onRecognitionFinished
槽函数,确保任何网络请求完成后,该槽函数都会被自动调用。 - 创建
QProgressDialog
实例,并进行详细配置。将其设置为模态(Qt::WindowModal
),这将在对话框显示时阻止用户与主窗口进行其他交互。隐藏取消按钮并设置范围为(0, 0),使其呈现为一个无限循环的进度条样式,完美契合等待异步任务的场景。
3.3.3 更新 createActions()
方法
为实现图像比对与切换功能,需要在createActions()
方法中创建对应的QAction
对象,并将它们的triggered
信号连接到相应的处理槽函数。
代码清单: mainwindow.cpp
void MainWindow::createActions()
{// ... (原有Action的创建)// "智能识别" ActionrecognizeAct = new QAction(QIcon(":/icons/recognize.png"), QStringLiteral("智能识别"), this);recognizeAct->setStatusTip(QStringLiteral("上传图像至服务器进行智能识别"));connect(recognizeAct, &QAction::triggered, this, &MainWindow::onRecognize);// --- 新增Action ---// "正面比对" ActioncompareFrontAct = new QAction(QIcon(":/icons/compare_front.png"), QStringLiteral("正面比对"), this);compareFrontAct->setStatusTip(QStringLiteral("显示原始正面图像与识别结果的对比"));compareFrontAct->setEnabled(false); // 初始状态不可用connect(compareFrontAct, &QAction::triggered, this, &MainWindow::onCompareFront);// "反面比对" ActioncompareBackAct = new QAction(QIcon(":/icons/compare_back.png"), QStringLiteral("反面比对"), this);compareBackAct->setStatusTip(QStringLiteral("显示原始反面图像与识别结果的对比"));compareBackAct->setEnabled(false); // 初始状态不可用connect(compareBackAct, &QAction::triggered, this, &MainWindow::onCompareBack);// "原始图像" ActionshowOriginalsAct = new QAction(QIcon(":/icons/originals.png"), QStringLiteral("原始图像"), this);showOriginalsAct->setStatusTip(QStringLiteral("显示采集并校正后的6张原始多光谱图像"));showOriginalsAct->setEnabled(false); // 初始状态不可用connect(showOriginalsAct, &QAction::triggered, this, &MainWindow::onShowOriginals);// ... (其他原有Action的创建)
}
这里新增了“正面比对”、“反面比对”和“原始图像”三个QAction
。重要的是,它们都被默认设置为禁用状态(setEnabled(false)
)。这些功能按钮只应在成功从服务器获取到识别结果后才被激活,以保证交互逻辑的严谨性。
3.3.4 更新 createToolBars()
方法
创建好新的QAction
后,需要将它们添加到主工具栏中,建议放置在“智能识别”按钮之后,形成一个功能组。
代码清单: mainwindow.cpp
void MainWindow::createToolBars()
{// ... (原有工具栏创建与Action添加代码)mainToolBar->addAction(captureBackAct);mainToolBar->addSeparator();mainToolBar->addAction(recognizeAct);// --- 将新增的Action添加到工具栏 ---mainToolBar->addAction(compareFrontAct);mainToolBar->addAction(compareBackAct);mainToolBar->addAction(showOriginalsAct);mainToolBar->addSeparator(); // 在功能组后添加分隔符mainToolBar->addAction(saveAct);mainToolBar->addSeparator();// ... (后续代码)
}
3.3.5 实现图像上传 (onRecognize
槽函数)
此槽函数是整个识别流程的起点,负责数据打包、格式转换和网络请求的发起。它将在用户点击“智能识别”按钮时被调用。
代码清单: mainwindow.cpp
// 图像转Base64的辅助函数
QString MainWindow::pixmapToBase64(const QPixmap& pixmap)
{if (pixmap.isNull()) {return QString();}QByteArray byteArray;QBuffer buffer(&byteArray);buffer.open(QIODevice::WriteOnly);// 将QPixmap以JPG格式保存到内存中的QByteArraypixmap.save(&buffer, "JPG", 85); // 85为JPEG压缩质量// 将字节数组进行Base64编码return QString::fromLatin1(byteArray.toBase64());
}void MainWindow::onRecognize()
{// 检查是否已采集到必须的图像(至少正反面第一张白光图要有)if (m_correctedFrontImages[0].isNull() || m_correctedBackImages[0].isNull()) {QMessageBox::warning(this, QStringLiteral("提示"), QStringLiteral("请先采集证件的正反面图像。"));return;}// 1. 提取国家代码的数字部分QString currentCountry = countryComboBox->currentText();QString countryCode = "156";// 设置默认值为中国QRegularExpression re("_(\\d{3})$"); // 匹配末尾的 "_数字" 部分QRegularExpressionMatch match = re.match(currentCountry);if (match.hasMatch()) {countryCode = match.captured(1); // 提取第一个捕获组,即三位数字}else{QMessageBox::warning(this, QStringLiteral("提示"), QStringLiteral("请先选择证照来源国家/地区。"));return;}// 2. 将所有采集图像转换为Base64字符串QString frontWhiteBase64 = pixmapToBase64(QPixmap::fromImage(m_correctedFrontImages[0]));QString frontUvBase64 = pixmapToBase64(QPixmap::fromImage(m_correctedFrontImages[2]));QString frontIrBase64 = pixmapToBase64(QPixmap::fromImage(m_correctedFrontImages[1]));QString backWhiteBase64 = pixmapToBase64(QPixmap::fromImage(m_correctedBackImages[0]));QString backUvBase64 = pixmapToBase64(QPixmap::fromImage(m_correctedBackImages[2]));QString backIrBase64 = pixmapToBase64(QPixmap::fromImage(m_correctedBackImages[1]));// 3. 构建JSON请求体QJsonObject jsonObject;jsonObject["country_code"] = countryCode;jsonObject["image_front_white"] = frontWhiteBase64;jsonObject["image_front_uv"] = frontUvBase64;jsonObject["image_front_ir"] = frontIrBase64;jsonObject["image_back_white"] = backWhiteBase64;jsonObject["image_back_uv"] = backUvBase64;jsonObject["image_back_ir"] = backIrBase64;QJsonDocument jsonDoc(jsonObject);QByteArray postData = jsonDoc.toJson();// 4. 配置并发送HTTP POST请求// 注意: 请将 "127.0.0.1" 替换为FastAPI服务器实际的局域网IP地址QUrl url("http://127.0.0.1:5000/api/recognize");QNetworkRequest request(url);request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");networkManager->post(request, postData);// 5. 显示等待对话框,锁定UIif (!progressDialog) { // 检查指针是否为nullptr// 如果是第一次使用,则创建并配置实例progressDialog = new QProgressDialog(this);progressDialog->setWindowModality(Qt::WindowModal);progressDialog->setCancelButton(nullptr);progressDialog->setRange(0, 0);progressDialog->setLabelText(QStringLiteral("请等待,识别中..."));progressDialog->setMinimumDuration(0);}progressDialog->show();
}
该函数执行了以下关键步骤:
- 数据校验:确保正反面图像均已采集,否则无法进行识别。
- 国家代码提取:从
QComboBox
的当前文本(例如 “中国_156”)中,利用正则表达式精确提取出三位数字代码 “156”。 - 图像编码:借助一个静态辅助函数
pixmapToBase64
,将6张QPixmap
图像逐一转换为Base64编码的字符串。该辅助函数内部先将图像以JPG格式存入内存字节流,再执行Base64转换。 - 构建JSON:创建一个
QJsonObject
,将国家代码和6个图像的Base64字符串按照API接口规范填入其中,然后转换为字节数组。 - 发送请求:配置
QNetworkRequest
,设置请求URL和Content-Type
头,然后调用networkManager->post()
方法,以异步方式将请求发送出去。 - 用户反馈:请求发出后,立即显示模态的进度对话框,锁定界面,明确告知用户当前正处于处理状态。
3.3.6 实现异步结果处理 (onRecognitionFinished
槽函数)
当服务器返回响应后,QNetworkAccessManager
的finished
信号被触发,从而自动调用此槽函数。该函数是所有响应处理的中心。
代码清单: mainwindow.cpp
// Base64转图像的辅助函数
QPixmap MainWindow::base64ToPixmap(const QString& base64)
{QByteArray byteArray = QByteArray::fromBase64(base64.toLatin1());QPixmap pixmap;pixmap.loadFromData(byteArray);return pixmap;
}void MainWindow::onRecognitionFinished(QNetworkReply *reply)
{// 1. 无论成功与否,首先隐藏等待对话框progressDialog->hide();// 2. 检查网络请求本身是否出错(如网络不通、服务器无响应等)if (reply->error() != QNetworkReply::NoError) {QMessageBox::critical(this, QStringLiteral("网络错误"), QStringLiteral("请求失败: %1").arg(reply->errorString()));reply->deleteLater(); // 必须清理reply对象return;}// 3. 读取并解析服务器返回的JSON响应体QByteArray responseData = reply->readAll();QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData);if (jsonDoc.isNull() || !jsonDoc.isObject()) {QMessageBox::critical(this, QStringLiteral("解析错误"), QStringLiteral("无效的服务器响应格式。"));reply->deleteLater();return;}QJsonObject jsonObj = jsonDoc.object();int code = jsonObj.value("code").toInt();QString message = jsonObj.value("message").toString();// 4. 根据业务状态码(code)执行相应逻辑if (code == 1) { // 识别成功// a. 更新右侧“识别结果”界面的文本resultTextEdit->setPlainText(message);// b. 解码返回的4张图像并存入成员变量receivedFrontWhitePixmap = base64ToPixmap(jsonObj.value("result_front_white").toString());receivedFrontUvPixmap = base64ToPixmap(jsonObj.value("result_front_uv").toString());receivedBackWhitePixmap = base64ToPixmap(jsonObj.value("result_back_white").toString());receivedBackUvPixmap = base64ToPixmap(jsonObj.value("result_back_uv").toString());// c. 激活比对和查看原始图像的按钮compareFrontAct->setEnabled(true);compareBackAct->setEnabled(true);showOriginalsAct->setEnabled(true);// d. 默认显示正面比对结果onCompareFront(); QMessageBox::information(this, QStringLiteral("成功"), QStringLiteral("证件识别成功!"));} else if (code == -1) { // 识别失败(业务层面)QMessageBox::warning(this, QStringLiteral("识别失败"), QStringLiteral("未在样证库中识别到同类证件。"));} else { // 其他未知codeQMessageBox::warning(this, QStringLiteral("未知状态"), QStringLiteral("服务器返回未知状态码: %1").arg(code));}// 5. 安全地释放QNetworkReply对象reply->deleteLater();
}
此函数是响应处理的核心,其逻辑严谨地覆盖了各种情况:
- 关闭反馈:无论请求结果如何,首先隐藏进度对话框,恢复界面交互。
- 网络层错误处理:通过检查
reply->error()
,优先处理连接超时、主机未找到等网络传输层面的问题。 - 数据解析与校验:读取响应数据,使用
QJsonDocument
进行解析,并校验返回的是否为合法的JSON对象。 - 业务逻辑分支:
- 成功 (
code == 1
):将返回的文本信息设置到QTextEdit
中。调用base64ToPixmap
辅助函数将4张图像的Base64字符串解码为QPixmap
对象,并保存在对应的成员变量中。此时,激活之前禁用的三个功能按钮,并主动调用onCompareFront()
,使界面在识别成功后默认展示正面图像的对比视图。 - 失败 (
code == -1
):根据接口约定,弹窗提示用户未找到匹配的证件。
- 成功 (
- 资源回收:在函数末尾必须调用
reply->deleteLater()
,将QNetworkReply
对象交由Qt的事件循环进行安全的延迟释放,这是避免内存泄漏的关键操作。
3.3.7 实现图像比对与切换显示
在 onRecognitionFinished
槽函数中,已经成功接收并解码了服务器返回的四张结果图像,并将它们存储在 MainWindow
的成员变量中。接下来,需要实现三个新的槽函数(onCompareFront
、onCompareBack
和 onShowOriginals
)来控制主界面中心imageLabel
的内容,为用户提供灵活的视图切换功能。
此功能将严格遵循第一篇博客中建立的图像显示方法:通过QPainter
在内存中动态绘制一张包含所有需要显示的子图像的合成图,然后将这张合成图一次性设置给主界面的imageLabel
。
首先,在 mainwindow.h
中为新的私有方法添加声明。
代码清单: mainwindow.h
// ... (之前的 #include 和声明)class MainWindow : public QMainWindow
{// ... (Q_OBJECT, public, private slots, 等)private:// ... (原有的私有方法, 如 createActions, createToolBars)// --- 新增私有方法 ---// 用于将多张小图拼接成一张大图并更新显示void updateCompositeImage(const QList<QPixmap>& pixmaps, int columns);// ... (原有的成员变量)
};
现在,在 mainwindow.cpp
中实现这个新的辅助方法以及三个槽函数。
代码清单: mainwindow.cpp
// 在文件顶部添加必要的头文件
#include <QPainter>
#include <QList>// ... (其他原有函数)/*** @brief 更新主界面的图像显示区域* @param pixmaps 包含所有待显示图像的QPixmap列表* @param columns 指定拼接后的图像布局为多少列* * 此函数会根据传入的图像列表和列数,动态计算布局,* 将所有小图绘制到一张大的背景图上,最后更新到主界面的imageLabel。*/
void MainWindow::updateCompositeImage(const QList<QPixmap>& pixmaps, int columns)
{if (pixmaps.isEmpty() || columns <= 0) {imageLabel->clear(); // 如果没有图像,则清空显示return;}// 获取主显示区域的尺寸,以此为基础创建画布const int displayWidth = imageLabel->width();const int displayHeight = imageLabel->height();// 计算行数int rows = (pixmaps.size() + columns - 1) / columns;// 计算每个子图像显示单元格的尺寸const int cellWidth = displayWidth / columns;const int cellHeight = displayHeight / rows;// 创建一张与显示区域一样大的空白QPixmap作为画布QPixmap compositePixmap(displayWidth, displayHeight);compositePixmap.fill(Qt::darkGray); // 填充背景色// 创建QPainter,用于在画布上绘制QPainter painter(&compositePixmap);painter.setRenderHint(QPainter::Antialiasing, true);painter.setRenderHint(QPainter::SmoothPixmapTransform, true);// 遍历所有待显示的图像for (int i = 0; i < pixmaps.size(); ++i) {// 计算当前图像所在的行和列int row = i / columns;int col = i % columns;// 获取当前子图像QPixmap pixmap = pixmaps.at(i);if (pixmap.isNull()) continue; // 跳过空图像// 将子图像缩放到适应单元格尺寸,保持其宽高比QPixmap scaledPixmap = pixmap.scaled(cellWidth, cellHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation);// 计算绘制的起始坐标(x, y),使其在单元格内居中int x = col * cellWidth + (cellWidth - scaledPixmap.width()) / 2;int y = row * cellHeight + (cellHeight - scaledPixmap.height()) / 2;// 将缩放后的图像绘制到画布的指定位置painter.drawPixmap(x, y, scaledPixmap);}// 绘制结束painter.end();// 将最终合成的图像设置到主界面的imageLabel上imageLabel->setPixmap(compositePixmap);
}// 槽函数:正面比对
void MainWindow::onCompareFront()
{// 设置状态栏提示信息,告知用户当前视图statusBar()->showMessage(tr("当前视图:正面原始图像 vs 识别结果"), 5000);// 准备一个包含4张图像的列表,顺序为:// [左上] 原始正面白光, [右上] 原始正面紫外// [左下] 结果正面白光, [右下] 结果正面紫外QList<QPixmap> pixmapsToShow;pixmapsToShow << QPixmap::fromImage(m_correctedFrontImages[0])<< QPixmap::fromImage(m_correctedFrontImages[2])<< receivedFrontWhitePixmap<< receivedFrontUvPixmap;// 调用辅助函数,以2列的布局更新显示updateCompositeImage(pixmapsToShow, 2);
}// 槽函数:反面比对
void MainWindow::onCompareBack()
{// 设置状态栏提示信息statusBar()->showMessage(tr("当前视图:反面原始图像 vs 识别结果"), 5000);// 准备包含4张反面图像的列表QList<QPixmap> pixmapsToShow;pixmapsToShow << QPixmap::fromImage(m_correctedBackImages[0])<< QPixmap::fromImage(m_correctedBackImages[2])<< receivedBackWhitePixmap<< receivedBackUvPixmap;// 调用辅助函数,以2列的布局更新显示updateCompositeImage(pixmapsToShow, 2);
}// 槽函数:显示全部6张原始图像
void MainWindow::onShowOriginals()
{updateDisplayImage();
}
这段代码的逻辑和优势在于:
- 代码复用:核心的图像拼接与绘制逻辑被封装在
updateCompositeImage
辅助函数中。三个槽函数 (onCompareFront
,onCompareBack
,onShowOriginals
) 的职责变得非常简单:准备好需要显示的QPixmap
列表,然后调用这个辅助函数即可。这极大地减少了重复代码,使逻辑更清晰。 - 动态布局:
updateCompositeImage
函数能够根据传入的图像数量和指定的列数,自动计算行数、单元格大小,并将每张图片居中绘制到其对应的单元格中,实现了灵活的网格布局。 - 风格统一:该实现方式与第一篇博客中显示初始采集图像的方法完全一致,保持了项目代码风格的连贯性和可维护性。
至此,一个连接客户端与后端AI服务的、功能完备的图像上传与结果接收模块已全部开发完成。它不仅实现了核心的数据通信,还提供了异步处理、用户等待反馈以及灵活的结果视图切换功能,为后续集成真实的AI识别算法打下了坚实的基础。