You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

981 lines
36 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#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<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);
}
// 产品定位用的「最大连通域外接框」。
//
// 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<long long>(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<long long>(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<std::chrono::milliseconds>(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<int>(label, cv::CC_STAT_AREA);
if (area < 5) continue;
// 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 区域均值:仅在 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<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);
}
// logStep("4.缺陷分类决策");
// // 总耗时
// {
// auto tEnd = std::chrono::high_resolution_clock::now();
// double totalMs = std::chrono::duration_cast<std::chrono::milliseconds>(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<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;
}
// ============================================================
// 独立检测接口
// 对单张图做:阈值定位→裁剪→缩放→高斯模糊→自适应二值化
// 输出残点二值图 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<std::chrono::milliseconds>(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<int>(matCrop.cols * m_cpCfg.fZoomRatio));
const int outH = std::max(1, static_cast<int>(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<std::chrono::milliseconds>(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<double>(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<int>(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<double>(m_rtCrop.width) / m_matBlob.cols : 1.0;
const double scaleY = (m_matBlob.rows > 0) ? static_cast<double>(m_rtCrop.height) / m_matBlob.rows : 1.0;
for (auto& info : m_vecDefectInfo)
{
info.nDefectX = static_cast<int>(info.nDefectX * scaleX + 0.5) + m_rtCrop.x;
info.nDefectY = static_cast<int>(info.nDefectY * scaleY + 0.5) + m_rtCrop.y;
info.nDefectWidth = std::max(1, static_cast<int>(info.nDefectWidth * scaleX + 0.5));
info.nDefectHeight = std::max(1, static_cast<int>(info.nDefectHeight * scaleY + 0.5));
info.nDefectArea = std::max(1, static_cast<int>(info.nDefectArea * scaleX * scaleY + 0.5));
}
auto tEnd = std::chrono::high_resolution_clock::now();
double totalMs = std::chrono::duration_cast<std::chrono::milliseconds>(tEnd - tStart).count();
std::cout << "[TraditionalClassify] 总耗时(ms): " << totalMs << std::endl;
return static_cast<int>(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<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次 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<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);
}