From 5a241a3e9e6e2b6e8e693f7aacb249b010239963 Mon Sep 17 00:00:00 2001 From: GuanYuankai Date: Sun, 4 Jan 2026 16:32:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(video=20Service):=20=E5=AE=8C=E6=88=90ev?= =?UTF-8?q?=20score=E5=B9=B3=E5=9D=87=E5=88=86=E7=B1=BB=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/videoService/video_pipeline.cpp | 226 +++++++++++++++++++--------- src/videoService/video_pipeline.hpp | 43 +++--- 2 files changed, 173 insertions(+), 96 deletions(-) diff --git a/src/videoService/video_pipeline.cpp b/src/videoService/video_pipeline.cpp index fc3740e..e72f132 100644 --- a/src/videoService/video_pipeline.cpp +++ b/src/videoService/video_pipeline.cpp @@ -1,16 +1,13 @@ #include "video_pipeline.hpp" +#include // for std::max, std::min #include -VideoPipeline::VideoPipeline() : running_(false) { - // [新增] 实例化检测器并加载模型 +VideoPipeline::VideoPipeline() : running_(false), next_track_id_(0) { detector_ = std::make_unique(); - - // 模型路径写死为 models/vehicle_model.rknn - int ret = detector_->init("../models/vehicle_model.rknn"); - if (ret != 0) { - spdlog::error("Failed to initialize YoloDetector with model: models/vehicle_model.rknn"); - // 注意:这里如果初始化失败,后续 detect 会直接返回空结果,建议检查日志 + // 模型路径 + if (detector_->init("../models/vehicle_model.rknn") != 0) { + spdlog::error("Failed to initialize YoloDetector"); } else { spdlog::info("YoloDetector initialized successfully."); } @@ -24,8 +21,6 @@ void VideoPipeline::Start(const std::string& inputUrl, const std::string& output if (running_) return; running_ = true; - spdlog::info("Starting VideoPipeline with Input: {}", inputUrl); - processingThread_ = std::thread(&VideoPipeline::processLoop, this, inputUrl, outputUrl, false); } @@ -33,9 +28,6 @@ void VideoPipeline::StartTest(const std::string& filePath, const std::string& ou if (running_) return; running_ = true; - spdlog::info("Starting VideoPipeline (File Test Mode) Input: {}", filePath); - - // true 表示是文件源 processingThread_ = std::thread(&VideoPipeline::processLoop, this, filePath, outputUrl, true); } @@ -43,33 +35,120 @@ void VideoPipeline::Stop() { if (!running_) return; running_ = false; - if (processingThread_.joinable()) { + if (processingThread_.joinable()) processingThread_.join(); - } spdlog::info("VideoPipeline Stopped."); } -// [删除] mockInference 函数已移除 +// [新增] 计算两个矩形的交并比 (IoU) +float VideoPipeline::computeIOU(const cv::Rect& box1, const cv::Rect& box2) { + int x1 = std::max(box1.x, box2.x); + int y1 = std::max(box1.y, box2.y); + int x2 = std::min(box1.x + box1.width, box2.x + box2.width); + int y2 = std::min(box1.y + box1.height, box2.y + box2.height); -void VideoPipeline::drawOverlay(cv::Mat& frame, const std::vector& results) { - for (const auto& res : results) { - // 根据不同类别使用不同颜色 (示例: 0为红色, 1为绿色) - cv::Scalar color = (res.class_id == 0) ? cv::Scalar(0, 0, 255) : cv::Scalar(0, 255, 0); + if (x1 >= x2 || y1 >= y2) + return 0.0f; - cv::rectangle(frame, cv::Rect(res.x, res.y, res.width, res.height), color, 2); + float intersection = (float)((x2 - x1) * (y2 - y1)); + float area1 = (float)(box1.width * box1.height); + float area2 = (float)(box2.width * box2.height); + return intersection / (area1 + area2 - intersection); +} - std::string text = res.label + " " + std::to_string(res.confidence).substr(0, 4); - - // 简单的防止文字跑出边界的处理 - int y_pos = res.y - 5; - if (y_pos < 10) - y_pos = res.y + 15; - - cv::putText(frame, text, cv::Point(res.x, y_pos), cv::FONT_HERSHEY_SIMPLEX, 0.6, color, 2); +// [新增] 核心逻辑:追踪与平滑 +void VideoPipeline::updateTracker(const std::vector& detections) { + // 1. 标记所有现有轨迹为丢失 (missing_frames + 1) + for (auto& pair : tracks_) { + pair.second.missing_frames++; } - // 添加水印 - cv::putText(frame, "RK3588 YOLOv8 Live", cv::Point(20, 50), cv::FONT_HERSHEY_SIMPLEX, 1.0, + // 2. 匹配当前帧检测结果与现有轨迹 + for (const auto& det : detections) { + cv::Rect detBox(det.x, det.y, det.width, det.height); + + int best_match_id = -1; + float max_iou = 0.0f; + + // 寻找 IoU 最大的匹配 + for (auto& pair : tracks_) { + float iou = computeIOU(detBox, pair.second.box); + if (iou > 0.3f && iou > max_iou) { // 阈值 0.3 可根据需要调整 + max_iou = iou; + best_match_id = pair.first; + } + } + + // 假设: class_id == 1 是新能源(Green), class_id == 0 是油车(Fuel) + // 请根据你模型的实际定义修改这里的 ID + float current_is_ev = (det.class_id == 1) ? 1.0f : 0.0f; + + if (best_match_id != -1) { + // === 匹配成功:更新平滑分数 === + TrackedVehicle& track = tracks_[best_match_id]; + track.box = detBox; + track.missing_frames = 0; // 重置丢失计数 + + // [核心算法] 指数移动平均 (EMA) + // alpha = 0.1 表示新结果占10%权重,历史占90%,数值越小越平滑,反应越慢 + float alpha = 0.15f; + track.ev_score = track.ev_score * (1.0f - alpha) + current_is_ev * alpha; + + } else { + // === 未匹配:创建新轨迹 === + TrackedVehicle newTrack; + newTrack.id = next_track_id_++; + newTrack.box = detBox; + newTrack.missing_frames = 0; + newTrack.ev_score = current_is_ev; // 初始分数为当前检测结果 + newTrack.last_class_id = det.class_id; + + tracks_[newTrack.id] = newTrack; + } + } + + // 3. 移除长时间丢失的轨迹 & 更新显示状态 + for (auto it = tracks_.begin(); it != tracks_.end();) { + if (it->second.missing_frames > 10) { // 超过10帧未检测到则移除 + it = tracks_.erase(it); + } else { + // 根据平滑后的分数决定最终类别 + // 阈值 0.5: 分数 > 0.5 判定为新能源 + int final_class = (it->second.ev_score > 0.5f) ? 1 : 0; + + it->second.last_class_id = final_class; + it->second.label = (final_class == 1) ? "Green" : "Fuel"; // 显示文本 + + ++it; + } + } +} + +// [修改] 绘制函数使用 TrackedVehicle +void VideoPipeline::drawOverlay(cv::Mat& frame, const std::vector& trackedObjects) { + for (const auto& trk : trackedObjects) { + // 如果丢失了几帧但还在内存里,用虚线或者灰色表示(可选),这里保持原样 + if (trk.missing_frames > 0) + continue; // 暂时只画当前帧存在的 + + // 颜色:新能源用绿色,油车用红色 + cv::Scalar color = (trk.last_class_id == 1) ? cv::Scalar(0, 255, 0) : cv::Scalar(0, 0, 255); + + cv::rectangle(frame, trk.box, color, 2); + + // 显示 类别 + 平滑后的分数 + // 例如: Green 0.85 + std::string text = fmt::format("{} {:.2f}", trk.label, trk.ev_score); + + int y_pos = trk.box.y - 5; + if (y_pos < 10) + y_pos = trk.box.y + 15; + + cv::putText(frame, text, cv::Point(trk.box.x, y_pos), cv::FONT_HERSHEY_SIMPLEX, 0.6, color, + 2); + } + + cv::putText(frame, "RK3588 YOLOv8 Smooth", cv::Point(20, 50), cv::FONT_HERSHEY_SIMPLEX, 1.0, cv::Scalar(0, 255, 255), 2); } @@ -83,18 +162,13 @@ void VideoPipeline::processLoop(std::string inputUrl, std::string outputUrl, boo return; } - // 降低一点目标 FPS,因为推理需要时间。RK3588 NPU 很快,但 25/30 FPS 比较稳妥 - const double TARGET_FPS = 30.0; + const double TARGET_FPS = 60.0; const double FRAME_DURATION_MS = 1000.0 / TARGET_FPS; - int width = cap.get(cv::CAP_PROP_FRAME_WIDTH); int height = cap.get(cv::CAP_PROP_FRAME_HEIGHT); - spdlog::info("Source: {}x{} | Mode: {}", width, height, - isFileSource ? "FILE LOOP" : "LIVE STREAM"); - + // GStreamer pipeline string (保持原样...) std::stringstream pipeline; - // 注意:推流分辨率这里保持和输入一致,或者可以 resize pipeline << "appsrc ! " << "videoconvert ! " << "video/x-raw,format=NV12,width=" << width << ",height=" << height @@ -106,70 +180,72 @@ void VideoPipeline::processLoop(std::string inputUrl, std::string outputUrl, boo cv::VideoWriter writer; writer.open(pipeline.str(), cv::CAP_GSTREAMER, 0, TARGET_FPS, cv::Size(width, height), true); - if (!writer.isOpened()) { - spdlog::error("Failed to initialize VideoWriter."); - } - cv::Mat frame; long frame_count = 0; + using Clock = std::chrono::high_resolution_clock; + using Ms = std::chrono::duration; + while (running_) { auto loop_start = std::chrono::steady_clock::now(); - + auto loop_begin_tp = Clock::now(); + auto t1 = Clock::now(); if (!cap.read(frame)) { if (isFileSource) { - spdlog::info("End of file reached, looping..."); cap.set(cv::CAP_PROP_POS_FRAMES, 0); continue; } else { - spdlog::warn("Frame read failed. Reconnecting..."); std::this_thread::sleep_for(std::chrono::seconds(1)); cap.release(); cap.open(inputUrl); continue; } } - if (frame.empty()) continue; - - // === 1. 真实 RKNN 推理 === - // detect 内部已经包含了预处理、NPU推理、后处理和坐标还原 + auto t2 = Clock::now(); + // 1. 推理 std::vector results = detector_->detect(frame); - - frame_count++; - // spdlog::info("frame count {}", frame_count); - if (!results.empty()) { - spdlog::info("Frame {}: Detected {} objects", frame_count, results.size()); - for (const auto& res : results) { - spdlog::info(" -> [Class: {} ({})] Conf: {:.2f} Box: [{}, {}, {}x{}]", - res.class_id, res.label, res.confidence, res.x, res.y, res.width, - res.height); - } - } else { - // 如果连续没有检测到,每隔一秒(20帧)提示一次,确认代码还在跑 - if (frame_count % 20 == 0) { - spdlog::debug("Frame {}: No objects detected.", frame_count); - } + auto t3 = Clock::now(); + // 2. [修改] 更新追踪器和平滑分数 + updateTracker(results); + auto t4 = Clock::now(); + // 3. [修改] 准备绘图数据 + std::vector tracks_to_draw; + for (const auto& pair : tracks_) { + tracks_to_draw.push_back(pair.second); } - // === 2. 绘制叠加 === - drawOverlay(frame, results); - // === 3. 推流 === - if (writer.isOpened()) { + // 4. [修改] 绘制 + drawOverlay(frame, tracks_to_draw); + auto t5 = Clock::now(); + // 5. 推流 + if (writer.isOpened()) writer.write(frame); + auto t6 = Clock::now(); + + // ----------------- 计算耗时 ----------------- + double ms_read = std::chrono::duration_cast(t2 - t1).count(); + double ms_infer = std::chrono::duration_cast(t3 - t2).count(); + double ms_track = std::chrono::duration_cast(t4 - t3).count(); + double ms_draw = std::chrono::duration_cast(t5 - t4).count(); + double ms_write = std::chrono::duration_cast(t6 - t5).count(); + double ms_total = std::chrono::duration_cast(t6 - t1).count(); + frame_count++; + if (frame_count % 60 == 0 || ms_total > 18.0) { + spdlog::info( + "Frame[{}] Cost: Total={:.1f}ms | Read={:.1f} Infer={:.1f} Track={:.1f} " + "Draw={:.1f} Write={:.1f}", + frame_count, ms_total, ms_read, ms_infer, ms_track, ms_draw, ms_write); } - // 文件模式下控制播放速度,直播流则不需要 wait + // FPS控制 (保持原样) if (isFileSource) { auto loop_end = std::chrono::steady_clock::now(); - std::chrono::duration elapsed = loop_end - loop_start; - - double elapsed_ms = elapsed.count(); + double elapsed_ms = + std::chrono::duration(loop_end - loop_start).count(); double wait_ms = FRAME_DURATION_MS - elapsed_ms; - - if (wait_ms > 0) { + if (wait_ms > 0) std::this_thread::sleep_for(std::chrono::milliseconds((int)wait_ms)); - } } } diff --git a/src/videoService/video_pipeline.hpp b/src/videoService/video_pipeline.hpp index 7d1bd37..a733b34 100644 --- a/src/videoService/video_pipeline.hpp +++ b/src/videoService/video_pipeline.hpp @@ -1,52 +1,53 @@ #pragma once #include -#include // [新增] 用于 unique_ptr +#include // [新增] +#include #include #include #include #include #include "spdlog/spdlog.h" -#include "yoloDetector/yolo_detector.hpp" // [新增] 包含检测器头文件 (同时也包含了 DetectionResult 定义) +#include "yoloDetector/yolo_detector.hpp" -// [删除] 移除此处定义的 DetectionResult,直接使用 yolo_detector.hpp 中的定义 -/* -struct DetectionResult { - int x; - int y; - int width; - int height; - std::string label; - float confidence; +// [新增] 定义追踪车辆的结构体 +struct TrackedVehicle { + int id; // 唯一ID + cv::Rect box; // 当前位置 + float ev_score; // 新能源分数 (0.0 - 1.0), 越接近1.0越可能是新能源 + int missing_frames; // 连续丢失帧数 (用于删除消失的车辆) + int last_class_id; // 上一次显示的类别ID (避免标签闪烁) + std::string label; // 当前显示的标签 }; -*/ class VideoPipeline { public: VideoPipeline(); ~VideoPipeline(); - // 启动视频流处理 void Start(const std::string& inputUrl, const std::string& outputUrl); void StartTest(const std::string& filePath, const std::string& outputUrl); - - // 停止处理 void Stop(); private: void processLoop(std::string inputUrl, std::string outputUrl, bool isFileSource); - // [删除] 移除模拟推理函数 - // std::vector mockInference(const cv::Mat& frame); + // [修改] 绘图函数现在接收追踪列表,而不是原始检测结果 + void drawOverlay(cv::Mat& frame, const std::vector& trackedObjects); - // 绘图函数 - void drawOverlay(cv::Mat& frame, const std::vector& results); + // [新增] 核心:更新追踪和分数逻辑 + void updateTracker(const std::vector& detections); + + // [新增] 辅助函数:计算 IoU (交并比) + float computeIOU(const cv::Rect& box1, const cv::Rect& box2); private: std::atomic running_; std::thread processingThread_; - - // [新增] YOLO 检测器实例 std::unique_ptr detector_; + + // [新增] 追踪列表,Key是ID + std::map tracks_; + int next_track_id_ = 0; };