From e138c826a5d3f96c16d25986af5847d0571c73e4 Mon Sep 17 00:00:00 2001 From: liusiyang Date: Thu, 17 Sep 2026 13:54:43 +0800 Subject: [PATCH] =?UTF-8?q?feat=20=E4=BC=98=E5=8C=96=E4=BC=A0=E7=BB=9F?= =?UTF-8?q?=E6=A3=80=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TcsCheckModule/include/TcsCheck.h | 2 + TcsCheckModule/src/TcsCheck.cpp | 102 +++++++++++++++++++++++------- 2 files changed, 80 insertions(+), 24 deletions(-) diff --git a/TcsCheckModule/include/TcsCheck.h b/TcsCheckModule/include/TcsCheck.h index 3ddb7ae..93263f8 100644 --- a/TcsCheckModule/include/TcsCheck.h +++ b/TcsCheckModule/include/TcsCheck.h @@ -126,6 +126,8 @@ private: cv::Rect GetBoundingRect(cv::Mat matBinary); cv::Rect GetCropArea(cv::Rect rtValid); cv::Mat AdaptiveBinary(cv::Mat matBlur); + // 局部背景基准图(大窗口中值),用于分块动态阈值的阈值中心 + cv::Mat EstimateBackgroundMap(const cv::Mat& matGray, int blockSize); void Process(bool bDraw = false); diff --git a/TcsCheckModule/src/TcsCheck.cpp b/TcsCheckModule/src/TcsCheck.cpp index f6c8f28..fd2a556 100644 --- a/TcsCheckModule/src/TcsCheck.cpp +++ b/TcsCheckModule/src/TcsCheck.cpp @@ -44,7 +44,12 @@ void CTcsCheck::SetCheckDir(std::string dirIn,std::string dirOut) } void CTcsCheck::SetChecConfig(CHECK_PARAM* cp) { - memcpy(&m_cpCfg,cp,sizeof(CHECK_PARAM)); + // CHECK_PARAM 内含 std::string,不能用 memcpy:memcpy 会把源对象 string 内部的 + // _M_data 指针一并拷过来,于是拷贝出的 string 认为自己持有堆内存, + // 析构时会 free 到源对象的栈缓冲(短串)或重复释放(长串),随机崩溃。 + // 整体赋值(拷贝构造/赋值)才是正确做法。 + if (cp == nullptr) return; + m_cpCfg = *cp; } std::string CTcsCheck::GetFileName(const std::string& path) const { @@ -145,33 +150,82 @@ cv::Rect CTcsCheck::GetCropArea(cv::Rect rtValid) } return innerRect; } +// ============================================================ +// EstimateBackgroundMap — 局部背景基准图(抗大缺陷污染) +// +// 旧实现用「块内灰度均值」当阈值中心,缺陷一旦覆盖块内较大面积, +// 均值就会被缺陷灰度带偏: +// · 均值落在「背景峰」与「缺陷峰」之间的空隙 → 容忍带内一个像素都没有 +// → 反相后整块变白(大面积误检,缺陷其实没占满块); +// · 均值贴近缺陷灰度 → 缺陷被判成背景、背景反被判成缺陷(极性翻转)。 +// +// 这里改用「中值」估计背景:中值一定是窗口内真实存在的灰度值, +// 只要缺陷不占到窗口一半以上就不会被带偏。窗口取 3 倍块边长(覆盖 3×3 个块), +// 因此单块甚至相邻几块内的大缺陷都污染不了该块的背景基准。 +// +// 为控制耗时,先在 1/ds 缩略图上做中值(等效窗口 = ds × 粗粒度核), +// 再插值放大回原尺寸 —— 背景本身是低频的,放大误差可忽略。 +// 实测 1500×3750:全分辨率大核中值 ~146ms,降采样后 ~10ms。 +// ============================================================ +cv::Mat CTcsCheck::EstimateBackgroundMap(const cv::Mat& matGray, int blockSize) +{ + cv::Mat gray8; + if (matGray.type() == CV_8UC1) { + gray8 = matGray; + } else { + matGray.convertTo(gray8, CV_8UC1); + } + if (gray8.empty()) return cv::Mat(); + + const int bgWinScale = 3; // 背景估计窗口 = 3 倍块边长 + const int kMaxSafe = 201; // medianBlur 大核上限(本机 OpenCV 4.5 实测 >301 断言失败) + + // 降采样倍数:保证粗粒度核不超过 kMaxSafe + int ds = 4; + while (bgWinScale * blockSize / ds > kMaxSafe) ds *= 2; + + // 缩略图:INTER_AREA 即块内均值,缺陷只会让个别单元变成离群点,中值可剔除 + cv::Mat small; + cv::resize(gray8, small, + cv::Size(std::max(1, gray8.cols / ds), std::max(1, gray8.rows / ds)), 0, 0, cv::INTER_AREA); + + int k = bgWinScale * std::max(1, blockSize / ds); + if (k % 2 == 0) ++k; + if (k < 3) k = 3; // 允许 k 大于缩略图尺寸,medianBlur 会按边界复制处理 + + cv::Mat smallBg; + cv::medianBlur(small, smallBg, k); + + cv::Mat matBg; + cv::resize(smallBg, matBg, gray8.size(), 0, 0, cv::INTER_LINEAR); + return matBg; +} + cv::Mat CTcsCheck::AdaptiveBinary(cv::Mat matBlur) { - // 按 localBlockSize_ 分块,使用每个块的局部均值做动态阈值。 - // 当前规则:落在 [avg-lower_, avg+upper_] 内置 0,超出置 255。 + // 按 nBlockSize 分块,用每个块的局部背景基准做动态阈值。 + // 规则:落在 [ref-low, ref+high] 内置 0,超出置 255。 + // 注意 ref 取「大窗口中值背景」而非块内均值,避免大缺陷把阈值中心带偏导致整块变白。 + if (matBlur.empty()) return cv::Mat(); cv::Mat result = cv::Mat::zeros(matBlur.size(), CV_8UC1); - m_cpCfg.nBlockSize = std::min(m_cpCfg.nBlockSize, matBlur.cols); - m_cpCfg.nBlockSize = std::min(m_cpCfg.nBlockSize, matBlur.rows); - for (int y = 0; y < matBlur.rows; y += m_cpCfg.nBlockSize) { - for (int x = 0; x < matBlur.cols; x += m_cpCfg.nBlockSize) { - // const int blockW = std::min(m_cpCfg.nBlockSize, matBlur.cols - x); - // const int blockH = std::min(m_cpCfg.nBlockSize, matBlur.rows - y); - cv::Rect blockRect(x, y, m_cpCfg.nBlockSize, m_cpCfg.nBlockSize); - if(x + m_cpCfg.nBlockSize > matBlur.cols) - { - blockRect = cv::Rect(matBlur.cols - m_cpCfg.nBlockSize, y, m_cpCfg.nBlockSize, m_cpCfg.nBlockSize); - } - if(y + m_cpCfg.nBlockSize > matBlur.rows) - { - blockRect = cv::Rect(x, matBlur.rows - m_cpCfg.nBlockSize, m_cpCfg.nBlockSize, m_cpCfg.nBlockSize); - } - if(x + m_cpCfg.nBlockSize > matBlur.cols && y + m_cpCfg.nBlockSize > matBlur.rows) - { - blockRect = cv::Rect(matBlur.cols - m_cpCfg.nBlockSize, matBlur.rows - m_cpCfg.nBlockSize, m_cpCfg.nBlockSize, m_cpCfg.nBlockSize); - } + + // 块边长不超过图像尺寸;同步回 m_cpCfg,保证 ClassifyBlobs 的块网格一致 + const int blockSize = std::max(1, std::min(m_cpCfg.nBlockSize, std::min(matBlur.cols, matBlur.rows))); + m_cpCfg.nBlockSize = blockSize; + + // 局部背景基准图(大窗口中值) + const cv::Mat matBg = EstimateBackgroundMap(matBlur, blockSize); + + for (int y = 0; y < matBlur.rows; y += blockSize) { + for (int x = 0; x < matBlur.cols; x += blockSize) { + // 末列/末行窗口贴边对齐(与相邻窗口重叠),不产生残缺块 + const int bx = std::min(x, std::max(0, matBlur.cols - blockSize)); + const int by = std::min(y, std::max(0, matBlur.rows - blockSize)); + const cv::Rect blockRect(bx, by, blockSize, blockSize); cv::Mat block = matBlur(blockRect); - const double avg = cv::mean(block)[0]; + // 背景基准:取该块范围内背景图的均值(抹平中值图的量化台阶) + const double avg = matBg.empty() ? cv::mean(block)[0] : cv::mean(matBg(blockRect))[0]; const double minGrey = std::max(0.0, avg - m_cpCfg.nFilterLow); const double maxGrey = std::min(255.0, avg + m_cpCfg.nFilterHigh); @@ -183,7 +237,7 @@ cv::Mat CTcsCheck::AdaptiveBinary(cv::Mat matBlur) // 传统检测残点图存图 if (m_cpCfg.bDebugsaveImg) { - std::string roi_ext = "[" + std::to_string(x) + "," + std::to_string(y) + "]"; + std::string roi_ext = "[" + std::to_string(bx) + "," + std::to_string(by) + "]"; std::string strSaveDir = "/home/aidlux/BOE/CELL_AOI/AI_Detect/" + m_cpCfg.productId + "/"; std::string strSavePath_in = strSaveDir + m_cpCfg.productChannel +"_"+ roi_ext + "_"+ "in" + ".png"; std::string strSavePath_out = strSaveDir + m_cpCfg.productChannel +"_"+ roi_ext + "_"+ "out" + ".png";