From 3377bfd34f7d44759ce235cae355e84cb5a7b29c Mon Sep 17 00:00:00 2001 From: liusiyang Date: Mon, 13 Jul 2026 10:11:31 +0800 Subject: [PATCH] =?UTF-8?q?update=20=E5=B4=A9=E8=BE=B9=E6=A3=80=E6=B5=8B?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E6=9B=B2=E7=BA=BF=E6=8B=9F=E5=90=88=E8=BE=B9?= =?UTF-8?q?=E7=BC=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AlgorithmModule/include/Edge_QX_Det.h | 2 + AlgorithmModule/src/Edge_QX_Det.cpp | 321 +++++++++++++++++++++++--- 2 files changed, 285 insertions(+), 38 deletions(-) diff --git a/AlgorithmModule/include/Edge_QX_Det.h b/AlgorithmModule/include/Edge_QX_Det.h index 907f04e..15cadcd 100644 --- a/AlgorithmModule/include/Edge_QX_Det.h +++ b/AlgorithmModule/include/Edge_QX_Det.h @@ -173,6 +173,7 @@ public: std::vector Det_region; bool bSaveResultImg; + std::shared_ptr detlog; DetConfigResult() { Init(); @@ -186,6 +187,7 @@ public: alginResult.Init(); Det_region.clear(); bSaveResultImg = false; + detlog = nullptr; } }; diff --git a/AlgorithmModule/src/Edge_QX_Det.cpp b/AlgorithmModule/src/Edge_QX_Det.cpp index ca1f0a2..db31521 100644 --- a/AlgorithmModule/src/Edge_QX_Det.cpp +++ b/AlgorithmModule/src/Edge_QX_Det.cpp @@ -31,7 +31,9 @@ float pointToLineDistance(const cv::Point &pt, const cv::Vec4f &line) float x = pt.x, y = pt.y; return std::abs(vy * x - vx * y + (vx * y0 - vy * x0)) / std::sqrt(vx * vx + vy * vy); } -bool FitLineWithOutlierRemoval(const std::vector &inputPoints, cv::Vec4f &outputLine, int ransacIters = 100, float inlierThresh = 10) +bool FitLineWithOutlierRemoval(const std::vector &inputPoints, cv::Vec4f &outputLine, + int ransacIters = 100, float inlierThresh = 10, + std::vector *outInliers = nullptr) { if (inputPoints.size() < 2) return false; @@ -80,10 +82,62 @@ bool FitLineWithOutlierRemoval(const std::vector &inputPoints, cv::Ve if (bestInlierPoints.size() < 2) return false; + // 输出 inlier 点集,供后续曲线拟合使用 + if (outInliers) + *outInliers = bestInlierPoints; + cv::fitLine(bestInlierPoints, outputLine, cv::DIST_L2, 0, 0.01, 0.01); return true; } +// 二次曲线拟合(最小二乘法) +// isHorizontal=true: 拟合 y = a*x² + b*x + c,coeffs = [a, b, c] +// isHorizontal=false: 拟合 x = a*y² + b*y + c,coeffs = [a, b, c] +bool FitQuadraticCurve(const std::vector &points, cv::Vec3f &coeffs, bool isHorizontal) +{ + if (points.size() < 3) + return false; + + int n = (int)points.size(); + cv::Mat A(n, 3, CV_64F); + cv::Mat B(n, 1, CV_64F); + + for (int i = 0; i < n; i++) + { + double t = isHorizontal ? points[i].x : points[i].y; + double v = isHorizontal ? points[i].y : points[i].x; + A.at(i, 0) = t * t; + A.at(i, 1) = t; + A.at(i, 2) = 1.0; + B.at(i, 0) = v; + } + + cv::Mat X; + if (!cv::solve(A, B, X, cv::DECOMP_SVD)) + return false; + + coeffs[0] = (float)X.at(0, 0); + coeffs[1] = (float)X.at(1, 0); + coeffs[2] = (float)X.at(2, 0); + return true; +} + +// 将点投影到二次曲线上(垂直投影:保持 x 不变求 y,或保持 y 不变求 x) +cv::Point ProjectPointToCurve(const cv::Point &pt, const cv::Vec3f &coeffs, bool isHorizontal) +{ + float a = coeffs[0], b = coeffs[1], c = coeffs[2]; + if (isHorizontal) + { + float y = a * pt.x * pt.x + b * pt.x + c; + return cv::Point(pt.x, cvRound(y)); + } + else + { + float x = a * pt.y * pt.y + b * pt.y + c; + return cv::Point(cvRound(x), pt.y); + } +} + void drawFittedLine(cv::Mat &image, const cv::Vec4f &line, const cv::Scalar &color, int thickness = 2) { double scale = std::max(image.cols, image.rows) * 2.0; @@ -312,8 +366,130 @@ int Edge_QX_Det::Detect(const cv::Mat &img, DetConfigResult *pDetConfig) else return 1; + // 用RANSAC剔除崩边凹陷的异常点后,拟合二次曲线使ROI贴合产品边缘的自然弧度 + // 水平边缘(上下): 拟合 y = a*x² + b*x + c + // 垂直边缘(左右): 拟合 x = a*y² + b*y + c + // 若曲线拟合失败(点数不足), 则回退到直线投影 + + // 上边缘:RANSAC获取inlier → 二次曲线拟合 → 投影到曲线 + { + cv::Vec4f fittedLine_up; + std::vector inliers_up; + cv::Vec3f curveCoeffs_up; + bool useCurve = false; + if (FitLineWithOutlierRemoval(up_edge, fittedLine_up, 100, 15, &inliers_up)) + { + useCurve = FitQuadraticCurve(inliers_up, curveCoeffs_up, true); + } + for (auto& line : Up_line) + { + if (useCurve) + { + line.p1 = ProjectPointToCurve(line.p1, curveCoeffs_up, true); + line.p2 = ProjectPointToCurve(line.p2, curveCoeffs_up, true); + } + else + { + float vx = fittedLine_up[0], vy = fittedLine_up[1], x0 = fittedLine_up[2], y0 = fittedLine_up[3]; + float denom = vx * vx + vy * vy; + float t1 = ((line.p1.x - x0) * vx + (line.p1.y - y0) * vy) / denom; + float t2 = ((line.p2.x - x0) * vx + (line.p2.y - y0) * vy) / denom; + line.p1 = cv::Point(cvRound(x0 + t1 * vx), cvRound(y0 + t1 * vy)); + line.p2 = cv::Point(cvRound(x0 + t2 * vx), cvRound(y0 + t2 * vy)); + } + } + } + + // 下边缘 + { + cv::Vec4f fittedLine_down; + std::vector inliers_down; + cv::Vec3f curveCoeffs_down; + bool useCurve = false; + if (FitLineWithOutlierRemoval(down_edge, fittedLine_down, 100, 15, &inliers_down)) + { + useCurve = FitQuadraticCurve(inliers_down, curveCoeffs_down, true); + } + for (auto& line : down_line) + { + if (useCurve) + { + line.p1 = ProjectPointToCurve(line.p1, curveCoeffs_down, true); + line.p2 = ProjectPointToCurve(line.p2, curveCoeffs_down, true); + } + else + { + float vx = fittedLine_down[0], vy = fittedLine_down[1], x0 = fittedLine_down[2], y0 = fittedLine_down[3]; + float denom = vx * vx + vy * vy; + float t1 = ((line.p1.x - x0) * vx + (line.p1.y - y0) * vy) / denom; + float t2 = ((line.p2.x - x0) * vx + (line.p2.y - y0) * vy) / denom; + line.p1 = cv::Point(cvRound(x0 + t1 * vx), cvRound(y0 + t1 * vy)); + line.p2 = cv::Point(cvRound(x0 + t2 * vx), cvRound(y0 + t2 * vy)); + } + } + } + + // 左边缘 + { + cv::Vec4f fittedLine_left; + std::vector inliers_left; + cv::Vec3f curveCoeffs_left; + bool useCurve = false; + if (FitLineWithOutlierRemoval(left_edge, fittedLine_left, 100, 15, &inliers_left)) + { + useCurve = FitQuadraticCurve(inliers_left, curveCoeffs_left, false); + } + for (auto& line : left_line) + { + if (useCurve) + { + line.p1 = ProjectPointToCurve(line.p1, curveCoeffs_left, false); + line.p2 = ProjectPointToCurve(line.p2, curveCoeffs_left, false); + } + else + { + float vx = fittedLine_left[0], vy = fittedLine_left[1], x0 = fittedLine_left[2], y0 = fittedLine_left[3]; + float denom = vx * vx + vy * vy; + float t1 = ((line.p1.x - x0) * vx + (line.p1.y - y0) * vy) / denom; + float t2 = ((line.p2.x - x0) * vx + (line.p2.y - y0) * vy) / denom; + line.p1 = cv::Point(cvRound(x0 + t1 * vx), cvRound(y0 + t1 * vy)); + line.p2 = cv::Point(cvRound(x0 + t2 * vx), cvRound(y0 + t2 * vy)); + } + } + } + + // 右边缘 + { + cv::Vec4f fittedLine_right; + std::vector inliers_right; + cv::Vec3f curveCoeffs_right; + bool useCurve = false; + if (FitLineWithOutlierRemoval(right_edge, fittedLine_right, 100, 15, &inliers_right)) + { + useCurve = FitQuadraticCurve(inliers_right, curveCoeffs_right, false); + } + for (auto& line : right_line) + { + if (useCurve) + { + line.p1 = ProjectPointToCurve(line.p1, curveCoeffs_right, false); + line.p2 = ProjectPointToCurve(line.p2, curveCoeffs_right, false); + } + else + { + float vx = fittedLine_right[0], vy = fittedLine_right[1], x0 = fittedLine_right[2], y0 = fittedLine_right[3]; + float denom = vx * vx + vy * vy; + float t1 = ((line.p1.x - x0) * vx + (line.p1.y - y0) * vy) / denom; + float t2 = ((line.p2.x - x0) * vx + (line.p2.y - y0) * vy) / denom; + line.p1 = cv::Point(cvRound(x0 + t1 * vx), cvRound(y0 + t1 * vy)); + line.p2 = cv::Point(cvRound(x0 + t2 * vx), cvRound(y0 + t2 * vy)); + } + } + } + // 生成 检测的 roi。 int roi_wh = pDetConfig->pBaseCheckFunction->edgeDet.Det_Range; + cv::Rect imgBounds(0, 0, img.cols, img.rows); // 图像边界,用于裁剪越界 ROI std::vector up_det_roi; std::vector down_det_roi; @@ -327,7 +503,8 @@ int Edge_QX_Det::Detect(const cv::Mat &img, DetConfigResult *pDetConfig) tem.plist.push_back(cv::Point(line.p2.x, line.p2.y + roi_wh)); tem.plist.push_back(cv::Point(line.p1.x, line.p1.y + roi_wh)); - tem.roi = cv::boundingRect(tem.plist); + tem.roi = cv::boundingRect(tem.plist) & imgBounds; + if (tem.roi.width <= 0 || tem.roi.height <= 0) continue; pDetConfig->edge_det_roi.push_back(tem); up_det_roi.push_back(tem); } @@ -339,7 +516,8 @@ int Edge_QX_Det::Detect(const cv::Mat &img, DetConfigResult *pDetConfig) tem.plist.push_back(cv::Point(line.p2.x, line.p2.y - roi_wh)); tem.plist.push_back(cv::Point(line.p1.x, line.p1.y - roi_wh)); - tem.roi = cv::boundingRect(tem.plist); + tem.roi = cv::boundingRect(tem.plist) & imgBounds; + if (tem.roi.width <= 0 || tem.roi.height <= 0) continue; pDetConfig->edge_det_roi.push_back(tem); down_det_roi.push_back(tem); } @@ -351,7 +529,8 @@ int Edge_QX_Det::Detect(const cv::Mat &img, DetConfigResult *pDetConfig) tem.plist.push_back(cv::Point(line.p2.x + roi_wh, line.p2.y)); tem.plist.push_back(cv::Point(line.p1.x + roi_wh, line.p1.y)); - tem.roi = cv::boundingRect(tem.plist); + tem.roi = cv::boundingRect(tem.plist) & imgBounds; + if (tem.roi.width <= 0 || tem.roi.height <= 0) continue; pDetConfig->edge_det_roi.push_back(tem); left_det_roi.push_back(tem); } @@ -363,7 +542,8 @@ int Edge_QX_Det::Detect(const cv::Mat &img, DetConfigResult *pDetConfig) tem.plist.push_back(cv::Point(line.p2.x - roi_wh, line.p2.y)); tem.plist.push_back(cv::Point(line.p1.x - roi_wh, line.p1.y)); - tem.roi = cv::boundingRect(tem.plist); + tem.roi = cv::boundingRect(tem.plist) & imgBounds; + if (tem.roi.width <= 0 || tem.roi.height <= 0) continue; pDetConfig->edge_det_roi.push_back(tem); right_det_roi.push_back(tem); } @@ -563,11 +743,6 @@ int Edge_QX_Det::GetEdgePoint(const cv::Mat &img, Edge_Search_Config *pEdge_Sear // 对每个搜索点进行 y方向搜索 for (int y = sy;; y = y + nCurPoint_Step) { - if (y < 0 || y >= img.rows) - { - continue; - } - // if (nCurPoint_Step > 0 && y > ey) { @@ -577,7 +752,10 @@ int Edge_QX_Det::GetEdgePoint(const cv::Mat &img, Edge_Search_Config *pEdge_Sear { break; } - offt = y * img.cols; + if (y < 0 || y >= img.rows) + { + continue; + } int range_okNum = 0; // 对一定范围的点进行判断 for (int k = nrange_start; k < nrange_end; k++) @@ -587,13 +765,13 @@ int Edge_QX_Det::GetEdgePoint(const cv::Mat &img, Edge_Search_Config *pEdge_Sear { continue; } - offt += rangeX; - if (offt < 0 || offt >= img.cols * img.rows) + int cur_offt = y * img.cols + rangeX; + if (cur_offt < 0 || cur_offt >= img.cols * img.rows) { - printf("rangeX %d nCurPoint_Step %d off %d x %d y %d sy %d ey %d %d %d\n", rangeX, nCurPoint_Step, offt, x, y, sy, ey, img.cols, img.rows); + continue; } - if (pdata[offt] >= pEdge_Search_Config->nValueThreshold) // 找到 + if (pdata[cur_offt] >= pEdge_Search_Config->nValueThreshold) // 找到 { range_okNum++; } @@ -648,13 +826,9 @@ int Edge_QX_Det::GetEdgePoint(const cv::Mat &img, Edge_Search_Config *pEdge_Sear int cur_y = y; bool bSucc = false; - // 对每个搜索点进行 y方向搜索 + // 对每个搜索点进行 x方向搜索 for (int x = sx;; x = x + nCurPoint_Step) { - if (x < 0 || x >= img.cols) - { - continue; - } // if (nCurPoint_Step > 0 && x > ex) { @@ -664,6 +838,10 @@ int Edge_QX_Det::GetEdgePoint(const cv::Mat &img, Edge_Search_Config *pEdge_Sear { break; } + if (x < 0 || x >= img.cols) + { + continue; + } int range_okNum = 0; // 对一定范围的点进行判断 @@ -674,12 +852,12 @@ int Edge_QX_Det::GetEdgePoint(const cv::Mat &img, Edge_Search_Config *pEdge_Sear { continue; } - offt = rangey * img.cols + x; - if (offt < 0 || offt >= img.cols * img.rows) + int cur_offt = rangey * img.cols + x; + if (cur_offt < 0 || cur_offt >= img.cols * img.rows) { - printf("rangey %d off %d x %d y %d ey %d %d %d\n", rangey, offt, x, y, ey, img.cols, img.rows); + continue; } - if (pdata[offt] >= pEdge_Search_Config->nValueThreshold) // 找到 + if (pdata[cur_offt] >= pEdge_Search_Config->nValueThreshold) // 找到 { range_okNum++; } @@ -933,12 +1111,14 @@ int Edge_QX_Det::Det_qx(const cv::Mat &img, std::vector roilist, // 进行开操作(先腐蚀后膨胀)可以去除小白点 cv::morphologyEx(roiMask, roiMask, cv::MORPH_OPEN, kernel); + // getchar(); // cv::imwrite("detimg.png", img(DetRoi)); // cv::imwrite("detimg_mask.png", roiMask); - // getchar(); - + bool jiao_f_1 = false; bool jiao_f_2 = false; + string jiao_str_1; + string jiao_str_2; cv::Point jiao_p_1; cv::Point jiao_p_2; if (type == Det_ROI_Type_UP) @@ -947,6 +1127,8 @@ int Edge_QX_Det::Det_qx(const cv::Mat &img, std::vector roilist, jiao_f_2 = pDetConfig->pBaseCheckFunction->edgeDet.queJiao_RU_Open; jiao_p_1 = cv::Point(0, 0); jiao_p_2 = cv::Point(roiMask.cols, 0); + jiao_str_1 = "queJiao_LU"; + jiao_str_2 = "queJiao_RU"; } else if (type == Det_ROI_Type_DOWN) { @@ -954,49 +1136,105 @@ int Edge_QX_Det::Det_qx(const cv::Mat &img, std::vector roilist, jiao_f_2 = pDetConfig->pBaseCheckFunction->edgeDet.queJiao_RD_Open; jiao_p_1 = cv::Point(0, roiMask.rows); jiao_p_2 = cv::Point(roiMask.cols, roiMask.rows); + jiao_str_1 = "queJiao_LD"; + jiao_str_2 = "queJiao_RD"; } else if (type == Det_ROI_Type_LEFT) { jiao_f_1 = pDetConfig->pBaseCheckFunction->edgeDet.queJiao_LU_Open; jiao_f_2 = pDetConfig->pBaseCheckFunction->edgeDet.queJiao_LD_Open; - jiao_p_1 = cv::Point(0, 0); jiao_p_2 = cv::Point(0, roiMask.rows); + jiao_str_1 = "queJiao_LU"; + jiao_str_2 = "queJiao_LD"; } else if (type == Det_ROI_Type_RIGHT) { jiao_f_1 = pDetConfig->pBaseCheckFunction->edgeDet.queJiao_RU_Open; - jiao_f_2 = pDetConfig->pBaseCheckFunction->edgeDet.queJiao_RU_Open; + jiao_f_2 = pDetConfig->pBaseCheckFunction->edgeDet.queJiao_RD_Open; jiao_p_1 = cv::Point(roiMask.cols, 0); jiao_p_2 = cv::Point(roiMask.cols, roiMask.rows); + jiao_str_1 = "queJiao_RU"; + jiao_str_2 = "queJiao_RD"; } // 寻找轮廓 vector> contours; cv::findContours(roiMask, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE); - // 找到最大面积的轮廓 - double maxArea = -1; - int maxAreaIdx = -1; + // 缺角过滤配置日志 + if (pDetConfig->detlog) + { + pDetConfig->detlog->AddCheckstr(PrintLevel_1, "EdgeQX_Det", "type=%d contours=%zu %s=%d %s=%d thres(w=%d,h=%d)", + type, contours.size(), + jiao_str_1.c_str(), jiao_f_1, + jiao_str_2.c_str(), jiao_f_2, + pDetConfig->pBaseCheckFunction->edgeDet.queJiao_width, + pDetConfig->pBaseCheckFunction->edgeDet.queJiao_height); + } + + int jiao_filter_count = 0; for (size_t i = 0; i < contours.size(); ++i) { cv::Rect rect = cv::boundingRect(contours[i]); cv::Point pc(rect.x + rect.width / 2, rect.y + rect.height / 2); if (jiao_f_1) { - // printf("1=%d =========== %d %d\n\n", type, abs(pc.x - jiao_p_1.x), abs(pc.y - jiao_p_1.y)); - if (abs(pc.x - jiao_p_1.x) < pDetConfig->pBaseCheckFunction->edgeDet.queJiao_width && abs(pc.y - jiao_p_1.y) < pDetConfig->pBaseCheckFunction->edgeDet.queJiao_height) + int dx1 = abs(pc.x - jiao_p_1.x); + int dy1 = abs(pc.y - jiao_p_1.y); + if (dx1 < pDetConfig->pBaseCheckFunction->edgeDet.queJiao_width && dy1 < pDetConfig->pBaseCheckFunction->edgeDet.queJiao_height) { + if (pDetConfig->detlog) + { + pDetConfig->detlog->AddCheckstr(PrintLevel_2, "EdgeQX_Det", "type=%d %s contour[%zu] center=(%d,%d) coor=(%d,%d) dx=%d dy=%d thres(w=%d,h=%d) --> succ", + type, jiao_str_1.c_str(), i, pc.x, pc.y, jiao_p_1.x, jiao_p_1.y, + dx1, dy1, + pDetConfig->pBaseCheckFunction->edgeDet.queJiao_width, + pDetConfig->pBaseCheckFunction->edgeDet.queJiao_height); + } + jiao_filter_count++; continue; } + else + { + if (pDetConfig->detlog) + { + pDetConfig->detlog->AddCheckstr(PrintLevel_2, "EdgeQX_Det", "type=%d %s contour[%zu] center=(%d,%d) coor=(%d,%d) dx=%d dy=%d thres(w=%d,h=%d) --> fail", + type, jiao_str_1.c_str(), i, pc.x, pc.y, jiao_p_1.x, jiao_p_1.y, + dx1, dy1, + pDetConfig->pBaseCheckFunction->edgeDet.queJiao_width, + pDetConfig->pBaseCheckFunction->edgeDet.queJiao_height); + } + } } if (jiao_f_2) { - // printf("2=%d============ %d %d\n\n", type, abs(pc.x - jiao_p_2.x), abs(pc.y - jiao_p_2.y)); - if (abs(pc.x - jiao_p_2.x) < pDetConfig->pBaseCheckFunction->edgeDet.queJiao_width && abs(pc.y - jiao_p_2.y) < pDetConfig->pBaseCheckFunction->edgeDet.queJiao_height) + int dx2 = abs(pc.x - jiao_p_2.x); + int dy2 = abs(pc.y - jiao_p_2.y); + if (dx2 < pDetConfig->pBaseCheckFunction->edgeDet.queJiao_width && dy2 < pDetConfig->pBaseCheckFunction->edgeDet.queJiao_height) { + if (pDetConfig->detlog) + { + pDetConfig->detlog->AddCheckstr(PrintLevel_2, "EdgeQX_Det", "type=%d %s contour[%zu] center=(%d,%d) coor=(%d,%d) dx=%d dy=%d thres(w=%d,h=%d) --> succ", + type, jiao_str_2.c_str(), i, pc.x, pc.y, jiao_p_2.x, jiao_p_2.y, + dx2, dy2, + pDetConfig->pBaseCheckFunction->edgeDet.queJiao_width, + pDetConfig->pBaseCheckFunction->edgeDet.queJiao_height); + } + jiao_filter_count++; continue; } + else + { + if (pDetConfig->detlog) + { + pDetConfig->detlog->AddCheckstr(PrintLevel_2, "EdgeQX_Det", "type=%d %s contour[%zu] center=(%d,%d) coor=(%d,%d) dx=%d dy=%d thres(w=%d,h=%d) --> fail", + type, jiao_str_2.c_str(), i, pc.x, pc.y, jiao_p_2.x, jiao_p_2.y, + dx2, dy2, + pDetConfig->pBaseCheckFunction->edgeDet.queJiao_width, + pDetConfig->pBaseCheckFunction->edgeDet.queJiao_height); + } + } } if (rect.width >= pedgeDet->QX_Widht_min && rect.width <= pedgeDet->QX_Widht_max && @@ -1021,18 +1259,25 @@ int Edge_QX_Det::Det_qx(const cv::Mat &img, std::vector roilist, int Edge_QX_Det::applyMaskInROI(const cv::Mat &grayImg, const Det_ROI_Config &config, cv::Mat &result, int threshold) { + // 0. 裁剪 ROI,防止 RANSAC 投影后越界 + cv::Rect safeRoi = config.roi & cv::Rect(0, 0, grayImg.cols, grayImg.rows); + if (safeRoi.width <= 0 || safeRoi.height <= 0) + { + result = cv::Mat(); + return -1; + } // 1. 获取 ROI 区域图像(不 clone,只引用) - cv::Mat roiGray = grayImg(config.roi); + cv::Mat roiGray = grayImg(safeRoi); // cv::imwrite("roiGray.png", roiGray); // 2. 二值化(用 compare 更快) cv::Mat binary; cv::compare(roiGray, threshold, binary, cv::CMP_LT); // binary = roiGray > 128 ? 255 : 0 // cv::imwrite("binary.png", binary); - // 3. 构建局部坐标的多边形(避免每次 new) + // 3. 构建局部坐标的多边形(相对于裁剪后的 safeRoi) std::vector localPts; localPts.reserve(config.plist.size()); for (const auto &pt : config.plist) - localPts.emplace_back(pt.x - config.roi.x, pt.y - config.roi.y); + localPts.emplace_back(pt.x - safeRoi.x, pt.y - safeRoi.y); // 4. 快速创建 mask 并填充 cv::Mat mask = cv::Mat::zeros(roiGray.size(), CV_8UC1);