diff --git a/AlgorithmModule/include/CheckUtil.hpp b/AlgorithmModule/include/CheckUtil.hpp index df240f3..ae7ae16 100644 --- a/AlgorithmModule/include/CheckUtil.hpp +++ b/AlgorithmModule/include/CheckUtil.hpp @@ -19,12 +19,225 @@ #include #include #include +#include #include +#include +#include +#include +#include #include -#include #include +#include +#include +#include #include using namespace std; + +// ============================================================================ +// 异步存图器(header-only 单例) +// 场景:检测热点循环里直接 cv::imwrite,编码 + 写盘会把主流程拖慢 +// (例:CTcsCheck::AdaptiveBinary 逐块存残点调试图)。 +// 做法:调用方把图 clone 后入队即返回,独立后台线程串行编码落盘, +// 调用方只付一次内存拷贝的代价,不再等磁盘 IO。 +// 线程安全;队列有长度上限,写盘慢于入队时 Push 会阻塞(背压),避免内存暴涨。 +// 实现放在头文件、不依赖 CheckUtil.cpp:TcsCheck 这类未链接 AlgorithmModule +// 的模块也能直接使用(否则会形成反向链接依赖)。 +// ============================================================================ +class CAsyncImgSaver +{ +public: + static CAsyncImgSaver &Instance() + { + static CAsyncImgSaver s_inst; // C++11 起局部静态初始化线程安全 + return s_inst; + } + + // 入队一张图(img 会被 clone,返回后调用方可立即释放/复用原图) + void Push(const std::string &strPath, const cv::Mat &img, const std::vector &vParams) + { + if (strPath.empty() || img.empty()) + { + return; + } + + ImgTask task; + task.strPath = strPath; + task.vParams = vParams; + task.mat = img.clone(); // 深拷贝,隔离调用方数据的生命周期 + + std::unique_lock lk(m_mtx); + if (m_bStop) + { + return; // 已关闭:直接丢弃,避免后台线程退出后无限等待 + } + if (m_nQueueLimit > 0) + { + // 队列满则阻塞等待(背压),顺带让 cv::Mat 内存占用可控 + m_cvHasRoom.wait(lk, [this] { return m_bStop || m_deqTask.size() < m_nQueueLimit; }); + if (m_bStop) + { + return; + } + } + m_deqTask.push_back(std::move(task)); + ++m_nPending; + lk.unlock(); + m_cvHasTask.notify_one(); + } + + // 等待队列中(含正在写盘)的图全部落盘 + void WaitIdle() + { + std::unique_lock lk(m_mtx); + m_cvIdle.wait(lk, [this] { return m_nPending == 0; }); + } + + // 队列长度上限(张);调大后唤醒被背压阻塞的入队方 + void SetQueueLimit(std::size_t nLimit) + { + { + std::lock_guard lk(m_mtx); + m_nQueueLimit = nLimit; + } + m_cvHasRoom.notify_all(); + } + + // 关闭:排空剩余队列后退出后台线程(可重复调用) + void Stop() + { + { + std::lock_guard lk(m_mtx); + m_bStop = true; + } + m_cvHasTask.notify_all(); + m_cvHasRoom.notify_all(); + if (m_thread.joinable()) + { + m_thread.join(); + } + } + +private: + struct ImgTask + { + std::string strPath; + cv::Mat mat; + std::vector vParams; + }; + + CAsyncImgSaver() + : m_bStop(false) + , m_nQueueLimit(4096) // 默认最多积压 4096 张待写图 + , m_nPending(0) + { + m_thread = std::thread(&CAsyncImgSaver::Worker, this); + } + + ~CAsyncImgSaver() + { + Stop(); // 进程退出兜底:把队列里剩余的图写完再退 + } + + CAsyncImgSaver(const CAsyncImgSaver &) = delete; + CAsyncImgSaver &operator=(const CAsyncImgSaver &) = delete; + + // 后台线程主循环:串行取图 -> 建目录 -> 编码落盘 + void Worker() + { + std::string strLastDir; // 缓存上次建好的目录,避免每张图都做一次 mkdir + while (true) + { + ImgTask task; + { + std::unique_lock lk(m_mtx); + m_cvHasTask.wait(lk, [this] { return m_bStop || !m_deqTask.empty(); }); + if (m_deqTask.empty()) + { + return; // 收到停止信号且队列已排空 + } + task = std::move(m_deqTask.front()); + m_deqTask.pop_front(); + } + m_cvHasRoom.notify_one(); // 队列腾出空位,唤醒可能被阻塞的入队方 + + if (!task.mat.empty()) + { + EnsureParentDir(task.strPath, strLastDir); + try + { + cv::imwrite(task.strPath, task.mat, task.vParams); + } + catch (const cv::Exception &e) + { + std::cerr << "[SaveImgAsync] imwrite failed: " << task.strPath << " : " << e.what() << std::endl; + } + } + + { + std::lock_guard lk(m_mtx); + if (m_nPending > 0) + { + --m_nPending; + } + if (m_nPending == 0) + { + m_cvIdle.notify_all(); + } + } + } + } + + // 递归创建多级目录(已存在视为成功) + static bool MakeDirs(const std::string &strPath) + { + if (strPath.empty()) + { + return false; + } + if (mkdir(strPath.c_str(), 0755) == 0 || errno == EEXIST) + { + return true; + } + const std::size_t pos = strPath.find_last_of('/'); + if (pos == std::string::npos || pos == 0) + { + return false; + } + if (!MakeDirs(strPath.substr(0, pos))) + { + return false; + } + return (mkdir(strPath.c_str(), 0755) == 0 || errno == EEXIST); + } + + // 创建文件所在目录;strCacheDir 命中则省掉一次 mkdir + static void EnsureParentDir(const std::string &strPath, std::string &strCacheDir) + { + const std::size_t pos = strPath.find_last_of('/'); + if (pos == std::string::npos || pos == 0) + { + return; // 没有目录部分(相对文件名) + } + const std::string strDir = strPath.substr(0, pos); + if (strDir == strCacheDir) + { + return; + } + MakeDirs(strDir); + strCacheDir = strDir; + } + + std::thread m_thread; + std::mutex m_mtx; + std::condition_variable m_cvHasTask; // 有新任务 + std::condition_variable m_cvHasRoom; // 队列有空位 + std::condition_variable m_cvIdle; // 全部落盘 + std::deque m_deqTask; + bool m_bStop; + std::size_t m_nQueueLimit; + std::size_t m_nPending; // 已入队但尚未写完的数量(含正在写) +}; + class CheckUtil { public: @@ -63,6 +276,29 @@ public: static void PrintRect(cv::Rect roi, std::string str = ""); static int cutSmallImg(cv::Mat img, std::vector &samllRoiList, cv::Rect config_roi, int config_SmallImg_Width, int config_SmallImg_Height, int config_MinOverlap_Width, int config_MinOverlap_Height); static cv::Point2f transformPoint(const cv::Point2f &point, const cv::Mat &transform_matrix); + + // ===================== 异步存图(实现见文件顶部 CAsyncImgSaver) ===================== + // 入队即返回,父目录不存在会在后台自动创建;img 会被深拷贝,调用方可立即复用原图 + static void SaveImgAsync(const std::string &strPath, const cv::Mat &img, + const std::vector &vParams = std::vector()) + { + CAsyncImgSaver::Instance().Push(strPath, img, vParams); + } + // 等待队列里所有异步存图落盘(退出/校验结果前调用) + static void WaitSaveImgDone() + { + CAsyncImgSaver::Instance().WaitIdle(); + } + // 队列上限(张),默认 4096;超限时 SaveImgAsync 会阻塞(背压),0 表示不限制 + static void SetSaveImgQueueLimit(std::size_t nLimit) + { + CAsyncImgSaver::Instance().SetQueueLimit(nLimit); + } + // 主动关闭后台存图线程(排空剩余队列后退出);不调用也会在进程退出时自动收尾 + static void StopSaveImg() + { + CAsyncImgSaver::Instance().Stop(); + } }; // 无参数重载:直接返回原字符串,避免 -Wformat-security 警告 static inline std::string str_Format(const std::string &format) diff --git a/AlgorithmModule/src/ImgCheckAnalysisy.cpp b/AlgorithmModule/src/ImgCheckAnalysisy.cpp index 6707ddc..7f4c747 100644 --- a/AlgorithmModule/src/ImgCheckAnalysisy.cpp +++ b/AlgorithmModule/src/ImgCheckAnalysisy.cpp @@ -451,7 +451,7 @@ Point2f GetCoorPoint(Point2f point, Mat img_mat) return new_point; } -int GetEdgeRoi(Mat img, Rect &new_roi, cv::RotatedRect &rotated_roi, float scale_x, float scale_y){ +int GetEdgeRoi(Mat img, Rect &new_roi, cv::RotatedRect &rotated_roi, float scale_x, float scale_y, bool bSaveImg, const std::string &strCameraName){ if (img.empty()) { return 1; @@ -469,6 +469,13 @@ int GetEdgeRoi(Mat img, Rect &new_roi, cv::RotatedRect &rotated_roi, float scale // 做一步闭运算 Mat r_img_bin_close; morphologyEx(r_img_bin, r_img_bin_close, MORPH_CLOSE, Mat::ones(15, 15, CV_8U)); + + if (bSaveImg) + { + cv::imwrite(strCameraName + "_r_img_bin.png", r_img_bin); + cv::imwrite(strCameraName + "_r_img_bin_close.png", r_img_bin_close); + } + std::vector> contours; cv::findContours(r_img_bin_close, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); if (contours.empty()) @@ -611,7 +618,14 @@ int ImgCheckAnalysisy::Adapt_Config(Mat img, Rect cur_roi, cv::RotatedRect& rot_ return 1; } // cv::RotatedRect rot_cur_roi; - int get_edge_roi = GetEdgeRoi(img, cur_roi, rot_cur_roi, 20, 20); + // 只有 -s (bsaveProcessImg) 时才保存粗定位中间图;文件名以相机名开头 + bool bSaveProcessImg = (DetImgInfo_shareP != nullptr && DetImgInfo_shareP->bsaveProcessImg); + std::string strCameraName = m_AnalysisyConfig.commonCheckConfig.baseConfig.strCamearName; + if (strCameraName.empty() && DetImgInfo_shareP != nullptr) + { + strCameraName = DetImgInfo_shareP->strCameraName; + } + int get_edge_roi = GetEdgeRoi(img, cur_roi, rot_cur_roi, 20, 20, bSaveProcessImg, strCameraName); if(get_edge_roi != 0){ return 2; } diff --git a/TcsCheckModule/CMakeLists.txt b/TcsCheckModule/CMakeLists.txt index acf1c84..d1b0ec7 100644 --- a/TcsCheckModule/CMakeLists.txt +++ b/TcsCheckModule/CMakeLists.txt @@ -11,6 +11,8 @@ include(${PROJECT_SOURCE_DIR}/cmake/print_archs.cmake) include_directories( /usr/local/include ${CMAKE_CURRENT_SOURCE_DIR}/include +# CheckUtil.hpp (异步存图等通用工具) +${PROJECT_SOURCE_DIR}/AlgorithmModule/include ${OpenCV_INCLUDE_DIRS} ) link_directories( diff --git a/TcsCheckModule/src/TcsCheck.cpp b/TcsCheckModule/src/TcsCheck.cpp index 50d1276..f6c8f28 100644 --- a/TcsCheckModule/src/TcsCheck.cpp +++ b/TcsCheckModule/src/TcsCheck.cpp @@ -1,25 +1,5 @@ #include "TcsCheck.h" - -namespace { -// 静默递归创建多级目录(成功或已存在返回 true) -bool MakeDirs(const std::string& path, mode_t mode = 0755) -{ - if (path.empty()) { - return false; - } - if (mkdir(path.c_str(), mode) == 0 || errno == EEXIST) { - return true; - } - const std::size_t pos = path.find_last_of('/'); - if (pos == std::string::npos || pos == 0) { - return false; - } - if (!MakeDirs(path.substr(0, pos), mode)) { - return false; - } - return (mkdir(path.c_str(), mode) == 0 || errno == EEXIST); -} -} // namespace +#include "CheckUtil.hpp" // 异步存图:CheckUtil::SaveImgAsync / WaitSaveImgDone class CLock { @@ -204,12 +184,12 @@ cv::Mat CTcsCheck::AdaptiveBinary(cv::Mat matBlur) if (m_cpCfg.bDebugsaveImg) { std::string roi_ext = "[" + std::to_string(x) + "," + std::to_string(y) + "]"; - std::string strSaveDir = "/home/aidlux/BOE/CELL_AOI/Tradition_Detect/" + m_cpCfg.productId + "/"; - MakeDirs(strSaveDir); + std::string strSaveDir = "/home/aidlux/BOE/CELL_AOI/AI_Detect/" + m_cpCfg.productId + "/"; std::string strSavePath_in = strSaveDir + m_cpCfg.productChannel +"_"+ roi_ext + "_"+ "in" + ".png"; std::string strSavePath_out = strSaveDir + m_cpCfg.productChannel +"_"+ roi_ext + "_"+ "out" + ".png"; - cv::imwrite(strSavePath_in, block); - cv::imwrite(strSavePath_out, blockBin); + // 父目录由异步存图器按需创建 + CheckUtil::SaveImgAsync(strSavePath_in, block); + CheckUtil::SaveImgAsync(strSavePath_out, blockBin); } } } @@ -528,6 +508,9 @@ void CTcsCheck::ProcessImages(bool bDrawResult) Process(bDrawResult); } + // 异步存图收尾:确保队列里的调试图全部落盘后再返回 + CheckUtil::WaitSaveImgDone(); + }