Compare commits

...

49 Commits

Author SHA1 Message Date
liusiyang 12404cc400 feat 初步添加暗场复用明场定位结果
2 months ago
liusiyang 92850246a4 update 优化内存泄漏
2 months ago
liusiyang db455bd0cb feat 临时补丁
2 months ago
liusiyang 6099c864ee update 优化物理坐标计算
2 months ago
liusiyang ca8f64b104 feat 打印参数版本
2 months ago
liusiyang 6daa5eef5e v1.2.4
2 months ago
liusiyang 40d1e97ec9 update 修改拼接方式
2 months ago
liusiyang e138c826a5 feat 优化传统检测
2 months ago
liusiyang 5d56785769 feat 添加异步存图
2 months ago
liusiyang 2a987af65f v1.2.3
2 months ago
liusiyang d13a6d5f39 fix -f传不了相对路径
2 months ago
liusiyang de04f149f8 feat 适配hard存图文件名
2 months ago
liusiyang 2a5404c6b1 feat 添加输出参数接口
2 months ago
liusiyang e871478bc0 feat 优化上下错位
2 months ago
liusiyang b955d4725f v1.2.1
2 months ago
liusiyang ffdd6db394 feat 开放hoverlap参数
2 months ago
liusiyang 1adad0a250 fix 错位bug,添加物理坐标绘制
2 months ago
liusiyang 5d80fc5c8e update 拼图上下错位
2 months ago
liusiyang 718d12b26b update 优化返回的物理坐标和宽高
2 months ago
liusiyang 76edbade98 v1.2.0
2 months ago
liusiyang 844a32c181 fix AI结果小图存储
2 months ago
liusiyang 90c604ac45 feat 拼图去除overlap
2 months ago
liusiyang 61f3682680 update 传统检测优化过滤
2 months ago
liusiyang 0e72b46f87 update 优化传统分类耗时
2 months ago
liusiyang a979c07179 feat 添加AI检测存图
2 months ago
liusiyang 640f121f05 feat 添加传统检测存图,传统检测区域自适应
2 months ago
liusiyang 444e57a52d update 同步云平台边损2
2 months ago
liusiyang d509bcbaa2 update 同步云平台ResultJudge
2 months ago
liusiyang 7aca87a0bf update 优化传统检测
2 months ago
liusiyang ae6cfc95c1 update 修改模型读取路径,添加暗场模型
2 months ago
liusiyang e102fc5edd 暗场临时不使用AI定位
2 months ago
liusiyang 9f5ea55415 feat 添加检测开关
2 months ago
liusiyang 53eb1362ed v.test
2 months ago
liusiyang 1d9168128b Revert "feat 传统检测模式添加AI结果存图"
2 months ago
liusiyang 3655110ce2 update 改为简单拼接
2 months ago
liusiyang 4643bc12d0 update 改为4显卡
2 months ago
liusiyang 1a1edf8475 feat 传统检测模式添加AI结果存图
2 months ago
liusiyang b663c7f676 update 同步云平台边损更新
3 months ago
liusiyang c2f9d93b2f update 参数配置添加高低阈值,分块逻辑修改
4 months ago
liusiyang 740cc346e6 fix 批量跑进行拼接
4 months ago
liusiyang a86bd821b6 fix 传统检测区域位置偏移
4 months ago
liusiyang 3377bfd34f update 崩边检测使用曲线拟合边缘
4 months ago
liusiyang 22dd824f94 feat 添加传统检测相关参数配置
4 months ago
liusiyang e16100f205 fix 传统检测resize回原图尺寸
4 months ago
liusiyang ac5c51758c feat 初步实现传统检测
4 months ago
liusiyang 2e6bf04df7 fix InitMat使用Scalar赋值mat崩溃
4 months ago
liusiyang c08187e3ae update 优化传统检测模块
4 months ago
liusiyang 4005330c7f update 优化自适应区域
4 months ago
liusiyang d64b07472e update 修改为4相机
4 months ago

1
.gitignore vendored

@ -3,4 +3,5 @@
/include
/data
/SaveImg
/tmp
.vscode/launch.json

@ -103,6 +103,8 @@ int main(int argc, char *argv[])
GPU_Config gpu;
gpu.gpu_0 = true;
gpu.gpu_1 = true;
gpu.gpu_2 = true;
gpu.gpu_3 = true;
AI_Factory->InitALLAIModle(gpu);
std::shared_ptr<AIModel_Base> BOE_Edge_Detect = AI_Factory->BOE_Edge_Detect;

@ -31,10 +31,14 @@ struct GPU_Config
{
bool gpu_0;
bool gpu_1;
bool gpu_2;
bool gpu_3;
GPU_Config()
{
gpu_0 = true;
gpu_1 = true;
gpu_2 = true;
gpu_3 = true;
}
int GetNum()
{
@ -47,12 +51,22 @@ struct GPU_Config
{
num++;
}
if (gpu_2)
{
num++;
}
if (gpu_3)
{
num++;
}
return num;
}
void copy(GPU_Config tem)
{
this->gpu_0 = tem.gpu_0;
this->gpu_1 = tem.gpu_1;
this->gpu_2 = tem.gpu_2;
this->gpu_3 = tem.gpu_3;
}
};
@ -156,8 +170,10 @@ public:
std::shared_ptr<AIModel_Base> CELL_Mark;
std::shared_ptr<AIModel_Base> CELL_Align;
std::shared_ptr<AIModel_Base> CELL_CA_Det;
std::shared_ptr<AIModel_Base> CELL_TA_Det;
std::shared_ptr<AIModel_Base> CELL_BCA_Det;
std::shared_ptr<AIModel_Base> CELL_BTA_Det;
std::shared_ptr<AIModel_Base> CELL_DCA_Det;
std::shared_ptr<AIModel_Base> CELL_DTA_Det;
std::shared_ptr<AIModel_Base> CELL_CA_Cls;
std::shared_ptr<AIModel_Base> CELL_TA_Cls;

