392 lines
15 KiB
C++
392 lines
15 KiB
C++
// 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.");
|
||
} |