|
|
|
@ -19,6 +19,8 @@
|
|
|
|
#include <stdlib.h>
|
|
|
|
#include <stdlib.h>
|
|
|
|
#include <fstream>
|
|
|
|
#include <fstream>
|
|
|
|
#include <thread>
|
|
|
|
#include <thread>
|
|
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
|
|
|
#include <cmath>
|
|
|
|
#include "snowflake.hpp"
|
|
|
|
#include "snowflake.hpp"
|
|
|
|
|
|
|
|
|
|
|
|
int _sysmkdir_2(const std::string &dir)
|
|
|
|
int _sysmkdir_2(const std::string &dir)
|
|
|
|
@ -720,8 +722,144 @@ cv::Point2f CheckUtil::transformPoint(const cv::Point2f &point, const cv::Mat &t
|
|
|
|
return cv::Point2f(result_mat.at<double>(0), result_mat.at<double>(1));
|
|
|
|
return cv::Point2f(result_mat.at<double>(0), result_mat.at<double>(1));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 拟合边缘直线:val ≈ fK * idx + fB(点集为 (扫描序号, 边界值))
|
|
|
|
|
|
|
|
// 直边检测:在 |k| <= fMaxSlope 的斜率范围内搜索“带宽 ±fTol 内点数最多”的直线:
|
|
|
|
|
|
|
|
// 主体直边段的点最多且共线 → 被选中;把手段(点数更少)、角部过渡/另一侧边缘
|
|
|
|
|
|
|
|
// (斜率超出限制)等都不会获胜。随后在带内点集上做最小二乘精修(两轮收紧容差)。
|
|
|
|
|
|
|
|
// 可调参数(应对实际情况):
|
|
|
|
|
|
|
|
// fTol:带宽容差(工作图像素)。调大→主体边缘带变宽,浅把手更容易被并入主体(漏检)、
|
|
|
|
|
|
|
|
// 但更能压住量化台阶毛刺;调小→台阶噪声可能分裂支撑或拟合到非主体结构。
|
|
|
|
|
|
|
|
// 默认由调用方按“量化台阶+2px”传入(见 GetMaskProtrusions 第 5 步)。
|
|
|
|
|
|
|
|
// fMaxSlope:直线搜索斜率上限。调大→允许产品倾斜更大;过大时角部斜过渡段可能
|
|
|
|
|
|
|
|
// 被当成直边;调小→产品倾斜稍大时拟合失败(退回常量线)。
|
|
|
|
|
|
|
|
// 点集为空返回 false;点太少/无有效直线时退回常量线(值取中位数)
|
|
|
|
|
|
|
|
static bool FitEdgeLine(const std::vector<cv::Point2f> &pts, double fTol, double fMaxSlope, double &fK, double &fB)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
if (pts.empty())
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// 常量线兜底:值取中位数
|
|
|
|
|
|
|
|
fK = 0.0;
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
std::vector<float> valList;
|
|
|
|
|
|
|
|
valList.reserve(pts.size());
|
|
|
|
|
|
|
|
for (size_t i = 0; i < pts.size(); i++)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
valList.push_back(pts[i].y);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
std::nth_element(valList.begin(), valList.begin() + valList.size() / 2, valList.end());
|
|
|
|
|
|
|
|
fB = (double)valList[valList.size() / 2];
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (pts.size() < 16)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
return true; // 点太少:用常量线兜底
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 1、粗搜索:斜率网格 × 抽样锚点,取带内点数最多的直线
|
|
|
|
|
|
|
|
int nBestCnt = -1;
|
|
|
|
|
|
|
|
double fBestK = 0.0;
|
|
|
|
|
|
|
|
double fBestB = fB;
|
|
|
|
|
|
|
|
const size_t nStride = std::max<size_t>(1, pts.size() / 64);
|
|
|
|
|
|
|
|
auto fnCount = [&](double fk, double fb) -> int {
|
|
|
|
|
|
|
|
int nCnt = 0;
|
|
|
|
|
|
|
|
for (size_t i = 0; i < pts.size(); i++)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
if (std::fabs((double)pts[i].y - (fk * pts[i].x + fb)) <= fTol)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
nCnt++;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return nCnt;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
const int nCoarseN = (int)(fMaxSlope / 0.03 + 0.5); // 斜率粗网格:±nCoarseN×0.03
|
|
|
|
|
|
|
|
for (int nk = -nCoarseN; nk <= nCoarseN; nk++)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
double fk = nk * 0.03;
|
|
|
|
|
|
|
|
for (size_t i = 0; i < pts.size(); i += nStride)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
double fb = (double)pts[i].y - fk * (double)pts[i].x;
|
|
|
|
|
|
|
|
int nCnt = fnCount(fk, fb);
|
|
|
|
|
|
|
|
if (nCnt > nBestCnt)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
nBestCnt = nCnt;
|
|
|
|
|
|
|
|
fBestK = fk;
|
|
|
|
|
|
|
|
fBestB = fb;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// 2、细搜索:在粗搜索最优斜率附近收紧网格
|
|
|
|
|
|
|
|
for (int nk = -6; nk <= 6; nk++)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
double fk = fBestK + nk * 0.005;
|
|
|
|
|
|
|
|
for (size_t i = 0; i < pts.size(); i += nStride)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
double fb = (double)pts[i].y - fk * (double)pts[i].x;
|
|
|
|
|
|
|
|
int nCnt = fnCount(fk, fb);
|
|
|
|
|
|
|
|
if (nCnt > nBestCnt)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
nBestCnt = nCnt;
|
|
|
|
|
|
|
|
fBestK = fk;
|
|
|
|
|
|
|
|
fBestB = fb;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 3、带内点最小二乘精修(两轮收紧容差)
|
|
|
|
|
|
|
|
fK = fBestK;
|
|
|
|
|
|
|
|
fB = fBestB;
|
|
|
|
|
|
|
|
double fUseTol = fTol;
|
|
|
|
|
|
|
|
for (int nIter = 0; nIter < 2; nIter++)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
std::vector<cv::Point2f> inl;
|
|
|
|
|
|
|
|
inl.reserve(pts.size());
|
|
|
|
|
|
|
|
for (size_t i = 0; i < pts.size(); i++)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
if (std::fabs((double)pts[i].y - (fK * pts[i].x + fB)) <= fUseTol)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
inl.push_back(pts[i]);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (inl.size() < 8)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
cv::Vec4f line;
|
|
|
|
|
|
|
|
cv::fitLine(inl, line, cv::DIST_L2, 0, 1e-3, 1e-3);
|
|
|
|
|
|
|
|
if (std::fabs((double)line[0]) > 1e-6)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
fK = (double)line[1] / (double)line[0];
|
|
|
|
|
|
|
|
fB = (double)line[3] - fK * (double)line[2];
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
fUseTol = std::max(3.0, fUseTol * 0.5);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 从外轮廓掩膜(outer定位mask,如bigmask)中分离凸起区域(如把手):
|
|
|
|
|
|
|
|
// 产品主体是圆角矩形、把手基本出现在四角附近,把手在掩膜上表现为超出主体四条边缘的外凸块:
|
|
|
|
|
|
|
|
// 1、取最大连通域为主体;
|
|
|
|
|
|
|
|
// 2、逐行/逐列扫描主体边界,得到 左/右(x=f(y))、上/下(y=f(x)) 四条边缘的边界点集;
|
|
|
|
|
|
|
|
// 3、对每条边缘做直线拟合(见 FitEdgeLine:取带内点数最多的直线,允许产品小角度倾斜);
|
|
|
|
|
|
|
|
// 4、掩膜中超出四条边缘(超过容差带)的像素按连通域切分成凸起区域,
|
|
|
|
|
|
|
|
// 并按质心所在象限给出就近角(0 左上 1 右上 2 左下 3 右下);深度/面积不足的量化毛刺被过滤。
|
|
|
|
|
|
|
|
// 可调参数(应对实际情况):
|
|
|
|
|
|
|
|
// nMinDepth:凸起最小深度(输入图像素;<=0 时按掩膜短边/150 自动取值,下限 6)
|
|
|
|
|
|
|
|
// 调大:只报明显外凸的把手,浅把手会漏检,可压住边缘毛刺/噪声;
|
|
|
|
|
|
|
|
// 调小:浅凸起与噪声都易报出(误报变多)。mask 由模型输出放大而来时直边带量化台阶
|
|
|
|
|
|
|
|
// (台阶高≈量化步长),建议取 2 个量化步长左右(调用方见 CheckHandleByBigMask)。
|
|
|
|
|
|
|
|
// nMinArea:凸起最小面积(输入图像素²;<=0 时按 nMinDepth² 自动取值)
|
|
|
|
|
|
|
|
// 调大:小碎块被过滤,把手很小或被遮挡一部分时可能漏检;
|
|
|
|
|
|
|
|
// 调小:碎块残留为候选的几率变大(仍会被 nMinDepth 再过滤一遍)。
|
|
|
|
|
|
|
|
// nEdgeMargin:边缘外扩量(输入图像素;<=0 时自动取“最小深度的一半”)
|
|
|
|
|
|
|
|
// 把四条边缘线整体向外推该距离后再判凸起,等效于把凸起区域相应减小:
|
|
|
|
|
|
|
|
// 调大:边缘顺边毛刺/线条偏差被吞掉,凸起区域更小更干净;过大时浅把手漏检;
|
|
|
|
|
|
|
|
// 调小:更灵敏,但边缘毛刺容易连进凸起区域。
|
|
|
|
|
|
|
|
// 边缘拟合的细节参数(带宽容差/斜率上限)见 FitEdgeLine 注释。
|
|
|
|
|
|
|
|
// pBodyQuad:可选输出,拟合出的主体四边形(左上/右上/右下/左下,输入图坐标),仅 -s 存图观察用
|
|
|
|
|
|
|
|
// 返回:0 成功、1 掩膜为空、2 无有效前景
|
|
|
|
int CheckUtil::GetMaskProtrusions(const cv::Mat &binMask, std::vector<Mask_Protrusion_> &protrusions,
|
|
|
|
int CheckUtil::GetMaskProtrusions(const cv::Mat &binMask, std::vector<Mask_Protrusion_> &protrusions,
|
|
|
|
int nJumpThresh, int nMinRun, int nMinDepth, int nMaxRun)
|
|
|
|
int nMinDepth, int nMinArea, std::vector<cv::Point2f> *pBodyQuad, int nEdgeMargin)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
protrusions.clear();
|
|
|
|
protrusions.clear();
|
|
|
|
if (binMask.empty())
|
|
|
|
if (binMask.empty())
|
|
|
|
@ -742,7 +880,17 @@ int CheckUtil::GetMaskProtrusions(const cv::Mat &binMask, std::vector<Mask_Protr
|
|
|
|
cv::Mat maskFull;
|
|
|
|
cv::Mat maskFull;
|
|
|
|
cv::threshold(gray, maskFull, 0, 255, cv::THRESH_BINARY);
|
|
|
|
cv::threshold(gray, maskFull, 0, 255, cv::THRESH_BINARY);
|
|
|
|
|
|
|
|
|
|
|
|
// 1.1 大掩膜先降采样再分析:凸起尺寸远大于降采样误差,可显著降低耗时
|
|
|
|
// 参数默认值按输入图尺度取
|
|
|
|
|
|
|
|
if (nMinDepth <= 0)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
nMinDepth = std::max(6, std::min(maskFull.cols, maskFull.rows) / 150);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (nMinArea <= 0)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
nMinArea = std::max(64, nMinDepth * nMinDepth);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 2、大掩膜先降采样再分析:凸起尺寸远大于降采样误差,可显著降低耗时
|
|
|
|
int nScale = 1;
|
|
|
|
int nScale = 1;
|
|
|
|
while (nScale < 4 && std::max(maskFull.cols, maskFull.rows) / nScale > 2000)
|
|
|
|
while (nScale < 4 && std::max(maskFull.cols, maskFull.rows) / nScale > 2000)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
@ -757,12 +905,11 @@ int CheckUtil::GetMaskProtrusions(const cv::Mat &binMask, std::vector<Mask_Protr
|
|
|
|
{
|
|
|
|
{
|
|
|
|
mask = maskFull;
|
|
|
|
mask = maskFull;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// 阈值按降采样比例换算到工作图尺度
|
|
|
|
// 阈值按比例换算到工作图尺度
|
|
|
|
nJumpThresh = std::max(2, nJumpThresh / nScale);
|
|
|
|
const int nMinDepthWork = std::max(2, nMinDepth / nScale);
|
|
|
|
nMinRun = std::max(3, nMinRun / nScale);
|
|
|
|
const int nMinAreaWork = std::max(4, nMinArea / (nScale * nScale));
|
|
|
|
nMinDepth = std::max(2, nMinDepth / nScale);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 2、只保留最大连通域,避免背景杂物参与边界统计
|
|
|
|
// 3、只保留最大连通域作为主体(过滤背景杂物)
|
|
|
|
cv::Mat labels, stats, centroids;
|
|
|
|
cv::Mat labels, stats, centroids;
|
|
|
|
int nLabels = cv::connectedComponentsWithStats(mask, labels, stats, centroids, 8, CV_32S);
|
|
|
|
int nLabels = cv::connectedComponentsWithStats(mask, labels, stats, centroids, 8, CV_32S);
|
|
|
|
if (nLabels <= 1)
|
|
|
|
if (nLabels <= 1)
|
|
|
|
@ -780,8 +927,6 @@ int CheckUtil::GetMaskProtrusions(const cv::Mat &binMask, std::vector<Mask_Protr
|
|
|
|
nMainIdx = i;
|
|
|
|
nMainIdx = i;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3、裁剪到主体外接矩形,缩小后续扫描范围
|
|
|
|
|
|
|
|
cv::Rect boxMain(stats.at<int>(nMainIdx, cv::CC_STAT_LEFT), stats.at<int>(nMainIdx, cv::CC_STAT_TOP),
|
|
|
|
cv::Rect boxMain(stats.at<int>(nMainIdx, cv::CC_STAT_LEFT), stats.at<int>(nMainIdx, cv::CC_STAT_TOP),
|
|
|
|
stats.at<int>(nMainIdx, cv::CC_STAT_WIDTH), stats.at<int>(nMainIdx, cv::CC_STAT_HEIGHT));
|
|
|
|
stats.at<int>(nMainIdx, cv::CC_STAT_WIDTH), stats.at<int>(nMainIdx, cv::CC_STAT_HEIGHT));
|
|
|
|
boxMain &= cv::Rect(0, 0, mask.cols, mask.rows);
|
|
|
|
boxMain &= cv::Rect(0, 0, mask.cols, mask.rows);
|
|
|
|
@ -789,201 +934,216 @@ int CheckUtil::GetMaskProtrusions(const cv::Mat &binMask, std::vector<Mask_Protr
|
|
|
|
{
|
|
|
|
{
|
|
|
|
return 2;
|
|
|
|
return 2;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
cv::Mat sub = (labels(boxMain) == nMainIdx); // 0/255
|
|
|
|
cv::Mat mainMask;
|
|
|
|
const int H = sub.rows;
|
|
|
|
cv::compare(labels(boxMain), nMainIdx, mainMask, cv::CMP_EQ); // 主体连通域(0/255)
|
|
|
|
const int W = sub.cols;
|
|
|
|
const int H = boxMain.height;
|
|
|
|
if (nMaxRun <= 0)
|
|
|
|
const int W = boxMain.width;
|
|
|
|
{
|
|
|
|
|
|
|
|
nMaxRun = std::max(100, std::min(H, W) / 3);
|
|
|
|
// 4、逐行/逐列扫描主体边界,收集四条边缘的边界点集(工作图坐标)
|
|
|
|
}
|
|
|
|
std::vector<cv::Point2f> ptsL, ptsR, ptsT, ptsB;
|
|
|
|
|
|
|
|
ptsL.reserve(H);
|
|
|
|
// 4、逐边抠凸起:
|
|
|
|
ptsR.reserve(H);
|
|
|
|
// 主体近似矩形,矩形边上的凸起会让该边的"边界值"先突变到外侧(进入凸起)、再突变回主体(离开凸起);
|
|
|
|
ptsT.reserve(W);
|
|
|
|
// 区间内的边界值相对"首尾边界值线性插值"得到的基线持续外偏,即判定为一个凸起。
|
|
|
|
ptsB.reserve(W);
|
|
|
|
for (int nSide = 0; nSide < 4; nSide++)
|
|
|
|
for (int y = 0; y < H; y++)
|
|
|
|
{
|
|
|
|
|
|
|
|
const bool bRowScan = (nSide < 2); // 左右边按行扫描,上下边按列扫描
|
|
|
|
|
|
|
|
const int nLine = bRowScan ? H : W; // 扫描的行数/列数
|
|
|
|
|
|
|
|
const int nDir = (nSide == 0 || nSide == 2) ? 1 : -1; // 凸起内边界值变小(左侧/上侧)取 +1,变大取 -1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 4.1 从每条 行/列 的外侧向内找第一个前景像素,得到该边的边界值 profile
|
|
|
|
|
|
|
|
std::vector<int> posList; // 有前景的行号/列号
|
|
|
|
|
|
|
|
std::vector<int> valList; // 对应的边界值
|
|
|
|
|
|
|
|
posList.reserve(nLine);
|
|
|
|
|
|
|
|
valList.reserve(nLine);
|
|
|
|
|
|
|
|
for (int i = 0; i < nLine; i++)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
int nV = -1;
|
|
|
|
|
|
|
|
if (nSide == 0) // 左:自左向右
|
|
|
|
|
|
|
|
{
|
|
|
|
{
|
|
|
|
const uchar *p = sub.ptr<uchar>(i);
|
|
|
|
const uchar *p = mainMask.ptr<uchar>(y);
|
|
|
|
|
|
|
|
int nXL = -1;
|
|
|
|
|
|
|
|
int nXR = -1;
|
|
|
|
for (int x = 0; x < W; x++)
|
|
|
|
for (int x = 0; x < W; x++)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
if (p[x])
|
|
|
|
if (p[x]) { nXL = x; break; }
|
|
|
|
{
|
|
|
|
|
|
|
|
nV = x;
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (nSide == 1) // 右:自右向左
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
const uchar *p = sub.ptr<uchar>(i);
|
|
|
|
|
|
|
|
for (int x = W - 1; x >= 0; x--)
|
|
|
|
for (int x = W - 1; x >= 0; x--)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
if (p[x])
|
|
|
|
if (p[x]) { nXR = x; break; }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (nXL >= 0)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
nV = x;
|
|
|
|
ptsL.emplace_back((float)(y + boxMain.y), (float)(nXL + boxMain.x)); // (行号, 左边界的x)
|
|
|
|
break;
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (nXR >= 0)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
ptsR.emplace_back((float)(y + boxMain.y), (float)(nXR + boxMain.x)); // (行号, 右边界的x)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (nSide == 2) // 上:自上向下
|
|
|
|
for (int x = 0; x < W; x++)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
|
|
|
|
int nYT = -1;
|
|
|
|
|
|
|
|
int nYB = -1;
|
|
|
|
for (int y = 0; y < H; y++)
|
|
|
|
for (int y = 0; y < H; y++)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
if (sub.ptr<uchar>(y)[i])
|
|
|
|
if (mainMask.ptr<uchar>(y)[x]) { nYT = y; break; }
|
|
|
|
{
|
|
|
|
|
|
|
|
nV = y;
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
else // 下:自下向上
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
for (int y = H - 1; y >= 0; y--)
|
|
|
|
for (int y = H - 1; y >= 0; y--)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
if (sub.ptr<uchar>(y)[i])
|
|
|
|
if (mainMask.ptr<uchar>(y)[x]) { nYB = y; break; }
|
|
|
|
{
|
|
|
|
|
|
|
|
nV = y;
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (nYT >= 0)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
ptsT.emplace_back((float)(x + boxMain.x), (float)(nYT + boxMain.y)); // (列号, 上边界的y)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (nV >= 0)
|
|
|
|
if (nYB >= 0)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
posList.push_back(i);
|
|
|
|
ptsB.emplace_back((float)(x + boxMain.x), (float)(nYB + boxMain.y)); // (列号, 下边界的y)
|
|
|
|
valList.push_back(nV);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const int n = (int)valList.size();
|
|
|
|
// 5、拟合四条边缘直线:左/右 x=f(y)、上/下 y=f(x)
|
|
|
|
if (n < nMinRun + 2)
|
|
|
|
// 直边判定:|k|<=0.6(近轴对齐)且带内(±fTol)点数最多的直线;fTol 覆盖量化台阶+网格误差
|
|
|
|
|
|
|
|
const int nFitStep = std::max(1, nMinDepthWork / 2); // 量化台阶估计(工作图)
|
|
|
|
|
|
|
|
// nExp:边缘外扩量(工作图像素)。显式给定(nEdgeMargin>0)时把边缘线整体向外推该距离,
|
|
|
|
|
|
|
|
// 等效于减小凸起区域;未给定时按最小深度的一半自动取值
|
|
|
|
|
|
|
|
const int nExp = (nEdgeMargin > 0) ? std::max(1, nEdgeMargin / nScale) : nFitStep;
|
|
|
|
|
|
|
|
const double fTol = std::max(6.0, (double)(nFitStep + 2));
|
|
|
|
|
|
|
|
double fKL = 0, fBL = 0, fKR = 0, fBR = 0, fKT = 0, fBT = 0, fKB = 0, fBB = 0;
|
|
|
|
|
|
|
|
if (!FitEdgeLine(ptsL, fTol, 0.6, fKL, fBL) || !FitEdgeLine(ptsR, fTol, 0.6, fKR, fBR) ||
|
|
|
|
|
|
|
|
!FitEdgeLine(ptsT, fTol, 0.6, fKT, fBT) || !FitEdgeLine(ptsB, fTol, 0.6, fKB, fBB))
|
|
|
|
{
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
return 2;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 主体中心(工作图坐标)与边缘合理性检查
|
|
|
|
// 4.2 配对"进入/离开"突变,扣除该边上的凸起
|
|
|
|
const float fXc = boxMain.x + W * 0.5f;
|
|
|
|
int nUsedTo = -1; // 已统计过的凸起末端,避免同一个凸起重复输出
|
|
|
|
const float fYc = boxMain.y + H * 0.5f;
|
|
|
|
for (int s = 0; s + 1 < n; s++)
|
|
|
|
if ((float)(fKR * fYc + fBR) - (float)(fKL * fYc + fBL) < 4.0f ||
|
|
|
|
|
|
|
|
(float)(fKB * fXc + fBB) - (float)(fKT * fXc + fBT) < 4.0f)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
if (s <= nUsedTo)
|
|
|
|
return 2;
|
|
|
|
{
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// 进入凸起:边界值朝外侧突变
|
|
|
|
// 可选输出:主体四边形(四角=相邻两条边缘直线的交点),供 -s 存图观察拟合效果;
|
|
|
|
if ((valList[s + 1] - valList[s]) * nDir >= -nJumpThresh)
|
|
|
|
// 左/右边缘: x = k*y + b;上/下边缘: y = k*x + b,联立求交(行列式≈0 说明两线平行,放弃)
|
|
|
|
|
|
|
|
if (pBodyQuad != NULL)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
pBodyQuad->clear();
|
|
|
|
}
|
|
|
|
auto fnCross = [](double fkX, double fbX, double fkY, double fbY, cv::Point2f &pt) -> bool {
|
|
|
|
// 离开凸起:在其后 nMaxRun 范围内找第一个反向突变
|
|
|
|
double fDet = 1.0 - fkX * fkY;
|
|
|
|
int e = -1;
|
|
|
|
if (std::fabs(fDet) < 1e-6)
|
|
|
|
for (int j = s + nMinRun; j + 1 < n && j - s <= nMaxRun; j++)
|
|
|
|
|
|
|
|
{
|
|
|
|
{
|
|
|
|
if ((valList[j + 1] - valList[j]) * nDir > nJumpThresh)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
pt.x = (float)((fbX + fkX * fbY) / fDet);
|
|
|
|
|
|
|
|
pt.y = (float)(fkY * pt.x + fbY);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
cv::Point2f ptTL, ptTR, ptBR, ptBL;
|
|
|
|
|
|
|
|
if (fnCross(fKL, fBL, fKT, fBT, ptTL) && fnCross(fKR, fBR, fKT, fBT, ptTR) &&
|
|
|
|
|
|
|
|
fnCross(fKR, fBR, fKB, fBB, ptBR) && fnCross(fKL, fBL, fKB, fBB, ptBL))
|
|
|
|
{
|
|
|
|
{
|
|
|
|
e = j;
|
|
|
|
// 工作图坐标 -> 输入图坐标
|
|
|
|
break;
|
|
|
|
pBodyQuad->push_back(cv::Point2f(ptTL.x * nScale, ptTL.y * nScale)); // 左上
|
|
|
|
|
|
|
|
pBodyQuad->push_back(cv::Point2f(ptTR.x * nScale, ptTR.y * nScale)); // 右上
|
|
|
|
|
|
|
|
pBodyQuad->push_back(cv::Point2f(ptBR.x * nScale, ptBR.y * nScale)); // 右下
|
|
|
|
|
|
|
|
pBodyQuad->push_back(cv::Point2f(ptBL.x * nScale, ptBL.y * nScale)); // 左下
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (e < 0)
|
|
|
|
|
|
|
|
|
|
|
|
// 6、凸起 = 超出四条边缘(超过容差带 nExp)的掩膜像素;容差带吸收边缘量化毛刺,
|
|
|
|
|
|
|
|
// 并可由 nEdgeMargin 显式加大(边缘线整体外推)来压制顺边毛刺/减小凸起区域
|
|
|
|
|
|
|
|
cv::Mat protrMask = cv::Mat::zeros(mask.size(), CV_8UC1);
|
|
|
|
|
|
|
|
for (int y = boxMain.y; y < boxMain.y + H; y++)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
const uchar *pSrc = mainMask.ptr<uchar>(y - boxMain.y);
|
|
|
|
|
|
|
|
uchar *pDst = protrMask.ptr<uchar>(y);
|
|
|
|
|
|
|
|
const float fLineL = (float)(fKL * y + fBL) - nExp;
|
|
|
|
|
|
|
|
const float fLineR = (float)(fKR * y + fBR) + nExp;
|
|
|
|
|
|
|
|
for (int x = boxMain.x; x < boxMain.x + W; x++)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
if (!pSrc[x - boxMain.x])
|
|
|
|
{
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (x < fLineL || x > fLineR)
|
|
|
|
// 4.3 区间内的边界值应持续超出"首尾边界值线性插值"得到的基线
|
|
|
|
|
|
|
|
const int kk = e - s;
|
|
|
|
|
|
|
|
int nDevOk = 0;
|
|
|
|
|
|
|
|
float fMaxDev = 0.0f;
|
|
|
|
|
|
|
|
int nLow = valList[s + 1];
|
|
|
|
|
|
|
|
int nHigh = valList[s + 1];
|
|
|
|
|
|
|
|
for (int i = 1; i <= kk; i++)
|
|
|
|
|
|
|
|
{
|
|
|
|
{
|
|
|
|
float fBase = valList[s] + (valList[e + 1] - valList[s]) * (float)i / (float)(kk + 1);
|
|
|
|
pDst[x] = 255;
|
|
|
|
float fDev = (fBase - valList[s + i]) * nDir;
|
|
|
|
continue;
|
|
|
|
fMaxDev = std::max(fMaxDev, fDev);
|
|
|
|
}
|
|
|
|
if (fDev >= nMinDepth)
|
|
|
|
if (y < (float)(fKT * x + fBT) - nExp || y > (float)(fKB * x + fBB) + nExp)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
nDevOk++;
|
|
|
|
pDst[x] = 255;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
nLow = std::min(nLow, valList[s + i]);
|
|
|
|
|
|
|
|
nHigh = std::max(nHigh, valList[s + i]);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (fMaxDev < nMinDepth)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// 凸起内大部分采样点都要明显外偏,避免把主体的斜边/圆角当成凸起
|
|
|
|
|
|
|
|
if (nDevOk < (int)(kk * 0.8f))
|
|
|
|
// 7、按连通域切分成凸起区域:深度(相对四条边缘)/面积不足的毛刺过滤
|
|
|
|
|
|
|
|
cv::Mat labelsP, statsP, centP;
|
|
|
|
|
|
|
|
int nP = cv::connectedComponentsWithStats(protrMask, labelsP, statsP, centP, 8, CV_32S);
|
|
|
|
|
|
|
|
if (nP <= 1)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
return 0; // 无凸起
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 4.4 计算凸起外接矩形(先算在 sub 图上的位置)
|
|
|
|
for (int i = 1; i < nP; i++)
|
|
|
|
const int nLen = posList[e] - posList[s + 1] + 1; // 沿该边的长度
|
|
|
|
|
|
|
|
cv::Rect roiSub;
|
|
|
|
|
|
|
|
if (nSide == 0 || nSide == 2) // 左/上:外侧取小值,主体边缘取大值
|
|
|
|
|
|
|
|
{
|
|
|
|
{
|
|
|
|
int nOut = nLow;
|
|
|
|
int nArea = statsP.at<int>(i, cv::CC_STAT_AREA);
|
|
|
|
int nInner = std::max(valList[s], valList[e + 1]);
|
|
|
|
if (nArea < nMinAreaWork)
|
|
|
|
if (nSide == 0)
|
|
|
|
|
|
|
|
{
|
|
|
|
{
|
|
|
|
roiSub = cv::Rect(nOut, posList[s + 1], nInner - nOut + 1, nLen);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
int nX = statsP.at<int>(i, cv::CC_STAT_LEFT);
|
|
|
|
|
|
|
|
int nY = statsP.at<int>(i, cv::CC_STAT_TOP);
|
|
|
|
|
|
|
|
int nW = statsP.at<int>(i, cv::CC_STAT_WIDTH);
|
|
|
|
|
|
|
|
int nH = statsP.at<int>(i, cv::CC_STAT_HEIGHT);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 深度:组件像素相对四条边缘线的最大外伸(工作图像素)
|
|
|
|
|
|
|
|
float fDepth = 0.0f;
|
|
|
|
|
|
|
|
for (int yy = nY; yy < nY + nH; yy++)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
roiSub = cv::Rect(posList[s + 1], nOut, nLen, nInner - nOut + 1);
|
|
|
|
const int *pLab = labelsP.ptr<int>(yy);
|
|
|
|
}
|
|
|
|
const float fLineL = (float)(fKL * yy + fBL);
|
|
|
|
}
|
|
|
|
const float fLineR = (float)(fKR * yy + fBR);
|
|
|
|
else // 右/下:外侧取大值,主体边缘取小值
|
|
|
|
for (int xx = nX; xx < nX + nW; xx++)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
int nOut = nHigh;
|
|
|
|
if (pLab[xx] != i)
|
|
|
|
int nInner = std::min(valList[s], valList[e + 1]);
|
|
|
|
|
|
|
|
if (nSide == 1)
|
|
|
|
|
|
|
|
{
|
|
|
|
{
|
|
|
|
roiSub = cv::Rect(nInner, posList[s + 1], nOut - nInner + 1, nLen);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
// 到四条边缘的最大外伸(在边缘内侧为负,取 0 忽略)
|
|
|
|
{
|
|
|
|
float fDev = std::max(fLineL - xx, xx - fLineR);
|
|
|
|
roiSub = cv::Rect(posList[s + 1], nInner, nLen, nOut - nInner + 1);
|
|
|
|
fDev = std::max(fDev, (float)(fKT * xx + fBT) - yy);
|
|
|
|
|
|
|
|
fDev = std::max(fDev, yy - (float)(fKB * xx + fBB));
|
|
|
|
|
|
|
|
fDepth = std::max(fDepth, fDev);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
roiSub &= cv::Rect(0, 0, W, H);
|
|
|
|
if (fDepth < nMinDepthWork)
|
|
|
|
if (roiSub.width <= 0 || roiSub.height <= 0)
|
|
|
|
|
|
|
|
{
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 特征 + 坐标(工作图 -> 输入图)
|
|
|
|
Mask_Protrusion_ protr;
|
|
|
|
Mask_Protrusion_ protr;
|
|
|
|
protr.nSide = nSide;
|
|
|
|
protr.roi = cv::Rect(nX * nScale, nY * nScale, nW * nScale, nH * nScale);
|
|
|
|
// 从工作图尺度还原到输入图尺度
|
|
|
|
|
|
|
|
protr.roi = cv::Rect((roiSub.x + boxMain.x) * nScale, (roiSub.y + boxMain.y) * nScale,
|
|
|
|
|
|
|
|
roiSub.width * nScale, roiSub.height * nScale);
|
|
|
|
|
|
|
|
protr.roi &= cv::Rect(0, 0, binMask.cols, binMask.rows);
|
|
|
|
protr.roi &= cv::Rect(0, 0, binMask.cols, binMask.rows);
|
|
|
|
if (protr.roi.width <= 0 || protr.roi.height <= 0)
|
|
|
|
if (protr.roi.width <= 0 || protr.roi.height <= 0)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
protr.fLen = (float)nLen * nScale;
|
|
|
|
cv::Point2f ptCen;
|
|
|
|
protr.fDepth = fMaxDev * nScale;
|
|
|
|
if (centP.type() == CV_32F)
|
|
|
|
int nMaskArea = cv::countNonZero(sub(roiSub));
|
|
|
|
{
|
|
|
|
protr.fArea = (float)nMaskArea * nScale * nScale;
|
|
|
|
ptCen = centP.at<cv::Point2f>(i);
|
|
|
|
protr.fFillRatio = (float)nMaskArea / (float)(roiSub.width * roiSub.height);
|
|
|
|
}
|
|
|
|
|
|
|
|
else
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
cv::Vec2d vCen = centP.at<cv::Vec2d>(i);
|
|
|
|
|
|
|
|
ptCen = cv::Point2f((float)vCen[0], (float)vCen[1]);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
protr.ptCenter = cv::Point2f(ptCen.x * nScale, ptCen.y * nScale);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 就近角:按主体中心分象限 -> 0 左上 1 右上 2 左下 3 右下
|
|
|
|
|
|
|
|
bool bRight = ptCen.x > fXc;
|
|
|
|
|
|
|
|
bool bBottom = ptCen.y > fYc;
|
|
|
|
|
|
|
|
protr.nCorner = bBottom ? (bRight ? 3 : 2) : (bRight ? 1 : 0);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protr.fAngle = (float)(std::atan((fKL + fKR) * 0.5) * 180.0 / CV_PI); // 左右边缘平均倾角(度,相对垂直方向)
|
|
|
|
|
|
|
|
protr.fLen = (float)std::max(nW, nH) * nScale;
|
|
|
|
|
|
|
|
protr.fDepth = fDepth * nScale;
|
|
|
|
|
|
|
|
protr.fArea = (float)nArea * nScale * nScale;
|
|
|
|
|
|
|
|
protr.fFillRatio = (float)nArea / (float)(nW * nH);
|
|
|
|
protrusions.push_back(protr);
|
|
|
|
protrusions.push_back(protr);
|
|
|
|
nUsedTo = e; // 该凸起区间不再重复统计
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 按面积从大到小排序(日志与匹配更稳定)
|
|
|
|
|
|
|
|
std::sort(protrusions.begin(), protrusions.end(),
|
|
|
|
|
|
|
|
[](const Mask_Protrusion_ &a, const Mask_Protrusion_ &b) { return a.fArea > b.fArea; });
|
|
|
|
return 0;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
}
|