|
|
|
|
@ -0,0 +1,981 @@
|
|
|
|
|
#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);
|
|
|
|
|
}
|