剖析c/c++的findcontours崩溃完美解决方案
解决 Windows 平台 OpenCV findContours
崩溃:一种更稳定的方法
许多在 Windows 平台上使用 OpenCV 的开发者可能会在使用 findContours
函数时,遇到令人头疼的程序崩溃问题。尽管网络上流传着多种解决方案,但它们并非总能根治此问题。
常见的“药方”包括:
- 修改项目配置:配置属性 -> 常规 -> 项目默认值 -> MFC的使用 -> 在共享DLL中使用MFC;
- 调整C/C++代码生成选项:C/C++ -> 代码生成 -> 运行库 -> 多线程DLL(/MD);
- 代码层面规范:例如 vector 使用 cv::vector,vector<vector<Point>> 声明时预分配空间等。
然而,现实情况是,许多开发者尝试上述方法后,问题依旧。即便少数情况下问题得到偶然解决,程序在迁移到不同环境或 OpenCV 版本时,仍可能面临兼容性风险。
本文将深入剖析此问题的潜在原因,并提供一个更可靠的定制化实现方案。
探究崩溃的根源
为了有效地解决 findContours
引发的异常,理解其内部机制至关重要。cv::findContours
的C++接口实际上是对底层C语言风格函数 cvFindContourjss
(或其变体 cvFindContours_Impl
)的一层封装。
一个关键的观察点是:直接调用C语言风格的 cvFindContours
函数往往能够正常运行,这暗示问题很可能出在C++封装层对数据结构的处理上。
仔细研读 cv::findContours
的源码(尽管具体实现可能随版本略有差异),我们会注意到其处理输出参数 _contours
(通常是 std::vector<std::vector<cv::Point>>
类型)的方式:
// OpenCV findContours 源码示意片段 void cv::findContours( InputOutputArray _image, OutputArrayOfArrays _contours, OutputArray _hierarchy, int mode, int method, Point offset ) { // ... (一系列检查和准备工作) ... // _contours 的内存分配与数据填充,示意如下: // _contours.create(total, 1, 0, -1, true); // 为所有轮廓的集合分配概要空间 // ... // for( i = 0; i < total; i++, ++it ) // { // CvSeq* c = *it; // // ... // _contours.create((int)c->total, 1, CV_32SC2, i, true); // 为单个轮廓分配空间 // Mat ci = _contours.getMat(i); // 获取该轮廓对应的 Mat 头 // cvCvtSeqToArray(c, ci.ptr()); // 将 CvSeq 数据拷贝到 Mat 指向的内存 // } // ... }
上述代码片段揭示了潜在的风险点:OpenCV 在为 _contours
分配内存时,尤其是后续通过 _contours.getMat(i)
获取 Mat
对象并用 cvCvtSeqToArray
填充数据时,它可能对 std::vector<std::vector<cv::Point>>
的内部内存布局做出了某些假设。这种直接将 CvSeq
中的数据拷贝到由 Mat
管理的内存区域,如果该内存区域未能被 std::vector
正确识别和管理,就可能导致内存损坏。
推测原因为:
_contours.create()
方法可能不完全适用于std::vector
这种复杂类型的内存分配和初始化。std::vector
的数据存储并非总是能被简单地视为一块连续内存区域,并允许通过外部指针直接进行填充,特别是对于嵌套的vector
。
这种不匹配的操作极易破坏 std::vector
的内部状态,最终导致程序在后续访问这些轮廓数据时发生崩溃。
更稳健的解决方案:自定义封装 cvFindContours
既然底层的 cvFindContours
函数相对稳定,那么我们可以绕过 cv::findContours
中可能存在问题的内存操作,通过重新封装 cvFindContours
来实现一个更安全、更可控的轮廓查找函数。
以下是提供的自定义 findContours
函数实现,它直接调用C接口并手动管理 std::vector
的数据填充:
#include <opencv2/opencv.hpp> // 根据需要包含具体的头文件,如 imgproc.hpp, core.hpp #include <vector> // 注意:此函数签名和实现源自您提供的原始代码 void findContours_custom(const cv::Mat& src, std::vector<std::vector<cv::Point>>& contours, std::vector<cv::Vec4i>& hierarchy, int retr, int method, cv::Point offset = cv::Point()) { contours.clear(); // 清空输出 hierarchy.clear(); // 根据OpenCV版本处理CvMat,您提供的代码片段如下: #if CV_VERSION_REVISION &landroidt;= 6 // 注意:CV_VERSION_REVISION 是较老版本OpenCV的宏 CvMat c_image = src; // 在旧版本中,cv::Mat可以直接转换为CvMat // 但请注意,cvFindContours可能会修改图像,所以最好使用副本 // CvMat c_image = src.clone(); 这样更安全 #else // 对于较新的OpenCV版本 (3.x, 4.x) cv::Mat mutable_src = src.clone(); // cvFindContours会修改输入图像,务必使用副本 CvMat c_image = cvMat(mutable_src.rows, mutable_src.cols, mutable_src.type(), mutable_src.data); c_image.step = static_cast<int>(mutable_src.step[0]); // 显式转换size_t到int c_image.type = (c_image.type & ~cv::Mat::CONTINUOUS_FLAG) | (mutable_src.flags & cv::Mat::CONTINUOUS_FLAG); #endif cv::MemStorage storage(cvCreateMemStorage(0)); // 创建内存存储区 CvSeq* _ccontours = nullptr; // C风格的轮廓序列指针 // 根据OpenCV版本调用cvFindContours,您提供的代码片段如下: #if CV_VERSION_REVISION <= 6 cvFindContours(&c_image, storage, &_ccontours, sizeof(CvContour), retr, method, cvPoint(offset.x, offset.y)); #else cvFindContours(&c_image, storage, &_ccontours, sizeof(CvContour), retr, method, cvPoint(offset.x, offset.y)); // CvPoint构造方式一致 #endif if (!_ccontours) // 如果编程客栈没有找到轮廓 { contours.clear(); // 再次确保清空 hierarchy.clear(); // storage 会在 cv::MemStorage 对象析构时自动释放 return; } // 使用 cvTreeToNodeSeq 获取所有轮廓的扁平序列,这对于后续处理(尤其是层级结构)更方便 cv::Seq<CvSeq*> all_contours(cvTreeToNodeSeq(_ccontours, sizeof(CvSeq), storage)); size_t total = all_contours.size(); contours.resize(total); // 为轮廓数据预分配空间 hierarchy.resize(total); // 为层级数据预分配空间 cv::SeqIterator<CvSeq*> it = all_contours.begin(); for (size_t i = 0; i < total; ++i, ++it) { CvSeq* c = *it; // 将轮廓的颜色(CvContour的成员)设置为其索引,用于后续层级信息的链接 reinterpret_cast<CvContour*>(c)->color = static_cast<int>(i); int count = c->total; // 当前轮廓包含的点数 if (count > 0) { // 您提供的原始代码中使用 new int[] 来中转点坐标 int* data = new int[static_cast<size_t>(count * 2)]; // 分配临时内存存储x,y坐标对 cvCvtSeqToArray(c, data, CV_WHOLE_SEQ); // 将CvSeq中的点集数据拷贝到data数组 contours[i].reserve(count); // 为当前轮廓的点集预分配空间 for (int j = 0; j < count; ++j) { contours[i].push_back(cv::Point(data[j * 2], data[j * 2 + 1])); } delete[] data; // 释放临时内存 } } // 填充层级信息 (hierarchy) it = all_contours.begin(); // 重置迭代器 for (size_t i = 0; i < total; ++i, ++it) { CvSeq* c = *it; // 通过之前设置的 color (即索引) 来获取层级关系 int h_next = c->h_next ? reinterpret_cast<CvContour*>(c->h_next)->color : -1; int h_prev = c->h_prev ? reinterpret_cast<CvContour*>(c->h_prev)->color : -1; int v_next = c->v_next ? reinterpret_cast<CvContour*>(c->v_next)->color : -1; // 第一个子轮廓 编程int v_prev = c->v_prev ? reinterpret_cast<CvContour*>(c->v_prev)->color : -1; // 父轮廓 hierarchy[i] = cv::Vec4i(h_next, h_prev, v_next, v_prev); } // storage 会在 cv::MemStorage 对象析构时自动释放,无需显式调用 cvReleaseMemStorage }
自定义函数的关键改进点:
- 直接调用C接口:函数核心是调用
cvFindContours
,避免了C++封装层中可疑的内存操作。 - 安全的内存管理:使用
cv::MemStorage
为C函数管理内存。 - 显式数据转换与填充:
- 通过
cvTreeToNodeSeq
获取所有轮廓的扁平列表,这简化了迭代和层级构建。 - 为
std::vector<std::vector<cv::Point>> contours
和std::vector<cv::Vec4i> hierarchy
调用resize
进行预分配。 - 对于每个
CvSeq
,您的原始方案是先用cvCvtSeqToArray
将点数据读入一个临时的int
数组data
,然后再遍历这个data
数组,逐点构造cv::Point
对象并push_back
到对应的contours[i]
中。 - 这种方式虽然多了一步中转,但确保了
std::vector
完全自主地管理其元素的内存。
- 通过
- 层级信息构建:通过在第一次遍历轮廓时将
CvContour
的color
成员设置为其在all_contours
序列中的索引,然后在第二次遍历时利用这个索引来正确构建hierarchy
向量。
这种方法虽然代码量稍多,但给予了开发者对内存操作更大的控制权,从而有效规避了标准 cv::findContours
C++ 接口在特定情况下可能引发的内存问题。
测试用例
下面是一个使用上述 findContours_custom
函数的C++示例程序。
test_custom_findcontours_cn.cpp
:
#include <opencv2/imgproc.hpp> #include <opencv2/highgui.hpp> #include <opencv2/core/types_c.h> // 为了 CvMat, CvSeq 等C语言结构 #include <opencv2/imgproc/types_c.h> // 为了 CV_*, CvContour, CvPoint 等 #include <IOStream> #include <vector> // --- [粘贴上面提供的 findContours_custom 函数代码到这里] --- void findContours_custom(const cv::Mat& src, std::vector<std::vector<cv::Point>>& contours, std::vector<cv::Vec4i>& hierarchy, int retr, int method, cv::Point offset = cv::Point()) { contours.clear(); hierarchy.clear(); #if CV_VERSION_REVISION <= 6 cv::Mat mutable_src_for_c_api = src.clone(); // 为旧版API准备可修改的副本 CvMat c_image = mutable_src_for_c_api; #else cv::Mat mutable_src_for_c_api = src.clone(); CvMat c_image = cvMat(mutable_src_for_c_api.rows, mutable_src_for_c_api.cols, mutable_src_for_c_api.type(), mutable_src_for_c_api.data); c_image.step = static_cast<int>(mutable_src_for_c_api.step[0]); c_image.type = (c_image.type & ~cv::Mat::CONTINUOUS_FLAG) | (mutable_src_for_c_api.flags & cv::Mat::CONTINUOUS_FLAG); #endif cv::MemStorage storage(cvCreateMemStorage(0)); CvSeq* _ccontours = nullptr; #if CV_VERSION_REVISION <= 6 cvFindContours(&c_image, storage, &_ccontours, sizeof(CvContour), retr, method, cvPoint(offset.x, offset.y)); #else cvFindContours(&c_image, storage, &_ccontours, sizeof(CvContour), retr, method, cvPoint(offset.x, offset.y)); #endif if (!_ccontours) { contours.clear(); hierarchy.clear(); return; } cv::Seq<CvSeq*> all_contours(cvTreeToNodeSeq(_ccontours, sizeof(CvSeq), storage)); size_t total = all_contours.size(); contours.resize(total); hierarchy.resize(total); cv::SeqIterator<CvSeq*> it = all_contours.begin(); for (size_t i = 0; i < total; ++i, ++it) { CvSeq* c = *it; reinterpret_cast<CvContour*>(c)->color = static_cast<int>(i); int count = c->total; if (count > 0) { int* data = new int[static_cast<size_t>(count * 2)]; cvCvtSeqToArray(c, data, CV_WHOLE_SEQ); contours[i].reserve(count); for (int j = 0; j < count; ++j) { contours[i].push_back(cv::Point(data[j * 2], data[j * 2 + 1])); } delete[] data; } } it = all_contours.begin(); for (size_t i = 0; i < total; ++i, ++it) { CvSeq* c = *it; int h_next = c->h_next ? reinterpret_cast<CvContour*>(c->h_next)->color : -1; int h_prev = c->h_prev ? reinterpret_cast<CvContour*>(c->h_prev)->color : -1; int v_next = c->v_next ? reinterpret_cast<CvContour*>(c->v_next)->color : -1; int v_prev = c->v_prev ? reinterpret_cast<CvContour*>(c->v_prev)->color : -1; hierarchy[i] = cv::Vec4i(h_next, h_prev, v_next, v_prev); } } // --- [findContours_custom 函数代码结束] --- int main() { // 1. 创建一个示例二值图像 cv::Mat image = cv::Mat::zeros(300, 300, CV_8UC1); // 黑色背景 // 绘制一个白色外层矩形 cv::rectangle(image, cv::Rect(30, 30, 240, 240), cv::Scalar(255), cv::FILLED); // 在外层矩形内部绘制一个黑色矩形(形成一个“洞”) cv::rectangle(image, cv::Rect(80, 80, 140, 140), cv::Scalar(0), cv::FILLED); // 再绘制一个独立的白色小矩形 cv::rectangle(image, cv::Rect(10, 10, 50, 50), cv::Scalar(255), cv::FILLED); if (image.empty()) { std::cerr << "错误:无法创建示例图像。" << std::endl; return -1; } // 2. 准备输出容器 std::vector<std::vector<cv::Point>> contours_vec; std::vector<cv::Vec4i> hierarchy_vec; // 3. 调用自定义的 findContours_custom 函数 // 使用 cv::RETR_TREE 来测试层级结构 findContours_custom(image, contours_vec, hierarchy_vec, cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE); // 4. 输出结果 std::cout << "自定义函数找到的轮廓数量: " << contours_vec.size() << std::endl; for (size_t i = 0; i < contours_vec.size(); ++i) { std::cout << "轮廓 #" << i << ": " << contours_vec[i].size() << " 个点. "; std::cout << "层级信息: " << hierarchy_vec[i] << std::endl; } // 5. 可选: 显示编程结果图像 cv::Mat contour_output_image = cv::Mat::zeros(image.size(), CV_8UC3); cv::RNG rng(12345); // 用于生成随机颜色 for (size_t i = 0; i < contours_vec.size(); i++) { // 为每个轮廓随机选择一种颜色 cv::Scalar color = cv::Scalar(rng.uniform(0, 256), rng.uniform(0, 256), rng.uniform(0, 256)); // 绘制轮廓 cv::drawContours(contour_output_image, contours_vec, (int)i, color, 2, cv::LINE_8, hierarchy_vec, 0); } cv::imshow("原始测试图像", image); cv::imshow("检测到的轮廓 (自定义函数)", contour_output_image); cv::waitKey(0); // 等待按键 return 0; }
编译和运行示例 (使用g++):
g++ test_custom_findcontours_cn.cpp -o test_custom_findcontours_cn $(pkg-config --cflags --libs opencv4) ./test_custom_findcontours_cn
(如果你的 OpenCV 版本不是4,或者 pkg-config
未正确配置,请相应调整 opencv4
为 opencv
或你的实际库名和路径)。
此测试用例会创建一个包含嵌套结构的简单图像,调用 findContours_custom
函数,打印检测到的轮廓数量及其层级信息,并最终将检测结果可视化显示。在之前可能导致崩溃的 Windows 环境下,此自定义函数应该能够稳定运行。
通过采用这种自定义封装策略,开发者可以更从容地应对 OpenCV 在特定平台下可能出现的稳定性问题,确保轮廓检测功能的可靠性。
到此这篇关于c/c++的findcontours崩溃解决方案的文章就介绍到这了,更多相关c++ findcontours崩溃内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!
精彩评论