目錄
- 一、概述
- 1.1 背景介紹:賦予應用“視力”
- 1.2 學習目標
- 二、集成OpenCV
- 2.1 安裝OpenCV
- 2.2 在Qt項目中配置CMake
- 三、項目數據集介紹與準備
- 四、圖像的橋梁:`ImageProvider`與格式轉換
- 五、加載、轉換并顯示圖像
- 六、總結與展望
一、概述
1.1 背景介紹:賦予應用“視力”
在此前的文章中,我們已經成功搭建了UI與邏輯之間的通信橋梁。現在,我們的應用程序擁有了美觀的“面孔”和響應迅速的“神經系統”。然而,作為一個視覺檢測應用,它還缺少最核心的器官——“眼睛”。
本篇文章的核心任務,就是為我們的應用安裝一雙強大的“眼睛”——集成全球最流行的開源計算機視覺庫OpenCV (Open Source Computer Vision Library)。通過集成OpenCV,我們的程序將首次獲得處理和顯示真實圖像的能力,這是后續實現一切AI視覺功能的前提和基礎。
1.2 學習目標
通過本篇的學習,讀者將能夠:
- 在Windows環境下正確配置Qt項目以集成OpenCV庫。
- 掌握在C++中OpenCV的圖像數據結構
cv::Mat
與Qt的QImage
之間的相互轉換,這是兩者協同工作的關鍵。 - 實現從本地加載一張螺絲圖片,并通過我們架設好的前后端橋梁,最終在QML界面上成功顯示出來。
二、集成OpenCV
2.1 安裝OpenCV
首先,需要在開發環境中準備好OpenCV。
- 下載: 訪問OpenCV官網的發布頁面,下載適用于Windows的最新預編譯版本(例如 4.12.0)。
- 解壓: 運行下載的
.exe
文件,它實際上是一個自解壓程序。將其解壓到一個不含中文和空格的穩定路徑并重命名為opencv4.12.0,例如D:\toolplace\opencv4.12.0
。解壓后,關鍵的目錄是D:\toolplace\opencv4.12.0\build
。
2.2 在Qt項目中配置CMake
要讓我們的ScrewDetector
項目能夠找到并使用OpenCV,需要在CMakeLists.txt
中添加配置。這是集成任何第三方C++庫的標準流程。
1. 編寫代碼 (CMakeLists.txt)
打開項目根目錄下的CMakeLists.txt
文件,在find_package(Qt6 ...)
之后,添加以下代碼:
# --- 開始集成OpenCV ---
# 1. 設置OpenCV的根目錄,請根據您的實際安裝路徑修改
set(OpenCV_DIR "D:/toolplace/opencv4.12.0/build")# 2. 查找OpenCV包,Core和Imgproc是我們目前需要的核心和圖像處理模塊
find_package(OpenCV REQUIRED COMPONENTS core imgproc)# 3. 包含OpenCV的頭文件目錄,以便#include指令能夠找到它們
include_directories(${OpenCV_INCLUDE_DIRS})
# --- 結束集成OpenCV ---# ... (qt_add_executable等保持不變) ...# 在鏈接Qt庫之后,鏈接OpenCV庫
target_link_libraries(appScrewDetector PRIVATE# ... (原有的Qt6::Core, Qt6::Gui等)${OpenCV_LIBS} # 鏈接OpenCV庫
)
關鍵代碼分析:
(1) set(OpenCV_DIR ...)
: 這一行是關鍵,它明確告訴CMake去哪里尋找OpenCV的配置文件。路徑必須指向包含OpenCVConfig.cmake
文件的build
目錄。請務必根據自己的實際解壓路徑進行修改,并注意使用正斜杠/
。
(2) find_package(OpenCV REQUIRED ...)
: CMake會根據OpenCV_DIR
的路徑,查找OpenCV的配置,并加載其頭文件路徑、庫文件路徑等信息到CMake變量中(如OpenCV_INCLUDE_DIRS
和OpenCV_LIBS
)。
(3) include_directories(...)
和 target_link_libraries(...)
: 這兩行分別將OpenCV的頭文件目錄和庫文件添加到了我們項目的編譯和鏈接步驟中。
3. 驗證集成
在backend.h
的頂部添加以下兩行:
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
然后按Ctrl+B
重新構建項目。如果項目能成功編譯,沒有任何“找不到文件”的錯誤,則說明OpenCV已經成功集成!
三、項目數據集介紹與準備
【核心概念:數據是AI的燃料】
無論是傳統圖像處理還是現代AI,一切算法都離不開數據。一個高質量、標準化的數據集是項目成功的基石。從本章開始,我們將引入一個貫穿后續所有算法章節的公開數據集。
我們將采用 MVTec Anomaly Detection Dataset (MVTec AD)。這是一個用于異常檢測(在工業領域通常等同于瑕疵檢測)的行業基準數據集,由德國MVTec公司發布,質量極高且下載方便。它包含了多種工業品類,我們正好可以使用其中的**“螺絲(screw)”**類別。
- 官方網站:https://www.mvtec.com/company/research/datasets/mvtec-ad
1. 下載與組織數據集
-
從上述官網鏈接進入,填寫相關信息后跳轉到下載頁面,找到對應的
Screw
下載鏈接并下載screw.tar.xz
文件。這是一個壓縮包,需要使用支持.tar.xz
格式的解壓軟件(如7-Zip)進行解壓。
-
在我們的
ScrewDetector
項目根目錄(與CMakeLists.txt
同級)下,創建一個名為dataset
的文件夾。 -
將解壓后的
screw
文件夾完整地拷貝到這個dataset
文件夾中。
最終的項目目錄結構應如下所示:
ScrewDetector/
├── CMakeLists.txt
├── main.cpp
├── Main.qml
├── logo.rc
├── backend.h
├── backend.cpp
├── icons/
│ └── appicon.png
│ └── appicon.ico
└── dataset/ <-- 新建的數據集文件夾└── screw/ <-- 從壓縮包解壓出的文件夾├── train/│ └── good/│ ├── 000.png│ └── ...└── test/├── good/├── scratch_head/│ ├── 000.png│ └── ...└── ...
說明:將數據集放在項目源碼目錄之外,是一種良好的工程實踐,可以保持代碼倉庫的整潔,避免將龐大的數據文件包含進版本控制系統。
數據集部分樣本圖片如下:
四、圖像的橋梁:ImageProvider
與格式轉換
為了讓QML能夠顯示由C++動態加載的圖像,我們需要一個特殊的橋梁——QQuickImageProvider
。您可以把QQuickImageProvider
想象成一個內置在您應用中的、私有的、輕量級的“圖像服務器”。
- QML (客戶端): 像瀏覽器一樣,向一個特殊的URL地址發起請求,例如
image://liveImage/current_screw
。 QQuickImageProvider
(服務器): 它會接收到這個請求,解析出URL中的ID(current_screw
),然后在C++代碼中找到或生成對應的QImage
對象,并將其作為響應“發回”給QML。同時,我們還需要實現cv::Mat
到QImage
的轉換。
【例5-1】 創建ImageProvider
并實現格式轉換。
1. 創建ImageProvider
類
- 在Qt Creator中,右鍵點擊項目,
添加新文件...
->C++
->C++ Class
。- 類名:
ImageProvider
- 基類: 選擇
QObject
。
- 類名:
- 完成后,我們需要手動修改
imageprovider.h
和ImageProvider.cpp
使其繼承自QQuickImageProvider
。
2. 編寫代碼 (imageprovider.h)
#ifndef IMAGEPROVIDER_H
#define IMAGEPROVIDER_H#include <QQuickImageProvider>
#include <QImage>// 繼承自 QQuickImageProvider
class ImageProvider : public QQuickImageProvider
{
public:ImageProvider();// QML引擎會調用這個純虛函數來請求圖片QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override;// 一個公共方法,用于從C++的Backend更新圖片void updateImage(const QImage &image);private:// 用于存儲當前要顯示的圖像QImage m_image;
};#endif // IMAGEPROVIDER_H
3. 編寫代碼 (imageprovider.cpp)
#include "imageprovider.h"ImageProvider::ImageProvider()// 構造函數中必須指定Provider的類型: QQuickImageProvider(QQuickImageProvider::Image)
{
}QImage ImageProvider::requestImage(const QString &id, QSize *size, const QSize &requestedSize)
{Q_UNUSED(id);Q_UNUSED(requestedSize);if (size) {*size = m_image.size();}return m_image;
}void ImageProvider::updateImage(const QImage &image)
{if (m_image != image) {m_image = image;}
}
4. cv::Mat
到QImage
的轉換函數 (backend.cpp)
修改backend.cpp,添加圖像轉換函數:
// 在 backend.cpp 的頂部
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>static QImage matToQImage(const cv::Mat &mat) {if (mat.empty()) { return QImage(); }if (mat.type() == CV_8UC3) {return QImage(mat.data, mat.cols, mat.rows, mat.step, QImage::Format_RGB888).rgbSwapped();} else if (mat.type() == CV_8UC1) {return QImage(mat.data, mat.cols, mat.rows, mat.step, QImage::Format_Grayscale8);}return QImage();
}
五、加載、轉換并顯示圖像
現在,我們將把Backend
和ImageProvider
串聯起來,完成整個流程。
【例5-2】 改造Backend
以使用ImageProvider
。
1. 注冊ImageProvider
(main.cpp)
QML引擎必須“知道”我們的ImageProvider
的存在。
// main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QIcon>
#include <QQmlContext>
#include "backend.h"
#include "imageprovider.h" // 1. 包含ImageProvider頭文件int main(int argc, char *argv[])
{QGuiApplication app(argc, argv);app.setWindowIcon(QIcon(":/icons/appicon.png"));QQmlApplicationEngine engine;// 2. 實例化ImageProviderImageProvider *imageProvider = new ImageProvider();// 3. 將Provider注冊到QML引擎,并給它一個URL方案名,例如"liveImage"engine.addImageProvider(QLatin1String("liveImage"), imageProvider);// 4. 創建Backend實例時,將provider的指針傳給它Backend backend(imageProvider);engine.rootContext()->setContextProperty("backend", &backend);// ... (后續代碼不變)return app.exec();
}
2. 修改Backend (backend.h & backend.cpp)
Backend
通過OpenCV讀取圖像,轉換為QImage后更新ImageProvider
,然后通知QML去刷新圖像。
// backend.h
#ifndef BACKEND_H
#define BACKEND_H
#include <QObject>
#include <QString>
// 移除 #include <QImage>
class ImageProvider; // 使用前向聲明class Backend : public QObject
{Q_OBJECT
public:// 構造函數需要接收ImageProvider的指針explicit Backend(ImageProvider *provider, QObject *parent = nullptr);Q_INVOKABLE void startScan();
signals:// 信號不再傳遞QImage,只傳遞一個“刷新”指令和圖像IDvoid imageReady(const QString &imageId);void statusMessageChanged(const QString &message);
private:ImageProvider *m_imageProvider;
};
#endif // BACKEND_H
// backend.cpp
#include "backend.h"
#include "imageprovider.h" // 在cpp中包含完整的頭文件
#include <QDebug>
#include <QDir>
#include <opencv2/imgcodecs.hpp>// ... (matToQImage輔助函數)Backend::Backend(ImageProvider *provider, QObject *parent): QObject(parent), m_imageProvider(provider) // 在構造函數中保存指針
{}void Backend::startScan()
{qDebug() << "C++: Loading image with OpenCV...";emit statusMessageChanged("正在從數據集加載圖像...");// 1. 構建數據集圖片的絕對路徑// QDir::currentPath() 獲取的是構建目錄,需要向上兩級到項目根目錄// 我們從MVTec數據集中挑選一張帶瑕疵的圖片作為示例QString imagePath = QDir::currentPath() + "/../../dataset/screw/test/scratch_head/000.png";// 2. 使用OpenCV從文件系統加載圖像// 注意:imread需要一個標準C++字符串cv::Mat imageMat = cv::imread(imagePath.toStdString());if (imageMat.empty()) {qDebug() << "Error: Could not load image from path:" << imagePath;emit statusMessageChanged("錯誤:無法從數據集加載圖像!請檢查路徑。");return;}// 3. 將cv::Mat轉換為QImageQImage imageQ = matToQImage(imageMat);if (imageQ.isNull()){emit statusMessageChanged("錯誤:圖像格式轉換失敗!");return;}// 4. 更新ImageProvider中的圖像m_imageProvider->updateImage(imageQ);// 5. 發射信號,只告訴QML需要刷新的圖像IDemit imageReady("screw_sample");emit statusMessageChanged("圖像顯示成功!");
}
3. 修改QML以使用URL (Main.qml)
這是最后一步,讓QML的Image
組件使用我們自定義的image://
URL。
// Main.qml
import QtQuick
// ...Window {// ...Connections {target: backendfunction onStatusMessageChanged(message) {statusLabel.text = message;}function onImageReady(imageId) {// 1. 構建URL: "image://<provider_name>/<image_id>"// 2. 附加一個時間戳來強制刷新,避免QML使用緩存videoDisplay.source = "image://liveImage/" + imageId+ "?" + new Date().getTime();}}ColumnLayout {// ...Frame {id: videoFrame// ...Image {id: videoDisplayanchors.fill: parentfillMode: Image.PreserveAspectFitcache: false // 顯式禁用緩存,增加刷新可靠性}}// ...}
}
4. 運行結果
現在再次運行程序,點擊“開始檢測”按鈕。流程變為:
- QML: 調用
backend.startScan()
。 - C++
Backend
: 加載cv::Mat
-> 轉換為QImage
-> 調用m_imageProvider->updateImage()
將新圖像存入Provider -> 發射imageReady("screw_sample")
信號。 - QML
Connections
: 收到信號,執行onImageReady("screw_sample")
。 - QML
Image
:source
屬性被設置為"image://liveImage/screw_sample?..."
。 - QML引擎: 看到
image://
協議,將請求轉發給名為liveImage
的ImageProvider
。 - C++
ImageProvider
:requestImage()
被調用,它返回存儲在m_image
中的最新圖像。 - QML
Image
: 成功獲取并顯示圖像。
最終的運行效果如下:
六、總結與展望
在本篇文章中,我們完成了一個關鍵的里程碑:成功地將強大的OpenCV視覺庫集成到了Qt項目中。我們不僅掌握了在CMake中配置第三方庫的方法,引入了項目后續將持續使用的真實、高質量數據集,還攻克了核心技術難點——cv::Mat
與QImage
之間的無縫轉換。
現在,我們的應用程序不再只是一個空殼,它真正擁有了處理圖像的“視力”。這座連接C++后端和QML前端的橋梁,已經成功地運輸了第一批“貨物”——圖像數據。
在下一篇文章【《使用Qt Quick從零構建AI螺絲瑕疵檢測系統》——6. 傳統算法實戰:用OpenCV測量螺絲尺寸】中,我們將利用OpenCV的能力,對圖像進行更深入的分析,實現第一個真正的機器視覺功能。