update 初步优化把手检测

dev_lsy
liusiyang 4 days ago
parent 4e6b88e825
commit 4b7e1677ff

@ -25,6 +25,26 @@
#include <thread>
#include <opencv2/opencv.hpp>
using namespace std;
// 掩膜主体(近似矩形)四周凸起(如把手)的描述
struct Mask_Protrusion_
{
int nSide; // 所在边0 左 1 右 2 上 3 下
cv::Rect roi; // 凸起外接矩形(输入图坐标系,已裁剪到图像范围内)
float fLen; // 沿该边的长度(像素)
float fDepth; // 伸出主体的深度(像素)
float fArea; // 凸起面积roi 内掩膜像素数)
float fFillRatio; // roi 内掩膜像素占比(用于判定是否"类似矩形块"
Mask_Protrusion_()
{
nSide = 0;
fLen = 0.0f;
fDepth = 0.0f;
fArea = 0.0f;
fFillRatio = 0.0f;
}
};
class CheckUtil
{
public:
@ -58,6 +78,14 @@ public:
// 找图片的最大外轮廓
static std::vector<cv::Point> getLargestContourROI(const cv::Mat &binaryImg, bool &found);
// 抠出掩膜四周的凸起:主体近似矩形,凸起在该边表现为边界值的一次突变(进入)与一次反向突变(离开)
// binMask掩膜非零为前景兼容 0/1 与 0/255protrusions输出的凸起列表
// nJumpThresh边界突变阈值(输入图像素)nMinRun凸起最小长度(输入图像素)nMinDepth凸起最小深度(输入图像素)
// nMaxRun凸起最大长度(输入图像素)<=0 时取 掩膜短边/3
// 返回0 成功、1 掩膜为空、2 无有效前景
static int GetMaskProtrusions(const cv::Mat &binMask, std::vector<Mask_Protrusion_> &protrusions,
int nJumpThresh = 40, int nMinRun = 40, int nMinDepth = 40, int nMaxRun = 0);
static std::string GetRectString(cv::Rect rect);
//创建目录
static int CreateDir(const std::string &dir);

@ -49,6 +49,24 @@ enum AT_THRESHOLD_TYPE_
// 全局静态变量, 记录图像灰度值异常累计数量
static int g_nImgBrightnessErrorCount = 0;
// 把手候选bigmask 凸起 + 实测特征(后续增加面积/灰阶等判定时直接用这些实测值)
struct Handle_Candidate_
{
Mask_Protrusion_ protr; // 凸起几何信息roi / 沿边长度 / 深度 / 面积 / 填充率)
int nLen; // 沿边长度(像素)
int nDepth; // 垂直深度(像素)
float fGrayDiff; // 凸起区域与周边背景的平均灰度差(预留灰阶判定)
bool bHandle; // 是否为把手
Handle_Candidate_()
{
nLen = 0;
nDepth = 0;
fGrayDiff = 0.0f;
bHandle = false;
}
};
class ImgCheckAnalysisy : public ImgCheckBase
{
@ -117,6 +135,13 @@ private:
// 检测
int CheckRun();
int AI_Edge(const cv::Mat &img, cv::RotatedRect &outerRoi, cv::RotatedRect &innerRoi, std::vector<cv::RotatedRect> &tagroiList);
// 用 bigmask 检测把手bigmask 主体近似矩形,把手是四周凸起的矩形块)
int CheckHandleByBigMask(const cv::Mat &img, const cv::Mat &bigmask);
// 判断凸起是否为把手:参数里每一项开启的判定条件(尺寸/填充率/面积/灰阶…)都满足才算把手
bool IsHandleProtrusion(const Handle_Candidate_ &cand, const Handle_Check_Param &param,
const cv::Rect &handleBox, const cv::Size &maskSize);
// 计算凸起区域与周边背景的平均灰度差灰阶判定的预留量测值img 需为单通道)
float CalProtrusionGrayDiff(const cv::Mat &grayImg, const cv::Rect &roi);
// 计算产品尺寸
int CalProductSize();
// 图片预处理
@ -289,6 +314,14 @@ private:
cv::RotatedRect m_outer_rroi;
cv::RotatedRect m_inner_rroi;
std::vector<cv::RotatedRect> m_tag_roiList;
// 把手检测结果bigmask 四周凸起)
std::vector<Handle_Candidate_> m_HandleCandList; // bigmask 四周抠出的凸起候选 + 实测特征
std::vector<cv::Rect> m_HandleRoiList; // 判定为把手的凸起 roi检测图坐标
cv::Rect m_HandleBoxParamImg; // 参数里绘制的把手区域(检测图坐标)
bool m_bHandleDetSucc; // 本次是否完成把手检测
bool m_bHandleFound; // 是否检测到把手
bool m_bHandleExpect; // 参数中是否绘制了把手区域(有期望)
};
#endif

@ -693,3 +693,271 @@ 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));
}
int CheckUtil::GetMaskProtrusions(const cv::Mat &binMask, std::vector<Mask_Protrusion_> &protrusions,
int nJumpThresh, int nMinRun, int nMinDepth, int nMaxRun)
{
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);
// 1.1 大掩膜先降采样再分析:凸起尺寸远大于降采样误差,可显著降低耗时
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;
}
// 阈值按降采样比例换算到工作图尺度
nJumpThresh = std::max(2, nJumpThresh / nScale);
nMinRun = std::max(3, nMinRun / nScale);
nMinDepth = std::max(2, nMinDepth / nScale);
// 2、只保留最大连通域避免背景杂物参与边界统计
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;
}
}
// 3、裁剪到主体外接矩形缩小后续扫描范围
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 sub = (labels(boxMain) == nMainIdx); // 0/255
const int H = sub.rows;
const int W = sub.cols;
if (nMaxRun <= 0)
{
nMaxRun = std::max(100, std::min(H, W) / 3);
}
// 4、逐边抠凸起
// 主体近似矩形,矩形边上的凸起会让该边的"边界值"先突变到外侧(进入凸起)、再突变回主体(离开凸起)
// 区间内的边界值相对"首尾边界值线性插值"得到的基线持续外偏,即判定为一个凸起。
for (int nSide = 0; nSide < 4; nSide++)
{
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);
for (int x = 0; x < W; x++)
{
if (p[x])
{
nV = x;
break;
}
}
}
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();
if (n < nMinRun + 2)
{
continue;
}
// 4.2 配对"进入/离开"突变,扣除该边上的凸起
int nUsedTo = -1; // 已统计过的凸起末端,避免同一个凸起重复输出
for (int s = 0; s + 1 < n; s++)
{
if (s <= nUsedTo)
{
continue;
}
// 进入凸起:边界值朝外侧突变
if ((valList[s + 1] - valList[s]) * nDir >= -nJumpThresh)
{
continue;
}
// 离开凸起:在其后 nMaxRun 范围内找第一个反向突变
int e = -1;
for (int j = s + nMinRun; j + 1 < n && j - s <= nMaxRun; j++)
{
if ((valList[j + 1] - valList[j]) * nDir > nJumpThresh)
{
e = j;
break;
}
}
if (e < 0)
{
continue;
}
// 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);
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 图上的位置)
const int nLen = posList[e] - posList[s + 1] + 1; // 沿该边的长度
cv::Rect roiSub;
if (nSide == 0 || nSide == 2) // 左/上:外侧取小值,主体边缘取大值
{
int nOut = nLow;
int nInner = std::max(valList[s], valList[e + 1]);
if (nSide == 0)
{
roiSub = cv::Rect(nOut, posList[s + 1], nInner - nOut + 1, nLen);
}
else
{
roiSub = cv::Rect(posList[s + 1], nOut, nLen, nInner - nOut + 1);
}
}
else // 右/下:外侧取大值,主体边缘取小值
{
int nOut = nHigh;
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);
}
}
roiSub &= cv::Rect(0, 0, W, H);
if (roiSub.width <= 0 || roiSub.height <= 0)
{
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; // 该凸起区间不再重复统计
}
}
return 0;
}