@ -48,22 +48,22 @@ int AIModel_Impl::Init(AIModelRun_Config config)
if (m_modelRun_Config.gpuconfig.gpu_0)
{
re = LoadEngine(0);
// 加载失败
if (re != 0)
{
}
}
if (m_modelRun_Config.gpuconfig.gpu_1)
{
re = LoadEngine(1);
// 加载失败
if (re != 0)
}
if (m_modelRun_Config.gpuconfig.gpu_2)
{
re = LoadEngine(2);
}
if (m_modelRun_Config.gpuconfig.gpu_3)
{
re = LoadEngine(3);
}
int nGPUNum = m_GPU_Engine.size();
// 模型载入失败
if (nGPUNum <= 0 || nGPUNum > 2)
if (nGPUNum <= 0 || nGPUNum > 4)
{
printf(" %s model Init error \n", m_modelRun_Config.strName.c_str());
return 2;
@ -113,12 +113,16 @@ int AIModel_Impl::Init(AIModelRun_Config config)
// 生产推理用的 参数信息
if (m_GPU_Engine.size() > 1)
{
if (m_GPU_Engine.at(0)->cudaSteams.size() != m_GPU_Engine.at(1)->cudaSteams.size())
size_t firstStreamNum = m_GPU_Engine.at(0)->cudaSteams.size();
for (size_t i = 1; i < m_GPU_Engine.size(); i++)
{
if (m_GPU_Engine.at(i)->cudaSteams.size() != firstStreamNum)
{
printf(" %s model Init error \n", m_modelRun_Config.strName.c_str());
return 3;
}
}
}
if (streamNum <= 0)
{
printf(" %s model Init streamNum error \n", m_modelRun_Config.strName.c_str());
@ -175,17 +179,24 @@ cv::Mat AIModel_Impl::InitMat(int channel, int w, int h)
{
if (w <= 0 || h <= 0 || channel <= 0 || channel > 3)
{
printf("InitMat: invalid params channel=%d w=%d h=%d\n", channel, w, h);
return cv::Mat();
}
cv::Mat dst;
if (channel == 1)
{
dst = cv::Mat(h, w, CV_8UC1, cv::Scalar(0));
dst = cv::Mat::zeros(h, w, CV_8UC1);
}
else
{
dst = cv::Mat(h, w, CV_8UC3, cv::Scalar(0, 0, 0));
dst = cv::Mat::zeros(h, w, CV_8UC3);
}
if (dst.empty() || dst.data == NULL)
{
printf("InitMat: cv::Mat::zeros failed, channel=%d w=%d h=%d\n", channel, w, h);
return cv::Mat();
}
return dst;
}

@ -37,34 +37,56 @@ int AIFactory::InitALLAIModle(GPU_Config gupconfig)
return 1;
}
if (!CELL_CA_Det)
if (!CELL_BCA_Det)
{
CELL_CA_Det = AIModel_Base::GetInstance();
CELL_BCA_Det = AIModel_Base::GetInstance();
AIModel_Base::AIModelRun_Config edge_config;
edge_config.gpuconfig.copy(gupconfig);
edge_config.strPath = "/home/aidlux/BOE/UseModel_CellAOI/Detect_CA.engine";
edge_config.strName = "CA_Det";
edge_config.strPath = "/home/aidlux/BOE/UseModel_CellAOI_CD/Detect_BCA.engine";
edge_config.strName = "BCA_Det";
edge_config.inputType = AIModel_Base::Input_HWC;
edge_config.Stream_num = 2;
CELL_CA_Det->Init(edge_config);
CELL_BCA_Det->Init(edge_config);
}
if (!CELL_TA_Det)
if (!CELL_BTA_Det)
{
CELL_TA_Det = AIModel_Base::GetInstance();
CELL_BTA_Det = AIModel_Base::GetInstance();
AIModel_Base::AIModelRun_Config edge_config;
edge_config.gpuconfig.copy(gupconfig);
edge_config.strPath = "/home/aidlux/BOE/UseModel_CellAOI/Detect_TA.engine";
edge_config.strName = "TA_Det";
edge_config.strPath = "/home/aidlux/BOE/UseModel_CellAOI_CD/Detect_BTA.engine";
edge_config.strName = "BTA_Det";
edge_config.inputType = AIModel_Base::Input_HWC;
edge_config.Stream_num = 2;
CELL_TA_Det->Init(edge_config);
CELL_BTA_Det->Init(edge_config);
}
if (!CELL_DCA_Det)
{
CELL_DCA_Det = AIModel_Base::GetInstance();
AIModel_Base::AIModelRun_Config edge_config;
edge_config.gpuconfig.copy(gupconfig);
edge_config.strPath = "/home/aidlux/BOE/UseModel_CellAOI_CD/Detect_DCA.engine";
edge_config.strName = "DCA_Det";
edge_config.inputType = AIModel_Base::Input_HWC;
edge_config.Stream_num = 2;
CELL_DCA_Det->Init(edge_config);
}
if (!CELL_DTA_Det)
{
CELL_DTA_Det = AIModel_Base::GetInstance();
AIModel_Base::AIModelRun_Config edge_config;
edge_config.gpuconfig.copy(gupconfig);
edge_config.strPath = "/home/aidlux/BOE/UseModel_CellAOI_CD/Detect_DTA.engine";
edge_config.strName = "DTA_Det";
edge_config.inputType = AIModel_Base::Input_HWC;
edge_config.Stream_num = 2;
CELL_DTA_Det->Init(edge_config);
}
if (!CELL_Align)
{
CELL_Align = AIModel_Base::GetInstance();
AIModel_Base::AIModelRun_Config edge_config;
edge_config.gpuconfig.copy(gupconfig);
edge_config.strPath = "/home/aidlux/BOE/UseModel_CellAOI/Align.engine";
edge_config.strPath = "/home/aidlux/BOE/UseModel_CellAOI_CD/Align.engine";
edge_config.strName = "Align";
edge_config.inputType = AIModel_Base::Input_HWC;
CELL_Align->Init(edge_config);
@ -74,7 +96,7 @@ int AIFactory::InitALLAIModle(GPU_Config gupconfig)
CELL_Mark = AIModel_Base::GetInstance();
AIModel_Base::AIModelRun_Config edge_config;
edge_config.gpuconfig.copy(gupconfig);
edge_config.strPath = "/home/aidlux/BOE/UseModel_CellAOI/Cell_marker.engine";
edge_config.strPath = "/home/aidlux/BOE/UseModel_CellAOI_CD/Cell_marker.engine";
edge_config.strName = "Mark";
edge_config.inputType = AIModel_Base::Input_HWC;
CELL_Mark->Init(edge_config);
@ -84,7 +106,7 @@ int AIFactory::InitALLAIModle(GPU_Config gupconfig)
CELL_CA_Cls = AIModel_Base::GetInstance();
AIModel_Base::AIModelRun_Config jbl_config;
jbl_config.gpuconfig.copy(gupconfig);
jbl_config.strPath = "/home/aidlux/BOE/UseModel_CellAOI/Class_10.engine";
jbl_config.strPath = "/home/aidlux/BOE/UseModel_CellAOI_CD/Class_10.engine";
jbl_config.inputType = AIModel_Base::Input_HWC;
jbl_config.strName = "CA_Class";
jbl_config.IsClass = true;
@ -95,7 +117,7 @@ int AIFactory::InitALLAIModle(GPU_Config gupconfig)
CELL_TA_Cls = AIModel_Base::GetInstance();
AIModel_Base::AIModelRun_Config jbl_config;
jbl_config.gpuconfig.copy(gupconfig);
jbl_config.strPath = "/home/aidlux/BOE/UseModel_CellAOI/Class_10_TA.engine";
jbl_config.strPath = "/home/aidlux/BOE/UseModel_CellAOI_CD/Class_10_TA.engine";
jbl_config.inputType = AIModel_Base::Input_HWC;
jbl_config.strName = "TA_Class";
jbl_config.IsClass = true;

@ -46,6 +46,7 @@ ${PROJECT_SOURCE_DIR}/ConfigModule/include
${PROJECT_SOURCE_DIR}/Common/include
${PROJECT_SOURCE_DIR}/AIEngineModule/include
${PROJECT_SOURCE_DIR}/AIEngineModule/include_base
${PROJECT_SOURCE_DIR}/TcsCheckModule/include
)
link_directories(
/usr/local/lib/
@ -67,6 +68,7 @@ add_library(TY_Check SHARED ${SRC_LISTS})
target_link_libraries(TY_Check
nvinfer
Config
TcsCheck
${OpenCV_LIBS}
${CUDA_LIBRARIES}
)

@ -61,6 +61,9 @@ public:
// 更新参数 pconfig 参数指针,nConfigType 需要更新的参数类型 返回:0 成功 其他异常
int UpdateConfig(void *pconfig, int nConfigType);
// 获取指定相机在更新参数后回传的参数值(strCamera 为相机名) 返回:0 成功 其他异常
int GetUpdateParamOut(const std::string &strCamera, UpdateParamOutST &outParam);
std::string GetVersion();
std::string GetErrorInfo();
@ -72,6 +75,9 @@ private:
// 加载分析参数
int LoadCheckConfig(void *p);
// 更新参数后, 按相机从配置中提取需要回传的参数值
int UpdateParamOutConfig();
/// @brief 初始化并且启动程序
/// @return
int InitRun();
@ -139,6 +145,10 @@ private:
ConfigManagerBase *m_pConfigManager;
// 更新参数后需要回传给调用方的参数值: 相机名 -> 参数
std::unordered_map<std::string, UpdateParamOutST> m_UpdateParamOutMap;
std::mutex mtx_UpdateParamOut; // 互斥量, 保护 m_UpdateParamOutMap
// 图片处理线程
// 相机处理模块,负责完成对应相机图片的处理功能。

@ -0,0 +1,141 @@
#ifndef BRIGHT_GEOSHARE_HPP_
#define BRIGHT_GEOSHARE_HPP_
// ============================================================================
// 明场(B) -> 暗场(D) 几何结果共享
//
// 背景:本机 4 台相机 BCA/DCA/BTA/DTA 中,B 与 D 是同一工位的两个光照通道
// (配对:BCA<->DCA, BTA<->DTA),产品/物料在图像中的位置一致,仅明暗场不同。
// 暗场图对比度低,定位(GetEdgeRoi/AI_Edge)与参数自适应(Adapt)不稳定,
// 因此 D 相机直接复用 B 相机算出的:
// 1) 定位结果 m_CutRoi(检测裁切框)
// 2) Adapt 目标:产品旋转外接矩形(rot_cur_roi)
// 3) 产品真实宽高(mm)
//
// 用法:
// Bright 侧(BCA/BTA)在几何计算完成后 Publish 快照;
// Dark 侧(DCA/DTA)在需要时 Take 快照(带超时,拿不到则回退自身逻辑)。
//
// 匹配键 = 工位对("CA"/"TA"),不按产品名匹配:实际流程中四个相机可能以各自独立
// 的“产品”送检(如 ET 套图流程按 BCA→BTA→DCA→DTA 顺序各送一次),B 与 D 的产品名
// 并不相同。取用即消费(Take 成功后删除),保证一份明场结果只服务一次暗场,
// 不会把上一块板的坐标复用到下一块板;键集合只有 CA/TA 两个,永不增长。
//
// 线程安全:内部 mutex + condition_variable,可在多相机线程间并发使用。
// 关闭开关:环境变量 AOI_DISABLE_BRIGHT_DARK_SHARE=1(用于 A/B 对比旧行为)。
// ============================================================================
#include <string>
#include <map>
#include <mutex>
#include <condition_variable>
#include <chrono>
#include <cstdlib>
#include <opencv2/opencv.hpp>
// 等待明场结果的超时(ms):超时后暗场回退到自身定位/自适应逻辑。
// 明场与暗场同时开始检查,该值只需覆盖明场“拼接 + 粗定位 + Adapt + AI_Edge”的耗时。
static const long kBrightGeoWaitTimeoutMs = 60000;
// 明暗场共享开关(默认开启)
inline bool BrightGeoShareEnabled()
{
static const bool bEnabled = []() -> bool {
const char *pEnv = std::getenv("AOI_DISABLE_BRIGHT_DARK_SHARE");
if (pEnv == nullptr)
{
return true;
}
std::string s(pEnv);
return !(s == "1" || s == "true" || s == "TRUE" || s == "on" || s == "ON");
}();
return bEnabled;
}
// 相机代码(BCA/BTA/DCA/DTA) -> 工位对(CA/TA);非预期输入返回空串
inline std::string BrightGeoPairOfCamera(const std::string &strCameraCode)
{
if (strCameraCode.find("CA") != std::string::npos)
{
return "CA";
}
if (strCameraCode.find("TA") != std::string::npos)
{
return "TA";
}
return "";
}
// 是否明场相机(代码以 B 开头,如 BCA/BTA)
inline bool BrightGeoCameraIsBright(const std::string &strCameraCode)
{
return !strCameraCode.empty() && (strCameraCode[0] == 'B' || strCameraCode[0] == 'b');
}
// 是否暗场相机(代码以 D 开头,如 DCA/DTA)
inline bool BrightGeoCameraIsDark(const std::string &strCameraCode)
{
return !strCameraCode.empty() && (strCameraCode[0] == 'D' || strCameraCode[0] == 'd');
}
// 共享快照的键:工位对("CA"/"TA")
// (注:不按产品名匹配,原因见文件头说明)
// 明场发布给暗场的几何快照
struct BrightGeoSnapshot
{
std::string strProduct; // 发布该快照的产品名(仅用于日志追踪)
bool bValid = false; // 快照是否可用于暗场(明场定位流程是否正常完成)
bool bRoiValid = false; // 产品旋转外接矩形是否有效(明场 Adapt 是否成功)
cv::RotatedRect rotRoi; // 产品旋转外接矩形(原图坐标)
cv::Rect cutRoi; // 定位裁切框(原图坐标)
cv::Size imgSize; // 明场拼接图尺寸(暗场做一致性校验)
float productWidth_mm = 0.f; // 产品真实宽度(mm)
float productHeight_mm = 0.f; // 产品真实高度(mm)
};
class BrightGeoShare
{
public:
static BrightGeoShare &Instance()
{
static BrightGeoShare s_ins;
return s_ins;
}
// 明场发布(同一工位对覆盖旧值),唤醒等待中的暗场线程
void Publish(const std::string &strPair, const BrightGeoSnapshot &snap)
{
{
std::lock_guard<std::mutex> lk(m_mtx);
m_map[strPair] = snap;
}
m_cv.notify_all();
}
// 暗场等待并取走明场结果(超时返回 false;成功则取走并删除,一份结果只服务一次)
bool Take(const std::string &strPair, BrightGeoSnapshot &snap, long nTimeoutMs)
{
std::unique_lock<std::mutex> lk(m_mtx);
bool bOk = m_cv.wait_for(lk, std::chrono::milliseconds(nTimeoutMs),
[&]() { return m_map.find(strPair) != m_map.end(); });
if (!bOk)
{
return false;
}
auto it = m_map.find(strPair);
snap = it->second;
m_map.erase(it);
return true;
}
private:
BrightGeoShare() {}
~BrightGeoShare() {}
std::mutex m_mtx;
std::condition_variable m_cv;
std::map<std::string, BrightGeoSnapshot> m_map;
};
#endif

@ -188,6 +188,13 @@ private:
int GetLine(const cv::Mat &img, std::vector<cv::Point> &pointList, int lineNum, int xory, int &outP);
// 找到产品(亮色区域)所在的第一行(顶部边缘), 用于左右图上下对齐
int GetProductFirstRow(const cv::Mat &img, int &outRow);
// 拟合产品顶边直线段: y = outK * x + outB, outP1/outP2 为线段两端点(图像坐标系)
// 返回 0 成功; 非 0 失败(未找到顶边或有效点太少)
int FitProductTopLine(const cv::Mat &img, float &outK, float &outB, cv::Point2f &outP1, cv::Point2f &outP2);
public:
// 运行的基本参数
RunInfoST m_RunConfig;

@ -19,12 +19,225 @@
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <string>
#include <sys/time.h>
#include <sys/stat.h>
#include <cerrno>
#include <cstddef>
#include <memory>
#include <mutex>
#include <vector>
#include <thread>
#include <condition_variable>
#include <deque>
#include <vector>
#include <opencv2/opencv.hpp>
using namespace std;
// ============================================================================
// 异步存图器(header-only 单例)
// 场景:检测热点循环里直接 cv::imwrite,编码 + 写盘会把主流程拖慢
// (例:CTcsCheck::AdaptiveBinary 逐块存残点调试图)。
// 做法:调用方把图 clone 后入队即返回,独立后台线程串行编码落盘,
// 调用方只付一次内存拷贝的代价,不再等磁盘 IO。
// 线程安全;队列有长度上限,写盘慢于入队时 Push 会阻塞(背压),避免内存暴涨。
// 实现放在头文件、不依赖 CheckUtil.cpp:TcsCheck 这类未链接 AlgorithmModule
// 的模块也能直接使用(否则会形成反向链接依赖)。
// ============================================================================
class CAsyncImgSaver
{
public:
static CAsyncImgSaver &Instance()
{
static CAsyncImgSaver s_inst; // C++11 起局部静态初始化线程安全
return s_inst;
}
// 入队一张图(img 会被 clone,返回后调用方可立即释放/复用原图)
void Push(const std::string &strPath, const cv::Mat &img, const std::vector<int> &vParams)
{
if (strPath.empty() || img.empty())
{
return;
}
ImgTask task;
task.strPath = strPath;
task.vParams = vParams;
task.mat = img.clone(); // 深拷贝,隔离调用方数据的生命周期
std::unique_lock<std::mutex> lk(m_mtx);
if (m_bStop)
{
return; // 已关闭:直接丢弃,避免后台线程退出后无限等待
}
if (m_nQueueLimit > 0)
{
// 队列满则阻塞等待(背压),顺带让 cv::Mat 内存占用可控
m_cvHasRoom.wait(lk, [this] { return m_bStop || m_deqTask.size() < m_nQueueLimit; });
if (m_bStop)
{
return;
}
}
m_deqTask.push_back(std::move(task));
++m_nPending;
lk.unlock();
m_cvHasTask.notify_one();
}
// 等待队列中(含正在写盘)的图全部落盘
void WaitIdle()
{
std::unique_lock<std::mutex> lk(m_mtx);
m_cvIdle.wait(lk, [this] { return m_nPending == 0; });
}
// 队列长度上限(张);调大后唤醒被背压阻塞的入队方
void SetQueueLimit(std::size_t nLimit)
{
{
std::lock_guard<std::mutex> lk(m_mtx);
m_nQueueLimit = nLimit;
}
m_cvHasRoom.notify_all();
}
// 关闭:排空剩余队列后退出后台线程(可重复调用)
void Stop()
{
{
std::lock_guard<std::mutex> lk(m_mtx);
m_bStop = true;
}
m_cvHasTask.notify_all();
m_cvHasRoom.notify_all();
if (m_thread.joinable())
{
m_thread.join();
}
}
private:
struct ImgTask
{
std::string strPath;
cv::Mat mat;
std::vector<int> vParams;
};
CAsyncImgSaver()
: m_bStop(false)
, m_nQueueLimit(4096) // 默认最多积压 4096 张待写图
, m_nPending(0)
{
m_thread = std::thread(&CAsyncImgSaver::Worker, this);
}
~CAsyncImgSaver()
{
Stop(); // 进程退出兜底:把队列里剩余的图写完再退
}
CAsyncImgSaver(const CAsyncImgSaver &) = delete;
CAsyncImgSaver &operator=(const CAsyncImgSaver &) = delete;
// 后台线程主循环:串行取图 -> 建目录 -> 编码落盘
void Worker()
{
std::string strLastDir; // 缓存上次建好的目录,避免每张图都做一次 mkdir
while (true)
{
ImgTask task;
{
std::unique_lock<std::mutex> lk(m_mtx);
m_cvHasTask.wait(lk, [this] { return m_bStop || !m_deqTask.empty(); });
if (m_deqTask.empty())
{
return; // 收到停止信号且队列已排空
}
task = std::move(m_deqTask.front());
m_deqTask.pop_front();
}
m_cvHasRoom.notify_one(); // 队列腾出空位,唤醒可能被阻塞的入队方
if (!task.mat.empty())
{
EnsureParentDir(task.strPath, strLastDir);
try
{
cv::imwrite(task.strPath, task.mat, task.vParams);
}
catch (const cv::Exception &e)
{
std::cerr << "[SaveImgAsync] imwrite failed: " << task.strPath << " : " << e.what() << std::endl;
}
}
{
std::lock_guard<std::mutex> lk(m_mtx);
if (m_nPending > 0)
{
--m_nPending;
}
if (m_nPending == 0)
{
m_cvIdle.notify_all();
}
}
}
}
// 递归创建多级目录(已存在视为成功)
static bool MakeDirs(const std::string &strPath)
{
if (strPath.empty())
{
return false;
}
if (mkdir(strPath.c_str(), 0755) == 0 || errno == EEXIST)
{
return true;
}
const std::size_t pos = strPath.find_last_of('/');
if (pos == std::string::npos || pos == 0)
{
return false;
}
if (!MakeDirs(strPath.substr(0, pos)))
{
return false;
}
return (mkdir(strPath.c_str(), 0755) == 0 || errno == EEXIST);
}
// 创建文件所在目录;strCacheDir 命中则省掉一次 mkdir
static void EnsureParentDir(const std::string &strPath, std::string &strCacheDir)
{
const std::size_t pos = strPath.find_last_of('/');
if (pos == std::string::npos || pos == 0)
{
return; // 没有目录部分(相对文件名)
}
const std::string strDir = strPath.substr(0, pos);
if (strDir == strCacheDir)
{
return;
}
MakeDirs(strDir);
strCacheDir = strDir;
}
std::thread m_thread;
std::mutex m_mtx;
std::condition_variable m_cvHasTask; // 有新任务
std::condition_variable m_cvHasRoom; // 队列有空位
std::condition_variable m_cvIdle; // 全部落盘
std::deque<ImgTask> m_deqTask;
bool m_bStop;
std::size_t m_nQueueLimit;
std::size_t m_nPending; // 已入队但尚未写完的数量(含正在写)
};
class CheckUtil
{
public:
@ -63,6 +276,29 @@ public:
static void PrintRect(cv::Rect roi, std::string str = "");
static int cutSmallImg(cv::Mat img, std::vector<cv::Rect> &samllRoiList, cv::Rect config_roi, int config_SmallImg_Width, int config_SmallImg_Height, int config_MinOverlap_Width, int config_MinOverlap_Height);
static cv::Point2f transformPoint(const cv::Point2f &point, const cv::Mat &transform_matrix);
// ===================== 异步存图(实现见文件顶部 CAsyncImgSaver) =====================
// 入队即返回,父目录不存在会在后台自动创建;img 会被深拷贝,调用方可立即复用原图
static void SaveImgAsync(const std::string &strPath, const cv::Mat &img,
const std::vector<int> &vParams = std::vector<int>())
{
CAsyncImgSaver::Instance().Push(strPath, img, vParams);
}
// 等待队列里所有异步存图落盘(退出/校验结果前调用)
static void WaitSaveImgDone()
{
CAsyncImgSaver::Instance().WaitIdle();
}
// 队列上限(张),默认 4096;超限时 SaveImgAsync 会阻塞(背压),0 表示不限制
static void SetSaveImgQueueLimit(std::size_t nLimit)
{
CAsyncImgSaver::Instance().SetQueueLimit(nLimit);
}
// 主动关闭后台存图线程(排空剩余队列后退出);不调用也会在进程退出时自动收尾
static void StopSaveImg()
{
CAsyncImgSaver::Instance().Stop();
}
};
// 无参数重载:直接返回原字符串,避免 -Wformat-security 警告
static inline std::string str_Format(const std::string &format)

@ -134,8 +134,9 @@ public:
// 检测小区域的信息
struct Det_ROI_Config
{
cv::Rect roi;
std::vector<cv::Point> plist;
cv::Rect roi; // 轴对齐包围盒(由 rrect.boundingRect() 计算,保持兼容)
cv::RotatedRect rrect; // 旋转矩形,贴合倾斜产品的边缘方向,避免过检
std::vector<cv::Point> plist; // 检测区域的多边形顶点
};
struct Algin_Result
{
@ -173,6 +174,7 @@ public:
std::vector<cv::Point> Det_region;
bool bSaveResultImg;
std::shared_ptr<DetLog> detlog;
DetConfigResult()
{
Init();
@ -186,6 +188,7 @@ public:
alginResult.Init();
Det_region.clear();
bSaveResultImg = false;
detlog = nullptr;
}
};
@ -213,10 +216,15 @@ private:
// 检测缺陷
int Det_qx(const cv::Mat &img, std::vector<Det_ROI_Config> roilist, Det_ROI_Type type, DetConfigResult *pDetConfig);
// 多边形边缘缺陷检测(无方向/角点过滤,每个ROI独立处理,避免巨型mask)
int Det_qx_polygon(const cv::Mat &img, const std::vector<Det_ROI_Config> &roilist, DetConfigResult *pDetConfig);
int applyMaskInROI(const cv::Mat &grayImg, const Det_ROI_Config &config, cv::Mat &result, int threshold);
// 通过手绘的方式来检测
int Draw_Det(const cv::Mat &img, DetConfigResult *pDetConfig);
// 使用定位区域巡边检测
int Draw_Det_Align(const cv::Mat &img, DetConfigResult *pDetConfig);
private:
/// @brief

@ -70,6 +70,20 @@ public:
float fscale_detToresult_x = 0;
float fscale_detToresult_y = 0;
// 产品左上角(rot_cur_roi.boundingRect)在检测图 detImg 中的偏移,缺陷坐标转换到产品坐标系时需减去该间隙
// 注意:这是轴对齐的近似换算,仅在无旋转矩形(vProductQuadInDet 为空)时作为退化路径使用
cv::Point ptRoiOffsetInDet = cv::Point(0, 0);
// 产品旋转外接矩形四角在 detImg 坐标系下的坐标,顺序固定为 0左上 1右上 2左下 3右下
// 非空时用「缺陷点到两条边线段的距离」把点换算成产品坐标,可抗产品倾斜
std::vector<cv::Point2f> vProductQuadInDet;
// 缺陷点(detImg 坐标) -> 产品坐标(像素)
// x = 到左边线段(左下->左上)的垂距,y = 到顶边线段(左上->右上)的垂距,落在矩形内为正
// 这样同一物理位置在产品任意倾斜角下都能得到相同的产品坐标(轴对齐相减做不到)
// vProductQuadInDet 无效时退化为 ptRoiOffsetInDet 轴对齐平移(旧行为)
cv::Point2f DetPointToProductCoord(const cv::Point2f &ptDet) const;
int NG_num = 0;
int YS_num = 0;
int Ok_num = 0;

@ -18,7 +18,9 @@ struct QX_ERROR_INFO_
int Idx;
int result;
std::string result_name;
cv::Rect roi;
cv::Rect roi; // 缺陷框(产品坐标系,抗倾斜;roi.x/y 为框左上角)
cv::Rect roiDet; // 缺陷框(检测图 detImg 坐标系,小图裁剪/定位等内部用途)
cv::Point2f ptProduct = cv::Point2f(0.f, 0.f); // 缺陷中心的产品坐标(像素,抗倾斜,float 精度)
int area;
int energy;
float JudgArea;
@ -59,7 +61,9 @@ struct QX_ERROR_INFO_
result = 0;
result_name = "OK";
density = 0;
cv::Rect roi = cv::Rect(0, 0, 0, 0);
roi = cv::Rect(0, 0, 0, 0);
roiDet = cv::Rect(0, 0, 0, 0);
ptProduct = cv::Point2f(0.f, 0.f);
detLogList.erase(detLogList.begin(), detLogList.end());
detLogList.clear();
detRegionidxList.clear();

@ -29,6 +29,7 @@ private:
Draw_rect,
Draw_circle,
Draw_str,
Draw_lines,
};
struct txtDrawBackgroundk
{
@ -91,6 +92,7 @@ private:
int dr = 0;
float txtsize = 0.8;
std::shared_ptr<txtDrawBackgroundk> txtbackgroundk;
std::vector<cv::Point> linePts; // 连线端点列表(用于 Draw_lines)
};
struct singleQxInfo{
@ -104,6 +106,7 @@ private:
float scale_Y;
float judge_dis;
int judge_num;
int param_group_id; // 参数组唯一标识,用于区分不同参数组的缺陷集合
Point center;
vector<singleQxInfo> qxList;
void Init(){
@ -111,6 +114,7 @@ private:
scale_Y = 0;
judge_dis = 0;
judge_num = 0;
param_group_id = -1;
center = Point(0, 0);
qxList.clear();
}
@ -131,7 +135,10 @@ private:
{
if(calulateDisSquared(qxInfo.pcenter) < judge_dis * judge_dis)
{
center = Point((qxInfo.pcenter.x + center.x) / 2, (qxInfo.pcenter.y + center.y) / 2);
// 质心更新:所有点等权重的算术平均
int n = (int)qxList.size() + 1;
center.x = (int)(((int64_t)center.x * (n - 1) + qxInfo.pcenter.x) / n);
center.y = (int)(((int64_t)center.y * (n - 1) + qxInfo.pcenter.y) / n);
qxList.push_back(qxInfo);
return true;
}
@ -152,6 +159,12 @@ private:
struct iregionQxZoningInfo{
vector<ictQxZoningInfo> ictQxZoningList;
};
// 用于 DrawResult 的分组连线信息
struct QxZoningDrawGroup{
bool isNG; // true=NG组, false=YS组
std::vector<cv::Point> centers; // 组内各缺陷中心点(detection坐标系,绘制时需乘缩放系数)
};
public:
ImageResultJudge(/* args */);
~ImageResultJudge();
@ -172,6 +185,9 @@ private:
cv::Rect GetCutRoi(cv::Rect &roi, const cv::Mat &img);
// 获取缺陷 对应的AI输入 输出图片,
int GetAIDetImg(std::shared_ptr<ImageAllResult> pImageResult, cv::Point pcenter, cv::Mat &AI_InImg, cv::Mat &AI_OutImg);
// 构建缺陷图片结果(裁剪、缩放、填充字段、获取AI图片),返回完整的 QXImageResult
QXImageResult BuildDefectImage(std::shared_ptr<ImageAllResult> pImageResult, QX_ERROR_INFO_ *QX_info,
int qxidx, cv::Point pCenter, float fs_resize_x, float fs_resize_y);
int sendTask(std::shared_ptr<DrawInfo> task);
@ -203,6 +219,9 @@ private:
std::condition_variable m_task_cv_;
std::shared_ptr<std::thread> ptr_thread_Draw;
std::condition_variable m_drawComplate_cv_;
// 分组连线信息(ResultJudge 填充,DrawResult 消费)
std::vector<QxZoningDrawGroup> m_zoningDrawGroups;
};
#endif

@ -36,6 +36,8 @@
#include "AI_Factory.h"
#include "ImageAllResult.h"
#include "Task.h"
#include "TcsCheck.h"
#include "BrightGeoShare.hpp"
using namespace std;
using namespace cv;
@ -117,6 +119,10 @@ private:
int AI_Edge(const cv::Mat &img, cv::Rect &cutRoi);
// 计算产品尺寸
int CalProductSize();
// 计算产品物理宽高(mm)(未取整前的原始换算 + 现有格式取整),供 CalProductSize 与明场发布共用
void ComputeProductSizeValues(float &fw, float &fh, float &fhPixel);
// 解析当前检测图的相机代码(BCA/BTA/DCA/DTA),无法解析返回空串
std::string GetCurCameraCode();
// 图片预处理
int ImgPreDet();
@ -195,11 +201,15 @@ private:
// 边缘缺陷检测
int Edge_Qx_Det(const cv::Mat &img);
// 缺陷框(detImg 坐标) -> 产品坐标(抗倾斜),并记录 detImg 坐标框
int ConvertDetRoiToProduct(QX_ERROR_INFO_ &qxerror);
// 把blob 汇总成 检测结果。
int BLobToDetResult();
//自适应参数区域更新
int Adapt_Config(Mat img, Rect roi, bool b_update);
// pTargetRoi != nullptr 时使用外部(明场相机)提供的产品旋转矩形作为仿射目标,跳过自身边缘定位
int Adapt_Config(Mat img, Rect cur_roi, cv::RotatedRect& rot_cur_roi, bool b_update, const cv::RotatedRect *pTargetRoi = nullptr);
std::shared_ptr<AIFactory> AI_Factory;
@ -240,6 +250,12 @@ private:
cv::Rect m_CutRoi;
cv::Rect m_Crop_Roi_paramImg; // 裁切在参数模板图上的 位置。
cv::RotatedRect m_rot_cur_roi; // 产品旋转外接矩形(原图坐标)
bool m_bRotCurRoiValid = false; // m_rot_cur_roi 是否有效
// 暗场(DCA/DTA)复用明场(BCA/BTA)几何结果(定位/Adapt目标/产品宽高),详见 BrightGeoShare.hpp
bool m_bDarkUseBrightGeo = false; // 本次检测暗场是否成功拿到明场结果
BrightGeoSnapshot m_brightGeoSnap; // 暗场复用的明场结果快照
std::string m_strCurDetChannel; // 当前处理的图片通道
@ -263,6 +279,9 @@ private:
Edge_QX_Det::DetConfigResult m_Edge_DetConfig;
std::vector<QXImageResult> m_Draw_qxImageResult; // 缺陷小图结果
// 传统检测模块
CTcsCheck m_tcsCheck;
std::vector<cv::Rect> SmallRoiList;
std::string m_strRootPath_TA_cls;
@ -283,10 +302,11 @@ private:
// AI mask 在这一列是否有 缺陷残点,用以 加速 blob 分析。
unsigned char *m_ImgBlobHFlagData;
// 原始区域数据
Rect m_old_productROI = Rect(0, 0, 0, 0);
// 原始区域数据(使用 RotatedRect 以保留 region 的旋转角度信息)
cv::RotatedRect m_old_productROI;
std::vector<cv::Point> m_old_cur_edgeDet_region;
std::vector<cv::Point> m_old_cur_markLine_region;
std::vector<cv::Point> m_old_cur_traditional_region;
cv::Point m_old_cur_markLine_mark1;
cv::Point m_old_cur_markLine_mark2;
std::vector<RegionConfigST> m_old_cur_regionConfigArr;

@ -51,6 +51,52 @@ struct RunInfoST
this->str2 = tem.str2;
}
};
/*******************更新参数回传结构***********************************************************************************************************************/
// 说明: 更新参数(UpdateConfig / 检测前更新参数)时, 算法侧把从 json 读到的参数值填进本结构体,
// 再通过接口返回给调用方。需要回传的字段直接往这里加即可。
struct flaw_info
{
float area; // 缺陷面积
float gray_dis; // 缺陷灰阶
flaw_info()
{
area = 0.0f;
gray_dis = 0.0f;
}
void copy(flaw_info tem)
{
this->area = tem.area;
this->gray_dis = tem.gray_dis;
}
};
struct UpdateParamOutST
{
float pixel_precision_x; // x成像检测精度
float pixel_precision_y; // y成像检测精度
flaw_info aotudian; // 凹凸点参数
flaw_info zangwu; // 脏污参数
flaw_info line; // 线状参数
flaw_info dianzhuang; // 点状参数
flaw_info xianwei; // 纤维参数
flaw_info posun; // 破损参数
UpdateParamOutST()
{
pixel_precision_x = 0.0f;
pixel_precision_y = 0.0f;
}
void copy(UpdateParamOutST tem)
{
this->pixel_precision_x = tem.pixel_precision_x;
this->pixel_precision_y = tem.pixel_precision_y;
this->aotudian.copy(tem.aotudian);
this->zangwu.copy(tem.zangwu);
this->line.copy(tem.line);
this->dianzhuang.copy(tem.dianzhuang);
this->xianwei.copy(tem.xianwei);
this->posun.copy(tem.posun);
}
};
/*******************一般调用流程*************************************************************************************************************************/
/*******************1、初始化 UpdateConfig*************************************************************************************************************************/
/*******************2、初始化 Init 并开启 *************************************************************************************************************************/
@ -84,6 +130,9 @@ public:
// 更新参数 pconfig 参数指针,nConfigType 需要更新的参数类型 返回:0 成功 其他异常
virtual int UpdateConfig(void *pconfig, int nConfigType) = 0;
// 获取指定相机在更新参数后回传的参数值(strCamera 为相机名) 返回:0 成功 其他异常
virtual int GetUpdateParamOut(const std::string &strCamera, UpdateParamOutST &outParam) = 0;
// 返回检测版本信息
virtual std::string GetVersion() = 0;
// 返回错误信息

