update 重构把手检测

dev_lsy
liusiyang 3 weeks ago
parent 2db9918acb
commit 30a04fb6bd

@ -26,18 +26,22 @@
#include <opencv2/opencv.hpp> #include <opencv2/opencv.hpp>
using namespace std; using namespace std;
// 掩膜主体(近似矩形)四周凸起(如把手)的描述 // 掩膜主体(圆角矩形)外的凸起区域(如把手)的描述
struct Mask_Protrusion_ struct Mask_Protrusion_
{ {
int nSide; // 所在边:0 左 1 右 2 上 3 下 int nCorner; // 就近角(产品主轴坐标系):0 左上 1 右上 2 左下 3 右下
cv::Rect roi; // 凸起外接矩形(输入图坐标系,已裁剪到图像范围内) cv::Rect roi; // 凸起外接矩形(输入图坐标系,已裁剪到图像范围内)
float fLen; // 沿该边的长度(像素) cv::Point2f ptCenter; // 凸起质心(输入图坐标系)
float fDepth; // 伸出主体的深度(像素) float fAngle; // 边缘倾角(度,日志排查用)
float fArea; // 凸起面积(roi 内掩膜像素数) float fLen; // 凸起外接矩形长边(像素)
float fFillRatio; // roi 内掩膜像素占比(用于判定是否"类似矩形块") float fDepth; // 凸起相对主体直边的最大伸出深度(像素)
float fArea; // 凸起面积(掩膜像素数)
float fFillRatio; // 凸起外接矩形内掩膜像素占比
Mask_Protrusion_() Mask_Protrusion_()
{ {
nSide = 0; nCorner = 0;
ptCenter = cv::Point2f(0.0f, 0.0f);
fAngle = 0.0f;
fLen = 0.0f; fLen = 0.0f;
fDepth = 0.0f; fDepth = 0.0f;
fArea = 0.0f; fArea = 0.0f;
@ -78,23 +82,28 @@ public:
// 找图片的最大外轮廓 // 找图片的最大外轮廓
static std::vector<cv::Point> getLargestContourROI(const cv::Mat &binaryImg, bool &found); static std::vector<cv::Point> getLargestContourROI(const cv::Mat &binaryImg, bool &found);
// 抠出掩膜四周的凸起:主体近似矩形,凸起在该边表现为边界值的一次突变(进入)与一次反向突变(离开) // 从外轮廓掩膜(outer定位mask)中分离凸起区域(如把手):
// binMask:掩膜(非零为前景,兼容 0/1 与 0/255);protrusions:输出的凸起列表 // 主体是圆角矩形、把手基本出现在四角附近:逐行/逐列扫描掩膜边界,为每条边缘搜索带内点数最多的直线;
// 参数调整影响(nJumpThresh/nMinRun/nMinDepth 为输入图像素,内部按降采样比例 nScale(1/2/4) 缩小后使用;nMaxRun 不换算,直接用工作图像素): // 掩膜中超出四条边缘(超过容差带)的像素按连通域切分成凸起区域(含就近角/质心/面积/深度/填充率)。
// nJumpThresh:边界突变阈值,相邻行/列边界值一次变化超过它才算"进入/离开"台阶; // binMask:掩膜(非零为前景,兼容 0/1 与 0/255);protrusions:输出的凸起列表(按面积从大到小)
// 调大:只认陡峭台阶,圆角/噪声/锯齿被过滤,过大时边缘圆滑的真把手会漏检; // 可调参数(应对实际情况):
// 调小:敏感,噪声/斜边易触发假配对(可能形成跨度很大的假凸起)。 // nMinDepth:凸起最小深度(输入图像素;<=0 时按掩膜短边/150 自动取值,下限 6)
// nMinRun:凸起最小长度("离开"突变搜索从 s+nMinRun 开始;profile 比它短的边直接跳过); // 调大:只报明显外凸的把手,浅把手会漏检,可压住边缘毛刺/噪声;
// 调大:短凸起被排除,若大于真把手长度,真"离开"突变被跳过、会配到更远突变 → 出更大的框; // 调小:浅凸起与噪声都易报出(误报变多)。mask 由模型输出放大而来时直边带量化台阶
// 调小:小抖动/碎框变多。 // (台阶高≈量化步长),建议取 2 个量化步长左右(调用方见 CheckHandleByBigMask)。
// nMinDepth:凸起最小深度,区间内 ≥80% 采样点相对首尾连线基线外偏 ≥ 它(且最大外偏 ≥ 它)才保留(滤斜边/圆角); // nMinArea:凸起最小面积(输入图像素²;<=0 时按 nMinDepth² 自动取值)
// 调大:只保留明显凸出的,浅凸起漏检,可挡"浅而长"的假配对; // 调大:小碎块被过滤,把手很小或被遮挡一部分时可能漏检;
// 调小:浅波纹/边缘抖动也过检,假凸起变多。 // 调小:碎块残留为候选的几率变大(仍会被 nMinDepth 再过滤一遍)。
// nMaxRun:凸起最大长度(框沿边长度的硬上限);<=0 时取 max(100, 掩膜短边/3)(工作图像素); // nEdgeMargin:边缘外扩量(输入图像素;<=0 时自动取“最小深度的一半”)
// 调大:允许远距离突变配对 → 会出"覆盖整边"的大框;调小:框被截短,小于真把手长度时真把手漏检。 // 把四条边缘线整体向外推该距离后再判凸起,等效于把凸起区域相应减小:
// 调大:边缘顺边毛刺/线条偏差被吞掉,凸起区域更小更干净;过大时浅把手漏检;
// 调小:更灵敏,但边缘毛刺容易连进凸起区域。
// 边缘拟合的细节参数(带宽容差/斜率上限)见 FitEdgeLine 注释。
// pBodyQuad:可选输出,拟合出的主体四边形(左上/右上/右下/左下,输入图坐标),仅 -s 存图观察用
// 返回:0 成功、1 掩膜为空、2 无有效前景 // 返回:0 成功、1 掩膜为空、2 无有效前景
static int GetMaskProtrusions(const cv::Mat &binMask, std::vector<Mask_Protrusion_> &protrusions, static int GetMaskProtrusions(const cv::Mat &binMask, std::vector<Mask_Protrusion_> &protrusions,
int nJumpThresh = 40, int nMinRun = 40, int nMinDepth = 40, int nMaxRun = 0); int nMinDepth = 0, int nMinArea = 0, std::vector<cv::Point2f> *pBodyQuad = NULL,
int nEdgeMargin = 0);
static std::string GetRectString(cv::Rect rect); static std::string GetRectString(cv::Rect rect);
//创建目录 //创建目录

@ -50,19 +50,6 @@ enum AT_THRESHOLD_TYPE_
// 全局静态变量, 记录图像灰度值异常累计数量 // 全局静态变量, 记录图像灰度值异常累计数量
static int g_nImgBrightnessErrorCount = 0; static int g_nImgBrightnessErrorCount = 0;
// 把手候选:bigmask 凸起 + 实测特征(面积/填充率,仅供日志排查)
struct Handle_Candidate_
{
Mask_Protrusion_ protr; // 凸起几何信息(roi / 沿边长度 / 深度 / 面积 / 填充率)
int nLen; // 沿边长度(像素)
int nDepth; // 垂直深度(像素)
Handle_Candidate_()
{
nLen = 0;
nDepth = 0;
}
};
// 单个把手区域的判定结果 // 单个把手区域的判定结果
struct Handle_Region_Result_ struct Handle_Region_Result_
{ {
@ -194,13 +181,19 @@ private:
bool TryReuseAlignFromSource(std::string &strSrcCamera, std::string &strSrcChannel, int &nWaitMs); bool TryReuseAlignFromSource(std::string &strSrcCamera, std::string &strSrcChannel, int &nWaitMs);
// 发布本图定位结果/失败标记(供同产品其它相机复用) // 发布本图定位结果/失败标记(供同产品其它相机复用)
void PublishAlignShare(bool bSucc); void PublishAlignShare(bool bSucc);
// 用 bigmask 检测把手(bigmask 主体近似矩形,把手是四周凸起的矩形块) // 用 outer 定位 mask(bigmask) 检测把手:拟合主体上/下/左/右四条边缘,
// 把边缘外的凸起按连通域分离为把手候选(见 CheckUtil::GetMaskProtrusions),
// 再与参数的把手区域匹配;某个区域附近找不到匹配凸起 → 该区域把手缺失
// -s(存过程图)时输出 {通道}_handle_det.jpg:绿色四边形=拟合主体、红框=凸起(带编号/角/深度)、
// 蓝框=参数把手区域、绿框=匹配到的把手,便于从图像排查
int CheckHandleByBigMask(const cv::Mat &bigmask); int CheckHandleByBigMask(const cv::Mat &bigmask);
// 标签检测(独立步骤):用 (图像 + bigmask) 跑 Tag_Loc 模型,检出结果存 m_tag_roiList // 标签检测(独立步骤):用 (图像 + bigmask) 跑 Tag_Loc 模型,检出结果存 m_tag_roiList
// 明场传自身 bigmask;暗场不做定位模型,传复用明场的 bigmask // 明场传自身 bigmask;暗场不做定位模型,传复用明场的 bigmask
int CheckTagByBigMask(const cv::Mat &img, const cv::Mat &bigmask); int CheckTagByBigMask(const cv::Mat &img, const cv::Mat &bigmask);
// 在某个把手区域附近找宽高匹配的凸起:返回凸起在 candList 中的下标(-1=没找到) // 在某个把手区域附近找宽高匹配的凸起:返回凸起在 candList 中的下标(-1=没找到)
int MatchHandleRegion(const std::vector<Handle_Candidate_> &candList, const std::vector<bool> &usedList, // 匹配参数(搜索范围 handle_search_ratio、大小比区间 handle_size_min/handle_size_max)
// 可 JSON(Handle_Detect.form.handle_param)配置,未配置时用 cpp 顶部宏默认值,见 ImgCheckAnalysisy.cpp 参数注释
int MatchHandleRegion(const std::vector<Mask_Protrusion_> &candList, const std::vector<bool> &usedList,
const cv::Rect &boxRegion, cv::Rect &roiHandle); const cv::Rect &boxRegion, cv::Rect &roiHandle);
// 计算产品尺寸 // 计算产品尺寸
int CalProductSize(); int CalProductSize();
@ -381,8 +374,8 @@ private:
cv::RotatedRect m_inner_rroi; cv::RotatedRect m_inner_rroi;
std::vector<cv::RotatedRect> m_tag_roiList; std::vector<cv::RotatedRect> m_tag_roiList;
// 把手检测结果(bigmask 四周凸起 + 各把手区域判定结果) // 把手检测结果(从 outer 定位 mask 分离的凸起区域 + 各把手区域判定结果)
std::vector<Handle_Candidate_> m_HandleCandList; // 从 bigmask 四周抠出的凸起候选 + 实测特征 std::vector<Mask_Protrusion_> m_HandleProtrusionList; // 分离出的凸起区域(把手候选:roi/质心/深度/就近角等)
std::vector<Handle_Region_Result_> m_HandleRegionResultList; // 各把手区域(最多4个)的判定结果 std::vector<Handle_Region_Result_> m_HandleRegionResultList; // 各把手区域(最多4个)的判定结果
bool m_bHandleDetSucc; // 本次是否完成把手检测 bool m_bHandleDetSucc; // 本次是否完成把手检测
bool m_bHandleFound; // 所有非空把手区域是否都找到了把手 bool m_bHandleFound; // 所有非空把手区域是否都找到了把手

@ -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;
// 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++)
{ {
nMaxRun = std::max(100, std::min(H, W) / 3); 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++)
// 4、逐边抠凸起:
// 主体近似矩形,矩形边上的凸起会让该边的"边界值"先突变到外侧(进入凸起)、再突变回主体(离开凸起);
// 区间内的边界值相对"首尾边界值线性插值"得到的基线持续外偏,即判定为一个凸起。
for (int nSide = 0; nSide < 4; nSide++)
{ {
const bool bRowScan = (nSide < 2); // 左右边按行扫描,上下边按列扫描 int nYT = -1;
const int nLine = bRowScan ? H : W; // 扫描的行数/列数 int nYB = -1;
const int nDir = (nSide == 0 || nSide == 2) ? 1 : -1; // 凸起内边界值变小(左侧/上侧)取 +1,变大取 -1 for (int y = 0; y < H; y++)
// 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 (mainMask.ptr<uchar>(y)[x]) { nYT = y; break; }
if (nSide == 0) // 左:自左向右 }
{ for (int y = H - 1; y >= 0; y--)
const uchar *p = sub.ptr<uchar>(i); {
for (int x = 0; x < W; x++) if (mainMask.ptr<uchar>(y)[x]) { nYB = y; break; }
{ }
if (p[x]) if (nYT >= 0)
{ {
nV = x; ptsT.emplace_back((float)(x + boxMain.x), (float)(nYT + boxMain.y)); // (列号, 上边界的y)
break; }
} if (nYB >= 0)
} {
} ptsB.emplace_back((float)(x + boxMain.x), (float)(nYB + boxMain.y)); // (列号, 下边界的y)
else if (nSide == 1) // 右:自右向左
{
const uchar *p = sub.ptr<uchar>(i);
for (int x = W - 1; x >= 0; x--)
{
if (p[x])
{
nV = x;
break;
}
}
}
else if (nSide == 2) // 上:自上向下
{
for (int y = 0; y < H; y++)
{
if (sub.ptr<uchar>(y)[i])
{
nV = y;
break;
}
}
}
else // 下:自下向上
{
for (int y = H - 1; y >= 0; y--)
{
if (sub.ptr<uchar>(y)[i])
{
nV = y;
break;
}
}
}
if (nV >= 0)
{
posList.push_back(i);
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))
{
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))
{ {
continue; // 工作图坐标 -> 输入图坐标
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)); // 左下
} }
}
// 4.2 配对"进入/离开"突变,扣除该边上的凸起 // 6、凸起 = 超出四条边缘(超过容差带 nExp)的掩膜像素;容差带吸收边缘量化毛刺,
int nUsedTo = -1; // 已统计过的凸起末端,避免同一个凸起重复输出 // 并可由 nEdgeMargin 显式加大(边缘线整体外推)来压制顺边毛刺/减小凸起区域
for (int s = 0; s + 1 < n; s++) 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 (s <= nUsedTo) if (!pSrc[x - boxMain.x])
{ {
continue; continue;
} }
// 进入凸起:边界值朝外侧突变 if (x < fLineL || x > fLineR)
if ((valList[s + 1] - valList[s]) * nDir >= -nJumpThresh)
{ {
pDst[x] = 255;
continue; continue;
} }
// 离开凸起:在其后 nMaxRun 范围内找第一个反向突变 if (y < (float)(fKT * x + fBT) - nExp || y > (float)(fKB * x + fBB) + nExp)
int e = -1;
for (int j = s + nMinRun; j + 1 < n && j - s <= nMaxRun; j++)
{ {
if ((valList[j + 1] - valList[j]) * nDir > nJumpThresh) pDst[x] = 255;
{
e = j;
break;
}
}
if (e < 0)
{
continue;
} }
}
}
// 4.3 区间内的边界值应持续超出"首尾边界值线性插值"得到的基线 // 7、按连通域切分成凸起区域:深度(相对四条边缘)/面积不足的毛刺过滤
const int kk = e - s; cv::Mat labelsP, statsP, centP;
int nDevOk = 0; int nP = cv::connectedComponentsWithStats(protrMask, labelsP, statsP, centP, 8, CV_32S);
float fMaxDev = 0.0f; if (nP <= 1)
int nLow = valList[s + 1]; {
int nHigh = valList[s + 1]; return 0; // 无凸起
for (int i = 1; i <= kk; i++) }
{
float fBase = valList[s] + (valList[e + 1] - valList[s]) * (float)i / (float)(kk + 1);
float fDev = (fBase - valList[s + i]) * nDir;
fMaxDev = std::max(fMaxDev, fDev);
if (fDev >= nMinDepth)
{
nDevOk++;
}
nLow = std::min(nLow, valList[s + i]);
nHigh = std::max(nHigh, valList[s + i]);
}
if (fMaxDev < nMinDepth)
{
continue;
}
// 凸起内大部分采样点都要明显外偏,避免把主体的斜边/圆角当成凸起
if (nDevOk < (int)(kk * 0.8f))
{
continue;
}
// 4.4 计算凸起外接矩形(先算在 sub 图上的位置) for (int i = 1; i < nP; i++)
const int nLen = posList[e] - posList[s + 1] + 1; // 沿该边的长度 {
cv::Rect roiSub; int nArea = statsP.at<int>(i, cv::CC_STAT_AREA);
if (nSide == 0 || nSide == 2) // 左/上:外侧取小值,主体边缘取大值 if (nArea < nMinAreaWork)
{ {
int nOut = nLow; continue;
int nInner = std::max(valList[s], valList[e + 1]); }
if (nSide == 0) int nX = statsP.at<int>(i, cv::CC_STAT_LEFT);
{ int nY = statsP.at<int>(i, cv::CC_STAT_TOP);
roiSub = cv::Rect(nOut, posList[s + 1], nInner - nOut + 1, nLen); int nW = statsP.at<int>(i, cv::CC_STAT_WIDTH);
} int nH = statsP.at<int>(i, cv::CC_STAT_HEIGHT);
else
{ // 深度:组件像素相对四条边缘线的最大外伸(工作图像素)
roiSub = cv::Rect(posList[s + 1], nOut, nLen, nInner - nOut + 1); float fDepth = 0.0f;
} for (int yy = nY; yy < nY + nH; yy++)
} {
else // 右/下:外侧取大值,主体边缘取小值 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++)
{ {
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);
}
else
{ {
roiSub = cv::Rect(posList[s + 1], nInner, nLen, nOut - nInner + 1); 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);
} }
roiSub &= cv::Rect(0, 0, W, H); }
if (roiSub.width <= 0 || roiSub.height <= 0) if (fDepth < nMinDepthWork)
{ {
continue; continue;
} }
Mask_Protrusion_ protr;
protr.nSide = nSide;
// 从工作图尺度还原到输入图尺度
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);
if (protr.roi.width <= 0 || protr.roi.height <= 0)
{
continue;
}
protr.fLen = (float)nLen * nScale;
protr.fDepth = fMaxDev * nScale;
int nMaskArea = cv::countNonZero(sub(roiSub));
protr.fArea = (float)nMaskArea * nScale * nScale;
protr.fFillRatio = (float)nMaskArea / (float)(roiSub.width * roiSub.height);
protrusions.push_back(protr); // 特征 + 坐标(工作图 -> 输入图)
nUsedTo = e; // 该凸起区间不再重复统计 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; return 0;
} }

