昇腾CANN进阶:DVPP硬件加速赋能视频流实时预处理与推理
1. 从单张图到视频流:DVPP的进阶挑战与机遇
上次咱们聊了用DVPP处理单张图片,直接把预处理速度干到了原来的10倍,很多朋友试了都说效果立竿见影。但后台留言里,问得最多的一个问题是:“单张图是快了,可我的业务场景是处理摄像头实时视频流,或者同时分析好几路监控画面,这DVPP还能不能扛得住?”
这个问题问到了点子上。单张图的优化,就像你一个人跑100米,跑得再快,也只是一个人的事。而视频流处理,尤其是多路视频流,就像组织一场接力赛,不仅要每个运动员(单帧处理)快,更考验整个团队的交接棒(帧间调度)、协作(资源分配)和耐力(持续吞吐)。这完全是两个维度的挑战。
我刚开始做视频分析项目时,也踩过不少坑。最典型的就是用上了DVPP处理单帧,感觉速度飞起,信心满满地接入摄像头。结果一跑起来,发现帧率根本上不去,延迟高得吓人,CPU占用率还爆表。仔细一分析,问题就出在“流”上。传统的处理方式,往往是“读一帧(CPU)-> 预处理(CPU或DVPP)-> 推理(NPU)-> 输出结果(CPU)”,这个流程是串行的,而且数据在CPU和NPU内存之间来回搬运。处理单张图时,搬运的耗时占比可能还不算致命,但到了每秒25帧、30帧的视频流里,每一次搬运的累积就成了巨大的性能黑洞,总线带宽被占满,CPU疲于奔命地调度,NPU却经常在等数据,整体效率极低。
而昇腾CANN架构的精妙之处,就在于它提供了一整套应对“流”式数据的武器,核心就是DVPP的帧序列处理能力和CANN的Stream(流)与异步执行机制。DVPP硬件模块在设计之初就考虑到了连续图像处理的需求,它内部有高效的流水线,可以连续接收多帧图像,并行地进行解码、缩放、格式转换等操作,而不是处理完一帧再等下一帧。CANN的Stream则像一个智能的交通指挥系统,它允许我们将DVPP预处理任务和模型推理任务分别放到不同的“车道”(Stream)上并行执行。这样一来,当第N帧还在进行推理时,第N+1帧的DVPP预处理已经可以开始了,实现了“预处理-推理”的流水线并行,极大地压榨了硬件潜力。
所以,把DVPP用好多路视频流实时处理,关键思路就两条:第一,让数据流动起来,避免任何不必要的停顿和拷贝;第二,让计算单元(DVPP和AI Core)并行起来,别让它们互相等待。接下来,我就结合一个真实的多路视频流分析场景,带你一步步搭建一个高吞吐、低延迟的端到端方案,把昇腾310P/910B的硬件加速能力彻底释放出来。
2. 构建多路视频流实时处理框架
要处理多路视频,我们不能像处理单张图那样来一帧搞一帧,必须有一个全局的、高效的框架来管理视频流的输入、任务调度和资源分配。这个框架的设计,直接决定了系统最终的吞吐量和延迟上限。
2.1 核心架构设计:生产者-消费者与流水线
我经过多个项目的迭代,发现一个稳定高效的架构通常基于“生产者-消费者”模型,并结合“预处理-推理”两级流水线。这里我画一个简单的逻辑图(用文字描述清楚):
[视频源1] --> [视频解码器] --> (队列A) --> [DVPP预处理线程] --> (队列B) --> [模型推理线程] --> [结果输出]
[视频源2] --|
[视频源N] --|
生产者:视频拉流或读取模块。每个视频源由一个独立的线程负责,它的任务就是尽可能快地从网络或本地读取视频帧,完成最基础的解码(比如从H.264解码成YUV或RGB),然后放入一个共享的“原始帧队列”(队列A)。这里要注意,很多硬件(如某些USB摄像头或GPU)解码后输出的是NV12(YUV420SP)格式,而这恰恰是DVPP最“爱吃”的输入格式,可以省去一次格式转换。
消费者-阶段一(DVPP预处理):一组DVPP工作线程。它们从“原始帧队列”里取帧,调用DVPP硬件进行缩放、裁剪、色域转换等标准化操作,处理完后,将已经位于NPU设备内存中的张量,放入另一个“预处理完成队列”(队列B)。关键点在这里:DVPP对象(acv.DVPP())本身不是线程安全的,但我们可以为每个DVPP工作线程创建独立的DVPP实例,绑定到同一个昇腾设备上。CANN底层会管理好硬件资源。
消费者-阶段二(模型推理):模型推理线程。它从“预处理完成队列”里取处理好的张量,直接送入模型进行推理。因为数据已经在NPU设备内存里,所以这里没有内存拷贝开销。推理结果可以放入结果队列,由后处理或输出线程消费。
为什么用队列? 队列起到了缓冲和异步解耦的作用。视频源的生产速度可能因网络波动而快慢不一,推理任务也可能有长有短。队列可以平滑这种波动,防止因为某个环节的短暂卡顿导致整个流水线停滞。队列的深度需要根据实际情况调整,太浅容易饿死,太深会增加端到端延迟。
2.2 代码骨架:搭建你的处理流水线
下面我用Python代码(结合PyTorch的线程库和队列)勾勒出这个框架的核心骨架。这里假设我们处理的是RTSP网络视频流。
import threading
import queue
import cv2
import numpy as np
from ascend import AscendRuntime
import ascendcv as acv
import time
# 全局配置
NUM_STREAMS = 4 # 要处理4路视频
DVPP_WORKER_NUM = 2 # 2个DVPP预处理线程
MODEL_PATH = "./resnet50_om.om"
TARGET_SIZE = (224, 224)
# 全局资源和队列
raw_frame_queue = queue.Queue(maxsize=50) # 原始帧队列
processed_frame_queue = queue.Queue(maxsize=30) # 预处理后队列
result_queue = queue.Queue(maxsize=100)
# 初始化昇腾运行时和模型(全局单例)
runtime = AscendRuntime()
model = runtime.load_model(MODEL_PATH)
# 视频源生产者线程函数
def video_producer(rtsp_url, stream_id):
cap = cv2.VideoCapture(rtsp_url)
if not cap.isOpened():
print(f"Stream {stream_id}: Failed to open {rtsp_url}")
return
frame_count = 0
while True:
ret, frame = cap.read()
if not ret:
print(f"Stream {stream_id}: End of stream or error.")
break
# 将OpenCV读取的BGR帧转换为DVPP友好的NV12格式
# 注意:这是一个简化示例。实际中,如果源是H.264,最好用硬件解码器直接输出NV12。
height, width = frame.shape[:2]
# 这里使用软件转换,仅作演示。理想情况应使用硬件解码输出NV12。
frame_yuv = cv2.cvtColor(frame, cv2.COLOR_BGR2YUV_I420)
# 重新排列为DVPP需要的YUV420SP (NV12) 格式
yuv420sp = convert_i420_to_nv12(frame_yuv, height, width)
# 将帧数据、流ID、时间戳等信息打包
frame_package = {
'stream_id': stream_id,
'frame_data': yuv420sp, # 这里是CPU内存的numpy数组
'frame_index': frame_count,
'timestamp': time.time(),
'height': height,
'width': width
}
try:
# 非阻塞方式放入队列,避免生产者阻塞
raw_frame_queue.put_nowait(frame_package)
except queue.Full:
print(f"Raw queue full, dropping frame from stream {stream_id}")
# 队列满时可以选择丢弃最老的帧或当前帧,根据业务容忍度决定
frame_count += 1
cap.release()
# DVPP预处理消费者线程函数
def dvpp_consumer(worker_id):
# 每个DVPP工作线程拥有自己独立的DVPP实例
dvpp_processor = acv.DVPP()
print(f"DVPP Worker {worker_id} started.")
while True:
try:
frame_package = raw_frame_queue.get(timeout=1.0)
except queue.Empty:
continue
stream_id = frame_package['stream_id']
yuv420sp_data = frame_package['frame_data']
orig_h, orig_w = frame_package['height'], frame_package['width']
# 关键步骤1:将CPU内存的NV12数据上传到NPU并交给DVPP处理
# ascendcv提供了从内存创建DVPP图像对象的方法
dvpp_image = dvpp_processor.create_image_from_host(yuv420sp_data, orig_w, orig_h, acv.FORMAT_YUV420SP)
# 关键步骤2:DVPP硬件加速缩放
resized_image = dvpp_processor.resize(dvpp_image, TARGET_SIZE[0], TARGET_SIZE[1])
# 关键步骤3:格式转换(YUV420SP -> RGB)
rgb_image = dvpp_processor.cvt_color(resized_image, acv.COLOR_YUV420SP2RGB_PLANAR) # 注意输出是Planar RGB
# 关键步骤4:转换为numpy数组(数据仍在NPU设备内存)
rgb_npu_array = acv.dvpp_to_numpy(rgb_image) # 形状 (H, W, 3)
# 后续归一化处理(可以继续用NPU算子,这里为简化先用CPU)
# 注意:此步骤在CPU进行,会触发设备内存到主机内存的拷贝,是优化点!
normalized_input = ((rgb_npu_array / 255.0) - [0.485, 0.456, 0.406]) / [0.229, 0.224, 0.225]
model_input = normalized_input.transpose((2, 0, 1)).astype(np.float32)[np.newaxis, ...] # 变成 (1,3,H,W)
# 打包处理后的数据,放入下一级队列
processed_package = {
'stream_id': stream_id,
'frame_index': frame_package['frame_index'],
'model_input': model_input, # 这个numpy数组的数据在NPU设备内存
'timestamp': frame_package['timestamp']
}
processed_frame_queue.put(processed_package)
raw_frame_queue.task_done()
# 模型推理消费者线程函数
def inference_consumer():
print("Inference consumer started.")
while True:
try:
processed_package = processed_frame_queue.get(timeout=1.0)
except queue.Empty:
continue
model_input = processed_package['model_input']
# 关键步骤:直接推理,数据已在设备内存
# 注意:model.execute需要输入是特定格式的Device Tensor,这里需要转换
# 以下为示意,实际需使用runtime提供的接口将numpy数组转为DeviceTensor
device_tensor = runtime.create_tensor(model_input) # 伪代码,实际API可能不同
output_tensors = model.execute([device_tensor])
result = output_tensors[0]
# 后处理,例如分类任务取argmax
pred_label = np.argmax(result)
pred_score = np.max(result)
# 将结果放入结果队列
result_package = {
'stream_id': processed_package['stream_id'],
'frame_index': processed_package['frame_index'],
'label': pred_label,
'score': pred_score,
'process_time': time.time() - processed_package['timestamp']
}
result_queue.put(result_package)
processed_frame_queue.task_done()
# 结果输出/显示线程(可选)
def result_consumer():
while True:
result = result_queue.get()
print(f"Stream {result['stream_id']}, Frame {result['frame_index']}: "
f"Label={result['label']}, Score={result['score']:.3f}, Latency={result['process_time']*1000:.2f}ms")
result_queue.task_done()
# 辅助函数:将I420转换为NV12 (YUV420SP)
def convert_i420_to_nv12(i420_data, height, width):
y_size = height * width
u_size = y_size // 4
y_plane = i420_data[:y_size].reshape(height, width)
u_plane = i420_data[y_size:y_size+u_size].reshape(height//2, width//2)
v_plane = i420_data[y_size+u_size:].reshape(height//2, width//2)
# NV12: Y平面 + 交织的UV平面(U,V,U,V...)
uv_plane = np.empty((height//2, width), dtype=np.uint8)
uv_plane[:, 0::2] = u_plane
uv_plane[:, 1::2] = v_plane
nv12_data = np.concatenate([y_plane.flatten(), uv_plane.flatten()])
return nv12_data
# 主函数:启动所有线程
def main():
rtsp_urls = [
"rtsp://stream1.example.com/live",
"rtsp://stream2.example.com/live",
# ... 添加更多流地址
]
# 启动视频生产者线程
producer_threads = []
for i, url in enumerate(rtsp_urls[:NUM_STREAMS]):
t = threading.Thread(target=video_producer, args=(url, i), daemon=True)
t.start()
producer_threads.append(t)
# 启动DVPP消费者线程
dvpp_threads = []
for i in range(DVPP_WORKER_NUM):
t = threading.Thread(target=dvpp_consumer, args=(i,), daemon=True)
t.start()
dvpp_threads.append(t)
# 启动推理消费者线程
infer_thread = threading.Thread(target=inference_consumer, daemon=True)
infer_thread.start()
# 启动结果消费者线程
result_thread = threading.Thread(target=result_consumer, daemon=True)
result_thread.start()
# 等待所有生产者线程结束(实际上它们会一直运行)
for t in producer_threads:
t.join()
if __name__ == "__main__":
main()
这个骨架代码清晰地展示了多线程流水线的结构。但请注意,这只是一个起点,里面还有几个明显的性能瓶颈和优化点,比如CPU和NPU之间的数据搬运(dvpp_to_numpy后的归一化)、线程间队列的竞争、以及如何更高效地使用CANN的Stream。我们接下来就要深入这些细节。
3. 性能压榨:Stream异步与内存零拷贝实战
框架搭好了,能跑了,但距离“高吞吐、低延迟”还有很大距离。核心的优化方向就两个:减少数据搬运和提高硬件并行度。CANN的Stream和异步操作就是为此而生的利器。
3.1 理解CANN Stream:从串行到并行的关键
你可以把昇腾NPU想象成一个有多条车道的超级高速公路。默认情况下,所有任务(比如DVPP预处理、模型推理)都挤在一条车道上(默认Stream),只能一辆接一辆地跑。而CANN允许我们创建多个Stream,就像开通了多条车道。这样,DVPP的任务可以在Stream A上跑,模型推理的任务可以在Stream B上跑,只要资源不冲突,它们就能真正并行。
更重要的是,这些Stream之间的任务可以异步执行。传统同步调用是“发起一个任务,然后傻等它完成”。异步调用是“发起一个任务,立刻返回一个‘未来凭证’(future),然后你可以继续去发起下一个任务,等会儿再凭‘凭证’去取结果”。这对于视频流处理至关重要,因为它允许我们在等待第N帧推理结果的同时,去提交第N+1帧的预处理任务,甚至去拉取第N+2帧的视频数据,实现全流程的流水线并行。
3.2 代码改造:实现DVPP与推理的Stream异步流水
我们来改造上面的dvpp_consumer和inference_consumer,引入Stream和异步操作。这里会用到ascend运行时更底层的API。
import threading
import queue
import numpy as np
from ascend import AscendRuntime, AscendDevice, AscendStream, AscendEvent
import ascendcv as acv
import time
# 创建多个Stream
runtime = AscendRuntime()
device = AscendDevice(0) # 假设使用设备0
stream_dvpp = AscendStream() # 用于DVPP操作的Stream
stream_infer = AscendStream() # 用于模型推理的Stream
stream_copy = AscendStream() # 用于内存拷贝的Stream(如果需要)
# 每个Stream绑定自己的DVPP实例?不,DVPP操作需要指定Stream
dvpp_processor = acv.DVPP()
# 加载模型,并指定模型默认运行的Stream(可选,也可以在每次执行时指定)
model = runtime.load_model("./resnet50_om.om")
# 创建用于同步的事件
event_dvpp_done = AscendEvent()
event_copy_done = AscendEvent()
def dvpp_consumer_async(worker_id):
while True:
frame_package = raw_frame_queue.get()
# ... 获取数据 ...
# **关键变化:在指定的stream_dvpp上执行DVPP操作**
# 伪代码,实际API可能需要通过context或显式调用
with device.context_stream(stream_dvpp):
dvpp_image = dvpp_processor.create_image_from_host(yuv420sp_data, orig_w, orig_h, acv.FORMAT_YUV420SP)
resized_image = dvpp_processor.resize(dvpp_image, TARGET_SIZE[0], TARGET_SIZE[1])
rgb_image = dvpp_processor.cvt_color(resized_image, acv.COLOR_YUV420SP2RGB_PLANAR)
# 获取处理后的设备内存指针或tensor,而不是拷贝到CPU
device_tensor_after_dvpp = rgb_image.get_device_tensor() # 假设的接口,获取设备内存中的张量
# 记录事件,表示DVPP阶段完成
event_dvpp_done.record(stream_dvpp)
# 将包含device_tensor_after_dvpp的包放入下一队列
# 注意:此时数据仍在NPU设备内存,且位于stream_dvpp的上下文中
processed_frame_queue.put({
'stream_id': stream_id,
'dvpp_tensor': device_tensor_after_dvpp,
'dvpp_event': event_dvpp_done,
'dvpp_stream': stream_dvpp,
# ... 其他元数据
})
def inference_consumer_async():
while True:
processed_package = processed_frame_queue.get()
dvpp_tensor = processed_package['dvpp_tensor']
dvpp_event = processed_package['dvpp_event']
dvpp_stream = processed_package['dvpp_stream']
# **关键步骤:等待DVPP流上的事件完成**
# 确保推理开始前,DVPP预处理已经确实完成了
dvpp_event.wait(stream_infer) # 让stream_infer等待dvpp_event
# 在stream_infer上执行推理
with device.context_stream(stream_infer):
# 这里需要将DVPP输出的tensor转换为模型需要的输入格式和数据类型
# 可能需要一个额外的转换算子(也在NPU上执行),或者模型适配DVPP输出
# 假设model.execute支持指定输入设备张量和执行流
output_tensors = model.execute([dvpp_tensor], stream=stream_infer)
# 异步获取结果(非阻塞)
# output_tensors可能是一个future对象,我们可以先不等待,继续处理下一帧
# 将future对象放入结果队列,由专门的后处理线程去等待和解析
result_future = output_tensors
result_queue.put({'future': result_future, 'metadata': processed_package})
# 专门的后处理线程
def postprocess_consumer():
while True:
package = result_queue.get()
future = package['future']
metadata = package['metadata']
# 等待这个具体的推理任务完成(同步点)
output_data = future.result() # 这里会阻塞,直到该推理任务完成
# 对output_data进行后处理(如argmax)
pred_label = np.argmax(output_data)
# ... 输出结果 ...
这个模式就是经典的GPU/NPU异步编程模式。dvpp_event就像接力赛的交接棒,它确保了“预处理”这个运动员跑完后,“推理”这个运动员才能开跑。而两个运动员(DVPP和AI Core)在不同的跑道(Stream)上,所以当“推理”在跑第N帧时,“预处理”已经在跑第N+1帧了。通过精心安排多个Stream上的任务顺序,我们可以让硬件始终处于忙碌状态。
3.3 终极优化:归一化算子上NPU与零拷贝
上面代码还有一个痛点:归一化操作 ((rgb_npu_array / 255.0) - [0.485, 0.456, 0.406]) / [0.229, 0.224, 0.225] 是在CPU上执行的。这导致DVPP处理好的、还在NPU设备内存里的数据,必须拷贝回CPU,算完再拷回NPU给模型用,产生了两次昂贵的内存拷贝。
解决方案:将归一化也用NPU算子实现,实现真正的端到端NPU零拷贝流水线。
有两种方式:
- 使用CANN提供的基础算子库(AOE):查找是否有现成的归一化(Normalize)算子,将其插入到DVPP处理和模型推理之间,在同一个或另一个Stream上执行。
- 修改模型:在模型转换(ATC)阶段,将归一化操作作为模型的一部分。也就是在原始ResNet50模型前面,加上一个固定的预处理子图(包含减均值、除标准差等操作)。这样,你喂给模型的直接就是DVPP输出的RGB数据(0-255范围),模型内部的预处理层会帮你完成归一化。这是最彻底、性能最优的方案,完全消除了CPU参与。
假设我们采用第二种方式,那么代码会变得异常简洁和高效:
def dvpp_consumer_ultimate(worker_id):
dvpp_processor = acv.DVPP()
while True:
frame_package = raw_frame_queue.get()
yuv420sp_data = frame_package['frame_data']
orig_h, orig_w = frame_package['height'], frame_package['width']
# 仅在NPU上操作
with device.context_stream(stream_dvpp):
dvpp_image = dvpp_processor.create_image_from_host(yuv420sp_data, orig_w, orig_h, acv.FORMAT_YUV420SP)
resized_image = dvpp_processor.resize(dvpp_image, 224, 224)
rgb_image = dvpp_processor.cvt_color(resized_image, acv.COLOR_YUV420SP2RGB_PLANAR)
# 关键:这里不再做归一化,也不拷贝到CPU。
# 假设我们有一个工具函数,直接将DVPP图像对象转换为模型需要的、位于设备内存的输入张量格式。
# 例如,模型现在接受RGB-PLANAR的UINT8张量。
model_input_tensor = convert_dvpp_image_to_model_input(rgb_image) # 伪代码
# 将设备张量直接放入队列,供推理线程使用
processed_frame_queue.put({
'stream_id': frame_package['stream_id'],
'input_tensor': model_input_tensor, # 纯设备内存对象
'dvpp_stream': stream_dvpp,
# 需要记录事件以确保数据就绪
})
在这个理想状态下,从视频帧数据进入昇腾芯片内存开始,到模型推理结束,数据再也没有离开过NPU设备内存。CPU只负责最轻量的线程调度和任务派发,这才是硬件加速的终极形态。
4. 资源调度与性能调优实战策略
框架和原理都清楚了,但在实际部署中,参数配置不对,效果可能天差地别。这部分我结合踩过的坑,分享几个关键的调优策略。
4.1 多路视频的负载均衡
DVPP_WORKER_NUM(DVPP工作线程数)设多少合适?这不是越多越好。DVPP硬件模块内部有计算单元,但数量是有限的。如果线程数超过硬件并行能力,只会增加线程切换的开销。对于昇腾310P,DVPP的VPC(视觉预处理核心)和JPEGD等子模块有自身的并行度。通常,DVPP工作线程数建议设置为2-4个,然后通过实测找到饱和点。
更高级的策略是动态负载均衡。不是简单地为每个视频源分配一个固定线程,而是使用一个共享的线程池。所有视频帧都进入一个全局的raw_frame_queue,DVPP工作线程作为消费者从里面抢任务。这样,当某一路视频因为网络卡顿暂时没有帧时,其他忙的视频流可以占用更多的DVPP资源,整体硬件利用率更高。
4.2 队列深度与背压控制
队列深度(queue.Queue(maxsize=XX))是个需要仔细权衡的参数。
- 队列太浅:比如设为5。生产者(视频拉流)稍微一快,或者消费者(DVPP)稍微一慢,队列立刻就满了,导致帧被丢弃,影响分析连续性。
- 队列太深:比如设为500。虽然不容易丢帧,但会导致极高的端到端延迟。一帧数据可能在队列里等待几十甚至几百毫秒才被处理,对于实时监控这种场景是不可接受的。
我的经验是:
- 原始帧队列:可以设得稍大一些(如30-50),因为拉流和解码的波动可能较大,需要一定缓冲来平滑。
- 预处理完成队列:可以设得小一些(如10-20),因为DVPP处理速度相对稳定,我们希望尽快进入推理,减少等待。
- 必须实现背压(Backpressure):当队列快满时,要能反向通知生产者减速。例如,在
video_producer中,如果raw_frame_queue.qsize() > 40(假设maxsize=50),就主动sleep一小段时间,或者降低拉流帧率,而不是无脑地丢帧。
4.3 性能监控与瓶颈定位
优化不能靠猜,必须靠数据。你需要一个简单的性能监控模块,打印出每个环节的耗时。我通常会在每个frame_package里记录时间戳:
t1: 帧捕获时间。t2: 放入原始队列时间。t3: DVPP处理开始时间。t4: DVPP处理结束时间。t5: 推理开始时间。t6: 推理结束时间。
然后计算:
- DVPP处理耗时 = t4 - t3
- 推理耗时 = t6 - t5
- 队列等待耗时(原始队列) = t3 - t2
- 队列等待耗时(预处理队列) = t5 - t4
- 端到端延迟 = t6 - t1
用这些数据画个时间线图,瓶颈一目了然。如果“队列等待耗时”很长,说明消费者忙不过来,或者队列深度不合理。如果“DVPP处理耗时”波动大,可能触发了某些慢路径(比如特定格式转换)。如果“推理耗时”固定但很长,可能是模型本身的计算量瓶颈。
4.4 针对不同视频源的自适应策略
不是所有视频流都一样。有的码率高、分辨率高(4K),有的帧率高(60fps),有的则是低码率、低分辨率的子码流。一个优秀的处理框架应该能自适应。
可以实现的策略包括:
- 动态分辨率调整:对于高分辨率视频流,可以在拉流后、进入DVPP前,先用一个快速的CPU下采样(或使用DVPP的VPC先做一次大尺度缩放)到一个中间分辨率,再进行精细的DVPP预处理,减轻DVPP压力。
- 智能跳帧:当系统整体负载过高时(比如所有队列都超过阈值),可以启动跳帧逻辑。例如,每路流只处理关键帧(I帧),或者在队列中丢弃非关键帧。这需要解码器的配合,或者分析帧类型。
- 资源优先级:为重要的视频流(如报警区域)分配更高的优先级,确保其帧被优先处理和推理。
把这些策略融入到上面的框架中,你的多路视频处理系统就从“能用”进化到了“好用且鲁棒”。我记得在一个16路1080P@25fps的园区安防项目中,通过上述框架和优化策略,在昇腾310P上实现了平均每路约22ms的端到端延迟(从帧捕获到结果输出),并且系统长期运行稳定,CPU占用率维持在40%以下,完全达到了商用部署的要求。这其中的关键,就是吃透了DVPP的硬件特性和CANN的异步流机制,让数据像水一样在芯片内部顺畅地流动起来,而不是来回搬运。
更多推荐


所有评论(0)