384 lines
14 KiB
C++
384 lines
14 KiB
C++
#include <qthread.h>
|
|
#include <opencv.hpp>
|
|
#include <qqueue.h>
|
|
#include <qdatetime.h>
|
|
#include <algorithm>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <cctype>
|
|
#include <detData.h>
|
|
#include <paramSyncManager.h>
|
|
#include <global.h>
|
|
enum imgSaveThreadType {
|
|
oriImg,
|
|
resImg,
|
|
segImg,
|
|
clsImg
|
|
};
|
|
class ImageSavePathFormatter {
|
|
public:
|
|
QString rootDir = "";
|
|
QString genPath(std::shared_ptr<detData> data, imgSaveThreadType type, QString customPath = "", std::vector<std::pair <QString, QString>>customParams = std::vector<std::pair <QString, QString>>()){
|
|
QString pathTemplate = !customPath.isEmpty() ? customPath : defaultPath;
|
|
QString result = pathTemplate;
|
|
|
|
// 替换日期时间占位符
|
|
QDateTime now = QDateTime::currentDateTime();
|
|
result.replace("{yyyy-MM-dd}", now.toString("yyyy-MM-dd"));
|
|
result.replace("{HH}", QString("%1").arg(now.time().hour(), 2, 10, QChar('0')));
|
|
result.replace("{mm}", QString("%1").arg(now.time().minute(), 2, 10, QChar('0')));
|
|
result.replace("{ss}", QString("%1").arg(now.time().second(), 2, 10, QChar('0')));
|
|
|
|
// 替换图片类型占位符
|
|
static const QMap<imgSaveThreadType, QString> typeMap{
|
|
{ oriImg, "原图" },
|
|
{ resImg, "结果图" },
|
|
{ segImg, "结果图" },
|
|
{ clsImg, "结果图" }
|
|
};
|
|
result.replace("{imageType}", typeMap.value(type, "Unknown"));
|
|
|
|
// 替换检测结果占位符
|
|
QString processResult;
|
|
if (!data->aiDetOK && !data->halconOK) processResult = "NG";
|
|
else if (!data->aiDetOK) processResult = "aiNG";
|
|
else if (!data->halconOK) processResult = "halconNG";
|
|
else processResult = "OK";
|
|
result.replace("{processResult}", processResult);
|
|
|
|
// 替换SN/设备信息占位符
|
|
QString snCode = (data->SNcode.size() > 0 && data->SNcode.size() < 50)
|
|
? QString::fromStdString(repairWindowsFileName(data->SNcode))
|
|
: QString::fromStdString(repairWindowsFileName(data->cameraKeyName));
|
|
result.replace("{SN}", snCode);
|
|
result.replace("{cameraName}", QString::fromStdString(data->cameraKeyName));
|
|
|
|
// 替换根目录占位符
|
|
result.replace("{imgSaveRootDir}", rootDir.isEmpty() ? "." : rootDir);
|
|
|
|
// 替换根目录占位符
|
|
result.replace("{saveObjectName}", data->saveObjectName.isEmpty() ? "" : data->saveObjectName);
|
|
|
|
// 确保路径以/结尾
|
|
if (!result.endsWith("/") && !result.endsWith("\\")) {
|
|
result += "/";
|
|
}
|
|
for (auto& pair : customParams) {
|
|
result.replace(pair.first, pair.second);
|
|
}
|
|
return result;
|
|
}
|
|
std::string genFileName(std::shared_ptr<detData> data, imgSaveThreadType type, int index = 0, QString customFileName = "", std::vector<std::pair <QString, QString>> customParams = std::vector<std::pair <QString, QString>>(), const std::string& format = "bmp") {
|
|
QString fileTemplate = !customFileName.isEmpty() ? customFileName : defaultFileName;
|
|
QString result = fileTemplate;
|
|
|
|
// 替换SN/设备信息占位符
|
|
QString snCode = (data->SNcode.size() > 0 && data->SNcode.size() < 50)
|
|
? QString::fromStdString(repairWindowsFileName(data->SNcode))
|
|
: QString::fromStdString(repairWindowsFileName(data->cameraKeyName));
|
|
|
|
result.replace("{SN}", snCode);
|
|
result.replace("{cameraName}", QString::fromStdString(data->cameraKeyName));
|
|
|
|
// 替换索引占位符
|
|
result.replace("{index}", QString::number(index));
|
|
|
|
// 替换时间戳占位符
|
|
QDateTime now = QDateTime::currentDateTime();
|
|
result.replace("{yyyy-MM-dd}", now.toString("yyyy-MM-dd"));
|
|
result.replace("{HH-mm-ss}", now.toString("HH-mm-ss"));
|
|
result.replace("{ms}", QString("%1").arg(now.time().msec(), 3, 10, QChar('0')));
|
|
|
|
// 替换图片类型占位符
|
|
static const QMap<imgSaveThreadType, QString> typeMap{
|
|
{ oriImg, "ORI" },
|
|
{ resImg, "RES" },
|
|
{ segImg, "SEG" },
|
|
{ clsImg, "CLS" }
|
|
};
|
|
result.replace("{imageType}", typeMap.value(type, "UNKNOWN"));
|
|
for (auto& pair : customParams) {
|
|
result.replace(pair.first, pair.second);
|
|
}
|
|
|
|
return result.toLocal8Bit().toStdString() + "." + format;
|
|
}
|
|
std::vector<std::string> genFileNames(std::shared_ptr<detData> data, imgSaveThreadType type, std::vector<cv::Mat>& imgs) {
|
|
|
|
std::vector<std::string> result;
|
|
switch (type) {
|
|
case oriImg:
|
|
for (int i = 0; i < imgs.size(); i++) {
|
|
if (i < data->oriFormat.size())
|
|
result.push_back(genFileName(data, type, i, data->customFileName, data->customImageSaveTemplateParams, data->oriFormat[i]));
|
|
else
|
|
result.push_back(genFileName(data, type, i, data->customFileName, data->customImageSaveTemplateParams));
|
|
}
|
|
break;
|
|
|
|
case resImg:
|
|
for (int i = 0; i < imgs.size(); i++) {
|
|
result.push_back(genFileName(data, type, i, data->customFileName, data->customImageSaveTemplateParams, "jpg"));
|
|
}
|
|
break;
|
|
|
|
default:
|
|
for (int i = 0; i < imgs.size(); i++) {
|
|
result.push_back(genFileName(data, type, i, data->customFileName, data->customImageSaveTemplateParams));
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
std::vector<std::string> genClsFileNames(std::shared_ptr<detData> data, imgSaveThreadType type, std::vector<std::pair<std::string, cv::Mat>>& imgs, QString customFileName = "") {
|
|
std::vector<std::string> result;
|
|
for (int i = 0; i < imgs.size(); i++) {
|
|
result.push_back(genFileName(data, type, i, defaultFileName, data->customImageSaveTemplateParams));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// 清理Windows文件名/文件夹名中不支持的字符
|
|
std::string repairWindowsFileName(const std::string& input) {
|
|
std::string result = input;
|
|
|
|
// Windows文件名/文件夹名不能包含以下字符: \ / : * ? " < > |
|
|
// 以及控制字符(ASCII 0-31)
|
|
std::string invalidChars = "\\/:*?\"<>|";
|
|
|
|
// 移除所有不合法字符
|
|
for (char invalidChar : invalidChars) {
|
|
result.erase(std::remove(result.begin(), result.end(), invalidChar), result.end());
|
|
}
|
|
|
|
// 移除控制字符(ASCII 0-31)
|
|
result.erase(std::remove_if(result.begin(), result.end(),
|
|
[](char c) { return c >= 0 && c <= 31; }), result.end());
|
|
|
|
// 移除首尾空格和点号
|
|
while (!result.empty() && (result.front() == ' ' || result.front() == '.')) {
|
|
result.erase(result.begin());
|
|
}
|
|
while (!result.empty() && (result.back() == ' ' || result.back() == '.')) {
|
|
result.pop_back();
|
|
}
|
|
|
|
// 如果结果为空,提供默认名称
|
|
if (result.empty()) {
|
|
result = "unnamed";
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private:
|
|
QString defaultPath = "{imgSaveRootDir}/{yyyy-MM-dd}/{saveObjectName}{imageType}/{HH}H/{processResult}/{SN}/";
|
|
QString defaultFileName = "{SN}_{cameraName}_{imageType}_{index}_{HH-mm-ss}-{ms}";
|
|
};
|
|
struct saveJob {
|
|
saveJob(std::string jobNameIn) :jobName(jobNameIn) {}
|
|
std::string jobName;
|
|
QQueue<std::pair<std::string, cv::Mat>> imgQueue;
|
|
};
|
|
class imgSaveThread : public QThread {
|
|
|
|
public:
|
|
enum type {
|
|
normal,
|
|
priority
|
|
};
|
|
QMutex mutex;
|
|
std::vector<int> compressionParams{ cv::IMWRITE_PNG_COMPRESSION ,9 };
|
|
QQueue< std::shared_ptr<saveJob>>priorityQueue;
|
|
QQueue< std::shared_ptr<saveJob>>defaultQueue;
|
|
int imgRemainCount = 0;
|
|
void run() {
|
|
|
|
while (true) {
|
|
QQueue<std::pair<std::string, cv::Mat>> localQueue;
|
|
std::shared_ptr<saveJob> job = nullptr;
|
|
// 优先处理 priorityQueue
|
|
{
|
|
QMutexLocker locker(&mutex);
|
|
if (!priorityQueue.isEmpty()) {
|
|
job = priorityQueue.dequeue();
|
|
}
|
|
else if (!defaultQueue.isEmpty()) {
|
|
job = defaultQueue.dequeue();
|
|
}
|
|
}
|
|
|
|
// 没有任务就跳出
|
|
if (job == nullptr) {
|
|
break;
|
|
}
|
|
|
|
qDebug() << "saveJob start" << QString::fromStdString(job->jobName) << "remain" << imgRemainCount;
|
|
imgRemainCount -= job->imgQueue.size();
|
|
localQueue.swap(job->imgQueue);
|
|
// 批量保存
|
|
while (!localQueue.isEmpty()) {
|
|
auto data = std::move(localQueue.dequeue());
|
|
#if saveImgEnable
|
|
cv::imwrite(data.first, data.second);
|
|
#endif
|
|
}
|
|
|
|
qDebug() << "saveJob end " << QString::fromStdString(job->jobName);
|
|
}
|
|
}
|
|
|
|
void addJob(std::shared_ptr<saveJob> job, type type_ = normal) {
|
|
|
|
QMutexLocker locker(&mutex);
|
|
switch (type_) {
|
|
case normal:
|
|
defaultQueue.append(job);
|
|
break;
|
|
case priority:
|
|
priorityQueue.append(job);
|
|
break;
|
|
}
|
|
imgRemainCount += job->imgQueue.size();
|
|
qDebug() << "savejob enqueue" << QString::fromStdString(job->jobName) << "queue size" << priorityQueue.size() << "|" << defaultQueue.size() << " imgRemain" << imgRemainCount;
|
|
this->start();
|
|
}
|
|
|
|
int queueSize() {
|
|
QMutexLocker locker(&mutex);
|
|
int size = defaultQueue.size();
|
|
return size;
|
|
}
|
|
};
|
|
class imgSaveThreadManager {
|
|
public:
|
|
imgSaveThreadManager(std::string pathIn, paramSyncManager* paramSyncManagerIn) {
|
|
_ImageSavePathFormatter.rootDir = QString::fromStdString(pathIn);
|
|
_paramSyncManager = paramSyncManagerIn;
|
|
saveOri = _paramSyncManager->get(_paramSyncManager->getGroup(0), "存储原图", true).toBool();
|
|
saveRes = _paramSyncManager->get(_paramSyncManager->getGroup(0), "存储结果图", true).toBool();
|
|
saveSeg = _paramSyncManager->get(_paramSyncManager->getGroup(0), "存储Seg图", true).toBool();
|
|
saveCls = _paramSyncManager->get(_paramSyncManager->getGroup(0), "存储Cls图", true).toBool();
|
|
}
|
|
~imgSaveThreadManager() {};
|
|
imgSaveThread saveThread;
|
|
void saveImage(std::shared_ptr <detData> data) {
|
|
if (getParamTimer.elapsed() > 5000) {
|
|
saveOri = _paramSyncManager->get(_paramSyncManager->getGroup(0), "存储原图", true).toBool();
|
|
saveRes = _paramSyncManager->get(_paramSyncManager->getGroup(0), "存储结果图", true).toBool();
|
|
saveSeg = _paramSyncManager->get(_paramSyncManager->getGroup(0), "存储Seg图", true).toBool();
|
|
saveCls = _paramSyncManager->get(_paramSyncManager->getGroup(0), "存储Cls图", true).toBool();
|
|
getParamTimer.restart();
|
|
}
|
|
|
|
std::shared_ptr<saveJob> oriJob(new saveJob("[ORI_JOB]" + data->SNcode));
|
|
std::shared_ptr<saveJob> resJob(new saveJob("[RES_JOB]" + data->SNcode));
|
|
|
|
QString oriSavePath = _ImageSavePathFormatter.genPath(data, oriImg, data->customPath, data->customImageSaveTemplateParams);//获取文件夹名
|
|
if (saveOri == true || saveSeg == true || saveCls == true) {
|
|
createDirectoryIfNotExists(oriSavePath);//不存在此文件夹则创建
|
|
if (data->oriImgs.size() != 0 && saveOri == true) {
|
|
std::vector<std::string>fileNames = _ImageSavePathFormatter.genFileNames(data, oriImg, data->oriImgs);//按规则为文件创建命名
|
|
packageData(oriJob->imgQueue, oriSavePath.toLocal8Bit().toStdString(), fileNames, data->oriImgs);//打包数据
|
|
}
|
|
//SEG原图
|
|
if (data->segOriginImgs.size() != 0 && saveSeg == true) {
|
|
std::vector<std::string>fileNames = _ImageSavePathFormatter.genFileNames(data, segImg, data->segOriginImgs);//按规则为文件创建命名
|
|
packageData(resJob->imgQueue, oriSavePath.toLocal8Bit().toStdString(), fileNames, data->segOriginImgs);//打包数据
|
|
}
|
|
//CLS原图
|
|
if (data->clsOriginImgs.size() != 0 && saveCls == true) {
|
|
std::vector<std::string>fileNames = _ImageSavePathFormatter.genClsFileNames(data, clsImg, data->clsOriginImgs);//按规则为文件创建命名
|
|
packageData(resJob->imgQueue, oriSavePath.toLocal8Bit().toStdString(), fileNames, data->clsOriginImgs);//打包数据
|
|
}
|
|
}
|
|
|
|
|
|
QString resSavePath = _ImageSavePathFormatter.genPath(data, resImg, data->customPath, data->customImageSaveTemplateParams);//获取文件夹名
|
|
if (saveRes == true){
|
|
createDirectoryIfNotExists(resSavePath);//不存在此文件夹则创建
|
|
//结果图保存
|
|
if (data->resultImgs.size() != 0) {
|
|
std::vector<std::string>fileNames = _ImageSavePathFormatter.genFileNames(data, resImg, data->resultImgs);//按规则为文件创建命名
|
|
packageData(resJob->imgQueue, resSavePath.toLocal8Bit().toStdString(), fileNames, data->resultImgs);//打包数据
|
|
}
|
|
}
|
|
|
|
if (oriJob->imgQueue.size() != 0)
|
|
saveThread.addJob(oriJob);
|
|
if (resJob->imgQueue.size() != 0)
|
|
saveThread.addJob(resJob, imgSaveThread::priority);
|
|
//额外储存NG文件夹
|
|
//std::string additionalSaveNgImgPath = genDirName(data, additionalSaveNgImg);
|
|
//std::vector<std::string>additionalSaveNgImgNames = genFileNames(data, additionalSaveNgImg);
|
|
//if (additionalSaveNgImgNames.size() > 0)
|
|
// createDirectoryIfNotExists(additionalSaveNgImgPath);//不存在此文件夹则创建
|
|
//int additionalSaveNgImgNamesIndex = 0;
|
|
//for (int index = 0; index < data->segOriginImgs.size()&& additionalSaveNgImgNames.size() > additionalSaveNgImgNamesIndex; index++)
|
|
//{
|
|
// saveThread.addData(additionalSaveNgImgPath + additionalSaveNgImgNames[additionalSaveNgImgNamesIndex], data->segOriginImgs[index]);
|
|
// additionalSaveNgImgNamesIndex++;
|
|
//}
|
|
//for (int index = 0; index < data->oriImgs.size() && additionalSaveNgImgNames.size() > additionalSaveNgImgNamesIndex; index++)
|
|
//{
|
|
// saveThread.addData(additionalSaveNgImgPath + additionalSaveNgImgNames[additionalSaveNgImgNamesIndex], data->oriImgs[index]);
|
|
// additionalSaveNgImgNamesIndex++;
|
|
//}
|
|
//for (int index = 0; index < data->resultImgs.size() && additionalSaveNgImgNames.size() > additionalSaveNgImgNamesIndex; index++)
|
|
//{
|
|
// saveThread.addData(additionalSaveNgImgPath + additionalSaveNgImgNames[additionalSaveNgImgNamesIndex], data->resultImgs[index]);
|
|
// additionalSaveNgImgNamesIndex++;
|
|
//}
|
|
}
|
|
private:
|
|
bool saveOri = true, saveRes = true, saveSeg = true, saveCls = true;
|
|
QElapsedTimer getParamTimer;//防止加锁次数过多导致卡顿
|
|
paramSyncManager* _paramSyncManager;
|
|
ImageSavePathFormatter _ImageSavePathFormatter;
|
|
void packageData(QQueue<std::pair<std::string, cv::Mat>>& jobQueue, std::string imgSavaPath, std::vector<std::string> fileNames, std::vector<cv::Mat>& mats) {
|
|
if (fileNames.size() != mats.size())
|
|
qWarning() << "检测到存图文件名数量与图片数量不符";
|
|
|
|
for (int index = 0; index < fileNames.size() && index < mats.size(); index++) {
|
|
try {
|
|
|
|
if (mats[index].data == nullptr) {
|
|
qDebug() << "在存图线程中发现空指针";
|
|
continue;
|
|
}
|
|
}
|
|
catch (...) {
|
|
qDebug() << "在存图线程中发现无效图像";
|
|
continue;
|
|
}
|
|
|
|
cv::Mat img;
|
|
mats[index].copyTo(img);
|
|
jobQueue.enqueue(std::make_pair(imgSavaPath + fileNames[index], img));
|
|
}
|
|
}
|
|
void packageData(QQueue<std::pair<std::string, cv::Mat>>& jobQueue, std::string imgSavaPath, std::vector<std::string> fileNames, std::vector<std::pair<std::string, cv::Mat>>& mats) {
|
|
if (fileNames.size() != mats.size())
|
|
qWarning() << "检测到存图文件名数量与图片数量不符";
|
|
|
|
for (int index = 0; index < fileNames.size() && index < mats.size(); index++) {
|
|
try {
|
|
|
|
if (mats[index].second.data == nullptr) {
|
|
qDebug() << "在存图线程中发现空指针";
|
|
continue;
|
|
}
|
|
}
|
|
catch (...) {
|
|
qDebug() << "在存图线程中发现无效图像";
|
|
continue;
|
|
}
|
|
|
|
cv::Mat img;
|
|
mats[index].second.copyTo(img);
|
|
jobQueue.enqueue(std::make_pair(imgSavaPath + fileNames[index], img));
|
|
}
|
|
|
|
}
|
|
};
|
|
|
|
#pragma once |