#include "TcsCheck.h" #include "CheckUtil.hpp" // 异步存图:CheckUtil::SaveImgAsync / WaitSaveImgDone class CLock { public: CLock(pthread_mutex_t * attr) : m_attr(attr) { pthread_mutex_lock(m_attr); }; ~CLock() { pthread_mutex_unlock(m_attr); }; protected: pthread_mutex_t * m_attr; }; CTcsCheck::CTcsCheck() : m_nInitStart(0) , m_bSystemExit(0) , m_nInitEnd(0) { m_cpCfg.nAreaLowFilter = 80; m_cpCfg.nBlockSize = 100; m_cpCfg.nDiscardTop = 170; m_cpCfg.nDiscardBottom = 170; m_cpCfg.nDiscardLeft = 180; m_cpCfg.nDiscardRight = 460; m_cpCfg.fZoomRatio = 0.25; m_cpCfg.nFilterLow = 15; m_cpCfg.nFilterHigh = 15; m_cpCfg.nAreaFilter = 10; m_cpCfg.nCountFilter = 50; } CTcsCheck::~CTcsCheck() { } void CTcsCheck::SetCheckDir(std::string dirIn,std::string dirOut) { m_strDirIn = dirIn; m_strDirOut = dirOut; } void CTcsCheck::SetChecConfig(CHECK_PARAM* cp) { // 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 { std::size_t pos = path.find_last_of("/\\"); std::string name = (pos == std::string::npos) ? path : path.substr(pos + 1); std::size_t dot = name.find_last_of('.'); if (dot == std::string::npos) { return name; } return name.substr(0, dot); } // 递归创建多级目录 bool CTcsCheck::CreateDirectories(std::string path, mode_t mode ) { std::string fullPath = path; size_t pos = 0; while ((pos = fullPath.find_first_of("/", pos + 1)) != std::string::npos) { std::string subPath = fullPath.substr(0, pos); if (subPath.empty()) continue; if (mkdir(subPath.c_str(), mode) != 0 && errno != EEXIST) { std::cerr << "创建子目录失败: " << subPath << " - " << strerror(errno) << std::endl; return false; } } // 创建最后一级目录 if (mkdir(path.c_str(), mode) != 0 && errno != EEXIST) { std::cerr << "创建最终目录失败: " << path << " - " << strerror(errno) << std::endl; return false; } std::cout << "多级目录创建成功: " << path << std::endl; return true; } void CTcsCheck::LoadImages() { if (access(m_strDirIn.c_str(),F_OK) == 0) { cv::glob(m_strDirIn, m_fileList); } else { std::cout << "Dir " << m_strDirIn << " is NOT existed!" << std::endl; } std::cout << "Create OUT directory : " << m_strDirOut << std::endl; CreateDirectories(m_strDirOut); } // 在给定二值图上取「最大连通域的外接框」(输入需为 CV_8UC1) static cv::Rect LargestComponentRect(const cv::Mat &matBinary) { 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); if (nLabels <= 1) { return cv::Rect(0,0,0,0); } int maxArea = 0; int maxLabel = -1; for (int label = 1; label < nLabels; ++label) { const int area = stats.at(label, cv::CC_STAT_AREA); if (area > maxArea) { maxArea = area; maxLabel = label; } } if (maxLabel < 0) { return cv::Rect(0,0,0,0); } const int x = stats.at(maxLabel, cv::CC_STAT_LEFT); const int y = stats.at(maxLabel, cv::CC_STAT_TOP); const int w = stats.at(maxLabel, cv::CC_STAT_WIDTH); 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 宽度。 // 若尺寸不足以同时内收,则保持原 ROI(不做"部分边"裁剪)。 const int outW = rtValid.width - m_cpCfg.nDiscardLeft - m_cpCfg.nDiscardRight; const int outH = rtValid.height - m_cpCfg.nDiscardTop - m_cpCfg.nDiscardBottom; if (outW <= 0 || outH <= 0) { return rtValid; } cv::Rect innerRect(rtValid.x + m_cpCfg.nDiscardLeft, rtValid.y + m_cpCfg.nDiscardTop, outW, outH); const cv::Rect imageRect(0, 0, m_sizeImage.width, m_sizeImage.height); innerRect = innerRect & imageRect; if (innerRect.width <= 0 || innerRect.height <= 0) { return 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) { // 按 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,保证 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 = 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); cv::Mat blockBin; cv::inRange(block, minGrey, maxGrey, blockBin); cv::bitwise_not(blockBin, blockBin); blockBin.copyTo(result(blockRect)); // 传统检测残点图存图 if (m_cpCfg.bDebugsaveImg) { 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"; // 父目录由异步存图器按需创建 CheckUtil::SaveImgAsync(strSavePath_in, block); CheckUtil::SaveImgAsync(strSavePath_out, blockBin); } } } return result; } // ============================================================ // ClassifyBlobs — 纯分类函数 // 对残点二值图做连通域分析 + 缺陷分类,结果写入 m_vecDefectInfo // ============================================================ void CTcsCheck::ClassifyBlobs(const cv::Mat& blurCrop, const cv::Mat& imgBlob) { m_vecDefectInfo.clear(); // 分步计时:记录每个步骤的耗时(ms) auto tStart = std::chrono::high_resolution_clock::now(); auto tPrev = tStart; auto logStep = [&](const std::string& step) { auto tNow = std::chrono::high_resolution_clock::now(); double elapsedMs = std::chrono::duration_cast(tNow - tPrev).count(); std::cout << "[ClassifyBlobs] " << step << " 耗时(ms): " << elapsedMs << std::endl; tPrev = tNow; }; cv::Mat labels; cv::Mat stats; cv::Mat centroids; const int nLabels = cv::connectedComponentsWithStats(imgBlob, labels, stats, centroids, 8, CV_32S); // logStep("1.连通域分析"); if (nLabels <= 1) return; // 大块缺陷的面积阈值(分类决策用,非过滤) const int largeAreaThreshold = std::max(1, m_cpCfg.nBlockSize * m_cpCfg.nBlockSize / 4); // 面积/个数过滤已统一上移到 BLobToDetResult 处理, // 此处对所有连通域逐一分类,仅做缺陷类型打标签。 for (int label = 1; label < nLabels; ++label) { const int area = stats.at(label, cv::CC_STAT_AREA); if (area < 5) continue; // bounding rect const int x = stats.at(label, cv::CC_STAT_LEFT); const int y = stats.at(label, cv::CC_STAT_TOP); const int w = stats.at(label, cv::CC_STAT_WIDTH); const int h = stats.at(label, cv::CC_STAT_HEIGHT); // blob 区域均值:仅在 bounding rect 局部计算,避免全图 mask cv::Mat blobMask = (labels(cv::Rect(x, y, w, h)) == label); double blobMean = cv::mean(blurCrop(cv::Rect(x, y, w, h)), blobMask)[0]; // 所属局部块均值 const double cx = centroids.at(label, 0); const double cy = centroids.at(label, 1); int blockX = std::max(0, std::min(static_cast(cx) / m_cpCfg.nBlockSize, (blurCrop.cols - 1) / m_cpCfg.nBlockSize)); int blockY = std::max(0, std::min(static_cast(cy) / m_cpCfg.nBlockSize, (blurCrop.rows - 1) / m_cpCfg.nBlockSize)); const int roiX = blockX * m_cpCfg.nBlockSize; const int roiY = blockY * m_cpCfg.nBlockSize; const int roiW = std::min(m_cpCfg.nBlockSize, blurCrop.cols - roiX); const int roiH = std::min(m_cpCfg.nBlockSize, blurCrop.rows - roiY); const double localMean = cv::mean(blurCrop(cv::Rect(roiX, roiY, roiW, roiH)))[0]; const double greyDiff = blobMean - localMean; // ============================================================ // 缺陷分类决策树 // 长宽比 >= 3 → SCRATCH (划伤) // 紧凑 + 面积小 → POINT (硬质颗粒) // 紧凑 + 大面积+暗 → DIRTY (脏污) // 紧凑 + 大面积+亮 → FADING (淡斑) // ============================================================ float aspectRatio = static_cast(std::max(w, h)) / std::max(1, std::min(w, h)); int defectType = DEFECT_TYPE_OK; if (aspectRatio >= 3.0f) { defectType = DEFECT_TYPE_SCRATCH; } else if (area < largeAreaThreshold) { defectType = DEFECT_TYPE_POINT; } else if (greyDiff < 0) { defectType = DEFECT_TYPE_DIRTY; } else { defectType = DEFECT_TYPE_FADING_SPOTS; } DEFECT_INFO info; info.nDefectType = defectType; info.nDefectArea = area; info.nDefectX = x; info.nDefectY = y; info.nDefectWidth = w; info.nDefectHeight = h; info.dGreyDiff = greyDiff; info.strDefectCode = DEFECT_TYPE_CODE[defectType]; info.strDefectDesc = DEFECT_TYPE_DESC[defectType]; m_vecDefectInfo.push_back(info); } // logStep("4.缺陷分类决策"); // // 总耗时 // { // auto tEnd = std::chrono::high_resolution_clock::now(); // double totalMs = std::chrono::duration_cast(tEnd - tStart).count(); // std::cout << "[ClassifyBlobs] 总耗时(ms): " << totalMs << std::endl; // } } // ============================================================ // DrawBlobInfoImage — 纯绘制函数 // 基于 m_vecDefectInfo 在 crop 图上绘制缺陷标注 // ============================================================ cv::Mat CTcsCheck::DrawBlobInfoImage(const cv::Mat& imgCrop, const cv::Mat& imgBlob) { cv::Mat cropColor; cv::cvtColor(imgCrop, cropColor, cv::COLOR_GRAY2BGR); for (const auto& info : m_vecDefectInfo) { const cv::Rect blobRect(info.nDefectX, info.nDefectY, info.nDefectWidth, info.nDefectHeight); // 根据缺陷类型使用不同颜色绘制 cv::Scalar drawColor; switch (info.nDefectType) { case DEFECT_TYPE_POINT: drawColor = cv::Scalar(0, 255, 0); break; // 绿色 case DEFECT_TYPE_SCRATCH: drawColor = cv::Scalar(0, 0, 255); break; // 红色 case DEFECT_TYPE_DIRTY: drawColor = cv::Scalar(255, 0, 0); break; // 蓝色 case DEFECT_TYPE_FADING_SPOTS: drawColor = cv::Scalar(255, 255, 0); break; // 青色 default: drawColor = cv::Scalar(0, 255, 0); break; } // 在 blob 区域内找轮廓 cv::Mat roiBlob = imgBlob(blobRect & cv::Rect(0, 0, imgBlob.cols, imgBlob.rows)); std::vector> contours; cv::findContours(roiBlob.clone(), contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); // 偏移轮廓坐标到原图位置 for (auto& cnt : contours) { for (auto& pt : cnt) { pt.x += info.nDefectX; pt.y += info.nDefectY; } } cv::drawContours(cropColor, contours, -1, drawColor, 2); cv::rectangle(cropColor, blobRect, cv::Scalar(0, 0, 255), 1); // 文本标注 std::ostringstream oss; oss << info.strDefectCode << " A:" << info.nDefectArea << " dG:" << std::fixed << std::setprecision(1) << info.dGreyDiff; int textX = std::max(0, info.nDefectX); int textY = std::max(15, info.nDefectY - 3); cv::putText(cropColor, oss.str(), cv::Point(textX, textY), cv::FONT_HERSHEY_SIMPLEX, 0.45, drawColor, 1); } return cropColor; } // ============================================================ // 独立检测接口 // 对单张图做:阈值定位→裁剪→缩放→高斯模糊→自适应二值化 // 输出残点二值图 blobImg,内部缓存 m_matBlur 供后续分类 // 返回: 0=成功, -1=无产品或输入异常 // ============================================================ int CTcsCheck::TraditionalDetect(const cv::Mat& img, cv::Rect detRoi, cv::Mat& blobImg) { if (img.empty()) { blobImg = cv::Mat(); return -1; } // 分步计时:记录每个步骤的耗时(ms) auto tStart = std::chrono::high_resolution_clock::now(); auto tPrev = tStart; auto logStep = [&](const std::string& step) { auto tNow = std::chrono::high_resolution_clock::now(); double elapsedMs = std::chrono::duration_cast(tNow - tPrev).count(); std::cout << "[TraditionalDetect] " << step << " 耗时(ms): " << elapsedMs << std::endl; tPrev = tNow; }; // getchar(); // cv::Mat showImg = img.clone(); // cv::rectangle(showImg, detRoi, cv::Scalar(255), 2); // cv::imwrite("detRoi.png", showImg); m_matLoad = img; // 确保 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()) { // 1. 全局阈值 → 产品区域定位 cv::Mat matBinary; cv::threshold(m_matLoad, matBinary, m_cpCfg.nAreaLowFilter, 255, cv::THRESH_BINARY); // logStep("1.全局阈值"); // 2. 获取最大连通域外接矩形 cv::Rect rtValid = GetBoundingRect(matBinary); if (rtValid == cv::Rect(0, 0, 0, 0)) { m_matLoad.release(); // 不再需要引用 detImg blobImg = cv::Mat(); return -1; } // logStep("2.最大连通域外接矩形"); // 3. 裁剪边缘 rtCrop = GetCropArea(rtValid); // logStep("3.裁剪边缘"); } // 安全裁剪: 确保 rtCrop 不超出 m_matLoad 边界 { const cv::Rect imgRect(0, 0, m_matLoad.cols, m_matLoad.rows); rtCrop = rtCrop & imgRect; if (rtCrop.width <= 0 || rtCrop.height <= 0) { m_matLoad.release(); // 不再需要引用 detImg blobImg = cv::Mat(); return -1; } } // 缓存裁剪区域,供 TraditionalClassify 将小图坐标映射回原图 m_rtCrop = rtCrop; // logStep("4.安全裁剪"); cv::Mat matCrop = m_matLoad(rtCrop); // logStep("5.ROI裁剪"); // 6. 缩放 const int outW = std::max(1, static_cast(matCrop.cols * m_cpCfg.fZoomRatio)); const int outH = std::max(1, static_cast(matCrop.rows * m_cpCfg.fZoomRatio)); cv::Mat matResized; cv::resize(matCrop, matResized, cv::Size(outW, outH), 0, 0, cv::INTER_AREA); // logStep("6.缩放"); // 7. 高斯模糊 — 缓存到 m_matBlur cv::GaussianBlur(matResized, m_matBlur, cv::Size(5, 5), 0); // cv::imwrite("matResized.png", matResized); // cv::imwrite("m_matBlur.png", m_matBlur); // logStep("7.高斯模糊"); // 8. 自适应二值化检测 m_matBlob = AdaptiveBinary(m_matBlur); blobImg = m_matBlob; // logStep("8.自适应二值化"); // 9. 反向映射:只生成下游需要的全尺寸残点 mask(resultMaskImg / Blob 计算)。 // 下游 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; 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(); double totalMs = std::chrono::duration_cast(tEnd - tStart).count(); // std::cout << "[TraditionalDetect] 总耗时(ms): " << totalMs << std::endl; } return 0; } // ============================================================ // TraditionalClassify — 独立分类接口 // 优先在小图缓存(m_matBlur/m_matBlob)上做连通域分析+缺陷分类, // 再将结果坐标映射回原图,避免对全尺寸 mask 做昂贵计算。 // 返回: 分类到的缺陷数量,<0 表示异常 // ============================================================ int CTcsCheck::TraditionalClassify(const cv::Mat& blobImg) { // 全尺寸 mask 仅是小图 INTER_NEAREST 放大结果,连通域数量/形状完全等价, // 在小图上分类可避免对全尺寸图做昂贵的 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()); } auto tStart = std::chrono::high_resolution_clock::now(); ClassifyBlobs(m_matBlur, m_matBlob); // 小图坐标/面积映射回原图坐标系,与下游 blobs.blobTab(全尺寸坐标)对齐 const double scaleX = (m_matBlob.cols > 0) ? static_cast(m_rtCrop.width) / m_matBlob.cols : 1.0; const double scaleY = (m_matBlob.rows > 0) ? static_cast(m_rtCrop.height) / m_matBlob.rows : 1.0; for (auto& info : m_vecDefectInfo) { info.nDefectX = static_cast(info.nDefectX * scaleX + 0.5) + m_rtCrop.x; info.nDefectY = static_cast(info.nDefectY * scaleY + 0.5) + m_rtCrop.y; info.nDefectWidth = std::max(1, static_cast(info.nDefectWidth * scaleX + 0.5)); info.nDefectHeight = std::max(1, static_cast(info.nDefectHeight * scaleY + 0.5)); info.nDefectArea = std::max(1, static_cast(info.nDefectArea * scaleX * scaleY + 0.5)); } auto tEnd = std::chrono::high_resolution_clock::now(); double totalMs = std::chrono::duration_cast(tEnd - tStart).count(); std::cout << "[TraditionalClassify] 总耗时(ms): " << totalMs << std::endl; return static_cast(m_vecDefectInfo.size()); } void CTcsCheck::ProcessImages(bool bDrawResult) { LoadImages(); std::cout << "TOTAL file count = " << m_fileList.size() << std::endl; for (int i = 0; i < m_fileList.size(); i ++ ) { m_matLoad = cv::imread(m_fileList[i],cv::IMREAD_GRAYSCALE); m_strCurFile = GetFileName(m_fileList[i]); m_sizeImage = m_matLoad.size(); Process(bDrawResult); } // 异步存图收尾:确保队列里的调试图全部落盘后再返回 CheckUtil::WaitSaveImgDone(); } void CTcsCheck::DetectWithAdaptiveBinary(bool bDraw ) { cv::Mat matBinary,matCrop,matResized,matBlur,matDraw; auto tStart = std::chrono::high_resolution_clock::now(); cv::threshold(m_matLoad,matBinary,m_cpCfg.nAreaLowFilter,255,cv::THRESH_BINARY); // Obtain Key Region Range cv::Rect rtValid = GetBoundingRect(matBinary); if (rtValid == cv::Rect(0,0,0,0)) { std::cout << "NO product" << std::endl; return ; } // crop image cv::Rect rtCrop = GetCropArea(rtValid); matCrop = m_matLoad(rtCrop).clone(); // image zoom const int outW = std::max(1, static_cast(matBinary.cols * m_cpCfg.fZoomRatio)); const int outH = std::max(1, static_cast(matBinary.rows * m_cpCfg.fZoomRatio)); cv::resize(matCrop, matResized, cv::Size(outW, outH), 0, 0, cv::INTER_AREA); // image blur cv::GaussianBlur(matResized, matBlur, cv::Size(5, 5), 0); // 二值化 m_matBlob = AdaptiveBinary(matBlur); // 分类:对残点二值图做连通域分析+缺陷分类 ClassifyBlobs(matBlur, m_matBlob); auto tEnd = std::chrono::high_resolution_clock::now(); double elapsedMs = std::chrono::duration_cast(tEnd - tStart).count(); std::cout << "耗时(ms): " << elapsedMs << std::endl; if (bDraw) { matDraw = DrawBlobInfoImage(matResized, m_matBlob); std::string strOutFile,strDebug; strOutFile = m_strDirOut + "/" + m_strCurFile + "_Binary.png"; cv::imwrite(strOutFile,matBinary); strOutFile = m_strDirOut + "/" +m_strCurFile + "_Crop.png"; cv::imwrite(strOutFile,matResized); strOutFile = m_strDirOut + "/" +m_strCurFile + "_Blob.png"; cv::imwrite(strOutFile,m_matBlob); strOutFile = m_strDirOut + "/" +m_strCurFile + "_Draw.png"; cv::imwrite(strOutFile,matDraw); } } void CTcsCheck::DetectWithDoH(bool bDraw,double dSigma,int nThreshold ) { cv::Mat matBinary,matCrop,matResized,matBlur,matDraw; auto tStart = std::chrono::high_resolution_clock::now(); cv::threshold(m_matLoad,matBinary,m_cpCfg.nAreaLowFilter,255,cv::THRESH_BINARY); // Obtain Key Region Range cv::Rect rtValid = GetBoundingRect(matBinary); if (rtValid == cv::Rect(0,0,0,0)) { std::cout << "NO product" << std::endl; return ; } // crop image cv::Rect rtCrop = GetCropArea(rtValid); matCrop = m_matLoad(rtCrop).clone(); // image zoom const int outW = std::max(1, static_cast(matBinary.cols * m_cpCfg.fZoomRatio)); const int outH = std::max(1, static_cast(matBinary.rows * m_cpCfg.fZoomRatio)); cv::resize(matCrop, matResized, cv::Size(outW, outH), 0, 0, cv::INTER_AREA); cv::Mat matFloat,matDoh,matNorm; // 转换为浮点型以进行精确的导数计算 matResized.convertTo(matFloat, CV_64F); // 2. 高斯平滑(可选,用于去噪和定义尺度) double sigma = dSigma; // 根据斑点大小调整 cv::Mat smoothed; cv::GaussianBlur(matFloat, matBlur, cv::Size(0, 0), sigma); // 3. 计算二阶导数 Ixx, Iyy, Ixy cv::Mat Ixx, Iyy, Ixy; cv::Sobel(matBlur, Ixx, CV_64F, 2, 0, 3); // 对 x 求二阶导 cv::Sobel(matBlur, Iyy, CV_64F, 0, 2, 3); // 对 y 求二阶导 cv::Sobel(matBlur, Ixy, CV_64F, 1, 1, 3); // 先对 x 求导,再对 y 求导 // 4. 计算 DoH 响应:det(H) = Ixx * Iyy - Ixy^2 cv::multiply(Ixx, Iyy, matDoh); cv::subtract(matDoh, Ixy.mul(Ixy), matDoh); // 5. 后处理:寻找局部极大值作为斑点 cv::normalize(matDoh, matNorm, 0, 255, cv::NORM_MINMAX, CV_8U); auto tEnd = std::chrono::high_resolution_clock::now(); double elapsedMs = std::chrono::duration_cast(tEnd - tStart).count(); std::cout << "耗时(ms): " << elapsedMs << std::endl; // 6. 绘制并显示结果 if ( bDraw) { // 寻找局部极大值(简化示例,实际应用需更严谨的邻域比较) std::vector keypoints; int threshold_value = nThreshold; // 需要根据图像调整的阈值 for (int i = 1; i < matDoh.rows - 1; ++i) { for (int j = 1; j < matDoh.cols - 1; ++j) { double val = matDoh.at(i, j); if (val > threshold_value) //&& val > matDoh.at(i - 1, j) && val > matDoh.at(i + 1, j) && //val > matDoh.at(i, j - 1) && val > matDoh.at(i, j + 1)) { // 此处可更精确地计算斑点的尺度和响应强度 keypoints.push_back(cv::KeyPoint(j, i, 2 * sigma)); } } } cv::cvtColor(matNorm, matDraw, cv::COLOR_GRAY2BGR); cv::drawKeypoints(matDraw, keypoints, matDraw, cv::Scalar(0, 0, 255), cv::DrawMatchesFlags::DRAW_RICH_KEYPOINTS); std::string strOutFile; strOutFile = m_strDirOut + "/" + m_strCurFile + "_Resize.png"; cv::imwrite(strOutFile,matResized); strOutFile = m_strDirOut + "/" + m_strCurFile + "_Norm.png"; cv::imwrite(strOutFile,matNorm); strOutFile = m_strDirOut + "/" + m_strCurFile + "_Draw.png"; cv::imwrite(strOutFile,matDraw); } } void CTcsCheck::DetectWithLoG(bool bDraw, double dSigma, int nThreshold) { // =================================================================== // LoG (Laplacian of Gaussian) 斑点检测 // 原理: LoG = trace(H) = Ixx + Iyy,是 Hessian 矩阵的迹 // 相比 DoH (det(H)=Ixx*Iyy-Ixy²): // - 速度: 只需1次 Laplacian,DoH 需要3次 Sobel + 乘减运算 (~3x 加速) // - 精度: LoG 是旋转不变的各向同性斑点检测器,对圆形缺陷响应最优 // =================================================================== cv::Mat matBinary, matCrop, matResized, matBlur, matDraw; auto tStart = std::chrono::high_resolution_clock::now(); // 1. 全局阈值 -> 定位产品区域 cv::threshold(m_matLoad, matBinary, m_cpCfg.nAreaLowFilter, 255, cv::THRESH_BINARY); // 2. 获取有效区域外接矩形 cv::Rect rtValid = GetBoundingRect(matBinary); if (rtValid == cv::Rect(0, 0, 0, 0)) { std::cout << "NO product" << std::endl; return; } // 3. 裁剪边缘 cv::Rect rtCrop = GetCropArea(rtValid); matCrop = m_matLoad(rtCrop).clone(); // 4. 缩放 const int outW = std::max(1, static_cast(matBinary.cols * m_cpCfg.fZoomRatio)); const int outH = std::max(1, static_cast(matBinary.rows * m_cpCfg.fZoomRatio)); cv::resize(matCrop, matResized, cv::Size(outW, outH), 0, 0, cv::INTER_AREA); // 5. 转浮点型 + 高斯平滑(尺度选择) cv::Mat matFloat; matResized.convertTo(matFloat, CV_64F); cv::GaussianBlur(matFloat, matBlur, cv::Size(0, 0), dSigma); // 6. 核心: Laplacian 响应(等价于 Ixx + Iyy) // 相比 DoH 省去了 Ixy 计算和乘减步骤 cv::Mat matLoG; cv::Laplacian(matBlur, matLoG, CV_64F, 3); // 7. 取绝对值(同时检测亮斑和暗斑) matLoG = cv::abs(matLoG); // 8. 归一化到 [0,255] cv::Mat matNorm; cv::normalize(matLoG, matNorm, 0, 255, cv::NORM_MINMAX, CV_8U); // 保存斑点图供外部使用 m_matBlob = matNorm.clone(); auto tEnd = std::chrono::high_resolution_clock::now(); double elapsedMs = std::chrono::duration_cast(tEnd - tStart).count(); std::cout << "LoG 耗时(ms): " << elapsedMs << std::endl; // 9. 可视化 if (bDraw) { // 非极大值抑制 + 阈值筛选关键点 std::vector keypoints; for (int i = 1; i < matLoG.rows - 1; ++i) { for (int j = 1; j < matLoG.cols - 1; ++j) { double val = matLoG.at(i, j); // 局部极大值检测(3×3邻域) if (val > nThreshold && val >= matLoG.at(i - 1, j) && val >= matLoG.at(i + 1, j) && val >= matLoG.at(i, j - 1) && val >= matLoG.at(i, j + 1)) { keypoints.push_back(cv::KeyPoint(j, i, 2.0f * static_cast(dSigma))); } } } cv::cvtColor(matNorm, matDraw, cv::COLOR_GRAY2BGR); cv::drawKeypoints(matDraw, keypoints, matDraw, cv::Scalar(0, 0, 255), cv::DrawMatchesFlags::DRAW_RICH_KEYPOINTS); std::string strOutFile; strOutFile = m_strDirOut + "/" + m_strCurFile + "_Resize.png"; cv::imwrite(strOutFile, matResized); strOutFile = m_strDirOut + "/" + m_strCurFile + "_Norm.png"; cv::imwrite(strOutFile, matNorm); strOutFile = m_strDirOut + "/" + m_strCurFile + "_Draw.png"; cv::imwrite(strOutFile, matDraw); } } void CTcsCheck::DetectWithAdaptiveBinaryOptimized(bool bDraw) { // =================================================================== // DetectWithAdaptiveBinary 优化版 // 与原始版保持相同的 OpenCV SIMD 加速路径, 避免 naive 像素循环 // // 当下真正有效的优化方向 (非本次实现): // - cv::UMat: 启用 OpenCL GPU 加速, 代码改动 2 行 (Mat → UMat) // - 多线程并行: 用 cv::parallel_for_ 处理分块 // - 缩小 nBlockSize 减少块数 (精度/速度 trade-off) // =================================================================== cv::Mat matBinary, matCrop, matResized, matBlur, matDraw; auto tStart = std::chrono::high_resolution_clock::now(); cv::threshold(m_matLoad, matBinary, m_cpCfg.nAreaLowFilter, 255, cv::THRESH_BINARY); // Obtain Key Region Range cv::Rect rtValid = GetBoundingRect(matBinary); if (rtValid == cv::Rect(0, 0, 0, 0)) { std::cout << "NO product" << std::endl; return; } // crop image cv::Rect rtCrop = GetCropArea(rtValid); matCrop = m_matLoad(rtCrop).clone(); // image zoom const int outW = std::max(1, static_cast(matBinary.cols * m_cpCfg.fZoomRatio)); const int outH = std::max(1, static_cast(matBinary.rows * m_cpCfg.fZoomRatio)); cv::resize(matCrop, matResized, cv::Size(outW, outH), 0, 0, cv::INTER_AREA); // image blur cv::GaussianBlur(matResized, matBlur, cv::Size(5, 5), 0); // 二值化 — 使用原始 AdaptiveBinary (OpenCV SIMD 内部加速) m_matBlob = AdaptiveBinary(matBlur); // 分类:对残点二值图做连通域分析+缺陷分类 ClassifyBlobs(matBlur, m_matBlob); auto tEnd = std::chrono::high_resolution_clock::now(); double elapsedMs = std::chrono::duration_cast(tEnd - tStart).count(); std::cout << "AdaptiveBinaryOpt 耗时(ms): " << elapsedMs << std::endl; if (bDraw) { matDraw = DrawBlobInfoImage(matResized, m_matBlob); std::string strOutFile; strOutFile = m_strDirOut + "/" + m_strCurFile + "_Binary.png"; cv::imwrite(strOutFile, matBinary); strOutFile = m_strDirOut + "/" + m_strCurFile + "_Crop.png"; cv::imwrite(strOutFile, matResized); strOutFile = m_strDirOut + "/" + m_strCurFile + "_Blob.png"; cv::imwrite(strOutFile, m_matBlob); strOutFile = m_strDirOut + "/" + m_strCurFile + "_Draw.png"; cv::imwrite(strOutFile, matDraw); } } void CTcsCheck::Process(bool bDraw) { //默认使用优化版 AdaptiveBinary:积分图加速 + findNonZero 快速定位 //DetectWithAdaptiveBinaryOptimized(bDraw); // 备选方法: DetectWithAdaptiveBinary(bDraw); // DetectWithLoG(bDraw, 2.0, 48); // DetectWithDoH(bDraw, 2.0, 48); }