From d8e953e1603e32439f4c82def438f84b1f692b03 Mon Sep 17 00:00:00 2001 From: liusiyang Date: Thu, 8 Oct 2026 12:27:59 +0800 Subject: [PATCH] =?UTF-8?q?update=20=E6=95=B4=E5=90=88=E9=80=9A=E9=81=93?= =?UTF-8?q?=EF=BC=8C=E7=9B=B8=E6=9C=BA=E5=8C=BA=E5=88=86=E6=98=8E=E6=9A=97?= =?UTF-8?q?=E5=9C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AlgorithmModule/include/ImgCheckAnalysisy.hpp | 4 + AlgorithmModule/src/ImgCheckAnalysisy.cpp | 167 +++++++++++++++++- example/deal.cpp | 3 +- 3 files changed, 166 insertions(+), 8 deletions(-) diff --git a/AlgorithmModule/include/ImgCheckAnalysisy.hpp b/AlgorithmModule/include/ImgCheckAnalysisy.hpp index 1fd448a..8d79dbf 100644 --- a/AlgorithmModule/include/ImgCheckAnalysisy.hpp +++ b/AlgorithmModule/include/ImgCheckAnalysisy.hpp @@ -153,11 +153,15 @@ private: // 取当前图的产品ID/相机名(优先 productBaseResult/cameraBaseResult,兕底送图信息) void GetProductCameraStr(std::string &strProduct, std::string &strCamera); // 判断并在需要时等待"源相机(明场)"的定位结果;成功返回 true(结果已装入成员变量) + // 注意:标签检测不在此复用,由 CheckTagByBigMask 独立执行 bool TryReuseAlignFromSource(std::string &strSrcCamera, std::string &strSrcChannel, int &nWaitMs); // 发布本图定位结果/失败标记(供同产品其它相机复用) void PublishAlignShare(bool bSucc); // 用 bigmask 检测把手(bigmask 主体近似矩形,把手是四周凸起的矩形块) int CheckHandleByBigMask(const cv::Mat &img, const cv::Mat &bigmask); + // 标签检测(独立步骤):用 (图像 + bigmask) 跑 Tag_Loc 模型,检出结果存 m_tag_roiList + // 明场传自身 bigmask;暗场不做定位模型,传复用明场的 bigmask + int CheckTagByBigMask(const cv::Mat &img, const cv::Mat &bigmask); // 在某个把手区域附近找宽高匹配的凸起:返回凸起在 candList 中的下标(-1=没找到) int MatchHandleRegion(const std::vector &candList, const std::vector &usedList, const cv::Rect &boxRegion, const Handle_Check_Param ¶m, cv::Rect &roiHandle); diff --git a/AlgorithmModule/src/ImgCheckAnalysisy.cpp b/AlgorithmModule/src/ImgCheckAnalysisy.cpp index 80d10a5..fbb7e47 100644 --- a/AlgorithmModule/src/ImgCheckAnalysisy.cpp +++ b/AlgorithmModule/src/ImgCheckAnalysisy.cpp @@ -628,7 +628,7 @@ void ImgCheckAnalysisy::PublishAlignShare(bool bSucc) } // 暗场相机不做定位模型:等待并复用明场相机的定位结果 -// 返回 true 时:m_pEdge_Align_Result / m_outer_rroi / m_inner_rroi / m_tag_roiList 已用明场结果填充 +// 返回 true 时:m_pEdge_Align_Result / m_outer_rroi / m_inner_rroi 已用明场结果填充(标签检测独立执行) // 返回 false:非复用相机,或复用失败(超时/源失败/无源图/尺寸不一致),调用方走自有定位 bool ImgCheckAnalysisy::TryReuseAlignFromSource(std::string &strSrcCamera, std::string &strSrcChannel, int &nWaitMs) { @@ -640,13 +640,19 @@ bool ImgCheckAnalysisy::TryReuseAlignFromSource(std::string &strSrcCamera, std:: std::string strCamera; GetProductCameraStr(strProduct, strCamera); - // 相机名/通道名都要能按 dark->bright 替换,否则本相机不在复用规则内 + // 是否复用由相机名决定(明/暗场是相机属性):相机名必须能按 dark->bright 替换 std::string strSrcCam = ReplaceDarkWithBright(strCamera); - std::string strSrcChn = ReplaceDarkWithBright(m_strCurDetChannel); - if (strSrcCam.empty() || strSrcChn.empty()) + if (strSrcCam.empty()) { return false; } + // 通道名可能不带 dark/bright(同一面的明/暗场相机共用一个通道名),此时沿用本相机通道名; + // 若带 dark 则同样替换,保证与源相机发布结果时的 key 一致 + std::string strSrcChn = ReplaceDarkWithBright(m_strCurDetChannel); + if (strSrcChn.empty()) + { + strSrcChn = m_strCurDetChannel; + } strSrcCamera = strSrcCam; strSrcChannel = strSrcChn; @@ -714,14 +720,13 @@ bool ImgCheckAnalysisy::TryReuseAlignFromSource(std::string &strSrcCamera, std:: } } - // 复用明场定位结果:ROI/掩码/映射都用明场的 + // 复用明场定位结果:ROI/掩码/映射都用明场的(标签检测由 CheckTagByBigMask 独立执行,不用明场的标签) m_pEdge_Align_Result = pSrc; m_outer_rroi = pSrc->bigroi[0]; if (!pSrc->smallroi.empty()) { m_inner_rroi = pSrc->smallroi[0]; } - m_tag_roiList = pSrc->tagroi; // 直接赋值:明场没有标签时清空(避免沿用上一张的标签) return true; #endif } @@ -797,7 +802,7 @@ int ImgCheckAnalysisy::CheckRun() long time_edge_e; if (TryReuseAlignFromSource(strAlignSrcCamera, strAlignSrcChannel, nAlignReuseWaitMs)) { - // 复用成功:m_pEdge_Align_Result / m_outer_rroi / m_inner_rroi / m_tag_roiList 已用明场结果填充 + // 复用成功:m_pEdge_Align_Result / m_outer_rroi / m_inner_rroi 已用明场结果填充(标签检测独立执行,不复用明场标签) PublishAlignShare(true); time_edge_e = CheckUtil::getcurTime(); m_pdetlog->AddCheckstr(PrintLevel_0, "2、pre detect", @@ -810,6 +815,20 @@ int ImgCheckAnalysisy::CheckRun() long handle_e = CheckUtil::getcurTime(); m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Handle", "handle detect use time %ld", handle_e - handle_s); } + // 标签检测(独立步骤):暗场图像 + 明场 bigmask(不再沿用明场检出的标签) + { + long tag_s = CheckUtil::getcurTime(); + int reTag = CheckTagByBigMask(m_CheckResult_shareP->in_shareImage->img, m_pEdge_Align_Result->bigmask); + long tag_e = CheckUtil::getcurTime(); + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Tag", "tag detect use time %ld", tag_e - tag_s); + if (reTag != 0) + { + m_pdetlog->AddCheckstr(PrintLevel_0, "Error", "CheckTagByBigMask is error type = %d", reTag); + m_nErrorCode = reTag; + m_nCheckResultErrorCode = m_nErrorCode; + return m_nErrorCode; + } + } } else { @@ -2688,6 +2707,140 @@ int ImgCheckAnalysisy::CheckHandleByBigMask(const cv::Mat &img, const cv::Mat &b return 0; } +// 标签检测(独立步骤):用 (图像 + 定位结果) 跑 Tag_Loc 模型 +// 裁剪区域与定位流程里 Tag_Loc 的口径完全一致(定位得到的产品外接旋转矩形): +// 明场相机用自己的定位结果;暗场相机没做定位模型,用复用明场的结果 —— 保证暗/明场对同一摆位切出同样区域。 +// 注意:标签模型对裁剪尺度很敏感(裁剪差 3px 就会让检出个数变化),不能另用 bigmask 重推裁剪框。 +// bigmask 仅在定位外接矩形异常时作兜底(最大轮廓外接框)。 +// 检出的标签 roi 存 m_tag_roiList(裁剪图坐标系,与定位流程 tagroi 的约定一致),由 CheckRun 统一上报 NG。 +int ImgCheckAnalysisy::CheckTagByBigMask(const cv::Mat &img, const cv::Mat &bigmask) +{ + m_tag_roiList.clear(); + if (!AI_Factory || !AI_Factory->Tag_Loc) + { + m_pdetlog->AddCheckstr(PrintLevel_0, "Error", "Tag_Loc model is null"); + return 1; + } + if (img.empty()) + { + m_pdetlog->AddCheckstr(PrintLevel_0, "Error", "tag check img is empty"); + return 2; + } + + // 1、裁剪区域:与定位流程 Tag_Loc 的裁剪口径一致(产品外接旋转矩形,暗场为复用明场的值) + cv::Rect roiCrop = m_outer_rroi.boundingRect() & cv::Rect(0, 0, img.cols, img.rows); + if (roiCrop.width <= 0 || roiCrop.height <= 0) + { + // 异常兜底:定位外接矩形无效时,用 bigmask 最大轮廓外接框 + if (bigmask.empty()) + { + m_pdetlog->AddCheckstr(PrintLevel_0, "Error", "tag check roi invalid and bigmask is empty"); + return 3; + } + cv::Mat binMask; + { + cv::Mat grayMask; + if (bigmask.channels() != 1) + { + cv::cvtColor(bigmask, grayMask, cv::COLOR_BGR2GRAY); + } + else + { + grayMask = bigmask; + } + double dMin = 0; + double dMax = 0; + cv::minMaxLoc(grayMask, &dMin, &dMax); + // 兼容 0/1 与 0/255 掩膜:取半强度作阈值(重采样后的边缘取到真实边界) + cv::threshold(grayMask, binMask, dMax > 1 ? dMax * 0.5 : 0, 255, cv::THRESH_BINARY); + } + bool bFound = false; + std::vector contour = CheckUtil::getLargestContourROI(binMask, bFound); + if (!bFound || contour.size() < 3) + { + m_pdetlog->AddCheckstr(PrintLevel_0, "Error", "tag check bigmask contour too few"); + return 4; + } + roiCrop = cv::minAreaRect(contour).boundingRect() & cv::Rect(0, 0, img.cols, img.rows); + if (roiCrop.width <= 0 || roiCrop.height <= 0) + { + m_pdetlog->AddCheckstr(PrintLevel_0, "Error", "tag check crop roi error [%d %d %d %d]", roiCrop.x, roiCrop.y, roiCrop.width, roiCrop.height); + return 5; + } + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Tag", "tag check use bigmask roi [%d %d %d %d]", + roiCrop.x, roiCrop.y, roiCrop.width, roiCrop.height); + } + + // 2、裁剪 -> resize 到 Tag_Loc 模型输入尺寸 -> 灰度 + std::shared_ptr pTag = AI_Factory->Tag_Loc; + cv::Size sz(pTag->input_0.width, pTag->input_0.height); + if (sz.width <= 0 || sz.height <= 0) + { + m_pdetlog->AddCheckstr(PrintLevel_0, "Error", "tag check invalid model input size"); + return 6; + } + cv::Mat detImg; + cv::resize(img(roiCrop), detImg, sz); + if (detImg.channels() == 3) + { + cv::cvtColor(detImg, detImg, cv::COLOR_BGR2GRAY); + } + else if (detImg.channels() == 4) + { + cv::cvtColor(detImg, detImg, cv::COLOR_BGRA2GRAY); + } + + // 3、推理 + cv::Mat mask; + int re = pTag->AIDet(detImg, mask); + if (re != 0) + { + m_pdetlog->AddCheckstr(PrintLevel_0, "Error", "Tag_Loc AIDet error %d", re); + return 100 + re; + } + if (mask.empty()) + { + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Tag", "tag mask empty, no tag"); + return 0; + } + + // 调试存图(仅 -s 时保存) + if (DetImgInfo_shareP && DetImgInfo_shareP->bsaveProcessImg) + { + cv::imwrite(DetImgInfo_shareP->strChannel + "_tag_in.png", detImg); + cv::imwrite(DetImgInfo_shareP->strChannel + "_tag_out_mask.png", mask); + } + + // 4、mask 轮廓 -> 还原到裁剪图坐标 -> RotatedRect(与定位流程 tagroi 的坐标系一致) + std::vector> contours; + std::vector hierarchy; + cv::findContours(mask.clone(), contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); + const float scale_x = (float)roiCrop.width / (float)sz.width; + const float scale_y = (float)roiCrop.height / (float)sz.height; + for (size_t i = 0; i < contours.size(); i++) + { + if (contours[i].size() < 3 || cv::contourArea(contours[i]) <= 0) + { + continue; + } + std::vector srcContour; + srcContour.reserve(contours[i].size()); + for (const auto &pt : contours[i]) + { + srcContour.emplace_back(pt.x * scale_x, pt.y * scale_y); + } + m_tag_roiList.emplace_back(cv::minAreaRect(srcContour)); + } + // 面积从大到小排序(与定位流程 tagroi 的顺序一致) + std::sort(m_tag_roiList.begin(), m_tag_roiList.end(), + [](const cv::RotatedRect &a, const cv::RotatedRect &b) + { return a.size.width * a.size.height > b.size.width * b.size.height; }); + + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Tag", "tag region num %ld crop [%d %d %d %d] model [%d %d]", + m_tag_roiList.size(), roiCrop.x, roiCrop.y, roiCrop.width, roiCrop.height, sz.width, sz.height); + return 0; +} + // 在某个把手区域附近找宽高匹配的凸起 // 匹配条件:1、凸起中心落在把手区域附近(区域宽/高各外扩 HANDLE_NEAR_EXPAND_RATIO) // 2、凸起宽高与该区域外接矩形宽高的比值都在 [HANDLE_SIZE_MIN_RATIO, HANDLE_SIZE_MAX_RATIO](允许横竖互换) diff --git a/example/deal.cpp b/example/deal.cpp index 9b35f72..2f4b804 100644 --- a/example/deal.cpp +++ b/example/deal.cpp @@ -1039,7 +1039,8 @@ int deal::SendImgToCheck(std::shared_ptr pDetImageInfo, IN_IMG_S tem->strImgProductID = pDetImageInfo->strProductID; tem->imgstr = m_strCurDate; tem->strCameraName = pDetImageInfo->strCamID; - tem->camera_ID = 0; + // 按相机区分相机ID(明场=0/暗场=1):两个相机通道名相同时,避免结果文件同名互相覆盖 + tem->camera_ID = (pDetImageInfo->strCamID.find("dark") != std::string::npos) ? 1 : 0; if (m_nSaveDetprocessImg == 1) { tem->otherValue = 9;