|
|
|
@ -3,6 +3,7 @@
|
|
|
|
#include <sys/time.h>
|
|
|
|
#include <sys/time.h>
|
|
|
|
#include <sys/types.h>
|
|
|
|
#include <sys/types.h>
|
|
|
|
#include <sys/stat.h>
|
|
|
|
#include <sys/stat.h>
|
|
|
|
|
|
|
|
#include <ctime>
|
|
|
|
#include "MemMonitor.h"
|
|
|
|
#include "MemMonitor.h"
|
|
|
|
#include <unistd.h>
|
|
|
|
#include <unistd.h>
|
|
|
|
#include <string>
|
|
|
|
#include <string>
|
|
|
|
@ -1326,13 +1327,320 @@ int deal::DetImg()
|
|
|
|
return 0;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// @brief 监控模式:每 5 分钟执行一次模型推理,并把当前设备资源消耗(CPU 内存 / GPU 显存)
|
|
|
|
|
|
|
|
/// 写入 monitor.log;当某次推理输出 out 图至少一个像素不为全黑时,保存该 out 图并退出监控。
|
|
|
|
|
|
|
|
/// @return 0 表示成功,非 0 表示初始化失败
|
|
|
|
|
|
|
|
int deal::MonitorRun()
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
// 输入目录:优先使用命令行指定路径,否则使用默认路径
|
|
|
|
|
|
|
|
std::string strSrcRoot = runConfig.filePath;
|
|
|
|
|
|
|
|
if (strSrcRoot.empty())
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
strSrcRoot = "/home/aidlux/BOE/FOG/AI_Detect/";
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
else if (strSrcRoot.back() != '/')
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
strSrcRoot += "/";
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
std::shared_ptr<AIFactory> pAIFactory = AIFactory::GetInstance();
|
|
|
|
|
|
|
|
std::shared_ptr<AIModel_Base> pAI_Model = pAIFactory->AI_defect_NF;
|
|
|
|
|
|
|
|
if (!pAI_Model || !pAI_Model->m_bInitSuccess)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
printf("MonitorRun: AI model (AI_defect_NF) not ready!\n");
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const int nModelInCh = pAI_Model->input_0.channel;
|
|
|
|
|
|
|
|
const int nModelInW = pAI_Model->input_0.width;
|
|
|
|
|
|
|
|
const int nModelInH = pAI_Model->input_0.height;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
printf("MonitorRun: model input [c=%d w=%d h=%d] output [c=%d w=%d h=%d]\n",
|
|
|
|
|
|
|
|
nModelInCh, nModelInW, nModelInH,
|
|
|
|
|
|
|
|
pAI_Model->output_0.channel, pAI_Model->output_0.width, pAI_Model->output_0.height);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 收集所有 *_in.png 文件
|
|
|
|
|
|
|
|
std::vector<std::string> inList;
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
if (!fs::exists(strSrcRoot))
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
printf("MonitorRun: src dir not exist: %s\n", strSrcRoot.c_str());
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const auto &entry : fs::recursive_directory_iterator(strSrcRoot))
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
if (!entry.is_regular_file())
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string name = entry.path().filename().string();
|
|
|
|
|
|
|
|
const std::string suffix = "_in.png";
|
|
|
|
|
|
|
|
if (name.size() >= suffix.size() &&
|
|
|
|
|
|
|
|
name.compare(name.size() - suffix.size(), suffix.size(), suffix) == 0)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
inList.push_back(entry.path().string());
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
catch (const std::exception &e)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
printf("MonitorRun: traverse error %s\n", e.what());
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
std::sort(inList.begin(), inList.end());
|
|
|
|
|
|
|
|
printf("MonitorRun: total %zu in images\n", inList.size());
|
|
|
|
|
|
|
|
if (inList.empty())
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
printf("MonitorRun: no *_in.png found\n");
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 预处理成模型输入(只读一次,每轮复用)
|
|
|
|
|
|
|
|
std::vector<cv::Mat> inImgs;
|
|
|
|
|
|
|
|
inImgs.reserve(inList.size());
|
|
|
|
|
|
|
|
for (size_t i = 0; i < inList.size(); i++)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
cv::Mat inImg = cv::imread(inList[i], cv::IMREAD_UNCHANGED);
|
|
|
|
|
|
|
|
if (inImg.empty())
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
printf("MonitorRun: read fail: %s\n", inList[i].c_str());
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (inImg.channels() != nModelInCh)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
cv::Mat tmp;
|
|
|
|
|
|
|
|
if (inImg.channels() >= 3)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
cv::cvtColor(inImg, tmp, cv::COLOR_BGR2GRAY);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
else
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
tmp = inImg.clone();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
inImg = tmp;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (inImg.type() != CV_8U)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
inImg.convertTo(inImg, CV_8U);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (inImg.cols != nModelInW || inImg.rows != nModelInH)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
cv::resize(inImg, inImg, cv::Size(nModelInW, nModelInH));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
inImgs.push_back(inImg);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (inImgs.empty())
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
printf("MonitorRun: no valid in image\n");
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 输出目录:输入目录去掉末尾 '/' 后追加 "_TEST/"(与 TestAIDet 保持一致)
|
|
|
|
|
|
|
|
std::string strDstRoot = strSrcRoot;
|
|
|
|
|
|
|
|
while (strDstRoot.size() > 1 && strDstRoot.back() == '/')
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
strDstRoot.pop_back();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
strDstRoot += "_TEST/";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 本次监控的时间戳输出目录(存图和 log 都放这里)
|
|
|
|
|
|
|
|
time_t startT = time(NULL);
|
|
|
|
|
|
|
|
struct tm stmv;
|
|
|
|
|
|
|
|
localtime_r(&startT, &stmv);
|
|
|
|
|
|
|
|
char dirTs[64] = {0};
|
|
|
|
|
|
|
|
strftime(dirTs, sizeof(dirTs), "monitor_%Y%m%d_%H%M%S", &stmv);
|
|
|
|
|
|
|
|
std::string strOutDir = strDstRoot + dirTs + "/";
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
std::error_code ec;
|
|
|
|
|
|
|
|
fs::create_directories(strOutDir, ec);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 打开监控日志(追加写)
|
|
|
|
|
|
|
|
std::string strLogPath = strOutDir + "monitor.log";
|
|
|
|
|
|
|
|
std::ofstream logFile(strLogPath, std::ios::app);
|
|
|
|
|
|
|
|
if (!logFile)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
printf("MonitorRun: open log fail: %s\n", strLogPath.c_str());
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 同时输出到终端和 log 文件
|
|
|
|
|
|
|
|
auto logLine = [&logFile](const std::string &line)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
printf("%s\n", line.c_str());
|
|
|
|
|
|
|
|
logFile << line << std::endl;
|
|
|
|
|
|
|
|
logFile.flush();
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const int nIntervalSec = 5 * 60; // 5 分钟
|
|
|
|
|
|
|
|
const int nMaxRunSec = 30 * 60; // 30 分钟
|
|
|
|
|
|
|
|
long nRound = 0;
|
|
|
|
|
|
|
|
auto monitorStart = std::chrono::steady_clock::now();
|
|
|
|
|
|
|
|
logLine("===== monitor start (run max 30 min) =====");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化 CPU 采样基准(用于计算 CPU 使用率)
|
|
|
|
|
|
|
|
struct rusage usagePrev;
|
|
|
|
|
|
|
|
getrusage(RUSAGE_SELF, &usagePrev);
|
|
|
|
|
|
|
|
CPUStat cpuStatPrev;
|
|
|
|
|
|
|
|
getSystemCPUStat(cpuStatPrev);
|
|
|
|
|
|
|
|
auto wallPrev = std::chrono::steady_clock::now();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
while (true)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
// 0. 运行 30 分钟后停止
|
|
|
|
|
|
|
|
double elapsedSec = std::chrono::duration<double>(
|
|
|
|
|
|
|
|
std::chrono::steady_clock::now() - monitorStart)
|
|
|
|
|
|
|
|
.count();
|
|
|
|
|
|
|
|
if (elapsedSec >= nMaxRunSec)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
logLine("===== monitor exit (timeout 30 min) =====");
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
nRound++;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 采集设备资源消耗
|
|
|
|
|
|
|
|
long rssKB = getRSS_KB();
|
|
|
|
|
|
|
|
struct rusage usage;
|
|
|
|
|
|
|
|
getrusage(RUSAGE_SELF, &usage);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
long memTotalKB = -1;
|
|
|
|
|
|
|
|
long memFreeKB = -1;
|
|
|
|
|
|
|
|
long memAvailKB = -1;
|
|
|
|
|
|
|
|
getSystemMemInfo(memTotalKB, memFreeKB, memAvailKB);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CPUStat cpuStat;
|
|
|
|
|
|
|
|
getSystemCPUStat(cpuStat);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
long gpuUsedMB = -1;
|
|
|
|
|
|
|
|
long gpuTotalMB = -1;
|
|
|
|
|
|
|
|
getGPUMemInfo(gpuUsedMB, gpuTotalMB);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
auto wallNow = std::chrono::steady_clock::now();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 系统 CPU 使用率(两次采样之间)
|
|
|
|
|
|
|
|
double sysCpuUsage = -1.0;
|
|
|
|
|
|
|
|
unsigned long long totalDelta = cpuStat.total() - cpuStatPrev.total();
|
|
|
|
|
|
|
|
unsigned long long idleDelta = cpuStat.idleTotal() - cpuStatPrev.idleTotal();
|
|
|
|
|
|
|
|
if (totalDelta > 0)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
sysCpuUsage = 100.0 * (1.0 - (double)idleDelta / (double)totalDelta);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 进程 CPU 使用率(getrusage 的 CPU 时间 / 墙钟时间,多核下可能超过 100%)
|
|
|
|
|
|
|
|
double procCpuUsage = -1.0;
|
|
|
|
|
|
|
|
double wallSec = std::chrono::duration<double>(wallNow - wallPrev).count();
|
|
|
|
|
|
|
|
if (wallSec > 0)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
procCpuUsage = 100.0 * (cpuTimeSec(usage) - cpuTimeSec(usagePrev)) / wallSec;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 更新采样基准
|
|
|
|
|
|
|
|
usagePrev = usage;
|
|
|
|
|
|
|
|
cpuStatPrev = cpuStat;
|
|
|
|
|
|
|
|
wallPrev = wallNow;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
time_t now = time(NULL);
|
|
|
|
|
|
|
|
struct tm tmv;
|
|
|
|
|
|
|
|
localtime_r(&now, &tmv);
|
|
|
|
|
|
|
|
char ts[64] = {0};
|
|
|
|
|
|
|
|
strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", &tmv);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
char buf[512];
|
|
|
|
|
|
|
|
snprintf(buf, sizeof(buf),
|
|
|
|
|
|
|
|
"[round %ld] %s | VmRSS: %ld MB | maxRSS: %ld MB | ProcCPU: %.1f%% | SysCPU: %.1f%% | Mem total %ld / free %ld / avail %ld MB | GPU: %ld/%ld MB",
|
|
|
|
|
|
|
|
nRound, ts, rssKB / 1024, usage.ru_maxrss / 1024,
|
|
|
|
|
|
|
|
procCpuUsage, sysCpuUsage,
|
|
|
|
|
|
|
|
memTotalKB / 1024, memFreeKB / 1024, memAvailKB / 1024,
|
|
|
|
|
|
|
|
gpuUsedMB, gpuTotalMB);
|
|
|
|
|
|
|
|
logLine(buf);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 逐图推理,遇到 out 非全黑则保存对应的输入图和输出图
|
|
|
|
|
|
|
|
int nHit = 0;
|
|
|
|
|
|
|
|
for (size_t i = 0; i < inImgs.size(); i++)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
cv::Mat outImg;
|
|
|
|
|
|
|
|
int re = pAI_Model->AIDet(inImgs[i], outImg);
|
|
|
|
|
|
|
|
if (re != 0 || outImg.empty())
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
snprintf(buf, sizeof(buf),
|
|
|
|
|
|
|
|
"[round %ld] infer fail(%d) on image %zu: %s",
|
|
|
|
|
|
|
|
nRound, re, i, inList[i].c_str());
|
|
|
|
|
|
|
|
logLine(buf);
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
int nNonZero = cv::countNonZero(outImg);
|
|
|
|
|
|
|
|
if (nNonZero > 0)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
// 输出图不全黑 -> 保存对应的输入图和输出图
|
|
|
|
|
|
|
|
std::string saveInPath = strOutDir + "monitor_hit_round" +
|
|
|
|
|
|
|
|
std::to_string(nRound) + "_img" +
|
|
|
|
|
|
|
|
std::to_string(i) + "_in.png";
|
|
|
|
|
|
|
|
std::string saveOutPath = strOutDir + "monitor_hit_round" +
|
|
|
|
|
|
|
|
std::to_string(nRound) + "_img" +
|
|
|
|
|
|
|
|
std::to_string(i) + "_out.png";
|
|
|
|
|
|
|
|
cv::imwrite(saveInPath, inImgs[i]);
|
|
|
|
|
|
|
|
cv::imwrite(saveOutPath, outImg);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
snprintf(buf, sizeof(buf),
|
|
|
|
|
|
|
|
"[round %ld] hit! out image %zu non-zero pixels: %d -> save %s / %s",
|
|
|
|
|
|
|
|
nRound, i, nNonZero, saveInPath.c_str(), saveOutPath.c_str());
|
|
|
|
|
|
|
|
logLine(buf);
|
|
|
|
|
|
|
|
nHit++;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
snprintf(buf, sizeof(buf), "[round %ld] %d non-black out saved, sleep %d s ...",
|
|
|
|
|
|
|
|
nRound, nHit, nIntervalSec);
|
|
|
|
|
|
|
|
logLine(buf);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 休眠(不超过剩余运行时间)
|
|
|
|
|
|
|
|
double remainSec = nMaxRunSec - elapsedSec;
|
|
|
|
|
|
|
|
int sleepSec = (int)(remainSec < nIntervalSec ? remainSec : nIntervalSec);
|
|
|
|
|
|
|
|
if (sleepSec > 0)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
std::this_thread::sleep_for(std::chrono::seconds(sleepSec));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logFile.close();
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// @brief AI 检测独立测试:遍历 /home/aidlux/BOE/FOG/AI_Detect/ 下所有 *_in.png,
|
|
|
|
/// @brief AI 检测独立测试:遍历 /home/aidlux/BOE/FOG/AI_Detect/ 下所有 *_in.png,
|
|
|
|
/// 送入 AI_defect_NF 模型推理,将输出 *_out.png 保存到 /home/aidlux/BOE/FOG/AI_Detect_TEST/
|
|
|
|
/// 送入 AI_defect_NF 模型推理,将输出 *_out.png 保存到 /home/aidlux/BOE/FOG/AI_Detect_TEST/
|
|
|
|
/// @return 0 表示成功
|
|
|
|
/// @return 0 表示成功
|
|
|
|
int deal::TestAIDet()
|
|
|
|
int deal::TestAIDet()
|
|
|
|
{
|
|
|
|
{
|
|
|
|
std::string strSrcRoot = "/home/aidlux/BOE/FOG/AI_Detect/";
|
|
|
|
// 输入目录:优先使用命令行指定路径,否则使用默认路径
|
|
|
|
std::string strDstRoot = "/home/aidlux/BOE/FOG/AI_Detect_TEST/";
|
|
|
|
std::string strSrcRoot = runConfig.filePath;
|
|
|
|
|
|
|
|
if (strSrcRoot.empty())
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
strSrcRoot = "/home/aidlux/BOE/FOG/AI_Detect/";
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
else if (strSrcRoot.back() != '/')
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
strSrcRoot += "/";
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 输出目录:输入目录去掉末尾 '/' 后追加 "_TEST/"
|
|
|
|
|
|
|
|
// 默认输入 /home/aidlux/BOE/FOG/AI_Detect/ -> 输出 /home/aidlux/BOE/FOG/AI_Detect_TEST/
|
|
|
|
|
|
|
|
std::string strDstRoot = strSrcRoot;
|
|
|
|
|
|
|
|
while (strDstRoot.size() > 1 && strDstRoot.back() == '/')
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
strDstRoot.pop_back();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
strDstRoot += "_TEST/";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if(runConfig.bmonitor)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
// 监控模式:每 5 分钟执行一次模型推理,记录设备资源消耗到 log;
|
|
|
|
|
|
|
|
// 当某次推理输出 out 图至少一个像素不为全黑时退出监控。
|
|
|
|
|
|
|
|
return MonitorRun();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::shared_ptr<AIFactory> pAIFactory = AIFactory::GetInstance();
|
|
|
|
std::shared_ptr<AIFactory> pAIFactory = AIFactory::GetInstance();
|
|
|
|
std::shared_ptr<AIModel_Base> pAI_Model = pAIFactory->AI_defect_NF;
|
|
|
|
std::shared_ptr<AIModel_Base> pAI_Model = pAIFactory->AI_defect_NF;
|
|
|
|
|