From 92850246a448aede5489fe75b545a0e409b7352c Mon Sep 17 00:00:00 2001 From: liusiyang Date: Sun, 20 Sep 2026 12:39:17 +0800 Subject: [PATCH] =?UTF-8?q?update=20=E4=BC=98=E5=8C=96=E5=86=85=E5=AD=98?= =?UTF-8?q?=E6=B3=84=E6=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AlgorithmModule/include/ImgMmPatch.hpp | 53 ++++++---- AlgorithmModule/src/ImgCheckAnalysisy.cpp | 6 ++ TcsCheckModule/src/TcsCheck.cpp | 117 ++++++++++++++++++++-- 3 files changed, 151 insertions(+), 25 deletions(-) diff --git a/AlgorithmModule/include/ImgMmPatch.hpp b/AlgorithmModule/include/ImgMmPatch.hpp index 3b29cb9..cdb8dca 100644 --- a/AlgorithmModule/include/ImgMmPatch.hpp +++ b/AlgorithmModule/include/ImgMmPatch.hpp @@ -7,12 +7,8 @@ // 背景:产品沿长度方向运动/扫描时速率不恒定(尾端有拉伸),像素↔mm 不是单一 // 比例,实测标定出 5 段,每段斜率不同。 // -// 分段表(分段点 = 像素,**以产品顶边为 0 点**;斜率为 mm/px): -// 0 ~ 10928 0.016869 -// 10928 ~ 17002 0.016463 (6074px ↔ 100mm) -// 17002 ~ 23167 0.016220 (6165px ↔ 100mm) -// 23167 ~ 29549 0.015669 (6382px ↔ 100mm) -// >= 29549 0.014337 +// 分段表(分段点 = 像素,**以产品顶边为 0 点**;斜率为 mm/px)见函数内 +// yPx1..yPx5 / pmm1..pmm5,改标定只需改那一段(此处不重复写数值,避免两处不同步)。 // 分段点处前后两段取值相同,函数连续,不会在分段点跳变。 // // 共用方(必须共用同一张表,否则"产品高度"和"缺陷坐标"会各用一把尺子): @@ -23,29 +19,48 @@ // ============================================================================ inline float PixelYToMm(float fYPixel) { + // ================= 标定参数:改标定只需改这一段 ================= + // 各段斜率(mm/px) + const float pmm1 = 0.016869; + const float pmm2 = 0.016463; + const float pmm3 = 0.016220; + const float pmm4 = 0.015669; + const float pmm5 = 0.014337; + // 各段起始像素(以产品顶边为 0 点) + const float yPx1 = 0.0f; + const float yPx2 = 10928.0f; + const float yPx3 = 17002.0f; + const float yPx4 = 23167.0f; + const float yPx5 = 29549.0f; + // 各段像素长度(px) + const float dPx1 = yPx2 - yPx1; + const float dPx2 = yPx3 - yPx2; + const float dPx3 = yPx4 - yPx3; + const float dPx4 = yPx5 - yPx4; + // 各段末端的累计物理长度(mm) - const float d1 = 10928 * 0.016869; - const float d2 = 6074 * 0.016463; - const float d3 = 6165 * 0.016220; - const float d4 = 6382 * 0.015669; + const float d1 = dPx1 * pmm1; + const float d2 = d1 + dPx2 * pmm2; + const float d3 = d2 + dPx3 * pmm3; + const float d4 = d3 + dPx4 * pmm4; - if (fYPixel < 10928.0f) + if (fYPixel < yPx2) { - return fYPixel * 0.016869; + return fYPixel * pmm1; } - if (fYPixel < 17002.0f) + if (fYPixel < yPx3) { - return (fYPixel - 10928.0f) * 0.016463 + d1; + return (fYPixel - yPx2) * pmm2 + d1; } - if (fYPixel < 23167.0f) + if (fYPixel < yPx4) { - return (fYPixel - 10928.0f - 6074.0f) * 0.016220 + d1 + d2; + return (fYPixel - yPx3) * pmm3 + d2; } - if (fYPixel < 29549.0f) + if (fYPixel < yPx5) { - return (fYPixel - 10928.0f - 6074.0f - 6165.0f) * 0.015669 + d1 + d2 + d3; + return (fYPixel - yPx4) * pmm4 + d3; } - return (fYPixel - 10928.0f - 6074.0f - 6165.0f - 6382.0f) * 0.014337 + d1 + d2 + d3 + d4; + return (fYPixel - yPx5) * pmm5 + d4; } #endif diff --git a/AlgorithmModule/src/ImgCheckAnalysisy.cpp b/AlgorithmModule/src/ImgCheckAnalysisy.cpp index ade2795..865270a 100644 --- a/AlgorithmModule/src/ImgCheckAnalysisy.cpp +++ b/AlgorithmModule/src/ImgCheckAnalysisy.cpp @@ -1968,6 +1968,12 @@ int ImgCheckAnalysisy::Traditional_Detect_Thread(const cv::Mat &img, cv::Mat &Re Base_Function_TraditionDet traditionParam = m_pbaseCheckFunction->traditionDet; { CHECK_PARAM cp; + // 四边裁边量必须显式赋值:CHECK_PARAM 是 POD,未赋值的字段是栈垃圾, + // 一旦走到「自动定位产品」(detArea 未配置/覆盖整幅图)就会用垃圾值算裁剪框 + cp.nDiscardTop = 170; + cp.nDiscardBottom = 170; + cp.nDiscardLeft = 180; + cp.nDiscardRight = 460; cp.nAreaLowFilter = 80; cp.nBlockSize = traditionParam.nBlockSize; cp.fZoomRatio = traditionParam.fZoomRatio; diff --git a/TcsCheckModule/src/TcsCheck.cpp b/TcsCheckModule/src/TcsCheck.cpp index fd2a556..32dcf22 100644 --- a/TcsCheckModule/src/TcsCheck.cpp +++ b/TcsCheckModule/src/TcsCheck.cpp @@ -101,9 +101,12 @@ void CTcsCheck::LoadImages() CreateDirectories(m_strDirOut); } -cv::Rect CTcsCheck::GetBoundingRect(cv::Mat matBinary) +// 在给定二值图上取「最大连通域的外接框」(输入需为 CV_8UC1) +static cv::Rect LargestComponentRect(const cv::Mat &matBinary) { - cv::Mat labels; + if (matBinary.empty()) return cv::Rect(0, 0, 0, 0); + + cv::Mat labels; cv::Mat stats; cv::Mat centroids; const int nLabels = cv::connectedComponentsWithStats(matBinary, labels, stats, centroids, 8, CV_32S); @@ -131,6 +134,67 @@ cv::Rect CTcsCheck::GetBoundingRect(cv::Mat matBinary) const int h = stats.at(maxLabel, cv::CC_STAT_HEIGHT); return cv::Rect(x, y, w, h); } + +// 产品定位用的「最大连通域外接框」。 +// +// connectedComponentsWithStats 的输出 labels 是 CV_32S —— 4 字节/像素: +// 22754x60000 的整幅图 → labels 单次就要 5.5 GB,实测该分支进程峰值 9.3 GB; +// 4 个相机并发时这是最容易把机器内存打爆的地方(整机 OOM 时最后失败的分配 +// 往往落在 TensorRT 内嵌的 LLVM JIT 上,日志表现为 “LLVM ERROR: out of memory”)。 +// +// 因此先在 1/ds 缩略图上定位最大连通域,再在原图对应局部窗口(外扩 2*ds)上精修: +// 最大块在缩略图上的外接框与原图外接框最多相差 ds 像素,外扩后窗口必然完整包含它, +// 所以精修结果与「直接对全图做 CC」一致,而 labels 内存降到 1/ds² 量级。 +cv::Rect CTcsCheck::GetBoundingRect(cv::Mat matBinary) +{ + if (matBinary.empty() || matBinary.type() != CV_8UC1) + { + return cv::Rect(0, 0, 0, 0); + } + + const int ds = 8; // 缩略图倍数 + const long long kCoarseMinPixels = 4LL * 1024 * 1024; // 小于 4M 像素直接走原逻辑 + const long long imgPixels = static_cast(matBinary.cols) * matBinary.rows; + + if (imgPixels <= kCoarseMinPixels) + { + return LargestComponentRect(matBinary); + } + + // 1) 粗定位:INTER_AREA 后 >0 即视为该单元内有残点 + cv::Mat small; + cv::resize(matBinary, small, + cv::Size(std::max(1, matBinary.cols / ds), std::max(1, matBinary.rows / ds)), 0, 0, cv::INTER_AREA); + cv::Mat smallBin; + cv::threshold(small, smallBin, 0, 255, cv::THRESH_BINARY); + + const cv::Rect smallRect = LargestComponentRect(smallBin); + if (smallRect.width <= 0 || smallRect.height <= 0) + { + return cv::Rect(0, 0, 0, 0); + } + + // 2) 缩略图坐标 → 原图坐标,外扩 2*ds 覆盖降采样带来的边界误差 + const int pad = 2 * ds; + cv::Rect coarse(smallRect.x * ds - pad, smallRect.y * ds - pad, + (smallRect.width + 2) * ds + 2 * pad, (smallRect.height + 2) * ds + 2 * pad); + coarse = coarse & cv::Rect(0, 0, matBinary.cols, matBinary.rows); + + // 粗定位明显不可信(外接框面积不足全图 0.1%)时退回全图 CC,保证行为不变 + const long long coarseArea = static_cast(smallRect.width) * smallRect.height * ds * ds; + if (coarse.width <= 0 || coarse.height <= 0 || coarseArea * 1000 < imgPixels) + { + return LargestComponentRect(matBinary); + } + + // 3) 在原图局部窗口内精修,得到像素级精确的外接框 + const cv::Rect local = LargestComponentRect(matBinary(coarse)); + if (local.width <= 0 || local.height <= 0) + { + return LargestComponentRect(matBinary); + } + return cv::Rect(coarse.x + local.x, coarse.y + local.y, local.width, local.height); +} cv::Rect CTcsCheck::GetCropArea(cv::Rect rtValid) { // 在原图坐标系中对 ROI 四边分别内收对应的 discard 宽度。 @@ -427,6 +491,13 @@ int CTcsCheck::TraditionalDetect(const cv::Mat& img, cv::Rect detRoi, cv::Mat& b // 确保 m_sizeImage 始终与 m_matLoad 同步 m_sizeImage = img.size(); + // 先把上一张图的中间结果清掉:任何提前 return 都不允许下游(TraditionalClassify) + // 误用旧图的 mask / ROI(坐标会整体错位,且会多攥住一份 GB 级缓冲) + m_matBlur.release(); + m_matBlob.release(); + m_rtCrop = cv::Rect(0, 0, 0, 0); + m_vecDefectInfo.clear(); + cv::Rect rtCrop = detRoi; if(detRoi.size() == img.size()) { @@ -439,6 +510,7 @@ int CTcsCheck::TraditionalDetect(const cv::Mat& img, cv::Rect detRoi, cv::Mat& b cv::Rect rtValid = GetBoundingRect(matBinary); if (rtValid == cv::Rect(0, 0, 0, 0)) { + m_matLoad.release(); // 不再需要引用 detImg blobImg = cv::Mat(); return -1; } @@ -455,6 +527,7 @@ int CTcsCheck::TraditionalDetect(const cv::Mat& img, cv::Rect detRoi, cv::Mat& b rtCrop = rtCrop & imgRect; if (rtCrop.width <= 0 || rtCrop.height <= 0) { + m_matLoad.release(); // 不再需要引用 detImg blobImg = cv::Mat(); return -1; } @@ -485,17 +558,36 @@ int CTcsCheck::TraditionalDetect(const cv::Mat& img, cv::Rect detRoi, cv::Mat& b // logStep("8.自适应二值化"); // 9. 反向映射:只生成下游需要的全尺寸残点 mask(resultMaskImg / Blob 计算)。 - // 直接 resize 到 ROI 上,省去中间 matCropSize 缓冲与 copyTo; - // m_matBlob 保持小图即可,全尺寸副本无任何消费者,已移除。 + // 下游 GetALLBlob / BLobToDetResult 都以 detImg 为坐标基准,所以 mask 必须与 detImg 等大, + // 且 ROI 之外必须是 0。 + // 写法上不再用 cv::Mat::zeros(全图)(那会先给整幅图做一次 memset,22754x60000 时约 1.3 GB), + // 而是创建一个不初始化的缓冲,只把 ROI 之外的条带清零 —— 结果完全等价,省一次全图 memset。 if (m_cpCfg.fZoomRatio > 0) { - cv::Mat fullMask = cv::Mat::zeros(m_matLoad.size(), CV_8UC1); + cv::Mat fullMask; + fullMask.create(m_matLoad.size(), CV_8UC1); // create 不做初始化 + + const int y0 = rtCrop.y; + const int y1 = rtCrop.y + rtCrop.height; + const int x0 = rtCrop.x; + const int x1 = rtCrop.x + rtCrop.width; + if (y0 > 0) fullMask.rowRange(0, y0).setTo(0); + if (y1 < fullMask.rows) fullMask.rowRange(y1, fullMask.rows).setTo(0); + cv::Mat mid = fullMask.rowRange(y0, y1); + if (x0 > 0) mid.colRange(0, x0).setTo(0); + if (x1 < mid.cols) mid.colRange(x1, mid.cols).setTo(0); + cv::resize(m_matBlob, fullMask(rtCrop), cv::Size(rtCrop.width, rtCrop.height), 0, 0, cv::INTER_NEAREST); blobImg = fullMask; // logStep("9.反向映射(mask)"); } + // 检测期间只需要引用 detImg,结束即松开: + // 否则 CTcsCheck 会一直攥住整幅检测图(GB 级)不放,上层即使释放 detImg 也回收不掉。 + // (分类阶段只用 m_matBlur / m_matBlob 这两张小图) + m_matLoad.release(); + // 总耗时 { auto tEnd = std::chrono::high_resolution_clock::now(); @@ -518,8 +610,21 @@ int CTcsCheck::TraditionalClassify(const cv::Mat& blobImg) // 在小图上分类可避免对全尺寸图做昂贵的 connectedComponentsWithStats。 if (m_matBlur.empty() || m_matBlob.empty()) { - // 回退:无小图缓存时,退化为全尺寸分类(旧行为) + // 回退路径:旧行为是对全尺寸 mask 直接做连通域分析, + // 而 connectedComponentsWithStats 的 labels 是 CV_32S(4 B/px): + // 22754x60000 的 mask 单次就要 5.5 GB,是明确的 OOM 风险点。 + // 正常流程里 TraditionalDetect 必然已经填好小图缓存,这里只保留小图的兼容路径。 + m_vecDefectInfo.clear(); if (m_matLoad.empty() || blobImg.empty()) return -1; + + const double pxCount = static_cast(blobImg.cols) * blobImg.rows; + if (pxCount > 16.0 * 1024 * 1024) + { + std::cout << "[TraditionalClassify] no small-image cache, mask " + << blobImg.cols << "x" << blobImg.rows + << " -> refuse full-size connectedComponentsWithStats (OOM risk)" << std::endl; + return -1; + } ClassifyBlobs(m_matLoad, blobImg); return static_cast(m_vecDefectInfo.size()); }