diff --git a/AlgorithmModule/include/CheckUtil.hpp b/AlgorithmModule/include/CheckUtil.hpp index 627edb8..aa1fe60 100644 --- a/AlgorithmModule/include/CheckUtil.hpp +++ b/AlgorithmModule/include/CheckUtil.hpp @@ -26,18 +26,22 @@ #include using namespace std; -// 掩膜主体(近似矩形)四周凸起(如把手)的描述 +// 掩膜主体(圆角矩形)外的凸起区域(如把手)的描述 struct Mask_Protrusion_ { - int nSide; // 所在边:0 左 1 右 2 上 3 下 - cv::Rect roi; // 凸起外接矩形(输入图坐标系,已裁剪到图像范围内) - float fLen; // 沿该边的长度(像素) - float fDepth; // 伸出主体的深度(像素) - float fArea; // 凸起面积(roi 内掩膜像素数) - float fFillRatio; // roi 内掩膜像素占比(用于判定是否"类似矩形块") + int nCorner; // 就近角(产品主轴坐标系):0 左上 1 右上 2 左下 3 右下 + cv::Rect roi; // 凸起外接矩形(输入图坐标系,已裁剪到图像范围内) + cv::Point2f ptCenter; // 凸起质心(输入图坐标系) + float fAngle; // 边缘倾角(度,日志排查用) + float fLen; // 凸起外接矩形长边(像素) + float fDepth; // 凸起相对主体直边的最大伸出深度(像素) + float fArea; // 凸起面积(掩膜像素数) + float fFillRatio; // 凸起外接矩形内掩膜像素占比 Mask_Protrusion_() { - nSide = 0; + nCorner = 0; + ptCenter = cv::Point2f(0.0f, 0.0f); + fAngle = 0.0f; fLen = 0.0f; fDepth = 0.0f; fArea = 0.0f; @@ -78,23 +82,28 @@ public: // 找图片的最大外轮廓 static std::vector getLargestContourROI(const cv::Mat &binaryImg, bool &found); - // 抠出掩膜四周的凸起:主体近似矩形,凸起在该边表现为边界值的一次突变(进入)与一次反向突变(离开) - // binMask:掩膜(非零为前景,兼容 0/1 与 0/255);protrusions:输出的凸起列表 - // 参数调整影响(nJumpThresh/nMinRun/nMinDepth 为输入图像素,内部按降采样比例 nScale(1/2/4) 缩小后使用;nMaxRun 不换算,直接用工作图像素): - // nJumpThresh:边界突变阈值,相邻行/列边界值一次变化超过它才算"进入/离开"台阶; - // 调大:只认陡峭台阶,圆角/噪声/锯齿被过滤,过大时边缘圆滑的真把手会漏检; - // 调小:敏感,噪声/斜边易触发假配对(可能形成跨度很大的假凸起)。 - // nMinRun:凸起最小长度("离开"突变搜索从 s+nMinRun 开始;profile 比它短的边直接跳过); - // 调大:短凸起被排除,若大于真把手长度,真"离开"突变被跳过、会配到更远突变 → 出更大的框; - // 调小:小抖动/碎框变多。 - // nMinDepth:凸起最小深度,区间内 ≥80% 采样点相对首尾连线基线外偏 ≥ 它(且最大外偏 ≥ 它)才保留(滤斜边/圆角); - // 调大:只保留明显凸出的,浅凸起漏检,可挡"浅而长"的假配对; - // 调小:浅波纹/边缘抖动也过检,假凸起变多。 - // nMaxRun:凸起最大长度(框沿边长度的硬上限);<=0 时取 max(100, 掩膜短边/3)(工作图像素); - // 调大:允许远距离突变配对 → 会出"覆盖整边"的大框;调小:框被截短,小于真把手长度时真把手漏检。 + // 从外轮廓掩膜(outer定位mask)中分离凸起区域(如把手): + // 主体是圆角矩形、把手基本出现在四角附近:逐行/逐列扫描掩膜边界,为每条边缘搜索带内点数最多的直线; + // 掩膜中超出四条边缘(超过容差带)的像素按连通域切分成凸起区域(含就近角/质心/面积/深度/填充率)。 + // binMask:掩膜(非零为前景,兼容 0/1 与 0/255);protrusions:输出的凸起列表(按面积从大到小) + // 可调参数(应对实际情况): + // nMinDepth:凸起最小深度(输入图像素;<=0 时按掩膜短边/150 自动取值,下限 6) + // 调大:只报明显外凸的把手,浅把手会漏检,可压住边缘毛刺/噪声; + // 调小:浅凸起与噪声都易报出(误报变多)。mask 由模型输出放大而来时直边带量化台阶 + // (台阶高≈量化步长),建议取 2 个量化步长左右(调用方见 CheckHandleByBigMask)。 + // nMinArea:凸起最小面积(输入图像素²;<=0 时按 nMinDepth² 自动取值) + // 调大:小碎块被过滤,把手很小或被遮挡一部分时可能漏检; + // 调小:碎块残留为候选的几率变大(仍会被 nMinDepth 再过滤一遍)。 + // nEdgeMargin:边缘外扩量(输入图像素;<=0 时自动取“最小深度的一半”) + // 把四条边缘线整体向外推该距离后再判凸起,等效于把凸起区域相应减小: + // 调大:边缘顺边毛刺/线条偏差被吞掉,凸起区域更小更干净;过大时浅把手漏检; + // 调小:更灵敏,但边缘毛刺容易连进凸起区域。 + // 边缘拟合的细节参数(带宽容差/斜率上限)见 FitEdgeLine 注释。 + // pBodyQuad:可选输出,拟合出的主体四边形(左上/右上/右下/左下,输入图坐标),仅 -s 存图观察用 // 返回:0 成功、1 掩膜为空、2 无有效前景 static int GetMaskProtrusions(const cv::Mat &binMask, std::vector &protrusions, - int nJumpThresh = 40, int nMinRun = 40, int nMinDepth = 40, int nMaxRun = 0); + int nMinDepth = 0, int nMinArea = 0, std::vector *pBodyQuad = NULL, + int nEdgeMargin = 0); static std::string GetRectString(cv::Rect rect); //创建目录 diff --git a/AlgorithmModule/include/ImgCheckAnalysisy.hpp b/AlgorithmModule/include/ImgCheckAnalysisy.hpp index f977569..623a223 100644 --- a/AlgorithmModule/include/ImgCheckAnalysisy.hpp +++ b/AlgorithmModule/include/ImgCheckAnalysisy.hpp @@ -50,19 +50,6 @@ enum AT_THRESHOLD_TYPE_ // 全局静态变量, 记录图像灰度值异常累计数量 static int g_nImgBrightnessErrorCount = 0; -// 把手候选:bigmask 凸起 + 实测特征(面积/填充率,仅供日志排查) -struct Handle_Candidate_ -{ - Mask_Protrusion_ protr; // 凸起几何信息(roi / 沿边长度 / 深度 / 面积 / 填充率) - int nLen; // 沿边长度(像素) - int nDepth; // 垂直深度(像素) - Handle_Candidate_() - { - nLen = 0; - nDepth = 0; - } -}; - // 单个把手区域的判定结果 struct Handle_Region_Result_ { @@ -194,13 +181,19 @@ private: bool TryReuseAlignFromSource(std::string &strSrcCamera, std::string &strSrcChannel, int &nWaitMs); // 发布本图定位结果/失败标记(供同产品其它相机复用) void PublishAlignShare(bool bSucc); - // 用 bigmask 检测把手(bigmask 主体近似矩形,把手是四周凸起的矩形块) + // 用 outer 定位 mask(bigmask) 检测把手:拟合主体上/下/左/右四条边缘, + // 把边缘外的凸起按连通域分离为把手候选(见 CheckUtil::GetMaskProtrusions), + // 再与参数的把手区域匹配;某个区域附近找不到匹配凸起 → 该区域把手缺失 + // -s(存过程图)时输出 {通道}_handle_det.jpg:绿色四边形=拟合主体、红框=凸起(带编号/角/深度)、 + // 蓝框=参数把手区域、绿框=匹配到的把手,便于从图像排查 int CheckHandleByBigMask(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, + // 匹配参数(搜索范围 handle_search_ratio、大小比区间 handle_size_min/handle_size_max) + // 可 JSON(Handle_Detect.form.handle_param)配置,未配置时用 cpp 顶部宏默认值,见 ImgCheckAnalysisy.cpp 参数注释 + int MatchHandleRegion(const std::vector &candList, const std::vector &usedList, const cv::Rect &boxRegion, cv::Rect &roiHandle); // 计算产品尺寸 int CalProductSize(); @@ -381,8 +374,8 @@ private: cv::RotatedRect m_inner_rroi; std::vector m_tag_roiList; - // 把手检测结果(bigmask 四周凸起 + 各把手区域判定结果) - std::vector m_HandleCandList; // 从 bigmask 四周抠出的凸起候选 + 实测特征 + // 把手检测结果(从 outer 定位 mask 分离的凸起区域 + 各把手区域判定结果) + std::vector m_HandleProtrusionList; // 分离出的凸起区域(把手候选:roi/质心/深度/就近角等) std::vector m_HandleRegionResultList; // 各把手区域(最多4个)的判定结果 bool m_bHandleDetSucc; // 本次是否完成把手检测 bool m_bHandleFound; // 所有非空把手区域是否都找到了把手 diff --git a/AlgorithmModule/src/CheckUtil.cpp b/AlgorithmModule/src/CheckUtil.cpp index 25bdee8..34b1bcc 100644 --- a/AlgorithmModule/src/CheckUtil.cpp +++ b/AlgorithmModule/src/CheckUtil.cpp @@ -19,6 +19,8 @@ #include #include #include +#include +#include #include "snowflake.hpp" int _sysmkdir_2(const std::string &dir) @@ -720,8 +722,144 @@ cv::Point2f CheckUtil::transformPoint(const cv::Point2f &point, const cv::Mat &t return cv::Point2f(result_mat.at(0), result_mat.at(1)); } +// 拟合边缘直线:val ≈ fK * idx + fB(点集为 (扫描序号, 边界值)) +// 直边检测:在 |k| <= fMaxSlope 的斜率范围内搜索“带宽 ±fTol 内点数最多”的直线: +// 主体直边段的点最多且共线 → 被选中;把手段(点数更少)、角部过渡/另一侧边缘 +// (斜率超出限制)等都不会获胜。随后在带内点集上做最小二乘精修(两轮收紧容差)。 +// 可调参数(应对实际情况): +// fTol:带宽容差(工作图像素)。调大→主体边缘带变宽,浅把手更容易被并入主体(漏检)、 +// 但更能压住量化台阶毛刺;调小→台阶噪声可能分裂支撑或拟合到非主体结构。 +// 默认由调用方按“量化台阶+2px”传入(见 GetMaskProtrusions 第 5 步)。 +// fMaxSlope:直线搜索斜率上限。调大→允许产品倾斜更大;过大时角部斜过渡段可能 +// 被当成直边;调小→产品倾斜稍大时拟合失败(退回常量线)。 +// 点集为空返回 false;点太少/无有效直线时退回常量线(值取中位数) +static bool FitEdgeLine(const std::vector &pts, double fTol, double fMaxSlope, double &fK, double &fB) +{ + if (pts.empty()) + { + return false; + } + // 常量线兜底:值取中位数 + fK = 0.0; + { + std::vector valList; + valList.reserve(pts.size()); + for (size_t i = 0; i < pts.size(); i++) + { + valList.push_back(pts[i].y); + } + std::nth_element(valList.begin(), valList.begin() + valList.size() / 2, valList.end()); + fB = (double)valList[valList.size() / 2]; + } + if (pts.size() < 16) + { + return true; // 点太少:用常量线兜底 + } + + // 1、粗搜索:斜率网格 × 抽样锚点,取带内点数最多的直线 + int nBestCnt = -1; + double fBestK = 0.0; + double fBestB = fB; + const size_t nStride = std::max(1, pts.size() / 64); + auto fnCount = [&](double fk, double fb) -> int { + int nCnt = 0; + for (size_t i = 0; i < pts.size(); i++) + { + if (std::fabs((double)pts[i].y - (fk * pts[i].x + fb)) <= fTol) + { + nCnt++; + } + } + return nCnt; + }; + const int nCoarseN = (int)(fMaxSlope / 0.03 + 0.5); // 斜率粗网格:±nCoarseN×0.03 + for (int nk = -nCoarseN; nk <= nCoarseN; nk++) + { + double fk = nk * 0.03; + for (size_t i = 0; i < pts.size(); i += nStride) + { + double fb = (double)pts[i].y - fk * (double)pts[i].x; + int nCnt = fnCount(fk, fb); + if (nCnt > nBestCnt) + { + nBestCnt = nCnt; + fBestK = fk; + fBestB = fb; + } + } + } + // 2、细搜索:在粗搜索最优斜率附近收紧网格 + for (int nk = -6; nk <= 6; nk++) + { + double fk = fBestK + nk * 0.005; + for (size_t i = 0; i < pts.size(); i += nStride) + { + double fb = (double)pts[i].y - fk * (double)pts[i].x; + int nCnt = fnCount(fk, fb); + if (nCnt > nBestCnt) + { + nBestCnt = nCnt; + fBestK = fk; + fBestB = fb; + } + } + } + + // 3、带内点最小二乘精修(两轮收紧容差) + fK = fBestK; + fB = fBestB; + double fUseTol = fTol; + for (int nIter = 0; nIter < 2; nIter++) + { + std::vector inl; + inl.reserve(pts.size()); + for (size_t i = 0; i < pts.size(); i++) + { + if (std::fabs((double)pts[i].y - (fK * pts[i].x + fB)) <= fUseTol) + { + inl.push_back(pts[i]); + } + } + if (inl.size() < 8) + { + break; + } + cv::Vec4f line; + cv::fitLine(inl, line, cv::DIST_L2, 0, 1e-3, 1e-3); + if (std::fabs((double)line[0]) > 1e-6) + { + fK = (double)line[1] / (double)line[0]; + fB = (double)line[3] - fK * (double)line[2]; + } + fUseTol = std::max(3.0, fUseTol * 0.5); + } + return true; +} + +// 从外轮廓掩膜(outer定位mask,如bigmask)中分离凸起区域(如把手): +// 产品主体是圆角矩形、把手基本出现在四角附近,把手在掩膜上表现为超出主体四条边缘的外凸块: +// 1、取最大连通域为主体; +// 2、逐行/逐列扫描主体边界,得到 左/右(x=f(y))、上/下(y=f(x)) 四条边缘的边界点集; +// 3、对每条边缘做直线拟合(见 FitEdgeLine:取带内点数最多的直线,允许产品小角度倾斜); +// 4、掩膜中超出四条边缘(超过容差带)的像素按连通域切分成凸起区域, +// 并按质心所在象限给出就近角(0 左上 1 右上 2 左下 3 右下);深度/面积不足的量化毛刺被过滤。 +// 可调参数(应对实际情况): +// nMinDepth:凸起最小深度(输入图像素;<=0 时按掩膜短边/150 自动取值,下限 6) +// 调大:只报明显外凸的把手,浅把手会漏检,可压住边缘毛刺/噪声; +// 调小:浅凸起与噪声都易报出(误报变多)。mask 由模型输出放大而来时直边带量化台阶 +// (台阶高≈量化步长),建议取 2 个量化步长左右(调用方见 CheckHandleByBigMask)。 +// nMinArea:凸起最小面积(输入图像素²;<=0 时按 nMinDepth² 自动取值) +// 调大:小碎块被过滤,把手很小或被遮挡一部分时可能漏检; +// 调小:碎块残留为候选的几率变大(仍会被 nMinDepth 再过滤一遍)。 +// nEdgeMargin:边缘外扩量(输入图像素;<=0 时自动取“最小深度的一半”) +// 把四条边缘线整体向外推该距离后再判凸起,等效于把凸起区域相应减小: +// 调大:边缘顺边毛刺/线条偏差被吞掉,凸起区域更小更干净;过大时浅把手漏检; +// 调小:更灵敏,但边缘毛刺容易连进凸起区域。 +// 边缘拟合的细节参数(带宽容差/斜率上限)见 FitEdgeLine 注释。 +// pBodyQuad:可选输出,拟合出的主体四边形(左上/右上/右下/左下,输入图坐标),仅 -s 存图观察用 +// 返回:0 成功、1 掩膜为空、2 无有效前景 int CheckUtil::GetMaskProtrusions(const cv::Mat &binMask, std::vector &protrusions, - int nJumpThresh, int nMinRun, int nMinDepth, int nMaxRun) + int nMinDepth, int nMinArea, std::vector *pBodyQuad, int nEdgeMargin) { protrusions.clear(); if (binMask.empty()) @@ -742,7 +880,17 @@ int CheckUtil::GetMaskProtrusions(const cv::Mat &binMask, std::vector 2000) { @@ -757,12 +905,11 @@ int CheckUtil::GetMaskProtrusions(const cv::Mat &binMask, std::vector(nMainIdx, cv::CC_STAT_LEFT), stats.at(nMainIdx, cv::CC_STAT_TOP), stats.at(nMainIdx, cv::CC_STAT_WIDTH), stats.at(nMainIdx, cv::CC_STAT_HEIGHT)); boxMain &= cv::Rect(0, 0, mask.cols, mask.rows); @@ -789,201 +934,216 @@ int CheckUtil::GetMaskProtrusions(const cv::Mat &binMask, std::vector ptsL, ptsR, ptsT, ptsB; + ptsL.reserve(H); + ptsR.reserve(H); + ptsT.reserve(W); + ptsB.reserve(W); + for (int y = 0; y < H; y++) { - nMaxRun = std::max(100, std::min(H, W) / 3); + const uchar *p = mainMask.ptr(y); + int nXL = -1; + int nXR = -1; + for (int x = 0; x < W; x++) + { + if (p[x]) { nXL = x; break; } + } + for (int x = W - 1; x >= 0; x--) + { + if (p[x]) { nXR = x; break; } + } + if (nXL >= 0) + { + ptsL.emplace_back((float)(y + boxMain.y), (float)(nXL + boxMain.x)); // (行号, 左边界的x) + } + if (nXR >= 0) + { + ptsR.emplace_back((float)(y + boxMain.y), (float)(nXR + boxMain.x)); // (行号, 右边界的x) + } } - - // 4、逐边抠凸起: - // 主体近似矩形,矩形边上的凸起会让该边的"边界值"先突变到外侧(进入凸起)、再突变回主体(离开凸起); - // 区间内的边界值相对"首尾边界值线性插值"得到的基线持续外偏,即判定为一个凸起。 - for (int nSide = 0; nSide < 4; nSide++) + for (int x = 0; x < W; x++) { - const bool bRowScan = (nSide < 2); // 左右边按行扫描,上下边按列扫描 - const int nLine = bRowScan ? H : W; // 扫描的行数/列数 - const int nDir = (nSide == 0 || nSide == 2) ? 1 : -1; // 凸起内边界值变小(左侧/上侧)取 +1,变大取 -1 - - // 4.1 从每条 行/列 的外侧向内找第一个前景像素,得到该边的边界值 profile - std::vector posList; // 有前景的行号/列号 - std::vector valList; // 对应的边界值 - posList.reserve(nLine); - valList.reserve(nLine); - for (int i = 0; i < nLine; i++) + int nYT = -1; + int nYB = -1; + for (int y = 0; y < H; y++) { - int nV = -1; - if (nSide == 0) // 左:自左向右 - { - const uchar *p = sub.ptr(i); - for (int x = 0; x < W; x++) - { - if (p[x]) - { - nV = x; - break; - } - } - } - else if (nSide == 1) // 右:自右向左 - { - const uchar *p = sub.ptr(i); - for (int x = W - 1; x >= 0; x--) - { - if (p[x]) - { - nV = x; - break; - } - } - } - else if (nSide == 2) // 上:自上向下 - { - for (int y = 0; y < H; y++) - { - if (sub.ptr(y)[i]) - { - nV = y; - break; - } - } - } - else // 下:自下向上 - { - for (int y = H - 1; y >= 0; y--) - { - if (sub.ptr(y)[i]) - { - nV = y; - break; - } - } - } - if (nV >= 0) - { - posList.push_back(i); - valList.push_back(nV); - } + if (mainMask.ptr(y)[x]) { nYT = y; break; } + } + for (int y = H - 1; y >= 0; y--) + { + if (mainMask.ptr(y)[x]) { nYB = y; break; } + } + if (nYT >= 0) + { + ptsT.emplace_back((float)(x + boxMain.x), (float)(nYT + boxMain.y)); // (列号, 上边界的y) + } + if (nYB >= 0) + { + ptsB.emplace_back((float)(x + boxMain.x), (float)(nYB + boxMain.y)); // (列号, 下边界的y) } + } - const int n = (int)valList.size(); - if (n < nMinRun + 2) + // 5、拟合四条边缘直线:左/右 x=f(y)、上/下 y=f(x) + // 直边判定:|k|<=0.6(近轴对齐)且带内(±fTol)点数最多的直线;fTol 覆盖量化台阶+网格误差 + const int nFitStep = std::max(1, nMinDepthWork / 2); // 量化台阶估计(工作图) + // nExp:边缘外扩量(工作图像素)。显式给定(nEdgeMargin>0)时把边缘线整体向外推该距离, + // 等效于减小凸起区域;未给定时按最小深度的一半自动取值 + const int nExp = (nEdgeMargin > 0) ? std::max(1, nEdgeMargin / nScale) : nFitStep; + const double fTol = std::max(6.0, (double)(nFitStep + 2)); + double fKL = 0, fBL = 0, fKR = 0, fBR = 0, fKT = 0, fBT = 0, fKB = 0, fBB = 0; + if (!FitEdgeLine(ptsL, fTol, 0.6, fKL, fBL) || !FitEdgeLine(ptsR, fTol, 0.6, fKR, fBR) || + !FitEdgeLine(ptsT, fTol, 0.6, fKT, fBT) || !FitEdgeLine(ptsB, fTol, 0.6, fKB, fBB)) + { + return 2; + } + // 主体中心(工作图坐标)与边缘合理性检查 + const float fXc = boxMain.x + W * 0.5f; + const float fYc = boxMain.y + H * 0.5f; + if ((float)(fKR * fYc + fBR) - (float)(fKL * fYc + fBL) < 4.0f || + (float)(fKB * fXc + fBB) - (float)(fKT * fXc + fBT) < 4.0f) + { + return 2; + } + // 可选输出:主体四边形(四角=相邻两条边缘直线的交点),供 -s 存图观察拟合效果; + // 左/右边缘: x = k*y + b;上/下边缘: y = k*x + b,联立求交(行列式≈0 说明两线平行,放弃) + if (pBodyQuad != NULL) + { + pBodyQuad->clear(); + auto fnCross = [](double fkX, double fbX, double fkY, double fbY, cv::Point2f &pt) -> bool { + double fDet = 1.0 - fkX * fkY; + if (std::fabs(fDet) < 1e-6) + { + return false; + } + pt.x = (float)((fbX + fkX * fbY) / fDet); + pt.y = (float)(fkY * pt.x + fbY); + return true; + }; + cv::Point2f ptTL, ptTR, ptBR, ptBL; + if (fnCross(fKL, fBL, fKT, fBT, ptTL) && fnCross(fKR, fBR, fKT, fBT, ptTR) && + fnCross(fKR, fBR, fKB, fBB, ptBR) && fnCross(fKL, fBL, fKB, fBB, ptBL)) { - continue; + // 工作图坐标 -> 输入图坐标 + pBodyQuad->push_back(cv::Point2f(ptTL.x * nScale, ptTL.y * nScale)); // 左上 + pBodyQuad->push_back(cv::Point2f(ptTR.x * nScale, ptTR.y * nScale)); // 右上 + pBodyQuad->push_back(cv::Point2f(ptBR.x * nScale, ptBR.y * nScale)); // 右下 + pBodyQuad->push_back(cv::Point2f(ptBL.x * nScale, ptBL.y * nScale)); // 左下 } + } - // 4.2 配对"进入/离开"突变,扣除该边上的凸起 - int nUsedTo = -1; // 已统计过的凸起末端,避免同一个凸起重复输出 - for (int s = 0; s + 1 < n; s++) + // 6、凸起 = 超出四条边缘(超过容差带 nExp)的掩膜像素;容差带吸收边缘量化毛刺, + // 并可由 nEdgeMargin 显式加大(边缘线整体外推)来压制顺边毛刺/减小凸起区域 + cv::Mat protrMask = cv::Mat::zeros(mask.size(), CV_8UC1); + for (int y = boxMain.y; y < boxMain.y + H; y++) + { + const uchar *pSrc = mainMask.ptr(y - boxMain.y); + uchar *pDst = protrMask.ptr(y); + const float fLineL = (float)(fKL * y + fBL) - nExp; + const float fLineR = (float)(fKR * y + fBR) + nExp; + for (int x = boxMain.x; x < boxMain.x + W; x++) { - if (s <= nUsedTo) + if (!pSrc[x - boxMain.x]) { continue; } - // 进入凸起:边界值朝外侧突变 - if ((valList[s + 1] - valList[s]) * nDir >= -nJumpThresh) + if (x < fLineL || x > fLineR) { + pDst[x] = 255; continue; } - // 离开凸起:在其后 nMaxRun 范围内找第一个反向突变 - int e = -1; - for (int j = s + nMinRun; j + 1 < n && j - s <= nMaxRun; j++) + if (y < (float)(fKT * x + fBT) - nExp || y > (float)(fKB * x + fBB) + nExp) { - if ((valList[j + 1] - valList[j]) * nDir > nJumpThresh) - { - e = j; - break; - } - } - if (e < 0) - { - continue; + pDst[x] = 255; } + } + } - // 4.3 区间内的边界值应持续超出"首尾边界值线性插值"得到的基线 - const int kk = e - s; - int nDevOk = 0; - float fMaxDev = 0.0f; - int nLow = valList[s + 1]; - int nHigh = valList[s + 1]; - for (int i = 1; i <= kk; i++) - { - float fBase = valList[s] + (valList[e + 1] - valList[s]) * (float)i / (float)(kk + 1); - float fDev = (fBase - valList[s + i]) * nDir; - fMaxDev = std::max(fMaxDev, fDev); - if (fDev >= nMinDepth) - { - nDevOk++; - } - nLow = std::min(nLow, valList[s + i]); - nHigh = std::max(nHigh, valList[s + i]); - } - if (fMaxDev < nMinDepth) - { - continue; - } - // 凸起内大部分采样点都要明显外偏,避免把主体的斜边/圆角当成凸起 - if (nDevOk < (int)(kk * 0.8f)) - { - continue; - } + // 7、按连通域切分成凸起区域:深度(相对四条边缘)/面积不足的毛刺过滤 + cv::Mat labelsP, statsP, centP; + int nP = cv::connectedComponentsWithStats(protrMask, labelsP, statsP, centP, 8, CV_32S); + if (nP <= 1) + { + return 0; // 无凸起 + } - // 4.4 计算凸起外接矩形(先算在 sub 图上的位置) - const int nLen = posList[e] - posList[s + 1] + 1; // 沿该边的长度 - cv::Rect roiSub; - if (nSide == 0 || nSide == 2) // 左/上:外侧取小值,主体边缘取大值 - { - int nOut = nLow; - int nInner = std::max(valList[s], valList[e + 1]); - if (nSide == 0) - { - roiSub = cv::Rect(nOut, posList[s + 1], nInner - nOut + 1, nLen); - } - else - { - roiSub = cv::Rect(posList[s + 1], nOut, nLen, nInner - nOut + 1); - } - } - else // 右/下:外侧取大值,主体边缘取小值 + for (int i = 1; i < nP; i++) + { + int nArea = statsP.at(i, cv::CC_STAT_AREA); + if (nArea < nMinAreaWork) + { + continue; + } + int nX = statsP.at(i, cv::CC_STAT_LEFT); + int nY = statsP.at(i, cv::CC_STAT_TOP); + int nW = statsP.at(i, cv::CC_STAT_WIDTH); + int nH = statsP.at(i, cv::CC_STAT_HEIGHT); + + // 深度:组件像素相对四条边缘线的最大外伸(工作图像素) + float fDepth = 0.0f; + for (int yy = nY; yy < nY + nH; yy++) + { + const int *pLab = labelsP.ptr(yy); + const float fLineL = (float)(fKL * yy + fBL); + const float fLineR = (float)(fKR * yy + fBR); + for (int xx = nX; xx < nX + nW; xx++) { - int nOut = nHigh; - int nInner = std::min(valList[s], valList[e + 1]); - if (nSide == 1) - { - roiSub = cv::Rect(nInner, posList[s + 1], nOut - nInner + 1, nLen); - } - else + if (pLab[xx] != i) { - roiSub = cv::Rect(posList[s + 1], nInner, nLen, nOut - nInner + 1); + continue; } + // 到四条边缘的最大外伸(在边缘内侧为负,取 0 忽略) + float fDev = std::max(fLineL - xx, xx - fLineR); + fDev = std::max(fDev, (float)(fKT * xx + fBT) - yy); + fDev = std::max(fDev, yy - (float)(fKB * xx + fBB)); + fDepth = std::max(fDepth, fDev); } - roiSub &= cv::Rect(0, 0, W, H); - if (roiSub.width <= 0 || roiSub.height <= 0) - { - continue; - } - - Mask_Protrusion_ protr; - protr.nSide = nSide; - // 从工作图尺度还原到输入图尺度 - protr.roi = cv::Rect((roiSub.x + boxMain.x) * nScale, (roiSub.y + boxMain.y) * nScale, - roiSub.width * nScale, roiSub.height * nScale); - protr.roi &= cv::Rect(0, 0, binMask.cols, binMask.rows); - if (protr.roi.width <= 0 || protr.roi.height <= 0) - { - continue; - } - protr.fLen = (float)nLen * nScale; - protr.fDepth = fMaxDev * nScale; - int nMaskArea = cv::countNonZero(sub(roiSub)); - protr.fArea = (float)nMaskArea * nScale * nScale; - protr.fFillRatio = (float)nMaskArea / (float)(roiSub.width * roiSub.height); + } + if (fDepth < nMinDepthWork) + { + continue; + } - protrusions.push_back(protr); - nUsedTo = e; // 该凸起区间不再重复统计 + // 特征 + 坐标(工作图 -> 输入图) + Mask_Protrusion_ protr; + protr.roi = cv::Rect(nX * nScale, nY * nScale, nW * nScale, nH * nScale); + protr.roi &= cv::Rect(0, 0, binMask.cols, binMask.rows); + if (protr.roi.width <= 0 || protr.roi.height <= 0) + { + continue; + } + cv::Point2f ptCen; + if (centP.type() == CV_32F) + { + ptCen = centP.at(i); + } + else + { + cv::Vec2d vCen = centP.at(i); + ptCen = cv::Point2f((float)vCen[0], (float)vCen[1]); } + protr.ptCenter = cv::Point2f(ptCen.x * nScale, ptCen.y * nScale); + + // 就近角:按主体中心分象限 -> 0 左上 1 右上 2 左下 3 右下 + bool bRight = ptCen.x > fXc; + bool bBottom = ptCen.y > fYc; + protr.nCorner = bBottom ? (bRight ? 3 : 2) : (bRight ? 1 : 0); + + protr.fAngle = (float)(std::atan((fKL + fKR) * 0.5) * 180.0 / CV_PI); // 左右边缘平均倾角(度,相对垂直方向) + protr.fLen = (float)std::max(nW, nH) * nScale; + protr.fDepth = fDepth * nScale; + protr.fArea = (float)nArea * nScale * nScale; + protr.fFillRatio = (float)nArea / (float)(nW * nH); + protrusions.push_back(protr); } + // 按面积从大到小排序(日志与匹配更稳定) + std::sort(protrusions.begin(), protrusions.end(), + [](const Mask_Protrusion_ &a, const Mask_Protrusion_ &b) { return a.fArea > b.fArea; }); return 0; } \ No newline at end of file diff --git a/AlgorithmModule/src/ImgCheckAnalysisy.cpp b/AlgorithmModule/src/ImgCheckAnalysisy.cpp index 00991f4..a6283dc 100644 --- a/AlgorithmModule/src/ImgCheckAnalysisy.cpp +++ b/AlgorithmModule/src/ImgCheckAnalysisy.cpp @@ -22,12 +22,33 @@ // 待分类模型部署后,将 AI_CLASS_MODEL_ENABLE 置 1,即可恢复 AI_QX_Class_Thread 中的AI分类流程 #define AI_CLASS_MODEL_ENABLE 0 -// 把手判定参数: -// 每个非空的"把手区域"附近都应该有一个把手:凸起宽高与该区域外接矩形宽高的比值落在下面范围内才算匹配 +// 把手提取/判定参数(下面宏是“未配置 JSON 时”的默认值;现场可在参数 JSON 的 Handle_Detect.form.handle_param +// 下按注释标注的键名配置同名参数覆盖它们,按产品/现场情况调整,无需改代码重编译): +// 提取:用 outer 定位 mask(bigmask) 拟合主体(圆角矩形)的上/下/左/右四条边缘,把边缘外的凸起按连通域分离为把手候选 +// (见 CheckUtil::GetMaskProtrusions;产品主体是圆角矩形、把手基本出现在四角附近) +// 判定:每个非空的"把手区域"附近都应该有一个把手:凸起宽高与该区域外接矩形宽高的比值落在下面范围内才算匹配 +// 调整影响: +// HANDLE_SIZE_MIN/MAX_RATIO(JSON: handle_size_min / handle_size_max):凸起与参数把手区域外接矩形的宽高比允许区间 +// 放宽(如 0.7~1.4):把手尺寸/摆放差异大时不易漏检匹配,但可能让邻近错误凸起也匹配上; +// 收紧(如 0.9~1.1):匹配更严格,把手实际尺寸与参数画得差异大时会误报"把手缺失" #define HANDLE_SIZE_MIN_RATIO 0.8f #define HANDLE_SIZE_MAX_RATIO 1.2f -// "附近"的搜索范围:把手区域外接矩形在宽/高方向各外扩该比例 +// "附近"的搜索范围(JSON: handle_search_ratio):把手区域外接矩形在宽/高方向各外扩该比例 +// 调大:位置约束变松,产品摆放偏移大也不易漏(但可能匹配到别的凸起); +// 调小:位置约束更严,偏移稍大就会判"附近没有把手" #define HANDLE_NEAR_EXPAND_RATIO 5 +// 把手提取:最小深度系数(× 定位模型量化步长)(JSON: handle_min_depth) +// 凸起区域算大了/过于灵敏(边缘顺边毛刺与把手连成一块、高度多算) → 调大(如 3~4); +// 浅把手漏检 → 调小(如 1.5)。它同时决定:①凸起深度门槛;②边缘容差带(容差带=最小深度/2) +#define HANDLE_MIN_DEPTH_STEP_COEF 2 +// 把手提取:最小面积系数(× 最小深度²)(JSON: handle_min_area)。碎块多 → 调大;小把手漏检 → 调小 +#define HANDLE_MIN_AREA_COEF 2 +// 把手提取:边缘外扩量系数(× 量化步长)(JSON: handle_edge_margin,0=按"最小深度一半"自动): +// 把主体边缘线整体向外推,变相减小凸起区域。 +// 凸起区域算大了(边缘顺边毛刺与把手连成一块、高度多算) → 调大(如 2~3); +// 浅把手漏检/想更灵敏 → 调小。 +// 注意:凸起区域会同步变小,匹配用的宽高比(HANDLE_SIZE_*_RATIO)会偏小,不宜调得过大 +#define HANDLE_EDGE_MARGIN_STEP_COEF 3 // 把手缺失检测开关见 Base层 Function_Support_Det::bDetHandle(CheckConfigDefine.h) // 分类映射配置文件(每行:label索引:缺陷名称;冒号后名称为该缺陷类型名称,行的顺序即缺陷类型索引) @@ -2778,7 +2799,7 @@ int ImgCheckAnalysisy::AI_Edge(const cv::Mat &img, cv::RotatedRect &outerRoi, cv // 无条件赋值:Tag_Loc 未检出或 bDetTag 关闭(tagroi 为空)时清空,避免残留上一张图的标签 tagroiList = m_pEdge_Align_Result->tagroi; - /*使用m_pEdge_Align_Result->bigmask检测把手是否缺失*/ + /* 用 outer 定位 mask(bigmask) 分离凸起区域,检测把手是否缺失 */ { long handle_s = CheckUtil::getcurTime(); CheckHandleByBigMask(m_pEdge_Align_Result->bigmask); @@ -2788,18 +2809,20 @@ int ImgCheckAnalysisy::AI_Edge(const cv::Mat &img, cv::RotatedRect &outerRoi, cv return re; } -// 用 bigmask 检测把手: -// 1、bigmask 主体近似矩形,把手是主体四周凸起的"类矩形块" -// 2、把主体四周的凸起抠出来(凸起在该边的边界值上表现为一次突变进入 + 一次反向突变离开) -// 3、参数里每个非空的"把手区域"附近都应该有一个把手:在区域附近找凸起, -// 凸起宽高与区域外接矩形宽高相近(HANDLE_SIZE_MIN_RATIO ~ HANDLE_SIZE_MAX_RATIO)才算匹配 -// 4、凸起本身不算 NG,只有"某个把手区域附近找不到匹配的凸起"才算把手缺失(NG 由 CheckRun 上报) +// 用 outer 定位 mask(bigmask) 检测把手(产品主体是圆角矩形、把手基本出现在四角附近): +// 1、分离凸起区域:拟合主体上/下/左/右四条边缘,把边缘外的掩膜按连通域切分成凸起区域 +// (提取细节见 CheckUtil::GetMaskProtrusions,含就近角/质心/面积/深度/填充率) +// 2、参数里每个非空的"把手区域"附近都应该有一个把手:在区域附近找凸起, +// 凸起宽高与区域外接矩形宽高相近(尺寸比区间见 handle_size_min/handle_size_max,可 JSON 配置)才算匹配 +// 3、凸起本身不算 NG,只有"某个把手区域附近找不到匹配的凸起"才算把手缺失(NG 由 CheckRun 上报) +// 4、-s(存过程图)时输出 {通道}_handle_det.jpg:绿色四边形=拟合主体、红框=凸起(带编号/角/深度)、 +// 蓝框=参数把手区域、绿框=匹配到的把手,便于从图像观察问题 int ImgCheckAnalysisy::CheckHandleByBigMask(const cv::Mat &bigmask) { m_bHandleDetSucc = false; m_bHandleFound = false; m_bHandleExpect = false; - m_HandleCandList.clear(); + m_HandleProtrusionList.clear(); m_HandleRegionResultList.clear(); if (bigmask.empty()) @@ -2808,9 +2831,40 @@ int ImgCheckAnalysisy::CheckHandleByBigMask(const cv::Mat &bigmask) return 1; } - // 1、抠出 bigmask 四周凸起的矩形块 - std::vector protrusionList; - int re = CheckUtil::GetMaskProtrusions(bigmask, protrusionList); + // 1、从 bigmask 分离凸起区域 + // 最小深度 = 系数 × 量化步长:mask 由模型输出 resize 回原图,直边带量化台阶毛刺(台阶高≈模型 1 像素=量化步长); + // 系数优先取参数配置(JSON handle_param 下的 handle_min_depth/handle_min_area/handle_edge_margin), + // 未配置时用文件顶部宏的默认值; + // 凸起区域算大了/过于灵敏 → 首选调大 handle_edge_margin(把边缘线整体外推、变相减小凸起区域, + // 不影响深度门槛);也可调大 handle_min_depth(深度门槛+容差带一起变大);浅把手漏检 → 反向调小; + // 拿不到模型尺寸时传 0,由 GetMaskProtrusions 按掩膜尺寸自动取值 + float fMinDepthCoef = HANDLE_MIN_DEPTH_STEP_COEF; + float fMinAreaCoef = HANDLE_MIN_AREA_COEF; + float fEdgeMarginCoef = HANDLE_EDGE_MARGIN_STEP_COEF; + if (m_pbaseCheckFunction != NULL) + { + const Function_Support_Det &supportDetCfg = m_pbaseCheckFunction->supportDet; + if (supportDetCfg.handle_min_depth_coef > 0) fMinDepthCoef = supportDetCfg.handle_min_depth_coef; + if (supportDetCfg.handle_min_area_coef > 0) fMinAreaCoef = supportDetCfg.handle_min_area_coef; + if (supportDetCfg.handle_edge_margin_coef >= 0) fEdgeMarginCoef = supportDetCfg.handle_edge_margin_coef; // 0=按“最小深度一半”自动 + } + int nMinDepth = 0; + int nMinArea = 0; + int nEdgeMargin = 0; + if (AI_Factory && AI_Factory->Align_Outer && AI_Factory->Align_Outer->input_0.width > 0 && AI_Factory->Align_Outer->input_0.height > 0) + { + int nStepX = (int)std::ceil(bigmask.cols * 1.0 / AI_Factory->Align_Outer->input_0.width); + int nStepY = (int)std::ceil(bigmask.rows * 1.0 / AI_Factory->Align_Outer->input_0.height); + int nStep = std::max(1, std::max(nStepX, nStepY)); + nMinDepth = (int)(fMinDepthCoef * nStep + 0.5f); + nMinArea = (int)(fMinAreaCoef * nMinDepth * nMinDepth + 0.5f); + if (fEdgeMarginCoef > 0) + { + nEdgeMargin = (int)(fEdgeMarginCoef * nStep + 0.5f); + } + } + std::vector vecBodyQuad; // 拟合出的主体四边形(仅 -s 存图观察用) + int re = CheckUtil::GetMaskProtrusions(bigmask, m_HandleProtrusionList, nMinDepth, nMinArea, &vecBodyQuad, nEdgeMargin); if (re != 0) { m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Handle", "GetMaskProtrusions error %d", re); @@ -2832,33 +2886,27 @@ int ImgCheckAnalysisy::CheckHandleByBigMask(const cv::Mat &bigmask) } m_bHandleExpect = (bDetHandle && !regionBoxList.empty()); m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Handle", "protrusion num %ld handle region num %ld bDetHandle %s expect %s", - protrusionList.size(), regionBoxList.size(), BOOL_TO_STR(bDetHandle), BOOL_TO_STR(m_bHandleExpect)); - - // 3、凸起候选实测特征(面积/填充率,仅用于日志排查) - for (size_t i = 0; i < protrusionList.size(); i++) - { - Handle_Candidate_ cand; - cand.protr = protrusionList[i]; - // 左/右凸起:沿边为 y 方向,深度为 x 方向;上/下凸起相反 - bool bHorz = (cand.protr.nSide == 0 || cand.protr.nSide == 1); - cand.nLen = bHorz ? cand.protr.roi.height : cand.protr.roi.width; - cand.nDepth = bHorz ? cand.protr.roi.width : cand.protr.roi.height; - m_HandleCandList.push_back(cand); + m_HandleProtrusionList.size(), regionBoxList.size(), BOOL_TO_STR(bDetHandle), BOOL_TO_STR(m_bHandleExpect)); + + // 3、凸起区域实测特征(就近角/面积/深度/填充率,用于日志排查) + for (size_t i = 0; i < m_HandleProtrusionList.size(); i++) + { + const Mask_Protrusion_ &protr = m_HandleProtrusionList[i]; m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Handle", - "protrusion %ld side %d roi [%d %d %d %d] len %d depth %d area %0.0f fill %0.2f", - i, cand.protr.nSide, cand.protr.roi.x, cand.protr.roi.y, cand.protr.roi.width, cand.protr.roi.height, - cand.nLen, cand.nDepth, cand.protr.fArea, cand.protr.fFillRatio); + "protrusion %ld corner %d roi [%d %d %d %d] len %.0f depth %.0f area %0.0f fill %0.2f", + i, protr.nCorner, protr.roi.x, protr.roi.y, protr.roi.width, protr.roi.height, + protr.fLen, protr.fDepth, protr.fArea, protr.fFillRatio); } // 4、逐个把手区域:在区域附近找宽高匹配的凸起(同一个凸起只匹配给一个区域) - std::vector usedList(m_HandleCandList.size(), false); + std::vector usedList(m_HandleProtrusionList.size(), false); int nFound = 0; for (size_t ir = 0; ir < regionBoxList.size(); ir++) { Handle_Region_Result_ result; result.nIdx = (int)ir + 1; result.boxRegion = regionBoxList[ir]; - int nMatchIdx = MatchHandleRegion(m_HandleCandList, usedList, result.boxRegion, result.roiHandle); + int nMatchIdx = MatchHandleRegion(m_HandleProtrusionList, usedList, result.boxRegion, result.roiHandle); result.bFound = (nMatchIdx >= 0); if (result.bFound) { @@ -2882,7 +2930,9 @@ int ImgCheckAnalysisy::CheckHandleByBigMask(const cv::Mat &bigmask) m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, "Handle", "handle region num %ld found num %d -> %s", regionBoxList.size(), nFound, m_bHandleFound ? "handle ok" : "handle missing !!"); - // 5、存过程图:凸起红框、把手区域蓝框、匹配上的把手绿框(仅调试时) + // 5、存过程图(-s 时):用于从图像观察把手提取/匹配情况; + // 绿色四边形=拟合出的主体四条边缘;红框=分离出的凸起区域(带编号/就近角/深度); + // 蓝框=参数把手区域;绿框=匹配到的把手 if (DetImgInfo_shareP->bsaveProcessImg) { cv::Mat show; @@ -2894,9 +2944,25 @@ int ImgCheckAnalysisy::CheckHandleByBigMask(const cv::Mat &bigmask) { show = bigmask.clone(); } - for (size_t i = 0; i < m_HandleCandList.size(); i++) + // 主体四边形(拟合出的四条边缘) + if (vecBodyQuad.size() == 4) { - cv::rectangle(show, m_HandleCandList[i].protr.roi, cv::Scalar(0, 0, 255), 8); + std::vector vecQuad; + for (size_t i = 0; i < vecBodyQuad.size(); i++) + { + vecQuad.push_back(cv::Point(cvRound(vecBodyQuad[i].x), cvRound(vecBodyQuad[i].y))); + } + cv::polylines(show, vecQuad, true, cv::Scalar(0, 255, 0), 8); + } + // 凸起区域:红框 + 编号/就近角/深度 + char strTxt[128]; + for (size_t i = 0; i < m_HandleProtrusionList.size(); i++) + { + const Mask_Protrusion_ &protr = m_HandleProtrusionList[i]; + cv::rectangle(show, protr.roi, cv::Scalar(0, 0, 255), 8); + snprintf(strTxt, sizeof(strTxt), "#%d C%d d%.0f", (int)i, protr.nCorner, protr.fDepth); + cv::putText(show, strTxt, cv::Point(protr.roi.x, std::max(30, protr.roi.y - 8)), + cv::FONT_HERSHEY_SIMPLEX, 2.0, cv::Scalar(0, 255, 255), 4); } for (size_t i = 0; i < m_HandleRegionResultList.size(); i++) { @@ -3050,10 +3116,12 @@ int ImgCheckAnalysisy::CheckTagByBigMask(const cv::Mat &img, const cv::Mat &bigm } // 在某个把手区域附近找宽高匹配的凸起 -// 匹配条件:1、凸起中心落在把手区域附近(区域宽/高各外扩 HANDLE_NEAR_EXPAND_RATIO) -// 2、凸起宽高与该区域外接矩形宽高的比值都在 [HANDLE_SIZE_MIN_RATIO, HANDLE_SIZE_MAX_RATIO](允许横竖互换) +// 匹配条件:1、凸起中心落在把手区域附近(区域宽/高各外扩 fNearRatio 倍) +// 2、凸起宽高与该区域外接矩形宽高的比值都在 [fSizeMin, fSizeMax](允许横竖互换) +// 范围参数优先取参数配置(JSON handle_param 下的 handle_search_ratio/handle_size_min/handle_size_max), +// 未配置时用文件顶部宏的默认值 // 返回凸起在 candList 中的下标,-1 表示没找到 -int ImgCheckAnalysisy::MatchHandleRegion(const std::vector &candList, const std::vector &usedList, +int ImgCheckAnalysisy::MatchHandleRegion(const std::vector &candList, const std::vector &usedList, const cv::Rect &boxRegion, cv::Rect &roiHandle) { if (boxRegion.width <= 0 || boxRegion.height <= 0) @@ -3061,9 +3129,21 @@ int ImgCheckAnalysisy::MatchHandleRegion(const std::vector &c return -1; } - // 搜索范围:把手区域外扩(宽/高各外扩区域尺寸的一半),容忍产品摆放偏差 - int nx = std::max(10, (int)(boxRegion.width * HANDLE_NEAR_EXPAND_RATIO)); - int ny = std::max(10, (int)(boxRegion.height * HANDLE_NEAR_EXPAND_RATIO)); + // 匹配范围参数:JSON 配置优先,未配置时用默认值 + float fNearRatio = HANDLE_NEAR_EXPAND_RATIO; + float fSizeMin = HANDLE_SIZE_MIN_RATIO; + float fSizeMax = HANDLE_SIZE_MAX_RATIO; + if (m_pbaseCheckFunction != NULL) + { + const Function_Support_Det &supportDetCfg = m_pbaseCheckFunction->supportDet; + if (supportDetCfg.handle_search_ratio > 0) fNearRatio = supportDetCfg.handle_search_ratio; + if (supportDetCfg.handle_size_min_ratio > 0) fSizeMin = supportDetCfg.handle_size_min_ratio; + if (supportDetCfg.handle_size_max_ratio > 0) fSizeMax = supportDetCfg.handle_size_max_ratio; + } + + // 搜索范围:把手区域外扩(宽/高各外扩搜索倍数倍),容忍产品摆放偏差 + int nx = std::max(10, (int)(boxRegion.width * fNearRatio)); + int ny = std::max(10, (int)(boxRegion.height * fNearRatio)); cv::Rect searchBox(boxRegion.x - nx, boxRegion.y - ny, boxRegion.width + 2 * nx, boxRegion.height + 2 * ny); cv::Point pRegionCenter(boxRegion.x + boxRegion.width / 2, boxRegion.y + boxRegion.height / 2); @@ -3077,8 +3157,8 @@ int ImgCheckAnalysisy::MatchHandleRegion(const std::vector &c { continue; } - const Handle_Candidate_ &cand = candList[i]; - const cv::Rect &roi = cand.protr.roi; + const Mask_Protrusion_ &cand = candList[i]; + const cv::Rect &roi = cand.roi; if (roi.width <= 0 || roi.height <= 0) { continue; @@ -3100,12 +3180,12 @@ int ImgCheckAnalysisy::MatchHandleRegion(const std::vector &c // 宽高:与把手区域外接矩形的宽高相近(允许横竖互换) float fw = (float)roi.width; float fh = (float)roi.height; - bool bSizeOK = (fw >= boxRegion.width * HANDLE_SIZE_MIN_RATIO && fw <= boxRegion.width * HANDLE_SIZE_MAX_RATIO && - fh >= boxRegion.height * HANDLE_SIZE_MIN_RATIO && fh <= boxRegion.height * HANDLE_SIZE_MAX_RATIO); + bool bSizeOK = (fw >= boxRegion.width * fSizeMin && fw <= boxRegion.width * fSizeMax && + fh >= boxRegion.height * fSizeMin && fh <= boxRegion.height * fSizeMax); if (!bSizeOK) { - bSizeOK = (fw >= boxRegion.height * HANDLE_SIZE_MIN_RATIO && fw <= boxRegion.height * HANDLE_SIZE_MAX_RATIO && - fh >= boxRegion.width * HANDLE_SIZE_MIN_RATIO && fh <= boxRegion.width * HANDLE_SIZE_MAX_RATIO); + bSizeOK = (fw >= boxRegion.height * fSizeMin && fw <= boxRegion.height * fSizeMax && + fh >= boxRegion.width * fSizeMin && fh <= boxRegion.width * fSizeMax); } if (!bSizeOK) { @@ -3122,17 +3202,17 @@ int ImgCheckAnalysisy::MatchHandleRegion(const std::vector &c if (nBestIdx < 0 && nNearIdx >= 0) { // 没匹配上时打印区域内最近的凸起,便于排查阈值 - const cv::Rect &roi = candList[nNearIdx].protr.roi; + const cv::Rect &roi = candList[nNearIdx].roi; m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Handle", - "region box [%d %d %d %d] no match, nearest protrusion roi [%d %d %d %d] w/h ratio %0.2f %0.2f", + "region box [%d %d %d %d] no match, nearest protrusion corner %d roi [%d %d %d %d] w/h ratio %0.2f %0.2f", boxRegion.x, boxRegion.y, boxRegion.width, boxRegion.height, - roi.x, roi.y, roi.width, roi.height, + candList[nNearIdx].nCorner, roi.x, roi.y, roi.width, roi.height, (float)roi.width / (float)boxRegion.width, (float)roi.height / (float)boxRegion.height); } if (nBestIdx >= 0) { - roiHandle = candList[nBestIdx].protr.roi; + roiHandle = candList[nBestIdx].roi; } return nBestIdx; } diff --git a/ConfigModule/include/CheckConfigDefine.h b/ConfigModule/include/CheckConfigDefine.h index 06a74e9..a76c116 100644 --- a/ConfigModule/include/CheckConfigDefine.h +++ b/ConfigModule/include/CheckConfigDefine.h @@ -936,6 +936,14 @@ struct Function_Support_Det std::vector> handleRegions; std::vector handleBoxes; float handle_diff; // 把手差异阈值 + // 把手提取/匹配范围参数:JSON(Handle_Detect.form.handle_param)下同名键可选配置,未配置用 ImgCheckAnalysisy.cpp + // 顶部宏的默认值;各参数含义/调整影响见该文件参数注释。现场调这些参数不用改代码重编译 + float handle_size_min_ratio; // handle_size_min:凸起宽高 / 区域宽高 比值下限;<=0=未配置(默认 0.8) + float handle_size_max_ratio; // handle_size_max:比值上限;<=0=未配置(默认 1.2) + float handle_search_ratio; // handle_search_ratio:区域附近搜索范围外扩倍数;<=0=未配置(默认 5) + float handle_min_depth_coef; // handle_min_depth:凸起最小深度系数(×量化步长);<=0=未配置(默认 2) + float handle_min_area_coef; // handle_min_area:凸起最小面积系数(×最小深度²);<=0=未配置(默认 2) + float handle_edge_margin_coef; // handle_edge_margin:边缘线外扩系数(×量化步长);<0=未配置(默认 3),0=按“最小深度一半”自动 // 标签参数(BQ_Detect) bool bDetTag; // 标签检测开关(BQ_Detect.isOpen) float BQ_area; // 标签检测参数:面积 @@ -962,6 +970,13 @@ struct Function_Support_Det handleRegions.clear(); handleBoxes.clear(); handle_diff = 20; + // 范围参数默认“未配置”:0=未配置(用代码默认),edge_margin 用 -1 表示未配置(0=自动) + handle_size_min_ratio = 0; + handle_size_max_ratio = 0; + handle_search_ratio = 0; + handle_min_depth_coef = 0; + handle_min_area_coef = 0; + handle_edge_margin_coef = -1; bDetTag = false; BQ_area = 0; BQ_energy = 0; @@ -982,6 +997,12 @@ struct Function_Support_Det this->handleRegions.assign(tem.handleRegions.begin(), tem.handleRegions.end()); this->handleBoxes.assign(tem.handleBoxes.begin(), tem.handleBoxes.end()); this->handle_diff = tem.handle_diff; + this->handle_size_min_ratio = tem.handle_size_min_ratio; + this->handle_size_max_ratio = tem.handle_size_max_ratio; + this->handle_search_ratio = tem.handle_search_ratio; + this->handle_min_depth_coef = tem.handle_min_depth_coef; + this->handle_min_area_coef = tem.handle_min_area_coef; + this->handle_edge_margin_coef = tem.handle_edge_margin_coef; this->bDetTag = tem.bDetTag; this->BQ_area = tem.BQ_area; this->BQ_energy = tem.BQ_energy; @@ -990,14 +1011,18 @@ struct Function_Support_Det } void print(std::string str) { - printf("%s>>bDetSupport %d bDrawSupport %d bDetHandle %d bDetTag %d x_standard %f y_standard %f x_offset %f y_offset %f r_offset %f edge num %ld handleRegion num %ld handle_diff %f BQ[area %f energy %f len %f hj %f]\n", - str.c_str(), bDetSupport, bDrawSupport, bDetHandle, bDetTag, x_standard, y_standard, x_offset, y_offset, r_offset, edge_select.size(), handleRegions.size(), handle_diff, BQ_area, BQ_energy, BQ_len, BQ_hj); + printf("%s>>bDetSupport %d bDrawSupport %d bDetHandle %d bDetTag %d x_standard %f y_standard %f x_offset %f y_offset %f r_offset %f edge num %ld handleRegion num %ld handle_diff %f handleRange[size %.2f~%.2f search %.1f depth %.1f area %.1f margin %.1f(0=自动,<0=默认)] BQ[area %f energy %f len %f hj %f]\n", + str.c_str(), bDetSupport, bDrawSupport, bDetHandle, bDetTag, x_standard, y_standard, x_offset, y_offset, r_offset, edge_select.size(), handleRegions.size(), handle_diff, + handle_size_min_ratio, handle_size_max_ratio, handle_search_ratio, handle_min_depth_coef, handle_min_area_coef, handle_edge_margin_coef, + BQ_area, BQ_energy, BQ_len, BQ_hj); } std::string GetInfo(std::string str) { - char buffer[512]; - sprintf(buffer, "%s>>bDetSupport %d bDrawSupport %d bDetHandle %d bDetTag %d x_standard %f y_standard %f x_offset %f y_offset %f r_offset %f edge num %ld handleRegion num %ld handle_diff %f BQ[area %f energy %f len %f hj %f]\n", - str.c_str(), bDetSupport, bDrawSupport, bDetHandle, bDetTag, x_standard, y_standard, x_offset, y_offset, r_offset, edge_select.size(), handleRegions.size(), handle_diff, BQ_area, BQ_energy, BQ_len, BQ_hj); + char buffer[1024]; + sprintf(buffer, "%s>>bDetSupport %d bDrawSupport %d bDetHandle %d bDetTag %d x_standard %f y_standard %f x_offset %f y_offset %f r_offset %f edge num %ld handleRegion num %ld handle_diff %f handleRange[size %.2f~%.2f search %.1f depth %.1f area %.1f margin %.1f(0=自动,<0=默认)] BQ[area %f energy %f len %f hj %f]\n", + str.c_str(), bDetSupport, bDrawSupport, bDetHandle, bDetTag, x_standard, y_standard, x_offset, y_offset, r_offset, edge_select.size(), handleRegions.size(), handle_diff, + handle_size_min_ratio, handle_size_max_ratio, handle_search_ratio, handle_min_depth_coef, handle_min_area_coef, handle_edge_margin_coef, + BQ_area, BQ_energy, BQ_len, BQ_hj); std::string str123 = buffer; return str123; } diff --git a/ConfigModule/src/JsonConfig.cpp b/ConfigModule/src/JsonConfig.cpp index 1d3950b..1270b62 100644 --- a/ConfigModule/src/JsonConfig.cpp +++ b/ConfigModule/src/JsonConfig.cpp @@ -973,6 +973,37 @@ int BaseFuntonConfigJson::GetFunction(Json::Value value) { _config.supportDet.handle_diff = handle_p["handle_diff"].asFloat(); } + // 把手提取/匹配范围参数(均可选:未配置时用 ImgCheckAnalysisy.cpp 顶部宏的默认值) + // handle_size_min/max :凸起宽高与把手区域外接矩形宽高的比值允许区间(默认 0.8 ~ 1.2) + // handle_search_ratio :把手区域附近搜索范围外扩倍数(默认 5) + // handle_min_depth :凸起最小深度系数(× 定位模型量化步长)(默认 2) + // handle_min_area :凸起最小面积系数(× 最小深度²)(默认 2) + // handle_edge_margin :主体边缘线外扩系数(× 量化步长),0=按“最小深度一半”自动(默认 3) + // 含义/调整影响详见 ImgCheckAnalysisy.cpp 顶部参数注释 + if (handle_p["handle_size_min"]) + { + _config.supportDet.handle_size_min_ratio = handle_p["handle_size_min"].asFloat(); + } + if (handle_p["handle_size_max"]) + { + _config.supportDet.handle_size_max_ratio = handle_p["handle_size_max"].asFloat(); + } + if (handle_p["handle_search_ratio"]) + { + _config.supportDet.handle_search_ratio = handle_p["handle_search_ratio"].asFloat(); + } + if (handle_p["handle_min_depth"]) + { + _config.supportDet.handle_min_depth_coef = handle_p["handle_min_depth"].asFloat(); + } + if (handle_p["handle_min_area"]) + { + _config.supportDet.handle_min_area_coef = handle_p["handle_min_area"].asFloat(); + } + if (handle_p["handle_edge_margin"]) + { + _config.supportDet.handle_edge_margin_coef = handle_p["handle_edge_margin"].asFloat(); + } bool bNewFormat = handle_p["handle_region1"].isArray(); for (int idx = 0; idx < HANDLE_REGION_MAX_NUM; idx++) {