|
|
|
|
@ -59,6 +59,27 @@
|
|
|
|
|
// edge_refine_step:沿边采样步长(像素)。不需要每个点都找,调大可省时间;调小边缘拟合更稳
|
|
|
|
|
#define EDGE_REFINE_STEP_DEFAULT 40
|
|
|
|
|
|
|
|
|
|
// ===== 支架方向精细化默认参数 =====
|
|
|
|
|
// (JSON: Support_Detect.form.support_param 下的
|
|
|
|
|
// support_refine_search / support_refine_step / support_refine_segments,均可选)
|
|
|
|
|
// 背景:支架由 Align_Inner 模型(smallmask,1696×800)检出,上采样回原图后边缘量化误差约 4px,
|
|
|
|
|
// 且支架区域接近方形时 minAreaRect 的"长轴"方向不稳定,用它求旋转角误差大。
|
|
|
|
|
// 改为用模型支架边缘做初值,在原图(detImg 灰度)上拟合支架长边(朝产品内侧的边)求方向角。
|
|
|
|
|
// support_refine_search:取模型支架外接矩形最外侧多少列作为边缘搜索带(未配置默认 30 列)。
|
|
|
|
|
// 右边缘 → 取外接矩形最右 nStrip 列,从右往左逐像素找灰度梯度最大的点;
|
|
|
|
|
// 左边缘 → 取最左 nStrip 列,从左往右找(相反逻辑)
|
|
|
|
|
// 调大:覆盖更大的模型误差/边缘更靠内,但可能扫到支架内部结构;
|
|
|
|
|
// 调小:更严格,模型边缘偏差大时找不到边缘(回退 minAreaRect 方案)
|
|
|
|
|
#define SUPPORT_REFINE_STRIP_DEFAULT 30
|
|
|
|
|
// support_refine_step:沿支架长边采样的步长(像素)。调大可省时间;调小拟合更稳
|
|
|
|
|
#define SUPPORT_REFINE_STEP_DEFAULT 20
|
|
|
|
|
// support_refine_segments:把采样点沿边分成 n 段,每段稳健拟合一条直线,取各段方向角的中位数。
|
|
|
|
|
// 支架边缘有凸起时:凸起处采样点成为所在段的离群点(MAD 剔除);凸起外边缘与支架边缘平行,
|
|
|
|
|
// 即使拟合到凸起外边缘也不影响方向角
|
|
|
|
|
#define SUPPORT_REFINE_SEGMENTS_DEFAULT 6
|
|
|
|
|
// 搜索列带内相邻像素(3 行灰度均值)的灰度差小于该值视为无边缘(极性无关,支架比外侧亮/暗均可)
|
|
|
|
|
#define SUPPORT_REFINE_MIN_CONTRAST 15.0
|
|
|
|
|
|
|
|
|
|
// 分类映射配置文件(每行:label索引:缺陷名称;冒号后名称为该缺陷类型名称,行的顺序即缺陷类型索引)
|
|
|
|
|
static const std::string CLS_LABEL_CONFIG_FILE = "/home/aidlux/BOE/UseModel_ZB/BM_cls.txt";
|
|
|
|
|
|
|
|
|
|
@ -1623,68 +1644,523 @@ int ImgCheckAnalysisy::CheckRun()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 3.5、旋转偏移:支架最小外接旋转矩形(长轴对应左/右边缘、短轴对应上/下边缘)
|
|
|
|
|
// 长轴与产品左/右边缘的夹角 > r_offset(度) → 偏移;r_offset<=0 不判定(兼容旧参数)
|
|
|
|
|
float fSupportAngle = 0; // 支架长轴相对产品左/右边缘的夹角(度,带符号;正=支架长轴相对产品边缘向右倾)
|
|
|
|
|
bool bRotOffset = false;
|
|
|
|
|
bool bSupportAngleValid = false; // 是否用上了原图精细化角度(false=走 minAreaRect 兜底)
|
|
|
|
|
if (supportDet.r_offset > 0 && rsContour.size() >= 3)
|
|
|
|
|
{
|
|
|
|
|
// 轮廓先映射回原图坐标再拟合旋转矩形(smallmask 的 x/y 缩放可能不同,不还原会带角度误差)
|
|
|
|
|
// 注意:smallmask 是 Align_Inner 在"大轮廓裁剪图"上的输出,映射原图时要加裁剪原点 bigRect
|
|
|
|
|
std::vector<cv::Point2f> rsContourSrc;
|
|
|
|
|
rsContourSrc.reserve(rsContour.size());
|
|
|
|
|
for (size_t ip = 0; ip < rsContour.size(); ip++)
|
|
|
|
|
{
|
|
|
|
|
rsContourSrc.emplace_back(rsContour[ip].x * fx, rsContour[ip].y * fy);
|
|
|
|
|
rsContourSrc.emplace_back(bigRect.x + rsContour[ip].x * fx,
|
|
|
|
|
bigRect.y + rsContour[ip].y * fy);
|
|
|
|
|
}
|
|
|
|
|
cv::RotatedRect rsRotRect = cv::minAreaRect(rsContourSrc);
|
|
|
|
|
cv::Point2f rpts[4];
|
|
|
|
|
rsRotRect.points(rpts);
|
|
|
|
|
cv::Point2f d01 = rpts[1] - rpts[0];
|
|
|
|
|
cv::Point2f d12 = rpts[2] - rpts[1];
|
|
|
|
|
float len01 = (float)cv::norm(d01);
|
|
|
|
|
float len12 = (float)cv::norm(d12);
|
|
|
|
|
// 长轴方向 = 长边方向(长轴对应左/右边缘),并统一朝下(与左边缘 上->下 方向一致)
|
|
|
|
|
cv::Point2f vLong(0, 1);
|
|
|
|
|
if (len01 >= len12 && len01 > 1e-3f)
|
|
|
|
|
{
|
|
|
|
|
vLong = d01 * (1.0f / len01);
|
|
|
|
|
}
|
|
|
|
|
else if (len12 > 1e-3f)
|
|
|
|
|
{
|
|
|
|
|
vLong = d12 * (1.0f / len12);
|
|
|
|
|
}
|
|
|
|
|
if (vLong.y < 0)
|
|
|
|
|
{
|
|
|
|
|
vLong = -vLong;
|
|
|
|
|
}
|
|
|
|
|
// 参考方向 = 产品左边缘(ptSegAArr[0]->ptSegBArr[0],拟合边/回退矩形边);退化时用垂直方向
|
|
|
|
|
cv::Point2f vRef = ptSegBArr[0] - ptSegAArr[0];
|
|
|
|
|
float lenRef = (float)cv::norm(vRef);
|
|
|
|
|
if (lenRef > 1e-3f)
|
|
|
|
|
{
|
|
|
|
|
vRef *= (1.0f / lenRef);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
vRef = cv::Point2f(0, 1);
|
|
|
|
|
}
|
|
|
|
|
if (vRef.y < 0)
|
|
|
|
|
{
|
|
|
|
|
vRef = -vRef;
|
|
|
|
|
}
|
|
|
|
|
// 有符号夹角(度);长轴是“线”,角度归一化到 [-90, 90]
|
|
|
|
|
double dCross = (double)vLong.x * vRef.y - (double)vLong.y * vRef.x;
|
|
|
|
|
double dDot = (double)vLong.x * vRef.x + (double)vLong.y * vRef.y;
|
|
|
|
|
fSupportAngle = (float)(std::atan2(dCross, dDot) * 180.0 / CV_PI);
|
|
|
|
|
if (fSupportAngle > 90.0f)
|
|
|
|
|
|
|
|
|
|
// ===== 精细化计算支架方向(原图灰度上拟合支架长边)=====
|
|
|
|
|
// 模型 smallmask 分辨率低(1696×800 上采样回原图,量化误差约 4px),且支架区域接近方形时
|
|
|
|
|
// minAreaRect 的"长轴"方向不稳定,直接用它求旋转角误差大;这里用模型支架边缘做初值,
|
|
|
|
|
// 在 detImg 灰度原图上重新拟合支架长边方向:
|
|
|
|
|
// 1、把 smallmask 最大轮廓映射回原图,得到支架外接矩形(rsRect);
|
|
|
|
|
// 取外接矩形最外侧 nStrip 列(默认 30 列)作为该侧边缘的搜索带
|
|
|
|
|
// 2、结合 edge_select:选 left → 拟合支架右边缘;选 right → 拟合支架左边缘
|
|
|
|
|
// (up/down 对应支架上下短边,水平的线不好拟合,不处理,仍用 minAreaRect 结果)
|
|
|
|
|
// 3、沿外接矩形高度按 step 采样;每行在搜索带内逐像素找灰度梯度最大的位置作为边缘点:
|
|
|
|
|
// 右边缘取最右 nStrip 列、从右往左找;左边缘取最左 nStrip 列、从左往右找;
|
|
|
|
|
// 梯度不足(该侧没有真实边缘)的行不参与拟合
|
|
|
|
|
// 4、采样点沿 y 分成 n 段,每段用 FitLineRobustTV 稳健拟合直线,取各段方向角的中位数:
|
|
|
|
|
// 支架边缘的凸起是矩形,凸起外边缘与支架长边平行,拟合到凸起处也不影响方向角;
|
|
|
|
|
// 凸起侧边/内部纹理等离群点由 MAD 剔除
|
|
|
|
|
// 失败(有效采样/分段不足、角度离散过大)时保留下面的 minAreaRect 长轴方案
|
|
|
|
|
if (!m_pImageAllResult->detImg.empty() && m_pImageAllResult->detImg.type() == CV_8UC1)
|
|
|
|
|
{
|
|
|
|
|
fSupportAngle -= 180.0f;
|
|
|
|
|
const cv::Mat &grayRef = m_pImageAllResult->detImg;
|
|
|
|
|
// 边缘搜索带宽度(列):取模型支架外接矩形最外侧多少列;未配置默认 30
|
|
|
|
|
int nStrip = (supportDet.support_refine_search > 0)
|
|
|
|
|
? cvRound(supportDet.support_refine_search)
|
|
|
|
|
: SUPPORT_REFINE_STRIP_DEFAULT;
|
|
|
|
|
int nStep = (supportDet.support_refine_step > 0)
|
|
|
|
|
? cvRound(supportDet.support_refine_step)
|
|
|
|
|
: SUPPORT_REFINE_STEP_DEFAULT;
|
|
|
|
|
int nSegments = (supportDet.support_refine_segments > 0)
|
|
|
|
|
? cvRound(supportDet.support_refine_segments)
|
|
|
|
|
: SUPPORT_REFINE_SEGMENTS_DEFAULT;
|
|
|
|
|
nStrip = std::max(4, nStrip);
|
|
|
|
|
nStep = std::max(4, nStep);
|
|
|
|
|
nSegments = std::max(1, nSegments);
|
|
|
|
|
|
|
|
|
|
// 3 行灰度均值(抑制单行噪声;调用处保证 y 在 [1, rows-2])
|
|
|
|
|
auto gray3 = [&grayRef](int y, int x) -> float {
|
|
|
|
|
return (grayRef.at<uchar>(y - 1, x) + grayRef.at<uchar>(y, x) + grayRef.at<uchar>(y + 1, x)) * (1.0f / 3.0f);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 拟合支架一条长边:nSide 0=支架左边缘(外侧 -x)、1=支架右边缘(外侧 +x);
|
|
|
|
|
// nRefIdx 0=产品左边缘、1=产品右边缘
|
|
|
|
|
// 返回 true 时 fAngleOut=支架长边相对产品该边缘的带符号夹角(度),fEdgeVertOut=支架长边相对竖直方向角度(度)
|
|
|
|
|
// pSamplesOut/pSegLinesOut 非空时输出采样边缘点和各段拟合线(检测图坐标),供 -s 绘制调试图
|
|
|
|
|
auto FitSupportEdgeAngle = [&](int nSide, int nRefIdx, float &fAngleOut, int &nSampleOut, int &nSegOut, float &fEdgeVertOut,
|
|
|
|
|
std::vector<cv::Point2f> *pSamplesOut, std::vector<std::vector<cv::Point2f>> *pSegLinesOut) -> bool {
|
|
|
|
|
fAngleOut = 0.0f;
|
|
|
|
|
fEdgeVertOut = 0.0f;
|
|
|
|
|
nSampleOut = 0;
|
|
|
|
|
nSegOut = 0;
|
|
|
|
|
if (pSamplesOut != NULL)
|
|
|
|
|
{
|
|
|
|
|
pSamplesOut->clear();
|
|
|
|
|
}
|
|
|
|
|
if (pSegLinesOut != NULL)
|
|
|
|
|
{
|
|
|
|
|
pSegLinesOut->clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 产品基准边缘方向(统一朝下,与原有旋转角符号约定一致)
|
|
|
|
|
cv::Point2f vRef = ptSegBArr[nRefIdx] - ptSegAArr[nRefIdx];
|
|
|
|
|
float fRefLen = (float)cv::norm(vRef);
|
|
|
|
|
if (fRefLen < 1e-3f)
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
vRef *= (1.0f / fRefLen);
|
|
|
|
|
if (vRef.y < 0)
|
|
|
|
|
{
|
|
|
|
|
vRef = -vRef;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== 取模型支架外接矩形(rsRect)最外侧 nStrip 列,逐行找边缘点 =====
|
|
|
|
|
// 右边缘:取外接矩形最右 nStrip 列,从右往左扫;左边缘:取最左 nStrip 列,从左往右扫。
|
|
|
|
|
// 每行按灰度梯度找边缘点;梯度不足(该侧没有真实边缘)的行不参与拟合
|
|
|
|
|
int nXStart = 0; // 外侧起始列(原图坐标)
|
|
|
|
|
int nXEnd = 0; // 内侧结束列(原图坐标)
|
|
|
|
|
if (nSide == 0)
|
|
|
|
|
{
|
|
|
|
|
nXStart = rsRect.x;
|
|
|
|
|
nXEnd = rsRect.x + nStrip - 1;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
nXStart = rsRect.x + rsRect.width - 1;
|
|
|
|
|
nXEnd = rsRect.x + rsRect.width - nStrip;
|
|
|
|
|
}
|
|
|
|
|
const int nStepX = (nXEnd >= nXStart) ? 1 : -1;
|
|
|
|
|
|
|
|
|
|
// 沿外接矩形高度采样(两端各缩 5%,至少 6px,避开圆角)
|
|
|
|
|
double fY0 = rsRect.y;
|
|
|
|
|
double fY1 = rsRect.y + rsRect.height - 1;
|
|
|
|
|
const double fTrim = std::max(6.0, (fY1 - fY0) * 0.05);
|
|
|
|
|
fY0 += fTrim;
|
|
|
|
|
fY1 -= fTrim;
|
|
|
|
|
if (fY1 - fY0 < 20.0)
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<cv::Point2f> vecEdgePts; // (t,v) = (yDet, xDet)
|
|
|
|
|
vecEdgePts.reserve((int)((fY1 - fY0) / nStep) + 1);
|
|
|
|
|
|
|
|
|
|
for (double fT = fY0; fT <= fY1; fT += nStep)
|
|
|
|
|
{
|
|
|
|
|
// 原图 -> 检测图(detImg) 为纯平移;列带两端换算到检测图坐标
|
|
|
|
|
cv::Point2f ptDetStart = MatAffinePoint(m_matSrc2Det, cv::Point2f((float)nXStart, (float)fT));
|
|
|
|
|
cv::Point2f ptDetEnd = MatAffinePoint(m_matSrc2Det, cv::Point2f((float)nXEnd, (float)fT));
|
|
|
|
|
const int nYDet = cvRound(ptDetStart.y);
|
|
|
|
|
if (nYDet < 2 || nYDet >= grayRef.rows - 2)
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const int nX0 = cvRound(ptDetStart.x); // 外侧端
|
|
|
|
|
const int nX1 = cvRound(ptDetEnd.x); // 内侧端
|
|
|
|
|
if ((nStepX > 0 && nX1 <= nX0) || (nStepX < 0 && nX1 >= nX0))
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 外侧 -> 内侧逐像素扫,取灰度梯度最大的位置作为该行支架边缘点
|
|
|
|
|
int nEdge = -1;
|
|
|
|
|
float fBestGrad = 0.0f;
|
|
|
|
|
for (int x = nX0; (nStepX > 0) ? (x + nStepX <= nX1) : (x + nStepX >= nX1); x += nStepX)
|
|
|
|
|
{
|
|
|
|
|
if (x < 1 || x + nStepX < 1 || x >= grayRef.cols - 1 || x + nStepX >= grayRef.cols - 1)
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const float fGrad = fabs(gray3(nYDet, x + nStepX) - gray3(nYDet, x));
|
|
|
|
|
if (fGrad > fBestGrad)
|
|
|
|
|
{
|
|
|
|
|
fBestGrad = fGrad;
|
|
|
|
|
nEdge = x;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (nEdge < 0 || fBestGrad < (float)SUPPORT_REFINE_MIN_CONTRAST)
|
|
|
|
|
{
|
|
|
|
|
continue; // 该行列带内没有明显灰度阶跃,本行不参与拟合
|
|
|
|
|
}
|
|
|
|
|
vecEdgePts.push_back(cv::Point2f((float)nYDet, (float)(nEdge + nStepX * 0.5f)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测",
|
|
|
|
|
"support edge diag(side %d): strip %d cols [%d,%d], pts %d",
|
|
|
|
|
nSide, nStrip, nXStart, nXEnd, (int)vecEdgePts.size());
|
|
|
|
|
|
|
|
|
|
nSampleOut = (int)vecEdgePts.size();
|
|
|
|
|
if (nSampleOut < 8)
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 沿边分成 n 段,每段稳健拟合一条直线(凸起处点被 MAD 剔除;拟合到凸起外边缘也不影响方向)
|
|
|
|
|
std::vector<float> vecAngles;
|
|
|
|
|
std::vector<float> vecAnglesVert;
|
|
|
|
|
vecAngles.reserve(nSegments);
|
|
|
|
|
vecAnglesVert.reserve(nSegments);
|
|
|
|
|
for (int s = 0; s < nSegments; s++)
|
|
|
|
|
{
|
|
|
|
|
const size_t i0 = (size_t)((long long)s * nSampleOut / nSegments);
|
|
|
|
|
const size_t i1 = (size_t)((long long)(s + 1) * nSampleOut / nSegments);
|
|
|
|
|
if (i1 - i0 < 4)
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
std::vector<cv::Point2f> vecSeg(vecEdgePts.begin() + i0, vecEdgePts.begin() + i1);
|
|
|
|
|
double fKSeg = 0, fBSeg = 0;
|
|
|
|
|
if (FitLineRobustTV(vecSeg, fKSeg, fBSeg) < 4)
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 拟合线方向(统一朝下)与产品基准边缘的带符号夹角
|
|
|
|
|
cv::Point2f vSeg((float)fKSeg, 1.0f);
|
|
|
|
|
float fSegLen = (float)cv::norm(vSeg);
|
|
|
|
|
if (fSegLen < 1e-6f)
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
vSeg *= (1.0f / fSegLen);
|
|
|
|
|
const double dCross = (double)vSeg.x * vRef.y - (double)vSeg.y * vRef.x;
|
|
|
|
|
const double dDot = (double)vSeg.x * vRef.x + (double)vSeg.y * vRef.y;
|
|
|
|
|
const float fAngle = (float)(std::atan2(dCross, dDot) * 180.0 / CV_PI);
|
|
|
|
|
if (fabs(fAngle) > 45.0f)
|
|
|
|
|
{
|
|
|
|
|
continue; // 与产品边缘夹角不合理,丢弃该段
|
|
|
|
|
}
|
|
|
|
|
vecAngles.push_back(fAngle);
|
|
|
|
|
vecAnglesVert.push_back((float)(std::atan2(fKSeg, 1.0) * 180.0 / CV_PI));
|
|
|
|
|
// 记录该段拟合直线的两端点(检测图坐标),-s 时用于绘制支架边缘
|
|
|
|
|
if (pSegLinesOut != NULL)
|
|
|
|
|
{
|
|
|
|
|
const float fSegT0 = vecSeg.front().x; // vecSeg 点为 (t,v) = (yDet, xDet)
|
|
|
|
|
const float fSegT1 = vecSeg.back().x;
|
|
|
|
|
std::vector<cv::Point2f> vecLine(2);
|
|
|
|
|
vecLine[0] = cv::Point2f((float)(fKSeg * fSegT0 + fBSeg), fSegT0);
|
|
|
|
|
vecLine[1] = cv::Point2f((float)(fKSeg * fSegT1 + fBSeg), fSegT1);
|
|
|
|
|
pSegLinesOut->push_back(vecLine);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
nSegOut = (int)vecAngles.size();
|
|
|
|
|
if (nSegOut < 2)
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 取各段角度中位数,并用 MAD 检查各段一致性(凸起/纹理导致的异常段会被中位数稀释)
|
|
|
|
|
std::vector<float> vecTmp = vecAngles;
|
|
|
|
|
std::nth_element(vecTmp.begin(), vecTmp.begin() + vecTmp.size() / 2, vecTmp.end());
|
|
|
|
|
const float fMedian = vecTmp[vecTmp.size() / 2];
|
|
|
|
|
for (size_t i = 0; i < vecAngles.size(); i++)
|
|
|
|
|
{
|
|
|
|
|
vecTmp[i] = fabs(vecAngles[i] - fMedian);
|
|
|
|
|
}
|
|
|
|
|
std::nth_element(vecTmp.begin(), vecTmp.begin() + vecTmp.size() / 2, vecTmp.end());
|
|
|
|
|
if (vecTmp[vecTmp.size() / 2] > 5.0f)
|
|
|
|
|
{
|
|
|
|
|
return false; // 各段方向离散过大(可能拟合到了不同结构),不采用
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fAngleOut = fMedian;
|
|
|
|
|
std::nth_element(vecAnglesVert.begin(), vecAnglesVert.begin() + vecAnglesVert.size() / 2, vecAnglesVert.end());
|
|
|
|
|
fEdgeVertOut = vecAnglesVert[vecAnglesVert.size() / 2];
|
|
|
|
|
if (pSamplesOut != NULL)
|
|
|
|
|
{
|
|
|
|
|
pSamplesOut->reserve(vecEdgePts.size());
|
|
|
|
|
for (size_t i = 0; i < vecEdgePts.size(); i++)
|
|
|
|
|
{
|
|
|
|
|
pSamplesOut->push_back(cv::Point2f(vecEdgePts[i].y, vecEdgePts[i].x)); // (t,v) -> (x,y)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 按 edge_select 选择拟合方向:left -> 支架右边缘,right -> 支架左边缘(up/down 不处理)
|
|
|
|
|
int nCandSide[2] = {0, 0};
|
|
|
|
|
int nCandRef[2] = {0, 0};
|
|
|
|
|
int nCandNum = 0;
|
|
|
|
|
for (size_t iselect = 0; iselect < supportDet.edge_select.size(); iselect++)
|
|
|
|
|
{
|
|
|
|
|
const std::string &strEdge = supportDet.edge_select[iselect];
|
|
|
|
|
int nSide = -1;
|
|
|
|
|
int nRef = -1;
|
|
|
|
|
if (strEdge == "left")
|
|
|
|
|
{
|
|
|
|
|
nSide = 1; // 拟合支架右边缘
|
|
|
|
|
nRef = 0; // 参考产品左边缘
|
|
|
|
|
}
|
|
|
|
|
else if (strEdge == "right")
|
|
|
|
|
{
|
|
|
|
|
nSide = 0; // 拟合支架左边缘
|
|
|
|
|
nRef = 1; // 参考产品右边缘
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
bool bDup = false;
|
|
|
|
|
for (int ic = 0; ic < nCandNum; ic++)
|
|
|
|
|
{
|
|
|
|
|
if (nCandSide[ic] == nSide && nCandRef[ic] == nRef)
|
|
|
|
|
{
|
|
|
|
|
bDup = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!bDup && nCandNum < 2)
|
|
|
|
|
{
|
|
|
|
|
nCandSide[nCandNum] = nSide;
|
|
|
|
|
nCandRef[nCandNum] = nRef;
|
|
|
|
|
nCandNum++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int nBestCand = -1;
|
|
|
|
|
int nBestSample = 0;
|
|
|
|
|
int nBestSeg = 0;
|
|
|
|
|
float fBestAngle = 0.0f;
|
|
|
|
|
float fBestEdgeVert = 0.0f;
|
|
|
|
|
int nBestSideUsed = -1;
|
|
|
|
|
int nBestRefUsed = -1;
|
|
|
|
|
std::vector<cv::Point2f> vecBestSamples; // 采样边缘点(检测图坐标)
|
|
|
|
|
std::vector<std::vector<cv::Point2f>> vecBestSegLines; // 各段拟合线(检测图坐标)
|
|
|
|
|
for (int ic = 0; ic < nCandNum; ic++)
|
|
|
|
|
{
|
|
|
|
|
float fAngle = 0.0f;
|
|
|
|
|
float fEdgeVert = 0.0f;
|
|
|
|
|
int nSample = 0;
|
|
|
|
|
int nSeg = 0;
|
|
|
|
|
std::vector<cv::Point2f> vecCandSamples;
|
|
|
|
|
std::vector<std::vector<cv::Point2f>> vecCandSegLines;
|
|
|
|
|
// 只拟合 edge_select 指定的支架长边;该侧失败时保留 minAreaRect 结果,不换边
|
|
|
|
|
// (两条长边方向角虽一致,但换边会静默得到另一条边的结果,与配置含义不符)
|
|
|
|
|
bool bOk = FitSupportEdgeAngle(nCandSide[ic], nCandRef[ic], fAngle, nSample, nSeg, fEdgeVert,
|
|
|
|
|
&vecCandSamples, &vecCandSegLines);
|
|
|
|
|
const int nUseSide = nCandSide[ic];
|
|
|
|
|
if (!bOk)
|
|
|
|
|
if (!bOk)
|
|
|
|
|
{
|
|
|
|
|
m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测",
|
|
|
|
|
"support angle refine(ref %s): %s support edge fail, keep minAreaRect",
|
|
|
|
|
(nCandRef[ic] == 0) ? "left" : "right", (nCandSide[ic] == 0) ? "left" : "right");
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const cv::Point2f vRefLog = ptSegBArr[nCandRef[ic]] - ptSegAArr[nCandRef[ic]];
|
|
|
|
|
m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测",
|
|
|
|
|
"support angle refine(ref %s): use %s support edge: sample %d seg %d angle %.2f deg (edge vs vert %.2f, ref vs vert %.2f)",
|
|
|
|
|
(nCandRef[ic] == 0) ? "left" : "right", (nCandSide[ic] == 0) ? "left" : "right",
|
|
|
|
|
nSample, nSeg, fAngle, fEdgeVert,
|
|
|
|
|
(float)(std::atan2((double)vRefLog.x, (double)vRefLog.y) * 180.0 / CV_PI));
|
|
|
|
|
if (nSample > nBestSample)
|
|
|
|
|
{
|
|
|
|
|
nBestSample = nSample;
|
|
|
|
|
nBestSeg = nSeg;
|
|
|
|
|
fBestAngle = fAngle;
|
|
|
|
|
fBestEdgeVert = fEdgeVert;
|
|
|
|
|
nBestSideUsed = nUseSide;
|
|
|
|
|
nBestRefUsed = nCandRef[ic];
|
|
|
|
|
vecBestSamples = vecCandSamples;
|
|
|
|
|
vecBestSegLines = vecCandSegLines;
|
|
|
|
|
nBestCand = ic;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (nBestCand >= 0)
|
|
|
|
|
{
|
|
|
|
|
fSupportAngle = fBestAngle;
|
|
|
|
|
bRotOffset = (fabs(fSupportAngle) > supportDet.r_offset);
|
|
|
|
|
bSupportAngleValid = true;
|
|
|
|
|
m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测",
|
|
|
|
|
"support angle refine ok: angle %.2f deg (sample %d seg %d), replace minAreaRect",
|
|
|
|
|
fSupportAngle, nBestSample, nBestSeg);
|
|
|
|
|
|
|
|
|
|
// -s(存过程图):绘制精细化拟合的支架边缘,便于从图像确认拟合效果
|
|
|
|
|
// 蓝=smallmask 支架轮廓 黄=参与拟合的采样边缘点 绿=各段稳健拟合直线
|
|
|
|
|
// 红=最终方向线(产品基准边方向 + 精细夹角,过采样点重心) 品红=产品基准边
|
|
|
|
|
if (DetImgInfo_shareP->bsaveProcessImg && !vecBestSamples.empty())
|
|
|
|
|
{
|
|
|
|
|
cv::Mat show;
|
|
|
|
|
if (grayRef.channels() == 1)
|
|
|
|
|
{
|
|
|
|
|
cv::cvtColor(grayRef, show, cv::COLOR_GRAY2BGR);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
show = grayRef.clone();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 支架区域轮廓(蓝):smallmask 最大轮廓映射到检测图坐标
|
|
|
|
|
std::vector<cv::Point> vecRsPoly;
|
|
|
|
|
vecRsPoly.reserve(rsContour.size());
|
|
|
|
|
for (size_t ip = 0; ip < rsContour.size(); ip++)
|
|
|
|
|
{
|
|
|
|
|
cv::Point2f ptSrc(bigRect.x + rsContour[ip].x * fx, bigRect.y + rsContour[ip].y * fy);
|
|
|
|
|
cv::Point2f ptDet = MatAffinePoint(m_matSrc2Det, ptSrc);
|
|
|
|
|
vecRsPoly.push_back(cv::Point(cvRound(ptDet.x), cvRound(ptDet.y)));
|
|
|
|
|
}
|
|
|
|
|
if (vecRsPoly.size() >= 3)
|
|
|
|
|
{
|
|
|
|
|
cv::polylines(show, vecRsPoly, true, cv::Scalar(255, 0, 0), 4);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 采样边缘点(黄)
|
|
|
|
|
for (size_t i = 0; i < vecBestSamples.size(); i++)
|
|
|
|
|
{
|
|
|
|
|
cv::circle(show, cv::Point(cvRound(vecBestSamples[i].x), cvRound(vecBestSamples[i].y)), 4,
|
|
|
|
|
cv::Scalar(0, 255, 255), -1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 各段稳健拟合的直线(绿)
|
|
|
|
|
for (size_t i = 0; i < vecBestSegLines.size(); i++)
|
|
|
|
|
{
|
|
|
|
|
if (vecBestSegLines[i].size() == 2)
|
|
|
|
|
{
|
|
|
|
|
cv::line(show, cv::Point(cvRound(vecBestSegLines[i][0].x), cvRound(vecBestSegLines[i][0].y)),
|
|
|
|
|
cv::Point(cvRound(vecBestSegLines[i][1].x), cvRound(vecBestSegLines[i][1].y)),
|
|
|
|
|
cv::Scalar(0, 255, 0), 4);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 最终方向线(红):产品基准边方向旋转支架精细夹角,过采样点重心
|
|
|
|
|
cv::Point2f vRefBest = ptSegBArr[nBestRefUsed] - ptSegAArr[nBestRefUsed];
|
|
|
|
|
float fRefBestLen = (float)cv::norm(vRefBest);
|
|
|
|
|
if (fRefBestLen > 1e-3f)
|
|
|
|
|
{
|
|
|
|
|
vRefBest *= (1.0f / fRefBestLen);
|
|
|
|
|
if (vRefBest.y < 0)
|
|
|
|
|
{
|
|
|
|
|
vRefBest = -vRefBest;
|
|
|
|
|
}
|
|
|
|
|
const double dRad = (double)fSupportAngle * CV_PI / 180.0;
|
|
|
|
|
cv::Point2f vDir((float)(vRefBest.x * std::cos(dRad) + vRefBest.y * std::sin(dRad)),
|
|
|
|
|
(float)(-vRefBest.x * std::sin(dRad) + vRefBest.y * std::cos(dRad)));
|
|
|
|
|
float fDirLen = (float)cv::norm(vDir);
|
|
|
|
|
if (fDirLen > 1e-6f)
|
|
|
|
|
{
|
|
|
|
|
vDir *= (1.0f / fDirLen);
|
|
|
|
|
}
|
|
|
|
|
cv::Point2f ptCenter(0.0f, 0.0f);
|
|
|
|
|
float fSampleYMin = vecBestSamples[0].y;
|
|
|
|
|
float fSampleYMax = vecBestSamples[0].y;
|
|
|
|
|
for (size_t i = 0; i < vecBestSamples.size(); i++)
|
|
|
|
|
{
|
|
|
|
|
ptCenter += vecBestSamples[i];
|
|
|
|
|
fSampleYMin = std::min(fSampleYMin, vecBestSamples[i].y);
|
|
|
|
|
fSampleYMax = std::max(fSampleYMax, vecBestSamples[i].y);
|
|
|
|
|
}
|
|
|
|
|
ptCenter *= (1.0f / (float)vecBestSamples.size());
|
|
|
|
|
const float fHalf = std::max(20.0f, (fSampleYMax - fSampleYMin) * 0.6f);
|
|
|
|
|
cv::line(show, cv::Point(cvRound(ptCenter.x - vDir.x * fHalf), cvRound(ptCenter.y - vDir.y * fHalf)),
|
|
|
|
|
cv::Point(cvRound(ptCenter.x + vDir.x * fHalf), cvRound(ptCenter.y + vDir.y * fHalf)),
|
|
|
|
|
cv::Scalar(0, 0, 255), 4);
|
|
|
|
|
|
|
|
|
|
// 产品基准边缘(品红)
|
|
|
|
|
cv::Point2f ptRefA = MatAffinePoint(m_matSrc2Det, ptSegAArr[nBestRefUsed]);
|
|
|
|
|
cv::Point2f ptRefB = MatAffinePoint(m_matSrc2Det, ptSegBArr[nBestRefUsed]);
|
|
|
|
|
cv::line(show, cv::Point(cvRound(ptRefA.x), cvRound(ptRefA.y)),
|
|
|
|
|
cv::Point(cvRound(ptRefB.x), cvRound(ptRefB.y)),
|
|
|
|
|
cv::Scalar(255, 0, 255), 3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 文字信息:所用支架边/参考产品边/采样与分段数/角度
|
|
|
|
|
char strDbg[256];
|
|
|
|
|
snprintf(strDbg, sizeof(strDbg), "support edge refine: side %s ref %s sample %d seg %d",
|
|
|
|
|
(nBestSideUsed == 0) ? "left" : "right", (nBestRefUsed == 0) ? "left" : "right",
|
|
|
|
|
nBestSample, nBestSeg);
|
|
|
|
|
cv::putText(show, strDbg, cv::Point(20, 70), cv::FONT_HERSHEY_SIMPLEX, 2.0, cv::Scalar(0, 255, 255), 4);
|
|
|
|
|
cv::Point2f vRefDbg = ptSegBArr[nBestRefUsed] - ptSegAArr[nBestRefUsed];
|
|
|
|
|
snprintf(strDbg, sizeof(strDbg), "angle %.2f deg (edge vs vert %.2f, ref vs vert %.2f)",
|
|
|
|
|
fSupportAngle, fBestEdgeVert,
|
|
|
|
|
(float)(std::atan2((double)vRefDbg.x, (double)vRefDbg.y) * 180.0 / CV_PI));
|
|
|
|
|
cv::putText(show, strDbg, cv::Point(20, 140), cv::FONT_HERSHEY_SIMPLEX, 2.0, cv::Scalar(0, 255, 255), 4);
|
|
|
|
|
|
|
|
|
|
cv::Mat showSmall;
|
|
|
|
|
cv::resize(show, showSmall, cv::Size(show.cols / 4, show.rows / 4));
|
|
|
|
|
std::vector<int> paramJpg = {cv::IMWRITE_JPEG_QUALITY, 90};
|
|
|
|
|
cv::imwrite(DetImgInfo_shareP->strChannel + "_support_angle.jpg", showSmall, paramJpg);
|
|
|
|
|
m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测",
|
|
|
|
|
"support angle debug image saved: %s_support_angle.jpg", DetImgInfo_shareP->strChannel.c_str());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (!supportDet.edge_select.empty())
|
|
|
|
|
{
|
|
|
|
|
m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测",
|
|
|
|
|
"support angle refine not applied(no left/right edge or fit failed), use minAreaRect");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (fSupportAngle < -90.0f)
|
|
|
|
|
|
|
|
|
|
// 3.5、旋转偏移(兜底):支架最小外接旋转矩形(长轴对应左/右边缘、短轴对应上/下边缘)
|
|
|
|
|
// 长轴与产品左/右边缘的夹角 > r_offset(度) → 偏移;r_offset<=0 不判定(兼容旧参数)
|
|
|
|
|
if (!bSupportAngleValid)
|
|
|
|
|
{
|
|
|
|
|
fSupportAngle += 180.0f;
|
|
|
|
|
cv::Point2f rpts[4];
|
|
|
|
|
rsRotRect.points(rpts);
|
|
|
|
|
cv::Point2f d01 = rpts[1] - rpts[0];
|
|
|
|
|
cv::Point2f d12 = rpts[2] - rpts[1];
|
|
|
|
|
float len01 = (float)cv::norm(d01);
|
|
|
|
|
float len12 = (float)cv::norm(d12);
|
|
|
|
|
// 长轴方向 = 长边方向(长轴对应左/右边缘),并统一朝下(与左边缘 上->下 方向一致)
|
|
|
|
|
cv::Point2f vLong(0, 1);
|
|
|
|
|
if (len01 >= len12 && len01 > 1e-3f)
|
|
|
|
|
{
|
|
|
|
|
vLong = d01 * (1.0f / len01);
|
|
|
|
|
}
|
|
|
|
|
else if (len12 > 1e-3f)
|
|
|
|
|
{
|
|
|
|
|
vLong = d12 * (1.0f / len12);
|
|
|
|
|
}
|
|
|
|
|
if (vLong.y < 0)
|
|
|
|
|
{
|
|
|
|
|
vLong = -vLong;
|
|
|
|
|
}
|
|
|
|
|
// 参考方向 = 产品左边缘(ptSegAArr[0]->ptSegBArr[0],拟合边/回退矩形边);退化时用垂直方向
|
|
|
|
|
cv::Point2f vRef = ptSegBArr[0] - ptSegAArr[0];
|
|
|
|
|
float lenRef = (float)cv::norm(vRef);
|
|
|
|
|
if (lenRef > 1e-3f)
|
|
|
|
|
{
|
|
|
|
|
vRef *= (1.0f / lenRef);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
vRef = cv::Point2f(0, 1);
|
|
|
|
|
}
|
|
|
|
|
if (vRef.y < 0)
|
|
|
|
|
{
|
|
|
|
|
vRef = -vRef;
|
|
|
|
|
}
|
|
|
|
|
// 有符号夹角(度);长轴是“线”,角度归一化到 [-90, 90]
|
|
|
|
|
double dCross = (double)vLong.x * vRef.y - (double)vLong.y * vRef.x;
|
|
|
|
|
double dDot = (double)vLong.x * vRef.x + (double)vLong.y * vRef.y;
|
|
|
|
|
fSupportAngle = (float)(std::atan2(dCross, dDot) * 180.0 / CV_PI);
|
|
|
|
|
if (fSupportAngle > 90.0f)
|
|
|
|
|
{
|
|
|
|
|
fSupportAngle -= 180.0f;
|
|
|
|
|
}
|
|
|
|
|
else if (fSupportAngle < -90.0f)
|
|
|
|
|
{
|
|
|
|
|
fSupportAngle += 180.0f;
|
|
|
|
|
}
|
|
|
|
|
bRotOffset = (fabs(fSupportAngle) > supportDet.r_offset);
|
|
|
|
|
}
|
|
|
|
|
bRotOffset = (fabs(fSupportAngle) > supportDet.r_offset);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 像素 -> mm
|
|
|
|
|
@ -1766,13 +2242,13 @@ int ImgCheckAnalysisy::CheckRun()
|
|
|
|
|
m_SupportDrawInfo.ptCenter = cv::Point2f(fCx, fCy);
|
|
|
|
|
|
|
|
|
|
m_pdetlog->AddCheckstr(PrintLevel_0, "支架检测",
|
|
|
|
|
"rp [%d %d %d %d] rs [%d %d %d %d] support area %.2f mm2 (min %.2f) scale (%.4f %.4f) center (%.1f %.1f) edge %s quad %ld dL %.1f dR %.1f dT %.1f dB %.1f | select %s diff %s avgDiff %.1f limit %.1f | rot %.2f deg limit %.2f ng %d",
|
|
|
|
|
"rp [%d %d %d %d] rs [%d %d %d %d] support area %.2f mm2 (min %.2f) scale (%.4f %.4f) center (%.1f %.1f) edge %s quad %ld dL %.1f dR %.1f dT %.1f dB %.1f | select %s diff %s avgDiff %.1f limit %.1f | rot %.2f deg limit %.2f(%s) ng %d",
|
|
|
|
|
rpRect.x, rpRect.y, rpRect.width, rpRect.height,
|
|
|
|
|
rsRect.x, rsRect.y, rsRect.width, rsRect.height,
|
|
|
|
|
dSupportAreaMm2, supportDet.support_min_area,
|
|
|
|
|
fx, fy, fCx, fCy, bFitQuad ? "fitquad" : "rpbox", (long)m_HandleBodyQuad.size(), fDL, fDR, fDT, fDB,
|
|
|
|
|
strSelect.c_str(), strDiff.c_str(), fAvgDiff, supportDet.x_offset,
|
|
|
|
|
fSupportAngle, supportDet.r_offset, bOffset);
|
|
|
|
|
fSupportAngle, supportDet.r_offset, bSupportAngleValid ? "refine" : "rect", bOffset);
|
|
|
|
|
|
|
|
|
|
if (bOffset)
|
|
|
|
|
{
|
|
|
|
|
|