在前面的章节中,我们已经掌握了模型推理的基础,并了解了如何通过DVPP和AIPP等技术优化数据预处理流程。然而,在许多真实世界的AI应用场景中,我们处理的不再是静态的图片,而是连续的视频流,例如来自监控摄像头的RTSP流。更复杂的场景甚至需要同时处理多路视频输入,并进行高效的并行推理。
这篇文章将带你进入视频处理的世界,我们将探讨两个核心主题:
- 单路RTSP视频流处理:如何从一个网络摄像头或视频源获取RTSP流,并利用昇腾AI硬件进行实时推理。
- 多路输入的并行处理与多卡调度:当需要同时处理多个视频源时,如何构建一个高效的并行处理流水线,并利用多张昇腾AI处理器来分担计算压力。
我们将通过分析两个具体的示例------sampleResnetRtsp和sampleYOLOV7MultiInput------来深入理解这些技术的实现细节。
1. 单路RTSP视频流处理:sampleResnetRtsp 示例解析
想象一下,你需要对一个网络摄像头的实时画面进行物体识别。这通常涉及到从RTSP(Real-Time Streaming Protocol)流中拉取视频帧,然后一帧一帧地送入AI模型进行推理。sampleResnetRtsp示例完美地展示了这一流程。
这个示例的核心在于利用AclLiteVideoProc这个强大的工具类来简化视频处理。它封装了视频解码的复杂过程,让我们可以像读取本地视频文件一样,轻松地从RTSP流中获取图像帧。
1.1 AclLiteVideoProc:RTSP流处理的利器
AclLiteVideoProc是AclLite库中的一个组件,专门用于处理视频输入。它的设计非常巧妙,无论是处理本地视频文件(如MP4)还是网络RTSP流,它都提供了统一的接口。
当你用一个RTSP地址(例如 rtsp://username:password@ip_address:port/stream_path)来初始化AclLiteVideoProc时,它会在后台自动处理RTSP握手、拉流和解码等一系列复杂操作。你所需要做的,仅仅是调用Read()方法,就能源源不断地获取解码后的ImageData(图像数据)。
1.2 sampleResnetRtsp 的核心代码逻辑
让我们深入sampleResnetRtsp.cpp的核心代码,看看它是如何工作的。
cpp
// main.cpp in sampleResnetRtsp
int main(int argc, char *argv[])
{
// ... 初始化模型路径、宽度、高度等 ...
std::string inputDataPath = string(argv[1]); // 接收RTSP地址或视频文件路径
// 1. 初始化资源
SampleResnetRtsp sampleResnetRtsp(modelPath, modelWidth, modelHeight);
Result ret = sampleResnetRtsp.InitResource(inputDataPath);
if (ret) {
ACLLITE_LOG_ERROR("Init resource failed, error %d", ret);
return FAILED;
}
// 2. 创建视频处理实例
AclLiteVideoProc cap = AclLiteVideoProc(inputDataPath, device);
if (!cap.IsOpened()) {
ACLLITE_LOG_ERROR("Open camera failed");
return FAILED;
}
// 3. 循环读取并处理视频帧
while (true) {
std::vector<InferenceOutput> inferOutputs;
ImageData image;
// 3.1 从视频源读取一帧图像
AclLiteError ret = cap.Read(image);
if (ret) {
break; // 视频结束或读取失败
}
// 3.2 预处理(例如,使用DVPP进行缩放)
ret = sampleResnetRtsp.ProcessInput(image);
if (ret) {
ACLLITE_LOG_ERROR("Inference image failed, error %d", ret);
return FAILED;
}
// 3.3 执行模型推理
ret = sampleResnetRtsp.Inference(inferOutputs);
if (ret) {
ACLLITE_LOG_ERROR("Inference image failed");
return FAILED;
}
// 3.4 后处理并获取结果
sampleResnetRtsp.GetResult(inferOutputs);
}
ACLLITE_LOG_INFO("Execute sample success");
return SUCCESS;
}
代码的逻辑非常清晰:
- 初始化:加载模型,初始化AscendCL资源。
- 打开视频源 :使用
AclLiteVideoProc打开RTSP流或本地视频。 - 循环处理 :在一个
while循环中,不断地:- 调用
cap.Read(image)获取一帧图像。 - 对图像进行预处理(
ProcessInput),这里通常会利用DVPP硬件加速能力进行图像缩放,以匹配模型的输入尺寸。 - 执行推理(
Inference)。 - 处理推理结果(
GetResult)。
- 调用
这个简单的线性流程非常适合单路视频处理的场景。但是,如果我们需要同时处理几十甚至上百路视频流,这种模式的效率就会成为瓶颈。
2. 多路输入并行处理:sampleYOLOV7MultiInput 示例解析
当面对多路视频输入的挑战时,我们必须转向并行处理架构。sampleYOLOV7MultiInput示例为我们展示了如何构建一个基于多线程的、可扩展的并行处理流水线,并且能够灵活地在多张昇腾AI处理器上进行任务调度。
这个示例的核心思想是将整个处理流程拆分成多个独立的阶段,每个阶段由一个或多个专门的线程来负责。这种"流水线"式的设计可以极大地提升系统的吞吐量。
2.1 基于线程的并行流水线架构
sampleYOLOV7MultiInput将一个典型的AI处理任务拆分成了以下几个线程:
- DataInputThread:数据输入线程。负责从视频文件或RTSP流中读取原始数据。
- PreprocessThread:预处理线程。接收原始数据,并进行解码、缩放等预处理操作,为模型推理做准备。
- DetectInferenceThread:推理线程。接收预处理好的数据,执行模型推理。
- PostprocessThread:后处理线程。对推理结果进行解析,例如,在目标检测任务中,解析出物体的位置坐标和类别。
- DataOutputThread / RtspPushThread:输出线程。将处理结果进行可视化(例如,在图像上绘制检测框),然后保存为视频文件或推送到一个新的RTSP流中,供其他应用消费。
这些线程各司其职,通过消息队列(Message Queue)进行通信,实现了任务的解耦和并行执行。例如,当推理线程正在处理第N帧时,预处理线程可以同时处理第N+1帧,而数据输入线程则在读取第N+2帧,从而实现了高效的流水线作业。
2.2 test.json:灵活的配置与调度中心
这个并行架构的强大之处在于其灵活性,而这种灵活性很大程度上来自于test.json这个配置文件。它就像是整个系统的大脑,指挥着各个线程如何工作,以及如何在硬件资源上进行分配。
让我们看一个简化的test.json示例:
json
{
"device_config":[
{
"device_id":0,
"model_config":[
{
"infer_thread_name":"infer_thread_0",
"model_path":"../model/yolov7x.om",
"model_width":640,
"model_heigth":640,
"io_info":[
{
"input_path":"../data/car0.mp4",
"input_type":"video",
"output_path":"../out/outcar0.mp4",
"output_type":"video",
"channel_id":0
},
{
"input_path":"rtsp://admin:password@192.168.1.100/stream1",
"input_type":"rtsp",
"output_path":"rtsp://localhost:8554/stream_out_0",
"output_type":"rtsp",
"channel_id":1
}
]
}
]
},
{
"device_id":1,
"model_config":[
{
"infer_thread_name":"infer_thread_1",
"model_path":"../model/yolov7_person.om",
// ...
"io_info":[
{
"input_path":"../data/person.mp4",
// ...
}
]
}
]
}
]
}
通过这个JSON文件,我们可以实现非常复杂的配置:
- 多路输入 :在
io_info数组中,我们可以定义多个输入源。在上面的例子中,device_id: 0上的模型同时处理一个本地视频文件 (car0.mp4) 和一个RTSP流。系统会为每个输入源创建一套独立的DataInput->Preprocess->Postprocess->DataOutput线程链。 - 多卡调度 :在
device_config数组中,我们可以定义多个设备。上面的例子中,系统会在device_id: 0上加载yolov7x.om模型来处理车辆相关的视频流,同时在device_id: 1上加载yolov7_person.om模型来处理行人相关的视频流。这实现了真正的多卡并行计算。 - 资源共享 :同一个设备上的多个输入流可以共享同一个推理线程(
DetectInferenceThread),从而节省模型加载和内存占用的开销。
2.3 main.cpp:线程的创建与管理
main.cpp中的CreateALLThreadInstance函数是实现这一切魔法的核心。它负责解析test.json文件,并根据配置动态地创建所有需要的线程实例。
cpp
// main.cpp in sampleYOLOV7MultiInput (Simplified)
void CreateALLThreadInstance(vector<AclLiteThreadParam>& threadTbl, AclLiteResource& aclDev)
{
// ... 读取并解析 test.json ...
if (reader.parse(srcFile, root))
{
// 遍历所有设备配置
for (int i = 0; i < root["device_config"].size(); i++)
{
uint32_t deviceId = root["device_config"][i]["device_id"].asInt();
aclrtContext context = aclDev.GetContextByDevice(deviceId);
// 遍历该设备上的所有模型配置
for (int j = 0; j < root["device_config"][i]["model_config"].size(); j++)
{
// 1. 创建推理线程
AclLiteThreadParam inferParam;
inferParam.threadInst = new DetectInferenceThread(modelPath);
// ... 设置线程参数 ...
threadTbl.push_back(inferParam);
// 2. 遍历该模型的所有输入输出配置
for (int k = 0; k < root["device_config"][i]["model_config"][j]["io_info"].size(); k++)
{
// 3. 为每个IO创建一套处理线程
// 创建 DataInputThread
AclLiteThreadParam dataInputParam;
dataInputParam.threadInst = new DataInputThread(...);
threadTbl.push_back(dataInputParam);
// 创建 PreprocessThread, PostprocessThread, DataOutputThread ...
// ...
}
}
}
}
}
这段代码的逻辑清晰地反映了配置文件的结构:逐层遍历device_config -> model_config -> io_info,并依次创建推理线程和与之关联的各个处理线程。
3. 总结
从单路RTSP流的简单处理,到多路输入、多线程流水线、多卡调度的复杂并行架构,我们看到了CANN在处理真实世界视频AI应用时的强大能力和灵活性。
- 对于简单的应用,
AclLiteVideoProc提供了极其便利的接口,可以快速上手。 - 对于追求极致性能和高吞吐量的复杂场景,
sampleYOLOV7MultiInput所展示的基于配置文件的多线程并行流水线架构,提供了一个绝佳的参考实现。它将硬件能力、软件调度和应用需求完美地结合在了一起。
通过理解这两个示例,你不仅学会了如何处理视频流,更重要的是,掌握了如何根据应用需求设计不同复杂度的AI处理系统。在后续的开发中,你可以基于sampleYOLOV7MultiInput的框架,通过修改test.json或自定义新的线程类型,来快速适配更多样化的AI应用场景。