@ -0,0 +1,79 @@
#ifndef _ImgMmPatch_HPP_
#define _ImgMmPatch_HPP_
// ============================================================================
// 【临时补丁】产品长度方向(y) 像素 -> 物理毫米 的分段换算
//
// 背景:产品沿长度方向运动/扫描时速率不恒定(尾端有拉伸),像素↔mm 不是单一
// 比例,实测标定出 6 段,每段斜率不同。
//
// 分段表(分段点 = 像素,**以产品顶边为 0 点**;斜率为 mm/px)见函数内
// yPx0..yPx5 / pmm0..pmm5(共 6 段),改标定只需改那一段
// (此处不重复写数值,避免两处不同步)。
// 分段点处前后两段取值相同,函数连续,不会在分段点跳变。
// 5000px 是最前端新加的分段点:把原来的 0~10928 拆成 0~5000 与 5000~10928,
// 当 pmm0 == pmm1 == 0.016869 时与拆分前逐位等价。
//
// 共用方(必须共用同一张表,否则"产品高度"和"缺陷坐标"会各用一把尺子):
// 1) 缺陷 y 物理坐标:ImageResultJudge::BuildDefectImage()
// 2) 产品物理高度 :ImgCheckAnalysisy::CalProductSize()
//
// 注:正式方案建议把这张分段表放进配置(json),由标定数据下发;这里先硬编码。
// ============================================================================
inline float PixelYToMm(float fYPixel)
{
// ================= 标定参数:改标定只需改这一段 =================
// 各段斜率(mm/px),下标 0..5 依次对应 6 个分段
// 注:pmm0 / pmm1 是新增分段点 5000 拆出来的两段,标定值待填;
// 两者都取原 0~10928 段的 0.016869 时,与拆分前结果逐位相同。
const float pmm0 = 0.016869; // 0 ~ 5000
const float pmm1 = 0.016869; // 5000 ~ 10928
const float pmm2 = 0.016463; // 10928 ~ 17002
const float pmm3 = 0.016220; // 17002 ~ 23167
const float pmm4 = 0.015669; // 23167 ~ 29549
const float pmm5 = 0.014337; // >= 29549
// 各段起始像素(以产品顶边为 0 点)
const float yPx0 = 0.0f;
const float yPx1 = 5000.0f;
const float yPx2 = 10928.0f;
const float yPx3 = 17002.0f;
const float yPx4 = 23167.0f;
const float yPx5 = 29549.0f;
// 各段像素长度(px)
const float dPx0 = yPx1 - yPx0;
const float dPx1 = yPx2 - yPx1;
const float dPx2 = yPx3 - yPx2;
const float dPx3 = yPx4 - yPx3;
const float dPx4 = yPx5 - yPx4;
// 各段末端的累计物理长度(mm)
const float d0 = dPx0 * pmm0;
const float d1 = d0 + dPx1 * pmm1;
const float d2 = d1 + dPx2 * pmm2;
const float d3 = d2 + dPx3 * pmm3;
const float d4 = d3 + dPx4 * pmm4;
if (fYPixel < yPx1)
{
return fYPixel * pmm0;
}
if (fYPixel < yPx2)
{
return (fYPixel - yPx1) * pmm1 + d0;
}
if (fYPixel < yPx3)
{
return (fYPixel - yPx2) * pmm2 + d1;
}
if (fYPixel < yPx4)
{
return (fYPixel - yPx3) * pmm3 + d2;
}
if (fYPixel < yPx5)
{
return (fYPixel - yPx4) * pmm4 + d3;
}
return (fYPixel - yPx5) * pmm5 + d4;
}
#endif