@ -22,12 +22,33 @@
// 待分类模型部署后,将 AI_CLASS_MODEL_ENABLE 置 1,即可恢复 AI_QX_Class_Thread 中的AI分类流程 // 待分类模型部署后,将 AI_CLASS_MODEL_ENABLE 置 1,即可恢复 AI_QX_Class_Thread 中的AI分类流程
#define AI_CLASS_MODEL_ENABLE 0 #define AI_CLASS_MODEL_ENABLE 0
// 把手判定参数: // 把手提取/判定参数(下面宏是“未配置 JSON 时”的默认值;现场可在参数 JSON 的 Handle_Detect.form.handle_param
// 每个非空的"把手区域"附近都应该有一个把手:凸起宽高与该区域外接矩形宽高的比值落在下面范围内才算匹配 // 下按注释标注的键名配置同名参数覆盖它们,按产品/现场情况调整,无需改代码重编译):
// 提取:用 outer 定位 mask(bigmask) 拟合主体(圆角矩形)的上/下/左/右四条边缘,把边缘外的凸起按连通域分离为把手候选
// (见 CheckUtil::GetMaskProtrusions;产品主体是圆角矩形、把手基本出现在四角附近)
// 判定:每个非空的"把手区域"附近都应该有一个把手:凸起宽高与该区域外接矩形宽高的比值落在下面范围内才算匹配
// 调整影响:
// HANDLE_SIZE_MIN/MAX_RATIO(JSON: handle_size_min / handle_size_max):凸起与参数把手区域外接矩形的宽高比允许区间
// 放宽(如 0.7~1.4):把手尺寸/摆放差异大时不易漏检匹配,但可能让邻近错误凸起也匹配上;
// 收紧(如 0.9~1.1):匹配更严格,把手实际尺寸与参数画得差异大时会误报"把手缺失"
#define HANDLE_SIZE_MIN_RATIO 0.8f #define HANDLE_SIZE_MIN_RATIO 0.8f
#define HANDLE_SIZE_MAX_RATIO 1.2f #define HANDLE_SIZE_MAX_RATIO 1.2f
// "附近"的搜索范围:把手区域外接矩形在宽/高方向各外扩该比例 // "附近"的搜索范围(JSON: handle_search_ratio):把手区域外接矩形在宽/高方向各外扩该比例
// 调大:位置约束变松,产品摆放偏移大也不易漏(但可能匹配到别的凸起);
// 调小:位置约束更严,偏移稍大就会判"附近没有把手"
#define HANDLE_NEAR_EXPAND_RATIO 5 #define HANDLE_NEAR_EXPAND_RATIO 5
// 把手提取:最小深度系数(× 定位模型量化步长)(JSON: handle_min_depth)
// 凸起区域算大了/过于灵敏(边缘顺边毛刺与把手连成一块、高度多算) → 调大(如 3~4);
// 浅把手漏检 → 调小(如 1.5)。它同时决定:①凸起深度门槛;②边缘容差带(容差带=最小深度/2)
#define HANDLE_MIN_DEPTH_STEP_COEF 2
// 把手提取:最小面积系数(× 最小深度²)(JSON: handle_min_area)。碎块多 → 调大;小把手漏检 → 调小
#define HANDLE_MIN_AREA_COEF 2
// 把手提取:边缘外扩量系数(× 量化步长)(JSON: handle_edge_margin,0=按"最小深度一半"自动):
// 把主体边缘线整体向外推,变相减小凸起区域。
// 凸起区域算大了(边缘顺边毛刺与把手连成一块、高度多算) → 调大(如 2~3);
// 浅把手漏检/想更灵敏 → 调小。
// 注意:凸起区域会同步变小,匹配用的宽高比(HANDLE_SIZE_*_RATIO)会偏小,不宜调得过大
#define HANDLE_EDGE_MARGIN_STEP_COEF 3
// 把手缺失检测开关见 Base层 Function_Support_Det::bDetHandle(CheckConfigDefine.h) // 把手缺失检测开关见 Base层 Function_Support_Det::bDetHandle(CheckConfigDefine.h)
// 分类映射配置文件(每行:label索引:缺陷名称;冒号后名称为该缺陷类型名称,行的顺序即缺陷类型索引) // 分类映射配置文件(每行:label索引:缺陷名称;冒号后名称为该缺陷类型名称,行的顺序即缺陷类型索引)
@ -2778,7 +2799,7 @@ int ImgCheckAnalysisy::AI_Edge(const cv::Mat &img, cv::RotatedRect &outerRoi, cv
// 无条件赋值:Tag_Loc 未检出或 bDetTag 关闭(tagroi 为空)时清空,避免残留上一张图的标签 // 无条件赋值:Tag_Loc 未检出或 bDetTag 关闭(tagroi 为空)时清空,避免残留上一张图的标签
tagroiList = m_pEdge_Align_Result->tagroi; tagroiList = m_pEdge_Align_Result->tagroi;
/*使用m_pEdge_Align_Result->bigmask检测把手是否缺失*/ /* 用 outer 定位 mask(bigmask) 分离凸起区域,检测把手是否缺失 */
{ {
long handle_s = CheckUtil::getcurTime(); long handle_s = CheckUtil::getcurTime();
CheckHandleByBigMask(m_pEdge_Align_Result->bigmask); CheckHandleByBigMask(m_pEdge_Align_Result->bigmask);
@ -2788,18 +2809,20 @@ int ImgCheckAnalysisy::AI_Edge(const cv::Mat &img, cv::RotatedRect &outerRoi, cv
return re; return re;
} }
// 用 bigmask 检测把手: // 用 outer 定位 mask(bigmask) 检测把手(产品主体是圆角矩形、把手基本出现在四角附近):
// 1、bigmask 主体近似矩形,把手是主体四周凸起的"类矩形块" // 1、分离凸起区域:拟合主体上/下/左/右四条边缘,把边缘外的掩膜按连通域切分成凸起区域
// 2、把主体四周的凸起抠出来(凸起在该边的边界值上表现为一次突变进入 + 一次反向突变离开) // (提取细节见 CheckUtil::GetMaskProtrusions,含就近角/质心/面积/深度/填充率)
// 3、参数里每个非空的"把手区域"附近都应该有一个把手:在区域附近找凸起, // 2、参数里每个非空的"把手区域"附近都应该有一个把手:在区域附近找凸起,
// 凸起宽高与区域外接矩形宽高相近(HANDLE_SIZE_MIN_RATIO ~ HANDLE_SIZE_MAX_RATIO)才算匹配 // 凸起宽高与区域外接矩形宽高相近(尺寸比区间见 handle_size_min/handle_size_max,可 JSON 配置)才算匹配
// 4、凸起本身不算 NG,只有"某个把手区域附近找不到匹配的凸起"才算把手缺失(NG 由 CheckRun 上报) // 3、凸起本身不算 NG,只有"某个把手区域附近找不到匹配的凸起"才算把手缺失(NG 由 CheckRun 上报)
// 4、-s(存过程图)时输出 {通道}_handle_det.jpg:绿色四边形=拟合主体、红框=凸起(带编号/角/深度)、
// 蓝框=参数把手区域、绿框=匹配到的把手,便于从图像观察问题
int ImgCheckAnalysisy::CheckHandleByBigMask(const cv::Mat &bigmask) int ImgCheckAnalysisy::CheckHandleByBigMask(const cv::Mat &bigmask)
{ {
m_bHandleDetSucc = false; m_bHandleDetSucc = false;
m_bHandleFound = false; m_bHandleFound = false;
m_bHandleExpect = false; m_bHandleExpect = false;
m_HandleCandList.clear(); m_HandleProtrusionList.clear();
m_HandleRegionResultList.clear(); m_HandleRegionResultList.clear();
if (bigmask.empty()) if (bigmask.empty())
@ -2808,9 +2831,40 @@ int ImgCheckAnalysisy::CheckHandleByBigMask(const cv::Mat &bigmask)
return 1; return 1;
} }
// 1、抠出 bigmask 四周凸起的矩形块 // 1、从 bigmask 分离凸起区域
std::vector<Mask_Protrusion_> protrusionList; // 最小深度 = 系数 × 量化步长:mask 由模型输出 resize 回原图,直边带量化台阶毛刺(台阶高≈模型 1 像素=量化步长);
int re = CheckUtil::GetMaskProtrusions(bigmask, protrusionList); // 系数优先取参数配置(JSON handle_param 下的 handle_min_depth/handle_min_area/handle_edge_margin),
// 未配置时用文件顶部宏的默认值;
// 凸起区域算大了/过于灵敏 → 首选调大 handle_edge_margin(把边缘线整体外推、变相减小凸起区域,
// 不影响深度门槛);也可调大 handle_min_depth(深度门槛+容差带一起变大);浅把手漏检 → 反向调小;
// 拿不到模型尺寸时传 0,由 GetMaskProtrusions 按掩膜尺寸自动取值
float fMinDepthCoef = HANDLE_MIN_DEPTH_STEP_COEF;
float fMinAreaCoef = HANDLE_MIN_AREA_COEF;
float fEdgeMarginCoef = HANDLE_EDGE_MARGIN_STEP_COEF;
if (m_pbaseCheckFunction != NULL)
{
const Function_Support_Det &supportDetCfg = m_pbaseCheckFunction->supportDet;
if (supportDetCfg.handle_min_depth_coef > 0) fMinDepthCoef = supportDetCfg.handle_min_depth_coef;
if (supportDetCfg.handle_min_area_coef > 0) fMinAreaCoef = supportDetCfg.handle_min_area_coef;
if (supportDetCfg.handle_edge_margin_coef >= 0) fEdgeMarginCoef = supportDetCfg.handle_edge_margin_coef; // 0=按“最小深度一半”自动
}
int nMinDepth = 0;
int nMinArea = 0;
int nEdgeMargin = 0;
if (AI_Factory && AI_Factory->Align_Outer && AI_Factory->Align_Outer->input_0.width > 0 && AI_Factory->Align_Outer->input_0.height > 0)
{
int nStepX = (int)std::ceil(bigmask.cols * 1.0 / AI_Factory->Align_Outer->input_0.width);
int nStepY = (int)std::ceil(bigmask.rows * 1.0 / AI_Factory->Align_Outer->input_0.height);
int nStep = std::max(1, std::max(nStepX, nStepY));
nMinDepth = (int)(fMinDepthCoef * nStep + 0.5f);
nMinArea = (int)(fMinAreaCoef * nMinDepth * nMinDepth + 0.5f);
if (fEdgeMarginCoef > 0)
{
nEdgeMargin = (int)(fEdgeMarginCoef * nStep + 0.5f);
}
}
std::vector<cv::Point2f> vecBodyQuad; // 拟合出的主体四边形(仅 -s 存图观察用)
int re = CheckUtil::GetMaskProtrusions(bigmask, m_HandleProtrusionList, nMinDepth, nMinArea, &vecBodyQuad, nEdgeMargin);
if (re != 0) if (re != 0)
{ {
m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Handle", "GetMaskProtrusions error %d", re); m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Handle", "GetMaskProtrusions error %d", re);
@ -2832,33 +2886,27 @@ int ImgCheckAnalysisy::CheckHandleByBigMask(const cv::Mat &bigmask)
} }
m_bHandleExpect = (bDetHandle && !regionBoxList.empty()); m_bHandleExpect = (bDetHandle && !regionBoxList.empty());
m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Handle", "protrusion num %ld handle region num %ld bDetHandle %s expect %s", m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Handle", "protrusion num %ld handle region num %ld bDetHandle %s expect %s",
protrusionList.size(), regionBoxList.size(), BOOL_TO_STR(bDetHandle), BOOL_TO_STR(m_bHandleExpect)); m_HandleProtrusionList.size(), regionBoxList.size(), BOOL_TO_STR(bDetHandle), BOOL_TO_STR(m_bHandleExpect));
// 3、凸起候选实测特征(面积/填充率,仅用于日志排查) // 3、凸起区域实测特征(就近角/面积/深度/填充率,用于日志排查)
for (size_t i = 0; i < protrusionList.size(); i++) for (size_t i = 0; i < m_HandleProtrusionList.size(); i++)
{ {
Handle_Candidate_ cand; const Mask_Protrusion_ &protr = m_HandleProtrusionList[i];
cand.protr = protrusionList[i];
// 左/右凸起:沿边为 y 方向,深度为 x 方向;上/下凸起相反
bool bHorz = (cand.protr.nSide == 0 || cand.protr.nSide == 1);
cand.nLen = bHorz ? cand.protr.roi.height : cand.protr.roi.width;
cand.nDepth = bHorz ? cand.protr.roi.width : cand.protr.roi.height;
m_HandleCandList.push_back(cand);
m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Handle", m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Handle",
"protrusion %ld side %d roi [%d %d %d %d] len %d depth %d area %0.0f fill %0.2f", "protrusion %ld corner %d roi [%d %d %d %d] len %.0f depth %.0f area %0.0f fill %0.2f",
i, cand.protr.nSide, cand.protr.roi.x, cand.protr.roi.y, cand.protr.roi.width, cand.protr.roi.height, i, protr.nCorner, protr.roi.x, protr.roi.y, protr.roi.width, protr.roi.height,
cand.nLen, cand.nDepth, cand.protr.fArea, cand.protr.fFillRatio); protr.fLen, protr.fDepth, protr.fArea, protr.fFillRatio);
} }
// 4、逐个把手区域:在区域附近找宽高匹配的凸起(同一个凸起只匹配给一个区域) // 4、逐个把手区域:在区域附近找宽高匹配的凸起(同一个凸起只匹配给一个区域)
std::vector<bool> usedList(m_HandleCandList.size(), false); std::vector<bool> usedList(m_HandleProtrusionList.size(), false);
int nFound = 0; int nFound = 0;
for (size_t ir = 0; ir < regionBoxList.size(); ir++) for (size_t ir = 0; ir < regionBoxList.size(); ir++)
{ {
Handle_Region_Result_ result; Handle_Region_Result_ result;
result.nIdx = (int)ir + 1; result.nIdx = (int)ir + 1;
result.boxRegion = regionBoxList[ir]; result.boxRegion = regionBoxList[ir];
int nMatchIdx = MatchHandleRegion(m_HandleCandList, usedList, result.boxRegion, result.roiHandle); int nMatchIdx = MatchHandleRegion(m_HandleProtrusionList, usedList, result.boxRegion, result.roiHandle);
result.bFound = (nMatchIdx >= 0); result.bFound = (nMatchIdx >= 0);
if (result.bFound) if (result.bFound)
{ {
@ -2882,7 +2930,9 @@ int ImgCheckAnalysisy::CheckHandleByBigMask(const cv::Mat &bigmask)
m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, "Handle", "handle region num %ld found num %d -> %s", m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, "Handle", "handle region num %ld found num %d -> %s",
regionBoxList.size(), nFound, m_bHandleFound ? "handle ok" : "handle missing !!"); regionBoxList.size(), nFound, m_bHandleFound ? "handle ok" : "handle missing !!");
// 5、存过程图:凸起红框、把手区域蓝框、匹配上的把手绿框(仅调试时) // 5、存过程图(-s 时):用于从图像观察把手提取/匹配情况;
// 绿色四边形=拟合出的主体四条边缘;红框=分离出的凸起区域(带编号/就近角/深度);
// 蓝框=参数把手区域;绿框=匹配到的把手
if (DetImgInfo_shareP->bsaveProcessImg) if (DetImgInfo_shareP->bsaveProcessImg)
{ {
cv::Mat show; cv::Mat show;
@ -2894,9 +2944,25 @@ int ImgCheckAnalysisy::CheckHandleByBigMask(const cv::Mat &bigmask)
{ {
show = bigmask.clone(); show = bigmask.clone();
} }
for (size_t i = 0; i < m_HandleCandList.size(); i++) // 主体四边形(拟合出的四条边缘)
if (vecBodyQuad.size() == 4)
{ {
cv::rectangle(show, m_HandleCandList[i].protr.roi, cv::Scalar(0, 0, 255), 8); std::vector<cv::Point> vecQuad;
for (size_t i = 0; i < vecBodyQuad.size(); i++)
{
vecQuad.push_back(cv::Point(cvRound(vecBodyQuad[i].x), cvRound(vecBodyQuad[i].y)));
}
cv::polylines(show, vecQuad, true, cv::Scalar(0, 255, 0), 8);
}
// 凸起区域:红框 + 编号/就近角/深度
char strTxt[128];
for (size_t i = 0; i < m_HandleProtrusionList.size(); i++)
{
const Mask_Protrusion_ &protr = m_HandleProtrusionList[i];
cv::rectangle(show, protr.roi, cv::Scalar(0, 0, 255), 8);
snprintf(strTxt, sizeof(strTxt), "#%d C%d d%.0f", (int)i, protr.nCorner, protr.fDepth);
cv::putText(show, strTxt, cv::Point(protr.roi.x, std::max(30, protr.roi.y - 8)),
cv::FONT_HERSHEY_SIMPLEX, 2.0, cv::Scalar(0, 255, 255), 4);
} }
for (size_t i = 0; i < m_HandleRegionResultList.size(); i++) for (size_t i = 0; i < m_HandleRegionResultList.size(); i++)
{ {
@ -3050,10 +3116,12 @@ int ImgCheckAnalysisy::CheckTagByBigMask(const cv::Mat &img, const cv::Mat &bigm
} }
// 在某个把手区域附近找宽高匹配的凸起 // 在某个把手区域附近找宽高匹配的凸起
// 匹配条件:1、凸起中心落在把手区域附近(区域宽/高各外扩 HANDLE_NEAR_EXPAND_RATIO) // 匹配条件:1、凸起中心落在把手区域附近(区域宽/高各外扩 fNearRatio 倍)
// 2、凸起宽高与该区域外接矩形宽高的比值都在 [HANDLE_SIZE_MIN_RATIO, HANDLE_SIZE_MAX_RATIO](允许横竖互换) // 2、凸起宽高与该区域外接矩形宽高的比值都在 [fSizeMin, fSizeMax](允许横竖互换)
// 范围参数优先取参数配置(JSON handle_param 下的 handle_search_ratio/handle_size_min/handle_size_max),
// 未配置时用文件顶部宏的默认值
// 返回凸起在 candList 中的下标,-1 表示没找到 // 返回凸起在 candList 中的下标,-1 表示没找到
int ImgCheckAnalysisy::MatchHandleRegion(const std::vector<Handle_Candidate_> &candList, const std::vector<bool> &usedList, int ImgCheckAnalysisy::MatchHandleRegion(const std::vector<Mask_Protrusion_> &candList, const std::vector<bool> &usedList,
const cv::Rect &boxRegion, cv::Rect &roiHandle) const cv::Rect &boxRegion, cv::Rect &roiHandle)
{ {
if (boxRegion.width <= 0 || boxRegion.height <= 0) if (boxRegion.width <= 0 || boxRegion.height <= 0)
@ -3061,9 +3129,21 @@ int ImgCheckAnalysisy::MatchHandleRegion(const std::vector<Handle_Candidate_> &c
return -1; return -1;
} }
// 搜索范围:把手区域外扩(宽/高各外扩区域尺寸的一半),容忍产品摆放偏差 // 匹配范围参数:JSON 配置优先,未配置时用默认值
int nx = std::max(10, (int)(boxRegion.width * HANDLE_NEAR_EXPAND_RATIO)); float fNearRatio = HANDLE_NEAR_EXPAND_RATIO;
int ny = std::max(10, (int)(boxRegion.height * HANDLE_NEAR_EXPAND_RATIO)); float fSizeMin = HANDLE_SIZE_MIN_RATIO;
float fSizeMax = HANDLE_SIZE_MAX_RATIO;
if (m_pbaseCheckFunction != NULL)
{
const Function_Support_Det &supportDetCfg = m_pbaseCheckFunction->supportDet;
if (supportDetCfg.handle_search_ratio > 0) fNearRatio = supportDetCfg.handle_search_ratio;
if (supportDetCfg.handle_size_min_ratio > 0) fSizeMin = supportDetCfg.handle_size_min_ratio;
if (supportDetCfg.handle_size_max_ratio > 0) fSizeMax = supportDetCfg.handle_size_max_ratio;
}
// 搜索范围:把手区域外扩(宽/高各外扩搜索倍数倍),容忍产品摆放偏差
int nx = std::max(10, (int)(boxRegion.width * fNearRatio));
int ny = std::max(10, (int)(boxRegion.height * fNearRatio));
cv::Rect searchBox(boxRegion.x - nx, boxRegion.y - ny, boxRegion.width + 2 * nx, boxRegion.height + 2 * ny); cv::Rect searchBox(boxRegion.x - nx, boxRegion.y - ny, boxRegion.width + 2 * nx, boxRegion.height + 2 * ny);
cv::Point pRegionCenter(boxRegion.x + boxRegion.width / 2, boxRegion.y + boxRegion.height / 2); cv::Point pRegionCenter(boxRegion.x + boxRegion.width / 2, boxRegion.y + boxRegion.height / 2);
@ -3077,8 +3157,8 @@ int ImgCheckAnalysisy::MatchHandleRegion(const std::vector<Handle_Candidate_> &c
{ {
continue; continue;
} }
const Handle_Candidate_ &cand = candList[i]; const Mask_Protrusion_ &cand = candList[i];
const cv::Rect &roi = cand.protr.roi; const cv::Rect &roi = cand.roi;
if (roi.width <= 0 || roi.height <= 0) if (roi.width <= 0 || roi.height <= 0)
{ {
continue; continue;
@ -3100,12 +3180,12 @@ int ImgCheckAnalysisy::MatchHandleRegion(const std::vector<Handle_Candidate_> &c
// 宽高:与把手区域外接矩形的宽高相近(允许横竖互换) // 宽高:与把手区域外接矩形的宽高相近(允许横竖互换)
float fw = (float)roi.width; float fw = (float)roi.width;
float fh = (float)roi.height; float fh = (float)roi.height;
bool bSizeOK = (fw >= boxRegion.width * HANDLE_SIZE_MIN_RATIO && fw <= boxRegion.width * HANDLE_SIZE_MAX_RATIO && bool bSizeOK = (fw >= boxRegion.width * fSizeMin && fw <= boxRegion.width * fSizeMax &&
fh >= boxRegion.height * HANDLE_SIZE_MIN_RATIO && fh <= boxRegion.height * HANDLE_SIZE_MAX_RATIO); fh >= boxRegion.height * fSizeMin && fh <= boxRegion.height * fSizeMax);
if (!bSizeOK) if (!bSizeOK)
{ {
bSizeOK = (fw >= boxRegion.height * HANDLE_SIZE_MIN_RATIO && fw <= boxRegion.height * HANDLE_SIZE_MAX_RATIO && bSizeOK = (fw >= boxRegion.height * fSizeMin && fw <= boxRegion.height * fSizeMax &&
fh >= boxRegion.width * HANDLE_SIZE_MIN_RATIO && fh <= boxRegion.width * HANDLE_SIZE_MAX_RATIO); fh >= boxRegion.width * fSizeMin && fh <= boxRegion.width * fSizeMax);
} }
if (!bSizeOK) if (!bSizeOK)
{ {
@ -3122,17 +3202,17 @@ int ImgCheckAnalysisy::MatchHandleRegion(const std::vector<Handle_Candidate_> &c
if (nBestIdx < 0 && nNearIdx >= 0) if (nBestIdx < 0 && nNearIdx >= 0)
{ {
// 没匹配上时打印区域内最近的凸起,便于排查阈值 // 没匹配上时打印区域内最近的凸起,便于排查阈值
const cv::Rect &roi = candList[nNearIdx].protr.roi; const cv::Rect &roi = candList[nNearIdx].roi;
m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Handle", m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Handle",
"region box [%d %d %d %d] no match, nearest protrusion roi [%d %d %d %d] w/h ratio %0.2f %0.2f", "region box [%d %d %d %d] no match, nearest protrusion corner %d roi [%d %d %d %d] w/h ratio %0.2f %0.2f",
boxRegion.x, boxRegion.y, boxRegion.width, boxRegion.height, boxRegion.x, boxRegion.y, boxRegion.width, boxRegion.height,
roi.x, roi.y, roi.width, roi.height, candList[nNearIdx].nCorner, roi.x, roi.y, roi.width, roi.height,
(float)roi.width / (float)boxRegion.width, (float)roi.height / (float)boxRegion.height); (float)roi.width / (float)boxRegion.width, (float)roi.height / (float)boxRegion.height);
} }
if (nBestIdx >= 0) if (nBestIdx >= 0)
{ {
roiHandle = candList[nBestIdx].protr.roi; roiHandle = candList[nBestIdx].roi;
} }
return nBestIdx; return nBestIdx;
} }

@ -936,6 +936,14 @@ struct Function_Support_Det
std::vector<std::vector<cv::Point>> handleRegions; std::vector<std::vector<cv::Point>> handleRegions;
std::vector<cv::Rect> handleBoxes; std::vector<cv::Rect> handleBoxes;
float handle_diff; // 把手差异阈值 float handle_diff; // 把手差异阈值
// 把手提取/匹配范围参数:JSON(Handle_Detect.form.handle_param)下同名键可选配置,未配置用 ImgCheckAnalysisy.cpp
// 顶部宏的默认值;各参数含义/调整影响见该文件参数注释。现场调这些参数不用改代码重编译
float handle_size_min_ratio; // handle_size_min:凸起宽高 / 区域宽高 比值下限;<=0=未配置(默认 0.8)
float handle_size_max_ratio; // handle_size_max:比值上限;<=0=未配置(默认 1.2)
float handle_search_ratio; // handle_search_ratio:区域附近搜索范围外扩倍数;<=0=未配置(默认 5)
float handle_min_depth_coef; // handle_min_depth:凸起最小深度系数(×量化步长);<=0=未配置(默认 2)
float handle_min_area_coef; // handle_min_area:凸起最小面积系数(×最小深度²);<=0=未配置(默认 2)
float handle_edge_margin_coef; // handle_edge_margin:边缘线外扩系数(×量化步长);<0=未配置(默认 3),0=按“最小深度一半”自动
// 标签参数(BQ_Detect) // 标签参数(BQ_Detect)
bool bDetTag; // 标签检测开关(BQ_Detect.isOpen) bool bDetTag; // 标签检测开关(BQ_Detect.isOpen)
float BQ_area; // 标签检测参数:面积 float BQ_area; // 标签检测参数:面积
@ -962,6 +970,13 @@ struct Function_Support_Det
handleRegions.clear(); handleRegions.clear();
handleBoxes.clear(); handleBoxes.clear();
handle_diff = 20; handle_diff = 20;
// 范围参数默认“未配置”:0=未配置(用代码默认),edge_margin 用 -1 表示未配置(0=自动)
handle_size_min_ratio = 0;
handle_size_max_ratio = 0;
handle_search_ratio = 0;
handle_min_depth_coef = 0;
handle_min_area_coef = 0;
handle_edge_margin_coef = -1;
bDetTag = false; bDetTag = false;
BQ_area = 0; BQ_area = 0;
BQ_energy = 0; BQ_energy = 0;
@ -982,6 +997,12 @@ struct Function_Support_Det
this->handleRegions.assign(tem.handleRegions.begin(), tem.handleRegions.end()); this->handleRegions.assign(tem.handleRegions.begin(), tem.handleRegions.end());
this->handleBoxes.assign(tem.handleBoxes.begin(), tem.handleBoxes.end()); this->handleBoxes.assign(tem.handleBoxes.begin(), tem.handleBoxes.end());
this->handle_diff = tem.handle_diff; this->handle_diff = tem.handle_diff;
this->handle_size_min_ratio = tem.handle_size_min_ratio;
this->handle_size_max_ratio = tem.handle_size_max_ratio;
this->handle_search_ratio = tem.handle_search_ratio;
this->handle_min_depth_coef = tem.handle_min_depth_coef;
this->handle_min_area_coef = tem.handle_min_area_coef;
this->handle_edge_margin_coef = tem.handle_edge_margin_coef;
this->bDetTag = tem.bDetTag; this->bDetTag = tem.bDetTag;
this->BQ_area = tem.BQ_area; this->BQ_area = tem.BQ_area;
this->BQ_energy = tem.BQ_energy; this->BQ_energy = tem.BQ_energy;
@ -990,14 +1011,18 @@ struct Function_Support_Det
} }
void print(std::string str) void print(std::string str)
{ {
printf("%s>>bDetSupport %d bDrawSupport %d bDetHandle %d bDetTag %d x_standard %f y_standard %f x_offset %f y_offset %f r_offset %f edge num %ld handleRegion num %ld handle_diff %f BQ[area %f energy %f len %f hj %f]\n", printf("%s>>bDetSupport %d bDrawSupport %d bDetHandle %d bDetTag %d x_standard %f y_standard %f x_offset %f y_offset %f r_offset %f edge num %ld handleRegion num %ld handle_diff %f handleRange[size %.2f~%.2f search %.1f depth %.1f area %.1f margin %.1f(0=自动,<0=默认)] BQ[area %f energy %f len %f hj %f]\n",
str.c_str(), bDetSupport, bDrawSupport, bDetHandle, bDetTag, x_standard, y_standard, x_offset, y_offset, r_offset, edge_select.size(), handleRegions.size(), handle_diff, BQ_area, BQ_energy, BQ_len, BQ_hj); str.c_str(), bDetSupport, bDrawSupport, bDetHandle, bDetTag, x_standard, y_standard, x_offset, y_offset, r_offset, edge_select.size(), handleRegions.size(), handle_diff,
handle_size_min_ratio, handle_size_max_ratio, handle_search_ratio, handle_min_depth_coef, handle_min_area_coef, handle_edge_margin_coef,
BQ_area, BQ_energy, BQ_len, BQ_hj);
} }
std::string GetInfo(std::string str) std::string GetInfo(std::string str)
{ {
char buffer[512]; char buffer[1024];
sprintf(buffer, "%s>>bDetSupport %d bDrawSupport %d bDetHandle %d bDetTag %d x_standard %f y_standard %f x_offset %f y_offset %f r_offset %f edge num %ld handleRegion num %ld handle_diff %f BQ[area %f energy %f len %f hj %f]\n", sprintf(buffer, "%s>>bDetSupport %d bDrawSupport %d bDetHandle %d bDetTag %d x_standard %f y_standard %f x_offset %f y_offset %f r_offset %f edge num %ld handleRegion num %ld handle_diff %f handleRange[size %.2f~%.2f search %.1f depth %.1f area %.1f margin %.1f(0=自动,<0=默认)] BQ[area %f energy %f len %f hj %f]\n",
str.c_str(), bDetSupport, bDrawSupport, bDetHandle, bDetTag, x_standard, y_standard, x_offset, y_offset, r_offset, edge_select.size(), handleRegions.size(), handle_diff, BQ_area, BQ_energy, BQ_len, BQ_hj); str.c_str(), bDetSupport, bDrawSupport, bDetHandle, bDetTag, x_standard, y_standard, x_offset, y_offset, r_offset, edge_select.size(), handleRegions.size(), handle_diff,
handle_size_min_ratio, handle_size_max_ratio, handle_search_ratio, handle_min_depth_coef, handle_min_area_coef, handle_edge_margin_coef,
BQ_area, BQ_energy, BQ_len, BQ_hj);
std::string str123 = buffer; std::string str123 = buffer;
return str123; return str123;
} }

@ -973,6 +973,37 @@ int BaseFuntonConfigJson::GetFunction(Json::Value value)
{ {
_config.supportDet.handle_diff = handle_p["handle_diff"].asFloat(); _config.supportDet.handle_diff = handle_p["handle_diff"].asFloat();
} }
// 把手提取/匹配范围参数(均可选:未配置时用 ImgCheckAnalysisy.cpp 顶部宏的默认值)
// handle_size_min/max :凸起宽高与把手区域外接矩形宽高的比值允许区间(默认 0.8 ~ 1.2)
// handle_search_ratio :把手区域附近搜索范围外扩倍数(默认 5)
// handle_min_depth :凸起最小深度系数(× 定位模型量化步长)(默认 2)
// handle_min_area :凸起最小面积系数(× 最小深度²)(默认 2)
// handle_edge_margin :主体边缘线外扩系数(× 量化步长),0=按“最小深度一半”自动(默认 3)
// 含义/调整影响详见 ImgCheckAnalysisy.cpp 顶部参数注释
if (handle_p["handle_size_min"])
{
_config.supportDet.handle_size_min_ratio = handle_p["handle_size_min"].asFloat();
}
if (handle_p["handle_size_max"])
{
_config.supportDet.handle_size_max_ratio = handle_p["handle_size_max"].asFloat();
}
if (handle_p["handle_search_ratio"])
{
_config.supportDet.handle_search_ratio = handle_p["handle_search_ratio"].asFloat();
}
if (handle_p["handle_min_depth"])
{
_config.supportDet.handle_min_depth_coef = handle_p["handle_min_depth"].asFloat();
}
if (handle_p["handle_min_area"])
{
_config.supportDet.handle_min_area_coef = handle_p["handle_min_area"].asFloat();
}
if (handle_p["handle_edge_margin"])
{
_config.supportDet.handle_edge_margin_coef = handle_p["handle_edge_margin"].asFloat();
}
bool bNewFormat = handle_p["handle_region1"].isArray(); bool bNewFormat = handle_p["handle_region1"].isArray();
for (int idx = 0; idx < HANDLE_REGION_MAX_NUM; idx++) for (int idx = 0; idx < HANDLE_REGION_MAX_NUM; idx++)
{ {

Loading…
Cancel
Save