#include "SPAServer.h" #include "spdlog/spdlog.h" // #include "CurlHttp.h" #include // #include "CurlFtp.h" #include "ThreadPool/ThreadPool.h" #include #include SPAServer::SPAServer() { m_logger = spdlog::get("SPAServer"); if(m_logger == nullptr) { SPDLOG_ERROR("APAServer logger is nullptr"); return; } /* 读取全局的配置文件 */ QString strConfigFile = QApplication::applicationDirPath() + "/config.ini"; if(!g_config.readConfig(strConfigFile)) { /* 读取配置文件失败,直接退出程序 */ return; } g_config.printValue(); m_threadRunning = true; /* 初始化WebAPI */ m_toEQMDataBase.initWebApi("http://192.1.3.133:31000/v6/", "", "4c2f9fc91c22dd98331e47af2e2964f4"); /* 模拟违禁品算法ID,后续需要动态调整 */ m_keyContraband = "OD210_026_005246_001-IZRTKyEx"; } SPAServer::~SPAServer() { } /* 启动服务 */ void SPAServer::startServer() { /* 添加获取基础信息的线程 */ // CPPTP.add_task(&SPAServer::threadFromSuperBrain, this); /* 测试Redis读取并解析数据线程 */ CameraThreadInfo info; info.RedisIP = "172.16.36.80"; info.RedisPort = 32222; info.RedisPWD = "Ff1z@TOFr^iwd%Ra"; info.DeviceID = 117; info.vecAction.push_back("OD210_026_005246_001-IZRTKyEx"); m_keyContraband = "OD210_026_005246_001-IZRTKyEx"; // CPPTP.add_task(&SPAServer::threadFromRedis, this, info); // threadFromRedis(info); } /** * @brief 从基础平台获取算法信息和设备信息的线程函数 * */ void SPAServer::threadFromSuperBrain() { SPDLOG_LOGGER_INFO(m_logger, "开启 fromSuperBrainThread 线程"); /* 创建变量 */ std::vector vecAlgNewInfo; std::vector vecDevNewInfo; /* 获取一次token,后续失效了再获取 */ m_fromSuperBrain.getToken(); while (m_threadRunning) { SPDLOG_LOGGER_INFO(m_logger, "刷新算法和设备信息"); /* 先更新数据库的信息,防止从其他地方更改了数据库,这里没有刷新本地缓存 */ m_toEQMDataBase.getAlgorithmInfo(m_vecEqmAlgInfo); m_toEQMDataBase.getDeviceInfo(m_vecEqmDevInfo); m_toEQMDataBase.getDeviceAlgorithmInfo(m_vecEqmDevInfo, m_listDevIDDelete); /* 从超脑获取基础信息 */ m_fromSuperBrain.getTaskTypeList(vecAlgNewInfo); m_fromSuperBrain.getDeviceList(vecDevNewInfo); /* 处理算法信息 */ bool algIsUpdate = processAlgorithmInfo(vecAlgNewInfo); /* 处理设备信息 */ bool devIsUpdate = processDeviceInfo(vecDevNewInfo); vecAlgNewInfo.clear(); vecDevNewInfo.clear(); /* 更新算法详细信息 */ m_mutexActionInfo.lock(); m_toEQMDataBase.getActionInfo(m_listActionInfo); m_mutexActionInfo.unlock(); std::this_thread::sleep_for(std::chrono::seconds(10)); } } /* 处理算法信息,返回值为true,说明有改变,需要重新读取 */ bool SPAServer::processAlgorithmInfo(std::vector vecNewAlgInfo) { std::vector vecAlgUpdate; std::vector vecAlgDelete; /* 对比数据库表格信息,这里只有插入和删除,没有更新 */ compareAlgorithmInfo(vecNewAlgInfo, vecAlgUpdate, vecAlgDelete); /* 更新数据库,先删除,再写入刷新 */ bool isUpdate = false; if(vecAlgDelete.size() > 0) { SPDLOG_LOGGER_DEBUG(m_logger, "删除算法信息"); m_toEQMDataBase.deleteAlgorithmInfo(vecAlgDelete); isUpdate = true; } if(vecAlgUpdate.size() > 0) { SPDLOG_LOGGER_DEBUG(m_logger, "写入算法信息"); m_toEQMDataBase.writeAlgorithmInfo(vecAlgUpdate); isUpdate = true; } return isUpdate; } /** * @brief 处理设备信息 * * @param vecNewDevInfo 传入新获取到的值 * @return true 需要重新读取数据库,获取新的数据 * @return false 无需读取数据库 */ bool SPAServer::processDeviceInfo(std::vector vecNewDevInfo) { std::vector vecDevInsert; std::vector vecDevUpdate; std::vector vecDevDelete; /*------------------------------------------------------------------------- ****** 这里只对比设备信息,不对比设备的算法信息,算法信息在下面单独对比 ******* *------------------------------------------------------------------------*/ /* 如果本地缓存没有数据,那么就全部插入 */ if(m_vecEqmDevInfo.size() > 0) { for(auto& DevInfo : vecNewDevInfo) { bool isExist = false; for(auto& it0 : m_vecEqmDevInfo) { if(DevInfo.DeviceID == it0.DeviceID) { isExist = true; /* 对比其他项是否相等,不相等就更新 */ if(DevInfo == it0) { continue; }else { vecDevUpdate.push_back(DevInfo); } break; } } if(!isExist) { vecDevInsert.push_back(DevInfo); } } }else { vecDevInsert = vecNewDevInfo; } /* 获取删除列表 */ if(vecNewDevInfo.size() > 0) { bool isExist = false; for(const auto& it : m_vecEqmDevInfo) { isExist = false; for(const auto& it0 : vecNewDevInfo) { if(it.DeviceID == it0.DeviceID) { isExist = true; break; } } if(!isExist) { vecDevDelete.push_back(it); } } }else { vecDevDelete = m_vecEqmDevInfo; } bool isUpdate = false; /* 先删除多余的数据 */ if(vecDevDelete.size() > 0) { SPDLOG_LOGGER_DEBUG(m_logger, "删除设备信息"); m_toEQMDataBase.deleteDeviceInfo(vecDevDelete); isUpdate = true; } /* 更新数据 */ if(vecDevUpdate.size() > 0) { SPDLOG_LOGGER_DEBUG(m_logger, "更新设备信息"); m_toEQMDataBase.updateDeviceInfo(vecDevUpdate); isUpdate = true; } /* 插入数据 */ if(vecDevInsert.size() > 0) { SPDLOG_LOGGER_DEBUG(m_logger, "插入设备信息"); m_toEQMDataBase.insertDeviceInfo(vecDevInsert); isUpdate = true; } /*------------------------------------------------------------------------- ************* 处理设备和算子关联的表格,单独对比设备的算法信息 ************* *------------------------------------------------------------------------*/ /* 插入新的设备信息 */ if(vecDevInsert.size() > 0) { SPDLOG_LOGGER_DEBUG(m_logger, "插入设备和算法关联表"); m_toEQMDataBase.insertDeviceAlgorithmInfo(vecDevInsert); isUpdate = true; } vecDevUpdate.clear(); /* 对比现有的设备是否需要更新算法 */ compareDeviceAlgorithmInfo(vecNewDevInfo, vecDevUpdate); if(vecDevUpdate.size() > 0) { SPDLOG_LOGGER_DEBUG(m_logger, "更新设备和算法关联表, 更新设备数目:{}", vecDevUpdate.size()); m_toEQMDataBase.updateDeviceAlgorithmInfo(vecDevUpdate); } /* 删除tActionCamer表中消失的设备信息 */ if(m_listDevIDDelete.size() > 0) { SPDLOG_LOGGER_DEBUG(m_logger, "删除消失的设备关联的算法"); m_toEQMDataBase.deleteDeviceAlgorithmInfo(m_listDevIDDelete); isUpdate = true; } return isUpdate; } /* 对比现有的数据和新获取到的数据,取出要删除和添加的数据 */ void SPAServer::compareAlgorithmInfo(const std::vector& vecNewInfo, std::vector& vecAlgUpdate, std::vector& vecAlgDelete) { /* 取出要添加的,如果本地缓存是0,那么全部都要添加 */ if(m_vecEqmAlgInfo.size() > 0) { for(const auto& it : vecNewInfo) { bool isExist = false; for(const auto& it0 : m_vecEqmAlgInfo) { /* 如果存在就退出循环 */ if(it.ActionID == it0.ActionID) { isExist = true; break; } } if(!isExist) { vecAlgUpdate.push_back(it); } } }else { vecAlgUpdate = vecNewInfo; } /* 取出要删除的,如果新的数据是0,那么全部都要删除 */ if(vecNewInfo.size() > 0) { bool isExist = false; for(const auto& it : m_vecEqmAlgInfo) { isExist = false; for(const auto& it0 : vecNewInfo) { if(it.ActionID == it0.ActionID) { isExist = true; break; } } if(!isExist) { vecAlgDelete.push_back(it); } } }else { vecAlgDelete = m_vecEqmAlgInfo; } } /** * @brief 对比设备和算法关联表是否需要更新 * 对比规则: * 1、这里只对比已有的设备ID,需要删除的ID在获取到tActionCamer表是就已经取出来了 * 2、如果设备ID相等,那么进一步对比算法信息是否相等 * 3、如果设备ID相等,但是算法信息数目不相等,那么直接加入更新列表 * 4、如果设备ID相等,算法信息数目相等,进一步对比算法信息 * * @param vecNewInfo * @param vecDevUpdate */ void SPAServer::compareDeviceAlgorithmInfo(const std::vector& vecNewInfo, std::vector& vecDevUpdate) { vecDevUpdate.clear(); for(const auto& it0 : vecNewInfo) { for(const auto& it1 : m_vecEqmDevInfo) { if(it0.DeviceID == it1.DeviceID) { /* 设备的算法信息数目不相等,直接加入更新列表 */ if(it0.vecAlgorithmInfo.size() != it1.vecAlgorithmInfo.size()) { vecDevUpdate.push_back(it0); break; } /* 设备的算法信息数目相等,进一步对比算法信息 */ bool isEquality = true; for(const auto& it2 : it0.vecAlgorithmInfo) { bool isEq2 = false; for(const auto& it3 : it1.vecAlgorithmInfo) { /* 这里只对比算法ID */ if(it2.ActionID != it3.ActionID) { continue; }else { isEq2 = true; break; } } if(!isEq2) { isEquality = false; break; } } if(!isEquality) { vecDevUpdate.push_back(it0); break; } } } } } /** * @brief 从Redis获取数据线程函数,这个是摄像机线程 * 一个设备一个线程,这个线程的相关变量只在这个线程中使用 * (注意,这个函数未被使用,不从这里获取Redis数据) * @param info */ void SPAServer::threadFromRedis(const CameraThreadInfo& info) { SPDLOG_LOGGER_INFO(m_logger, "开启 fromRedisThread 线程,设备ID:{}", info.DeviceID); FromRedis fromRedis; fromRedis.setRedisIPAndPort(info.RedisIP, info.RedisPort); fromRedis.setRedisPassword(info.RedisPWD); if(fromRedis.connectRedis()) { SPDLOG_LOGGER_INFO(m_logger, "连接Redis成功"); }else { SPDLOG_LOGGER_ERROR(m_logger, "连接Redis失败"); return; } CameraThreadInfo threadInfo = info; // std::string strKey = "117:OD210_026_005246_001-IZRTKyEx"; /* 取出该设备的Key */ std::vector vecKey; for(const auto& it : info.vecAction) { std::string strKey = std::to_string(info.DeviceID) + ":" + it; vecKey.push_back(strKey); } std::string strRetValue; /* 进入线程循环 */ while (m_threadRunning) { /* 循环读取这个设备关联的算法信息 */ for(const auto& it : vecKey) { SPDLOG_LOGGER_INFO(m_logger, "读取Redis信息, Key: {}", it); if( !fromRedis.getRedisString(it, strRetValue) ) { continue; } SPDLOG_LOGGER_TRACE(m_logger, "Redis Value:\n{}", strRetValue); /* 解析数据 */ AlarmInfo alarmInfo; alarmInfo.ActionID = it.substr(it.find(":") + 1); /* 解析数据 */ parseRedisData(strRetValue, alarmInfo); /* 信息时间有的效性判断,主要是记录Redis数据的有效性,是否长时间没有变化,排查错误时用的 * 如果数据长时间数据不变,那么超脑那里就挂了,写入日志 */ isEventTimeVaild(alarmInfo.EventTime); /* 是否需要写入EQM数据库判断 * 人员计数和区域人员检测需要多次判断,其他的直接写入即可 */ } std::this_thread::sleep_for(std::chrono::seconds(2)); } return; } /* 解析Redis基础数据 */ void SPAServer::parseRedisData(const std::string& strData, AlarmInfo& alarmInfo) { try { nJson json0; json0 = nJson::parse(strData); alarmInfo.AlarmID = json0["alarmId"].get(); alarmInfo.ChannelID = json0["channel"].get(); alarmInfo.PicUrl = json0["picUrl"].get(); alarmInfo.ImageInfo = json0["imageInfo"].get(); /* 解析时间,需要将时间中的“T”换成空格 */ alarmInfo.StartTime = json0["beginTime"].get(); std::replace(alarmInfo.StartTime.begin(), alarmInfo.StartTime.end(), 'T', ' '); alarmInfo.EndTime = json0["endTime"].get(); std::replace(alarmInfo.EndTime.begin(), alarmInfo.EndTime.end(), 'T', ' '); alarmInfo.EventTime = json0["eventTime"].get(); std::replace(alarmInfo.EventTime.begin(), alarmInfo.EventTime.end(), 'T', ' '); /* 判断bBoxes有无数据,有数据就解析,没数据就直接返回了 */ nJson json1 = json0["bBoxes"]; std::string labelList; /* 记录违禁品 */ std::string bboxList; /* 记录bbox */ if(!json1.empty()) { for(auto& it0 : json1) { /* 如果status是true,就不是报警,直接跳过 */ bool status = it0["status"].get(); if(status) { continue; } /* 这一条Box数据报警了将其内容存储起来 */ alarmInfo.Is_Alarm = true; /* 解析label,违禁品关键字,先判断这个是不是违禁品检测的算法ID */ if(alarmInfo.ActionID == m_keyContraband) { /* 解析报警,取出报警类型 */ nJson label = it0["label"]; for(auto& it1 : label) { std::string strLabel = it1.get(); /* 检测是否已经加入到字符串中了 */ strLabel += "|"; if(labelList.find(strLabel) != std::string::npos) { continue; } labelList += strLabel; } } /* 解析bbox,貌似是在图像中的位置 */ nJson bbox = it0["bbox"]; std::string strBbox; for(auto& it1 : bbox) { strBbox += std::to_string(it1.get()) + ","; } /* 去掉最后一个“,” */ if(!strBbox.empty()) { strBbox.pop_back(); } bboxList += strBbox + "|"; } /* 去掉最后一个“|” */ if(!labelList.empty()) { labelList.pop_back(); } if(!bboxList.empty()) { bboxList.pop_back(); } SPDLOG_LOGGER_DEBUG(m_logger, "违禁品列表:{}", labelList); SPDLOG_LOGGER_DEBUG(m_logger, "bbox列表:{}", bboxList); } /* 如果有报警的Box */ if(alarmInfo.Is_Alarm) { /* 添加报警信息的提示信息 */ alarmInfo.BboxList = bboxList; if( (alarmInfo.ActionID == m_keyContraband) && !labelList.empty() ) { alarmInfo.ActionDes = fmt::format("出现违禁品[{}]告警", labelList); SPDLOG_LOGGER_INFO(m_logger, "{}", alarmInfo.ActionDes); }else { alarmInfo.ActionDes = json0["actionDes"].get(); } /* 判断有没有报警数据 */ if(alarmInfo.ImageInfo.empty()) { SPDLOG_LOGGER_ERROR(m_logger, "有报警区域,但是没有图片信息"); return; } /* 如果是人员报警,就存储人员报警信息 */ if(alarmInfo.ActionID == m_KeyFace) { nJson jsonArray = json0["personList"]; for(auto& it : jsonArray) { PersonInfo personInfo; personInfo.PersonID = it["personId"].get(); personInfo.PersonName = it["personName"].get(); alarmInfo.vecPersonInfo.push_back(personInfo); } } } } catch (const nJson::parse_error& e) { SPDLOG_LOGGER_ERROR(m_logger, "解析Redis数据失败:{}, 错误ID:{}", e.what(), e.id); return; } catch (const nJson::type_error& e) { SPDLOG_LOGGER_ERROR(m_logger, "解析Redis数据失败:{}, 错误ID:{}", e.what(), e.id); return; } } /** * @brief 解析Redis的基础通用数据,不包含bBoxes数组数据 * * @param strData Redis返回的源数据,JSON格式 * @param alarmInfo 解析出来的数据 */ void SPAServer::parseRedisBaseData(const std::string& strData, AlarmInfo& alarmInfo) { try { nJson json0; json0 = nJson::parse(strData); alarmInfo.AlarmID = json0["alarmId"].get(); alarmInfo.ChannelID = json0["channel"].get(); alarmInfo.PicUrl = json0["picUrl"].get(); alarmInfo.ImageInfo = json0["imageInfo"].get(); /* 解析时间,需要将时间中的“T”换成空格 */ alarmInfo.StartTime = json0["beginTime"].get(); std::replace(alarmInfo.StartTime.begin(), alarmInfo.StartTime.end(), 'T', ' '); alarmInfo.EndTime = json0["endTime"].get(); std::replace(alarmInfo.EndTime.begin(), alarmInfo.EndTime.end(), 'T', ' '); alarmInfo.EventTime = json0["eventTime"].get(); std::replace(alarmInfo.EventTime.begin(), alarmInfo.EventTime.end(), 'T', ' '); } catch (const nJson::parse_error& e) { SPDLOG_LOGGER_ERROR(m_logger, "解析Redis数据失败:{}, 错误ID:{}", e.what(), e.id); return; } catch (const nJson::type_error& e) { SPDLOG_LOGGER_ERROR(m_logger, "解析Redis数据失败:{}, 错误ID:{}", e.what(), e.id); return; } } /** * @brief 解析Redis的bBoxes数据,这个内容可能根据算法ID不同,内容不同 * * @param strData * @param alarmInfo */ void SPAServer::parseRedisBBoxesData(const std::string& strData, AlarmInfo& alarmInfo) { try { nJson json0; json0 = nJson::parse(strData); /* 判断bBoxes有无数据,有数据就解析,没数据就直接返回了 */ nJson json1 = json0["bBoxes"]; std::string labelList; /* 记录违禁品 */ std::string bboxList; /* 记录bbox */ if(!json1.empty()) { for(auto& it0 : json1) { /* 如果status是true,就不是报警,直接跳过 */ bool status = it0["status"].get(); if(status) { continue; } /* 这一条Box数据报警了将其内容存储起来 */ alarmInfo.Is_Alarm = true; /* 解析label,违禁品关键字,先判断这个是不是违禁品检测的算法ID */ if(alarmInfo.ActionID == m_keyContraband) { /* 解析报警,取出报警类型 */ nJson label = it0["label"]; for(auto& it1 : label) { std::string strLabel = it1.get(); /* 检测是否已经加入到字符串中了 */ strLabel += "|"; if(labelList.find(strLabel) != std::string::npos) { continue; } labelList += strLabel; } } /* 解析bbox,貌似是在图像中的位置 */ nJson bbox = it0["bbox"]; std::string strBbox; for(auto& it1 : bbox) { strBbox += std::to_string(it1.get()) + ","; } /* 去掉最后一个“,” */ if(!strBbox.empty()) { strBbox.pop_back(); } bboxList += strBbox + "|"; } /* 去掉最后一个“|” */ if(!labelList.empty()) { labelList.pop_back(); } if(!bboxList.empty()) { bboxList.pop_back(); } SPDLOG_LOGGER_DEBUG(m_logger, "违禁品列表:{}", labelList); SPDLOG_LOGGER_DEBUG(m_logger, "bbox列表:{}", bboxList); } /* 如果有报警的Box,解析出报警的说明 */ if(alarmInfo.Is_Alarm) { /* 添加报警信息的提示信息 */ alarmInfo.BboxList = bboxList; /* 违禁品报警信息,违禁品列表不是空的,就添加补充的文字 */ if( (alarmInfo.ActionID == m_keyContraband) && !labelList.empty() ) { alarmInfo.ActionDes = fmt::format("出现违禁品[{}]告警", labelList); SPDLOG_LOGGER_INFO(m_logger, "{}", alarmInfo.ActionDes); }else { /* 其他报警信息,直接获取 */ alarmInfo.ActionDes = json0["actionDes"].get(); } /* 判断有没有报警数据 */ if(alarmInfo.ImageInfo.empty()) { SPDLOG_LOGGER_WARN(m_logger, "有报警区域,但是没有图片信息"); return; } /* 如果是人员报警,就存储人员报警信息 */ if(alarmInfo.ActionID == m_KeyFace) { nJson jsonArray = json0["personList"]; for(auto& it : jsonArray) { PersonInfo personInfo; personInfo.PersonID = it["personId"].get(); personInfo.PersonName = it["personName"].get(); alarmInfo.vecPersonInfo.push_back(personInfo); } } } } catch (const nJson::parse_error& e) { SPDLOG_LOGGER_ERROR(m_logger, "解析Redis数据失败:{}, 错误ID:{}", e.what(), e.id); return; } catch (const nJson::type_error& e) { SPDLOG_LOGGER_ERROR(m_logger, "解析Redis数据失败:{}, 错误ID:{}", e.what(), e.id); return; } } /** * @brief 判断时间是否长时间没有更新,默认的是600秒,超过这个时间Redis还未更新,可能是超脑挂了 * * @param strTime * @return true * @return false */ bool SPAServer::isEventTimeVaild(const std::string& strTime) { /* 获取当前时间 */ std::chrono::system_clock::time_point now = std::chrono::system_clock::now(); /* 字符串转成时间 */ std::istringstream iss(strTime); std::tm tmEvent = {}; iss >> std::get_time(&tmEvent, "%Y-%m-%d %H:%M:%S"); /* 时间差 */ std::chrono::system_clock::time_point eventTime = std::chrono::system_clock::from_time_t(std::mktime(&tmEvent)); std::chrono::duration diff = now - eventTime; // SPDLOG_LOGGER_DEBUG(m_logger, "now:{} eventTime: {} 时间差:{}秒",now, eventTime, diff); if(diff.count() > 600) { // SPDLOG_LOGGER_ERROR(m_logger, "Redis数据长时间没有更新,EventTime:{}", strTime); return false; } return true; } /** * @brief 分派任务的线程 1、 线程定时刷新房间和摄像机的关联关系,以及和算法Action的关联关系 2、 将算法信息加入到不同的列表中 需要多个摄像机配合的加入到m_runListRoomActionInfo列表 不需要多个摄像机配合的加入到m_runListActionInfo列表 3、 每次刷新都会清空ActionInfo或者RoomActionInfo的摄像机列表,但是不会动其他基本信息,如果是列表中没有对应的信息, 就会创建新的ActionInfo,那么RunState的状态是INIT,需要开启新的线程 如果刷新完成后,算法对应的摄像机列表为空,那么对应的线程就会停止运行,将RunState设置为STOP,本线程的下一轮 循环就会删除这个ActionInfo */ void SPAServer::threadRoomCamera() { /* 房间相机关联信息 */ std::list listRC; /* 创建连接数据库实例 */ std::shared_ptr toEQMDataBase = std::make_shared(); while (m_threadRunning) { /* 先获取EQM数据库信息,取出房间和摄像机关联信息 */ m_mutexActionInfo.lock(); toEQMDataBase->getActionInfo(m_listActionInfo); /* 取出每个房间的所有算法,int是RoomID,string是Action */ // std::multimap mapCameraActionID; m_mutexRunRAI.lock(); m_mutexRunAI.lock(); /* 先清理已经退出的线程所用到的Action或者RoomAction */ m_runListRoomActionInfo.clearStopRoomAction(); m_runListActionInfo.clearStopAction(); /* 将算法信息加入到不同的列表中 * 需要多个摄像机配合的加入到m_runListRoomActionInfo列表 * 不需要多个摄像机配合的加入到m_runListActionInfo列表 * */ m_runListRoomActionInfo.clearCameraList(); m_runListActionInfo.clearCameraList(); for(const auto& it : m_listActionInfo.getData()) { if(it->ActionID == ActPersonWork || it->ActionID == ActPersonNumber) { m_runListRoomActionInfo.addActionInfo(*it); }else { m_runListActionInfo.addActionCamera(it); } } m_mutexActionInfo.unlock(); /* 这个容器只用于多个摄像机融合的算法,一个功能是一个线程 */ for(const auto& it0 : m_runListRoomActionInfo.getData()) { /* 人员在岗识别 */ if(it0->ActionID == ActPersonWork) { SPDLOG_LOGGER_INFO(m_logger, "RoomID:{} 人员在岗识别", it0->RoomID); /* 判断是否需要创建新的线程 */ if(it0->RunState == RunTimeState::RUN_STATE_INIT) { /* 创建新的线程 */ CPPTP.add_task(&SPAServer::threadActPersonWork, this, it0); } } /* 区域人员检测 */ else if (it0->ActionID == ActPersonNumber) { SPDLOG_LOGGER_INFO(m_logger, "RoomID:{} 区域人员检测", it0->RoomID); if(it0->RunState == RunTimeState::RUN_STATE_INIT) { CPPTP.add_task(&SPAServer::threadActRegionalPersonnelDetection, this, it0); } } /* 非法入侵 */ else if( it0->ActionID == ActIllegalInvasion) { } } /* 这个容器用于不关联的算法信息,每个设备上的每个算法是一个线程 * ActionInfo.RunState = RUN_STATE_INIT ,就是新增的算法,需要创建新的线程 */ for(const auto& it0 : m_runListActionInfo.getData()) { /* 违禁物品识别,不需要多个摄像机融合判断,多少个报警都直接上报 */ if (it0->ActionID == ActContraband) { SPDLOG_LOGGER_INFO(m_logger, "RoomID:{} 违禁品检测", it0->RoomID); it0->strActionName = "违禁品检测"; if(it0->RunState == RunTimeState::RUN_STATE_INIT) { CPPTP.add_task(&SPAServer::threadActNormal, this, it0); } } /* 非法入侵检测,这个也不需要摄像机融合 */ else if (it0->ActionID == ActIllegalInvasion) { SPDLOG_LOGGER_INFO(m_logger, "RoomID:{} 非法入侵检测", it0->RoomID); } /* 疲劳检测,不需要摄像机融合检测,直接上报 */ else if (it0->ActionID == ActFatigueDetection) { SPDLOG_LOGGER_INFO(m_logger, "RoomID:{} 疲劳检测", it0->RoomID); } } m_mutexRunRAI.unlock(); m_mutexRunAI.unlock(); /* 休眠n秒,默认应该是300秒 */ std::this_thread::sleep_for(std::chrono::seconds(g_config.CheckSet)); } } /* 将该算法对应的摄像机放入摄像机列表 */ bool SPAServer::insertCameraToAction(RoomActionInfo* pRAInfo, std::list& listRC, std::multimap& mapCameraActionID) { for(const auto& rci : listRC) { if(rci.RoomID == pRAInfo->RoomID) { /* 这个摄像机在这个房间内,再判断这个摄像机有没有这个算法 */ for(const auto& camID : rci.listCameraID) { for(const auto& cai : mapCameraActionID) { if(camID == cai.first) { /* 再判断这个摄像机的算法是否是当前需要的 */ if(cai.second == pRAInfo->ActionID) { pRAInfo->listCameraID.push_back(camID); } } } } } } return true; } /* 更新房间、算法需要的摄像机个数,从内存中更新 */ bool SPAServer::updateRoomActionCameraCount(std::shared_ptr pRAInfo) { std::lock_guard look(m_mutexRunRAI); for(auto& it : m_runListRoomActionInfo.getData()) { if((it->RoomID == pRAInfo->RoomID) && (it->ActionID == pRAInfo->ActionID)) { it->listCameraID = pRAInfo->listCameraID; break; } } return true; } /* 更新算法的摄像机ID,这里是从内存中的数组里获取 */ bool SPAServer::updateActionCameraID(std::shared_ptr pInfo) { pInfo->CameraID = -1; std::lock_guard look(m_mutexRunAI); for(auto& it : m_runListActionInfo.getData()) { if(it->isEqualBaseInfo(*pInfo)) { pInfo->CameraID = it->CameraID; break; } } return true; } /* 设置线程状态 */ void SPAServer::setThreadState(std::shared_ptr pInfo, RunTimeState state) { std::lock_guard look(m_mutexRunAI); for(auto& it : m_runListActionInfo.getData()) { if(it->isEqualBaseInfo(*pInfo)) { it->RunState = state; break; } } } /* 设置线程状态 */ void SPAServer::setThreadState(std::shared_ptr pInfo, RunTimeState state) { std::lock_guard look(m_mutexRunRAI); for(auto& it : m_runListRoomActionInfo.getData()) { if(it->isEqualBaseInfo(*pInfo)) { it->RunState = state; break; } } } /** * @brief 人员在岗识别线程,应该是人脸识别线程,这个需要房间内多个摄像机共同识别 1、离岗判断条件:直播间无人算离岗(直播间所有摄像头的区域人员检测算法输出结果都为0时记为离岗开始, 当结果存在非0时记为离岗结束) 2、离岗输出结果:当判断出离岗行为时,依据人脸识别结果,找到离岗时间点前有人的最近一帧画面中的人员, 判断为离岗人员,并计算离岗时长,形成离岗记录(频率、机房、离岗人员、离岗开始时间、离岗结束时间、离 岗持续时长) 3、在岗时长计算方式:每天00:00:00-23:59:59人脸识别结果中所有人员名称的出现次数✖抽帧频率(如5秒抽 取1帧,则出现200次人名表示在岗1000秒时长),形成每日的人员在岗时长记录 4、报警判断条件:依据离岗次数和单次离岗时长进行报警,展示报警结果 * * @param pRAInfo 传入房间ID和算法ID */ void SPAServer::threadActPersonWork(RoomActionInfo* RAInfo) { SPDLOG_LOGGER_INFO(m_logger, "开启人员在岗识别线程,RoomID:{} ,Action:{}", RAInfo->RoomID, RAInfo->ActionID); /* 创建读取Redis的实例 */ std::shared_ptr fromRedis = std::make_shared(); /* 局部变量 */ std::shared_ptr pRAInfo = std::make_shared(); *pRAInfo = *RAInfo; int CameraCount = pRAInfo->listCameraID.size(); /* 摄像机数目小于0就退出线程 */ while (m_threadRunning) { /* 更新算法关联的摄像机个数 */ pRAInfo->listCameraID.clear(); updateRoomActionCameraCount(pRAInfo); CameraCount = pRAInfo->listCameraID.size(); if(CameraCount == 0) { break; } /* 读取Redis数据 */ for(const auto& camID : pRAInfo->listCameraID) { std::string strKey = std::to_string(camID) + ":" + pRAInfo->ActionID; std::string strRetValue; if(!fromRedis->getRedisString(strKey, strRetValue)) { SPDLOG_LOGGER_ERROR(m_logger, "读取Redis数据失败, Key:{}", strKey); continue; } /* 解析数据 */ AlarmInfo alarmInfo; parseRedisData(strRetValue, alarmInfo); } } } /** * @brief 区域人员检测,检测这个区域内的人数,不能少于多少人,不能多余多少人 * 1、报警判断条件:直播间报警:当前频率所有直播间摄像头的区域人员检测算法输出结果均大于或小于设定人 数值时,记为报警行为,直接展示报警结果 2、录播间报警:当前频率所有录播间摄像头的区域人员检测算法输出结果均大于或小于设定人数值时,记为报 警行为,直接展示报警结果 3、直播间+录播间报警:当前频率直播间人数+录播间人数大于或小于设定人数值时,记为报警行为,直接展示 报警结果 * * @param RAInfo 传入的房间ID和算法ID */ void SPAServer::threadActRegionalPersonnelDetection(RoomActionInfo* RAInfo) { SPDLOG_LOGGER_INFO(m_logger, "开启区域人员检测线程,RoomID:{} ,Action:{}", RAInfo->RoomID, RAInfo->ActionID); /* 创建读取Redis的实例 */ std::shared_ptr fromRedis = std::make_shared(); /* 局部变量 */ std::shared_ptr pRAInfo = std::make_shared(); /* 创建写入数据库实例 */ std::shared_ptr toEQMDataBase = std::make_shared(); *pRAInfo = *RAInfo; int CameraCount = 0; /* 保存每个摄像机的报警信息 */ std::shared_ptr> pListAlarmInfo = std::make_shared>(); while (m_threadRunning) { /* 更新算法关联的摄像机个数 */ pRAInfo->listCameraID.clear(); updateRoomActionCameraCount(pRAInfo); CameraCount = pRAInfo->listCameraID.size(); /* 摄像机数目等于0就退出线程 */ if(CameraCount == 0) { break; } /* 读取Redis数据 */ for(const auto& camID : pRAInfo->listCameraID) { std::string strKey = std::to_string(camID) + ":" + pRAInfo->ActionID; std::string strRetValue; if(!fromRedis->getRedisString(strKey, strRetValue)) { SPDLOG_LOGGER_ERROR(m_logger, "读取Redis数据失败, Key:{}", strKey); std::this_thread::sleep_for(std::chrono::milliseconds(100)); continue; } /* 解析数据 */ AlarmInfo alarmInfo; parseRedisBaseData(strRetValue, alarmInfo); parseRedisBBoxesData(strRetValue, alarmInfo); } } setThreadState(pRAInfo, RunTimeState::RUN_STATE_STOP); SPDLOG_LOGGER_INFO(m_logger, "关闭区域人员检测线程,RoomID:{} ,Action:{}", RAInfo->RoomID, RAInfo->ActionID); } /** * @brief 普通任务识别,一个算法只需要一个摄像机,包括 违禁品识别、玩手机检测、老鼠等 1、报警判断条件:机房内出现摄像头的违禁物品识别算法输出结果包含指定违规内容时,记为报警行为,直接 展示报警结果 2、这里应该是不区分违禁物品是什么,只要有违禁物品就报警。如果一个违禁物品消失之前又出现了第二个违禁物品, 两个违禁物品都消失后,这次报警才会结束。 3、一有报警信息就写入到EQM数据库的tAlarmInfo表中,报警开始时间是EventTime 4、如果报警结束时间小于设置的最小间隔,就删除这条数据,超过这个时间,就更新结束时间 5、报警信息要有图片,如果当前没有图片,就使用上一张图片 * * @param info 线程信息 */ void SPAServer::threadActNormal(ActionInfo* info) { SPDLOG_LOGGER_INFO(m_logger, "开启 {} 线程,RoomID:{} ,Action:{}",info->strActionName, info->RoomID, info->ActionID); /* 创建读取Redis的实例 */ std::shared_ptr fromRedis = std::make_shared(); /* 创建写入EQM数据库实例 */ std::shared_ptr toEQMDataBase = std::make_shared(); /* 局部变量,保存这个线程的信息 */ std::shared_ptr pActInfo = std::make_shared(); *pActInfo = *info; /* 保存报警数据 */ std::shared_ptr pAlarmInfo = std::make_shared(); while (m_threadRunning) { /* 更新算法关联的摄像机ID */ updateActionCameraID(pActInfo); /* 如果摄像机ID是小于0,那么这个算法就没有对应的摄像机了,线程就退出了 */ if(pActInfo->CameraID < 0) { break; } /* 读取Redis数据 */ std::string strKey = std::to_string(pActInfo->CameraID) + ":" + pActInfo->ActionID; std::string strRetValue; if(!fromRedis->getRedisString(strKey, strRetValue)) { SPDLOG_LOGGER_ERROR(m_logger, "读取Redis数据失败, Key:{}", strKey); std::this_thread::sleep_for(std::chrono::milliseconds(100)); continue; } /* 解析数据 */ AlarmInfo alarmInfo; parseRedisBaseData(strRetValue, alarmInfo); parseRedisBBoxesData(strRetValue, alarmInfo); /* 判断事件的时效性,超过多少秒不更新就可能是超脑挂了 */ if(isEventTimeVaild(alarmInfo.EventTime)) { SPDLOG_LOGGER_WARN(m_logger, "Redis Key:{} 数据长时间没有更新,EventTime:{}",strKey, alarmInfo.EventTime); std::this_thread::sleep_for(std::chrono::milliseconds(100)); continue; } /* 判断有无报警记录,新的报警就写入到EQM的tAlarmInfo表中,正在报警的记录就判断和更新, * 结束报警就更新tAlarmInfo的报警数据的结束时间,并清空pAlarmInfo */ if(pAlarmInfo->Is_Alarm) { /* 正在报警中,检查是否报警结束 */ if(alarmInfo.Is_Alarm) { /* 更新图片信息 */ if(!alarmInfo.ImageInfo.empty()) { pAlarmInfo->ImageInfo = alarmInfo.ImageInfo; } } else { /* 报警结束,判断时长,更新数据库结束时间,结束时间是此时电脑时间 */ if(timeDiffWithNow(pAlarmInfo->EventTime) > g_config.Contraband) { pAlarmInfo->EndTime = chronoToStrTime(std::chrono::system_clock::now()); toEQMDataBase->updateAlarmEndTime(*pAlarmInfo); }else { /* 不够报警时间,目前不做任何处理,不删除EQM报警记录 */ } /* 清空报警信息 */ pAlarmInfo->reInit(); } } else { /* 是新的报警 */ if(alarmInfo.Is_Alarm) { /* 布片不能是空,如果是空的,就不写入数据库 */ if(!alarmInfo.ImageInfo.empty()) { /* 违禁品检测,开始时间是事件时间 */ alarmInfo.StartTime = alarmInfo.EventTime; alarmInfo.EndTime = ""; if(toEQMDataBase->insertAlarmInfo(alarmInfo)) { /* 保存新的报警记录 */ *pAlarmInfo = alarmInfo; }else { SPDLOG_LOGGER_ERROR(m_logger, "写入tAlarmInfo报警数据失败, Key: {}", strKey); } }else { SPDLOG_LOGGER_ERROR(m_logger, "频道:{}, 房间:{}, 摄像机:{}, 算法:{}, 有报警区域, 但是没有图片信息", pActInfo->ChannelID, pActInfo->RoomID, pActInfo->CameraID, pActInfo->ActionID); } } } /* 休眠设置的时间 */ std::this_thread::sleep_for(std::chrono::seconds(g_config.ThreadSleepMS)); } /* 设置线程退出的状态 */ setThreadState(pActInfo, RunTimeState::RUN_STATE_STOP); SPDLOG_LOGGER_INFO(m_logger, "{} 线程退出,RoomID:{} ,CameraID, Action:{}",pActInfo->strActionName, pActInfo->RoomID, pActInfo->CameraID, pActInfo->ActionID); } /** * @brief 非法入侵检测 1、野猫、宠物识别:报警判断条件:机房内出现摄像头的宠物识别算法输出报警结果时,记为报警行为,直接 展示报警结果 2、无权限人员识别:报警判断条件:当判断出区域非法入侵行为时,依据人脸识别结果,判断是否为人脸库人 员,非人脸库的人员时判断为非法入侵行为(背影需要报警需要结合区域人员检测算法判断) * * @param info */ void SPAServer::threadActIllegalInvasion(ActionInfo* info) { SPDLOG_LOGGER_INFO(m_logger, "开启非法入侵检测线程,RoomID:{} ,Action:{}", info->RoomID, info->ActionID); /* 创建读取Redis的实例 */ std::shared_ptr fromRedis = std::make_shared(); /* 局部变量,保存这个线程的信息 */ std::shared_ptr pActInfo = std::make_shared(); *pActInfo = *info; /* 摄像机ID小于0就退出线程 */ while (m_threadRunning) { /* 更新算法关联的摄像机ID */ updateActionCameraID(pActInfo); /* 如果摄像机ID是小于0,那么这个算法就没有对应的摄像机了,线程就退出了 */ if(pActInfo->CameraID < 0) { break; } /* 读取Redis数据 */ std::string strKey = std::to_string(pActInfo->CameraID) + ":" + pActInfo->ActionID; std::string strRetValue; if(!fromRedis->getRedisString(strKey, strRetValue)) { SPDLOG_LOGGER_ERROR(m_logger, "读取Redis数据失败, Key:{}", strKey); continue; } /* 解析数据 */ AlarmInfo alarmInfo; parseRedisData(strRetValue, alarmInfo); /* 判断事件的时效性,超过多少秒不更新就可能是超脑挂了 */ if(isEventTimeVaild(alarmInfo.EventTime)) { SPDLOG_LOGGER_WARN(m_logger, "Redis Key:{} 数据长时间没有更新,EventTime:{}",strKey, alarmInfo.EventTime); continue; } /* 判断有无报警记录,写入到EQM数据库 */ } /* 设置线程退出的状态 */ setThreadState(pActInfo, RunTimeState::RUN_STATE_STOP); SPDLOG_LOGGER_INFO(m_logger, "非法入侵检测线程退出,RoomID:{} ,CameraID:{}, Action:{}", pActInfo->RoomID, pActInfo->CameraID, pActInfo->ActionID); } /* 计算与当前时间的时间差,返回秒 */ int SPAServer::timeDiffWithNow(const std::string& strTime) { auto now = std::chrono::system_clock::now(); auto eventTime = strTimeToChrono(strTime); std::chrono::duration diff = now - eventTime; return diff.count(); } /* 字符串时间转换成std::chrono时间点 */ std::chrono::system_clock::time_point SPAServer::strTimeToChrono(const std::string& strTime) { std::istringstream iss(strTime); std::tm tmEvent = {}; iss >> std::get_time(&tmEvent, "%Y-%m-%d %H:%M:%S"); return std::chrono::system_clock::from_time_t(std::mktime(&tmEvent)); } /* 时间点转换成字符串 */ std::string SPAServer::chronoToStrTime(const std::chrono::system_clock::time_point& timePoint) { std::time_t time = std::chrono::system_clock::to_time_t(timePoint); std::tm tmEvent = *std::localtime(&time); char buf[64] = {0}; std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tmEvent); return std::string(buf); }