bonus-edge-proxy/src/rknn/video_service.cc

392 lines
15 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// video_service.cpp
#include "video_service.h"
#include <stdio.h>
#include "opencv2/imgproc/imgproc.hpp"
#include "rknn/rkYolov5s.hpp"
#include "rknn/rknnPool.hpp"
#include "spdlog/spdlog.h"
#include <chrono>
#include <algorithm>
void VideoService::trigger_alarm(int person_id, const cv::Rect& box) {
printf("[ALARM] Intrusion detected! Person ID: %d at location (%d, %d, %d, %d)\n",
person_id, box.x, box.y, box.width, box.height);
// TODO: 在这里实现真正的报警逻辑,例如发送网络消息、写入数据库等。
}
double VideoService::get_current_time_seconds() {
return std::chrono::duration_cast<std::chrono::duration<double>>(
std::chrono::high_resolution_clock::now().time_since_epoch()
).count();
}
VideoService::VideoService(std::string model_path,
int thread_num,
std::string input_url,
std::string output_rtsp_url)
: model_path_(model_path),
thread_num_(thread_num),
input_url_(input_url),
output_rtsp_url_(output_rtsp_url),
running_(false)
{
log_prefix_ = "[VideoService: " + input_url + "]";
next_track_id_ = 1;
intrusion_time_threshold_ = 3.0; // 3秒
intrusion_zone_ = cv::Rect(0, 0, 0, 0); // 默认无效
spdlog::info("{} Created. Input: {}, Output: {}", log_prefix_, input_url_.c_str(), output_rtsp_url_.c_str());
}
VideoService::~VideoService() {
if (running_) {
stop();
}
}
bool VideoService::start() {
rknn_pool_ = std::make_unique<rknnPool<rkYolov5s, cv::Mat, detect_result_group_t>>(model_path_.c_str(), thread_num_);
if (rknn_pool_->init() != 0) {
printf("rknnPool init fail!\n");
return false;
}
printf("rknnPool init success.\n");
// setenv("OPENCV_FFMPEG_CAPTURE_OPTIONS", "rtsp_transport;tcp", 1);
// printf("Set RTSP transport protocol to TCP\n");
std::string gst_input_pipeline =
"rtspsrc location=" + input_url_ + " latency=0 protocols=tcp ! "
"rtph265depay ! "
"h265parse ! "
"mppvideodec format=16 ! "
"video/x-raw,format=BGR ! " // <-- 关键:直接请求 mppvideodec 输出 BGR 格式
"appsink";
spdlog::info("Try to Open RTSP Stream");
capture_.open(gst_input_pipeline, cv::CAP_GSTREAMER);
if (!capture_.isOpened()) {
printf("Error: Could not open RTSP stream: %s\n", input_url_.c_str());
return false;
}else{
spdlog::info("RTSP Stream Opened!");
}
frame_width_ = static_cast<int>(capture_.get(cv::CAP_PROP_FRAME_WIDTH));
frame_height_ = static_cast<int>(capture_.get(cv::CAP_PROP_FRAME_HEIGHT));
frame_fps_ = capture_.get(cv::CAP_PROP_FPS);
if (frame_fps_ <= 0) frame_fps_ = 25.0;
printf("RTSP stream opened successfully! (%dx%d @ %.2f FPS)\n", frame_width_, frame_height_, frame_fps_);
std::string gst_pipeline =
"appsrc ! "
"queue max-size-buffers=2 leaky=downstream ! "
"video/x-raw,format=BGR ! " // OpenCV VideoWriter 输入 BGR 数据
"videoconvert ! " // <-- 使用 CPU 将 BGR 转换为 NV12
"video/x-raw,format=NV12 ! " // 明确指定 videoconvert 输出 NV12
"mpph265enc gop=25 rc-mode=fixqp qp-init=26 ! " // 硬件编码器接收 NV12 数据
"h265parse ! "
"rtspclientsink location=" + output_rtsp_url_ + " latency=0 protocols=tcp";
printf("Using GStreamer output pipeline: %s\n", gst_pipeline.c_str());
writer_.open(gst_pipeline,
cv::CAP_GSTREAMER,
0,
frame_fps_,
cv::Size(frame_width_, frame_height_),
true);
if (!writer_.isOpened()) {
printf("Error: Could not open VideoWriter with GStreamer pipeline.\n");
capture_.release();
return false;
}
printf("VideoWriter opened successfully.\n");
running_ = true;
reading_thread_ = std::thread(&VideoService::reading_loop, this);
processing_thread_ = std::thread(&VideoService::processing_loop, this);
printf("Processing thread started.\n");
return true;
}
void VideoService::stop() {
printf("Stopping VideoService...\n");
running_ = false;
if (reading_thread_.joinable()) {
reading_thread_.join();
}
if (processing_thread_.joinable()) {
processing_thread_.join();
}
printf("Processing thread joined.\n");
if (capture_.isOpened()) {
capture_.release();
}
if (writer_.isOpened()) {
writer_.release();
}
printf("VideoService stopped.\n");
}
void VideoService::update_tracker(detect_result_group_t &detect_result_group, const cv::Size& frame_size)
{
// 如果入侵区域无效,则设置为帧中心的 1/4 区域 (基于原始帧大小)
if (intrusion_zone_.width <= 0 || intrusion_zone_.height <= 0) {
intrusion_zone_ = cv::Rect(frame_size.width / 4, frame_size.height / 4, frame_size.width / 2, frame_size.height / 2);
}
// --- 缩放比例计算 ---
// !!! 重要: 请确保这里的 640.0f 与您 OpenCV resize 时的目标尺寸一致 !!!
const float model_input_width = 640.0f;
const float model_input_height = 640.0f;
float scale_x = (float)frame_size.width / model_input_width;
float scale_y = (float)frame_size.height / model_input_height;
// --- 结束缩放比例计算 ---
std::vector<cv::Rect> current_detections; // 存储当前帧检测到的、已缩放到原始尺寸的框
for (int i = 0; i < detect_result_group.count; i++) {
detect_result_t *det_result = &(detect_result_group.results[i]);
// 只处理 "person" 类别
if (strcmp(det_result->name, "person") == 0) {
// --- 将模型输出坐标按比例缩放回原始帧坐标 ---
int original_left = static_cast<int>(det_result->box.left * scale_x);
int original_top = static_cast<int>(det_result->box.top * scale_y);
int original_right = static_cast<int>(det_result->box.right * scale_x);
int original_bottom = static_cast<int>(det_result->box.bottom * scale_y);
// --- 边界检查与修正 ---
// 确保坐标不会超出原始图像边界
original_left = std::max(0, std::min(original_left, frame_size.width - 1));
original_top = std::max(0, std::min(original_top, frame_size.height - 1));
// 确保 right >= left, bottom >= top
original_right = std::max(original_left, std::min(original_right, frame_size.width));
original_bottom = std::max(original_top, std::min(original_bottom, frame_size.height));
// --- 结束边界检查 ---
// 只有当框有效时宽度和高度大于0才添加到检测列表
if (original_right > original_left && original_bottom > original_top) {
current_detections.push_back(cv::Rect(
original_left, original_top,
original_right - original_left, // width
original_bottom - original_top // height
));
}
// --- 结束坐标缩放 ---
}
}
for (auto it = tracked_persons_.begin(); it != tracked_persons_.end(); ++it) {
it->second.frames_unseen++;
}
std::vector<int> matched_track_ids; // 记录本帧已匹配到的跟踪ID防止一个跟踪ID匹配多个检测框
// 尝试将当前检测框与已跟踪目标进行匹配
for (const auto& det_box : current_detections) {
int best_match_id = -1;
double max_iou_threshold = 0.3; // IoU 匹配阈值
double best_iou = 0.0; // 用于寻找最佳匹配
for (auto const& [id, person] : tracked_persons_) {
// 检查该跟踪ID是否已在本帧匹配过
bool already_matched = false;
for(int matched_id : matched_track_ids) {
if (id == matched_id) {
already_matched = true;
break;
}
}
if (already_matched) {
continue; // 跳过已匹配的跟踪目标
}
// 计算 IoU (现在 det_box 和 person.box 都是原始坐标系)
double iou = (double)(det_box & person.box).area() / (double)(det_box | person.box).area();
if (iou > best_iou && iou >= max_iou_threshold) { // 必须大于等于阈值才能成为候选
best_iou = iou;
best_match_id = id;
}
}
if (best_match_id != -1) {
// 找到匹配,更新跟踪信息
tracked_persons_[best_match_id].box = det_box;
tracked_persons_[best_match_id].frames_unseen = 0;
matched_track_ids.push_back(best_match_id); // 记录已匹配
} else {
// 没有找到匹配,创建新的跟踪目标
TrackedPerson new_person;
new_person.id = next_track_id_++; // 分配新ID
new_person.box = det_box;
new_person.entry_time = 0;
new_person.is_in_zone = false;
new_person.alarm_triggered = false;
new_person.frames_unseen = 0;
tracked_persons_[new_person.id] = new_person;
}
}
// 更新每个跟踪目标的区域状态和报警状态
double current_time = get_current_time_seconds();
for (auto it = tracked_persons_.begin(); it != tracked_persons_.end(); ++it) {
TrackedPerson& person = it->second;
// 检查人与入侵区域的交集 (现在都是原始坐标系)
bool currently_in_zone = (intrusion_zone_ & person.box).area() > 0;
if (currently_in_zone) {
if (!person.is_in_zone) { // 刚进入区域
person.is_in_zone = true;
person.entry_time = current_time;
person.alarm_triggered = false; // 重置报警状态
} else { // 持续在区域内
// 检查是否达到报警时间阈值且尚未报警
if (!person.alarm_triggered && (current_time - person.entry_time) >= intrusion_time_threshold_) {
person.alarm_triggered = true;
trigger_alarm(person.id, person.box); // 触发报警
}
}
} else { // 当前不在区域内
if (person.is_in_zone) { // 刚离开区域
person.is_in_zone = false;
person.entry_time = 0; // 重置进入时间
person.alarm_triggered = false; // 重置报警状态
}
}
}
for (auto it = tracked_persons_.begin(); it != tracked_persons_.end(); /* 无自增 */) {
if (it->second.frames_unseen > 50) { // 超过 50 帧未见则移除
it = tracked_persons_.erase(it); // erase 返回下一个有效迭代器
} else {
++it; // 手动移动到下一个
}
}
}
void VideoService::reading_loop() {
cv::Mat frame;
spdlog::info("Reading thread started.");
while (running_) {
if (!capture_.read(frame)) {
spdlog::warn("Reading loop: Failed to read frame from capture. Stopping service.");
running_ = false;
break;
}
if (frame.empty()) {
continue;
}
{
std::lock_guard<std::mutex> lock(frame_mutex_);
latest_frame_ = frame;
new_frame_available_ = true;
}
frame_cv_.notify_one();
}
frame_cv_.notify_all();
spdlog::info("Reading loop finished.");
}
void VideoService::draw_results(cv::Mat& frame)
{
cv::rectangle(frame, this->intrusion_zone_, cv::Scalar(255, 255, 0), 2); // 黄色
for (auto const& [id, person] : this->tracked_persons_) {
cv::Scalar box_color = person.alarm_triggered ? cv::Scalar(0, 0, 255) : cv::Scalar(0, 255, 0);
int line_thickness = person.alarm_triggered ? 3 : 2;
cv::rectangle(frame, person.box, box_color, line_thickness);
std::string label = "Person " + std::to_string(id);
if (person.is_in_zone) {
label += " (In Zone)";
}
cv::putText(frame, label, cv::Point(person.box.x, person.box.y - 10),
cv::FONT_HERSHEY_SIMPLEX, 0.5, box_color, 2);
}
}
void VideoService::processing_loop() {
cv::Mat frame;
detect_result_group_t detection_results;
while (running_) {
{
std::unique_lock<std::mutex> lock(frame_mutex_);
frame_cv_.wait(lock, [&]{
return new_frame_available_ || !running_;
});
if (!running_) {
break;
}
// 确认是有新帧
frame = latest_frame_.clone();
new_frame_available_ = false;
}
if (frame.empty()) {
continue;
}
cv::Mat model_input_image;
cv::resize(frame, model_input_image, cv::Size(640, 640));
if (!model_input_image.isContinuous()) {
model_input_image = model_input_image.clone();
}
if (rknn_pool_->put(model_input_image) != 0) {
spdlog::error("VideoService: Failed to put frame into rknnPool. Stopping.");
running_ = false;
break;
}
if (rknn_pool_->get(detection_results) != 0) {
spdlog::error("VideoService: Failed to get frame from rknnPool. Stopping.");
running_ = false;
break;
}
// auto t_infer = std::chrono::high_resolution_clock::now();
this->update_tracker(detection_results, frame.size());
this->draw_results(frame);
auto t_track_draw = std::chrono::high_resolution_clock::now();
if (writer_.isOpened()) {
writer_.write(frame);
}
// auto t_write = std::chrono::high_resolution_clock::now();
// [保留] 性能日志
// double read_ms = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(t_read - t_start).count();
// double infer_ms = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(t_infer - t_read).count();
// double track_ms = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(t_track_draw - t_infer).count();
// double write_ms = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(t_write - t_track_draw).count();
// double total_ms = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(t_write - t_start).count();
// printf("Loop time: Total=%.1fms [Read=%.1f, Infer=%.1f, Track=%.1f, Write=%.1f]\n",
// total_ms, read_ms, infer_ms, track_ms, write_ms);
}
spdlog::info("VideoService: Processing loop finished.");
}