|
|
|
@ -101,9 +101,12 @@ void CTcsCheck::LoadImages()
|
|
|
|
CreateDirectories(m_strDirOut);
|
|
|
|
CreateDirectories(m_strDirOut);
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
cv::Rect CTcsCheck::GetBoundingRect(cv::Mat matBinary)
|
|
|
|
// 在给定二值图上取「最大连通域的外接框」(输入需为 CV_8UC1)
|
|
|
|
|
|
|
|
static cv::Rect LargestComponentRect(const cv::Mat &matBinary)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
cv::Mat labels;
|
|
|
|
if (matBinary.empty()) return cv::Rect(0, 0, 0, 0);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cv::Mat labels;
|
|
|
|
cv::Mat stats;
|
|
|
|
cv::Mat stats;
|
|
|
|
cv::Mat centroids;
|
|
|
|
cv::Mat centroids;
|
|
|
|
const int nLabels = cv::connectedComponentsWithStats(matBinary, labels, stats, centroids, 8, CV_32S);
|
|
|
|
const int nLabels = cv::connectedComponentsWithStats(matBinary, labels, stats, centroids, 8, CV_32S);
|
|
|
|
@ -131,6 +134,67 @@ cv::Rect CTcsCheck::GetBoundingRect(cv::Mat matBinary)
|
|
|
|
const int h = stats.at<int>(maxLabel, cv::CC_STAT_HEIGHT);
|
|
|
|
const int h = stats.at<int>(maxLabel, cv::CC_STAT_HEIGHT);
|
|
|
|
return cv::Rect(x, y, w, h);
|
|
|
|
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)
|
|
|
|
cv::Rect CTcsCheck::GetCropArea(cv::Rect rtValid)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
// 在原图坐标系中对 ROI 四边分别内收对应的 discard 宽度。
|
|
|
|
// 在原图坐标系中对 ROI 四边分别内收对应的 discard 宽度。
|
|
|
|
@ -427,6 +491,13 @@ int CTcsCheck::TraditionalDetect(const cv::Mat& img, cv::Rect detRoi, cv::Mat& b
|
|
|
|
// 确保 m_sizeImage 始终与 m_matLoad 同步
|
|
|
|
// 确保 m_sizeImage 始终与 m_matLoad 同步
|
|
|
|
m_sizeImage = img.size();
|
|
|
|
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;
|
|
|
|
cv::Rect rtCrop = detRoi;
|
|
|
|
if(detRoi.size() == img.size())
|
|
|
|
if(detRoi.size() == img.size())
|
|
|
|
{
|
|
|
|
{
|
|
|
|
@ -439,6 +510,7 @@ int CTcsCheck::TraditionalDetect(const cv::Mat& img, cv::Rect detRoi, cv::Mat& b
|
|
|
|
cv::Rect rtValid = GetBoundingRect(matBinary);
|
|
|
|
cv::Rect rtValid = GetBoundingRect(matBinary);
|
|
|
|
if (rtValid == cv::Rect(0, 0, 0, 0))
|
|
|
|
if (rtValid == cv::Rect(0, 0, 0, 0))
|
|
|
|
{
|
|
|
|
{
|
|
|
|
|
|
|
|
m_matLoad.release(); // 不再需要引用 detImg
|
|
|
|
blobImg = cv::Mat();
|
|
|
|
blobImg = cv::Mat();
|
|
|
|
return -1;
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -455,6 +527,7 @@ int CTcsCheck::TraditionalDetect(const cv::Mat& img, cv::Rect detRoi, cv::Mat& b
|
|
|
|
rtCrop = rtCrop & imgRect;
|
|
|
|
rtCrop = rtCrop & imgRect;
|
|
|
|
if (rtCrop.width <= 0 || rtCrop.height <= 0)
|
|
|
|
if (rtCrop.width <= 0 || rtCrop.height <= 0)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
|
|
|
|
m_matLoad.release(); // 不再需要引用 detImg
|
|
|
|
blobImg = cv::Mat();
|
|
|
|
blobImg = cv::Mat();
|
|
|
|
return -1;
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -485,17 +558,36 @@ int CTcsCheck::TraditionalDetect(const cv::Mat& img, cv::Rect detRoi, cv::Mat& b
|
|
|
|
// logStep("8.自适应二值化");
|
|
|
|
// logStep("8.自适应二值化");
|
|
|
|
|
|
|
|
|
|
|
|
// 9. 反向映射:只生成下游需要的全尺寸残点 mask(resultMaskImg / Blob 计算)。
|
|
|
|
// 9. 反向映射:只生成下游需要的全尺寸残点 mask(resultMaskImg / Blob 计算)。
|
|
|
|
// 直接 resize 到 ROI 上,省去中间 matCropSize 缓冲与 copyTo;
|
|
|
|
// 下游 GetALLBlob / BLobToDetResult 都以 detImg 为坐标基准,所以 mask 必须与 detImg 等大,
|
|
|
|
// m_matBlob 保持小图即可,全尺寸副本无任何消费者,已移除。
|
|
|
|
// 且 ROI 之外必须是 0。
|
|
|
|
|
|
|
|
// 写法上不再用 cv::Mat::zeros(全图)(那会先给整幅图做一次 memset,22754x60000 时约 1.3 GB),
|
|
|
|
|
|
|
|
// 而是创建一个不初始化的缓冲,只把 ROI 之外的条带清零 —— 结果完全等价,省一次全图 memset。
|
|
|
|
if (m_cpCfg.fZoomRatio > 0)
|
|
|
|
if (m_cpCfg.fZoomRatio > 0)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
cv::Mat fullMask = cv::Mat::zeros(m_matLoad.size(), CV_8UC1);
|
|
|
|
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);
|
|
|
|
cv::resize(m_matBlob, fullMask(rtCrop), cv::Size(rtCrop.width, rtCrop.height), 0, 0, cv::INTER_NEAREST);
|
|
|
|
blobImg = fullMask;
|
|
|
|
blobImg = fullMask;
|
|
|
|
|
|
|
|
|
|
|
|
// logStep("9.反向映射(mask)");
|
|
|
|
// logStep("9.反向映射(mask)");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 检测期间只需要引用 detImg,结束即松开:
|
|
|
|
|
|
|
|
// 否则 CTcsCheck 会一直攥住整幅检测图(GB 级)不放,上层即使释放 detImg 也回收不掉。
|
|
|
|
|
|
|
|
// (分类阶段只用 m_matBlur / m_matBlob 这两张小图)
|
|
|
|
|
|
|
|
m_matLoad.release();
|
|
|
|
|
|
|
|
|
|
|
|
// 总耗时
|
|
|
|
// 总耗时
|
|
|
|
{
|
|
|
|
{
|
|
|
|
auto tEnd = std::chrono::high_resolution_clock::now();
|
|
|
|
auto tEnd = std::chrono::high_resolution_clock::now();
|
|
|
|
@ -518,8 +610,21 @@ int CTcsCheck::TraditionalClassify(const cv::Mat& blobImg)
|
|
|
|
// 在小图上分类可避免对全尺寸图做昂贵的 connectedComponentsWithStats。
|
|
|
|
// 在小图上分类可避免对全尺寸图做昂贵的 connectedComponentsWithStats。
|
|
|
|
if (m_matBlur.empty() || m_matBlob.empty())
|
|
|
|
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;
|
|
|
|
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);
|
|
|
|
ClassifyBlobs(m_matLoad, blobImg);
|
|
|
|
return static_cast<int>(m_vecDefectInfo.size());
|
|
|
|
return static_cast<int>(m_vecDefectInfo.size());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|