diff --git a/CMakeLists.txt b/CMakeLists.txt index 09ea4ac..1808a0e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,6 +65,9 @@ MESSAGE("ExtractImageModule") add_subdirectory(ConfigModule) MESSAGE("ConfigModule") +add_subdirectory(TcsCheckModule) +MESSAGE("TcsCheckModule") + # CommonUtil add_subdirectory(AlgorithmModule) MESSAGE("AlgorithmModule") diff --git a/TcsCheckModule/CMakeLists.txt b/TcsCheckModule/CMakeLists.txt new file mode 100644 index 0000000..acf1c84 --- /dev/null +++ b/TcsCheckModule/CMakeLists.txt @@ -0,0 +1,43 @@ +#版本限定 +cmake_minimum_required (VERSION 3.5) + +set(ModuleName "TcsCheckModule") + +include(${PROJECT_SOURCE_DIR}/cmake/default_variabes.cmake) +include(${PROJECT_SOURCE_DIR}/cmake/cpp_c_flags.cmake) +include(${PROJECT_SOURCE_DIR}/cmake/print_archs.cmake) + +#头文件 +include_directories( +/usr/local/include +${CMAKE_CURRENT_SOURCE_DIR}/include +${OpenCV_INCLUDE_DIRS} +) +link_directories( +/usr/local/lib/ +) + +# 用set设置变量不能使用*.cpp +file(GLOB SRC_LISTS ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp) + +add_library(TcsCheck SHARED ${SRC_LISTS}) + +target_link_libraries(TcsCheck + ${OpenCV_LIBS} + pthread + ) + +set(ModuleName "") + +# make install 安装到/usr/local下 +# 自定义安装前缀 +set(CMAKE_INSTALL_PREFIX /usr/local/cellAOI CACHE PATH "Install path prefix" FORCE) +set(HEADER_FILES include/TcsCheck.h include/TcsConfig.h) +# 安装动态库 +install(TARGETS TcsCheck + LIBRARY DESTINATION lib # 安装到 CMAKE_INSTALL_PREFIX/lib + ARCHIVE DESTINATION lib/static + RUNTIME DESTINATION bin + PUBLIC_HEADER DESTINATION include) # 安装到 CMAKE_INSTALL_PREFIX/include +# 安装头文件 +install(FILES ${HEADER_FILES} DESTINATION include) diff --git a/TcsCheckModule/include/TcsCheck.h b/TcsCheckModule/include/TcsCheck.h new file mode 100644 index 0000000..a214a2b --- /dev/null +++ b/TcsCheckModule/include/TcsCheck.h @@ -0,0 +1,131 @@ +#ifndef _TCSCHECK_H +#define _TCSCHECK_H +#include +#include +#include +#include +#include +#include // 必需:mkdir 函数声明 +#include // 可选:通常被 sys/stat.h 包含,但建议显式包含 +#include "TcsConfig.h" +#include + + +struct CHECK_PARAM{ + int nAreaLowFilter; // low filter of the product area + int nDiscardTop; //Discard top edge width of the effective area + int nDiscardBottom; //Discard bottom edge width of the effective area + int nDiscardLeft; //Discard left edge width of the effective area + int nDiscardRight; //Discard right edge width of the effective area + + float fZoomRatio; // scaling ratio of the source image + + int nFilterLow; // low threshold of the filter + int nFilterHigh; // high threshold of the filter + + int nBlockSize; // the size of the block + + int nAreaFilter; // blob filter for drawing, only draw blob with area >= nAreaFilter + + int nCountFilter; // blob filter for counting, Only the first nCountFilters with areas arranged from largest to smallest +}; +/* +长宽比 >= 3 ? + ├─ 是 → SCRATCH (划伤) + └─ 否(紧凑形状): + ├─ area < 大块阈值 → POINT (点型,不论黑白) + └─ area >= 大块阈值: + ├─ greyDiff < 0 → DIRTY (黑色大块脏污) + └─ greyDiff > 0 → FADING_SPOTS (白色大块淡斑) +*/ +enum DEFECT_TYPE_DEFINE{ + DEFECT_TYPE_OK = 0, + DEFECT_TYPE_POINT = 1, + DEFECT_TYPE_SCRATCH = 2, // white line + DEFECT_TYPE_DIRTY = 3, + DEFECT_TYPE_FADING_SPOTS = 4 +}; +inline const std::string DEFECT_TYPE_CODE[5] = { + "P0000", + "MA505", + "MA506", + "MA504", + "P0003" +}; +inline const std::string DEFECT_TYPE_DESC[5] = { + "OK", + "硬质颗粒", + "划伤", + "脏污", + "淡斑" +}; + +struct DEFECT_INFO{ + int nDefectType; + + int nDefectArea; + int nDefectX; + int nDefectY; + int nDefectWidth; + int nDefectHeight; + double dGreyDiff; // 灰阶差:blob均值 - 所属局部块均值 + std::string strDefectCode; + std::string strDefectDesc; +}; +class CTcsCheck{ +public: + int m_nInitStart; + + int m_bSystemExit; + + CHECK_PARAM m_cpCfg; + + int m_nInitEnd; + + pthread_mutex_t m_mutex; + + std::vector m_fileList; + + std::vector m_vecDefectInfo; + + CTcsCheck(); + ~CTcsCheck(); + + void SetCheckDir(std::string dirIn,std::string dirOut); + void SetChecConfig(CHECK_PARAM* cp); + void ProcessImages(bool bDrawResult); + +private: + + std::string m_strDirIn; + std::string m_strDirOut; + + cv::Mat m_matLoad; + cv::Mat m_matBlob; + cv::Mat m_matDraw; + + cv::Size m_sizeImage; + + std::string m_strCurFile; + + bool CreateDirectories(std::string path, mode_t mode = 0755) ; + void LoadImages(); + std::string GetFileName(const std::string& path) const; + cv::Rect GetBoundingRect(cv::Mat matBinary); + cv::Rect GetCropArea(cv::Rect rtValid); + cv::Mat AdaptiveBinary(cv::Mat matBlur); + + void Process(bool bDraw = false); + + // 纯分类:对二值图做连通域分析+缺陷分类,结果写入 m_vecDefectInfo + void ClassifyBlobs(const cv::Mat& blurCrop, const cv::Mat& imgBlob); + // 纯绘制:基于 m_vecDefectInfo 绘制缺陷标注 + cv::Mat DrawBlobInfoImage(const cv::Mat& imgCrop, const cv::Mat& imgBlob); + + void DetectWithAdaptiveBinary(bool bDraw = false); + void DetectWithAdaptiveBinaryOptimized(bool bDraw = false); + void DetectWithDoH(bool bDraw = false, double dSigma = 1.0 ,int nThreshold = 50); + void DetectWithLoG(bool bDraw = false, double dSigma = 1.0, int nThreshold = 50); +}; + +#endif \ No newline at end of file diff --git a/TcsCheckModule/include/TcsConfig.h b/TcsCheckModule/include/TcsConfig.h new file mode 100644 index 0000000..5b5dfde --- /dev/null +++ b/TcsCheckModule/include/TcsConfig.h @@ -0,0 +1,8 @@ +#ifndef _TCSCONFIG_H +#define _TCSCONFIG_H + +#include + + + +#endif diff --git a/TcsCheckModule/src/TcsCheck.cpp b/TcsCheckModule/src/TcsCheck.cpp new file mode 100644 index 0000000..b4aea37 --- /dev/null +++ b/TcsCheckModule/src/TcsCheck.cpp @@ -0,0 +1,635 @@ +#include "TcsCheck.h" + +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() +{ + memset(&m_nInitStart, 0, offsetof(CTcsCheck, m_nInitEnd) - offsetof(CTcsCheck, m_nInitStart) + sizeof(m_nInitEnd)); + + 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) +{ + memcpy(&m_cpCfg,cp,sizeof(CHECK_PARAM)); +} +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::Rect CTcsCheck::GetBoundingRect(cv::Mat matBinary) +{ + 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); +} +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; +} +cv::Mat CTcsCheck::AdaptiveBinary(cv::Mat matBlur) +{ + // 按 localBlockSize_ 分块,使用每个块的局部均值做动态阈值。 + // 当前规则:落在 [avg-lower_, avg+upper_] 内置 0,超出置 255。 + cv::Mat result = cv::Mat::zeros(matBlur.size(), CV_8UC1); + 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, blockW, blockH); + cv::Mat block = matBlur(blockRect); + + const double avg = cv::mean(block)[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)); + } + } + return result; +} +// ============================================================ +// ClassifyBlobs — 纯分类函数 +// 对残点二值图做连通域分析 + 缺陷分类,结果写入 m_vecDefectInfo +// ============================================================ +void CTcsCheck::ClassifyBlobs(const cv::Mat& blurCrop, const cv::Mat& imgBlob) +{ + m_vecDefectInfo.clear(); + + cv::Mat labels; + cv::Mat stats; + cv::Mat centroids; + const int nLabels = cv::connectedComponentsWithStats(imgBlob, labels, stats, centroids, 8, CV_32S); + if (nLabels <= 1) return; + + struct BlobItem { int label; int area; }; + std::vector blobs; + blobs.reserve(std::max(0, nLabels - 1)); + for (int label = 1; label < nLabels; ++label) { + const int area = stats.at(label, cv::CC_STAT_AREA); + if (area > m_cpCfg.nAreaFilter) { + blobs.push_back({ label, area }); + } + } + if (blobs.empty()) return; + + std::sort(blobs.begin(), blobs.end(), [](const BlobItem& a, const BlobItem& b) { + return a.area > b.area; + }); + + // 大块缺陷的面积阈值 + const int largeAreaThreshold = std::max(1, m_cpCfg.nBlockSize * m_cpCfg.nBlockSize / 4); + const int topK = std::min(m_cpCfg.nCountFilter, static_cast(blobs.size())); + + for (int i = 0; i < topK; ++i) { + const int label = blobs[i].label; + const int area = blobs[i].area; + + // 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 区域均值 + cv::Mat blobMask = (labels == label); + double blobMean = cv::mean(blurCrop, 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); + } +} + +// ============================================================ +// 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; +} +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); + } + +} + + +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); +} \ No newline at end of file diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index e5dbb8d..b5f64d4 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -15,6 +15,7 @@ ${CMAKE_CURRENT_SOURCE_DIR}/include ${PROJECT_SOURCE_DIR}/AlgorithmModule/include ${PROJECT_SOURCE_DIR}/ConfigModule/include ${PROJECT_SOURCE_DIR}/ExtractImageModule/include +${PROJECT_SOURCE_DIR}/TcsCheckModule/include ) link_directories( @@ -22,6 +23,8 @@ link_directories( /usr/local/cuda/lib64 ) file(GLOB SRC_LISTS ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp) +# 排除独立测试文件 +list(FILTER SRC_LISTS EXCLUDE REGEX ".*/tcs_test\\.cpp$") add_executable("test_CellAOI" ${SRC_LISTS}) @@ -32,6 +35,7 @@ target_link_libraries("test_CellAOI" TY_Check Config ExtractImage + TcsCheck #/usr/local/cuda-12.1/targets/x86_64-linux/lib/libcudart.so ${OpenCV_LIBS} ) @@ -39,4 +43,16 @@ target_link_libraries("test_CellAOI" set_target_properties("test_CellAOI" PROPERTIES BUILD_RPATH "\$ORIGIN" ) + +# TCS 传统算法检测独立测试程序 +add_executable("test_CellAOI_Tcs" tcs_test.cpp) +target_link_libraries("test_CellAOI_Tcs" + TcsCheck + ${OpenCV_LIBS} + pthread +) +set_target_properties("test_CellAOI_Tcs" PROPERTIES + BUILD_RPATH "\$ORIGIN" +) + set(ModuleName "") \ No newline at end of file diff --git a/example/tcs_test.cpp b/example/tcs_test.cpp new file mode 100644 index 0000000..b2caed7 --- /dev/null +++ b/example/tcs_test.cpp @@ -0,0 +1,32 @@ +#include "TcsCheck.h" + +int main(int argc, char* argv[]) +{ + CHECK_PARAM cp; + cp.nAreaLowFilter = 80; + cp.nBlockSize = 100; + cp.nDiscardTop = 200; + cp.nDiscardBottom = 200; + cp.nDiscardLeft = 300; + cp.nDiscardRight = 600; + cp.fZoomRatio = 0.25; + cp.nFilterLow = 15; + cp.nFilterHigh = 15; + cp.nAreaFilter = 10; + cp.nCountFilter = 50; + + CTcsCheck detect; + detect.SetChecConfig(&cp); + + std::string strDirIn = "/home/aidlux/cbh/CellAoiTcs/TestImage/G3A00M62293829AL03"; + std::string strDirOut = "/home/aidlux/cbh/CellAoiTcs/OutImage"; + + detect.SetCheckDir(strDirIn, strDirOut); + + std::cout << "Image start process" << std::endl; + detect.ProcessImages(true); + + std::cout << "Image finished " << std::endl; + + return 0; +}