@ -12,6 +12,19 @@
#include <omp.h>
#include "AICommonDefine.h"
#include <algorithm>
// 把手判定参数:
// 1、有参数参数里绘制了把手区域凸起"沿边长度"与手绘区域对应边长度的比值范围
#define HANDLE_LEN_MIN_RATIO 0.75f
#define HANDLE_LEN_MAX_RATIO 1.5f
// 2、凸起"垂直深度"与手绘区域对应边宽度的比值范围(凸起被图像边界截断时不校深度)
#define HANDLE_DEPTH_MIN_RATIO 0.8f
#define HANDLE_DEPTH_MAX_RATIO 1.5f
// 3、无参数时按掩膜短边推算沿边长度/垂直深度范围(最小/最大占比)
#define HANDLE_DEFAULT_MIN_RATIO 0.03f
#define HANDLE_DEFAULT_MAX_RATIO 0.40f
// 4、凸起填充率下限、面积/灰阶等判定参数的默认值都在 Handle_Check_ParamCheckConfigDefine.h
// 用于排序轮廓的比较函数
static bool compareContourAreas(const vector<Point> &contour1, const vector<Point> &contour2)
{
@ -598,6 +611,60 @@ int ImgCheckAnalysisy::CheckRun()
}
}
/* 把手缺失检测bigmask 的凸起本身不算 NG只有"参数里画了把手区域但没检出把手"才算 NG */
{
if (m_bHandleDetSucc && m_bHandleExpect && !m_bHandleFound)
{
QX_ERROR_INFO_ temerror;
temerror.Idx = m_pDetResult->pQx_ErrorList->size();
// 上报参数里绘制的把手区域位置:原图坐标 -> 检测图(detImg)坐标
cv::Rect roi = m_HandleBoxParamImg;
roi.x -= m_outer_roi.x;
roi.y -= m_outer_roi.y;
roi &= cv::Rect(0, 0, m_pImageAllResult->detImg.cols, m_pImageAllResult->detImg.rows);
if (roi.width <= 0 || roi.height <= 0)
{
roi = cv::Rect(0, 0, m_pImageAllResult->detImg.cols, m_pImageAllResult->detImg.rows);
}
temerror.roi = roi;
temerror.area = roi.width * roi.height;
temerror.JudgArea = roi.width * m_fImgage_Scale_X * roi.height * m_fImgage_Scale_Y;
temerror.JudgArea_second = temerror.JudgArea;
float w = roi.width;
float h = roi.height;
temerror.flen = (w > h ? w : h) * m_fImgage_Scale_X;
temerror.fbreadth = (w > h ? h : w) * m_fImgage_Scale_Y;
temerror.nconfig_qx_type = CONFIG_QX_NAME_handle_loss;
temerror.qx_name = CONFIG_QX_NAME_Names[CONFIG_QX_NAME_handle_loss];
temerror.result = QX_RESULT_TYPE_NG;
temerror.result_name = QX_RESULT_TYPE_Names[QX_RESULT_TYPE_NG];
// 计算把手中心所在的检测区域
cv::Point pCenter;
pCenter.x = roi.x + roi.width * 0.5;
pCenter.y = roi.y + roi.height * 0.5;
int nmaxregionIdx = 0;
for (int iregion = 0; iregion < m_DetRoiList.roiList_Src.size(); iregion++)
{
const std::vector<cv::Point> &polygon = m_DetRoiList.roiList_Src[iregion];
double result = cv::pointPolygonTest(polygon, pCenter, false);
if (result < 0)
{
continue;
}
nmaxregionIdx = iregion;
}
temerror.detRegionidxList.push_back(nmaxregionIdx);
m_pDetResult->pQx_ErrorList->push_back(temerror);
m_pdetlog->AddCheckstr(PrintLevel_0, "把手检测", "%s %d roi [%d %d %d %d] handle param box [%d %d %d %d]",
temerror.qx_name.c_str(), temerror.Idx, roi.x, roi.y, roi.width, roi.height,
m_HandleBoxParamImg.x, m_HandleBoxParamImg.y, m_HandleBoxParamImg.width, m_HandleBoxParamImg.height);
}
}
/* Tag检测 */
{
for (const auto &tagRoi : m_tag_roiList)
@ -2065,95 +2132,262 @@ int ImgCheckAnalysisy::AI_Edge(const cv::Mat &img, cv::RotatedRect &outerRoi, cv
}
/*使用m_pEdge_Align_Result->bigmask检测把手是否缺失*/
// 截取把手大致区域(使用把手模板完整 boundingRect而非整图高度
RotatedRect tpl_handle_rroi = m_pFuntion->function.f_supportDet.handleRect;
Rect handle_rect = tpl_handle_rroi.boundingRect() & Rect(0, 0, m_pEdge_Align_Result->bigmask.cols, m_pEdge_Align_Result->bigmask.rows);
if (handle_rect.width <= 0 || handle_rect.height <= 0)
{
long handle_s = CheckUtil::getcurTime();
CheckHandleByBigMask(img, m_pEdge_Align_Result->bigmask);
long handle_e = CheckUtil::getcurTime();
m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Handle", "handle detect use time %ld", handle_e - handle_s);
}
return re;
}
// 用 bigmask 检测把手:
// 1、bigmask 主体近似矩形,把手是主体四周凸起的"类矩形块"
// 2、把主体四周的凸起抠掉凸起在该边的边界值上表现为一次突变进入 + 一次反向突变离开),剩下的就是矩形主体
// 3、按参数里绘制的把手区域尺寸沿边长度 + 垂直深度,带容差)逐个判定凸起是否为把手
int ImgCheckAnalysisy::CheckHandleByBigMask(const cv::Mat &img, const cv::Mat &bigmask)
{
m_bHandleDetSucc = false;
m_bHandleFound = false;
m_bHandleExpect = false;
m_HandleBoxParamImg = cv::Rect(0, 0, 0, 0);
m_HandleCandList.clear();
m_HandleRoiList.clear();
if (bigmask.empty())
{
m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Handle", "bigmask is empty, skip handle detect");
return 1;
}
// 1、抠出 bigmask 四周凸起的矩形块
std::vector<Mask_Protrusion_> protrusionList;
int re = CheckUtil::GetMaskProtrusions(bigmask, protrusionList);
if (re != 0)
{
m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Handle", "GetMaskProtrusions error %d", re);
return re;
}
Mat handle_roi = m_pEdge_Align_Result->bigmask(handle_rect).clone();
m_bHandleDetSucc = true;
// 对handle_roi做一下开运算
Mat element = getStructuringElement(MORPH_RECT, Size(std::max(1, tpl_handle_rroi.boundingRect().width / 3), std::max(1, tpl_handle_rroi.boundingRect().height / 3)));
morphologyEx(handle_roi, handle_roi, MORPH_OPEN, element);
// 2、把手参照尺寸 + 判定参数:都来自参数里绘制的把手区域(保留图像 x/y 方向)
Handle_Check_Param handleParam;
if (m_pFuntion != NULL)
{
const Function_Support_Det &supportDet = m_pFuntion->function.f_supportDet;
handleParam = supportDet.handleParam;
if (supportDet.handleRegion.size() > 0)
{
m_HandleBoxParamImg = cv::boundingRect(supportDet.handleRegion);
}
}
m_bHandleExpect = (handleParam.bOpen && m_HandleBoxParamImg.width > 1 && m_HandleBoxParamImg.height > 1);
m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Handle",
"protrusion num %ld handle param box [%d %d %d %d] expect %s",
protrusionList.size(), m_HandleBoxParamImg.x, m_HandleBoxParamImg.y,
m_HandleBoxParamImg.width, m_HandleBoxParamImg.height, BOOL_TO_STR(m_bHandleExpect));
m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Handle", "%s", handleParam.GetInfo("judge").c_str());
// imwrite("handle_roi.png", handle_roi);
// 灰阶判定是预留项:只有开启时才做灰度换算,避免无谓开销
cv::Mat grayImg;
if (handleParam.bJudgeGray && !img.empty())
{
if (img.channels() != 1)
{
cv::cvtColor(img, grayImg, cv::COLOR_BGR2GRAY);
}
else
{
grayImg = img;
}
}
// handle_roi找到最大连通域和tpl_handle_rroi做面积形状比较差异过大判为把手缺失添加到缺陷并NG
// 3、逐个凸起先算实测特征沿边长度/深度/面积/灰度差),再做判定
cv::Size maskSize(bigmask.cols, bigmask.rows);
for (size_t i = 0; i < protrusionList.size(); i++)
{
Function_Support_Det &handleDet = m_pFuntion->function.f_supportDet;
if (handleDet.bOpen && tpl_handle_rroi.size.width > 0 && tpl_handle_rroi.size.height > 0 && !handle_roi.empty())
Handle_Candidate_ cand;
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;
if (!grayImg.empty())
{
std::vector<std::vector<cv::Point>> handle_contours;
cv::findContours(handle_roi, handle_contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
cand.fGrayDiff = CalProtrusionGrayDiff(grayImg, cand.protr.roi);
}
cand.bHandle = IsHandleProtrusion(cand, handleParam, m_HandleBoxParamImg, maskSize);
if (cand.bHandle)
{
m_HandleRoiList.push_back(cand.protr.roi);
}
m_HandleCandList.push_back(cand);
// grayDiff 打印 -1 表示本次未开启灰阶判定(未计算)
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 grayDiff %0.1f -> %s",
i, cand.protr.nSide, cand.protr.roi.x, cand.protr.roi.y, cand.protr.roi.width, cand.protr.roi.height,
cand.nLen, cand.nDepth, cand.protr.fArea, cand.protr.fFillRatio,
grayImg.empty() ? -1.0f : cand.fGrayDiff, cand.bHandle ? "handle" : "not handle");
}
m_bHandleFound = (!m_HandleRoiList.empty());
bool bHandleLoss = false;
if (handle_contours.empty())
{
bHandleLoss = true; // 没有连通域,把手完全缺失
}
else
{
// 取最大连通域
std::sort(handle_contours.begin(), handle_contours.end(), compareContourAreas);
cv::RotatedRect cur_handle_rrect = cv::minAreaRect(handle_contours[0]);
double curArea = cv::contourArea(handle_contours[0]);
// 面积比较
double tplArea = tpl_handle_rroi.size.width * tpl_handle_rroi.size.height;
double areaRatio = (tplArea > 0.0) ? (curArea / tplArea) : 0.0;
// 形状比较(长宽比)
float tplW = std::max(tpl_handle_rroi.size.width, tpl_handle_rroi.size.height);
float tplH = std::min(tpl_handle_rroi.size.width, tpl_handle_rroi.size.height);
float curW = std::max(cur_handle_rrect.size.width, cur_handle_rrect.size.height);
float curH = std::min(cur_handle_rrect.size.width, cur_handle_rrect.size.height);
float tplRatio = (tplH > 0.0f) ? (tplW / tplH) : 0.0f;
float curRatio = (curH > 0.0f) ? (curW / curH) : 0.0f;
float ratioDiff = (tplRatio > 0.0f) ? fabs(curRatio - tplRatio) / tplRatio : 0.0f;
// 面积差异过大 或 形状差异过大 判为把手缺失
if (areaRatio < 0.6f || ratioDiff > 0.6f)
{
bHandleLoss = true;
}
m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, "Handle", "handle num %ld found %s use handle param %s",
m_HandleRoiList.size(), BOOL_TO_STR(m_bHandleFound), BOOL_TO_STR(m_bHandleExpect));
m_pdetlog->AddCheckstr(PrintLevel_0, "把手检测", "areaRatio %f ratioDiff %f", areaRatio, ratioDiff);
}
// 参数里绘制了把手区域,但 bigmask 四周没有满足判定条件的凸起 -> 把手缺失NG 由 CheckRun 上报)
if (m_bHandleExpect && !m_bHandleFound)
{
m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, "Handle", "handle is missing !!");
m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, "Handle", "handle param box [%d %d %d %d]",
m_HandleBoxParamImg.x, m_HandleBoxParamImg.y, m_HandleBoxParamImg.width, m_HandleBoxParamImg.height);
}
if (bHandleLoss)
{
QX_ERROR_INFO_ temerror;
temerror.Idx = m_pDetResult->pQx_ErrorList->size();
// 4、存过程图凸起红框、把手绿框仅调试时
if (DetImgInfo_shareP->bsaveProcessImg)
{
cv::Mat show;
if (bigmask.channels() == 1)
{
cv::cvtColor(bigmask, show, cv::COLOR_GRAY2BGR);
}
else
{
show = bigmask.clone();
}
for (size_t i = 0; i < m_HandleCandList.size(); i++)
{
cv::rectangle(show, m_HandleCandList[i].protr.roi, cv::Scalar(0, 0, 255), 8);
}
for (size_t i = 0; i < m_HandleRoiList.size(); i++)
{
cv::rectangle(show, m_HandleRoiList[i], cv::Scalar(0, 255, 0), 12);
}
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 + "_handle_det.jpg", showSmall, paramJpg);
}
// 把手模板 roi 在原图坐标系,转换为检测图(detImg)坐标系
cv::Rect roi = tpl_handle_rroi.boundingRect();
roi.x -= outerRoi.boundingRect().x;
roi.y -= outerRoi.boundingRect().y;
roi &= cv::Rect(0, 0, outerRoi.boundingRect().width, outerRoi.boundingRect().height);
temerror.roi = roi;
temerror.area = roi.width * roi.height;
temerror.JudgArea = roi.width * m_fImgage_Scale_X * roi.height * m_fImgage_Scale_Y;
temerror.JudgArea_second = temerror.JudgArea;
float w = tpl_handle_rroi.size.width;
float h = tpl_handle_rroi.size.height;
temerror.flen = (w > h ? w : h) * m_fImgage_Scale_X;
temerror.fbreadth = (w > h ? h : w) * m_fImgage_Scale_Y;
temerror.nconfig_qx_type = CONFIG_QX_NAME_support_loss;
temerror.qx_name = CONFIG_QX_NAME_Names[temerror.nconfig_qx_type];
temerror.result = QX_RESULT_TYPE_NG;
temerror.result_name = QX_RESULT_TYPE_Names[QX_RESULT_TYPE_NG];
temerror.detRegionidxList.push_back(0);
return 0;
}
m_pDetResult->pQx_ErrorList->push_back(temerror);
// 判断凸起是否为把手:参数里每一项"开启"的判定条件都满足才算把手
// 尺寸参照参数里绘制的把手区域handleBox保留图像 x/y 方向):
// 沿边长度 对应 handleBox 在该边方向上的长度,垂直深度 对应 handleBox 的宽度;
// 凸起外边缘被图像边界截断时,深度不可信,只校验沿边长度
// 参数里没写显式像素范围时按把手区域尺寸的比例推算HANDLE_LEN_*_RATIO / HANDLE_DEPTH_*_RATIO
bool ImgCheckAnalysisy::IsHandleProtrusion(const Handle_Candidate_ &cand, const Handle_Check_Param &param,
const cv::Rect &handleBox, const cv::Size &maskSize)
{
const Mask_Protrusion_ &protr = cand.protr;
if (protr.roi.width <= 0 || protr.roi.height <= 0)
{
return false;
}
m_pdetlog->AddCheckstr(PrintLevel_0, "把手检测", "handle loss NG roi [%d %d %d %d]", roi.x, roi.y, roi.width, roi.height);
}
}
// 左/右凸起:沿边为 y 方向(长度=height),深度为 x 方向(width);上/下凸起相反
const bool bHorz = (protr.nSide == 0 || protr.nSide == 1);
// 凸起外边缘是否贴到图像边界(被截断)
bool bClip = bHorz ? (protr.roi.x <= 0 || protr.roi.x + protr.roi.width >= maskSize.width)
: (protr.roi.y <= 0 || protr.roi.y + protr.roi.height >= maskSize.height);
// 该凸起对应的期望尺寸范围
int nMinLen = 0, nMaxLen = 0, nMinDepth = 0, nMaxDepth = 0;
if (handleBox.width > 1 && handleBox.height > 1)
{
int nExpLen = bHorz ? handleBox.height : handleBox.width;
int nExpDepth = bHorz ? handleBox.width : handleBox.height;
nMinLen = (int)(nExpLen * HANDLE_LEN_MIN_RATIO);
nMaxLen = (int)(nExpLen * HANDLE_LEN_MAX_RATIO);
nMinDepth = (int)(nExpDepth * HANDLE_DEPTH_MIN_RATIO);
nMaxDepth = (int)(nExpDepth * HANDLE_DEPTH_MAX_RATIO);
}
else // 参数里没画把手区域:按掩膜短边推算一个大致范围
{
int nMinSide = std::min(maskSize.width, maskSize.height);
nMinLen = nMinDepth = (int)(nMinSide * HANDLE_DEFAULT_MIN_RATIO);
nMaxLen = nMaxDepth = (int)(nMinSide * HANDLE_DEFAULT_MAX_RATIO);
}
// 参数里写了显式像素范围时以参数值为准
if (param.bJudgeLen && param.fLenMin > 0)
{
nMinLen = (int)param.fLenMin;
}
if (param.bJudgeLen && param.fLenMax > 0)
{
nMaxLen = (int)param.fLenMax;
}
if (param.bJudgeDepth && param.fDepthMin > 0)
{
nMinDepth = (int)param.fDepthMin;
}
if (param.bJudgeDepth && param.fDepthMax > 0)
{
nMaxDepth = (int)param.fDepthMax;
}
return re;
// 以下条件逐条校验,任意一条不满足即不是把手
// 1、类似矩形块凸起区域内掩膜填充率要高
if (param.bJudgeFill && protr.fFillRatio < param.fFillMin)
{
return false;
}
// 2、沿边长度
if (param.bJudgeLen && (cand.nLen < nMinLen || cand.nLen > nMaxLen))
{
return false;
}
// 3、垂直深度凸起被图像边界截断时跳过
if (param.bJudgeDepth && !bClip && (cand.nDepth < nMinDepth || cand.nDepth > nMaxDepth))
{
return false;
}
// 4、预留凸起面积
if (param.bJudgeArea && param.fAreaMax > param.fAreaMin &&
(protr.fArea < param.fAreaMin || protr.fArea > param.fAreaMax))
{
return false;
}
// 5、预留凸起与背景的灰度差
if (param.bJudgeGray && param.fGrayMax > param.fGrayMin &&
(cand.fGrayDiff < param.fGrayMin || cand.fGrayDiff > param.fGrayMax))
{
return false;
}
return true;
}
// 计算凸起区域与周边背景的平均灰度差灰阶判定的预留量测值grayImg 需为单通道)
float ImgCheckAnalysisy::CalProtrusionGrayDiff(const cv::Mat &grayImg, const cv::Rect &roi)
{
if (grayImg.empty() || grayImg.channels() != 1 || roi.width <= 0 || roi.height <= 0)
{
return 0.0f;
}
// 凸起区域通常整个都在掩膜内,向外扩一圈才能取到背景
int nExpand = std::max(20, std::min(roi.width, roi.height) / 2);
cv::Rect roiBg(roi.x - nExpand, roi.y - nExpand, roi.width + 2 * nExpand, roi.height + 2 * nExpand);
roiBg &= cv::Rect(0, 0, grayImg.cols, grayImg.rows);
if (roiBg.width <= 0 || roiBg.height <= 0)
{
return 0.0f;
}
cv::Mat grayRoi = grayImg(roiBg);
cv::Mat maskIn = cv::Mat::zeros(roiBg.size(), CV_8UC1);
cv::Rect inter = roi & roiBg;
cv::rectangle(maskIn, inter - roiBg.tl(), cv::Scalar(255), cv::FILLED);
cv::Mat maskOut;
cv::bitwise_not(maskIn, maskOut);
if (cv::countNonZero(maskIn) <= 0 || cv::countNonZero(maskOut) <= 0)
{
return 0.0f;
}
double fMeanIn = cv::mean(grayRoi, maskIn)[0];
double fMeanOut = cv::mean(grayRoi, maskOut)[0];
double fDiff = fMeanIn - fMeanOut;
return (float)(fDiff >= 0 ? fDiff : -fDiff);
}
int ImgCheckAnalysisy::CalProductSize()

