|
|
|
|
@ -19,6 +19,9 @@
|
|
|
|
|
#include <stdlib.h>
|
|
|
|
|
#include <fstream>
|
|
|
|
|
#include <thread>
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <cmath>
|
|
|
|
|
#include <fcntl.h>
|
|
|
|
|
#include "snowflake.hpp"
|
|
|
|
|
|
|
|
|
|
int _sysmkdir_2(const std::string &dir)
|
|
|
|
|
@ -67,8 +70,10 @@ std::string CheckUtil::getCurrentDate()
|
|
|
|
|
// 将 tv_sec 转换为 time_t 类型,代表自1970年1月1日起的秒数
|
|
|
|
|
time_t raw_time = tv.tv_sec;
|
|
|
|
|
|
|
|
|
|
// 使用 localtime 将 time_t 转换为 tm 结构体
|
|
|
|
|
struct tm *time_info = localtime(&raw_time);
|
|
|
|
|
// 使用 localtime_r 转换为 tm 结构体。
|
|
|
|
|
// 注意:localtime() 返回共享的静态缓冲区,多线程并发调用会互相覆盖(tm 内容串数据)。
|
|
|
|
|
struct tm time_buf;
|
|
|
|
|
struct tm *time_info = localtime_r(&raw_time, &time_buf);
|
|
|
|
|
|
|
|
|
|
// 使用字符串流格式化为 "YYYY-MM-DD"
|
|
|
|
|
std::ostringstream oss;
|
|
|
|
|
@ -78,20 +83,41 @@ std::string CheckUtil::getCurrentDate()
|
|
|
|
|
|
|
|
|
|
return oss.str(); // 返回日期字符串
|
|
|
|
|
}
|
|
|
|
|
std::string CheckUtil::getCurrentDateYMD()
|
|
|
|
|
{
|
|
|
|
|
struct timeval tv;
|
|
|
|
|
gettimeofday(&tv, NULL);
|
|
|
|
|
|
|
|
|
|
time_t raw_time = tv.tv_sec;
|
|
|
|
|
// localtime_r 为线程安全版本
|
|
|
|
|
struct tm time_buf;
|
|
|
|
|
struct tm *time_info = localtime_r(&raw_time, &time_buf);
|
|
|
|
|
|
|
|
|
|
// 固定 8 位 "YYYYMMDD", 便于文件名检索与排序
|
|
|
|
|
std::ostringstream oss;
|
|
|
|
|
oss << std::setfill('0')
|
|
|
|
|
<< std::setw(4) << (time_info->tm_year + 1900)
|
|
|
|
|
<< std::setw(2) << (time_info->tm_mon + 1)
|
|
|
|
|
<< std::setw(2) << time_info->tm_mday;
|
|
|
|
|
|
|
|
|
|
return oss.str();
|
|
|
|
|
}
|
|
|
|
|
std::string CheckUtil::getCurTimeHMS()
|
|
|
|
|
{
|
|
|
|
|
struct timeval tv;
|
|
|
|
|
gettimeofday(&tv, NULL); // 获取当前时间
|
|
|
|
|
|
|
|
|
|
time_t raw_time = tv.tv_sec;
|
|
|
|
|
struct tm *time_info = localtime(&raw_time);
|
|
|
|
|
// 使用 localtime_r(线程安全);localtime() 的静态缓冲会被其它线程并发覆盖
|
|
|
|
|
struct tm time_buf;
|
|
|
|
|
struct tm *time_info = localtime_r(&raw_time, &time_buf);
|
|
|
|
|
|
|
|
|
|
int millisec = tv.tv_usec / 1000; // 微秒转毫秒
|
|
|
|
|
|
|
|
|
|
std::ostringstream oss;
|
|
|
|
|
oss << std::setfill('0') << std::setw(2) << time_info->tm_hour << ":"
|
|
|
|
|
<< std::setfill('0') << std::setw(2) << time_info->tm_min << ":"
|
|
|
|
|
<< std::setfill('0') << std::setw(2) << time_info->tm_sec << "."
|
|
|
|
|
oss << std::setfill('0') << std::setw(2) << time_info->tm_hour << "-"
|
|
|
|
|
<< std::setfill('0') << std::setw(2) << time_info->tm_min << "-"
|
|
|
|
|
<< std::setfill('0') << std::setw(2) << time_info->tm_sec << "-"
|
|
|
|
|
<< std::setfill('0') << std::setw(3) << millisec;
|
|
|
|
|
|
|
|
|
|
return oss.str(); // 返回格式 "HH:MM:SS.mmm"
|
|
|
|
|
@ -118,6 +144,40 @@ int64_t CheckUtil::getSnowId()
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string CheckUtil::GetUniqueImgPath(const std::string &strDir, const std::string &strPrefix, const std::string &strName, const std::string &strExt)
|
|
|
|
|
{
|
|
|
|
|
if (strDir.empty())
|
|
|
|
|
{
|
|
|
|
|
return strDir + strPrefix + strName + strExt;
|
|
|
|
|
}
|
|
|
|
|
// 目录不存在时 O_CREAT 会失败, 先建目录(已存在时内部无副作用)
|
|
|
|
|
CreateDir(strDir);
|
|
|
|
|
|
|
|
|
|
// 去重序号插在前缀之后: <年月日>[_序号]_<通道>_<用途>.png
|
|
|
|
|
// 同一产品的每个 strName 每轮只会写一次, 所以各文件序号天然同步:
|
|
|
|
|
// 第 1 轮 -> <年月日>_<名称>.png、第 2 轮 -> <年月日>_1_<名称>.png、第 3 轮 -> <年月日>_2_<名称>.png ...
|
|
|
|
|
const std::string strHead = strPrefix.empty() ? std::string("") : (strPrefix + "_");
|
|
|
|
|
for (int i = 0; i < 100000; i++)
|
|
|
|
|
{
|
|
|
|
|
std::string strPath = strDir + strHead + (i == 0 ? std::string("") : std::to_string(i) + "_") + strName + strExt;
|
|
|
|
|
// O_EXCL: 文件不存在才能创建成功, 即是"原子占位", 并发安全
|
|
|
|
|
int fd = ::open(strPath.c_str(), O_CREAT | O_EXCL | O_WRONLY, 0644);
|
|
|
|
|
if (fd >= 0)
|
|
|
|
|
{
|
|
|
|
|
::close(fd);
|
|
|
|
|
return strPath;
|
|
|
|
|
}
|
|
|
|
|
if (errno != EEXIST)
|
|
|
|
|
{
|
|
|
|
|
// 非"已存在"类错误(权限/路径等)不再重试, 直接返回让 imwrite 把错误暴露出来
|
|
|
|
|
printf("create img file[%s] error: %d %s\n", strPath.c_str(), errno, strerror(errno));
|
|
|
|
|
return strPath;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// 序号用尽的兜底(实际不会发生)
|
|
|
|
|
return strDir + strHead + std::to_string(CheckUtil::getcurTime()) + "_" + strName + strExt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool CheckUtil::JudgRect(cv::Rect roi, int img_w, int img_h)
|
|
|
|
|
{
|
|
|
|
|
if (img_w <= 0 || img_h <= 0)
|
|
|
|
|
@ -501,6 +561,32 @@ int CheckUtil::CreateDir(const std::string &dir)
|
|
|
|
|
return ret;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int CheckUtil::SaveImgAsync(const std::string &path, const cv::Mat &img)
|
|
|
|
|
{
|
|
|
|
|
if (path.empty() || img.empty())
|
|
|
|
|
{
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 拷贝图像,避免异步线程执行时原图像被释放或修改
|
|
|
|
|
cv::Mat imgCopy = img.clone();
|
|
|
|
|
|
|
|
|
|
// 创建保存路径的父目录
|
|
|
|
|
std::string::size_type pos = path.find_last_of('/');
|
|
|
|
|
if (pos != std::string::npos)
|
|
|
|
|
{
|
|
|
|
|
std::string parentDir = path.substr(0, pos);
|
|
|
|
|
CheckUtil::CreateDir(parentDir);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 启动后台线程异步存图
|
|
|
|
|
std::thread([path, imgCopy]() {
|
|
|
|
|
cv::imwrite(path, imgCopy);
|
|
|
|
|
}).detach();
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool CheckUtil::bcalDis(cv::Point p1, cv::Point p2, int disT)
|
|
|
|
|
{
|
|
|
|
|
double dis = std::sqrt((p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y));
|
|
|
|
|
@ -693,3 +779,410 @@ 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));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 迭代裁剪轮数:每轮"带内点 cv::fitLine(L2) 精修 + 容差收紧一半",3 轮足够收敛
|
|
|
|
|
#define FIT_EDGE_REFINE_ITERS 3
|
|
|
|
|
|
|
|
|
|
// 拟合边缘直线:val ≈ fK * idx + fB(点集为 (扫描序号, 边界值))
|
|
|
|
|
// 做法(简洁版,不再做斜率网格搜索):
|
|
|
|
|
// 1) cv::fitLine(DIST_HUBER) 初始化:Huber 是 M 估计,把手凸起/角部过渡等偏离点被自动降权,
|
|
|
|
|
// 不会像普通最小二乘那样被拉偏(旧实现靠粗/细斜率网格找带内最多点,代码长且难调);
|
|
|
|
|
// 2) 迭代裁剪 + cv::fitLine(DIST_L2) 精修:每轮只保留距当前直线 ±fUseTol 内的点做最小二乘,
|
|
|
|
|
// 容差从 fTol 逐轮收紧到 3px(与旧实现“带内点两轮收紧容差”等价,实测四条边结果与旧实现一致);
|
|
|
|
|
// 3) |k| > fMaxSlope(不是近轴直边,多半是角部斜过渡/另一侧边缘)→ 常量线兜底。
|
|
|
|
|
// 可调参数(应对实际情况):
|
|
|
|
|
// 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() < 8)
|
|
|
|
|
{
|
|
|
|
|
return true; // 点太少:用常量线兜底
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// cv::fitLine 的输出(单位方向向量 + 线上一点)换算成 v = k*t + b
|
|
|
|
|
auto fnToKB = [](const cv::Vec4f &line, double &k, double &b) -> bool {
|
|
|
|
|
if (std::fabs((double)line[0]) < 1e-6)
|
|
|
|
|
{
|
|
|
|
|
return false; // 近垂直线:v 随 t 剧烈变化,无法用 k/b 表示
|
|
|
|
|
}
|
|
|
|
|
k = (double)line[1] / (double)line[0];
|
|
|
|
|
b = (double)line[3] - k * (double)line[2];
|
|
|
|
|
return true;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 1、Huber 稳健初始化(1.345 = 高斯噪声下 ~95% 效率的标准 Huber 系数)
|
|
|
|
|
cv::Vec4f line;
|
|
|
|
|
cv::fitLine(pts, line, cv::DIST_HUBER, 1.345, 1e-3, 1e-3);
|
|
|
|
|
double fKFit = 0, fBFit = 0;
|
|
|
|
|
if (!fnToKB(line, fKFit, fBFit) || std::fabs(fKFit) > fMaxSlope)
|
|
|
|
|
{
|
|
|
|
|
return true; // 拟不出近轴直边:常量线兜底
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2、迭代裁剪 + L2 精修(容差逐轮收紧;某轮点不够或结果不合理时保留上一轮结果)
|
|
|
|
|
double fUseTol = fTol;
|
|
|
|
|
for (int nIter = 0; nIter < FIT_EDGE_REFINE_ITERS; nIter++)
|
|
|
|
|
{
|
|
|
|
|
std::vector<cv::Point2f> vecInlier;
|
|
|
|
|
vecInlier.reserve(pts.size());
|
|
|
|
|
for (size_t i = 0; i < pts.size(); i++)
|
|
|
|
|
{
|
|
|
|
|
if (std::fabs((double)pts[i].y - (fKFit * pts[i].x + fBFit)) <= fUseTol)
|
|
|
|
|
{
|
|
|
|
|
vecInlier.push_back(pts[i]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (vecInlier.size() < 8)
|
|
|
|
|
{
|
|
|
|
|
break; // 支撑点太少:保留当前结果,不再收紧
|
|
|
|
|
}
|
|
|
|
|
cv::Vec4f lineRefit;
|
|
|
|
|
cv::fitLine(vecInlier, lineRefit, cv::DIST_L2, 0, 1e-3, 1e-3);
|
|
|
|
|
double fKNew = 0, fBNew = 0;
|
|
|
|
|
if (!fnToKB(lineRefit, fKNew, fBNew) || std::fabs(fKNew) > fMaxSlope)
|
|
|
|
|
{
|
|
|
|
|
break; // 精修结果不合理:保留上一轮
|
|
|
|
|
}
|
|
|
|
|
fKFit = fKNew;
|
|
|
|
|
fBFit = fBNew;
|
|
|
|
|
fUseTol = std::max(3.0, fUseTol * 0.5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fK = fKFit;
|
|
|
|
|
fB = fBFit;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 从外轮廓掩膜(outer定位mask,如bigmask)中分离凸起区域(如把手):
|
|
|
|
|
// 产品主体是圆角矩形、把手基本出现在四角附近,把手在掩膜上表现为超出主体四条边缘的外凸块:
|
|
|
|
|
// 1、取最大连通域为主体;
|
|
|
|
|
// 2、逐行/逐列扫描主体边界,得到 左/右(x=f(y))、上/下(y=f(x)) 四条边缘的边界点集;
|
|
|
|
|
// 3、对每条边缘做直线拟合(见 FitEdgeLine:cv::fitLine(Huber) 定初值 + 迭代裁剪 + cv::fitLine(L2) 精修,
|
|
|
|
|
// 允许产品小角度倾斜);
|
|
|
|
|
// 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 nMinDepth, int nMinArea, std::vector<cv::Point2f> *pBodyQuad, int nEdgeMargin)
|
|
|
|
|
{
|
|
|
|
|
protrusions.clear();
|
|
|
|
|
if (binMask.empty())
|
|
|
|
|
{
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 1、二值化(兼容 0/1 与 0/255 掩膜)
|
|
|
|
|
cv::Mat gray;
|
|
|
|
|
if (binMask.channels() != 1)
|
|
|
|
|
{
|
|
|
|
|
cv::cvtColor(binMask, gray, cv::COLOR_BGR2GRAY);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
gray = binMask;
|
|
|
|
|
}
|
|
|
|
|
cv::Mat maskFull;
|
|
|
|
|
cv::threshold(gray, maskFull, 0, 255, cv::THRESH_BINARY);
|
|
|
|
|
|
|
|
|
|
// 参数默认值按输入图尺度取
|
|
|
|
|
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;
|
|
|
|
|
while (nScale < 4 && std::max(maskFull.cols, maskFull.rows) / nScale > 2000)
|
|
|
|
|
{
|
|
|
|
|
nScale *= 2;
|
|
|
|
|
}
|
|
|
|
|
cv::Mat mask;
|
|
|
|
|
if (nScale > 1)
|
|
|
|
|
{
|
|
|
|
|
cv::resize(maskFull, mask, cv::Size(maskFull.cols / nScale, maskFull.rows / nScale), 0, 0, cv::INTER_NEAREST);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
mask = maskFull;
|
|
|
|
|
}
|
|
|
|
|
// 阈值按比例换算到工作图尺度
|
|
|
|
|
const int nMinDepthWork = std::max(2, nMinDepth / nScale);
|
|
|
|
|
const int nMinAreaWork = std::max(4, nMinArea / (nScale * nScale));
|
|
|
|
|
|
|
|
|
|
// 3、只保留最大连通域作为主体(过滤背景杂物)
|
|
|
|
|
cv::Mat labels, stats, centroids;
|
|
|
|
|
int nLabels = cv::connectedComponentsWithStats(mask, labels, stats, centroids, 8, CV_32S);
|
|
|
|
|
if (nLabels <= 1)
|
|
|
|
|
{
|
|
|
|
|
return 2;
|
|
|
|
|
}
|
|
|
|
|
int nMainIdx = 1;
|
|
|
|
|
int nMainArea = 0;
|
|
|
|
|
for (int i = 1; i < nLabels; i++)
|
|
|
|
|
{
|
|
|
|
|
int nArea = stats.at<int>(i, cv::CC_STAT_AREA);
|
|
|
|
|
if (nArea > nMainArea)
|
|
|
|
|
{
|
|
|
|
|
nMainArea = nArea;
|
|
|
|
|
nMainIdx = i;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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));
|
|
|
|
|
boxMain &= cv::Rect(0, 0, mask.cols, mask.rows);
|
|
|
|
|
if (boxMain.width <= 0 || boxMain.height <= 0)
|
|
|
|
|
{
|
|
|
|
|
return 2;
|
|
|
|
|
}
|
|
|
|
|
cv::Mat mainMask;
|
|
|
|
|
cv::compare(labels(boxMain), nMainIdx, mainMask, cv::CMP_EQ); // 主体连通域(0/255)
|
|
|
|
|
const int H = boxMain.height;
|
|
|
|
|
const int W = boxMain.width;
|
|
|
|
|
|
|
|
|
|
// 4、逐行/逐列扫描主体边界,收集四条边缘的边界点集(工作图坐标)
|
|
|
|
|
std::vector<cv::Point2f> ptsL, ptsR, ptsT, ptsB;
|
|
|
|
|
ptsL.reserve(H);
|
|
|
|
|
ptsR.reserve(H);
|
|
|
|
|
ptsT.reserve(W);
|
|
|
|
|
ptsB.reserve(W);
|
|
|
|
|
for (int y = 0; y < H; y++)
|
|
|
|
|
{
|
|
|
|
|
const uchar *p = mainMask.ptr<uchar>(y);
|
|
|
|
|
int nXL = -1;
|
|
|
|
|
int nXR = -1;
|
|
|
|
|
for (int x = 0; x < W; x++)
|
|
|
|
|
{
|
|
|
|
|
if (p[x]) { nXL = x; break; }
|
|
|
|
|
}
|
|
|
|
|
for (int x = W - 1; x >= 0; x--)
|
|
|
|
|
{
|
|
|
|
|
if (p[x]) { nXR = x; break; }
|
|
|
|
|
}
|
|
|
|
|
if (nXL >= 0)
|
|
|
|
|
{
|
|
|
|
|
ptsL.emplace_back((float)(y + boxMain.y), (float)(nXL + boxMain.x)); // (行号, 左边界的x)
|
|
|
|
|
}
|
|
|
|
|
if (nXR >= 0)
|
|
|
|
|
{
|
|
|
|
|
ptsR.emplace_back((float)(y + boxMain.y), (float)(nXR + boxMain.x)); // (行号, 右边界的x)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for (int x = 0; x < W; x++)
|
|
|
|
|
{
|
|
|
|
|
int nYT = -1;
|
|
|
|
|
int nYB = -1;
|
|
|
|
|
for (int y = 0; y < H; y++)
|
|
|
|
|
{
|
|
|
|
|
if (mainMask.ptr<uchar>(y)[x]) { nYT = y; break; }
|
|
|
|
|
}
|
|
|
|
|
for (int y = H - 1; y >= 0; y--)
|
|
|
|
|
{
|
|
|
|
|
if (mainMask.ptr<uchar>(y)[x]) { nYB = y; break; }
|
|
|
|
|
}
|
|
|
|
|
if (nYT >= 0)
|
|
|
|
|
{
|
|
|
|
|
ptsT.emplace_back((float)(x + boxMain.x), (float)(nYT + boxMain.y)); // (列号, 上边界的y)
|
|
|
|
|
}
|
|
|
|
|
if (nYB >= 0)
|
|
|
|
|
{
|
|
|
|
|
ptsB.emplace_back((float)(x + boxMain.x), (float)(nYB + boxMain.y)); // (列号, 下边界的y)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 5、拟合四条边缘直线:左/右 x=f(y)、上/下 y=f(x)
|
|
|
|
|
// 稳健拟合(先用 cv::fitLine(L1) 定初值,再迭代裁剪 + cv::fitLine(L2) 精修),|k|>0.6(非近轴)时退回常量线
|
|
|
|
|
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))
|
|
|
|
|
{
|
|
|
|
|
return 2;
|
|
|
|
|
}
|
|
|
|
|
// 主体中心(工作图坐标)与边缘合理性检查
|
|
|
|
|
const float fXc = boxMain.x + W * 0.5f;
|
|
|
|
|
const float fYc = boxMain.y + H * 0.5f;
|
|
|
|
|
if ((float)(fKR * fYc + fBR) - (float)(fKL * fYc + fBL) < 4.0f ||
|
|
|
|
|
(float)(fKB * fXc + fBB) - (float)(fKT * fXc + fBT) < 4.0f)
|
|
|
|
|
{
|
|
|
|
|
return 2;
|
|
|
|
|
}
|
|
|
|
|
// 可选输出:主体四边形(四角=相邻两条边缘直线的交点),供 -s 存图观察拟合效果;
|
|
|
|
|
// 左/右边缘: x = k*y + b;上/下边缘: y = k*x + b,联立求交(行列式≈0 说明两线平行,放弃)
|
|
|
|
|
if (pBodyQuad != NULL)
|
|
|
|
|
{
|
|
|
|
|
pBodyQuad->clear();
|
|
|
|
|
auto fnCross = [](double fkX, double fbX, double fkY, double fbY, cv::Point2f &pt) -> bool {
|
|
|
|
|
double fDet = 1.0 - fkX * fkY;
|
|
|
|
|
if (std::fabs(fDet) < 1e-6)
|
|
|
|
|
{
|
|
|
|
|
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))
|
|
|
|
|
{
|
|
|
|
|
// 工作图坐标 -> 输入图坐标
|
|
|
|
|
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)); // 左下
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
if (x < fLineL || x > fLineR)
|
|
|
|
|
{
|
|
|
|
|
pDst[x] = 255;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (y < (float)(fKT * x + fBT) - nExp || y > (float)(fKB * x + fBB) + nExp)
|
|
|
|
|
{
|
|
|
|
|
pDst[x] = 255;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 7、按连通域切分成凸起区域:深度(相对四条边缘)/面积不足的毛刺过滤
|
|
|
|
|
cv::Mat labelsP, statsP, centP;
|
|
|
|
|
int nP = cv::connectedComponentsWithStats(protrMask, labelsP, statsP, centP, 8, CV_32S);
|
|
|
|
|
if (nP <= 1)
|
|
|
|
|
{
|
|
|
|
|
return 0; // 无凸起
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int i = 1; i < nP; i++)
|
|
|
|
|
{
|
|
|
|
|
int nArea = statsP.at<int>(i, cv::CC_STAT_AREA);
|
|
|
|
|
if (nArea < nMinAreaWork)
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
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++)
|
|
|
|
|
{
|
|
|
|
|
const int *pLab = labelsP.ptr<int>(yy);
|
|
|
|
|
const float fLineL = (float)(fKL * yy + fBL);
|
|
|
|
|
const float fLineR = (float)(fKR * yy + fBR);
|
|
|
|
|
for (int xx = nX; xx < nX + nW; xx++)
|
|
|
|
|
{
|
|
|
|
|
if (pLab[xx] != i)
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
// 到四条边缘的最大外伸(在边缘内侧为负,取 0 忽略)
|
|
|
|
|
float fDev = std::max(fLineL - xx, xx - fLineR);
|
|
|
|
|
fDev = std::max(fDev, (float)(fKT * xx + fBT) - yy);
|
|
|
|
|
fDev = std::max(fDev, yy - (float)(fKB * xx + fBB));
|
|
|
|
|
fDepth = std::max(fDepth, fDev);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (fDepth < nMinDepthWork)
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 特征 + 坐标(工作图 -> 输入图)
|
|
|
|
|
Mask_Protrusion_ protr;
|
|
|
|
|
protr.roi = cv::Rect(nX * nScale, nY * nScale, nW * nScale, nH * nScale);
|
|
|
|
|
protr.roi &= cv::Rect(0, 0, binMask.cols, binMask.rows);
|
|
|
|
|
if (protr.roi.width <= 0 || protr.roi.height <= 0)
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
cv::Point2f ptCen;
|
|
|
|
|
if (centP.type() == CV_32F)
|
|
|
|
|
{
|
|
|
|
|
ptCen = centP.at<cv::Point2f>(i);
|
|
|
|
|
}
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 按面积从大到小排序(日志与匹配更稳定)
|
|
|
|
|
std::sort(protrusions.begin(), protrusions.end(),
|
|
|
|
|
[](const Mask_Protrusion_ &a, const Mask_Protrusion_ &b) { return a.fArea > b.fArea; });
|
|
|
|
|
return 0;
|
|
|
|
|
}
|