diff --git a/AlgorithmModule/include/ImageAllResult.h b/AlgorithmModule/include/ImageAllResult.h index fb78851..b789910 100644 --- a/AlgorithmModule/include/ImageAllResult.h +++ b/AlgorithmModule/include/ImageAllResult.h @@ -64,6 +64,8 @@ public: cv::Mat detImg; // 裁剪后的彩色检测图(保留原图通道,供裁剪图 _RE_CutImg 输出;灰度源图时为空) cv::Mat detColorImg; + // 只做"裁剪"、不叠加任何 mask 的检测图(同 detImg 尺寸/坐标系) + cv::Mat detImgNoMask; // AI 推理的mask图片 cv::Mat AIMaskImg; // 结果图片 diff --git a/AlgorithmModule/src/ImgCheckAnalysisy.cpp b/AlgorithmModule/src/ImgCheckAnalysisy.cpp index f87c9cc..58cc82d 100644 --- a/AlgorithmModule/src/ImgCheckAnalysisy.cpp +++ b/AlgorithmModule/src/ImgCheckAnalysisy.cpp @@ -1,4324 +1,4489 @@ -/* - * @Author: your name - * @Date: 2022-04-20 15:50:00 - * @LastEditTime: 2025-09-23 11:34:53 - * @LastEditors: xiewenji 527774126@qq.com - * @Description: 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE - * @FilePath: /ZCXD_MonitorPlatform/src/CoreLogicModule/src/CamDeal.cpp - */ -#include "ImgCheckAnalysisy.hpp" -#include "CheckUtil.hpp" -#include "Define.h" -#include -#include "AICommonDefine.h" -#include "AlignShare.h" -#include -#include -#include -#include -#include - -// AI缺陷分类模型开关:当前暂无分类模型(Class.engine),分类流程代码先预留; -// 待分类模型部署后,将 AI_CLASS_MODEL_ENABLE 置 1,即可恢复 AI_QX_Class_Thread 中的AI分类流程 -#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_MAX_RATIO 1.2f -// "附近"的搜索范围(JSON: handle_search_ratio):把手区域外接矩形在宽/高方向各外扩该比例 -// 调大:位置约束变松,产品摆放偏移大也不易漏(但可能匹配到别的凸起); -// 调小:位置约束更严,偏移稍大就会判"附近没有把手" -#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) - -// 支架边缘原图精细化默认参数(JSON: Support_Detect.form.support_param 下的 edge_refine_search / edge_refine_step): -// 仅对 edge_select 选中的边缘生效:沿边缘按步长采样,从边缘向产品内侧逐像素找第一个 255 像素作为新边缘点, -// 有效点足够时重新拟合边缘线(找不到的采样点没有新信息,不参与拟合,相当于沿用原边缘点)。 -// edge_refine_search:最多向内搜索的像素数。搜不到 → 该采样点无贡献;调大更灵敏但可能搜到内部亮区 -#define EDGE_REFINE_SEARCH_DEFAULT 80 -// edge_refine_step:沿边采样步长(像素)。不需要每个点都找,调大可省时间;调小边缘拟合更稳 -#define EDGE_REFINE_STEP_DEFAULT 40 - -// ===== 支架方向精细化默认参数 ===== -// (JSON: Support_Detect.form.support_param 下的 -// support_refine_search / support_refine_step / support_refine_segments,均可选) -// 背景:支架由 Align_Inner 模型(smallmask,1696×800)检出,上采样回原图后边缘量化误差约 4px, -// 且支架区域接近方形时 minAreaRect 的"长轴"方向不稳定,用它求旋转角误差大。 -// 改为用模型支架边缘做初值,在原图(detImg 灰度)上拟合支架长边(朝产品内侧的边)求方向角。 -// support_refine_search:取模型支架外接矩形最外侧多少列作为边缘搜索带(未配置默认 30 列)。 -// 右边缘 → 取外接矩形最右 nStrip 列,从右往左逐像素找灰度梯度最大的点; -// 左边缘 → 取最左 nStrip 列,从左往右找(相反逻辑) -// 调大:覆盖更大的模型误差/边缘更靠内,但可能扫到支架内部结构; -// 调小:更严格,模型边缘偏差大时找不到边缘(回退 minAreaRect 方案) -#define SUPPORT_REFINE_STRIP_DEFAULT 30 -// support_refine_step:沿支架长边采样的步长(像素)。调大可省时间;调小拟合更稳 -#define SUPPORT_REFINE_STEP_DEFAULT 20 -// support_refine_segments:把采样点沿边分成 n 段,每段稳健拟合一条直线,取各段方向角的中位数。 -// 支架边缘有凸起时:凸起处采样点成为所在段的离群点(MAD 剔除);凸起外边缘与支架边缘平行, -// 即使拟合到凸起外边缘也不影响方向角 -#define SUPPORT_REFINE_SEGMENTS_DEFAULT 6 -// 搜索列带内相邻像素(3 行灰度均值)的灰度差小于该值视为无边缘(极性无关,支架比外侧亮/暗均可) -#define SUPPORT_REFINE_MIN_CONTRAST 15.0 - -// 分类映射配置文件(每行:label索引:缺陷名称;冒号后名称为该缺陷类型名称,行的顺序即缺陷类型索引) -static const std::string CLS_LABEL_CONFIG_FILE = "/home/aidlux/BOE/UseModel_ZB/BM_cls.txt"; - -// 去掉字符串首尾的空白字符和引号 -static std::string ClsTrimStr(const std::string &str) -{ - size_t begin = str.find_first_not_of(" \t\r\n\""); - if (begin == std::string::npos) - { - return ""; - } - size_t end = str.find_last_not_of(" \t\r\n\""); - return str.substr(begin, end - begin + 1); -} - -// 组装程序内默认缺陷名称列表(AI_CLass_QX_NAME_Names) -static std::vector BuildDefaultAIQXNames() -{ - std::vector vecNames; - for (int i = 0; i < AI_CLass_QX_NAME_count; i++) - { - vecNames.push_back(AI_CLass_QX_NAME_Names[i]); - } - return vecNames; -} - -// 分类映射配置:缺陷名称列表(索引=行序) + 分类label->缺陷名称 映射 -struct ClsLabelConfig -{ - std::vector vecNames; // 缺陷名称列表 - std::map mapLabelName; // 分类 label -> 缺陷名称 -}; - -// 读取分类映射配置(每行:label索引:缺陷名称;冒号后名称按行序构成缺陷名称列表;# 后内容忽略) -static ClsLabelConfig ReadClsLabelConfig(const std::string &strFilePath) -{ - ClsLabelConfig config; - std::ifstream ifs(strFilePath.c_str()); - if (!ifs.is_open()) - { - printf("Open cls label config failed, use default mapping: %s\n", strFilePath.c_str()); - return config; - } - - std::string strLine; - while (std::getline(ifs, strLine)) - { - // # 后内容(含注释)忽略 - size_t nPosComment = strLine.find('#'); - if (nPosComment != std::string::npos) - { - strLine = strLine.substr(0, nPosComment); - } - size_t nPos = strLine.find(':'); - if (nPos == std::string::npos) - { - // 无冒号:非标准行,跳过并提示(仅支持 label索引:缺陷名称 格式) - std::string strTrim = ClsTrimStr(strLine); - if (!strTrim.empty()) - { - printf("Cls label config skip invalid line: %s\n", strTrim.c_str()); - } - continue; - } - // label索引:缺陷名称(冒号后名称按行序构成缺陷名称列表) - std::string strKey = ClsTrimStr(strLine.substr(0, nPos)); - std::string strValue = ClsTrimStr(strLine.substr(nPos + 1)); - if (!strKey.empty() && !strValue.empty()) - { - config.mapLabelName[atoi(strKey.c_str())] = strValue; - config.vecNames.push_back(strValue); // 行序即缺陷类型索引 - } - } - return config; -} - -// 获取分类映射配置(只读一次) -static const ClsLabelConfig &GetClsLabelConfig() -{ - static const ClsLabelConfig config = ReadClsLabelConfig(CLS_LABEL_CONFIG_FILE); - return config; -} - -// 获取缺陷名称列表:优先取配置文件的名称列表,读不到用程序内默认列表 -static const std::vector &GetAIQXNameList() -{ - static const std::vector vecDefaultNames = BuildDefaultAIQXNames(); - const ClsLabelConfig &config = GetClsLabelConfig(); - if (!config.vecNames.empty()) - { - return config.vecNames; - } - return vecDefaultNames; -} - -// 缺陷名称 -> AI_CLass 枚举(找不到返回 count 表示无效) -static int ClsNameToAItype(const std::string &strName) -{ - const std::vector &vecNames = GetAIQXNameList(); - for (size_t i = 0; i < vecNames.size(); i++) - { - if (strName == vecNames[i]) - { - return (int)i; - } - } - return AI_CLass_QX_NAME_count; -} - -// 解析 label->缺陷名称 映射为 label->AI_CLass枚举(只读一次;无效名称仅提示一次) -static std::map BuildLabelTypeMap() -{ - const ClsLabelConfig &config = GetClsLabelConfig(); - std::map mapLabelType; - for (std::map::const_iterator it = config.mapLabelName.begin(); it != config.mapLabelName.end(); ++it) - { - int nType = ClsNameToAItype(it->second); - if (nType < AI_CLass_QX_NAME_count) - { - mapLabelType[it->first] = nType; - } - else - { - printf("Cls label config unknown name: %s\n", it->second.c_str()); - } - } - return mapLabelType; -} - -// 获取分类 label 对应的 AI 缺陷类型:优先配置文件(只读一次),读不到用默认顺序 -static int GetClsTypeByLabel(int nLabel) -{ - static const std::map mapLabelType = BuildLabelTypeMap(); - std::map::const_iterator it = mapLabelType.find(nLabel); - if (it != mapLabelType.end()) - { - return it->second; - } - - // 映射文件未读到、或该 label 未在文件中配置时,按程序内默认顺序兜底(避免文件缺失/不全导致异常) - int cls_num = AI_CLass_QX_NAME_count; - switch (nLabel) - { - case 0: - cls_num = AI_CLass_QX_NAME_CGMark; - break; - case 1: - cls_num = AI_CLass_QX_NAME_BQBMark; - break; - case 2: - cls_num = AI_CLass_QX_NAME_TBMark; - break; - default: - cls_num = AI_CLass_QX_NAME_CGMark; - break; - } - return cls_num; -} - -// 用于排序轮廓的比较函数 -static bool compareContourAreas(const vector &contour1, const vector &contour2) -{ - double i = contourArea(contour1); - double j = contourArea(contour2); - return (i > j); -} - -// 2x3 仿射矩阵小工具:变换点、矩形外接、组合矩阵(检测图裁剪坐标映射用) -static cv::Point2f MatAffinePoint(const cv::Mat &mat32, const cv::Point2f &pt) -{ - if (mat32.empty() || mat32.rows != 2 || mat32.cols != 3) - { - return pt; - } - const double *m = mat32.ptr(0); - return cv::Point2f((float)(m[0] * pt.x + m[1] * pt.y + m[2]), - (float)(m[3] * pt.x + m[4] * pt.y + m[5])); -} - -// 矩形四角经仿射变换后取外接矩形 -static cv::Rect MatAffineRect(const cv::Mat &mat32, const cv::Rect &roi) -{ - std::vector pts(4); - pts[0] = MatAffinePoint(mat32, cv::Point2f((float)roi.x, (float)roi.y)); - pts[1] = MatAffinePoint(mat32, cv::Point2f((float)(roi.x + roi.width), (float)roi.y)); - pts[2] = MatAffinePoint(mat32, cv::Point2f((float)(roi.x + roi.width), (float)(roi.y + roi.height))); - pts[3] = MatAffinePoint(mat32, cv::Point2f((float)roi.x, (float)(roi.y + roi.height))); - - float fminX = pts[0].x, fmaxX = pts[0].x, fminY = pts[0].y, fmaxY = pts[0].y; - for (int i = 1; i < 4; i++) - { - if (pts[i].x < fminX) - fminX = pts[i].x; - if (pts[i].x > fmaxX) - fmaxX = pts[i].x; - if (pts[i].y < fminY) - fminY = pts[i].y; - if (pts[i].y > fmaxY) - fmaxY = pts[i].y; - } - int x0 = cvFloor(fminX); - int y0 = cvFloor(fminY); - int x1 = cvCeil(fmaxX); - int y1 = cvCeil(fmaxY); - return cv::Rect(x0, y0, x1 - x0, y1 - y0); -} - -// 组合仿射:先应用 mB 再应用 mA,即 mA ∘ mB,结果仍为 2x3 -static cv::Mat MatAffineCompose(const cv::Mat &mA, const cv::Mat &mB) -{ - if (mA.empty() || mA.rows != 2 || mA.cols != 3) - { - return mB.clone(); - } - if (mB.empty() || mB.rows != 2 || mB.cols != 3) - { - return mA.clone(); - } - cv::Mat hA = cv::Mat::eye(3, 3, CV_64F); - cv::Mat hB = cv::Mat::eye(3, 3, CV_64F); - mA.copyTo(hA(cv::Rect(0, 0, 3, 2))); - mB.copyTo(hB(cv::Rect(0, 0, 3, 2))); - cv::Mat hC = hA * hB; - return hC(cv::Rect(0, 0, 3, 2)).clone(); -} - -// 稳健直线拟合:pts 为 (t, v) 采样点,拟合 v = k*t + b(左右边 t=y,v=x;上下边 t=x,v=y) -// 先最小二乘,再按“残差中位数的绝对偏差(MAD)”剔除离群点(把手凸起/边缘缺口/内部亮区等),重新拟合; -// 返回参与拟合的内点数,<4 表示拟合失败(调用方沿用原边缘) -static int FitLineRobustTV(const std::vector &pts, double &fK, double &fB) -{ - auto fnFit = [](const std::vector &p, double &k, double &b) -> bool { - if (p.size() < 2) - { - return false; - } - double fSumT = 0, fSumV = 0; - for (size_t i = 0; i < p.size(); i++) - { - fSumT += p[i].x; - fSumV += p[i].y; - } - const double fMeanT = fSumT / p.size(); - const double fMeanV = fSumV / p.size(); - double fSxx = 0, fSxy = 0; - for (size_t i = 0; i < p.size(); i++) - { - const double fDt = p[i].x - fMeanT; - fSxx += fDt * fDt; - fSxy += fDt * (p[i].y - fMeanV); - } - if (fSxx < 1e-6) - { - return false; - } - k = fSxy / fSxx; - b = fMeanV - k * fMeanT; - return true; - }; - - if (pts.size() < 4) - { - return 0; - } - double fKFit = 0, fBFit = 0; - if (!fnFit(pts, fKFit, fBFit)) - { - return 0; - } - - std::vector vecInlier; - for (int nIter = 0; nIter < 2; nIter++) - { - std::vector vecRes; - vecRes.reserve(pts.size()); - for (size_t i = 0; i < pts.size(); i++) - { - vecRes.push_back(pts[i].y - (fKFit * pts[i].x + fBFit)); - } - std::vector vecAbs = vecRes; - std::nth_element(vecAbs.begin(), vecAbs.begin() + vecAbs.size() / 2, vecAbs.end()); - const double fMed = vecAbs[vecAbs.size() / 2]; - for (size_t i = 0; i < vecAbs.size(); i++) - { - vecAbs[i] = std::fabs(vecAbs[i] - fMed); - } - std::nth_element(vecAbs.begin(), vecAbs.begin() + vecAbs.size() / 2, vecAbs.end()); - const double fMad = vecAbs[vecAbs.size() / 2]; - const double fThr = std::max(2.0, 3.0 * 1.4826 * fMad); - - vecInlier.clear(); - for (size_t i = 0; i < pts.size(); i++) - { - if (std::fabs(pts[i].y - (fKFit * pts[i].x + fBFit)) <= fThr) - { - vecInlier.push_back(pts[i]); - } - } - if (vecInlier.size() < 4 || !fnFit(vecInlier, fKFit, fBFit)) - { - break; - } - } - if (vecInlier.size() < 4) - { - return 0; - } - fK = fKFit; - fB = fBFit; - return (int)vecInlier.size(); -} - -ImgCheckAnalysisy::ImgCheckAnalysisy() - : m_pAI_Edge_Algin(m_pdetlog) -{ - - m_nErrorCode = CHECK_OK; - m_nThreadIdx = -1; - m_bInitSucc = false; - m_bExit = false; - m_nRun_Status = CHECK_THREAD_STATUS_IDLE; - - m_fImgage_Scale_X = 0.03f; - m_fImgage_Scale_Y = 0.03f; - m_pBasicConfig = NULL; - - m_strCurDetChannel = ""; - m_bupdateconfig = false; - m_pbaseCheckFunction = &m_AnalysisyConfig.baseFunction; - - m_strLastDate = ""; - m_strRootPath_TA_cls = "/home/aidlux/BOE/Cls/TA/"; - m_strRootPath_CA_cls = "/home/aidlux/BOE/Cls/CA/"; - creatsavedir(); - m_pImageStorage = ImageStorage::getInstance(); - m_nConfigIdx = -1; - m_ImgBlobHFlagData = NULL; -} - -ImgCheckAnalysisy::~ImgCheckAnalysisy() -{ - ExitSystem(); - if (m_ImgBlobHFlagData) - { - delete[] m_ImgBlobHFlagData; - m_ImgBlobHFlagData = NULL; - } -} -int ImgCheckAnalysisy::UpdateConfig(void *pconfig, int nConfigType) -{ - int re = 0; - switch (nConfigType) - { - case CHECK_CONFIG_Run: - re = LoadRunConfig(pconfig); - if (re == 0) - { - // printf("---> LoadRunConfig Succ\n"); - } - else - { - printf("---> LoadRunConfig Fail\n"); - } - break; - case CHECK_CONFIG_Module: - re = LoadCheckConfig(pconfig); - if (re == 0) - { - // printf("---> LoadAnalysisConfig Succ\n"); - } - else - { - printf("---> LoadAnalysisConfig Fail\n"); - } - break; - default: - break; - } - return re; -} -int ImgCheckAnalysisy::RunStart(void *pconfig1) -{ - - // 1 、更新参数 并判断参数是否合法 - int re = CHECK_OK; - re = SetNewConfig(); - if (CHECK_OK != re) - { - m_nErrorCode = re; - return m_nErrorCode; - } - // printf("---> RunStart Start m_RunConfig.nThreadIdx %d \n", m_RunConfig.nThreadIdx); - - m_nThreadIdx = m_RunConfig.nThreadIdx; - - re = InitRun(m_RunConfig.nCpu_start_Idx); - if (CHECK_OK != re) - { - m_nErrorCode = re; - return m_nErrorCode; - } - runner = std::make_shared(); - runner->Start(); - - m_nErrorCode = CHECK_OK; - // printf("ImgCheckAnalysisy >>>> ImgCheckThread %d Start Succ \n", m_nThreadIdx); - return m_nErrorCode; -} - -int ImgCheckAnalysisy::SetDataRun_SharePtr(std::shared_ptr p) -{ - // 设置正在检测 - - m_pImageAllResult = p; - DetImgInfo_shareP = p->result->in_shareImage; - m_CheckResult_shareP = p->result; - m_pImageAllResult->setStep(ImageAllResult::DetStep_Deting); - m_pdetlog = p->detlog; - m_pDetResult = p->pDetResult; - StartCheck(); - m_nErrorCode = CHECK_OK; - return m_nErrorCode; -} - -int ImgCheckAnalysisy::GetCheckReuslt(std::shared_ptr &pResult) -{ - - m_CheckResult_shareP.reset(); - DetImgInfo_shareP.reset(); - SetIDLE(); - // m_nErrorCode = CHECK_OK; - // printf("4 DetImgInfo_shareP count %ld m_nCheckResultErrorCode %d \n", DetImgInfo_shareP.use_count(), m_nCheckResultErrorCode); - return m_nCheckResultErrorCode; -} - -int ImgCheckAnalysisy::CheckImg(std::shared_ptr p, std::shared_ptr &pResult) -{ - - return 0; -} - -int ImgCheckAnalysisy::ReJsonResul(std::shared_ptr p, std::shared_ptr &pResult) -{ - - return 0; -} - -int ImgCheckAnalysisy::InitRun(int nId) -{ - - int re = CHECK_OK; - if (m_bInitSucc) - { - return CHECK_OK; - } - InitModel(); - AI_Factory = AIFactory::GetInstance(); - - m_nRun_Status = CHECK_THREAD_STATUS_IDLE; - re = StartThread(nId); - if (CHECK_OK != re) - { - return re; - } - m_bInitSucc = true; - return re; -} - -int ImgCheckAnalysisy::GetStatus() -{ - return m_nRun_Status; -} - -std::string ImgCheckAnalysisy::GetVersion() -{ - return std::string("BOE_1.3.0"); -} - -std::string ImgCheckAnalysisy::GetErrorInfo() -{ - std::string str = GetErrorCodeInfo(m_nErrorCode); - printf("%s\n", str.c_str()); - return str; -} - -int ImgCheckAnalysisy::creatsavedir() -{ - std::string curDate = CheckUtil::getCurrentDate(); - if (curDate == m_strLastDate) - { - return 0; - } - m_strLastDate = curDate; - m_strRootPath_TA_cls += m_strLastDate + "/"; - m_strRootPath_CA_cls += m_strLastDate + "/"; - for (int i = 0; i < AI_CLass_QX_NAME_count; i++) - { - CheckUtil::CreateDir(m_strRootPath_TA_cls + std::to_string(i) + "/"); - CheckUtil::CreateDir(m_strRootPath_CA_cls + std::to_string(i) + "/"); - } - return 0; -} - -void ImgCheckAnalysisy::SetTplInfo(const cv::RotatedRect &outerRect) -{ - m_tplOuterRect = outerRect; -} - -int ImgCheckAnalysisy::LoadRunConfig(void *p) -{ - if (p == NULL) - { - m_nErrorCode = CHECK_ERROR_Config_Null; - return m_nErrorCode; - } - RunInfoST *pconfig = (RunInfoST *)p; - m_RunConfig.copy(*pconfig); - - return CHECK_OK; -} - -int ImgCheckAnalysisy::LoadCheckConfig(void *p) -{ - if (p == NULL) - { - m_nErrorCode = CHECK_ERROR_Config_Null; - return m_nErrorCode; - } - m_pConfig = (ConfigBase *)p; - m_nConfigIdx = m_pConfig->GetConfigIdx(); - m_nErrorCode = CHECK_OK; - return m_nErrorCode; -} - -// 开启检测 -int ImgCheckAnalysisy::StartCheck() -{ - m_nRun_Status = CHECK_THREAD_STATUS_READY; - return 0; -} - -int ImgCheckAnalysisy::SetIDLE() -{ - // 更新参数 - SetNewConfig(); - m_nRun_Status = CHECK_THREAD_STATUS_IDLE; - return 0; -} - -int ImgCheckAnalysisy::StartThread(int nId) -{ - // 开启检测线程 - ptr_thread_Run = std::make_shared(std::bind(&ImgCheckAnalysisy::Run, this, nId)); - if (!m_RunConfig.bRetest) - { - ptr_thread_AI = std::make_shared(std::bind(&ImgCheckAnalysisy::ThreadTask, this, nId + 1)); - } - - return 0; -} - -int ImgCheckAnalysisy::StopThread() -{ - m_bExit = true; - if (ptr_thread_Run != nullptr) - { - if (ptr_thread_Run->joinable()) - { - ptr_thread_Run->join(); - } - } - if (ptr_thread_AI != nullptr) - { - if (ptr_thread_AI->joinable()) - { - ptr_thread_AI->join(); - } - } - return 0; -} - -int ImgCheckAnalysisy::ExitSystem() -{ - StopThread(); - return 0; -} - -int ImgCheckAnalysisy::InitModel() -{ - - // 获取当前gpu号确定的 AI处理线程 - - m_OtherDet_Config.nDeviceId = m_RunConfig.nDeviceId; - - m_pAI_Edge_Algin.Init(&m_OtherDet_Config); - m_pAI_Edge_Algin.InitModel_ALL(); - - return 0; -} - -cv::Scalar ImgCheckAnalysisy::calc_blob_info_withstats(cv::Mat &img, const cv::Mat &mask, cv::Rect &stats, cv::Size k_size, int expand, double threshold) -{ - - // 解包 stats(x, y, w, h, area) - int x = stats.x, y = stats.y, w = stats.width, h = stats.height; - - // 将图像转换为灰度图像 - - // 计算感兴趣区域 (ROI) - cv::Rect roi(x - expand, y - expand, w + 2 * expand, h + 2 * expand); - roi &= cv::Rect(0, 0, img.cols, img.rows); // 确保ROI在图像内 - - cv::Mat cimg = img(roi); - - // cv::cvtColor(cimg, cimg, cv::COLOR_BGR2GRAY); - // return cv::Scalar(0, 0, 0); - - cv::Mat cmask = mask(roi); - // getchar(); - // 扩张掩膜 - - cv::Scalar mean_bk = cv::mean(cimg, ~cmask); - double fbk = mean_bk[0]; - - cv::Scalar mean_det = cv::mean(cimg, cmask); - double fdet = mean_det[0]; - - // 计算差异图像 - cv::Mat diff = cv::abs(cimg - fbk); - - // cv::imwrite("cimg.png",cimg); - // cv::imwrite("cmask.png",cmask); - // printf("%f %f - %f \n",fbk,fdet,fbk-fdet); - // getchar(); - - // diff = diff.mul(cmask > 0); - cv::Mat masked_image; - diff.copyTo(masked_image, cmask); - // 计算能量 - double energy = cv::sum(masked_image)[0]; - - // 计算 hj(差异图像大于0的像素均值) - // double hj = std::abs(fbk - fdet); - // double hj = CheckUtil::CalHj(cimg, cmask, mean_bk.val[0]); - double hj = CheckUtil::CalHjWeighted(cimg, cmask, mean_bk.val[0], 2.0f); - - int worb = 0; - if (fdet >= fbk) - { - worb = 1; - } - // cv::imwrite("cimg.png", cimg); - // cv::imwrite("cmask.png", cmask); - // cv::imwrite("diff.png", diff); - // cv::imwrite("masked_image.png", masked_image); - // printf("fbk %f fdet %f energy %f\n", fbk, fdet, energy); - // getchar(); - - return cv::Scalar(worb, energy, hj); -} - -int ImgCheckAnalysisy::Adapt_Config(RotatedRect tplOuterRect, RotatedRect curOuterRect, Mat& det_img){ - vector cur_vertices = CheckUtil::sort_vertices(curOuterRect); - vector tpl_vertices = CheckUtil::sort_vertices(tplOuterRect); - - cv::Point2f src_pts[3] = { - cur_vertices[0], // 对应左上 - cur_vertices[1], // 对应右上 - cur_vertices[2] // 对应左下 - }; - cv::Point2f dst_pts[3] = { - tpl_vertices[0], // 左上 - tpl_vertices[1], // 右上 - tpl_vertices[2] // 左下 - }; - cv::Mat affine_mat = cv::getAffineTransform(src_pts, dst_pts); - - // 仿射变换 - cv::warpAffine(det_img, det_img, affine_mat, det_img.size()); - - // 将m_outer_rroi和m_inner_rroi变换到模板图像坐标系下 - auto transformRotatedRect = [&affine_mat](cv::RotatedRect &rrect) - { - const double *M = affine_mat.ptr(0); - - cv::Point2f pts[4]; - rrect.points(pts); - - std::vector transformed_pts(4); - for (int i = 0; i < 4; i++) - { - float x = static_cast(M[0] * pts[i].x + M[1] * pts[i].y + M[2]); - float y = static_cast(M[3] * pts[i].x + M[4] * pts[i].y + M[5]); - transformed_pts[i] = cv::Point2f(x, y); - } - rrect = cv::minAreaRect(transformed_pts); - }; - - transformRotatedRect(m_outer_rroi); - transformRotatedRect(m_inner_rroi); - - return 0; -} - -// 暗场复用明场定位:把字符串里的 "dark"(大小写不敏感)替换为 "bright";未命中返回空串 -static std::string ReplaceDarkWithBright(const std::string &strIn) -{ - const std::string strKey = ALIGN_REUSE_FOLLOWER_KEY; // "dark" - const std::string strSrc = ALIGN_REUSE_SOURCE_KEY; // "bright" - if (strIn.size() < strKey.size()) - { - return ""; - } - for (size_t i = 0; i + strKey.size() <= strIn.size(); i++) - { - size_t k = 0; - for (; k < strKey.size(); k++) - { - if (::tolower((unsigned char)strIn[i + k]) != (unsigned char)strKey[k]) - { - break; - } - } - if (k == strKey.size()) - { - std::string strOut = strIn; - strOut.replace(i, strKey.size(), strSrc); - return strOut; - } - } - return ""; -} - -// ===== 暗场复用明场定位结果(ALIGN_REUSE_* 配置) ===== -// 取当前图的产品ID/相机名:优先用 ImageAllResult 关联的基础结果,兕底用送图信息 -void ImgCheckAnalysisy::GetProductCameraStr(std::string &strProduct, std::string &strCamera) -{ - strProduct = ""; - strCamera = ""; - if (m_pImageAllResult) - { - if (m_pImageAllResult->productBaseResult) - { - strProduct = m_pImageAllResult->productBaseResult->strproductName; - } - if (m_pImageAllResult->cameraBaseResult) - { - strCamera = m_pImageAllResult->cameraBaseResult->strCameraName; - } - } - if (m_CheckResult_shareP && m_CheckResult_shareP->in_shareImage) - { - if (strProduct.empty()) - { - strProduct = m_CheckResult_shareP->in_shareImage->strImgProductID; - } - if (strCamera.empty()) - { - strCamera = m_CheckResult_shareP->in_shareImage->strCameraName; - } - } -} - -// 发布本图定位结果(成功)或失败标记(供同产品其它相机复用) -void ImgCheckAnalysisy::PublishAlignShare(bool bSucc) -{ - std::string strProduct; - std::string strCamera; - GetProductCameraStr(strProduct, strCamera); - if (strProduct.empty() || strCamera.empty()) - { - return; - } - if (bSucc) - { - if (!m_pEdge_Align_Result) - { - return; - } - cv::Size sz(0, 0); - if (m_CheckResult_shareP && m_CheckResult_shareP->in_shareImage) - { - sz = m_CheckResult_shareP->in_shareImage->img.size(); - } - AlignShare::GetInstance().PublishSuccess(strProduct, strCamera, m_strCurDetChannel, m_pEdge_Align_Result, sz); - } - else - { - AlignShare::GetInstance().PublishFailed(strProduct, strCamera, m_strCurDetChannel); - } -} - -// 暗场相机不做定位模型:等待并复用明场相机的定位结果 -// 返回 true 时:m_pEdge_Align_Result / m_outer_rroi / m_inner_rroi 已用明场结果填充(标签检测独立执行) -// 返回 false:非复用相机,或复用失败(超时/源失败/无源图/尺寸不一致),调用方走自有定位 -bool ImgCheckAnalysisy::TryReuseAlignFromSource(std::string &strSrcCamera, std::string &strSrcChannel, int &nWaitMs) -{ - nWaitMs = -1; -#if !ALIGN_REUSE_ENABLE - return false; -#else - std::string strProduct; - std::string strCamera; - GetProductCameraStr(strProduct, strCamera); - - // 是否复用由相机名决定(明/暗场是相机属性):相机名必须能按 dark->bright 替换 - std::string strSrcCam = ReplaceDarkWithBright(strCamera); - if (strSrcCam.empty()) - { - return false; - } - // 通道名可能不带 dark/bright(同一面的明/暗场相机共用一个通道名),此时沿用本相机通道名; - // 若带 dark 则同样替换,保证与源相机发布结果时的 key 一致 - std::string strSrcChn = ReplaceDarkWithBright(m_strCurDetChannel); - if (strSrcChn.empty()) - { - strSrcChn = m_strCurDetChannel; - } - strSrcCamera = strSrcCam; - strSrcChannel = strSrcChn; - - if (!m_pImageAllResult || !m_pImageAllResult->productBaseResult || !m_pImageAllResult->productBaseResult->alignPushState) - { - m_pdetlog->AddCheckstr(PrintLevel_0, "Align reuse", "reuse from %s|%s failed(no push state), do align by self", strSrcCam.c_str(), strSrcChn.c_str()); - return false; - } - - m_pdetlog->AddCheckstr(PrintLevel_0, "Align reuse", "wait %s|%s (timeout %d ms)", strSrcCam.c_str(), strSrcChn.c_str(), ALIGN_REUSE_WAIT_TIMEOUT_MS); - - AlignShare &alignShare = AlignShare::GetInstance(); - std::shared_ptr pSrc; - cv::Size srcSize; - const long t1 = CheckUtil::getcurTime(); - bool bGot = false; - bool bSucc = false; - std::string strFailReason = "timeout"; - while (true) - { - if (alignShare.TryGet(strProduct, strSrcCam, strSrcChn, bSucc, pSrc, srcSize)) - { - bGot = true; - if (!bSucc) - { - strFailReason = "source align failed"; - } - break; - } - // 源图不会再来了(送图已结束,且源相机没有图):立即回退,不干等超时 - if (!m_pImageAllResult->productBaseResult->alignPushState->MayStillCome(strSrcCam)) - { - strFailReason = "no source image"; - break; - } - if (CheckUtil::getcurTime() - t1 > ALIGN_REUSE_WAIT_TIMEOUT_MS) - { - break; - } - std::this_thread::sleep_for(std::chrono::milliseconds(ALIGN_REUSE_POLL_MS)); - } - nWaitMs = (int)(CheckUtil::getcurTime() - t1); - - if (!bGot || !bSucc) - { - m_pdetlog->AddCheckstr(PrintLevel_0, "Align reuse", "reuse from %s|%s failed(wait %d ms, %s), do align by self", - strSrcCam.c_str(), strSrcChn.c_str(), nWaitMs, strFailReason.c_str()); - return false; - } - if (!pSrc || pSrc->bigroi.empty()) - { - m_pdetlog->AddCheckstr(PrintLevel_0, "Align reuse", "reuse from %s|%s failed(wait %d ms, src roi empty), do align by self", - strSrcCam.c_str(), strSrcChn.c_str(), nWaitMs); - return false; - } - // 图像尺寸必须与源图一致,否则源坐标系不可用 - if (srcSize.width > 0 && srcSize.height > 0 && m_CheckResult_shareP && m_CheckResult_shareP->in_shareImage) - { - const cv::Mat &img = m_CheckResult_shareP->in_shareImage->img; - if (img.cols != srcSize.width || img.rows != srcSize.height) - { - m_pdetlog->AddCheckstr(PrintLevel_0, "Align reuse", "reuse from %s|%s failed, size %dx%d != cur %dx%d, do align by self", - strSrcCam.c_str(), strSrcChn.c_str(), srcSize.width, srcSize.height, img.cols, img.rows); - return false; - } - } - - // 复用明场定位结果:ROI/掩码/映射都用明场的(标签检测由 CheckTagByBigMask 独立执行,不用明场的标签) - m_pEdge_Align_Result = pSrc; - m_outer_rroi = pSrc->bigroi[0]; - if (!pSrc->smallroi.empty()) - { - m_inner_rroi = pSrc->smallroi[0]; - } - return true; -#endif -} - -int ImgCheckAnalysisy::CheckRun() -{ - - // printf(">>>%s ================start \n", m_pImageAllResult->strBaseInfo.c_str()); - - long t1, t2, t3, t4, t5, t6, t7; - SetNewConfig(); - t1 = CheckUtil::getcurTime(); - - CheckImgInit(); - if (DetImgInfo_shareP->bsaveProcessImg) - { - m_pdetlog->bPrintStr = true; - } - // m_pdetlog->bPrintStr = true; - m_fImgage_Scale_X = m_pBasicConfig->fImage_Scale_x; - m_fImgage_Scale_Y = m_pBasicConfig->fImage_Scale_y; - - m_pdetlog->AddCheckstr(PrintLevel_0, "1、basic Info", "---------------------------1、basic Info---------------------------------"); - m_pdetlog->AddCheckstr(PrintLevel_0, "Version", "%s", GetVersion().c_str()); - m_pdetlog->AddCheckstr(PrintLevel_0, "Updateconfig", "%d", m_bupdateconfig); - m_pdetlog->AddCheckstr(PrintLevel_0, "Start", "%s", m_pImageAllResult->strBaseInfo.c_str()); - m_pdetlog->AddCheckstr(PrintLevel_0, "ImgageScale", "Scale_X = %f Scale_Y = %f", m_fImgage_Scale_X, m_fImgage_Scale_Y); - m_bupdateconfig = false; - // 返回结果状态初始化 - m_CheckResult_shareP->checkStatus = 1; - m_CheckResult_shareP->nresult = -1; - - m_CheckResult_shareP->basicResult.img_id = m_CheckResult_shareP->in_shareImage->img_id; - m_CheckResult_shareP->basicResult.imgtype = m_CheckResult_shareP->in_shareImage->imgtype; - m_CheckResult_shareP->basicResult.imgstr = m_CheckResult_shareP->in_shareImage->imgstr; - m_CheckResult_shareP->basicResult.strChannel = m_CheckResult_shareP->in_shareImage->strChannel; - m_strCurDetChannel = m_CheckResult_shareP->basicResult.strChannel; - - /*参数检查*/ - int rec = ConfigCheck(DetImgInfo_shareP->img); - if (rec != CHECK_OK) - { - m_pdetlog->AddCheckstr(PrintLevel_0, "Error", "ConfigCheck is error type = %d", rec); - PublishAlignShare(false); // 本图不会产出定位结果,通知可能的等待方(暗场复用) - m_nErrorCode = rec; - m_nCheckResultErrorCode = m_nErrorCode; - return m_nErrorCode; - } - long time_edge_s = CheckUtil::getcurTime(); - - m_pFuntion = GetChannelFuntion(m_strCurDetChannel); - if (m_pFuntion != NULL) - { - m_pdetlog->AddCheckstr(PrintLevel_0, "Detect function", "%s", - m_pFuntion->GetInfo("").c_str()); - } - else - { - // 通道层允许为空(仅打印,不再按错误处理) - m_pdetlog->AddCheckstr(PrintLevel_0, "Detect function", "no channel config, use base function only"); - } - - /*AI 边缘定位(内外边缘)*/ - // 暗场相机不做定位模型,等待复用明场相机的定位结果(见 ALIGN_REUSE_* 配置); - // 非复用相机、或复用失败(源超时/源失败/无源图/尺寸不一致)时,仍走原有定位流程 - int reedge = 0; - int nAlignReuseWaitMs = -1; - std::string strAlignSrcCamera; - std::string strAlignSrcChannel; - long time_edge_e; - if (TryReuseAlignFromSource(strAlignSrcCamera, strAlignSrcChannel, nAlignReuseWaitMs)) - { - // 复用成功:m_pEdge_Align_Result / m_outer_rroi / m_inner_rroi 已用明场结果填充(标签检测独立执行,不复用明场标签) - PublishAlignShare(true); - time_edge_e = CheckUtil::getcurTime(); - m_pdetlog->AddCheckstr(PrintLevel_0, "2、pre detect", - "-------------------AI_Edge reuse from %s|%s--wait %d ms----succ---time %ld----\n", - strAlignSrcCamera.c_str(), strAlignSrcChannel.c_str(), nAlignReuseWaitMs, time_edge_e - time_edge_s); - // 定位模型不跑,但把手检测仍要执行:暗场图像 + 明场 bigmask - { - long handle_s = CheckUtil::getcurTime(); - CheckHandleByBigMask(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); - } - // 标签检测(独立步骤):暗场图像 + 明场 bigmask(不再沿用明场检出的标签);bDetTag 关闭时跳过 - { - if (m_pbaseCheckFunction->supportDet.bDetTag) - { - long tag_s = CheckUtil::getcurTime(); - int reTag = CheckTagByBigMask(m_CheckResult_shareP->in_shareImage->img, m_pEdge_Align_Result->bigmask); - long tag_e = CheckUtil::getcurTime(); - m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Tag", "tag detect use time %ld", tag_e - tag_s); - if (reTag != 0) - { - m_pdetlog->AddCheckstr(PrintLevel_0, "Error", "CheckTagByBigMask is error type = %d", reTag); - m_nErrorCode = reTag; - m_nCheckResultErrorCode = m_nErrorCode; - return m_nErrorCode; - } - } - else - { - m_tag_roiList.clear(); - m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Tag", "bDetTag close, skip tag detect"); - } - } - } - else - { - reedge = AI_Edge(m_CheckResult_shareP->in_shareImage->img, m_outer_rroi, m_inner_rroi, m_tag_roiList); - if (reedge != 0) - { - m_pdetlog->AddCheckstr(PrintLevel_0, "Error", "AI_Edge is error type = %d", reedge); - PublishAlignShare(false); // 定位失败:通知可能的等待方(暗场复用),让其回退自行定位 - m_nErrorCode = reedge; - m_nCheckResultErrorCode = m_nErrorCode; - return m_nErrorCode; - } - PublishAlignShare(true); - time_edge_e = CheckUtil::getcurTime(); - m_pdetlog->AddCheckstr(PrintLevel_0, "2、pre detect", "-------------------------AI_Edge------------succ---time %ld----\n", time_edge_e - time_edge_s); - } - - /*生成检测图:用 m_outer_rroi(旋转矩形)的外接区域直接从原图裁剪(不再旋转摆正,省掉旋转重采样耗时); - 原图坐标 -> 检测图坐标 的映射为纯平移,存入 m_matSrc2Det / m_matDet2Src,后续坐标换算仍走这对矩阵*/ - cv::Mat image = m_CheckResult_shareP->in_shareImage->img; - cv::Rect imgRect(0, 0, image.cols, image.rows); - - // 1、检测图裁剪区域 = 旋转矩形外接区域(与原图求交,防越界) - cv::Rect padRoi = m_outer_rroi.boundingRect() & imgRect; - if (padRoi.width <= 0 || padRoi.height <= 0) - { - m_pdetlog->AddCheckstr(PrintLevel_1, "Error", "crop roi error [%d %d %d %d]", padRoi.x, padRoi.y, padRoi.width, padRoi.height); - m_nErrorCode = CHECK_ERROR_CheckImg_Empty; - m_nCheckResultErrorCode = m_nErrorCode; - return m_nErrorCode; - } - int detW = padRoi.width; - int detH = padRoi.height; - - // 2、原图 -> 检测图 为纯平移(无旋转):检测图坐标 = 原图坐标 - 裁剪原点 - m_matSrc2Det = (cv::Mat_(2, 3) << 1, 0, -(double)padRoi.x, 0, 1, -(double)padRoi.y); - m_matDet2Src = (cv::Mat_(2, 3) << 1, 0, (double)padRoi.x, 0, 1, (double)padRoi.y); - - // 3、检测图:单通道灰度(彩色原图按 BGR 顺序转灰度,与 imread(path,0) 的亮度一致),直接裁剪; - // 彩色原图时再输出一张同样裁剪的彩色检测图,供裁剪图(_RE_CutImg)保存时保留原图通道 - // (检测算法仍只用灰度 detImg) - m_deskewPadRoi = padRoi; - cv::Mat colorPatch = image(padRoi); - if (colorPatch.channels() == 1) - { - colorPatch.copyTo(m_pImageAllResult->detImg); - m_pImageAllResult->detColorImg.release(); - } - else if (colorPatch.channels() == 3) - { - cv::cvtColor(colorPatch, m_pImageAllResult->detImg, cv::COLOR_BGR2GRAY); - colorPatch.copyTo(m_pImageAllResult->detColorImg); - } - else - { - cv::cvtColor(colorPatch, m_pImageAllResult->detImg, cv::COLOR_BGRA2GRAY); - colorPatch.copyTo(m_pImageAllResult->detColorImg); - } - - // 4、检测图坐标系下 m_outer_roi 即整张检测图;原图坐标系下 CutRoi 仍取旋转矩形外接区域(兼容旧语义/JSON) - m_outer_roi = cv::Rect(0, 0, detW, detH); - m_Crop_Roi_paramImg = padRoi; - m_pImageAllResult->pDetResult->CutRoi = m_Crop_Roi_paramImg; - m_pImageAllResult->pDetResult->Param_CropRoi = m_Crop_Roi_paramImg; - // 检测图坐标 -> 原图坐标 的映射,供结果解析时取回缺陷小图/换算坐标 - m_pImageAllResult->pDetResult->detToSrcMat = m_matDet2Src.clone(); - - // 5、参数(模板)坐标 -> 检测图坐标 的映射: - // 有对齐时先把参数点映射到原图再平移到检测图;无对齐时参数按原图坐标系处理, - // 直接按裁剪原点平移(与旧的裁剪平移完全一致) - { - if (m_pEdge_Align_Result && m_pEdge_Align_Result->buseOfft) - { - if (!m_pEdge_Align_Result->H.empty()) - { - m_matParam2Det = MatAffineCompose(m_matSrc2Det, m_pEdge_Align_Result->H); - } - else - { - cv::Mat matOfft = (cv::Mat_(2, 3) << 1, 0, (double)m_pEdge_Align_Result->offt_x, 0, 1, (double)m_pEdge_Align_Result->offt_y); - m_matParam2Det = MatAffineCompose(m_matSrc2Det, matOfft); - } - } - else - { - m_matParam2Det = m_matSrc2Det.clone(); - } - } - long time_calan_e = CheckUtil::getcurTime(); - m_pdetlog->AddCheckstr(PrintLevel_0, "2.1、pre detect", "-------------------------crop--------------succ--- det [%d %d] src bbox [%d %d %d %d] time %ld----\n", - detW, detH, m_Crop_Roi_paramImg.x, m_Crop_Roi_paramImg.y, m_Crop_Roi_paramImg.width, m_Crop_Roi_paramImg.height, - time_calan_e - time_edge_s); - /*多线程开启 AI 推理检测*/ - long time_AI_s = CheckUtil::getcurTime(); - m_AItask = std::make_shared(); - m_AItask->taskname = Task_AI; - m_task.sendTask(m_AItask); - - ImgPreDet(); - /*更新检测区域*/ - Update_DetRoiList(); - - m_pdetlog->AddCheckstr(PrintLevel_0, "3、pre detect", "-------------------------pre Det--------------- \n"); - - /* 检测支架缺失 + 支架偏移(bDetSupport 开关关闭时不做支架判定) - 不使用参数绘制区域,结合两次定位的 mask 计算: - 1、smallmask(=Align_Inner 模型输出)最大连通域 = 支架区域 rs(按定位裁剪区 m_outer_rroi 外接矩形映射回原图坐标) - 2、支架缺失:支架面积(轮廓面积 × 缩放 × 成像精度 X/Y,单位 mm²) < support_min_area → 支架缺失 NG(ZJ_QueShi), - 缺失时不再做偏移判定(几何不可靠,避免重复/误报) - 3、产品边缘:优先复用把手提取对 bigmask 做的"四边抗凸起拟合"主体四边形 - (CheckHandleByBigMask -> GetMaskProtrusions;边缘上的凸起(把手等)不会把边缘带偏), - 拟合不可用/无效时回退 bigmask 最大连通域外接矩形 rp(近似矩形,四周可能有凸起) - 3.1、对 edge_select 选中的边缘再结合原图(detImg 灰度)精细化:沿边缘按步长采样,从边缘向产品内侧 - 找第一个 255 像素作为新边缘点(最多向内 edge_refine_search 像素,找不到沿用原边缘点), - 有效点足够时稳健拟合新的边缘线;采样步长 edge_refine_step(像素) - 4、按 edge_select 指定方向(left/right/up/down)计算 rs 中心到产品对应边缘的距离 - 5、计算值与标准值(x_standard/y_standard)做差取平均,平均差超过 offset(x_offset) → 偏移 - 6、旋转偏移:支架最小外接旋转矩形的长轴(对应左/右边缘)与产品左/右边缘夹角超过 r_offset(度) → 偏移 */ - { - Function_Support_Det &supportDet = m_pbaseCheckFunction->supportDet; - m_SupportDrawInfo.Reset(); // 清空本帧绘制信息(未开启/计算无效时不绘制) - if (supportDet.bDetSupport) - { - const cv::Mat &bigmask = m_pEdge_Align_Result->bigmask; - const cv::Mat &smallmask = m_pEdge_Align_Result->smallmask; - if (bigmask.empty() || smallmask.empty()) - { - m_pdetlog->AddCheckstr(PrintLevel_0, "支架检测", "mask empty (big %d small %d), skip support offset", bigmask.empty(), smallmask.empty()); - } - else - { - // 1、mask 二值化(兼容 0/1 与 0/255 掩膜) - cv::Mat bigBin, smallBin; - { - cv::Mat grayMask; - if (bigmask.channels() != 1) - { - cv::cvtColor(bigmask, grayMask, cv::COLOR_BGR2GRAY); - } - else - { - grayMask = bigmask; - } - double dMin = 0; - double dMax = 0; - cv::minMaxLoc(grayMask, &dMin, &dMax); - cv::threshold(grayMask, bigBin, dMax > 1 ? dMax * 0.5 : 0, 255, cv::THRESH_BINARY); - } - { - cv::Mat grayMask; - if (smallmask.channels() != 1) - { - cv::cvtColor(smallmask, grayMask, cv::COLOR_BGR2GRAY); - } - else - { - grayMask = smallmask; - } - double dMin = 0; - double dMax = 0; - cv::minMaxLoc(grayMask, &dMin, &dMax); - cv::threshold(grayMask, smallBin, dMax > 1 ? dMax * 0.5 : 0, 255, cv::THRESH_BINARY); - } - - // 2、最大连通域轮廓:bigmask -> 产品区域 rp(原图坐标);smallmask -> 支架区域 rs(smallmask 局部坐标) - bool bFoundRp = false; - std::vector rpContour = CheckUtil::getLargestContourROI(bigBin, bFoundRp); - bool bFoundRs = false; - std::vector rsContour = CheckUtil::getLargestContourROI(smallBin, bFoundRs); - - // smallmask -> 原图坐标的比例:smallmask 对应定位时裁剪的大轮廓区域(m_outer_rroi 外接矩形),按比例映射 - cv::Rect bigRect = m_outer_rroi.boundingRect(); - float fx = smallmask.cols > 0 ? bigRect.width * 1.0f / smallmask.cols : 1.0f; - float fy = smallmask.rows > 0 ? bigRect.height * 1.0f / smallmask.rows : 1.0f; - - // rs 映射回原图坐标(支架轮廓缺失时保持空矩形,面积按 0 处理) - cv::Rect rsRect; - if (bFoundRs && rsContour.size() >= 3) - { - cv::Rect rsSmallRect = cv::boundingRect(rsContour); - rsRect = cv::Rect( - cvRound(bigRect.x + rsSmallRect.x * fx), - cvRound(bigRect.y + rsSmallRect.y * fy), - cvRound(rsSmallRect.width * fx), - cvRound(rsSmallRect.height * fy)); - } - - // 3、支架缺失判定:支架由 Align_Inner 模型(smallmask)检出,支架面积 < support_min_area(单位 mm²) → 缺失 - // 面积换算:smallmask 轮廓面积 × fx × fy = 原图像素面积,再乘成像精度 X/Y(mm/px) - float fScaleX = (m_fImgage_Scale_X > 0) ? m_fImgage_Scale_X : 1.0f; - float fScaleY = (m_fImgage_Scale_Y > 0) ? m_fImgage_Scale_Y : 1.0f; - double dSupportAreaSmall = (bFoundRs && rsContour.size() >= 3) ? cv::contourArea(rsContour) : 0.0; - double dSupportAreaMm2 = dSupportAreaSmall * fx * fy * fScaleX * fScaleY; - bool bSupportMiss = (supportDet.support_min_area > 0) && (dSupportAreaMm2 < supportDet.support_min_area); - m_pdetlog->AddCheckstr(PrintLevel_0, "支架检测", - "support area %.2f mm2 (small px %.1f, scale %.4f %.4f) min_area %.2f miss %d", - dSupportAreaMm2, dSupportAreaSmall, fx * fScaleX, fy * fScaleY, - supportDet.support_min_area, bSupportMiss); - - if (bSupportMiss) - { - // 支架缺失 NG:ROI 取支架区域(有轮廓时)或整个产品区域(无轮廓时),映射到检测图(detImg)坐标 - cv::Rect missRect = (rsRect.width > 0 && rsRect.height > 0) ? rsRect : bigRect; - // cv::Rect roi = MatAffineRect(m_matSrc2Det, missRect); - cv::Rect roi = missRect; - roi &= cv::Rect(0, 0, m_pImageAllResult->detImg.cols, m_pImageAllResult->detImg.rows); - - QX_ERROR_INFO_ temerror; - temerror.Idx = m_pDetResult->pQx_ErrorList->size(); - temerror.roi = roi; - temerror.area = roi.width * roi.height; - temerror.JudgArea = roi.width * fScaleX * roi.height * fScaleY; - temerror.JudgArea_second = temerror.JudgArea; - float w = missRect.width; - float h = missRect.height; - temerror.flen = (w > h ? w : h) * fScaleX; - temerror.fbreadth = (w > h ? h : w) * fScaleY; - temerror.nconfig_qx_type = CONFIG_QX_NAME_cell_ZJ_QueShi; - 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]; - - // 计算支架(缺失区域)中心所在的检测区域 - 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 &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 support missing: area %.2f mm2 < min_area %.2f roi [%d %d %d %d]", - temerror.qx_name.c_str(), temerror.Idx, dSupportAreaMm2, supportDet.support_min_area, - roi.x, roi.y, roi.width, roi.height); - } - else if (!bFoundRp || rpContour.size() < 3 || !bFoundRs || rsContour.size() < 3) - { - m_pdetlog->AddCheckstr(PrintLevel_0, "支架检测", "contour invalid (rp %d %ld, rs %d %ld), skip support offset", - bFoundRp, rpContour.size(), bFoundRs, rsContour.size()); - } - else - { - // rp 边缘(拟合不可用时的回退):产品最大连通域的外接矩形(含凸起) - cv::Rect rpRect = cv::boundingRect(rpContour); - - // 3、产品边缘:优先复用把手提取对 bigmask 做的"四边抗凸起拟合"主体四边形 - // (CheckHandleByBigMask -> GetMaskProtrusions 输出):边缘上的凸起(把手等)不会把边缘带偏; - // 拟合不可用/无效时回退到产品最大连通域外接矩形 rpRect(含凸起) - bool bFitQuad = false; // 是否用上了拟合主体四边形 - float fDL = 0, fDR = 0, fDT = 0, fDB = 0; // 支架中心到产品左/右/上/下边缘的距离(原图像素,后乘缩放系数) - cv::Point2f ptEdgeArr[4]; // 连线端点(边缘线上):0 left 1 right 2 up 3 down - cv::Point2f ptSegAArr[4]; // 边缘线段端点A - cv::Point2f ptSegBArr[4]; // 边缘线段端点B - - float fCx = rsRect.x + rsRect.width * 0.5f; - float fCy = rsRect.y + rsRect.height * 0.5f; - if (m_HandleBodyQuad.size() == 4) - { - // 四边形角点顺序:左上/右上/右下/左下;左/右边 x=k*y+b、上/下边 y=k*x+b(与 GetMaskProtrusions 约定一致); - // 边长过小说明拟合退化,不用 - const cv::Point2f &ptTL = m_HandleBodyQuad[0]; - const cv::Point2f &ptTR = m_HandleBodyQuad[1]; - const cv::Point2f &ptBR = m_HandleBodyQuad[2]; - const cv::Point2f &ptBL = m_HandleBodyQuad[3]; - if (fabs(ptBL.y - ptTL.y) > 2.0f && fabs(ptBR.y - ptTR.y) > 2.0f && - fabs(ptTR.x - ptTL.x) > 2.0f && fabs(ptBR.x - ptBL.x) > 2.0f) - { - float fKL = (ptBL.x - ptTL.x) / (ptBL.y - ptTL.y); // 左边 x = k*y + b - float fBL = ptTL.x - fKL * ptTL.y; - float fKR = (ptBR.x - ptTR.x) / (ptBR.y - ptTR.y); // 右边 x = k*y + b - float fBR = ptTR.x - fKR * ptTR.y; - float fKT = (ptTR.y - ptTL.y) / (ptTR.x - ptTL.x); // 上边 y = k*x + b - float fBT = ptTL.y - fKT * ptTL.x; - float fKB = (ptBR.y - ptBL.y) / (ptBR.x - ptBL.x); // 下边 y = k*x + b - float fBB = ptBL.y - fKB * ptBL.x; - // 连线端点:取边缘线在支架中心行/列上的点;长度即中心到边缘的水平/垂直距离 - float fXEdgeL = fKL * fCy + fBL; - float fXEdgeR = fKR * fCy + fBR; - float fYEdgeT = fKT * fCx + fBT; - float fYEdgeB = fKB * fCx + fBB; - fDL = fCx - fXEdgeL; - fDR = fXEdgeR - fCx; - fDT = fCy - fYEdgeT; - fDB = fYEdgeB - fCy; - // 拟合有效性:支架中心应位于四条拟合边内侧(四个方向距离均为正),否则放弃拟合 - if (fDL > 0 && fDR > 0 && fDT > 0 && fDB > 0) - { - ptEdgeArr[0] = cv::Point2f(fXEdgeL, fCy); - ptEdgeArr[1] = cv::Point2f(fXEdgeR, fCy); - ptEdgeArr[2] = cv::Point2f(fCx, fYEdgeT); - ptEdgeArr[3] = cv::Point2f(fCx, fYEdgeB); - ptSegAArr[0] = ptTL; ptSegBArr[0] = ptBL; // left:整条拟合左边 - ptSegAArr[1] = ptTR; ptSegBArr[1] = ptBR; // right:整条拟合右边 - ptSegAArr[2] = ptTL; ptSegBArr[2] = ptTR; // up:整条拟合上边 - ptSegAArr[3] = ptBL; ptSegBArr[3] = ptBR; // down:整条拟合下边 - bFitQuad = true; - } - } - } - if (!bFitQuad) - { - // 回退:产品区域外接矩形(含凸起)的四条边 - fDL = fCx - rpRect.x; - fDR = (rpRect.x + rpRect.width) - fCx; - fDT = fCy - rpRect.y; - fDB = (rpRect.y + rpRect.height) - fCy; - ptEdgeArr[0] = cv::Point2f((float)rpRect.x, fCy); - ptEdgeArr[1] = cv::Point2f((float)(rpRect.x + rpRect.width), fCy); - ptEdgeArr[2] = cv::Point2f(fCx, (float)rpRect.y); - ptEdgeArr[3] = cv::Point2f(fCx, (float)(rpRect.y + rpRect.height)); - ptSegAArr[0] = cv::Point2f((float)rpRect.x, (float)rpRect.y); - ptSegBArr[0] = cv::Point2f((float)rpRect.x, (float)(rpRect.y + rpRect.height)); - ptSegAArr[1] = cv::Point2f((float)(rpRect.x + rpRect.width), (float)rpRect.y); - ptSegBArr[1] = cv::Point2f((float)(rpRect.x + rpRect.width), (float)(rpRect.y + rpRect.height)); - ptSegAArr[2] = cv::Point2f((float)rpRect.x, (float)rpRect.y); - ptSegBArr[2] = cv::Point2f((float)(rpRect.x + rpRect.width), (float)rpRect.y); - ptSegAArr[3] = cv::Point2f((float)rpRect.x, (float)(rpRect.y + rpRect.height)); - ptSegBArr[3] = cv::Point2f((float)(rpRect.x + rpRect.width), (float)(rpRect.y + rpRect.height)); - } - - // 3.2、结合原图精细化检测边缘(只处理 edge_select 选中的方向): - // 在"所计算边缘"(拟合四边形边/回退外接矩形边)上按步长采样,从边缘向产品内侧逐像素扫描原图 - // (detImg 灰度,未做其它处理;产品区域为饱和白),找到的第一个 255 像素作为该采样点的新边缘点; - // 最多向内搜索 edge_refine_search 像素(JSON,默认见 EDGE_REFINE_SEARCH_DEFAULT), - // 找不到的采样点没有新边缘信息,不参与拟合(相当于沿用原边缘点); - // edge_refine_step(JSON,默认见 EDGE_REFINE_STEP_DEFAULT)为沿边采样步长,不需要每个点都找; - // 有效点足够时稳健拟合新的边缘直线,更新测距点(中心行/列)与绘制线段。 - { - const cv::Mat &grayEdgeImg = m_pImageAllResult->detImg; - int nRefineSearch = EDGE_REFINE_SEARCH_DEFAULT; - int nRefineStep = EDGE_REFINE_STEP_DEFAULT; - if (supportDet.edge_refine_search > 0) - { - nRefineSearch = (int)(supportDet.edge_refine_search + 0.5f); - } - if (supportDet.edge_refine_step > 0) - { - nRefineStep = (int)(supportDet.edge_refine_step + 0.5f); - } - nRefineSearch = std::max(1, nRefineSearch); - nRefineStep = std::max(4, nRefineStep); - - bool bEdgeRefined[4] = {false, false, false, false}; // edge_select 配重复时只处理一次 - if (grayEdgeImg.type() == CV_8UC1 && grayEdgeImg.cols > 0 && grayEdgeImg.rows > 0) - { - for (size_t iselect = 0; iselect < supportDet.edge_select.size(); iselect++) - { - const std::string &strEdge = supportDet.edge_select[iselect]; - int gi = -1; // 方向索引:0 left 1 right 2 up 3 down - if (strEdge == "left") - { - gi = 0; - } - else if (strEdge == "right") - { - gi = 1; - } - else if (strEdge == "up") - { - gi = 2; - } - else if (strEdge == "down") - { - gi = 3; - } - if (gi < 0 || bEdgeRefined[gi]) - { - continue; - } - bEdgeRefined[gi] = true; - - // 当前边缘直线:左右边 x=k*y+b,上下边 y=k*x+b(与拟合四边形约定一致) - const bool bVertEdge = (gi == 0 || gi == 1); - const cv::Point2f ptSegA = ptSegAArr[gi]; - const cv::Point2f ptSegB = ptSegBArr[gi]; - const double fDelta = bVertEdge ? ((double)ptSegB.y - ptSegA.y) : ((double)ptSegB.x - ptSegA.x); - if (std::fabs(fDelta) < 4.0) - { - continue; // 边太短/退化,保持原边缘 - } - const double fKOld = bVertEdge ? ((double)ptSegB.x - ptSegA.x) / fDelta - : ((double)ptSegB.y - ptSegA.y) / fDelta; - const double fBOld = bVertEdge ? (ptSegA.x - fKOld * ptSegA.y) - : (ptSegA.y - fKOld * ptSegA.x); - - // 沿边采样区间:两端各去掉 5%(至少 4px),避开圆角过渡段 - const double fT0 = bVertEdge ? std::min(ptSegA.y, ptSegB.y) : std::min(ptSegA.x, ptSegB.x); - const double fT1 = bVertEdge ? std::max(ptSegA.y, ptSegB.y) : std::max(ptSegA.x, ptSegB.x); - const double fTrim = std::max(4.0, (fT1 - fT0) * 0.05); - const double fTStart = fT0 + fTrim; - const double fTEnd = fT1 - fTrim; - if (fTEnd - fTStart < 8.0) - { - continue; - } - - std::vector vecRefinePts; // 有效的新边缘点(t, v):左右 t=y/v=x,上下 t=x/v=y - int nSampleNum = 0; - for (double fT = fTStart; fT <= fTEnd; fT += nRefineStep) - { - nSampleNum++; - const double fEdgePos = fKOld * fT + fBOld; - const cv::Point2f ptSrc = bVertEdge ? cv::Point2f((float)fEdgePos, (float)fT) - : cv::Point2f((float)fT, (float)fEdgePos); - // 原图坐标 -> 检测图(detImg)坐标(纯平移);在检测图灰度上找第一个 255 - const cv::Point2f ptDet = MatAffinePoint(m_matSrc2Det, ptSrc); - const int nFix = bVertEdge ? cvRound(ptDet.y) : cvRound(ptDet.x); // 固定坐标(行/列) - if ((bVertEdge && (nFix < 0 || nFix >= grayEdgeImg.rows)) || - (!bVertEdge && (nFix < 0 || nFix >= grayEdgeImg.cols))) - { - continue; // 固定坐标越界,该采样点无法搜索 - } - - bool bFound = false; - double fNewPos = fEdgePos; // 找到的新边缘位置(原图坐标) - if (gi == 0) // 左边:向产品内侧(+x)找 - { - const int nX0 = (int)std::ceil(ptDet.x); - const int nXEnd = std::min(grayEdgeImg.cols - 1, nX0 + nRefineSearch); - for (int x = std::max(0, nX0); x <= nXEnd; x++) - { - if (grayEdgeImg.at(nFix, x) == 255) - { - const cv::Point2f ptNew = MatAffinePoint(m_matDet2Src, cv::Point2f((float)x, ptDet.y)); - fNewPos = ptNew.x; - bFound = true; - break; - } - } - } - else if (gi == 1) // 右边:向产品内侧(-x)找 - { - const int nX0 = (int)std::floor(ptDet.x); - const int nXEnd = std::max(0, nX0 - nRefineSearch); - for (int x = std::min(grayEdgeImg.cols - 1, nX0); x >= nXEnd && x >= 0; x--) - { - if (grayEdgeImg.at(nFix, x) == 255) - { - const cv::Point2f ptNew = MatAffinePoint(m_matDet2Src, cv::Point2f((float)x, ptDet.y)); - fNewPos = ptNew.x; - bFound = true; - break; - } - } - } - else if (gi == 2) // 上边:向产品内侧(+y)找 - { - const int nY0 = (int)std::ceil(ptDet.y); - const int nYEnd = std::min(grayEdgeImg.rows - 1, nY0 + nRefineSearch); - for (int y = std::max(0, nY0); y <= nYEnd; y++) - { - if (grayEdgeImg.at(y, nFix) == 255) - { - const cv::Point2f ptNew = MatAffinePoint(m_matDet2Src, cv::Point2f(ptDet.x, (float)y)); - fNewPos = ptNew.y; - bFound = true; - break; - } - } - } - else // 下边:向产品内侧(-y)找 - { - const int nY0 = (int)std::floor(ptDet.y); - const int nYEnd = std::max(0, nY0 - nRefineSearch); - for (int y = std::min(grayEdgeImg.rows - 1, nY0); y >= nYEnd && y >= 0; y--) - { - if (grayEdgeImg.at(y, nFix) == 255) - { - const cv::Point2f ptNew = MatAffinePoint(m_matDet2Src, cv::Point2f(ptDet.x, (float)y)); - fNewPos = ptNew.y; - bFound = true; - break; - } - } - } - if (bFound) - { - vecRefinePts.push_back(cv::Point2f((float)fT, (float)fNewPos)); - } - // 找不到:该采样点没有新边缘信息,不参与拟合(相当于沿用原边缘点) - } - - // 有效采样点足够时重新拟合边缘线;否则保留原边缘 - if (vecRefinePts.size() < 4) - { - m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测", - "edge refine %s skip: sample %d found %ld (need 4), keep old", - strEdge.c_str(), nSampleNum, (long)vecRefinePts.size()); - continue; - } - double fKNew = 0, fBNew = 0; - if (FitLineRobustTV(vecRefinePts, fKNew, fBNew) < 4) - { - m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测", - "edge refine %s skip: fit failed, keep old", strEdge.c_str()); - continue; - } - - // 中心行/列上的新边缘位置 + 合理性检查(仍需在中心外侧,相对原边缘偏移不超过搜索范围) - const double fOldCenterPos = bVertEdge ? ptEdgeArr[gi].x : ptEdgeArr[gi].y; - const double fNewCenterPos = bVertEdge ? (fKNew * fCy + fBNew) : (fKNew * fCx + fBNew); - const bool bCenterSideOK = (gi == 0) ? (fNewCenterPos < fCx) - : (gi == 1) ? (fNewCenterPos > fCx) - : (gi == 2) ? (fNewCenterPos < fCy) - : (fNewCenterPos > fCy); - if (!bCenterSideOK || std::fabs(fNewCenterPos - fOldCenterPos) > nRefineSearch + 2.0) - { - m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测", - "edge refine %s reject: center %.1f -> %.1f (limit %d), keep old", - strEdge.c_str(), fOldCenterPos, fNewCenterPos, nRefineSearch); - continue; - } - - // 应用新边缘线:更新测距点(中心行/列)与绘制线段两端(沿边坐标不变,位置取新线) - if (bVertEdge) - { - ptEdgeArr[gi].x = (float)fNewCenterPos; - ptSegAArr[gi].x = (float)(fKNew * ptSegAArr[gi].y + fBNew); - ptSegBArr[gi].x = (float)(fKNew * ptSegBArr[gi].y + fBNew); - if (gi == 0) - { - fDL = fCx - ptEdgeArr[0].x; - } - else - { - fDR = ptEdgeArr[1].x - fCx; - } - } - else - { - ptEdgeArr[gi].y = (float)fNewCenterPos; - ptSegAArr[gi].y = (float)(fKNew * ptSegAArr[gi].x + fBNew); - ptSegBArr[gi].y = (float)(fKNew * ptSegBArr[gi].x + fBNew); - if (gi == 2) - { - fDT = fCy - ptEdgeArr[2].y; - } - else - { - fDB = ptEdgeArr[3].y - fCy; - } - } - - m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测", - "edge refine %s: sample %d found %ld center %.1f -> %.1f shift %.1f", - strEdge.c_str(), nSampleNum, (long)vecRefinePts.size(), - fOldCenterPos, fNewCenterPos, fNewCenterPos - fOldCenterPos); - } - } - else if (!supportDet.edge_select.empty()) - { - m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测", - "edge refine skip: detImg not CV_8UC1 (type %d)", grayEdgeImg.type()); - } - } - - - - // 3.5、旋转偏移:支架最小外接旋转矩形(长轴对应左/右边缘、短轴对应上/下边缘) - // 长轴与产品左/右边缘的夹角 > r_offset(度) → 偏移;r_offset<=0 不判定(兼容旧参数) - float fSupportAngle = 0; // 支架长轴相对产品左/右边缘的夹角(度,带符号;正=支架长轴相对产品边缘向右倾) - bool bRotOffset = false; - bool bSupportAngleValid = false; // 是否用上了原图精细化角度(false=走 minAreaRect 兜底) - if (supportDet.r_offset > 0 && rsContour.size() >= 3) - { - // 轮廓先映射回原图坐标再拟合旋转矩形(smallmask 的 x/y 缩放可能不同,不还原会带角度误差) - // 注意:smallmask 是 Align_Inner 在"大轮廓裁剪图"上的输出,映射原图时要加裁剪原点 bigRect - std::vector rsContourSrc; - rsContourSrc.reserve(rsContour.size()); - for (size_t ip = 0; ip < rsContour.size(); ip++) - { - rsContourSrc.emplace_back(bigRect.x + rsContour[ip].x * fx, - bigRect.y + rsContour[ip].y * fy); - } - cv::RotatedRect rsRotRect = cv::minAreaRect(rsContourSrc); - - // ===== 精细化计算支架方向(原图灰度上拟合支架长边)===== - // 模型 smallmask 分辨率低(1696×800 上采样回原图,量化误差约 4px),且支架区域接近方形时 - // minAreaRect 的"长轴"方向不稳定,直接用它求旋转角误差大;这里用模型支架边缘做初值, - // 在 detImg 灰度原图上重新拟合支架长边方向: - // 1、把 smallmask 最大轮廓映射回原图,得到支架外接矩形(rsRect); - // 取外接矩形最外侧 nStrip 列(默认 30 列)作为该侧边缘的搜索带 - // 2、结合 edge_select:选 left → 拟合支架右边缘;选 right → 拟合支架左边缘 - // (up/down 对应支架上下短边,水平的线不好拟合,不处理,仍用 minAreaRect 结果) - // 3、沿外接矩形高度按 step 采样;每行在搜索带内逐像素找灰度梯度最大的位置作为边缘点: - // 右边缘取最右 nStrip 列、从右往左找;左边缘取最左 nStrip 列、从左往右找; - // 梯度不足(该侧没有真实边缘)的行不参与拟合 - // 4、采样点沿 y 分成 n 段,每段用 FitLineRobustTV 稳健拟合直线,取各段方向角的中位数: - // 支架边缘的凸起是矩形,凸起外边缘与支架长边平行,拟合到凸起处也不影响方向角; - // 凸起侧边/内部纹理等离群点由 MAD 剔除 - // 失败(有效采样/分段不足、角度离散过大)时保留下面的 minAreaRect 长轴方案 - if (!m_pImageAllResult->detImg.empty() && m_pImageAllResult->detImg.type() == CV_8UC1) - { - const cv::Mat &grayRef = m_pImageAllResult->detImg; - // 边缘搜索带宽度(列):取模型支架外接矩形最外侧多少列;未配置默认 30 - int nStrip = (supportDet.support_refine_search > 0) - ? cvRound(supportDet.support_refine_search) - : SUPPORT_REFINE_STRIP_DEFAULT; - int nStep = (supportDet.support_refine_step > 0) - ? cvRound(supportDet.support_refine_step) - : SUPPORT_REFINE_STEP_DEFAULT; - int nSegments = (supportDet.support_refine_segments > 0) - ? cvRound(supportDet.support_refine_segments) - : SUPPORT_REFINE_SEGMENTS_DEFAULT; - nStrip = std::max(4, nStrip); - nStep = std::max(4, nStep); - nSegments = std::max(1, nSegments); - - // 3 行灰度均值(抑制单行噪声;调用处保证 y 在 [1, rows-2]) - auto gray3 = [&grayRef](int y, int x) -> float { - return (grayRef.at(y - 1, x) + grayRef.at(y, x) + grayRef.at(y + 1, x)) * (1.0f / 3.0f); - }; - - // 拟合支架一条长边:nSide 0=支架左边缘(外侧 -x)、1=支架右边缘(外侧 +x); - // nRefIdx 0=产品左边缘、1=产品右边缘 - // 返回 true 时 fAngleOut=支架长边相对产品该边缘的带符号夹角(度),fEdgeVertOut=支架长边相对竖直方向角度(度) - // pSamplesOut/pSegLinesOut 非空时输出采样边缘点和各段拟合线(检测图坐标),供 -s 绘制调试图 - auto FitSupportEdgeAngle = [&](int nSide, int nRefIdx, float &fAngleOut, int &nSampleOut, int &nSegOut, float &fEdgeVertOut, - std::vector *pSamplesOut, std::vector> *pSegLinesOut) -> bool { - fAngleOut = 0.0f; - fEdgeVertOut = 0.0f; - nSampleOut = 0; - nSegOut = 0; - if (pSamplesOut != NULL) - { - pSamplesOut->clear(); - } - if (pSegLinesOut != NULL) - { - pSegLinesOut->clear(); - } - - // 产品基准边缘方向(统一朝下,与原有旋转角符号约定一致) - cv::Point2f vRef = ptSegBArr[nRefIdx] - ptSegAArr[nRefIdx]; - float fRefLen = (float)cv::norm(vRef); - if (fRefLen < 1e-3f) - { - return false; - } - vRef *= (1.0f / fRefLen); - if (vRef.y < 0) - { - vRef = -vRef; - } - - // ===== 取模型支架外接矩形(rsRect)最外侧 nStrip 列,逐行找边缘点 ===== - // 右边缘:取外接矩形最右 nStrip 列,从右往左扫;左边缘:取最左 nStrip 列,从左往右扫。 - // 每行按灰度梯度找边缘点;梯度不足(该侧没有真实边缘)的行不参与拟合 - int nXStart = 0; // 外侧起始列(原图坐标) - int nXEnd = 0; // 内侧结束列(原图坐标) - if (nSide == 0) - { - nXStart = rsRect.x; - nXEnd = rsRect.x + nStrip - 1; - } - else - { - nXStart = rsRect.x + rsRect.width - 1; - nXEnd = rsRect.x + rsRect.width - nStrip; - } - const int nStepX = (nXEnd >= nXStart) ? 1 : -1; - - // 沿外接矩形高度采样(两端各缩 5%,至少 6px,避开圆角) - double fY0 = rsRect.y; - double fY1 = rsRect.y + rsRect.height - 1; - const double fTrim = std::max(6.0, (fY1 - fY0) * 0.05); - fY0 += fTrim; - fY1 -= fTrim; - if (fY1 - fY0 < 20.0) - { - return false; - } - - std::vector vecEdgePts; // (t,v) = (yDet, xDet) - vecEdgePts.reserve((int)((fY1 - fY0) / nStep) + 1); - - for (double fT = fY0; fT <= fY1; fT += nStep) - { - // 原图 -> 检测图(detImg) 为纯平移;列带两端换算到检测图坐标 - cv::Point2f ptDetStart = MatAffinePoint(m_matSrc2Det, cv::Point2f((float)nXStart, (float)fT)); - cv::Point2f ptDetEnd = MatAffinePoint(m_matSrc2Det, cv::Point2f((float)nXEnd, (float)fT)); - const int nYDet = cvRound(ptDetStart.y); - if (nYDet < 2 || nYDet >= grayRef.rows - 2) - { - continue; - } - const int nX0 = cvRound(ptDetStart.x); // 外侧端 - const int nX1 = cvRound(ptDetEnd.x); // 内侧端 - if ((nStepX > 0 && nX1 <= nX0) || (nStepX < 0 && nX1 >= nX0)) - { - continue; - } - - // 外侧 -> 内侧逐像素扫,取灰度梯度最大的位置作为该行支架边缘点 - int nEdge = -1; - float fBestGrad = 0.0f; - for (int x = nX0; (nStepX > 0) ? (x + nStepX <= nX1) : (x + nStepX >= nX1); x += nStepX) - { - if (x < 1 || x + nStepX < 1 || x >= grayRef.cols - 1 || x + nStepX >= grayRef.cols - 1) - { - continue; - } - const float fGrad = fabs(gray3(nYDet, x + nStepX) - gray3(nYDet, x)); - if (fGrad > fBestGrad) - { - fBestGrad = fGrad; - nEdge = x; - } - } - if (nEdge < 0 || fBestGrad < (float)SUPPORT_REFINE_MIN_CONTRAST) - { - continue; // 该行列带内没有明显灰度阶跃,本行不参与拟合 - } - vecEdgePts.push_back(cv::Point2f((float)nYDet, (float)(nEdge + nStepX * 0.5f))); - } - - m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测", - "support edge diag(side %d): strip %d cols [%d,%d], pts %d", - nSide, nStrip, nXStart, nXEnd, (int)vecEdgePts.size()); - - nSampleOut = (int)vecEdgePts.size(); - if (nSampleOut < 8) - { - return false; - } - - // 沿边分成 n 段,每段稳健拟合一条直线(凸起处点被 MAD 剔除;拟合到凸起外边缘也不影响方向) - std::vector vecAngles; - std::vector vecAnglesVert; - vecAngles.reserve(nSegments); - vecAnglesVert.reserve(nSegments); - for (int s = 0; s < nSegments; s++) - { - const size_t i0 = (size_t)((long long)s * nSampleOut / nSegments); - const size_t i1 = (size_t)((long long)(s + 1) * nSampleOut / nSegments); - if (i1 - i0 < 4) - { - continue; - } - std::vector vecSeg(vecEdgePts.begin() + i0, vecEdgePts.begin() + i1); - double fKSeg = 0, fBSeg = 0; - if (FitLineRobustTV(vecSeg, fKSeg, fBSeg) < 4) - { - continue; - } - - // 拟合线方向(统一朝下)与产品基准边缘的带符号夹角 - cv::Point2f vSeg((float)fKSeg, 1.0f); - float fSegLen = (float)cv::norm(vSeg); - if (fSegLen < 1e-6f) - { - continue; - } - vSeg *= (1.0f / fSegLen); - const double dCross = (double)vSeg.x * vRef.y - (double)vSeg.y * vRef.x; - const double dDot = (double)vSeg.x * vRef.x + (double)vSeg.y * vRef.y; - const float fAngle = (float)(std::atan2(dCross, dDot) * 180.0 / CV_PI); - if (fabs(fAngle) > 45.0f) - { - continue; // 与产品边缘夹角不合理,丢弃该段 - } - vecAngles.push_back(fAngle); - vecAnglesVert.push_back((float)(std::atan2(fKSeg, 1.0) * 180.0 / CV_PI)); - // 记录该段拟合直线的两端点(检测图坐标),-s 时用于绘制支架边缘 - if (pSegLinesOut != NULL) - { - const float fSegT0 = vecSeg.front().x; // vecSeg 点为 (t,v) = (yDet, xDet) - const float fSegT1 = vecSeg.back().x; - std::vector vecLine(2); - vecLine[0] = cv::Point2f((float)(fKSeg * fSegT0 + fBSeg), fSegT0); - vecLine[1] = cv::Point2f((float)(fKSeg * fSegT1 + fBSeg), fSegT1); - pSegLinesOut->push_back(vecLine); - } - } - nSegOut = (int)vecAngles.size(); - if (nSegOut < 2) - { - return false; - } - - // 取各段角度中位数,并用 MAD 检查各段一致性(凸起/纹理导致的异常段会被中位数稀释) - std::vector vecTmp = vecAngles; - std::nth_element(vecTmp.begin(), vecTmp.begin() + vecTmp.size() / 2, vecTmp.end()); - const float fMedian = vecTmp[vecTmp.size() / 2]; - for (size_t i = 0; i < vecAngles.size(); i++) - { - vecTmp[i] = fabs(vecAngles[i] - fMedian); - } - std::nth_element(vecTmp.begin(), vecTmp.begin() + vecTmp.size() / 2, vecTmp.end()); - if (vecTmp[vecTmp.size() / 2] > 5.0f) - { - return false; // 各段方向离散过大(可能拟合到了不同结构),不采用 - } - - fAngleOut = fMedian; - std::nth_element(vecAnglesVert.begin(), vecAnglesVert.begin() + vecAnglesVert.size() / 2, vecAnglesVert.end()); - fEdgeVertOut = vecAnglesVert[vecAnglesVert.size() / 2]; - if (pSamplesOut != NULL) - { - pSamplesOut->reserve(vecEdgePts.size()); - for (size_t i = 0; i < vecEdgePts.size(); i++) - { - pSamplesOut->push_back(cv::Point2f(vecEdgePts[i].y, vecEdgePts[i].x)); // (t,v) -> (x,y) - } - } - return true; - }; - - // 按 edge_select 选择拟合方向:left -> 支架右边缘,right -> 支架左边缘(up/down 不处理) - int nCandSide[2] = {0, 0}; - int nCandRef[2] = {0, 0}; - int nCandNum = 0; - for (size_t iselect = 0; iselect < supportDet.edge_select.size(); iselect++) - { - const std::string &strEdge = supportDet.edge_select[iselect]; - int nSide = -1; - int nRef = -1; - if (strEdge == "left") - { - nSide = 1; // 拟合支架右边缘 - nRef = 0; // 参考产品左边缘 - } - else if (strEdge == "right") - { - nSide = 0; // 拟合支架左边缘 - nRef = 1; // 参考产品右边缘 - } - else - { - continue; - } - bool bDup = false; - for (int ic = 0; ic < nCandNum; ic++) - { - if (nCandSide[ic] == nSide && nCandRef[ic] == nRef) - { - bDup = true; - } - } - if (!bDup && nCandNum < 2) - { - nCandSide[nCandNum] = nSide; - nCandRef[nCandNum] = nRef; - nCandNum++; - } - } - - int nBestCand = -1; - int nBestSample = 0; - int nBestSeg = 0; - float fBestAngle = 0.0f; - float fBestEdgeVert = 0.0f; - int nBestSideUsed = -1; - int nBestRefUsed = -1; - std::vector vecBestSamples; // 采样边缘点(检测图坐标) - std::vector> vecBestSegLines; // 各段拟合线(检测图坐标) - for (int ic = 0; ic < nCandNum; ic++) - { - float fAngle = 0.0f; - float fEdgeVert = 0.0f; - int nSample = 0; - int nSeg = 0; - std::vector vecCandSamples; - std::vector> vecCandSegLines; - // 只拟合 edge_select 指定的支架长边;该侧失败时保留 minAreaRect 结果,不换边 - // (两条长边方向角虽一致,但换边会静默得到另一条边的结果,与配置含义不符) - bool bOk = FitSupportEdgeAngle(nCandSide[ic], nCandRef[ic], fAngle, nSample, nSeg, fEdgeVert, - &vecCandSamples, &vecCandSegLines); - const int nUseSide = nCandSide[ic]; - if (!bOk) - if (!bOk) - { - m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测", - "support angle refine(ref %s): %s support edge fail, keep minAreaRect", - (nCandRef[ic] == 0) ? "left" : "right", (nCandSide[ic] == 0) ? "left" : "right"); - continue; - } - const cv::Point2f vRefLog = ptSegBArr[nCandRef[ic]] - ptSegAArr[nCandRef[ic]]; - m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测", - "support angle refine(ref %s): use %s support edge: sample %d seg %d angle %.2f deg (edge vs vert %.2f, ref vs vert %.2f)", - (nCandRef[ic] == 0) ? "left" : "right", (nCandSide[ic] == 0) ? "left" : "right", - nSample, nSeg, fAngle, fEdgeVert, - (float)(std::atan2((double)vRefLog.x, (double)vRefLog.y) * 180.0 / CV_PI)); - if (nSample > nBestSample) - { - nBestSample = nSample; - nBestSeg = nSeg; - fBestAngle = fAngle; - fBestEdgeVert = fEdgeVert; - nBestSideUsed = nUseSide; - nBestRefUsed = nCandRef[ic]; - vecBestSamples = vecCandSamples; - vecBestSegLines = vecCandSegLines; - nBestCand = ic; - } - } - if (nBestCand >= 0) - { - fSupportAngle = fBestAngle; - bRotOffset = (fabs(fSupportAngle) > supportDet.r_offset); - bSupportAngleValid = true; - m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测", - "support angle refine ok: angle %.2f deg (sample %d seg %d), replace minAreaRect", - fSupportAngle, nBestSample, nBestSeg); - - // -s(存过程图):绘制精细化拟合的支架边缘,便于从图像确认拟合效果 - // 蓝=smallmask 支架轮廓 黄=参与拟合的采样边缘点 绿=各段稳健拟合直线 - // 红=最终方向线(产品基准边方向 + 精细夹角,过采样点重心) 品红=产品基准边 - if (DetImgInfo_shareP->bsaveProcessImg && !vecBestSamples.empty()) - { - cv::Mat show; - if (grayRef.channels() == 1) - { - cv::cvtColor(grayRef, show, cv::COLOR_GRAY2BGR); - } - else - { - show = grayRef.clone(); - } - - // 支架区域轮廓(蓝):smallmask 最大轮廓映射到检测图坐标 - std::vector vecRsPoly; - vecRsPoly.reserve(rsContour.size()); - for (size_t ip = 0; ip < rsContour.size(); ip++) - { - cv::Point2f ptSrc(bigRect.x + rsContour[ip].x * fx, bigRect.y + rsContour[ip].y * fy); - cv::Point2f ptDet = MatAffinePoint(m_matSrc2Det, ptSrc); - vecRsPoly.push_back(cv::Point(cvRound(ptDet.x), cvRound(ptDet.y))); - } - if (vecRsPoly.size() >= 3) - { - cv::polylines(show, vecRsPoly, true, cv::Scalar(255, 0, 0), 4); - } - - // 采样边缘点(黄) - for (size_t i = 0; i < vecBestSamples.size(); i++) - { - cv::circle(show, cv::Point(cvRound(vecBestSamples[i].x), cvRound(vecBestSamples[i].y)), 4, - cv::Scalar(0, 255, 255), -1); - } - - // 各段稳健拟合的直线(绿) - for (size_t i = 0; i < vecBestSegLines.size(); i++) - { - if (vecBestSegLines[i].size() == 2) - { - cv::line(show, cv::Point(cvRound(vecBestSegLines[i][0].x), cvRound(vecBestSegLines[i][0].y)), - cv::Point(cvRound(vecBestSegLines[i][1].x), cvRound(vecBestSegLines[i][1].y)), - cv::Scalar(0, 255, 0), 4); - } - } - - // 最终方向线(红):产品基准边方向旋转支架精细夹角,过采样点重心 - cv::Point2f vRefBest = ptSegBArr[nBestRefUsed] - ptSegAArr[nBestRefUsed]; - float fRefBestLen = (float)cv::norm(vRefBest); - if (fRefBestLen > 1e-3f) - { - vRefBest *= (1.0f / fRefBestLen); - if (vRefBest.y < 0) - { - vRefBest = -vRefBest; - } - const double dRad = (double)fSupportAngle * CV_PI / 180.0; - cv::Point2f vDir((float)(vRefBest.x * std::cos(dRad) + vRefBest.y * std::sin(dRad)), - (float)(-vRefBest.x * std::sin(dRad) + vRefBest.y * std::cos(dRad))); - float fDirLen = (float)cv::norm(vDir); - if (fDirLen > 1e-6f) - { - vDir *= (1.0f / fDirLen); - } - cv::Point2f ptCenter(0.0f, 0.0f); - float fSampleYMin = vecBestSamples[0].y; - float fSampleYMax = vecBestSamples[0].y; - for (size_t i = 0; i < vecBestSamples.size(); i++) - { - ptCenter += vecBestSamples[i]; - fSampleYMin = std::min(fSampleYMin, vecBestSamples[i].y); - fSampleYMax = std::max(fSampleYMax, vecBestSamples[i].y); - } - ptCenter *= (1.0f / (float)vecBestSamples.size()); - const float fHalf = std::max(20.0f, (fSampleYMax - fSampleYMin) * 0.6f); - cv::line(show, cv::Point(cvRound(ptCenter.x - vDir.x * fHalf), cvRound(ptCenter.y - vDir.y * fHalf)), - cv::Point(cvRound(ptCenter.x + vDir.x * fHalf), cvRound(ptCenter.y + vDir.y * fHalf)), - cv::Scalar(0, 0, 255), 4); - - // 产品基准边缘(品红) - cv::Point2f ptRefA = MatAffinePoint(m_matSrc2Det, ptSegAArr[nBestRefUsed]); - cv::Point2f ptRefB = MatAffinePoint(m_matSrc2Det, ptSegBArr[nBestRefUsed]); - cv::line(show, cv::Point(cvRound(ptRefA.x), cvRound(ptRefA.y)), - cv::Point(cvRound(ptRefB.x), cvRound(ptRefB.y)), - cv::Scalar(255, 0, 255), 3); - } - - // 文字信息:所用支架边/参考产品边/采样与分段数/角度 - char strDbg[256]; - snprintf(strDbg, sizeof(strDbg), "support edge refine: side %s ref %s sample %d seg %d", - (nBestSideUsed == 0) ? "left" : "right", (nBestRefUsed == 0) ? "left" : "right", - nBestSample, nBestSeg); - cv::putText(show, strDbg, cv::Point(20, 70), cv::FONT_HERSHEY_SIMPLEX, 2.0, cv::Scalar(0, 255, 255), 4); - cv::Point2f vRefDbg = ptSegBArr[nBestRefUsed] - ptSegAArr[nBestRefUsed]; - snprintf(strDbg, sizeof(strDbg), "angle %.2f deg (edge vs vert %.2f, ref vs vert %.2f)", - fSupportAngle, fBestEdgeVert, - (float)(std::atan2((double)vRefDbg.x, (double)vRefDbg.y) * 180.0 / CV_PI)); - cv::putText(show, strDbg, cv::Point(20, 140), cv::FONT_HERSHEY_SIMPLEX, 2.0, cv::Scalar(0, 255, 255), 4); - - cv::Mat showSmall; - cv::resize(show, showSmall, cv::Size(show.cols / 4, show.rows / 4)); - std::vector paramJpg = {cv::IMWRITE_JPEG_QUALITY, 90}; - cv::imwrite(DetImgInfo_shareP->strChannel + "_support_angle.jpg", showSmall, paramJpg); - m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测", - "support angle debug image saved: %s_support_angle.jpg", DetImgInfo_shareP->strChannel.c_str()); - } - } - else if (!supportDet.edge_select.empty()) - { - m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测", - "support angle refine not applied(no left/right edge or fit failed), use minAreaRect"); - } - } - - // 3.5、旋转偏移(兜底):支架最小外接旋转矩形(长轴对应左/右边缘、短轴对应上/下边缘) - // 长轴与产品左/右边缘的夹角 > r_offset(度) → 偏移;r_offset<=0 不判定(兼容旧参数) - if (!bSupportAngleValid) - { - cv::Point2f rpts[4]; - rsRotRect.points(rpts); - cv::Point2f d01 = rpts[1] - rpts[0]; - cv::Point2f d12 = rpts[2] - rpts[1]; - float len01 = (float)cv::norm(d01); - float len12 = (float)cv::norm(d12); - // 长轴方向 = 长边方向(长轴对应左/右边缘),并统一朝下(与左边缘 上->下 方向一致) - cv::Point2f vLong(0, 1); - if (len01 >= len12 && len01 > 1e-3f) - { - vLong = d01 * (1.0f / len01); - } - else if (len12 > 1e-3f) - { - vLong = d12 * (1.0f / len12); - } - if (vLong.y < 0) - { - vLong = -vLong; - } - // 参考方向 = 产品左边缘(ptSegAArr[0]->ptSegBArr[0],拟合边/回退矩形边);退化时用垂直方向 - cv::Point2f vRef = ptSegBArr[0] - ptSegAArr[0]; - float lenRef = (float)cv::norm(vRef); - if (lenRef > 1e-3f) - { - vRef *= (1.0f / lenRef); - } - else - { - vRef = cv::Point2f(0, 1); - } - if (vRef.y < 0) - { - vRef = -vRef; - } - // 有符号夹角(度);长轴是“线”,角度归一化到 [-90, 90] - double dCross = (double)vLong.x * vRef.y - (double)vLong.y * vRef.x; - double dDot = (double)vLong.x * vRef.x + (double)vLong.y * vRef.y; - fSupportAngle = (float)(std::atan2(dCross, dDot) * 180.0 / CV_PI); - if (fSupportAngle > 90.0f) - { - fSupportAngle -= 180.0f; - } - else if (fSupportAngle < -90.0f) - { - fSupportAngle += 180.0f; - } - bRotOffset = (fabs(fSupportAngle) > supportDet.r_offset); - } - } - - // 像素 -> mm - fDL *= m_fImgage_Scale_X; - fDR *= m_fImgage_Scale_X; - fDT *= m_fImgage_Scale_Y; - fDB *= m_fImgage_Scale_Y; - float fDistArr[4] = {fDL, fDR, fDT, fDB}; // 顺序固定:0 left 1 right 2 up 3 down - - // 4、按 edge_select 指定方向(left/right/up/down)取计算值,与标准值做差取平均,平均差超过 offset → 偏移 - // left/right 用 x_standard,up/down 用 y_standard;offset 用 x_offset - float fSumDiff = 0; - int nDiffNum = 0; - std::string strSelect = ""; - std::string strDiff = ""; - char strTmp[64]; - for (size_t iselect = 0; iselect < supportDet.edge_select.size(); iselect++) - { - const std::string &strEdge = supportDet.edge_select[iselect]; - int gi = -1; // 方向索引:0 left 1 right 2 up 3 down - float fStandard = 0; - if (strEdge == "left") - { - gi = 0; - fStandard = supportDet.x_standard; - } - else if (strEdge == "right") - { - gi = 1; - fStandard = supportDet.x_standard; - } - else if (strEdge == "up") - { - gi = 2; - fStandard = supportDet.y_standard; - } - else if (strEdge == "down") - { - gi = 3; - fStandard = supportDet.y_standard; - } - else - { - continue; - } - - float fDist = fDistArr[gi]; - float fDiff = fabs(fDist - fStandard); - fSumDiff += fDiff; - nDiffNum++; - // 记录该方向绘制信息:|实测-标准|超限差(x_offset)则绘制为红色 - { - Support_Draw_Info_::Edge_Draw_ edgeDraw; - edgeDraw.strEdge = strEdge; - edgeDraw.fDist = fDist; - edgeDraw.fStandard = fStandard; - edgeDraw.bExceed = (fDiff > supportDet.x_offset); - edgeDraw.ptEdge = ptEdgeArr[gi]; - edgeDraw.ptEdgeA = ptSegAArr[gi]; - edgeDraw.ptEdgeB = ptSegBArr[gi]; - m_SupportDrawInfo.edgeList.push_back(edgeDraw); - } - sprintf(strTmp, "%s(%.1f/%.1f) ", strEdge.c_str(), fDist, fStandard); - strSelect += strTmp; - sprintf(strTmp, "%.1f ", fDiff); - strDiff += strTmp; - } - // edge_select 为空或无效时 nDiffNum=0,距离不参与判定(仅打日志) - float fAvgDiff = (nDiffNum > 0) ? (fSumDiff / nDiffNum) : 0.0f; - bool bOffsetDist = (nDiffNum > 0) && (fAvgDiff > supportDet.x_offset); - // 距离偏移 或 旋转偏移(r_offset) 任一超限 → 支架偏移 - bool bOffset = bOffsetDist || bRotOffset; - - // 记录绘制信息(DrawResult_Step_1 用):支架中心十字 + 各方向距离 - m_SupportDrawInfo.bValid = true; - m_SupportDrawInfo.bOffset = bOffset; - m_SupportDrawInfo.rpRect = rpRect; - m_SupportDrawInfo.rsRect = rsRect; - m_SupportDrawInfo.ptCenter = cv::Point2f(fCx, fCy); - - m_pdetlog->AddCheckstr(PrintLevel_0, "支架检测", - "rp [%d %d %d %d] rs [%d %d %d %d] support area %.2f mm2 (min %.2f) scale (%.4f %.4f) center (%.1f %.1f) edge %s quad %ld dL %.1f dR %.1f dT %.1f dB %.1f | select %s diff %s avgDiff %.1f limit %.1f | rot %.2f deg limit %.2f(%s) ng %d", - rpRect.x, rpRect.y, rpRect.width, rpRect.height, - rsRect.x, rsRect.y, rsRect.width, rsRect.height, - dSupportAreaMm2, supportDet.support_min_area, - fx, fy, fCx, fCy, bFitQuad ? "fitquad" : "rpbox", (long)m_HandleBodyQuad.size(), fDL, fDR, fDT, fDB, - strSelect.c_str(), strDiff.c_str(), fAvgDiff, supportDet.x_offset, - fSupportAngle, supportDet.r_offset, bSupportAngleValid ? "refine" : "rect", bOffset); - - if (bOffset) - { - QX_ERROR_INFO_ temerror; - temerror.Idx = m_pDetResult->pQx_ErrorList->size(); - - // roi 在原图坐标系,按坐标映射转换到检测图(detImg)坐标系 - // cv::Rect roi = MatAffineRect(m_matSrc2Det, rsRect); - cv::Rect roi = rsRect; - 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 = rsRect.width; - float h = rsRect.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_cell_ZJ_PianYi; - 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]; - - // 计算支架中心所在的检测区域 - 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 &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); - } - } - } - } - } - - /* 把手缺失检测:bigmask 的凸起本身不算 NG,只有"参数里的某个把手区域附近找不到把手"才算 NG(每个缺失区域报一条) */ - { - if (m_bHandleDetSucc && m_bHandleExpect && !m_bHandleFound) - { - for (size_t iRegion = 0; iRegion < m_HandleRegionResultList.size(); iRegion++) - { - Handle_Region_Result_ ®ionResult = m_HandleRegionResultList[iRegion]; - if (regionResult.bFound) - { - continue; - } - - // 上报该把手区域的位置:参数(模板)坐标 -> 检测图(detImg)坐标 - // cv::Rect roi = MatAffineRect(m_matParam2Det, regionResult.boxRegion); - cv::Rect roi = regionResult.boxRegion; - roi &= cv::Rect(0, 0, m_pImageAllResult->detImg.cols, m_pImageAllResult->detImg.rows); - if (roi.width <= 0 || roi.height <= 0) - { - continue; - } - - QX_ERROR_INFO_ temerror; - temerror.Idx = m_pDetResult->pQx_ErrorList->size(); - 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_cell_CGMo_QueShi; - temerror.qx_name = CONFIG_QX_NAME_Names[CONFIG_QX_NAME_cell_CGMo_QueShi]; - 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 &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 region %d roi [%d %d %d %d]", - temerror.qx_name.c_str(), temerror.Idx, regionResult.nIdx, - roi.x, roi.y, roi.width, roi.height); - } - } - } - - /* Tag检测(bDetTag 关闭时不上报标签 NG) */ - { - if (!m_pbaseCheckFunction->supportDet.bDetTag) - { - m_tag_roiList.clear(); - } - - // 标签能量/灰阶差(hj)计算用掩膜:所有标签区域填白(与 blob 检测用 AIMaskImg 的口径一致), - // 由 calc_blob_info_withstats 按“前景=标签区域、背景=外扩一圈”计算 - long tagEnergy_s = CheckUtil::getcurTime(); - cv::Mat tagMask; - if (!m_tag_roiList.empty() && !m_pImageAllResult->detImg.empty()) - { - tagMask = cv::Mat::zeros(m_pImageAllResult->detImg.size(), CV_8UC1); - std::vector> tagPolys; - tagPolys.reserve(m_tag_roiList.size()); - for (const auto &tagRoiMask : m_tag_roiList) - { - cv::Point2f ptfMask[4]; - tagRoiMask.points(ptfMask); - std::vector polyMask(4); - for (int k = 0; k < 4; k++) - { - polyMask[k] = cv::Point(cvRound(ptfMask[k].x), cvRound(ptfMask[k].y)); - } - tagPolys.push_back(std::move(polyMask)); - } - cv::fillPoly(tagMask, tagPolys, cv::Scalar(255)); - } - - for (const auto &tagRoi : m_tag_roiList) - { - // tag roi 在原图坐标系,按坐标映射转换到检测图(detImg)坐标系 - // cv::Rect roi = MatAffineRect(m_matSrc2Det, tagRoi.boundingRect()); - cv::Rect roi = tagRoi.boundingRect(); - roi &= cv::Rect(0, 0, m_pImageAllResult->detImg.cols, m_pImageAllResult->detImg.rows); - if (roi.width <= 0 || roi.height <= 0) - { - continue; - } - - QX_ERROR_INFO_ temerror; - temerror.Idx = m_pDetResult->pQx_ErrorList->size(); - 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 tw = tagRoi.size.width; - float th = tagRoi.size.height; - temerror.flen = (tw > th ? tw : th) * m_fImgage_Scale_X; - temerror.fbreadth = (tw > th ? th : tw) * m_fImgage_Scale_Y; - - // 标签区域的能量/灰阶差(hj):与 blob 检测相同口径(前景=标签区域,背景=外扩一圈) - if (!tagMask.empty()) - { - cv::Rect roiStat = roi; - cv::Scalar tagStat = calc_blob_info_withstats(m_pImageAllResult->detImg, tagMask, roiStat); - temerror.energy = (int)tagStat[1]; - temerror.grayDis = (float)tagStat[2]; - } - - temerror.nconfig_qx_type = CONFIG_QX_NAME_cell_TBBQ; - temerror.qx_name = CONFIG_QX_NAME_Names[CONFIG_QX_NAME_cell_TBBQ]; - 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 &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, "Tag检测", "tag %d roi [%d %d %d %d] energy %d hj %.1f -> NG", - temerror.Idx, roi.x, roi.y, roi.width, roi.height, temerror.energy, temerror.grayDis); - } - if (!m_tag_roiList.empty()) - { - long tagEnergy_e = CheckUtil::getcurTime(); - m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Tag", "tag energy calc use time %ld", tagEnergy_e - tagEnergy_s); - } - } - - m_CheckResult_shareP->nresult = 0; - - /*把临时可以绘制的结果都绘制出来。*/ - DrawResult_Step_1(); - long time_AI_e; - /*AI 推理生成*/ - { - long t211 = CheckUtil::getcurTime(); - // 对AI的结果进行 处理,等待 AI 推理全部完成。 - rec = AIMaskDet(); - if (rec != CHECK_OK) - { - m_nErrorCode = rec; - m_nCheckResultErrorCode = m_nErrorCode; - return m_nErrorCode; - } - time_AI_e = CheckUtil::getcurTime(); - m_pdetlog->AddCheckstr(PrintLevel_0, "4、detect", "-------------------------2、AI Detect--------%ld wait AI complate %ld-------\n", time_AI_e - time_AI_s, time_AI_e - t211); - - m_CheckResult_shareP->resultMaskImg = m_pImageAllResult->AIMaskImg; - } - - { - - t4 = CheckUtil::getcurTime(); - rec = GetCheckResultBLob(); - if (rec != CHECK_OK) - { - m_nErrorCode = rec; - m_nCheckResultErrorCode = m_nErrorCode; - return m_nErrorCode; - } - t5 = CheckUtil::getcurTime(); - m_pdetlog->AddCheckstr(PrintLevel_0, "5、BLob ", "------------------------ --------%ld -------\n", t5 - t4); - } - // m_pdetlog->printLog(m_strCurDetChannel); - - long te = CheckUtil::getcurTime(); - m_pdetlog->bPrintStr = true; - m_pdetlog->AddCheckstr(PrintLevel_1, "result", " ALL use Time %ld edge %ld AI %ld blob %ld", - te - t1, time_edge_e - time_edge_s, time_AI_e - time_AI_s, t5 - t4); - - m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, "End", " Check Run"); - - return 0; -} - -int ImgCheckAnalysisy::SetNewConfig() -{ - if (m_nConfigIdx < 0) - { - return 1; - /* code */ - } - - if (m_pConfig->GetConfigUpdataStatus(ConfigType_Analysisy_Common_XL, m_nConfigIdx)) - { - m_bupdateconfig = true; - // printf("************** ImgCheckAnalysisy::SetNewConfig m_nConfigIdx %d\n", m_nConfigIdx); - m_old_productROI = cv::Rect(0, 0, 0, 0); - m_pConfig->GetConfig(ConfigType_Analysisy_Common_XL, &m_AnalysisyConfig); - if (m_AnalysisyConfig.commonCheckConfig.nodeConfigArr.size() > 0) - { - m_pCommonAnalysisyConfig = &m_AnalysisyConfig.commonCheckConfig.nodeConfigArr.at(0); - m_pBasicConfig = &m_AnalysisyConfig.commonCheckConfig.baseConfig; - m_pRegionAnalysisyParam = &m_pCommonAnalysisyConfig->regionConfigArr.at(0); - - UpdateImgageScale(); - if (false) - { - printf("SetNewConfig m_nConfigIdx %d m_CheckConfig.strSkuName %s \n", m_nConfigIdx, m_AnalysisyConfig.strSkuName.c_str()); - } - } - else - { - printf("m_AnalysisyConfig.commonCheckConfig.nodeConfigArr == 0 \n"); - } - } - else - { - // printf("ConfigType_Analysisy_Common_XL no Update \n"); - } - m_nErrorCode = CHECK_OK; - return CHECK_OK; -} - -ChannelCheckFunction *ImgCheckAnalysisy::GetChannelFuntion(std::string strChannelName) -{ - ChannelCheckFunction *p = NULL; - for (int i = 0; i < m_AnalysisyConfig.checkFunction.channelFunctionArr.size(); i++) - { - if (CheckUtil::compareIgnoreCase(m_AnalysisyConfig.checkFunction.channelFunctionArr[i].strChannelName, strChannelName)) - { - p = &m_AnalysisyConfig.checkFunction.channelFunctionArr[i]; - } - } - - return p; -} -int ImgCheckAnalysisy::Run(int nId) -{ - std::vector vi; - vi.push_back(nId); - auto nRet = set_cpu_id(vi); - // printf("Check So %d bind cpu ret %d, %d\n", m_nThreadIdx, nRet, nId); - while (!m_bExit) - { - // 数据准备完成,开启检测 - if (m_nRun_Status == CHECK_THREAD_STATUS_READY) - { - m_nRun_Status = CHECK_THREAD_STATUS_BUSY; - /* 检测 */ - CheckRun(); - m_nRun_Status = CHECK_THREAD_STATUS_COMPLETE; - m_pImageAllResult->setStep(ImageAllResult::DetStep_Complet); - m_nRun_Status = CHECK_THREAD_STATUS_IDLE; - // printf("*--------%d\n", m_nRun_Status); - } - else - { - - usleep(1000); - } - // printf("*-"); - usleep(1000); - } - return 0; -} -int ImgCheckAnalysisy::set_cpu_id(const std::vector &cpu_set_vec) -{ - // for cpu affinity - int nRet = 0; -#ifdef __linux - cpu_set_t _cur_cpu_set; - CPU_ZERO(&_cur_cpu_set); - for (auto _id : cpu_set_vec) - { - CPU_SET(_id, &_cur_cpu_set); - } - if (0 > pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &_cur_cpu_set)) - { - perror("set cpu affinity failed: "); - printf("Warning: set cpu affinity failed ... ...\n"); - nRet = -1; - } -#endif //__linux - return nRet; -} - -int ImgCheckAnalysisy::CalBlob_Other() -{ - - m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "CalBlob_Other", " Start"); - - float fs_x = m_fImgage_Scale_X; - float fs_y = m_fImgage_Scale_Y; - - float fs_resize_x = m_pImageAllResult->fscale_detToresult_x; - float fs_resize_y = m_pImageAllResult->fscale_detToresult_y; - // 遍历每个检测blob - for (int i = 0; i < blobs.blobCount; i++) - { - - // m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Blob", "%d/%d start", i, blobs.blobCount); - - cv::Rect roi; - roi.x = blobs.blobTab[i].minx; - roi.y = blobs.blobTab[i].miny; - roi.width = blobs.blobTab[i].maxx - blobs.blobTab[i].minx + 1; - roi.height = blobs.blobTab[i].maxy - blobs.blobTab[i].miny + 1; - - cv::Scalar result = calc_blob_info_withstats(m_pImageAllResult->detImg, m_pImageAllResult->AIMaskImg, roi); - - int QX_whiteBLACK = CONFIG_QX_BLACK; - if (result[0] > 0) - { - QX_whiteBLACK = CONFIG_QX_WHITE; - } - - double hj = result[2]; - double energe = result[1]; - blobs.blobTab[i].energy = energe; - blobs.blobTab[i].grayDis = hj; - - float JudgArea = blobs.blobTab[i].area * fs_x * fs_y; - blobs.blobTab[i].JudgArea = JudgArea; - float flen = roi.width * fs_x; - float fwid = roi.height * fs_x; - if (roi.height * fs_y > flen) - { - flen = roi.height * fs_y; - fwid = roi.width * fs_y; - } - blobs.blobTab[i].len = flen; - blobs.blobTab[i].breadth = fwid; - - int nerrortype = 0; - int checkFlage = 0; - - float fmaxScore = 0; - int config_qx_type = 0; - - if (blobs.blobTab[i].ErrType == ERR_TYPE_2) - { - QX_whiteBLACK = CONFIG_QX_BLACK; // 强制为 黑色 - } - blobs.blobTab[i].whiteOrblack = QX_whiteBLACK; - - // 精确计算长度 - vector re_len = Cal_QXLen(m_pImageAllResult->detImg(roi), config_qx_type, fs_x, fs_y); - - if (re_len.size() > 2) - { - if(re_len[0] >= 0){ - flen = re_len[0]; - blobs.blobTab[i].len = flen; - } - if(re_len[1] >= 0){ - fwid = re_len[1]; - blobs.blobTab[i].breadth = fwid; - } - - } - } - return 0; -} - -int ImgCheckAnalysisy::GetClassImg(const cv::Mat &img, cv::Mat &AIdetImg, cv::Rect qx_roi, int detwidth, int detheight) -{ - - cv::Rect cutroi; - cv::Rect roi = qx_roi; - bool bresize = false; - { - - int pc_x = roi.x + roi.width * 0.5; - int pc_y = roi.y + roi.height * 0.5; - - if (roi.width < detwidth && roi.height < detheight) - { - cutroi.width = detwidth; - cutroi.x = pc_x - detwidth * 0.5; - cutroi.height = detheight; - cutroi.y = pc_y - detheight * 0.5; - } - else - { - // 宽 高 - if (roi.width > roi.height) - { - cutroi.width = roi.width + 20; - cutroi.x = roi.x - 10; - - float fsx = detheight * 1.0f / detwidth; - cutroi.height = cutroi.width * fsx; - cutroi.y = pc_y - cutroi.height * 0.5; - } - else - { - cutroi.height = roi.height + 20; - cutroi.y = roi.y - 10; - - float fsy = detwidth * 1.0f / detheight; - cutroi.width = cutroi.height * fsy; - cutroi.x = pc_x - cutroi.width * 0.5; - } - - bresize = true; - } - - if (cutroi.x < 0) - { - cutroi.x = 0; - } - if (cutroi.y < 0) - { - cutroi.y = 0; - } - if (cutroi.x + cutroi.width >= img.cols) - { - cutroi.x = img.cols - cutroi.width; - if (cutroi.x < 0) - { - cutroi.x = 0; - if (cutroi.x + cutroi.width >= img.cols) - { - cutroi.width = img.cols; - } - } - } - if (cutroi.y + cutroi.height >= img.rows) - { - cutroi.y = img.rows - cutroi.height; - if (cutroi.y < 0) - { - cutroi.y = 0; - if (cutroi.y + cutroi.height >= img.rows) - { - cutroi.height = img.rows; - } - } - } - } - if (!CheckUtil::RoiInImg(cutroi, img)) - { - return 1; - } - cv::Size sz; - sz.width = detwidth; - sz.height = detheight; - if (cutroi.width != sz.width || cutroi.height != sz.height) - { - cv::resize(img(cutroi), AIdetImg, sz); - } - else - { - AIdetImg = img(cutroi).clone(); - } - - if (1 != AIdetImg.channels()) - { - cv::cvtColor(AIdetImg, AIdetImg, cv::COLOR_BGR2GRAY); - } - return 0; -} - -vector ImgCheckAnalysisy::Cal_QXLen(cv::Mat qx_maskImg, int qx_type, float fsc_x, float fsc_y) -{ - vector resLen(3); - - float nlen = -1; - cv::Mat detimg = qx_maskImg; - - resLen[0] = nlen; - // 寻找轮廓 - vector> contours; - cv::findContours(detimg, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE); - - // 找到最大面积的轮廓 - double maxArea = -1; - int maxAreaIdx = -1; - for (size_t i = 0; i < contours.size(); ++i) - { - double area = contourArea(contours[i]); - if (area > maxArea) - { - maxArea = area; - maxAreaIdx = i; - } - } - - // 如果找到了最大面积的轮廓 - if (maxAreaIdx >= 0) - { - - // 使用minAreaRect找到最小外接矩形 - cv::RotatedRect rect = cv::minAreaRect(contours[maxAreaIdx]); - - // // 绘制最小外接矩形 - // Point2f vertices[4]; - // rect.points(vertices); - // for (int i = 0; i < 4; ++i) - // { - // line(detimg, vertices[i], vertices[(i + 1) % 4], Scalar(128, 255, 0), 2); // 绿色线 - // } - // 输出结果 - - // 获取最小外接矩形的尺寸 - float width = rect.size.width; - float height = rect.size.height; - - // std::cout << "1 Width: " << width << ", Height: " << height << std::endl; - Point2f vertices[4]; - rect.points(vertices); - - vector newcont; - for (int i = 0; i < 4; ++i) - { - Point2f p; - p.x = vertices[i].x * fsc_x; - p.y = vertices[i].y * fsc_y; - newcont.push_back(p); - } - vector> contours_New; - contours_New.push_back(newcont); - // Recreate the rotated rectangle with scaled vertices - RotatedRect scaledRect = minAreaRect(contours_New[0]); - - // Calculate scaled width and height - width = scaledRect.size.width; - height = scaledRect.size.height; - // std::cout << "2 width: " << width << ", height: " << height << std::endl; - // float scale = 0.5; // 缩放比例 - // rect.size.width *= fsc_x; - // rect.size.height *= fsc_y; - // std::cout << "fsc_x: " << fsc_x << ", fsc_y: " << fsc_y << std::endl; - // std::cout << "Width: " << width << ", Height: " << height << std::endl; - // 获取最小外接矩形的尺寸 - // width = rect.size.width; - // height = rect.size.height; - if (width > height) - { - nlen = width; - resLen[0] = width; - resLen[1] = height; - /* code */ - } - else - { - nlen = height; - resLen[0] = height; - resLen[1] = width; - } - } - - // getchar(); - return resLen; -} - -int ImgCheckAnalysisy::GetCheckResultBLob() -{ - long t1, t2, t3, t4, t5, t6, t7; - t1 = CheckUtil::getcurTime(); - int re = GetALLBlob(); - if (re != 0) - { - return re; - } - - // 缺陷分类 多线程 实现。 - long t11 = CheckUtil::getcurTime(); - m_Classtask = std::make_shared(); - m_Classtask->taskname = Task_Class; - m_task.sendTask(m_Classtask); - - re = CalBlob_Other(); - if (re != 0) - { - return re; - } - long t12 = CheckUtil::getcurTime(); - // 等待分类 完成。 - m_Classtask->waitComplate(); - long t13 = CheckUtil::getcurTime(); - // printf("=========== cls time %ld waite time %ld\n", t13 - t11, t13 - t12); - - BLobToDetResult(); - - return 0; -} - -void worker(const unsigned char *img, int w, int startRow, int endRow, unsigned char *rowFlags) -{ - for (int y = startRow; y < endRow; y++) - { - const unsigned char *p = img + (size_t)y * w; - rowFlags[y] = 0; - for (int x = 0; x < w; x++) - { - if (p[x] != 0) - { - rowFlags[y] = 1; - break; - } - } - } -} -int ImgCheckAnalysisy::GetALLBlob() -{ - - std::string strBaseLog = "Blob"; - m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, strBaseLog, "image Blob Analysis Start"); - if (m_pImageAllResult->AIMaskImg.empty()) - { - return 1; - } - unsigned char *pGrayErrordata = (unsigned char *)m_pImageAllResult->AIMaskImg.data; - int width = m_pImageAllResult->AIMaskImg.cols; - int height = m_pImageAllResult->AIMaskImg.rows; - - long t1 = CheckUtil::getcurTime(); - - memset(&blobs, 0x00, sizeof(ERROR_DOTS_BLOBS)); - - ERROR_DOTS_BLOBS blobs_v1; - memset(&blobs_v1, 0x00, sizeof(ERROR_DOTS_BLOBS)); - ERROR_DOTS_BLOBS blobs_big; - memset(&blobs_big, 0x00, sizeof(ERROR_DOTS_BLOBS)); - ERROR_DOTS_BLOBS blobs_big_2; - memset(&blobs_big_2, 0x00, sizeof(ERROR_DOTS_BLOBS)); - printf("=====>>>> GetALLBlob m_strCurDetChannel %s \n", m_strCurDetChannel.c_str()); - if (m_strCurDetChannel == "CA") - { - printf("=====>>>>GetALLBlob USE CA %s \n", m_strCurDetChannel.c_str()); - GetBlobs_ALL_New(&blobs_v1, pGrayErrordata, m_ImgBlobHFlagData, width, height, 15); - m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, strBaseLog, "blob 1 num %d", blobs_v1.blobCount); - - if (blobs_v1.srcBlobCount >= _MAX_ERROR_DOT_BLOB) - { - - GetBlobs_ALL_New(&blobs_big, pGrayErrordata, m_ImgBlobHFlagData, width, height, 200); - m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, strBaseLog, "blob 2 num %d", blobs_big.blobCount); - - if (blobs_big.srcBlobCount >= _MAX_ERROR_DOT_BLOB) - { - - m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, strBaseLog, "blob 3 num %d", blobs_big_2.blobCount); - GetBlobs_ALL_New(&blobs_big_2, pGrayErrordata, m_ImgBlobHFlagData, width, height, 500); - } - } - } - else - { - GetBlobs_oneLabe(&blobs_v1, pGrayErrordata, m_ImgBlobHFlagData, width, height, 15); - m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, strBaseLog, "blob 1 num %d", blobs_v1.blobCount); - - if (blobs_v1.srcBlobCount >= _MAX_ERROR_DOT_BLOB) - { - - GetBlobs_oneLabe(&blobs_big, pGrayErrordata, m_ImgBlobHFlagData, width, height, 200); - m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, strBaseLog, "blob 2 num %d", blobs_big.blobCount); - - if (blobs_big.srcBlobCount >= _MAX_ERROR_DOT_BLOB) - { - - m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, strBaseLog, "blob 3 num %d", blobs_big_2.blobCount); - GetBlobs_oneLabe(&blobs_big_2, pGrayErrordata, m_ImgBlobHFlagData, width, height, 500); - } - } - } - - long t2 = CheckUtil::getcurTime(); - PushBlob(&blobs, &blobs_big_2); - PushBlob(&blobs, &blobs_big); - PushBlob(&blobs, &blobs_v1); - - m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, strBaseLog, "blob ALL num %d", blobs.blobCount); - // long t2 = CheckUtil::getcurTime(); - // printf(" BLob time %ld \n", t2 - t1); - - if (false || DetImgInfo_shareP->bsaveProcessImg) - { - int font_face = cv::FONT_HERSHEY_SIMPLEX; - double font_scale = 0.5; - int thickness = 1; - - m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, strBaseLog, "Save Tem Img"); - cv::Mat tm = m_pImageAllResult->AIMaskImg.clone(); // 保持 mask 单通道,不再灰度转假彩 - for (int i = 0; i < blobs.blobCount; i++) - { - cv::Rect roi; - roi.x = blobs.blobTab[i].minx; - roi.y = blobs.blobTab[i].miny; - roi.width = blobs.blobTab[i].maxx - blobs.blobTab[i].minx + 1; - roi.height = blobs.blobTab[i].maxy - blobs.blobTab[i].miny + 1; - - if (blobs.blobTab[i].ErrType == 0) - { - cv::rectangle(tm, roi, CheckUtil::GetDrawColor(tm, cv::Scalar(0, 0, 255))); - } - else - { - cv::rectangle(tm, roi, CheckUtil::GetDrawColor(tm, cv::Scalar(0, 255, 255))); - } - - char buffer[128]; - sprintf(buffer, "id:%d type:%d m %0.1f", i,blobs.blobTab[i].ErrType, blobs.blobTab[i].density); - std::string text = buffer; - cv::Point origin = cv::Point(roi.x, roi.y); - cv::putText(tm, text, origin, font_face, font_scale, CheckUtil::GetDrawColor(tm, cv::Scalar(0, 255, 0)), thickness, 1, 0); - - // printf("type: %d %d %d %d %d %d %d\n", blobs.blobTab[i].ErrType, blobs.blobTab[i].area, blobs.blobTab[i].energy, roi.x, roi.y, roi.width, roi.height); - } - cv::imwrite(m_CheckResult_shareP->in_shareImage->strChannel + "_image_resize_blob.png", tm); - } - - long te = CheckUtil::getcurTime(); - m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, strBaseLog, "image Blob Analysis End use time %ld blob %ld ms", te - t1, t2 - t1); - - // getchar(); - return 0; -} - -int ImgCheckAnalysisy::AIMaskDet() -{ - - // 计算 mask 图片的一行 是否有 残点,用以加速 Blob的计算。 - if (m_ImgBlobHFlagData) - { - delete[] m_ImgBlobHFlagData; - m_ImgBlobHFlagData = NULL; - } - m_ImgBlobHFlagData = new unsigned char[m_pImageAllResult->detImg.rows]; - memset(m_ImgBlobHFlagData, 0, sizeof(unsigned char) * m_pImageAllResult->detImg.rows); - - while (true) - { - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - std::shared_ptr AImaskResult; - { - std::lock_guard lock(mtx_AIMaskImgBLobQueue); - if (m_AIMaskImgBLobQueue.size() > 0) - { - AImaskResult = std::move(m_AIMaskImgBLobQueue.front()); - m_AIMaskImgBLobQueue.pop(); - // printf("size ============== %ld\n", m_AIMaskImgBLobQueue.size()); - } - } - - // 有AI mask 的结果 - if (AImaskResult) - { - // static int ss = 0; - cv::Mat &outimg = *(AImaskResult->output); - // cv::imwrite(std::to_string(ss++) + ".png", outimg); - - unsigned char *pGrayErrordata = (unsigned char *)outimg.data; - int width = outimg.cols; - int height = outimg.rows; - int start_Y = AImaskResult->roi.y; - if (cv::countNonZero(outimg != 0) != 0) - { - // #pragma omp parallel for - for (int y = 0; y < height; y++) - { - if (m_ImgBlobHFlagData[y + start_Y] == 0) - { - unsigned char *p = pGrayErrordata + y * width; - for (int x = 0; x < width; x++) - { - if (p[x] != 0) - { - m_ImgBlobHFlagData[y + start_Y] = 1; - break; - } - } - } - } - } - } - else - { - /* code */ - - if (m_AItask->isComplate()) - { - break; - } - } - } - // printf("===========================================1 \n"); - m_AItask->waitComplate(); - int rec = m_AItask->nresult; - if (rec != CHECK_OK) - { - return rec; - } - // printf("===========================================2 \n"); - return 0; -} - -int ImgCheckAnalysisy::Contours() -{ - // std::string strBaseLog = "Contours"; - // // m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, strBaseLog, "Contours Start"); - // cv::Mat detimg; - // { - // // 膨胀 - // cv::Mat se = getStructuringElement(0, Size(5, 5)); // 构造矩形结构元素 - // cv::dilate(m_pdetlog->temImgList[TEM_IMG_IDX_AImask], detimg, se); - // } - - // std::vector> contours; - // std::vector hierarchy; - // cv::findContours(detimg, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE); // 只找最外层轮廓 - - // // cv::cvtColor(m_pdetlog->temImgList[TEM_IMG_IDX_SrcCrop], m_pdetlog->temImgList[TEM_IMG_IDX_DrawSrc], cv::COLOR_GRAY2BGR); - - // for (int i = 0; i < contours.size(); ++i) - // { // 绘制所有轮廓 - // cv::drawContours(m_pdetlog->temImgList[TEM_IMG_IDX_DrawSrc], contours, i, cv::Scalar(255, 0, 255)); // thickness为-1时为填充整个轮廓 - // } - - // float fx = m_pdetlog->temImgList[TEM_IMG_IDX_Result].cols * 1.0f / m_pdetlog->temImgList[TEM_IMG_IDX_SrcCrop].cols; - // float fy = m_pdetlog->temImgList[TEM_IMG_IDX_Result].rows * 1.0f / m_pdetlog->temImgList[TEM_IMG_IDX_SrcCrop].rows; - // for (int i = 0; i < contours.size(); ++i) - // { - // for (int j = 0; j < contours.at(i).size(); ++j) - // { - // contours.at(i).at(j).x *= fx; - // contours.at(i).at(j).y *= fy; - // } - // } - // // cv::cvtColor(m_pdetlog->temImgList[TEM_IMG_IDX_Result], m_pdetlog->temImgList[TEM_IMG_IDX_Result], cv::COLOR_GRAY2BGR); - - // for (int i = 0; i < contours.size(); ++i) - // { // 绘制所有轮廓 - // cv::drawContours(m_pdetlog->temImgList[TEM_IMG_IDX_Result], contours, i, cv::Scalar(255, 0, 255)); // thickness为-1时为填充整个轮廓 - // } - // cv::imwrite("TEM_IMG_IDX_Result.png", m_pdetlog->temImgList[TEM_IMG_IDX_Result]); - // cv::imwrite("deeeee.png", m_pdetlog->temImgList[TEM_IMG_IDX_Drawmask]); - // getchar(); - // m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, strBaseLog, "Contours End"); - return 0; -} - -int ImgCheckAnalysisy::CheckImgInit() -{ - // 1、初始化 - - m_pDetResult->pQx_ErrorList = std::make_shared>(); - m_pdetlog->Init(); - m_pdetlog->addLogLevel = DET_LOG_LEVEL_3; - memset(&blobs, 0, sizeof(ERROR_DOTS_BLOBS)); - m_nCheckResultErrorCode = 0; - - m_Draw_qxImageResult.erase(m_Draw_qxImageResult.begin(), m_Draw_qxImageResult.end()); - - m_DetRoiList.Init(); - - return 0; -} - -int ImgCheckAnalysisy::ConfigCheck(cv::Mat img) -{ - - m_pdetlog->AddCheckstr(PrintLevel_0, "start", "Config Check"); - int re = CHECK_OK; - if (img.empty()) - { - m_nErrorCode = CHECK_ERROR_CheckImg_Empty; - m_pdetlog->AddCheckstr(PrintLevel_1, "Error", "check Img empty"); - return m_nErrorCode; - } - - // if (m_pCommonAnalysisyConfig->regionConfigArr.size() <= 0) - // { - // m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Error", "regionConfig Num = 0"); - // m_nErrorCode = CHECK_ERROR_Config_Value; - // return m_nErrorCode; - // } - - m_pdetlog->AddCheckstr(PrintLevel_0, "Succ", "Config Check Succ"); - return re; -} - -int ImgCheckAnalysisy::AI_Detect_Thread(const cv::Mat &img, cv::Mat &ResultImg) -{ - std::string cur_time_s = CheckUtil::getCurTimeHMS(); - - std::shared_ptr pAIDet; - // printf("=====>>>>AI_Detect_Thread m_strCurDetChannel %s \n", m_strCurDetChannel.c_str()); - bool bUseDarkModel = false; - std::string strCameraName; - { - // printf("=====>>>>AI_Detect_Thread USE CA %s \n", m_strCurDetChannel.c_str()); - // 暗场图像(相机名含 "dark",与 Define_Base.h ALIGN_REUSE_* 的明/暗场判定一致)用暗场检测模型(05DefectDark.engine); - // 其余(明场)用明场检测模型(01DefectLight.engine);暗场模型未加载成功时回退明场模型 - std::string strProduct; - GetProductCameraStr(strProduct, strCameraName); - bUseDarkModel = !ReplaceDarkWithBright(strCameraName).empty() && AI_Factory->Defect_Dark && AI_Factory->Defect_Dark->m_bInitSuccess; - pAIDet = bUseDarkModel ? AI_Factory->Defect_Dark : AI_Factory->Defect; - } - - - std::string strBaseLog = "AI_Detect"; - m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, strBaseLog, "AI_Detect Start"); - m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, strBaseLog, "Detect Model %s (camera %s)", - bUseDarkModel ? "DefectDark" : "DefectLight", strCameraName.c_str()); - long t1, t2, t3; - t1 = CheckUtil::getcurTime(); - - int deal_image_width = pAIDet->input_0.width; - int deal_image_height = pAIDet->input_0.height; - if (img.empty() || deal_image_width <= 0 || deal_image_height <= 0) - { - m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, strBaseLog, " img empty %d model input size error [w %d h %d]", img.empty(), deal_image_width, deal_image_height); - m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, strBaseLog, "AI_Detect End"); - return 1; - } - - // 不再拆分小图:整图 resize 到模型输入尺寸推理,输出 mask 再 resize 回原图尺寸 - cv::Mat inputImg; - cv::resize(img, inputImg, cv::Size(deal_image_width, deal_image_height)); - // 模型输入要求单通道;原图为彩色时按 BGR 顺序转灰度 - if (inputImg.channels() == 3) - { - cv::cvtColor(inputImg, inputImg, cv::COLOR_BGR2GRAY); - } - else if (inputImg.channels() == 4) - { - cv::cvtColor(inputImg, inputImg, cv::COLOR_BGRA2GRAY); - } - - // 临时存图:模型输入 - if (DetImgInfo_shareP->bsaveProcessImg) - { - cv::imwrite(DetImgInfo_shareP->strCameraName + "_AI_Det_in.png", inputImg); - } - - // 提交整图推理任务并等待结果 - std::shared_ptr task = std::make_shared(); - task->id = 0; - task->roi = cv::Rect(0, 0, img.cols, img.rows); - task->input = inputImg; - task->output = std::make_shared(); - task->engine = pAIDet; - - t2 = CheckUtil::getcurTime(); - runner->SubmitTask(task); - - std::shared_ptr result; - while (!runner->PopResult(result)) - { - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - t3 = CheckUtil::getcurTime(); - - cv::Mat &outimg = *(result->output); // 模型输出尺寸的 mask - - // 输出 mask resize 回原图尺寸:mask 为缺陷二值图,用最近邻插值(避免插值晕圈被 Blob 当作缺陷多检出) - ResultImg = cv::Mat::zeros(img.size(), CV_8UC1); - if (!outimg.empty()) - { - cv::resize(outimg, ResultImg, img.size(), 0, 0, cv::INTER_NEAREST); - } - else - { - m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, strBaseLog, " model output mask is empty, result mask all zero"); - } - - m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, strBaseLog, " AI Run Time: sum %ld pre %ld Run %ld model in [w %d h %d] mask [w %d h %d] -> [w %d h %d]", - t3 - t1, t2 - t1, t3 - t2, deal_image_width, deal_image_height, outimg.cols, outimg.rows, img.cols, img.rows); - - // 供 AIMaskDet 标记 mask 有缺陷的行(加速 Blob 计算):整图坐标 + 整图尺寸 - { - std::shared_ptr maskTask = std::make_shared(); - maskTask->id = 0; - maskTask->roi = cv::Rect(0, 0, img.cols, img.rows); - maskTask->input = result->input; - maskTask->output = std::make_shared(ResultImg); - maskTask->engine = pAIDet; - std::lock_guard lock(mtx_AIMaskImgBLobQueue); - m_AIMaskImgBLobQueue.push(maskTask); - } - - // 留存 AI 输入/输出(模型尺寸),供缺陷结果按中心点取 AI 图(GetAIDetImg) - { - std::shared_ptr temAIresult = std::make_shared(); - temAIresult->roi = cv::Rect(0, 0, img.cols, img.rows); - temAIresult->AI_inImg = result->input; - temAIresult->AI_mask = outimg; - m_pImageAllResult->AI_Qx_MaskList.push_back(temAIresult); - } - - m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, strBaseLog, "AI_Detect End"); - - if (m_pbaseCheckFunction->saveImg.bSaveAIDetImg && !ResultImg.empty()) - { - std::string strSaveDir = "/home/aidlux/BOE/AI_Detect/" + DetImgInfo_shareP->strImgProductID + "/"; - CheckUtil::CreateDir(strSaveDir); - std::string strSavePath_in = strSaveDir + cur_time_s + "_" + strCameraName + "_in.png"; - std::string strSavePath_out = strSaveDir + cur_time_s + "_" + strCameraName + "_out.png"; - CheckUtil::SaveImgAsync(strSavePath_in, m_CheckResult_shareP->cutSrcimg); - CheckUtil::SaveImgAsync(strSavePath_out, ResultImg); - } - - // 临时存图 - if (DetImgInfo_shareP->bsaveProcessImg) - { - cv::imwrite(DetImgInfo_shareP->strCameraName + "_AI_mask.png", ResultImg); - } - - // getchar(); - return 0; -} - -int ImgCheckAnalysisy::AI_QX_Class_Thread() -{ - - m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "QX_Class", " Start"); - -#if !AI_CLASS_MODEL_ENABLE - // 当前暂无AI分类模型(Class.engine):所有缺陷先统一按 铜箔标记(TBMark) 类型返回; - // 原AI分类流程代码先预留(见下方),待分类模型部署后置 AI_CLASS_MODEL_ENABLE 为 1 即可恢复 - { - int defaultqx = AIClassTypeToConfigType(AI_CLass_QX_NAME_TBMark); - for (int i = 0; i < blobs.blobCount; i++) - { - blobs.blobTab[i].AIclasstype = defaultqx; - } - m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "QX_Class", " no class model, all %d qx -> TBMark(%d)", blobs.blobCount, defaultqx); - } - return 0; -#endif // !AI_CLASS_MODEL_ENABLE - - // ==================== 以下为AI分类模型流程(模型未部署期间代码预留,勿删) ==================== - std::shared_ptr pAIDet; - // printf("=====>>>>AI_QX_Class_Thread m_strCurDetChannel %s \n", m_strCurDetChannel.c_str()); - { - // printf("=====>>>>AI_QX_Class_Thread USE CA %s \n", m_strCurDetChannel.c_str()); - pAIDet = AI_Factory->Class; - } - - int deal_image_width = pAIDet->input_0.width; - int deal_image_height = pAIDet->input_0.height; - - const int totalTasks = blobs.blobCount; - int submitted = 0; - int completed = 0; - int detblobIdx = 0; - int clsnum = 0; - // std::string str_Root = m_strRootPath + pDetConfig->strProductID + "_" + pDetConfig->strchannel + "_"; - long t2 = CheckUtil::getcurTime(); - while (completed < totalTasks) - { - // std::this_thread::sleep_for(std::chrono::milliseconds(1)); - if (submitted < totalTasks) - { - ERROR_DOTS_BLOB_DATA *pblob = &blobs.blobTab[submitted]; - if (pblob->ErrType == ERR_TYPE_2) - { - // ERR_TYPE_2不参与分类,按默认AI类别(把手标记)处理 - pblob->AIclasstype = CONFIG_QX_NAME_cell_CGMark; - submitted++; - completed++; - continue; - } - else - { - // 如果任务还没提交完,且当前处理任务数 < 2,提交新任务 - if (runner->GetProcessingCount() < 10) - { - clsnum++; - cv::Rect roi; - roi.x = pblob->minx; - roi.y = pblob->miny; - roi.width = pblob->maxx - pblob->minx + 1; - roi.height = pblob->maxy - pblob->miny + 1; - - std::shared_ptr task = std::make_shared(); - task->id = submitted; - int re = GetClassImg(m_pImageAllResult->detImg, task->input, roi, deal_image_width, deal_image_height); - if (re == 0) - { - task->bclass = true; - task->engine = pAIDet; - runner->SubmitTask(task); - } - else - { - completed++; - } - - submitted++; - } - } - } - - // 尝试取结果 - std::shared_ptr result; - if (runner->PopResult(result)) - { - - int temClass = result->cls_label; - - // 分类 label 映射:优先读映射配置文件(BM_cls.txt),读不到用默认顺序 - int cls_num = GetClsTypeByLabel(temClass); - - // std::string strclassName = AI_CLass_QX_NAME_Names[cls_num]; - // printf("AI Class num %d = %s %f\n", cls_num, strclassName.c_str(), result->cls_score); - - // 分类存图 开启。 - if (m_pbaseCheckFunction->saveImg.bSaveAlginImg) - { - - std::string saveimgpaht = ""; - - if (m_strCurDetChannel == "TA") - { - saveimgpaht = m_strRootPath_TA_cls + std::to_string(cls_num) + "/" + std::to_string(CheckUtil::getcurTime()) + "_" + std::to_string(result->cls_score) + ".png"; - } - else - { - saveimgpaht = m_strRootPath_CA_cls + std::to_string(cls_num) + "/" + std::to_string(CheckUtil::getcurTime()) + "_" + std::to_string(result->cls_score) + ".png"; - } - - { - m_pImageStorage->addImage(saveimgpaht, result->input); - } - } - - int configqx = AIClassTypeToConfigType(cls_num); - - ERROR_DOTS_BLOB_DATA *pblob = &blobs.blobTab[result->id]; - pblob->AIclasstype = configqx; - - completed++; - } - else - { - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - } - // printf("=====>>>>AI_QX_Class_Thread clsnum %d \n", clsnum); - return 0; -} - -int ImgCheckAnalysisy::ThreadTask(int nId) -{ - - while (!m_bExit) - { - std::this_thread::sleep_for(std::chrono::milliseconds(20)); - // 等待是否有任务 - std::shared_ptr task = m_task.GetTask(); - - // 把任务发送给对应的任务处理函数进行处理 - switch (task->taskname) - { - case Task_AI: - TaskFun_AIDet(task); - break; - case Task_Class: - TaskFun_QxClass(task); - break; - default: - break; - } - } - return 0; -} - -int ImgCheckAnalysisy::ResizeImg() -{ - - cv::Size sz; - sz.width = RESIZE_IMAGE_WIDTH; - - float fw = RESIZE_IMAGE_WIDTH * 1.0f / m_pImageAllResult->detImg.cols; - sz.height = int(m_pImageAllResult->detImg.rows * fw); - - // 结果图画布取自原图裁剪区(平移映射),保持原图通道(彩色原图 -> 真实彩色,不再灰度转假彩); - // 异常情况(如未走裁剪流程)直接用检测图兜底 - const cv::Mat &srcImg = m_CheckResult_shareP->in_shareImage->img; - if (m_CheckResult_shareP->in_shareImage->Det_Mode != DET_MODE_ReJson && - !srcImg.empty() && - m_deskewPadRoi.width > 0 && m_deskewPadRoi.height > 0 && - CheckUtil::RoiInImg(m_deskewPadRoi, srcImg)) - { - cv::resize(srcImg(m_deskewPadRoi), m_pImageAllResult->resultImg, sz); - } - else - { - cv::resize(m_pImageAllResult->detImg, m_pImageAllResult->resultImg, sz); - } - m_CheckResult_shareP->resultimg = m_pImageAllResult->resultImg; - m_pdetlog->AddCheckstr(PrintLevel_1, "showImg size", " [w %d,h %d]-> show img [w %d,h %d] ", - m_outer_roi.width, m_outer_roi.height, sz.width, sz.height); - return 0; -} - -void ImgCheckAnalysisy::TaskFun_AIDet(std::shared_ptr task) -{ - task->SetStatus(TaskStep_run); - long t1, t2; - // AI 推理生成 - { - t1 = CheckUtil::getcurTime(); - - int rec = AI_Detect_Thread(m_pImageAllResult->detImg, m_pImageAllResult->AIMaskImg); - - t2 = CheckUtil::getcurTime(); - task->nresult = rec; - } - task->SetStatus(TaskStep_compate); -} - -void ImgCheckAnalysisy::TaskFun_QxClass(std::shared_ptr task) -{ - task->SetStatus(TaskStep_run); - long t1, t2; - // AI 推理生成 - { - t1 = CheckUtil::getcurTime(); - - int rec = AI_QX_Class_Thread(); - - t2 = CheckUtil::getcurTime(); - task->nresult = rec; - } - task->SetStatus(TaskStep_compate); -} - -int ImgCheckAnalysisy::Update_DetRoiList() -{ - float fx = m_pImageAllResult->fscale_detToresult_x; - float fy = m_pImageAllResult->fscale_detToresult_y; - - // 参数多边形 -> 检测图(裁剪坐标系) 的映射在裁剪时已算好(m_matParam2Det) - for (const auto ®ion : m_pCommonAnalysisyConfig->regionConfigArr) - { - m_DetRoiList.Update_Mat(region.basicInfo.pointArry, m_matParam2Det, fx, fy); - } - - m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "info", "roiList_Src roi num %ld", m_DetRoiList.roiList_Src.size()); - // m_DetRoiList.print("m_DetRoiList"); - return 0; -} - -int ImgCheckAnalysisy::DrawResult_Step_1() -{ - - // std::cout << m_pBasicConfig->bDrawPreRoi << std::endl; - if (m_pBasicConfig->bDrawPreRoi) - { - int n = 0; - // std::cout << m_DetRoiList.roiList_Show.size() << std::endl; - for (const auto &polygon : m_DetRoiList.roiList_Show) - { - // n++; - // if (n == 1) - // { - // continue; - // } - - // std::cout << polygon << std::endl; - - // 绘制多边形的边界(不填充),线宽为1;颜色按画布通道自适应 - cv::polylines(m_CheckResult_shareP->resultimg, polygon, true, - CheckUtil::GetDrawColor(m_CheckResult_shareP->resultimg, cv::Scalar(128, 128, 255)), 1); // true表示闭合多边形 - } - // cv::imwrite(DetImgInfo_shareP->strChannel + "roi_src.png", m_pdetlog->temImgList[TEM_IMG_IDX_DrawSrc]); - // cv::imwrite(DetImgInfo_shareP->strChannel + "roi_ss.png", m_pdetlog->temImgList[TEM_IMG_IDX_Result]); - - // getchar(); - } - - float fs_resize_x = m_pImageAllResult->resultImg.cols * 1.0f / m_pImageAllResult->detImg.cols; - float fs_resize_y = m_pImageAllResult->resultImg.rows * 1.0f / m_pImageAllResult->detImg.rows; - - - // 注:不再绘制 inner 定位蓝色框(m_inner_rroi 为大轮廓裁剪子图局部坐标,直接映射会缺裁剪原点偏移) - - if (m_pbaseCheckFunction && m_pbaseCheckFunction->markLine.region.size() > 1) - { - std::vector product_roi_show; - for (const auto &pt : m_pbaseCheckFunction->markLine.region) - { - // 参数(模板)坐标 -> 检测图(裁剪) -> 结果图 - cv::Point2f detPt = MatAffinePoint(m_matParam2Det, cv::Point2f((float)pt.x, (float)pt.y)); - cv::Point draw_pt; - draw_pt.x = cvRound(detPt.x * fs_resize_x); - draw_pt.y = cvRound(detPt.y * fs_resize_y); - product_roi_show.push_back(draw_pt); - } - - cv::polylines( - m_CheckResult_shareP->resultimg, - product_roi_show, - true, - CheckUtil::GetDrawColor(m_CheckResult_shareP->resultimg, cv::Scalar(0, 255, 0)), - 1); - } - if (m_pEdge_Align_Result.get() != NULL) - { - - if (m_pbaseCheckFunction->markLine.bDraw) - { - for (size_t i = 0; i < m_pEdge_Align_Result->markresulList.size(); i++) - { - // if (m_pEdge_Align_Result->markresulList[i].status == Mark_Result_Status_OK) - { - const cv::Point &markPt = m_pEdge_Align_Result->markresulList[i].det_Local_DetImg; - cv::Point2f detPt = MatAffinePoint(m_matSrc2Det, cv::Point2f((float)markPt.x, (float)markPt.y)); - cv::Point p; - p.x = cvRound(detPt.x * fs_resize_x); - p.y = cvRound(detPt.y * fs_resize_y); - cv::circle(m_CheckResult_shareP->resultimg, p, 5, - CheckUtil::GetDrawColor(m_CheckResult_shareP->resultimg, cv::Scalar(0, 255, 0))); - } - } - } - } - - // 支架偏移检测绘制(受参数 bDrawSupport 控制,默认关闭): - // 支架中心十字(绿=正常/红=偏移NG)+ 各方向"所计算的产品边缘"线段(抗凸起拟合直边,见 CheckRun 支架块) - // + 支架中心到该边缘的连线(连线长度=支架中心到所计算边缘的距离;绿=正常/红=超标准)+ 实测/标准(mm)文字 - if (m_SupportDrawInfo.bValid && m_pbaseCheckFunction->supportDet.bDrawSupport && !m_CheckResult_shareP->resultimg.empty()) - { - // 原图坐标 -> 检测图(裁剪) -> 结果图 - auto srcToResult = [&](const cv::Point2f &pt) -> cv::Point { - cv::Point2f detPt = MatAffinePoint(m_matSrc2Det, pt); - return cv::Point(cvRound(detPt.x * fs_resize_x), cvRound(detPt.y * fs_resize_y)); - }; - - // 支架中心十字:NG 红色 / 正常绿色 - cv::Point pCenter = srcToResult(m_SupportDrawInfo.ptCenter); - cv::Scalar colorCross = m_SupportDrawInfo.bOffset ? cv::Scalar(0, 0, 255) : cv::Scalar(0, 255, 0); - colorCross = CheckUtil::GetDrawColor(m_CheckResult_shareP->resultimg, colorCross); - const int nCrossLen = 40; // 结果图上十字半长(px) - cv::line(m_CheckResult_shareP->resultimg, cv::Point(pCenter.x - nCrossLen, pCenter.y), cv::Point(pCenter.x + nCrossLen, pCenter.y), colorCross, 2); - cv::line(m_CheckResult_shareP->resultimg, cv::Point(pCenter.x, pCenter.y - nCrossLen), cv::Point(pCenter.x, pCenter.y + nCrossLen), colorCross, 2); - - // 各方向:所计算边缘线段(抗凸起拟合直边/回退外接矩形边) + 中心到边缘点的连线(长度=点到所计算边缘) + 实测/标准文字 - // 几何(边缘点/线段两端点)由 CheckRun 支架块算好存入 edgeDraw,此处只做坐标映射与绘制 - char strTxt[64]; - for (size_t i = 0; i < m_SupportDrawInfo.edgeList.size(); i++) - { - const Support_Draw_Info_::Edge_Draw_ &edgeDraw = m_SupportDrawInfo.edgeList[i]; - - cv::Point pEdge = srcToResult(edgeDraw.ptEdge); - cv::Point pEdgeA = srcToResult(edgeDraw.ptEdgeA); - cv::Point pEdgeB = srcToResult(edgeDraw.ptEdgeB); - - // 超标准红色 / 正常绿色 - cv::Scalar colorEdge = edgeDraw.bExceed ? cv::Scalar(0, 0, 255) : cv::Scalar(0, 255, 0); - colorEdge = CheckUtil::GetDrawColor(m_CheckResult_shareP->resultimg, colorEdge); - - // 1、所计算边缘线段:标出参与计算的产品边缘(细线;抗凸起拟合直边/回退边) - cv::line(m_CheckResult_shareP->resultimg, pEdgeA, pEdgeB, colorEdge, 1); - - // 2、中心 -> 边缘点连线:长度即支架中心到所计算边缘的距离 - cv::line(m_CheckResult_shareP->resultimg, pCenter, pEdge, colorEdge, 2); - - // 3、距离文字:实测/标准(mm) - snprintf(strTxt, sizeof(strTxt), "%s %.0f/%.0f", edgeDraw.strEdge.c_str(), edgeDraw.fDist, edgeDraw.fStandard); - cv::putText(m_CheckResult_shareP->resultimg, strTxt, - cv::Point((pCenter.x + pEdge.x) / 2 + 6, (pCenter.y + pEdge.y) / 2 - 6), - cv::FONT_HERSHEY_SIMPLEX, 0.8, colorEdge, 2); - } - } - - // cv::imwrite("sss.png", m_CheckResult_shareP->resultimg); - return 0; -} - -int ImgCheckAnalysisy::AIClassTypeToConfigType(int nAIQXType) -{ - - // 只有主检测+分类的缺陷在映射表中;支架偏移/支架缺失/把手缺失(传统检出)、铜箔标签(标签检测)不走AI分类 - int resultError_type = CONFIG_QX_NAME_cell_CGMark; - switch (nAIQXType) - { - case AI_CLass_QX_NAME_CGMark: - resultError_type = CONFIG_QX_NAME_cell_CGMark; - break; - case AI_CLass_QX_NAME_BQBMark: - resultError_type = CONFIG_QX_NAME_cell_BQBMark; - break; - case AI_CLass_QX_NAME_TBMark: - resultError_type = CONFIG_QX_NAME_cell_TBMark; - break; - case AI_CLass_QX_NAME_count: // 无效类别(分类映射配置中不存在):不参与缺陷分析 - resultError_type = CONFIG_QX_NAME_count; - break; - default: - break; - } - return resultError_type; -} - -int ImgCheckAnalysisy::UpdateImgageScale() -{ - - if (m_pBasicConfig->fImage_Scale_x > 0 && m_pBasicConfig->fImage_Scale_x < 1 && - m_pBasicConfig->fImage_Scale_y > 0 && m_pBasicConfig->fImage_Scale_y < 1) - { - - m_fImgage_Scale_X = m_pBasicConfig->fImage_Scale_x; - m_fImgage_Scale_Y = m_pBasicConfig->fImage_Scale_y; - } - - return 0; -} - - -int ImgCheckAnalysisy::BLobToDetResult() -{ - long t1 = CheckUtil::getcurTime(); - m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "BLobToDetResult", " Start old qx num %ld", m_pDetResult->pQx_ErrorList->size()); - - // 遍历每个检测blob - for (int i = 0; i < blobs.blobCount; i++) - { - cv::Rect roi; - roi.x = blobs.blobTab[i].minx; - roi.y = blobs.blobTab[i].miny; - roi.width = blobs.blobTab[i].maxx - blobs.blobTab[i].minx + 1; - roi.height = blobs.blobTab[i].maxy - blobs.blobTab[i].miny + 1; - - int config_qx_type = blobs.blobTab[i].AIclasstype; - if (config_qx_type < 0 || config_qx_type >= CONFIG_QX_NAME_count) - { - // AI 分类无对应缺陷类型(无效类别)时,不参与缺陷结果分析 - m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "BLobToDetResult", - "blob idx %d invalid AIclasstype %d skip", i, config_qx_type); - continue; - } - - float JudgArea = blobs.blobTab[i].JudgArea; - float fsecondArea = JudgArea; - std::string qx_name = CONFIG_QX_NAME_Names[config_qx_type]; - - if (true) - { - QX_ERROR_INFO_ temerror; - temerror.roi = roi; - temerror.Idx = m_pDetResult->pQx_ErrorList->size(); - temerror.area = blobs.blobTab[i].area; - temerror.JudgArea = JudgArea; - temerror.JudgArea_second = fsecondArea; - temerror.energy = blobs.blobTab[i].energy; - temerror.flen = blobs.blobTab[i].len; - temerror.fbreadth = blobs.blobTab[i].breadth; - temerror.nconfig_qx_type = config_qx_type; - temerror.qx_name = qx_name; - temerror.maxValue = blobs.blobTab[i].maxValue; - temerror.grayDis = blobs.blobTab[i].grayDis; - temerror.density = blobs.blobTab[i].density; - temerror.fUpIou = 0; - temerror.whiteOrBlack = blobs.blobTab[i].whiteOrblack; - - { - 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 &polygon = m_DetRoiList.roiList_Src[iregion]; - double result = cv::pointPolygonTest(polygon, pCenter, false); - if (result < 0) - { - continue; - } - nmaxregionIdx = iregion; - } - - temerror.detRegionidxList.push_back(nmaxregionIdx); - } - - // { - // cv::Point pCenter; - // pCenter.x = roi.x + roi.width * 0.5; - // pCenter.y = roi.y + roi.height * 0.5; - // for (int iregion = 0; iregion < m_DetRoiList.roiList_Src.size(); iregion++) - // { - // const std::vector &polygon = m_DetRoiList.roiList_Src[iregion]; - // double result = cv::pointPolygonTest(polygon, pCenter, false); - // if (result < 0) - // { - // continue; - // } - // temerror.detRegionidxList.push_back(iregion); - // } - // if (temerror.detRegionidxList.size() <= 0) - // { - // temerror.detRegionidxList.push_back(0); - // } - // } - - m_pDetResult->pQx_ErrorList->push_back(temerror); - - // printf("- %s idx %d a %f v %d h %f l %f\n", DetImgInfo_shareP->strChannel.c_str(), i, JudgArea, blobs.blobTab[i].maxValue, blobs.blobTab[i].grayDis, flen); - } - } - long t2 = CheckUtil::getcurTime(); - m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "BLobToDetResult", " end qx num %ld use time %ld", m_pDetResult->pQx_ErrorList->size(), t2 - t1); - return 0; -} - -int ImgCheckAnalysisy::AI_Edge(const cv::Mat &img, cv::RotatedRect &outerRoi, cv::RotatedRect &innerRoi, vector &tagroiList) -{ - m_pdetlog->AddCheckstr(PrintLevel_0, "AI_Edge", "-------------------start--------------"); - - AI_Edge_Algin::DetConfig config; - - if (DetImgInfo_shareP->bsaveProcessImg) - { - config.bSaveResultImg = true; - } - config.strChannel = DetImgInfo_shareP->strChannel; - config.pBaseCheckFunction = m_pbaseCheckFunction; - config.bDetTag = m_pbaseCheckFunction->supportDet.bDetTag; // 标签检测开关:关闭时不跑 Tag_Loc - config.strProductID = DetImgInfo_shareP->strImgProductID; // 定位存图文件名首位使用产品ID - - /* 定位获取三个roi */ - int re = m_pAI_Edge_Algin.Detect(img, &config, m_pEdge_Align_Result); - if (re != 0) - { - printf("AI_Edge Is Error = %d \n", re); - } - if (!m_pEdge_Align_Result->bigroi.empty()) - { - outerRoi = m_pEdge_Align_Result->bigroi[0]; - } - if (!m_pEdge_Align_Result->smallroi.empty()) - { - innerRoi = m_pEdge_Align_Result->smallroi[0]; - } - // 无条件赋值:Tag_Loc 未检出或 bDetTag 关闭(tagroi 为空)时清空,避免残留上一张图的标签 - tagroiList = m_pEdge_Align_Result->tagroi; - - /* 用 outer 定位 mask(bigmask) 分离凸起区域,检测把手是否缺失 */ - { - long handle_s = CheckUtil::getcurTime(); - CheckHandleByBigMask(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; -} - -// 用 outer 定位 mask(bigmask) 检测把手(产品主体是圆角矩形、把手基本出现在四角附近): -// 1、分离凸起区域:拟合主体上/下/左/右四条边缘,把边缘外的掩膜按连通域切分成凸起区域 -// (提取细节见 CheckUtil::GetMaskProtrusions,含就近角/质心/面积/深度/填充率) -// 2、参数里每个非空的"把手区域"附近都应该有一个把手:在区域附近找凸起, -// 凸起宽高与区域外接矩形宽高相近(尺寸比区间见 handle_size_min/handle_size_max,可 JSON 配置)才算匹配 -// 3、凸起本身不算 NG,只有"某个把手区域附近找不到匹配的凸起"才算把手缺失(NG 由 CheckRun 上报) -// 4、-s(存过程图)时输出 {通道}_handle_det.jpg:绿色四边形=拟合主体、红框=凸起(带编号/角/深度)、 -// 蓝框=参数把手区域、绿框=匹配到的把手,便于从图像观察问题 -// 5、拟合出的主体四边形存 m_HandleBodyQuad:支架偏移检测复用其四边(抗凸起)做测距与绘制 -int ImgCheckAnalysisy::CheckHandleByBigMask(const cv::Mat &bigmask) -{ - m_bHandleDetSucc = false; - m_bHandleFound = false; - m_bHandleExpect = false; - m_HandleProtrusionList.clear(); - m_HandleRegionResultList.clear(); - m_HandleBodyQuad.clear(); - - if (bigmask.empty()) - { - m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Handle", "bigmask is empty, skip handle detect"); - return 1; - } - - // 1、从 bigmask 分离凸起区域 - // 最小深度 = 系数 × 量化步长:mask 由模型输出 resize 回原图,直边带量化台阶毛刺(台阶高≈模型 1 像素=量化步长); - // 系数优先取参数配置(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); - } - } - // m_HandleBodyQuad:拟合出的主体四边形(左上/右上/右下/左下);-s 存图观察用,支架偏移检测复用其四边做抗凸起测距/绘制 - int re = CheckUtil::GetMaskProtrusions(bigmask, m_HandleProtrusionList, nMinDepth, nMinArea, &m_HandleBodyQuad, nEdgeMargin); - if (re != 0) - { - m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Handle", "GetMaskProtrusions error %d", re); - return re; - } - m_bHandleDetSucc = true; - - // 2、把手检测开关 + 把手区域(最多4个,非空区域表示该处应该有一个把手) - bool bDetHandle = false; - std::vector regionBoxList; - if (m_pbaseCheckFunction != NULL) - { - const Function_Support_Det &supportDet = m_pbaseCheckFunction->supportDet; - bDetHandle = supportDet.bDetHandle; - for (size_t i = 0; i < supportDet.handleBoxes.size() && i < HANDLE_REGION_MAX_NUM; i++) - { - regionBoxList.push_back(supportDet.handleBoxes[i]); - } - } - 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_HandleProtrusionList.size(), regionBoxList.size(), BOOL_TO_STR(bDetHandle), BOOL_TO_STR(m_bHandleExpect)); - - // 3、凸起区域实测特征(就近角/面积/深度/填充率,用于日志排查) - for (size_t i = 0; i < m_HandleProtrusionList.size(); i++) - { - const Mask_Protrusion_ &protr = m_HandleProtrusionList[i]; - m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Handle", - "protrusion %ld corner %d roi [%d %d %d %d] len %.0f depth %.0f area %0.0f fill %0.2f", - i, protr.nCorner, protr.roi.x, protr.roi.y, protr.roi.width, protr.roi.height, - protr.fLen, protr.fDepth, protr.fArea, protr.fFillRatio); - } - - // 4、逐个把手区域:在区域附近找宽高匹配的凸起(同一个凸起只匹配给一个区域) - std::vector usedList(m_HandleProtrusionList.size(), false); - int nFound = 0; - for (size_t ir = 0; ir < regionBoxList.size(); ir++) - { - Handle_Region_Result_ result; - result.nIdx = (int)ir + 1; - result.boxRegion = regionBoxList[ir]; - int nMatchIdx = MatchHandleRegion(m_HandleProtrusionList, usedList, result.boxRegion, result.roiHandle); - result.bFound = (nMatchIdx >= 0); - if (result.bFound) - { - usedList[nMatchIdx] = true; - nFound++; - } - m_HandleRegionResultList.push_back(result); - - std::string strResult = "handle missing !!"; - if (result.bFound) - { - strResult = "handle roi [" + std::to_string(result.roiHandle.x) + " " + std::to_string(result.roiHandle.y) + " " + - std::to_string(result.roiHandle.width) + " " + std::to_string(result.roiHandle.height) + "]"; - } - m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Handle", "handle region %d box [%d %d %d %d] -> %s", - result.nIdx, result.boxRegion.x, result.boxRegion.y, result.boxRegion.width, result.boxRegion.height, - strResult.c_str()); - } - m_bHandleFound = (!regionBoxList.empty() && nFound == (int)regionBoxList.size()); - - 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 !!"); - - // 5、存过程图(-s 时):用于从图像观察把手提取/匹配情况; - // 绿色四边形=拟合出的主体四条边缘;红框=分离出的凸起区域(带编号/就近角/深度); - // 蓝框=参数把手区域;绿框=匹配到的把手 - if (DetImgInfo_shareP->bsaveProcessImg) - { - cv::Mat show; - if (bigmask.channels() == 1) - { - cv::cvtColor(bigmask, show, cv::COLOR_GRAY2BGR); - } - else - { - show = bigmask.clone(); - } - // 主体四边形(拟合出的四条边缘) - if (m_HandleBodyQuad.size() == 4) - { - std::vector vecQuad; - for (size_t i = 0; i < m_HandleBodyQuad.size(); i++) - { - vecQuad.push_back(cv::Point(cvRound(m_HandleBodyQuad[i].x), cvRound(m_HandleBodyQuad[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++) - { - cv::rectangle(show, m_HandleRegionResultList[i].boxRegion, cv::Scalar(255, 0, 0), 8); - if (m_HandleRegionResultList[i].bFound) - { - cv::rectangle(show, m_HandleRegionResultList[i].roiHandle, cv::Scalar(0, 255, 0), 12); - } - } - cv::Mat showSmall; - cv::resize(show, showSmall, cv::Size(show.cols / 4, show.rows / 4)); - std::vector paramJpg = {cv::IMWRITE_JPEG_QUALITY, 90}; - cv::imwrite(DetImgInfo_shareP->strChannel + "_handle_det.jpg", showSmall, paramJpg); - } - - return 0; -} - -// 标签检测(独立步骤):用 (图像 + 定位结果) 跑 Tag_Loc 模型 -// 裁剪区域与定位流程里 Tag_Loc 的口径完全一致(定位得到的产品外接旋转矩形): -// 明场相机用自己的定位结果;暗场相机没做定位模型,用复用明场的结果 —— 保证暗/明场对同一摆位切出同样区域。 -// 注意:标签模型对裁剪尺度很敏感(裁剪差 3px 就会让检出个数变化),不能另用 bigmask 重推裁剪框。 -// bigmask 仅在定位外接矩形异常时作兜底(最大轮廓外接框)。 -// 检出的标签 roi 存 m_tag_roiList(裁剪图坐标系,与定位流程 tagroi 的约定一致),由 CheckRun 统一上报 NG。 -int ImgCheckAnalysisy::CheckTagByBigMask(const cv::Mat &img, const cv::Mat &bigmask) -{ - m_tag_roiList.clear(); - if (!AI_Factory || !AI_Factory->Tag_Loc) - { - m_pdetlog->AddCheckstr(PrintLevel_0, "Error", "Tag_Loc model is null"); - return 1; - } - if (img.empty()) - { - m_pdetlog->AddCheckstr(PrintLevel_0, "Error", "tag check img is empty"); - return 2; - } - - // 1、裁剪区域:与定位流程 Tag_Loc 的裁剪口径一致(产品外接旋转矩形,暗场为复用明场的值) - cv::Rect roiCrop = m_outer_rroi.boundingRect() & cv::Rect(0, 0, img.cols, img.rows); - if (roiCrop.width <= 0 || roiCrop.height <= 0) - { - // 异常兜底:定位外接矩形无效时,用 bigmask 最大轮廓外接框 - if (bigmask.empty()) - { - m_pdetlog->AddCheckstr(PrintLevel_0, "Error", "tag check roi invalid and bigmask is empty"); - return 3; - } - cv::Mat binMask; - { - cv::Mat grayMask; - if (bigmask.channels() != 1) - { - cv::cvtColor(bigmask, grayMask, cv::COLOR_BGR2GRAY); - } - else - { - grayMask = bigmask; - } - double dMin = 0; - double dMax = 0; - cv::minMaxLoc(grayMask, &dMin, &dMax); - // 兼容 0/1 与 0/255 掩膜:取半强度作阈值(重采样后的边缘取到真实边界) - cv::threshold(grayMask, binMask, dMax > 1 ? dMax * 0.5 : 0, 255, cv::THRESH_BINARY); - } - bool bFound = false; - std::vector contour = CheckUtil::getLargestContourROI(binMask, bFound); - if (!bFound || contour.size() < 3) - { - m_pdetlog->AddCheckstr(PrintLevel_0, "Error", "tag check bigmask contour too few"); - return 4; - } - roiCrop = cv::minAreaRect(contour).boundingRect() & cv::Rect(0, 0, img.cols, img.rows); - if (roiCrop.width <= 0 || roiCrop.height <= 0) - { - m_pdetlog->AddCheckstr(PrintLevel_0, "Error", "tag check crop roi error [%d %d %d %d]", roiCrop.x, roiCrop.y, roiCrop.width, roiCrop.height); - return 5; - } - m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Tag", "tag check use bigmask roi [%d %d %d %d]", - roiCrop.x, roiCrop.y, roiCrop.width, roiCrop.height); - } - - // 2、裁剪 -> resize 到 Tag_Loc 模型输入尺寸 -> 灰度 - std::shared_ptr pTag = AI_Factory->Tag_Loc; - cv::Size sz(pTag->input_0.width, pTag->input_0.height); - if (sz.width <= 0 || sz.height <= 0) - { - m_pdetlog->AddCheckstr(PrintLevel_0, "Error", "tag check invalid model input size"); - return 6; - } - cv::Mat detImg; - cv::resize(img(roiCrop), detImg, sz); - if (detImg.channels() == 3) - { - cv::cvtColor(detImg, detImg, cv::COLOR_BGR2GRAY); - } - else if (detImg.channels() == 4) - { - cv::cvtColor(detImg, detImg, cv::COLOR_BGRA2GRAY); - } - - // 3、推理 - cv::Mat mask; - int re = pTag->AIDet(detImg, mask); - if (re != 0) - { - m_pdetlog->AddCheckstr(PrintLevel_0, "Error", "Tag_Loc AIDet error %d", re); - return 100 + re; - } - if (mask.empty()) - { - m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Tag", "tag mask empty, no tag"); - return 0; - } - - if (m_pbaseCheckFunction->saveImg.bSaveBQDetImg) - { - std::string cur_time_s = CheckUtil::getCurTimeHMS(); - std::string strSaveDir = "/home/aidlux/BOE/AI_Detect/" + DetImgInfo_shareP->strImgProductID + "/"; - CheckUtil::CreateDir(strSaveDir); - std::string strSavePath_out = strSaveDir + cur_time_s + "_" + DetImgInfo_shareP->strCameraName + "_BQ_out.png"; - CheckUtil::SaveImgAsync(strSavePath_out, mask); - } - - // 调试存图(仅 -s 时保存) - if (DetImgInfo_shareP && DetImgInfo_shareP->bsaveProcessImg) - { - cv::imwrite(DetImgInfo_shareP->strChannel + "_tag_in.png", detImg); - cv::imwrite(DetImgInfo_shareP->strChannel + "_tag_out_mask.png", mask); - } - - // 4、mask 轮廓 -> 还原到裁剪图坐标 -> RotatedRect(与定位流程 tagroi 的坐标系一致) - std::vector> contours; - std::vector hierarchy; - cv::findContours(mask.clone(), contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); - const float scale_x = (float)roiCrop.width / (float)sz.width; - const float scale_y = (float)roiCrop.height / (float)sz.height; - for (size_t i = 0; i < contours.size(); i++) - { - if (contours[i].size() < 3 || cv::contourArea(contours[i]) <= 0) - { - continue; - } - std::vector srcContour; - srcContour.reserve(contours[i].size()); - for (const auto &pt : contours[i]) - { - srcContour.emplace_back(pt.x * scale_x, pt.y * scale_y); - } - m_tag_roiList.emplace_back(cv::minAreaRect(srcContour)); - } - // 面积从大到小排序(与定位流程 tagroi 的顺序一致) - std::sort(m_tag_roiList.begin(), m_tag_roiList.end(), - [](const cv::RotatedRect &a, const cv::RotatedRect &b) - { return a.size.width * a.size.height > b.size.width * b.size.height; }); - - m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Tag", "tag region num %ld crop [%d %d %d %d] model [%d %d]", - m_tag_roiList.size(), roiCrop.x, roiCrop.y, roiCrop.width, roiCrop.height, sz.width, sz.height); - return 0; -} - -// 在某个把手区域附近找宽高匹配的凸起 -// 匹配条件:1、凸起中心落在把手区域附近(区域宽/高各外扩 fNearRatio 倍) -// 2、凸起宽高与该区域外接矩形宽高的比值都在 [fSizeMin, fSizeMax](允许横竖互换) -// 范围参数优先取参数配置(JSON handle_param 下的 handle_search_ratio/handle_size_min/handle_size_max), -// 未配置时用文件顶部宏的默认值 -// 返回凸起在 candList 中的下标,-1 表示没找到 -int ImgCheckAnalysisy::MatchHandleRegion(const std::vector &candList, const std::vector &usedList, - const cv::Rect &boxRegion, cv::Rect &roiHandle) -{ - if (boxRegion.width <= 0 || boxRegion.height <= 0) - { - return -1; - } - - // 匹配范围参数:JSON 配置优先,未配置时用默认值 - float fNearRatio = 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::Point pRegionCenter(boxRegion.x + boxRegion.width / 2, boxRegion.y + boxRegion.height / 2); - - int nBestIdx = -1; - double fBestDis = 1e18; - int nNearIdx = -1; // 区域内离区域中心最近的凸起(仅用于日志排查) - double fNearDis = 1e18; - for (size_t i = 0; i < candList.size(); i++) - { - if (i < usedList.size() && usedList[i]) - { - continue; - } - const Mask_Protrusion_ &cand = candList[i]; - const cv::Rect &roi = cand.roi; - if (roi.width <= 0 || roi.height <= 0) - { - continue; - } - - // 位置:凸起中心要落在把手区域附近 - cv::Point pCenter(roi.x + roi.width / 2, roi.y + roi.height / 2); - if (!searchBox.contains(pCenter)) - { - continue; - } - double fDis = CheckUtil::calDis(pCenter, pRegionCenter); - if (fDis < fNearDis) - { - fNearDis = fDis; - nNearIdx = (int)i; - } - - // 宽高:与把手区域外接矩形的宽高相近(允许横竖互换) - float fw = (float)roi.width; - float fh = (float)roi.height; - bool bSizeOK = (fw >= boxRegion.width * fSizeMin && fw <= boxRegion.width * fSizeMax && - fh >= boxRegion.height * fSizeMin && fh <= boxRegion.height * fSizeMax); - if (!bSizeOK) - { - bSizeOK = (fw >= boxRegion.height * fSizeMin && fw <= boxRegion.height * fSizeMax && - fh >= boxRegion.width * fSizeMin && fh <= boxRegion.width * fSizeMax); - } - if (!bSizeOK) - { - continue; - } - - if (fDis < fBestDis) - { - fBestDis = fDis; - nBestIdx = (int)i; - } - } - - if (nBestIdx < 0 && nNearIdx >= 0) - { - // 没匹配上时打印区域内最近的凸起,便于排查阈值 - const cv::Rect &roi = candList[nNearIdx].roi; - m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Handle", - "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, - candList[nNearIdx].nCorner, roi.x, roi.y, roi.width, roi.height, - (float)roi.width / (float)boxRegion.width, (float)roi.height / (float)boxRegion.height); - } - - if (nBestIdx >= 0) - { - roiHandle = candList[nBestIdx].roi; - } - return nBestIdx; -} - -int ImgCheckAnalysisy::CalProductSize() -{ - float fw = m_outer_roi.width * m_fImgage_Scale_X; - float fh = m_outer_roi.height * m_fImgage_Scale_Y; - fw = std::ceil(fw * 10) / 10; - fh = std::ceil(fh * 10) / 10; - m_CheckResult_shareP->productWidht_mm = fw; - m_CheckResult_shareP->productHeight_mm = fh; - - m_pdetlog->AddCheckstr(PrintLevel_1, "Det ROI", " [w %d,h %d] (piexl) -> [w %f h %f](mm),scale %f %f", - m_outer_roi.width, m_outer_roi.height, fw, fh, m_fImgage_Scale_X, m_fImgage_Scale_Y); - return 0; -} - -int ImgCheckAnalysisy::ImgPreDet() -{ - - // 计算产品尺寸 - CalProductSize(); - - if (m_pbaseCheckFunction->saveImg.bSaveClsImg) - { - creatsavedir(); - } - - // 裁剪图(_RE_CutImg)保留原图通道:源图为彩色时用彩色裁剪图,否则回退灰度检测图 - m_CheckResult_shareP->cutSrcimg = m_pImageAllResult->detColorImg.empty() - ? m_pImageAllResult->detImg - : m_pImageAllResult->detColorImg; - ResizeImg(); - - m_pImageAllResult->fscale_detToresult_x = m_pImageAllResult->resultImg.cols * 1.0f / m_pImageAllResult->detImg.cols; - m_pImageAllResult->fscale_detToresult_y = m_pImageAllResult->resultImg.rows * 1.0f / m_pImageAllResult->detImg.rows; - return 0; -} +/* + * @Author: your name + * @Date: 2022-04-20 15:50:00 + * @LastEditTime: 2025-09-23 11:34:53 + * @LastEditors: xiewenji 527774126@qq.com + * @Description: 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE + * @FilePath: /ZCXD_MonitorPlatform/src/CoreLogicModule/src/CamDeal.cpp + */ +#include "ImgCheckAnalysisy.hpp" +#include "CheckUtil.hpp" +#include "Define.h" +#include +#include "AICommonDefine.h" +#include "AlignShare.h" +#include +#include +#include +#include +#include + +// AI缺陷分类模型开关:当前暂无分类模型(Class.engine),分类流程代码先预留; +// 待分类模型部署后,将 AI_CLASS_MODEL_ENABLE 置 1,即可恢复 AI_QX_Class_Thread 中的AI分类流程 +#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_MAX_RATIO 1.2f +// "附近"的搜索范围(JSON: handle_search_ratio):把手区域外接矩形在宽/高方向各外扩该比例 +// 调大:位置约束变松,产品摆放偏移大也不易漏(但可能匹配到别的凸起); +// 调小:位置约束更严,偏移稍大就会判"附近没有把手" +#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) + +// 支架边缘原图精细化默认参数(JSON: Support_Detect.form.support_param 下的 +// edge_refine_search / edge_refine_step / edge_refine_gray): +// 仅对 edge_select 选中的边缘生效:沿边缘按步长采样,从边缘向产品内侧逐像素找第一个灰度 >= 阈值的像素作为新边缘点, +// 有效点足够时重新拟合边缘线(找不到的采样点没有新信息,不参与拟合,相当于沿用原边缘点)。 +// edge_refine_search:最多向内搜索的像素数。搜不到 → 该采样点无贡献;调大更灵敏但可能搜到内部亮区 +#define EDGE_REFINE_SEARCH_DEFAULT 80 +// edge_refine_step:沿边采样步长(像素)。不需要每个点都找,调大可省时间;调小边缘拟合更稳 +#define EDGE_REFINE_STEP_DEFAULT 40 +// edge_refine_gray:产品边缘灰度阈值(0~255)。产品区域比背景亮,扫描时灰度 >= 该值视为已经进入产品: +// 调低:更灵敏(背景较亮/产品边缘偏暗时能搜到),但可能把暗部纹理/脏污当成边缘,边缘偏小; +// 调高:更严格(避开大部分杂散亮块),但产品边缘不够亮时搜不到(该采样点不参与拟合) +#define EDGE_REFINE_GRAY_THRESHOLD 240 + +// ===== 支架方向精细化默认参数 ===== +// (JSON: Support_Detect.form.support_param 下的 +// support_refine_search / support_refine_step / support_refine_segments,均可选) +// 背景:支架由 Align_Inner 模型(smallmask,1696×800)检出,上采样回原图后边缘量化误差约 4px, +// 且支架区域接近方形时 minAreaRect 的"长轴"方向不稳定,用它求旋转角误差大。 +// 改为用模型支架边缘做初值,在原图(detImg 灰度)上拟合支架长边(朝产品内侧的边)求方向角。 +// support_refine_search:取 outer 定位后的 ROI 原图(detImg)最外侧多少列作为边缘搜索带(未配置默认 50 列)。 +// 注意:搜索带位置只按 outer ROi 原图取(右边缘取最右 N 列、左边缘取最左 N 列),不再按 smallmask 的边界取: +// 模型 smallmask 上采样回原图后边界不准,按它取带会把真实支架边缘切到带外(边缘点被 mask 掉)。 +// 调大:覆盖更大的范围,但可能扫到支架内部结构;调小:更严格,但 mask 偏差大时仍可能漏掉边缘 +#define SUPPORT_REFINE_STRIP_DEFAULT 50 +// support_refine_step:沿支架长边采样的步长(像素)。调大可省时间;调小拟合更稳 +#define SUPPORT_REFINE_STEP_DEFAULT 10 +// support_refine_segments:把采样点沿边分成 n 段,每段稳健拟合一条直线,取各段方向角的中位数。 +// 支架边缘有凸起时:凸起处采样点成为所在段的离群点(MAD 剔除);凸起外边缘与支架边缘平行, +// 即使拟合到凸起外边缘也不影响方向角 +#define SUPPORT_REFINE_SEGMENTS_DEFAULT 6 +// 搜索列带内相邻像素(3 行灰度均值)的灰度差小于该值视为无边缘(极性无关,支架比外侧亮/暗均可) +#define SUPPORT_REFINE_MIN_CONTRAST 15.0 + +// 分类映射配置文件(每行:label索引:缺陷名称;冒号后名称为该缺陷类型名称,行的顺序即缺陷类型索引) +static const std::string CLS_LABEL_CONFIG_FILE = "/home/aidlux/BOE/UseModel_ZB/BM_cls.txt"; + +// 去掉字符串首尾的空白字符和引号 +static std::string ClsTrimStr(const std::string &str) +{ + size_t begin = str.find_first_not_of(" \t\r\n\""); + if (begin == std::string::npos) + { + return ""; + } + size_t end = str.find_last_not_of(" \t\r\n\""); + return str.substr(begin, end - begin + 1); +} + +// 组装程序内默认缺陷名称列表(AI_CLass_QX_NAME_Names) +static std::vector BuildDefaultAIQXNames() +{ + std::vector vecNames; + for (int i = 0; i < AI_CLass_QX_NAME_count; i++) + { + vecNames.push_back(AI_CLass_QX_NAME_Names[i]); + } + return vecNames; +} + +// 分类映射配置:缺陷名称列表(索引=行序) + 分类label->缺陷名称 映射 +struct ClsLabelConfig +{ + std::vector vecNames; // 缺陷名称列表 + std::map mapLabelName; // 分类 label -> 缺陷名称 +}; + +// 读取分类映射配置(每行:label索引:缺陷名称;冒号后名称按行序构成缺陷名称列表;# 后内容忽略) +static ClsLabelConfig ReadClsLabelConfig(const std::string &strFilePath) +{ + ClsLabelConfig config; + std::ifstream ifs(strFilePath.c_str()); + if (!ifs.is_open()) + { + printf("Open cls label config failed, use default mapping: %s\n", strFilePath.c_str()); + return config; + } + + std::string strLine; + while (std::getline(ifs, strLine)) + { + // # 后内容(含注释)忽略 + size_t nPosComment = strLine.find('#'); + if (nPosComment != std::string::npos) + { + strLine = strLine.substr(0, nPosComment); + } + size_t nPos = strLine.find(':'); + if (nPos == std::string::npos) + { + // 无冒号:非标准行,跳过并提示(仅支持 label索引:缺陷名称 格式) + std::string strTrim = ClsTrimStr(strLine); + if (!strTrim.empty()) + { + printf("Cls label config skip invalid line: %s\n", strTrim.c_str()); + } + continue; + } + // label索引:缺陷名称(冒号后名称按行序构成缺陷名称列表) + std::string strKey = ClsTrimStr(strLine.substr(0, nPos)); + std::string strValue = ClsTrimStr(strLine.substr(nPos + 1)); + if (!strKey.empty() && !strValue.empty()) + { + config.mapLabelName[atoi(strKey.c_str())] = strValue; + config.vecNames.push_back(strValue); // 行序即缺陷类型索引 + } + } + return config; +} + +// 获取分类映射配置(只读一次) +static const ClsLabelConfig &GetClsLabelConfig() +{ + static const ClsLabelConfig config = ReadClsLabelConfig(CLS_LABEL_CONFIG_FILE); + return config; +} + +// 获取缺陷名称列表:优先取配置文件的名称列表,读不到用程序内默认列表 +static const std::vector &GetAIQXNameList() +{ + static const std::vector vecDefaultNames = BuildDefaultAIQXNames(); + const ClsLabelConfig &config = GetClsLabelConfig(); + if (!config.vecNames.empty()) + { + return config.vecNames; + } + return vecDefaultNames; +} + +// 缺陷名称 -> AI_CLass 枚举(找不到返回 count 表示无效) +static int ClsNameToAItype(const std::string &strName) +{ + const std::vector &vecNames = GetAIQXNameList(); + for (size_t i = 0; i < vecNames.size(); i++) + { + if (strName == vecNames[i]) + { + return (int)i; + } + } + return AI_CLass_QX_NAME_count; +} + +// 解析 label->缺陷名称 映射为 label->AI_CLass枚举(只读一次;无效名称仅提示一次) +static std::map BuildLabelTypeMap() +{ + const ClsLabelConfig &config = GetClsLabelConfig(); + std::map mapLabelType; + for (std::map::const_iterator it = config.mapLabelName.begin(); it != config.mapLabelName.end(); ++it) + { + int nType = ClsNameToAItype(it->second); + if (nType < AI_CLass_QX_NAME_count) + { + mapLabelType[it->first] = nType; + } + else + { + printf("Cls label config unknown name: %s\n", it->second.c_str()); + } + } + return mapLabelType; +} + +// 获取分类 label 对应的 AI 缺陷类型:优先配置文件(只读一次),读不到用默认顺序 +static int GetClsTypeByLabel(int nLabel) +{ + static const std::map mapLabelType = BuildLabelTypeMap(); + std::map::const_iterator it = mapLabelType.find(nLabel); + if (it != mapLabelType.end()) + { + return it->second; + } + + // 映射文件未读到、或该 label 未在文件中配置时,按程序内默认顺序兜底(避免文件缺失/不全导致异常) + int cls_num = AI_CLass_QX_NAME_count; + switch (nLabel) + { + case 0: + cls_num = AI_CLass_QX_NAME_CGMark; + break; + case 1: + cls_num = AI_CLass_QX_NAME_BQBMark; + break; + case 2: + cls_num = AI_CLass_QX_NAME_TBMark; + break; + default: + cls_num = AI_CLass_QX_NAME_CGMark; + break; + } + return cls_num; +} + +// 用于排序轮廓的比较函数 +static bool compareContourAreas(const vector &contour1, const vector &contour2) +{ + double i = contourArea(contour1); + double j = contourArea(contour2); + return (i > j); +} + +// 2x3 仿射矩阵小工具:变换点、矩形外接、组合矩阵(检测图裁剪坐标映射用) +static cv::Point2f MatAffinePoint(const cv::Mat &mat32, const cv::Point2f &pt) +{ + if (mat32.empty() || mat32.rows != 2 || mat32.cols != 3) + { + return pt; + } + const double *m = mat32.ptr(0); + return cv::Point2f((float)(m[0] * pt.x + m[1] * pt.y + m[2]), + (float)(m[3] * pt.x + m[4] * pt.y + m[5])); +} + +// 矩形四角经仿射变换后取外接矩形 +static cv::Rect MatAffineRect(const cv::Mat &mat32, const cv::Rect &roi) +{ + std::vector pts(4); + pts[0] = MatAffinePoint(mat32, cv::Point2f((float)roi.x, (float)roi.y)); + pts[1] = MatAffinePoint(mat32, cv::Point2f((float)(roi.x + roi.width), (float)roi.y)); + pts[2] = MatAffinePoint(mat32, cv::Point2f((float)(roi.x + roi.width), (float)(roi.y + roi.height))); + pts[3] = MatAffinePoint(mat32, cv::Point2f((float)roi.x, (float)(roi.y + roi.height))); + + float fminX = pts[0].x, fmaxX = pts[0].x, fminY = pts[0].y, fmaxY = pts[0].y; + for (int i = 1; i < 4; i++) + { + if (pts[i].x < fminX) + fminX = pts[i].x; + if (pts[i].x > fmaxX) + fmaxX = pts[i].x; + if (pts[i].y < fminY) + fminY = pts[i].y; + if (pts[i].y > fmaxY) + fmaxY = pts[i].y; + } + int x0 = cvFloor(fminX); + int y0 = cvFloor(fminY); + int x1 = cvCeil(fmaxX); + int y1 = cvCeil(fmaxY); + return cv::Rect(x0, y0, x1 - x0, y1 - y0); +} + +// 组合仿射:先应用 mB 再应用 mA,即 mA ∘ mB,结果仍为 2x3 +static cv::Mat MatAffineCompose(const cv::Mat &mA, const cv::Mat &mB) +{ + if (mA.empty() || mA.rows != 2 || mA.cols != 3) + { + return mB.clone(); + } + if (mB.empty() || mB.rows != 2 || mB.cols != 3) + { + return mA.clone(); + } + cv::Mat hA = cv::Mat::eye(3, 3, CV_64F); + cv::Mat hB = cv::Mat::eye(3, 3, CV_64F); + mA.copyTo(hA(cv::Rect(0, 0, 3, 2))); + mB.copyTo(hB(cv::Rect(0, 0, 3, 2))); + cv::Mat hC = hA * hB; + return hC(cv::Rect(0, 0, 3, 2)).clone(); +} + +// 稳健直线拟合:pts 为 (t, v) 采样点,拟合 v = k*t + b(左右边 t=y,v=x;上下边 t=x,v=y) +// 先最小二乘,再按“残差中位数的绝对偏差(MAD)”剔除离群点(把手凸起/边缘缺口/内部亮区等),重新拟合; +// 返回参与拟合的内点数,<4 表示拟合失败(调用方沿用原边缘) +static int FitLineRobustTV(const std::vector &pts, double &fK, double &fB) +{ + auto fnFit = [](const std::vector &p, double &k, double &b) -> bool { + if (p.size() < 2) + { + return false; + } + double fSumT = 0, fSumV = 0; + for (size_t i = 0; i < p.size(); i++) + { + fSumT += p[i].x; + fSumV += p[i].y; + } + const double fMeanT = fSumT / p.size(); + const double fMeanV = fSumV / p.size(); + double fSxx = 0, fSxy = 0; + for (size_t i = 0; i < p.size(); i++) + { + const double fDt = p[i].x - fMeanT; + fSxx += fDt * fDt; + fSxy += fDt * (p[i].y - fMeanV); + } + if (fSxx < 1e-6) + { + return false; + } + k = fSxy / fSxx; + b = fMeanV - k * fMeanT; + return true; + }; + + if (pts.size() < 4) + { + return 0; + } + double fKFit = 0, fBFit = 0; + if (!fnFit(pts, fKFit, fBFit)) + { + return 0; + } + + std::vector vecInlier; + for (int nIter = 0; nIter < 2; nIter++) + { + std::vector vecRes; + vecRes.reserve(pts.size()); + for (size_t i = 0; i < pts.size(); i++) + { + vecRes.push_back(pts[i].y - (fKFit * pts[i].x + fBFit)); + } + std::vector vecAbs = vecRes; + std::nth_element(vecAbs.begin(), vecAbs.begin() + vecAbs.size() / 2, vecAbs.end()); + const double fMed = vecAbs[vecAbs.size() / 2]; + for (size_t i = 0; i < vecAbs.size(); i++) + { + vecAbs[i] = std::fabs(vecAbs[i] - fMed); + } + std::nth_element(vecAbs.begin(), vecAbs.begin() + vecAbs.size() / 2, vecAbs.end()); + const double fMad = vecAbs[vecAbs.size() / 2]; + const double fThr = std::max(2.0, 3.0 * 1.4826 * fMad); + + vecInlier.clear(); + for (size_t i = 0; i < pts.size(); i++) + { + if (std::fabs(pts[i].y - (fKFit * pts[i].x + fBFit)) <= fThr) + { + vecInlier.push_back(pts[i]); + } + } + if (vecInlier.size() < 4 || !fnFit(vecInlier, fKFit, fBFit)) + { + break; + } + } + if (vecInlier.size() < 4) + { + return 0; + } + fK = fKFit; + fB = fBFit; + return (int)vecInlier.size(); +} + +ImgCheckAnalysisy::ImgCheckAnalysisy() + : m_pAI_Edge_Algin(m_pdetlog) +{ + + m_nErrorCode = CHECK_OK; + m_nThreadIdx = -1; + m_bInitSucc = false; + m_bExit = false; + m_nRun_Status = CHECK_THREAD_STATUS_IDLE; + + m_fImgage_Scale_X = 0.03f; + m_fImgage_Scale_Y = 0.03f; + m_pBasicConfig = NULL; + + m_strCurDetChannel = ""; + m_bupdateconfig = false; + m_pbaseCheckFunction = &m_AnalysisyConfig.baseFunction; + + m_strLastDate = ""; + m_strRootPath_TA_cls = "/home/aidlux/BOE/Cls/TA/"; + m_strRootPath_CA_cls = "/home/aidlux/BOE/Cls/CA/"; + creatsavedir(); + m_pImageStorage = ImageStorage::getInstance(); + m_nConfigIdx = -1; + m_ImgBlobHFlagData = NULL; +} + +ImgCheckAnalysisy::~ImgCheckAnalysisy() +{ + ExitSystem(); + if (m_ImgBlobHFlagData) + { + delete[] m_ImgBlobHFlagData; + m_ImgBlobHFlagData = NULL; + } +} +int ImgCheckAnalysisy::UpdateConfig(void *pconfig, int nConfigType) +{ + int re = 0; + switch (nConfigType) + { + case CHECK_CONFIG_Run: + re = LoadRunConfig(pconfig); + if (re == 0) + { + // printf("---> LoadRunConfig Succ\n"); + } + else + { + printf("---> LoadRunConfig Fail\n"); + } + break; + case CHECK_CONFIG_Module: + re = LoadCheckConfig(pconfig); + if (re == 0) + { + // printf("---> LoadAnalysisConfig Succ\n"); + } + else + { + printf("---> LoadAnalysisConfig Fail\n"); + } + break; + default: + break; + } + return re; +} +int ImgCheckAnalysisy::RunStart(void *pconfig1) +{ + + // 1 、更新参数 并判断参数是否合法 + int re = CHECK_OK; + re = SetNewConfig(); + if (CHECK_OK != re) + { + m_nErrorCode = re; + return m_nErrorCode; + } + // printf("---> RunStart Start m_RunConfig.nThreadIdx %d \n", m_RunConfig.nThreadIdx); + + m_nThreadIdx = m_RunConfig.nThreadIdx; + + re = InitRun(m_RunConfig.nCpu_start_Idx); + if (CHECK_OK != re) + { + m_nErrorCode = re; + return m_nErrorCode; + } + runner = std::make_shared(); + runner->Start(); + + m_nErrorCode = CHECK_OK; + // printf("ImgCheckAnalysisy >>>> ImgCheckThread %d Start Succ \n", m_nThreadIdx); + return m_nErrorCode; +} + +int ImgCheckAnalysisy::SetDataRun_SharePtr(std::shared_ptr p) +{ + // 设置正在检测 + + m_pImageAllResult = p; + DetImgInfo_shareP = p->result->in_shareImage; + m_CheckResult_shareP = p->result; + m_pImageAllResult->setStep(ImageAllResult::DetStep_Deting); + m_pdetlog = p->detlog; + m_pDetResult = p->pDetResult; + StartCheck(); + m_nErrorCode = CHECK_OK; + return m_nErrorCode; +} + +int ImgCheckAnalysisy::GetCheckReuslt(std::shared_ptr &pResult) +{ + + m_CheckResult_shareP.reset(); + DetImgInfo_shareP.reset(); + SetIDLE(); + // m_nErrorCode = CHECK_OK; + // printf("4 DetImgInfo_shareP count %ld m_nCheckResultErrorCode %d \n", DetImgInfo_shareP.use_count(), m_nCheckResultErrorCode); + return m_nCheckResultErrorCode; +} + +int ImgCheckAnalysisy::CheckImg(std::shared_ptr p, std::shared_ptr &pResult) +{ + + return 0; +} + +int ImgCheckAnalysisy::ReJsonResul(std::shared_ptr p, std::shared_ptr &pResult) +{ + + return 0; +} + +int ImgCheckAnalysisy::InitRun(int nId) +{ + + int re = CHECK_OK; + if (m_bInitSucc) + { + return CHECK_OK; + } + InitModel(); + AI_Factory = AIFactory::GetInstance(); + + m_nRun_Status = CHECK_THREAD_STATUS_IDLE; + re = StartThread(nId); + if (CHECK_OK != re) + { + return re; + } + m_bInitSucc = true; + return re; +} + +int ImgCheckAnalysisy::GetStatus() +{ + return m_nRun_Status; +} + +std::string ImgCheckAnalysisy::GetVersion() +{ + return std::string("BOE_1.3.0"); +} + +std::string ImgCheckAnalysisy::GetErrorInfo() +{ + std::string str = GetErrorCodeInfo(m_nErrorCode); + printf("%s\n", str.c_str()); + return str; +} + +int ImgCheckAnalysisy::creatsavedir() +{ + std::string curDate = CheckUtil::getCurrentDate(); + if (curDate == m_strLastDate) + { + return 0; + } + m_strLastDate = curDate; + m_strRootPath_TA_cls += m_strLastDate + "/"; + m_strRootPath_CA_cls += m_strLastDate + "/"; + for (int i = 0; i < AI_CLass_QX_NAME_count; i++) + { + CheckUtil::CreateDir(m_strRootPath_TA_cls + std::to_string(i) + "/"); + CheckUtil::CreateDir(m_strRootPath_CA_cls + std::to_string(i) + "/"); + } + return 0; +} + +void ImgCheckAnalysisy::SetTplInfo(const cv::RotatedRect &outerRect) +{ + m_tplOuterRect = outerRect; +} + +int ImgCheckAnalysisy::LoadRunConfig(void *p) +{ + if (p == NULL) + { + m_nErrorCode = CHECK_ERROR_Config_Null; + return m_nErrorCode; + } + RunInfoST *pconfig = (RunInfoST *)p; + m_RunConfig.copy(*pconfig); + + return CHECK_OK; +} + +int ImgCheckAnalysisy::LoadCheckConfig(void *p) +{ + if (p == NULL) + { + m_nErrorCode = CHECK_ERROR_Config_Null; + return m_nErrorCode; + } + m_pConfig = (ConfigBase *)p; + m_nConfigIdx = m_pConfig->GetConfigIdx(); + m_nErrorCode = CHECK_OK; + return m_nErrorCode; +} + +// 开启检测 +int ImgCheckAnalysisy::StartCheck() +{ + m_nRun_Status = CHECK_THREAD_STATUS_READY; + return 0; +} + +int ImgCheckAnalysisy::SetIDLE() +{ + // 更新参数 + SetNewConfig(); + m_nRun_Status = CHECK_THREAD_STATUS_IDLE; + return 0; +} + +int ImgCheckAnalysisy::StartThread(int nId) +{ + // 开启检测线程 + ptr_thread_Run = std::make_shared(std::bind(&ImgCheckAnalysisy::Run, this, nId)); + if (!m_RunConfig.bRetest) + { + ptr_thread_AI = std::make_shared(std::bind(&ImgCheckAnalysisy::ThreadTask, this, nId + 1)); + } + + return 0; +} + +int ImgCheckAnalysisy::StopThread() +{ + m_bExit = true; + if (ptr_thread_Run != nullptr) + { + if (ptr_thread_Run->joinable()) + { + ptr_thread_Run->join(); + } + } + if (ptr_thread_AI != nullptr) + { + if (ptr_thread_AI->joinable()) + { + ptr_thread_AI->join(); + } + } + return 0; +} + +int ImgCheckAnalysisy::ExitSystem() +{ + StopThread(); + return 0; +} + +int ImgCheckAnalysisy::InitModel() +{ + + // 获取当前gpu号确定的 AI处理线程 + + m_OtherDet_Config.nDeviceId = m_RunConfig.nDeviceId; + + m_pAI_Edge_Algin.Init(&m_OtherDet_Config); + m_pAI_Edge_Algin.InitModel_ALL(); + + return 0; +} + +cv::Scalar ImgCheckAnalysisy::calc_blob_info_withstats(cv::Mat &img, const cv::Mat &mask, cv::Rect &stats, cv::Size k_size, int expand, double threshold) +{ + + // 解包 stats(x, y, w, h, area) + int x = stats.x, y = stats.y, w = stats.width, h = stats.height; + + // 将图像转换为灰度图像 + + // 计算感兴趣区域 (ROI) + cv::Rect roi(x - expand, y - expand, w + 2 * expand, h + 2 * expand); + roi &= cv::Rect(0, 0, img.cols, img.rows); // 确保ROI在图像内 + + cv::Mat cimg = img(roi); + + // cv::cvtColor(cimg, cimg, cv::COLOR_BGR2GRAY); + // return cv::Scalar(0, 0, 0); + + cv::Mat cmask = mask(roi); + // getchar(); + // 扩张掩膜 + + cv::Scalar mean_bk = cv::mean(cimg, ~cmask); + double fbk = mean_bk[0]; + + cv::Scalar mean_det = cv::mean(cimg, cmask); + double fdet = mean_det[0]; + + // 计算差异图像 + cv::Mat diff = cv::abs(cimg - fbk); + + // cv::imwrite("cimg.png",cimg); + // cv::imwrite("cmask.png",cmask); + // printf("%f %f - %f \n",fbk,fdet,fbk-fdet); + // getchar(); + + // diff = diff.mul(cmask > 0); + cv::Mat masked_image; + diff.copyTo(masked_image, cmask); + // 计算能量 + double energy = cv::sum(masked_image)[0]; + + // 计算 hj(差异图像大于0的像素均值) + // double hj = std::abs(fbk - fdet); + // double hj = CheckUtil::CalHj(cimg, cmask, mean_bk.val[0]); + double hj = CheckUtil::CalHjWeighted(cimg, cmask, mean_bk.val[0], 2.0f); + + int worb = 0; + if (fdet >= fbk) + { + worb = 1; + } + // cv::imwrite("cimg.png", cimg); + // cv::imwrite("cmask.png", cmask); + // cv::imwrite("diff.png", diff); + // cv::imwrite("masked_image.png", masked_image); + // printf("fbk %f fdet %f energy %f\n", fbk, fdet, energy); + // getchar(); + + return cv::Scalar(worb, energy, hj); +} + +int ImgCheckAnalysisy::Adapt_Config(RotatedRect tplOuterRect, RotatedRect curOuterRect, Mat& det_img){ + vector cur_vertices = CheckUtil::sort_vertices(curOuterRect); + vector tpl_vertices = CheckUtil::sort_vertices(tplOuterRect); + + cv::Point2f src_pts[3] = { + cur_vertices[0], // 对应左上 + cur_vertices[1], // 对应右上 + cur_vertices[2] // 对应左下 + }; + cv::Point2f dst_pts[3] = { + tpl_vertices[0], // 左上 + tpl_vertices[1], // 右上 + tpl_vertices[2] // 左下 + }; + cv::Mat affine_mat = cv::getAffineTransform(src_pts, dst_pts); + + // 仿射变换 + cv::warpAffine(det_img, det_img, affine_mat, det_img.size()); + + // 将m_outer_rroi和m_inner_rroi变换到模板图像坐标系下 + auto transformRotatedRect = [&affine_mat](cv::RotatedRect &rrect) + { + const double *M = affine_mat.ptr(0); + + cv::Point2f pts[4]; + rrect.points(pts); + + std::vector transformed_pts(4); + for (int i = 0; i < 4; i++) + { + float x = static_cast(M[0] * pts[i].x + M[1] * pts[i].y + M[2]); + float y = static_cast(M[3] * pts[i].x + M[4] * pts[i].y + M[5]); + transformed_pts[i] = cv::Point2f(x, y); + } + rrect = cv::minAreaRect(transformed_pts); + }; + + transformRotatedRect(m_outer_rroi); + transformRotatedRect(m_inner_rroi); + + return 0; +} + +// 暗场复用明场定位:把字符串里的 "dark"(大小写不敏感)替换为 "bright";未命中返回空串 +static std::string ReplaceDarkWithBright(const std::string &strIn) +{ + const std::string strKey = ALIGN_REUSE_FOLLOWER_KEY; // "dark" + const std::string strSrc = ALIGN_REUSE_SOURCE_KEY; // "bright" + if (strIn.size() < strKey.size()) + { + return ""; + } + for (size_t i = 0; i + strKey.size() <= strIn.size(); i++) + { + size_t k = 0; + for (; k < strKey.size(); k++) + { + if (::tolower((unsigned char)strIn[i + k]) != (unsigned char)strKey[k]) + { + break; + } + } + if (k == strKey.size()) + { + std::string strOut = strIn; + strOut.replace(i, strKey.size(), strSrc); + return strOut; + } + } + return ""; +} + +// ===== 暗场复用明场定位结果(ALIGN_REUSE_* 配置) ===== +// 取当前图的产品ID/相机名:优先用 ImageAllResult 关联的基础结果,兕底用送图信息 +void ImgCheckAnalysisy::GetProductCameraStr(std::string &strProduct, std::string &strCamera) +{ + strProduct = ""; + strCamera = ""; + if (m_pImageAllResult) + { + if (m_pImageAllResult->productBaseResult) + { + strProduct = m_pImageAllResult->productBaseResult->strproductName; + } + if (m_pImageAllResult->cameraBaseResult) + { + strCamera = m_pImageAllResult->cameraBaseResult->strCameraName; + } + } + if (m_CheckResult_shareP && m_CheckResult_shareP->in_shareImage) + { + if (strProduct.empty()) + { + strProduct = m_CheckResult_shareP->in_shareImage->strImgProductID; + } + if (strCamera.empty()) + { + strCamera = m_CheckResult_shareP->in_shareImage->strCameraName; + } + } +} + +// 发布本图定位结果(成功)或失败标记(供同产品其它相机复用) +void ImgCheckAnalysisy::PublishAlignShare(bool bSucc) +{ + std::string strProduct; + std::string strCamera; + GetProductCameraStr(strProduct, strCamera); + if (strProduct.empty() || strCamera.empty()) + { + return; + } + if (bSucc) + { + if (!m_pEdge_Align_Result) + { + return; + } + cv::Size sz(0, 0); + if (m_CheckResult_shareP && m_CheckResult_shareP->in_shareImage) + { + sz = m_CheckResult_shareP->in_shareImage->img.size(); + } + AlignShare::GetInstance().PublishSuccess(strProduct, strCamera, m_strCurDetChannel, m_pEdge_Align_Result, sz); + } + else + { + AlignShare::GetInstance().PublishFailed(strProduct, strCamera, m_strCurDetChannel); + } +} + +// 暗场相机不做定位模型:等待并复用明场相机的定位结果 +// 返回 true 时:m_pEdge_Align_Result / m_outer_rroi / m_inner_rroi 已用明场结果填充(标签检测独立执行) +// 返回 false:非复用相机,或复用失败(超时/源失败/无源图/尺寸不一致),调用方走自有定位 +bool ImgCheckAnalysisy::TryReuseAlignFromSource(std::string &strSrcCamera, std::string &strSrcChannel, int &nWaitMs) +{ + nWaitMs = -1; +#if !ALIGN_REUSE_ENABLE + return false; +#else + std::string strProduct; + std::string strCamera; + GetProductCameraStr(strProduct, strCamera); + + // 是否复用由相机名决定(明/暗场是相机属性):相机名必须能按 dark->bright 替换 + std::string strSrcCam = ReplaceDarkWithBright(strCamera); + if (strSrcCam.empty()) + { + return false; + } + // 通道名可能不带 dark/bright(同一面的明/暗场相机共用一个通道名),此时沿用本相机通道名; + // 若带 dark 则同样替换,保证与源相机发布结果时的 key 一致 + std::string strSrcChn = ReplaceDarkWithBright(m_strCurDetChannel); + if (strSrcChn.empty()) + { + strSrcChn = m_strCurDetChannel; + } + strSrcCamera = strSrcCam; + strSrcChannel = strSrcChn; + + if (!m_pImageAllResult || !m_pImageAllResult->productBaseResult || !m_pImageAllResult->productBaseResult->alignPushState) + { + m_pdetlog->AddCheckstr(PrintLevel_0, "Align reuse", "reuse from %s|%s failed(no push state), do align by self", strSrcCam.c_str(), strSrcChn.c_str()); + return false; + } + + m_pdetlog->AddCheckstr(PrintLevel_0, "Align reuse", "wait %s|%s (timeout %d ms)", strSrcCam.c_str(), strSrcChn.c_str(), ALIGN_REUSE_WAIT_TIMEOUT_MS); + + AlignShare &alignShare = AlignShare::GetInstance(); + std::shared_ptr pSrc; + cv::Size srcSize; + const long t1 = CheckUtil::getcurTime(); + bool bGot = false; + bool bSucc = false; + std::string strFailReason = "timeout"; + while (true) + { + if (alignShare.TryGet(strProduct, strSrcCam, strSrcChn, bSucc, pSrc, srcSize)) + { + bGot = true; + if (!bSucc) + { + strFailReason = "source align failed"; + } + break; + } + // 源图不会再来了(送图已结束,且源相机没有图):立即回退,不干等超时 + if (!m_pImageAllResult->productBaseResult->alignPushState->MayStillCome(strSrcCam)) + { + strFailReason = "no source image"; + break; + } + if (CheckUtil::getcurTime() - t1 > ALIGN_REUSE_WAIT_TIMEOUT_MS) + { + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(ALIGN_REUSE_POLL_MS)); + } + nWaitMs = (int)(CheckUtil::getcurTime() - t1); + + if (!bGot || !bSucc) + { + m_pdetlog->AddCheckstr(PrintLevel_0, "Align reuse", "reuse from %s|%s failed(wait %d ms, %s), do align by self", + strSrcCam.c_str(), strSrcChn.c_str(), nWaitMs, strFailReason.c_str()); + return false; + } + if (!pSrc || pSrc->bigroi.empty()) + { + m_pdetlog->AddCheckstr(PrintLevel_0, "Align reuse", "reuse from %s|%s failed(wait %d ms, src roi empty), do align by self", + strSrcCam.c_str(), strSrcChn.c_str(), nWaitMs); + return false; + } + // 图像尺寸必须与源图一致,否则源坐标系不可用 + if (srcSize.width > 0 && srcSize.height > 0 && m_CheckResult_shareP && m_CheckResult_shareP->in_shareImage) + { + const cv::Mat &img = m_CheckResult_shareP->in_shareImage->img; + if (img.cols != srcSize.width || img.rows != srcSize.height) + { + m_pdetlog->AddCheckstr(PrintLevel_0, "Align reuse", "reuse from %s|%s failed, size %dx%d != cur %dx%d, do align by self", + strSrcCam.c_str(), strSrcChn.c_str(), srcSize.width, srcSize.height, img.cols, img.rows); + return false; + } + } + + // 复用明场定位结果:ROI/掩码/映射都用明场的(标签检测由 CheckTagByBigMask 独立执行,不用明场的标签) + m_pEdge_Align_Result = pSrc; + m_outer_rroi = pSrc->bigroi[0]; + if (!pSrc->smallroi.empty()) + { + m_inner_rroi = pSrc->smallroi[0]; + } + return true; +#endif +} + +int ImgCheckAnalysisy::CheckRun() +{ + + // printf(">>>%s ================start \n", m_pImageAllResult->strBaseInfo.c_str()); + + long t1, t2, t3, t4, t5, t6, t7; + SetNewConfig(); + t1 = CheckUtil::getcurTime(); + + CheckImgInit(); + if (DetImgInfo_shareP->bsaveProcessImg) + { + m_pdetlog->bPrintStr = true; + } + // m_pdetlog->bPrintStr = true; + m_fImgage_Scale_X = m_pBasicConfig->fImage_Scale_x; + m_fImgage_Scale_Y = m_pBasicConfig->fImage_Scale_y; + + m_pdetlog->AddCheckstr(PrintLevel_0, "1、basic Info", "---------------------------1、basic Info---------------------------------"); + m_pdetlog->AddCheckstr(PrintLevel_0, "Version", "%s", GetVersion().c_str()); + m_pdetlog->AddCheckstr(PrintLevel_0, "Updateconfig", "%d", m_bupdateconfig); + m_pdetlog->AddCheckstr(PrintLevel_0, "Start", "%s", m_pImageAllResult->strBaseInfo.c_str()); + m_pdetlog->AddCheckstr(PrintLevel_0, "ImgageScale", "Scale_X = %f Scale_Y = %f", m_fImgage_Scale_X, m_fImgage_Scale_Y); + m_bupdateconfig = false; + // 返回结果状态初始化 + m_CheckResult_shareP->checkStatus = 1; + m_CheckResult_shareP->nresult = -1; + + m_CheckResult_shareP->basicResult.img_id = m_CheckResult_shareP->in_shareImage->img_id; + m_CheckResult_shareP->basicResult.imgtype = m_CheckResult_shareP->in_shareImage->imgtype; + m_CheckResult_shareP->basicResult.imgstr = m_CheckResult_shareP->in_shareImage->imgstr; + m_CheckResult_shareP->basicResult.strChannel = m_CheckResult_shareP->in_shareImage->strChannel; + m_strCurDetChannel = m_CheckResult_shareP->basicResult.strChannel; + + /*参数检查*/ + int rec = ConfigCheck(DetImgInfo_shareP->img); + if (rec != CHECK_OK) + { + m_pdetlog->AddCheckstr(PrintLevel_0, "Error", "ConfigCheck is error type = %d", rec); + PublishAlignShare(false); // 本图不会产出定位结果,通知可能的等待方(暗场复用) + m_nErrorCode = rec; + m_nCheckResultErrorCode = m_nErrorCode; + return m_nErrorCode; + } + long time_edge_s = CheckUtil::getcurTime(); + + m_pFuntion = GetChannelFuntion(m_strCurDetChannel); + if (m_pFuntion != NULL) + { + m_pdetlog->AddCheckstr(PrintLevel_0, "Detect function", "%s", + m_pFuntion->GetInfo("").c_str()); + } + else + { + // 通道层允许为空(仅打印,不再按错误处理) + m_pdetlog->AddCheckstr(PrintLevel_0, "Detect function", "no channel config, use base function only"); + } + + /*AI 边缘定位(内外边缘)*/ + // 暗场相机不做定位模型,等待复用明场相机的定位结果(见 ALIGN_REUSE_* 配置); + // 非复用相机、或复用失败(源超时/源失败/无源图/尺寸不一致)时,仍走原有定位流程 + int reedge = 0; + int nAlignReuseWaitMs = -1; + std::string strAlignSrcCamera; + std::string strAlignSrcChannel; + long time_edge_e; + if (TryReuseAlignFromSource(strAlignSrcCamera, strAlignSrcChannel, nAlignReuseWaitMs)) + { + // 复用成功:m_pEdge_Align_Result / m_outer_rroi / m_inner_rroi 已用明场结果填充(标签检测独立执行,不复用明场标签) + PublishAlignShare(true); + time_edge_e = CheckUtil::getcurTime(); + m_pdetlog->AddCheckstr(PrintLevel_0, "2、pre detect", + "-------------------AI_Edge reuse from %s|%s--wait %d ms----succ---time %ld----\n", + strAlignSrcCamera.c_str(), strAlignSrcChannel.c_str(), nAlignReuseWaitMs, time_edge_e - time_edge_s); + // 定位模型不跑,但把手检测仍要执行:暗场图像 + 明场 bigmask + { + long handle_s = CheckUtil::getcurTime(); + CheckHandleByBigMask(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); + } + // 标签检测(独立步骤):暗场图像 + 明场 bigmask(不再沿用明场检出的标签);bDetTag 关闭时跳过 + { + if (m_pbaseCheckFunction->supportDet.bDetTag) + { + long tag_s = CheckUtil::getcurTime(); + int reTag = CheckTagByBigMask(m_CheckResult_shareP->in_shareImage->img, m_pEdge_Align_Result->bigmask); + long tag_e = CheckUtil::getcurTime(); + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Tag", "tag detect use time %ld", tag_e - tag_s); + if (reTag != 0) + { + m_pdetlog->AddCheckstr(PrintLevel_0, "Error", "CheckTagByBigMask is error type = %d", reTag); + m_nErrorCode = reTag; + m_nCheckResultErrorCode = m_nErrorCode; + return m_nErrorCode; + } + } + else + { + m_tag_roiList.clear(); + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Tag", "bDetTag close, skip tag detect"); + } + } + } + else + { + reedge = AI_Edge(m_CheckResult_shareP->in_shareImage->img, m_outer_rroi, m_inner_rroi, m_tag_roiList); + if (reedge != 0) + { + m_pdetlog->AddCheckstr(PrintLevel_0, "Error", "AI_Edge is error type = %d", reedge); + PublishAlignShare(false); // 定位失败:通知可能的等待方(暗场复用),让其回退自行定位 + m_nErrorCode = reedge; + m_nCheckResultErrorCode = m_nErrorCode; + return m_nErrorCode; + } + PublishAlignShare(true); + time_edge_e = CheckUtil::getcurTime(); + m_pdetlog->AddCheckstr(PrintLevel_0, "2、pre detect", "-------------------------AI_Edge------------succ---time %ld----\n", time_edge_e - time_edge_s); + } + + /*生成检测图:用 m_outer_rroi(旋转矩形)的外接区域直接从原图裁剪(不再旋转摆正,省掉旋转重采样耗时); + 原图坐标 -> 检测图坐标 的映射为纯平移,存入 m_matSrc2Det / m_matDet2Src,后续坐标换算仍走这对矩阵*/ + cv::Mat image = m_CheckResult_shareP->in_shareImage->img; + cv::Rect imgRect(0, 0, image.cols, image.rows); + + // 1、检测图裁剪区域 = 旋转矩形外接区域(与原图求交,防越界) + cv::Rect padRoi = m_outer_rroi.boundingRect() & imgRect; + if (padRoi.width <= 0 || padRoi.height <= 0) + { + m_pdetlog->AddCheckstr(PrintLevel_1, "Error", "crop roi error [%d %d %d %d]", padRoi.x, padRoi.y, padRoi.width, padRoi.height); + m_nErrorCode = CHECK_ERROR_CheckImg_Empty; + m_nCheckResultErrorCode = m_nErrorCode; + return m_nErrorCode; + } + int detW = padRoi.width; + int detH = padRoi.height; + + // 2、原图 -> 检测图 为纯平移(无旋转):检测图坐标 = 原图坐标 - 裁剪原点 + m_matSrc2Det = (cv::Mat_(2, 3) << 1, 0, -(double)padRoi.x, 0, 1, -(double)padRoi.y); + m_matDet2Src = (cv::Mat_(2, 3) << 1, 0, (double)padRoi.x, 0, 1, (double)padRoi.y); + + // 3、检测图:单通道灰度(彩色原图按 BGR 顺序转灰度,与 imread(path,0) 的亮度一致),直接裁剪; + // 彩色原图时再输出一张同样裁剪的彩色检测图,供裁剪图(_RE_CutImg)保存时保留原图通道 + // 注:这里先只做"裁剪",灰度结果存 detImgNoMask;是否叠加 mask 见 3.1 + m_deskewPadRoi = padRoi; + cv::Mat colorPatch = image(padRoi); + if (colorPatch.channels() == 1) + { + colorPatch.copyTo(m_pImageAllResult->detImgNoMask); + m_pImageAllResult->detColorImg.release(); + } + else if (colorPatch.channels() == 3) + { + cv::cvtColor(colorPatch, m_pImageAllResult->detImgNoMask, cv::COLOR_BGR2GRAY); + colorPatch.copyTo(m_pImageAllResult->detColorImg); + } + else + { + cv::cvtColor(colorPatch, m_pImageAllResult->detImgNoMask, cv::COLOR_BGRA2GRAY); + colorPatch.copyTo(m_pImageAllResult->detColorImg); + } + + // 3.1、两胀检测图 + // ① detImgNoMask:只做"裁剪"、不叠加任何 mask 的 ROI 灰度图——支架搜边用它(外侧保留真实灰度台阶) + // ② detImg :detImgNoMask 再叠加 outer 定位 mask(bigmask) 之外置 0——缺陷检测/AI 推理用它 + { + const cv::Mat &bigmask = m_pEdge_Align_Result->bigmask; + bool bMaskApplied = false; + int nMaskFg = -1; + m_pImageAllResult->detImg = m_pImageAllResult->detImgNoMask.clone(); + if (!bigmask.empty() && bigmask.size() == image.size()) + { + cv::Mat maskPatch = bigmask(padRoi); + // 注意:必须先 clone,否则 threshold(原地) 会写回 bigmask + cv::Mat binMask; + if (maskPatch.channels() != 1) + { + cv::cvtColor(maskPatch, binMask, cv::COLOR_BGR2GRAY); + } + else + { + binMask = maskPatch.clone(); + } + double dMin = 0, dMax = 0; + cv::minMaxLoc(binMask, &dMin, &dMax); + // 兼容 0/1 与 0/255 掩膜 + cv::threshold(binMask, binMask, dMax > 1 ? dMax * 0.5 : 0, 255, cv::THRESH_BINARY); + m_pImageAllResult->detImg.setTo(0, binMask == 0); // mask 之外置 0 + nMaskFg = cv::countNonZero(binMask); + bMaskApplied = true; + } + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "2.2、pre detect", + "det img crop [%d %d], mask(bigmask) applied %s, fg %d", + m_pImageAllResult->detImgNoMask.cols, m_pImageAllResult->detImgNoMask.rows, + bMaskApplied ? "yes" : "no(mask empty or size mismatch)", nMaskFg); + + // -s:把两张裁剪图都存下来,便于核对外侧是否被 mask 覆盖 + // 文件名带相机名,避免明/暗场同名通道互相覆盖 + if (DetImgInfo_shareP->bsaveProcessImg) + { + const std::string strCropBase = DetImgInfo_shareP->strCameraName + "_" + DetImgInfo_shareP->strChannel; + cv::imwrite(strCropBase + "_det_crop_used.png", m_pImageAllResult->detImg); + cv::imwrite(strCropBase + "_det_crop_nomask.png", m_pImageAllResult->detImgNoMask); + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "2.2、pre detect", + "det crop images saved: %s_det_crop_used.png / %s_det_crop_nomask.png", + strCropBase.c_str(), strCropBase.c_str()); + } + } + + // 4、检测图坐标系下 m_outer_roi 即整张检测图;原图坐标系下 CutRoi 仍取旋转矩形外接区域(兼容旧语义/JSON) + m_outer_roi = cv::Rect(0, 0, detW, detH); + m_Crop_Roi_paramImg = padRoi; + m_pImageAllResult->pDetResult->CutRoi = m_Crop_Roi_paramImg; + m_pImageAllResult->pDetResult->Param_CropRoi = m_Crop_Roi_paramImg; + // 检测图坐标 -> 原图坐标 的映射,供结果解析时取回缺陷小图/换算坐标 + m_pImageAllResult->pDetResult->detToSrcMat = m_matDet2Src.clone(); + + // 5、参数(模板)坐标 -> 检测图坐标 的映射: + // 有对齐时先把参数点映射到原图再平移到检测图;无对齐时参数按原图坐标系处理, + // 直接按裁剪原点平移(与旧的裁剪平移完全一致) + { + if (m_pEdge_Align_Result && m_pEdge_Align_Result->buseOfft) + { + if (!m_pEdge_Align_Result->H.empty()) + { + m_matParam2Det = MatAffineCompose(m_matSrc2Det, m_pEdge_Align_Result->H); + } + else + { + cv::Mat matOfft = (cv::Mat_(2, 3) << 1, 0, (double)m_pEdge_Align_Result->offt_x, 0, 1, (double)m_pEdge_Align_Result->offt_y); + m_matParam2Det = MatAffineCompose(m_matSrc2Det, matOfft); + } + } + else + { + m_matParam2Det = m_matSrc2Det.clone(); + } + } + long time_calan_e = CheckUtil::getcurTime(); + m_pdetlog->AddCheckstr(PrintLevel_0, "2.1、pre detect", "-------------------------crop--------------succ--- det [%d %d] src bbox [%d %d %d %d] time %ld----\n", + detW, detH, m_Crop_Roi_paramImg.x, m_Crop_Roi_paramImg.y, m_Crop_Roi_paramImg.width, m_Crop_Roi_paramImg.height, + time_calan_e - time_edge_s); + /*多线程开启 AI 推理检测*/ + long time_AI_s = CheckUtil::getcurTime(); + m_AItask = std::make_shared(); + m_AItask->taskname = Task_AI; + m_task.sendTask(m_AItask); + + ImgPreDet(); + /*更新检测区域*/ + Update_DetRoiList(); + + m_pdetlog->AddCheckstr(PrintLevel_0, "3、pre detect", "-------------------------pre Det--------------- \n"); + + /* 检测支架缺失 + 支架偏移(bDetSupport 开关关闭时不做支架判定) + 不使用参数绘制区域,结合两次定位的 mask 计算: + 1、smallmask(=Align_Inner 模型输出)最大连通域 = 支架区域 rs(按定位裁剪区 m_outer_rroi 外接矩形映射回原图坐标) + 2、支架缺失:支架面积(轮廓面积 × 缩放 × 成像精度 X/Y,单位 mm²) < support_min_area → 支架缺失 NG(ZJ_QueShi), + 缺失时不再做偏移判定(几何不可靠,避免重复/误报) + 3、产品边缘:优先复用把手提取对 bigmask 做的"四边抗凸起拟合"主体四边形 + (CheckHandleByBigMask -> GetMaskProtrusions;边缘上的凸起(把手等)不会把边缘带偏), + 拟合不可用/无效时回退 bigmask 最大连通域外接矩形 rp(近似矩形,四周可能有凸起) + 3.1、对 edge_select 选中的边缘再结合原图(detImg 灰度)精细化:沿边缘按步长采样,从边缘向产品内侧 + 找第一个灰度 >= 阈值(edge_refine_gray,默认 240)的像素作为新边缘点(最多向内 edge_refine_search 像素,找不到沿用原边缘点), + 有效点足够时稳健拟合新的边缘线;采样步长 edge_refine_step(像素) + 4、按 edge_select 指定方向(left/right/up/down)计算 rs 中心到产品对应边缘的距离 + 5、计算值与标准值(x_standard/y_standard)做差取平均,平均差超过 offset(x_offset) → 偏移 + 6、旋转偏移:支架最小外接旋转矩形的长轴(对应左/右边缘)与产品左/右边缘夹角超过 r_offset(度) → 偏移 */ + { + Function_Support_Det &supportDet = m_pbaseCheckFunction->supportDet; + m_SupportDrawInfo.Reset(); // 清空本帧绘制信息(未开启/计算无效时不绘制) + if (supportDet.bDetSupport) + { + const cv::Mat &bigmask = m_pEdge_Align_Result->bigmask; + const cv::Mat &smallmask = m_pEdge_Align_Result->smallmask; + if (bigmask.empty() || smallmask.empty()) + { + m_pdetlog->AddCheckstr(PrintLevel_0, "支架检测", "mask empty (big %d small %d), skip support offset", bigmask.empty(), smallmask.empty()); + } + else + { + // 1、mask 二值化(兼容 0/1 与 0/255 掩膜) + cv::Mat bigBin, smallBin; + { + cv::Mat grayMask; + if (bigmask.channels() != 1) + { + cv::cvtColor(bigmask, grayMask, cv::COLOR_BGR2GRAY); + } + else + { + grayMask = bigmask; + } + double dMin = 0; + double dMax = 0; + cv::minMaxLoc(grayMask, &dMin, &dMax); + cv::threshold(grayMask, bigBin, dMax > 1 ? dMax * 0.5 : 0, 255, cv::THRESH_BINARY); + } + { + cv::Mat grayMask; + if (smallmask.channels() != 1) + { + cv::cvtColor(smallmask, grayMask, cv::COLOR_BGR2GRAY); + } + else + { + grayMask = smallmask; + } + double dMin = 0; + double dMax = 0; + cv::minMaxLoc(grayMask, &dMin, &dMax); + cv::threshold(grayMask, smallBin, dMax > 1 ? dMax * 0.5 : 0, 255, cv::THRESH_BINARY); + } + + // 2、最大连通域轮廓:bigmask -> 产品区域 rp(原图坐标);smallmask -> 支架区域 rs(smallmask 局部坐标) + bool bFoundRp = false; + std::vector rpContour = CheckUtil::getLargestContourROI(bigBin, bFoundRp); + bool bFoundRs = false; + std::vector rsContour = CheckUtil::getLargestContourROI(smallBin, bFoundRs); + + // smallmask -> 原图坐标的比例:smallmask 对应定位时裁剪的大轮廓区域(m_outer_rroi 外接矩形),按比例映射 + cv::Rect bigRect = m_outer_rroi.boundingRect(); + float fx = smallmask.cols > 0 ? bigRect.width * 1.0f / smallmask.cols : 1.0f; + float fy = smallmask.rows > 0 ? bigRect.height * 1.0f / smallmask.rows : 1.0f; + + // rs 映射回原图坐标(支架轮廓缺失时保持空矩形,面积按 0 处理) + cv::Rect rsRect; + if (bFoundRs && rsContour.size() >= 3) + { + cv::Rect rsSmallRect = cv::boundingRect(rsContour); + rsRect = cv::Rect( + cvRound(bigRect.x + rsSmallRect.x * fx), + cvRound(bigRect.y + rsSmallRect.y * fy), + cvRound(rsSmallRect.width * fx), + cvRound(rsSmallRect.height * fy)); + } + + // 3、支架缺失判定:支架由 Align_Inner 模型(smallmask)检出,支架面积 < support_min_area(单位 mm²) → 缺失 + // 面积换算:smallmask 轮廓面积 × fx × fy = 原图像素面积,再乘成像精度 X/Y(mm/px) + float fScaleX = (m_fImgage_Scale_X > 0) ? m_fImgage_Scale_X : 1.0f; + float fScaleY = (m_fImgage_Scale_Y > 0) ? m_fImgage_Scale_Y : 1.0f; + double dSupportAreaSmall = (bFoundRs && rsContour.size() >= 3) ? cv::contourArea(rsContour) : 0.0; + double dSupportAreaMm2 = dSupportAreaSmall * fx * fy * fScaleX * fScaleY; + bool bSupportMiss = (supportDet.support_min_area > 0) && (dSupportAreaMm2 < supportDet.support_min_area); + m_pdetlog->AddCheckstr(PrintLevel_0, "支架检测", + "support area %.2f mm2 (small px %.1f, scale %.4f %.4f) min_area %.2f miss %d", + dSupportAreaMm2, dSupportAreaSmall, fx * fScaleX, fy * fScaleY, + supportDet.support_min_area, bSupportMiss); + + if (bSupportMiss) + { + // 支架缺失 NG:ROI 取支架区域(有轮廓时)或整个产品区域(无轮廓时),映射到检测图(detImg)坐标 + cv::Rect missRect = (rsRect.width > 0 && rsRect.height > 0) ? rsRect : bigRect; + // cv::Rect roi = MatAffineRect(m_matSrc2Det, missRect); + cv::Rect roi = missRect; + roi &= cv::Rect(0, 0, m_pImageAllResult->detImg.cols, m_pImageAllResult->detImg.rows); + + QX_ERROR_INFO_ temerror; + temerror.Idx = m_pDetResult->pQx_ErrorList->size(); + temerror.roi = roi; + temerror.area = roi.width * roi.height; + temerror.JudgArea = roi.width * fScaleX * roi.height * fScaleY; + temerror.JudgArea_second = temerror.JudgArea; + float w = missRect.width; + float h = missRect.height; + temerror.flen = (w > h ? w : h) * fScaleX; + temerror.fbreadth = (w > h ? h : w) * fScaleY; + temerror.nconfig_qx_type = CONFIG_QX_NAME_cell_ZJ_QueShi; + 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]; + + // 计算支架(缺失区域)中心所在的检测区域 + 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 &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 support missing: area %.2f mm2 < min_area %.2f roi [%d %d %d %d]", + temerror.qx_name.c_str(), temerror.Idx, dSupportAreaMm2, supportDet.support_min_area, + roi.x, roi.y, roi.width, roi.height); + } + else if (!bFoundRp || rpContour.size() < 3 || !bFoundRs || rsContour.size() < 3) + { + m_pdetlog->AddCheckstr(PrintLevel_0, "支架检测", "contour invalid (rp %d %ld, rs %d %ld), skip support offset", + bFoundRp, rpContour.size(), bFoundRs, rsContour.size()); + } + else + { + // rp 边缘(拟合不可用时的回退):产品最大连通域的外接矩形(含凸起) + cv::Rect rpRect = cv::boundingRect(rpContour); + + // 3、产品边缘:优先复用把手提取对 bigmask 做的"四边抗凸起拟合"主体四边形 + // (CheckHandleByBigMask -> GetMaskProtrusions 输出):边缘上的凸起(把手等)不会把边缘带偏; + // 拟合不可用/无效时回退到产品最大连通域外接矩形 rpRect(含凸起) + bool bFitQuad = false; // 是否用上了拟合主体四边形 + float fDL = 0, fDR = 0, fDT = 0, fDB = 0; // 支架中心到产品左/右/上/下边缘的距离(原图像素,后乘缩放系数) + cv::Point2f ptEdgeArr[4]; // 连线端点(边缘线上):0 left 1 right 2 up 3 down + cv::Point2f ptSegAArr[4]; // 边缘线段端点A + cv::Point2f ptSegBArr[4]; // 边缘线段端点B + + float fCx = rsRect.x + rsRect.width * 0.5f; + float fCy = rsRect.y + rsRect.height * 0.5f; + if (m_HandleBodyQuad.size() == 4) + { + // 四边形角点顺序:左上/右上/右下/左下;左/右边 x=k*y+b、上/下边 y=k*x+b(与 GetMaskProtrusions 约定一致); + // 边长过小说明拟合退化,不用 + const cv::Point2f &ptTL = m_HandleBodyQuad[0]; + const cv::Point2f &ptTR = m_HandleBodyQuad[1]; + const cv::Point2f &ptBR = m_HandleBodyQuad[2]; + const cv::Point2f &ptBL = m_HandleBodyQuad[3]; + if (fabs(ptBL.y - ptTL.y) > 2.0f && fabs(ptBR.y - ptTR.y) > 2.0f && + fabs(ptTR.x - ptTL.x) > 2.0f && fabs(ptBR.x - ptBL.x) > 2.0f) + { + float fKL = (ptBL.x - ptTL.x) / (ptBL.y - ptTL.y); // 左边 x = k*y + b + float fBL = ptTL.x - fKL * ptTL.y; + float fKR = (ptBR.x - ptTR.x) / (ptBR.y - ptTR.y); // 右边 x = k*y + b + float fBR = ptTR.x - fKR * ptTR.y; + float fKT = (ptTR.y - ptTL.y) / (ptTR.x - ptTL.x); // 上边 y = k*x + b + float fBT = ptTL.y - fKT * ptTL.x; + float fKB = (ptBR.y - ptBL.y) / (ptBR.x - ptBL.x); // 下边 y = k*x + b + float fBB = ptBL.y - fKB * ptBL.x; + // 连线端点:取边缘线在支架中心行/列上的点;长度即中心到边缘的水平/垂直距离 + float fXEdgeL = fKL * fCy + fBL; + float fXEdgeR = fKR * fCy + fBR; + float fYEdgeT = fKT * fCx + fBT; + float fYEdgeB = fKB * fCx + fBB; + fDL = fCx - fXEdgeL; + fDR = fXEdgeR - fCx; + fDT = fCy - fYEdgeT; + fDB = fYEdgeB - fCy; + // 拟合有效性:支架中心应位于四条拟合边内侧(四个方向距离均为正),否则放弃拟合 + if (fDL > 0 && fDR > 0 && fDT > 0 && fDB > 0) + { + ptEdgeArr[0] = cv::Point2f(fXEdgeL, fCy); + ptEdgeArr[1] = cv::Point2f(fXEdgeR, fCy); + ptEdgeArr[2] = cv::Point2f(fCx, fYEdgeT); + ptEdgeArr[3] = cv::Point2f(fCx, fYEdgeB); + ptSegAArr[0] = ptTL; ptSegBArr[0] = ptBL; // left:整条拟合左边 + ptSegAArr[1] = ptTR; ptSegBArr[1] = ptBR; // right:整条拟合右边 + ptSegAArr[2] = ptTL; ptSegBArr[2] = ptTR; // up:整条拟合上边 + ptSegAArr[3] = ptBL; ptSegBArr[3] = ptBR; // down:整条拟合下边 + bFitQuad = true; + } + } + } + if (!bFitQuad) + { + // 回退:产品区域外接矩形(含凸起)的四条边 + fDL = fCx - rpRect.x; + fDR = (rpRect.x + rpRect.width) - fCx; + fDT = fCy - rpRect.y; + fDB = (rpRect.y + rpRect.height) - fCy; + ptEdgeArr[0] = cv::Point2f((float)rpRect.x, fCy); + ptEdgeArr[1] = cv::Point2f((float)(rpRect.x + rpRect.width), fCy); + ptEdgeArr[2] = cv::Point2f(fCx, (float)rpRect.y); + ptEdgeArr[3] = cv::Point2f(fCx, (float)(rpRect.y + rpRect.height)); + ptSegAArr[0] = cv::Point2f((float)rpRect.x, (float)rpRect.y); + ptSegBArr[0] = cv::Point2f((float)rpRect.x, (float)(rpRect.y + rpRect.height)); + ptSegAArr[1] = cv::Point2f((float)(rpRect.x + rpRect.width), (float)rpRect.y); + ptSegBArr[1] = cv::Point2f((float)(rpRect.x + rpRect.width), (float)(rpRect.y + rpRect.height)); + ptSegAArr[2] = cv::Point2f((float)rpRect.x, (float)rpRect.y); + ptSegBArr[2] = cv::Point2f((float)(rpRect.x + rpRect.width), (float)rpRect.y); + ptSegAArr[3] = cv::Point2f((float)rpRect.x, (float)(rpRect.y + rpRect.height)); + ptSegBArr[3] = cv::Point2f((float)(rpRect.x + rpRect.width), (float)(rpRect.y + rpRect.height)); + } + + // 3.2、结合原图精细化检测边缘(只处理 edge_select 选中的方向): + // 在"所计算边缘"(拟合四边形边/回退外接矩形边)上按步长采样,从边缘向产品内侧逐像素扫描原图 + // (detImg 灰度,未做其它处理;产品区域为饱和白),找到的第一个灰度 >= 阈值的像素作为该采样点的新边缘点; + // 阈值优先取 edge_refine_gray(JSON,默认见 EDGE_REFINE_GRAY_THRESHOLD); + // 最多向内搜索 edge_refine_search 像素(JSON,默认见 EDGE_REFINE_SEARCH_DEFAULT), + // 找不到的采样点没有新边缘信息,不参与拟合(相当于沿用原边缘点); + // edge_refine_step(JSON,默认见 EDGE_REFINE_STEP_DEFAULT)为沿边采样步长,不需要每个点都找; + // 有效点足够时稳健拟合新的边缘直线,更新测距点(中心行/列)与绘制线段。 + { + const cv::Mat &grayEdgeImg = m_pImageAllResult->detImg; + int nRefineSearch = EDGE_REFINE_SEARCH_DEFAULT; + int nRefineStep = EDGE_REFINE_STEP_DEFAULT; + int nRefineGray = EDGE_REFINE_GRAY_THRESHOLD; + if (supportDet.edge_refine_search > 0) + { + nRefineSearch = (int)(supportDet.edge_refine_search + 0.5f); + } + if (supportDet.edge_refine_step > 0) + { + nRefineStep = (int)(supportDet.edge_refine_step + 0.5f); + } + if (supportDet.edge_refine_gray > 0) + { + nRefineGray = (int)(supportDet.edge_refine_gray + 0.5f); + } + nRefineSearch = std::max(1, nRefineSearch); + nRefineStep = std::max(4, nRefineStep); + nRefineGray = std::min(255, std::max(0, nRefineGray)); + + bool bEdgeRefined[4] = {false, false, false, false}; // edge_select 配重复时只处理一次 + if (grayEdgeImg.type() == CV_8UC1 && grayEdgeImg.cols > 0 && grayEdgeImg.rows > 0) + { + // -s 存过程图:在检测图(灰度转 BGR)上绘制 + // 绿线=精细化前的边缘线 蓝点=向产品内侧找到的 255 边缘点 红线=稳健拟合后的边缘线 + const bool bSaveRefineImg = (DetImgInfo_shareP != nullptr && DetImgInfo_shareP->bsaveProcessImg); + cv::Mat showRefine; + int nRefineDrawPts = 0; // 已绘制的蓝点数量 + int nRefineDrawEdge = 0; // 已绘制的边缘条数 + if (bSaveRefineImg) + { + cv::cvtColor(grayEdgeImg, showRefine, cv::COLOR_GRAY2BGR); + } + + for (size_t iselect = 0; iselect < supportDet.edge_select.size(); iselect++) + { + const std::string &strEdge = supportDet.edge_select[iselect]; + int gi = -1; // 方向索引:0 left 1 right 2 up 3 down + if (strEdge == "left") + { + gi = 0; + } + else if (strEdge == "right") + { + gi = 1; + } + else if (strEdge == "up") + { + gi = 2; + } + else if (strEdge == "down") + { + gi = 3; + } + if (gi < 0 || bEdgeRefined[gi]) + { + continue; + } + bEdgeRefined[gi] = true; + + // 当前边缘直线:左右边 x=k*y+b,上下边 y=k*x+b(与拟合四边形约定一致) + const bool bVertEdge = (gi == 0 || gi == 1); + const cv::Point2f ptSegA = ptSegAArr[gi]; + const cv::Point2f ptSegB = ptSegBArr[gi]; + const double fDelta = bVertEdge ? ((double)ptSegB.y - ptSegA.y) : ((double)ptSegB.x - ptSegA.x); + if (std::fabs(fDelta) < 4.0) + { + continue; // 边太短/退化,保持原边缘 + } + const double fKOld = bVertEdge ? ((double)ptSegB.x - ptSegA.x) / fDelta + : ((double)ptSegB.y - ptSegA.y) / fDelta; + const double fBOld = bVertEdge ? (ptSegA.x - fKOld * ptSegA.y) + : (ptSegA.y - fKOld * ptSegA.x); + + // 沿边采样区间:两端各去掉 5%(至少 4px),避开圆角过渡段 + const double fT0 = bVertEdge ? std::min(ptSegA.y, ptSegB.y) : std::min(ptSegA.x, ptSegB.x); + const double fT1 = bVertEdge ? std::max(ptSegA.y, ptSegB.y) : std::max(ptSegA.x, ptSegB.x); + const double fTrim = std::max(4.0, (fT1 - fT0) * 0.05); + const double fTStart = fT0 + fTrim; + const double fTEnd = fT1 - fTrim; + if (fTEnd - fTStart < 8.0) + { + continue; + } + + // -s 调试图:精细化前的边缘线(绿),便于和蓝点/红线对比 + if (bSaveRefineImg && !showRefine.empty()) + { + const cv::Point2f ptA = MatAffinePoint(m_matSrc2Det, ptSegA); + const cv::Point2f ptB = MatAffinePoint(m_matSrc2Det, ptSegB); + cv::line(showRefine, cv::Point(cvRound(ptA.x), cvRound(ptA.y)), + cv::Point(cvRound(ptB.x), cvRound(ptB.y)), cv::Scalar(0, 255, 0), 4); + } + + std::vector vecRefinePts; // 有效的新边缘点(t, v):左右 t=y/v=x,上下 t=x/v=y + int nSampleNum = 0; + for (double fT = fTStart; fT <= fTEnd; fT += nRefineStep) + { + nSampleNum++; + const double fEdgePos = fKOld * fT + fBOld; + const cv::Point2f ptSrc = bVertEdge ? cv::Point2f((float)fEdgePos, (float)fT) + : cv::Point2f((float)fT, (float)fEdgePos); + // 原图坐标 -> 检测图(detImg)坐标(纯平移);在检测图灰度上找第一个灰度 >= 阈值的像素 + const cv::Point2f ptDet = MatAffinePoint(m_matSrc2Det, ptSrc); + const int nFix = bVertEdge ? cvRound(ptDet.y) : cvRound(ptDet.x); // 固定坐标(行/列) + if ((bVertEdge && (nFix < 0 || nFix >= grayEdgeImg.rows)) || + (!bVertEdge && (nFix < 0 || nFix >= grayEdgeImg.cols))) + { + continue; // 固定坐标越界,该采样点无法搜索s + } + + bool bFound = false; + double fNewPos = fEdgePos; // 找到的新边缘位置(原图坐标) + if (gi == 0) // 左边:向产品内侧(+x)找 + { + const int nX0 = (int)std::ceil(ptDet.x); + const int nXEnd = std::min(grayEdgeImg.cols - 1, nX0 + nRefineSearch); + for (int x = std::max(0, nX0); x <= nXEnd; x++) + { + if (grayEdgeImg.at(nFix, x) >= nRefineGray) + { + const cv::Point2f ptNew = MatAffinePoint(m_matDet2Src, cv::Point2f((float)x, ptDet.y)); + fNewPos = ptNew.x; + bFound = true; + break; + } + } + } + else if (gi == 1) // 右边:向产品内侧(-x)找 + { + const int nX0 = (int)std::floor(ptDet.x); + const int nXEnd = std::max(0, nX0 - nRefineSearch); + for (int x = std::min(grayEdgeImg.cols - 1, nX0); x >= nXEnd && x >= 0; x--) + { + if (grayEdgeImg.at(nFix, x) >= nRefineGray) + { + const cv::Point2f ptNew = MatAffinePoint(m_matDet2Src, cv::Point2f((float)x, ptDet.y)); + fNewPos = ptNew.x; + bFound = true; + break; + } + } + } + else if (gi == 2) // 上边:向产品内侧(+y)找 + { + const int nY0 = (int)std::ceil(ptDet.y); + const int nYEnd = std::min(grayEdgeImg.rows - 1, nY0 + nRefineSearch); + for (int y = std::max(0, nY0); y <= nYEnd; y++) + { + if (grayEdgeImg.at(y, nFix) >= nRefineGray) + { + const cv::Point2f ptNew = MatAffinePoint(m_matDet2Src, cv::Point2f(ptDet.x, (float)y)); + fNewPos = ptNew.y; + bFound = true; + break; + } + } + } + else // 下边:向产品内侧(-y)找 + { + const int nY0 = (int)std::floor(ptDet.y); + const int nYEnd = std::max(0, nY0 - nRefineSearch); + for (int y = std::min(grayEdgeImg.rows - 1, nY0); y >= nYEnd && y >= 0; y--) + { + if (grayEdgeImg.at(y, nFix) >= nRefineGray) + { + const cv::Point2f ptNew = MatAffinePoint(m_matDet2Src, cv::Point2f(ptDet.x, (float)y)); + fNewPos = ptNew.y; + bFound = true; + break; + } + } + } + if (bFound) + { + vecRefinePts.push_back(cv::Point2f((float)fT, (float)fNewPos)); + // -s 调试图:蓝点=找到的新边缘像素(换回检测图坐标绘制) + if (bSaveRefineImg && !showRefine.empty()) + { + const cv::Point2f ptFoundSrc = bVertEdge ? cv::Point2f((float)fNewPos, (float)fT) + : cv::Point2f((float)fT, (float)fNewPos); + const cv::Point2f ptFoundDet = MatAffinePoint(m_matSrc2Det, ptFoundSrc); + cv::circle(showRefine, cv::Point(cvRound(ptFoundDet.x), cvRound(ptFoundDet.y)), 10, + cv::Scalar(255, 0, 0), -1); + nRefineDrawPts++; + } + } + // 找不到:该采样点没有新边缘信息,不参与拟合(相当于沿用原边缘点) + } + + // 有效采样点足够时重新拟合边缘线;否则保留原边缘 + if (vecRefinePts.size() < 4) + { + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测", + "edge refine %s skip: sample %d found %ld (need 4), keep old", + strEdge.c_str(), nSampleNum, (long)vecRefinePts.size()); + continue; + } + double fKNew = 0, fBNew = 0; + if (FitLineRobustTV(vecRefinePts, fKNew, fBNew) < 4) + { + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测", + "edge refine %s skip: fit failed, keep old", strEdge.c_str()); + continue; + } + + // 中心行/列上的新边缘位置 + 合理性检查(仍需在中心外侧,相对原边缘偏移不超过搜索范围) + const double fOldCenterPos = bVertEdge ? ptEdgeArr[gi].x : ptEdgeArr[gi].y; + const double fNewCenterPos = bVertEdge ? (fKNew * fCy + fBNew) : (fKNew * fCx + fBNew); + const bool bCenterSideOK = (gi == 0) ? (fNewCenterPos < fCx) + : (gi == 1) ? (fNewCenterPos > fCx) + : (gi == 2) ? (fNewCenterPos < fCy) + : (fNewCenterPos > fCy); + if (!bCenterSideOK || std::fabs(fNewCenterPos - fOldCenterPos) > nRefineSearch + 2.0) + { + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测", + "edge refine %s reject: center %.1f -> %.1f (limit %d), keep old", + strEdge.c_str(), fOldCenterPos, fNewCenterPos, nRefineSearch); + continue; + } + + // 应用新边缘线:更新测距点(中心行/列)与绘制线段两端(沿边坐标不变,位置取新线) + if (bVertEdge) + { + ptEdgeArr[gi].x = (float)fNewCenterPos; + ptSegAArr[gi].x = (float)(fKNew * ptSegAArr[gi].y + fBNew); + ptSegBArr[gi].x = (float)(fKNew * ptSegBArr[gi].y + fBNew); + if (gi == 0) + { + fDL = fCx - ptEdgeArr[0].x; + } + else + { + fDR = ptEdgeArr[1].x - fCx; + } + } + else + { + ptEdgeArr[gi].y = (float)fNewCenterPos; + ptSegAArr[gi].y = (float)(fKNew * ptSegAArr[gi].x + fBNew); + ptSegBArr[gi].y = (float)(fKNew * ptSegBArr[gi].x + fBNew); + if (gi == 2) + { + fDT = fCy - ptEdgeArr[2].y; + } + else + { + fDB = ptEdgeArr[3].y - fCy; + } + } + + // -s 调试图:红线=稳健拟合后的边缘线 + if (bSaveRefineImg && !showRefine.empty()) + { + const cv::Point2f ptA = MatAffinePoint(m_matSrc2Det, ptSegAArr[gi]); + const cv::Point2f ptB = MatAffinePoint(m_matSrc2Det, ptSegBArr[gi]); + cv::line(showRefine, cv::Point(cvRound(ptA.x), cvRound(ptA.y)), + cv::Point(cvRound(ptB.x), cvRound(ptB.y)), cv::Scalar(0, 0, 255), 4); + nRefineDrawEdge++; + } + + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测", + "edge refine %s: sample %d found %ld center %.1f -> %.1f shift %.1f", + strEdge.c_str(), nSampleNum, (long)vecRefinePts.size(), + fOldCenterPos, fNewCenterPos, fNewCenterPos - fOldCenterPos); + } + + // -s 存图:边缘点调试图(绿=原边缘线 蓝=找到的边缘点 红=拟合后边缘线) + if (bSaveRefineImg && !showRefine.empty() && nRefineDrawPts > 0) + { + char strDbg[256]; + snprintf(strDbg, sizeof(strDbg), "edge refine: refined edges %d, found points %d, gray thr %d", nRefineDrawEdge, nRefineDrawPts, nRefineGray); + cv::putText(showRefine, strDbg, cv::Point(20, 70), cv::FONT_HERSHEY_SIMPLEX, 2.0, cv::Scalar(0, 255, 255), 4); + cv::putText(showRefine, "green=old edge blue=found edge point red=refined edge", cv::Point(20, 140), + cv::FONT_HERSHEY_SIMPLEX, 1.6, cv::Scalar(0, 255, 255), 4); + // 调试图按检测图原分辨率保存(不再缩小到 1/4):边缘点/拟合线位置可逐像素核对, + // 存图耗时/文件大小靠 edge_refine_step 控制采样点数量,而不是靠缩小图像 + std::vector paramJpg = {cv::IMWRITE_JPEG_QUALITY, 95}; + cv::imwrite(DetImgInfo_shareP->strChannel + "_edge_refine.jpg", showRefine, paramJpg); + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测", + "edge refine debug image saved: %s_edge_refine.jpg (refined edges %d, found pts %d)", + DetImgInfo_shareP->strChannel.c_str(), nRefineDrawEdge, nRefineDrawPts); + } + } + else if (!supportDet.edge_select.empty()) + { + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测", + "edge refine skip: detImg not CV_8UC1 (type %d)", grayEdgeImg.type()); + } + } + + + + // 3.5、旋转偏移:支架最小外接旋转矩形(长轴对应左/右边缘、短轴对应上/下边缘) + // 长轴与产品左/右边缘的夹角 > r_offset(度) → 偏移;r_offset<=0 不判定(兼容旧参数) + float fSupportAngle = 0; // 支架长轴相对产品左/右边缘的夹角(度,带符号;正=支架长轴相对产品边缘向右倾) + bool bRotOffset = false; + bool bSupportAngleValid = false; // 是否用上了原图精细化角度(false=走 minAreaRect 兜底) + if (supportDet.r_offset > 0 && rsContour.size() >= 3) + { + // 轮廓先映射回原图坐标再拟合旋转矩形(smallmask 的 x/y 缩放可能不同,不还原会带角度误差) + // 注意:smallmask 是 Align_Inner 在"大轮廓裁剪图"上的输出,映射原图时要加裁剪原点 bigRect + std::vector rsContourSrc; + rsContourSrc.reserve(rsContour.size()); + for (size_t ip = 0; ip < rsContour.size(); ip++) + { + rsContourSrc.emplace_back(bigRect.x + rsContour[ip].x * fx, + bigRect.y + rsContour[ip].y * fy); + } + cv::RotatedRect rsRotRect = cv::minAreaRect(rsContourSrc); + + // ===== 精细化计算支架方向(原图灰度上拟合支架长边)===== + // 模型 smallmask 分辨率低(1696×800 上采样回原图,量化误差约 4px),且支架区域接近方形时 + // minAreaRect 的"长轴"方向不稳定,直接用它求旋转角误差大;这里用模型支架边缘做初值, + // 在 detImg 灰度原图上重新拟合支架长边方向: + // 1、把 smallmask 最大轮廓映射回原图,得到支架外接矩形(rsRect),只用它的 y 范围(采样行); + // 列带不使用 mask 边界,而是直接取 outer 定位后的 ROI 原图(detImg)最外侧 nStrip 列, + // 避免 smallmask 精度低时把真实边缘切到带外(边缘点被 mask 掉) + // 2、结合 edge_select:选 left → 拟合支架右边缘;选 right → 拟合支架左边缘 + // (up/down 对应支架上下短边,水平的线不好拟合,不处理,仍用 minAreaRect 结果) + // 3、沿支架区域的 y 范围按 step 采样;每行在搜索带内逐像素找灰度梯度最大的位置作为边缘点: + // 右边缘取 ROI 最右 nStrip 列、从右往左找;左边缘取 ROI 最左 nStrip 列、从左往右找; + // 梯度不足(该侧没有真实边缘)的行不参与拟合 + // 4、采样点沿 y 分成 n 段,每段用 FitLineRobustTV 稳健拟合直线,取各段方向角的中位数: + // 支架边缘的凸起是矩形,凸起外边缘与支架长边平行,拟合到凸起处也不影响方向角; + // 凸起侧边/内部纹理等离群点由 MAD 剔除 + // 失败(有效采样/分段不足、角度离散过大)时保留下面的 minAreaRect 长轴方案 + // 搜边输入用 detImgNoMask(只裁剪、未叠加 mask 的检测图):detImg 被 mask 置 0 后外侧是硬切, + // 用 detImg 找边相当于在测 mask 边界;未生成时(异常)回退 detImg + const cv::Mat &graySupportImg = (!m_pImageAllResult->detImgNoMask.empty() && m_pImageAllResult->detImgNoMask.type() == CV_8UC1) + ? m_pImageAllResult->detImgNoMask + : m_pImageAllResult->detImg; + if (!graySupportImg.empty() && graySupportImg.type() == CV_8UC1) + { + const cv::Mat &grayRef = graySupportImg; + // 边缘搜索带宽度(列):取 outer ROI 原图最外侧多少列;未配置默认 50 + int nStrip = (supportDet.support_refine_search > 0) + ? cvRound(supportDet.support_refine_search) + : SUPPORT_REFINE_STRIP_DEFAULT; + int nStep = (supportDet.support_refine_step > 0) + ? cvRound(supportDet.support_refine_step) + : SUPPORT_REFINE_STEP_DEFAULT; + int nSegments = (supportDet.support_refine_segments > 0) + ? cvRound(supportDet.support_refine_segments) + : SUPPORT_REFINE_SEGMENTS_DEFAULT; + nStrip = std::max(4, nStrip); + nStep = std::max(4, nStep); + nSegments = std::max(1, nSegments); + + // 3 行灰度均值(抑制单行噪声;调用处保证 y 在 [1, rows-2]) + auto gray3 = [&grayRef](int y, int x) -> float { + return (grayRef.at(y - 1, x) + grayRef.at(y, x) + grayRef.at(y + 1, x)) * (1.0f / 3.0f); + }; + + // 拟合支架一条长边:nSide 0=支架左边缘(外侧 -x)、1=支架右边缘(外侧 +x); + // nRefIdx 0=产品左边缘、1=产品右边缘 + // 返回 true 时 fAngleOut=支架长边相对产品该边缘的带符号夹角(度),fEdgeVertOut=支架长边相对竖直方向角度(度) + // pSamplesOut/pSegLinesOut 非空时输出采样边缘点和各段拟合线(检测图坐标),供 -s 绘制调试图 + auto FitSupportEdgeAngle = [&](int nSide, int nRefIdx, float &fAngleOut, int &nSampleOut, int &nSegOut, float &fEdgeVertOut, + std::vector *pSamplesOut, std::vector> *pSegLinesOut) -> bool { + fAngleOut = 0.0f; + fEdgeVertOut = 0.0f; + nSampleOut = 0; + nSegOut = 0; + if (pSamplesOut != NULL) + { + pSamplesOut->clear(); + } + if (pSegLinesOut != NULL) + { + pSegLinesOut->clear(); + } + + // 产品基准边缘方向(统一朝下,与原有旋转角符号约定一致) + cv::Point2f vRef = ptSegBArr[nRefIdx] - ptSegAArr[nRefIdx]; + float fRefLen = (float)cv::norm(vRef); + if (fRefLen < 1e-3f) + { + return false; + } + vRef *= (1.0f / fRefLen); + if (vRef.y < 0) + { + vRef = -vRef; + } + + // ===== 取 outer 定位后的 ROI 原图(detImg)最外侧 nStrip 列,逐行找边缘点 ===== + // 右边缘:取 ROI 最右 nStrip 列,从右往左扫;左边缘:取 ROI 最左 nStrip 列,从左往右扫。 + // 搜索带只依赖 outer ROI 原图,不依赖 smallmask 边界(mask 上采样精度低,会把真实边缘切到带外) + int nXStart = 0; // 外侧起始列(detImg 坐标) + int nXEnd = 0; // 内侧结束列(detImg 坐标) + if (nSide == 0) + { + nXStart = 0; + nXEnd = std::min(grayRef.cols - 1, nStrip - 1); + } + else + { + nXStart = grayRef.cols - 1; + nXEnd = std::max(0, grayRef.cols - nStrip); + } + const int nStepX = (nXEnd >= nXStart) ? 1 : -1; + + // 沿外接矩形高度采样(两端各缩 5%,至少 6px,避开圆角) + double fY0 = rsRect.y; + double fY1 = rsRect.y + rsRect.height - 1; + const double fTrim = std::max(6.0, (fY1 - fY0) * 0.05); + fY0 += fTrim; + fY1 -= fTrim; + if (fY1 - fY0 < 20.0) + { + return false; + } + + std::vector vecEdgePts; // (t,v) = (yDet, xDet) + vecEdgePts.reserve((int)((fY1 - fY0) / nStep) + 1); + + for (double fT = fY0; fT <= fY1; fT += nStep) + { + // 行号由原图坐标(t)换算到 detImg;列带已经是 detImg 坐标 + cv::Point2f ptDetRow = MatAffinePoint(m_matSrc2Det, cv::Point2f(0.0f, (float)fT)); + const int nYDet = cvRound(ptDetRow.y); + if (nYDet < 2 || nYDet >= grayRef.rows - 2) + { + continue; + } + const int nX0 = nXStart; // 外侧端 + const int nX1 = nXEnd; // 内侧端 + if ((nStepX > 0 && nX1 <= nX0) || (nStepX < 0 && nX1 >= nX0)) + { + continue; + } + + // 外侧 -> 内侧逐像素扫,取灰度梯度最大的位置作为该行支架边缘点 + int nEdge = -1; + float fBestGrad = 0.0f; + for (int x = nX0; (nStepX > 0) ? (x + nStepX <= nX1) : (x + nStepX >= nX1); x += nStepX) + { + if (x < 1 || x + nStepX < 1 || x >= grayRef.cols - 1 || x + nStepX >= grayRef.cols - 1) + { + continue; + } + const float fGrad = fabs(gray3(nYDet, x + nStepX) - gray3(nYDet, x)); + if (fGrad > fBestGrad) + { + fBestGrad = fGrad; + nEdge = x; + } + } + if (nEdge < 0 || fBestGrad < (float)SUPPORT_REFINE_MIN_CONTRAST) + { + continue; // 该行列带内没有明显灰度阶跃,本行不参与拟合 + } + vecEdgePts.push_back(cv::Point2f((float)nYDet, (float)(nEdge + nStepX * 0.5f))); + } + + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测", + "support edge diag(side %d): strip %d cols (ROI) [%d,%d], pts %d", + nSide, nStrip, std::min(nXStart, nXEnd), std::max(nXStart, nXEnd), + (int)vecEdgePts.size()); + // -s 存图:把本侧搜边实际使用的条带(outer ROI 原图最外侧 nStrip 列)裁出来, + // 并把找到的边缘点(黄点)画上去,便于确认条带是否覆盖到真实支架边缘、边缘点是否准确 + if (DetImgInfo_shareP->bsaveProcessImg && !vecEdgePts.empty()) + { + const char *strSide = (nSide == 0) ? "left" : "right"; + const int nStripX0 = std::min(nXStart, nXEnd); + const int nStripW = std::abs(nXEnd - nXStart) + 1; + // 列范围直接用 detImg 坐标;行范围取支架区域(rsRect)的 y 范围,换算到 detImg + cv::Point2f ptDetY0 = MatAffinePoint(m_matSrc2Det, cv::Point2f(0.0f, (float)rsRect.y)); + cv::Point2f ptDetY1 = MatAffinePoint(m_matSrc2Det, cv::Point2f(0.0f, (float)(rsRect.y + rsRect.height))); + cv::Rect stripRoi(nStripX0, cvRound(ptDetY0.y), nStripW, cvRound(ptDetY1.y - ptDetY0.y)); + stripRoi &= cv::Rect(0, 0, grayRef.cols, grayRef.rows); + if (stripRoi.width > 0 && stripRoi.height > 0) + { + cv::Mat showStrip; + if (grayRef.channels() == 1) + { + cv::cvtColor(grayRef(stripRoi), showStrip, cv::COLOR_GRAY2BGR); + } + else + { + showStrip = grayRef(stripRoi).clone(); + } + for (size_t i = 0; i < vecEdgePts.size(); i++) + { + // vecEdgePts 存 (t,v)=(yDet,xDet),转成条带内局部坐标 + const int nPx = cvRound(vecEdgePts[i].y) - stripRoi.x; + const int nPy = cvRound(vecEdgePts[i].x) - stripRoi.y; + if (nPx < 0 || nPx >= showStrip.cols || nPy < 0 || nPy >= showStrip.rows) + { + continue; + } + cv::circle(showStrip, cv::Point(nPx, nPy), 3, cv::Scalar(0, 255, 255), -1); + } + std::string strStripPath = DetImgInfo_shareP->strChannel + "_support_edge_strip_" + strSide + ".png"; + cv::imwrite(strStripPath, showStrip); + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测", + "support edge strip saved: %s (side %s strip %d cols [%d,%d] roi [%d %d %d %d] pts %d)", + strStripPath.c_str(), strSide, nStripW, nStripX0, nStripX0 + nStripW - 1, + stripRoi.x, stripRoi.y, stripRoi.width, stripRoi.height, (int)vecEdgePts.size()); + } + } + + nSampleOut = (int)vecEdgePts.size(); + if (nSampleOut < 8) + { + return false; + } + + // 沿边分成 n 段,每段稳健拟合一条直线(凸起处点被 MAD 剔除;拟合到凸起外边缘也不影响方向) + std::vector vecAngles; + std::vector vecAnglesVert; + vecAngles.reserve(nSegments); + vecAnglesVert.reserve(nSegments); + for (int s = 0; s < nSegments; s++) + { + const size_t i0 = (size_t)((long long)s * nSampleOut / nSegments); + const size_t i1 = (size_t)((long long)(s + 1) * nSampleOut / nSegments); + if (i1 - i0 < 4) + { + continue; + } + std::vector vecSeg(vecEdgePts.begin() + i0, vecEdgePts.begin() + i1); + double fKSeg = 0, fBSeg = 0; + if (FitLineRobustTV(vecSeg, fKSeg, fBSeg) < 4) + { + continue; + } + + // 拟合线方向(统一朝下)与产品基准边缘的带符号夹角 + cv::Point2f vSeg((float)fKSeg, 1.0f); + float fSegLen = (float)cv::norm(vSeg); + if (fSegLen < 1e-6f) + { + continue; + } + vSeg *= (1.0f / fSegLen); + const double dCross = (double)vSeg.x * vRef.y - (double)vSeg.y * vRef.x; + const double dDot = (double)vSeg.x * vRef.x + (double)vSeg.y * vRef.y; + const float fAngle = (float)(std::atan2(dCross, dDot) * 180.0 / CV_PI); + if (fabs(fAngle) > 45.0f) + { + continue; // 与产品边缘夹角不合理,丢弃该段 + } + vecAngles.push_back(fAngle); + vecAnglesVert.push_back((float)(std::atan2(fKSeg, 1.0) * 180.0 / CV_PI)); + // 记录该段拟合直线的两端点(检测图坐标),-s 时用于绘制支架边缘 + if (pSegLinesOut != NULL) + { + const float fSegT0 = vecSeg.front().x; // vecSeg 点为 (t,v) = (yDet, xDet) + const float fSegT1 = vecSeg.back().x; + std::vector vecLine(2); + vecLine[0] = cv::Point2f((float)(fKSeg * fSegT0 + fBSeg), fSegT0); + vecLine[1] = cv::Point2f((float)(fKSeg * fSegT1 + fBSeg), fSegT1); + pSegLinesOut->push_back(vecLine); + } + } + nSegOut = (int)vecAngles.size(); + if (nSegOut < 2) + { + return false; + } + + // 取各段角度中位数,并用 MAD 检查各段一致性(凸起/纹理导致的异常段会被中位数稀释) + std::vector vecTmp = vecAngles; + std::nth_element(vecTmp.begin(), vecTmp.begin() + vecTmp.size() / 2, vecTmp.end()); + const float fMedian = vecTmp[vecTmp.size() / 2]; + for (size_t i = 0; i < vecAngles.size(); i++) + { + vecTmp[i] = fabs(vecAngles[i] - fMedian); + } + std::nth_element(vecTmp.begin(), vecTmp.begin() + vecTmp.size() / 2, vecTmp.end()); + if (vecTmp[vecTmp.size() / 2] > 5.0f) + { + return false; // 各段方向离散过大(可能拟合到了不同结构),不采用 + } + + fAngleOut = fMedian; + std::nth_element(vecAnglesVert.begin(), vecAnglesVert.begin() + vecAnglesVert.size() / 2, vecAnglesVert.end()); + fEdgeVertOut = vecAnglesVert[vecAnglesVert.size() / 2]; + if (pSamplesOut != NULL) + { + pSamplesOut->reserve(vecEdgePts.size()); + for (size_t i = 0; i < vecEdgePts.size(); i++) + { + pSamplesOut->push_back(cv::Point2f(vecEdgePts[i].y, vecEdgePts[i].x)); // (t,v) -> (x,y) + } + } + return true; + }; + + // 按 edge_select 选择拟合方向:left -> 支架右边缘,right -> 支架左边缘(up/down 不处理) + int nCandSide[2] = {0, 0}; + int nCandRef[2] = {0, 0}; + int nCandNum = 0; + for (size_t iselect = 0; iselect < supportDet.edge_select.size(); iselect++) + { + const std::string &strEdge = supportDet.edge_select[iselect]; + int nSide = -1; + int nRef = -1; + if (strEdge == "left") + { + nSide = 1; // 拟合支架右边缘 + nRef = 0; // 参考产品左边缘 + } + else if (strEdge == "right") + { + nSide = 0; // 拟合支架左边缘 + nRef = 1; // 参考产品右边缘 + } + else + { + continue; + } + bool bDup = false; + for (int ic = 0; ic < nCandNum; ic++) + { + if (nCandSide[ic] == nSide && nCandRef[ic] == nRef) + { + bDup = true; + } + } + if (!bDup && nCandNum < 2) + { + nCandSide[nCandNum] = nSide; + nCandRef[nCandNum] = nRef; + nCandNum++; + } + } + + int nBestCand = -1; + int nBestSample = 0; + int nBestSeg = 0; + float fBestAngle = 0.0f; + float fBestEdgeVert = 0.0f; + int nBestSideUsed = -1; + int nBestRefUsed = -1; + std::vector vecBestSamples; // 采样边缘点(检测图坐标) + std::vector> vecBestSegLines; // 各段拟合线(检测图坐标) + for (int ic = 0; ic < nCandNum; ic++) + { + float fAngle = 0.0f; + float fEdgeVert = 0.0f; + int nSample = 0; + int nSeg = 0; + std::vector vecCandSamples; + std::vector> vecCandSegLines; + // 只拟合 edge_select 指定的支架长边;该侧失败时保留 minAreaRect 结果,不换边 + // (两条长边方向角虽一致,但换边会静默得到另一条边的结果,与配置含义不符) + bool bOk = FitSupportEdgeAngle(nCandSide[ic], nCandRef[ic], fAngle, nSample, nSeg, fEdgeVert, + &vecCandSamples, &vecCandSegLines); + const int nUseSide = nCandSide[ic]; + if (!bOk) + if (!bOk) + { + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测", + "support angle refine(ref %s): %s support edge fail, keep minAreaRect", + (nCandRef[ic] == 0) ? "left" : "right", (nCandSide[ic] == 0) ? "left" : "right"); + continue; + } + const cv::Point2f vRefLog = ptSegBArr[nCandRef[ic]] - ptSegAArr[nCandRef[ic]]; + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测", + "support angle refine(ref %s): use %s support edge: sample %d seg %d angle %.2f deg (edge vs vert %.2f, ref vs vert %.2f)", + (nCandRef[ic] == 0) ? "left" : "right", (nCandSide[ic] == 0) ? "left" : "right", + nSample, nSeg, fAngle, fEdgeVert, + (float)(std::atan2((double)vRefLog.x, (double)vRefLog.y) * 180.0 / CV_PI)); + if (nSample > nBestSample) + { + nBestSample = nSample; + nBestSeg = nSeg; + fBestAngle = fAngle; + fBestEdgeVert = fEdgeVert; + nBestSideUsed = nUseSide; + nBestRefUsed = nCandRef[ic]; + vecBestSamples = vecCandSamples; + vecBestSegLines = vecCandSegLines; + nBestCand = ic; + } + } + if (nBestCand >= 0) + { + fSupportAngle = fBestAngle; + bRotOffset = (fabs(fSupportAngle) > supportDet.r_offset); + bSupportAngleValid = true; + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测", + "support angle refine ok: angle %.2f deg (sample %d seg %d), replace minAreaRect", + fSupportAngle, nBestSample, nBestSeg); + + // -s(存过程图):绘制精细化拟合的支架边缘,便于从图像确认拟合效果 + // 蓝=smallmask 支架轮廓 黄=参与拟合的采样边缘点 绿=各段稳健拟合直线 + // 红=最终方向线(产品基准边方向 + 精细夹角,过采样点重心) 品红=产品基准边 + if (DetImgInfo_shareP->bsaveProcessImg && !vecBestSamples.empty()) + { + cv::Mat show; + if (grayRef.channels() == 1) + { + cv::cvtColor(grayRef, show, cv::COLOR_GRAY2BGR); + } + else + { + show = grayRef.clone(); + } + + // 支架区域轮廓(蓝):smallmask 最大轮廓映射到检测图坐标 + std::vector vecRsPoly; + vecRsPoly.reserve(rsContour.size()); + for (size_t ip = 0; ip < rsContour.size(); ip++) + { + cv::Point2f ptSrc(bigRect.x + rsContour[ip].x * fx, bigRect.y + rsContour[ip].y * fy); + cv::Point2f ptDet = MatAffinePoint(m_matSrc2Det, ptSrc); + vecRsPoly.push_back(cv::Point(cvRound(ptDet.x), cvRound(ptDet.y))); + } + if (vecRsPoly.size() >= 3) + { + cv::polylines(show, vecRsPoly, true, cv::Scalar(255, 0, 0), 4); + } + + // 采样边缘点(黄):放大到半径 8,1/4 缩略后仍清晰可见 + for (size_t i = 0; i < vecBestSamples.size(); i++) + { + cv::circle(show, cv::Point(cvRound(vecBestSamples[i].x), cvRound(vecBestSamples[i].y)), 8, + cv::Scalar(0, 255, 255), -1); + } + + // 各段稳健拟合的直线(绿) + for (size_t i = 0; i < vecBestSegLines.size(); i++) + { + if (vecBestSegLines[i].size() == 2) + { + cv::line(show, cv::Point(cvRound(vecBestSegLines[i][0].x), cvRound(vecBestSegLines[i][0].y)), + cv::Point(cvRound(vecBestSegLines[i][1].x), cvRound(vecBestSegLines[i][1].y)), + cv::Scalar(0, 255, 0), 4); + } + } + + // 最终方向线(红):产品基准边方向旋转支架精细夹角,过采样点重心 + cv::Point2f vRefBest = ptSegBArr[nBestRefUsed] - ptSegAArr[nBestRefUsed]; + float fRefBestLen = (float)cv::norm(vRefBest); + if (fRefBestLen > 1e-3f) + { + vRefBest *= (1.0f / fRefBestLen); + if (vRefBest.y < 0) + { + vRefBest = -vRefBest; + } + const double dRad = (double)fSupportAngle * CV_PI / 180.0; + cv::Point2f vDir((float)(vRefBest.x * std::cos(dRad) + vRefBest.y * std::sin(dRad)), + (float)(-vRefBest.x * std::sin(dRad) + vRefBest.y * std::cos(dRad))); + float fDirLen = (float)cv::norm(vDir); + if (fDirLen > 1e-6f) + { + vDir *= (1.0f / fDirLen); + } + cv::Point2f ptCenter(0.0f, 0.0f); + float fSampleYMin = vecBestSamples[0].y; + float fSampleYMax = vecBestSamples[0].y; + for (size_t i = 0; i < vecBestSamples.size(); i++) + { + ptCenter += vecBestSamples[i]; + fSampleYMin = std::min(fSampleYMin, vecBestSamples[i].y); + fSampleYMax = std::max(fSampleYMax, vecBestSamples[i].y); + } + ptCenter *= (1.0f / (float)vecBestSamples.size()); + const float fHalf = std::max(20.0f, (fSampleYMax - fSampleYMin) * 0.6f); + cv::line(show, cv::Point(cvRound(ptCenter.x - vDir.x * fHalf), cvRound(ptCenter.y - vDir.y * fHalf)), + cv::Point(cvRound(ptCenter.x + vDir.x * fHalf), cvRound(ptCenter.y + vDir.y * fHalf)), + cv::Scalar(0, 0, 255), 4); + + // 产品基准边缘(品红) + cv::Point2f ptRefA = MatAffinePoint(m_matSrc2Det, ptSegAArr[nBestRefUsed]); + cv::Point2f ptRefB = MatAffinePoint(m_matSrc2Det, ptSegBArr[nBestRefUsed]); + cv::line(show, cv::Point(cvRound(ptRefA.x), cvRound(ptRefA.y)), + cv::Point(cvRound(ptRefB.x), cvRound(ptRefB.y)), + cv::Scalar(255, 0, 255), 3); + } + + // 文字信息:所用支架边/参考产品边/采样与分段数/角度 + char strDbg[256]; + snprintf(strDbg, sizeof(strDbg), "support edge refine: side %s ref %s sample %d seg %d", + (nBestSideUsed == 0) ? "left" : "right", (nBestRefUsed == 0) ? "left" : "right", + nBestSample, nBestSeg); + cv::putText(show, strDbg, cv::Point(20, 70), cv::FONT_HERSHEY_SIMPLEX, 2.0, cv::Scalar(0, 255, 255), 4); + cv::Point2f vRefDbg = ptSegBArr[nBestRefUsed] - ptSegAArr[nBestRefUsed]; + snprintf(strDbg, sizeof(strDbg), "angle %.2f deg (edge vs vert %.2f, ref vs vert %.2f)", + fSupportAngle, fBestEdgeVert, + (float)(std::atan2((double)vRefDbg.x, (double)vRefDbg.y) * 180.0 / CV_PI)); + cv::putText(show, strDbg, cv::Point(20, 140), cv::FONT_HERSHEY_SIMPLEX, 2.0, cv::Scalar(0, 255, 255), 4); + cv::putText(show, "blue=support contour yellow=edge points green=seg fit red=dir magenta=ref edge", + cv::Point(20, 210), cv::FONT_HERSHEY_SIMPLEX, 1.6, cv::Scalar(0, 255, 255), 4); + + // 调试图按检测图原分辨率保存(不再缩小到 1/4):边缘点/拟合线位置可逐像素核对, + // 存图耗时/文件大小靠 support_refine_step 控制采样点数量,而不是靠缩小图像 + std::vector paramJpg = {cv::IMWRITE_JPEG_QUALITY, 95}; + cv::imwrite(DetImgInfo_shareP->strChannel + "_support_angle.jpg", show, paramJpg); + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测", + "support angle debug image saved: %s_support_angle.jpg", DetImgInfo_shareP->strChannel.c_str()); + } + } + else if (!supportDet.edge_select.empty()) + { + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "支架检测", + "support angle refine not applied(no left/right edge or fit failed), use minAreaRect"); + } + } + + // 3.5、旋转偏移(兜底):支架最小外接旋转矩形(长轴对应左/右边缘、短轴对应上/下边缘) + // 长轴与产品左/右边缘的夹角 > r_offset(度) → 偏移;r_offset<=0 不判定(兼容旧参数) + if (!bSupportAngleValid) + { + cv::Point2f rpts[4]; + rsRotRect.points(rpts); + cv::Point2f d01 = rpts[1] - rpts[0]; + cv::Point2f d12 = rpts[2] - rpts[1]; + float len01 = (float)cv::norm(d01); + float len12 = (float)cv::norm(d12); + // 长轴方向 = 长边方向(长轴对应左/右边缘),并统一朝下(与左边缘 上->下 方向一致) + cv::Point2f vLong(0, 1); + if (len01 >= len12 && len01 > 1e-3f) + { + vLong = d01 * (1.0f / len01); + } + else if (len12 > 1e-3f) + { + vLong = d12 * (1.0f / len12); + } + if (vLong.y < 0) + { + vLong = -vLong; + } + // 参考方向 = 产品左边缘(ptSegAArr[0]->ptSegBArr[0],拟合边/回退矩形边);退化时用垂直方向 + cv::Point2f vRef = ptSegBArr[0] - ptSegAArr[0]; + float lenRef = (float)cv::norm(vRef); + if (lenRef > 1e-3f) + { + vRef *= (1.0f / lenRef); + } + else + { + vRef = cv::Point2f(0, 1); + } + if (vRef.y < 0) + { + vRef = -vRef; + } + // 有符号夹角(度);长轴是“线”,角度归一化到 [-90, 90] + double dCross = (double)vLong.x * vRef.y - (double)vLong.y * vRef.x; + double dDot = (double)vLong.x * vRef.x + (double)vLong.y * vRef.y; + fSupportAngle = (float)(std::atan2(dCross, dDot) * 180.0 / CV_PI); + if (fSupportAngle > 90.0f) + { + fSupportAngle -= 180.0f; + } + else if (fSupportAngle < -90.0f) + { + fSupportAngle += 180.0f; + } + bRotOffset = (fabs(fSupportAngle) > supportDet.r_offset); + } + } + + // 像素 -> mm + fDL *= m_fImgage_Scale_X; + fDR *= m_fImgage_Scale_X; + fDT *= m_fImgage_Scale_Y; + fDB *= m_fImgage_Scale_Y; + float fDistArr[4] = {fDL, fDR, fDT, fDB}; // 顺序固定:0 left 1 right 2 up 3 down + + // 4、按 edge_select 指定方向(left/right/up/down)取计算值,与标准值做差取平均,平均差超过 offset → 偏移 + // left/right 用 x_standard,up/down 用 y_standard;offset 用 x_offset + float fSumDiff = 0; + int nDiffNum = 0; + std::string strSelect = ""; + std::string strDiff = ""; + char strTmp[64]; + for (size_t iselect = 0; iselect < supportDet.edge_select.size(); iselect++) + { + const std::string &strEdge = supportDet.edge_select[iselect]; + int gi = -1; // 方向索引:0 left 1 right 2 up 3 down + float fStandard = 0; + if (strEdge == "left") + { + gi = 0; + fStandard = supportDet.x_standard; + } + else if (strEdge == "right") + { + gi = 1; + fStandard = supportDet.x_standard; + } + else if (strEdge == "up") + { + gi = 2; + fStandard = supportDet.y_standard; + } + else if (strEdge == "down") + { + gi = 3; + fStandard = supportDet.y_standard; + } + else + { + continue; + } + + float fDist = fDistArr[gi]; + float fDiff = fabs(fDist - fStandard); + fSumDiff += fDiff; + nDiffNum++; + // 记录该方向绘制信息:|实测-标准|超限差(x_offset)则绘制为红色 + { + Support_Draw_Info_::Edge_Draw_ edgeDraw; + edgeDraw.strEdge = strEdge; + edgeDraw.fDist = fDist; + edgeDraw.fStandard = fStandard; + edgeDraw.bExceed = (fDiff > supportDet.x_offset); + edgeDraw.ptEdge = ptEdgeArr[gi]; + edgeDraw.ptEdgeA = ptSegAArr[gi]; + edgeDraw.ptEdgeB = ptSegBArr[gi]; + m_SupportDrawInfo.edgeList.push_back(edgeDraw); + } + sprintf(strTmp, "%s(%.1f/%.1f) ", strEdge.c_str(), fDist, fStandard); + strSelect += strTmp; + sprintf(strTmp, "%.1f ", fDiff); + strDiff += strTmp; + } + // edge_select 为空或无效时 nDiffNum=0,距离不参与判定(仅打日志) + float fAvgDiff = (nDiffNum > 0) ? (fSumDiff / nDiffNum) : 0.0f; + bool bOffsetDist = (nDiffNum > 0) && (fAvgDiff > supportDet.x_offset); + // 距离偏移 或 旋转偏移(r_offset) 任一超限 → 支架偏移 + bool bOffset = bOffsetDist || bRotOffset; + + // 记录绘制信息(DrawResult_Step_1 用):支架中心十字 + 各方向距离 + m_SupportDrawInfo.bValid = true; + m_SupportDrawInfo.bOffset = bOffset; + m_SupportDrawInfo.rpRect = rpRect; + m_SupportDrawInfo.rsRect = rsRect; + m_SupportDrawInfo.ptCenter = cv::Point2f(fCx, fCy); + + m_pdetlog->AddCheckstr(PrintLevel_0, "支架检测", + "rp [%d %d %d %d] rs [%d %d %d %d] support area %.2f mm2 (min %.2f) scale (%.4f %.4f) center (%.1f %.1f) edge %s quad %ld dL %.1f dR %.1f dT %.1f dB %.1f | select %s diff %s avgDiff %.1f limit %.1f | rot %.2f deg limit %.2f(%s) ng %d", + rpRect.x, rpRect.y, rpRect.width, rpRect.height, + rsRect.x, rsRect.y, rsRect.width, rsRect.height, + dSupportAreaMm2, supportDet.support_min_area, + fx, fy, fCx, fCy, bFitQuad ? "fitquad" : "rpbox", (long)m_HandleBodyQuad.size(), fDL, fDR, fDT, fDB, + strSelect.c_str(), strDiff.c_str(), fAvgDiff, supportDet.x_offset, + fSupportAngle, supportDet.r_offset, bSupportAngleValid ? "refine" : "rect", bOffset); + + if (bOffset) + { + QX_ERROR_INFO_ temerror; + temerror.Idx = m_pDetResult->pQx_ErrorList->size(); + + // roi 在原图坐标系,按坐标映射转换到检测图(detImg)坐标系 + // cv::Rect roi = MatAffineRect(m_matSrc2Det, rsRect); + cv::Rect roi = rsRect; + 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 = rsRect.width; + float h = rsRect.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_cell_ZJ_PianYi; + 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]; + + // 计算支架中心所在的检测区域 + 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 &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); + } + } + } + } + } + + /* 把手缺失检测:bigmask 的凸起本身不算 NG,只有"参数里的某个把手区域附近找不到把手"才算 NG(每个缺失区域报一条) */ + { + if (m_bHandleDetSucc && m_bHandleExpect && !m_bHandleFound) + { + for (size_t iRegion = 0; iRegion < m_HandleRegionResultList.size(); iRegion++) + { + Handle_Region_Result_ ®ionResult = m_HandleRegionResultList[iRegion]; + if (regionResult.bFound) + { + continue; + } + + // 上报该把手区域的位置:参数(模板)坐标 -> 检测图(detImg)坐标 + // cv::Rect roi = MatAffineRect(m_matParam2Det, regionResult.boxRegion); + cv::Rect roi = regionResult.boxRegion; + roi &= cv::Rect(0, 0, m_pImageAllResult->detImg.cols, m_pImageAllResult->detImg.rows); + if (roi.width <= 0 || roi.height <= 0) + { + continue; + } + + QX_ERROR_INFO_ temerror; + temerror.Idx = m_pDetResult->pQx_ErrorList->size(); + 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_cell_CGMo_QueShi; + temerror.qx_name = CONFIG_QX_NAME_Names[CONFIG_QX_NAME_cell_CGMo_QueShi]; + 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 &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 region %d roi [%d %d %d %d]", + temerror.qx_name.c_str(), temerror.Idx, regionResult.nIdx, + roi.x, roi.y, roi.width, roi.height); + } + } + } + + /* Tag检测(bDetTag 关闭时不上报标签 NG) */ + { + if (!m_pbaseCheckFunction->supportDet.bDetTag) + { + m_tag_roiList.clear(); + } + + // 标签能量/灰阶差(hj)计算用掩膜:所有标签区域填白(与 blob 检测用 AIMaskImg 的口径一致), + // 由 calc_blob_info_withstats 按“前景=标签区域、背景=外扩一圈”计算 + long tagEnergy_s = CheckUtil::getcurTime(); + cv::Mat tagMask; + if (!m_tag_roiList.empty() && !m_pImageAllResult->detImg.empty()) + { + tagMask = cv::Mat::zeros(m_pImageAllResult->detImg.size(), CV_8UC1); + std::vector> tagPolys; + tagPolys.reserve(m_tag_roiList.size()); + for (const auto &tagRoiMask : m_tag_roiList) + { + cv::Point2f ptfMask[4]; + tagRoiMask.points(ptfMask); + std::vector polyMask(4); + for (int k = 0; k < 4; k++) + { + polyMask[k] = cv::Point(cvRound(ptfMask[k].x), cvRound(ptfMask[k].y)); + } + tagPolys.push_back(std::move(polyMask)); + } + cv::fillPoly(tagMask, tagPolys, cv::Scalar(255)); + } + + for (const auto &tagRoi : m_tag_roiList) + { + // tag roi 在原图坐标系,按坐标映射转换到检测图(detImg)坐标系 + // cv::Rect roi = MatAffineRect(m_matSrc2Det, tagRoi.boundingRect()); + cv::Rect roi = tagRoi.boundingRect(); + roi &= cv::Rect(0, 0, m_pImageAllResult->detImg.cols, m_pImageAllResult->detImg.rows); + if (roi.width <= 0 || roi.height <= 0) + { + continue; + } + + QX_ERROR_INFO_ temerror; + temerror.Idx = m_pDetResult->pQx_ErrorList->size(); + 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 tw = tagRoi.size.width; + float th = tagRoi.size.height; + temerror.flen = (tw > th ? tw : th) * m_fImgage_Scale_X; + temerror.fbreadth = (tw > th ? th : tw) * m_fImgage_Scale_Y; + + // 标签区域的能量/灰阶差(hj):与 blob 检测相同口径(前景=标签区域,背景=外扩一圈) + if (!tagMask.empty()) + { + cv::Rect roiStat = roi; + cv::Scalar tagStat = calc_blob_info_withstats(m_pImageAllResult->detImg, tagMask, roiStat); + temerror.energy = (int)tagStat[1]; + temerror.grayDis = (float)tagStat[2]; + } + + temerror.nconfig_qx_type = CONFIG_QX_NAME_cell_TBBQ; + temerror.qx_name = CONFIG_QX_NAME_Names[CONFIG_QX_NAME_cell_TBBQ]; + 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 &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, "Tag检测", "tag %d roi [%d %d %d %d] energy %d hj %.1f -> NG", + temerror.Idx, roi.x, roi.y, roi.width, roi.height, temerror.energy, temerror.grayDis); + } + if (!m_tag_roiList.empty()) + { + long tagEnergy_e = CheckUtil::getcurTime(); + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Tag", "tag energy calc use time %ld", tagEnergy_e - tagEnergy_s); + } + } + + m_CheckResult_shareP->nresult = 0; + + /*把临时可以绘制的结果都绘制出来。*/ + DrawResult_Step_1(); + long time_AI_e; + /*AI 推理生成*/ + { + long t211 = CheckUtil::getcurTime(); + // 对AI的结果进行 处理,等待 AI 推理全部完成。 + rec = AIMaskDet(); + if (rec != CHECK_OK) + { + m_nErrorCode = rec; + m_nCheckResultErrorCode = m_nErrorCode; + return m_nErrorCode; + } + time_AI_e = CheckUtil::getcurTime(); + m_pdetlog->AddCheckstr(PrintLevel_0, "4、detect", "-------------------------2、AI Detect--------%ld wait AI complate %ld-------\n", time_AI_e - time_AI_s, time_AI_e - t211); + + m_CheckResult_shareP->resultMaskImg = m_pImageAllResult->AIMaskImg; + } + + { + + t4 = CheckUtil::getcurTime(); + rec = GetCheckResultBLob(); + if (rec != CHECK_OK) + { + m_nErrorCode = rec; + m_nCheckResultErrorCode = m_nErrorCode; + return m_nErrorCode; + } + t5 = CheckUtil::getcurTime(); + m_pdetlog->AddCheckstr(PrintLevel_0, "5、BLob ", "------------------------ --------%ld -------\n", t5 - t4); + } + // m_pdetlog->printLog(m_strCurDetChannel); + + long te = CheckUtil::getcurTime(); + m_pdetlog->bPrintStr = true; + m_pdetlog->AddCheckstr(PrintLevel_1, "result", " ALL use Time %ld edge %ld AI %ld blob %ld", + te - t1, time_edge_e - time_edge_s, time_AI_e - time_AI_s, t5 - t4); + + m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, "End", " Check Run"); + + return 0; +} + +int ImgCheckAnalysisy::SetNewConfig() +{ + if (m_nConfigIdx < 0) + { + return 1; + /* code */ + } + + if (m_pConfig->GetConfigUpdataStatus(ConfigType_Analysisy_Common_XL, m_nConfigIdx)) + { + m_bupdateconfig = true; + // printf("************** ImgCheckAnalysisy::SetNewConfig m_nConfigIdx %d\n", m_nConfigIdx); + m_old_productROI = cv::Rect(0, 0, 0, 0); + m_pConfig->GetConfig(ConfigType_Analysisy_Common_XL, &m_AnalysisyConfig); + if (m_AnalysisyConfig.commonCheckConfig.nodeConfigArr.size() > 0) + { + m_pCommonAnalysisyConfig = &m_AnalysisyConfig.commonCheckConfig.nodeConfigArr.at(0); + m_pBasicConfig = &m_AnalysisyConfig.commonCheckConfig.baseConfig; + m_pRegionAnalysisyParam = &m_pCommonAnalysisyConfig->regionConfigArr.at(0); + + UpdateImgageScale(); + if (false) + { + printf("SetNewConfig m_nConfigIdx %d m_CheckConfig.strSkuName %s \n", m_nConfigIdx, m_AnalysisyConfig.strSkuName.c_str()); + } + } + else + { + printf("m_AnalysisyConfig.commonCheckConfig.nodeConfigArr == 0 \n"); + } + } + else + { + // printf("ConfigType_Analysisy_Common_XL no Update \n"); + } + m_nErrorCode = CHECK_OK; + return CHECK_OK; +} + +ChannelCheckFunction *ImgCheckAnalysisy::GetChannelFuntion(std::string strChannelName) +{ + ChannelCheckFunction *p = NULL; + for (int i = 0; i < m_AnalysisyConfig.checkFunction.channelFunctionArr.size(); i++) + { + if (CheckUtil::compareIgnoreCase(m_AnalysisyConfig.checkFunction.channelFunctionArr[i].strChannelName, strChannelName)) + { + p = &m_AnalysisyConfig.checkFunction.channelFunctionArr[i]; + } + } + + return p; +} +int ImgCheckAnalysisy::Run(int nId) +{ + std::vector vi; + vi.push_back(nId); + auto nRet = set_cpu_id(vi); + // printf("Check So %d bind cpu ret %d, %d\n", m_nThreadIdx, nRet, nId); + while (!m_bExit) + { + // 数据准备完成,开启检测 + if (m_nRun_Status == CHECK_THREAD_STATUS_READY) + { + m_nRun_Status = CHECK_THREAD_STATUS_BUSY; + /* 检测 */ + CheckRun(); + m_nRun_Status = CHECK_THREAD_STATUS_COMPLETE; + m_pImageAllResult->setStep(ImageAllResult::DetStep_Complet); + m_nRun_Status = CHECK_THREAD_STATUS_IDLE; + // printf("*--------%d\n", m_nRun_Status); + } + else + { + + usleep(1000); + } + // printf("*-"); + usleep(1000); + } + return 0; +} +int ImgCheckAnalysisy::set_cpu_id(const std::vector &cpu_set_vec) +{ + // for cpu affinity + int nRet = 0; +#ifdef __linux + cpu_set_t _cur_cpu_set; + CPU_ZERO(&_cur_cpu_set); + for (auto _id : cpu_set_vec) + { + CPU_SET(_id, &_cur_cpu_set); + } + if (0 > pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &_cur_cpu_set)) + { + perror("set cpu affinity failed: "); + printf("Warning: set cpu affinity failed ... ...\n"); + nRet = -1; + } +#endif //__linux + return nRet; +} + +int ImgCheckAnalysisy::CalBlob_Other() +{ + + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "CalBlob_Other", " Start"); + + float fs_x = m_fImgage_Scale_X; + float fs_y = m_fImgage_Scale_Y; + + float fs_resize_x = m_pImageAllResult->fscale_detToresult_x; + float fs_resize_y = m_pImageAllResult->fscale_detToresult_y; + // 遍历每个检测blob + for (int i = 0; i < blobs.blobCount; i++) + { + + // m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Blob", "%d/%d start", i, blobs.blobCount); + + cv::Rect roi; + roi.x = blobs.blobTab[i].minx; + roi.y = blobs.blobTab[i].miny; + roi.width = blobs.blobTab[i].maxx - blobs.blobTab[i].minx + 1; + roi.height = blobs.blobTab[i].maxy - blobs.blobTab[i].miny + 1; + + cv::Scalar result = calc_blob_info_withstats(m_pImageAllResult->detImg, m_pImageAllResult->AIMaskImg, roi); + + int QX_whiteBLACK = CONFIG_QX_BLACK; + if (result[0] > 0) + { + QX_whiteBLACK = CONFIG_QX_WHITE; + } + + double hj = result[2]; + double energe = result[1]; + blobs.blobTab[i].energy = energe; + blobs.blobTab[i].grayDis = hj; + + float JudgArea = blobs.blobTab[i].area * fs_x * fs_y; + blobs.blobTab[i].JudgArea = JudgArea; + float flen = roi.width * fs_x; + float fwid = roi.height * fs_x; + if (roi.height * fs_y > flen) + { + flen = roi.height * fs_y; + fwid = roi.width * fs_y; + } + blobs.blobTab[i].len = flen; + blobs.blobTab[i].breadth = fwid; + + int nerrortype = 0; + int checkFlage = 0; + + float fmaxScore = 0; + int config_qx_type = 0; + + if (blobs.blobTab[i].ErrType == ERR_TYPE_2) + { + QX_whiteBLACK = CONFIG_QX_BLACK; // 强制为 黑色 + } + blobs.blobTab[i].whiteOrblack = QX_whiteBLACK; + + // 精确计算长度 + vector re_len = Cal_QXLen(m_pImageAllResult->detImg(roi), config_qx_type, fs_x, fs_y); + + if (re_len.size() > 2) + { + if(re_len[0] >= 0){ + flen = re_len[0]; + blobs.blobTab[i].len = flen; + } + if(re_len[1] >= 0){ + fwid = re_len[1]; + blobs.blobTab[i].breadth = fwid; + } + + } + } + return 0; +} + +int ImgCheckAnalysisy::GetClassImg(const cv::Mat &img, cv::Mat &AIdetImg, cv::Rect qx_roi, int detwidth, int detheight) +{ + + cv::Rect cutroi; + cv::Rect roi = qx_roi; + bool bresize = false; + { + + int pc_x = roi.x + roi.width * 0.5; + int pc_y = roi.y + roi.height * 0.5; + + if (roi.width < detwidth && roi.height < detheight) + { + cutroi.width = detwidth; + cutroi.x = pc_x - detwidth * 0.5; + cutroi.height = detheight; + cutroi.y = pc_y - detheight * 0.5; + } + else + { + // 宽 高 + if (roi.width > roi.height) + { + cutroi.width = roi.width + 20; + cutroi.x = roi.x - 10; + + float fsx = detheight * 1.0f / detwidth; + cutroi.height = cutroi.width * fsx; + cutroi.y = pc_y - cutroi.height * 0.5; + } + else + { + cutroi.height = roi.height + 20; + cutroi.y = roi.y - 10; + + float fsy = detwidth * 1.0f / detheight; + cutroi.width = cutroi.height * fsy; + cutroi.x = pc_x - cutroi.width * 0.5; + } + + bresize = true; + } + + if (cutroi.x < 0) + { + cutroi.x = 0; + } + if (cutroi.y < 0) + { + cutroi.y = 0; + } + if (cutroi.x + cutroi.width >= img.cols) + { + cutroi.x = img.cols - cutroi.width; + if (cutroi.x < 0) + { + cutroi.x = 0; + if (cutroi.x + cutroi.width >= img.cols) + { + cutroi.width = img.cols; + } + } + } + if (cutroi.y + cutroi.height >= img.rows) + { + cutroi.y = img.rows - cutroi.height; + if (cutroi.y < 0) + { + cutroi.y = 0; + if (cutroi.y + cutroi.height >= img.rows) + { + cutroi.height = img.rows; + } + } + } + } + if (!CheckUtil::RoiInImg(cutroi, img)) + { + return 1; + } + cv::Size sz; + sz.width = detwidth; + sz.height = detheight; + if (cutroi.width != sz.width || cutroi.height != sz.height) + { + cv::resize(img(cutroi), AIdetImg, sz); + } + else + { + AIdetImg = img(cutroi).clone(); + } + + if (1 != AIdetImg.channels()) + { + cv::cvtColor(AIdetImg, AIdetImg, cv::COLOR_BGR2GRAY); + } + return 0; +} + +vector ImgCheckAnalysisy::Cal_QXLen(cv::Mat qx_maskImg, int qx_type, float fsc_x, float fsc_y) +{ + vector resLen(3); + + float nlen = -1; + cv::Mat detimg = qx_maskImg; + + resLen[0] = nlen; + // 寻找轮廓 + vector> contours; + cv::findContours(detimg, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE); + + // 找到最大面积的轮廓 + double maxArea = -1; + int maxAreaIdx = -1; + for (size_t i = 0; i < contours.size(); ++i) + { + double area = contourArea(contours[i]); + if (area > maxArea) + { + maxArea = area; + maxAreaIdx = i; + } + } + + // 如果找到了最大面积的轮廓 + if (maxAreaIdx >= 0) + { + + // 使用minAreaRect找到最小外接矩形 + cv::RotatedRect rect = cv::minAreaRect(contours[maxAreaIdx]); + + // // 绘制最小外接矩形 + // Point2f vertices[4]; + // rect.points(vertices); + // for (int i = 0; i < 4; ++i) + // { + // line(detimg, vertices[i], vertices[(i + 1) % 4], Scalar(128, 255, 0), 2); // 绿色线 + // } + // 输出结果 + + // 获取最小外接矩形的尺寸 + float width = rect.size.width; + float height = rect.size.height; + + // std::cout << "1 Width: " << width << ", Height: " << height << std::endl; + Point2f vertices[4]; + rect.points(vertices); + + vector newcont; + for (int i = 0; i < 4; ++i) + { + Point2f p; + p.x = vertices[i].x * fsc_x; + p.y = vertices[i].y * fsc_y; + newcont.push_back(p); + } + vector> contours_New; + contours_New.push_back(newcont); + // Recreate the rotated rectangle with scaled vertices + RotatedRect scaledRect = minAreaRect(contours_New[0]); + + // Calculate scaled width and height + width = scaledRect.size.width; + height = scaledRect.size.height; + // std::cout << "2 width: " << width << ", height: " << height << std::endl; + // float scale = 0.5; // 缩放比例 + // rect.size.width *= fsc_x; + // rect.size.height *= fsc_y; + // std::cout << "fsc_x: " << fsc_x << ", fsc_y: " << fsc_y << std::endl; + // std::cout << "Width: " << width << ", Height: " << height << std::endl; + // 获取最小外接矩形的尺寸 + // width = rect.size.width; + // height = rect.size.height; + if (width > height) + { + nlen = width; + resLen[0] = width; + resLen[1] = height; + /* code */ + } + else + { + nlen = height; + resLen[0] = height; + resLen[1] = width; + } + } + + // getchar(); + return resLen; +} + +int ImgCheckAnalysisy::GetCheckResultBLob() +{ + long t1, t2, t3, t4, t5, t6, t7; + t1 = CheckUtil::getcurTime(); + int re = GetALLBlob(); + if (re != 0) + { + return re; + } + + // 缺陷分类 多线程 实现。 + long t11 = CheckUtil::getcurTime(); + m_Classtask = std::make_shared(); + m_Classtask->taskname = Task_Class; + m_task.sendTask(m_Classtask); + + re = CalBlob_Other(); + if (re != 0) + { + return re; + } + long t12 = CheckUtil::getcurTime(); + // 等待分类 完成。 + m_Classtask->waitComplate(); + long t13 = CheckUtil::getcurTime(); + // printf("=========== cls time %ld waite time %ld\n", t13 - t11, t13 - t12); + + BLobToDetResult(); + + return 0; +} + +void worker(const unsigned char *img, int w, int startRow, int endRow, unsigned char *rowFlags) +{ + for (int y = startRow; y < endRow; y++) + { + const unsigned char *p = img + (size_t)y * w; + rowFlags[y] = 0; + for (int x = 0; x < w; x++) + { + if (p[x] != 0) + { + rowFlags[y] = 1; + break; + } + } + } +} +int ImgCheckAnalysisy::GetALLBlob() +{ + + std::string strBaseLog = "Blob"; + m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, strBaseLog, "image Blob Analysis Start"); + if (m_pImageAllResult->AIMaskImg.empty()) + { + return 1; + } + unsigned char *pGrayErrordata = (unsigned char *)m_pImageAllResult->AIMaskImg.data; + int width = m_pImageAllResult->AIMaskImg.cols; + int height = m_pImageAllResult->AIMaskImg.rows; + + long t1 = CheckUtil::getcurTime(); + + memset(&blobs, 0x00, sizeof(ERROR_DOTS_BLOBS)); + + ERROR_DOTS_BLOBS blobs_v1; + memset(&blobs_v1, 0x00, sizeof(ERROR_DOTS_BLOBS)); + ERROR_DOTS_BLOBS blobs_big; + memset(&blobs_big, 0x00, sizeof(ERROR_DOTS_BLOBS)); + ERROR_DOTS_BLOBS blobs_big_2; + memset(&blobs_big_2, 0x00, sizeof(ERROR_DOTS_BLOBS)); + printf("=====>>>> GetALLBlob m_strCurDetChannel %s \n", m_strCurDetChannel.c_str()); + if (m_strCurDetChannel == "CA") + { + printf("=====>>>>GetALLBlob USE CA %s \n", m_strCurDetChannel.c_str()); + GetBlobs_ALL_New(&blobs_v1, pGrayErrordata, m_ImgBlobHFlagData, width, height, 15); + m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, strBaseLog, "blob 1 num %d", blobs_v1.blobCount); + + if (blobs_v1.srcBlobCount >= _MAX_ERROR_DOT_BLOB) + { + + GetBlobs_ALL_New(&blobs_big, pGrayErrordata, m_ImgBlobHFlagData, width, height, 200); + m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, strBaseLog, "blob 2 num %d", blobs_big.blobCount); + + if (blobs_big.srcBlobCount >= _MAX_ERROR_DOT_BLOB) + { + + m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, strBaseLog, "blob 3 num %d", blobs_big_2.blobCount); + GetBlobs_ALL_New(&blobs_big_2, pGrayErrordata, m_ImgBlobHFlagData, width, height, 500); + } + } + } + else + { + GetBlobs_oneLabe(&blobs_v1, pGrayErrordata, m_ImgBlobHFlagData, width, height, 15); + m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, strBaseLog, "blob 1 num %d", blobs_v1.blobCount); + + if (blobs_v1.srcBlobCount >= _MAX_ERROR_DOT_BLOB) + { + + GetBlobs_oneLabe(&blobs_big, pGrayErrordata, m_ImgBlobHFlagData, width, height, 200); + m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, strBaseLog, "blob 2 num %d", blobs_big.blobCount); + + if (blobs_big.srcBlobCount >= _MAX_ERROR_DOT_BLOB) + { + + m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, strBaseLog, "blob 3 num %d", blobs_big_2.blobCount); + GetBlobs_oneLabe(&blobs_big_2, pGrayErrordata, m_ImgBlobHFlagData, width, height, 500); + } + } + } + + long t2 = CheckUtil::getcurTime(); + PushBlob(&blobs, &blobs_big_2); + PushBlob(&blobs, &blobs_big); + PushBlob(&blobs, &blobs_v1); + + m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, strBaseLog, "blob ALL num %d", blobs.blobCount); + // long t2 = CheckUtil::getcurTime(); + // printf(" BLob time %ld \n", t2 - t1); + + if (false || DetImgInfo_shareP->bsaveProcessImg) + { + int font_face = cv::FONT_HERSHEY_SIMPLEX; + double font_scale = 0.5; + int thickness = 1; + + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, strBaseLog, "Save Tem Img"); + cv::Mat tm = m_pImageAllResult->AIMaskImg.clone(); // 保持 mask 单通道,不再灰度转假彩 + for (int i = 0; i < blobs.blobCount; i++) + { + cv::Rect roi; + roi.x = blobs.blobTab[i].minx; + roi.y = blobs.blobTab[i].miny; + roi.width = blobs.blobTab[i].maxx - blobs.blobTab[i].minx + 1; + roi.height = blobs.blobTab[i].maxy - blobs.blobTab[i].miny + 1; + + if (blobs.blobTab[i].ErrType == 0) + { + cv::rectangle(tm, roi, CheckUtil::GetDrawColor(tm, cv::Scalar(0, 0, 255))); + } + else + { + cv::rectangle(tm, roi, CheckUtil::GetDrawColor(tm, cv::Scalar(0, 255, 255))); + } + + char buffer[128]; + sprintf(buffer, "id:%d type:%d m %0.1f", i,blobs.blobTab[i].ErrType, blobs.blobTab[i].density); + std::string text = buffer; + cv::Point origin = cv::Point(roi.x, roi.y); + cv::putText(tm, text, origin, font_face, font_scale, CheckUtil::GetDrawColor(tm, cv::Scalar(0, 255, 0)), thickness, 1, 0); + + // printf("type: %d %d %d %d %d %d %d\n", blobs.blobTab[i].ErrType, blobs.blobTab[i].area, blobs.blobTab[i].energy, roi.x, roi.y, roi.width, roi.height); + } + cv::imwrite(m_CheckResult_shareP->in_shareImage->strChannel + "_image_resize_blob.png", tm); + } + + long te = CheckUtil::getcurTime(); + m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, strBaseLog, "image Blob Analysis End use time %ld blob %ld ms", te - t1, t2 - t1); + + // getchar(); + return 0; +} + +int ImgCheckAnalysisy::AIMaskDet() +{ + + // 计算 mask 图片的一行 是否有 残点,用以加速 Blob的计算。 + if (m_ImgBlobHFlagData) + { + delete[] m_ImgBlobHFlagData; + m_ImgBlobHFlagData = NULL; + } + m_ImgBlobHFlagData = new unsigned char[m_pImageAllResult->detImg.rows]; + memset(m_ImgBlobHFlagData, 0, sizeof(unsigned char) * m_pImageAllResult->detImg.rows); + + while (true) + { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + std::shared_ptr AImaskResult; + { + std::lock_guard lock(mtx_AIMaskImgBLobQueue); + if (m_AIMaskImgBLobQueue.size() > 0) + { + AImaskResult = std::move(m_AIMaskImgBLobQueue.front()); + m_AIMaskImgBLobQueue.pop(); + // printf("size ============== %ld\n", m_AIMaskImgBLobQueue.size()); + } + } + + // 有AI mask 的结果 + if (AImaskResult) + { + // static int ss = 0; + cv::Mat &outimg = *(AImaskResult->output); + // cv::imwrite(std::to_string(ss++) + ".png", outimg); + + unsigned char *pGrayErrordata = (unsigned char *)outimg.data; + int width = outimg.cols; + int height = outimg.rows; + int start_Y = AImaskResult->roi.y; + if (cv::countNonZero(outimg != 0) != 0) + { + // #pragma omp parallel for + for (int y = 0; y < height; y++) + { + if (m_ImgBlobHFlagData[y + start_Y] == 0) + { + unsigned char *p = pGrayErrordata + y * width; + for (int x = 0; x < width; x++) + { + if (p[x] != 0) + { + m_ImgBlobHFlagData[y + start_Y] = 1; + break; + } + } + } + } + } + } + else + { + /* code */ + + if (m_AItask->isComplate()) + { + break; + } + } + } + // printf("===========================================1 \n"); + m_AItask->waitComplate(); + int rec = m_AItask->nresult; + if (rec != CHECK_OK) + { + return rec; + } + // printf("===========================================2 \n"); + return 0; +} + +int ImgCheckAnalysisy::Contours() +{ + // std::string strBaseLog = "Contours"; + // // m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, strBaseLog, "Contours Start"); + // cv::Mat detimg; + // { + // // 膨胀 + // cv::Mat se = getStructuringElement(0, Size(5, 5)); // 构造矩形结构元素 + // cv::dilate(m_pdetlog->temImgList[TEM_IMG_IDX_AImask], detimg, se); + // } + + // std::vector> contours; + // std::vector hierarchy; + // cv::findContours(detimg, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE); // 只找最外层轮廓 + + // // cv::cvtColor(m_pdetlog->temImgList[TEM_IMG_IDX_SrcCrop], m_pdetlog->temImgList[TEM_IMG_IDX_DrawSrc], cv::COLOR_GRAY2BGR); + + // for (int i = 0; i < contours.size(); ++i) + // { // 绘制所有轮廓 + // cv::drawContours(m_pdetlog->temImgList[TEM_IMG_IDX_DrawSrc], contours, i, cv::Scalar(255, 0, 255)); // thickness为-1时为填充整个轮廓 + // } + + // float fx = m_pdetlog->temImgList[TEM_IMG_IDX_Result].cols * 1.0f / m_pdetlog->temImgList[TEM_IMG_IDX_SrcCrop].cols; + // float fy = m_pdetlog->temImgList[TEM_IMG_IDX_Result].rows * 1.0f / m_pdetlog->temImgList[TEM_IMG_IDX_SrcCrop].rows; + // for (int i = 0; i < contours.size(); ++i) + // { + // for (int j = 0; j < contours.at(i).size(); ++j) + // { + // contours.at(i).at(j).x *= fx; + // contours.at(i).at(j).y *= fy; + // } + // } + // // cv::cvtColor(m_pdetlog->temImgList[TEM_IMG_IDX_Result], m_pdetlog->temImgList[TEM_IMG_IDX_Result], cv::COLOR_GRAY2BGR); + + // for (int i = 0; i < contours.size(); ++i) + // { // 绘制所有轮廓 + // cv::drawContours(m_pdetlog->temImgList[TEM_IMG_IDX_Result], contours, i, cv::Scalar(255, 0, 255)); // thickness为-1时为填充整个轮廓 + // } + // cv::imwrite("TEM_IMG_IDX_Result.png", m_pdetlog->temImgList[TEM_IMG_IDX_Result]); + // cv::imwrite("deeeee.png", m_pdetlog->temImgList[TEM_IMG_IDX_Drawmask]); + // getchar(); + // m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, strBaseLog, "Contours End"); + return 0; +} + +int ImgCheckAnalysisy::CheckImgInit() +{ + // 1、初始化 + + m_pDetResult->pQx_ErrorList = std::make_shared>(); + m_pdetlog->Init(); + m_pdetlog->addLogLevel = DET_LOG_LEVEL_3; + memset(&blobs, 0, sizeof(ERROR_DOTS_BLOBS)); + m_nCheckResultErrorCode = 0; + + m_Draw_qxImageResult.erase(m_Draw_qxImageResult.begin(), m_Draw_qxImageResult.end()); + + m_DetRoiList.Init(); + + return 0; +} + +int ImgCheckAnalysisy::ConfigCheck(cv::Mat img) +{ + + m_pdetlog->AddCheckstr(PrintLevel_0, "start", "Config Check"); + int re = CHECK_OK; + if (img.empty()) + { + m_nErrorCode = CHECK_ERROR_CheckImg_Empty; + m_pdetlog->AddCheckstr(PrintLevel_1, "Error", "check Img empty"); + return m_nErrorCode; + } + + // if (m_pCommonAnalysisyConfig->regionConfigArr.size() <= 0) + // { + // m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Error", "regionConfig Num = 0"); + // m_nErrorCode = CHECK_ERROR_Config_Value; + // return m_nErrorCode; + // } + + m_pdetlog->AddCheckstr(PrintLevel_0, "Succ", "Config Check Succ"); + return re; +} + +int ImgCheckAnalysisy::AI_Detect_Thread(const cv::Mat &img, cv::Mat &ResultImg) +{ + std::string cur_time_s = CheckUtil::getCurTimeHMS(); + + std::shared_ptr pAIDet; + // printf("=====>>>>AI_Detect_Thread m_strCurDetChannel %s \n", m_strCurDetChannel.c_str()); + bool bUseDarkModel = false; + std::string strCameraName; + { + // printf("=====>>>>AI_Detect_Thread USE CA %s \n", m_strCurDetChannel.c_str()); + // 暗场图像(相机名含 "dark",与 Define_Base.h ALIGN_REUSE_* 的明/暗场判定一致)用暗场检测模型(05DefectDark.engine); + // 其余(明场)用明场检测模型(01DefectLight.engine);暗场模型未加载成功时回退明场模型 + std::string strProduct; + GetProductCameraStr(strProduct, strCameraName); + bUseDarkModel = !ReplaceDarkWithBright(strCameraName).empty() && AI_Factory->Defect_Dark && AI_Factory->Defect_Dark->m_bInitSuccess; + pAIDet = bUseDarkModel ? AI_Factory->Defect_Dark : AI_Factory->Defect; + } + + + std::string strBaseLog = "AI_Detect"; + m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, strBaseLog, "AI_Detect Start"); + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, strBaseLog, "Detect Model %s (camera %s)", + bUseDarkModel ? "DefectDark" : "DefectLight", strCameraName.c_str()); + long t1, t2, t3; + t1 = CheckUtil::getcurTime(); + + int deal_image_width = pAIDet->input_0.width; + int deal_image_height = pAIDet->input_0.height; + if (img.empty() || deal_image_width <= 0 || deal_image_height <= 0) + { + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, strBaseLog, " img empty %d model input size error [w %d h %d]", img.empty(), deal_image_width, deal_image_height); + m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, strBaseLog, "AI_Detect End"); + return 1; + } + + // 不再拆分小图:整图 resize 到模型输入尺寸推理,输出 mask 再 resize 回原图尺寸 + cv::Mat inputImg; + cv::resize(img, inputImg, cv::Size(deal_image_width, deal_image_height)); + // 模型输入要求单通道;原图为彩色时按 BGR 顺序转灰度 + if (inputImg.channels() == 3) + { + cv::cvtColor(inputImg, inputImg, cv::COLOR_BGR2GRAY); + } + else if (inputImg.channels() == 4) + { + cv::cvtColor(inputImg, inputImg, cv::COLOR_BGRA2GRAY); + } + + // 临时存图:模型输入 + if (DetImgInfo_shareP->bsaveProcessImg) + { + cv::imwrite(DetImgInfo_shareP->strCameraName + "_AI_Det_in.png", inputImg); + } + + // 提交整图推理任务并等待结果 + std::shared_ptr task = std::make_shared(); + task->id = 0; + task->roi = cv::Rect(0, 0, img.cols, img.rows); + task->input = inputImg; + task->output = std::make_shared(); + task->engine = pAIDet; + + t2 = CheckUtil::getcurTime(); + runner->SubmitTask(task); + + std::shared_ptr result; + while (!runner->PopResult(result)) + { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + t3 = CheckUtil::getcurTime(); + + cv::Mat &outimg = *(result->output); // 模型输出尺寸的 mask + + // 输出 mask resize 回原图尺寸:mask 为缺陷二值图,用最近邻插值(避免插值晕圈被 Blob 当作缺陷多检出) + ResultImg = cv::Mat::zeros(img.size(), CV_8UC1); + if (!outimg.empty()) + { + cv::resize(outimg, ResultImg, img.size(), 0, 0, cv::INTER_NEAREST); + } + else + { + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, strBaseLog, " model output mask is empty, result mask all zero"); + } + + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, strBaseLog, " AI Run Time: sum %ld pre %ld Run %ld model in [w %d h %d] mask [w %d h %d] -> [w %d h %d]", + t3 - t1, t2 - t1, t3 - t2, deal_image_width, deal_image_height, outimg.cols, outimg.rows, img.cols, img.rows); + + // 供 AIMaskDet 标记 mask 有缺陷的行(加速 Blob 计算):整图坐标 + 整图尺寸 + { + std::shared_ptr maskTask = std::make_shared(); + maskTask->id = 0; + maskTask->roi = cv::Rect(0, 0, img.cols, img.rows); + maskTask->input = result->input; + maskTask->output = std::make_shared(ResultImg); + maskTask->engine = pAIDet; + std::lock_guard lock(mtx_AIMaskImgBLobQueue); + m_AIMaskImgBLobQueue.push(maskTask); + } + + // 留存 AI 输入/输出(模型尺寸),供缺陷结果按中心点取 AI 图(GetAIDetImg) + { + std::shared_ptr temAIresult = std::make_shared(); + temAIresult->roi = cv::Rect(0, 0, img.cols, img.rows); + temAIresult->AI_inImg = result->input; + temAIresult->AI_mask = outimg; + m_pImageAllResult->AI_Qx_MaskList.push_back(temAIresult); + } + + m_pdetlog->AddCheckstr(PrintLevel_0, DET_LOG_LEVEL_3, strBaseLog, "AI_Detect End"); + + if (m_pbaseCheckFunction->saveImg.bSaveAIDetImg && !ResultImg.empty()) + { + std::string strSaveDir = "/home/aidlux/BOE/AI_Detect/" + DetImgInfo_shareP->strImgProductID + "/"; + CheckUtil::CreateDir(strSaveDir); + std::string strSavePath_in = strSaveDir + cur_time_s + "_" + strCameraName + "_in.png"; + std::string strSavePath_out = strSaveDir + cur_time_s + "_" + strCameraName + "_out.png"; + CheckUtil::SaveImgAsync(strSavePath_in, m_CheckResult_shareP->cutSrcimg); + CheckUtil::SaveImgAsync(strSavePath_out, ResultImg); + } + + // 临时存图 + if (DetImgInfo_shareP->bsaveProcessImg) + { + cv::imwrite(DetImgInfo_shareP->strCameraName + "_AI_mask.png", ResultImg); + } + + // getchar(); + return 0; +} + +int ImgCheckAnalysisy::AI_QX_Class_Thread() +{ + + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "QX_Class", " Start"); + +#if !AI_CLASS_MODEL_ENABLE + // 当前暂无AI分类模型(Class.engine):所有缺陷先统一按 铜箔标记(TBMark) 类型返回; + // 原AI分类流程代码先预留(见下方),待分类模型部署后置 AI_CLASS_MODEL_ENABLE 为 1 即可恢复 + { + int defaultqx = AIClassTypeToConfigType(AI_CLass_QX_NAME_TBMark); + for (int i = 0; i < blobs.blobCount; i++) + { + blobs.blobTab[i].AIclasstype = defaultqx; + } + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "QX_Class", " no class model, all %d qx -> TBMark(%d)", blobs.blobCount, defaultqx); + } + return 0; +#endif // !AI_CLASS_MODEL_ENABLE + + // ==================== 以下为AI分类模型流程(模型未部署期间代码预留,勿删) ==================== + std::shared_ptr pAIDet; + // printf("=====>>>>AI_QX_Class_Thread m_strCurDetChannel %s \n", m_strCurDetChannel.c_str()); + { + // printf("=====>>>>AI_QX_Class_Thread USE CA %s \n", m_strCurDetChannel.c_str()); + pAIDet = AI_Factory->Class; + } + + int deal_image_width = pAIDet->input_0.width; + int deal_image_height = pAIDet->input_0.height; + + const int totalTasks = blobs.blobCount; + int submitted = 0; + int completed = 0; + int detblobIdx = 0; + int clsnum = 0; + // std::string str_Root = m_strRootPath + pDetConfig->strProductID + "_" + pDetConfig->strchannel + "_"; + long t2 = CheckUtil::getcurTime(); + while (completed < totalTasks) + { + // std::this_thread::sleep_for(std::chrono::milliseconds(1)); + if (submitted < totalTasks) + { + ERROR_DOTS_BLOB_DATA *pblob = &blobs.blobTab[submitted]; + if (pblob->ErrType == ERR_TYPE_2) + { + // ERR_TYPE_2不参与分类,按默认AI类别(把手标记)处理 + pblob->AIclasstype = CONFIG_QX_NAME_cell_CGMark; + submitted++; + completed++; + continue; + } + else + { + // 如果任务还没提交完,且当前处理任务数 < 2,提交新任务 + if (runner->GetProcessingCount() < 10) + { + clsnum++; + cv::Rect roi; + roi.x = pblob->minx; + roi.y = pblob->miny; + roi.width = pblob->maxx - pblob->minx + 1; + roi.height = pblob->maxy - pblob->miny + 1; + + std::shared_ptr task = std::make_shared(); + task->id = submitted; + int re = GetClassImg(m_pImageAllResult->detImg, task->input, roi, deal_image_width, deal_image_height); + if (re == 0) + { + task->bclass = true; + task->engine = pAIDet; + runner->SubmitTask(task); + } + else + { + completed++; + } + + submitted++; + } + } + } + + // 尝试取结果 + std::shared_ptr result; + if (runner->PopResult(result)) + { + + int temClass = result->cls_label; + + // 分类 label 映射:优先读映射配置文件(BM_cls.txt),读不到用默认顺序 + int cls_num = GetClsTypeByLabel(temClass); + + // std::string strclassName = AI_CLass_QX_NAME_Names[cls_num]; + // printf("AI Class num %d = %s %f\n", cls_num, strclassName.c_str(), result->cls_score); + + // 分类存图 开启。 + if (m_pbaseCheckFunction->saveImg.bSaveAlginImg) + { + + std::string saveimgpaht = ""; + + if (m_strCurDetChannel == "TA") + { + saveimgpaht = m_strRootPath_TA_cls + std::to_string(cls_num) + "/" + std::to_string(CheckUtil::getcurTime()) + "_" + std::to_string(result->cls_score) + ".png"; + } + else + { + saveimgpaht = m_strRootPath_CA_cls + std::to_string(cls_num) + "/" + std::to_string(CheckUtil::getcurTime()) + "_" + std::to_string(result->cls_score) + ".png"; + } + + { + m_pImageStorage->addImage(saveimgpaht, result->input); + } + } + + int configqx = AIClassTypeToConfigType(cls_num); + + ERROR_DOTS_BLOB_DATA *pblob = &blobs.blobTab[result->id]; + pblob->AIclasstype = configqx; + + completed++; + } + else + { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + } + // printf("=====>>>>AI_QX_Class_Thread clsnum %d \n", clsnum); + return 0; +} + +int ImgCheckAnalysisy::ThreadTask(int nId) +{ + + while (!m_bExit) + { + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + // 等待是否有任务 + std::shared_ptr task = m_task.GetTask(); + + // 把任务发送给对应的任务处理函数进行处理 + switch (task->taskname) + { + case Task_AI: + TaskFun_AIDet(task); + break; + case Task_Class: + TaskFun_QxClass(task); + break; + default: + break; + } + } + return 0; +} + +int ImgCheckAnalysisy::ResizeImg() +{ + + cv::Size sz; + sz.width = RESIZE_IMAGE_WIDTH; + + float fw = RESIZE_IMAGE_WIDTH * 1.0f / m_pImageAllResult->detImg.cols; + sz.height = int(m_pImageAllResult->detImg.rows * fw); + + // 结果图画布取自原图裁剪区(平移映射),保持原图通道(彩色原图 -> 真实彩色,不再灰度转假彩); + // 异常情况(如未走裁剪流程)直接用检测图兜底 + const cv::Mat &srcImg = m_CheckResult_shareP->in_shareImage->img; + if (m_CheckResult_shareP->in_shareImage->Det_Mode != DET_MODE_ReJson && + !srcImg.empty() && + m_deskewPadRoi.width > 0 && m_deskewPadRoi.height > 0 && + CheckUtil::RoiInImg(m_deskewPadRoi, srcImg)) + { + cv::resize(srcImg(m_deskewPadRoi), m_pImageAllResult->resultImg, sz); + } + else + { + cv::resize(m_pImageAllResult->detImg, m_pImageAllResult->resultImg, sz); + } + m_CheckResult_shareP->resultimg = m_pImageAllResult->resultImg; + m_pdetlog->AddCheckstr(PrintLevel_1, "showImg size", " [w %d,h %d]-> show img [w %d,h %d] ", + m_outer_roi.width, m_outer_roi.height, sz.width, sz.height); + return 0; +} + +void ImgCheckAnalysisy::TaskFun_AIDet(std::shared_ptr task) +{ + task->SetStatus(TaskStep_run); + long t1, t2; + // AI 推理生成 + { + t1 = CheckUtil::getcurTime(); + + int rec = AI_Detect_Thread(m_pImageAllResult->detImg, m_pImageAllResult->AIMaskImg); + + t2 = CheckUtil::getcurTime(); + task->nresult = rec; + } + task->SetStatus(TaskStep_compate); +} + +void ImgCheckAnalysisy::TaskFun_QxClass(std::shared_ptr task) +{ + task->SetStatus(TaskStep_run); + long t1, t2; + // AI 推理生成 + { + t1 = CheckUtil::getcurTime(); + + int rec = AI_QX_Class_Thread(); + + t2 = CheckUtil::getcurTime(); + task->nresult = rec; + } + task->SetStatus(TaskStep_compate); +} + +int ImgCheckAnalysisy::Update_DetRoiList() +{ + float fx = m_pImageAllResult->fscale_detToresult_x; + float fy = m_pImageAllResult->fscale_detToresult_y; + + // 参数多边形 -> 检测图(裁剪坐标系) 的映射在裁剪时已算好(m_matParam2Det) + for (const auto ®ion : m_pCommonAnalysisyConfig->regionConfigArr) + { + m_DetRoiList.Update_Mat(region.basicInfo.pointArry, m_matParam2Det, fx, fy); + } + + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "info", "roiList_Src roi num %ld", m_DetRoiList.roiList_Src.size()); + // m_DetRoiList.print("m_DetRoiList"); + return 0; +} + +int ImgCheckAnalysisy::DrawResult_Step_1() +{ + + // std::cout << m_pBasicConfig->bDrawPreRoi << std::endl; + if (m_pBasicConfig->bDrawPreRoi) + { + int n = 0; + // std::cout << m_DetRoiList.roiList_Show.size() << std::endl; + for (const auto &polygon : m_DetRoiList.roiList_Show) + { + // n++; + // if (n == 1) + // { + // continue; + // } + + // std::cout << polygon << std::endl; + + // 绘制多边形的边界(不填充),线宽为1;颜色按画布通道自适应 + cv::polylines(m_CheckResult_shareP->resultimg, polygon, true, + CheckUtil::GetDrawColor(m_CheckResult_shareP->resultimg, cv::Scalar(128, 128, 255)), 1); // true表示闭合多边形 + } + // cv::imwrite(DetImgInfo_shareP->strChannel + "roi_src.png", m_pdetlog->temImgList[TEM_IMG_IDX_DrawSrc]); + // cv::imwrite(DetImgInfo_shareP->strChannel + "roi_ss.png", m_pdetlog->temImgList[TEM_IMG_IDX_Result]); + + // getchar(); + } + + float fs_resize_x = m_pImageAllResult->resultImg.cols * 1.0f / m_pImageAllResult->detImg.cols; + float fs_resize_y = m_pImageAllResult->resultImg.rows * 1.0f / m_pImageAllResult->detImg.rows; + + + // 注:不再绘制 inner 定位蓝色框(m_inner_rroi 为大轮廓裁剪子图局部坐标,直接映射会缺裁剪原点偏移) + + if (m_pbaseCheckFunction && m_pbaseCheckFunction->markLine.region.size() > 1) + { + std::vector product_roi_show; + for (const auto &pt : m_pbaseCheckFunction->markLine.region) + { + // 参数(模板)坐标 -> 检测图(裁剪) -> 结果图 + cv::Point2f detPt = MatAffinePoint(m_matParam2Det, cv::Point2f((float)pt.x, (float)pt.y)); + cv::Point draw_pt; + draw_pt.x = cvRound(detPt.x * fs_resize_x); + draw_pt.y = cvRound(detPt.y * fs_resize_y); + product_roi_show.push_back(draw_pt); + } + + cv::polylines( + m_CheckResult_shareP->resultimg, + product_roi_show, + true, + CheckUtil::GetDrawColor(m_CheckResult_shareP->resultimg, cv::Scalar(0, 255, 0)), + 1); + } + if (m_pEdge_Align_Result.get() != NULL) + { + + if (m_pbaseCheckFunction->markLine.bDraw) + { + for (size_t i = 0; i < m_pEdge_Align_Result->markresulList.size(); i++) + { + // if (m_pEdge_Align_Result->markresulList[i].status == Mark_Result_Status_OK) + { + const cv::Point &markPt = m_pEdge_Align_Result->markresulList[i].det_Local_DetImg; + cv::Point2f detPt = MatAffinePoint(m_matSrc2Det, cv::Point2f((float)markPt.x, (float)markPt.y)); + cv::Point p; + p.x = cvRound(detPt.x * fs_resize_x); + p.y = cvRound(detPt.y * fs_resize_y); + cv::circle(m_CheckResult_shareP->resultimg, p, 5, + CheckUtil::GetDrawColor(m_CheckResult_shareP->resultimg, cv::Scalar(0, 255, 0))); + } + } + } + } + + // 支架偏移检测绘制(受参数 bDrawSupport 控制,默认关闭): + // 支架中心十字(绿=正常/红=偏移NG)+ 各方向"所计算的产品边缘"线段(抗凸起拟合直边,见 CheckRun 支架块) + // + 支架中心到该边缘的连线(连线长度=支架中心到所计算边缘的距离;绿=正常/红=超标准)+ 实测/标准(mm)文字 + if (m_SupportDrawInfo.bValid && m_pbaseCheckFunction->supportDet.bDrawSupport && !m_CheckResult_shareP->resultimg.empty()) + { + // 原图坐标 -> 检测图(裁剪) -> 结果图 + auto srcToResult = [&](const cv::Point2f &pt) -> cv::Point { + cv::Point2f detPt = MatAffinePoint(m_matSrc2Det, pt); + return cv::Point(cvRound(detPt.x * fs_resize_x), cvRound(detPt.y * fs_resize_y)); + }; + + // 支架中心十字:NG 红色 / 正常绿色 + cv::Point pCenter = srcToResult(m_SupportDrawInfo.ptCenter); + cv::Scalar colorCross = m_SupportDrawInfo.bOffset ? cv::Scalar(0, 0, 255) : cv::Scalar(0, 255, 0); + colorCross = CheckUtil::GetDrawColor(m_CheckResult_shareP->resultimg, colorCross); + const int nCrossLen = 40; // 结果图上十字半长(px) + cv::line(m_CheckResult_shareP->resultimg, cv::Point(pCenter.x - nCrossLen, pCenter.y), cv::Point(pCenter.x + nCrossLen, pCenter.y), colorCross, 2); + cv::line(m_CheckResult_shareP->resultimg, cv::Point(pCenter.x, pCenter.y - nCrossLen), cv::Point(pCenter.x, pCenter.y + nCrossLen), colorCross, 2); + + // 各方向:所计算边缘线段(抗凸起拟合直边/回退外接矩形边) + 中心到边缘点的连线(长度=点到所计算边缘) + 实测/标准文字 + // 几何(边缘点/线段两端点)由 CheckRun 支架块算好存入 edgeDraw,此处只做坐标映射与绘制 + char strTxt[64]; + for (size_t i = 0; i < m_SupportDrawInfo.edgeList.size(); i++) + { + const Support_Draw_Info_::Edge_Draw_ &edgeDraw = m_SupportDrawInfo.edgeList[i]; + + cv::Point pEdge = srcToResult(edgeDraw.ptEdge); + cv::Point pEdgeA = srcToResult(edgeDraw.ptEdgeA); + cv::Point pEdgeB = srcToResult(edgeDraw.ptEdgeB); + + // 超标准红色 / 正常绿色 + cv::Scalar colorEdge = edgeDraw.bExceed ? cv::Scalar(0, 0, 255) : cv::Scalar(0, 255, 0); + colorEdge = CheckUtil::GetDrawColor(m_CheckResult_shareP->resultimg, colorEdge); + + // 1、所计算边缘线段:标出参与计算的产品边缘(细线;抗凸起拟合直边/回退边) + cv::line(m_CheckResult_shareP->resultimg, pEdgeA, pEdgeB, colorEdge, 1); + + // 2、中心 -> 边缘点连线:长度即支架中心到所计算边缘的距离 + cv::line(m_CheckResult_shareP->resultimg, pCenter, pEdge, colorEdge, 2); + + // 3、距离文字:实测/标准(mm) + snprintf(strTxt, sizeof(strTxt), "%s %.0f/%.0f", edgeDraw.strEdge.c_str(), edgeDraw.fDist, edgeDraw.fStandard); + cv::putText(m_CheckResult_shareP->resultimg, strTxt, + cv::Point((pCenter.x + pEdge.x) / 2 + 6, (pCenter.y + pEdge.y) / 2 - 6), + cv::FONT_HERSHEY_SIMPLEX, 0.8, colorEdge, 2); + } + } + + // cv::imwrite("sss.png", m_CheckResult_shareP->resultimg); + return 0; +} + +int ImgCheckAnalysisy::AIClassTypeToConfigType(int nAIQXType) +{ + + // 只有主检测+分类的缺陷在映射表中;支架偏移/支架缺失/把手缺失(传统检出)、铜箔标签(标签检测)不走AI分类 + int resultError_type = CONFIG_QX_NAME_cell_CGMark; + switch (nAIQXType) + { + case AI_CLass_QX_NAME_CGMark: + resultError_type = CONFIG_QX_NAME_cell_CGMark; + break; + case AI_CLass_QX_NAME_BQBMark: + resultError_type = CONFIG_QX_NAME_cell_BQBMark; + break; + case AI_CLass_QX_NAME_TBMark: + resultError_type = CONFIG_QX_NAME_cell_TBMark; + break; + case AI_CLass_QX_NAME_count: // 无效类别(分类映射配置中不存在):不参与缺陷分析 + resultError_type = CONFIG_QX_NAME_count; + break; + default: + break; + } + return resultError_type; +} + +int ImgCheckAnalysisy::UpdateImgageScale() +{ + + if (m_pBasicConfig->fImage_Scale_x > 0 && m_pBasicConfig->fImage_Scale_x < 1 && + m_pBasicConfig->fImage_Scale_y > 0 && m_pBasicConfig->fImage_Scale_y < 1) + { + + m_fImgage_Scale_X = m_pBasicConfig->fImage_Scale_x; + m_fImgage_Scale_Y = m_pBasicConfig->fImage_Scale_y; + } + + return 0; +} + + +int ImgCheckAnalysisy::BLobToDetResult() +{ + long t1 = CheckUtil::getcurTime(); + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "BLobToDetResult", " Start old qx num %ld", m_pDetResult->pQx_ErrorList->size()); + + // 遍历每个检测blob + for (int i = 0; i < blobs.blobCount; i++) + { + cv::Rect roi; + roi.x = blobs.blobTab[i].minx; + roi.y = blobs.blobTab[i].miny; + roi.width = blobs.blobTab[i].maxx - blobs.blobTab[i].minx + 1; + roi.height = blobs.blobTab[i].maxy - blobs.blobTab[i].miny + 1; + + int config_qx_type = blobs.blobTab[i].AIclasstype; + if (config_qx_type < 0 || config_qx_type >= CONFIG_QX_NAME_count) + { + // AI 分类无对应缺陷类型(无效类别)时,不参与缺陷结果分析 + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "BLobToDetResult", + "blob idx %d invalid AIclasstype %d skip", i, config_qx_type); + continue; + } + + float JudgArea = blobs.blobTab[i].JudgArea; + float fsecondArea = JudgArea; + std::string qx_name = CONFIG_QX_NAME_Names[config_qx_type]; + + if (true) + { + QX_ERROR_INFO_ temerror; + temerror.roi = roi; + temerror.Idx = m_pDetResult->pQx_ErrorList->size(); + temerror.area = blobs.blobTab[i].area; + temerror.JudgArea = JudgArea; + temerror.JudgArea_second = fsecondArea; + temerror.energy = blobs.blobTab[i].energy; + temerror.flen = blobs.blobTab[i].len; + temerror.fbreadth = blobs.blobTab[i].breadth; + temerror.nconfig_qx_type = config_qx_type; + temerror.qx_name = qx_name; + temerror.maxValue = blobs.blobTab[i].maxValue; + temerror.grayDis = blobs.blobTab[i].grayDis; + temerror.density = blobs.blobTab[i].density; + temerror.fUpIou = 0; + temerror.whiteOrBlack = blobs.blobTab[i].whiteOrblack; + + { + 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 &polygon = m_DetRoiList.roiList_Src[iregion]; + double result = cv::pointPolygonTest(polygon, pCenter, false); + if (result < 0) + { + continue; + } + nmaxregionIdx = iregion; + } + + temerror.detRegionidxList.push_back(nmaxregionIdx); + } + + // { + // cv::Point pCenter; + // pCenter.x = roi.x + roi.width * 0.5; + // pCenter.y = roi.y + roi.height * 0.5; + // for (int iregion = 0; iregion < m_DetRoiList.roiList_Src.size(); iregion++) + // { + // const std::vector &polygon = m_DetRoiList.roiList_Src[iregion]; + // double result = cv::pointPolygonTest(polygon, pCenter, false); + // if (result < 0) + // { + // continue; + // } + // temerror.detRegionidxList.push_back(iregion); + // } + // if (temerror.detRegionidxList.size() <= 0) + // { + // temerror.detRegionidxList.push_back(0); + // } + // } + + m_pDetResult->pQx_ErrorList->push_back(temerror); + + // printf("- %s idx %d a %f v %d h %f l %f\n", DetImgInfo_shareP->strChannel.c_str(), i, JudgArea, blobs.blobTab[i].maxValue, blobs.blobTab[i].grayDis, flen); + } + } + long t2 = CheckUtil::getcurTime(); + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "BLobToDetResult", " end qx num %ld use time %ld", m_pDetResult->pQx_ErrorList->size(), t2 - t1); + return 0; +} + +int ImgCheckAnalysisy::AI_Edge(const cv::Mat &img, cv::RotatedRect &outerRoi, cv::RotatedRect &innerRoi, vector &tagroiList) +{ + m_pdetlog->AddCheckstr(PrintLevel_0, "AI_Edge", "-------------------start--------------"); + + AI_Edge_Algin::DetConfig config; + + if (DetImgInfo_shareP->bsaveProcessImg) + { + config.bSaveResultImg = true; + } + config.strChannel = DetImgInfo_shareP->strChannel; + config.pBaseCheckFunction = m_pbaseCheckFunction; + config.bDetTag = m_pbaseCheckFunction->supportDet.bDetTag; // 标签检测开关:关闭时不跑 Tag_Loc + config.strProductID = DetImgInfo_shareP->strImgProductID; // 定位存图文件名首位使用产品ID + + /* 定位获取三个roi */ + int re = m_pAI_Edge_Algin.Detect(img, &config, m_pEdge_Align_Result); + if (re != 0) + { + printf("AI_Edge Is Error = %d \n", re); + } + if (!m_pEdge_Align_Result->bigroi.empty()) + { + outerRoi = m_pEdge_Align_Result->bigroi[0]; + } + if (!m_pEdge_Align_Result->smallroi.empty()) + { + innerRoi = m_pEdge_Align_Result->smallroi[0]; + } + // 无条件赋值:Tag_Loc 未检出或 bDetTag 关闭(tagroi 为空)时清空,避免残留上一张图的标签 + tagroiList = m_pEdge_Align_Result->tagroi; + + /* 用 outer 定位 mask(bigmask) 分离凸起区域,检测把手是否缺失 */ + { + long handle_s = CheckUtil::getcurTime(); + CheckHandleByBigMask(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; +} + +// 用 outer 定位 mask(bigmask) 检测把手(产品主体是圆角矩形、把手基本出现在四角附近): +// 1、分离凸起区域:拟合主体上/下/左/右四条边缘,把边缘外的掩膜按连通域切分成凸起区域 +// (提取细节见 CheckUtil::GetMaskProtrusions,含就近角/质心/面积/深度/填充率) +// 2、参数里每个非空的"把手区域"附近都应该有一个把手:在区域附近找凸起, +// 凸起宽高与区域外接矩形宽高相近(尺寸比区间见 handle_size_min/handle_size_max,可 JSON 配置)才算匹配 +// 3、凸起本身不算 NG,只有"某个把手区域附近找不到匹配的凸起"才算把手缺失(NG 由 CheckRun 上报) +// 4、-s(存过程图)时输出 {通道}_handle_det.jpg:绿色四边形=拟合主体、红框=凸起(带编号/角/深度)、 +// 蓝框=参数把手区域、绿框=匹配到的把手,便于从图像观察问题 +// 5、拟合出的主体四边形存 m_HandleBodyQuad:支架偏移检测复用其四边(抗凸起)做测距与绘制 +int ImgCheckAnalysisy::CheckHandleByBigMask(const cv::Mat &bigmask) +{ + m_bHandleDetSucc = false; + m_bHandleFound = false; + m_bHandleExpect = false; + m_HandleProtrusionList.clear(); + m_HandleRegionResultList.clear(); + m_HandleBodyQuad.clear(); + + if (bigmask.empty()) + { + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Handle", "bigmask is empty, skip handle detect"); + return 1; + } + + // 1、从 bigmask 分离凸起区域 + // 最小深度 = 系数 × 量化步长:mask 由模型输出 resize 回原图,直边带量化台阶毛刺(台阶高≈模型 1 像素=量化步长); + // 系数优先取参数配置(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); + } + } + // m_HandleBodyQuad:拟合出的主体四边形(左上/右上/右下/左下);-s 存图观察用,支架偏移检测复用其四边做抗凸起测距/绘制 + int re = CheckUtil::GetMaskProtrusions(bigmask, m_HandleProtrusionList, nMinDepth, nMinArea, &m_HandleBodyQuad, nEdgeMargin); + if (re != 0) + { + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Handle", "GetMaskProtrusions error %d", re); + return re; + } + m_bHandleDetSucc = true; + + // 2、把手检测开关 + 把手区域(最多4个,非空区域表示该处应该有一个把手) + bool bDetHandle = false; + std::vector regionBoxList; + if (m_pbaseCheckFunction != NULL) + { + const Function_Support_Det &supportDet = m_pbaseCheckFunction->supportDet; + bDetHandle = supportDet.bDetHandle; + for (size_t i = 0; i < supportDet.handleBoxes.size() && i < HANDLE_REGION_MAX_NUM; i++) + { + regionBoxList.push_back(supportDet.handleBoxes[i]); + } + } + 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_HandleProtrusionList.size(), regionBoxList.size(), BOOL_TO_STR(bDetHandle), BOOL_TO_STR(m_bHandleExpect)); + + // 3、凸起区域实测特征(就近角/面积/深度/填充率,用于日志排查) + for (size_t i = 0; i < m_HandleProtrusionList.size(); i++) + { + const Mask_Protrusion_ &protr = m_HandleProtrusionList[i]; + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Handle", + "protrusion %ld corner %d roi [%d %d %d %d] len %.0f depth %.0f area %0.0f fill %0.2f", + i, protr.nCorner, protr.roi.x, protr.roi.y, protr.roi.width, protr.roi.height, + protr.fLen, protr.fDepth, protr.fArea, protr.fFillRatio); + } + + // 4、逐个把手区域:在区域附近找宽高匹配的凸起(同一个凸起只匹配给一个区域) + std::vector usedList(m_HandleProtrusionList.size(), false); + int nFound = 0; + for (size_t ir = 0; ir < regionBoxList.size(); ir++) + { + Handle_Region_Result_ result; + result.nIdx = (int)ir + 1; + result.boxRegion = regionBoxList[ir]; + int nMatchIdx = MatchHandleRegion(m_HandleProtrusionList, usedList, result.boxRegion, result.roiHandle); + result.bFound = (nMatchIdx >= 0); + if (result.bFound) + { + usedList[nMatchIdx] = true; + nFound++; + } + m_HandleRegionResultList.push_back(result); + + std::string strResult = "handle missing !!"; + if (result.bFound) + { + strResult = "handle roi [" + std::to_string(result.roiHandle.x) + " " + std::to_string(result.roiHandle.y) + " " + + std::to_string(result.roiHandle.width) + " " + std::to_string(result.roiHandle.height) + "]"; + } + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Handle", "handle region %d box [%d %d %d %d] -> %s", + result.nIdx, result.boxRegion.x, result.boxRegion.y, result.boxRegion.width, result.boxRegion.height, + strResult.c_str()); + } + m_bHandleFound = (!regionBoxList.empty() && nFound == (int)regionBoxList.size()); + + 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 !!"); + + // 5、存过程图(-s 时):用于从图像观察把手提取/匹配情况; + // 绿色四边形=拟合出的主体四条边缘;红框=分离出的凸起区域(带编号/就近角/深度); + // 蓝框=参数把手区域;绿框=匹配到的把手 + if (DetImgInfo_shareP->bsaveProcessImg) + { + cv::Mat show; + if (bigmask.channels() == 1) + { + cv::cvtColor(bigmask, show, cv::COLOR_GRAY2BGR); + } + else + { + show = bigmask.clone(); + } + // 主体四边形(拟合出的四条边缘) + if (m_HandleBodyQuad.size() == 4) + { + std::vector vecQuad; + for (size_t i = 0; i < m_HandleBodyQuad.size(); i++) + { + vecQuad.push_back(cv::Point(cvRound(m_HandleBodyQuad[i].x), cvRound(m_HandleBodyQuad[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++) + { + cv::rectangle(show, m_HandleRegionResultList[i].boxRegion, cv::Scalar(255, 0, 0), 8); + if (m_HandleRegionResultList[i].bFound) + { + cv::rectangle(show, m_HandleRegionResultList[i].roiHandle, cv::Scalar(0, 255, 0), 12); + } + } + cv::Mat showSmall; + cv::resize(show, showSmall, cv::Size(show.cols / 4, show.rows / 4)); + std::vector paramJpg = {cv::IMWRITE_JPEG_QUALITY, 90}; + cv::imwrite(DetImgInfo_shareP->strChannel + "_handle_det.jpg", showSmall, paramJpg); + } + + return 0; +} + +// 标签检测(独立步骤):用 (图像 + 定位结果) 跑 Tag_Loc 模型 +// 裁剪区域与定位流程里 Tag_Loc 的口径完全一致(定位得到的产品外接旋转矩形): +// 明场相机用自己的定位结果;暗场相机没做定位模型,用复用明场的结果 —— 保证暗/明场对同一摆位切出同样区域。 +// 注意:标签模型对裁剪尺度很敏感(裁剪差 3px 就会让检出个数变化),不能另用 bigmask 重推裁剪框。 +// bigmask 仅在定位外接矩形异常时作兜底(最大轮廓外接框)。 +// 检出的标签 roi 存 m_tag_roiList(裁剪图坐标系,与定位流程 tagroi 的约定一致),由 CheckRun 统一上报 NG。 +int ImgCheckAnalysisy::CheckTagByBigMask(const cv::Mat &img, const cv::Mat &bigmask) +{ + m_tag_roiList.clear(); + if (!AI_Factory || !AI_Factory->Tag_Loc) + { + m_pdetlog->AddCheckstr(PrintLevel_0, "Error", "Tag_Loc model is null"); + return 1; + } + if (img.empty()) + { + m_pdetlog->AddCheckstr(PrintLevel_0, "Error", "tag check img is empty"); + return 2; + } + + // 1、裁剪区域:与定位流程 Tag_Loc 的裁剪口径一致(产品外接旋转矩形,暗场为复用明场的值) + cv::Rect roiCrop = m_outer_rroi.boundingRect() & cv::Rect(0, 0, img.cols, img.rows); + if (roiCrop.width <= 0 || roiCrop.height <= 0) + { + // 异常兜底:定位外接矩形无效时,用 bigmask 最大轮廓外接框 + if (bigmask.empty()) + { + m_pdetlog->AddCheckstr(PrintLevel_0, "Error", "tag check roi invalid and bigmask is empty"); + return 3; + } + cv::Mat binMask; + { + cv::Mat grayMask; + if (bigmask.channels() != 1) + { + cv::cvtColor(bigmask, grayMask, cv::COLOR_BGR2GRAY); + } + else + { + grayMask = bigmask; + } + double dMin = 0; + double dMax = 0; + cv::minMaxLoc(grayMask, &dMin, &dMax); + // 兼容 0/1 与 0/255 掩膜:取半强度作阈值(重采样后的边缘取到真实边界) + cv::threshold(grayMask, binMask, dMax > 1 ? dMax * 0.5 : 0, 255, cv::THRESH_BINARY); + } + bool bFound = false; + std::vector contour = CheckUtil::getLargestContourROI(binMask, bFound); + if (!bFound || contour.size() < 3) + { + m_pdetlog->AddCheckstr(PrintLevel_0, "Error", "tag check bigmask contour too few"); + return 4; + } + roiCrop = cv::minAreaRect(contour).boundingRect() & cv::Rect(0, 0, img.cols, img.rows); + if (roiCrop.width <= 0 || roiCrop.height <= 0) + { + m_pdetlog->AddCheckstr(PrintLevel_0, "Error", "tag check crop roi error [%d %d %d %d]", roiCrop.x, roiCrop.y, roiCrop.width, roiCrop.height); + return 5; + } + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Tag", "tag check use bigmask roi [%d %d %d %d]", + roiCrop.x, roiCrop.y, roiCrop.width, roiCrop.height); + } + + // 2、裁剪 -> resize 到 Tag_Loc 模型输入尺寸 -> 灰度 + std::shared_ptr pTag = AI_Factory->Tag_Loc; + cv::Size sz(pTag->input_0.width, pTag->input_0.height); + if (sz.width <= 0 || sz.height <= 0) + { + m_pdetlog->AddCheckstr(PrintLevel_0, "Error", "tag check invalid model input size"); + return 6; + } + cv::Mat detImg; + cv::resize(img(roiCrop), detImg, sz); + if (detImg.channels() == 3) + { + cv::cvtColor(detImg, detImg, cv::COLOR_BGR2GRAY); + } + else if (detImg.channels() == 4) + { + cv::cvtColor(detImg, detImg, cv::COLOR_BGRA2GRAY); + } + + // 3、推理 + cv::Mat mask; + int re = pTag->AIDet(detImg, mask); + if (re != 0) + { + m_pdetlog->AddCheckstr(PrintLevel_0, "Error", "Tag_Loc AIDet error %d", re); + return 100 + re; + } + if (mask.empty()) + { + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Tag", "tag mask empty, no tag"); + return 0; + } + + if (m_pbaseCheckFunction->saveImg.bSaveBQDetImg) + { + std::string cur_time_s = CheckUtil::getCurTimeHMS(); + std::string strSaveDir = "/home/aidlux/BOE/AI_Detect/" + DetImgInfo_shareP->strImgProductID + "/"; + CheckUtil::CreateDir(strSaveDir); + std::string strSavePath_out = strSaveDir + cur_time_s + "_" + DetImgInfo_shareP->strCameraName + "_BQ_out.png"; + CheckUtil::SaveImgAsync(strSavePath_out, mask); + } + + // 调试存图(仅 -s 时保存) + if (DetImgInfo_shareP && DetImgInfo_shareP->bsaveProcessImg) + { + cv::imwrite(DetImgInfo_shareP->strChannel + "_tag_in.png", detImg); + cv::imwrite(DetImgInfo_shareP->strChannel + "_tag_out_mask.png", mask); + } + + // 4、mask 轮廓 -> 还原到裁剪图坐标 -> RotatedRect(与定位流程 tagroi 的坐标系一致) + std::vector> contours; + std::vector hierarchy; + cv::findContours(mask.clone(), contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); + const float scale_x = (float)roiCrop.width / (float)sz.width; + const float scale_y = (float)roiCrop.height / (float)sz.height; + for (size_t i = 0; i < contours.size(); i++) + { + if (contours[i].size() < 3 || cv::contourArea(contours[i]) <= 0) + { + continue; + } + std::vector srcContour; + srcContour.reserve(contours[i].size()); + for (const auto &pt : contours[i]) + { + srcContour.emplace_back(pt.x * scale_x, pt.y * scale_y); + } + m_tag_roiList.emplace_back(cv::minAreaRect(srcContour)); + } + // 面积从大到小排序(与定位流程 tagroi 的顺序一致) + std::sort(m_tag_roiList.begin(), m_tag_roiList.end(), + [](const cv::RotatedRect &a, const cv::RotatedRect &b) + { return a.size.width * a.size.height > b.size.width * b.size.height; }); + + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Tag", "tag region num %ld crop [%d %d %d %d] model [%d %d]", + m_tag_roiList.size(), roiCrop.x, roiCrop.y, roiCrop.width, roiCrop.height, sz.width, sz.height); + return 0; +} + +// 在某个把手区域附近找宽高匹配的凸起 +// 匹配条件:1、凸起中心落在把手区域附近(区域宽/高各外扩 fNearRatio 倍) +// 2、凸起宽高与该区域外接矩形宽高的比值都在 [fSizeMin, fSizeMax](允许横竖互换) +// 范围参数优先取参数配置(JSON handle_param 下的 handle_search_ratio/handle_size_min/handle_size_max), +// 未配置时用文件顶部宏的默认值 +// 返回凸起在 candList 中的下标,-1 表示没找到 +int ImgCheckAnalysisy::MatchHandleRegion(const std::vector &candList, const std::vector &usedList, + const cv::Rect &boxRegion, cv::Rect &roiHandle) +{ + if (boxRegion.width <= 0 || boxRegion.height <= 0) + { + return -1; + } + + // 匹配范围参数:JSON 配置优先,未配置时用默认值 + float fNearRatio = 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::Point pRegionCenter(boxRegion.x + boxRegion.width / 2, boxRegion.y + boxRegion.height / 2); + + int nBestIdx = -1; + double fBestDis = 1e18; + int nNearIdx = -1; // 区域内离区域中心最近的凸起(仅用于日志排查) + double fNearDis = 1e18; + for (size_t i = 0; i < candList.size(); i++) + { + if (i < usedList.size() && usedList[i]) + { + continue; + } + const Mask_Protrusion_ &cand = candList[i]; + const cv::Rect &roi = cand.roi; + if (roi.width <= 0 || roi.height <= 0) + { + continue; + } + + // 位置:凸起中心要落在把手区域附近 + cv::Point pCenter(roi.x + roi.width / 2, roi.y + roi.height / 2); + if (!searchBox.contains(pCenter)) + { + continue; + } + double fDis = CheckUtil::calDis(pCenter, pRegionCenter); + if (fDis < fNearDis) + { + fNearDis = fDis; + nNearIdx = (int)i; + } + + // 宽高:与把手区域外接矩形的宽高相近(允许横竖互换) + float fw = (float)roi.width; + float fh = (float)roi.height; + bool bSizeOK = (fw >= boxRegion.width * fSizeMin && fw <= boxRegion.width * fSizeMax && + fh >= boxRegion.height * fSizeMin && fh <= boxRegion.height * fSizeMax); + if (!bSizeOK) + { + bSizeOK = (fw >= boxRegion.height * fSizeMin && fw <= boxRegion.height * fSizeMax && + fh >= boxRegion.width * fSizeMin && fh <= boxRegion.width * fSizeMax); + } + if (!bSizeOK) + { + continue; + } + + if (fDis < fBestDis) + { + fBestDis = fDis; + nBestIdx = (int)i; + } + } + + if (nBestIdx < 0 && nNearIdx >= 0) + { + // 没匹配上时打印区域内最近的凸起,便于排查阈值 + const cv::Rect &roi = candList[nNearIdx].roi; + m_pdetlog->AddCheckstr(PrintLevel_1, DET_LOG_LEVEL_3, "Handle", + "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, + candList[nNearIdx].nCorner, roi.x, roi.y, roi.width, roi.height, + (float)roi.width / (float)boxRegion.width, (float)roi.height / (float)boxRegion.height); + } + + if (nBestIdx >= 0) + { + roiHandle = candList[nBestIdx].roi; + } + return nBestIdx; +} + +int ImgCheckAnalysisy::CalProductSize() +{ + float fw = m_outer_roi.width * m_fImgage_Scale_X; + float fh = m_outer_roi.height * m_fImgage_Scale_Y; + fw = std::ceil(fw * 10) / 10; + fh = std::ceil(fh * 10) / 10; + m_CheckResult_shareP->productWidht_mm = fw; + m_CheckResult_shareP->productHeight_mm = fh; + + m_pdetlog->AddCheckstr(PrintLevel_1, "Det ROI", " [w %d,h %d] (piexl) -> [w %f h %f](mm),scale %f %f", + m_outer_roi.width, m_outer_roi.height, fw, fh, m_fImgage_Scale_X, m_fImgage_Scale_Y); + return 0; +} + +int ImgCheckAnalysisy::ImgPreDet() +{ + + // 计算产品尺寸 + CalProductSize(); + + if (m_pbaseCheckFunction->saveImg.bSaveClsImg) + { + creatsavedir(); + } + + // 裁剪图(_RE_CutImg)保留原图通道:源图为彩色时用彩色裁剪图,否则回退灰度检测图 + m_CheckResult_shareP->cutSrcimg = m_pImageAllResult->detColorImg.empty() + ? m_pImageAllResult->detImg + : m_pImageAllResult->detColorImg; + ResizeImg(); + + m_pImageAllResult->fscale_detToresult_x = m_pImageAllResult->resultImg.cols * 1.0f / m_pImageAllResult->detImg.cols; + m_pImageAllResult->fscale_detToresult_y = m_pImageAllResult->resultImg.rows * 1.0f / m_pImageAllResult->detImg.rows; + return 0; +} diff --git a/ConfigModule/include/CheckConfigDefine.h b/ConfigModule/include/CheckConfigDefine.h index 8420d6f..1abdd44 100644 --- a/ConfigModule/include/CheckConfigDefine.h +++ b/ConfigModule/include/CheckConfigDefine.h @@ -934,9 +934,10 @@ struct Function_Support_Det float r_offset; // 支架旋转偏移阈值(度):支架旋转矩形长轴(对应左/右边缘)与产品左/右边缘夹角超过该值判偏移;<=0 不判定 float support_min_area; // 支架缺失判定:支架面积阈值(单位 mm²)。支架由 Align_Inner 模型(smallmask)检出,支架面积 < 该值判缺失;<=0 不判定 // 支架边缘原图精细化(Support_Detect.form.support_param,均可选;仅对 edge_select 选中的边缘生效): - // 沿边缘按步长采样,从边缘向产品内侧逐像素找第一个 255 像素作为新边缘点,再重新拟合边缘线 + // 沿边缘按步长采样,从边缘向产品内侧逐像素找第一个灰度 >= 阈值的像素作为新边缘点,再重新拟合边缘线 float edge_refine_search; // edge_refine_search:最多向内搜索的像素数;<=0=未配置(默认 80) float edge_refine_step; // edge_refine_step:沿边采样步长(像素,不需要每个点都找);<=0=未配置(默认 40) + float edge_refine_gray; // edge_refine_gray:边缘点灰度阈值(0~255,灰度 >= 该值视为产品边缘);<=0=未配置(默认 240) // 支架方向精细化(Support_Detect.form.support_param,均可选):模型 smallmask 精度低 // (1696×800 上采样回原图),minAreaRect 求支架方向角误差大;改为在原图(detImg)上拟合支架长边求方向角 float support_refine_search; // support_refine_search:取模型支架外接矩形最外侧多少列作为边缘搜索带;<=0=未配置(默认 30) @@ -982,6 +983,7 @@ struct Function_Support_Det support_min_area = 0; edge_refine_search = 0; edge_refine_step = 0; + edge_refine_gray = 0; support_refine_search = 0; support_refine_step = 0; support_refine_segments = 0; @@ -1016,6 +1018,7 @@ struct Function_Support_Det this->support_min_area = tem.support_min_area; this->edge_refine_search = tem.edge_refine_search; this->edge_refine_step = tem.edge_refine_step; + this->edge_refine_gray = tem.edge_refine_gray; this->support_refine_search = tem.support_refine_search; this->support_refine_step = tem.support_refine_step; this->support_refine_segments = tem.support_refine_segments; @@ -1037,16 +1040,16 @@ struct Function_Support_Det } void print(std::string str) { - printf("%s>>bDetSupport %d bDrawSupport %d bUseTraditionEdge %d bDetHandle %d bDetTag %d x_standard %f y_standard %f x_offset %f y_offset %f r_offset %f support_min_area %f edgeRefine[search %.0f step %.0f] supportRefine[search %.0f step %.0f seg %.0f] 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, bUseTraditionEdge, bDetHandle, bDetTag, x_standard, y_standard, x_offset, y_offset, r_offset, support_min_area, edge_refine_search, edge_refine_step, support_refine_search, support_refine_step, support_refine_segments, edge_select.size(), handleRegions.size(), handle_diff, + printf("%s>>bDetSupport %d bDrawSupport %d bUseTraditionEdge %d bDetHandle %d bDetTag %d x_standard %f y_standard %f x_offset %f y_offset %f r_offset %f support_min_area %f edgeRefine[search %.0f step %.0f gray %.0f] supportRefine[search %.0f step %.0f seg %.0f] 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, bUseTraditionEdge, bDetHandle, bDetTag, x_standard, y_standard, x_offset, y_offset, r_offset, support_min_area, edge_refine_search, edge_refine_step, edge_refine_gray, support_refine_search, support_refine_step, support_refine_segments, 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) { char buffer[1024]; - sprintf(buffer, "%s>>bDetSupport %d bDrawSupport %d bUseTraditionEdge %d bDetHandle %d bDetTag %d x_standard %f y_standard %f x_offset %f y_offset %f r_offset %f support_min_area %f edgeRefine[search %.0f step %.0f] supportRefine[search %.0f step %.0f seg %.0f] 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, bUseTraditionEdge, bDetHandle, bDetTag, x_standard, y_standard, x_offset, y_offset, r_offset, support_min_area, edge_refine_search, edge_refine_step, support_refine_search, support_refine_step, support_refine_segments, edge_select.size(), handleRegions.size(), handle_diff, + sprintf(buffer, "%s>>bDetSupport %d bDrawSupport %d bUseTraditionEdge %d bDetHandle %d bDetTag %d x_standard %f y_standard %f x_offset %f y_offset %f r_offset %f support_min_area %f edgeRefine[search %.0f step %.0f gray %.0f] supportRefine[search %.0f step %.0f seg %.0f] 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, bUseTraditionEdge, bDetHandle, bDetTag, x_standard, y_standard, x_offset, y_offset, r_offset, support_min_area, edge_refine_search, edge_refine_step, edge_refine_gray, support_refine_search, support_refine_step, support_refine_segments, 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; diff --git a/ConfigModule/src/JsonConfig.cpp b/ConfigModule/src/JsonConfig.cpp index e53ee6d..56815da 100644 --- a/ConfigModule/src/JsonConfig.cpp +++ b/ConfigModule/src/JsonConfig.cpp @@ -962,9 +962,10 @@ int BaseFuntonConfigJson::GetFunction(Json::Value value) _config.supportDet.support_min_area = value_param["support_min_area"].asFloat(); } - // 支架边缘原图精细化(可选):沿边缘采样,从边缘向内找第一个 255 像素作为新边缘点, + // 支架边缘原图精细化(可选):沿边缘采样,从边缘向内找第一个灰度 >= 阈值的像素作为新边缘点, // edge_refine_search:最多向内搜索像素数(未配置默认 80) // edge_refine_step :沿边采样步长(像素)(未配置默认 40,不需要每个点都找) + // edge_refine_gray :边缘点灰度阈值(0~255,灰度 >= 该值视为产品边缘)(未配置默认 240) if (value_param["edge_refine_search"]) { _config.supportDet.edge_refine_search = value_param["edge_refine_search"].asFloat(); @@ -973,6 +974,10 @@ int BaseFuntonConfigJson::GetFunction(Json::Value value) { _config.supportDet.edge_refine_step = value_param["edge_refine_step"].asFloat(); } + if (value_param["edge_refine_gray"]) + { + _config.supportDet.edge_refine_gray = value_param["edge_refine_gray"].asFloat(); + } // 支架方向精细化(可选):模型 smallmask 精度低(1696×800 上采样回原图), // minAreaRect 求支架方向角误差大,改为在 detImg 灰度原图上拟合支架长边求方向角。