@ -238,6 +238,8 @@ int ALLImgCheckAnalysisy::UpdateConfig(void *pconfig, int nConfigType)
break;
case CHECK_CONFIG_Module:
m_pConfigManager = (ConfigManagerBase *)pconfig;
// 更新参数后, 立即按相机提取需要回传的参数值
UpdateParamOutConfig();
break;
default:
break;
@ -246,6 +248,112 @@ int ALLImgCheckAnalysisy::UpdateConfig(void *pconfig, int nConfigType)
return 0;
}
// 缺陷项 -> 回传结构体字段 的对应关系
struct QX_FIELD_MAP_
{
int nQxType; // 缺陷类型(CONFIG_QX_NAME_)
flaw_info UpdateParamOutST::*pField; // 回传结构体中对应的缺陷参数
};
// 从单个相机的配置中提取需要回传的参数值
// 取值位置与 ImageResultJudge::GetParamidx() 保持一致:
// 成像精度 <- commonCheckConfig.baseConfig.fImage_Scale_x / y
// 缺陷面积/灰阶 <- 第0个节点第0个区域的参数, 按缺陷名(param_name)匹配
static void FillUpdateParamOut(const AnalysisyConfigST &config, UpdateParamOutST &outParam)
{
// 1、成像检测精度
outParam.pixel_precision_x = config.commonCheckConfig.baseConfig.fImage_Scale_x;
outParam.pixel_precision_y = config.commonCheckConfig.baseConfig.fImage_Scale_y;
// 2、缺陷参数(面积/灰阶)
if (config.commonCheckConfig.nodeConfigArr.size() <= 0)
{
return;
}
const CommonConfigNodeST &node = config.commonCheckConfig.nodeConfigArr.at(0);
if (node.regionConfigArr.size() <= 0)
{
return;
}
const RegionConfigST &region = node.regionConfigArr.at(0);
const CheckConfig_Regions_type &typeParam = region.checkConfig_Regions_type[ANALYSIS_TYPE_TF];
const QX_FIELD_MAP_ qxFieldMap[] = {
{CONFIG_QX_NAME_cell_aotudian, &UpdateParamOutST::aotudian},
{CONFIG_QX_NAME_cell_zangwu, &UpdateParamOutST::zangwu},
{CONFIG_QX_NAME_cell_line, &UpdateParamOutST::line},
{CONFIG_QX_NAME_cell_dianzhuang, &UpdateParamOutST::dianzhuang},
{CONFIG_QX_NAME_cell_xianwei, &UpdateParamOutST::xianwei},
{CONFIG_QX_NAME_cell_posun, &UpdateParamOutST::posun},
};
const int nQxFieldNum = sizeof(qxFieldMap) / sizeof(qxFieldMap[0]);
for (int i = 0; i < nQxFieldNum; i++)
{
const std::string strQxName = CONFIG_QX_NAME_Names[qxFieldMap[i].nQxType];
flaw_info *pOut = &(outParam.*(qxFieldMap[i].pField));
for (int j = 0; j < typeParam.checkConfig_Regions_Param.size(); j++)
{
const CheckConfig_Regions_Param &param = typeParam.checkConfig_Regions_Param.at(j);
if (param.param_name != strQxName || param.paramArr.size() <= 0)
{
continue;
}
pOut->area = param.paramArr.at(0).area; // 缺陷面积
pOut->gray_dis = param.paramArr.at(0).hj; // 缺陷灰阶
break;
}
}
}
int ALLImgCheckAnalysisy::UpdateParamOutConfig()
{
std::lock_guard<std::mutex> lock(mtx_UpdateParamOut);
m_UpdateParamOutMap.clear();
if (m_pConfigManager == nullptr)
{
return 1;
}
// 每个相机一份参数(Config_instances_ 的 key 为 json 中的 cameraCode)
for (const auto &config : m_pConfigManager->Config_instances_)
{
if (!config.second)
{
continue;
}
AnalysisyConfigST analysisyConfig;
if (config.second->GetConfig(ConfigType_Analysisy_Common_XL, &analysisyConfig) != 0)
{
continue;
}
UpdateParamOutST paramOut;
FillUpdateParamOut(analysisyConfig, paramOut);
m_UpdateParamOutMap[config.first] = paramOut;
}
printf("ALLImgCheckAnalysisy::UpdateParamOutConfig cam num %zu \n", m_UpdateParamOutMap.size());
return m_UpdateParamOutMap.size() > 0 ? 0 : 2;
}
int ALLImgCheckAnalysisy::GetUpdateParamOut(const std::string &strCamera, UpdateParamOutST &outParam)
{
std::lock_guard<std::mutex> lock(mtx_UpdateParamOut);
auto it = m_UpdateParamOutMap.find(strCamera);
if (it != m_UpdateParamOutMap.end())
{
outParam.copy(it->second);
return 0;
}
return 1;
}
std::string ALLImgCheckAnalysisy::GetVersion()
{
return std::string("BOE_2.0.5");
@ -302,6 +410,8 @@ int ALLImgCheckAnalysisy::InitAIFactory()
GPU_Config gpu;
gpu.gpu_0 = true;
gpu.gpu_1 = true;
gpu.gpu_2 = true;
gpu.gpu_3 = true;
AI_Factory->InitALLAIModle(gpu);
return 0;
@ -418,7 +528,8 @@ int ALLImgCheckAnalysisy::InitData()
int ALLImgCheckAnalysisy::Det_Product(std::shared_ptr<Product> &product)
{
string cur_time_s = CheckUtil::getCurTimeHMS();
printf("[%s]>>>>>>>>>>>>>>>Det_Product****************det Start************\n", cur_time_s.c_str());
// 处理每个相机
while (true)
{
@ -447,8 +558,8 @@ int ALLImgCheckAnalysisy::Det_Product(std::shared_ptr<Product> &product)
// AnalysiyAll(0);
SetProductResult(product);
product->SetCheckEnd();
string cur_time = CheckUtil::getCurTimeHMS();
printf("[%s]>>>>>>>>>>>>>>>Det_Product****************det End************\n", cur_time.c_str());
string cur_time_e = CheckUtil::getCurTimeHMS();
printf("[%s]>>>>>>>>>>>>>>>Det_Product****************det End************\n", cur_time_e.c_str());
return 0;
}

@ -9,6 +9,8 @@
#include "CameraCheckAnalysisy.hpp"
#include "CheckUtil.hpp"
#include "Define.h"
#include <algorithm>
#include <cmath>
CameraCheckAnalysisy::CameraCheckAnalysisy()
{
@ -105,7 +107,7 @@ string JudgeMaterialPosition(Mat img) {
bool ifExchange(cv::Mat &img, const cv::Mat &img_B, std::string strcam ){
string img_position = JudgeMaterialPosition(img);
string img_B_position = JudgeMaterialPosition(img_B);
if (strcam == "TA"){
if (strcam.find("TA") != std::string::npos){
if (img_position == "Left" && img_B_position == "Right"){
return true;
}
@ -153,19 +155,162 @@ int CameraCheckAnalysisy::Detect_Pre()
return 1;
}
//判断图片左右,0为左,1为右
bool exchange_img = ifExchange(pImageResult->result->in_shareImage->img, pImageResult->result->in_shareImage->img_B, pImageResult->result->in_shareImage->strCameraName);
if(exchange_img){
std::swap(pImageResult->result->in_shareImage->img, pImageResult->result->in_shareImage->img_B);
}
// //判断图片左右,0为左,1为右
// bool exchange_img = ifExchange(pImageResult->result->in_shareImage->img, pImageResult->result->in_shareImage->img_B, pImageResult->result->in_shareImage->strCameraName);
// if(exchange_img){
// std::swap(pImageResult->result->in_shareImage->img, pImageResult->result->in_shareImage->img_B);
// }
// printf("===66666666666666666\n");
int re = Mergimg(pImageResult->result->in_shareImage->img, pImageResult->result->in_shareImage->img_B,
pImageResult->result->in_shareImage->strCameraName, pImageResult->result->in_shareImage->strChannel);
// int re = Mergimg(pImageResult->result->in_shareImage->img, pImageResult->result->in_shareImage->img_B,
// pImageResult->result->in_shareImage->strCameraName, pImageResult->result->in_shareImage->strChannel);
// int re = MergimgTest(pImageResult->result->in_shareImage->img, pImageResult->result->in_shareImage->img_B,
// pImageResult->result->in_shareImage->strCameraName, pImageResult->result->in_shareImage->strChannel);
// 简单硬拼接:img 在左,img_B 在右,拼接结果写回 img
// 先把重叠区 overlap 左右各裁一半,再做顶边对齐,最后水平拼接
if (pImageResult->result->in_shareImage->img.size() != pImageResult->result->in_shareImage->img_B.size() ||
pImageResult->result->in_shareImage->img.type() != pImageResult->result->in_shareImage->img_B.type())
{
m_pdetlog->AddCheckstr(PrintLevel_1, "Detect_Pre", "Cam %s ==================img size/type != img_B ", strBasic.c_str());
return 1;
}
cv::Mat &img = pImageResult->result->in_shareImage->img;
cv::Mat &img_B = pImageResult->result->in_shareImage->img_B;
// 1、裁剪重叠区域: overlap 区域左右各裁一半
// 左图裁掉最右侧 cutLeft, 右图裁掉最左侧 cutRight, 两者之和等于 overlap, 拼接后总宽度与原逻辑一致
int overlap = cvRound(m_pbaseCheckFunction->markLine.hoverlap);
if (overlap > 0 && (img.cols <= overlap || img_B.cols <= overlap))
{
m_pdetlog->AddCheckstr(PrintLevel_1, "Detect_Pre", "Cam %s ==================img.cols <= overlap %d ", strBasic.c_str(), overlap);
return 1;
}
const int cutLeft = overlap / 2; // 左图右侧裁掉的宽度
const int cutRight = overlap - cutLeft; // 右图左侧裁掉的宽度
if (cutLeft > 0)
{
img = img(cv::Rect(0, 0, img.cols - cutLeft, img.rows));
}
if (cutRight > 0)
{
img_B = img_B(cv::Rect(cutRight, 0, img_B.cols - cutRight, img_B.rows));
}
m_pdetlog->AddCheckstr(PrintLevel_1, "Detect_Pre", "Cam %s ==================overlap %d cutLeft %d cutRight %d L.cols %d R.cols %d",
strBasic.c_str(), overlap, cutLeft, cutRight, img.cols, img_B.cols);
// 拼接缝: 左图右边界在拼接图中的 x
const int seamX = img.cols;
// 2、裁剪后做顶边对齐: 分别拟合左右图产品顶边直线, 用直线在拼接缝处的行坐标做上下平移。
// 只用单一坐标值(首行)相减误差大, 产品倾斜时会拼接不准; 用拟合出的直线取值更稳定。
// 这里只做上下平移, 不旋转图像。
bool bFitOK = false; // 顶边拟合是否成功
cv::Point2f pL1, pL2; // 左图顶边线段(端点)
cv::Point2f pR1, pR2; // 右图顶边线段(端点)
int dyAlign = 0; // 右图的上下平移量(>0 向下, <0 向上)
{
float kL = 0.0f, bL = 0.0f; // 左图顶边直线: y = kL * x + bL
float kR = 0.0f, bR = 0.0f; // 右图顶边直线: y = kR * x + bR
int reL = FitProductTopLine(img, kL, bL, pL1, pL2);
int reR = FitProductTopLine(img_B, kR, bR, pR1, pR2);
if (reL == 0 && reR == 0)
{
// 左图顶边直线在拼接缝处(x = seamX)的行坐标 = 左图线段右端点的高度
const double seamY = kL * seamX + bL;
// 右图顶边直线在其左边界(x = 0)处的行坐标 = 右图线段左端点的高度
const double rightY = bR;
// 让两条线段的连接端点在拼接缝处相接: 右图上下平移 seamY - rightY 行
dyAlign = (int)cvRound(seamY - rightY);
m_pdetlog->AddCheckstr(PrintLevel_1, "Detect_Pre",
"Cam %s ==================topline L(k %.5f b %.1f) R(k %.5f b %.1f) seamY %.1f rightY %.1f dy %d",
strBasic.c_str(), kL, bL, kR, bR, seamY, rightY, dyAlign);
// 上下平移右图, 空出的部分补黑
if (dyAlign != 0 && dyAlign > -img_B.rows && dyAlign < img_B.rows)
{
cv::Mat shifted = cv::Mat::zeros(img_B.size(), img_B.type());
if (dyAlign > 0)
{
// 内容向下平移 dyAlign 行, 顶部补黑
img_B(cv::Rect(0, 0, img_B.cols, img_B.rows - dyAlign))
.copyTo(shifted(cv::Rect(0, dyAlign, img_B.cols, img_B.rows - dyAlign)));
}
else
{
// 内容向上平移 |dyAlign| 行, 底部补黑
const int d = -dyAlign;
img_B(cv::Rect(0, d, img_B.cols, img_B.rows - d))
.copyTo(shifted(cv::Rect(0, 0, img_B.cols, img_B.rows - d)));
}
img_B = shifted;
}
bFitOK = true;
}
else
{
m_pdetlog->AddCheckstr(PrintLevel_1, "Detect_Pre", "Cam %s ==================FitProductTopLine fail(L %d R %d), skip align", strBasic.c_str(), reL, reR);
}
}
// 3、上下对齐后水平拼接
{
cv::Mat merge_img;
cv::hconcat(img, img_B, merge_img);
img = merge_img;
}
// 拼接完成后 img_B 不再需要,立即释放内存
img_B.release();
if (pImageResult->result->in_shareImage->Det_Mode == DET_MODE_MergeImg)
{
cv::imwrite(pImageResult->result->in_shareImage->strCameraName + "_MergeImg.png", pImageResult->result->in_shareImage->img);
cv::imwrite(pImageResult->result->in_shareImage->strCameraName + "_MergeImg.jpg", pImageResult->result->in_shareImage->img);
// 保存绘制了顶边线段的图(绿: 左图顶边线段, 红: 右图顶边线段, 黄/青点: 两条线段的连接端点)
if (bFitOK)
{
// 先缩放到可读尺寸再画线, 避免在超大图上做彩色转换(内存/耗时)
const cv::Mat &mergeImg = pImageResult->result->in_shareImage->img;
const int dbgW = 4096;
const double sc = std::min(1.0, (double)dbgW / mergeImg.cols);
cv::Mat small;
cv::resize(mergeImg, small, cv::Size(), sc, sc, cv::INTER_AREA);
cv::Mat dbgLineImg;
if (small.channels() == 1)
{
cv::cvtColor(small, dbgLineImg, cv::COLOR_GRAY2BGR);
}
else
{
dbgLineImg = small.clone();
}
// 左图顶边线段(拼接图左侧)
const cv::Point2f a1((float)(pL1.x * sc), (float)(pL1.y * sc));
const cv::Point2f a2((float)(pL2.x * sc), (float)(pL2.y * sc));
// 右图顶边线段(拼接图右侧, 并叠加本次的上下平移量)
const cv::Point2f b1((float)((seamX + pR1.x) * sc), (float)((pR1.y + dyAlign) * sc));
const cv::Point2f b2((float)((seamX + pR2.x) * sc), (float)((pR2.y + dyAlign) * sc));
const int thick = std::max(2, (int)cvRound(9 * sc));
cv::line(dbgLineImg, a1, a2, cv::Scalar(0, 255, 0), thick); // 绿: 左图顶边线段
cv::line(dbgLineImg, b1, b2, cv::Scalar(0, 0, 255), thick); // 红: 右图顶边线段
// 两条线段的连接端点(平移对齐后应重合在拼接缝处)
const int r = std::max(4, (int)cvRound(14 * sc));
cv::circle(dbgLineImg, a2, r, cv::Scalar(0, 255, 255), -1);
cv::circle(dbgLineImg, b1, r, cv::Scalar(255, 255, 0), -1);
cv::imwrite(pImageResult->result->in_shareImage->strCameraName + "_MergeImg_Line.jpg", dbgLineImg);
}
return 2;
}
@ -188,7 +333,7 @@ int CameraCheckAnalysisy::Mergimg(cv::Mat &img, const cv::Mat &img_B, std::strin
cv::Mat AllImg = cv::Mat::zeros(img.rows, img.cols * 2, img.type());
// cv::imwrite(strcam + strchannel + "_img.png", img);
// cv::imwrite(strcam + strchannel + "img_B.png", img_B);
if (strcam == "TA")
if (strcam.find("TA") != std::string::npos)
{
cv::Rect Left_Tem_Roi;
cv::Point Left_Tem_Point_Up;
@ -1494,3 +1639,168 @@ int CameraCheckAnalysisy::GetLine(const cv::Mat &img, std::vector<cv::Point> &po
return 0;
}
// 拟合图像中产品(亮色区域)的顶边直线段: 对顶边上的点做最小二乘拟合, 得到 y = k * x + b
// 产品倾斜时顶边在每一列的高度不同, 用逐列顶边点拟合出的直线才能真正反映倾斜,
// 左右两张图各拟合一条线段后, 就可以让它们在拼接缝处首尾相连。
// 返回 0 成功; 非 0 失败(未找到顶边或有效点太少)
int CameraCheckAnalysisy::FitProductTopLine(const cv::Mat &img, float &outK, float &outB,
cv::Point2f &outP1, cv::Point2f &outP2)
{
outK = 0.0f;
outB = 0.0f;
outP1 = cv::Point2f(0.0f, 0.0f);
outP2 = cv::Point2f(0.0f, 0.0f);
if (img.empty())
{
return 1;
}
cv::Mat gray;
if (img.channels() == 1)
{
gray = img;
}
else if (img.channels() == 3)
{
cv::cvtColor(img, gray, cv::COLOR_BGR2GRAY);
}
else
{
return 1;
}
const uchar brightThreshold = 35; // 亮像素灰度阈值(与背景判断阈值保持一致)
const int needBrightCount = std::max(1, (int)(gray.cols * 0.02)); // 该行亮像素数量占比超过 20% 认为是产品所在行
const int rowStep = 16; // 跳行扫描步长
// 判断某一行是否为产品所在行(亮像素数量达到阈值)
auto isProductRow = [&](int row) -> bool
{
const uchar *p = gray.ptr<uchar>(row);
int brightCount = 0;
for (int col = 0; col < gray.cols; ++col)
{
if (p[col] >= brightThreshold)
{
if (++brightCount >= needBrightCount)
{
return true;
}
}
}
return false;
};
// 1、跳行粗扫: 定位产品顶边的大致位置
int rowFound = -1;
for (int row = 0; row < gray.rows; row += rowStep)
{
if (isProductRow(row))
{
rowFound = row;
break;
}
}
if (rowFound < 0)
{
return 2; // 未找到产品顶边
}
// 2、逐列定位顶边点: 在粗定位行附近的范围内, 从上往下找到第一段连续亮像素
// 产品倾斜时顶边在每列的行坐标不同, 这些点连起来才是真实的顶边线段
const int yMargin = std::max(rowStep * 4, 60); // 纵向搜索范围(要能覆盖倾斜造成的高度差)
const int yStart = std::max(0, rowFound - yMargin);
const int yEnd = std::min(gray.rows - 1, rowFound + yMargin);
const int colStep = std::max(1, gray.cols / 200); // 采样列步长, 避免逐列遍历超大图
const int minRunLen = 3; // 连续亮像素个数, 用于过滤噪点
std::vector<cv::Point2f> points;
points.reserve(gray.cols / colStep + 1);
for (int col = 0; col < gray.cols; col += colStep)
{
for (int row = yStart; row <= yEnd; ++row)
{
int runLen = 0;
for (int k = 0; k < minRunLen && row + k <= yEnd; ++k)
{
if (gray.at<uchar>(row + k, col) >= brightThreshold)
{
runLen++;
}
else
{
break;
}
}
if (runLen >= minRunLen)
{
points.push_back(cv::Point2f((float)col, (float)row));
break;
}
}
}
if (points.size() < 10)
{
return 3; // 顶边点太少, 无法拟合
}
// 3、迭代最小二乘拟合 y = k * x + b, 并用残差剔除离群点
std::vector<cv::Point2f> fitPts = points;
float k = 0.0f;
float b = 0.0f;
for (int iter = 0; iter < 3; ++iter)
{
const int n = (int)fitPts.size();
if (n < 2)
{
break;
}
double sx = 0.0, sy = 0.0, sxx = 0.0, sxy = 0.0;
for (const auto &p : fitPts)
{
sx += p.x;
sy += p.y;
sxx += (double)p.x * p.x;
sxy += (double)p.x * p.y;
}
const double denom = n * sxx - sx * sx;
if (std::abs(denom) < 1e-6)
{
k = 0.0f; // 所有点几乎在同一列, 无法确定斜率
b = (float)(sy / n);
}
else
{
k = (float)((n * sxy - sx * sy) / denom);
b = (float)((sy - k * sx) / n);
}
// 残差剔除: 偏离拟合直线过多的点不参与下一次拟合
std::vector<cv::Point2f> inliers;
inliers.reserve(fitPts.size());
for (const auto &p : fitPts)
{
if (std::abs(p.y - (k * p.x + b)) <= 3.0f)
{
inliers.push_back(p);
}
}
if (inliers.size() == fitPts.size() || inliers.size() < 10)
{
break;
}
fitPts.swap(inliers);
}
outK = k;
outB = b;
// 线段端点: 取图像左右边界上的点
outP1 = cv::Point2f(0.0f, b);
outP2 = cv::Point2f((float)(gray.cols - 1), k * (gray.cols - 1) + b);
return 0;
}

File diff suppressed because it is too large Load Diff

@ -8,6 +8,9 @@
*/
#include "ImageAllResult.h"
#include <algorithm>
#include <cmath>
ImageAllResult::ImageAllResult()
{
detlog = std::make_shared<DetLog>();
@ -44,6 +47,55 @@ bool ImageAllResult::IsNotDet()
return false;
}
// 点到「线段所在直线」的有符号垂距,法线 n 指向的一侧为正
static bool SignedPerpDistToEdge(const cv::Point2f &p, const cv::Point2f &a, const cv::Point2f &b, float &fDist)
{
const cv::Point2f d = b - a;
const float len = std::sqrt(d.x * d.x + d.y * d.y);
if (len < 1e-6f)
{
return false;
}
// 顺时针顶点序(左上->右上->右下->左下)下,内侧法线 = (-dy, dx)/|d|
const cv::Point2f n(-d.y / len, d.x / len);
fDist = (p.x - a.x) * n.x + (p.y - a.y) * n.y;
return true;
}
cv::Point2f ImageAllResult::DetPointToProductCoord(const cv::Point2f &ptDet) const
{
if (vProductQuadInDet.size() != 4)
{
// 没有旋转矩形:退化为轴对齐平移(旧行为)
return cv::Point2f(ptDet.x - ptRoiOffsetInDet.x, ptDet.y - ptRoiOffsetInDet.y);
}
// 固定四角顺序:0左上 1右上 2左下 3右下(先按 y 再按 x 排序)
std::vector<cv::Point2f> v = vProductQuadInDet;
std::sort(v.begin(), v.end(), [](const cv::Point2f &a, const cv::Point2f &b) { return a.y < b.y; });
if (v[0].x > v[1].x)
{
std::swap(v[0], v[1]);
}
if (v[2].x > v[3].x)
{
std::swap(v[2], v[3]);
}
const cv::Point2f &pLT = v[0];
const cv::Point2f &pRT = v[1];
const cv::Point2f &pLB = v[2];
float fX = 0.f;
float fY = 0.f;
// x: 到左边线段(左下->左上)的垂距;y: 到顶边线段(左上->右上)的垂距
if (!SignedPerpDistToEdge(ptDet, pLB, pLT, fX) ||
!SignedPerpDistToEdge(ptDet, pLT, pRT, fY))
{
return cv::Point2f(ptDet.x - ptRoiOffsetInDet.x, ptDet.y - ptRoiOffsetInDet.y);
}
return cv::Point2f(fX, fY);
}
int ImageAllResult::AddDetImage(std::shared_ptr<shareImage> p)
{
{

@ -1,5 +1,6 @@
#include "ImageResultJudge.h"
#include "ImgMmPatch.hpp"
ImageResultJudge::ImageResultJudge(/* args */)
{
@ -116,6 +117,89 @@ int ImageResultJudge::GetAIDetImg(std::shared_ptr<ImageAllResult> pImageResult,
}
return 0;
}
QXImageResult ImageResultJudge::BuildDefectImage(std::shared_ptr<ImageAllResult> pImageResult, QX_ERROR_INFO_ *QX_info,
int qxidx, cv::Point pCenter, float fs_resize_x, float fs_resize_y)
{
cv::Rect roi = QX_info->roi; // 缺陷坐标(产品坐标系,抗倾斜)
// 检测图(detImg)坐标框,用于缺陷小图裁剪等内部用途。
// 优先用 ImgCheckAnalysisy 记录的真实 detImg 框:产品倾斜时无法再由产品坐标加回偏移还原
cv::Rect roi_det = QX_info->roiDet;
if (roi_det.empty())
{
roi_det = roi;
roi_det.x += pImageResult->ptRoiOffsetInDet.x;
roi_det.y += pImageResult->ptRoiOffsetInDet.y;
}
float JudgArea = QX_info->JudgArea;
float flen = QX_info->flen;
float fbreadth = QX_info->fbreadth;
float grayDis = QX_info->grayDis;
int energy = QX_info->energy;
int maxValue = QX_info->maxValue;
int config_qx_type = QX_info->nconfig_qx_type;
QXImageResult tem;
tem.idx = qxidx;
cv::Rect CutRoi = GetCutRoi(roi_det, pImageResult->detImg);
cv::Size sz = cv::Size(QX_SAMLLIMG_WIDTH, QX_SAMLLIMG_HEIGHT);
if (pImageResult->detImg.channels() == 1)
{
cv::cvtColor(pImageResult->detImg(CutRoi), tem.srcImg, cv::COLOR_GRAY2BGR);
}
else
{
tem.srcImg = pImageResult->detImg(CutRoi).clone();
}
cv::resize(tem.srcImg, tem.resizeImg, sz);
int nqx_type = ConfigTypeToResultType(config_qx_type);
tem.type = nqx_type;
tem.area = JudgArea;
tem.energy = energy;
tem.hj = grayDis;
tem.max_v = maxValue;
tem.strTypeName = QX_Result_Names[nqx_type];
tem.qx_Code = QX_Result_Code[nqx_type];
tem.srcImgroi = roi_det;
tem.len = flen;
tem.breadth = fbreadth;
tem.qx_type = 0;
tem.fScore = 0;
tem.density = 0;
tem.resizeImgroi.x = roi_det.x * fs_resize_x;
tem.resizeImgroi.width = roi_det.width * fs_resize_x;
tem.resizeImgroi.y = roi_det.y * fs_resize_y;
tem.resizeImgroi.height = roi_det.height * fs_resize_y;
// 缺陷中心:产品坐标系(像素/物理),相对产品左边/顶边
if (!QX_info->roiDet.empty())
{
// 抗倾斜路径:直接用点到左边/顶边线段的垂距(float 精度,不受框取整影响)
tem.x_pixel = QX_info->ptProduct.x;
tem.y_pixel = QX_info->ptProduct.y;
}
else
{
tem.x_pixel = roi.x + roi.width * 0.5;
tem.y_pixel = roi.y + roi.height * 0.5;
}
tem.x_mm = tem.x_pixel * m_fImgage_Scale_X;
// 【临时补丁】长度方向(y):产品运动速率不恒定(尾端有拉伸),按分段像素进度换算像素->mm
// 分段表见 ImgMmPatch.hpp,与产品物理高度(ImgCheckAnalysisy::CalProductSize)共用
tem.y_mm = PixelYToMm(tem.y_pixel);
tem.CutImgroi = roi_det;
tem.CutImgroi.x -= CutRoi.x;
tem.CutImgroi.y -= CutRoi.y;
#ifdef AI_Time
GetAIDetImg(pImageResult, pCenter, tem.AI_in_Img, tem.AI_out_img);
#endif
return tem;
}
int ImageResultJudge::UpdateImgageScale()
{
@ -151,6 +235,7 @@ int ImageResultJudge::ResultJudge(std::shared_ptr<ImageAllResult> pImageResult)
map<string, vector<singleQxZoningInfo>> allQxZoning;
set<string> qx_name_list;
m_zoningDrawGroups.clear();
for (int qxidx = 0; qxidx < qxNum; qxidx++)
{
@ -173,17 +258,32 @@ int ImageResultJudge::ResultJudge(std::shared_ptr<ImageAllResult> pImageResult)
std::string qx_wb = WHITE_BLCAK_Names[QX_whiteBLACK];
pQxLog->AddCheckstr(PrintLevel_1,
"", "qxidx: %d roi[%d %d %d %d] qx %d %s %s JudgArea %f hj %f len %f breadth %f energy %d region num %ld",
qxidx, roi.x, roi.y, roi.width, roi.height, config_qx_type, qx_name.c_str(), qx_wb.c_str(),
"", "qxidx: %d roi[%d %d %d %d] roidet[%d %d %d %d] qx %d %s %s JudgArea %f hj %f len %f breadth %f energy %d region num %ld",
qxidx, roi.x, roi.y, roi.width, roi.height,
QX_info->roiDet.x, QX_info->roiDet.y, QX_info->roiDet.width, QX_info->roiDet.height,
config_qx_type, qx_name.c_str(), qx_wb.c_str(),
JudgArea, grayDis, flen, fbreadth, energy, QX_info->detRegionidxList.size());
// 中心点用 detImg 坐标:检测区域(roiList_Src)与结果图绘制都在该坐标系下
// (产品坐标会整体平移一个常量,不影响两两距离,但绘制会偏)
cv::Point pCenter;
if (!QX_info->roiDet.empty())
{
pCenter.x = QX_info->roiDet.x + QX_info->roiDet.width * 0.5;
pCenter.y = QX_info->roiDet.y + QX_info->roiDet.height * 0.5;
}
else
{
pCenter.x = roi.x + roi.width * 0.5;
pCenter.y = roi.y + roi.height * 0.5;
}
bool bNG_Status = false;
bool bYS_Status = false;
bool Judge_Status = false;
int matched_group_id = -1;
float matched_dis = 0;
float matched_num = 0;
QX_info->result = QX_RESULT_TYPE_NoJduge;
@ -259,18 +359,6 @@ int ImageResultJudge::ResultJudge(std::shared_ptr<ImageAllResult> pImageResult)
float dis = pParam->paramArr[j].dis;
float num = pParam->paramArr[j].num;
if(dis > 0 && num > 0 && allQxZoning[qx_name].size() == 0){
pQxLog->AddCheckstr(PrintLevel_3, DET_LOG_LEVEL_3, "Dis | Num", "qx_name : %s, dis : %f, num : %f, scale_X : %f, scale_Y : %f",
qx_name.c_str(), dis, num, m_fImgage_Scale_X, m_fImgage_Scale_Y);
singleQxZoningInfo singleQxZoningInfoTemp;
singleQxZoningInfoTemp.Init();
singleQxZoningInfoTemp.judge_dis = dis;
singleQxZoningInfoTemp.judge_num = num;
singleQxZoningInfoTemp.scale_X = m_fImgage_Scale_X;
singleQxZoningInfoTemp.scale_Y = m_fImgage_Scale_Y;
allQxZoning[qx_name].push_back(singleQxZoningInfoTemp);
}
// 黑缺陷
if (QX_whiteBLACK == CONFIG_QX_WHITE)
{
@ -304,6 +392,35 @@ int ImageResultJudge::ResultJudge(std::shared_ptr<ImageAllResult> pImageResult)
flen >= Len &&
fbreadth >= Breadth)
{
if(dis > 0 && num > 0 && ict == ANALYSIS_TYPE_TF)
{
matched_group_id = j;
matched_dis = dis;
matched_num = num;
int if_insert = false;
singleQxInfo current_YS_info(qxidx, QX_RESULT_TYPE_YS, pCenter);
// 查找与当前缺陷参数组标识匹配的分区进行插入
for(int i = 0; i < allQxZoning[qx_name].size(); i++){
if(allQxZoning[qx_name][i].param_group_id == matched_group_id &&
allQxZoning[qx_name][i].Insert(current_YS_info)){
if_insert = true;
break;
}
}
if(!if_insert && matched_group_id >= 0){
singleQxZoningInfo newZoningInfoTemp;
newZoningInfoTemp.Init();
newZoningInfoTemp.param_group_id = matched_group_id;
newZoningInfoTemp.judge_dis = matched_dis;
newZoningInfoTemp.judge_num = matched_num;
newZoningInfoTemp.center = pCenter;
newZoningInfoTemp.scale_X = m_fImgage_Scale_X;
newZoningInfoTemp.scale_Y = m_fImgage_Scale_Y;
newZoningInfoTemp.qxList.push_back(current_YS_info);
allQxZoning[qx_name].push_back(newZoningInfoTemp); // 插入失败,添加一个新分组
}
continue;
}
nerrortype = 1;
result = false;
if (ict == ANALYSIS_TYPE_YS)
@ -394,25 +511,6 @@ int ImageResultJudge::ResultJudge(std::shared_ptr<ImageAllResult> pImageResult)
else if (bYS_Status)
{
QX_info->result = QX_RESULT_TYPE_YS;
int if_insert = false;
singleQxInfo current_YS_info(qxidx, QX_RESULT_TYPE_YS, pCenter);
for(int i = 0; i < allQxZoning[qx_name].size(); i++){
if(allQxZoning[qx_name][i].Insert(current_YS_info)){
if_insert = true;
break;
}
}
if(!if_insert && allQxZoning[qx_name].size() > 0){
singleQxZoningInfo newZoningInfoTemp;
newZoningInfoTemp.Init();
newZoningInfoTemp.judge_dis = allQxZoning[qx_name][0].judge_dis;
newZoningInfoTemp.judge_num = allQxZoning[qx_name][0].judge_num;
newZoningInfoTemp.center = pCenter;
newZoningInfoTemp.scale_X = m_fImgage_Scale_X;
newZoningInfoTemp.scale_Y = m_fImgage_Scale_Y;
newZoningInfoTemp.qxList.push_back(current_YS_info);
allQxZoning[qx_name].push_back(newZoningInfoTemp); // 插入失败,添加一个新分组
}
}
else
{
@ -495,55 +593,8 @@ int ImageResultJudge::ResultJudge(std::shared_ptr<ImageAllResult> pImageResult)
// 生成缺陷小图
if (bYS_Status || bNG_Status)
{
// 生成图片
QXImageResult tem;
tem.idx = qxidx;
cv::Rect CutRoi = GetCutRoi(roi, pImageResult->detImg);
// CheckUtil::printROI(CutRoi, "CutRoi");
cv::Size sz = cv::Size(QX_SAMLLIMG_WIDTH, QX_SAMLLIMG_HEIGHT);
if (pImageResult->detImg.channels() == 1)
{
cv::cvtColor(pImageResult->detImg(CutRoi), tem.srcImg, cv::COLOR_GRAY2BGR);
}
else
{
tem.srcImg = pImageResult->detImg(CutRoi).clone();
}
cv::resize(tem.srcImg, tem.resizeImg, sz);
QXImageResult tem = BuildDefectImage(pImageResult, QX_info, qxidx, pCenter, fs_resize_x, fs_resize_y);
int nqx_type = ConfigTypeToResultType(config_qx_type);
tem.type = nqx_type;
tem.area = JudgArea;
tem.energy = energy;
tem.hj = grayDis;
tem.max_v = maxValue;
tem.strTypeName = QX_Result_Names[nqx_type];
tem.qx_Code = QX_Result_Code[nqx_type];
tem.srcImgroi = roi;
tem.srcImgroi.x += pDetResult->CutRoi.x;
tem.srcImgroi.y += pDetResult->CutRoi.y;
tem.len = flen;
tem.breadth = fbreadth;
tem.qx_type = 0;
tem.fScore = 0;
tem.density = 0;
tem.resizeImgroi.x = roi.x * fs_resize_x;
tem.resizeImgroi.width = roi.width * fs_resize_x;
tem.resizeImgroi.y = roi.y * fs_resize_y;
tem.resizeImgroi.height = roi.height * fs_resize_y;
tem.x_pixel = roi.x + roi.width * 0.5;
tem.y_pixel = roi.y + roi.height * 0.5;
tem.x_mm = tem.x_pixel * m_fImgage_Scale_X;
tem.y_mm = tem.y_pixel * m_fImgage_Scale_Y;
tem.CutImgroi = roi;
tem.CutImgroi.x -= CutRoi.x;
tem.CutImgroi.y -= CutRoi.y;
if (bNG_Status)
{
@ -557,22 +608,16 @@ int ImageResultJudge::ResultJudge(std::shared_ptr<ImageAllResult> pImageResult)
m_CheckResult_shareP->nresult = nqx_type;
}
GetAIDetImg(pImageResult, pCenter, tem.AI_in_Img, tem.AI_out_img);
m_CheckResult_shareP->qxImageResult.push_back(tem);
pQxLog->bPrintStr = true;
pQxLog->AddCheckstr(PrintLevel_4, DET_LOG_LEVEL_3, " result ", " ---name: %s ---code: %s ---srcImgroi.x: %d ---srcImgroi.y: %d\n", tem.strTypeName.c_str(), tem.qx_Code.c_str(), tem.srcImgroi.x, tem.srcImgroi.y);
pQxLog->bPrintStr = false;
}
else
{
if (m_CheckResult_shareP->nYS_result <= ERROR_TYPE_OK)
{
m_CheckResult_shareP->nYS_result = nqx_type;
}
GetAIDetImg(pImageResult, pCenter, tem.AI_in_Img, tem.AI_out_img);
m_CheckResult_shareP->YS_ImageResult.push_back(tem);
}
}
@ -583,86 +628,24 @@ int ImageResultJudge::ResultJudge(std::shared_ptr<ImageAllResult> pImageResult)
string qx_name = name;
for(int i = 0; i < allQxZoning[qx_name].size(); i++)
{
// 个数超过设定值,YS改为NG
if(allQxZoning[qx_name][i].judgeNum()){
// 个数超过设定值,判NG
if(allQxZoning[qx_name][i].judgeNum())
{
pImageLog->AddCheckstr(PrintLevel_3, DET_LOG_LEVEL_3, "Zoning NG", " name: %s, group_id: %d, count: %d >= %d → NG",
qx_name.c_str(), allQxZoning[qx_name][i].param_group_id,
(int)allQxZoning[qx_name][i].qxList.size(), allQxZoning[qx_name][i].judge_num);
for(int j = 0; j < allQxZoning[qx_name][i].qxList.size(); j++)
{
int qxidx = allQxZoning[qx_name][i].qxList[j].qxidx;
cv::Point pCenter = allQxZoning[qx_name][i].qxList[j].pcenter;
QX_ERROR_INFO_ *QX_info = &pDetResult->pQx_ErrorList->at(qxidx);
std::shared_ptr<DetLog> pQxLog = pDetResult->pQx_ErrorList->at(qxidx).detlog;
pQxLog->AddCheckstr(PrintLevel_3, DET_LOG_LEVEL_3, "YS to NG", " name: %s, idx: %d | Index %d / %d > %d", qx_name.c_str(), qxidx, j + 1, allQxZoning[qx_name][i].qxList.size(), allQxZoning[qx_name][i].judge_num);
// pQxLog->bPrintStr = true;
cv::Rect roi = QX_info->roi;
float JudgArea = QX_info->JudgArea;
float JudgArea_second = QX_info->JudgArea_second;
float flen = QX_info->flen;
float fbreadth = QX_info->fbreadth;
float grayDis = QX_info->grayDis;
int energy = QX_info->energy;
float fupS = QX_info->fUpIou;
int maxValue = QX_info->maxValue;
float density = QX_info->density;
int QX_whiteBLACK = QX_info->whiteOrBlack;
int config_qx_type = QX_info->nconfig_qx_type;
std::string qx_name = CONFIG_QX_NAME_Names[config_qx_type];
std::string qx_wb = WHITE_BLCAK_Names[QX_whiteBLACK];
QX_info->result = QX_RESULT_TYPE_NG;
QX_info->result_name = QX_RESULT_TYPE_Names[QX_info->result];
std::string resultType = QX_info->result_name;
m_CheckResult_shareP->nresult = 1;
// 生成图片
QXImageResult tem;
tem.idx = qxidx;
cv::Rect CutRoi = GetCutRoi(roi, pImageResult->detImg);
// CheckUtil::printROI(CutRoi, "CutRoi");
cv::Size sz = cv::Size(QX_SAMLLIMG_WIDTH, QX_SAMLLIMG_HEIGHT);
if (pImageResult->detImg.channels() == 1)
{
cv::cvtColor(pImageResult->detImg(CutRoi), tem.srcImg, cv::COLOR_GRAY2BGR);
}
else
{
tem.srcImg = pImageResult->detImg(CutRoi).clone();
}
cv::resize(tem.srcImg, tem.resizeImg, sz);
int nqx_type = ConfigTypeToResultType(config_qx_type);
tem.type = nqx_type;
tem.area = JudgArea;
tem.energy = energy;
tem.hj = grayDis;
tem.max_v = maxValue;
tem.strTypeName = QX_Result_Names[nqx_type];
tem.qx_Code = QX_Result_Code[nqx_type];
tem.srcImgroi = roi;
tem.srcImgroi.x += pDetResult->CutRoi.x;
tem.srcImgroi.y += pDetResult->CutRoi.y;
tem.len = flen;
tem.breadth = fbreadth;
tem.qx_type = 0;
tem.fScore = 0;
tem.density = 0;
tem.resizeImgroi.x = roi.x * fs_resize_x;
tem.resizeImgroi.width = roi.width * fs_resize_x;
tem.resizeImgroi.y = roi.y * fs_resize_y;
tem.resizeImgroi.height = roi.height * fs_resize_y;
tem.x_pixel = roi.x + roi.width * 0.5;
tem.y_pixel = roi.y + roi.height * 0.5;
tem.x_mm = tem.x_pixel * m_fImgage_Scale_X;
tem.y_mm = tem.y_pixel * m_fImgage_Scale_Y;
tem.CutImgroi = roi;
tem.CutImgroi.x -= CutRoi.x;
tem.CutImgroi.y -= CutRoi.y;
QXImageResult tem = BuildDefectImage(pImageResult, QX_info, qxidx, pCenter, fs_resize_x, fs_resize_y);
int nqx_type = ConfigTypeToResultType(QX_info->nconfig_qx_type);
m_CheckResult_shareP->defectResultList[nqx_type].nresult = 1;
m_CheckResult_shareP->defectResultList[nqx_type].keyName = QX_Result_Names[nqx_type];
@ -673,10 +656,46 @@ int ImageResultJudge::ResultJudge(std::shared_ptr<ImageAllResult> pImageResult)
m_CheckResult_shareP->nresult = nqx_type;
}
GetAIDetImg(pImageResult, pCenter, tem.AI_in_Img, tem.AI_out_img);
m_CheckResult_shareP->qxImageResult.push_back(tem);
}
}
// 数量未达标,判为YS
else
{
pImageLog->AddCheckstr(PrintLevel_3, DET_LOG_LEVEL_3, "Zoning YS", " name: %s, group_id: %d, count: %d < %d → YS",
qx_name.c_str(), allQxZoning[qx_name][i].param_group_id,
(int)allQxZoning[qx_name][i].qxList.size(), allQxZoning[qx_name][i].judge_num);
for(int j = 0; j < allQxZoning[qx_name][i].qxList.size(); j++)
{
int qxidx = allQxZoning[qx_name][i].qxList[j].qxidx;
cv::Point pCenter = allQxZoning[qx_name][i].qxList[j].pcenter;
QX_ERROR_INFO_ *QX_info = &pDetResult->pQx_ErrorList->at(qxidx);
QX_info->result_name = QX_RESULT_TYPE_Names[QX_info->result];
m_CheckResult_shareP->nYS_result = 1;
QXImageResult tem = BuildDefectImage(pImageResult, QX_info, qxidx, pCenter, fs_resize_x, fs_resize_y);
int nqx_type = ConfigTypeToResultType(QX_info->nconfig_qx_type);
if (m_CheckResult_shareP->nYS_result <= ERROR_TYPE_OK)
{
m_CheckResult_shareP->nYS_result = nqx_type;
}
m_CheckResult_shareP->YS_ImageResult.push_back(tem);
}
}
// 收集本组所有缺陷中心点,用于后续连线绘制(至少2个点才有意义)
if(allQxZoning[qx_name][i].qxList.size() >= 2)
{
QxZoningDrawGroup group;
group.isNG = allQxZoning[qx_name][i].judgeNum();
for(int j = 0; j < allQxZoning[qx_name][i].qxList.size(); j++)
{
group.centers.push_back(allQxZoning[qx_name][i].qxList[j].pcenter);
}
m_zoningDrawGroups.push_back(group);
}
}
}
@ -794,6 +813,11 @@ int ImageResultJudge::DrawResult(std::shared_ptr<ImageAllResult> pImageResult)
std::string text = buffer;
draw_str->strlist.push_back(text);
}
{
sprintf(buffer, " xy:%0.3f %0.3f mm", pqx->x_mm, pqx->y_mm);
std::string text = buffer;
draw_str->strlist.push_back(text);
}
{
sprintf(buffer, " len %0.2f A: %0.2f ", pqx->len, pqx->area);
std::string text = buffer;
@ -975,6 +999,28 @@ int ImageResultJudge::DrawResult(std::shared_ptr<ImageAllResult> pImageResult)
}
long t2 = CheckUtil::getcurTime();
// 绘制同组缺陷之间的连线(至少2个点才有连线意义)
cv::Scalar color_line_NG = cv::Scalar(0, 0, 255); // 红色,NG组连线
cv::Scalar color_line_YS = cv::Scalar(0, 200, 200); // 青黄色,YS组连线
for (const auto& group : m_zoningDrawGroups)
{
if (group.centers.size() < 2) continue;
std::shared_ptr<DrawInfo> draw_line = std::make_shared<DrawInfo>();
draw_line->drawImg = m_CheckResult_shareP->resultimg;
draw_line->type = Draw_lines;
draw_line->color = group.isNG ? color_line_NG : color_line_YS;
draw_line->dr = 1; // 线宽
// 将detection坐标系的中心点缩放到result图像坐标系
for (const auto& pt : group.centers)
{
draw_line->linePts.push_back(cv::Point(
(int)(pt.x * fs_resize_x),
(int)(pt.y * fs_resize_y)));
}
sendTask(draw_line);
}
// 等待绘制完成
WaiteDrawComplate();
long t3 = CheckUtil::getcurTime();
@ -1218,6 +1264,19 @@ int ImageResultJudge::DrawResultTask(std::shared_ptr<ImageResultJudge::DrawInfo>
// cv::circle(task->drawImg, task->cp, 3, task->color);
}
if (task->type == Draw_lines)
{
if (task->linePts.size() >= 2)
{
int thickness = task->dr > 0 ? task->dr : 1;
for (size_t i = 0; i < task->linePts.size() - 1; i++)
{
cv::line(task->drawImg, task->linePts[i], task->linePts[i + 1], task->color, thickness);
}
// 闭合连线:首尾相连
cv::line(task->drawImg, task->linePts.back(), task->linePts.front(), task->color, thickness);
}
}
return 0;
}

File diff suppressed because it is too large Load Diff

@ -19,7 +19,7 @@ message(STATUS "oPENCV Library status:")
message(STATUS ">version:${OpenCV_VERSION}")
message(STATUS "Include:${OpenCV_INCLUDE_DIRS}")
set(CMAKE_BUILD_TYPE "debug")
set(CMAKE_BUILD_TYPE "release")
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) # 要求编译器支持 C++17
set(CMAKE_CXX_EXTENSIONS OFF) # 禁用 GNU 特定的扩展
@ -65,6 +65,9 @@ MESSAGE("ExtractImageModule")
add_subdirectory(ConfigModule)
MESSAGE("ConfigModule")
add_subdirectory(TcsCheckModule)
MESSAGE("TcsCheckModule")
# CommonUtil
add_subdirectory(AlgorithmModule)
MESSAGE("AlgorithmModule")

@ -407,8 +407,9 @@ struct BasicConfig
float Product_Size_Height_mm; // 产品尺寸 高度 mm
float fImage_Scale_x; // 成像精度
float fImage_Scale_y; // 成像精度
bool bTraditionalDetect; // 使用传统算法检测
bool bDetect; // 是否检测
std::string strCamName; //
std::string strConfigVersion; // 配置版本
float density_R_mm; // 密度计算半径 像素
BasicConfig()
@ -428,9 +429,10 @@ struct BasicConfig
Product_Size_Height_mm = 1000;
fImage_Scale_x = 0.03;
fImage_Scale_y = 0.03;
bTraditionalDetect = false;
bDetect = true;
density_R_mm = 5;
strCamName = "";
strConfigVersion = "1.0";
strCamearName = EMPTY_CONFIG_NAME;
}
void copy(BasicConfig tem)
@ -452,16 +454,19 @@ struct BasicConfig
this->Product_Size_Height_mm = tem.Product_Size_Height_mm;
this->fImage_Scale_x = tem.fImage_Scale_x;
this->fImage_Scale_y = tem.fImage_Scale_y;
this->bTraditionalDetect = tem.bTraditionalDetect;
this->bDetect = tem.bDetect;
this->density_R_mm = tem.density_R_mm;
this->strCamearName = tem.strCamearName;
this->strConfigVersion = tem.strConfigVersion;
}
void print(std::string str = "")
{
printf("============================↓↓↓↓↓↓%s↓↓ %s ↓↓↓↓↓=========================\n", str.c_str(), strCamearName.c_str());
printf("bCal_ImageScale %d Product_Size_Width =%f Product_Size_Height =%f \n", bCal_ImageScale, Product_Size_Width_mm, Product_Size_Height_mm);
printf("fImage_Scale_x =%f fImage_Scale_y=%f \n", fImage_Scale_x, fImage_Scale_y);
printf("bTraditionalDetect %d density_R_mm=%f \n", bTraditionalDetect, density_R_mm);
printf("density_R_mm=%f \n", density_R_mm);
printf("bDetect=%d \n", bDetect);
// printf("height_min =%d height_max=%d \n", height_min, height_max);
printf("bDrawShieldRoi %d bShield_ZF %d DrawPreRoi %d fUP_IOU %f density_R_mm %f\n", bDrawShieldRoi, bShield_ZF, bDrawPreRoi, fUP_IOU, density_R_mm);
printf("============================↑↑↑↑↑↑%s↑↑↑↑↑↑=========================\n", str.c_str());
@ -958,7 +963,7 @@ struct Function_ShieldRegion
{
return;
}
shieldMask = cv::Mat(img_H, img_W, CV_8U, cv::Scalar(0));
shieldMask = cv::Mat::zeros(img_H, img_W, CV_8U);
if (pointArry1.size() > 0)
{
cv::fillPoly(shieldMask, pointArry1, cv::Scalar(255));
@ -1085,7 +1090,7 @@ struct Function_EdgeROI
// {
// return;
// }
EdgeMask = cv::Mat(img_H, img_W, CV_8U, cv::Scalar(0));
EdgeMask = cv::Mat::zeros(img_H, img_W, CV_8U);
if (pointArry1.size() > 0)
{
@ -1194,7 +1199,7 @@ struct Function_Image_Align
// pointArry1[i].y -= boundingRect.y;
// }
cv::Mat tem_feature_Mask = cv::Mat(img_H, img_W, CV_8U, cv::Scalar(0));
cv::Mat tem_feature_Mask = cv::Mat::zeros(img_H, img_W, CV_8U);
if (pointArry1.size() > 0)
{
@ -1697,6 +1702,7 @@ struct Base_Function_MarkLine
std::vector<cv::Point> region;
cv::Point mark_local_1;
cv::Point mark_local_2;
float hoverlap;
Base_Function_MarkLine()
{
@ -1710,6 +1716,7 @@ struct Base_Function_MarkLine
mark_local_1 = cv::Point(0, 0);
mark_local_2 = cv::Point(0, 0);
productROI = cv::Rect(0, 0, 0, 0);
hoverlap = 0;
region.clear();
}
@ -1722,17 +1729,18 @@ struct Base_Function_MarkLine
this->badapt_region = tem.badapt_region;
this->mark_local_1 = tem.mark_local_1;
this->mark_local_2 = tem.mark_local_2;
this->hoverlap = tem.hoverlap;
}
void print(std::string str)
{
printf("%s>>bOpen %d bDraw %d badapt_region %d mark1 [%d %d] mark2 [%d %d] \n", str.c_str(),
bOpen, bDraw, badapt_region,mark_local_1.x, mark_local_1.y, mark_local_2.x, mark_local_2.y);
printf("%s>>bOpen %d bDraw %d badapt_region %d mark1 [%d %d] mark2 [%d %d] hoverlap %f\n", str.c_str(),
bOpen, bDraw, badapt_region,mark_local_1.x, mark_local_1.y, mark_local_2.x, mark_local_2.y, hoverlap);
}
std::string GetInfo(std::string str)
{
char buffer[256];
sprintf(buffer, "%s>>bOpen %d bDraw %d badapt_region %d mark1 [%d %d] mark2 [%d %d] \n", str.c_str(),
bOpen, bDraw, badapt_region,mark_local_1.x, mark_local_1.y, mark_local_2.x, mark_local_2.y);
sprintf(buffer, "%s>>bOpen %d bDraw %d badapt_region %d mark1 [%d %d] mark2 [%d %d] hoverlap %f\n", str.c_str(),
bOpen, bDraw, badapt_region,mark_local_1.x, mark_local_1.y, mark_local_2.x, mark_local_2.y, hoverlap);
std::string str123 = buffer;
return str123;
}
@ -1746,6 +1754,7 @@ struct Base_Function_SaveImg
bool bSaveMarkImg; // mark 存图
bool bSaveClsImg; // 分类 存图
bool bSaveAlginImg; // 分类 存图
bool bSaveAIDetImg; // AI 检测 存图
Base_Function_SaveImg()
{
@ -1757,6 +1766,7 @@ struct Base_Function_SaveImg
bSaveMarkImg = false;
bSaveClsImg = false;
bSaveAlginImg = false;
bSaveAIDetImg = false;
}
void copy(Base_Function_SaveImg tem)
@ -1766,22 +1776,84 @@ struct Base_Function_SaveImg
this->bSaveMarkImg = tem.bSaveMarkImg;
this->bSaveClsImg = tem.bSaveClsImg;
this->bSaveAlginImg = tem.bSaveAlginImg;
this->bSaveAIDetImg = tem.bSaveAIDetImg;
}
void print(std::string str)
{
printf("%s>>bOpen %d bSaveMarkImg %d bSaveClsImg %d bSaveAlginImg %d \n", str.c_str(),
bOpen, bSaveMarkImg, bSaveClsImg, bSaveAlginImg);
printf("%s>>bOpen %d bSaveMarkImg %d bSaveClsImg %d bSaveAlginImg %d bSaveAIDetImg %d\n", str.c_str(),
bOpen, bSaveMarkImg, bSaveClsImg, bSaveAlginImg, bSaveAIDetImg);
}
std::string GetInfo(std::string str)
{
char buffer[256];
sprintf(buffer, "%s>>bOpen %d bSaveMarkImg %d bSaveClsImg %d bSaveAlginImg %d \n", str.c_str(),
bOpen, bSaveMarkImg, bSaveClsImg, bSaveAlginImg);
sprintf(buffer, "%s>>bOpen %d bSaveMarkImg %d bSaveClsImg %d bSaveAlginImg %d bSaveAIDetImg %d\n", str.c_str(),
bOpen, bSaveMarkImg, bSaveClsImg, bSaveAlginImg, bSaveAIDetImg);
std::string str123 = buffer;
return str123;
}
};
//传统检测
struct Base_Function_TraditionDet
{
bool bOpen; // 是否开启
float nAreaLowFilter; // 定位阈值
int nBlockSize; // 分块大小
float fZoomRatio; // 缩放比例
float nAreaFilter; // 面积过滤
int nCountFilter; // 数量过滤
int nFilterLow; // 低灰度过滤
int nFilterHigh; // 高灰度过滤
cv::Rect detArea_ROI;
std::vector<cv::Point> detArea;
bool bdetArea; // 是否使用区域
Base_Function_TraditionDet()
{
Init();
}
void Init()
{
bOpen = false;
nAreaLowFilter = 0;
nBlockSize = 0;
fZoomRatio = 0;
nAreaFilter = 0;
nCountFilter = 0;
nFilterLow = 0;
nFilterHigh = 0;
bdetArea = false;
detArea_ROI = cv::Rect(0, 0, 0, 0);
detArea.clear();
}
void copy(Base_Function_TraditionDet tem)
{
this->bOpen = tem.bOpen;
this->nAreaLowFilter = tem.nAreaLowFilter;
this->nBlockSize = tem.nBlockSize;
this->fZoomRatio = tem.fZoomRatio;
this->nAreaFilter = tem.nAreaFilter;
this->nCountFilter = tem.nCountFilter;
this->nFilterLow = tem.nFilterLow;
this->nFilterHigh = tem.nFilterHigh;
this->bdetArea = tem.bdetArea;
this->detArea_ROI = tem.detArea_ROI;
this->detArea.assign(tem.detArea.begin(), tem.detArea.end());
}
void print(std::string str)
{
printf("%s>>bOpen %d nAreaLowFilter %f nBlockSize %d fZoomRatio %f nAreaFilter %f nCountFilter %d nFilterLow %d nFilterHigh %d \n", str.c_str(),
bOpen, nAreaLowFilter, nBlockSize, fZoomRatio, nAreaFilter, nCountFilter, nFilterLow, nFilterHigh);
}
std::string GetInfo(std::string str)
{
char buffer[256];
sprintf(buffer, "%s>>bOpen %d nAreaLowFilter %f nBlockSize %d fZoomRatio %f nAreaFilter %f nCountFilter %d nFilterLow %d nFilterHigh %d \n", str.c_str(),
bOpen, nAreaLowFilter, nBlockSize, fZoomRatio, nAreaFilter, nCountFilter, nFilterLow, nFilterHigh);
std::string str123 = buffer;
return str123;
}
};
// 大缺陷 NG
struct Big_NG
{
@ -1882,6 +1954,7 @@ struct Base_Function_Edge_Det
int Search_threshold; // 搜索边缘阈值
int Det_threshold; // 缺陷检测阈值
int Det_Range; // 检测范围
int Det_Range_Offset; // 检测范围偏移
int QX_Widht_min; // 缺陷宽度-最小
int QX_Widht_max;
int QX_Height_min;
@ -1895,8 +1968,10 @@ struct Base_Function_Edge_Det
int queJiao_height;
bool bDrawRoi;
bool bUseAlignRoi;
cv::Rect draw_ROI;
std::vector<cv::Point> region;
std::vector<cv::Point> Align_region;
bool bDrawResult; // 是否绘制结果
Base_Function_Edge_Det()
@ -1909,6 +1984,7 @@ struct Base_Function_Edge_Det
Search_threshold = 30;
Det_threshold = 30;
Det_Range = 100;
Det_Range_Offset = 0;
QX_Widht_min = 10;
QX_Widht_max = 100;
QX_Height_min = 10;
@ -1921,6 +1997,7 @@ struct Base_Function_Edge_Det
queJiao_width = 50;
queJiao_height = 50;
bDrawRoi = false;
bUseAlignRoi = false;
draw_ROI = cv::Rect(0, 0, 0, 0);
region.clear();
region.erase(region.begin(), region.end());
@ -1933,6 +2010,7 @@ struct Base_Function_Edge_Det
this->Search_threshold = tem.Search_threshold;
this->Det_threshold = tem.Det_threshold;
this->Det_Range = tem.Det_Range;
this->Det_Range_Offset = tem.Det_Range_Offset;
this->QX_Widht_min = tem.QX_Widht_min;
this->QX_Widht_max = tem.QX_Widht_max;
this->QX_Height_min = tem.QX_Height_min;
@ -1948,18 +2026,19 @@ struct Base_Function_Edge_Det
this->region.assign(tem.region.begin(), tem.region.end());
this->draw_ROI = tem.draw_ROI;
this->bDrawRoi = tem.bDrawRoi;
this->bUseAlignRoi = tem.bUseAlignRoi;
this->bDrawResult = tem.bDrawResult;
}
void print(std::string str)
{
printf("%s>>bOpen %d Search_threshold %d Det_threshold %d Det_Range %d QX_Widht [%d %d] QX_Height [%d %d]\n", str.c_str(),
bOpen, Search_threshold, Det_threshold, Det_Range, QX_Widht_min, QX_Widht_max, QX_Height_min, QX_Height_max);
printf("%s>>bOpen %d Search_threshold %d Det_threshold %d Det_Range %d Det_Range_Offset %d QX_Widht [%d %d] QX_Height [%d %d]\n", str.c_str(),
bOpen, Search_threshold, Det_threshold, Det_Range, Det_Range_Offset, QX_Widht_min, QX_Widht_max, QX_Height_min, QX_Height_max);
}
std::string GetInfo(std::string str)
{
char buffer[256];
sprintf(buffer, "%s>>bOpen %d Search_threshold %d Det_threshold %d Det_Range %d QX_Widht [%d %d] QX_Height [%d %d]\n", str.c_str(),
bOpen, Search_threshold, Det_threshold, Det_Range, QX_Widht_min, QX_Widht_max, QX_Height_min, QX_Height_max);
sprintf(buffer, "%s>>bOpen %d Search_threshold %d Det_threshold %d Det_Range %d Det_Range_Offset %d QX_Widht [%d %d] QX_Height [%d %d]\n", str.c_str(),
bOpen, Search_threshold, Det_threshold, Det_Range, Det_Range_Offset, QX_Widht_min, QX_Widht_max, QX_Height_min, QX_Height_max);
std::string str123 = buffer;
return str123;
}
@ -1970,6 +2049,7 @@ struct BaseCheckFunction
Base_Function_MarkLine markLine;
Base_Function_Edge_Det edgeDet;
Base_Function_SaveImg saveImg;
Base_Function_TraditionDet traditionDet;
Base_Function_BigNG bigNG;
BaseCheckFunction()
{
@ -1980,6 +2060,7 @@ struct BaseCheckFunction
markLine.Init();
edgeDet.Init();
saveImg.Init();
traditionDet.Init();
bigNG.Init();
}
void copy(BaseCheckFunction tem)
@ -1987,6 +2068,7 @@ struct BaseCheckFunction
this->markLine.copy(tem.markLine);
this->edgeDet.copy(tem.edgeDet);
this->saveImg.copy(tem.saveImg);
this->traditionDet.copy(tem.traditionDet);
this->bigNG.copy(tem.bigNG);
}
void print(std::string str)
@ -1995,6 +2077,7 @@ struct BaseCheckFunction
markLine.print("markLine");
edgeDet.print("edgeDet");
saveImg.print("saveImg");
traditionDet.print("traditionDet");
bigNG.print("bigNG");
}
std::string GetInfo(std::string str)
@ -2003,6 +2086,7 @@ struct BaseCheckFunction
str123 += markLine.GetInfo("markLine");
str123 += edgeDet.GetInfo("edgeDet");
str123 += saveImg.GetInfo("saveImg");
str123 += traditionDet.GetInfo("traditionDet");
str123 += bigNG.GetInfo("bigNG");
// str123 += "\n";
return str123;

@ -134,7 +134,8 @@ int ConfigManager::UpdateConfig()
ReadFlawCodeConfig(defect_list_file);
bool bFileName = false;
// std::regex pattern(R"(param_[0-9]\.json)");
std::regex pattern(R"(param_(\d+|left|right)\.json)");
// 匹配 param_XXX.json (e.g. param_BCA.json, param_BTA.json, param_DCA.json, param_DTA.json)
std::regex pattern(R"(param_[A-Za-z0-9]+\.json)");
if (!fs::exists(m_strConfigRootPath))
{
std::cerr << "目录不存在: " << m_strConfigRootPath << std::endl;

@ -32,7 +32,7 @@ void CommonParamToCheckConfigJson::toObjectFromValue(Json::Value root)
std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
Json::Value rootvalue;
std::string err;
// std::cout << strJson << std::endl;
std::cout << strJson << std::endl;
auto nSize = strJson.size();
if (reader->parse(strJson.c_str(), strJson.c_str() + nSize, &rootvalue, &err))
{
@ -44,6 +44,11 @@ void CommonParamToCheckConfigJson::toObjectFromValue(Json::Value root)
// getchar();
if (value.isObject())
{
_config.baseConfig.strConfigVersion = value["version"].asString();
if ( _config.baseConfig.strConfigVersion == "")
{
_config.baseConfig.strConfigVersion = "NULL";
}
_config.baseConfig.image_widht = value["image_widht"].asInt();
_config.baseConfig.Image_height = value["Image_height"].asInt();
_config.baseConfig.bDrawShieldRoi = value["bDrawShieldRoi"].asBool();
@ -57,7 +62,7 @@ void CommonParamToCheckConfigJson::toObjectFromValue(Json::Value root)
_config.baseConfig.Product_Size_Height_mm = value["Product_Size_H"].asFloat();
_config.baseConfig.fImage_Scale_x = value["Image_Scale_X"].asFloat();
_config.baseConfig.fImage_Scale_y = value["Image_Scale_Y"].asFloat();
_config.baseConfig.bTraditionalDetect = value["bTraditionalDetect"].asFloat();
_config.baseConfig.bDetect = value["bDetect"].asFloat();
if (value["Density_R"])
{
_config.baseConfig.density_R_mm = value["Density_R"].asFloat();
@ -1163,6 +1168,10 @@ int BaseFuntonConfigJson::GetFunction(Json::Value value)
}
}
}
if (value_f["form"]["ALign_Config"]["hoverlap"])
{
_config.markLine.hoverlap = value_f["form"]["ALign_Config"]["hoverlap"].asFloat();
}
// _config.markLine.print("dfe");
// getchar();
}
@ -1192,6 +1201,10 @@ int BaseFuntonConfigJson::GetFunction(Json::Value value)
{
_config.edgeDet.Det_Range = value_f["form"]["Detect_Config"]["Det_Range"].asInt();
}
if (value_f["form"]["Detect_Config"]["Det_Range_Offset"])
{
_config.edgeDet.Det_Range_Offset = value_f["form"]["Detect_Config"]["Det_Range_Offset"].asInt();
}
if (value_f["form"]["Detect_Config"]["QX_Widht_min"])
{
@ -1249,6 +1262,11 @@ int BaseFuntonConfigJson::GetFunction(Json::Value value)
_config.edgeDet.bDrawRoi = value_f["form"]["Draw_Config"]["Open"].asBool();
}
if (value_f["form"]["Draw_Config"]["bUse_Align"])
{
_config.edgeDet.bUseAlignRoi = value_f["form"]["Draw_Config"]["bUse_Align"].asBool();
}
// 2、读取区域点
{
auto value_region = value_f["form"]["Draw_Config"]["Edge_ROI"];
@ -1301,6 +1319,10 @@ int BaseFuntonConfigJson::GetFunction(Json::Value value)
{
_config.saveImg.bSaveAlginImg = value_f["form"]["SaveImg_Config"]["Align_Big_Save"].asBool();
}
if (value_f["form"]["SaveImg_Config"]["Det_Save"])
{
_config.saveImg.bSaveAIDetImg = value_f["form"]["SaveImg_Config"]["Det_Save"].asBool();
}
}
else
{
@ -1309,6 +1331,70 @@ int BaseFuntonConfigJson::GetFunction(Json::Value value)
// _config.edgeDet.print("edgeDet");
// getchar();
}
if ("Tradition_Detect" == strCode)
{
auto value_f = value;
// std::cout << value_f << std::endl;
// getchar();
_config.traditionDet.bOpen = value_f["isOpen"].asBool();
if (_config.traditionDet.bOpen)
{
if (value_f["form"]["Tradition_Param"]["nBlockSize"])
{
_config.traditionDet.nBlockSize = value_f["form"]["Tradition_Param"]["nBlockSize"].asInt();
}
if (value_f["form"]["Tradition_Param"]["fZoomRatio"])
{
_config.traditionDet.fZoomRatio = value_f["form"]["Tradition_Param"]["fZoomRatio"].asFloat();
}
if (value_f["form"]["Tradition_Param"]["nAreaFilter"])
{
_config.traditionDet.nAreaFilter = value_f["form"]["Tradition_Param"]["nAreaFilter"].asFloat();
}
if (value_f["form"]["Tradition_Param"]["nCountFilter"])
{
_config.traditionDet.nCountFilter = value_f["form"]["Tradition_Param"]["nCountFilter"].asInt();
}
if (value_f["form"]["Tradition_Param"]["nFilterLow"])
{
_config.traditionDet.nFilterLow = value_f["form"]["Tradition_Param"]["nFilterLow"].asInt();
}
if (value_f["form"]["Tradition_Param"]["nFilterHigh"])
{
_config.traditionDet.nFilterHigh = value_f["form"]["Tradition_Param"]["nFilterHigh"].asInt();
}
// 2、读取区域点
{
auto value_region = value_f["form"]["Tradition_Param"]["detArea"];
if (value_region.isArray())
{
for (int idx = 0; idx < value_region.size(); idx++)
{
cv::Point p;
p.x = value_region[idx][0].asInt();
p.y = value_region[idx][1].asInt();
_config.traditionDet.detArea.emplace_back(p);
}
if (_config.traditionDet.detArea.size() > 0)
{
_config.traditionDet.detArea_ROI = boundingRect(_config.traditionDet.detArea);
}
}
}
if (value_f["form"]["Tradition_Param"]["bdetArea"])
{
_config.traditionDet.bdetArea = value_f["form"]["Tradition_Param"]["bdetArea"].asFloat();
}
}
else
{
_config.traditionDet.Init();
}
// _config.traditionDet.print("traditionDet");
// getchar();
}
if ("Big_NG" == strCode)
{
auto value_f = value;

@ -0,0 +1,45 @@
#版本限定
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
# CheckUtil.hpp (异步存图等通用工具)
${PROJECT_SOURCE_DIR}/AlgorithmModule/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,146 @@
#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
std::string productId; // product id
std::string productChannel; // product channel
bool bDebugsaveImg;
};
/*
长宽比 >= 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);
// ========== 独立检测/分类接口 ==========
// 传统检测,输出残点二值图 0=成功, -1=无产品/输入为空
int TraditionalDetect(const cv::Mat& img, cv::Rect detRoi, cv::Mat& blobImg);
// 传统分类,结果写入 m_vecDefectInfo
int TraditionalClassify(const cv::Mat& blobImg);
private:
std::string m_strDirIn;
std::string m_strDirOut;
cv::Mat m_matLoad;
cv::Mat m_matBlob;
cv::Mat m_matBlur;
cv::Mat m_matDraw;
cv::Rect m_rtCrop; // TraditionalDetect 中确定的裁剪区域(原图坐标),供分类结果坐标映射
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);
// 局部背景基准图(大窗口中值),用于分块动态阈值的阈值中心
cv::Mat EstimateBackgroundMap(const cv::Mat& matGray, int blockSize);
void Process(bool bDraw = false);
// 纯分类:对二值图做连通域分析+缺陷分类,结果写入 m_vecDefectInfo
// 面积/个数过滤已上移至 BLobToDetResult 统一处理,此处仅做缺陷类型打标签
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,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);
}

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

@ -8,6 +8,65 @@ std::string toLower(const std::string &str)
return lower;
}
// 4 相机代码, 顺序与原有实现保持一致
static const std::string g_strCameraCodes[] = {"BCA", "DCA", "BTA", "DTA"};
// 解析文件名中的 相机代码 与 半幅(左右)
// 支持两种命名:
// 1) {前缀}_{相机}_{0|1}_{后缀} 如 3ABD670013A8BG1_BCA_0_src (0=左半, 1=右半)
// 2) {前缀}{相机}{A|B} 如 BCAA / A_BCAB (A=左半, B=右半)
// 对应关系: BCA_0 <=> BCAA(左半), BCA_1 <=> BCAB(右半)
std::string Extract_Camera_And_Half(const std::string &strStem, std::string &strHalf)
{
strHalf = "";
// 方式1: {前缀}_{相机}_{0|1}_{后缀} (merg 目录下的命名)
// 形如 3ABD670013A8BG1_BCA_0_src.jpg, 其中 0 对应 BCAA, 1 对应 BCAB
for (const auto &strCam : g_strCameraCodes)
{
const std::string strKey = "_" + strCam + "_";
size_t pos = strStem.find(strKey);
while (pos != std::string::npos)
{
const size_t nHalfPos = pos + strKey.size();
if (nHalfPos < strStem.size() &&
(strStem[nHalfPos] == '0' || strStem[nHalfPos] == '1') &&
(nHalfPos + 1 >= strStem.size() || strStem[nHalfPos + 1] == '_' || strStem[nHalfPos + 1] == '.'))
{
strHalf = (strStem[nHalfPos] == '0') ? "A" : "B";
return strCam;
}
pos = strStem.find(strKey, pos + 1);
}
}
// 方式2: {前缀}{相机}{A|B} (原有的 4 相机命名)
for (const auto &strCam : g_strCameraCodes)
{
const size_t pos = strStem.find(strCam);
if (pos != std::string::npos && pos + strCam.size() < strStem.size())
{
const char cHalf = strStem[pos + strCam.size()];
if (cHalf == 'A' || cHalf == 'B')
{
strHalf = std::string(1, cHalf);
return strCam;
}
}
}
// 兜底: 文件名中只包含相机代码, 无法判定左右
for (const auto &strCam : g_strCameraCodes)
{
if (strStem.find(strCam) != std::string::npos)
{
return strCam;
}
}
return "";
}
int Extract_Name_Base::Read_Product_Image(std::string strPath)
{
@ -189,12 +248,17 @@ int Extract_ALL::Extract_Info(std::string strPath, std::string strproduct, int u
fs::path p = fs::path(strPath);
std::string filenameWithoutExt = p.stem().string();
// 记录半幅信息: 左半(A/BCAA/…_0_) / 右半(B/BCAB/…_1_), 用于配对时确定左右图
std::string strHalf;
Extract_Camera_And_Half(filenameWithoutExt, strHalf);
std::shared_ptr<Image_Info> pImage = std::make_shared<Image_Info>();
pImage->strProductID = strProductName;
pImage->strCamID = strCameraName;
pImage->strchannelName = strChannelName;
pImage->strName = filenameWithoutExt;
pImage->strHalf = strHalf;
pImage->strPath = strPath;
pCamera->image_list.push_back(pImage);
@ -207,15 +271,10 @@ std::string Extract_ALL::Extract_Camera_Name(std::string strPath, int userflag)
fs::path p = fs::path(strPath);
std::string filename = p.stem();
// 文件名包含 CA 或 TA 即判定对应相机
// 支持旧格式: CA_20250508074237.ytimage
// 支持新格式: A_CAA.png, M_CAB.png (A_/M_ 仅是打光方式,同属一个camera)
if (filename.find("TA") != std::string::npos)
return "TA";
else if (filename.find("CA") != std::string::npos)
return "CA";
return "";
// 相机名从统一的命名解析中获取:
// {相机}{A|B} 形式(BCAA/BCAB) 与 {相机}_{0|1} 形式(BCA_0/BCA_1) 均支持
std::string strHalf;
return Extract_Camera_And_Half(filename, strHalf);
}
std::string Extract_ALL::Extract_Product_Name(std::string strPath, int userflag)
@ -247,54 +306,67 @@ std::string Extract_ALL::Extract_Channel_Name(std::string strPath, int userflag)
fs::path p = fs::path(strPath);
std::string filename = p.stem();
// 新格式: {Prefix}_{Type}{AB}, 如 A_CAA, M_TAB
// 新格式: {Prefix}_{Camera}{AB}, 如 A_BCAA, M_DTAB, A_BCAB
// 通道名 = 前缀 (A/M), 表示不同打光方式
// 支持4相机: BCAA, BCAB, BTAA, BTAB, DCAA, DCAB, DTAA, DTAB
// 兼容 {Camera}_{0|1} 命名(如 BCA_0 / BCA_1), 其 0/1 对应 A/B
size_t underscorePos = filename.find('_');
if (underscorePos != std::string::npos)
{
std::string prefix = filename.substr(0, underscorePos);
std::string rest = filename.substr(underscorePos + 1);
if (rest == "CAA" || rest == "CAB" || rest == "TAA" || rest == "TAB")
// 4相机格式
if (rest == "BCAA" || rest == "BCAB" || rest == "BTAA" || rest == "BTAB" ||
rest == "DCAA" || rest == "DCAB" || rest == "DTAA" || rest == "DTAB" ||
rest == "BCA_0" || rest == "BCA_1" || rest == "BTA_0" || rest == "BTA_1" ||
rest == "DCA_0" || rest == "DCA_1" || rest == "DTA_0" || rest == "DTA_1")
{
return prefix; // "A" or "M"
}
}
// 旧格式兼容: CA_xxx.ytimage, TA_xxx.ytimage
if (filename.find("TAA") != std::string::npos)
strchannelName = "TA";
else if (filename.find("TAB") != std::string::npos)
strchannelName = "TA";
else if (filename.find("CAA") != std::string::npos)
strchannelName = "CA";
else if (filename.find("CAB") != std::string::npos)
strchannelName = "CA";
else if (filename.find("CA") != std::string::npos)
strchannelName = "CA";
else if (filename.find("TA") != std::string::npos)
strchannelName = "TA";
// 4相机格式兼容: BCA_xxx, BTA_xxx, DCA_xxx, DTA_xxx
if (filename.find("BCAA") != std::string::npos || filename.find("BCAB") != std::string::npos)
strchannelName = "BCA";
else if (filename.find("BTAA") != std::string::npos || filename.find("BTAB") != std::string::npos)
strchannelName = "BTA";
else if (filename.find("DCAA") != std::string::npos || filename.find("DCAB") != std::string::npos)
strchannelName = "DCA";
else if (filename.find("DTAA") != std::string::npos || filename.find("DTAB") != std::string::npos)
strchannelName = "DTA";
else if (filename.find("BCA") != std::string::npos)
strchannelName = "BCA";
else if (filename.find("BTA") != std::string::npos)
strchannelName = "BTA";
else if (filename.find("DCA") != std::string::npos)
strchannelName = "DCA";
else if (filename.find("DTA") != std::string::npos)
strchannelName = "DTA";
return strchannelName;
}
int Extract_ALL::Read_Image_List(std::string strPath, std::string strproduct)
{
std::string strSearchImgPath = strPath + "/*.png";
Read_Camera_Product_Image(strSearchImgPath, 0, strproduct);
strSearchImgPath = strPath + "/*.ytimage";
std::vector<std::string> imgExts = {"*.jpg", "*.png", "*.bmp", "*.tif"};
for (const auto &ext : imgExts)
{
std::string strSearchImgPath = strPath + "/" + ext;
Read_Camera_Product_Image(strSearchImgPath, 0, strproduct);
}
return 0;
}
int Extract_ALL::Read_Image_List(std::string strPath_left, std::string strPath_right, std::string strproduct)
{
std::string strSearchImgPath = strPath_left + "/*.png";
std::vector<std::string> imgExts = {"*.jpg", "*.png", "*.bmp", "*.tif"};
for (const auto &ext : imgExts)
{
std::string strSearchImgPath = strPath_left + "/" + ext;
Read_Camera_Product_Image(strSearchImgPath, 0, strproduct);
strSearchImgPath = strPath_right + "/*.png";
strSearchImgPath = strPath_right + "/" + ext;
Read_Camera_Product_Image(strSearchImgPath, 0, strproduct);
// strSearchImgPath = strPath + "/*.png";
// Read_Camera_Product_Image(strSearchImgPath, 1);
}
return 0;
}

@ -15,6 +15,20 @@
namespace fs = std::filesystem;
using namespace std;
// ==================== 图片命名解析 ====================
// 相机代码: BCA / BTA / DCA / DTA
// 目前支持两种图片命名方式:
// 1) {前缀}{相机}{A|B} 例: BCAA.jpg / A_BCAB.jpg / M_DTAB.jpg
// 末位 A = 左半, B = 右半
// 2) {前缀}_{相机}_{0|1}_{后缀} 例: 3ABD670013A8BG1_BCA_0_src.jpg
// 0 = 左半, 1 = 右半
// 两种命名的对应关系: BCA_0 等价于 BCAA, BCA_1 等价于 BCAB; BTA/DCA/DTA 同理。
//
// strStem : 去掉目录与扩展名的文件名
// strHalf : 输出半幅信息, "A"=左半, "B"=右半, 无法判定时为空
// 返回值 : 相机代码, 未识别到相机时返回空字符串
std::string Extract_Camera_And_Half(const std::string &strStem, std::string &strHalf);
// 图片信息
struct Image_Info
{
@ -23,6 +37,7 @@ struct Image_Info
std::string strchannelName; // 通道名称
std::string strPath; // 完整路径
std::string strName; // 图片名称
std::string strHalf; // 半幅: "A"=左半(BCAA/…_0_), "B"=右半(BCAB/…_1_)
cv::Mat img; // 图像数据
long readImg_start; // 读取开始时间
long readImg_end; // 读取结束时间
@ -41,6 +56,7 @@ struct Image_Info
strchannelName = "";
strPath = "";
strName = "";
strHalf = "";
img.release(); // 释放图像内存
readImg_start = 0;
readImg_end = 0;
@ -53,6 +69,7 @@ struct Image_Info
std::cout << "Channel Name: " << strchannelName << std::endl;
std::cout << "Image Path: " << strPath << std::endl;
std::cout << "Image Name: " << strName << std::endl;
std::cout << "Image Half: " << strHalf << std::endl;
std::cout << "Read Start Time: " << readImg_start << std::endl;
std::cout << "Read End Time: " << readImg_end << std::endl;
}

@ -60,7 +60,7 @@ enum CHECKIMGTRUNTYP_
// url长度
#define MAX_ULR_LEN 256
#define MAX_GPU_NUM 2
#define MAX_GPU_NUM 4
// ncnn 行人检测 跟踪 检测最大 线程个数
#define MAX_PERSONTRACKER_THREAD_NUM 4
@ -105,8 +105,10 @@ enum START_SYSTEM_STEP_
};
struct SystemConfigParam
{
std::string Analysis_Config_path;
std::string Analysis_Config_path_Cam2;
std::string Analysis_Config_path; // Cam1: BCA
std::string Analysis_Config_path_Cam2; // Cam2: BTA
std::string Analysis_Config_path_Cam3; // Cam3: DCA
std::string Analysis_Config_path_Cam4; // Cam4: DTA
std::string Check_Config_path;
std::string config_Root_Path;
@ -125,6 +127,9 @@ struct SystemConfigParam
bool valid()
{
if (Analysis_Config_path.size() &&
Analysis_Config_path_Cam2.size() &&
Analysis_Config_path_Cam3.size() &&
Analysis_Config_path_Cam4.size() &&
Check_Config_path.size() &&
preCheckImg_Path.size() &&
config_Root_Path.size() &&

@ -5,6 +5,7 @@
#include <sys/stat.h>
#include <unistd.h>
#include <string>
#include <filesystem>
#include "CheckUtil.hpp"
std::string ExtractFileNameWithoutExtension(const std::string &strImgPath)
{
@ -309,6 +310,112 @@ int deal::InitConfig()
return 0;
}
// 测试用: 仅加载配置, 调用 GetUpdateParamOut 把各相机的参数导出到 txt
// 不启动检测线程, 不加载模型
int deal::DumpUpdateParamOut(const std::string &strOutPath)
{
// 1、读取系统参数(含配置根目录 Config_Root_Path)
if (!ReadSystemConfig(FILE_SYSTEM_RUN_CONFIG))
{
printf("[outparam] ReadSystemConfig error\n");
return 1;
}
// 2、初始化参数模块并加载 param_*.json
InitConfig();
int re = LoadAnalysisConfig();
if (re != 0)
{
printf("[outparam] LoadAnalysisConfig error %d, Config_Root_Path = %s\n",
re, m_system_param.config_Root_Path.c_str());
return 2;
}
// 3、走“更新参数”流程(内部会按相机填充回传参数)
m_pALLImgCheckAnalysisy = ALLImgCheckBase::GetInstance();
if (m_pALLImgCheckAnalysisy == NULL || m_pConfigManager == NULL)
{
printf("[outparam] GetInstance error\n");
return 3;
}
re = m_pALLImgCheckAnalysisy->UpdateConfig((void *)m_pConfigManager, CHECK_CONFIG_Module);
if (re != 0)
{
printf("[outparam] UpdateConfig Module error %d\n", re);
return 4;
}
// 4、逐相机调用 GetUpdateParamOut 并写入 txt
std::ofstream ofs(strOutPath);
if (!ofs.is_open())
{
printf("[outparam] open out file fail: %s\n", strOutPath.c_str());
return 5;
}
ofs << "============ UpdateParamOut (cameraCode -> param) ============" << std::endl;
ofs << "Config_Root_Path : " << m_system_param.config_Root_Path << std::endl;
ofs << "camera num : " << m_pConfigManager->Config_instances_.size() << std::endl;
ofs << std::endl;
// 输出一行缺陷参数
auto writeFlaw = [&ofs](const std::string &strName, const flaw_info &info)
{
char line[128];
sprintf(line, " %-12s area %-14.6f gray_dis %.6f", strName.c_str(), info.area, info.gray_dis);
ofs << line << std::endl;
};
int nSucc = 0;
for (const auto &config : m_pConfigManager->Config_instances_)
{
const std::string strCamera = config.first;
UpdateParamOutST paramOut;
int rGet = m_pALLImgCheckAnalysisy->GetUpdateParamOut(strCamera, paramOut);
ofs << "----------------- camera: " << strCamera
<< " GetUpdateParamOut = " << rGet << " -----------------" << std::endl;
if (rGet != 0)
{
ofs << " GetUpdateParamOut Fail " << rGet << std::endl
<< std::endl;
printf("[outparam] cam %-8s GetUpdateParamOut Fail %d\n", strCamera.c_str(), rGet);
continue;
}
{
char line[128];
sprintf(line, " pixel_precision_x %.6f pixel_precision_y %.6f",
paramOut.pixel_precision_x, paramOut.pixel_precision_y);
ofs << line << std::endl;
}
ofs << " qx area / gray_dis" << std::endl;
writeFlaw("aotudian", paramOut.aotudian);
writeFlaw("zangwu", paramOut.zangwu);
writeFlaw("line", paramOut.line);
writeFlaw("dianzhuang", paramOut.dianzhuang);
writeFlaw("xianwei", paramOut.xianwei);
writeFlaw("posun", paramOut.posun);
ofs << std::endl;
printf("[outparam] cam %-8s ok pixel %.6f/%.6f aotudian(%.6f,%.6f) zangwu(%.6f,%.6f) line(%.6f,%.6f)\n",
strCamera.c_str(), paramOut.pixel_precision_x, paramOut.pixel_precision_y,
paramOut.aotudian.area, paramOut.aotudian.gray_dis,
paramOut.zangwu.area, paramOut.zangwu.gray_dis,
paramOut.line.area, paramOut.line.gray_dis);
nSucc++;
}
ofs << "============ end, camera ok num " << nSucc << " ============" << std::endl;
ofs.close();
printf("[outparam] dump done, cam ok %d / %zu, file: %s\n",
nSucc, m_pConfigManager->Config_instances_.size(), strOutPath.c_str());
return nSucc > 0 ? 0 : 6;
}
int deal::InitExtractImg()
{
m_pextractImg = ExtractBase::GetInstance();
@ -449,19 +556,26 @@ int deal::preCheck()
for (const auto pcam : product->camera_list)
{
auto& imgList = pcam->image_list;
// 同一camera内按配对处理:每两张图一组 (CAA+CAB / TAA+TAB)
// cv::glob 返回字母序,确保 A_CAA 在 A_CAB 前,M_CAA 在 M_CAB 前
// 同一camera内按配对处理:每两张图一组 (左半 + 右半)
// 支持 {相机}{A|B}(BCAA/BCAB) 与 {相机}_{0|1}(BCA_0/BCA_1, merg 目录) 两种命名
// 左半(A/…_0_)为左图, 右半(B/…_1_)为右图; 若读入顺序相反则交换
for (size_t i = 0; i < imgList.size(); i += 2)
{
std::shared_ptr<Image_Info> pLeft = imgList[i];
std::shared_ptr<Image_Info> pRight = (i + 1 < imgList.size()) ? imgList[i + 1] : nullptr;
if (pRight && pLeft->strHalf == "B" && pRight->strHalf == "A")
{
std::swap(pLeft, pRight);
}
std::shared_ptr<JC_IMAGE_INFO_> tem = std::make_shared<JC_IMAGE_INFO_>();
tem->strCamID = pcam->strCamName;
tem->strchannelName = imgList[i]->strchannelName;
tem->strName = imgList[i]->strName;
tem->strProductID = imgList[i]->strProductID;
tem->strPath = imgList[i]->strPath;
if (i + 1 < imgList.size())
tem->strchannelName = pLeft->strchannelName;
tem->strName = pLeft->strName;
tem->strProductID = pLeft->strProductID;
tem->strPath = pLeft->strPath;
if (pRight)
{
tem->strPath_B = imgList[i + 1]->strPath;
tem->strPath_B = pRight->strPath;
}
InsertReadImgInfo(tem);
AllImgNum++;
@ -1083,8 +1197,6 @@ bool deal::ReadSystemConfig(const std::string &strPath)
}
m_system_param.Use_CPU_StartIdx = root["Use_CPU_StartIdx"].asInt();
// path
m_system_param.Analysis_Config_path = root["Analysis_Config_path"].asString();
m_system_param.Analysis_Config_path_Cam2 = root["Analysis_Config_path_Cam2"].asString();
m_system_param.config_Root_Path = root["Config_Root_Path"].asString();
m_system_param.Check_Config_path = root["Check_Config_path"].asString();
@ -1093,8 +1205,83 @@ bool deal::ReadSystemConfig(const std::string &strPath)
m_system_param.preCHeck_defect = root["preCHeck_defect"].asInt();
m_nCurUseCPUIDX = m_system_param.Use_CPU_StartIdx;
// 从 Config_Root_Path 目录下自动扫描匹配 param_*.json 文件,映射到各相机
ScanConfigPaths();
return m_system_param.valid();
}
int deal::ScanConfigPaths()
{
std::string configRoot = m_system_param.config_Root_Path;
if (configRoot.empty())
{
printf("ScanConfigPaths: Config_Root_Path is empty\n");
return -1;
}
// 确保路径以 / 结尾
if (configRoot.back() != '/')
{
configRoot += '/';
}
// 相机名称与对应存储指针的映射: Cam1=BCA, Cam2=BTA, Cam3=DCA, Cam4=DTA
struct CamPathMapping
{
std::string camName;
std::string *targetPath;
};
CamPathMapping mappings[] = {
{"BCA", &m_system_param.Analysis_Config_path},
{"BTA", &m_system_param.Analysis_Config_path_Cam2},
{"DCA", &m_system_param.Analysis_Config_path_Cam3},
{"DTA", &m_system_param.Analysis_Config_path_Cam4},
};
// 先清空所有路径
for (auto &m : mappings)
{
*m.targetPath = "";
}
if (!std::filesystem::exists(configRoot))
{
printf("ScanConfigPaths: directory does not exist: %s\n", configRoot.c_str());
return -1;
}
int foundCount = 0;
for (const auto &entry : std::filesystem::directory_iterator(configRoot))
{
if (!entry.is_regular_file())
continue;
std::string filename = entry.path().filename().string();
// 匹配 param_XXX.json 格式
if (filename.size() < 11) // "param_X.json" 最少 11 字符
continue;
if (filename.substr(0, 6) != "param_" || filename.substr(filename.size() - 5) != ".json")
continue;
// 提取相机名称: param_BCA.json → BCA
std::string camName = filename.substr(6, filename.size() - 11);
for (auto &m : mappings)
{
if (camName == m.camName)
{
*m.targetPath = entry.path().string();
printf("ScanConfigPaths: auto-discovered %s config → %s\n",
m.camName.c_str(), m.targetPath->c_str());
foundCount++;
break;
}
}
}
printf("ScanConfigPaths: found %d param config file(s) in %s\n", foundCount, configRoot.c_str());
return foundCount > 0 ? 0 : -1;
}
int deal::GetJcImageInfo(std::string strpath, std::vector<JC_IMAGE_INFO_> &jcImageInfoList)
{
LoadOfflineCheckImg(strpath);
@ -1117,7 +1304,7 @@ int deal::GetDetImageInfo(std::string strProductID, std::string strSearchImg, st
std::string strs1 = strSearchImg + "/*.ytimage";
if(!runConfig.bdecode)
{
strs1 = strSearchImg + "/*.png";
strs1 = strSearchImg + "/*.jpg";
cv::glob(strs1, img_paths, true);
}
cv::glob(strs1, img_paths, true);
@ -1160,22 +1347,15 @@ int deal::GetImgInfo_POL_ET(std::string strImgPath, JC_IMAGE_INFO_ *pImageInfo)
strName = ExtractFileNameWithoutExtension(strImgPath);
printf("strName %s \n", strName.c_str());
std::string strImageChannel;
// 使用 find 检查是否包含 "CA" 或 "TA"
if (strName.find("CA") != std::string::npos || strName.find("CF") != std::string::npos)
{
strImageChannel = "CA";
}
else if (strName.find("TA") != std::string::npos || strName.find("TFT") != std::string::npos)
{
strImageChannel = "TA";
}
else
// 统一命名解析: 同时支持 {相机}{A|B}(BCAA/BCAB) 与 {相机}_{0|1}(BCA_0/BCA_1) 命名
// 对应关系: BCA_0 <=> BCAA, BCA_1 <=> BCAB; BTA/DCA/DTA 同理
std::string strHalf;
std::string strImageChannel = Extract_Camera_And_Half(strName, strHalf);
if (strImageChannel.empty())
{
return 1;
}
printf("strImageChannel %s\n", strImageChannel.c_str());
printf("strImageChannel %s strHalf %s\n", strImageChannel.c_str(), strHalf.c_str());
pImageInfo->strchannelName = strImageChannel;
pImageInfo->strName = strName;
pImageInfo->strPath = strImgPath;
@ -1582,13 +1762,21 @@ int deal::ReJson_product(std::vector<Json_Det_Path> &jsonList, std::string strPr
std::cout << str1 << std::endl;
std::cout << str2 << std::endl;
if (str1.find("CA") != std::string::npos)
if (str1.find("BCA") != std::string::npos)
{
str2 = "BCA";
}
else if (str1.find("BTA") != std::string::npos)
{
str2 = "BTA";
}
else if (str1.find("DCA") != std::string::npos)
{
str2 = "CA";
str2 = "DCA";
}
else if (str1.find("TA") != std::string::npos)
else if (str1.find("DTA") != std::string::npos)
{
str2 = "TA";
str2 = "DTA";
}
else
{
@ -2099,25 +2287,10 @@ int deal::LoadImgPath_JCSN(std::string strImgPath)
{
std::string path = img_paths[i];
// std::cout << img_paths[i] << std::endl;
// 提取所有文件夹名称
std::vector<std::string> allDirectories = extractAllDirectories(path);
// 解析出 SN 及一套图目录(保留路径的相对/绝对属性)
std::string strSN = "";
// 输出所有文件夹名称
std::string strAllPath = "/";
for (const auto &dir : allDirectories)
{
strAllPath += dir;
if (isValidString(dir))
{
strSN = dir;
// std::cout << strSN << std::endl;
break;
}
strAllPath += "/";
}
std::string strAllPath = "";
GetProductDirFromImgPath(path, strSN, strAllPath);
bool bh = false;
for (int j = 0; j < m_OffImageSNPathList.size(); j++)
@ -2163,6 +2336,51 @@ bool deal::isValidString(const std::string &str)
// 字符串满足条件
return true;
}
// 从单张图片路径解析出 SN 及 SN 所在目录(即一套图的目录)
// 说明: 该目录作为后续 glob 的根路径, 必须保留输入路径的相对/绝对属性。
// 原先用 "/" 作为前缀拼接目录名, 输入相对路径时会得到 "/../data/..." 这类非法路径, 导致 glob 抛异常。
// 返回: true 表示找到符合 SN 规则的目录
bool deal::GetProductDirFromImgPath(const std::string &strImgPath, std::string &strSN, std::string &strProductDir)
{
strSN = "";
strProductDir = "";
fs::path pathImg(strImgPath);
if (pathImg.empty())
{
strProductDir = ".";
return false;
}
// 逐级向下拼接目录分量, 取第一个符合 SN 规则的目录作为一套图的目录
// 首个分量可能是根 "/" 或相对路径的起点(如 ".."), 需要原样保留
fs::path pathDir;
bool bHasDir = false;
for (const auto &part : pathImg.parent_path())
{
if (!bHasDir)
{
pathDir = part;
bHasDir = true;
}
else
{
pathDir /= part;
}
if (isValidString(part.string()))
{
strSN = part.string();
strProductDir = pathDir.string();
return true;
}
}
// 没有找到 SN 规则的目录(如路径中不含 SN), 退化为图片所在目录
strProductDir = bHasDir ? pathDir.string() : ".";
return false;
}
int deal::LoadProductID(std::string strImgPath)
{
m_product_ID_List.erase(m_product_ID_List.begin(), m_product_ID_List.end());
@ -2182,23 +2400,10 @@ int deal::LoadProductID(std::string strImgPath)
{
std::string path = img_paths[i];
// 提取所有文件夹名称
std::vector<std::string> allDirectories = extractAllDirectories(path);
// 解析出 SN 及一套图目录(保留路径的相对/绝对属性, 避免拼成 /../data/... 这类非法路径)
std::string strSN = "";
// 输出所有文件夹名称
std::string strAllPath = "/";
for (const auto &dir : allDirectories)
{
// std::cout << dir << std::endl;
strAllPath += dir;
if (isValidString(dir))
{
strSN = dir;
break;
}
strAllPath += "/";
}
std::string strAllPath = "";
GetProductDirFromImgPath(path, strSN, strAllPath);
bool bh = false;
for (int j = 0; j < m_product_ID_List.size(); j++)
@ -2286,11 +2491,42 @@ int deal::Det_OneProduct_Cell_ET(std::string strProductName, int Idx)
int AllImgNum = detImgInfoList.size();
// 遍历所有图片 开始读图处理
for (int i = 0; i < detImgInfoList.size(); i++)
// 按相机分组,同相机内每两张配对 (A半+ B半 拼接送检,与 preCheck 一致)
// 注: 除旧的 BCAA/BCAB 命名外,同时支持 merg 目录下的 xxx_BCA_0_src / xxx_BCA_1_src 命名
std::map<std::string, std::vector<int>> camGroups;
for (int i = 0; i < (int)detImgInfoList.size(); i++)
{
camGroups[detImgInfoList[i].strCamID].push_back(i);
}
AllImgNum = 0;
for (auto& kv : camGroups)
{
auto& indices = kv.second;
for (size_t i = 0; i < indices.size(); i += 2)
{
int nLeftIdx = indices[i];
int nRightIdx = (i + 1 < indices.size()) ? indices[i + 1] : -1;
// 左右图由文件名决定: A半(BCAA/…_0_)为左图, B半(BCAB/…_1_)为右图; 顺序相反时交换
if (nRightIdx >= 0)
{
std::string strHalfL;
std::string strHalfR;
Extract_Camera_And_Half(detImgInfoList.at(nLeftIdx).strName, strHalfL);
Extract_Camera_And_Half(detImgInfoList.at(nRightIdx).strName, strHalfR);
if (strHalfL == "B" && strHalfR == "A")
{
std::swap(nLeftIdx, nRightIdx);
}
}
std::shared_ptr<JC_IMAGE_INFO_> tem = std::make_shared<JC_IMAGE_INFO_>();
tem->copy(detImgInfoList.at(i));
tem->copy(detImgInfoList.at(nLeftIdx));
if (nRightIdx >= 0)
{
tem->strPath_B = detImgInfoList.at(nRightIdx).strPath;
}
InsertReadImgInfo(tem);
AllImgNum++;
}
}
m_DetResult.startTime_S = getcurTime();
IN_IMG_Status_ temstatus = IN_IMG_Status_Start;

@ -517,6 +517,9 @@ public:
~deal();
int start();
// 测试用: 仅加载配置, 调用 GetUpdateParamOut 把各相机的参数导出到 txt
int DumpUpdateParamOut(const std::string &strOutPath);
private:
// 初始化
int Init();
@ -572,12 +575,18 @@ private:
int LoadImgPath_JCSN(std::string strImgPath);
bool isValidString(const std::string &str);
// 从图片路径解析出 SN 及其所在目录(一套图的目录)
// 注意: 返回的目录必须保留输入路径的相对/绝对属性, 不能直接拼根目录 "/"
bool GetProductDirFromImgPath(const std::string &strImgPath, std::string &strSN, std::string &strProductDir);
int LoadProductID(std::string strImgPath);
int set_cpu_id(const std::vector<int> &cpu_set_vec);
void GetDealResultToQueu();
// 加载系统配置文件
bool ReadSystemConfig(const std::string &strPath);
// 从 Config_Root_Path 自动扫描匹配 param_*.json 文件
int ScanConfigPaths();
int GetJcImageInfo(std::string strpath, std::vector<JC_IMAGE_INFO_> &jcImageInfoList);
int ReadTestImgaData();

@ -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;
}

@ -92,6 +92,7 @@ int main(int argc, char *argv[])
cout << "*** 6、./test_JBL_Check -rjson filepath: 单张复测,filepath xx/xx/3A3K380001B1DK_20240408_152157_Main_0_2_L255" << endl;
cout << "*** 6、./test_JBL_Check -rjsonall filepath: 一套图复测,filepath xx/xx/" << endl;
cout << "*** 7、./test_CellAOI -decode [dir]: 批量解码 .ytimage → .png 到 /home/aidlux/BOE/CELL_AOI/decode_img" << endl;
cout << "*** 8、./test_CellAOI -outparam [txt]: 导出各相机更新参数后的参数值到 txt,默认 ./UpdateParamOut.txt" << endl;
cout << "******************************************************" << endl;
return 0;
@ -106,6 +107,9 @@ int main(int argc, char *argv[])
bool bDecodeMode = false;
std::string decodeInputDir;
bool bOutParamMode = false;
std::string outParamTxt = "./UpdateParamOut.txt";
if (argc > 1 && string(argv[1]) != "-h")
{
@ -158,6 +162,15 @@ int main(int argc, char *argv[])
{
test.runConfig.det_Type = RUNTYPE_RUN_Merge_Img;
}
else if (string(argv[i]) == "-outparam")
{
bOutParamMode = true;
if (i + 1 < argc && argv[i + 1][0] != '-')
{
outParamTxt = string(argv[i + 1]);
i++;
}
}
if (string(argv[i]) == "-s")
{
test.runConfig.bSaveProcessImg = true;
@ -310,6 +323,18 @@ int main(int argc, char *argv[])
return (successCount > 0) ? 0 : -1;
}
// ====== 参数导出模式 (-outparam) ======
if (bOutParamMode)
{
printf("\n========== 参数导出模式 (-outparam) ==========\n");
printf(" 输出文件: %s\n", outParamTxt.c_str());
printf("==============================================\n\n");
int re = test.DumpUpdateParamOut(outParamTxt);
printf("[outparam] DumpUpdateParamOut return %d\n", re);
return re;
}
test.runConfig.print("config");
// getchar();
signal(SIGINT, handler);

Loading…
Cancel
Save