OpenCV imgproc模块深度解析:从C++图像处理基础到高级优化实战
1. 项目概述:为什么需要深入理解 imgproc?
如果你正在用 C++ 和 OpenCV 做图像处理,那么imgproc模块就是你绕不开的核心工具箱。很多人刚接触 OpenCV 时,可能只是调用imread读图、imshow显示,觉得图像处理不过如此。但当你真正想实现一个功能,比如把一张模糊的证件照变清晰,或者从复杂的背景里精准地抠出一个人物时,你就会发现,不深入imgproc,很多想法都只是空中楼阁。
imgproc是 “Image Processing” 的缩写,它包含了从最基础的色彩空间转换、几何变换,到复杂的图像滤波、形态学操作、边缘检测、轮廓分析、直方图处理等几乎所有经典的图像处理算法。这个模块的代码量在 OpenCV 中占比极大,其稳定性和效率经过了工业界长时间的锤炼。我见过不少项目,初期为了追求“快速验证”,直接用 Python 的 OpenCV 接口写原型,但到了需要部署到嵌入式设备或者要求实时性能的 C++ 服务端时,才发现对底层imgproc函数的理解不足,导致优化无从下手,甚至因为误用某个参数而引入了难以察觉的 Bug。
所以,今天这篇内容,不是简单的 API 罗列,而是基于我多年在安防、自动驾驶和工业视觉项目中的踩坑经验,带你深入imgproc模块的肌理。我会重点讲清楚每个功能背后的“为什么”——为什么这个滤波函数要这么用?那个形态学操作的核到底怎么选?轮廓分析的结果怎么解读才靠谱?目标是让你看完后,不仅能“调用”函数,更能“驾驭”它们,写出高效、鲁棒的 C++ 图像处理代码。
2. 核心模块功能全景与设计哲学
2.1 imgproc 的模块划分与内在逻辑
打开 OpenCV 的源码目录,modules/imgproc文件夹下内容繁多,但其内部组织有很强的逻辑性,理解这个结构有助于我们在需要时快速定位函数。大体上,可以将其分为以下几个层次:
- 基础像素与色彩操作:这是图像的“原子”层面。包括色彩空间转换(
cvtColor)、像素值统计(mean,minMaxLoc)、查找表(LUT)等。这些是构建更复杂操作的基石。 - 图像变换:涉及图像的空间关系。包括几何变换(
resize,warpAffine,warpPerspective)、重映射(remap)等。这部分的核心是理解各种变换矩阵(如仿射、透视)以及插值算法(如INTER_LINEAR,INTER_NEAREST)对结果的影响。 - 图像滤波与增强:目的是改善图像质量或突出特征。包括线性滤波(
blur,GaussianBlur,filter2D)、非线性滤波(medianBlur,bilateralFilter)、形态学操作(erode,dilate,morphologyEx)、边缘增强(Sobel,Scharr,Laplacian)等。这是imgproc最核心的部分之一,选择哪种滤波、核多大、迭代几次,直接决定了后续特征提取的成败。 - 图像分割与轮廓分析:目的是将图像中有意义的区域或对象分离出来。包括阈值化(
threshold,adaptiveThreshold)、边缘检测(Canny)、查找轮廓(findContours)、轮廓处理(approxPolyDP,convexHull)等。这是从“处理”到“理解”图像的关键一步。 - 直方图与特征描述:用于量化图像内容。包括计算直方图(
calcHist)、直方图均衡化(equalizeHist)、距离变换(distanceTransform)、霍夫变换(HoughLines,HoughCircles)等。这些结果为图像匹配、目标识别提供了特征向量。
OpenCVimgproc的设计哲学是“效率优先,兼顾易用”。绝大多数函数都针对 SSE、AVX 等指令集进行了优化,并且支持多线程(通过cv::parallel_for_)。它的接口设计是过程式的,一个函数完成一个明确的任务,这使得代码流程清晰,也便于性能分析和优化。
2.2 C++ API 与 Python API 的关键差异及选择
很多初学者是从 Python 的 OpenCV 入门,再转到 C++,会发现一些“水土不服”。理解这些差异至关重要:
- 内存管理:Python (NumPy) 中,数组是引用计数,自动管理内存。而在 C++ 中,
cv::Mat是核心对象,其内存管理基于引用计数和智能指针(cv::Ptr),但需要开发者更小心地处理生命周期和赋值操作(浅拷贝与深拷贝)。错误的内存操作是 C++ 版 OpenCV 程序崩溃的主要原因之一。实操心得:记住一个原则,
cv::Mat A = B;是浅拷贝,它们共享数据。如果你需要一份独立的副本,必须显式调用B.copyTo(A);或A = B.clone();。在函数间传递大型Mat时,尽量使用const cv::Mat&传入,避免不必要的拷贝。 - 性能表现:对于纯粹的、计算密集型的
imgproc函数,C++ 版本通常比 Python 版本快一个数量级甚至更多。这主要是因为避免了 Python 解释器的开销和 Python-to-C 的接口调用成本。在实时视频处理或处理高分辨率图像时,C++ 是唯一的选择。 - 错误处理:Python 中,OpenCV 函数出错通常会抛出异常。在 C++ 中,许多老式函数通过返回一个特殊值(如
-1)或设置一个全局错误状态来指示错误,而较新的函数可能使用 C++ 异常。需要查阅具体函数的文档。 - API 细微差别:有些函数在两种语言中参数顺序或默认值可能略有不同。例如,
cv::rectangle在 C++ 中参数是(图像, 矩形, 颜色, 粗细, 线型),而在 Python 中,thickness和lineType的位置可能因版本而异。永远以官方 C++ 文档为准。
为什么坚持用 C++ 学习 imgproc?因为只有深入到 C++ 层面,你才能完全控制数据流,理解算法的真实开销(例如,你可以精确测量某个滤波操作在 CPU 上的时钟周期),并进行底层的优化(如使用cv::UMat尝试 OpenCL 加速)。这对于构建高性能、可部署的视觉系统是不可或缺的。
3. 核心功能深度解析与实战要点
3.1 色彩空间转换:不仅仅是 RGB 到灰度
cvtColor可能是你调用最多的函数之一。但你真的了解从BGR到GRAY的权重公式吗?OpenCV 默认使用GRAY = 0.299*R + 0.587*G + 0.114*B。这个权重源于人眼对绿色最敏感,对蓝色最不敏感。
关键场景与选择:
- 目标检测与跟踪:经常需要转到
HSV或LAB空间。HSV颜色空间将亮度(Value)与色度(Hue, Saturation)分离,对于光照变化有一定鲁棒性,常用于基于颜色的阈值分割。例如,追踪一个红色的球,在 HSV 空间设定 Hue 的范围比在 BGR 空间设定 RGB 范围要稳定得多。cv::Mat bgrImage, hsvImage, mask; cv::cvtColor(bgrImage, hsvImage, cv::COLOR_BGR2HSV); // 定义红色范围(注意OpenCV中Hue范围是0-180) cv::Scalar lower_red(0, 70, 50); cv::Scalar upper_red(10, 255, 255); cv::Scalar lower_red2(170, 70, 50); // 红色在HSV色环上跨越0度,需要两个范围 cv::Scalar upper_red2(180, 255, 255); cv::inRange(hsvImage, lower_red, upper_red, mask1); cv::inRange(hsvImage, lower_red2, upper_red2, mask2); cv::bitwise_or(mask1, mask2, mask); - 人脸识别与图像增强:
LAB颜色空间的L通道代表明度,A和B通道代表颜色对立维度。有时在LAB空间对L通道进行均衡化或对比度拉伸,再转回BGR,可以得到更自然的人脸增强效果,避免在 RGB 空间直接操作导致的色彩失真。
注意事项:
cvtColor是一个相对耗时的操作,尤其是高分辨率图像。在视频处理流水线中,应尽量避免对每一帧进行不必要的色彩空间转换。如果后续步骤只需要亮度信息,尽早转换为灰度图并复用。
3.2 图像滤波:降噪与特征增强的艺术
滤波是图像处理的“万金油”,但用错地方就是“毒药”。
1. 线性滤波:
blur(均值滤波):核内取平均。简单粗暴,能平滑噪声,但也会严重模糊边缘。仅适用于对边缘信息要求不高的快速降噪。GaussianBlur(高斯滤波):加权平均,权重服从高斯分布,中心像素权重最高。这是最常用的平滑滤波器,能在抑制噪声的同时较好地保留边缘。关键参数是核大小ksize和标准差sigma。如果sigma为负,OpenCV 会根据ksize自动计算一个合理的值。// 消除细微噪声,为Canny边缘检测做准备 cv::Mat blurred; cv::GaussianBlur(src, blurred, cv::Size(5, 5), 1.5);实操心得:
ksize的宽度和高度最好是正奇数,这样滤波器才有明确的中心点。sigma越大,图像越模糊。通常sigma取 0.3*((ksize-1)*0.5 - 1) + 0.8 是一个经验值,但 OpenCV 的自动计算已经足够好。
2. 非线性滤波:
medianBlur(中值滤波):用核内中值代替中心像素。对椒盐噪声有奇效,且能较好保留边缘。核大小必须是大于1的奇数。bilateralFilter(双边滤波):一个“神奇”的滤波器。它同时考虑空间邻近度和像素值相似度,可以达到在平滑区域内部的同时,保持边缘锐利的效果,常用于图像去噪和美颜。但它的计算量很大,速度很慢。// 人物皮肤平滑,去皱纹但保持五官轮廓 cv::Mat smoothed; cv::bilateralFilter(portrait, smoothed, 9, 75, 75); // 参数:d(邻域直径)、sigmaColor(颜色空间标准差)、sigmaSpace(坐标空间标准差)
滤波器的选择策略:
| 噪声类型 / 需求 | 推荐滤波器 | 理由与注意事项 |
|---|---|---|
| 高斯白噪声 | GaussianBlur | 均衡考虑去噪和保边,通用性强。 |
| 椒盐噪声 | medianBlur | 中值滤波能完全滤除孤立的极值点。 |
| 纹理平滑,保持边缘 | bilateralFilter | 效果最优,但计算成本高,不适合实时视频。 |
| 快速初步平滑 | blur | 速度最快,但细节损失大。 |
| 边缘检测预处理 | GaussianBlur | 抑制噪声,防止噪声被误检为边缘。 |
3.3 形态学操作:塑造二值图像的“形体”
形态学操作是针对二值图像(或灰度图像,将其亮度视为高度)的领域操作,核心是“结构元素”(核)。这是处理分割后结果的利器。
腐蚀 (
erode) 与膨胀 (dilate):- 腐蚀:用核扫描图像,如果核覆盖的所有像素都是前景(白色),中心像素才保留为前景。效果是缩小白色区域,消除小斑点,断开细连接。
- 膨胀:用核扫描图像,如果核覆盖的像素中至少有一个是前景,中心像素就设为前景。效果是扩大白色区域,填补空洞,连接邻近区域。
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3)); cv::Mat eroded, dilated; cv::erode(binaryImage, eroded, kernel); // 腐蚀 cv::dilate(binaryImage, dilated, kernel); // 膨胀开运算与闭运算 (
morphologyEx):- 开运算 (MORPH_OPEN):先腐蚀后膨胀。用于消除小物体、平滑较大物体的边界,同时不显著改变其面积。非常适合去除二值图像中的噪声点。
- 闭运算 (MORPH_CLOSE):先膨胀后腐蚀。用于填充物体内的小孔洞、连接邻近物体、平滑边界。适合填补分割后物体内部的缺口。
cv::Mat opened, closed; cv::morphologyEx(binaryImage, opened, cv::MORPH_OPEN, kernel); cv::morphologyEx(binaryImage, closed, cv::MORPH_CLOSE, kernel);形态学梯度、顶帽、黑帽:
- 梯度 (MORPH_GRADIENT):膨胀图减腐蚀图。可以得到物体的轮廓。
- 顶帽 (MORPH_TOPHAT):原图减开运算图。用于提取比邻近区域亮的细小物体(如背景校正后的文本)。
- 黑帽 (MORPH_BLACKHAT):闭运算图减原图。用于提取比邻近区域暗的细小物体(如显微镜图像中的细胞)。
核心技巧:结构元素
kernel的形状和大小是形态学操作的灵魂。MORPH_RECT(矩形)作用最强,MORPH_ELLIPSE(椭圆形)更自然,MORPH_CROSS(十字形)适用于特定方向。大小选择需要根据目标物体和噪声的尺寸反复试验。一个常见的流程是:阈值分割 -> 开运算去小噪点 -> 闭运算填小空洞 -> 得到干净的二值掩膜。
3.4 边缘与轮廓:从像素到几何表达
这是将图像数据转化为高层语义信息的关键步骤。
1. 边缘检测 (Canny,Sobel):
Sobel算子:计算图像的一阶导数近似,得到在 X 和 Y 方向上的梯度。通常用来检测边缘的大致方向。cv::Mat grad_x, grad_y; cv::Sobel(grayImage, grad_x, CV_16S, 1, 0, 3); // 求x方向梯度,深度设为CV_16S防止溢出 cv::Sobel(grayImage, grad_y, CV_16S, 0, 1, 3); cv::convertScaleAbs(grad_x, abs_grad_x); // 转回8位无符号 cv::convertScaleAbs(grad_y, abs_grad_y); cv::addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, edgeImage); // 合并梯度Canny边缘检测:一个多阶段的经典算法,包含高斯滤波、计算梯度、非极大值抑制和双阈值滞后连接。它输出的是一幅二值的边缘图,边缘是细的、连续的。cv::Mat edges; // 参数:输入图像,输出边缘图,低阈值,高阈值,Sobel核大小(通常为3) cv::Canny(blurredImage, edges, 50, 150, 3);参数调优心法:
Canny的高低阈值是关键。高阈值用于确定强边缘,低阈值用于连接强边缘周围的弱边缘。一个经验是,高阈值大约是低阈值的2到3倍。可以先设一个较高的低阈值(如100),然后观察结果,如果丢失了重要边缘,就降低它;如果引入了太多噪声,就提高它。blurredImage最好是经过高斯模糊的,以抑制噪声。
2. 轮廓查找与分析 (findContours,drawContours): 这是将边缘像素组织成有意义的“对象”的过程。
std::vector<std::vector<cv::Point>> contours; std::vector<cv::Vec4i> hierarchy; cv::findContours(binaryImage, contours, hierarchy, cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE); cv::Mat drawing = cv::Mat::zeros(binaryImage.size(), CV_8UC3); for (size_t i = 0; i < contours.size(); i++) { cv::Scalar color = cv::Scalar(rand() & 255, rand() & 255, rand() & 255); cv::drawContours(drawing, contours, (int)i, color, 2); }- 检索模式 (
RetrievalModes):RETR_EXTERNAL:只检测最外层轮廓。对于只想找完整物体外边框的场景非常有用。RETR_LIST:检测所有轮廓,但不建立层级关系。速度最快。RETR_TREE:检测所有轮廓,并重建完整的嵌套层级关系。信息最全,但计算量稍大。最常用。
- 近似方法 (
ContourApproximationModes):CHAIN_APPROX_NONE:存储轮廓上所有的点。CHAIN_APPROX_SIMPLE:压缩水平、垂直和对角线段,只保留端点。例如,一个矩形的轮廓只会存储4个角点。在大多数情况下,这是节省内存的最佳选择。
轮廓分析实战:找到轮廓后,可以通过cv::contourArea()计算面积,cv::arcLength()计算周长,cv::boundingRect()获取外接矩形,cv::minAreaRect()获取最小外接旋转矩形,cv::approxPolyDP()进行多边形逼近来判断形状(如三角形、矩形、圆形)。
for (const auto& contour : contours) { double area = cv::contourArea(contour); if (area < 100) continue; // 过滤掉太小的轮廓(可能是噪声) cv::Rect bbox = cv::boundingRect(contour); float aspectRatio = (float)bbox.width / bbox.height; std::vector<cv::Point> approx; cv::approxPolyDP(contour, approx, 0.02 * cv::arcLength(contour, true), true); if (approx.size() == 4) { // 可能是矩形或正方形 cv::rectangle(drawing, bbox, cv::Scalar(0, 255, 0), 2); } }4. 高级应用与性能优化实战
4.1 直方图处理与图像增强
直方图是图像强度分布的统计图,是进行图像增强、对比度调整、颜色校正的基础。
计算与均衡化:
cv::Mat grayImage; cv::cvtColor(src, grayImage, cv::COLOR_BGR2GRAY); // 计算直方图 cv::Mat hist; int histSize = 256; // 灰度级数 float range[] = {0, 256}; const float* histRange = {range}; cv::calcHist(&grayImage, 1, 0, cv::Mat(), hist, 1, &histSize, &histRange); // 直方图均衡化 cv::Mat equalized; cv::equalizeHist(grayImage, equalized);equalizeHist通过重新分布像素强度来增强对比度,特别适用于背景和前景都太亮或太暗的图像。但要注意,它是全局操作,可能会放大噪声,并且不适用于需要保持自然色调的场景(如人脸)。自适应直方图均衡化 (CLAHE): 为了解决全局均衡化的问题,
imgproc提供了cv::createCLAHE。它将图像分成小块,对每个块进行均衡化,然后用双线性插值来消除块之间的边界。cv::Ptr<cv::CLAHE> clahe = cv::createCLAHE(); clahe->setClipLimit(2.0); // 对比度限制阈值,防止噪声放大 clahe->setTilesGridSize(cv::Size(8, 8)); // 分块大小 cv::Mat claheResult; clahe->apply(grayImage, claheResult);CLAHE 在医学图像(如X光片)和遥感图像增强中效果显著。
4.2 几何变换与图像配准
imgproc提供了完整的几何变换工具。
- 仿射变换与透视变换:
插值算法选择:// 仿射变换:需要三个点对 cv::Point2f srcTri[3], dstTri[3]; srcTri[0] = cv::Point2f(0, 0); srcTri[1] = cv::Point2f(1, 0); srcTri[2] = cv::Point2f(0, 1); dstTri[0] = cv::Point2f(0, 0); dstTri[1] = cv::Point2f(1.5, 0); dstTri[2] = cv::Point2f(0, 1.5); cv::Mat affineMat = cv::getAffineTransform(srcTri, dstTri); cv::Mat warped; cv::warpAffine(src, warped, affineMat, src.size()); // 透视变换:需要四个点对(常用于校正倾斜拍摄的文档、车牌) cv::Point2f srcQuad[4], dstQuad[4]; // ... 设置四个对应的角点 cv::Mat perspectiveMat = cv::getPerspectiveTransform(srcQuad, dstQuad); cv::warpPerspective(src, warped, perspectiveMat, src.size());warpAffine和warpPerspective中的flags参数用于指定插值方法。INTER_LINEAR(双线性插值)是质量和速度的平衡,最常用。INTER_NEAREST(最近邻插值)最快,但会产生锯齿。INTER_CUBIC(双三次插值)质量更高但更慢。INTER_LANCZOS4(Lanczos插值)质量最好,也最慢。
4.3 性能优化与多线程实践
在 C++ 中,性能至关重要。以下是一些优化imgproc操作的技巧:
- 避免不必要的拷贝和转换:如前所述,善用引用和
const。如果流程中多个步骤需要灰度图,只做一次cvtColor。 - 预分配内存:在循环中处理视频帧时,为输出
Mat对象预分配好内存(使用cv::Mat::create或直接指定大小和类型),可以避免频繁的内存分配和释放。cv::Mat resultFrame; resultFrame.create(frame.rows, frame.cols, frame.type()); // 在循环外预分配 while (capture.read(frame)) { processFrame(frame, resultFrame); // resultFrame 内存已分配 imshow("Result", resultFrame); } - 利用 OpenCV 的并行框架:OpenCV 4.x 之后,
cv::parallel_for_变得更加易用。你可以将一些可并行的循环(如对图像中每个像素进行独立操作)并行化。class ParallelProcess : public cv::ParallelLoopBody { public: ParallelProcess(cv::Mat& _src, cv::Mat& _dst) : src(_src), dst(_dst) {} virtual void operator()(const cv::Range& range) const CV_OVERRIDE { for (int r = range.start; r < range.end; r++) { // 处理第r行 uchar* pSrc = src.ptr<uchar>(r); uchar* pDst = dst.ptr<uchar>(r); for (int c = 0; c < src.cols; c++) { // 你的像素级处理逻辑 pDst[c] = cv::saturate_cast<uchar>(pSrc[c] * 1.5); } } } private: cv::Mat& src; cv::Mat& dst; }; // 使用 cv::Mat src, dst; dst.create(src.size(), src.type()); ParallelProcess parallelBody(src, dst); cv::parallel_for_(cv::Range(0, src.rows), parallelBody); - 使用 UMat 尝试异构计算:
cv::UMat是 OpenCV 的统一抽象,数据可以透明地存储在主机内存或 OpenCL 设备内存。如果系统支持 OpenCL,使用UMat可能自动获得 GPU 加速。
注意:对于小图像或简单操作,数据在主机和设备间传输的开销可能抵消掉 GPU 计算的优势,需要实际测试。cv::UMat uSrc, uDst; src.copyTo(uSrc); // 上传到设备(可能发生) cv::GaussianBlur(uSrc, uDst, cv::Size(5,5), 1.5); uDst.copyTo(dst); // 下载回主机(可能发生)
5. 常见问题排查与调试技巧
在实际项目中,使用imgproc时总会遇到各种奇怪的问题。这里记录几个我踩过的坑和解决方法。
问题1:findContours找到的轮廓数量为0或不对。
- 可能原因1:输入图像不是 8 位单通道二值图。
findContours要求输入是CV_8UC1类型,且背景为0(黑色),前景为非0(通常为255白色)。- 检查:确保你传入的是正确的二值图。使用
image.type() == CV_8UC1检查类型,用imshow可视化确认前景是白色。
- 检查:确保你传入的是正确的二值图。使用
- 可能原因2:阈值化参数不对,导致目标物体没有被正确分割出来。
- 调试:在调用
findContours前,先保存或显示你的二值图,确认目标区域是连通的白色块。
- 调试:在调用
- 可能原因3:使用了
RETR_EXTERNAL模式,而你想要的轮廓是嵌套在内部的。- 解决:尝试改用
RETR_TREE或RETR_LIST。
- 解决:尝试改用
问题2:形态学操作后,小物体没有被去除,或者大物体被破坏了。
- 可能原因:结构元素(核)的大小或形状不合适。
- 调试:这是一个试错过程。从小核开始(如 3x3),逐步增大。观察是腐蚀还是膨胀操作导致问题。通常,“开运算去小点”需要核的大小略大于噪声点的尺寸。“闭运算填小洞”同理。
问题3:Canny 边缘检测结果断断续续,或者噪声太多。
- 可能原因1:高低阈值设置不合理。
- 调试:使用滑动条动态调整阈值,观察边缘变化。确保输入图像已经过适当的高斯模糊。
- 可能原因2:图像本身对比度太低。
- 解决:在 Canny 之前,先进行图像增强,如直方图均衡化或对比度拉伸。
问题4:几何变换后图像出现黑边或扭曲。
- 可能原因:变换矩阵计算错误,或者目标图像大小设置不当。
- 调试:手动计算或打印出你的变换矩阵。对于透视变换,确保四个源点和目标点是一一对应且顺序一致的(通常是顺时针或逆时针)。使用
cv::warpPerspective时,可以尝试将dsize参数设得大一些,然后裁剪有效区域。
- 调试:手动计算或打印出你的变换矩阵。对于透视变换,确保四个源点和目标点是一一对应且顺序一致的(通常是顺时针或逆时针)。使用
问题5:程序处理速度慢,无法满足实时性要求。
- 排查步骤:
- 定位瓶颈:使用性能分析工具(如
cv::TickMeter)对每个主要函数计时。cv::TickMeter tm; tm.start(); cv::GaussianBlur(src, dst, cv::Size(5,5), 1.5); tm.stop(); std::cout << "GaussianBlur time: " << tm.getTimeMilli() << " ms" << std::endl; - 检查图像分辨率:是否在处理不必要的超高分辨率图像?考虑先
resize到合适尺寸。 - 检查算法复杂度:是否在循环内重复计算了不变的东西(如核、变换矩阵)?将其提到循环外。
- 检查内存操作:是否在循环内频繁创建和销毁大的
cv::Mat对象? - 考虑降级:能否用更快的算法达到近似效果?比如用
blur代替GaussianBlur,用CHAIN_APPROX_SIMPLE代替CHAIN_APPROX_NONE。
- 定位瓶颈:使用性能分析工具(如
一个实用的调试技巧:可视化流水线。在开发复杂的图像处理流程时,我习惯在每一个关键步骤后,都将中间结果保存或显示出来。这能帮你迅速定位是哪个环节出了问题。你可以创建一个调试模式,用cv::imshow和cv::waitKey(1)来实时观察每一帧的中间状态,虽然会牺牲一些性能,但对于调试来说是无价的。