@ -54,6 +54,7 @@ enum CONFIG_QX_NAME_
CONFIG_QX_NAME_cell_tag, // 分类 标签
CONFIG_QX_NAME_support_offset, // 支架偏移
CONFIG_QX_NAME_support_loss, // 支架缺失
CONFIG_QX_NAME_handle_loss, // 把手缺失
CONFIG_QX_NAME_count,
};
// 缺陷项对应在参数中的名称
@ -74,6 +75,7 @@ static std::vector<std::string> CONFIG_QX_NAME_Names =
"tag",
"support_offset",
"support_loss",
"handle_loss",
};
// 分析类型
@ -932,6 +934,65 @@ struct Function_Image_Align
return str123;
}
};
// 把手缺失判定参数:
// 当前用"沿边长度 + 垂直深度"判定,后续可继续添加面积、灰阶等参数,
// 每一项都有独立的开关,所有开启的条件都满足才把凸起判为把手
struct Handle_Check_Param
{
bool bOpen; // 是否检测把手缺失false 时不做把手判定,也不报 NG
bool bJudgeFill; // 是否用"类矩形块"填充率判定
float fFillMin; // 填充率下限
bool bJudgeLen; // 是否用"沿边长度"判定
float fLenMin; // 沿边长度下限(像素,<=0 时按把手区域尺寸比例推算)
float fLenMax; // 沿边长度上限(像素,<=0 时按把手区域尺寸比例推算)
bool bJudgeDepth; // 是否用"垂直深度"判定
float fDepthMin; // 垂直深度下限(像素,<=0 时按把手区域尺寸比例推算)
float fDepthMax; // 垂直深度上限(像素,<=0 时按把手区域尺寸比例推算)
bool bJudgeArea; // 预留:凸起面积判定
float fAreaMin;
float fAreaMax;
bool bJudgeGray; // 预留:凸起与背景灰度差判定
float fGrayMin;
float fGrayMax;
Handle_Check_Param()
{
Init();
}
void Init()
{
bOpen = true;
bJudgeFill = true;
fFillMin = 0.75f;
bJudgeLen = true;
fLenMin = 0;
fLenMax = 0;
bJudgeDepth = true;
fDepthMin = 0;
fDepthMax = 0;
bJudgeArea = false;
fAreaMin = 0;
fAreaMax = 0;
bJudgeGray = false;
fGrayMin = 0;
fGrayMax = 0;
}
void copy(Handle_Check_Param tem)
{
// 全为基本类型,直接赋值(避免后续加字段时漏拷)
*this = tem;
}
std::string GetInfo(std::string str)
{
char buffer[256];
sprintf(buffer, "%s>>bOpen %d fill[%d %0.2f] len[%d %0.0f %0.0f] depth[%d %0.0f %0.0f] area[%d %0.0f %0.0f] gray[%d %0.0f %0.0f]\n",
str.c_str(), bOpen, bJudgeFill, fFillMin, bJudgeLen, fLenMin, fLenMax,
bJudgeDepth, fDepthMin, fDepthMax, bJudgeArea, fAreaMin, fAreaMax, bJudgeGray, fGrayMin, fGrayMax);
std::string str123 = buffer;
return str123;
}
};
// 支架检测
struct Function_Support_Det
{
@ -943,6 +1004,7 @@ struct Function_Support_Det
float r_offset;
std::vector<cv::Point> handleRegion;
cv::RotatedRect handleRect;
Handle_Check_Param handleParam; // 把手缺失判定参数
Function_Support_Det ()
{
@ -959,6 +1021,7 @@ struct Function_Support_Det
r_offset = 0;
handleRegion.clear();
handleRect = cv::RotatedRect();
handleParam.Init();
}
void copy(Function_Support_Det tem)
{
@ -970,18 +1033,21 @@ struct Function_Support_Det
this->r_offset = tem.r_offset;
this->handleRegion.assign(tem.handleRegion.begin(), tem.handleRegion.end());
this->handleRect = tem.handleRect;
this->handleParam.copy(tem.handleParam);
}
void print(std::string str)
{
printf("%s>>bOpen %d x_offset %f y_offset %f r_offset %f\n", str.c_str(),
bOpen, x_offset, y_offset, r_offset);
printf("%s", handleParam.GetInfo("handleParam").c_str());
}
std::string GetInfo(std::string str)
{
char buffer[256];
sprintf(buffer, "%s>>bOpen %d x_offset %f y_offset %f r_offset %f\n", str.c_str(),
bOpen, x_offset, y_offset, r_offset);
bOpen, x_offset, y_offset, r_offset);
std::string str123 = buffer;
str123 += handleParam.GetInfo("handleParam");
return str123;
}

