feat(video service):完成tripwire设置。
增加了车辆通过tripwire截图并存入数据库。
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
|
@ -1,35 +1,35 @@
|
|||
{
|
||||
"alarm_rules_path": "alarms.json",
|
||||
"config_base_path": "/app/config/",
|
||||
"data_cache_db_path": "edge_data_cache.db",
|
||||
"data_storage_db_path": "edge_proxy_data.db",
|
||||
"db_database": "smart-car-dev",
|
||||
"db_host": "127.0.0.1",
|
||||
"db_pwd": "forlinx",
|
||||
"db_user": "forlinx",
|
||||
"device_id": "rk3588-proxy-002",
|
||||
"log_level": "info",
|
||||
"mqtt_broker": "tcp://localhost:1883",
|
||||
"mqtt_client_id_prefix": "vehicle-road-counter-",
|
||||
"piper_executable_path": "/usr/bin/piper",
|
||||
"piper_model_path": "/app/models/model.onnx",
|
||||
"tcp_server_ports": [
|
||||
12345
|
||||
],
|
||||
"video_config_path": "video_config.json",
|
||||
"web_server_port": 8080,
|
||||
"tripwire": {
|
||||
"enable": true,
|
||||
"line": {
|
||||
"p1": {
|
||||
"x": 0.0,
|
||||
"y": 0.8
|
||||
},
|
||||
"p2": {
|
||||
"x": 1.0,
|
||||
"y": 0.8
|
||||
}
|
||||
},
|
||||
"name": "Main_Gate_Line"
|
||||
}
|
||||
"alarm_rules_path": "alarms.json",
|
||||
"config_base_path": "/app/config/",
|
||||
"data_cache_db_path": "edge_data_cache.db",
|
||||
"data_storage_db_path": "edge_proxy_data.db",
|
||||
"db_database": "smart-car-dev",
|
||||
"db_host": "127.0.0.1",
|
||||
"db_pwd": "forlinx",
|
||||
"db_user": "forlinx",
|
||||
"device_id": "rk3588-proxy-002",
|
||||
"log_level": "info",
|
||||
"mqtt_broker": "tcp://localhost:1883",
|
||||
"mqtt_client_id_prefix": "vehicle-road-counter-",
|
||||
"piper_executable_path": "/usr/bin/piper",
|
||||
"piper_model_path": "/app/models/model.onnx",
|
||||
"tcp_server_ports": [
|
||||
12345
|
||||
],
|
||||
"tripwire": {
|
||||
"enable": true,
|
||||
"line": {
|
||||
"p1": {
|
||||
"x": 0.0,
|
||||
"y": 0.8
|
||||
},
|
||||
"p2": {
|
||||
"x": 1.0,
|
||||
"y": 0.8
|
||||
}
|
||||
},
|
||||
"name": "Main_Gate_Line"
|
||||
},
|
||||
"video_config_path": "video_config.json",
|
||||
"web_server_port": 8080
|
||||
}
|
||||
|
|
@ -8,18 +8,29 @@
|
|||
// 或者,如果 DetectionResult 很简单,建议把它也直接移到这里来
|
||||
// #include "yoloDetector/yolo_detector.hpp"
|
||||
|
||||
struct DetectionResult {
|
||||
int x;
|
||||
int y;
|
||||
int width;
|
||||
int height;
|
||||
std::string label;
|
||||
float confidence;
|
||||
int class_id;
|
||||
};
|
||||
|
||||
// ===========================
|
||||
// 1. 核心检测与追踪数据结构
|
||||
// ===========================
|
||||
|
||||
// 追踪到的车辆对象
|
||||
struct TrackedVehicle {
|
||||
int id; // 轨迹ID
|
||||
cv::Rect box; // 当前位置
|
||||
float ev_score; // 新能源平滑分数
|
||||
int missing_frames; // 丢失帧数计数
|
||||
int last_class_id; // 最终判定类别 (0: Fuel, 1: Green)
|
||||
std::string label; // 显示标签
|
||||
int id; // 轨迹ID
|
||||
cv::Rect box; // 当前位置
|
||||
float ev_score; // 新能源平滑分数
|
||||
int missing_frames; // 丢失帧数计数
|
||||
int last_class_id; // 最终判定类别 (0: Fuel, 1: Green)
|
||||
std::string label; // 显示标签
|
||||
cv::Point prev_bottom_center = cv::Point(-1, -1); // 车辆上一帧的位置
|
||||
|
||||
// [预留] 如果需要记录车辆经过线条的时间,可以在这里加字段
|
||||
// int64_t cross_line_timestamp = 0;
|
||||
|
|
@ -45,13 +56,3 @@ struct TripwireConfig {
|
|||
cv::Point2f p1_norm; // 起点
|
||||
cv::Point2f p2_norm; // 终点
|
||||
};
|
||||
|
||||
struct DetectionResult {
|
||||
int x;
|
||||
int y;
|
||||
int width;
|
||||
int height;
|
||||
std::string label;
|
||||
float confidence;
|
||||
int class_id;
|
||||
};
|
||||
|
|
@ -226,4 +226,44 @@ std::string ConfigManager::getDbHost() {
|
|||
}
|
||||
std::string ConfigManager::getDbName() {
|
||||
return get<std::string>("db_database", "smart-car-dev");
|
||||
}
|
||||
|
||||
TripwireConfig ConfigManager::getTripwireConfig() {
|
||||
TripwireConfig config;
|
||||
|
||||
std::shared_lock<std::shared_mutex> lock(m_mutex);
|
||||
|
||||
if (!m_config_json.contains("tripwire") || !m_config_json["tripwire"].is_object()) {
|
||||
spdlog::warn("Config key 'tripwire' not found or not an object. Using defaults.");
|
||||
return config;
|
||||
}
|
||||
|
||||
try {
|
||||
const auto& j = m_config_json.at("tripwire");
|
||||
|
||||
// 注意:JSON 中是 "enable",结构体中是 "enabled"
|
||||
config.enabled = j.value("enable", false);
|
||||
config.name = j.value("name", "Unnamed_Line");
|
||||
|
||||
// 5. 解析嵌套的 line 坐标
|
||||
if (j.contains("line") && j["line"].is_object()) {
|
||||
const auto& line = j.at("line");
|
||||
|
||||
// 解析 p1 (起点)
|
||||
if (line.contains("p1")) {
|
||||
config.p1_norm.x = line["p1"].value("x", 0.0f);
|
||||
config.p1_norm.y = line["p1"].value("y", 0.0f);
|
||||
}
|
||||
|
||||
// 解析 p2 (终点)
|
||||
if (line.contains("p2")) {
|
||||
config.p2_norm.x = line["p2"].value("x", 1.0f);
|
||||
config.p2_norm.y = line["p2"].value("y", 1.0f);
|
||||
}
|
||||
}
|
||||
} catch (const json::exception& e) {
|
||||
spdlog::error("Failed to parse TripwireConfig: {}", e.what());
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
|
@ -11,6 +11,8 @@
|
|||
#include <typeinfo>
|
||||
#include <vector>
|
||||
|
||||
#include "DTOs/common_types.hpp"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
class ConfigManager {
|
||||
|
|
@ -85,6 +87,8 @@ public:
|
|||
std::string getDbHost();
|
||||
std::string getDbName();
|
||||
|
||||
TripwireConfig getTripwireConfig();
|
||||
|
||||
private:
|
||||
ConfigManager() = default;
|
||||
~ConfigManager() = default;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,27 @@
|
|||
|
||||
#include <algorithm> // for std::max, std::min
|
||||
#include <chrono>
|
||||
#include <filesystem> // C++17
|
||||
#include <iomanip> // for std::put_time
|
||||
#include <sstream>
|
||||
|
||||
#include "config/config_manager.h"
|
||||
// 引入业务 DAO
|
||||
|
||||
#include "mysqlManager/DeviceIdentificationDao.h"
|
||||
#include "mysqlManager/ResourceFileDao.h"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const int YoloDetector::NPU_CORE_CNT;
|
||||
|
||||
// === 静态辅助函数 ===
|
||||
|
||||
// 获取矩形底部的中心点 (作为车辆的"脚")
|
||||
static cv::Point getBottomCenter(const cv::Rect& box) {
|
||||
return cv::Point(box.x + box.width / 2, box.y + box.height);
|
||||
}
|
||||
|
||||
VideoPipeline::VideoPipeline() : running_(false), next_track_id_(0) {
|
||||
detector_ = std::make_unique<YoloDetector>();
|
||||
// 模型路径
|
||||
|
|
@ -11,6 +31,15 @@ VideoPipeline::VideoPipeline() : running_(false), next_track_id_(0) {
|
|||
} else {
|
||||
spdlog::info("YoloDetector initialized successfully.");
|
||||
}
|
||||
try {
|
||||
TripwireConfig config = ConfigManager::getInstance().getTripwireConfig();
|
||||
setTripwire(config);
|
||||
spdlog::info("Tripwire loaded via ConfigManager: {}, Enabled: {}", config.name,
|
||||
config.enabled);
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
spdlog::warn("Failed to load tripwire config via manager: {}", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
VideoPipeline::~VideoPipeline() {
|
||||
|
|
@ -39,6 +68,16 @@ void VideoPipeline::Stop() {
|
|||
processingThread_.join();
|
||||
spdlog::info("VideoPipeline Stopped.");
|
||||
}
|
||||
|
||||
void VideoPipeline::setTripwire(const TripwireConfig& config) {
|
||||
// 可以在运行时更新配置
|
||||
tripwire_config_ = config;
|
||||
// 重置宽高,强制下一帧重新计算像素坐标
|
||||
current_frame_width_ = 0;
|
||||
current_frame_height_ = 0;
|
||||
spdlog::info("Tripwire config set: {}", config.name);
|
||||
}
|
||||
|
||||
void VideoPipeline::inferenceWorker() {
|
||||
while (running_) {
|
||||
FrameData data;
|
||||
|
|
@ -71,7 +110,7 @@ void VideoPipeline::inferenceWorker() {
|
|||
}
|
||||
}
|
||||
|
||||
// [新增] 计算两个矩形的交并比 (IoU)
|
||||
// 计算两个矩形的交并比 (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);
|
||||
|
|
@ -87,94 +126,253 @@ float VideoPipeline::computeIOU(const cv::Rect& box1, const cv::Rect& box2) {
|
|||
return intersection / (area1 + area2 - intersection);
|
||||
}
|
||||
|
||||
// [新增] 核心逻辑:追踪与平滑
|
||||
void VideoPipeline::updateTracker(const std::vector<DetectionResult>& detections) {
|
||||
// 1. 标记所有现有轨迹为丢失 (missing_frames + 1)
|
||||
// [核心算法] 判断两条线段是否相交 (A-B) 和 (C-D)
|
||||
bool VideoPipeline::isLineCrossed(const cv::Point& A, const cv::Point& B, const cv::Point& C,
|
||||
const cv::Point& D) {
|
||||
// 1. 快速排斥实验 (Bounding Box Check)
|
||||
if (std::max(C.x, D.x) < std::min(A.x, B.x) || std::max(A.x, B.x) < std::min(C.x, D.x) ||
|
||||
std::max(C.y, D.y) < std::min(A.y, B.y) || std::max(A.y, B.y) < std::min(C.y, D.y)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 跨立实验 (Cross Product)
|
||||
auto crossProduct = [](const cv::Point& a, const cv::Point& b,
|
||||
const cv::Point& c) -> long long {
|
||||
return (long long)(b.x - a.x) * (c.y - a.y) - (long long)(b.y - a.y) * (c.x - a.x);
|
||||
};
|
||||
|
||||
long long cp1 = crossProduct(A, B, C);
|
||||
long long cp2 = crossProduct(A, B, D);
|
||||
long long cp3 = crossProduct(C, D, A);
|
||||
long long cp4 = crossProduct(C, D, B);
|
||||
|
||||
// 如果 (C, D) 分布在 AB 两侧,且 (A, B) 分布在 CD 两侧,则相交
|
||||
if (((cp1 > 0 && cp2 < 0) || (cp1 < 0 && cp2 > 0)) &&
|
||||
((cp3 > 0 && cp4 < 0) || (cp3 < 0 && cp4 > 0))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// [业务逻辑] 处理跨线车辆 (异步截图入库)
|
||||
void VideoPipeline::processCrossing(const TrackedVehicle& vehicle, const cv::Mat& frame) {
|
||||
// 启动分离线程,避免阻塞主视频流
|
||||
std::thread([this, vehicle, frame]() {
|
||||
// === 1. 准备目录与文件名 ===
|
||||
std::string saveDir = "../captures";
|
||||
try {
|
||||
if (!fs::exists(saveDir)) {
|
||||
fs::create_directories(saveDir);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
spdlog::error("Failed to create directory {}: {}", saveDir, e.what());
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成基于时间戳的文件名: 20231027_103001_id15.jpg
|
||||
auto now = std::chrono::system_clock::now();
|
||||
std::time_t now_c = std::chrono::system_clock::to_time_t(now);
|
||||
std::stringstream ss;
|
||||
ss << std::put_time(std::localtime(&now_c), "%Y%m%d_%H%M%S");
|
||||
std::string timeStr = ss.str();
|
||||
|
||||
std::string fileName = fmt::format("{}_id{}.jpg", timeStr, vehicle.id);
|
||||
std::string fullPath = fmt::format("{}/{}", saveDir, fileName);
|
||||
|
||||
// === 2. 安全截图 ===
|
||||
cv::Rect safeBox = vehicle.box;
|
||||
// 边界钳制,防止 crash
|
||||
if (safeBox.x < 0)
|
||||
safeBox.x = 0;
|
||||
if (safeBox.y < 0)
|
||||
safeBox.y = 0;
|
||||
if (safeBox.x + safeBox.width > frame.cols)
|
||||
safeBox.width = frame.cols - safeBox.x;
|
||||
if (safeBox.y + safeBox.height > frame.rows)
|
||||
safeBox.height = frame.rows - safeBox.y;
|
||||
|
||||
if (safeBox.width <= 0 || safeBox.height <= 0) {
|
||||
spdlog::warn("Invalid box for screenshot, skip.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Clone 数据,因为主线程可能会复用 frame 内存 (虽然当前架构中 frame
|
||||
// 是独立的,但为了健壮性)
|
||||
cv::Mat snapshot = frame(safeBox).clone();
|
||||
if (cv::imwrite(fullPath, snapshot)) {
|
||||
spdlog::info("Snapshot saved: {}", fullPath);
|
||||
} else {
|
||||
spdlog::error("Failed to write image: {}", fullPath);
|
||||
return;
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
spdlog::error("Save image exception: {}", e.what());
|
||||
return;
|
||||
}
|
||||
|
||||
// === 3. 数据库入库 ===
|
||||
DeviceIdentificationDao deviceDao;
|
||||
ResourceFileDao fileDao;
|
||||
|
||||
// 3.1 准备枚举数据
|
||||
// class_id: 1 -> EV(Green), 0 -> Fuel(Blue)
|
||||
CarType cType = (vehicle.last_class_id == 1) ? CarType::ELECTRIC : CarType::GASOLINE;
|
||||
CarColor cColor = (vehicle.last_class_id == 1) ? CarColor::GREEN : CarColor::BLUE;
|
||||
|
||||
// 3.2 插入业务主表
|
||||
// 假设 SystemID 为 1,实际项目可能需配置
|
||||
int64_t systemId = 1;
|
||||
std::string location =
|
||||
tripwire_config_.name.empty() ? "Unkown_Line" : tripwire_config_.name;
|
||||
|
||||
int64_t dataId = deviceDao.ReportIdentification(systemId, location, cColor, cType);
|
||||
|
||||
if (dataId > 0) {
|
||||
// 3.3 插入文件关联表
|
||||
bool fileSaved = fileDao.SaveFile("tb_device_identification_data", // source_table
|
||||
dataId, // business_id
|
||||
fullPath, // file_path
|
||||
fileName, // source_file_name
|
||||
FileType::ORIGINAL // file_type
|
||||
);
|
||||
|
||||
if (fileSaved) {
|
||||
spdlog::debug("DB Transaction Complete. DataID: {}", dataId);
|
||||
} else {
|
||||
spdlog::error("Failed to save file record for DataID: {}", dataId);
|
||||
}
|
||||
} else {
|
||||
spdlog::error("Failed to insert device identification data.");
|
||||
}
|
||||
}).detach(); // 让线程后台运行
|
||||
}
|
||||
|
||||
// 核心逻辑:追踪、平滑与跨线检测
|
||||
void VideoPipeline::updateTracker(const FrameData& frameData) {
|
||||
const auto& detections = frameData.results;
|
||||
const auto& frame = frameData.original_frame;
|
||||
|
||||
// 1. 获取分辨率并更新绊线像素坐标
|
||||
int width = frame.cols;
|
||||
int height = frame.rows;
|
||||
|
||||
if (width != current_frame_width_ || height != current_frame_height_) {
|
||||
current_frame_width_ = width;
|
||||
current_frame_height_ = height;
|
||||
|
||||
if (tripwire_config_.enabled) {
|
||||
tripwire_p1_pixel_.x = (int)(tripwire_config_.p1_norm.x * width);
|
||||
tripwire_p1_pixel_.y = (int)(tripwire_config_.p1_norm.y * height);
|
||||
tripwire_p2_pixel_.x = (int)(tripwire_config_.p2_norm.x * width);
|
||||
tripwire_p2_pixel_.y = (int)(tripwire_config_.p2_norm.y * height);
|
||||
|
||||
spdlog::info("Tripwire pixel coords updated: ({},{}) -> ({},{})", tripwire_p1_pixel_.x,
|
||||
tripwire_p1_pixel_.y, tripwire_p2_pixel_.x, tripwire_p2_pixel_.y);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 标记现有轨迹丢失
|
||||
for (auto& pair : tracks_) {
|
||||
pair.second.missing_frames++;
|
||||
}
|
||||
|
||||
// 2. 匹配当前帧检测结果与现有轨迹
|
||||
// 3. 匹配当前帧
|
||||
for (const auto& det : detections) {
|
||||
cv::Rect detBox(det.x, det.y, det.width, det.height);
|
||||
cv::Point current_bottom = getBottomCenter(detBox);
|
||||
|
||||
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 可根据需要调整
|
||||
if (iou > 0.3f && iou > max_iou) {
|
||||
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%,数值越小越平滑,反应越慢
|
||||
// [新增] 跨线检测
|
||||
if (tripwire_config_.enabled && track.prev_bottom_center.x != -1) {
|
||||
bool crossed = isLineCrossed(tripwire_p1_pixel_, tripwire_p2_pixel_, // 线
|
||||
track.prev_bottom_center, current_bottom // 轨迹
|
||||
);
|
||||
|
||||
if (crossed) {
|
||||
spdlog::info(">>> Vehicle {} Crossed Line: {} <<<", track.id,
|
||||
tripwire_config_.name);
|
||||
// 触发业务逻辑
|
||||
processCrossing(track, frame);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新轨迹状态
|
||||
track.prev_bottom_center = current_bottom; // 更新"上一帧"位置为"当前"
|
||||
track.box = detBox;
|
||||
track.missing_frames = 0;
|
||||
|
||||
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.prev_bottom_center = current_bottom; // 初始位置
|
||||
newTrack.missing_frames = 0;
|
||||
newTrack.ev_score = current_is_ev; // 初始分数为当前检测结果
|
||||
newTrack.ev_score = current_is_ev;
|
||||
newTrack.last_class_id = det.class_id;
|
||||
|
||||
tracks_[newTrack.id] = newTrack;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 移除长时间丢失的轨迹 & 更新显示状态
|
||||
// 4. 移除过期轨迹 & 更新显示类别
|
||||
for (auto it = tracks_.begin(); it != tracks_.end();) {
|
||||
if (it->second.missing_frames > 10) { // 超过10帧未检测到则移除
|
||||
if (it->second.missing_frames > 10) {
|
||||
it = tracks_.erase(it);
|
||||
} else {
|
||||
// 根据平滑后的分数决定最终类别
|
||||
// 阈值 0.5: 分数 > 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->second.label = (final_class == 1) ? "Green" : "Fuel";
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// [修改] 绘制函数使用 TrackedVehicle
|
||||
void VideoPipeline::drawOverlay(cv::Mat& frame, const std::vector<TrackedVehicle>& trackedObjects) {
|
||||
// 1. 画绊线 (如果启用)
|
||||
if (tripwire_config_.enabled) {
|
||||
cv::line(frame, tripwire_p1_pixel_, tripwire_p2_pixel_, cv::Scalar(0, 255, 255), 2);
|
||||
cv::putText(frame, tripwire_config_.name, tripwire_p1_pixel_, cv::FONT_HERSHEY_SIMPLEX, 0.7,
|
||||
cv::Scalar(0, 255, 255), 2);
|
||||
}
|
||||
|
||||
// 2. 画车辆
|
||||
for (const auto& trk : trackedObjects) {
|
||||
// 如果丢失了几帧但还在内存里,用虚线或者灰色表示(可选),这里保持原样
|
||||
if (trk.missing_frames > 0)
|
||||
continue; // 暂时只画当前帧存在的
|
||||
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);
|
||||
// 画中心点轨迹(可选,调试用)
|
||||
// cv::circle(frame, trk.prev_bottom_center, 3, cv::Scalar(255, 0, 0), -1);
|
||||
|
||||
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);
|
||||
}
|
||||
|
|
@ -192,17 +390,15 @@ void VideoPipeline::processLoop(std::string inputUrl, std::string outputUrl, boo
|
|||
return;
|
||||
}
|
||||
|
||||
// 1. 启动 3 个工作线程 (对应 3 个 NPU 核心)
|
||||
// 你的 NPU 利用率将在这里被填满
|
||||
// 1. 启动工作线程
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
worker_threads_.emplace_back(&VideoPipeline::inferenceWorker, this);
|
||||
}
|
||||
|
||||
int width = cap.get(cv::CAP_PROP_FRAME_WIDTH);
|
||||
int height = cap.get(cv::CAP_PROP_FRAME_HEIGHT);
|
||||
const double TARGET_FPS = 30.0; // 提升目标 FPS
|
||||
const double TARGET_FPS = 30.0;
|
||||
|
||||
// ... GStreamer pipeline string 设置保持不变 ...
|
||||
std::stringstream pipeline;
|
||||
pipeline << "appsrc ! "
|
||||
<< "videoconvert ! "
|
||||
|
|
@ -215,43 +411,38 @@ 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);
|
||||
|
||||
long read_frame_idx = 0; // 读取计数
|
||||
long write_frame_idx = 0; // 写入计数
|
||||
long read_frame_idx = 0;
|
||||
long write_frame_idx = 0;
|
||||
|
||||
while (running_) {
|
||||
// === 阶段 A: 读取并分发任务 (生产者) ===
|
||||
// 限制预读数量,防止内存爆满 (例如最多预读 5 帧)
|
||||
// === 阶段 A: 生产者 ===
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(input_mtx_);
|
||||
// 如果输入队列满了,等待工作线程处理
|
||||
if (input_queue_.size() < MAX_INPUT_QUEUE_SIZE) {
|
||||
cv::Mat frame;
|
||||
if (cap.read(frame) && !frame.empty()) {
|
||||
FrameData data;
|
||||
data.frame_id = read_frame_idx++;
|
||||
data.original_frame = frame; // 拷贝一份 (必须,因为 cv::Mat 是引用计数)
|
||||
data.original_frame = frame;
|
||||
|
||||
input_queue_.push(data);
|
||||
input_cv_.notify_one(); // 唤醒一个工作线程
|
||||
input_cv_.notify_one();
|
||||
} else {
|
||||
if (isFileSource) {
|
||||
// 文件读完了的处理...
|
||||
running_ = false;
|
||||
} else {
|
||||
// 网络流断线的处理...
|
||||
// Network disconnected logic...
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === 阶段 B: 按顺序收集结果并处理 (消费者) ===
|
||||
// === 阶段 B: 消费者 ===
|
||||
FrameData current_data;
|
||||
bool has_data = false;
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(output_mtx_);
|
||||
// 检查输出缓冲区里是否有我们期待的下一帧 (write_frame_idx)
|
||||
// 因为多线程处理,第5帧可能比第4帧先处理完,必须等待第4帧
|
||||
auto it = output_buffer_.find(write_frame_idx);
|
||||
if (it != output_buffer_.end()) {
|
||||
current_data = std::move(it->second);
|
||||
|
|
@ -262,38 +453,29 @@ void VideoPipeline::processLoop(std::string inputUrl, std::string outputUrl, boo
|
|||
}
|
||||
|
||||
if (has_data) {
|
||||
// 注意:updateTracker 和 drawOverlay 必须在主线程串行执行
|
||||
// 因为它们依赖 tracks_ 状态,且必须按时间顺序更新
|
||||
// [修改] 传递整个 FrameData 结构体
|
||||
updateTracker(current_data);
|
||||
|
||||
// 1. 追踪 (CPU)
|
||||
updateTracker(current_data.results);
|
||||
|
||||
// 2. 准备绘图数据 (CPU)
|
||||
// 准备绘图
|
||||
std::vector<TrackedVehicle> tracks_to_draw;
|
||||
for (const auto& pair : tracks_) {
|
||||
tracks_to_draw.push_back(pair.second);
|
||||
}
|
||||
|
||||
// 3. 绘图 (CPU)
|
||||
drawOverlay(current_data.original_frame, tracks_to_draw);
|
||||
|
||||
// 4. 推流 (IO)
|
||||
if (writer.isOpened()) {
|
||||
writer.write(current_data.original_frame);
|
||||
}
|
||||
|
||||
// 简单的 FPS 打印
|
||||
if (write_frame_idx % 60 == 0) {
|
||||
spdlog::info("Processed Frame ID: {}", write_frame_idx);
|
||||
}
|
||||
} else {
|
||||
// 如果没有等到当前帧,稍微休眠一下避免死循环占满 CPU
|
||||
// 但不要睡太久,否则延迟高
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
}
|
||||
|
||||
// 清理:通知线程退出并 Join
|
||||
input_cv_.notify_all();
|
||||
for (auto& t : worker_threads_) {
|
||||
if (t.joinable())
|
||||
|
|
@ -303,4 +485,4 @@ void VideoPipeline::processLoop(std::string inputUrl, std::string outputUrl, boo
|
|||
|
||||
cap.release();
|
||||
writer.release();
|
||||
}
|
||||
}
|
||||
|
|
@ -2,16 +2,18 @@
|
|||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <filesystem> // C++17 用于创建文件夹
|
||||
#include <iomanip> // 用于时间格式化
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <opencv2/opencv.hpp>
|
||||
#include <queue>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "DTOs/common_types.hpp"
|
||||
#include "spdlog/spdlog.h"
|
||||
#include "yoloDetector/yolo_detector.hpp"
|
||||
|
||||
|
|
@ -32,7 +34,7 @@ private:
|
|||
void inferenceWorker();
|
||||
|
||||
void drawOverlay(cv::Mat& frame, const std::vector<TrackedVehicle>& trackedObjects);
|
||||
void updateTracker(const std::vector<DetectionResult>& detections);
|
||||
void updateTracker(const FrameData& frameData);
|
||||
float computeIOU(const cv::Rect& box1, const cv::Rect& box2);
|
||||
|
||||
bool isLineCrossed(const cv::Point& p1, const cv::Point& p2, const cv::Point& line_start,
|
||||
|
|
@ -63,4 +65,10 @@ private:
|
|||
std::map<long, FrameData> output_buffer_;
|
||||
std::mutex output_mtx_;
|
||||
std::condition_variable output_cv_;
|
||||
|
||||
TripwireConfig tripwire_config_; // 配置数据
|
||||
cv::Point tripwire_p1_pixel_; // 缓存:转换后的起点像素坐标
|
||||
cv::Point tripwire_p2_pixel_; // 缓存:转换后的终点像素坐标
|
||||
int current_frame_width_ = 0; // 缓存:当前分辨率宽
|
||||
int current_frame_height_ = 0; // 缓存:当前分辨率高
|
||||
};
|
||||
|
|
|
|||