ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Qt调用OpenCV显示图片:Mat转QImage完整指南与避坑详解

Qt调用OpenCV显示图片:Mat转QImage完整指南与避坑详解

接手一个桌面图像处理项目时,最基础也最绕不开的一步,就是把OpenCV处理完的图片拿到Qt界面上显示出来。这个需求听起来简单,但实际落地时会遇到一堆说大不大、说小不小的坑:图片颜色蓝红互换、显示空白、内存异常、实时预览卡顿、编译时链接库报错……我自己前前后后写过好几个不同形态的版本,也帮身边的同事排查过不少类似问题,所以今天想把这套“Qt调用OpenCV显示图片”的完整思路、核心细节和避坑经验一次性整理出来。这篇文章适合正在做图像标注工具、视觉调试台、图像处理Demo或者任何需要把CV计算结果显示在GUI里的开发者,无论你用qmake还是CMake,底层原理都是相通的。

我会先从方案选型讲起,对比几种常见的显示方式,然后重点拆解cv::Mat和QImage之间的转换细节、内存管理这些关键点,再给出一份可以直接抄作业的完整代码,最后把我在实际调试中遇到的典型问题整理成一张排查速查表。如果你赶时间只想快速跑通一个最小示例,可以直接跳到第3部分复制代码;如果你想弄明白“为什么必须这么写”,那我建议你把整篇文章读一遍,很多问题其实都出在你不以为然的细节里。

1. 整体思路与方案选型

1.1 为什么是 Qt + OpenCV,而不是直接用 imshow

OpenCV 自带一个namedWindow和imshow,能把图片弹出一个独立窗口显示,这在纯算法调试阶段确实很方便。但到了做完整工具、产品原型,或者给用户交付一个带业务逻辑的桌面程序时,imshow的短板就非常明显了:

  • 它创建的窗口跟 Qt 主窗口完全是两个体系,没法嵌入到你的界面布局里,用户不可能在同一个窗体里一边看算法结果一边操作其他控件。
  • 窗口事件、按键回调、交互逻辑都受 OpenCV 的 HighGUI 机制约束,想和 Qt 的菜单、工具栏、状态栏联动,基本要靠全局坐标换算,用起来非常别扭。
  • 在 Qt 的程序里直接调用imshow,还可能出现两个事件循环互相打架的问题,比如 Qt 的定时器不刷新、按钮响应变慢,这类问题排查起来很浪费时间。

所以正经的桌面应用开发里,大家都会选择“数据接口”方案:OpenCV 负责图像读取、预处理、算法计算,结果以cv::Mat形式存在内存里,然后由我们把它转成 Qt 能识别的QImage,再用QLabel或者自定义控件展示出来。这样既保留了 OpenCV 强大的视觉算法能力,又充分利用 Qt 成熟稳定的界面框架,分工清晰,边界明确。

1.2 显示方式的四种选择,怎么评估

我见过不少初学者上来就问“哪种显示方式最好”,其实没有绝对最优,只有适不适合你的场景。这里把主流的选择列一下,方便你快速对号入座。

显示方式实现难度交互能力适用场景
QLabel+setPixmap最低基本无交互静态图片、单帧结果展示、缩略图
QWidget重写paintEvent中等可以自己画线条、ROI、跟踪叠加图像标注、多图层叠加、自定义绘制
QGraphicsView/QGraphicsPixmapItem较高自带缩放、平移、图元管理大图浏览、缩放平移需求明显的场景
OpenCV 的imshow最低(但不宜集成)受限于 HighGUI纯算法测试脚本,不适合正式界面

以我个人的实践经验,最稳妥的起步方案是“QLabel显示 + 必要时升级为自定义QWidget”。原因很简单:QLabel一行代码就能把QImage怼上去,适合快速验证整个数据链路对不对;等你要加鼠标缩放、框选标注这些交互时,再把底层换成重写paintEvent的自定义控件,核心的 Mat 转 QImage 那部分代码完全复用,迁移成本很低。不要一开始就上QGraphicsView,它的坐标系和图元机制学习曲线比较陡,等数据链路跑通后再引入也不会晚。

1.3 数据流设计:从 Mat 到 QImage 的生命周期