@ -470,6 +470,72 @@ int ChannelFuntonConfigJson::GetFunction(Json::Value value, CheckFunction &funct
}
}
}
// 把手缺失判定参数(参数缺失时保留默认值:长度/深度按把手区域尺寸比例推算)
{
auto handle_p = value_f["form"]["handle_param"];
Handle_Check_Param &handleParam = function.f_supportDet.handleParam;
if (handle_p["handle_disabled"].isBool())
{
handleParam.bOpen = !handle_p["handle_disabled"].asBool();
}
if (handle_p["handle_fill_disabled"].isBool())
{
handleParam.bJudgeFill = !handle_p["handle_fill_disabled"].asBool();
}
if (handle_p["handle_fill_min"].isNumeric())
{
handleParam.fFillMin = handle_p["handle_fill_min"].asFloat();
}
if (handle_p["handle_len_disabled"].isBool())
{
handleParam.bJudgeLen = !handle_p["handle_len_disabled"].asBool();
}
if (handle_p["handle_len_min"].isNumeric())
{
handleParam.fLenMin = handle_p["handle_len_min"].asFloat();
}
if (handle_p["handle_len_max"].isNumeric())
{
handleParam.fLenMax = handle_p["handle_len_max"].asFloat();
}
if (handle_p["handle_depth_disabled"].isBool())
{
handleParam.bJudgeDepth = !handle_p["handle_depth_disabled"].asBool();
}
if (handle_p["handle_depth_min"].isNumeric())
{
handleParam.fDepthMin = handle_p["handle_depth_min"].asFloat();
}
if (handle_p["handle_depth_max"].isNumeric())
{
handleParam.fDepthMax = handle_p["handle_depth_max"].asFloat();
}
if (handle_p["handle_area_disabled"].isBool())
{
handleParam.bJudgeArea = !handle_p["handle_area_disabled"].asBool();
}
if (handle_p["handle_area_min"].isNumeric())
{
handleParam.fAreaMin = handle_p["handle_area_min"].asFloat();
}
if (handle_p["handle_area_max"].isNumeric())
{
handleParam.fAreaMax = handle_p["handle_area_max"].asFloat();
}
if (handle_p["handle_gray_disabled"].isBool())
{
handleParam.bJudgeGray = !handle_p["handle_gray_disabled"].asBool();
}
if (handle_p["handle_gray_min"].isNumeric())
{
handleParam.fGrayMin = handle_p["handle_gray_min"].asFloat();
}
if (handle_p["handle_gray_max"].isNumeric())
{
handleParam.fGrayMax = handle_p["handle_gray_max"].asFloat();
}
}
}
else
{

Loading…
Cancel
Save