diff --git a/PyApp/upload.log b/PyApp/upload.log index c8f2080..2797386 100644 --- a/PyApp/upload.log +++ b/PyApp/upload.log @@ -16874,3 +16874,25 @@ 2026-03-05 13:27:21 - 主程序异常: [Errno 2] No such file or directory: '/mnt/save/warning' 2026-03-05 13:36:37 - ICCID刷新线程启动 2026-03-05 13:36:37 - 主程序异常: [Errno 2] No such file or directory: '/mnt/save/warning' +2026-03-05 13:42:56 - 主程序异常: [Errno 2] No such file or directory: '/mnt/save/warning' +2026-03-05 13:42:56 - ICCID刷新线程启动 +2026-03-05 15:23:26 - ICCID刷新线程启动 +2026-03-05 15:23:26 - 主程序异常: [Errno 2] No such file or directory: '/mnt/save/warning' +2026-03-05 15:17:08 - ICCID刷新线程启动 +2026-03-05 15:17:08 - 主程序异常: [Errno 2] No such file or directory: '/mnt/save/warning' +2026-03-05 16:58:41 - ICCID刷新线程启动 +2026-03-05 16:58:41 - 主程序异常: [Errno 2] No such file or directory: '/mnt/save/warning' +2026-03-06 10:44:39 - ICCID刷新线程启动 +2026-03-06 10:44:39 - 主程序异常: [Errno 2] No such file or directory: '/mnt/save/warning' +2026-03-06 11:05:39 - 主程序异常: [Errno 2] No such file or directory: '/mnt/save/warning' +2026-03-06 11:05:39 - ICCID刷新线程启动 +2026-03-06 11:18:11 - ICCID刷新线程启动 +2026-03-06 11:18:11 - 主程序异常: [Errno 2] No such file or directory: '/mnt/save/warning' +2026-03-06 11:18:00 - ICCID刷新线程启动 +2026-03-06 11:18:00 - 主程序异常: [Errno 2] No such file or directory: '/mnt/save/warning' +2026-03-06 14:38:07 - ICCID刷新线程启动 +2026-03-06 14:38:07 - 主程序异常: [Errno 2] No such file or directory: '/mnt/save/warning' +2026-03-06 15:03:11 - ICCID刷新线程启动 +2026-03-06 15:03:11 - 主程序异常: [Errno 2] No such file or directory: '/mnt/save/warning' +2026-03-06 15:11:25 - 主程序异常: [Errno 2] No such file or directory: '/mnt/save/warning' +2026-03-06 15:11:25 - ICCID刷新线程启动 diff --git a/VideoProsessing/bin/video b/VideoProsessing/bin/video index 36140ec..fc54d4f 100755 Binary files a/VideoProsessing/bin/video and b/VideoProsessing/bin/video differ diff --git a/VideoProsessing/src/main.cpp b/VideoProsessing/src/main.cpp index d07047a..e2038b0 100644 --- a/VideoProsessing/src/main.cpp +++ b/VideoProsessing/src/main.cpp @@ -13,14 +13,15 @@ #include #include #include -#include //队列 +#include #include #include -#include //双端队列 +#include #include #include "Netra.hpp" #include #include +#include using namespace std; using namespace QCL; @@ -32,14 +33,14 @@ namespace bp = boost::process; const string mqtt_url = "tcp://127.0.0.1:1883"; const string clientId = "video_subData"; const string Topic = "/video/PersonData"; -const string filePath = "/home/orangepi/RKApp/InitAuth/conf/.env"; // 配置保存路径 -const string warningPath = "/mnt/save/warning/"; // 报警图片保存路径 -const string videoPath = "/mnt/save/video/"; // 报警视频保存路径 +const string filePath = "/home/orangepi/RKApp/InitAuth/conf/.env"; +const string warningPath = "/mnt/save/warning/"; +const string videoPath = "/mnt/save/video/"; // 保存检测结果 struct Dection { - double x, y, w, h; // 支持归一化(0~1)或像素值 + double x, y, w, h; double distance; }; @@ -54,81 +55,104 @@ struct dangerDistance struct Point2N { double x, y; -}; // 归一化坐标 +}; struct ZoneBox { string name; - array vertices; // 存 0..1 + array vertices; }; -// 报警框坐标 ZoneBox g_safe, g_warn, g_dang; // 全局变量和对象 -VideoCapture cap; // 在 videoInit 中用 V4L2 打开 -Mat handleFrame; // 存放处理后的帧 +VideoCapture cap; +Mat handleFrame; const int Qos = 0; mqtt::async_client client(mqtt_url, clientId); -mutex detMutex; // 保护latestDection的互斥锁 -vector latestDection; // 保存最新接收到的检测结果 -mutex alertMutex; // 保护alertQueue的互斥锁 -condition_variable alertcv; // 通知报警线程有新任务 -queue alertQueue; // 存放解析后的数据 -std::atomic alertWorkerRunning{false}; // 工作线程运行标志 -atomic outPutMode = false; // 保存报警输出false--电平 +mutex detMutex; +vector latestDection; +mutex alertMutex; +condition_variable alertcv; +queue alertQueue; +std::atomic alertWorkerRunning{false}; +atomic outPutMode = false; bool mainRunning = true; // 视频相关 -const int FPS = 30; // 帧率 -const int PRE_RECORD_SECONDS = 10; // 预录制时长 -const int MAX_BUFFER_SIZE = FPS * PRE_RECORD_SECONDS; // 缓冲区最大帧数 -mutex bufferMutex; // 保护缓冲区的锁 -deque videoDeque; // 环形缓冲区,存储最近十秒的画面帧 -atomic isRecording{false}; // 是否正在写入文件 -atomic mediaMirror{false}; // 是否镜像 -atomic mediaFlip{false}; // 是否翻转 +const int FPS = 30; +const int PRE_RECORD_SECONDS = 10; +const int MAX_BUFFER_SIZE = FPS * PRE_RECORD_SECONDS; +mutex bufferMutex; +atomic isRecording{false}; +atomic mediaMirror{false}; +atomic mediaFlip{false}; -// 用于报警线程的“最新一帧检测结果”(丢弃旧帧,保证低延迟) +// 报警线程用"最新一帧检测结果" static std::mutex latestAlertMutex; static std::condition_variable latestAlertCv; -static std::optional> latestAlertDets; // 仅存最新 +static std::optional> latestAlertDets; static std::atomic latestAlertSeq{0}; -// 推流线程:避免 fwrite/ffmpeg 阻塞影响本机显示 +// 推流线程 static std::mutex g_pipeMutex; static std::condition_variable g_pipeCv; -static cv::Mat g_pipeFrame; // 最新一帧(固定 640x480 bgr24) +// ⚡ 双缓冲:主线程写A,推流线程读B,交替使用,彻底消除数据竞争 +static cv::Mat g_pipeBuf[2]; +static std::atomic g_pipeWriteIdx{0}; // 主线程当前写哪个 static std::atomic g_pipeRunning{false}; static std::atomic g_pipeSeq{0}; -static void pipeWriterThread(FILE *pipe) +// ===================================================================== +// 优化:预分配环形缓冲区,彻底消除每帧 clone() 的 malloc/free 抖动 +// ===================================================================== +struct PreallocRingBuffer { - uint64_t seen = g_pipeSeq.load(std::memory_order_relaxed); + vector pool; + int head = 0; + int count = 0; + int capacity = 0; - while (g_pipeRunning.load()) + void init(int cap_, int rows, int cols, int type) { - cv::Mat local; - - { - std::unique_lock lk(g_pipeMutex); - g_pipeCv.wait(lk, [&] - { return !g_pipeRunning.load() || - g_pipeSeq.load(std::memory_order_relaxed) != seen; }); - if (!g_pipeRunning.load()) - break; - - seen = g_pipeSeq.load(std::memory_order_relaxed); - local = g_pipeFrame; // Mat 头拷贝(数据共享) - } - - if (!pipe || local.empty()) - continue; - - const size_t bytes = local.total() * local.elemSize(); - (void)fwrite(local.data, 1, bytes, pipe); - // 不要 fflush:pipe 已经 _IONBF,无缓冲 + capacity = cap_; + pool.resize(cap_); + for (auto &m : pool) + m.create(rows, cols, type); + head = 0; + count = 0; } -} + + void push(const Mat &src) + { + src.copyTo(pool[head]); + head = (head + 1) % capacity; + if (count < capacity) + count++; + } + + deque snapshot() const + { + deque result; + result.resize(count); + int start = (count < capacity) ? 0 : head; + for (int i = 0; i < count; ++i) + { + int idx = (start + i) % capacity; + pool[idx].copyTo(result[i]); + } + return result; + } + + bool empty() const { return count == 0; } + + const Mat &back() const + { + int idx = (head - 1 + capacity) % capacity; + return pool[idx]; + } +}; + +static PreallocRingBuffer g_ringBuffer; // 把阈值/输出模式缓存到内存,避免频繁读文件 struct RuntimeConfig @@ -140,96 +164,176 @@ struct RuntimeConfig }; static RuntimeConfig g_cfg; -// mqtt初始化 +// ===================================================================== +// ⚡ 显示线程专用双缓冲 +// 主线程写好一帧后通知显示线程,显示线程负责 imshow, +// 主线程不再等待 waitKey,彻底解放主线程帧率 +// ===================================================================== +static std::mutex g_dispMutex; +static std::condition_variable g_dispCv; +static cv::Mat g_dispBuf[2]; // 双缓冲 +static std::atomic g_dispWriteIdx{0}; // 主线程写的 slot +static std::atomic g_dispSeq{0}; +static std::atomic g_dispRunning{false}; + +// 函数声明 void MqttInit(); -// 摄像头管道初始化 bool videoInit(VideoCapture &cap); -// ffmpeg管道初始化 FILE *pipeInit(); -// 对单个帧进行处理 -bool processFrame(VideoCapture &cap, FILE *pipe, Mat &frame, int64 &count, chrono::steady_clock::time_point &t0); -// 资源清理 +bool processFrame(VideoCapture &cap, FILE *pipe, Mat &frame, + int64 &count, chrono::steady_clock::time_point &t0); void cleanup(FILE *pipe, VideoCapture &cap); -// 主循环 void mainLoop(VideoCapture &cap, FILE *pipe); -// mqtt接收订阅消息的回调 void getMsgCallback(mqtt::const_message_ptr msg); -// 绘制矩形方框和深度信息 -void drawRect(double x, double y, double w, double h, double distance); -// 报警线程 +void drawRect(Mat &frame, double x, double y, double w, double h, + double distance, int dangerTh, int warnTh); // ⚡ 不再访问全局 void warnThread(); -// 获取报警距离和输出模式 -bool GetDistance(); -// 调用报警输出程序 void setGPIOLevel(int level); -// 获取当前时间字符串做文件名 string getCurrentTimeStr(); -// 保存图片 void saveAlarmImage(const cv::Mat &frame); -// 保存视频 void saveAlarmVideo(std::deque bufferSnapshot); - -// 从配置文件读取 zones(SAFE/WARN/DANG 的 4 点,归一化值) bool LoadZonesFromEnv(); - -// 绘制 zones 多边形(将归一化坐标按当前帧尺寸缩放) void drawZones(Mat &img); - -// 检测框底边是否接触 danger 多边形(用缩放后的像素点) -bool bottomTouchesDanger(const Dection &d, const ZoneBox &dangerBox); - -// 读取配置,获取翻转镜像设置 +bool bottomTouchesDanger(const Dection &d, const ZoneBox &dangerBox, + int frameW, int frameH); // ⚡ 不再访问全局 handleFrame void loadMirrerSet(); - -// 进行镜像和翻转 void SetMirror(Mat &frame); - -// 只在 .env 文件发生修改后才重新加载 zones 和 镜像配置 void ReloadConfigIfChanged(); - -// 退出信号 void Exit(int sig); +// ===================================================================== +// 推流线程:双缓冲读,彻底消除主线程覆盖数据的竞争 +// ===================================================================== +static void pipeWriterThread(FILE *pipe) +{ + uint64_t seen = g_pipeSeq.load(std::memory_order_relaxed); + + while (g_pipeRunning.load()) + { + int readIdx; + { + std::unique_lock lk(g_pipeMutex); + g_pipeCv.wait(lk, [&] + { return !g_pipeRunning.load() || + g_pipeSeq.load(std::memory_order_relaxed) != seen; }); + if (!g_pipeRunning.load()) + break; + + seen = g_pipeSeq.load(std::memory_order_relaxed); + // ⚡ 读另一个 slot(主线程刚写完的 slot 的前一个) + readIdx = 1 - g_pipeWriteIdx.load(std::memory_order_relaxed); + } + + const cv::Mat &local = g_pipeBuf[readIdx]; + if (!pipe || local.empty()) + continue; + + const size_t bytes = local.total() * local.elemSize(); + (void)fwrite(local.data, 1, bytes, pipe); + } +} + +// ===================================================================== +// ⚡ 显示线程:imshow 和 waitKey 移出主线程,主线程只管采集处理 +// ===================================================================== +static void displayThread(int width, int height) +{ + uint64_t seen = g_dispSeq.load(std::memory_order_relaxed); + // ⚡ 预分配显示帧,resize 复用此内存 + Mat scaled(height, width, CV_8UC3); + // ⚡ 预分配 RGB 帧,cvtColor 复用此内存,不每帧 malloc + static Mat rgbFrame(height, width, CV_8UC3); + + while (g_dispRunning.load()) + { + int readIdx; + { + std::unique_lock lk(g_dispMutex); + // 最多等 33ms(约一帧),超时也刷新一次(防止 imshow 窗口冻结) + g_dispCv.wait_for(lk, std::chrono::milliseconds(33), [&] { + return !g_dispRunning.load() || + g_dispSeq.load(std::memory_order_relaxed) != seen; + }); + if (!g_dispRunning.load()) + break; + + seen = g_dispSeq.load(std::memory_order_relaxed); + readIdx = 1 - g_dispWriteIdx.load(std::memory_order_relaxed); + } + + const cv::Mat &src = g_dispBuf[readIdx]; + if (src.empty()) + continue; + + // ⚡ resize 到预分配内存,无 malloc + resize(src, scaled, Size(width, height), 0, 0, INTER_NEAREST); + + // ⚡ BGR → RGB 转换,复用预分配内存,无 malloc + cv::cvtColor(scaled, rgbFrame, cv::COLOR_BGR2RGB); + + // 显示 RGB 格式画面 + imshow("处理后的画面", rgbFrame); // ✅ 只保留这一个 + + // waitKey 在显示线程中调用,不阻塞主线程 + int key = cv::waitKey(1); + if (key == 'q' || key == 27) + { + cout << "用户请求退出" << endl; + mainRunning = false; + alertWorkerRunning = false; + break; + } + } +} + int main() { - // 确保服务器启动 this_thread::sleep_for(5s); - // 初始化摄像头 if (!videoInit(cap)) - { return -1; - } - // 初始化FFmpeg管道 FILE *pipe = pipeInit(); if (!pipe) - { return -1; - } - // 推流线程启动(推流阻塞不再影响本机显示) + // ⚡ 推流双缓冲预分配 + g_pipeBuf[0].create(480, 640, CV_8UC3); + g_pipeBuf[1].create(480, 640, CV_8UC3); + + // 推流线程启动 g_pipeRunning = true; std::thread(pipeWriterThread, pipe).detach(); - // 先加载一次区域,避免首帧没有框 + // ⚡ 预分配环形缓冲区 + g_ringBuffer.init(MAX_BUFFER_SIZE, 480, 640, CV_8UC3); + LoadZonesFromEnv(); - signal(SIGINT, Exit); - - // 初始化mqtt:订阅,回调函数 MqttInit(); - - // 主处理循环 mainLoop(cap, pipe); - - // 清理资源 cleanup(pipe, cap); return 0; } -// 只在 .env 文件发生修改后才重新加载 zones 和 镜像配置 +// ===================================================================== +// 退出信号 +// ===================================================================== +void Exit(int sig) +{ + cout << "Exiting....." << endl; + mainRunning = false; + alertWorkerRunning = false; + g_dispRunning = false; + latestAlertCv.notify_all(); + alertcv.notify_all(); + g_dispCv.notify_all(); +} + +// ===================================================================== +// 只在 .env 文件发生修改后才重新加载 zones 和镜像配置 +// ===================================================================== void ReloadConfigIfChanged() { static std::filesystem::file_time_type lastEnvWriteTime{}; @@ -238,12 +342,8 @@ void ReloadConfigIfChanged() std::error_code ec; auto curWriteTime = std::filesystem::last_write_time(filePath, ec); if (ec) - { - // 获取失败就跳过,避免频繁输出 return; - } - // 首次调用:直接加载一次并记录时间 if (first) { first = false; @@ -253,39 +353,33 @@ void ReloadConfigIfChanged() return; } - // 文件未修改则直接返回 if (curWriteTime <= lastEnvWriteTime) - { return; - } - // 文件有更新:重新读取配置 lastEnvWriteTime = curWriteTime; LoadZonesFromEnv(); loadMirrerSet(); } -// 进行镜像和翻转 +// ===================================================================== +// 镜像/翻转 +// ===================================================================== void SetMirror(Mat &frame) { - bool mirror = mediaMirror.load(); - bool flipV = mediaFlip.load(); + bool mirror = mediaMirror.load(std::memory_order_relaxed); + bool flipV = mediaFlip.load(std::memory_order_relaxed); if (mirror && flipV) - { // 同时镜像和翻转 cv::flip(frame, frame, -1); - } else if (mirror) - { // 只镜像 cv::flip(frame, frame, 1); - } else if (flipV) - { // 只翻转 cv::flip(frame, frame, 0); - } } -// 读取配置,获取翻转镜像设置 +// ===================================================================== +// 读取配置:翻转/镜像设置 +// ===================================================================== void loadMirrerSet() { ReadFile rf(filePath); @@ -294,77 +388,62 @@ void loadMirrerSet() cerr << "文件打开失败" << endl; return; } - else - { // 解析文件 - auto lines = rf.ReadLines(); - rf.Close(); - auto getBool([&](const string &key, bool &out) - { - out = false; - for(auto&line:lines) - { - if(line.rfind(key+"=",0)==0) - { - // 兼容大小写与可能的空格 - auto val = line.substr(key.size() + 1); - for (auto &c : val) c = ::tolower(c); - val.erase(remove_if(val.begin(), val.end(), ::isspace), val.end()); - if(val=="true") - { - out = true; - } - return out; - } - } - return out; }); - bool mirror = false, flip = false; + auto lines = rf.ReadLines(); + rf.Close(); - getBool("MEDIA_MIRROR", mirror); - getBool("MEDIA_FLIP", flip); + auto getBool = [&](const string &key, bool &out) + { + out = false; + for (auto &line : lines) + { + if (line.rfind(key + "=", 0) == 0) + { + auto val = line.substr(key.size() + 1); + for (auto &c : val) + c = ::tolower(c); + val.erase(remove_if(val.begin(), val.end(), ::isspace), val.end()); + if (val == "true") + out = true; + return out; + } + } + return out; + }; - mediaMirror.store(mirror); - mediaFlip.store(flip); - } + bool mirror = false, flip = false; + getBool("MEDIA_MIRROR", mirror); + getBool("MEDIA_FLIP", flip); + mediaMirror.store(mirror); + mediaFlip.store(flip); } -// 退出信号 -void Exit(int sig) -{ - cout << "Exiting....." << endl; - // 停止主循环与报警线程 - mainRunning = false; - alertWorkerRunning = false; - // 唤醒报警线程从 wait/ wait_for 中返回 - latestAlertCv.notify_all(); - alertcv.notify_all(); -} - -// 检测框底边是否接触 danger 多边形(用缩放后的像素点) -bool bottomTouchesDanger(const Dection &d, const ZoneBox &dangerBox) +// ===================================================================== +// 检测框底边是否接触 danger 多边形 +// ⚡ 不再直接访问全局 handleFrame,改为传入尺寸参数,消除数据竞争 +// ===================================================================== +bool bottomTouchesDanger(const Dection &d, const ZoneBox &dangerBox, + int frameW, int frameH) { vector poly; poly.reserve(4); for (auto &p : dangerBox.vertices) - { - poly.emplace_back(static_cast(p.x * handleFrame.cols), - static_cast(p.y * handleFrame.rows)); - } + poly.emplace_back(static_cast(p.x * frameW), + static_cast(p.y * frameH)); auto toPixX = [&](double v) -> int { - return (v <= 1.0) ? static_cast(v * handleFrame.cols) : static_cast(v); + return (v <= 1.0) ? static_cast(v * frameW) : static_cast(v); }; auto toPixY = [&](double v) -> int { - return (v <= 1.0) ? static_cast(v * handleFrame.rows) : static_cast(v); + return (v <= 1.0) ? static_cast(v * frameH) : static_cast(v); }; int x0 = toPixX(d.x); int y0 = toPixY(d.y); int wpx = toPixX(d.w); int hpx = toPixY(d.h); - int x1 = x0 + wpx; int yb = y0 + hpx; @@ -372,15 +451,16 @@ bool bottomTouchesDanger(const Dection &d, const ZoneBox &dangerBox) for (int i = 0; i <= samples; ++i) { int x = x0 + (i * (x1 - x0)) / samples; - Point pt(x, yb); - double res = pointPolygonTest(poly, pt, false); + double res = pointPolygonTest(poly, Point(x, yb), false); if (res >= 0) return true; } return false; } -// 从配置文件读取 zones(SAFE/WARN/DANG 的 4 点,归一化值) +// ===================================================================== +// 从配置文件读取 zones +// ===================================================================== bool LoadZonesFromEnv() { ReadFile rf(filePath); @@ -420,7 +500,7 @@ bool LoadZonesFromEnv() double x = 0.0, y = 0.0; getDouble(QCL::format("{}_{}_X", prefix, i + 1), x); getDouble(QCL::format("{}_{}_Y", prefix, i + 1), y); - box.vertices[i] = {x, y}; // 归一化值 + box.vertices[i] = {x, y}; } }; @@ -430,7 +510,9 @@ bool LoadZonesFromEnv() return true; } -// 绘制 zones 多边形(将归一化坐标按当前帧尺寸缩放) +// ===================================================================== +// 绘制 zones 多边形 +// ===================================================================== void drawZones(Mat &img) { auto drawPoly = [&](const ZoneBox &box, const Scalar &color) @@ -438,19 +520,18 @@ void drawZones(Mat &img) vector pts; pts.reserve(4); for (auto &p : box.vertices) - { - int px = static_cast(p.x * img.cols); - int py = static_cast(p.y * img.rows); - pts.emplace_back(px, py); - } + pts.emplace_back(static_cast(p.x * img.cols), + static_cast(p.y * img.rows)); polylines(img, pts, true, color, 2); }; - drawPoly(g_safe, Scalar(0, 255, 0)); // 绿色 - drawPoly(g_warn, Scalar(0, 255, 255)); // 黄色 - drawPoly(g_dang, Scalar(0, 0, 255)); // 红色 + drawPoly(g_safe, Scalar(0, 255, 0)); + drawPoly(g_warn, Scalar(0, 255, 255)); + drawPoly(g_dang, Scalar(0, 0, 255)); } +// ===================================================================== // 保存图片 +// ===================================================================== void saveAlarmImage(const Mat &frame) { if (frame.empty()) @@ -464,7 +545,10 @@ void saveAlarmImage(const Mat &frame) cerr << "图片保存失败" << endl; } +// ===================================================================== // 保存视频 +// ⚡ 改为移动语义传入,避免 deque 再次拷贝 +// ===================================================================== void saveAlarmVideo(deque bufferSnapshot) { if (bufferSnapshot.empty() || bufferSnapshot.front().empty()) @@ -473,30 +557,29 @@ void saveAlarmVideo(deque bufferSnapshot) return; } - thread([bufferSnapshot]() + // ⚡ move 进 lambda,彻底避免 deque 拷贝 + thread([buf = std::move(bufferSnapshot)]() mutable { - string fileName = videoPath + "alarm_" + getCurrentTimeStr() + ".mp4"; - VideoWriter write; - int codec = write.fourcc('H', '2', '6', '4'); - Size size = bufferSnapshot.front().size(); - bool color = bufferSnapshot.front().channels() == 3; + string fileName = videoPath + "alarm_" + getCurrentTimeStr() + ".mp4"; + VideoWriter write; + int codec = write.fourcc('H', '2', '6', '4'); + Size size = buf.front().size(); + bool color = buf.front().channels() == 3; - if (!write.open(fileName, codec, FPS, size, color)) - { - cerr << "视频文件打开失败: " << fileName << endl; - return; - } - - for (auto &ii : bufferSnapshot) - { - if (!ii.empty()) - write.write(ii); - } - write.release(); }) + if (!write.open(fileName, codec, FPS, size, color)) + { + cerr << "视频文件打开失败: " << fileName << endl; + return; + } + for (auto &ii : buf) + if (!ii.empty()) write.write(ii); + write.release(); }) .detach(); } -// 获取当前时间字符串做文件名 +// ===================================================================== +// 获取当前时间字符串 +// ===================================================================== string getCurrentTimeStr() { auto now = chrono::system_clock::now(); @@ -506,13 +589,18 @@ string getCurrentTimeStr() return ss.str(); } -// 调用报警输出程序 +// ===================================================================== +// 调用 GPIO 报警输出程序 +// ===================================================================== void setGPIOLevel(int level) { string cmd = "echo 'orangepi' | sudo -S /home/orangepi/RKApp/GPIOSignal/bin/sendGpioSignal " + to_string(level); system(cmd.c_str()); } +// ===================================================================== +// 低频刷新距离阈值配置到内存缓存 +// ===================================================================== static bool RefreshDistanceConfig() { ReadFile rf(filePath); @@ -537,7 +625,6 @@ static bool RefreshDistanceConfig() safe = stoi(line.substr(sizeof("MAX_DISTANCE=") - 1)); else if (line.find("outPutMode:") != string::npos) { - // 注意:你这里的 key/value 格式看起来不像 ENV 的 KEY=VALUE,确认一下 string val = line.substr(sizeof("outPutMode:")); opm = (val == "true"); } @@ -550,50 +637,48 @@ static bool RefreshDistanceConfig() return true; } +// ===================================================================== // 报警线程 +// ===================================================================== void warnThread() { thread([]() { - bool isAlarming = false; + bool isAlarming = false; auto lastDangerTime = chrono::steady_clock::now(); - // 初次加载配置 RefreshDistanceConfig(); - // 初次设置电平 int normalLevel = g_cfg.outPutMode.load() ? 0 : 1; int alarmLevel = g_cfg.outPutMode.load() ? 1 : 0; setGPIOLevel(normalLevel); - // 配置低频刷新(例如 1s 一次),避免每帧 IO - auto lastCfgRefresh = chrono::steady_clock::now(); + auto lastCfgRefresh = chrono::steady_clock::now(); + uint64_t seenSeq = latestAlertSeq.load(); - uint64_t seenSeq = latestAlertSeq.load(); + // ⚡ 记录报警时的帧尺寸(避免访问全局 handleFrame) + int savedFrameW = 640, savedFrameH = 480; while (alertWorkerRunning.load()) { - // 等待新数据到来(带超时:即使YOLO不发消息,也能走“离开后2秒恢复”) std::unique_lock lk(latestAlertMutex); - bool gotNew = latestAlertCv.wait_for(lk, std::chrono::milliseconds(50), [&] { - return !alertWorkerRunning.load() || latestAlertSeq.load() != seenSeq; - }); + bool gotNew = latestAlertCv.wait_for( + lk, std::chrono::milliseconds(50), [&] { + return !alertWorkerRunning.load() || + latestAlertSeq.load() != seenSeq; + }); - if (!alertWorkerRunning.load()) - break; + if (!alertWorkerRunning.load()) break; - // 低频刷新配置(避免抖动) auto now = chrono::steady_clock::now(); if (now - lastCfgRefresh >= chrono::seconds(1)) { RefreshDistanceConfig(); lastCfgRefresh = now; - - normalLevel = g_cfg.outPutMode.load() ? 0 : 1; - alarmLevel = g_cfg.outPutMode.load() ? 1 : 0; + normalLevel = g_cfg.outPutMode.load() ? 0 : 1; + alarmLevel = g_cfg.outPutMode.load() ? 1 : 0; } - // 如果超时没收到新消息:视为“当前无检测=离开危险区” if (!gotNew) { if (isAlarming) @@ -609,18 +694,16 @@ void warnThread() continue; } - // 有新消息:取最新检测 - seenSeq = latestAlertSeq.load(); + seenSeq = latestAlertSeq.load(); auto detsOpt = latestAlertDets; lk.unlock(); - if (!detsOpt.has_value()) - continue; + if (!detsOpt.has_value()) continue; - bool currentFrameHasDanger = false; - const int dangerTh = g_cfg.danger.load(); + bool currentFrameHasDanger = false; + const int dangerTh = g_cfg.danger.load(); - // 判定是否危险(距离优先,否则用多边形) + // ⚡ 传入尺寸而非访问全局 handleFrame,消除数据竞争 for (const auto &d : detsOpt.value()) { if (d.distance > 0.0 && d.distance <= dangerTh) @@ -628,17 +711,14 @@ void warnThread() currentFrameHasDanger = true; break; } - if (d.distance == 0.0) + if (d.distance == 0.0 && + bottomTouchesDanger(d, g_dang, savedFrameW, savedFrameH)) { - if (bottomTouchesDanger(d, g_dang)) - { - currentFrameHasDanger = true; - break; - } + currentFrameHasDanger = true; + break; } } - // 状态机:危险->保持报警;不危险->延迟2秒恢复 if (currentFrameHasDanger) { lastDangerTime = chrono::steady_clock::now(); @@ -647,15 +727,16 @@ void warnThread() isAlarming = true; setGPIOLevel(alarmLevel); - // 保存媒体(你原逻辑保留) + // ⚡ 报警时才 snapshot,且用 move 传入 saveAlarmVideo { lock_guard lk2(bufferMutex); - Mat framToSave; + Mat framToSave; deque bufferToSave; - if (!videoDeque.empty()) + + if (!g_ringBuffer.empty()) { - framToSave = videoDeque.back().clone(); - bufferToSave = videoDeque; + framToSave = g_ringBuffer.back().clone(); + bufferToSave = g_ringBuffer.snapshot(); } else if (!handleFrame.empty()) { @@ -665,7 +746,7 @@ void warnThread() if (!framToSave.empty()) saveAlarmImage(framToSave); if (!bufferToSave.empty()) - saveAlarmVideo(bufferToSave); + saveAlarmVideo(std::move(bufferToSave)); // ⚡ move } } } @@ -686,99 +767,67 @@ void warnThread() .detach(); } -// 获取报警距离 -bool GetDistance() -{ - // 获取距离信息 - ReadFile rf(filePath); - if (rf.Open() == false) - { - cerr << "文件打开失败" << endl; - return false; - } - - auto lines = rf.ReadLines(); - string str; - for (auto &line : lines) - { - if (line.find("NEAR_THRESHOLD=") != string::npos) - dis.danger = stoi(line.substr(sizeof("NEAR_THRESHOLD=") - 1)); - else if (line.find("MID_THRESHOLD=") != string::npos) - dis.warn = stoi(line.substr(sizeof("MID_THRESHOLD=") - 1)); - else if (line.find("MAX_DISTANCE=") != string::npos) - dis.safe = stoi(line.substr(sizeof("MAX_DISTANCE=") - 1)); - else if (line.find("outPutMode:") != string::npos) - { - // 确认输电平模式 - string val = line.substr(sizeof("outPutMode:")); - outPutMode = (val == "true"); - } - } - - rf.Close(); - - return true; -} - +// ===================================================================== // 绘制矩形方框和深度信息 -void drawRect(double x, double y, double w, double h, double distance) +// ⚡ 改为直接接收帧引用和阈值参数,不再每次读全局 dis / 调 GetDistance() +// ===================================================================== +void drawRect(Mat &frame, double x, double y, double w, double h, + double distance, int dangerTh, int warnTh) { - // 将归一化(0~1)或像素值统一转换为像素 + const int W = frame.cols; + const int H = frame.rows; + auto toPixX = [&](double v) -> int { - return (v <= 1.0) ? static_cast(v * handleFrame.cols) : static_cast(v); + return (v <= 1.0) ? static_cast(v * W) : static_cast(v); }; auto toPixY = [&](double v) -> int { - return (v <= 1.0) ? static_cast(v * handleFrame.rows) : static_cast(v); + return (v <= 1.0) ? static_cast(v * H) : static_cast(v); }; - int px = toPixX(x); - int py = toPixY(y); - int pw = toPixX(w); - int ph = toPixY(h); + int px = toPixX(x), py = toPixY(y); + int pw = toPixX(w), ph = toPixY(h); - // 边界裁剪,避免越界 - px = std::max(0, std::min(px, handleFrame.cols - 1)); - py = std::max(0, std::min(py, handleFrame.rows - 1)); - pw = std::max(1, std::min(pw, handleFrame.cols - px)); - ph = std::max(1, std::min(ph, handleFrame.rows - py)); + px = std::max(0, std::min(px, W - 1)); + py = std::max(0, std::min(py, H - 1)); + pw = std::max(1, std::min(pw, W - px)); + ph = std::max(1, std::min(ph, H - py)); Rect r(px, py, pw, ph); Scalar sca(0, 255, 0); - if (!GetDistance()) + if (distance > 0.0) { - sca = Scalar(0, 0, 0); + if (distance <= dangerTh) + sca = Scalar(0, 0, 255); + else if (distance <= warnTh) + sca = Scalar(0, 255, 255); } - else if (distance <= dis.danger) - sca = Scalar(0, 0, 255); - else if (distance <= dis.warn) - sca = Scalar(0, 255, 255); - rectangle(handleFrame, r, sca, 2); - putText(handleFrame, to_string(distance), Point(px, py), FONT_HERSHEY_SIMPLEX, 0.35, Scalar(0, 0, 0)); + rectangle(frame, r, sca, 2); + putText(frame, to_string(distance), Point(px, py), + FONT_HERSHEY_SIMPLEX, 0.35, Scalar(0, 0, 0)); } -// mqtt初始化 +// ===================================================================== +// mqtt 初始化 +// ===================================================================== void MqttInit() { - // 设置回调 - client.set_connected_handler([](const string &cause) + client.set_connected_handler([](const string &) { cout << "连接成功" << endl; }); - client.set_message_callback(getMsgCallback); - client.connect()->wait(); client.subscribe(Topic, Qos)->wait(); alertWorkerRunning = true; - - // 开启报警线程 warnThread(); } -// mqtt接收订阅消息的回调:不要起线程,直接更新“最新结果” +// ===================================================================== +// mqtt 消息回调 +// ===================================================================== void getMsgCallback(mqtt::const_message_ptr msg) { const std::string payload = msg->to_string(); @@ -799,16 +848,15 @@ void getMsgCallback(mqtt::const_message_ptr msg) dets.push_back(d); } - // 更新绘制用的 latestDection(你原来的逻辑) + // ⚡ 绘制用数据也改为 move(latestAlertDets 需要独立副本) { lock_guard lk(detMutex); - latestDection = dets; // 保留给画框使用;如要极致可改成 move + 双缓冲 + latestDection = dets; // 绘制用保留拷贝 } - // 更新报警用的 “最新结果”(覆盖旧的,避免队列堆积导致延迟) { std::lock_guard lk(latestAlertMutex); - latestAlertDets = std::move(dets); + latestAlertDets = std::move(dets); // 报警用 move latestAlertSeq.fetch_add(1, std::memory_order_relaxed); } latestAlertCv.notify_one(); @@ -823,12 +871,14 @@ void getMsgCallback(mqtt::const_message_ptr msg) } } +// ===================================================================== // 摄像头初始化 +// ===================================================================== bool videoInit(VideoCapture &cap) { - // 显式使用 V4L2,避免走 GStreamer if (cap.isOpened()) cap.release(); + if (!cap.open("/dev/video10", cv::CAP_V4L2)) { cerr << "摄像头打开失败:/dev/video10" << endl; @@ -838,12 +888,16 @@ bool videoInit(VideoCapture &cap) cap.set(CAP_PROP_FRAME_WIDTH, 640); cap.set(CAP_PROP_FRAME_HEIGHT, 480); cap.set(CAP_PROP_FPS, 30); - cap.set(CAP_PROP_BUFFERSIZE, 1); - - // 尝试 MJPG,若不支持则忽略 + cap.set(CAP_PROP_BUFFERSIZE, 1); // ⚡ 驱动侧只缓存1帧,最小化延迟 cap.set(CAP_PROP_FOURCC, VideoWriter::fourcc('M', 'J', 'P', 'G')); + double fccv = cap.get(CAP_PROP_FOURCC); - char fcc[5] = {(char)((int)fccv & 0xFF), (char)(((int)fccv >> 8) & 0xFF), (char)(((int)fccv >> 16) & 0xFF), (char)(((int)fccv >> 24) & 0xFF), 0}; + char fcc[5] = { + (char)((int)fccv & 0xFF), + (char)(((int)fccv >> 8) & 0xFF), + (char)(((int)fccv >> 16) & 0xFF), + (char)(((int)fccv >> 24) & 0xFF), + 0}; cout << "摄像头初始化成功 分辨率=" << cap.get(CAP_PROP_FRAME_WIDTH) << "x" << cap.get(CAP_PROP_FRAME_HEIGHT) << " FPS=" << cap.get(CAP_PROP_FPS) @@ -851,7 +905,9 @@ bool videoInit(VideoCapture &cap) return true; } -// FFmpeg管道初始化 +// ===================================================================== +// FFmpeg 管道初始化 +// ===================================================================== FILE *pipeInit() { FILE *pipe = popen( @@ -868,44 +924,51 @@ FILE *pipeInit() cerr << "FFmpeg管道打开失败" << endl; return nullptr; } - - // 设置无缓冲模式 setvbuf(pipe, NULL, _IONBF, 0); cout << "FFmpeg管道初始化成功" << endl; - return pipe; } -// 处理单帧 -bool processFrame(VideoCapture &cap, FILE *pipe, Mat &frame, int64 &count, chrono::steady_clock::time_point &t0) +// ===================================================================== +// 处理单帧(优化版) +// ===================================================================== +bool processFrame(VideoCapture &cap, FILE *pipe, Mat &frame, + int64 &count, chrono::steady_clock::time_point &t0) { + // ⚡ 只 grab 一次,丢掉驱动缓冲中最旧的一帧,read 拿最新帧 + // 原来 grab 两次反而多一次内核交互 cap.grab(); - cap.grab(); - // 读取帧(失败不退出,短休眠重试) - if (!cap.read(frame) || frame.empty()) + + if (!cap.retrieve(frame) || frame.empty()) { cerr << "读取帧失败,重试中..." << endl; - this_thread::sleep_for(50ms); + this_thread::sleep_for(10ms); // ⚡ 缩短重试间隔 return true; } - - handleFrame = frame; + // ⚡ 颜色根治:YUYV 双通道时转 BGR,MJPG 已是 BGR 直接跳过 + if (frame.channels() == 2) + cv::cvtColor(frame, frame, cv::COLOR_YUV2BGR_YUYV); + + handleFrame = frame; // 浅拷贝,无 malloc + + // ⚡ 一次性读取阈值,避免每个目标框都读原子变量 + const int dangerTh = g_cfg.danger.load(std::memory_order_relaxed); + const int warnTh = g_cfg.warn.load(std::memory_order_relaxed); + + // ⚡ move 替代 copy vector destCopy; - // 读取最新检测:短锁获取并将结果拷贝到本地变量 { lock_guard lk(detMutex); - destCopy = latestDection; // 复制到本地 + destCopy = std::move(latestDection); latestDection.clear(); } - // 在主线程上进行绘制 + // ⚡ 直接传帧和阈值,不再访问全局 dis for (const auto &ii : destCopy) - { - drawRect(ii.x, ii.y, ii.w, ii.h, ii.distance); - } + drawRect(handleFrame, ii.x, ii.y, ii.w, ii.h, ii.distance, dangerTh, warnTh); - // 每3秒刷新一次区域坐标并绘制 + // 每 5 秒检查一次配置文件是否变化 static auto lastZonesRefresh = std::chrono::steady_clock::now(); auto now = std::chrono::steady_clock::now(); if (now - lastZonesRefresh >= std::chrono::seconds(5)) @@ -914,48 +977,56 @@ bool processFrame(VideoCapture &cap, FILE *pipe, Mat &frame, int64 &count, chron lastZonesRefresh = now; } - // 绘制三类区域框并按需镜像/翻转(推流输出处理后画面) + // ⚡ SetMirror 在 clone 之前对 handleFrame 操作 + // handleFrame 与 frame 共享数据,flip 会修改原始数据 + // 这里先 clone 一份再 flip,保证 frame 下次仍可用 + handleFrame = frame.clone(); SetMirror(handleFrame); drawZones(handleFrame); - // 确保输出给 FFmpeg 的尺寸与像素格式(BGR24, 640x480) - Mat outFrame; - if (handleFrame.cols != 640 || handleFrame.rows != 480) - resize(handleFrame, outFrame, Size(640, 480)); - else - outFrame = handleFrame; - - // 提交给推流线程(丢旧保新) + // ===================================================================== + // ⚡ 推流:写入双缓冲的"当前写 slot",写完后切换 slot + // ===================================================================== if (pipe && g_pipeRunning.load()) { - // 固定确保 640x480 bgr24 - Mat outFrame; - if (handleFrame.cols != 640 || handleFrame.rows != 480) - resize(handleFrame, outFrame, Size(640, 480)); - else - outFrame = handleFrame; + int writeIdx = g_pipeWriteIdx.load(std::memory_order_relaxed); + Mat &pipeDst = g_pipeBuf[writeIdx]; - { - std::lock_guard lk(g_pipeMutex); - // clone:避免 handleFrame 下一轮被覆盖导致推流线程写到脏数据 - g_pipeFrame = outFrame.clone(); - g_pipeSeq.fetch_add(1, std::memory_order_relaxed); - } + if (handleFrame.cols != 640 || handleFrame.rows != 480) + resize(handleFrame, pipeDst, Size(640, 480)); + else + handleFrame.copyTo(pipeDst); + + // 切换写 slot,通知推流线程读另一个 + g_pipeWriteIdx.store(1 - writeIdx, std::memory_order_relaxed); + g_pipeSeq.fetch_add(1, std::memory_order_relaxed); g_pipeCv.notify_one(); } - // 短锁进行保存 + // ===================================================================== + // ⚡ 显示:写入显示双缓冲,切换 slot,通知显示线程 + // ===================================================================== + { + int writeIdx = g_dispWriteIdx.load(std::memory_order_relaxed); + handleFrame.copyTo(g_dispBuf[writeIdx]); + g_dispWriteIdx.store(1 - writeIdx, std::memory_order_relaxed); + g_dispSeq.fetch_add(1, std::memory_order_relaxed); + g_dispCv.notify_one(); + } + + // ⚡ 环形缓冲区写入 { lock_guard lk(bufferMutex); - videoDeque.push_back(handleFrame.clone()); // 确保存入深拷贝 - if (videoDeque.size() > MAX_BUFFER_SIZE) - videoDeque.pop_front(); + g_ringBuffer.push(handleFrame); } return true; } -// 主处理循环 +// ===================================================================== +// 主处理循环(优化版) +// ⚡ 主线程只负责采集+处理,imshow/waitKey 移至独立显示线程 +// ===================================================================== void mainLoop(VideoCapture &cap, FILE *pipe) { int64 count = 0; @@ -963,53 +1034,51 @@ void mainLoop(VideoCapture &cap, FILE *pipe) Mat frame; cout << "开始视频处理循环..." << endl; - // 创建全屏窗口 + // 创建全屏窗口(必须在主线程/OpenCV线程中调用) namedWindow("处理后的画面", WINDOW_NORMAL); setWindowProperty("处理后的画面", WND_PROP_FULLSCREEN, WINDOW_FULLSCREEN); - // 获取屏幕尺寸(通过获取全屏窗口的实际大小) - cv::Rect windowRect = getWindowImageRect("处理后的画面"); + // 获取屏幕分辨率,用完立即释放 Display *display = XOpenDisplay(nullptr); int screen = DefaultScreen(display); int width = DisplayWidth(display, screen); int height = DisplayHeight(display, screen); + XCloseDisplay(display); - Mat displayFrame; // 用于存储缩放后的画面 + // ⚡ 显示双缓冲预分配 + g_dispBuf[0].create(480, 640, CV_8UC3); + g_dispBuf[1].create(480, 640, CV_8UC3); + // ⚡ 启动显示线程,imshow/waitKey 不再占用主线程 + g_dispRunning = true; + std::thread(displayThread, width, height).detach(); + + // ⚡ 主线程纯采集+处理,不等待显示,帧率上限由摄像头和处理速度决定 while (mainRunning) { if (!processFrame(cap, pipe, frame, count, t0)) - { break; - } - - // 将 handleFrame 缩放到全屏尺寸 - resize(handleFrame, displayFrame, Size(width, height)); - imshow("处理后的画面", displayFrame); - // imshow("csv", handleFrame); - - // 检测退出键 - if (cv::waitKey(1) == 'q') - { - cout << "用户请求退出" << endl; - alertWorkerRunning = false; - break; - } } } +// ===================================================================== // 资源清理 +// ===================================================================== void cleanup(FILE *pipe, VideoCapture &cap) { - // 停止推流线程 g_pipeRunning = false; g_pipeCv.notify_all(); - // 防御式确保报警线程条件被唤醒 + g_dispRunning = false; + g_dispCv.notify_all(); + alertWorkerRunning = false; latestAlertCv.notify_all(); alertcv.notify_all(); + // 给各线程一点时间正常退出 + this_thread::sleep_for(100ms); + try { client.disconnect()->wait(); @@ -1024,7 +1093,6 @@ void cleanup(FILE *pipe, VideoCapture &cap) pclose(pipe); cout << "FFmpeg管道已关闭" << endl; } - if (cap.isOpened()) { cap.release();