From 2d1ce72a6f3d593a893eff5b84750674e35f12cf Mon Sep 17 00:00:00 2001 From: liusiyang Date: Mon, 19 Oct 2026 21:20:07 +0800 Subject: [PATCH] =?UTF-8?q?feat=20=E7=B2=BE=E7=BB=86=E5=8C=96=E6=89=BE?= =?UTF-8?q?=E6=94=AF=E6=9E=B6=E8=BE=B9=E7=BC=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AlgorithmModule/src/ImgCheckAnalysisy.cpp | 570 ++++++++++++++++++++-- ConfigModule/include/CheckConfigDefine.h | 19 +- ConfigModule/src/JsonConfig.cpp | 18 + 3 files changed, 556 insertions(+), 51 deletions(-) diff --git a/AlgorithmModule/src/ImgCheckAnalysisy.cpp b/AlgorithmModule/src/ImgCheckAnalysisy.cpp index 115958b..f87c9cc 100644 --- a/AlgorithmModule/src/ImgCheckAnalysisy.cpp +++ b/AlgorithmModule/src/ImgCheckAnalysisy.cpp @@ -59,6 +59,27 @@ // edge_refine_step:沿边采样步长(像素)。不需要每个点都找,调大可省时间;调小边缘拟合更稳 #define EDGE_REFINE_STEP_DEFAULT 40 +// ===== 支架方向精细化默认参数 ===== +// (JSON: Support_Detect.form.support_param 下的 +// support_refine_search / support_refine_step / support_refine_segments,均可选) +// 背景:支架由 Align_Inner 模型(smallmask,1696×800)检出,上采样回原图后边缘量化误差约 4px, +// 且支架区域接近方形时 minAreaRect 的"长轴"方向不稳定,用它求旋转角误差大。 +// 改为用模型支架边缘做初值,在原图(detImg 灰度)上拟合支架长边(朝产品内侧的边)求方向角。 +// support_refine_search:取模型支架外接矩形最外侧多少列作为边缘搜索带(未配置默认 30 列)。 +// 右边缘 → 取外接矩形最右 nStrip 列,从右往左逐像素找灰度梯度最大的点; +// 左边缘 → 取最左 nStrip 列,从左往右找(相反逻辑) +// 调大:覆盖更大的模型误差/边缘更靠内,但可能扫到支架内部结构; +// 调小:更严格,模型边缘偏差大时找不到边缘(回退 minAreaRect 方案) +#define SUPPORT_REFINE_STRIP_DEFAULT 30 +// support_refine_step:沿支架长边采样的步长(像素)。调大可省时间;调小拟合更稳 +#define SUPPORT_REFINE_STEP_DEFAULT 20 +// support_refine_segments:把采样点沿边分成 n 段,每段稳健拟合一条直线,取各段方向角的中位数。 +// 支架边缘有凸起时:凸起处采样点成为所在段的离群点(MAD 剔除);凸起外边缘与支架边缘平行, +// 即使拟合到凸起外边缘也不影响方向角 +#define SUPPORT_REFINE_SEGMENTS_DEFAULT 6 +// 搜索列带内相邻像素(3 行灰度均值)的灰度差小于该值视为无边缘(极性无关,支架比外侧亮/暗均可) +#define SUPPORT_REFINE_MIN_CONTRAST 15.0 + // 分类映射配置文件(每行:label索引:缺陷名称;冒号后名称为该缺陷类型名称,行的顺序即缺陷类型索引) static const std::string CLS_LABEL_CONFIG_FILE = "/home/aidlux/BOE/UseModel_ZB/BM_cls.txt"; @@ -1623,68 +1644,523 @@ int ImgCheckAnalysisy::CheckRun() } } + + // 3.5、旋转偏移:支架最小外接旋转矩形(长轴对应左/右边缘、短轴对应上/下边缘) // 长轴与产品左/右边缘的夹角 > r_offset(度) → 偏移;r_offset<=0 不判定(兼容旧参数) float fSupportAngle = 0; // 支架长轴相对产品左/右边缘的夹角(度,带符号;正=支架长轴相对产品边缘向右倾) bool bRotOffset = false; + bool bSupportAngleValid = false; // 是否用上了原图精细化角度(false=走 minAreaRect 兜底) if (supportDet.r_offset > 0 && rsContour.size() >= 3) { // 轮廓先映射回原图坐标再拟合旋转矩形(smallmask 的 x/y 缩放可能不同,不还原会带角度误差) + // 注意:smallmask 是 Align_Inner 在"大轮廓裁剪图"上的输出,映射原图时要加裁剪原点 bigRect std::vector rsContourSrc; rsContourSrc.reserve(rsContour.size()); for (size_t ip = 0; ip < rsContour.size(); ip++) { - rsContourSrc.emplace_back(rsContour[ip].x * fx, rsContour[ip].y * fy); + rsContourSrc.emplace_back(bigRect.x + rsContour[ip].x * fx, + bigRect.y + rsContour[ip].y * fy); } cv::RotatedRect rsRotRect = cv::minAreaRect(rsContourSrc); - cv::Point2f rpts[4]; - rsRotRect.points(rpts); - cv::Point2f d01 = rpts[1] - rpts[0]; - cv::Point2f d12 = rpts[2] - rpts[1]; - float len01 = (float)cv::norm(d01); - float len12 = (float)cv::norm(d12); - // 长轴方向 = 长边方向(长轴对应左/右边缘),并统一朝下(与左边缘 上->下 方向一致) - cv::Point2f vLong(0, 1); - if (len01 >= len12 && len01 > 1e-3f) - { - vLong = d01 * (1.0f / len01); - } - else if (len12 > 1e-3f) - { - vLong = d12 * (1.0f / len12); - } - if (vLong.y < 0) - { - vLong = -vLong; - } - // 参考方向 = 产品左边缘(ptSegAArr[0]->ptSegBArr[0],拟合边/回退矩形边);退化时用垂直方向 - cv::Point2f vRef = ptSegBArr[0] - ptSegAArr[0]; - float lenRef = (float)cv::norm(vRef); - if (lenRef > 1e-3f) - { - vRef *= (1.0f / lenRef); - } - else - { - vRef = cv::Point2f(0, 1); - } - if (vRef.y < 0) - { - vRef = -vRef; - } - // 有符号夹角(度);长轴是“线”,角度归一化到 [-90, 90] - double dCross = (double)vLong.x * vRef.y - (double)vLong.y * vRef.x; - double dDot = (double)vLong.x * vRef.x + (double)vLong.y * vRef.y; - fSupportAngle = (float)(std::atan2(dCross, dDot) * 180.0 / CV_PI); - if (fSupportAngle > 90.0f) + + // ===== 精细化计算支架方向(原图灰度上拟合支架长边)===== + // 模型 smallmask 分辨率低(1696×800 上采样回原图,量化误差约 4px),且支架区域接近方形时 + // minAreaRect 的"长轴"方向不稳定,直接用它求旋转角误差大;这里用模型支架边缘做初值, + // 在 detImg 灰度原图上重新拟合支架长边方向: + // 1、把 smallmask 最大轮廓映射回原图,得到支架外接矩形(rsRect); + // 取外接矩形最外侧 nStrip 列(默认 30 列)作为该侧边缘的搜索带 + // 2、结合 edge_select:选 left → 拟合支架右边缘;选 right → 拟合支架左边缘 + // (up/down 对应支架上下短边,水平的线不好拟合,不处理,仍用 minAreaRect 结果) + // 3、沿外接矩形高度按 step 采样;每行在搜索带内逐像素找灰度梯度最大的位置作为边缘点: + // 右边缘取最右 nStrip 列、从右往左找;左边缘取最左 nStrip 列、从左往右找; + // 梯度不足(该侧没有真实边缘)的行不参与拟合 + // 4、采样点沿 y 分成 n 段,每段用 FitLineRobustTV 稳健拟合直线,取各段方向角的中位数: + // 支架边缘的凸起是矩形,凸起外边缘与支架长边平行,拟合到凸起处也不影响方向角; + // 凸起侧边/内部纹理等离群点由 MAD 剔除 + // 失败(有效采样/分段不足、角度离散过大)时保留下面的 minAreaRect 长轴方案 + if (!m_pImageAllResult->detImg.empty() && m_pImageAllResult->detImg.type() == CV_8UC1) { - fSupportAngle -= 180.0f; + const cv::Mat &grayRef = m_pImageAllResult->detImg; + // 边缘搜索带宽度(列):取模型支架外接矩形最外侧多少列;未配置默认 30 + int nStrip = (supportDet.support_refine_search > 0) + ? cvRound(supportDet.support_refine_search) + : SUPPORT_REFINE_STRIP_DEFAULT; + int nStep = (supportDet.support_refine_step > 0) + ? cvRound(supportDet.support_refine_step) + : SUPPORT_REFINE_STEP_DEFAULT; + int nSegments = (supportDet.support_refine_segments > 0) + ? cvRound(supportDet.support_refine_segments) + : SUPPORT_REFINE_SEGMENTS_DEFAULT; + nStrip = std::max(4, nStrip); + nStep = std::max(4, nStep); + nSegments = std::max(1, nSegments); + + // 3 行灰度均值(抑制单行噪声;调用处保证 y 在 [1, rows-2]) + auto gray3 = [&grayRef](int y, int x) -> float { + return (grayRef.at(y - 1, x) + grayRef.at(y, x) + grayRef.at(y + 1, x)) * (1.0f / 3.0f); + }; + + // 拟合支架一条长边:nSide 0=支架左边缘(外侧 -x)、1=支架右边缘(外侧 +x); + // nRefIdx 0=产品左边缘、1=产品右边缘 + // 返回 true 时 fAngleOut=支架长边相对产品该边缘的带符号夹角(度),fEdgeVertOut=支架长边相对竖直方向角度(度) + // pSamplesOut/pSegLinesOut 非空时输出采样边缘点和各段拟合线(检测图坐标),供 -s 绘制调试图 + auto FitSupportEdgeAngle = [&](int nSide, int nRefIdx, float &fAngleOut, int &nSampleOut, int &nSegOut, float &fEdgeVertOut, + std::vector *pSamplesOut, std::vector> *pSegLinesOut) -> bool { + fAngleOut = 0.0f; + fEdgeVertOut = 0.0f; + nSampleOut = 0; + nSegOut = 0; + if (pSamplesOut != NULL) + { + pSamplesOut->clear(); + } + if (pSegLinesOut != NULL) + { + pSegLinesOut->clear(); + } + + // 产品基准边缘方向(统一朝下,与原有旋转角符号约定一致) + cv::Point2f vRef = ptSegBArr[nRefIdx] - ptSegAArr[nRefIdx]; + float fRefLen = (float)cv::norm(vRef); + if (fRefLen < 1e-3f) + { + return false; + } + vRef *= (1.0f / fRefLen); + if (vRef.y < 0) + { + vRef = -vRef; + } + + // ===== 取模型支架外接矩形(rsRect)最外侧 nStrip 列,逐行找边缘点 ===== + // 右边缘:取外接矩形最右 nStrip 列,从右往左扫;左边缘:取最左 nStrip 列,从左往右扫。 + // 每行按灰度梯度找边缘点;梯度不足(该侧没有真实边缘)的行不参与拟合 + int nXStart = 0; // 外侧起始列(原图坐标) + int nXEnd = 0; // 内侧结束列(原图坐标) + if (nSide == 0) + { + nXStart = rsRect.x; + nXEnd = rsRect.x + nStrip - 1; + } + else + { + nXStart = rsRect.x + rsRect.width - 1; + nXEnd = rsRect.x + rsRect.width - nStrip; + } + const int nStepX = (nXEnd >= nXStart) ? 1 : -1; + + // 沿外接矩形高度采样(两端各缩 5%,至少 6px,避开圆角) + double fY0 = rsRect.y; + double fY1 = rsRect.y + rsRect.height - 1; + const double fTrim = std::max(6.0, (fY1 - fY0) * 0.05); + fY0 += fTrim; + fY1 -= fTrim; + if (fY1 - fY0 < 20.0) + { + return false; + } + + std::vector vecEdgePts; // (t,v) = (yDet, xDet) + vecEdgePts.reserve((int)((fY1 - fY0) / nStep) + 1); + + for (double fT = fY0; fT <= fY1; fT += nStep) + { + // 原图 -> 检测图(detImg) 为纯平移;列带两端换算到检测图坐标 + cv::Point2f ptDetStart = MatAffinePoint(m_matSrc2Det, cv::Point2f((float)nXStart, (float)fT)); + cv::Point2f ptDetEnd = MatAffinePoint(m_matSrc2Det, cv::Point2f((float)nXEnd, (float)fT)); + const int nYDet = cvRound(ptDetStart.y); + if (nYDet < 2 || nYDet >= grayRef.rows - 2) + { + continue; + } + const int nX0 = cvRound(ptDetStart.x); // 外侧端 + const int nX1 = cvRound(ptDetEnd.x); // 内侧端 + if ((nStepX > 0 && nX1 <= nX0) || (nStepX < 0 && nX1 >= nX0)) + { + continue; + } + + // 外侧 -> 内侧逐像素扫,取灰度梯度最大的位置作为该行支架边缘点 + int nEdge = -1; + float fBestGrad = 0.0f; + for (int x = nX0; (nStepX > 0) ? (x + nStepX <= nX1) : (x + nStepX >= nX1); x += nStepX) + { + if (x < 1 || x + nStepX < 1 || x >= grayRef.cols - 1 || x + nStepX >= grayRef.cols - 1) + { + continue; + } + const float fGrad = fabs(gray3(nYDet, x + nStepX) - gray3(nYDet, x)); + if (fGrad > fBestGrad) + { + fBestGrad = fGrad; + nEdge = x; + } + } + if (nEdge < 0 || fBestGrad < (float)SUPPORT_REFINE_MIN_CONTRAST) + { + continue; // 该行列带内没有明显灰度阶跃,本行不参与拟合 + } + vecEdgePts.push_back(cv::Point2f((float)nYDet, (float)(nEdge + nStepX * 0.5f))); + } + + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测", + "support edge diag(side %d): strip %d cols [%d,%d], pts %d", + nSide, nStrip, nXStart, nXEnd, (int)vecEdgePts.size()); + + nSampleOut = (int)vecEdgePts.size(); + if (nSampleOut < 8) + { + return false; + } + + // 沿边分成 n 段,每段稳健拟合一条直线(凸起处点被 MAD 剔除;拟合到凸起外边缘也不影响方向) + std::vector vecAngles; + std::vector vecAnglesVert; + vecAngles.reserve(nSegments); + vecAnglesVert.reserve(nSegments); + for (int s = 0; s < nSegments; s++) + { + const size_t i0 = (size_t)((long long)s * nSampleOut / nSegments); + const size_t i1 = (size_t)((long long)(s + 1) * nSampleOut / nSegments); + if (i1 - i0 < 4) + { + continue; + } + std::vector vecSeg(vecEdgePts.begin() + i0, vecEdgePts.begin() + i1); + double fKSeg = 0, fBSeg = 0; + if (FitLineRobustTV(vecSeg, fKSeg, fBSeg) < 4) + { + continue; + } + + // 拟合线方向(统一朝下)与产品基准边缘的带符号夹角 + cv::Point2f vSeg((float)fKSeg, 1.0f); + float fSegLen = (float)cv::norm(vSeg); + if (fSegLen < 1e-6f) + { + continue; + } + vSeg *= (1.0f / fSegLen); + const double dCross = (double)vSeg.x * vRef.y - (double)vSeg.y * vRef.x; + const double dDot = (double)vSeg.x * vRef.x + (double)vSeg.y * vRef.y; + const float fAngle = (float)(std::atan2(dCross, dDot) * 180.0 / CV_PI); + if (fabs(fAngle) > 45.0f) + { + continue; // 与产品边缘夹角不合理,丢弃该段 + } + vecAngles.push_back(fAngle); + vecAnglesVert.push_back((float)(std::atan2(fKSeg, 1.0) * 180.0 / CV_PI)); + // 记录该段拟合直线的两端点(检测图坐标),-s 时用于绘制支架边缘 + if (pSegLinesOut != NULL) + { + const float fSegT0 = vecSeg.front().x; // vecSeg 点为 (t,v) = (yDet, xDet) + const float fSegT1 = vecSeg.back().x; + std::vector vecLine(2); + vecLine[0] = cv::Point2f((float)(fKSeg * fSegT0 + fBSeg), fSegT0); + vecLine[1] = cv::Point2f((float)(fKSeg * fSegT1 + fBSeg), fSegT1); + pSegLinesOut->push_back(vecLine); + } + } + nSegOut = (int)vecAngles.size(); + if (nSegOut < 2) + { + return false; + } + + // 取各段角度中位数,并用 MAD 检查各段一致性(凸起/纹理导致的异常段会被中位数稀释) + std::vector vecTmp = vecAngles; + std::nth_element(vecTmp.begin(), vecTmp.begin() + vecTmp.size() / 2, vecTmp.end()); + const float fMedian = vecTmp[vecTmp.size() / 2]; + for (size_t i = 0; i < vecAngles.size(); i++) + { + vecTmp[i] = fabs(vecAngles[i] - fMedian); + } + std::nth_element(vecTmp.begin(), vecTmp.begin() + vecTmp.size() / 2, vecTmp.end()); + if (vecTmp[vecTmp.size() / 2] > 5.0f) + { + return false; // 各段方向离散过大(可能拟合到了不同结构),不采用 + } + + fAngleOut = fMedian; + std::nth_element(vecAnglesVert.begin(), vecAnglesVert.begin() + vecAnglesVert.size() / 2, vecAnglesVert.end()); + fEdgeVertOut = vecAnglesVert[vecAnglesVert.size() / 2]; + if (pSamplesOut != NULL) + { + pSamplesOut->reserve(vecEdgePts.size()); + for (size_t i = 0; i < vecEdgePts.size(); i++) + { + pSamplesOut->push_back(cv::Point2f(vecEdgePts[i].y, vecEdgePts[i].x)); // (t,v) -> (x,y) + } + } + return true; + }; + + // 按 edge_select 选择拟合方向:left -> 支架右边缘,right -> 支架左边缘(up/down 不处理) + int nCandSide[2] = {0, 0}; + int nCandRef[2] = {0, 0}; + int nCandNum = 0; + for (size_t iselect = 0; iselect < supportDet.edge_select.size(); iselect++) + { + const std::string &strEdge = supportDet.edge_select[iselect]; + int nSide = -1; + int nRef = -1; + if (strEdge == "left") + { + nSide = 1; // 拟合支架右边缘 + nRef = 0; // 参考产品左边缘 + } + else if (strEdge == "right") + { + nSide = 0; // 拟合支架左边缘 + nRef = 1; // 参考产品右边缘 + } + else + { + continue; + } + bool bDup = false; + for (int ic = 0; ic < nCandNum; ic++) + { + if (nCandSide[ic] == nSide && nCandRef[ic] == nRef) + { + bDup = true; + } + } + if (!bDup && nCandNum < 2) + { + nCandSide[nCandNum] = nSide; + nCandRef[nCandNum] = nRef; + nCandNum++; + } + } + + int nBestCand = -1; + int nBestSample = 0; + int nBestSeg = 0; + float fBestAngle = 0.0f; + float fBestEdgeVert = 0.0f; + int nBestSideUsed = -1; + int nBestRefUsed = -1; + std::vector vecBestSamples; // 采样边缘点(检测图坐标) + std::vector> vecBestSegLines; // 各段拟合线(检测图坐标) + for (int ic = 0; ic < nCandNum; ic++) + { + float fAngle = 0.0f; + float fEdgeVert = 0.0f; + int nSample = 0; + int nSeg = 0; + std::vector vecCandSamples; + std::vector> vecCandSegLines; + // 只拟合 edge_select 指定的支架长边;该侧失败时保留 minAreaRect 结果,不换边 + // (两条长边方向角虽一致,但换边会静默得到另一条边的结果,与配置含义不符) + bool bOk = FitSupportEdgeAngle(nCandSide[ic], nCandRef[ic], fAngle, nSample, nSeg, fEdgeVert, + &vecCandSamples, &vecCandSegLines); + const int nUseSide = nCandSide[ic]; + if (!bOk) + if (!bOk) + { + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测", + "support angle refine(ref %s): %s support edge fail, keep minAreaRect", + (nCandRef[ic] == 0) ? "left" : "right", (nCandSide[ic] == 0) ? "left" : "right"); + continue; + } + const cv::Point2f vRefLog = ptSegBArr[nCandRef[ic]] - ptSegAArr[nCandRef[ic]]; + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测", + "support angle refine(ref %s): use %s support edge: sample %d seg %d angle %.2f deg (edge vs vert %.2f, ref vs vert %.2f)", + (nCandRef[ic] == 0) ? "left" : "right", (nCandSide[ic] == 0) ? "left" : "right", + nSample, nSeg, fAngle, fEdgeVert, + (float)(std::atan2((double)vRefLog.x, (double)vRefLog.y) * 180.0 / CV_PI)); + if (nSample > nBestSample) + { + nBestSample = nSample; + nBestSeg = nSeg; + fBestAngle = fAngle; + fBestEdgeVert = fEdgeVert; + nBestSideUsed = nUseSide; + nBestRefUsed = nCandRef[ic]; + vecBestSamples = vecCandSamples; + vecBestSegLines = vecCandSegLines; + nBestCand = ic; + } + } + if (nBestCand >= 0) + { + fSupportAngle = fBestAngle; + bRotOffset = (fabs(fSupportAngle) > supportDet.r_offset); + bSupportAngleValid = true; + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测", + "support angle refine ok: angle %.2f deg (sample %d seg %d), replace minAreaRect", + fSupportAngle, nBestSample, nBestSeg); + + // -s(存过程图):绘制精细化拟合的支架边缘,便于从图像确认拟合效果 + // 蓝=smallmask 支架轮廓 黄=参与拟合的采样边缘点 绿=各段稳健拟合直线 + // 红=最终方向线(产品基准边方向 + 精细夹角,过采样点重心) 品红=产品基准边 + if (DetImgInfo_shareP->bsaveProcessImg && !vecBestSamples.empty()) + { + cv::Mat show; + if (grayRef.channels() == 1) + { + cv::cvtColor(grayRef, show, cv::COLOR_GRAY2BGR); + } + else + { + show = grayRef.clone(); + } + + // 支架区域轮廓(蓝):smallmask 最大轮廓映射到检测图坐标 + std::vector vecRsPoly; + vecRsPoly.reserve(rsContour.size()); + for (size_t ip = 0; ip < rsContour.size(); ip++) + { + cv::Point2f ptSrc(bigRect.x + rsContour[ip].x * fx, bigRect.y + rsContour[ip].y * fy); + cv::Point2f ptDet = MatAffinePoint(m_matSrc2Det, ptSrc); + vecRsPoly.push_back(cv::Point(cvRound(ptDet.x), cvRound(ptDet.y))); + } + if (vecRsPoly.size() >= 3) + { + cv::polylines(show, vecRsPoly, true, cv::Scalar(255, 0, 0), 4); + } + + // 采样边缘点(黄) + for (size_t i = 0; i < vecBestSamples.size(); i++) + { + cv::circle(show, cv::Point(cvRound(vecBestSamples[i].x), cvRound(vecBestSamples[i].y)), 4, + cv::Scalar(0, 255, 255), -1); + } + + // 各段稳健拟合的直线(绿) + for (size_t i = 0; i < vecBestSegLines.size(); i++) + { + if (vecBestSegLines[i].size() == 2) + { + cv::line(show, cv::Point(cvRound(vecBestSegLines[i][0].x), cvRound(vecBestSegLines[i][0].y)), + cv::Point(cvRound(vecBestSegLines[i][1].x), cvRound(vecBestSegLines[i][1].y)), + cv::Scalar(0, 255, 0), 4); + } + } + + // 最终方向线(红):产品基准边方向旋转支架精细夹角,过采样点重心 + cv::Point2f vRefBest = ptSegBArr[nBestRefUsed] - ptSegAArr[nBestRefUsed]; + float fRefBestLen = (float)cv::norm(vRefBest); + if (fRefBestLen > 1e-3f) + { + vRefBest *= (1.0f / fRefBestLen); + if (vRefBest.y < 0) + { + vRefBest = -vRefBest; + } + const double dRad = (double)fSupportAngle * CV_PI / 180.0; + cv::Point2f vDir((float)(vRefBest.x * std::cos(dRad) + vRefBest.y * std::sin(dRad)), + (float)(-vRefBest.x * std::sin(dRad) + vRefBest.y * std::cos(dRad))); + float fDirLen = (float)cv::norm(vDir); + if (fDirLen > 1e-6f) + { + vDir *= (1.0f / fDirLen); + } + cv::Point2f ptCenter(0.0f, 0.0f); + float fSampleYMin = vecBestSamples[0].y; + float fSampleYMax = vecBestSamples[0].y; + for (size_t i = 0; i < vecBestSamples.size(); i++) + { + ptCenter += vecBestSamples[i]; + fSampleYMin = std::min(fSampleYMin, vecBestSamples[i].y); + fSampleYMax = std::max(fSampleYMax, vecBestSamples[i].y); + } + ptCenter *= (1.0f / (float)vecBestSamples.size()); + const float fHalf = std::max(20.0f, (fSampleYMax - fSampleYMin) * 0.6f); + cv::line(show, cv::Point(cvRound(ptCenter.x - vDir.x * fHalf), cvRound(ptCenter.y - vDir.y * fHalf)), + cv::Point(cvRound(ptCenter.x + vDir.x * fHalf), cvRound(ptCenter.y + vDir.y * fHalf)), + cv::Scalar(0, 0, 255), 4); + + // 产品基准边缘(品红) + cv::Point2f ptRefA = MatAffinePoint(m_matSrc2Det, ptSegAArr[nBestRefUsed]); + cv::Point2f ptRefB = MatAffinePoint(m_matSrc2Det, ptSegBArr[nBestRefUsed]); + cv::line(show, cv::Point(cvRound(ptRefA.x), cvRound(ptRefA.y)), + cv::Point(cvRound(ptRefB.x), cvRound(ptRefB.y)), + cv::Scalar(255, 0, 255), 3); + } + + // 文字信息:所用支架边/参考产品边/采样与分段数/角度 + char strDbg[256]; + snprintf(strDbg, sizeof(strDbg), "support edge refine: side %s ref %s sample %d seg %d", + (nBestSideUsed == 0) ? "left" : "right", (nBestRefUsed == 0) ? "left" : "right", + nBestSample, nBestSeg); + cv::putText(show, strDbg, cv::Point(20, 70), cv::FONT_HERSHEY_SIMPLEX, 2.0, cv::Scalar(0, 255, 255), 4); + cv::Point2f vRefDbg = ptSegBArr[nBestRefUsed] - ptSegAArr[nBestRefUsed]; + snprintf(strDbg, sizeof(strDbg), "angle %.2f deg (edge vs vert %.2f, ref vs vert %.2f)", + fSupportAngle, fBestEdgeVert, + (float)(std::atan2((double)vRefDbg.x, (double)vRefDbg.y) * 180.0 / CV_PI)); + cv::putText(show, strDbg, cv::Point(20, 140), cv::FONT_HERSHEY_SIMPLEX, 2.0, cv::Scalar(0, 255, 255), 4); + + 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 + "_support_angle.jpg", showSmall, paramJpg); + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测", + "support angle debug image saved: %s_support_angle.jpg", DetImgInfo_shareP->strChannel.c_str()); + } + } + else if (!supportDet.edge_select.empty()) + { + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测", + "support angle refine not applied(no left/right edge or fit failed), use minAreaRect"); + } } - else if (fSupportAngle < -90.0f) + + // 3.5、旋转偏移(兜底):支架最小外接旋转矩形(长轴对应左/右边缘、短轴对应上/下边缘) + // 长轴与产品左/右边缘的夹角 > r_offset(度) → 偏移;r_offset<=0 不判定(兼容旧参数) + if (!bSupportAngleValid) { - fSupportAngle += 180.0f; + cv::Point2f rpts[4]; + rsRotRect.points(rpts); + cv::Point2f d01 = rpts[1] - rpts[0]; + cv::Point2f d12 = rpts[2] - rpts[1]; + float len01 = (float)cv::norm(d01); + float len12 = (float)cv::norm(d12); + // 长轴方向 = 长边方向(长轴对应左/右边缘),并统一朝下(与左边缘 上->下 方向一致) + cv::Point2f vLong(0, 1); + if (len01 >= len12 && len01 > 1e-3f) + { + vLong = d01 * (1.0f / len01); + } + else if (len12 > 1e-3f) + { + vLong = d12 * (1.0f / len12); + } + if (vLong.y < 0) + { + vLong = -vLong; + } + // 参考方向 = 产品左边缘(ptSegAArr[0]->ptSegBArr[0],拟合边/回退矩形边);退化时用垂直方向 + cv::Point2f vRef = ptSegBArr[0] - ptSegAArr[0]; + float lenRef = (float)cv::norm(vRef); + if (lenRef > 1e-3f) + { + vRef *= (1.0f / lenRef); + } + else + { + vRef = cv::Point2f(0, 1); + } + if (vRef.y < 0) + { + vRef = -vRef; + } + // 有符号夹角(度);长轴是“线”,角度归一化到 [-90, 90] + double dCross = (double)vLong.x * vRef.y - (double)vLong.y * vRef.x; + double dDot = (double)vLong.x * vRef.x + (double)vLong.y * vRef.y; + fSupportAngle = (float)(std::atan2(dCross, dDot) * 180.0 / CV_PI); + if (fSupportAngle > 90.0f) + { + fSupportAngle -= 180.0f; + } + else if (fSupportAngle < -90.0f) + { + fSupportAngle += 180.0f; + } + bRotOffset = (fabs(fSupportAngle) > supportDet.r_offset); } - bRotOffset = (fabs(fSupportAngle) > supportDet.r_offset); } // 像素 -> mm @@ -1766,13 +2242,13 @@ int ImgCheckAnalysisy::CheckRun() m_SupportDrawInfo.ptCenter = cv::Point2f(fCx, fCy); m_pdetlog->AddCheckstr(PrintLevel_0, "支架检测", - "rp [%d %d %d %d] rs [%d %d %d %d] support area %.2f mm2 (min %.2f) scale (%.4f %.4f) center (%.1f %.1f) edge %s quad %ld dL %.1f dR %.1f dT %.1f dB %.1f | select %s diff %s avgDiff %.1f limit %.1f | rot %.2f deg limit %.2f ng %d", + "rp [%d %d %d %d] rs [%d %d %d %d] support area %.2f mm2 (min %.2f) scale (%.4f %.4f) center (%.1f %.1f) edge %s quad %ld dL %.1f dR %.1f dT %.1f dB %.1f | select %s diff %s avgDiff %.1f limit %.1f | rot %.2f deg limit %.2f(%s) ng %d", rpRect.x, rpRect.y, rpRect.width, rpRect.height, rsRect.x, rsRect.y, rsRect.width, rsRect.height, dSupportAreaMm2, supportDet.support_min_area, fx, fy, fCx, fCy, bFitQuad ? "fitquad" : "rpbox", (long)m_HandleBodyQuad.size(), fDL, fDR, fDT, fDB, strSelect.c_str(), strDiff.c_str(), fAvgDiff, supportDet.x_offset, - fSupportAngle, supportDet.r_offset, bOffset); + fSupportAngle, supportDet.r_offset, bSupportAngleValid ? "refine" : "rect", bOffset); if (bOffset) { diff --git a/ConfigModule/include/CheckConfigDefine.h b/ConfigModule/include/CheckConfigDefine.h index 01e332a..8420d6f 100644 --- a/ConfigModule/include/CheckConfigDefine.h +++ b/ConfigModule/include/CheckConfigDefine.h @@ -937,6 +937,11 @@ struct Function_Support_Det // 沿边缘按步长采样,从边缘向产品内侧逐像素找第一个 255 像素作为新边缘点,再重新拟合边缘线 float edge_refine_search; // edge_refine_search:最多向内搜索的像素数;<=0=未配置(默认 80) float edge_refine_step; // edge_refine_step:沿边采样步长(像素,不需要每个点都找);<=0=未配置(默认 40) + // 支架方向精细化(Support_Detect.form.support_param,均可选):模型 smallmask 精度低 + // (1696×800 上采样回原图),minAreaRect 求支架方向角误差大;改为在原图(detImg)上拟合支架长边求方向角 + float support_refine_search; // support_refine_search:取模型支架外接矩形最外侧多少列作为边缘搜索带;<=0=未配置(默认 30) + float support_refine_step; // support_refine_step:沿支架长边采样步长(像素);<=0=未配置(默认 20) + float support_refine_segments; // support_refine_segments:采样点分段数(每段稳健拟合一条线取角度中位数);<=0=未配置(默认 6) // 把手参数(Handle_Detect) bool bDetHandle; // 把手缺失检测开关(Handle_Detect.isOpen) // 把手区域(最多 HANDLE_REGION_MAX_NUM 个,只保存非空的;区域宽高用于把手卡控) @@ -977,6 +982,9 @@ struct Function_Support_Det support_min_area = 0; edge_refine_search = 0; edge_refine_step = 0; + support_refine_search = 0; + support_refine_step = 0; + support_refine_segments = 0; bDetHandle = false; handleRegions.clear(); handleBoxes.clear(); @@ -1008,6 +1016,9 @@ struct Function_Support_Det this->support_min_area = tem.support_min_area; this->edge_refine_search = tem.edge_refine_search; this->edge_refine_step = tem.edge_refine_step; + this->support_refine_search = tem.support_refine_search; + this->support_refine_step = tem.support_refine_step; + this->support_refine_segments = tem.support_refine_segments; this->bDetHandle = tem.bDetHandle; this->handleRegions.assign(tem.handleRegions.begin(), tem.handleRegions.end()); this->handleBoxes.assign(tem.handleBoxes.begin(), tem.handleBoxes.end()); @@ -1026,16 +1037,16 @@ struct Function_Support_Det } void print(std::string str) { - printf("%s>>bDetSupport %d bDrawSupport %d bUseTraditionEdge %d bDetHandle %d bDetTag %d x_standard %f y_standard %f x_offset %f y_offset %f r_offset %f support_min_area %f edgeRefine[search %.0f step %.0f] 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, bUseTraditionEdge, bDetHandle, bDetTag, x_standard, y_standard, x_offset, y_offset, r_offset, support_min_area, edge_refine_search, edge_refine_step, edge_select.size(), handleRegions.size(), handle_diff, + printf("%s>>bDetSupport %d bDrawSupport %d bUseTraditionEdge %d bDetHandle %d bDetTag %d x_standard %f y_standard %f x_offset %f y_offset %f r_offset %f support_min_area %f edgeRefine[search %.0f step %.0f] supportRefine[search %.0f step %.0f seg %.0f] 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, bUseTraditionEdge, bDetHandle, bDetTag, x_standard, y_standard, x_offset, y_offset, r_offset, support_min_area, edge_refine_search, edge_refine_step, support_refine_search, support_refine_step, support_refine_segments, 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[1024]; - sprintf(buffer, "%s>>bDetSupport %d bDrawSupport %d bUseTraditionEdge %d bDetHandle %d bDetTag %d x_standard %f y_standard %f x_offset %f y_offset %f r_offset %f support_min_area %f edgeRefine[search %.0f step %.0f] 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, bUseTraditionEdge, bDetHandle, bDetTag, x_standard, y_standard, x_offset, y_offset, r_offset, support_min_area, edge_refine_search, edge_refine_step, edge_select.size(), handleRegions.size(), handle_diff, + sprintf(buffer, "%s>>bDetSupport %d bDrawSupport %d bUseTraditionEdge %d bDetHandle %d bDetTag %d x_standard %f y_standard %f x_offset %f y_offset %f r_offset %f support_min_area %f edgeRefine[search %.0f step %.0f] supportRefine[search %.0f step %.0f seg %.0f] 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, bUseTraditionEdge, bDetHandle, bDetTag, x_standard, y_standard, x_offset, y_offset, r_offset, support_min_area, edge_refine_search, edge_refine_step, support_refine_search, support_refine_step, support_refine_segments, 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; diff --git a/ConfigModule/src/JsonConfig.cpp b/ConfigModule/src/JsonConfig.cpp index 4877c21..e53ee6d 100644 --- a/ConfigModule/src/JsonConfig.cpp +++ b/ConfigModule/src/JsonConfig.cpp @@ -974,6 +974,24 @@ int BaseFuntonConfigJson::GetFunction(Json::Value value) _config.supportDet.edge_refine_step = value_param["edge_refine_step"].asFloat(); } + // 支架方向精细化(可选):模型 smallmask 精度低(1696×800 上采样回原图), + // minAreaRect 求支架方向角误差大,改为在 detImg 灰度原图上拟合支架长边求方向角。 + // support_refine_search :取模型支架外接矩形最外侧多少列作为边缘搜索带(未配置默认 30) + // support_refine_step :沿支架长边采样步长(像素)(未配置默认 20) + // support_refine_segments:采样点分段数(每段稳健拟合一条线取角度中位数,未配置默认 6) + if (value_param["support_refine_search"]) + { + _config.supportDet.support_refine_search = value_param["support_refine_search"].asFloat(); + } + if (value_param["support_refine_step"]) + { + _config.supportDet.support_refine_step = value_param["support_refine_step"].asFloat(); + } + if (value_param["support_refine_segments"]) + { + _config.supportDet.support_refine_segments = value_param["support_refine_segments"].asFloat(); + } + auto value_qx = value_param["edge_select"]; if (value_qx.isArray()) {