无论选择哪种显示控件,核心数据链路都是一致的:cv::Mat→ 像素格式转换 →QImage→ 设置到控件(或绘制到绘图设备)。这里面最容易出错的是“谁拥有像素数据”的问题。

cv::Mat的数据存放在一块由它管理的连续内存里,而QImage有两种模式:一种是自己申请内存、持有数据;另一种是通过QImage(uchar* data, w, h, bytesPerLine, format)这种接口创建,它是引用外部数据的,不会自动释放。如果你在转换时图省事,直接用了后一种构造方式,而外部那个cv::Mat在函数返回后被销毁了,那QImage里存着的是一个悬空指针,显示出来就是花屏、乱码或者直接崩溃。

所以我在平时写代码时有一条基本纪律:凡是往控件上设置、或者要在异步流程里传递的QImage,一律在转换时做一次深度拷贝。虽然会多一次内存复制,但对于大多数图片尺寸来说开销完全可接受;如果你要做的是大分辨率图像的实时显示,后面我会在第3部分给出零拷贝的优化思路。

2. 核心细节解析与关键原理

2.1 cv::Mat 和 QImage 的格式映射关系

要正确转换,必须理解两边对像素格式的约定。OpenCV 默认使用BGR顺序存储彩色图像,也就是内存里每个像素按照“蓝、绿、红”排列;而 Qt 的QImage::Format_RGB888期望的是“红、绿、蓝”顺序。如果你不处理,直接把 Mat 的数据塞给 QImage,最典型的症状就是图片里红色和蓝色区域互换,人脸看起来跟去了夜店打蓝光似的。

这里给出常用的映射关系表,我用的是 OpenCV 4.x 和 Qt 5.15/6.x 都适用的写法:

cv::Mat 类型对应 QImage 格式是否需要通道转换
CV_8UC3(BGR)QImage::Format_RGB888需要,用cvtColor转到 RGB
CV_8UC3(RGB)QImage::Format_RGB888不需要,直接构造
CV_8UC1(灰度)QImage::Format_Grayscale8不需要
CV_16UC1(深度图)QImage::Format_Grayscale16不需要,但注意字节序
CV_8UC4(BGRA)QImage::Format_ARGB32推荐用cvtColor转标准格式

QImage 在 Qt 5.5 之后也提供了一个Format_BGR888枚举,可以直接承接 OpenCV 的 BGR 顺序,省掉一次cvtColor,图上会快一点。但我个人不建议把这样的 QImage 直接抛给其他 Qt 内部接口,因为它不符合 Qt 生态里“RGB888 才标准”的直觉,后面如果用QPainter叠加文字、绘制标注都会遇到一些隐蔽的通道顺序问题。更稳妥的做法是在接口层就用标准 RGB 格式,把转换集中到一个函数里,后续组件只认识标准格式。

2.2 Mat 的 step、行对齐和扫描线

初学者最容易忽视的是cv::Mat::step这个概念。OpenCV 为了内存访问效率,允许图像每一行在末尾填充一些额外字节,使得每行字节数是 4 或 8 的整数倍(行对齐)。这跟 BMP 文件的行对齐是同一个道理。如果你写循环时想当然地认为“行字节数 = width × channels”,然后直接从mat.data开始按连续内存读取,在大分辨率图片或者特殊裁剪场景下就会读错偏移,显示出来的图像是斜的。

正确的做法是使用mat.ptr<T>(row)来取每一行的起始指针,或者用mat.step1()/mat.step来手动计算偏移。在构造 QImage 时,bytesPerLine参数也建议直接用mat.step而不是手动计算width * channels,这样能保证 QImage 和 Mat 对同一份数据有相同的行布局认知。我踩过最惨的一次坑,就是手动算行字节数,结果某张 1080p 的图整幅画面偏差了若干像素,排查了很久才发现是对齐问题。

2.3 拷贝策略:什么时候必须深拷贝,什么时候可以零拷贝

在图像转换里,“拷贝”是个绕不开的话题。我的经验总结成一句话:显示之前,默认深拷贝;性能瓶颈出现,再考虑零拷贝。

深拷贝的标准写法是:

cv::Mat rgb; cv::cvtColor(mat, rgb, cv::COLOR_BGR2RGB); QImage img(rgb.data, rgb.cols, rgb.rows, rgb.step, QImage::Format_RGB888); QImage copy = img.copy(); // 强制深拷贝,rgbi 的内存由 copy 自己管理

img.copy()会生成一份全新的 QImage,和之前的 Mat 完全脱离关系。这样做的优点是安全,后续任何对 Mat 的重新分配、作用域结束都不会影响已显示的图片。

零拷贝的典型场景是摄像头实时预览。如果每帧都做一次 BGR → RGB 转换,再copy()一次,对一个 1080p 摄像头来说,每帧大约要处理 6MB 左右的像素数据,性能开销确实不小。此时可以考虑两种方案:

  • 将 Mat 数据直接映射为 QImage,在 Mat 存活周期内完成绘制:在paintEvent里临时构造 QImage,绘制完后立刻释放,不跨函数存储。
  • 用cv::cvtColor的转换结果直接喂给 QImage:转换本身已经产生了新的连续数据,可以让 QImage 浅拷贝这份数据,只要保证 Mat 在绘制期间不会被销毁即可。

这两种零拷贝方式写起来都不复杂,但需要你对变量的生命周期有清晰把握。我的建议是:新手先无脑用深拷贝,能跑通所有功能之后再回来优化性能;千万不要一开始就为了“效率”去裸奔引用外部数据,那点性能收益完全抵不过一个崩溃 bug 带来的调试成本。

3. 实操过程与核心实现

3.1 最小可运行示例:读取一张图片并显示

下面我给出一个完整的、可编译跑起来的最小工程。这里用 qmake 管理项目,因为简单直观,适合作为入门模板;如果你用 CMake,逻辑是完全一致的,区别只是链接库的写法。

第一步,创建QtOpenCVShow.pro文件:

QT += core gui greaterThan(QT_MAJOR_VERSION, 4): QT += widgets TARGET = QtOpenCVShow TEMPLATE = app CONFIG += c++11 # OpenCV 头文件路径 INCLUDEPATH += D:/opencv_4_8/include # OpenCV 库路径 LIBS += -LD:/opencv_4_8/x64/mingw/lib LIBS += -lopencv_core480 -lopencv_imgcodecs480 -lopencv_imgproc480 SOURCES += main.cpp \ MainWindow.cpp HEADERS += MainWindow.h

注意:这里opencv_core480后面的数字要跟你的 OpenCV 版本严格对应,比如 4.8.0 就是480。编译器和 OpenCV 库的类型也必须一致,如果你是 MSVC 编译 Qt,那 OpenCV 也必须是 MSVC 库;用 MinGW 就配 MinGW 的预编译库,混用会出现一堆“undefined reference”的链接错误。

第二步,写MainWindow.h:

#ifndef MAINWINDOW_H #define MAINWINDOW_H #include <QMainWindow> class QLabel; class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = nullptr); private: QLabel *m_imageLabel; }; #endif

第三步,写MainWindow.cpp,重点是里面的转换函数:

#include "MainWindow.h" #include <QLabel> #include <QImage> #include <opencv2/opencv.hpp> MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { m_imageLabel = new QLabel(this); setCentralWidget(m_imageLabel); resize(800, 600); // 读取图片,默认彩图为 BGR 顺序 cv::Mat mat = cv::imread("test.jpg"); if (mat.empty()) { m_imageLabel->setText("图片读取失败,请检查路径"); return; } // 关键转换:BGR -> RGB cv::Mat rgb; cv::cvtColor(mat, rgb, cv::COLOR_BGR2RGB); // 构造 QImage,注意 bytesPerLine 使用 rgb.step QImage img(rgb.data, rgb.cols, rgb.rows, rgb.step, QImage::Format_RGB888); // 深拷贝一份,确保 rgb 释放后 QImage 依然有效 QImage copy = img.copy(); // 设置到 QLabel m_imageLabel->setPixmap(QPixmap::fromImage(copy)); }

第四步,main.cpp:

#include <QApplication> #include "MainWindow.h" int main(int argc, char *argv[]) { QApplication a(argc, argv); MainWindow w; w.show(); return a.exec(); }

