剖析c/c++的findcontours崩潰完美解決方案
解決 Windows 平臺(tái) OpenCV findContours
崩潰:一種更穩(wěn)定的方法
許多在 Windows 平臺(tái)上使用 OpenCV 的開(kāi)發(fā)者可能會(huì)在使用 findContours
函數(shù)時(shí),遇到令人頭疼的程序崩潰問(wèn)題。盡管網(wǎng)絡(luò)上流傳著多種解決方案,但它們并非總能根治此問(wèn)題。
當(dāng)時(shí)我也是挨個(gè)排查才找到原來(lái)是findcontours的崩潰,他奔潰的在內(nèi)存上,有些圖不崩潰有些圖必崩潰搞得很莫名其妙,今天就來(lái)講講我的解決方案
常見(jiàn)的“藥方”包括:
- 修改項(xiàng)目配置:配置屬性 -> 常規(guī) -> 項(xiàng)目默認(rèn)值 -> MFC的使用 -> 在共享DLL中使用MFC;
- 調(diào)整C/C++代碼生成選項(xiàng):C/C++ -> 代碼生成 -> 運(yùn)行庫(kù) -> 多線(xiàn)程DLL(/MD);
- 代碼層面規(guī)范:例如 vector 使用 cv::vector,vector<vector<Point>> 聲明時(shí)預(yù)分配空間等。
然而,現(xiàn)實(shí)情況是,許多開(kāi)發(fā)者嘗試上述方法后,問(wèn)題依舊。即便少數(shù)情況下問(wèn)題得到偶然解決,程序在遷移到不同環(huán)境或 OpenCV 版本時(shí),仍可能面臨兼容性風(fēng)險(xiǎn)。
本文將深入剖析此問(wèn)題的潛在原因,并提供一個(gè)更可靠的定制化實(shí)現(xiàn)方案。
探究崩潰的根源
為了有效地解決 findContours
引發(fā)的異常,理解其內(nèi)部機(jī)制至關(guān)重要。cv::findContours
的C++接口實(shí)際上是對(duì)底層C語(yǔ)言風(fēng)格函數(shù) cvFindContours
(或其變體 cvFindContours_Impl
)的一層封裝。
一個(gè)關(guān)鍵的觀(guān)察點(diǎn)是:直接調(diào)用C語(yǔ)言風(fēng)格的 cvFindContours
函數(shù)往往能夠正常運(yùn)行,這暗示問(wèn)題很可能出在C++封裝層對(duì)數(shù)據(jù)結(jié)構(gòu)的處理上。
仔細(xì)研讀 cv::findContours
的源碼(盡管具體實(shí)現(xiàn)可能隨版本略有差異),我們會(huì)注意到其處理輸出參數(shù) _contours
(通常是 std::vector<std::vector<cv::Point>>
類(lèi)型)的方式:
// OpenCV findContours 源碼示意片段 void cv::findContours( InputOutputArray _image, OutputArrayOfArrays _contours, OutputArray _hierarchy, int mode, int method, Point offset ) { // ... (一系列檢查和準(zhǔn)備工作) ... // _contours 的內(nèi)存分配與數(shù)據(jù)填充,示意如下: // _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); // 為單個(gè)輪廓分配空間 // Mat ci = _contours.getMat(i); // 獲取該輪廓對(duì)應(yīng)的 Mat 頭 // cvCvtSeqToArray(c, ci.ptr()); // 將 CvSeq 數(shù)據(jù)拷貝到 Mat 指向的內(nèi)存 // } // ... }
上述代碼片段揭示了潛在的風(fēng)險(xiǎn)點(diǎn):OpenCV 在為 _contours
分配內(nèi)存時(shí),尤其是后續(xù)通過(guò) _contours.getMat(i)
獲取 Mat
對(duì)象并用 cvCvtSeqToArray
填充數(shù)據(jù)時(shí),它可能對(duì) std::vector<std::vector<cv::Point>>
的內(nèi)部?jī)?nèi)存布局做出了某些假設(shè)。這種直接將 CvSeq
中的數(shù)據(jù)拷貝到由 Mat
管理的內(nèi)存區(qū)域,如果該內(nèi)存區(qū)域未能被 std::vector
正確識(shí)別和管理,就可能導(dǎo)致內(nèi)存損壞。
推測(cè)原因?yàn)椋?/p>
_contours.create()
方法可能不完全適用于std::vector
這種復(fù)雜類(lèi)型的內(nèi)存分配和初始化。std::vector
的數(shù)據(jù)存儲(chǔ)并非總是能被簡(jiǎn)單地視為一塊連續(xù)內(nèi)存區(qū)域,并允許通過(guò)外部指針直接進(jìn)行填充,特別是對(duì)于嵌套的vector
。
這種不匹配的操作極易破壞 std::vector
的內(nèi)部狀態(tài),最終導(dǎo)致程序在后續(xù)訪(fǎng)問(wèn)這些輪廓數(shù)據(jù)時(shí)發(fā)生崩潰。
更穩(wěn)健的解決方案:自定義封裝 cvFindContours
既然底層的 cvFindContours
函數(shù)相對(duì)穩(wěn)定,那么我們可以繞過(guò) cv::findContours
中可能存在問(wèn)題的內(nèi)存操作,通過(guò)重新封裝 cvFindContours
來(lái)實(shí)現(xiàn)一個(gè)更安全、更可控的輪廓查找函數(shù)。
以下是提供的自定義 findContours
函數(shù)實(shí)現(xiàn),它直接調(diào)用C接口并手動(dòng)管理 std::vector
的數(shù)據(jù)填充:
#include <opencv2/opencv.hpp> // 根據(jù)需要包含具體的頭文件,如 imgproc.hpp, core.hpp #include <vector> // 注意:此函數(shù)簽名和實(shí)現(xiàn)源自您提供的原始代碼 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(); // 根據(jù)OpenCV版本處理CvMat,您提供的代碼片段如下: #if CV_VERSION_REVISION <= 6 // 注意:CV_VERSION_REVISION 是較老版本OpenCV的宏 CvMat c_image = src; // 在舊版本中,cv::Mat可以直接轉(zhuǎn)換為CvMat // 但請(qǐng)注意,cvFindContours可能會(huì)修改圖像,所以最好使用副本 // CvMat c_image = src.clone(); 這樣更安全 #else // 對(duì)于較新的OpenCV版本 (3.x, 4.x) cv::Mat mutable_src = src.clone(); // cvFindContours會(huì)修改輸入圖像,務(wù)必使用副本 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]); // 顯式轉(zhuǎn)換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)); // 創(chuàng)建內(nèi)存存儲(chǔ)區(qū) CvSeq* _ccontours = nullptr; // C風(fēng)格的輪廓序列指針 // 根據(jù)OpenCV版本調(diào)用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構(gòu)造方式一致 #endif if (!_ccontours) // 如果沒(méi)有找到輪廓 { contours.clear(); // 再次確保清空 hierarchy.clear(); // storage 會(huì)在 cv::MemStorage 對(duì)象析構(gòu)時(shí)自動(dòng)釋放 return; } // 使用 cvTreeToNodeSeq 獲取所有輪廓的扁平序列,這對(duì)于后續(xù)處理(尤其是層級(jí)結(jié)構(gòu))更方便 cv::Seq<CvSeq*> all_contours(cvTreeToNodeSeq(_ccontours, sizeof(CvSeq), storage)); size_t total = all_contours.size(); contours.resize(total); // 為輪廓數(shù)據(jù)預(yù)分配空間 hierarchy.resize(total); // 為層級(jí)數(shù)據(jù)預(yù)分配空間 cv::SeqIterator<CvSeq*> it = all_contours.begin(); for (size_t i = 0; i < total; ++i, ++it) { CvSeq* c = *it; // 將輪廓的顏色(CvContour的成員)設(shè)置為其索引,用于后續(xù)層級(jí)信息的鏈接 reinterpret_cast<CvContour*>(c)->color = static_cast<int>(i); int count = c->total; // 當(dāng)前輪廓包含的點(diǎn)數(shù) if (count > 0) { // 您提供的原始代碼中使用 new int[] 來(lái)中轉(zhuǎn)點(diǎn)坐標(biāo) int* data = new int[static_cast<size_t>(count * 2)]; // 分配臨時(shí)內(nèi)存存儲(chǔ)x,y坐標(biāo)對(duì) cvCvtSeqToArray(c, data, CV_WHOLE_SEQ); // 將CvSeq中的點(diǎn)集數(shù)據(jù)拷貝到data數(shù)組 contours[i].reserve(count); // 為當(dāng)前輪廓的點(diǎn)集預(yù)分配空間 for (int j = 0; j < count; ++j) { contours[i].push_back(cv::Point(data[j * 2], data[j * 2 + 1])); } delete[] data; // 釋放臨時(shí)內(nèi)存 } } // 填充層級(jí)信息 (hierarchy) it = all_contours.begin(); // 重置迭代器 for (size_t i = 0; i < total; ++i, ++it) { CvSeq* c = *it; // 通過(guò)之前設(shè)置的 color (即索引) 來(lái)獲取層級(jí)關(guān)系 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; // 第一個(gè)子輪廓 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 會(huì)在 cv::MemStorage 對(duì)象析構(gòu)時(shí)自動(dòng)釋放,無(wú)需顯式調(diào)用 cvReleaseMemStorage }
自定義函數(shù)的關(guān)鍵改進(jìn)點(diǎn):
- 直接調(diào)用C接口:函數(shù)核心是調(diào)用
cvFindContours
,避免了C++封裝層中可疑的內(nèi)存操作。 - 安全的內(nèi)存管理:使用
cv::MemStorage
為C函數(shù)管理內(nèi)存。 - 顯式數(shù)據(jù)轉(zhuǎn)換與填充:
- 通過(guò)
cvTreeToNodeSeq
獲取所有輪廓的扁平列表,這簡(jiǎn)化了迭代和層級(jí)構(gòu)建。 - 為
std::vector<std::vector<cv::Point>> contours
和std::vector<cv::Vec4i> hierarchy
調(diào)用resize
進(jìn)行預(yù)分配。 - 對(duì)于每個(gè)
CvSeq
,您的原始方案是先用cvCvtSeqToArray
將點(diǎn)數(shù)據(jù)讀入一個(gè)臨時(shí)的int
數(shù)組data
,然后再遍歷這個(gè)data
數(shù)組,逐點(diǎn)構(gòu)造cv::Point
對(duì)象并push_back
到對(duì)應(yīng)的contours[i]
中。 - 這種方式雖然多了一步中轉(zhuǎn),但確保了
std::vector
完全自主地管理其元素的內(nèi)存。
- 通過(guò)
- 層級(jí)信息構(gòu)建:通過(guò)在第一次遍歷輪廓時(shí)將
CvContour
的color
成員設(shè)置為其在all_contours
序列中的索引,然后在第二次遍歷時(shí)利用這個(gè)索引來(lái)正確構(gòu)建hierarchy
向量。
這種方法雖然代碼量稍多,但給予了開(kāi)發(fā)者對(duì)內(nèi)存操作更大的控制權(quán),從而有效規(guī)避了標(biāo)準(zhǔn) cv::findContours
C++ 接口在特定情況下可能引發(fā)的內(nèi)存問(wèn)題。
測(cè)試用例
下面是一個(gè)使用上述 findContours_custom
函數(shù)的C++示例程序。
test_custom_findcontours_cn.cpp
:
#include <opencv2/imgproc.hpp> #include <opencv2/highgui.hpp> #include <opencv2/core/types_c.h> // 為了 CvMat, CvSeq 等C語(yǔ)言結(jié)構(gòu) #include <opencv2/imgproc/types_c.h> // 為了 CV_*, CvContour, CvPoint 等 #include <iostream> #include <vector> // --- [粘貼上面提供的 findContours_custom 函數(shù)代碼到這里] --- 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準(zhǔn)備可修改的副本 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 函數(shù)代碼結(jié)束] --- int main() { // 1. 創(chuàng)建一個(gè)示例二值圖像 cv::Mat image = cv::Mat::zeros(300, 300, CV_8UC1); // 黑色背景 // 繪制一個(gè)白色外層矩形 cv::rectangle(image, cv::Rect(30, 30, 240, 240), cv::Scalar(255), cv::FILLED); // 在外層矩形內(nèi)部繪制一個(gè)黑色矩形(形成一個(gè)“洞”) cv::rectangle(image, cv::Rect(80, 80, 140, 140), cv::Scalar(0), cv::FILLED); // 再繪制一個(gè)獨(dú)立的白色小矩形 cv::rectangle(image, cv::Rect(10, 10, 50, 50), cv::Scalar(255), cv::FILLED); if (image.empty()) { std::cerr << "錯(cuò)誤:無(wú)法創(chuàng)建示例圖像。" << std::endl; return -1; } // 2. 準(zhǔn)備輸出容器 std::vector<std::vector<cv::Point>> contours_vec; std::vector<cv::Vec4i> hierarchy_vec; // 3. 調(diào)用自定義的 findContours_custom 函數(shù) // 使用 cv::RETR_TREE 來(lái)測(cè)試層級(jí)結(jié)構(gòu) findContours_custom(image, contours_vec, hierarchy_vec, cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE); // 4. 輸出結(jié)果 std::cout << "自定義函數(shù)找到的輪廓數(shù)量: " << contours_vec.size() << std::endl; for (size_t i = 0; i < contours_vec.size(); ++i) { std::cout << "輪廓 #" << i << ": " << contours_vec[i].size() << " 個(gè)點(diǎn). "; std::cout << "層級(jí)信息: " << hierarchy_vec[i] << std::endl; } // 5. 可選: 顯示結(jié)果圖像 cv::Mat contour_output_image = cv::Mat::zeros(image.size(), CV_8UC3); cv::RNG rng(12345); // 用于生成隨機(jī)顏色 for (size_t i = 0; i < contours_vec.size(); i++) { // 為每個(gè)輪廓隨機(jī)選擇一種顏色 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("原始測(cè)試圖像", image); cv::imshow("檢測(cè)到的輪廓 (自定義函數(shù))", contour_output_image); cv::waitKey(0); // 等待按鍵 return 0; }
編譯和運(yùn)行示例 (使用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
未正確配置,請(qǐng)相應(yīng)調(diào)整 opencv4
為 opencv
或你的實(shí)際庫(kù)名和路徑)。
此測(cè)試用例會(huì)創(chuàng)建一個(gè)包含嵌套結(jié)構(gòu)的簡(jiǎn)單圖像,調(diào)用 findContours_custom
函數(shù),打印檢測(cè)到的輪廓數(shù)量及其層級(jí)信息,并最終將檢測(cè)結(jié)果可視化顯示。在之前可能導(dǎo)致崩潰的 Windows 環(huán)境下,此自定義函數(shù)應(yīng)該能夠穩(wěn)定運(yùn)行。
通過(guò)采用這種自定義封裝策略,開(kāi)發(fā)者可以更從容地應(yīng)對(duì) OpenCV 在特定平臺(tái)下可能出現(xiàn)的穩(wěn)定性問(wèn)題,確保輪廓檢測(cè)功能的可靠性。
到此這篇關(guān)于c/c++的findcontours崩潰解決方案的文章就介紹到這了,更多相關(guān)c++ findcontours崩潰內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于C語(yǔ)言char與unsigned char的區(qū)別介紹
本篇文章小編為大家介紹,基于C語(yǔ)言char與unsigned char的區(qū)別介紹。需要的朋友參考下2013-04-04

Qt 使用Poppler實(shí)現(xiàn)pdf閱讀器的示例代碼

C語(yǔ)言通訊錄管理系統(tǒng)課程設(shè)計(jì)

C++新特性詳細(xì)分析基于范圍的for循環(huán)