RTSP(Real-Time Streaming Protocol)是一种网络协议,用于在客户端和服务器之间传输实时流媒体数据。在视频监控领域,RTSP通常用于从摄像头或其他视频源获取视频流并将其推送到其他设备或平台。在这种情况下,将视频流从源设备推送到目标设备的过程称为“推流”。
所以,RTSP推流指的是使用RTSP协议将实时流媒体数据从源设备推送到目标设备或平台的过程。
FFmpeg是一个开源的跨平台音视频处理工具库,包含了众多的音视频处理工具和库,其中包括音视频编解码库。因此,FFmpeg可以被用于进行音视频数据的编解码、格式转换、剪辑等操作。
在 IPC 摄像头视频流的应用场景中,通常需要对视频流进行解码,以便将原始视频流转换为可以进行后续处理的格式。这时候,就可以使用 FFmpeg 中的解码器进行解码。解码器会将编码后的视频数据进行解码,还原成原始的像素数据,然后输出给后续的处理模块。
一般情况下,先进行推流,将视频数据推送到服务器或者其他设备,然后在接收端使用解码器对推流的视频数据进行解码,得到原始的视频数据,然后再进行后续的处理或播放。
把推流的视频跟rtsp_server.yml, rtsp-simple-server(网上下载的,要确保下载的对应系统, Linux跟Windows不一样的) start_server.sh放在一个文件夹
启动
# Ubuntu安装ffmpeg
sudo apt-get install ffmpeg
# 赋予权限
chmod +x rtsp-simple-server
chmod +x start_server.sh
./start_server.sh
kill 进程
# 查看结果, 这里的15120是进程的PID, 每次不一样的
ps aux | grep rtsp
kill 15120
演示
./rtsp-simple-server rtsp_server.yml
(base) root@ubuntu-01:/data/Github/trt_demo/2.OpenCV/4.ipc_cam# ps aux | grep rtsp
root 15120 0.0 0.1 722544 16936 pts/57 Sl+ 21:51 0:00 ./rtsp-simple-server rtsp_server.yml
root 17040 0.0 0.0 13140 1124 pts/66 S+ 22:04 0:00 grep --color=auto rtsp
(base) root@ubuntu-01:/data/Github/trt_demo/2.OpenCV/4.ipc_cam# kill 15120
用OpenCV的方式就可以对视频流进行解码操作, 然后操作他的各种stream
cv::VideoCapture stream1 = cv::VideoCapture("rtsp地址", cv::CAP_FFMPEG);
首先这段代码是用cv::dnn::readNetFromTensorflow() 读取权重和配置文件, 这里要求输入是string,里面的参数分别是权重和配置文件。
然后在绘制图片的操作的时候先用cv::dnn::blobFromImage()处理成blog数据类型的输入数据, 里面的参数分别是图像, 缩放因子, 尺寸, 图像归一化, 是否交换RB通道, 是否裁剪。 这里的归一化的三个参数104.0, 177.0, 123.0是通过不同模型训练出来的经验值
setInput() net.forward() 加载权重"data", 前向传播网络detection_out。这两个关键词去配置文件可以找
经过 net.forward(“detection_out”) 推理后,得到的是一维数组 detection,每个元素是一个检测结果。其中每个检测结果有7个属性,所以可以通过 reshape 操作把这些元素重新排列成一个二维矩阵,方便访问。这里的代码实现是通过 cv::Mat detectionMat(detection.size[2], detection.size[3], CV_32F, detection.ptr()) 来构造一个 CV_32F 类型的二维矩阵,其中 detection.size[2] 表示行数,detection.size[3] 表示列数,detection.ptr() 是一维数组的指针。
detectionMat 二维数组, 包含: 序号, class, confidence, 归一化坐标,有了这个就可以通过置信度找回来了
通过设置置信度的阈值,找到需要画框的坐标,然后恢复成框, 这里需要把归一化坐标恢复, 才可以找到两点真实坐标
#include "opencv2/opencv.hpp"
#include
// 初始化模型
// 这里用const修饰, config跟weight无法被修改
const std::string config = "./weights/opencv_face_detector.pbtxt";
const std::string weight = "./weights/opencv_face_detector_uint8.pb";
// API要求: 权重路径, 配置路径 都要求String
cv::dnn::Net net = cv::dnn::readNetFromTensorflow(weight, config);
// 检测并绘制矩形框
void detectDrawRect(cv::Mat &frame) // 取地址直接对image/vedio操作
{
// 获取图像的宽高
int frameHeight = frame.rows;
int frameWidth = frame.cols;
/*
blobFromImage 会把数据处理成Blog的数据
预处理 resize + swapRB + mean + scale
1.0: 缩放因子,长宽等比例缩放
Size() 尺寸缩放到(300, 300)
Scalar对于三个通道的分别减104.0, 177.0, 123.0, 104.0, 177.0, 123.0
这些数值来自于一个已经在大规模图像数据上进行训练的模型,在对图像进行预处理时,这些数值被认为可以帮助提高模型的性能
false: 是否交换RB通道, 这里不交换
false: 是否进行裁剪, 这里不裁剪
*/
cv::Mat inputBlog = cv::dnn::blobFromImage(frame, 1.0, cv::Size(300, 300), cv::Scalar(104.0, 177.0, 123.0), false, false);
/*
推理
setInput() 把data传入进去, data在权重文件里面找到的
detection_out也是在权重文件里面找到的
前向推理完成后detection就变成了一个包含多个检测结果的一维数组
每个检测结果都是(x, y, w, h) 左上角的坐标和宽高
将一维数组的指针detection.ptr()传递给矩阵的构造函数,将其转换为二维矩阵detectionMat。
就可以使用detectionMat对检测结果进行访问
*/
net.setInput(inputBlog, "data");
cv::Mat detection = net.forward("detection_out");
/*
获取结果
detection.size[2]: rows
detection.size[3]: cols
CV_32F: 指定数据类型为float
detectionMat是个二维矩阵
detectionMat.at(i, j)表示第i行、第j列元素的值
第1列:序号
第2列:class
第3列:confidence
第4-7列:归一化坐标
*/
cv::Mat detectionMat(detection.size[2], detection.size[3], CV_32F, detection.ptr<float>());
for (int i = 0; i < detectionMat.rows; i++)
{
float confidence = detectionMat.at<float>(i, 2);
if (confidence > 0.2)
{
// 两点坐标, 把归一化坐标还原
int l = static_cast<int>(detectionMat.at<float>(i, 3) * frameWidth);
int t = static_cast<int>(detectionMat.at<float>(i, 4) * frameHeight);
int r = static_cast<int>(detectionMat.at<float>(i, 5) * frameWidth);
int b = static_cast<int>(detectionMat.at<float>(i, 6) * frameHeight);
// 画框
cv::rectangle(frame, cv::Point(l, t), cv::Point(r, b), cv::Scalar(0, 0, 255), 2);
}
}
}
// 图片测试
void imageTest()
{
// 读取图片
cv::Mat img = cv::imread("./media/test_face.jpg");
// 推理
detectDrawRect(img);
// 保存
cv::imwrite("./output/face_result1.jpg", img);
}
int main()
{
imageTest();
return 0;
}
void videoTest()
{
// 读取视频流, 实例化对象,传入参数为路径的字符串
cv::VideoCapture cap("./media/video.mp4");
// 获取视频流的宽高
int width = cap.get(cv::CAP_PROP_FRAME_WIDTH);
int height = cap.get(cv::CAP_PROP_FRAME_HEIGHT);
// 实例化写入器
// 参数: 写入路径, 编码格式(H264), 帧率
cv::VideoWriter writer("./output/result.mp4", cv::VideoWriter::fourcc('H', '2', '6', '4'), 25, cv::Size(width, height));
if (!cap.isOpened())
{
std::cout << "打不开这个视频" << std::endl;
// 退出
exit(1);
}
cv::Mat frame;
while (true)
{
if (!cap.read(frame))
{
std::cout << "读完了" << std::endl;
break;
}
// flip, 这里可以用于纠正摄像头铺货的镜像画面
cv::flip(frame, frame, 1);
// 推理
detectDrawRect(frame);
// writer写入
// writer在这里面就是写入器,他写入的是frame, frame最后储存在result.mp4
writer.write(frame);
}
}
.vscode task.json里面需要改一下可执行文件的
“command”: “./build/face_detect”, // 运行命令
lanuch.json改一下
“program”: “${workspaceFolder}/build/face_detect”, // 编译后的可执行文件
cmake_minimum_required(VERSION 3.10)
project(OpenCV_Face_Detect)
# 查找OpenCV
find_package(OpenCV REQUIRED)
# 查找gflags
find_package(gflags REQUIRED)
# 添加头文件路径
include_directories(${OpenCV_INCLUDE_DIRS} ${gflags_INCLUDE_DIRS})
# 添加可执行文件
add_executable(run_file src/main.cpp)
# 链接OpenCV和gflags库
target_link_libraries(run_file ${OpenCV_LIBS} ${gflags_LIBS})