From c4ca12293ef03e7a566e67b355b676a34f4665e8 Mon Sep 17 00:00:00 2001 From: liusiyang Date: Mon, 12 Oct 2026 23:10:41 +0800 Subject: [PATCH] =?UTF-8?q?feat=20=E6=B7=BB=E5=8A=A0=E7=9B=B4=E7=BA=BF?= =?UTF-8?q?=E6=8B=9F=E5=90=88=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AlgorithmModule/include/ImgCheckAnalysisy.hpp | 18 + AlgorithmModule/src/ImgCheckAnalysisy.cpp | 635 +++++++++++++++++- 2 files changed, 652 insertions(+), 1 deletion(-) diff --git a/AlgorithmModule/include/ImgCheckAnalysisy.hpp b/AlgorithmModule/include/ImgCheckAnalysisy.hpp index ab45249..a787fe6 100644 --- a/AlgorithmModule/include/ImgCheckAnalysisy.hpp +++ b/AlgorithmModule/include/ImgCheckAnalysisy.hpp @@ -40,6 +40,17 @@ using namespace std; using namespace cv; +// ============================ Pin 检测方案开关 ============================ +// 1 = 新方案(Pin 直接分析, 不做模板对比): 不做仿射变换, 检测图直接按当前外定位结果裁剪; +// 只分析检测Pin: 自动区分两排Pin(考虑图像旋转)并逐排拟合直线, +// 到直线(法线方向)距离超过 pin_y_offset 的判 偏移; +// 相邻两Pin中心连线距离超过 pin_x_offset 的判 两Pin中间缺失一个Pin。 +// 0 = 旧方案(模板对比/内定位仿射): 以内定位(无效时回退外定位)仿射变换到模板坐标系后与模板Pin对比。 +// 说明: 可通过编译参数 -DPIN_USE_DIRECT_MATCH=0/1 覆盖此处默认值。 +#ifndef PIN_USE_DIRECT_MATCH +#define PIN_USE_DIRECT_MATCH 0 +#endif + enum AT_THRESHOLD_TYPE_ { AT_THRESHOLD_TYPE_NULL, @@ -196,6 +207,11 @@ private: // 边缘缺陷检测 int Pin_Qx_Det(const cv::Mat &img); +#if PIN_USE_DIRECT_MATCH + // 新方案: 不做模板对比, 只分析检测Pin: 自动分两排(考虑旋转) -> 拟合直线判偏移 + 相邻间距判缺失 + int Pin_Direct_Check(); +#endif + // 把blob 汇总成 检测结果。 int BLobToDetResult(); @@ -303,6 +319,8 @@ private: cv::RotatedRect m_tplInnerRect; // 模板内边缘矩形 std::vector> m_tplPinContours; // 模板 pin blob 轮廓点集 std::vector> m_curPinContours; // 当前 pin blob 轮廓点集 + std::vector> m_pinFitLines; // 新方案: 两排Pin拟合直线段(当前检测图坐标系, 两端点) + std::vector m_pinMissMarks; // 新方案: 缺失Pin位置(相邻Pin中心连线中点) }; #endif \ No newline at end of file diff --git a/AlgorithmModule/src/ImgCheckAnalysisy.cpp b/AlgorithmModule/src/ImgCheckAnalysisy.cpp index 0fcb777..f427ebd 100644 --- a/AlgorithmModule/src/ImgCheckAnalysisy.cpp +++ b/AlgorithmModule/src/ImgCheckAnalysisy.cpp @@ -10,6 +10,8 @@ #include "CheckUtil.hpp" #include "Define.h" #include +#include +#include #include "AICommonDefine.h" // 用于排序轮廓的比较函数 static bool compareContourAreas(const vector &contour1, const vector &contour2) @@ -557,6 +559,27 @@ int ImgCheckAnalysisy::CheckRun() } /*投影对齐模板*/ +#if PIN_USE_DIRECT_MATCH + // 新方案(开关 PIN_USE_DIRECT_MATCH = 1): 不做仿射变换, 检测图直接按当前外定位结果裁剪; + // Pin分析在 Pin_Qx_Det -> Pin_Direct_Check 中完成(只分析检测Pin, 不做模板对比) + m_pdetlog->AddCheckstr(PrintLevel_0, "2.1、pre detect", "Align mode: Direct Pin Check (no affine)"); + { + cv::Rect curOuterBound = m_outer_rroi.boundingRect(); + cv::Rect curImgBound(0, 0, m_CheckResult_shareP->in_shareImage->img.cols, + m_CheckResult_shareP->in_shareImage->img.rows); + curOuterBound &= curImgBound; + if (curOuterBound.width <= 0 || curOuterBound.height <= 0) + { + cv::Rect rawBound = m_outer_rroi.boundingRect(); + m_pdetlog->AddCheckstr(PrintLevel_0, "Error", "Direct Pin Match: current outer roi invalid [%d %d %d %d]", + rawBound.x, rawBound.y, rawBound.width, rawBound.height); + m_nErrorCode = CHECK_ERROR_Config_cutRoi; + m_nCheckResultErrorCode = m_nErrorCode; + return m_nErrorCode; + } + m_outer_roi = curOuterBound; + } +#else // 检测PIN偏移/缺失对位置基准最敏感:PIN 位置的真实基准是产品内腔(内定位), // 因此优先用内定位做仿射变换(当前内边缘 -> 模板内边缘); // 模板内定位/当前内定位缺失或无效时,回退到外定位(与旧行为一致) @@ -573,6 +596,7 @@ int ImgCheckAnalysisy::CheckRun() Adapt_Config(m_tplOuterRect, m_outer_rroi, m_CheckResult_shareP->in_shareImage->img); } m_outer_roi = m_tplOuterRect.boundingRect(); +#endif m_Crop_Roi_paramImg = m_outer_roi; m_pImageAllResult->pDetResult->CutRoi = m_outer_roi; m_pImageAllResult->pDetResult->Param_CropRoi = m_Crop_Roi_paramImg; @@ -1850,7 +1874,27 @@ int ImgCheckAnalysisy::DrawResult_Step_1() float fs_resize_y = m_pImageAllResult->resultImg.rows * 1.0f / m_pImageAllResult->detImg.rows; // 绘制模板 pin 轮廓(绿色)与当前 pin 轮廓(红色) - for (const auto &contour : m_tplPinContours) +#if PIN_USE_DIRECT_MATCH + // 新方案: 模板pin已配准到当前检测图坐标系 + // 新方案: 不做模板对比; 将两排Pin拟合直线段与缺失标记组织成 2点"轮廓", 复用下方循环缩放绘制(绿色) + std::vector> tplDrawPinContours; + for (const auto &seg : m_pinFitLines) + { + tplDrawPinContours.push_back({cv::Point(cvRound(seg.first.x), cvRound(seg.first.y)), + cv::Point(cvRound(seg.second.x), cvRound(seg.second.y))}); + } + for (const auto &mark : m_pinMissMarks) + { + // 缺失位置: 十字标记(两条短线) + tplDrawPinContours.push_back({cv::Point(cvRound(mark.x - 6.0f), cvRound(mark.y)), + cv::Point(cvRound(mark.x + 6.0f), cvRound(mark.y))}); + tplDrawPinContours.push_back({cv::Point(cvRound(mark.x), cvRound(mark.y - 6.0f)), + cv::Point(cvRound(mark.x), cvRound(mark.y + 6.0f))}); + } +#else + const std::vector> &tplDrawPinContours = m_tplPinContours; +#endif + for (const auto &contour : tplDrawPinContours) { std::vector draw_pts; draw_pts.reserve(contour.size()); @@ -1978,6 +2022,10 @@ int ImgCheckAnalysisy::Pin_Qx_Det(const cv::Mat &img) m_curPinContours.push_back(mapped); } +#if PIN_USE_DIRECT_MATCH + // 新方案: 不做模板对比, 只分析检测Pin(自动分两排 -> 逐排拟合直线判偏移 + 相邻间距判缺失) + Pin_Direct_Check(); +#else // 根据模板Pin和当前Pin轮廓进行对比,判断当前有无缺失和偏移 { // 计算轮廓中心点(外接矩形中心) @@ -2110,6 +2158,7 @@ int ImgCheckAnalysisy::Pin_Qx_Det(const cv::Mat &img) m_pdetlog->AddCheckstr(PrintLevel_0, "Pin Check", "tpl %d cur %d miss %d offset %d", nTpl, nCur, nMiss, nOffset); } } +#endif if (m_Edge_DetConfig.bSaveResultImg) { @@ -2130,13 +2179,597 @@ int ImgCheckAnalysisy::Pin_Qx_Det(const cv::Mat &img) showimg = img.clone(); } cv::drawContours(showimg, m_curPinContours, -1, CheckUtil::GetDrawColor(showimg, cv::Scalar(0, 0, 255)), 1); +#if PIN_USE_DIRECT_MATCH + // 新方案: 绘制两排Pin的拟合直线(绿); 缺失位置以黄色十字标记 + for (const auto &seg : m_pinFitLines) + { + cv::line(showimg, cv::Point(cvRound(seg.first.x), cvRound(seg.first.y)), + cv::Point(cvRound(seg.second.x), cvRound(seg.second.y)), + CheckUtil::GetDrawColor(showimg, cv::Scalar(0, 255, 0)), 1); + } + for (const auto &mark : m_pinMissMarks) + { + cv::drawMarker(showimg, cv::Point(cvRound(mark.x), cvRound(mark.y)), + CheckUtil::GetDrawColor(showimg, cv::Scalar(0, 255, 255)), cv::MARKER_CROSS, 16, 1); + } +#else cv::drawContours(showimg, m_tplPinContours, -1, CheckUtil::GetDrawColor(showimg, cv::Scalar(0, 255, 0)), 1); +#endif imwrite("pin_contours.png", showimg); } return 0; } +#if PIN_USE_DIRECT_MATCH +#if 0 // ----- 旧配准匹配方案(Pin_Direct_Match)已停用, 仅保留代码参考; 如需启用需恢复 m_tplPinContoursCur 成员 ----- +// 新方案(开关 PIN_USE_DIRECT_MATCH = 1): 不做仿射变换, 直接对模板Pin与检测Pin做配准后对比 +// 输入: m_tplPinContours(模板裁剪坐标系) / m_curPinContours(当前检测图坐标系) +// 输出: m_tplPinContoursCur(配准到当前检测图坐标系的模板Pin轮廓) + 缺失/偏移缺陷上报 +// 思路: +// 1) 平移投票: 统计所有 "检测pin中心 - 模板pin中心" 的位移, 取出现次数最多的位移作为全局平移, +// 即让最多数量Pin对重合的平移(对缺失pin/误检pin鲁棒, 最大程度一一对应); +// 2) 由近到远贪心做一对一匹配, 并用匹配对的位移中值迭代细化全局平移(抗异常); +// 3) 配不上检测pin的模板pin -> 缺失; 配上但 x/y 偏移超阈值的 -> 偏移。 +int ImgCheckAnalysisy::Pin_Direct_Match() +{ + // 计算轮廓中心点(外接矩形中心) + auto getCenter = [](const std::vector &contour) -> cv::Point2f + { + cv::Rect r = cv::boundingRect(contour); + return cv::Point2f(r.x + r.width * 0.5f, r.y + r.height * 0.5f); + }; + + const int nTpl = static_cast(m_tplPinContours.size()); + const int nCur = static_cast(m_curPinContours.size()); + + m_tplPinContoursCur.clear(); + if (nTpl <= 0) + { + return 0; + } + + std::vector tplCenters(nTpl); + for (int i = 0; i < nTpl; i++) + { + tplCenters[i] = getCenter(m_tplPinContours[i]); + } + std::vector curCenters(nCur); + for (int i = 0; i < nCur; i++) + { + curCenters[i] = getCenter(m_curPinContours[i]); + } + + // 匹配半径: 取模板pin相邻最小间距的一半, 避免相邻pin被错配; pin数不足时用默认值 + float minGap = 0.0f; + for (int i = 0; i < nTpl; i++) + { + for (int j = i + 1; j < nTpl; j++) + { + float d = static_cast(cv::norm(tplCenters[i] - tplCenters[j])); + if (minGap <= 0.0f || d < minGap) + { + minGap = d; + } + } + } + float matchRadius = (minGap > 0.0f) ? (minGap * 0.5f) : 20.0f; + if (matchRadius < 2.0f) + { + matchRadius = 2.0f; + } + + // 1、平移投票: 统计出现次数最多的 "检测pin - 模板pin" 位移(4px 一档, 取峰值邻域均值细化) + cv::Point2f T(0.0f, 0.0f); + if (nCur > 0) + { + const float binSize = 4.0f; + std::map, int> hist; + int bestCount = 0; + float bestX = 0.0f; + float bestY = 0.0f; + for (int i = 0; i < nTpl; i++) + { + for (int j = 0; j < nCur; j++) + { + float dx = curCenters[j].x - tplCenters[i].x; + float dy = curCenters[j].y - tplCenters[i].y; + std::pair key(static_cast(std::lround(dx / binSize)), + static_cast(std::lround(dy / binSize))); + int cnt = ++hist[key]; + float cx = key.first * binSize; + float cy = key.second * binSize; + // 峰值并列时优先取位移更小的(两图裁剪都以产品为中心,真实位移通常较小) + if (cnt > bestCount || + (cnt == bestCount && cx * cx + cy * cy < bestX * bestX + bestY * bestY)) + { + bestCount = cnt; + bestX = cx; + bestY = cy; + } + } + } + + // 用峰值邻域(±1档)的所有位移取均值, 得到更精确的全局平移 + double sumX = 0.0; + double sumY = 0.0; + int sumN = 0; + for (int i = 0; i < nTpl; i++) + { + for (int j = 0; j < nCur; j++) + { + float dx = curCenters[j].x - tplCenters[i].x; + float dy = curCenters[j].y - tplCenters[i].y; + if (std::fabs(dx - bestX) <= binSize && std::fabs(dy - bestY) <= binSize) + { + sumX += dx; + sumY += dy; + sumN++; + } + } + } + if (sumN > 0) + { + T.x = static_cast(sumX / sumN); + T.y = static_cast(sumY / sumN); + } + else + { + T.x = bestX; + T.y = bestY; + } + } + + // 2、由近到远贪心做一对一匹配, 并用匹配对的位移中值迭代细化全局平移 + std::vector matchOfTpl(nTpl, -1); + for (int iter = 0; iter < 3; iter++) + { + // 收集满足匹配半径的候选对, 按距离升序 + std::vector>> cands; // (距离, (模板idx, 检测idx)) + cands.reserve(static_cast(nTpl) * static_cast(nCur)); + for (int i = 0; i < nTpl; i++) + { + cv::Point2f pred = tplCenters[i] + T; // 配准后模板pin在当前检测图中的预测位置 + for (int j = 0; j < nCur; j++) + { + double d = cv::norm(curCenters[j] - pred); + if (d <= matchRadius) + { + cands.push_back(std::make_pair(d, std::make_pair(i, j))); + } + } + } + std::sort(cands.begin(), cands.end()); + + std::vector tplUsed(nTpl, 0); + std::vector curUsed(nCur, 0); + matchOfTpl.assign(nTpl, -1); + + std::vector resDisX; + std::vector resDisY; + for (size_t k = 0; k < cands.size(); k++) + { + int i = cands[k].second.first; + int j = cands[k].second.second; + if (tplUsed[i] || curUsed[j]) + { + continue; + } + tplUsed[i] = 1; + curUsed[j] = 1; + matchOfTpl[i] = j; + resDisX.push_back(curCenters[j].x - (tplCenters[i].x + T.x)); + resDisY.push_back(curCenters[j].y - (tplCenters[i].y + T.y)); + } + + if (resDisX.empty()) + { + break; + } + + // 用位移中值(抗异常)更新全局平移, 使匹配对最大程度重合 + std::sort(resDisX.begin(), resDisX.end()); + std::sort(resDisY.begin(), resDisY.end()); + float mx = resDisX[resDisX.size() / 2]; + float my = resDisY[resDisY.size() / 2]; + T.x += mx; + T.y += my; + if (std::fabs(mx) < 0.5f && std::fabs(my) < 0.5f) + { + break; + } + } + + // 3、把模板pin轮廓按最终平移映射到当前检测图坐标系, 供匹配/绘制/上报使用 + m_tplPinContoursCur.reserve(nTpl); + for (int i = 0; i < nTpl; i++) + { + std::vector mapped; + mapped.reserve(m_tplPinContours[i].size()); + for (const auto &pt : m_tplPinContours[i]) + { + mapped.emplace_back(cvRound(pt.x + T.x), cvRound(pt.y + T.y)); + } + m_tplPinContoursCur.push_back(mapped); + } + + // 4、偏移阈值由基础参数 pin_x_offset / pin_y_offset (mm) 换算为像素 + float offsetThX = 0.0f; + float offsetThY = 0.0f; + if (m_pbaseCheckFunction != NULL) + { + if (m_fImgage_Scale_X > 0.0f) + { + offsetThX = m_pbaseCheckFunction->pinDet.pin_x_offset / m_fImgage_Scale_X; + } + if (m_fImgage_Scale_Y > 0.0f) + { + offsetThY = m_pbaseCheckFunction->pinDet.pin_y_offset / m_fImgage_Scale_Y; + } + } + + // 上报缺失/偏移缺陷(pin 位置取配准后的模板pin) + auto reportPinError = [&](int pinIdx, const cv::Point2f &tplCenter, const std::string &reason, int nconfig_qx_type) + { + QX_ERROR_INFO_ pinErr; + pinErr.Idx = static_cast(m_pDetResult->pQx_ErrorList->size()); + cv::Rect r = cv::boundingRect(m_tplPinContoursCur[pinIdx]); + pinErr.roi = r; + pinErr.area = static_cast(cv::contourArea(m_tplPinContoursCur[pinIdx])); + pinErr.JudgArea = pinErr.area * m_fImgage_Scale_X * m_fImgage_Scale_Y; + int longSide = (r.width > r.height) ? r.width : r.height; + int shortSide = (r.width < r.height) ? r.width : r.height; + pinErr.flen = longSide * m_fImgage_Scale_X; + pinErr.fbreadth = shortSide * m_fImgage_Scale_Y; + pinErr.nconfig_qx_type = nconfig_qx_type; + pinErr.qx_name = CONFIG_QX_NAME_Names[nconfig_qx_type]; + pinErr.detRegionidxList.push_back(0); + pinErr.detlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Pin Check", + "pin %d center(%d,%d) %s", + pinIdx, static_cast(tplCenter.x), static_cast(tplCenter.y), + reason.c_str()); + m_pDetResult->pQx_ErrorList->push_back(pinErr); + }; + + int nMiss = 0; + int nOffset = 0; + for (int i = 0; i < nTpl; i++) + { + // 配不上检测pin的模板pin -> 缺失 + if (matchOfTpl[i] < 0) + { + nMiss++; + reportPinError(i, tplCenters[i] + T, "miss", CONFIG_QX_NAME_PIN_QueShi); + m_pdetlog->AddCheckstr(PrintLevel_0, "Pin Check", "pin %d MISS", i); + continue; + } + + // x 或 y 方向偏移超过配置阈值 -> 偏移 + int j = matchOfTpl[i]; + float dx = curCenters[j].x - (tplCenters[i].x + T.x); + float dy = curCenters[j].y - (tplCenters[i].y + T.y); + float absDx = std::fabs(dx); + float absDy = std::fabs(dy); + if (absDx > offsetThX || absDy > offsetThY) + { + nOffset++; + char buf[64]; + snprintf(buf, sizeof(buf), "offset dx %.1f dy %.1f", absDx, absDy); + reportPinError(i, tplCenters[i] + T, buf, CONFIG_QX_NAME_PIN_PianYi); + m_pdetlog->AddCheckstr(PrintLevel_0, "Pin Check", "pin %d OFFSET dx %.1f dy %.1f", i, absDx, absDy); + } + } + + m_pdetlog->AddCheckstr(PrintLevel_0, "Pin Check", + "direct match: shift(%.1f, %.1f) tpl %d cur %d miss %d offset %d", + T.x, T.y, nTpl, nCur, nMiss, nOffset); + return 0; +} +#endif + +// 新方案(开关 PIN_USE_DIRECT_MATCH = 1): 不做模板对比, 只针对检测Pin分析 +// 1) 方向扫描(0~180°, 步长1°): 取 "两排Pin沿法线投影组内方差之和" 最小的角度作为Pin排方向(对图像旋转不敏感), +// 再沿该法线投影, 在最大间隙处把Pin分成两排(两排插口; Pin不足4个则按一排处理); +// 2) 每排做最小二乘直线拟合(cv::fitLine): pin中心到直线(沿法线方向)距离超过 pin_y_offset(换算像素) 判 偏移; +// 3) 每排内沿排方向排序: 相邻两pin中心连线距离超过 pin_x_offset(换算像素) 判 两pin中间缺失一个pin。 +int ImgCheckAnalysisy::Pin_Direct_Check() +{ + // 计算轮廓中心点(外接矩形中心) + auto getCenter = [](const std::vector &contour) -> cv::Point2f + { + cv::Rect r = cv::boundingRect(contour); + return cv::Point2f(r.x + r.width * 0.5f, r.y + r.height * 0.5f); + }; + + const int nCur = static_cast(m_curPinContours.size()); + + m_pinFitLines.clear(); + m_pinMissMarks.clear(); + if (nCur <= 0) + { + return 0; + } + + std::vector curCenters(nCur); + std::vector curRects(nCur); + for (int i = 0; i < nCur; i++) + { + curCenters[i] = getCenter(m_curPinContours[i]); + curRects[i] = cv::boundingRect(m_curPinContours[i]); + } + if (nCur < 2) + { + m_pdetlog->AddCheckstr(PrintLevel_0, "Pin Check", "direct check: pin %d < 2, skip", nCur); + return 0; + } + + // 阈值(mm 参数换算为像素): pin_x_offset -> 相邻pin间距(缺失)阈值; pin_y_offset -> 到拟合直线距离(偏移)阈值; <=0 时不判该项 + float offsetThX = 0.0f; + float offsetThY = 0.0f; + if (m_pbaseCheckFunction != NULL) + { + if (m_fImgage_Scale_X > 0.0f) + { + offsetThX = m_pbaseCheckFunction->pinDet.pin_x_offset / m_fImgage_Scale_X; + } + if (m_fImgage_Scale_Y > 0.0f) + { + offsetThY = m_pbaseCheckFunction->pinDet.pin_y_offset / m_fImgage_Scale_Y; + } + } + + // 沿给定方向投影排序, 在最大间隙处切分成两排(order[0..splitK] 一排, order[splitK+1..] 另一排) + // 两排各至少2个pin; pin不足4个时无法可靠分排, 返回 -1(按一排整体处理) + auto splitByNormal = [&](const cv::Point2f &nor, std::vector &proj, std::vector &order) -> int + { + order.resize(nCur); + proj.resize(nCur); + for (int i = 0; i < nCur; i++) + { + order[i] = i; + proj[i] = curCenters[i].dot(nor); + } + std::sort(order.begin(), order.end(), [&](int a, int b) { return proj[a] < proj[b]; }); + + int splitK = -1; + if (nCur >= 4) + { + float bestGap = -1.0f; + for (int k = 1; k <= nCur - 3; k++) + { + float gap = proj[order[k + 1]] - proj[order[k]]; + if (gap > bestGap) + { + bestGap = gap; + splitK = k; + } + } + } + return splitK; + }; + + // 组内投影方差之和: 正确排方向上两排pin各自近似共线, 沿法线投影最集中, 该值最小 + auto scoreOfRowDir = [&](const std::vector &proj, const std::vector &order, int splitK) -> double + { + double score = 0.0; + const int nGroup = (splitK >= 0) ? 2 : 1; + for (int g = 0; g < nGroup; g++) + { + int b = (g == 0) ? 0 : (splitK + 1); + int e = (g == 0) ? (splitK + 1) : nCur; + double m = 0.0; + for (int i = b; i < e; i++) + { + m += proj[order[i]]; + } + m /= (e - b); + for (int i = b; i < e; i++) + { + double d = proj[order[i]] - m; + score += d * d; + } + } + return score; + }; + + // 1、方向扫描 0~180°(1°一档), 找到让"两排沿法线投影组内方差之和"最小的排方向(考虑图像旋转) + float bestTheta = 0.0f; + if (nCur >= 4) + { + double bestScore = -1.0; + std::vector proj; + std::vector order; + for (int deg = 0; deg < 180; deg++) + { + float th = static_cast(deg * CV_PI / 180.0); + cv::Point2f nor(-std::sin(th), std::cos(th)); + int splitK = splitByNormal(nor, proj, order); + double score = scoreOfRowDir(proj, order, splitK); + if (bestScore < 0.0 || score < bestScore) + { + bestScore = score; + bestTheta = th; + } + } + } + else + { + // pin 太少无法分排, 用PCA主方向兜底(按一排处理) + cv::Point2f meanPt(0.0f, 0.0f); + for (int i = 0; i < nCur; i++) + { + meanPt += curCenters[i]; + } + meanPt.x /= nCur; + meanPt.y /= nCur; + double sxx = 0.0, sxy = 0.0, syy = 0.0; + for (int i = 0; i < nCur; i++) + { + double dx = curCenters[i].x - meanPt.x; + double dy = curCenters[i].y - meanPt.y; + sxx += dx * dx; + sxy += dx * dy; + syy += dy * dy; + } + bestTheta = static_cast(0.5 * std::atan2(2.0 * sxy, sxx - syy)); + } + + // 用最优排方向重新投影, 把Pin分成两排 + cv::Point2f dirRow(static_cast(std::cos(bestTheta)), static_cast(std::sin(bestTheta))); + cv::Point2f dirNor(-dirRow.y, dirRow.x); + std::vector proj; + std::vector order; + int splitK = splitByNormal(dirNor, proj, order); + + std::vector> rows; + if (splitK >= 0) + { + rows.push_back(std::vector(order.begin(), order.begin() + splitK + 1)); + rows.push_back(std::vector(order.begin() + splitK + 1, order.end())); + } + else + { + rows.push_back(order); + } + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Pin Check", + "row dir %.1f deg rows %d th[gap %.1f line %.1f] px", + bestTheta * 180.0f / static_cast(CV_PI), + static_cast(rows.size()), offsetThX, offsetThY); + + // 上报缺陷(roi/area 由调用处给出; grayDis 记录对应度量: 到直线距离 / 相邻间距, 单位像素) + auto reportPinError = [&](const cv::Rect &roi, int area, double grayDis, int nconfig_qx_type, const std::string &detail) + { + QX_ERROR_INFO_ pinErr; + pinErr.Idx = static_cast(m_pDetResult->pQx_ErrorList->size()); + pinErr.roi = roi; + pinErr.area = area; + pinErr.JudgArea = pinErr.area * m_fImgage_Scale_X * m_fImgage_Scale_Y; + int longSide = (roi.width > roi.height) ? roi.width : roi.height; + int shortSide = (roi.width < roi.height) ? roi.width : roi.height; + pinErr.flen = longSide * m_fImgage_Scale_X; + pinErr.fbreadth = shortSide * m_fImgage_Scale_Y; + pinErr.nconfig_qx_type = nconfig_qx_type; + pinErr.qx_name = CONFIG_QX_NAME_Names[nconfig_qx_type]; + pinErr.grayDis = grayDis; + pinErr.detRegionidxList.push_back(0); + pinErr.detlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Pin Check", "%s", detail.c_str()); + m_pDetResult->pQx_ErrorList->push_back(pinErr); + }; + + int nOffset = 0; + int nMiss = 0; + + // 2、3、逐排: 拟合直线 -> 判偏移; 沿排排序 -> 判相邻缺失 + for (size_t r = 0; r < rows.size(); r++) + { + const std::vector &rowIdx = rows[r]; + const int nRow = static_cast(rowIdx.size()); + if (nRow < 2) + { + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Pin Check", + "row %d pin num %d < 2, skip", static_cast(r), nRow); + continue; + } + + std::vector rowPts(nRow); + for (int i = 0; i < nRow; i++) + { + rowPts[i] = curCenters[rowIdx[i]]; + } + + // 最小二乘直线拟合: lineDir 单位方向向量, lineP0 直线上一点, lineNor 单位法线 + cv::Vec4f linePara; + cv::fitLine(rowPts, linePara, cv::DIST_L2, 0, 0.01, 0.01); + cv::Point2f lineDir(linePara[0], linePara[1]); + cv::Point2f lineP0(linePara[2], linePara[3]); + cv::Point2f lineNor(-lineDir.y, lineDir.x); + + // 记录拟合线段(沿排方向延伸到该排两端, 供绘制/回溯) + float tMin = 0.0f; + float tMax = 0.0f; + for (int i = 0; i < nRow; i++) + { + float t = (rowPts[i] - lineP0).dot(lineDir); + if (i == 0 || t < tMin) + { + tMin = t; + } + if (i == 0 || t > tMax) + { + tMax = t; + } + } + m_pinFitLines.push_back(std::make_pair(lineP0 + lineDir * tMin, lineP0 + lineDir * tMax)); + + // 2、pin中心到直线(沿法线方向)距离超过 pin_y_offset -> 偏移 + if (offsetThY > 0.0f) + { + for (int i = 0; i < nRow; i++) + { + float absDist = std::fabs((rowPts[i] - lineP0).dot(lineNor)); + if (absDist > offsetThY) + { + int idx = rowIdx[i]; + nOffset++; + char buf[96]; + snprintf(buf, sizeof(buf), "pin %d offset to row line %.1f px > %.1f px", idx, absDist, offsetThY); + reportPinError(curRects[idx], static_cast(cv::contourArea(m_curPinContours[idx])), + absDist, CONFIG_QX_NAME_PIN_PianYi, buf); + m_pdetlog->AddCheckstr(PrintLevel_0, "Pin Check", "row %d pin %d OFFSET %.1f px > %.1f px", + static_cast(r), idx, absDist, offsetThY); + } + } + } + + // 3、沿排方向排序, 相邻两pin中心连线距离超过 pin_x_offset -> 两pin中间缺失一个pin + if (offsetThX > 0.0f) + { + std::vector> tOrder; + tOrder.reserve(nRow); + for (int i = 0; i < nRow; i++) + { + tOrder.push_back(std::make_pair((rowPts[i] - lineP0).dot(lineDir), i)); + } + std::sort(tOrder.begin(), tOrder.end()); + + for (int i = 0; i + 1 < nRow; i++) + { + int ia = tOrder[i].second; + int ib = tOrder[i + 1].second; + float gap = static_cast(cv::norm(rowPts[ib] - rowPts[ia])); + if (gap > offsetThX) + { + int idxa = rowIdx[ia]; + int idxb = rowIdx[ib]; + cv::Point2f mid = (rowPts[ia] + rowPts[ib]) * 0.5f; + m_pinMissMarks.push_back(mid); + + // 缺失位置按两个相邻pin的平均尺寸上报 + cv::Rect roiAvg = curRects[idxa]; + roiAvg.width = (curRects[idxa].width + curRects[idxb].width + 1) / 2; + roiAvg.height = (curRects[idxa].height + curRects[idxb].height + 1) / 2; + cv::Rect roiMiss(cvRound(mid.x - roiAvg.width * 0.5f), cvRound(mid.y - roiAvg.height * 0.5f), + roiAvg.width, roiAvg.height); + int areaAvg = (static_cast(cv::contourArea(m_curPinContours[idxa])) + + static_cast(cv::contourArea(m_curPinContours[idxb]))) / 2; + nMiss++; + char buf[96]; + snprintf(buf, sizeof(buf), "miss between pin %d-%d gap %.1f px > %.1f px", idxa, idxb, gap, offsetThX); + reportPinError(roiMiss, areaAvg, gap, CONFIG_QX_NAME_PIN_QueShi, buf); + m_pdetlog->AddCheckstr(PrintLevel_0, "Pin Check", "row %d pin %d-%d MISS gap %.1f px > %.1f px", + static_cast(r), idxa, idxb, gap, offsetThX); + } + } + } + } + + m_pdetlog->AddCheckstr(PrintLevel_0, "Pin Check", + "direct check: pin %d rows %d miss %d offset %d", + nCur, static_cast(rows.size()), nMiss, nOffset); + return 0; +} +#endif // PIN_USE_DIRECT_MATCH + // 获取指定 残点(blob) 是由哪个(哪些)检测模型检出的 // 做法:对各模型自己输出的全图 mask,在 blob 的 roi 内统计非零像素; // 非零则说明该模型在该残点区域有检出。多个模型都检出时用 "+" 连接。