From aa5037e74d1bd7a4d42dce25b35bb302997a09fb Mon Sep 17 00:00:00 2001 From: liusiyang Date: Fri, 23 Oct 2026 16:46:01 +0800 Subject: [PATCH] =?UTF-8?q?feat=20=E6=B7=BB=E5=8A=A0pin=E8=84=9A=E6=8A=95?= =?UTF-8?q?=E5=BD=B1=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 | 9 + AlgorithmModule/src/ImgCheckAnalysisy.cpp | 671 +++++++++++++++++- ConfigModule/include/CheckConfigDefine.h | 1 + 3 files changed, 670 insertions(+), 11 deletions(-) diff --git a/AlgorithmModule/include/ImgCheckAnalysisy.hpp b/AlgorithmModule/include/ImgCheckAnalysisy.hpp index 7467fa0..f6e6e0d 100644 --- a/AlgorithmModule/include/ImgCheckAnalysisy.hpp +++ b/AlgorithmModule/include/ImgCheckAnalysisy.hpp @@ -199,9 +199,18 @@ private: // 当前 Pin 检测方案是否为"直接分析"(参数 pinDet.Det_Method == "line_fitting") bool IsDirectPinMethod() const; + // 当前 Pin 检测方案是否为"Pin 投影矩阵"(参数 pinDet.Det_Method == "Pin_Matrix") + bool IsPinMatrixMethod() const; + // 新方案: 不做模板对比, 只分析检测Pin: 自动分两排(考虑旋转) -> 拟合直线判偏移 + 相邻间距判缺失 int Pin_Direct_Check(); + // Pin_Matrix 方案: 用当前帧 Pin 拟合投影矩阵(上下两排分别计算相似变换), 把模板 Pin 投影到当前检测图坐标系 + int ComputePinProjectionMats(); + + // Pin_Matrix 方案: 过滤当前帧上明显的误检 pin blob(按模板 pin 的包围盒尺寸) + int FilterCurPinBlobsByTplSize(); + // 把blob 汇总成 检测结果。 int BLobToDetResult(); diff --git a/AlgorithmModule/src/ImgCheckAnalysisy.cpp b/AlgorithmModule/src/ImgCheckAnalysisy.cpp index 39c18a4..a521804 100644 --- a/AlgorithmModule/src/ImgCheckAnalysisy.cpp +++ b/AlgorithmModule/src/ImgCheckAnalysisy.cpp @@ -36,6 +36,91 @@ static float PinRowSideOfRect(const cv::RotatedRect &rr, const cv::Point2f &pt) return (pt - rr.center).dot(normalDir); } +// 规范化 Pin 检测方案字符串: 去掉首尾空白并转小写(兼容配置里的大小写/空格差异) +static std::string NormalizeDetMethodStr(const std::string &method) +{ + size_t begin = method.find_first_not_of(" \t\r\n"); + if (begin == std::string::npos) + { + return ""; + } + size_t end = method.find_last_not_of(" \t\r\n"); + std::string ret = method.substr(begin, end - begin + 1); + std::transform(ret.begin(), ret.end(), ret.begin(), + [](unsigned char c) + { return static_cast(std::tolower(c)); }); + return ret; +} + +// 应用 2x3 仿射矩阵(CV_64F): 模板图坐标 -> 目标图坐标 +static cv::Point2f ApplyAffine2D(const cv::Mat &M, const cv::Point2f &pt) +{ + return cv::Point2f(static_cast(M.at(0, 0) * pt.x + M.at(0, 1) * pt.y + M.at(0, 2)), + static_cast(M.at(1, 0) * pt.x + M.at(1, 1) * pt.y + M.at(1, 2))); +} + +// 把 Pin 中心按"排"分组(最多两排), 用于内定位法线无法分排时的兜底: +// 用 minAreaRect 求 Pin 群主方向, 沿短边(法线)投影, 在最大间隙处切分; +// 间隙过小(单排)或切分后某组不足 2 个 Pin 时按一排处理。 +static std::vector> SplitPinsIntoRows(const std::vector &pts) +{ + std::vector> rows; + const int n = static_cast(pts.size()); + std::vector all(n); + for (int i = 0; i < n; i++) + { + all[i] = i; + } + if (n < 4) + { + rows.push_back(all); + return rows; + } + + cv::RotatedRect rr = cv::minAreaRect(pts); + const double th = rr.angle * CV_PI / 180.0; + const cv::Point2f widthDir(static_cast(std::cos(th)), static_cast(-std::sin(th))); + const cv::Point2f heightDir(static_cast(std::sin(th)), static_cast(std::cos(th))); + cv::Point2f nor = (rr.size.width <= rr.size.height) ? widthDir : heightDir; + // 统一法线朝向, 避免 ±180 度只是翻转上下排的先后顺序 + if (nor.y < 0.0f || (nor.y == 0.0f && nor.x < 0.0f)) + { + nor = -nor; + } + + std::vector proj(n); + std::vector order(n); + for (int i = 0; i < n; i++) + { + order[i] = i; + proj[i] = pts[i].dot(nor); + } + std::sort(order.begin(), order.end(), [&](int a, int b) + { return proj[a] < proj[b]; }); + + int splitK = -1; + float bestGap = -1.0f; + for (int k = 1; k <= n - 3; k++) + { + float gap = proj[order[k + 1]] - proj[order[k]]; + if (gap > bestGap) + { + bestGap = gap; + splitK = k; + } + } + // 两排间距不明显(单排)或切分后某组不足 2 个 Pin -> 按一排处理 + if (splitK < 1 || splitK > n - 3 || bestGap < 5.0f) + { + rows.push_back(all); + return rows; + } + + rows.push_back(std::vector(order.begin(), order.begin() + splitK + 1)); + rows.push_back(std::vector(order.begin() + splitK + 1, order.end())); + return rows; +} + // 分类映射配置文件(每行:label索引:缺陷名称;冒号后名称为该缺陷类型名称,行的顺序即缺陷类型索引) static const std::string CLS_LABEL_CONFIG_FILE = "/home/aidlux/BOE/UseModel_ZB/KK_cls.txt"; @@ -842,6 +927,29 @@ int ImgCheckAnalysisy::CheckRun() m_outer_roi = curOuterBound; } } + else if (IsPinMatrixMethod()) + { + // 方案三(Det_Method == "Pin_Matrix"): 投影矩阵在 Pin_Qx_Det 中由当前帧 Pin 拟合(上下两排分别计算); + // 此处只需要用当前外定位包围框作为检测图裁剪区域(原图不 warp), 不依赖内/外定位仿射 + m_pdetlog->AddCheckstr(PrintLevel_0, "2.1、pre detect", + "Align mode: Pin Matrix (projection matrix fitted by pins, upper/lower rows separately, image NOT warped)"); + { + 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", "Pin Matrix: 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 { // 旧方案(Det_Method == "BTB_Marix" 或未配置): 不对原图做 warpAffine, 只计算矩阵, 并把模板 Pin 轮廓反变换到当前图坐标系。 @@ -2260,28 +2368,560 @@ int ImgCheckAnalysisy::UpdateImgageScale() } // 当前 Pin 检测方案是否为"直接分析"(新方案): 由参数 pinDet.Det_Method 在运行时决定 -// "line_fitting" -> 新方案(不做模板对比); 其它/未配置 -> 旧方案(模板对比) +// "line_fitting" -> 新方案(不做模板对比); 其它/未配置 -> 模板对比方案 bool ImgCheckAnalysisy::IsDirectPinMethod() const { if (m_pbaseCheckFunction == NULL) { return false; } + return NormalizeDetMethodStr(m_pbaseCheckFunction->pinDet.Det_Method) == "line_fitting"; +} - std::string method = m_pbaseCheckFunction->pinDet.Det_Method; - // 去掉首尾空白并转小写, 兼容大小写/带空格的配置值 - size_t begin = method.find_first_not_of(" \t\r\n"); - if (begin == std::string::npos) +// 当前 Pin 检测方案是否为"Pin 投影矩阵"方案: pinDet.Det_Method == "Pin_Matrix"(忽略大小写/空白) +// 该方案与 BTB_Marix 一样做模板 Pin 对比, 区别只在投影矩阵由 Pin 自身拟合(上下两排分别计算) +bool ImgCheckAnalysisy::IsPinMatrixMethod() const +{ + if (m_pbaseCheckFunction == NULL) { return false; } - size_t end = method.find_last_not_of(" \t\r\n"); - method = method.substr(begin, end - begin + 1); - std::transform(method.begin(), method.end(), method.begin(), - [](unsigned char c) - { return static_cast(std::tolower(c)); }); + return NormalizeDetMethodStr(m_pbaseCheckFunction->pinDet.Det_Method) == "pin_matrix"; +} + +// Pin_Matrix 方案: 过滤当前帧上明显的误检 blob(按模板 Pin 的包围框尺寸) +// 以模板 Pin 外接矩形的中位宽/高为基准(按当前/模板外框面积比缩放), 当前 blob 的宽/高需落在 [0.4, 2.5] 倍范围内; +// 模板 Pin 少于 3 个时不做过滤。 +int ImgCheckAnalysisy::FilterCurPinBlobsByTplSize() +{ + if (m_curPinContours.empty() || m_tplPinContours.size() < 3) + { + return 0; + } + + std::vector ws; + std::vector hs; + ws.reserve(m_tplPinContours.size()); + hs.reserve(m_tplPinContours.size()); + for (size_t i = 0; i < m_tplPinContours.size(); i++) + { + cv::Rect r = cv::boundingRect(m_tplPinContours[i]); + if (r.width > 0 && r.height > 0) + { + ws.push_back(r.width); + hs.push_back(r.height); + } + } + if (ws.size() < 3) + { + return 0; + } + std::sort(ws.begin(), ws.end()); + std::sort(hs.begin(), hs.end()); + float medW = static_cast(ws[ws.size() / 2]); + float medH = static_cast(hs[hs.size() / 2]); + + // 模板/当前外框面积比 -> 尺寸基准缩放(同一相机一般约为 1) + const double tplArea = static_cast(m_tplOuterRect.size.width) * m_tplOuterRect.size.height; + const double curArea = static_cast(m_outer_rroi.size.width) * m_outer_rroi.size.height; + if (tplArea > 0.0 && curArea > 0.0) + { + const float scaleRef = static_cast(std::sqrt(curArea / tplArea)); + medW *= scaleRef; + medH *= scaleRef; + } + + const float kMinRatio = 0.4f; // 尺寸下限比例 + const float kMaxRatio = 2.5f; // 尺寸上限比例 + const size_t nBefore = m_curPinContours.size(); + for (size_t i = m_curPinContours.size(); i-- > 0;) + { + cv::Rect r = cv::boundingRect(m_curPinContours[i]); + const bool bSizeOk = (r.width > 0 && r.height > 0) && + (r.width >= medW * kMinRatio) && (r.width <= medW * kMaxRatio) && + (r.height >= medH * kMinRatio) && (r.height <= medH * kMaxRatio); + if (!bSizeOk) + { + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Pin Matrix", + "filter suspicious pin blob idx %d size[%d %d] (tpl median[%.1f %.1f])", + static_cast(i), r.width, r.height, medW, medH); + m_curPinContours.erase(m_curPinContours.begin() + static_cast(i)); + } + } + if (m_curPinContours.size() != nBefore) + { + m_pdetlog->AddCheckstr(PrintLevel_0, "Pin Matrix", + "filter pin blobs: %ld -> %ld (tpl median size [%.1f %.1f])", + static_cast(nBefore), static_cast(m_curPinContours.size()), medW, medH); + } + return 0; +} + +// Det_Method == "Pin_Matrix": 与 BTB_Marix 一样做"模板 Pin vs 当前 Pin"的逐个比对(相交 -> 最近 -> dx/dy 超限), +// 区别只在投影矩阵的来源: BTB_Marix 用内/外定位矩形求仿射, 本方案直接由 Pin 拟合: +// 1) 内/外定位仿射只作为匹配初值(模板 Pin -> 当前检测图), 最终矩阵由 Pin 决定; +// 2) 模板 Pin 按当前帧内定位矩形的长边法线分成上下两排, 每排独立估计一个相似变换(旋转+等比缩放+平移): +// 一排 Pin 近似共线, 6 自由度全仿射在垂直排方向上病态, 因此用 4 自由度相似变换; +// 3) 估计准则 = "模板 Pin 投影后与当前 Pin 重合的数量最多": 先固定初值的旋转/缩放做平移投票, +// 再按最近邻一对一匹配 + RANSAC 拟合迭代求精, 取重合数最多的解; +// 4) 某排可匹配 Pin 少于 2 个(无法拟合)时, 该排退回内/外定位仿射投影, 保证缺失 Pin 的 roi 仍能落位。 +// 输出的 m_tplPinContoursCur 即按每排矩阵投影后的模板 Pin 轮廓(当前检测图坐标系), 供后续比对/绘制使用。 +int ImgCheckAnalysisy::ComputePinProjectionMats() +{ + m_tplPinContoursCur.clear(); + + if (m_tplPinContours.empty()) + { + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Pin Matrix", "no template pin contours, skip"); + return 0; + } + + // 0、过滤明显误检 blob(仅本方案; line_fitting / BTB_Marix 行为不变) + FilterCurPinBlobsByTplSize(); + + const cv::Rect tplBound = m_tplOuterRect.boundingRect(); + + 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()); + + // 1、匹配初值: 内定位优先/外定位回退(仅用于建立对应关系, 不参与最终判断) + const bool bUseInnerAlign = (m_tplInnerRect.size.width > 0 && m_tplInnerRect.size.height > 0 && + m_inner_rroi.size.width > 0 && m_inner_rroi.size.height > 0); + if (bUseInnerAlign) + { + CalAffineMats(m_tplInnerRect, m_inner_rroi); + } + else + { + CalAffineMats(m_tplOuterRect, m_outer_rroi); + } + + // T0: 模板图坐标 -> detImg 坐标 + cv::Mat T0; + if (m_bAffineValid && !m_affine_tpl2cur.empty()) + { + T0 = m_affine_tpl2cur.clone(); + T0.at(0, 2) -= m_outer_roi.x; + T0.at(1, 2) -= m_outer_roi.y; + } + else + { + // 防御: 定位矩形异常时退化为"模板外框中心 -> 当前外框中心"的平移 + const cv::Rect curOuter = m_outer_rroi.boundingRect(); + const double tx = (curOuter.x + curOuter.width * 0.5) - (tplBound.x + tplBound.width * 0.5) - m_outer_roi.x; + const double ty = (curOuter.y + curOuter.height * 0.5) - (tplBound.y + tplBound.height * 0.5) - m_outer_roi.y; + T0 = (cv::Mat_(2, 3) << 1.0, 0.0, tx, 0.0, 1.0, ty); + m_pdetlog->AddCheckstr(PrintLevel_0, "Pin Matrix", + "affine init invalid, fallback to bbox translation t(%.1f %.1f) px", tx, ty); + } + + // 2、模板 Pin 中心(模板图坐标) + 用初值投影到 detImg(仅用于分排) + std::vector tplCenters(nTpl); + std::vector tplCentersDet(nTpl); + for (int i = 0; i < nTpl; i++) + { + cv::Point2f c = getCenter(m_tplPinContours[i]); + tplCenters[i] = cv::Point2f(c.x + static_cast(tplBound.x), c.y + static_cast(tplBound.y)); + tplCentersDet[i] = ApplyAffine2D(T0, tplCenters[i]); + } + std::vector curCenters(nCur); + for (int j = 0; j < nCur; j++) + { + curCenters[j] = getCenter(m_curPinContours[j]); + } + + // 3、模板 Pin 分两排: 当前帧内定位矩形的长边法线(与整体偏移估计一致); 内定位无效时按 Pin 自身几何兜底 + std::vector> rows; + { + cv::RotatedRect innerRectDet = m_inner_rroi; + innerRectDet.center.x -= static_cast(m_outer_roi.x); + innerRectDet.center.y -= static_cast(m_outer_roi.y); + if (innerRectDet.size.width > 0 && innerRectDet.size.height > 0) + { + std::vector> sideIdx(2); + for (int i = 0; i < nTpl; i++) + { + sideIdx[(PinRowSideOfRect(innerRectDet, tplCentersDet[i]) < 0.0f) ? 0 : 1].push_back(i); + } + if (!sideIdx[0].empty() && !sideIdx[1].empty()) + { + rows.push_back(sideIdx[0]); + rows.push_back(sideIdx[1]); + } + } + if (rows.empty()) + { + rows = SplitPinsIntoRows(tplCentersDet); + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Pin Matrix", + "inner rect unavailable, split pins by pin geometry instead"); + } + } + + // 4、重合判据: pin_x_offset(mm) 换算为像素(pin_x_offset 未配置时用 pin_y_offset) + float matchRadius = 0.0f; + if (m_pbaseCheckFunction != NULL) + { + if (m_pbaseCheckFunction->pinDet.pin_x_offset > 0.0f && m_fImgage_Scale_X > 0.0f) + { + matchRadius = m_pbaseCheckFunction->pinDet.pin_x_offset / m_fImgage_Scale_X; + } + else if (m_pbaseCheckFunction->pinDet.pin_y_offset > 0.0f && m_fImgage_Scale_Y > 0.0f) + { + matchRadius = m_pbaseCheckFunction->pinDet.pin_y_offset / m_fImgage_Scale_Y; + } + } + if (matchRadius <= 0.0f) + { + matchRadius = 20.0f; // 未配置阈值时的默认匹配半径 + } + if (matchRadius < 2.0f) + { + matchRadius = 2.0f; + } + + // 5、逐排拟合: 每排放到对应的当前 Pin 组里做"平移投票(固定初值旋转/缩放) -> 最近邻一对一匹配 -> RANSAC 相似变换", + // 取"模板 Pin 投影后与当前 Pin 重合数最多"的解; 两排的对应关系取总重合数最大的配对(兼容上下颠倒) + struct PinRowFit + { + cv::Mat M; // 模板图坐标 -> detImg 坐标 + int nMatch = 0; // 投影后与当前 Pin 重合的数量 + float radius = 0.0f; + bool bFromPins = false; + }; + + // 当前帧 Pin 也按同一内定位法线分两排; 内定位无效时按 Pin 自身几何兜底 + std::vector> curRows; + { + cv::RotatedRect innerRectDet = m_inner_rroi; + innerRectDet.center.x -= static_cast(m_outer_roi.x); + innerRectDet.center.y -= static_cast(m_outer_roi.y); + if (innerRectDet.size.width > 0 && innerRectDet.size.height > 0) + { + std::vector> sideIdx(2); + for (int j = 0; j < nCur; j++) + { + sideIdx[(PinRowSideOfRect(innerRectDet, curCenters[j]) < 0.0f) ? 0 : 1].push_back(j); + } + if (!sideIdx[0].empty() && !sideIdx[1].empty()) + { + curRows = sideIdx; + } + } + if (curRows.empty() && nCur > 0) + { + curRows = SplitPinsIntoRows(curCenters); + } + } + std::vector allCurIdx(nCur); + for (int j = 0; j < nCur; j++) + { + allCurIdx[j] = j; + } + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Pin Matrix", + "pin rows: tpl %d cur %d", static_cast(rows.size()), + static_cast(curRows.size())); + + const double initAngle = std::atan2(T0.at(1, 0), T0.at(0, 0)); + auto bReasonableMat = [&](const cv::Mat &M) -> bool + { + if (M.empty() || M.rows != 2 || M.cols != 3) + { + return false; + } + const double a = M.at(0, 0), b = M.at(0, 1); + const double c = M.at(1, 0), d = M.at(1, 1); + if (a * d - b * c <= 0.0) + { + return false; // 不允许镜像/退化 + } + const double scale = std::sqrt(a * a + c * c); + if (scale < 0.5 || scale > 2.0) + { + return false; + } + double dAng = std::atan2(c, a) - initAngle; + while (dAng > CV_PI) + { + dAng -= 2.0 * CV_PI; + } + while (dAng < -CV_PI) + { + dAng += 2.0 * CV_PI; + } + return std::fabs(dAng) <= 30.0 * CV_PI / 180.0; + }; + + // 用模板 Pin 组 rowIdx 与当前 Pin 候选组 curIdxList 拟合一个相似变换(模板图坐标 -> detImg 坐标) + auto fitOneRow = [&](const std::vector &rowIdx, const std::vector &curIdxList, PinRowFit &fit) + { + const int nRow = static_cast(rowIdx.size()); + const int nCurSub = static_cast(curIdxList.size()); + + fit = PinRowFit(); + fit.M = T0.clone(); + fit.radius = matchRadius; + + // 匹配半径不能超过同排相邻 Pin 间距的一半, 否则会错配到相邻 Pin + { + float minGap = 0.0f; + for (int a = 0; a < nRow; a++) + { + for (int b = a + 1; b < nRow; b++) + { + float d = static_cast(cv::norm(tplCenters[rowIdx[a]] - tplCenters[rowIdx[b]])); + if (minGap <= 0.0f || d < minGap) + { + minGap = d; + } + } + } + if (minGap > 0.0f && fit.radius > minGap * 0.5f) + { + fit.radius = minGap * 0.5f; + } + } + if (fit.radius < 1.0f) + { + fit.radius = 1.0f; + } + + if (nRow < 2 || nCurSub < 2) + { + return; + } + + // 5.1) 平移投票: 固定初值的旋转/缩放, 取"投影后重合数最多"的平移 + const double l00 = T0.at(0, 0), l01 = T0.at(0, 1); + const double l10 = T0.at(1, 0), l11 = T0.at(1, 1); + const cv::Point2f t0(static_cast(T0.at(0, 2)), static_cast(T0.at(1, 2))); - return (method == "line_fitting"); + auto countOverlap = [&](const cv::Mat &M) -> int + { + // 粗略重合计数(允许多对一): 只看"投影点附近是否有当前 Pin", 用于平移投票 + int cnt = 0; + for (int a = 0; a < nRow; a++) + { + cv::Point2f p = ApplyAffine2D(M, tplCenters[rowIdx[a]]); + for (int k = 0; k < nCurSub; k++) + { + if (cv::norm(curCenters[curIdxList[k]] - p) <= fit.radius) + { + cnt++; + break; + } + } + } + return cnt; + }; + + int bestCnt = -1; + double bestTDist = 1e12; + cv::Point2f bestT = t0; + for (int a = 0; a < nRow; a++) + { + const cv::Point2f &tp = tplCenters[rowIdx[a]]; + const double lx = l00 * tp.x + l01 * tp.y; + const double ly = l10 * tp.x + l11 * tp.y; + for (int k = 0; k < nCurSub; k++) + { + const cv::Point2f &curPt = curCenters[curIdxList[k]]; + const cv::Point2f t(static_cast(curPt.x - lx), + static_cast(curPt.y - ly)); + cv::Mat M = T0.clone(); + M.at(0, 2) = t.x; + M.at(1, 2) = t.y; + const int cnt = countOverlap(M); + const double tDist = cv::norm(t - t0); + // 重合数最多; 并列时取离初值平移更近的(避免跨排/跨列错位) + if (cnt > bestCnt || (cnt == bestCnt && tDist < bestTDist)) + { + bestCnt = cnt; + bestT = t; + bestTDist = tDist; + } + } + } + + cv::Mat MCur = T0.clone(); + MCur.at(0, 2) = bestT.x; + MCur.at(1, 2) = bestT.y; + + // 5.2) 最近邻一对一匹配 + RANSAC 相似变换迭代求精, 取重合数最多的解 + auto matchPins = [&](const cv::Mat &M, std::vector> &matches) -> int + { + std::vector>> cands; + cands.reserve(static_cast(nRow) * static_cast(nCurSub)); + for (int a = 0; a < nRow; a++) + { + cv::Point2f p = ApplyAffine2D(M, tplCenters[rowIdx[a]]); + for (int k = 0; k < nCurSub; k++) + { + const int j = curIdxList[k]; + const double d = cv::norm(curCenters[j] - p); + if (d <= fit.radius) + { + cands.push_back(std::make_pair(d, std::make_pair(a, j))); + } + } + } + std::sort(cands.begin(), cands.end()); + std::vector usedT(nRow, 0); + std::vector usedC(nCur, 0); + matches.clear(); + for (size_t k = 0; k < cands.size(); k++) + { + const int a = cands[k].second.first; + const int j = cands[k].second.second; + if (usedT[a] || usedC[j]) + { + continue; + } + usedT[a] = 1; + usedC[j] = 1; + matches.push_back(cands[k].second); + } + return static_cast(matches.size()); + }; + + std::vector> matches; + int bestOverlap = matchPins(MCur, matches); + cv::Mat bestM = MCur.clone(); + for (int iter = 0; iter < 5 && bestOverlap < nRow; iter++) + { + if (static_cast(matches.size()) < 2) + { + break; + } + std::vector srcPts; + std::vector dstPts; + srcPts.reserve(matches.size()); + dstPts.reserve(matches.size()); + for (size_t k = 0; k < matches.size(); k++) + { + srcPts.push_back(tplCenters[rowIdx[matches[k].first]]); + dstPts.push_back(curCenters[matches[k].second]); + } + // 一排 Pin 近似共线, 用 4 自由度相似变换(旋转+等比缩放+平移)避免全仿射病态 + cv::Mat MNew = cv::estimateAffinePartial2D(srcPts, dstPts, cv::noArray(), cv::RANSAC, 3.0, 2000, 0.995, 10); + if (!bReasonableMat(MNew)) + { + break; + } + const int cnt = matchPins(MNew, matches); + if (cnt > bestOverlap) + { + bestOverlap = cnt; + bestM = MNew.clone(); + } + MCur = MNew; + } + + if (bestOverlap >= 2 && bReasonableMat(bestM)) + { + fit.M = bestM; + fit.nMatch = bestOverlap; + fit.bFromPins = true; + } + }; + + std::vector rowFits(rows.size()); + if (rows.size() == 2 && curRows.size() == 2) + { + // 两侧都成功分排: 每排分别与两个当前排各拟合一次, 再取总重合数更大的配对(兼容上下颠倒) + PinRowFit fits[2][2]; + for (int a = 0; a < 2; a++) + { + for (int b = 0; b < 2; b++) + { + fitOneRow(rows[a], curRows[b], fits[a][b]); + } + } + const int scoreDirect = fits[0][0].nMatch + fits[1][1].nMatch; + const int scoreSwap = fits[0][1].nMatch + fits[1][0].nMatch; + if (scoreSwap > scoreDirect) + { + rowFits[0] = fits[0][1]; + rowFits[1] = fits[1][0]; + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Pin Matrix", + "row pairing swapped (direct %d vs swap %d)", scoreDirect, scoreSwap); + } + else + { + rowFits[0] = fits[0][0]; + rowFits[1] = fits[1][1]; + } + } + else + { + // 无法两侧都分排: 每排与全部当前 Pin 匹配 + for (size_t r = 0; r < rows.size(); r++) + { + fitOneRow(rows[r], allCurIdx, rowFits[r]); + } + } + + for (size_t r = 0; r < rowFits.size(); r++) + { + if (!rowFits[r].bFromPins) + { + // 该排可匹配 Pin 不足(无法拟合), 退回内/外定位仿射投影, 保证缺失 Pin 的 roi 仍能落位 + rowFits[r].M = T0.clone(); + rowFits[r].nMatch = 0; + } + } + + // 6、按每排矩阵把模板 Pin 轮廓投影到 detImg 坐标系 + std::vector rowOfPin(nTpl, -1); + for (size_t r = 0; r < rows.size(); r++) + { + for (size_t k = 0; k < rows[r].size(); k++) + { + rowOfPin[rows[r][k]] = static_cast(r); + } + } + + m_tplPinContoursCur.resize(nTpl); + for (int i = 0; i < nTpl; i++) + { + const cv::Mat &M = (rowOfPin[i] >= 0) ? rowFits[rowOfPin[i]].M : T0; + std::vector mapped; + mapped.reserve(m_tplPinContours[i].size()); + for (size_t k = 0; k < m_tplPinContours[i].size(); k++) + { + const cv::Point2f srcPt(static_cast(m_tplPinContours[i][k].x + tplBound.x), + static_cast(m_tplPinContours[i][k].y + tplBound.y)); + const cv::Point2f dstPt = ApplyAffine2D(M, srcPt); + mapped.emplace_back(cvRound(dstPt.x), cvRound(dstPt.y)); + } + m_tplPinContoursCur[i] = std::move(mapped); + } + + // 7、日志: 每排的矩阵(旋转/缩放/平移)与重合数 + for (size_t r = 0; r < rows.size(); r++) + { + const cv::Mat &M = rowFits[r].M; + const double scale = std::sqrt(M.at(0, 0) * M.at(0, 0) + + M.at(1, 0) * M.at(1, 0)); + const double angle = std::atan2(M.at(1, 0), M.at(0, 0)) * 180.0 / CV_PI; + m_pdetlog->AddCheckstr(PrintLevel_0, "Pin Matrix", + "row %d pins %d matched %d/%d scale %.4f angle %.2f deg t(%.1f %.1f) px %s", + static_cast(r), static_cast(rows[r].size()), + rowFits[r].nMatch, static_cast(rows[r].size()), scale, angle, + M.at(0, 2), M.at(1, 2), + rowFits[r].bFromPins ? "" : "(fallback to align affine)"); + } + m_pdetlog->AddCheckstr(PrintLevel_0, "Pin Matrix", + "tpl %d cur %d rows %d match radius %.1f px (pin_x_offset %.2f mm scale_x %.4f)", + nTpl, nCur, static_cast(rows.size()), rowFits.empty() ? matchRadius : rowFits[0].radius, + m_pbaseCheckFunction != NULL ? m_pbaseCheckFunction->pinDet.pin_x_offset : 0.0f, + m_fImgage_Scale_X); + return 0; } int ImgCheckAnalysisy::Pin_Qx_Det(const cv::Mat &img) @@ -2330,6 +2970,13 @@ int ImgCheckAnalysisy::Pin_Qx_Det(const cv::Mat &img) else // 根据模板Pin和当前Pin轮廓进行对比,判断当前有无缺失和偏移 { + if (IsPinMatrixMethod()) + { + // Det_Method == "Pin_Matrix": 投影矩阵由当前帧 Pin 拟合(上下两排分别计算), + // 把模板 Pin 投影到当前检测图坐标系; 后续比对与 BTB_Marix 完全一致 + ComputePinProjectionMats(); + } + // 计算轮廓中心点(外接矩形中心) auto getCenter = [](const std::vector &contour) -> cv::Point2f { @@ -2362,6 +3009,8 @@ int ImgCheckAnalysisy::Pin_Qx_Det(const cv::Mat &img) std::vector curUsed(nCur, false); // 整体偏移估计与消除(阈值见文件顶部 PIN_SHIFT_* 常量): + // Pin_Matrix 方案的投影矩阵已由 Pin 自身拟合(上下两排分别计算), 不需要二次偏移矫正 + if (!IsPinMatrixMethod()) { // inner 定位 ROI 转到 detImg 坐标系(detImg 由 m_outer_roi 裁剪得到) cv::RotatedRect innerRectDet = m_inner_rroi; diff --git a/ConfigModule/include/CheckConfigDefine.h b/ConfigModule/include/CheckConfigDefine.h index 7002cdc..94dbacf 100644 --- a/ConfigModule/include/CheckConfigDefine.h +++ b/ConfigModule/include/CheckConfigDefine.h @@ -1249,6 +1249,7 @@ struct Base_Function_Pin_Det float pin_y_offset; // pin y 偏移 // "line_fitting" = 新方案(直接分析检测Pin: 分两排拟合直线判偏移 + 相邻间距判缺失, 不做模板对比) // "BTB_Marix" = 旧方案(内定位仿射 + 模板Pin对比) + // "Pin_Matrix" = 模板Pin对比方案, 投影矩阵由 Pin 自身拟合(上下两排分别计算, 不做二次偏移矫正) std::string Det_Method; // pin 检测方案 bool bDrawResult; // 是否绘制结果