update 优化传统检测模块

dev_lsy
liusiyang 1 month ago
parent 4005330c7f
commit c08187e3ae

@ -65,6 +65,9 @@ MESSAGE("ExtractImageModule")
add_subdirectory(ConfigModule)
MESSAGE("ConfigModule")
add_subdirectory(TcsCheckModule)
MESSAGE("TcsCheckModule")
# CommonUtil
add_subdirectory(AlgorithmModule)
MESSAGE("AlgorithmModule")

@ -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)

@ -0,0 +1,131 @@
#ifndef _TCSCHECK_H
#define _TCSCHECK_H
#include <thread>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <iostream>
#include <sys/stat.h> // 必需mkdir 函数声明
#include <sys/types.h> // 可选:通常被 sys/stat.h 包含,但建议显式包含
#include "TcsConfig.h"
#include <opencv2/opencv.hpp>
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<cv::String> m_fileList;
std::vector<DEFECT_INFO> 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

@ -0,0 +1,8 @@
#ifndef _TCSCONFIG_H
#define _TCSCONFIG_H
#include <opencv2/opencv.hpp>
#endif

@ -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<int>(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<int>(maxLabel, cv::CC_STAT_LEFT);
const int y = stats.at<int>(maxLabel, cv::CC_STAT_TOP);
const int w = stats.at<int>(maxLabel, cv::CC_STAT_WIDTH);
const int h = stats.at<int>(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<BlobItem> blobs;
blobs.reserve(std::max(0, nLabels - 1));
for (int label = 1; label < nLabels; ++label) {
const int area = stats.at<int>(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<int>(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<int>(label, cv::CC_STAT_LEFT);
const int y = stats.at<int>(label, cv::CC_STAT_TOP);
const int w = stats.at<int>(label, cv::CC_STAT_WIDTH);
const int h = stats.at<int>(label, cv::CC_STAT_HEIGHT);
// blob 区域均值
cv::Mat blobMask = (labels == label);
double blobMean = cv::mean(blurCrop, blobMask)[0];
// 所属局部块均值
const double cx = centroids.at<double>(label, 0);
const double cy = centroids.at<double>(label, 1);
int blockX = std::max(0, std::min(static_cast<int>(cx) / m_cpCfg.nBlockSize, (blurCrop.cols - 1) / m_cpCfg.nBlockSize));
int blockY = std::max(0, std::min(static_cast<int>(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<float>(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<std::vector<cv::Point>> 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<int>(matBinary.cols * m_cpCfg.fZoomRatio));
const int outH = std::max(1, static_cast<int>(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<std::chrono::milliseconds>(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<int>(matBinary.cols * m_cpCfg.fZoomRatio));
const int outH = std::max(1, static_cast<int>(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<std::chrono::milliseconds>(tEnd - tStart).count();
std::cout << "耗时(ms): " << elapsedMs << std::endl;
// 6. 绘制并显示结果
if ( bDraw)
{
// 寻找局部极大值(简化示例,实际应用需更严谨的邻域比较)
std::vector<cv::KeyPoint> 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<double>(i, j);
if (val > threshold_value)
//&& val > matDoh.at<double>(i - 1, j) && val > matDoh.at<double>(i + 1, j) &&
//val > matDoh.at<double>(i, j - 1) && val > matDoh.at<double>(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次 LaplacianDoH 需要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<int>(matBinary.cols * m_cpCfg.fZoomRatio));
const int outH = std::max(1, static_cast<int>(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<std::chrono::milliseconds>(tEnd - tStart).count();
std::cout << "LoG 耗时(ms): " << elapsedMs << std::endl;
// 9. 可视化
if (bDraw)
{
// 非极大值抑制 + 阈值筛选关键点
std::vector<cv::KeyPoint> keypoints;
for (int i = 1; i < matLoG.rows - 1; ++i)
{
for (int j = 1; j < matLoG.cols - 1; ++j)
{
double val = matLoG.at<double>(i, j);
// 局部极大值检测3×3邻域
if (val > nThreshold &&
val >= matLoG.at<double>(i - 1, j) &&
val >= matLoG.at<double>(i + 1, j) &&
val >= matLoG.at<double>(i, j - 1) &&
val >= matLoG.at<double>(i, j + 1))
{
keypoints.push_back(cv::KeyPoint(j, i, 2.0f * static_cast<float>(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<int>(matBinary.cols * m_cpCfg.fZoomRatio));
const int outH = std::max(1, static_cast<int>(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<std::chrono::milliseconds>(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);
}

@ -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 "")

@ -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;
}
Loading…
Cancel
Save