diff --git a/AlgorithmModule/include/CameraCheckAnalysisy.hpp b/AlgorithmModule/include/CameraCheckAnalysisy.hpp index f566b4a..b4b1b8b 100644 --- a/AlgorithmModule/include/CameraCheckAnalysisy.hpp +++ b/AlgorithmModule/include/CameraCheckAnalysisy.hpp @@ -191,6 +191,10 @@ private: // 找到产品(亮色区域)所在的第一行(顶部边缘), 用于左右图上下对齐 int GetProductFirstRow(const cv::Mat &img, int &outRow); + // 拟合产品顶边直线段: y = outK * x + outB, outP1/outP2 为线段两端点(图像坐标系) + // 返回 0 成功; 非 0 失败(未找到顶边或有效点太少) + int FitProductTopLine(const cv::Mat &img, float &outK, float &outB, cv::Point2f &outP1, cv::Point2f &outP2); + public: // 运行的基本参数 RunInfoST m_RunConfig; diff --git a/AlgorithmModule/src/CameraCheckAnalysisy.cpp b/AlgorithmModule/src/CameraCheckAnalysisy.cpp index 5939a9a..a624889 100644 --- a/AlgorithmModule/src/CameraCheckAnalysisy.cpp +++ b/AlgorithmModule/src/CameraCheckAnalysisy.cpp @@ -10,6 +10,7 @@ #include "CheckUtil.hpp" #include "Define.h" #include +#include CameraCheckAnalysisy::CameraCheckAnalysisy() { @@ -168,7 +169,7 @@ int CameraCheckAnalysisy::Detect_Pre() // pImageResult->result->in_shareImage->strCameraName, pImageResult->result->in_shareImage->strChannel); // 简单硬拼接:img 在左,img_B 在右,拼接结果写回 img - // 水平方向有overlap,拼接时固定去掉左图最右边overlap区域 + // 先去掉左图最右边overlap区域,再做首行判断与上下对齐,最后水平拼接 if (pImageResult->result->in_shareImage->img.size() != pImageResult->result->in_shareImage->img_B.size() || pImageResult->result->in_shareImage->img.type() != pImageResult->result->in_shareImage->img_B.type()) { @@ -178,50 +179,79 @@ int CameraCheckAnalysisy::Detect_Pre() cv::Mat &img = pImageResult->result->in_shareImage->img; cv::Mat &img_B = pImageResult->result->in_shareImage->img_B; - // 左右图存在上下错位: 找到左右图产品所在的第一行, 进行简单上下对齐 + // 1、先裁剪左图: 固定去掉左图最右侧与右图重叠的 overlap 区域 + int overlap = cvRound(m_pbaseCheckFunction->markLine.hoverlap); + if (img.cols <= overlap) { - int rowLeft = -1; - int rowRight = -1; - if (GetProductFirstRow(img, rowLeft) == 0 && GetProductFirstRow(img_B, rowRight) == 0) + m_pdetlog->AddCheckstr(PrintLevel_1, "Detect_Pre", "Cam %s ==================img.cols <= overlap %d ", strBasic.c_str(), overlap); + return 1; + } + img = img(cv::Rect(0, 0, img.cols - overlap, img.rows)); + + // 拼接缝: 左图右边界在拼接图中的 x + const int seamX = img.cols; + + // 2、裁剪后做顶边对齐: 分别拟合左右图产品顶边直线, 用直线在拼接缝处的行坐标做上下平移。 + // 只用单一坐标值(首行)相减误差大, 产品倾斜时会拼接不准; 用拟合出的直线取值更稳定。 + // 这里只做上下平移, 不旋转图像。 + bool bFitOK = false; // 顶边拟合是否成功 + cv::Point2f pL1, pL2; // 左图顶边线段(端点) + cv::Point2f pR1, pR2; // 右图顶边线段(端点) + int dyAlign = 0; // 右图的上下平移量(>0 向下, <0 向上) + { + float kL = 0.0f, bL = 0.0f; // 左图顶边直线: y = kL * x + bL + float kR = 0.0f, bR = 0.0f; // 右图顶边直线: y = kR * x + bR + + int reL = FitProductTopLine(img, kL, bL, pL1, pL2); + int reR = FitProductTopLine(img_B, kR, bR, pR1, pR2); + + if (reL == 0 && reR == 0) { - // 以较高的顶边为基准, 把较低的那张图向上平移, 使两图顶边对齐 - int refRow = std::min(rowLeft, rowRight); - int shiftLeft = refRow - rowLeft; // <= 0, 负值表示向上平移 - int shiftRight = refRow - rowRight; // <= 0 + // 左图顶边直线在拼接缝处(x = seamX)的行坐标 = 左图线段右端点的高度 + const double seamY = kL * seamX + bL; + // 右图顶边直线在其左边界(x = 0)处的行坐标 = 右图线段左端点的高度 + const double rightY = bR; - m_pdetlog->AddCheckstr(PrintLevel_1, "Detect_Pre", "Cam %s ==================align rowLeft %d rowRight %d shiftLeft %d shiftRight %d", - strBasic.c_str(), rowLeft, rowRight, shiftLeft, shiftRight); + // 让两条线段的连接端点在拼接缝处相接: 右图上下平移 seamY - rightY 行 + dyAlign = (int)cvRound(seamY - rightY); - if (shiftLeft < 0) - { - // 内容向上平移 |shiftLeft| 行,底部补黑。 - int dy = -shiftLeft; - cv::Mat shifted = cv::Mat::zeros(img.size(), img.type()); - img(cv::Rect(0, dy, img.cols, img.rows - dy)).copyTo(shifted(cv::Rect(0, 0, img.cols, img.rows - dy))); - img = shifted; - } - if (shiftRight < 0) + m_pdetlog->AddCheckstr(PrintLevel_1, "Detect_Pre", + "Cam %s ==================topline L(k %.5f b %.1f) R(k %.5f b %.1f) seamY %.1f rightY %.1f dy %d", + strBasic.c_str(), kL, bL, kR, bR, seamY, rightY, dyAlign); + + // 上下平移右图, 空出的部分补黑 + if (dyAlign != 0 && dyAlign > -img_B.rows && dyAlign < img_B.rows) { - int dy = -shiftRight; cv::Mat shifted = cv::Mat::zeros(img_B.size(), img_B.type()); - img_B(cv::Rect(0, dy, img_B.cols, img_B.rows - dy)).copyTo(shifted(cv::Rect(0, 0, img_B.cols, img_B.rows - dy))); + if (dyAlign > 0) + { + // 内容向下平移 dyAlign 行, 顶部补黑 + img_B(cv::Rect(0, 0, img_B.cols, img_B.rows - dyAlign)) + .copyTo(shifted(cv::Rect(0, dyAlign, img_B.cols, img_B.rows - dyAlign))); + } + else + { + // 内容向上平移 |dyAlign| 行, 底部补黑 + const int d = -dyAlign; + img_B(cv::Rect(0, d, img_B.cols, img_B.rows - d)) + .copyTo(shifted(cv::Rect(0, 0, img_B.cols, img_B.rows - d))); + } img_B = shifted; } + + bFitOK = true; } else { - m_pdetlog->AddCheckstr(PrintLevel_1, "Detect_Pre", "Cam %s ==================GetProductFirstRow fail, skip align", strBasic.c_str()); + m_pdetlog->AddCheckstr(PrintLevel_1, "Detect_Pre", "Cam %s ==================FitProductTopLine fail(L %d R %d), skip align", strBasic.c_str(), reL, reR); } } - int overlap = cvRound(m_pbaseCheckFunction->markLine.hoverlap); - if (img.cols <= overlap) + // 3、上下对齐后水平拼接 { - m_pdetlog->AddCheckstr(PrintLevel_1, "Detect_Pre", "Cam %s ==================img.cols <= overlap %d ", strBasic.c_str(), overlap); - return 1; + cv::Mat merge_img; + cv::hconcat(img, img_B, merge_img); + img = merge_img; } - cv::Mat merge_img; - cv::hconcat(img(cv::Rect(0, 0, img.cols - overlap, img.rows)), img_B, merge_img); - img = merge_img; // 拼接完成后 img_B 不再需要,立即释放内存 img_B.release(); @@ -229,6 +259,45 @@ int CameraCheckAnalysisy::Detect_Pre() if (pImageResult->result->in_shareImage->Det_Mode == DET_MODE_MergeImg) { cv::imwrite(pImageResult->result->in_shareImage->strCameraName + "_MergeImg.jpg", pImageResult->result->in_shareImage->img); + + // 保存绘制了顶边线段的图(绿: 左图顶边线段, 红: 右图顶边线段, 黄/青点: 两条线段的连接端点) + if (bFitOK) + { + // 先缩放到可读尺寸再画线, 避免在超大图上做彩色转换(内存/耗时) + const cv::Mat &mergeImg = pImageResult->result->in_shareImage->img; + const int dbgW = 4096; + const double sc = std::min(1.0, (double)dbgW / mergeImg.cols); + cv::Mat small; + cv::resize(mergeImg, small, cv::Size(), sc, sc, cv::INTER_AREA); + + cv::Mat dbgLineImg; + if (small.channels() == 1) + { + cv::cvtColor(small, dbgLineImg, cv::COLOR_GRAY2BGR); + } + else + { + dbgLineImg = small.clone(); + } + + // 左图顶边线段(拼接图左侧) + const cv::Point2f a1((float)(pL1.x * sc), (float)(pL1.y * sc)); + const cv::Point2f a2((float)(pL2.x * sc), (float)(pL2.y * sc)); + // 右图顶边线段(拼接图右侧, 并叠加本次的上下平移量) + const cv::Point2f b1((float)((seamX + pR1.x) * sc), (float)((pR1.y + dyAlign) * sc)); + const cv::Point2f b2((float)((seamX + pR2.x) * sc), (float)((pR2.y + dyAlign) * sc)); + + const int thick = std::max(2, (int)cvRound(9 * sc)); + cv::line(dbgLineImg, a1, a2, cv::Scalar(0, 255, 0), thick); // 绿: 左图顶边线段 + cv::line(dbgLineImg, b1, b2, cv::Scalar(0, 0, 255), thick); // 红: 右图顶边线段 + + // 两条线段的连接端点(平移对齐后应重合在拼接缝处) + const int r = std::max(4, (int)cvRound(14 * sc)); + cv::circle(dbgLineImg, a2, r, cv::Scalar(0, 255, 255), -1); + cv::circle(dbgLineImg, b1, r, cv::Scalar(255, 255, 0), -1); + + cv::imwrite(pImageResult->result->in_shareImage->strCameraName + "_MergeImg_Line.jpg", dbgLineImg); + } return 2; } @@ -1633,3 +1702,168 @@ int CameraCheckAnalysisy::GetProductFirstRow(const cv::Mat &img, int &outRow) return 2; // 理论上不会走到这里 } + +// 拟合图像中产品(亮色区域)的顶边直线段: 对顶边上的点做最小二乘拟合, 得到 y = k * x + b +// 产品倾斜时顶边在每一列的高度不同, 用逐列顶边点拟合出的直线才能真正反映倾斜, +// 左右两张图各拟合一条线段后, 就可以让它们在拼接缝处首尾相连。 +// 返回 0 成功; 非 0 失败(未找到顶边或有效点太少) +int CameraCheckAnalysisy::FitProductTopLine(const cv::Mat &img, float &outK, float &outB, + cv::Point2f &outP1, cv::Point2f &outP2) +{ + outK = 0.0f; + outB = 0.0f; + outP1 = cv::Point2f(0.0f, 0.0f); + outP2 = cv::Point2f(0.0f, 0.0f); + + if (img.empty()) + { + return 1; + } + + cv::Mat gray; + if (img.channels() == 1) + { + gray = img; + } + else if (img.channels() == 3) + { + cv::cvtColor(img, gray, cv::COLOR_BGR2GRAY); + } + else + { + return 1; + } + + const uchar brightThreshold = 45; // 亮像素灰度阈值(与背景判断阈值保持一致) + const int needBrightCount = std::max(1, (int)(gray.cols * 0.1)); // 该行亮像素数量占比超过 20% 认为是产品所在行 + const int rowStep = 16; // 跳行扫描步长 + + // 判断某一行是否为产品所在行(亮像素数量达到阈值) + auto isProductRow = [&](int row) -> bool + { + const uchar *p = gray.ptr(row); + int brightCount = 0; + for (int col = 0; col < gray.cols; ++col) + { + if (p[col] >= brightThreshold) + { + if (++brightCount >= needBrightCount) + { + return true; + } + } + } + return false; + }; + + // 1、跳行粗扫: 定位产品顶边的大致位置 + int rowFound = -1; + for (int row = 0; row < gray.rows; row += rowStep) + { + if (isProductRow(row)) + { + rowFound = row; + break; + } + } + + if (rowFound < 0) + { + return 2; // 未找到产品顶边 + } + + // 2、逐列定位顶边点: 在粗定位行附近的范围内, 从上往下找到第一段连续亮像素 + // 产品倾斜时顶边在每列的行坐标不同, 这些点连起来才是真实的顶边线段 + const int yMargin = std::max(rowStep * 4, 60); // 纵向搜索范围(要能覆盖倾斜造成的高度差) + const int yStart = std::max(0, rowFound - yMargin); + const int yEnd = std::min(gray.rows - 1, rowFound + yMargin); + const int colStep = std::max(1, gray.cols / 200); // 采样列步长, 避免逐列遍历超大图 + const int minRunLen = 3; // 连续亮像素个数, 用于过滤噪点 + + std::vector points; + points.reserve(gray.cols / colStep + 1); + for (int col = 0; col < gray.cols; col += colStep) + { + for (int row = yStart; row <= yEnd; ++row) + { + int runLen = 0; + for (int k = 0; k < minRunLen && row + k <= yEnd; ++k) + { + if (gray.at(row + k, col) >= brightThreshold) + { + runLen++; + } + else + { + break; + } + } + if (runLen >= minRunLen) + { + points.push_back(cv::Point2f((float)col, (float)row)); + break; + } + } + } + + if (points.size() < 10) + { + return 3; // 顶边点太少, 无法拟合 + } + + // 3、迭代最小二乘拟合 y = k * x + b, 并用残差剔除离群点 + std::vector fitPts = points; + float k = 0.0f; + float b = 0.0f; + for (int iter = 0; iter < 3; ++iter) + { + const int n = (int)fitPts.size(); + if (n < 2) + { + break; + } + double sx = 0.0, sy = 0.0, sxx = 0.0, sxy = 0.0; + for (const auto &p : fitPts) + { + sx += p.x; + sy += p.y; + sxx += (double)p.x * p.x; + sxy += (double)p.x * p.y; + } + const double denom = n * sxx - sx * sx; + if (std::abs(denom) < 1e-6) + { + k = 0.0f; // 所有点几乎在同一列, 无法确定斜率 + b = (float)(sy / n); + } + else + { + k = (float)((n * sxy - sx * sy) / denom); + b = (float)((sy - k * sx) / n); + } + + // 残差剔除: 偏离拟合直线过多的点不参与下一次拟合 + std::vector inliers; + inliers.reserve(fitPts.size()); + for (const auto &p : fitPts) + { + if (std::abs(p.y - (k * p.x + b)) <= 3.0f) + { + inliers.push_back(p); + } + } + if (inliers.size() == fitPts.size() || inliers.size() < 10) + { + break; + } + fitPts.swap(inliers); + } + + outK = k; + outB = b; + // 线段端点: 取图像左右边界上的点 + outP1 = cv::Point2f(0.0f, b); + outP2 = cv::Point2f((float)(gray.cols - 1), k * (gray.cols - 1) + b); + + return 0; +} diff --git a/TcsCheckModule/src/TcsCheck.cpp b/TcsCheckModule/src/TcsCheck.cpp index 639656a..50d1276 100644 --- a/TcsCheckModule/src/TcsCheck.cpp +++ b/TcsCheckModule/src/TcsCheck.cpp @@ -204,7 +204,7 @@ cv::Mat CTcsCheck::AdaptiveBinary(cv::Mat matBlur) if (m_cpCfg.bDebugsaveImg) { std::string roi_ext = "[" + std::to_string(x) + "," + std::to_string(y) + "]"; - std::string strSaveDir = "/home/aidlux/BOE/CELL_AOI/AI_Detect/" + m_cpCfg.productId + "/"; + std::string strSaveDir = "/home/aidlux/BOE/CELL_AOI/Tradition_Detect/" + m_cpCfg.productId + "/"; MakeDirs(strSaveDir); std::string strSavePath_in = strSaveDir + m_cpCfg.productChannel +"_"+ roi_ext + "_"+ "in" + ".png"; std::string strSavePath_out = strSaveDir + m_cpCfg.productChannel +"_"+ roi_ext + "_"+ "out" + ".png";