feat 优化上下错位

dev_lsy
liusiyang 6 days ago
parent b955d4725f
commit e871478bc0

@ -191,6 +191,10 @@ private:
// 找到产品(亮色区域)所在的第一行(顶部边缘), 用于左右图上下对齐 // 找到产品(亮色区域)所在的第一行(顶部边缘), 用于左右图上下对齐
int GetProductFirstRow(const cv::Mat &img, int &outRow); 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: public:
// 运行的基本参数 // 运行的基本参数
RunInfoST m_RunConfig; RunInfoST m_RunConfig;

@ -10,6 +10,7 @@
#include "CheckUtil.hpp" #include "CheckUtil.hpp"
#include "Define.h" #include "Define.h"
#include <algorithm> #include <algorithm>
#include <cmath>
CameraCheckAnalysisy::CameraCheckAnalysisy() CameraCheckAnalysisy::CameraCheckAnalysisy()
{ {
@ -168,7 +169,7 @@ int CameraCheckAnalysisy::Detect_Pre()
// pImageResult->result->in_shareImage->strCameraName, pImageResult->result->in_shareImage->strChannel); // pImageResult->result->in_shareImage->strCameraName, pImageResult->result->in_shareImage->strChannel);
// 简单硬拼接img 在左img_B 在右,拼接结果写回 img // 简单硬拼接img 在左img_B 在右,拼接结果写回 img
// 水平方向有overlap拼接时固定去掉左图最右边overlap区域 // 先去掉左图最右边overlap区域再做首行判断与上下对齐最后水平拼接
if (pImageResult->result->in_shareImage->img.size() != pImageResult->result->in_shareImage->img_B.size() || 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()) 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 = pImageResult->result->in_shareImage->img;
cv::Mat &img_B = pImageResult->result->in_shareImage->img_B; 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; m_pdetlog->AddCheckstr(PrintLevel_1, "Detect_Pre", "Cam %s ==================img.cols <= overlap %d ", strBasic.c_str(), overlap);
int rowRight = -1; return 1;
if (GetProductFirstRow(img, rowLeft) == 0 && GetProductFirstRow(img_B, rowRight) == 0) }
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
int refRow = std::min(rowLeft, rowRight); float kR = 0.0f, bR = 0.0f; // 右图顶边直线: y = kR * x + bR
int shiftLeft = refRow - rowLeft; // <= 0, 负值表示向上平移
int shiftRight = refRow - rowRight; // <= 0
m_pdetlog->AddCheckstr(PrintLevel_1, "Detect_Pre", "Cam %s ==================align rowLeft %d rowRight %d shiftLeft %d shiftRight %d", int reL = FitProductTopLine(img, kL, bL, pL1, pL2);
strBasic.c_str(), rowLeft, rowRight, shiftLeft, shiftRight); int reR = FitProductTopLine(img_B, kR, bR, pR1, pR2);
if (shiftLeft < 0) if (reL == 0 && reR == 0)
{ {
// 内容向上平移 |shiftLeft| 行,底部补黑。 // 左图顶边直线在拼接缝处(x = seamX)的行坐标 = 左图线段右端点的高度
int dy = -shiftLeft; const double seamY = kL * seamX + bL;
cv::Mat shifted = cv::Mat::zeros(img.size(), img.type()); // 右图顶边直线在其左边界(x = 0)处的行坐标 = 右图线段左端点的高度
img(cv::Rect(0, dy, img.cols, img.rows - dy)).copyTo(shifted(cv::Rect(0, 0, img.cols, img.rows - dy))); const double rightY = bR;
img = shifted;
} // 让两条线段的连接端点在拼接缝处相接: 右图上下平移 seamY - rightY 行
if (shiftRight < 0) dyAlign = (int)cvRound(seamY - rightY);
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()); 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; img_B = shifted;
} }
bFitOK = true;
} }
else 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); // 3、上下对齐后水平拼接
if (img.cols <= overlap)
{ {
m_pdetlog->AddCheckstr(PrintLevel_1, "Detect_Pre", "Cam %s ==================img.cols <= overlap %d ", strBasic.c_str(), overlap);
return 1;
}
cv::Mat merge_img; cv::Mat merge_img;
cv::hconcat(img(cv::Rect(0, 0, img.cols - overlap, img.rows)), img_B, merge_img); cv::hconcat(img, img_B, merge_img);
img = merge_img; img = merge_img;
}
// 拼接完成后 img_B 不再需要,立即释放内存 // 拼接完成后 img_B 不再需要,立即释放内存
img_B.release(); img_B.release();
@ -229,6 +259,45 @@ int CameraCheckAnalysisy::Detect_Pre()
if (pImageResult->result->in_shareImage->Det_Mode == DET_MODE_MergeImg) if (pImageResult->result->in_shareImage->Det_Mode == DET_MODE_MergeImg)
{ {
cv::imwrite(pImageResult->result->in_shareImage->strCameraName + "_MergeImg.jpg", pImageResult->result->in_shareImage->img); 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; return 2;
} }
@ -1633,3 +1702,168 @@ int CameraCheckAnalysisy::GetProductFirstRow(const cv::Mat &img, int &outRow)
return 2; // 理论上不会走到这里 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<uchar>(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<cv::Point2f> 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<uchar>(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<cv::Point2f> 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<cv::Point2f> 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;
}

@ -204,7 +204,7 @@ cv::Mat CTcsCheck::AdaptiveBinary(cv::Mat matBlur)
if (m_cpCfg.bDebugsaveImg) if (m_cpCfg.bDebugsaveImg)
{ {
std::string roi_ext = "[" + std::to_string(x) + "," + std::to_string(y) + "]"; 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); MakeDirs(strSaveDir);
std::string strSavePath_in = strSaveDir + m_cpCfg.productChannel +"_"+ roi_ext + "_"+ "in" + ".png"; std::string strSavePath_in = strSaveDir + m_cpCfg.productChannel +"_"+ roi_ext + "_"+ "in" + ".png";
std::string strSavePath_out = strSaveDir + m_cpCfg.productChannel +"_"+ roi_ext + "_"+ "out" + ".png"; std::string strSavePath_out = strSaveDir + m_cpCfg.productChannel +"_"+ roi_ext + "_"+ "out" + ".png";

Loading…
Cancel
Save