我决定开设一个全新的专栏,专门探讨 Qt+OpenCV 对于OpenCV+C++ 写法上的变化。尽管两者都是图像处理和计算机视觉领域中的强大工具,但在结合使用时,却有着许多显著的不同之处。而目前关于 Qt+OpenCV 的资料与教程相对稀缺,因此这个专栏不仅将填补这一空白,也将记录我在自学过程中的一些心得与经验分享。希望通过这一专栏,能为那些同样在探索这两个工具组合的开发者们提供一些有益的参考和启发。
开发环境准备
目前是在windows11系统上进行开发,后续会移植到麒麟系统,到时候继续说。
配置OpenCV与Qt
安装opencv成功后,可以使用QT代码来查看opencv是否加载成功。
首先打开.pro文件,加入几行代码
INCLUDEPATH += D:\compiler\opencv4\opencv\build\include
LIBS += -LD:\compiler\opencv4\opencv\build\x64\vc16\lib
# 查找 OpenCV 库
# Release 模式下的库文件
CONFIG(release, debug|release) {
LIBS += -lopencv_world4100
}
# Debug 模式下的库文件
CONFIG(debug, debug|release) {
LIBS += -lopencv_world4100d
}
根据自己得opencv安装路径进行调整,之后直接创建一个程序。
在Qt中检查OpenCV版本
在 main.cpp
中输入以下代码,验证OpenCV是否成功加载:
#include "mainwindow.h"
#include <QApplication>
#include <opencv2/opencv.hpp>
#include <QDebug>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
qDebug() << "OpenCV version: " << CV_VERSION ;
MainWindow w;
w.show();
return a.exec();
}
应用程序输出OpenCV version: 4.10.0则成功配置opencv,先说一个后面可能会出现的问题,显示图像时出现报错:由于找不到opencv_world4100d.dll,无法继续执行代码,你可以将opencv\build\x64\vc16\bin目录下的opencv_world4100d.dll复制到你的编译目录下,如果你用的debug就直接复制该目录下\build\Desktop_Qt_5_15_2_MSVC2019_64bit-Debug\debug。
在Qt中显示图像
在QT中使用opencv库,实现显示图像,需要如下几个头文件,我会一一讲解这几个头文件得作用。
#include <QLocale>
#include <opencv2/opencv.hpp>
#include <QPixmap>
#include <QImage>
-
这个头文件包含了 OpenCV 库的核心功能和基本图像处理操作。OpenCV 是一个强大的计算机视觉库,它提供了大量的功能来处理图像和视频,例如图像读取、处理、显示、特征提取、机器学习等。#include <opencv2/opencv.hpp>
opencv2/opencv.hpp
是一个常用的包含头文件,它会引入 OpenCV 库的大部分核心模块。 -
#include <QPixmap>
QPixmap
是 Qt 中的一个类,用于表示图像或图片。QPixmap
专门用于优化绘图操作,特别是在图形用户界面 (GUI) 中的显示操作。这个头文件提供了将图像数据加载、处理、显示到 Qt 应用程序窗口中的支持。 -
#include <QImage>
QImage
是 Qt 中另一个处理图像数据的类。与QPixmap
不同,QImage
更侧重于图像的直接操作和访问,例如像素的读取和修改。它也可以用来加载、保存和处理图像数据,但在操作图像像素或需要访问图像底层数据时更加方便。这个头文件引入了处理和操作图像数据的能力。
接下来,开始尝试显示图像
我在设计里创建了一个QLabel组件,名为image,并把该组件的水平和垂直策略设置为Expanding。后续成像便是在该组件中成像。
首先在mainwindows里输入代码。
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDebug>
#include <QPixmap>
#include <QImage>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
cv::Mat src = cv::imread("D:\\compiler\\project\\opencv\\Red.png");
//使用双斜杠避免转义字符
//cv::Mat 是一个多维的、通用的矩阵类,虽然它常常被认为是二维矩阵形式存储图像数据,但实际上其内部存储机制要比简单的二维矩阵复杂得多。
//可以用来存储不同类型的图像数据(灰度图像、彩色图像、浮点图像等)和其他矩阵数据。它不仅支持二维矩阵,还支持多维矩阵。
if(src.empty()) {
qDebug() << "Failed to load image.";
}
imshow("input", src); //展示图片
cv::waitKey(0);
//展示图片多少毫秒,然后继续执行,0是一直显示,1是1ms,这段时间内程序会停在这里,不继续向下执行
cv::destroyAllWindows();
}
这样一来,图片便成功显示出来了。需要特别注意的是,在 Qt 中编写 OpenCV 代码与在 Visual Studio 中编写 C++ 代码存在一定差异。在 Qt 中调用 OpenCV 库的函数时,必须在函数前加上 cv::
以明确指定命名空间,例如 cv::imshow()
。这一点在 Qt 环境下显得尤为重要,否则可能会出现函数无法识别或冲突的情况。
Mat
是 OpenCV 中用于存储图像和矩阵数据的核心数据结构。它可以理解为一个多维数组,用于高效地表示各种类型的图像和矩阵。下面从几个方面介绍 Mat
的结构及其存储内容:
内部存储格式
cv::Mat
主要由以下几个部分组成:
-
数据指针(data):指向存储图像像素数据的实际内存块。这个内存块通常是连续的,即图像数据按行排列(行优先存储)。
-
维度(dims):表示矩阵的维度。对于二维图像,
dims
通常为 2。 -
大小(size):表示矩阵在每个维度上的大小。例如,对于一张 640x480 的图像,
size[0]
为 480(行数),size[1]
为 640(列数)。 -
步长(step):表示矩阵每行(或每个维度)在内存中占用的字节数。对于连续存储的二维图像,
step[0]
是行步长,表示一行的字节数,step[1]
通常是像素步长,表示一个像素占用的字节数。 -
引用计数器(refcount):用于管理内存的引用计数,帮助
cv::Mat
实现智能内存管理,在共享内存时实现高效的浅拷贝。
数据存储方式
对于二维图像(最常见的情况),cv::Mat
的数据在内存中是以行优先顺序连续存储的。每行的数据紧密排列,然后逐行存储。对于彩色图像,每个像素通常包含多个通道(如 RGB 图像有三个通道),这些通道的数据通常也紧密排列在一起。
例如,对于一张 3 通道的彩色图像,假设大小为 width x height
,每个像素由 3 个字节(分别代表 B、G、R)组成,那么图像数据在内存中的存储顺序如下:
- 第1行:
BGR BGR BGR...
- 第2行:
BGR BGR BGR...
- ...
- 第n行:
BGR BGR BGR...
访问图像数据
可以通过多种方式访问和操作 cv::Mat
中的图像数据:
- 单像素访问:可以使用
at
函数按行列索引访问单个像素值。
cv::Vec3b pixel = src.at<cv::Vec3b>(row, col);
// 对于彩色图像(3通道),Vec3b 表示三个 uchar 类型的 BGR 值
-
直接访问数据指针:可以直接操作
data
指针来访问底层的图像数据。
uchar* pixelPtr = src.data + row * src.step[0] + col * src.step[1];
-
迭代器访问:可以使用迭代器遍历图像数据。
for (cv::MatIterator_<cv::Vec3b> it = src.begin<cv::Vec3b>(), end = src.end<cv::Vec3b>(); it != end; ++it) {
// 每次迭代访问一个像素
}
多维矩阵
虽然最常用的是二维矩阵(如图像),但 cv::Mat
也可以处理任意维度的矩阵。例如,在卷积神经网络中,可能需要使用四维或更高维度的矩阵来存储批量图像和特征图。
连续性
cv::Mat
的数据存储可以是连续的,也可以是非连续的。通常情况下,cv::Mat
是连续的,即数据在内存中是一块连续区域,所有像素都依次排列。但在某些情况下(如从原矩阵中裁剪出的子矩阵),数据可能是不连续的,此时 step
会记录每一行/维度在内存中的实际跨度。
内存管理
cv::Mat
使用引用计数机制来管理内存,支持浅拷贝和深拷贝。浅拷贝时,新对象与原对象共享同一块内存,不会立即复制数据;而深拷贝时会创建数据的副本。
cv::Mat shallowCopy = src; // 浅拷贝,共享数据
cv::Mat deepCopy = src.clone(); // 深拷贝,复制数据
但是这样还没结束,因为他并不是在我预设的窗口内显示,而是单独开辟了一个窗口。
在QLabel中显示图像
为了让他能正常的显示在我设定的窗口内,我们需要写一个显示函数,先在头文件中加入 QPixmap pixmap;
可以通过创建一个 `QLabel` 组件来显示图片,这样你可以避免直接使用 `QPainter` 绘制图像,并利用 `QLabel` 的 `setPixmap` 方法来显示图像。这种方法更加简单且稳定,因为 `QPainter` 的使用需要在特定的绘制事件(如 `paintEvent`)中调用,否则可能会出现诸如 `QWidget::paintEngine: Should no longer be called` 或 `QPainter::begin: Paint device returned engine == 0, type: 1` 之类的错误。这些错误通常发生在试图在不适合的上下文中使用 `QPainter` 时,例如在 `QWidget` 的构造函数中,或者在非绘制事件的回调中。
通过使用 `QLabel` 的 `setPixmap` 方法,可以更方便地在界面上显示图像,同时避免了不必要的复杂性和潜在的错误。这种方式既安全又符合 Qt 框架的设计规范。
void MainWindow::displayImage(){
// 1. 加载图像
cv::Mat src = cv::imread("D:\\compiler\\project\\opencv\\Red.png"); // 使用OpenCV的imread函数加载图像,路径为本地的一个图片文件
if (src.empty()) { // 检查图像是否加载成功,如果图像为空,说明加载失败
qDebug() << "Failed to load image!"; // 输出调试信息提示加载失败
return; // 退出函数,避免后续的处理步骤
}
// 输出原始图像大小
qDebug() << "Original Image Size:" << src.cols << "x" << src.rows;
// 2. 获取image QLabel的尺寸
int labelWidth = ui->image->width(); // 获取QWidget(命名为image)的宽度
int labelHeight = ui->image->height(); // 获取QWidget的高度
// 输出QLabel的大小
qDebug() << "QLabel Size:" << labelWidth << "x" << labelHeight;
// 3. 调整图像大小以适应QLabel
cv::Mat resized; // 创建一个空的cv::Mat对象用于存储调整大小后的图像
double aspectRatio = static_cast<double>(src.cols) / src.rows; // 计算原始图像的宽高比(宽度除以高度)
int newWidth = labelWidth ; // 初始化新图像的宽度为widget的宽度
int newHeight = static_cast<int>(labelWidth / aspectRatio); // 根据宽高比计算新的高度
if (newHeight > labelHeight ) { // 如果计算出来的新高度超过了widget的高度
newHeight = labelHeight ; // 将高度调整为widget的高度
newWidth = static_cast<int>(labelHeight * aspectRatio); // 根据高度重新计算宽度,保持原始宽高比
}
cv::resize(src, resized, cv::Size(newWidth, newHeight)); // 使用OpenCV的resize函数将原图缩放到新的尺寸
//cv::resize 是 OpenCV 中用于调整图像大小的函数。
//void cv::resize(const cv::Mat& src, cv::Mat& dst, cv::Size dsize, double fx = 0, double fy = 0, int interpolation = cv::INTER_LINEAR);
//我使用了前三个主要参数,通过读取输入图像(src),即想要调整大小的图像,将其调整为指定的新尺寸(dsize),并将结果存储在输出图像(dst)中
//fx 和 fy:图像宽度和高度的比例因子(可选)。当 dsize 为 cv::Size() 时,使用这两个参数分别指定宽度和高度的缩放因子。例如,fx = 0.5 和 fy = 0.5 将使图像缩小到原始尺寸的一半。
//interpolation:插值方法(可选)。它决定了缩放时如何计算新像素值。常见的插值方法包括:
//cv::INTER_LINEAR(默认):双线性插值,适合缩小图像。
//cv::INTER_NEAREST:最近邻插值,计算速度快,但图像质量可能较低。
//cv::INTER_CUBIC:双三次插值,适合放大图像,质量比双线性插值高。
//cv::INTER_LANCZOS4:Lanczos插值,适合对质量要求较高的图像缩放。
// 输出调整后图像的大小
qDebug() << "Resized Image Size:" << resized.cols << "x" << resized.rows;
// 4. 将图像转换为QImage//初始
QImage qimg(resized.data, resized.cols, resized.rows, resized.step, QImage::Format_RGB888);
// 将OpenCV的cv::Mat图像数据转换为QImage,QImage的格式指定为BGR888,因为OpenCV使用的是BGR格式
//目的:将 OpenCV Open 的CV图像数据 (cv::Mat) 转换为 Qt 能够理解和处理的图像格式 (QImage)。 存这样可以在 Qt 的 GUI 组件(储如 QWidget、QLabel 等)中显示和操作图像。
/*resized.data:指向图 的像数据的指针,resized.cols:图像的宽 格度(列数),resized.rows:图像的高度(行数),resized.step:每行的字节数,Image::Format_BGR888:图像的像素正确格式,
* 显示指定图像数据的颜色格式为 BGR,每个图颜色通道占 8 位,总像共 24 位(3 字。这节)表示一个像素。这个格式与 OpenCV是 的默认格式 (BGR) 相匹转换配。
*对于一个分辨率为1920x1080的图像:
高度(行数): 1080 像素
宽度(列数): 1920 像素
*/
qimg = qimg.rgbSwapped(); // 因为Qt的QImage默认使用RGB格式,OpenCV使用BGR,需要将BGR转换为RGB
// 5. 将QImage转换为QPixmap并显示在QLabel上
QPixmap pixmap = QPixmap::fromImage(qimg);
ui->image->setPixmap(pixmap); // 在QLabel上显示图像
ui->image->setAlignment(Qt::AlignCenter); // 使图像居中
}
为了确保在窗口加载完成后再加载图片,并避免因获取组件的长宽数据有误而导致的问题,我们可以通过重写 showEvent
函数来实现。这样,在窗口显示时,会自动调用该函数进行图像加载和调整,而无需在 MainWindow
的其他地方手动调用。
void MainWindow::showEvent(QShowEvent *event) {
QMainWindow::showEvent(event);
//重载showEvent,在窗口显示后进行图像加载和显示,
//避免获取image QLabel时,窗口和QLabel还没有完全布局好,导致获取到的尺寸不正确。
displayImage(); // 窗口显示后加载并显示图像
}
图像显示窗口的大小可以调整,以便根据窗口的尺寸自动缩放图像,使其保持原有的比例。但是,目前调整窗口大小时,图像不会动态地适应新窗口的尺寸,需要通过监听窗口大小变化事件来触发图像缩放的更新。
为此,可以在程序中添加一个事件监听器,当检测到窗口大小变化时,重新计算并调整图像的显示大小。这样,图像在窗口缩放时就能自动适应新的尺寸,保持其原始比例不变。
转换为灰度图像
接下来把图片转换成灰度图像,上条语句为显示原图,下条语句为转换为灰度图像。
// 4. 将图像转换为QImage//初始
QImage qimg(resized.data, resized.cols, resized.rows, resized.step, QImage::Format_RGB888);
// 4. 将灰度图像转换为QImage//灰度
cv::Mat gray;
cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);
//调整图像大小的代码以适应窗口
QImage qimg(resized.data, resized.cols, resized.rows, resized.step, QImage::Format_Grayscale8);