feat(video service):完成tripwire设置。

增加了车辆通过tripwire截图并存入数据库。
This commit is contained in:
GuanYuankai 2026-01-07 16:50:55 +08:00
parent 14330c393f
commit ab7fa085fc
18 changed files with 347 additions and 112 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@ -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
}

View File

@ -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;
};

View File

@ -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;
}

View File

@ -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;

View File

@ -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();
}
}

View File

@ -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; // 缓存:当前分辨率高
};