From 4b7e1677ff8028ef68fbc85be0b38db936b31d09 Mon Sep 17 00:00:00 2001 From: liusiyang Date: Thu, 17 Sep 2026 17:04:57 +0800 Subject: [PATCH] =?UTF-8?q?update=20=E5=88=9D=E6=AD=A5=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=8A=8A=E6=89=8B=E6=A3=80=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AlgorithmModule/include/CheckUtil.hpp | 28 ++ AlgorithmModule/include/ImgCheckAnalysisy.hpp | 33 ++ AlgorithmModule/src/CheckUtil.cpp | 268 ++++++++++++ AlgorithmModule/src/ImgCheckAnalysisy.cpp | 382 ++++++++++++++---- ConfigModule/include/CheckConfigDefine.h | 68 +++- ConfigModule/src/JsonConfig.cpp | 66 +++ 6 files changed, 770 insertions(+), 75 deletions(-) diff --git a/AlgorithmModule/include/CheckUtil.hpp b/AlgorithmModule/include/CheckUtil.hpp index 237b064..5635456 100644 --- a/AlgorithmModule/include/CheckUtil.hpp +++ b/AlgorithmModule/include/CheckUtil.hpp @@ -25,6 +25,26 @@ #include #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 内掩膜像素占比(用于判定是否"类似矩形块") + Mask_Protrusion_() + { + nSide = 0; + fLen = 0.0f; + fDepth = 0.0f; + fArea = 0.0f; + fFillRatio = 0.0f; + } +}; + class CheckUtil { public: @@ -58,6 +78,14 @@ public: // 找图片的最大外轮廓 static std::vector getLargestContourROI(const cv::Mat &binaryImg, bool &found); + // 抠出掩膜四周的凸起:主体近似矩形,凸起在该边表现为边界值的一次突变(进入)与一次反向突变(离开) + // binMask:掩膜(非零为前景,兼容 0/1 与 0/255);protrusions:输出的凸起列表 + // nJumpThresh:边界突变阈值(输入图像素);nMinRun:凸起最小长度(输入图像素);nMinDepth:凸起最小深度(输入图像素) + // nMaxRun:凸起最大长度(输入图像素),<=0 时取 掩膜短边/3 + // 返回: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); + static std::string GetRectString(cv::Rect rect); //创建目录 static int CreateDir(const std::string &dir); diff --git a/AlgorithmModule/include/ImgCheckAnalysisy.hpp b/AlgorithmModule/include/ImgCheckAnalysisy.hpp index 635fbc9..0d528a8 100644 --- a/AlgorithmModule/include/ImgCheckAnalysisy.hpp +++ b/AlgorithmModule/include/ImgCheckAnalysisy.hpp @@ -49,6 +49,24 @@ enum AT_THRESHOLD_TYPE_ // 全局静态变量, 记录图像灰度值异常累计数量 static int g_nImgBrightnessErrorCount = 0; + +// 把手候选:bigmask 凸起 + 实测特征(后续增加面积/灰阶等判定时直接用这些实测值) +struct Handle_Candidate_ +{ + Mask_Protrusion_ protr; // 凸起几何信息(roi / 沿边长度 / 深度 / 面积 / 填充率) + int nLen; // 沿边长度(像素) + int nDepth; // 垂直深度(像素) + float fGrayDiff; // 凸起区域与周边背景的平均灰度差(预留灰阶判定) + bool bHandle; // 是否为把手 + Handle_Candidate_() + { + nLen = 0; + nDepth = 0; + fGrayDiff = 0.0f; + bHandle = false; + } +}; + class ImgCheckAnalysisy : public ImgCheckBase { @@ -117,6 +135,13 @@ private: // 检测 int CheckRun(); int AI_Edge(const cv::Mat &img, cv::RotatedRect &outerRoi, cv::RotatedRect &innerRoi, std::vector &tagroiList); + // 用 bigmask 检测把手(bigmask 主体近似矩形,把手是四周凸起的矩形块) + int CheckHandleByBigMask(const cv::Mat &img, const cv::Mat &bigmask); + // 判断凸起是否为把手:参数里每一项开启的判定条件(尺寸/填充率/面积/灰阶…)都满足才算把手 + bool IsHandleProtrusion(const Handle_Candidate_ &cand, const Handle_Check_Param ¶m, + const cv::Rect &handleBox, const cv::Size &maskSize); + // 计算凸起区域与周边背景的平均灰度差(灰阶判定的预留量测值,img 需为单通道) + float CalProtrusionGrayDiff(const cv::Mat &grayImg, const cv::Rect &roi); // 计算产品尺寸 int CalProductSize(); // 图片预处理 @@ -289,6 +314,14 @@ private: cv::RotatedRect m_outer_rroi; cv::RotatedRect m_inner_rroi; std::vector m_tag_roiList; + + // 把手检测结果(bigmask 四周凸起) + std::vector m_HandleCandList; // bigmask 四周抠出的凸起候选 + 实测特征 + std::vector m_HandleRoiList; // 判定为把手的凸起 roi(检测图坐标) + cv::Rect m_HandleBoxParamImg; // 参数里绘制的把手区域(检测图坐标) + bool m_bHandleDetSucc; // 本次是否完成把手检测 + bool m_bHandleFound; // 是否检测到把手 + bool m_bHandleExpect; // 参数中是否绘制了把手区域(有期望) }; #endif \ No newline at end of file diff --git a/AlgorithmModule/src/CheckUtil.cpp b/AlgorithmModule/src/CheckUtil.cpp index 87e78de..160d97e 100644 --- a/AlgorithmModule/src/CheckUtil.cpp +++ b/AlgorithmModule/src/CheckUtil.cpp @@ -692,4 +692,272 @@ cv::Point2f CheckUtil::transformPoint(const cv::Point2f &point, const cv::Mat &t cv::Mat result_mat = transform_matrix * point_mat; return cv::Point2f(result_mat.at(0), result_mat.at(1)); +} + +int CheckUtil::GetMaskProtrusions(const cv::Mat &binMask, std::vector &protrusions, + int nJumpThresh, int nMinRun, int nMinDepth, int nMaxRun) +{ + protrusions.clear(); + if (binMask.empty()) + { + return 1; + } + + // 1、二值化(兼容 0/1 与 0/255 掩膜) + cv::Mat gray; + if (binMask.channels() != 1) + { + cv::cvtColor(binMask, gray, cv::COLOR_BGR2GRAY); + } + else + { + gray = binMask; + } + cv::Mat maskFull; + cv::threshold(gray, maskFull, 0, 255, cv::THRESH_BINARY); + + // 1.1 大掩膜先降采样再分析:凸起尺寸远大于降采样误差,可显著降低耗时 + int nScale = 1; + while (nScale < 4 && std::max(maskFull.cols, maskFull.rows) / nScale > 2000) + { + nScale *= 2; + } + cv::Mat mask; + if (nScale > 1) + { + cv::resize(maskFull, mask, cv::Size(maskFull.cols / nScale, maskFull.rows / nScale), 0, 0, cv::INTER_NEAREST); + } + else + { + mask = maskFull; + } + // 阈值按降采样比例换算到工作图尺度 + nJumpThresh = std::max(2, nJumpThresh / nScale); + nMinRun = std::max(3, nMinRun / nScale); + nMinDepth = std::max(2, nMinDepth / nScale); + + // 2、只保留最大连通域,避免背景杂物参与边界统计 + cv::Mat labels, stats, centroids; + int nLabels = cv::connectedComponentsWithStats(mask, labels, stats, centroids, 8, CV_32S); + if (nLabels <= 1) + { + return 2; + } + int nMainIdx = 1; + int nMainArea = 0; + for (int i = 1; i < nLabels; i++) + { + int nArea = stats.at(i, cv::CC_STAT_AREA); + if (nArea > nMainArea) + { + nMainArea = nArea; + nMainIdx = i; + } + } + + // 3、裁剪到主体外接矩形,缩小后续扫描范围 + cv::Rect boxMain(stats.at(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); + if (boxMain.width <= 0 || boxMain.height <= 0) + { + return 2; + } + cv::Mat sub = (labels(boxMain) == nMainIdx); // 0/255 + const int H = sub.rows; + const int W = sub.cols; + if (nMaxRun <= 0) + { + nMaxRun = std::max(100, std::min(H, W) / 3); + } + + // 4、逐边抠凸起: + // 主体近似矩形,矩形边上的凸起会让该边的"边界值"先突变到外侧(进入凸起)、再突变回主体(离开凸起); + // 区间内的边界值相对"首尾边界值线性插值"得到的基线持续外偏,即判定为一个凸起。 + for (int nSide = 0; nSide < 4; nSide++) + { + 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 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); + } + } + + const int n = (int)valList.size(); + if (n < nMinRun + 2) + { + continue; + } + + // 4.2 配对"进入/离开"突变,扣除该边上的凸起 + int nUsedTo = -1; // 已统计过的凸起末端,避免同一个凸起重复输出 + for (int s = 0; s + 1 < n; s++) + { + if (s <= nUsedTo) + { + continue; + } + // 进入凸起:边界值朝外侧突变 + if ((valList[s + 1] - valList[s]) * nDir >= -nJumpThresh) + { + continue; + } + // 离开凸起:在其后 nMaxRun 范围内找第一个反向突变 + int e = -1; + for (int j = s + nMinRun; j + 1 < n && j - s <= nMaxRun; j++) + { + if ((valList[j + 1] - valList[j]) * nDir > nJumpThresh) + { + e = j; + break; + } + } + if (e < 0) + { + continue; + } + + // 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; + } + + // 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 // 右/下:外侧取大值,主体边缘取小值 + { + 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 + { + roiSub = cv::Rect(posList[s + 1], nInner, nLen, nOut - nInner + 1); + } + } + 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); + + protrusions.push_back(protr); + nUsedTo = e; // 该凸起区间不再重复统计 + } + } + + return 0; } \ No newline at end of file diff --git a/AlgorithmModule/src/ImgCheckAnalysisy.cpp b/AlgorithmModule/src/ImgCheckAnalysisy.cpp index 57149f7..5b7fc10 100644 --- a/AlgorithmModule/src/ImgCheckAnalysisy.cpp +++ b/AlgorithmModule/src/ImgCheckAnalysisy.cpp @@ -12,6 +12,19 @@ #include #include "AICommonDefine.h" #include + +// 把手判定参数: +// 1、有参数(参数里绘制了把手区域)时,凸起"沿边长度"与手绘区域对应边长度的比值范围 +#define HANDLE_LEN_MIN_RATIO 0.75f +#define HANDLE_LEN_MAX_RATIO 1.5f +// 2、凸起"垂直深度"与手绘区域对应边宽度的比值范围(凸起被图像边界截断时不校深度) +#define HANDLE_DEPTH_MIN_RATIO 0.8f +#define HANDLE_DEPTH_MAX_RATIO 1.5f +// 3、无参数时,按掩膜短边推算沿边长度/垂直深度范围(最小/最大占比) +#define HANDLE_DEFAULT_MIN_RATIO 0.03f +#define HANDLE_DEFAULT_MAX_RATIO 0.40f +// 4、凸起填充率下限、面积/灰阶等判定参数的默认值都在 Handle_Check_Param(CheckConfigDefine.h)里 + // 用于排序轮廓的比较函数 static bool compareContourAreas(const vector &contour1, const vector &contour2) { @@ -598,6 +611,60 @@ int ImgCheckAnalysisy::CheckRun() } } + /* 把手缺失检测:bigmask 的凸起本身不算 NG,只有"参数里画了把手区域但没检出把手"才算 NG */ + { + if (m_bHandleDetSucc && m_bHandleExpect && !m_bHandleFound) + { + QX_ERROR_INFO_ temerror; + temerror.Idx = m_pDetResult->pQx_ErrorList->size(); + + // 上报参数里绘制的把手区域位置:原图坐标 -> 检测图(detImg)坐标 + cv::Rect roi = m_HandleBoxParamImg; + roi.x -= m_outer_roi.x; + roi.y -= m_outer_roi.y; + roi &= cv::Rect(0, 0, m_pImageAllResult->detImg.cols, m_pImageAllResult->detImg.rows); + if (roi.width <= 0 || roi.height <= 0) + { + roi = cv::Rect(0, 0, m_pImageAllResult->detImg.cols, m_pImageAllResult->detImg.rows); + } + temerror.roi = roi; + temerror.area = roi.width * roi.height; + temerror.JudgArea = roi.width * m_fImgage_Scale_X * roi.height * m_fImgage_Scale_Y; + temerror.JudgArea_second = temerror.JudgArea; + float w = roi.width; + float h = roi.height; + temerror.flen = (w > h ? w : h) * m_fImgage_Scale_X; + temerror.fbreadth = (w > h ? h : w) * m_fImgage_Scale_Y; + temerror.nconfig_qx_type = CONFIG_QX_NAME_handle_loss; + temerror.qx_name = CONFIG_QX_NAME_Names[CONFIG_QX_NAME_handle_loss]; + temerror.result = QX_RESULT_TYPE_NG; + temerror.result_name = QX_RESULT_TYPE_Names[QX_RESULT_TYPE_NG]; + + // 计算把手中心所在的检测区域 + cv::Point pCenter; + pCenter.x = roi.x + roi.width * 0.5; + pCenter.y = roi.y + roi.height * 0.5; + int nmaxregionIdx = 0; + for (int iregion = 0; iregion < m_DetRoiList.roiList_Src.size(); iregion++) + { + const std::vector &polygon = m_DetRoiList.roiList_Src[iregion]; + double result = cv::pointPolygonTest(polygon, pCenter, false); + if (result < 0) + { + continue; + } + nmaxregionIdx = iregion; + } + temerror.detRegionidxList.push_back(nmaxregionIdx); + + m_pDetResult->pQx_ErrorList->push_back(temerror); + + m_pdetlog->AddCheckstr(PrintLevel_0, "把手检测", "%s %d roi [%d %d %d %d] handle param box [%d %d %d %d]", + temerror.qx_name.c_str(), temerror.Idx, roi.x, roi.y, roi.width, roi.height, + m_HandleBoxParamImg.x, m_HandleBoxParamImg.y, m_HandleBoxParamImg.width, m_HandleBoxParamImg.height); + } + } + /* Tag检测 */ { for (const auto &tagRoi : m_tag_roiList) @@ -2065,95 +2132,262 @@ int ImgCheckAnalysisy::AI_Edge(const cv::Mat &img, cv::RotatedRect &outerRoi, cv } /*使用m_pEdge_Align_Result->bigmask检测把手是否缺失*/ - // 截取把手大致区域(使用把手模板完整 boundingRect,而非整图高度) - RotatedRect tpl_handle_rroi = m_pFuntion->function.f_supportDet.handleRect; - Rect handle_rect = tpl_handle_rroi.boundingRect() & Rect(0, 0, m_pEdge_Align_Result->bigmask.cols, m_pEdge_Align_Result->bigmask.rows); - if (handle_rect.width <= 0 || handle_rect.height <= 0) { + long handle_s = CheckUtil::getcurTime(); + CheckHandleByBigMask(img, m_pEdge_Align_Result->bigmask); + long handle_e = CheckUtil::getcurTime(); + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Handle", "handle detect use time %ld", handle_e - handle_s); + } + return re; +} + +// 用 bigmask 检测把手: +// 1、bigmask 主体近似矩形,把手是主体四周凸起的"类矩形块" +// 2、把主体四周的凸起抠掉(凸起在该边的边界值上表现为一次突变进入 + 一次反向突变离开),剩下的就是矩形主体 +// 3、按参数里绘制的把手区域尺寸(沿边长度 + 垂直深度,带容差)逐个判定凸起是否为把手 +int ImgCheckAnalysisy::CheckHandleByBigMask(const cv::Mat &img, const cv::Mat &bigmask) +{ + m_bHandleDetSucc = false; + m_bHandleFound = false; + m_bHandleExpect = false; + m_HandleBoxParamImg = cv::Rect(0, 0, 0, 0); + m_HandleCandList.clear(); + m_HandleRoiList.clear(); + + if (bigmask.empty()) + { + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Handle", "bigmask is empty, skip handle detect"); + return 1; + } + + // 1、抠出 bigmask 四周凸起的矩形块 + std::vector protrusionList; + int re = CheckUtil::GetMaskProtrusions(bigmask, protrusionList); + if (re != 0) + { + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Handle", "GetMaskProtrusions error %d", re); return re; } - Mat handle_roi = m_pEdge_Align_Result->bigmask(handle_rect).clone(); - - // 对handle_roi做一下开运算 - Mat element = getStructuringElement(MORPH_RECT, Size(std::max(1, tpl_handle_rroi.boundingRect().width / 3), std::max(1, tpl_handle_rroi.boundingRect().height / 3))); - morphologyEx(handle_roi, handle_roi, MORPH_OPEN, element); + m_bHandleDetSucc = true; - // imwrite("handle_roi.png", handle_roi); + // 2、把手参照尺寸 + 判定参数:都来自参数里绘制的把手区域(保留图像 x/y 方向) + Handle_Check_Param handleParam; + if (m_pFuntion != NULL) + { + const Function_Support_Det &supportDet = m_pFuntion->function.f_supportDet; + handleParam = supportDet.handleParam; + if (supportDet.handleRegion.size() > 0) + { + m_HandleBoxParamImg = cv::boundingRect(supportDet.handleRegion); + } + } + m_bHandleExpect = (handleParam.bOpen && m_HandleBoxParamImg.width > 1 && m_HandleBoxParamImg.height > 1); + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Handle", + "protrusion num %ld handle param box [%d %d %d %d] expect %s", + protrusionList.size(), m_HandleBoxParamImg.x, m_HandleBoxParamImg.y, + m_HandleBoxParamImg.width, m_HandleBoxParamImg.height, BOOL_TO_STR(m_bHandleExpect)); + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Handle", "%s", handleParam.GetInfo("judge").c_str()); - // handle_roi找到最大连通域,和tpl_handle_rroi做面积形状比较,差异过大判为把手缺失,添加到缺陷并NG + // 灰阶判定是预留项:只有开启时才做灰度换算,避免无谓开销 + cv::Mat grayImg; + if (handleParam.bJudgeGray && !img.empty()) { - Function_Support_Det &handleDet = m_pFuntion->function.f_supportDet; - if (handleDet.bOpen && tpl_handle_rroi.size.width > 0 && tpl_handle_rroi.size.height > 0 && !handle_roi.empty()) + if (img.channels() != 1) { - std::vector> handle_contours; - cv::findContours(handle_roi, handle_contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); + cv::cvtColor(img, grayImg, cv::COLOR_BGR2GRAY); + } + else + { + grayImg = img; + } + } - bool bHandleLoss = false; - if (handle_contours.empty()) - { - bHandleLoss = true; // 没有连通域,把手完全缺失 - } - else - { - // 取最大连通域 - std::sort(handle_contours.begin(), handle_contours.end(), compareContourAreas); - cv::RotatedRect cur_handle_rrect = cv::minAreaRect(handle_contours[0]); - double curArea = cv::contourArea(handle_contours[0]); - - // 面积比较 - double tplArea = tpl_handle_rroi.size.width * tpl_handle_rroi.size.height; - double areaRatio = (tplArea > 0.0) ? (curArea / tplArea) : 0.0; - - // 形状比较(长宽比) - float tplW = std::max(tpl_handle_rroi.size.width, tpl_handle_rroi.size.height); - float tplH = std::min(tpl_handle_rroi.size.width, tpl_handle_rroi.size.height); - float curW = std::max(cur_handle_rrect.size.width, cur_handle_rrect.size.height); - float curH = std::min(cur_handle_rrect.size.width, cur_handle_rrect.size.height); - float tplRatio = (tplH > 0.0f) ? (tplW / tplH) : 0.0f; - float curRatio = (curH > 0.0f) ? (curW / curH) : 0.0f; - float ratioDiff = (tplRatio > 0.0f) ? fabs(curRatio - tplRatio) / tplRatio : 0.0f; - - // 面积差异过大 或 形状差异过大 判为把手缺失 - if (areaRatio < 0.6f || ratioDiff > 0.6f) - { - bHandleLoss = true; - } + // 3、逐个凸起:先算实测特征(沿边长度/深度/面积/灰度差),再做判定 + cv::Size maskSize(bigmask.cols, bigmask.rows); + 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; + if (!grayImg.empty()) + { + cand.fGrayDiff = CalProtrusionGrayDiff(grayImg, cand.protr.roi); + } + cand.bHandle = IsHandleProtrusion(cand, handleParam, m_HandleBoxParamImg, maskSize); + if (cand.bHandle) + { + m_HandleRoiList.push_back(cand.protr.roi); + } + m_HandleCandList.push_back(cand); + // grayDiff 打印 -1 表示本次未开启灰阶判定(未计算) + 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 grayDiff %0.1f -> %s", + 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, + grayImg.empty() ? -1.0f : cand.fGrayDiff, cand.bHandle ? "handle" : "not handle"); + } + m_bHandleFound = (!m_HandleRoiList.empty()); - m_pdetlog->AddCheckstr(PrintLevel_0, "把手检测", "areaRatio %f ratioDiff %f", areaRatio, ratioDiff); - } + m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, "Handle", "handle num %ld found %s use handle param %s", + m_HandleRoiList.size(), BOOL_TO_STR(m_bHandleFound), BOOL_TO_STR(m_bHandleExpect)); - if (bHandleLoss) - { - QX_ERROR_INFO_ temerror; - temerror.Idx = m_pDetResult->pQx_ErrorList->size(); + // 参数里绘制了把手区域,但 bigmask 四周没有满足判定条件的凸起 -> 把手缺失(NG 由 CheckRun 上报) + if (m_bHandleExpect && !m_bHandleFound) + { + m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, "Handle", "handle is missing !!"); + m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, "Handle", "handle param box [%d %d %d %d]", + m_HandleBoxParamImg.x, m_HandleBoxParamImg.y, m_HandleBoxParamImg.width, m_HandleBoxParamImg.height); + } - // 把手模板 roi 在原图坐标系,转换为检测图(detImg)坐标系 - cv::Rect roi = tpl_handle_rroi.boundingRect(); - roi.x -= outerRoi.boundingRect().x; - roi.y -= outerRoi.boundingRect().y; - roi &= cv::Rect(0, 0, outerRoi.boundingRect().width, outerRoi.boundingRect().height); - temerror.roi = roi; - temerror.area = roi.width * roi.height; - temerror.JudgArea = roi.width * m_fImgage_Scale_X * roi.height * m_fImgage_Scale_Y; - temerror.JudgArea_second = temerror.JudgArea; - float w = tpl_handle_rroi.size.width; - float h = tpl_handle_rroi.size.height; - temerror.flen = (w > h ? w : h) * m_fImgage_Scale_X; - temerror.fbreadth = (w > h ? h : w) * m_fImgage_Scale_Y; - temerror.nconfig_qx_type = CONFIG_QX_NAME_support_loss; - temerror.qx_name = CONFIG_QX_NAME_Names[temerror.nconfig_qx_type]; - temerror.result = QX_RESULT_TYPE_NG; - temerror.result_name = QX_RESULT_TYPE_Names[QX_RESULT_TYPE_NG]; - temerror.detRegionidxList.push_back(0); + // 4、存过程图:凸起红框、把手绿框(仅调试时) + if (DetImgInfo_shareP->bsaveProcessImg) + { + cv::Mat show; + if (bigmask.channels() == 1) + { + cv::cvtColor(bigmask, show, cv::COLOR_GRAY2BGR); + } + else + { + show = bigmask.clone(); + } + for (size_t i = 0; i < m_HandleCandList.size(); i++) + { + cv::rectangle(show, m_HandleCandList[i].protr.roi, cv::Scalar(0, 0, 255), 8); + } + for (size_t i = 0; i < m_HandleRoiList.size(); i++) + { + cv::rectangle(show, m_HandleRoiList[i], cv::Scalar(0, 255, 0), 12); + } + cv::Mat showSmall; + cv::resize(show, showSmall, cv::Size(show.cols / 4, show.rows / 4)); + std::vector paramJpg = {cv::IMWRITE_JPEG_QUALITY, 90}; + cv::imwrite(DetImgInfo_shareP->strChannel + "_handle_det.jpg", showSmall, paramJpg); + } - m_pDetResult->pQx_ErrorList->push_back(temerror); + return 0; +} - m_pdetlog->AddCheckstr(PrintLevel_0, "把手检测", "handle loss NG roi [%d %d %d %d]", roi.x, roi.y, roi.width, roi.height); - } - } +// 判断凸起是否为把手:参数里每一项"开启"的判定条件都满足才算把手 +// 尺寸参照参数里绘制的把手区域(handleBox,保留图像 x/y 方向): +// 沿边长度 对应 handleBox 在该边方向上的长度,垂直深度 对应 handleBox 的宽度; +// 凸起外边缘被图像边界截断时,深度不可信,只校验沿边长度 +// 参数里没写显式像素范围时,按把手区域尺寸的比例推算(HANDLE_LEN_*_RATIO / HANDLE_DEPTH_*_RATIO) +bool ImgCheckAnalysisy::IsHandleProtrusion(const Handle_Candidate_ &cand, const Handle_Check_Param ¶m, + const cv::Rect &handleBox, const cv::Size &maskSize) +{ + const Mask_Protrusion_ &protr = cand.protr; + if (protr.roi.width <= 0 || protr.roi.height <= 0) + { + return false; } - return re; + // 左/右凸起:沿边为 y 方向(长度=height),深度为 x 方向(width);上/下凸起相反 + const bool bHorz = (protr.nSide == 0 || protr.nSide == 1); + // 凸起外边缘是否贴到图像边界(被截断) + bool bClip = bHorz ? (protr.roi.x <= 0 || protr.roi.x + protr.roi.width >= maskSize.width) + : (protr.roi.y <= 0 || protr.roi.y + protr.roi.height >= maskSize.height); + + // 该凸起对应的期望尺寸范围 + int nMinLen = 0, nMaxLen = 0, nMinDepth = 0, nMaxDepth = 0; + if (handleBox.width > 1 && handleBox.height > 1) + { + int nExpLen = bHorz ? handleBox.height : handleBox.width; + int nExpDepth = bHorz ? handleBox.width : handleBox.height; + nMinLen = (int)(nExpLen * HANDLE_LEN_MIN_RATIO); + nMaxLen = (int)(nExpLen * HANDLE_LEN_MAX_RATIO); + nMinDepth = (int)(nExpDepth * HANDLE_DEPTH_MIN_RATIO); + nMaxDepth = (int)(nExpDepth * HANDLE_DEPTH_MAX_RATIO); + } + else // 参数里没画把手区域:按掩膜短边推算一个大致范围 + { + int nMinSide = std::min(maskSize.width, maskSize.height); + nMinLen = nMinDepth = (int)(nMinSide * HANDLE_DEFAULT_MIN_RATIO); + nMaxLen = nMaxDepth = (int)(nMinSide * HANDLE_DEFAULT_MAX_RATIO); + } + // 参数里写了显式像素范围时以参数值为准 + if (param.bJudgeLen && param.fLenMin > 0) + { + nMinLen = (int)param.fLenMin; + } + if (param.bJudgeLen && param.fLenMax > 0) + { + nMaxLen = (int)param.fLenMax; + } + if (param.bJudgeDepth && param.fDepthMin > 0) + { + nMinDepth = (int)param.fDepthMin; + } + if (param.bJudgeDepth && param.fDepthMax > 0) + { + nMaxDepth = (int)param.fDepthMax; + } + + // 以下条件逐条校验,任意一条不满足即不是把手 + // 1、类似矩形块:凸起区域内掩膜填充率要高 + if (param.bJudgeFill && protr.fFillRatio < param.fFillMin) + { + return false; + } + // 2、沿边长度 + if (param.bJudgeLen && (cand.nLen < nMinLen || cand.nLen > nMaxLen)) + { + return false; + } + // 3、垂直深度(凸起被图像边界截断时跳过) + if (param.bJudgeDepth && !bClip && (cand.nDepth < nMinDepth || cand.nDepth > nMaxDepth)) + { + return false; + } + // 4、预留:凸起面积 + if (param.bJudgeArea && param.fAreaMax > param.fAreaMin && + (protr.fArea < param.fAreaMin || protr.fArea > param.fAreaMax)) + { + return false; + } + // 5、预留:凸起与背景的灰度差 + if (param.bJudgeGray && param.fGrayMax > param.fGrayMin && + (cand.fGrayDiff < param.fGrayMin || cand.fGrayDiff > param.fGrayMax)) + { + return false; + } + return true; +} + +// 计算凸起区域与周边背景的平均灰度差(灰阶判定的预留量测值,grayImg 需为单通道) +float ImgCheckAnalysisy::CalProtrusionGrayDiff(const cv::Mat &grayImg, const cv::Rect &roi) +{ + if (grayImg.empty() || grayImg.channels() != 1 || roi.width <= 0 || roi.height <= 0) + { + return 0.0f; + } + // 凸起区域通常整个都在掩膜内,向外扩一圈才能取到背景 + int nExpand = std::max(20, std::min(roi.width, roi.height) / 2); + cv::Rect roiBg(roi.x - nExpand, roi.y - nExpand, roi.width + 2 * nExpand, roi.height + 2 * nExpand); + roiBg &= cv::Rect(0, 0, grayImg.cols, grayImg.rows); + if (roiBg.width <= 0 || roiBg.height <= 0) + { + return 0.0f; + } + + cv::Mat grayRoi = grayImg(roiBg); + cv::Mat maskIn = cv::Mat::zeros(roiBg.size(), CV_8UC1); + cv::Rect inter = roi & roiBg; + cv::rectangle(maskIn, inter - roiBg.tl(), cv::Scalar(255), cv::FILLED); + cv::Mat maskOut; + cv::bitwise_not(maskIn, maskOut); + if (cv::countNonZero(maskIn) <= 0 || cv::countNonZero(maskOut) <= 0) + { + return 0.0f; + } + + double fMeanIn = cv::mean(grayRoi, maskIn)[0]; + double fMeanOut = cv::mean(grayRoi, maskOut)[0]; + double fDiff = fMeanIn - fMeanOut; + return (float)(fDiff >= 0 ? fDiff : -fDiff); } int ImgCheckAnalysisy::CalProductSize() diff --git a/ConfigModule/include/CheckConfigDefine.h b/ConfigModule/include/CheckConfigDefine.h index 74c476e..9a40dc8 100644 --- a/ConfigModule/include/CheckConfigDefine.h +++ b/ConfigModule/include/CheckConfigDefine.h @@ -54,6 +54,7 @@ enum CONFIG_QX_NAME_ CONFIG_QX_NAME_cell_tag, // 分类 标签 CONFIG_QX_NAME_support_offset, // 支架偏移 CONFIG_QX_NAME_support_loss, // 支架缺失 + CONFIG_QX_NAME_handle_loss, // 把手缺失 CONFIG_QX_NAME_count, }; // 缺陷项对应在参数中的名称 @@ -74,6 +75,7 @@ static std::vector CONFIG_QX_NAME_Names = "tag", "support_offset", "support_loss", + "handle_loss", }; // 分析类型 @@ -932,6 +934,65 @@ struct Function_Image_Align return str123; } }; +// 把手缺失判定参数: +// 当前用"沿边长度 + 垂直深度"判定,后续可继续添加面积、灰阶等参数, +// 每一项都有独立的开关,所有开启的条件都满足才把凸起判为把手 +struct Handle_Check_Param +{ + bool bOpen; // 是否检测把手缺失(false 时不做把手判定,也不报 NG) + bool bJudgeFill; // 是否用"类矩形块"填充率判定 + float fFillMin; // 填充率下限 + bool bJudgeLen; // 是否用"沿边长度"判定 + float fLenMin; // 沿边长度下限(像素,<=0 时按把手区域尺寸比例推算) + float fLenMax; // 沿边长度上限(像素,<=0 时按把手区域尺寸比例推算) + bool bJudgeDepth; // 是否用"垂直深度"判定 + float fDepthMin; // 垂直深度下限(像素,<=0 时按把手区域尺寸比例推算) + float fDepthMax; // 垂直深度上限(像素,<=0 时按把手区域尺寸比例推算) + bool bJudgeArea; // 预留:凸起面积判定 + float fAreaMin; + float fAreaMax; + bool bJudgeGray; // 预留:凸起与背景灰度差判定 + float fGrayMin; + float fGrayMax; + + Handle_Check_Param() + { + Init(); + } + void Init() + { + bOpen = true; + bJudgeFill = true; + fFillMin = 0.75f; + bJudgeLen = true; + fLenMin = 0; + fLenMax = 0; + bJudgeDepth = true; + fDepthMin = 0; + fDepthMax = 0; + bJudgeArea = false; + fAreaMin = 0; + fAreaMax = 0; + bJudgeGray = false; + fGrayMin = 0; + fGrayMax = 0; + } + void copy(Handle_Check_Param tem) + { + // 全为基本类型,直接赋值(避免后续加字段时漏拷) + *this = tem; + } + std::string GetInfo(std::string str) + { + char buffer[256]; + sprintf(buffer, "%s>>bOpen %d fill[%d %0.2f] len[%d %0.0f %0.0f] depth[%d %0.0f %0.0f] area[%d %0.0f %0.0f] gray[%d %0.0f %0.0f]\n", + str.c_str(), bOpen, bJudgeFill, fFillMin, bJudgeLen, fLenMin, fLenMax, + bJudgeDepth, fDepthMin, fDepthMax, bJudgeArea, fAreaMin, fAreaMax, bJudgeGray, fGrayMin, fGrayMax); + std::string str123 = buffer; + return str123; + } +}; + // 支架检测 struct Function_Support_Det { @@ -943,6 +1004,7 @@ struct Function_Support_Det float r_offset; std::vector handleRegion; cv::RotatedRect handleRect; + Handle_Check_Param handleParam; // 把手缺失判定参数 Function_Support_Det () { @@ -959,6 +1021,7 @@ struct Function_Support_Det r_offset = 0; handleRegion.clear(); handleRect = cv::RotatedRect(); + handleParam.Init(); } void copy(Function_Support_Det tem) { @@ -970,18 +1033,21 @@ struct Function_Support_Det this->r_offset = tem.r_offset; this->handleRegion.assign(tem.handleRegion.begin(), tem.handleRegion.end()); this->handleRect = tem.handleRect; + this->handleParam.copy(tem.handleParam); } void print(std::string str) { printf("%s>>bOpen %d x_offset %f y_offset %f r_offset %f\n", str.c_str(), bOpen, x_offset, y_offset, r_offset); + printf("%s", handleParam.GetInfo("handleParam").c_str()); } std::string GetInfo(std::string str) { char buffer[256]; sprintf(buffer, "%s>>bOpen %d x_offset %f y_offset %f r_offset %f\n", str.c_str(), - bOpen, x_offset, y_offset, r_offset); + bOpen, x_offset, y_offset, r_offset); std::string str123 = buffer; + str123 += handleParam.GetInfo("handleParam"); return str123; } diff --git a/ConfigModule/src/JsonConfig.cpp b/ConfigModule/src/JsonConfig.cpp index bcdba98..c1180ba 100644 --- a/ConfigModule/src/JsonConfig.cpp +++ b/ConfigModule/src/JsonConfig.cpp @@ -470,6 +470,72 @@ int ChannelFuntonConfigJson::GetFunction(Json::Value value, CheckFunction &funct } } } + + // 把手缺失判定参数(参数缺失时保留默认值:长度/深度按把手区域尺寸比例推算) + { + auto handle_p = value_f["form"]["handle_param"]; + Handle_Check_Param &handleParam = function.f_supportDet.handleParam; + if (handle_p["handle_disabled"].isBool()) + { + handleParam.bOpen = !handle_p["handle_disabled"].asBool(); + } + if (handle_p["handle_fill_disabled"].isBool()) + { + handleParam.bJudgeFill = !handle_p["handle_fill_disabled"].asBool(); + } + if (handle_p["handle_fill_min"].isNumeric()) + { + handleParam.fFillMin = handle_p["handle_fill_min"].asFloat(); + } + if (handle_p["handle_len_disabled"].isBool()) + { + handleParam.bJudgeLen = !handle_p["handle_len_disabled"].asBool(); + } + if (handle_p["handle_len_min"].isNumeric()) + { + handleParam.fLenMin = handle_p["handle_len_min"].asFloat(); + } + if (handle_p["handle_len_max"].isNumeric()) + { + handleParam.fLenMax = handle_p["handle_len_max"].asFloat(); + } + if (handle_p["handle_depth_disabled"].isBool()) + { + handleParam.bJudgeDepth = !handle_p["handle_depth_disabled"].asBool(); + } + if (handle_p["handle_depth_min"].isNumeric()) + { + handleParam.fDepthMin = handle_p["handle_depth_min"].asFloat(); + } + if (handle_p["handle_depth_max"].isNumeric()) + { + handleParam.fDepthMax = handle_p["handle_depth_max"].asFloat(); + } + if (handle_p["handle_area_disabled"].isBool()) + { + handleParam.bJudgeArea = !handle_p["handle_area_disabled"].asBool(); + } + if (handle_p["handle_area_min"].isNumeric()) + { + handleParam.fAreaMin = handle_p["handle_area_min"].asFloat(); + } + if (handle_p["handle_area_max"].isNumeric()) + { + handleParam.fAreaMax = handle_p["handle_area_max"].asFloat(); + } + if (handle_p["handle_gray_disabled"].isBool()) + { + handleParam.bJudgeGray = !handle_p["handle_gray_disabled"].asBool(); + } + if (handle_p["handle_gray_min"].isNumeric()) + { + handleParam.fGrayMin = handle_p["handle_gray_min"].asFloat(); + } + if (handle_p["handle_gray_max"].isNumeric()) + { + handleParam.fGrayMax = handle_p["handle_gray_max"].asFloat(); + } + } } else {