这里我想特别说明一下QImage img(...)和QImage copy = img.copy()两行为什么都要写。第一行是在告诉 QImage:这张图像的数据存在于某个 Mat 的内存区域,格式是 RGB888,行布局是rgb.step。这个 QImage 本身没有自己的内存,它是“借住”在 Mat 的房子里。第二行copy()是让 QImage 自己租一套房子,把数据完整搬过去,这样rgb这个 Mat 后面不管是被重新赋值、还是函数退出后被析构,我们显示用的copy都毫发无损。

如果你把copy那行删掉,然后直接把img设置给 QLabel,短看可能也能显示,因为QLabel::setPixmap本质上是把 QImage 转成QPixmap,转换时会读取 QImage 的数据并生成一份新的绘制资源。但这里有一个隐患:QLabel内部什么时候真正完成这个转换,在不同窗口系统下行为不完全一样;一旦转换延迟到 Mat 已经释放之后发生,花屏就会出现。所以我的建议始终是:转换处顺手copy(),别赌生命周期。

3.2 进阶:用 QTimer 显示摄像头实时视频

静态图片只是热身,真正高频的实战场景是摄像头实时显示。这时我们要做的就是在定时器里采帧、转换、刷新界面。我提供一个经过验证的结构:

// MainWindow.h 中增加成员 #include <QTimer> #include <opencv2/opencv.hpp> class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = nullptr); private slots: void updateFrame(); private: cv::VideoCapture m_cap; QTimer m_timer; QLabel *m_imageLabel; };
// 构造函数里初始化摄像头 MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { m_imageLabel = new QLabel(this); setCentralWidget(m_imageLabel); m_cap.open(0); // 打开默认摄像头 if (!m_cap.isOpened()) { m_imageLabel->setText("无法打开摄像头"); return; } m_timer.setInterval(33); // 约30fps connect(&m_timer, &QTimer::timeout, this, &MainWindow::updateFrame); m_timer.start(); } void MainWindow::updateFrame() { cv::Mat frame; if (!m_cap.read(frame)) { return; } // 同一套转换逻辑 cv::Mat rgb; cv::cvtColor(frame, rgb, cv::COLOR_BGR2RGB); QImage img(rgb.data, rgb.cols, rgb.rows, rgb.step, QImage::Format_RGB888); QImage copy = img.copy(); // 按窗口大小缩放显示,保持宽高比 QSize labelSize = m_imageLabel->size(); QPixmap pixmap = QPixmap::fromImage(copy); if (!labelSize.isEmpty()) { pixmap = pixmap.scaled(labelSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); } m_imageLabel->setPixmap(pixmap); }

这段代码里有几个细节值得反复品味。第一,定时器间隔别小于 15ms,因为大多数字摄像头实际帧率在 30fps 左右,设置更短的间隔并不会让画面更流畅,反而会堆积未处理的帧,导致 CPU 空转和界面假死。第二,scaled是一个性能消耗较大的操作,如果窗口尺寸没变,你可以把它提到外面只计算一次,不要每帧都缩放。第三,QImage::copy()在实时场景里是最大的一笔开销,你可以用“零拷贝 + 同步刷新”策略,让QImage直接引用rgb数据,前提是绘制动作同步发生在updateFrame内,不会跨函数延迟。

零拷贝实时显示的优化写法是:

