#include #include #include #include #include #include #include #include #include #include #include enum imgSaveThreadType { oriImg, resImg, segImg, clsImg }; class ImageSavePathFormatter { public: QString rootDir = ""; QString genPath(std::shared_ptr data, imgSaveThreadType type, QString customPath = "", std::vector>customParams = std::vector>()){ 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 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 data, imgSaveThreadType type, int index = 0, QString customFileName = "", std::vector> customParams = std::vector>(), 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 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 genFileNames(std::shared_ptr data, imgSaveThreadType type, std::vector& imgs) { std::vector 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 genClsFileNames(std::shared_ptr data, imgSaveThreadType type, std::vector>& imgs, QString customFileName = "") { std::vector 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> imgQueue; }; class imgSaveThread : public QThread { public: enum type { normal, priority }; QMutex mutex; std::vector compressionParams{ cv::IMWRITE_PNG_COMPRESSION ,9 }; QQueue< std::shared_ptr>priorityQueue; QQueue< std::shared_ptr>defaultQueue; int imgRemainCount = 0; void run() { while (true) { QQueue> localQueue; std::shared_ptr 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 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 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 oriJob(new saveJob("[ORI_JOB]" + data->SNcode)); std::shared_ptr 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::vectorfileNames = _ImageSavePathFormatter.genFileNames(data, oriImg, data->oriImgs);//按规则为文件创建命名 packageData(oriJob->imgQueue, oriSavePath.toLocal8Bit().toStdString(), fileNames, data->oriImgs);//打包数据 } //SEG原图 if (data->segOriginImgs.size() != 0 && saveSeg == true) { std::vectorfileNames = _ImageSavePathFormatter.genFileNames(data, segImg, data->segOriginImgs);//按规则为文件创建命名 packageData(resJob->imgQueue, oriSavePath.toLocal8Bit().toStdString(), fileNames, data->segOriginImgs);//打包数据 } //CLS原图 if (data->clsOriginImgs.size() != 0 && saveCls == true) { std::vectorfileNames = _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::vectorfileNames = _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::vectoradditionalSaveNgImgNames = 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>& jobQueue, std::string imgSavaPath, std::vector fileNames, std::vector& 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>& jobQueue, std::string imgSavaPath, std::vector fileNames, std::vector>& 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