项目实践1—全球证件智能识别系统(Qt客户端开发+FastAPI后端人工智能服务开发)
文章目录
- 一、任务概述
- 1.1 任务背景
- 1.2 本文任务
- 二、环境准备
- 2.1 Qt客户端开发环境(Windows 10)
- 2.2 FastAPI后端服务开发环境(Ubuntu 22.04)
- 三、开发图像采集模块
- 3.1 创建Qt程序
- 3.1.1 创建程序
- 3.1.2 设置程序窗体标题和初始大小
- 3.1.3 为程序添加图标
- 3.2 界面设计
- 3.2.1 资源与布局准备
- 3.2.2 界面代码实现
- 3.2.3 界面美化
- 3.3 多光谱图像采集
- 3.3.1 集成采集设备SDK库
- 3.3.2 图像采集和显示
- 3.3.3 图像保存
- 3.3.4 优化保存:异步处理与UI响应
- 3.3.5 固定窗体尺寸
- 3.4 曝光强度控制
- 3.4.1 创建参数设置对话框
- 3.4.2 集成INI文件持久化
- 3.4.3 连接对话框与主窗口
- 3.4.4 应用动态曝光值
- 3.5 图像自动矫正和裁剪
- 3.5.1 集成OpenCV库
- 3.5.2 创建图像处理模块
- 3.5.3 实现矫正工作流
- 3.5.4 优化最终显示逻辑
- 3.5.5 优化保存逻辑:优先保存矫正图像
- 3.5.6 优化处理:图像尺寸标准化
- 3.5.7 修复图像显示区域背景
- 四、小结
一、任务概述
1.1 任务背景
证件识别的挑战体现在以下几个维度:
- 归属地识别:准确判断证件所属的国家和地区。
- 信息翻译:将证件上的外文信息准确提取并翻译成中文,这需要先进的多语言光学字符识别(OCR)和机器翻译技术。
- 真伪鉴别:有效辨别证件的真伪,防止伪造证件带来的安全隐患。
为应对这些挑战,一种有效的解决方案是建立一个标准证件样本数据库。利用图像检索技术,将待查询的证件图像与数据库中的样本进行比对,快速检索出最相似的证件模板,从而确定其国家、地区和版本。对于信息的自动化翻译,可以借助先进的图文多模态大模型来高效处理。而在证件真伪辨别方面,则可通过专用的多光谱采集设备,获取证件在紫外光等特殊光源下的图像,并将其与已知的真品样证进行比对,以检验其防伪特征。
1.2 本文任务
本文旨在详述一款智能证件识别系统的开发全过程。该系统依托于一款已有的多光谱图像采集设备,该设备支持在自然光、红外光和紫外光三种模式下进行图像采集,能够获取光线均匀、成像清晰的高质量图像。
整个系统的开发将涵盖以下两个核心部分:
- Qt客户端开发:负责用户交互界面和硬件设备控制。
- FastAPI后端服务开发:部署人工智能算法,提供证件识别和信息处理的核心能力。
本文的撰写旨在为整个开发过程提供一份详尽的记录,同时通过循序渐进的讲解方式,为相关技术人员提供一个可供学习和交流的参考范例。
二、环境准备
本章节将详细介绍系统开发所需的环境配置,包括Qt客户端和FastAPI后端两部分。
2.1 Qt客户端开发环境(Windows 10)
客户端程序选用Qt框架进行开发,旨在实现跨平台的图形用户界面。开发环境搭建于Windows 10操作系统,并确保了对Windows 7及以上版本的良好兼容性。
- Qt版本:选择Qt 5.15.2 (LTS)。此版本稳定性高,且其构建的应用程序能够兼容Windows 7操作系统,有效扩大了软件的适用范围。
- IDE:采用与Qt 5.15.2配套的Qt Creator。该集成开发环境(IDE)对Qt项目有原生支持,同时新版本集成了Copilot等大模型辅助编程工具,能够显著提升开发效率。
- 编译器:使用Microsoft Visual Studio 2019 (MSVC2019)。该编译器与Qt 5.15.2兼容性良好,生成的64-bit程序性能稳定。
2.2 FastAPI后端服务开发环境(Ubuntu 22.04)
后端服务采用FastAPI框架,该框架以其高性能和便捷的API文档生成而著称。开发及部署环境选择在稳定且广泛使用的Linux发行版——Ubuntu 22.04.4 LTS上。
- 操作系统:Ubuntu 22.04.4 LTS
- Python版本:Python 3.8或更高版本。Ubuntu 22.04默认搭载Python 3.10。
三、开发图像采集模块
3.1 创建Qt程序
3.1.1 创建程序
首先创建一个Qt Widgets Application应用程序,项目名称为CardCheck,使用qmake来构建系统,构建套件选择MSVC2019 64bit,如下图所示。
创建完成后按Ctrl+R键运行这个初始项目,检查是否正常。
笔者在初次运行时出现下述错误:
:-1: error: dependent '..\..\..\..\..\..\toolplace\qt\5.15.2\msvc2019_64\include\QtWidgets\QMainWindow' does not exist.
解决方法:
在CardCheck.pro文件末尾添加如下代码:
QMAKE_PROJECT_DEPTH = 0
上述代码将使makefile包含绝对路径,从而可以解决问题。
3.1.2 设置程序窗体标题和初始大小
新创建的Qt程序,其默认窗体标题通常为MainWindow
,且初始尺寸较小。为提升软件的专业性和用户体验,需要对窗体标题进行自定义,并设置其初始显示状态为最大化。
窗体标题的设置可以在主窗口类MainWindow
的构造函数中完成。打开mainwindow.cpp
文件,找到MainWindow::MainWindow(QWidget *parent)
构造函数,在ui->setupUi(this);
之后添加代码。
QWidget
及其所有子类(包括QMainWindow
)都继承了setWindowTitle()
方法,该方法用于设置窗口的标题栏文本。
代码清单:mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{ui->setupUi(this);// 设置主窗体的标题this->setWindowTitle(QStringLiteral("证照智能识别系统"));
}MainWindow::~MainWindow()
{delete ui;
}
接下来实现窗体最大化显示。
程序的入口文件main.cpp
负责创建并显示主窗口实例。默认情况下,Qt应用程序通过调用show()
方法来显示窗口。要使窗口在启动时就以最大化状态显示,需要将show()
方法替换为showMaximized()
。
打开main.cpp
文件,修改主函数main
中的代码。
代码清单:main.cpp
#include "mainwindow.h"#include <QApplication>int main(int argc, char *argv[])
{QApplication a(argc, argv);MainWindow w;// 将w.show()修改为w.showMaximized()// w.show(); // 此为原始代码,将其注释或删除// 调用showMaximized()方法,使窗体在初次显示时便占据整个屏幕w.showMaximized();return a.exec();
}
完成以上两步修改后,按Ctrl+R
重新构建并运行项目。程序启动后,将看到一个最大化的窗口,并且窗口标题已更新为“证照智能识别系统”。
最后还需要做一些工作,让程序能够支持4K等高分屏。
具体实现方法就是启用HiDPI自动缩放
:通过设置Qt::AA_EnableHighDpiScaling应用属性,使Qt能够根据操作系统的显示缩放比例自动调整UI元素(如字体、控件)的大小,从而在4K等高分屏上获得清晰、适中的显示效果。此项设置必须在QApplication实例创建之前完成。
打开main.cpp
文件,修改主函数main
中的代码,在QApplication a(argc, argv)代码前添加该功能:
// 在QApplication实例化前,启用高DPI缩放
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); // 添加该行代码
QApplication a(argc, argv);
3.1.3 为程序添加图标
为应用程序设置一个独特的图标是提升其专业性和品牌辨识度的关键一步。图标会显示在可执行文件、任务栏以及窗口标题栏等位置。本节将详细阐述如何为Qt应用程序添加图标。
该过程主要分为两步:首先为Windows可执行文件(.exe)设置应用程序图标,然后为程序运行时的窗体设置标题栏图标。
1. 准备图标文件并添加到Qt资源系统
首先,需要一个.ico
格式的图标文件。该格式是Windows平台的标准图标格式,能够内含多种不同尺寸的图像,以适应不同显示场景下的缩放需求。准备一个图标文件并将其命名为app.ico
(相关图标文件可以从ICONS8上免费下载),然后将其放置在项目根目录下(与CardCheck.pro
文件同级)。
在Qt Creator中,右键点击项目根目录,选择“添加新文件…”,在弹出的对话框中选择Qt -> Qt Resource File,将资源文件命名为res.qrc
。创建完成后,双击打开res.qrc
文件,点击下方的“添加前缀”按钮,将默认前缀/new/prefix1
修改为/
。接着,单击“添加”按钮,选择“添加文件”,找到并选中项目根目录下的app.ico
文件。
完成上述操作后,Qt Creator会自动在项目配置文件CardCheck.pro
中添加一行代码以引入该资源文件。若未自动添加,可手动添加如下内容:
RESOURCES += \res.qrc
2. 设置应用程序图标
对于Windows平台,应用程序可执行文件的图标通过在.pro
文件中设置RC_ICONS
变量来指定。
打开CardCheck.pro
文件,在末尾添加以下代码:
代码清单:CardCheck.pro
# ... (文件中已有的其他配置)# 为Windows平台设置应用程序图标
RC_ICONS = app.ico
添加此行配置后,重新构建项目,生成的可执行文件CardCheck.exe
的图标便会更新,如下图所示:
3. 设置窗体图标
应用程序图标设置完成后,还需以编程方式为程序主窗口设置左上角的图标。这可以通过调用QWidget::setWindowIcon()
方法实现,并从先前创建的资源文件中加载图标。
打开mainwindow.cpp
文件,在MainWindow
类的构造函数中添加设置窗口图标的代码。
代码清单:mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QIcon> // 需要包含QIcon类的头文件MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{ui->setupUi(this);// 设置主窗体的标题this->setWindowTitle("证照智能识别系统");// 从Qt资源系统中加载图标并将其设置为窗体图标// 路径 ":/app.ico" 指向资源文件res.qrc中定义的app.ico文件this->setWindowIcon(QIcon(":/app.ico"));
}
上述代码中,QIcon
的构造函数参数":/app.ico"
是一个资源路径。其中,冒号:
代表从资源系统加载,/
是之前在res.qrc
文件中定义的前缀,app.ico
是图标文件的别名。
完成所有修改后,按Ctrl+R
重新运行程序。此时,应用程序的窗口标题栏左上角和Windows任务栏中都将显示新设置的图标。
至此,程序的图标配置工作全部完成。
3.2 界面设计
一个专业且直观的用户界面(UI)是系统易用性的基础。本节将详细介绍如何使用Qt Widgets通过纯代码方式设计“证照智能识别系统”的主界面。纯代码布局相较于UI Designer(.ui文件)拖拽控件的方式,具有更强的灵活性和可维护性,尤其在复杂或动态布局场景下优势更为明显。
主窗体将被划分为四个核心区域:
- 工具栏区域:位于顶部,用于放置常用功能按钮,如采集、识别、保存等。
- 图像展示区域:位于主窗体左侧,用于显示采集到的证件图像。
- 识别结果区域:位于主窗体右侧,用于展示OCR识别和翻译后的文本信息。
- 状态栏区域:位于底部,用于向用户反馈当前系统状态或操作提示。
为实现灵活的布局管理,图像展示区和识别结果区将通过一个可拖拽的分割条(QSplitter
)进行分隔,允许用户根据需要自由调整两个区域的相对宽度。整体风格将借鉴微软的Ribbon设计,采用大图标配以文字说明的工具栏按钮,提升视觉清晰度和可操作性。
3.2.1 资源与布局准备
在开始编码前,首先需要准备好工具栏按钮所需的图标文件,并通过Qt资源系统(.qrc
)进行管理。
1. 创建图标文件夹并添加资源
在项目根目录下创建一个名为icons
的文件夹,用于存放所有UI图标文件。为工具栏的五个按钮准备对应的PNG格式图标,并按功能命名,例如:
capture_front.png
(采集正面)capture_back.png
(采集反面)recognize.png
(识别)save.png
(保存)settings.png
(参数设置)
接下来,将这些图标文件添加到在3.1.3节中创建的res.qrc
资源文件中。双击res.qrc
,找到前缀/
,然后将icons
文件夹下的所有PNG文件添加到该前缀下,如下图所示:
2. 规划主窗口类
为了使代码结构清晰,主界面的初始化逻辑将在MainWindow
类的构造函数中完成,并通过独立的私有方法来创建不同的UI组件。首先,在mainwindow.h
头文件中声明这些即将用于UI创建的私有方法和成员变量。
代码清单:mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H#include <QMainWindow>// 前置声明,减少不必要的头文件包含
QT_BEGIN_NAMESPACE
class QAction;
class QToolBar;
class QLabel;
class QTextEdit;
QT_END_NAMESPACEQT_BEGIN_NAMESPACE
namespace Ui {
class MainWindow;
}
QT_END_NAMESPACEclass MainWindow : public QMainWindow
{Q_OBJECTpublic:MainWindow(QWidget *parent = nullptr);~MainWindow();private:// 初始化UI的私有方法void createActions(); // 创建所有QActionvoid createToolBars(); // 创建工具栏void createCentralWidget(); // 创建中心部件(图像展示区和识别结果区)void createStatusBar(); // 创建状态栏// 工具栏QToolBar *mainToolBar;// 中心部件的控件QLabel *imageLabel; // 图像展示标签QTextEdit *resultTextEdit; // 识别结果文本框// 动作(Actions)QAction *captureFrontAct;QAction *captureBackAct;QAction *recognizeAct;QAction *saveAct;QAction *settingsAct;private:Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H
3.2.2 界面代码实现
完成头文件的规划后,接下来在mainwindow.cpp
中实现具体的UI布局代码。
1. 主构造函数
MainWindow
的构造函数将作为入口,依次调用各个私有方法来完成界面的初始化。
代码清单:mainwindow.cpp
(构造函数部分)
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QIcon>
#include <QToolBar>
#include <QAction>
#include <QSplitter>
#include <QLabel>
#include <QTextEdit>
#include <QStatusBar>MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{ui->setupUi(this);// 设置主窗体的标题this->setWindowTitle(QStringLiteral("证照智能识别系统"));// 从Qt资源系统中加载图标并将其设置为窗体图标this->setWindowIcon(QIcon(":/app.ico"));// --- 按顺序创建UI组件 ---createActions(); // 首先创建ActionscreateToolBars(); // 然后创建工具栏并添加ActionscreateCentralWidget(); // 创建中心布局createStatusBar(); // 创建状态栏
}MainWindow::~MainWindow()
{delete ui;
}// 此处将接续实现createActions, createToolBars等方法...
2. 创建工具栏 (QToolBar
)
工具栏将容纳系统的主要操作按钮。为了实现Ribbon风格,需要设置较大的图标和“图下文字”的按钮样式。
代码清单:mainwindow.cpp
(Actions和ToolBar创建部分)
// 创建所有QAction
void MainWindow::createActions()
{// 采集正面captureFrontAct = new QAction(QIcon(":/icons/capture_front.png"), QStringLiteral("采集正面"), this);captureFrontAct->setStatusTip(QStringLiteral("采集证件的正面图像")); // 设置状态栏提示// 采集反面captureBackAct = new QAction(QIcon(":/icons/capture_back.png"), QStringLiteral("采集反面"), this);captureBackAct->setStatusTip(QStringLiteral("采集证件的反面图像"));// 识别recognizeAct = new QAction(QIcon(":/icons/recognize.png"), QStringLiteral("识别"), this);recognizeAct->setStatusTip(QStringLiteral("识别当前采集的证件图像"));// 保存saveAct = new QAction(QIcon(":/icons/save.png"), QStringLiteral("保存"), this);saveAct->setStatusTip(QStringLiteral("保存采集的图像和识别结果"));// 参数设置settingsAct = new QAction(QIcon(":/icons/settings.png"), QStringLiteral("参数设置"), this);settingsAct->setStatusTip(QStringLiteral("配置系统相关参数"));
}// 创建工具栏
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);mainToolBar->addAction(saveAct);mainToolBar->addSeparator();mainToolBar->addAction(settingsAct);// 将工具栏添加到主窗口this->addToolBar(mainToolBar);
}
3. 创建中心部件 (QSplitter
)
主界面的核心区域由一个水平QSplitter
构成,左侧放置用于显示图像的QLabel
,右侧放置用于显示结果的QTextEdit
。QSplitter
允许用户通过拖动分隔条自由调整二者的宽度。
代码清单:mainwindow.cpp
(CentralWidget创建部分)
// 创建中心部件
void MainWindow::createCentralWidget()
{// 创建一个水平分割器QSplitter *mainSplitter = new QSplitter(Qt::Horizontal, this);// 创建左侧图像展示区(使用QLabel作为占位符)imageLabel = new QLabel(QStringLiteral("图像展示区域"), mainSplitter);imageLabel->setAlignment(Qt::AlignCenter); // 居中对齐imageLabel->setFrameShape(QFrame::StyledPanel); // 设置边框样式以便观察// 创建右侧识别结果展示区resultTextEdit = new QTextEdit(mainSplitter);resultTextEdit->setPlaceholderText(QStringLiteral("识别结果将在此处显示..."));resultTextEdit->setReadOnly(true); // 设置为只读// 将控件添加到分割器中mainSplitter->addWidget(imageLabel);mainSplitter->addWidget(resultTextEdit);// 设置初始宽度比例为80:20// QSplitter会根据这个列表中的值按比例分配初始宽度mainSplitter->setSizes(QList<int>() << 800 << 200);// 将分割器设置为主窗口的中心部件this->setCentralWidget(mainSplitter);
}
4. 创建状态栏 (QStatusBar
)
状态栏位于窗口底部,用于显示简短的提示信息。
代码清单:mainwindow.cpp
(StatusBar创建部分)
// 创建状态栏
void MainWindow::createStatusBar()
{// QMainWindow默认包含一个状态栏,可通过statusBar()方法获取statusBar()->showMessage(QStringLiteral("系统准备就绪"), 5000); // 显示一条5秒后自动消失的消息
}
将以上所有代码片段整合到mainwindow.cpp
后,按Ctrl+R
重新编译并运行项目。此时,应用程序的界面应如下图所示:
3.2.3 界面美化
在完成基础界面布局后,为了提升系统的视觉专业性和用户体验,本节将采用Qt样式表(Qt Style Sheets, QSS)对界面进行美化。QSS是Qt框架提供的一种强大的UI定制机制,其语法和概念与网页开发中的CSS高度相似,允许开发者通过文本规则来定义控件的外观,从而实现UI与业务逻辑的解耦。
本次美化将以清新、专业的天空蓝为主题色调,通过渐变、边框和颜色搭配,优化工具栏、状态栏、文本编辑区等核心组件的视觉效果。
1. 创建并编辑样式表文件
首先,将所有样式规则集中存放在一个独立的.qss
文件中,便于统一管理和维护。
在Qt Creator的项目根目录下,用普通编辑器创建一个新文件,并将其命名为style.qss
。创建完成后,将以下QSS代码完整地复制到style.qss
文件中。
代码清单:style.qss
/* 全局字体和前景颜色设置 */
QWidget {font-family: "Microsoft YaHei"; /* 推荐使用跨平台且显示效果良好的字体 */color: #333333;
}/* 主窗口背景 */
QMainWindow {background-color: #F0F8FF; /* 爱丽丝蓝,作为基础背景色 */
}/* 工具栏样式,采用从上到下由白到浅蓝的线性渐变 */
QToolBar {background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1, stop:0 rgba(255, 255, 255, 255), stop:1 rgba(220, 230, 240, 255));border-bottom: 1px solid #C0C8D0; /* 底部添加一条分割线 */padding: 2px;
}/* 工具栏按钮样式 */
QToolBar QToolButton {background-color: transparent; /* 按钮背景透明 */border: 1px solid transparent;border-radius: 4px;margin: 2px;padding: 4px;
}/* 鼠标悬停在工具栏按钮上时的效果 */
QToolBar QToolButton:hover {background-color: rgba(135, 206, 250, 100); /* 淡天蓝色背景 */border: 1px solid #87CEFA; /* 天蓝色边框 */
}/* 鼠标按下工具栏按钮时的效果 */
QToolBar QToolButton:pressed {background-color: rgba(135, 206, 250, 150);
}/* 状态栏样式,采用从上到下由浅蓝到白的线性渐变 */
QStatusBar {background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1, stop:0 rgba(220, 230, 240, 255), stop:1 rgba(255, 255, 255, 255));border-top: 1px solid #C0C8D0; /* 顶部添加分割线 */border-top: 1px solid #D0D8D0; /* 顶部添加一条分割线 */
}/* 状态栏消息文本样式 */
QStatusBar::item {border: none;
}/* 图像显示区域标签样式 */
QLabel {background-color: #FFFFFF; /* 白色背景 */border: 1px solid #C0C8D0;border-radius: 4px;
}/* 识别结果文本框样式 */
QTextEdit {background-color: #FFFFFF;border: 1px solid #C0C8D0;border-radius: 4px;padding: 5px;
}/* 分割条手柄样式 */
QSplitter::handle {background-color: #E6F2FF; /* 淡蓝色背景 */border: 1px solid #C0C8D0;border-radius: 2px;margin: 1px 0;
}QSplitter::handle:horizontal {width: 5px; /* 水平分割条宽度 */
}QSplitter::handle:vertical {height: 5px; /* 垂直分割条高度 */
}QSplitter::handle:hover {background-color: #B0E0E6; /* 鼠标悬停时颜色加深 */
}
2. 将样式表文件添加到资源系统
为了确保.qss
文件能够被正确打包到最终的可执行文件中,需要将其添加到Qt资源系统(res.qrc
)。
在Qt Creator中,双击打开res.qrc
文件。选中前缀/
,然后点击下方的“添加文件”按钮,在弹出的文件对话框中选择刚刚创建的style.qss
文件。添加完成后,style.qss
会出现在资源列表中,其资源路径为:/style.qss
。
3. 在应用程序中加载样式表
最后一步是在程序启动时加载并应用style.qss
文件中的样式。这一操作需要在mainwindow.cpp
文件的初始化代码中完成。
打开mainwindow.cpp
文件,添加QFile
和QString
的头文件包含,并编写加载逻辑。
代码清单:mainwindow.cpp
MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{ui->setupUi(this);// 设置主窗体的标题this->setWindowTitle(QStringLiteral("证照智能识别系统"));// 从Qt资源系统中加载图标并将其设置为窗体图标this->setWindowIcon(QIcon(":/app.ico"));// --- 按顺序创建UI组件 ---createActions(); // 首先创建ActionscreateToolBars(); // 然后创建工具栏并添加ActionscreateCentralWidget(); // 创建中心布局createStatusBar(); // 创建状态栏// 优化界面风格QFile file(":/style.qss");file.open(QFile::ReadOnly);QString styleSheet = tr(file.readAll());qApp->setStyleSheet(styleSheet);file.close();
}
完成以上步骤后,按Ctrl+R
重新编译并运行项目。效果如下图所示:
至此,界面的基础设计与美化工作全部完成。
3.3 多光谱图像采集
3.3.1 集成采集设备SDK库
系统的核心功能之一是获取高质量、多光谱的证件图像。这依赖于一款专用的证照图像采集设备,如下图所示,该设备通过USB接口与计算机连接,内部集成了一个高清摄像头和三组不同的光源:白光(RGB)、红外光(IR)和紫外光(UVC)。其工作原理是:通过软件指令控制,依次点亮不同的光源,并在每种光源下进行一次拍照。这样,一次完整的采集流程可以获得三张图像,分别对应证件在自然光、红外光和紫外光下的成像,为后续的真伪鉴别和信息识别提供了丰富的数据。
要驱动该硬件设备,必须将其厂商提供的软件开发工具包(SDK)集成到Qt项目中。本节将详细阐述这一集成过程。
1. 准备SDK文件
首先,在项目根目录下(与CardCheck.pro
文件同级)创建一个名为jint
的文件夹。接着,将SDK提供的所有文件,包括头文件(.h
)、源文件(.c
, .cpp
)和库文件(.lib
),全部复制到这个新建的jint
文件夹中。文件清单如下:
hid.c
hidapi.h
jint.h
jint.lib
samapi.cpp
samapi.h
将这些文件整理到统一的目录下,便于项目进行统一管理和路径配置。项目目录结构将如下所示:
CardCheck/
├── CardCheck.pro
├── main.cpp
├── mainwindow.cpp
├── mainwindow.h
├── ...
└── jint/├── hid.c├── hidapi.h├── jint.h├── jint.lib├── samapi.cpp└── samapi.h
2. 配置项目文件 (.pro)
为使Qt项目能够识别并正确编译、链接SDK,必须在项目配置文件CardCheck.pro
中添加相应的配置,以指明SDK的头文件路径、源文件路径以及需要链接的库文件。
打开CardCheck.pro
文件,在文件末尾添加以下代码:
代码清单:CardCheck.pro
# ... (文件中已有的其他配置)# ----------------- 设备SDK库配置 -----------------
# 1. 指定SDK头文件的搜索路径
INCLUDEPATH += $$PWD/jint# 2. 将SDK的头文件和源文件添加到项目中,使其参与编译
HEADERS += \$$PWD/jint/samapi.h \$$PWD/jint/hidapi.hSOURCES += $$PWD/jint/samapi.cpp \$$PWD/jint/hid.c# 3. 链接SDK的静态库文件
LIBS += -lhid -lsetupapi # 添加hid库
LIBS += -L$$PWD/jint/ -ljint
完成上述配置后,在Qt Creator中执行一次“重新构建项目”(菜单栏 -> 构建 -> 重新构建项目 “CardCheck”)。如果整个过程没有出现编译或链接错误,则表明SDK已经成功集成到项目中。至此,开发环境已经准备就绪,可以在后续的章节中调用SDK提供的接口来实现设备的控制和图像采集功能。
3.3.2 图像采集和显示
在成功集成SDK后,本节将实现系统的核心交互功能:通过点击界面上的“采集正面”和“采集反面”按钮,驱动多光谱设备,按顺序(白光、红外、紫外)捕获图像,并将捕获到的六张图像实时拼接、展示在主界面的图像展示区域。
为确保用户界面的流畅响应,开发过程中必须避免使用任何会阻塞主线程的操作,如sleep()
。所有延时等待(如等待光源稳定、等待设备命令间隔)都将通过Qt的定时器QTimer
以异步、非阻塞的方式实现。
1. 头文件更新与成员变量声明
由于需要使用摄像头相关功能,因此在项目中要把Qt的多媒体模块导入进来。具体的,在CardCheck.pro
文件顶部,添加如下代码:
QT += core gui
# 引入摄像头库
QT += multimedia
QT += multimediawidgets
接下来,需要对mainwindow.h
头文件进行扩展,以引入相机、图像处理、定时器以及设备SDK所需的相关类,并声明用于管理采集流程的成员变量和私有槽函数。
代码清单: mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H#include <QMainWindow>// --- 新增头文件 ---
#include <QList>
#include <QCamera>
#include <QCameraInfo>
#include <QCameraImageCapture>
#include <QTimer>
#include <QImage>
#include "jint/samapi.h" // 导入SDK库头文件QT_BEGIN_NAMESPACE
class QAction;
class QToolBar;
class QLabel;
class QTextEdit;
QT_END_NAMESPACEQT_BEGIN_NAMESPACE
namespace Ui {
class MainWindow;
}
QT_END_NAMESPACEclass MainWindow : public QMainWindow
{Q_OBJECTpublic:MainWindow(QWidget *parent = nullptr);~MainWindow();private slots:// --- 新增私有槽函数 ---void onCaptureFrontClicked(); // 响应“采集正面”按钮点击void onCaptureBackClicked(); // 响应“采集反面”按钮点击void processCaptureSequence(); // 处理采集流程状态机void onImageCaptured(int id, const QImage &preview); // 响应图像捕获完成信号private:// 初始化UI的私有方法void createActions();void createToolBars();void createCentralWidget();void createStatusBar();// --- 新增私有方法 ---void initCameraAndDevice(); // 初始化摄像头和SDK设备void updateDisplayImage(); // 更新拼接后的显示图像// 工具栏QToolBar *mainToolBar;// 中心部件的控件QLabel *imageLabel;QTextEdit *resultTextEdit;// 动作(Actions)QAction *captureFrontAct;QAction *captureBackAct;QAction *recognizeAct;QAction *saveAct;QAction *settingsAct;// --- 新增设备与采集相关成员变量 ---samapi* m_pSamapi; // SDK设备控制对象QCamera *m_pCamera; // 摄像头对象QCameraImageCapture *m_pCap; // 图像捕捉对象bool m_isCameraReady; // 摄像头是否准备就绪// 采集流程控制变量QTimer *m_pCaptureTimer; // 用于非阻塞延时的定时器int m_captureStep; // 采集步骤状态机变量bool m_isCapturingFront; // 标记当前是采集正面还是反面// 存储捕获的图像QList<QImage> m_frontImages; // 存储正面三张图像QList<QImage> m_backImages; // 存储反面三张图像private:Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H
2. 在构造函数中初始化
在MainWindow
的构造函数中,需要完成信号与槽的连接,并调用新增的initCameraAndDevice()
方法来准备硬件设备。
代码清单: mainwindow.cpp
(构造函数部分)
#include <QDebug>
#include <QPainter>
#include <QMessageBox>// ... (其他include)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)
{ui->setupUi(this);// ... (原有UI初始化代码)// --- 新增初始化与信号槽连接 ---initCameraAndDevice(); // 初始化硬件设备// 连接Action的triggered信号到对应的槽函数connect(captureFrontAct, &QAction::triggered, this, &MainWindow::onCaptureFrontClicked);connect(captureBackAct, &QAction::triggered, this, &MainWindow::onCaptureBackClicked);
}MainWindow::~MainWindow()
{// 在析构函数中释放资源if (m_pCamera) {m_pCamera->stop();}if (m_pSamapi) {// 关闭所有灯光并关闭设备m_pSamapi->SetCamLightParam(0, 0, 0, 0);m_pSamapi->CloseSam();delete m_pSamapi;}delete ui;
}// ... (其他UI创建方法)
3. 实现设备初始化
initCameraAndDevice()
方法负责打开SDK设备和Qt摄像头,并设置好图像捕获的信号槽连接。
代码清单: mainwindow.cpp
(设备初始化)
void MainWindow::initCameraAndDevice()
{// 1. 初始化并打开SDK设备m_pSamapi = new samapi();if (m_pSamapi->OpenSam() != 0) {statusBar()->showMessage(QStringLiteral("错误:未能打开多光谱采集设备!"), 5000);QMessageBox::critical(this, QStringLiteral("设备错误"), QStringLiteral("未能打开多光谱采集设备,请检查设备连接。"));return;}// 关闭所有灯光作为初始状态m_pSamapi->SetCamLightParam(0, 0, 0, 0);// 2. 初始化Qt摄像头const QString vid = "3c4d";const QString pid = "e640";QList<QCameraInfo> infos = QCameraInfo::availableCameras();foreach(const QCameraInfo &info, infos){if (info.deviceName().contains(vid) && info.deviceName().contains(pid)){m_pCamera = new QCamera(info, this);m_pCap = new QCameraImageCapture(m_pCamera, this);m_pCamera->setCaptureMode(QCamera::CaptureStillImage);// 连接图像捕获信号到槽函数connect(m_pCap, &QCameraImageCapture::imageCaptured, this, &MainWindow::onImageCaptured);m_pCamera->start();m_isCameraReady = true;break;}}if (!m_isCameraReady) {statusBar()->showMessage(QStringLiteral("错误:未能找到指定的摄像头设备!"), 5000);QMessageBox::critical(this, QStringLiteral("设备错误"), QStringLiteral("未能找到指定的摄像头设备,请检查设备连接。"));return;}// 3. 初始化用于非阻塞延时的定时器m_pCaptureTimer = new QTimer(this);m_pCaptureTimer->setSingleShot(true); // 设置为单次触发connect(m_pCaptureTimer, &QTimer::timeout, this, &MainWindow::processCaptureSequence);
}
4. 实现非阻塞采集流程
采集流程的核心是一个由QTimer
驱动的状态机。点击按钮后,processCaptureSequence()
方法被首次调用,之后每一步操作(如开灯、延时、拍照)完成后,通过启动一个短暂的QTimer
来触发下一步,从而避免界面卡死。
代码清单: mainwindow.cpp
(采集流程控制)
// "采集正面"按钮的槽函数
void MainWindow::onCaptureFrontClicked()
{if (!m_isCameraReady || m_pCaptureTimer->isActive()) {statusBar()->showMessage(QStringLiteral("设备正忙,请稍候..."), 3000);return;}m_isCapturingFront = true;m_frontImages.clear(); // 清空之前的正面图像m_backImages.clear();// 清空之前的反面图像m_captureStep = 0; // 重置状态机processCaptureSequence(); // 启动采集序列
}// "采集反面"按钮的槽函数
void MainWindow::onCaptureBackClicked()
{if (!m_isCameraReady || m_pCaptureTimer->isActive()) {statusBar()->showMessage(QStringLiteral("设备正忙,请稍候..."), 3000);return;}m_isCapturingFront = false;m_backImages.clear(); // 清空之前的反面图像m_captureStep = 0; // 重置状态机processCaptureSequence(); // 启动采集序列
}// 采集序列状态机
void MainWindow::processCaptureSequence()
{// 亮度参数int rgbBrightness = 100;int infraredBrightness = 100;int uvcBrightness = 40;switch (m_captureStep){// --- 阶段1: 采集白光图像 ---case 0: // 开启白光statusBar()->showMessage(QStringLiteral("正在采集白光图像..."));m_pSamapi->SetCamLightParam(1, rgbBrightness, infraredBrightness, uvcBrightness);m_pCaptureTimer->start(100); // 延时100msbreak;case 1: // 设置相机白光模式m_pSamapi->SetCameraLen(1);m_pCaptureTimer->start(1500); // 延时1.5秒等待光源稳定break;case 2: // 捕获图像m_pCap->capture();// 等待 onImageCaptured 信号被触发后,再推进流程return;// --- 阶段2: 采集红外图像 ---case 3: // 开启红外光statusBar()->showMessage(QStringLiteral("正在采集红外图像..."));m_pSamapi->SetCamLightParam(2, rgbBrightness, infraredBrightness, uvcBrightness);m_pCaptureTimer->start(100);break;case 4: // 设置相机红外模式m_pSamapi->SetCameraLen(2);m_pCaptureTimer->start(1500);break;case 5: // 捕获图像m_pCap->capture();return;// --- 阶段3: 采集紫外图像 ---case 6: // 开启紫外光statusBar()->showMessage(QStringLiteral("正在采集紫外图像..."));m_pSamapi->SetCamLightParam(3, rgbBrightness, infraredBrightness, uvcBrightness);m_pCaptureTimer->start(100);break;case 7: // 设置相机紫外模式m_pSamapi->SetCameraLen(3);m_pCaptureTimer->start(1500);break;case 8: // 捕获图像m_pCap->capture();return;// --- 阶段4: 结束采集 ---case 9:m_pSamapi->SetCamLightParam(0, rgbBrightness, infraredBrightness, uvcBrightness); // 关闭所有灯statusBar()->showMessage(QStringLiteral("采集完成"), 3000);break;}// 推进状态机m_captureStep++;
}// 图像捕获完成的槽函数
void MainWindow::onImageCaptured(int id, const QImage &preview)
{Q_UNUSED(id);if (m_isCapturingFront) {m_frontImages.append(preview);} else {m_backImages.append(preview);}updateDisplayImage(); // 更新UI显示// 推进状态机到下一个阶段m_captureStep++;processCaptureSequence();
}
5. 图像拼接与显示
updateDisplayImage()
方法负责将采集到的所有图像(正面和/或反面)合并成一张大图,并以保持宽高比的方式显示在imageLabel
中。该方法使用QPainter
来绘制图像和文字标注。
代码清单: mainwindow.cpp
(图像显示)
// 图像拼接与显示
void MainWindow::updateDisplayImage()
{if (m_frontImages.isEmpty() && m_backImages.isEmpty()) {return; // 如果没有任何图像,则不进行操作}// 假设所有图像尺寸相同,以第一张有效图像为基准QImage firstImage = !m_frontImages.isEmpty() ? m_frontImages.first() : m_backImages.first();// 将图像尺寸缩小一半以适应显示int scaledWidth = firstImage.width() / 2;int scaledHeight = firstImage.height() / 2;int margin = 10; // 图像间的间距// 计算画布总尺寸int canvasWidth = scaledWidth * 3 + margin * 4;// 如果有反面图像,画布高度为两行,否则为一行int canvasHeight = m_backImages.isEmpty() ? (scaledHeight + margin * 2 + 80) : (scaledHeight * 2 + margin * 3 + 160);// 创建画布QPixmap canvas(canvasWidth, canvasHeight);canvas.fill(QColor("#F0F8FF")); // 使用与背景色一致的填充色QPainter painter(&canvas);painter.setRenderHint(QPainter::Antialiasing, true);painter.setRenderHint(QPainter::TextAntialiasing, true);// 设置字体QFont font("Microsoft YaHei", 48, QFont::Bold);painter.setFont(font);QString frontLabels[] = {QStringLiteral("正面白光"), QStringLiteral("正面红外"), QStringLiteral("正面紫外")};QString backLabels[] = {QStringLiteral("反面白光"), QStringLiteral("反面红外"), QStringLiteral("反面紫外")};// 绘制正面三张图for (int i = 0; i < m_frontImages.size(); ++i) {int x = margin * (i + 1) + scaledWidth * i;int y = margin;int textHeight = 80; // 预留给文本的高度// 绘制缩放后的图像QImage scaledImage = m_frontImages[i].scaled(scaledWidth, scaledHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation);// 设置黑色画笔用于绘制文字painter.setPen(Qt::black);painter.drawText(x, y, scaledWidth, textHeight, Qt::AlignCenter, frontLabels[i]);// 绘制图像边框,设置与UI风格一致的灰色画笔painter.setPen(QColor("#C0C8D0"));// 在图像将要绘制的位置绘制一个矩形边框painter.drawRect(x, y + textHeight, scaledWidth, scaledHeight);// 在边框内绘制图像painter.drawImage(x, y + textHeight, scaledImage);}// 绘制反面三张图for (int i = 0; i < m_backImages.size(); ++i) {int x = margin * (i + 1) + scaledWidth * i;int y = margin * 2 + scaledHeight + 80; // 调整Y坐标以适应第二行int textHeight = 80;// 绘制缩放后的图像QImage scaledImage = m_backImages[i].scaled(scaledWidth, scaledHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation);// 设置黑色画笔用于绘制文字painter.setPen(Qt::black);painter.drawText(x, y, scaledWidth, textHeight, Qt::AlignCenter, backLabels[i]);// --- 新增: 绘制图像边框 ---painter.setPen(QColor("#C0C8D0"));painter.drawRect(x, y + textHeight, scaledWidth, scaledHeight);// 在边框内绘制图像painter.drawImage(x, y + textHeight, scaledImage);}painter.end();// 将最终合成的图像以保持宽高比的方式显示在imageLabel中QPixmap scaledCanvas = canvas.scaled(imageLabel->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);imageLabel->setPixmap(scaledCanvas);
}
完成以上所有代码的添加和修改后,重新编译并运行项目。现在,点击“采集正面”和“采集反面”按钮,系统将自动完成多光谱图像的采集,并如下图所示,将带有清晰标注的六张图像整齐地呈现在界面上(考虑到数据隐私,采用一张广告纸来采集演示)。
3.3.3 图像保存
在完成图像采集后,需要实现图像的保存功能。用户点击工具栏上的“保存”按钮后,系统会将当前已采集的所有图像(无论是只有正面,还是正反面都有)保存至用户桌面上的指定文件夹中。
为了实现有序管理,保存逻辑将遵循以下规则:
- 在用户桌面上创建一个名为
CardImages
的主文件夹。 - 在
CardImages
文件夹内,根据当前的保存日期创建一个子文件夹,格式为YYYY-MM-DD
。 - 所有在同一次保存操作中生成的图像文件,将以当前的时分秒(
hh_mm_ss
)作为文件名前缀,以确保唯一性。 - 文件名将通过后缀明确标识其来源(正面/反面)和光谱类型(白光/红外/紫外)。
1. 头文件更新与槽函数声明
首先,需要在mainwindow.h
头文件中为“保存”按钮对应的QAction
声明一个私有槽函数onSaveClicked()
。
代码清单: mainwindow.h
// ... (保留原有代码)// 在 private slots: 部分添加新的槽函数声明
private slots:void onCaptureFrontClicked();void onCaptureBackClicked();void processCaptureSequence();void onImageCaptured(int id, const QImage &preview);void onSaveClicked(); // <-- 新增:保存按钮的槽函数// ... (保留原有代码)
2. 信号与槽的连接
接下来,在mainwindow.cpp
的构造函数中,将saveAct
的triggered
信号连接到新创建的onSaveClicked()
槽函数上。
代码清单: mainwindow.cpp
(构造函数部分)
// ... (保留原有代码)MainWindow::MainWindow(QWidget *parent): QMainWindow(parent)// ... (保留原有初始化列表)
{// ... (保留原有UI初始化代码)// 连接Action的triggered信号到对应的槽函数connect(captureFrontAct, &QAction::triggered, this, &MainWindow::onCaptureFrontClicked);connect(captureBackAct, &QAction::triggered, this, &MainWindow::onCaptureBackClicked);connect(saveAct, &QAction::triggered, this, &MainWindow::onSaveClicked); // <-- 新增连接
}// ... (保留原有代码)
3. 实现图像保存逻辑
onSaveClicked()
槽函数的实现是本节的核心。该函数将负责处理文件路径的构建、目录的创建以及图像文件的写入。为实现该功能,需要引入QStandardPaths
, QDir
, QDateTime
等相关类的头文件。
代码清单: mainwindow.cpp
(图像保存实现)
// 在文件顶部添加必要的头文件
#include <QStandardPaths>
#include <QDir>
#include <QDateTime>// ... (保留原有代码)// 实现保存图像的槽函数
void MainWindow::onSaveClicked()
{// 1. 检查是否存在可供保存的图像if (m_frontImages.isEmpty() && m_backImages.isEmpty()) {QMessageBox::warning(this, QStringLiteral("图像未采集全"), QStringLiteral("请先采集完正反面图像。"));return;}// 2. 获取桌面路径并构建目标文件夹路径QString desktopPath = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);QString baseDirName = "CardImages";QString dateDirName = QDateTime::currentDateTime().toString("yyyy-MM-dd");QDir baseDir(desktopPath);// 创建CardImages主文件夹if (!baseDir.exists(baseDirName)) {baseDir.mkdir(baseDirName);}QDir dateDir(desktopPath + "/" + baseDirName);// 创建日期子文件夹if (!dateDir.exists(dateDirName)) {dateDir.mkdir(dateDirName);}QString savePath = dateDir.filePath(dateDirName);// 3. 生成带时分秒前缀的文件名QString timePrefix = QDateTime::currentDateTime().toString("hh_mm_ss");// 4. 定义文件名后缀QString frontSuffixes[] = {"_front_white.jpg", "_front_hw.jpg", "_front_uv.jpg"};QString backSuffixes[] = {"_back_white.jpg", "_back_hw.jpg", "_back_uv.jpg"};bool success = true;// 5. 循环保存正面图像for (int i = 0; i < m_frontImages.size(); ++i) {if (i < 3) { // 安全检查,防止索引越界QString fileName = timePrefix + frontSuffixes[i];QString fullPath = savePath + "/" + fileName;if (!m_frontImages[i].save(fullPath, "JPG", 85)) { // 以JPG格式保存,质量为85success = false;qDebug() << "Failed to save image:" << fullPath;break; }}}// 6. 循环保存反面图像(如果成功保存了正面)if (success) {for (int i = 0; i < m_backImages.size(); ++i) {if (i < 3) {QString fileName = timePrefix + backSuffixes[i];QString fullPath = savePath + "/" + fileName;if (!m_backImages[i].save(fullPath, "JPG", 85)) {success = false;qDebug() << "Failed to save image:" << fullPath;break;}}}}// 7. 向用户反馈保存结果// 7. 向用户反馈保存结果if (success) {QMessageBox::information(this, QStringLiteral("保存成功"), QStringLiteral("图像已成功保存至:%1").arg(savePath));} else {QMessageBox::critical(this, QStringLiteral("保存失败"), QStringLiteral("图像保存过程中发生错误,请检查磁盘空间或文件权限。"));}
}
完成以上代码的添加后,重新编译并运行项目。在采集图像后,点击工具栏上的“保存”按钮,程序便会将采集到的图像文件存储到桌面CardImages
文件夹下对应日期的子目录中,文件名将严格遵循预设的格式。
3.3.4 优化保存:异步处理与UI响应
上一节中实现的图像保存功能虽然能够正确工作,但存在一个明显的用户体验问题:当点击“保存”按钮时,由于需要将多达六张高分辨率的图像写入磁盘,这是一个相对耗时的I/O(输入/输出)操作,可能会持续1到2秒。在此期间,由于保存逻辑是在主线程(也称GUI线程)中执行的,整个应用程序界面会处于“卡死”或“无响应”状态,无法处理任何用户输入。
为解决此问题,必须将耗时的保存操作从主线程中剥离,移至一个单独的工作线程中执行。这样,主线程可以继续保持流畅,负责UI更新和事件响应,从而显著提升用户体验。本节将利用Qt的并发编程框架 QtConcurrent
来实现这一优化。QtConcurrent
提供了一套简洁的高级API,能够轻松地将函数调用分派到后台线程执行,而无需手动管理复杂的线程生命周期。
为了在Qt项目中使用 QtConcurrent
框架,需要在 .pro
项目配置文件中添加 concurrent
模块。打开 CardCheck.pro
文件,找到 QT += core gui
这一行,然后在后面添加 concurrent
,如下所示:
QT += core gui concurrent
# ... (文件中已有的其他配置)
1. 头文件更新与成员声明
首先,需要对 mainwindow.h
文件进行修改,引入 QtConcurrent
相关的头文件,并声明用于监控后台任务的 QFutureWatcher
和任务完成后的处理槽函数。
代码清单: mainwindow.h
// ... (保留原有 #include)
#include <QtConcurrent> // <-- 新增:Qt并发框架
#include <QFuture> // <-- 新增:用于表示异步计算结果
#include <QFutureWatcher> // <-- 新增:用于监控QFuture的状态// ... (保留原有类前置声明)class MainWindow : public QMainWindow
{Q_OBJECTpublic:// ... (保留原有 public 成员)private slots:void onCaptureFrontClicked();void onCaptureBackClicked();void processCaptureSequence();void onImageCaptured(int id, const QImage &preview);void onSaveClicked();void onSaveFinished(); // <-- 新增:保存任务完成后的处理槽函数private:// ... (保留原有 private 方法)// <-- 新增:执行实际保存操作的私有方法 -->// 该方法将在后台线程中被调用bool performSaveImages(const QList<QImage> frontImages, const QList<QImage> backImages, const QString& savePath);// ... (保留原有 private 成员变量)// --- 新增异步保存相关成员 ---QFutureWatcher<bool> *m_pSaveWatcher; // 监控保存任务的Watcher
};
#endif // MAINWINDOW_H
这里新增了一个私有方法 performSaveImages()
,它封装了上一节中所有的文件写入逻辑。同时,新增了一个槽函数 onSaveFinished()
,用于在后台保存任务完成后弹出提示框。m_pSaveWatcher
成员则用于连接后台任务和主线程的信号槽。
2. 在构造函数中初始化
在 mainwindow.cpp
的构造函数中,需要实例化 QFutureWatcher
并将其 finished
信号连接到 onSaveFinished()
槽函数。
代码清单: mainwindow.cpp
(构造函数部分)
// ... (保留原有 #include)MainWindow::MainWindow(QWidget *parent): QMainWindow(parent)// ... (保留原有初始化列表)
{// ... (保留原有UI初始化代码)// 连接Action的triggered信号到对应的槽函数connect(captureFrontAct, &QAction::triggered, this, &MainWindow::onCaptureFrontClicked);connect(captureBackAct, &QAction::triggered, this, &MainWindow::onCaptureBackClicked);connect(saveAct, &QAction::triggered, this, &MainWindow::onSaveClicked);// --- 新增:初始化并连接保存任务的Watcher ---m_pSaveWatcher = new QFutureWatcher<bool>(this);connect(m_pSaveWatcher, &QFutureWatcher<bool>::finished, this, &MainWindow::onSaveFinished);
}// ... (保留原有代码)
3. 改造保存逻辑
接下来,将原有的保存逻辑进行拆分。onSaveClicked()
槽函数将不再直接执行文件写入,而是启动一个后台任务。实际的文件写入操作则移至 performSaveImages()
方法中。
代码清单: mainwindow.cpp
(异步保存实现)
// ... (保留原有代码)// 点击“保存”按钮的槽函数 - 启动后台保存任务
void MainWindow::onSaveClicked()
{// 1. 检查是否存在可供保存的图像if (m_frontImages.isEmpty() && m_backImages.isEmpty()) {QMessageBox::warning(this, QStringLiteral("图像未采集"), QStringLiteral("请先采集图像后再保存。"));return;}// 2. 检查是否已有保存任务正在进行if (m_pSaveWatcher->isRunning()) {statusBar()->showMessage(QStringLiteral("正在保存中,请稍候..."), 3000);return;}// 3. 禁用保存按钮,防止重复点击saveAct->setEnabled(false);statusBar()->showMessage(QStringLiteral("正在保存图像至桌面..."), 0); // 持续显示状态// 4. 准备保存路径 (这部分逻辑不耗时,在主线程完成)QString desktopPath = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);QString baseDirName = "CardImages";QString dateDirName = QDateTime::currentDateTime().toString("yyyy-MM-dd");QDir baseDir(desktopPath);if (!baseDir.exists(baseDirName)) {baseDir.mkdir(baseDirName);}QDir dateDir(desktopPath + "/" + baseDirName);if (!dateDir.exists(dateDirName)) {dateDir.mkdir(dateDirName);}QString savePath = dateDir.filePath(dateDirName);// 5. 使用 QtConcurrent::run 启动后台任务// 将成员变量 m_frontImages 和 m_backImages 作为值传递,确保线程安全QFuture<bool> future = QtConcurrent::run(this, &MainWindow::performSaveImages, m_frontImages, m_backImages, savePath);m_pSaveWatcher->setFuture(future);
}// 在后台线程中执行的实际保存操作
bool MainWindow::performSaveImages(const QList<QImage> frontImages, const QList<QImage> backImages, const QString& savePath)
{// 生成带时分秒前缀的文件名QString timePrefix = QDateTime::currentDateTime().toString("hh_mm_ss");// 定义文件名后缀QString frontSuffixes[] = {"_front_white.jpg", "_front_hw.jpg", "_front_uv.jpg"};QString backSuffixes[] = {"_back_white.jpg", "_back_hw.jpg", "_back_uv.jpg"};// 循环保存正面图像for (int i = 0; i < frontImages.size(); ++i) {if (i < 3) {QString fileName = timePrefix + frontSuffixes[i];QString fullPath = savePath + "/" + fileName;if (!frontImages[i].save(fullPath, "JPG", 85)) {qDebug() << "Failed to save image:" << fullPath;return false; // 保存失败,返回false}}}// 循环保存反面图像for (int i = 0; i < backImages.size(); ++i) {if (i < 3) {QString fileName = timePrefix + backSuffixes[i];QString fullPath = savePath + "/" + fileName;if (!backImages[i].save(fullPath, "JPG", 85)) {qDebug() << "Failed to save image:" << fullPath;return false; // 保存失败,返回false}}}return true; // 所有图像均保存成功
}// 保存任务完成后的槽函数 - 在主线程中执行
void MainWindow::onSaveFinished()
{// 1. 重新启用保存按钮saveAct->setEnabled(true);statusBar()->clearMessage(); // 清除状态栏消息// 2. 获取后台任务的执行结果bool success = m_pSaveWatcher->result();// 3. 根据结果向用户反馈if (success) {QMessageBox::information(this, QStringLiteral("保存成功"), QStringLiteral("图像已成功保存至桌面 CardImages 文件夹。"));} else {QMessageBox::critical(this, QStringLiteral("保存失败"), QStringLiteral("图像保存过程中发生错误,请检查磁盘空间或文件权限。"));}
}
完成以上修改后,重新编译并运行项目。再次执行采集和保存操作,会发现点击“保存”按钮后,状态栏立刻显示提示信息,但整个应用程序界面保持流畅,可以进行其他操作。几秒后,当图像全部保存完毕,系统会弹出一个提示框告知保存结果。至此,通过异步化处理,成功解决了UI卡顿问题,优化了系统的响应性能和用户体验。
3.3.5 固定窗体尺寸
在前面的章节中,通过调用showMaximized()
方法,程序启动后主窗体默认以最大化状态显示。在这种状态下,updateDisplayImage()
函数能够根据当前imageLabel
的尺寸,将拼接后的图像进行自适应缩放,达到最佳的显示效果。
然而,这种方式存在一个体验上的缺陷:当用户点击标题栏的“还原”按钮,或通过拖拽边框尝试调整窗体大小时,imageLabel
的尺寸会随之改变,可能导致已显示的图像缩放比例失调,或在高分辨率屏幕上显得过小。为了提供一个稳定、一致的视觉界面,避免因窗体尺寸变化带来的布局问题,本节将对程序进行优化,实现以下效果:
- 将窗体尺寸固定为当前屏幕可用区域(排除任务栏等)宽高的85%。
- 禁止用户通过拖拽边框来调整窗体大小。
- 移除窗体标题栏右上角的“最大化”按钮。
- 程序启动时,窗体自动居中显示在屏幕上。
这些调整将统一在应用程序的入口文件main.cpp
中完成,因为窗体的尺寸和位置应在其实例化后、首次显示前进行设置。
为了获取屏幕信息和使用高级布局功能,需要在main.cpp
文件的顶部包含<QScreen>
和<QStyle>
两个头文件。
修改main.cpp
中的main
函数,移除原有的w.showMaximized()
调用,并替换为一套完整的窗体尺寸与位置的设置逻辑。
代码清单: main.cpp
#include "mainwindow.h"#include <QApplication>
#include <QScreen> // 用于获取屏幕信息
#include <QStyle> // 用于辅助窗口居中int main(int argc, char *argv[])
{// 在QApplication实例化前,启用高DPI缩放QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);QApplication a(argc, argv);MainWindow w;// --- 开始:窗体尺寸与位置的定制化设置 ---// 1. 获取主屏幕的可用几何区域(即排除任务栏等系统界面)const QRect availableGeometry = QGuiApplication::primaryScreen()->availableGeometry();// 2. 计算期望的窗体尺寸(屏幕可用区域的85%)QSize newSize(availableGeometry.width() * 0.85, availableGeometry.height() * 0.85);// 3. 将计算出的尺寸设置为窗体的固定尺寸// 调用setFixedSize()后,用户将无法通过拖拽调整窗体大小w.setFixedSize(newSize);// 4. 移除窗体标题栏的最大化按钮// 通过位运算,在现有的窗口标志中移除Qt::WindowMaximizeButtonHintw.setWindowFlags(w.windowFlags() & ~Qt::WindowMaximizeButtonHint);// 5. 将窗体居中显示在屏幕上// 使用QStyle::alignedRect计算出窗体在指定区域内居中时应处的正确位置w.setGeometry(QStyle::alignedRect(Qt::LeftToRight,Qt::AlignCenter,w.size(),availableGeometry));// --- 结束:定制化设置 ---// 显示窗体。注意,此处不再使用 w.showMaximized()w.show();return a.exec();
}
完成以上修改后,按Ctrl+R
重新编译并运行项目。程序启动后,将呈现一个尺寸固定、位置居中且最大化按钮置灰的专业窗体。
至此,窗体的显示逻辑得到优化,为后续功能的开发提供了一个稳定可靠的UI基础。
3.4 曝光强度控制
在3.3.2节的图像采集中,白光、红外和紫外三种光源的亮度值被硬编码在程序中。然而,不同类型的证件对光照强度的敏感度各异,固定的曝光参数可能无法在所有情况下都获得最佳的成像质量。为了提升系统的适应性和专业性,本节将引入曝光强度控制功能。
该功能将通过一个独立的“参数设置”对话框来实现。用户可以实时调整三种光源的亮度值(范围0-255),这些设置将被持久化存储在本地的INI配置文件中。程序每次启动时会自动加载这些配置,从而确保采集参数的一致性。
3.4.1 创建参数设置对话框
首先,需要创建一个新的对话框类 SettingsDialog
,用于承载曝光参数的设置界面。该对话框将通过纯代码方式构建,包含三个用于输入整数的 QSpinBox
控件和标准的“确定”、“取消”按钮。
1. 创建对话框类文件
在Qt Creator中,右键点击项目根目录,选择“添加新文件…”,在弹出的对话框中选择 C++ -> C++ Class。将类名设置为 SettingsDialog
,基类选择 QObject
,然后完成创建。Qt Creator会自动生成 settingsdialog.h
和 settingsdialog.cpp
两个文件。
2. 编写头文件 settingsdialog.h
打开 settingsdialog.h
文件,定义对话框的UI组件和公共接口。接口将包括一个用于设置初始值的 setValues
方法和一个用于获取最终值的 getValues
方法。
代码清单: settingsdialog.h
#ifndef SETTINGSDIALOG_H
#define SETTINGSDIALOG_H#include <QDialog>// 前置声明,减少头文件依赖
class QSpinBox;
class QDialogButtonBox;class SettingsDialog : public QDialog
{Q_OBJECTpublic:explicit SettingsDialog(QWidget *parent = nullptr);~SettingsDialog();// 公共接口:设置初始曝光值void setValues(int rgb, int infrared, int uvc);// 公共接口:获取用户设置的曝光值void getValues(int &rgb, int &infrared, int &uvc) const;private:// UI控件成员变量QSpinBox *rgbSpinBox; // 白光亮度输入框QSpinBox *infraredSpinBox; // 红外亮度输入框QSpinBox *uvcSpinBox; // 紫外亮度输入框QDialogButtonBox *buttonBox; // 标准按钮盒(确定/取消)
};#endif // SETTINGSDIALOG_H
3. 实现源文件 settingsdialog.cpp
在 settingsdialog.cpp
中,将完成UI控件的实例化、布局以及信号与槽的连接。布局将采用 QFormLayout
,它非常适合创建标签和输入控件成对出现的表单。
代码清单: settingsdialog.cpp
#include "settingsdialog.h"
#include <QSpinBox>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QVBoxLayout>
#include <QLabel>SettingsDialog::SettingsDialog(QWidget *parent) : QDialog(parent)
{// --- 1. 实例化UI控件 ---// 白光rgbSpinBox = new QSpinBox(this);rgbSpinBox->setRange(0, 255); // 设置有效范围rgbSpinBox->setSuffix(" / 255"); // 添加单位提示// 红外infraredSpinBox = new QSpinBox(this);infraredSpinBox->setRange(0, 255);infraredSpinBox->setSuffix(" / 255");// 紫外uvcSpinBox = new QSpinBox(this);uvcSpinBox->setRange(0, 255);uvcSpinBox->setSuffix(" / 255");// 创建标准的“确定”和“取消”按钮盒buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);// --- 2. 布局设计 ---// 使用表单布局来排列标签和输入框QFormLayout *formLayout = new QFormLayout;formLayout->addRow(new QLabel(QStringLiteral("白光曝光强度:"), this), rgbSpinBox);formLayout->addRow(new QLabel(QStringLiteral("红外曝光强度:"), this), infraredSpinBox);formLayout->addRow(new QLabel(QStringLiteral("紫外曝光强度:"), this), uvcSpinBox);// 使用垂直布局来整合表单和按钮盒QVBoxLayout *mainLayout = new QVBoxLayout(this);mainLayout->addLayout(formLayout);mainLayout->addWidget(buttonBox);// 设置窗体主布局this->setLayout(mainLayout);// --- 3. 信号与槽连接 ---// 将按钮盒的 accepted 信号连接到对话框的 accept 槽connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);// 将按钮盒的 rejected 信号连接到对话框的 reject 槽connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);// --- 4. 设置窗体属性 ---this->setWindowTitle(QStringLiteral("参数设置"));this->setFixedSize(350, 200); // 固定对话框大小
}SettingsDialog::~SettingsDialog()
{
}// 实现设置初始值的公共方法
void SettingsDialog::setValues(int rgb, int infrared, int uvc)
{rgbSpinBox->setValue(rgb);infraredSpinBox->setValue(infrared);uvcSpinBox->setValue(uvc);
}// 实现获取最终值的公共方法
void SettingsDialog::getValues(int &rgb, int &infrared, int &uvc) const
{rgb = rgbSpinBox->value();infrared = infraredSpinBox->value();uvc = uvcSpinBox->value();
}
3.4.2 集成INI文件持久化
为了保存用户的设置,将使用Qt提供的 QSettings
类。QSettings
提供了一种便捷的、跨平台的机制来读写应用程序的配置信息,默认情况下在Windows上会以INI文件的形式存储在标准应用数据目录中。
1. 配置应用程序信息
为了让 QSettings
能够正确定位配置文件,需要在 main.cpp
中为应用程序设置组织名称和应用名称。
代码清单: main.cpp
// ... (保留原有 #include)int main(int argc, char *argv[])
{// ... (保留原有代码)QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);// --- 新增:设置组织和应用名称 ---QCoreApplication::setOrganizationName("MyCompany");QCoreApplication::setApplicationName("CardCheck");QApplication a(argc, argv);MainWindow w;// ... (保留原有窗体尺寸设置代码)w.show();return a.exec();
}
2. 在主窗口中添加配置管理
在 MainWindow
类中添加用于加载和保存配置的方法,并声明成员变量来存储曝光强度值。
代码清单: mainwindow.h
// ... (保留原有代码)class MainWindow : public QMainWindow
{Q_OBJECTpublic:MainWindow(QWidget *parent = nullptr);~MainWindow();private slots:// ... (保留原有槽函数)void onSettingsClicked(); // <-- 新增:响应参数设置按钮点击private:// ... (保留原有UI创建方法)void initCameraAndDevice();void updateDisplayImage();void loadSettings(); // <-- 新增:加载配置void saveSettings(); // <-- 新增:保存配置// ... (保留原有成员变量)// --- 新增:曝光参数成员变量 ---int m_rgbBrightness;int m_infraredBrightness;int m_uvcBrightness;// ... (保留原有代码)
};
3. 实现配置的加载与保存
在 mainwindow.cpp
中实现 loadSettings()
和 saveSettings()
方法,并在构造函数中调用 loadSettings()
来初始化参数。
代码清单: mainwindow.cpp
// 在文件顶部添加 QSettings 头文件
#include <QSettings>// ... (保留原有代码)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)// <-- 在初始化列表中可以移除对曝光参数的初始化,因为loadSettings会处理
{ui->setupUi(this);// --- 在UI创建前加载配置 ---loadSettings();// ... (原有UI初始化代码)// 连接Action的triggered信号到对应的槽函数connect(captureFrontAct, &QAction::triggered, this, &MainWindow::onCaptureFrontClicked);connect(captureBackAct, &QAction::triggered, this, &MainWindow::onCaptureBackClicked);connect(saveAct, &QAction::triggered, this, &MainWindow::onSaveClicked);connect(settingsAct, &QAction::triggered, this, &MainWindow::onSettingsClicked); // <-- 新增连接// ... (保留原有代码)
}// --- 新增:实现加载配置方法 ---
void MainWindow::loadSettings()
{QSettings settings; // 自动定位到应用的配置文件// 读取"Brightness"组下的键值,如果键不存在,则使用默认值m_rgbBrightness = settings.value("Brightness/RGB", 100).toInt();m_infraredBrightness = settings.value("Brightness/Infrared", 100).toInt();m_uvcBrightness = settings.value("Brightness/UVC", 40).toInt();
}// --- 新增:实现保存配置方法 ---
void MainWindow::saveSettings()
{QSettings settings;// 将当前的曝光参数写入"Brightness"组settings.setValue("Brightness/RGB", m_rgbBrightness);settings.setValue("Brightness/Infrared", m_infraredBrightness);settings.setValue("Brightness/UVC", m_uvcBrightness);
}// ... (保留原有代码)
3.4.3 连接对话框与主窗口
现在,需要将 SettingsDialog
与 MainWindow
关联起来。当用户点击“参数设置”按钮时,弹出对话框;当用户在对话框中点击“确定”后,更新 MainWindow
中的参数并保存到配置文件。
在 mainwindow.cpp
中实现 onSettingsClicked()
槽函数,该函数负责显示设置对话框并处理其返回结果。
代码清单: mainwindow.cpp
// 在文件顶部添加 settingsdialog.h 的头文件
#include "settingsdialog.h"// ... (保留原有代码)// --- 新增:实现参数设置按钮的槽函数 ---
void MainWindow::onSettingsClicked()
{SettingsDialog dlg(this);// 将当前的曝光参数设置到对话框中作为初始值dlg.setValues(m_rgbBrightness, m_infraredBrightness, m_uvcBrightness);// 以模态方式显示对话框,程序将在此处阻塞直到对话框关闭if (dlg.exec() == QDialog::Accepted) {// 如果用户点击了“确定”按钮// 从对话框获取更新后的值dlg.getValues(m_rgbBrightness, m_infraredBrightness, m_uvcBrightness);// 保存新的设置到INI文件saveSettings();statusBar()->showMessage(QStringLiteral("参数已更新并保存"), 3000);}
}
3.4.4 应用动态曝光值
最后一步是修改图像采集流程 processCaptureSequence()
,使其使用从配置中加载的 m_...Brightness
成员变量,而不是之前硬编码的局部变量。
代码清单: mainwindow.cpp
(修改 processCaptureSequence
)
// ... (保留原有代码)void MainWindow::processCaptureSequence()
{// 亮度参数int rgbBrightness = m_rgbBrightness;;int infraredBrightness = m_infraredBrightness;;int uvcBrightness = m_uvcBrightness;switch (m_captureStep){// --- 阶段1: 采集白光图像 ---case 0: // 开启白光statusBar()->showMessage(QStringLiteral("正在采集白光图像..."));m_pSamapi->SetCamLightParam(1, rgbBrightness, infraredBrightness, uvcBrightness);m_pCaptureTimer->start(100); // 延时100msbreak;case 1: // 设置相机白光模式m_pSamapi->SetCameraLen(1);m_pCaptureTimer->start(1500); // 延时1.5秒等待光源稳定break;case 2: // 捕获图像m_pCap->capture();// 等待 onImageCaptured 信号被触发后,再推进流程return;// --- 阶段2: 采集红外图像 ---case 3: // 开启红外光statusBar()->showMessage(QStringLiteral("正在采集红外图像..."));m_pSamapi->SetCamLightParam(2, rgbBrightness, infraredBrightness, uvcBrightness);m_pCaptureTimer->start(100);break;case 4: // 设置相机红外模式m_pSamapi->SetCameraLen(2);m_pCaptureTimer->start(1500);break;case 5: // 捕获图像m_pCap->capture();return;// --- 阶段3: 采集紫外图像 ---case 6: // 开启紫外光statusBar()->showMessage(QStringLiteral("正在采集紫外图像..."));m_pSamapi->SetCamLightParam(3, rgbBrightness, infraredBrightness, uvcBrightness);m_pCaptureTimer->start(100);break;case 7: // 设置相机紫外模式m_pSamapi->SetCameraLen(3);m_pCaptureTimer->start(1500);break;case 8: // 捕获图像m_pCap->capture();return;// --- 阶段4: 结束采集 ---case 9:m_pSamapi->SetCamLightParam(0, rgbBrightness, infraredBrightness, uvcBrightness); // 关闭所有灯statusBar()->showMessage(QStringLiteral("采集完成"), 3000);break;}// 推进状态机m_captureStep++;
}
完成以上所有修改后,重新构建并运行项目。点击工具栏上的“参数设置”按钮,将弹出如下所示的对话框。修改数值并点击“确定”后,新的曝光参数将在下一次图像采集中生效,并且会被自动保存,程序下次启动时依然有效。
3.5 图像自动矫正和裁剪
在证件图像采集中,由于物理放置的微小差异,采集到的图像往往存在轻微的旋转角度和不必要的背景边距。这些瑕疵不仅影响视觉美观,更会干扰后续OCR(光学字符识别)算法的准确性。为了解决这一问题,本节将引入基于OpenCV的图像处理功能,实现对采集图像的自动矫正与精确裁剪。
该功能的核心逻辑将在“采集反面”流程的最后阶段被触发。系统会首先确认全部六张(三张正面,三张反面)多光谱图像均已采集完毕。随后,利用红外(IR)图像颜色单一、轮廓分明的特点,将其作为基准进行处理:
- 计算变换参数:对正面红外图像进行轮廓检测,找到证件主体的最小外接矩形,并据此计算出将其旋转至水平位置所需的仿射变换矩阵。
- 应用变换:将此矩阵应用于正面的白光、红外、紫外三张图像,实现同步的旋转矫正和背景去除。
- 重复流程:对反面图像集执行相同的操作,即以反面红外图像为基准计算新的变换矩阵,并应用于整个反面图像集。
此方法确保了同一面的所有光谱图像都经过了完全一致的空间变换,保持了其内在的对齐关系。此外,考虑到不同国家或地区的证件版式可能存在差异(如正面竖版,反面横版),最终的图像拼接显示逻辑也将进行相应优化,以自适应不同尺寸和方向的图像。
3.5.1 集成OpenCV库
OpenCV 是一个功能强大的开源计算机视觉和机器学习软件库,包含了实现图像矫正所需的全部算法。要在Qt项目中使用它,首先需要完成库的集成配置。
1. 准备OpenCV文件
首先,下载并解压OpenCV 4.12.0版本。为便于项目管理,在项目根目录下(与CardCheck.pro
同级)创建一个名为opencv
的文件夹。然后,从解压后的OpenCV目录中,将build\include
文件夹和build\x64\vc16\lib
文件夹完整地复制到新建的opencv
文件夹中。同时,将build\x64\vc16\bin
目录下的opencv_world412.dll
动态链接库文件复制到Qt项目的构建输出目录(例如build-CardCheck-Desktop_Qt_5_15_2_MSVC2019_64bit-Release
)。
最终的项目目录结构应如下所示:
CardCheck/
├── CardCheck.pro
├── ...
└── opencv/├── include/ <-- OpenCV头文件└── lib/ <-- OpenCV库文件
2. 配置项目文件 (.pro)
打开CardCheck.pro
文件,在末尾添加以下配置代码,以告知qmake编译器在哪里寻找OpenCV的头文件和需要链接的库文件。
代码清单:CardCheck.pro
# ... (文件中已有的其他配置)# ----------------- OpenCV 库配置 -----------------
# 1. 指定OpenCV头文件的搜索路径
INCLUDEPATH += $$PWD/opencv/include# 2. 链接OpenCV库文件
LIBS += -L$$PWD/opencv/lib -lopencv_world4120
完成配置后,重新构建项目以使设置生效。如果无编译或链接错误,则表明OpenCV已成功集成。
3.5.2 创建图像处理模块
为了保持代码的模块化和可维护性,所有与OpenCV相关的图像处理功能将被封装在一个独立的工具类ImageProcessor
中。
1. 创建 ImageProcessor
类
在Qt Creator中,通过“添加新文件…”创建一个新的C++类,命名为ImageProcessor
,基类选择QObject
。
2. 编写头文件 imageprocessor.h
该头文件将声明图像矫正的核心方法以及QImage
与cv::Mat
之间相互转换的静态辅助函数。
代码清单: imageprocessor.h
#ifndef IMAGEPROCESSOR_H
#define IMAGEPROCESSOR_H#include <QObject>
#include <QImage>
#include <opencv2/opencv.hpp>class ImageProcessor : public QObject
{Q_OBJECT
public:explicit ImageProcessor(QObject *parent = nullptr);// 核心功能:矫正单张图像并输出变换矩阵cv::Mat correctDocument(const QImage& inputImage, bool& success, cv::Mat& outTransformMatrix, cv::Rect& boundingBox);// 静态辅助函数static cv::Mat qimageToMat(const QImage& image);static QImage matToQimage(const cv::Mat& mat);
};#endif // IMAGEPROCESSOR_H
3. 实现源文件 imageprocessor.cpp
在该文件中,实现具体的图像格式转换和矫正算法。矫正算法遵循“灰度化 -> 模糊 -> 二值化 -> 轮廓发现 -> 最小外接矩形 -> 仿射变换”的经典流程。
代码清单: imageprocessor.cpp
#include "imageprocessor.h"
#include <QDebug>ImageProcessor::ImageProcessor(QObject *parent) : QObject(parent)
{
}cv::Mat ImageProcessor::correctDocument(const QImage& inputImage, bool& success, cv::Mat& outTransformMatrix,cv::Rect& boundingBox)
{success = false;cv::Mat imageMat = qimageToMat(inputImage);if (imageMat.empty()) return cv::Mat();// 1. 预处理:转换为灰度图cv::Mat gray;if (imageMat.channels() == 4) {cv::cvtColor(imageMat, gray, cv::COLOR_BGRA2GRAY);} else if (imageMat.channels() == 3) {cv::cvtColor(imageMat, gray, cv::COLOR_BGR2GRAY);} else {gray = imageMat.clone();}// 2. 边缘增强与二值化cv::Mat blurred, thresh;cv::GaussianBlur(gray, blurred, cv::Size(5, 5), 0);// 使用自适应阈值或一个相对鲁棒的固定阈值cv::threshold(blurred, thresh, 50, 255, cv::THRESH_BINARY);// 3. 查找轮廓std::vector<std::vector<cv::Point>> contours;cv::findContours(thresh, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);if (contours.empty()) {qDebug() << "Correction failed: No contours found.";return cv::Mat();}// 4. 找到面积最大的轮廓,假设其为目标证件auto max_contour_it = std::max_element(contours.begin(), contours.end(),[](const auto& a, const auto& b) {return cv::contourArea(a) < cv::contourArea(b);});// 5. 获取最大轮廓的最小外接旋转矩形cv::RotatedRect rect = cv::minAreaRect(*max_contour_it);// 6. 计算旋转角度和仿射变换矩阵float angle = rect.angle;// 调整角度,使图像摆正if (angle > 45.0f) {angle = -90.0f + angle;}outTransformMatrix = cv::getRotationMatrix2D(rect.center, angle, 1.0);// 7. 应用仿射变换(旋转)cv::Mat rotated;cv::warpAffine(imageMat, rotated, outTransformMatrix, imageMat.size(), cv::INTER_CUBIC);// 8. 在旋转后的图像上重新计算边界框并裁剪std::vector<cv::Point2f> boxPoints(4);rect.points(boxPoints.data());cv::transform(boxPoints, boxPoints, outTransformMatrix);cv::Rect boundingBox = cv::boundingRect(boxPoints);// 边界检查,防止裁剪区域超出图像范围boundingBox &= cv::Rect(0, 0, rotated.cols, rotated.rows);cv::Mat cropped = rotated(boundingBox);success = true;return cropped;
}// QImage to cv::Mat
cv::Mat ImageProcessor::qimageToMat(const QImage& image)
{cv::Mat mat;switch (image.format()){case QImage::Format_ARGB32:case QImage::Format_RGB32:case QImage::Format_ARGB32_Premultiplied:mat = cv::Mat(image.height(), image.width(), CV_8UC4, (void*)image.constBits(), image.bytesPerLine());break;case QImage::Format_RGB888:mat = cv::Mat(image.height(), image.width(), CV_8UC3, (void*)image.constBits(), image.bytesPerLine());cv::cvtColor(mat, mat, cv::COLOR_RGB2BGR);break;case QImage::Format_Indexed8:mat = cv::Mat(image.height(), image.width(), CV_8UC1, (void*)image.constBits(), image.bytesPerLine());break;default:// 不支持的格式,先转换为支持的格式QImage converted = image.convertToFormat(QImage::Format_ARGB32);mat = cv::Mat(converted.height(), converted.width(), CV_8UC4, (void*)converted.constBits(), converted.bytesPerLine());}return mat;
}// cv::Mat to QImage
QImage ImageProcessor::matToQimage(const cv::Mat& mat)
{if (mat.type() == CV_8UC4) {return QImage(mat.data, mat.cols, mat.rows, static_cast<int>(mat.step), QImage::Format_ARGB32);}if (mat.type() == CV_8UC3) {return QImage(mat.data, mat.cols, mat.rows, static_cast<int>(mat.step), QImage::Format_RGB888).rgbSwapped();}if (mat.type() == CV_8UC1) {return QImage(mat.data, mat.cols, mat.rows, static_cast<int>(mat.step), QImage::Format_Grayscale8);}return QImage();
}
3.5.3 实现矫正工作流
现在,将ImageProcessor
集成到MainWindow
中,并在采集流程的末尾调用它。
1. 更新 mainwindow.h
在头文件中包含imageprocessor.h
,并声明图像处理器实例、用于存储矫正后图像的列表以及执行矫正操作的新方法。
代码清单: mainwindow.h
// ... (保留原有 #include)
#include "imageprocessor.h" // <-- 新增// ... (保留原有类前置声明)class MainWindow : public QMainWindow
{// ... (保留 Q_OBJECT 等)
public:// ... (保留构造/析构函数)private slots:// ... (保留原有槽函数)private:// ... (保留原有私有方法)void performCorrectionAndCropping(); // <-- 新增:执行矫正与裁剪// ... (保留原有成员变量)// --- 新增图像处理相关成员 ---ImageProcessor *m_pImgProcessor; // 图像处理器QList<QImage> m_correctedFrontImages; // 存储矫正后的正面图像QList<QImage> m_correctedBackImages; // 存储矫正后的反面图像
};
2. 修改 mainwindow.cpp
在构造函数中实例化ImageProcessor
,并修改采集流程以触发矫正。
代码清单: mainwindow.cpp
(构造函数)
// ... (保留原有 #include)
#include "imageprocessor.h"MainWindow::MainWindow(QWidget *parent): QMainWindow(parent)// ... (保留原有初始化列表)
{ui->setupUi(this);m_pImgProcessor = new ImageProcessor(this); // <-- 新增:实例化处理器// ... (保留原有代码)
}
接下来,修改采集序列状态机processCaptureSequence()
。在case 9
(采集结束)中,增加一个判断:如果当前采集的是反面,则启动矫正流程;否则,仅正常结束。
代码清单: mainwindow.cpp
(修改 processCaptureSequence
)
// ... (保留原有 processCaptureSequence 代码)// --- 阶段4: 结束采集 ---case 9:m_pSamapi->SetCamLightParam(0, rgbBrightness, infraredBrightness, uvcBrightness); // 关闭所有灯if (!m_isCapturingFront) { // 如果是采集反面结束statusBar()->showMessage(QStringLiteral("采集完成,正在进行图像矫正..."), 3000);performCorrectionAndCropping(); // <-- 调用矫正} else {statusBar()->showMessage(QStringLiteral("正面采集完成"), 3000);}break;}// 推进状态机m_captureStep++;
}
最后,实现performCorrectionAndCropping()
方法。该方法是连接采集与显示的桥梁,它调用ImageProcessor
完成所有图像的矫正,并将结果存入新的成员变量中,最后调用updateDisplayImage()
刷新界面。
代码清单: mainwindow.cpp
(实现矫正方法)
// ... (保留原有代码)void MainWindow::performCorrectionAndCropping()
{// 1. 安全检查:确保6张图都已采集if (m_frontImages.size() < 3 || m_backImages.size() < 3) {QMessageBox::warning(this, "图像不完整", "未能采集全部6张图像,无法进行矫正。");return;}// 清空旧的矫正结果m_correctedFrontImages.clear();m_correctedBackImages.clear();bool success = false;cv::Mat transformMatrix;cv::Rect boundingBox;// --- 2. 处理正面图像 ---// 以正面红外图为基准进行矫正cv::Mat correctedFrontIr = m_pImgProcessor->correctDocument(m_frontImages[1], success, transformMatrix, boundingBox);if (!success) {QMessageBox::critical(this, "处理失败", "正面图像矫正失败,请检查图像质量。");return;}// 应用相同的变换到白光和紫外光图像cv::Mat frontWhiteMat = ImageProcessor::qimageToMat(m_frontImages[0]);cv::Mat frontUvMat = ImageProcessor::qimageToMat(m_frontImages[2]);cv::Mat correctedFrontWhite, correctedFrontUv;cv::warpAffine(frontWhiteMat, correctedFrontWhite, transformMatrix, frontWhiteMat.size(), cv::INTER_CUBIC);cv::warpAffine(frontUvMat, correctedFrontUv, transformMatrix, frontUvMat.size(), cv::INTER_CUBIC);// 添加到结果列表m_correctedFrontImages.append(ImageProcessor::matToQimage(correctedFrontWhite(boundingBox)).copy());m_correctedFrontImages.append(ImageProcessor::matToQimage(correctedFrontIr).copy());m_correctedFrontImages.append(ImageProcessor::matToQimage(correctedFrontUv(boundingBox)).copy());// --- 3. 处理反面图像 ---// 以反面红外图为基准cv::Mat correctedBackIr = m_pImgProcessor->correctDocument(m_backImages[1], success, transformMatrix,boundingBox);if (!success) {QMessageBox::critical(this, "处理失败", "反面图像矫正失败,请检查图像质量。");return;}// 应用变换cv::Mat backWhiteMat = ImageProcessor::qimageToMat(m_backImages[0]);cv::Mat backUvMat = ImageProcessor::qimageToMat(m_backImages[2]);cv::Mat correctedBackWhite, correctedBackUv;cv::warpAffine(backWhiteMat, correctedBackWhite, transformMatrix, backWhiteMat.size(), cv::INTER_CUBIC);cv::warpAffine(backUvMat, correctedBackUv, transformMatrix, backUvMat.size(), cv::INTER_CUBIC);m_correctedBackImages.append(ImageProcessor::matToQimage(correctedBackWhite(boundingBox)).copy());m_correctedBackImages.append(ImageProcessor::matToQimage(correctedBackIr).copy());m_correctedBackImages.append(ImageProcessor::matToQimage(correctedBackUv(boundingBox)).copy());// 4. 更新UI显示updateDisplayImage();statusBar()->showMessage("图像矫正完成", 3000);
}
3.5.4 优化最终显示逻辑
矫正后的图像尺寸不再统一,updateDisplayImage
方法必须重构,以灵活应对不同尺寸的图像,并智能计算画布大小。
代码清单: mainwindow.cpp
(重构 updateDisplayImage
)
void MainWindow::updateDisplayImage()
{// 步骤 1: 确定要显示的图像源(优先使用矫正后的图像)const QList<QImage>& frontImages = m_correctedFrontImages.isEmpty() ? m_frontImages : m_correctedFrontImages;const QList<QImage>& backImages = m_correctedBackImages.isEmpty() ? m_backImages : m_correctedBackImages;// 如果没有任何图像,则直接返回if (frontImages.isEmpty() && backImages.isEmpty()) {return;}// 步骤 2: 计算标准单元格尺寸int margin = 2; // 图像与边框的间距int textHeight = 30; // 顶部标签文本区域的高度float scaleFactor = 0.2f; // 图像缩放因子,防止图像太大占用太高内存int standardWidth = 0;int standardHeight = 0;// 遍历所有图像,找出缩放后的最大宽度和高度,作为标准单元格尺寸for (const QImage& img : frontImages) {standardWidth = qMax(standardWidth, static_cast<int>(img.width() * scaleFactor));standardHeight = qMax(standardHeight, static_cast<int>(img.height() * scaleFactor));}for (const QImage& img : backImages) {standardWidth = qMax(standardWidth, static_cast<int>(img.width() * scaleFactor));standardHeight = qMax(standardHeight, static_cast<int>(img.height() * scaleFactor));}// 步骤 3: 根据标准单元格尺寸计算总画布大小int canvasWidth = standardWidth * 3 + margin * 4;int canvasHeight = margin; // 顶部边距if (!frontImages.isEmpty()) {canvasHeight += textHeight + standardHeight + margin;}if (!backImages.isEmpty()) {canvasHeight += textHeight + standardHeight + margin;}// 创建画布并填充背景色QPixmap canvas(canvasWidth, canvasHeight);canvas.fill(QColor("#F0F8FF"));QPainter painter(&canvas);painter.setRenderHint(QPainter::Antialiasing, true);painter.setRenderHint(QPainter::TextAntialiasing, true);painter.setFont(QFont("Microsoft YaHei", 12, QFont::Bold));QString frontLabels[] = {QStringLiteral("正面-白光"), QStringLiteral("正面-红外"), QStringLiteral("正面-紫外")};QString backLabels[] = {QStringLiteral("反面-白光"), QStringLiteral("反面-红外"), QStringLiteral("反面-紫外")};// 步骤 4: 绘制正面图像行if (!frontImages.isEmpty()) {int currentY = margin;for (int i = 0; i < frontImages.size(); ++i) {// 计算当前单元格的X坐标int currentX = margin * (i + 1) + standardWidth * i;// 绘制文本标签painter.setPen(Qt::black);painter.drawText(currentX, currentY, standardWidth, textHeight, Qt::AlignCenter, frontLabels[i]);// 定义单元格的绘图区域QRect cellRect(currentX, currentY + textHeight, standardWidth, standardHeight);// 将图像按比例缩放以适应单元格QImage scaledImage = frontImages[i].scaled(cellRect.size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);// 计算图像在单元格内居中显示的左上角坐标int drawX = cellRect.x() + (cellRect.width() - scaledImage.width()) / 2;int drawY = cellRect.y() + (cellRect.height() - scaledImage.height()) / 2;// 绘制单元格边框painter.setPen(QColor("#C0C8D0"));painter.drawRect(cellRect);// 在计算好的居中位置绘制图像painter.drawImage(drawX, drawY, scaledImage);}}// 步骤 5: 绘制反面图像行if (!backImages.isEmpty()) {// 计算第二行的起始Y坐标int currentY = margin * 2 + textHeight + standardHeight;for (int i = 0; i < backImages.size(); ++i) {int currentX = margin * (i + 1) + standardWidth * i;painter.setPen(Qt::black);painter.drawText(currentX, currentY, standardWidth, textHeight, Qt::AlignCenter, backLabels[i]);QRect cellRect(currentX, currentY + textHeight, standardWidth, standardHeight);QImage scaledImage = backImages[i].scaled(cellRect.size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);int drawX = cellRect.x() + (cellRect.width() - scaledImage.width()) / 2;int drawY = cellRect.y() + (cellRect.height() - scaledImage.height()) / 2;painter.setPen(QColor("#C0C8D0"));painter.drawRect(cellRect);painter.drawImage(drawX, drawY, scaledImage);}}painter.end();// 步骤 6: 将最终合成的画布显示在UI上imageLabel->setPixmap(canvas.scaled(imageLabel->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
}
最后,修改"采集正面"和“采集反面”按钮的槽函数,添加清空代码
// "采集正面"按钮的槽函数
void MainWindow::onCaptureFrontClicked()
{if (!m_isCameraReady || m_pCaptureTimer->isActive()) {statusBar()->showMessage(QStringLiteral("设备正忙,请稍候..."), 3000);return;}m_isCapturingFront = true;m_frontImages.clear(); m_backImages.clear();m_correctedFrontImages.clear();//新增,清空矫正后的正面图像m_correctedBackImages.clear();//新增,清空矫正后的反面图像m_captureStep = 0; processCaptureSequence();
}// "采集反面"按钮的槽函数
void MainWindow::onCaptureBackClicked()
{if (!m_isCameraReady || m_pCaptureTimer->isActive()) {statusBar()->showMessage(QStringLiteral("设备正忙,请稍候..."), 3000);return;}m_isCapturingFront = false;m_backImages.clear(); m_correctedFrontImages.clear();//新增,清空矫正后的正面图像m_correctedBackImages.clear();//新增,清空矫正后的反面图像m_captureStep = 0; processCaptureSequence();
}
至此,图像自动矫正和裁剪功能已全部实现。完成“采集反面”后,系统将自动对所有图像进行处理,并呈现出如下图所示的、背景干净且对齐规整的证件图像,为后续的识别模块提供了高质量的数据输入,如下图所示:
3.5.5 优化保存逻辑:优先保存矫正图像
在实现了图像的自动矫正与裁剪之后,系统中同时存在着原始采集图像(存储于 m_frontImages
, m_backImages
)和经过处理的矫正后图像(存储于 m_correctedFrontImages
, m_correctedBackImages
)。矫正后的图像质量更高,背景更干净,更适合作为最终的存档数据。因此,需要对图像保存功能进行优化,使其在用户点击“保存”按钮时,能够智能地判断并优先保存矫正后的图像版本。
本次优化的核心是对 onSaveClicked()
槽函数进行修改。该函数在启动后台保存任务前,将首先检查是否存在矫正后的图像。如果存在,则将矫正后的图像列表传递给后台线程进行保存;如果不存在(例如,用户只采集了正面图像而未触发矫正流程),则回退至保存原始采集的图像。这样既保证了保存数据的最优质量,也兼容了所有可能的操作场景。后台的 performSaveImages
方法由于其参数化设计,无需任何改动即可支持这一新逻辑。
代码清单: mainwindow.cpp
(更新 onSaveClicked
槽函数)
// 点击“保存”按钮的槽函数 - 启动后台保存任务
void MainWindow::onSaveClicked()
{// --- 新增:智能选择要保存的图像源 ---// 优先选择矫正后的图像,如果矫正图像列表为空,则回退到原始图像const QList<QImage>& frontImagesToSave = m_correctedFrontImages.isEmpty() ? m_frontImages : m_correctedFrontImages;const QList<QImage>& backImagesToSave = m_correctedBackImages.isEmpty() ? m_backImages : m_correctedBackImages;// 1. 检查是否存在可供保存的图像if (frontImagesToSave.isEmpty() && backImagesToSave.isEmpty()) {QMessageBox::warning(this, QStringLiteral("图像未采集"), QStringLiteral("请先采集图像后再保存。"));return;}// 2. 检查是否已有保存任务正在进行if (m_pSaveWatcher->isRunning()) {statusBar()->showMessage(QStringLiteral("正在保存中,请稍候..."), 3000);return;}// 3. 禁用保存按钮,防止重复点击saveAct->setEnabled(false);statusBar()->showMessage(QStringLiteral("正在保存图像至桌面..."), 0); // 持续显示状态// 4. 准备保存路径 (这部分逻辑不耗时,在主线程完成)QString desktopPath = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);QString baseDirName = "CardImages";QString dateDirName = QDateTime::currentDateTime().toString("yyyy-MM-dd");QDir baseDir(desktopPath);if (!baseDir.exists(baseDirName)) {baseDir.mkdir(baseDirName);}QDir dateDir(desktopPath + "/" + baseDirName);if (!dateDir.exists(dateDirName)) {dateDir.mkdir(dateDirName);}QString savePath = dateDir.filePath(dateDirName);// 5. 使用 QtConcurrent::run 启动后台任务// 将选择好的图像列表作为值传递,确保线程安全QFuture<bool> future = QtConcurrent::run(this, &MainWindow::performSaveImages, frontImagesToSave, backImagesToSave, savePath);m_pSaveWatcher->setFuture(future);
}
完成上述修改后,重新编译并运行程序。当完成正反面采集和自动矫正后,点击“保存”按钮,系统将自动把经过处理的、效果最佳的图像保存到指定目录。
3.5.6 优化处理:图像尺寸标准化
经过自动矫正与裁剪后,虽然图像质量得到了显著提升,但其分辨率仍然维持在采集时的原始高水平。高分辨率图像在占用较大磁盘空间的同时,也会在上传至后端服务器进行智能识别时消耗不必要的网络带宽,增加传输延迟。为了在保证识别精度的前提下优化系统性能,需要引入一个图像尺寸标准化的步骤。
该优化的核心逻辑是:在图像裁剪完成后,对所有处理过的图像进行一次统一的缩放操作,确保其最长边不超过一个固定的像素值(例如800像素)。具体规则如下:
- 对于横向图像(宽度大于高度),将其宽度等比例缩放至800像素。
- 对于纵向或方形图像(宽度小于或等于高度),将其高度等比例缩放至800像素。
通过这一处理,可以在不破坏图像原有宽高比和关键信息的前提下,大幅减小图像的文件大小,从而加快后续的保存和网络传输速度。
此功能将在performCorrectionAndCropping()
方法的末尾,更新UI显示之前实现。
代码清单: mainwindow.cpp
(更新 performCorrectionAndCropping
方法)
void MainWindow::performCorrectionAndCropping()
{// 1. 安全检查:确保6张图都已采集if (m_frontImages.size() < 3 || m_backImages.size() < 3) {QMessageBox::warning(this, "图像不完整", "未能采集全部6张图像,无法进行矫正。");return;}// 清空旧的矫正结果m_correctedFrontImages.clear();m_correctedBackImages.clear();bool success = false;cv::Mat transformMatrix;cv::Rect boundingBox;// --- 2. 处理正面图像 (此部分代码保持不变) ---// 以正面红外图为基准进行矫正cv::Mat correctedFrontIr = m_pImgProcessor->correctDocument(m_frontImages[1], success, transformMatrix, boundingBox);if (!success) {QMessageBox::critical(this, "处理失败", "正面图像矫正失败,请检查图像质量。");return;}// 应用相同的变换到白光和紫外光图像cv::Mat frontWhiteMat = ImageProcessor::qimageToMat(m_frontImages[0]);cv::Mat frontUvMat = ImageProcessor::qimageToMat(m_frontImages[2]);cv::Mat correctedFrontWhite, correctedFrontUv;cv::warpAffine(frontWhiteMat, correctedFrontWhite, transformMatrix, frontWhiteMat.size(), cv::INTER_CUBIC);cv::warpAffine(frontUvMat, correctedFrontUv, transformMatrix, frontUvMat.size(), cv::INTER_CUBIC);// 添加到结果列表m_correctedFrontImages.append(ImageProcessor::matToQimage(correctedFrontWhite(boundingBox)).copy());m_correctedFrontImages.append(ImageProcessor::matToQimage(correctedFrontIr).copy());m_correctedFrontImages.append(ImageProcessor::matToQimage(correctedFrontUv(boundingBox)).copy());// --- 3. 处理反面图像 (此部分代码保持不变) ---// 以反面红外图为基准cv::Mat correctedBackIr = m_pImgProcessor->correctDocument(m_backImages[1], success, transformMatrix,boundingBox);if (!success) {QMessageBox::critical(this, "处理失败", "反面图像矫正失败,请检查图像质量。");return;}// 应用变换cv::Mat backWhiteMat = ImageProcessor::qimageToMat(m_backImages[0]);cv::Mat backUvMat = ImageProcessor::qimageToMat(m_backImages[2]);cv::Mat correctedBackWhite, correctedBackUv;cv::warpAffine(backWhiteMat, correctedBackWhite, transformMatrix, backWhiteMat.size(), cv::INTER_CUBIC);cv::warpAffine(backUvMat, correctedBackUv, transformMatrix, backUvMat.size(), cv::INTER_CUBIC);m_correctedBackImages.append(ImageProcessor::matToQimage(correctedBackWhite(boundingBox)).copy());m_correctedBackImages.append(ImageProcessor::matToQimage(correctedBackIr).copy());m_correctedBackImages.append(ImageProcessor::matToQimage(correctedBackUv(boundingBox)).copy());// --- 4. 新增:图像尺寸标准化 ---// 统一将最长边缩放至800像素,以优化存储和传输for (int i = 0; i < m_correctedFrontImages.size(); ++i) {QImage img = m_correctedFrontImages[i];if (img.width() > img.height()) {m_correctedFrontImages[i] = img.scaledToWidth(800, Qt::SmoothTransformation);} else {m_correctedFrontImages[i] = img.scaledToHeight(800, Qt::SmoothTransformation);}}for (int i = 0; i < m_correctedBackImages.size(); ++i) {QImage img = m_correctedBackImages[i];if (img.width() > img.height()) {m_correctedBackImages[i] = img.scaledToWidth(800, Qt::SmoothTransformation);} else {m_correctedBackImages[i] = img.scaledToHeight(800, Qt::SmoothTransformation);}}// 5. 更新UI显示updateDisplayImage();statusBar()->showMessage("图像矫正与尺寸标准化完成", 3000);
}
然后修改updateDisplayImage函数,对其中的缩放因子进行调整。
void MainWindow::updateDisplayImage()
{// 修改 -图像缩放因子float scaleFactor = 0.2f; // 图像缩放因子,防止图像太大占用太高内存if(m_correctedFrontImages.isEmpty())scaleFactor = 0.2f;elsescaleFactor = 1.0f;// 其余代码不变
系统现已具备从多光谱设备采集图像,到自动完成矫正、裁剪、尺寸标准化并进行本地保存的全套功能,为后续的智能识别模块提供了高质量、标准化的数据输入。
3.5.7 修复图像显示区域背景
在3.2.3节的界面美化中,通过Qt样式表(QSS)为QLabel
控件统一设置了白色背景、灰色边框和圆角,旨在美化主界面的图像展示区。具体的样式规则如下:
/* 图像显示区域标签样式 */
QLabel {background-color: #FFFFFF; /* 白色背景 */border: 1px solid #C0C8D0;border-radius: 4px;
}
然而,这种全局性的类型选择器(QLabel
)会无差别地应用于程序中所有的QLabel
实例。这导致了一个预期之外的副作用:在3.4.1节创建的“参数设置”对话框中,用于提示用户的文本标签(如“白光曝光强度:”)也继承了这套样式,这并非设计的初衷,且影响了界面的协调性。
为了解决这个问题,需要将样式规则的应用范围精确限定在主界面的imageLabel
控件上,避免影响其他QLabel
。最高效的解决方案是利用Qt的对象名称(Object Name)属性和QSS中的ID选择器。
ID选择器(#objectName
)允许样式规则仅对具有特定对象名称的控件生效,从而实现像素级的精确定制。该过程分为两步:
1. 为目标控件设置唯一的对象名称
在MainWindow
类中创建中心部件的方法createCentralWidget()
里,为imageLabel
成员变量设置一个唯一的对象名称,例如imageDisplayLabel
。
打开mainwindow.cpp
文件,找到createCentralWidget
函数,并添加setObjectName()
调用。
代码清单: mainwindow.cpp
(更新 createCentralWidget
)
// 创建中心部件
void MainWindow::createCentralWidget()
{// 创建一个水平分割器QSplitter *mainSplitter = new QSplitter(Qt::Horizontal, this);// 创建左侧图像展示区(使用QLabel作为占位符)imageLabel = new QLabel(QStringLiteral("图像展示区域"), mainSplitter);imageLabel->setObjectName("imageDisplayLabel"); // <-- 新增:设置唯一的对象名称imageLabel->setAlignment(Qt::AlignCenter); imageLabel->setFrameShape(QFrame::StyledPanel);// 创建右侧识别结果展示区resultTextEdit = new QTextEdit(mainSplitter);resultTextEdit->setPlaceholderText(QStringLiteral("识别结果将在此处显示..."));resultTextEdit->setReadOnly(true); // 将控件添加到分割器中mainSplitter->addWidget(imageLabel);mainSplitter->addWidget(resultTextEdit);// 设置初始宽度比例为80:20mainSplitter->setSizes(QList<int>() << 800 << 200);// 将分割器设置为主窗口的中心部件this->setCentralWidget(mainSplitter);
}
2. 修改QSS文件以使用ID选择器
接下来,打开项目资源文件中的style.qss
,将原有的QLabel
类型选择器修改为#imageDisplayLabel
ID选择器。
代码清单: style.qss
/* ... (保留其他样式规则) *//* 图像显示区域标签样式 - 使用ID选择器精确定位 */
QLabel#imageDisplayLabel {background-color: #FFFFFF; /* 白色背景 */border: 1px solid #C0C8D0;border-radius: 4px;
}/* ... (保留其他样式规则) */
完成以上两步修改后,重新编译并运行项目。此时,主界面的图像展示区域imageLabel
依然保持着期望的白色背景和边框样式,而当打开“参数设置”对话框时,其中的QLabel
控件将恢复其默认的、无背景和边框的原始外观。
通过这种方式,成功地将样式的作用域限定在了预期的控件上,解决了样式污染问题,增强了UI代码的健壮性和可维护性。
四、小结
本文详尽阐述了“证照智能识别系统”客户端图像采集模块的完整开发流程。从基础的Qt项目创建与环境配置开始,逐步实现了专业的用户界面设计与QSS美化、多光谱硬件SDK的集成、基于QTimer
的非阻塞式图像采集、以及利用QtConcurrent
实现的异步图像保存。为确保后续识别的准确性,还引入了OpenCV库,开发了图像自动矫正、裁剪及尺寸标准化的核心功能,最终构建出一个能够稳定输出高质量、标准化证件图像的客户端应用程序,为后续的智能识别环节奠定了坚实的数据基础。
目前,客户端已具备完善的图像采集与预处理能力,但其最终目标是与后端服务联动,完成证件的智能识别。因此,本系列博客的下一篇文章将聚焦于客户端功能的扩展与和服务端的通信。具体内容将包括:在Qt界面中集成国家代码选择功能,允许操作员预先指定证件的国别,从而缩小后端服务的检索范围,提升识别效率;同时,将详细讲解如何通过HTTP协议,将预处理后的多光谱图像数据上传至FastAPI后端,并接收和解析返回的识别结果,最终将其呈现在用户界面上,完成整个业务流程的闭环。