void MainWindow::updateFrame() { cv::Mat frame; if (!m_cap.read(frame)) { return; } cv::Mat rgb; cv::cvtColor(frame, rgb, cv::COLOR_BGR2RGB); // 直接引用 rgb 数据,构造后立即绘制,不存成员变量 QImage img(rgb.data, rgb.cols, rgb.rows, rgb.step, QImage::Format_RGB888); QImage scaledImg = img.scaled(m_imageLabel->size(), Qt::KeepAspectRatio, Qt::FastTransformation); m_imageLabel->setPixmap(QPixmap::fromImage(scaledImg)); // 函数结束后 rgb 释放,但缩放发生在函数内部,安全 }

注意scaled使用FastTransformation而不是SmoothTransformation,因为实时预览里速度和画质要平衡,平滑缩放在 1080p 每帧 30fps 的场景会让 CPU 冲高。实测在我的项目里,从“深拷贝 + 平滑缩放”改成“零拷贝 + 快速缩放”,CPU 占用率能下降约 30% 到 50%,肉眼平滑感几乎没差别。

3.3 自定义控件实现缩放与交互

如果你的需求不仅仅是“显示”,还要鼠标滚轮缩放、拖拽平移,那QLabel就力不从心了。这时候就该把显示区域换成自定义QWidget,重写paintEvent,并把QImage作为内部成员保管。

这里我把核心骨架写出来,你需要关注的是“显示坐标和图像坐标的映射关系”。

// ImageViewer.h #include <QWidget> #include <QImage> class ImageViewer : public QWidget { Q_OBJECT public: explicit ImageViewer(QWidget *parent = nullptr); void setImage(const QImage &image); protected: void paintEvent(QPaintEvent *event) override; void wheelEvent(QWheelEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; private: QImage m_image; double m_scale = 1.0; QPointF m_offset; };
// ImageViewer.cpp 核心逻辑 void ImageViewer::setImage(const QImage &image) { m_image = image; update(); // 触发重绘 } void ImageViewer::paintEvent(QPaintEvent *) { QPainter painter(this); painter.setRenderHint(QPainter::SmoothPixmapTransform); if (m_image.isNull()) { painter.fillRect(rect(), Qt::darkGray); return; } // 把显示区域中心作为缩放中心 double scaledWidth = m_image.width() * m_scale; double scaledHeight = m_image.height() * m_scale; // 中心对齐 + 平移偏移 QPointF topLeft = rect().center() - QPointF(scaledWidth / 2, scaledHeight / 2) + m_offset; QRectF target(topLeft, QSizeF(scaledWidth, scaledHeight)); painter.drawImage(target, m_image); } void ImageViewer::wheelEvent(QWheelEvent *event) { double factor = event->angleDelta().y() > 0 ? 1.1 : 0.9; m_scale *= factor; if (m_scale < 0.01) m_scale = 0.01; if (m_scale > 100.0) m_scale = 100.0; update(); } void ImageViewer::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { m_lastPos = event->pos(); } } void ImageViewer::mouseMoveEvent(QMouseEvent *event) { if (event->buttons() & Qt::LeftButton) { m_offset += event->pos() - m_lastPos; m_lastPos = event->pos(); update(); } }

这套自定义控件的优点是把图像URL变成了一个可交互的画布,后续你要叠加 ROI 框、PT 点、轨迹线,统统可以在这个paintEvent里用QPainter加画,drawImage的坐标换算逻辑完全复用。我当初做十字光标标注时,就是在这套代码基础上加了鼠标位置映射和十字线绘制,整体改动不到五十行。

4. 常见问题与排查技巧实录

4.1 图片颜色蓝红互换

这是出现频率最高的问题,几乎每个新手都会遇到。原因我已经说过了:OpenCV 默认 BGR,QImage 的Format_RGB888期待 RGB。解决方法也很简单,转换前加一行:

cv::cvtColor(mat, mat, cv::COLOR_BGR2RGB);

但另一种姿势是偷懒:直接用QImage::Format_BGR888承接 BGR 数据,不调cvtColor。这在纯显示场景没问题,可一旦你用QImage::pixelColor()或者QPainter做合成输出,又会出问题。我的建议是:老老实实转 RG,把通道顺序统一性贯彻到整个工程,宁可多花一点 CPU,也不要埋下一个时隐时现的 bug。

4.2 图片显示出来是空的、斜的或者花屏

这类问题多半跟数据布局和生命周期有关,我把三个最典型的坑列在下面:

表现根因解决方式
完全空白cv::imread返回空 Mat,常见于路径不对或中文路径用cv::imdecode+QFile读文件流,能规避中文路径问题
图像是斜的或呈带状行对齐问题,bytesPerLine手工算错了构造QImage时直接用mat.step作为bytesPerLine
花屏或程序崩溃QImage 引用的 Mat 已经被释放,悬空指针在转换处执行img.copy(),保证 QImage 数据独立

中文路径这问题在 Windows 下特别坑,很多同事都是撞到之后才意识到imread对中文路径不友好。我的通用做法是写一个封装函数:

cv::Mat readImage(const QString &filePath) { QFile file(filePath); if (!file.open(QIODevice::ReadOnly)) { return cv::Mat(); } QByteArray data = file.readAll(); return cv::imdecode(std::vector<uchar>(data.begin(), data.end()), cv::IMREAD_COLOR); }

这样既能读中文路径,还能直接读取内存中的图像数据,等于多了一层防护。

4.3 实时视频卡顿、CPU 占用率高

实时场景的性能瓶颈一般不在 OpenCV 的算法,而在显示链路里的反复拷贝和缩放。如果 CPU 占用长期在 80% 以上,优先检查这几项:

  • 定时器间隔是否过小,导致处理队列堆积。
  • 是否每帧都调用了平滑缩放SmoothTransformation。
  • 是否在updateFrame里做了不必要的Mat::clone()。
  • 是否把 QImage 转QPixmap的操作放在了非主线程的槽函数里。

我在项目里做过的有效优化,是把摄像头帧率限制在 25fps,用零拷贝方式把 RGB 数据直接交给 QImage,再用快速缩放贴到控件上。这样改完之后,同样的场景 CPU 占用从 60% 降到了 20% 以下,界面明显跟手了。如果还是卡,就得考虑把显示和采集拆到不同线程,用信号槽传输QImage,不过那又是另一个复杂的话题,等有机会再单独写一篇。

4.4 编译链接报错:undefined reference 或 找不到 opencv_core480.dll

链接错误最烦人,十个里有八个是“OpenCV 版本号和编译器类型不匹配”。比如你用的是 MinGW 版 Qt,却在链接 MSVC 编译的 OpenCV 库,那么链接器会吐出一堆类似undefined reference to cv::imread的错误。查这个问题的顺序是:

  1. 确认 OpenCV 的预编译库类型(x64/vc16/lib对应 MSVC,x64/mingw/lib对应 MinGW)。
  2. 确认.pro或CMakeLists.txt里的链接参数和 OpenCV 版本数字一致,比如版本 4.8.0 就要写opencv_core480。
  3. 运行时如果提示缺少 DLL,把 OpenCV 的bin目录加到系统 PATH,或者把用到的 DLL 直接拷贝到可执行程序同级目录。

还有一个新手容易漏掉的点:Qt 和 OpenCV 都必须使用相同的编译配置(比如 Release 或 Debug)。混用 Debug 和 Release 库导致的崩溃和内存错误,往往比链接错误更难排查,因为第一次编译能过,运行起来却莫名其妙挂掉。如果你发现某些诡异的内存问题,检查一下是不是把CONFIG += release和 OpenCV 的 Debug 库混在一起用了。

4.5 性能优化检查清单(实时显示用)

为了让你少走弯路,我把实时显示场景下的优化项整理成一个速查清单,按优先级排列:

  • 第一优先:去掉不必要的copy(),改用“构造 QImage → 立即绘制 → 丢弃”的零拷贝链路。
  • 第二优先:缩放采用Qt::FastTransformation,仅保留一档必要的平滑绘制。
  • 第三优先:把QPixmap::fromImage的结果缓存起来,只在图像内容变化时才重新构造。
  • 第四优先:降低定时器频率,用 25fps 或 30fps 而不是 60fps,肉眼几乎感知不到差别,但 CPU 占用差距明显。
  • 第五优先:如果图像尺寸超过 2K,优先用QImage::convertToFormat转到目标格式,这比在paintEvent里绘制大图要高效。

把这几项逐条过一遍,大多数实时预览卡顿问题都能化解。关键是要记住一点:不要为了“省内存”把代码写得绕来绕去,性能优化的前提永远是正确性和可维护性。


最后说一点我个人的体会。Qt 调用 OpenCV 显示图片,本质上就是一个“格式翻译 + 内存管理”的练习,难度不在 API 本身,而在你是否清楚每一份像素数据的归属权。很多项目后来出问题,不是转换函数写错,而是代码里到处都是浅拷贝和悬空指针的隐患。我建议你在自己的工程里把“Mat 转 QImage”统一封装成一个函数,比如叫imageFromMat(const cv::Mat&),内部强制深拷贝,这样整个项目只用一处负责内存安全,其他业务代码就不用担惊受怕了。另外,每次调试图像显示问题时,可以先用一张固定尺寸的测试图片跑通最小链路,再叠加实时视频、缩放、叠加绘制这些复杂需求,定位问题会快得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进