CANN Samples(八):典型检测与后处理加速
在前面的文章中,我们已经多次接触到目标检测任务,但之前的讨论大多聚焦于模型推理本身。我们把图片或视频流送入模型,模型输出结果,似乎任务就完成了。然而,在真实的AI应用中,这仅仅是故事的一半。模型输出的原始数据(通常是结构复杂、数据量巨大的张量)并不能直接使用,它必须经过一个关键的“化妆”阶段——后处理,才能转换成我们真正需要的、清晰明了的结果,比如给物体画上边界框,或是统计目标的数量。
这个后处理阶段,尤其在目标检测这类任务中,常常因为计算量巨大,而成为整个AI应用流程的性能瓶颈。如果后处理跟不上,即便模型推理再快,用户看到的也会是卡顿和延迟。
因此,这篇文章将带你深入探讨目标检测的“最后一公里”——后处理的优化。我们将以经典的YOLOv7模型为例,从一个标准的CPU实现开始,逐步揭示一种将后处理也交给NPU硬件加速的巧妙方法。最后,我们还会跳出常规的“画框”思维,通过一个有趣的人群计数案例,来拓宽我们对“检测”和“后处理”的理解。
我们将通过分析三个关键示例展开:
sampleYOLOV7:一切的基础,标准的、完全在CPU上执行的后处理流程。sampleYOLOV7NMSONNX:性能飞跃的关键,一种将后处理中最耗时的NMS操作也打包成模型、交由NPU硬件来完成的加速技巧。sampleCrowdCounting:换个思路看问题,一种后处理核心不再是定位,而是“计数”的非典型检测任务。
1. 标准的CPU后处理:sampleYOLOV7 详解
我们首先来看最常规的实现方式。在sampleYOLOV7这个C++示例中,后处理的所有计算都由CPU来完成。这套流程虽然直观,但当需要处理高帧率视频或海量图片时,CPU的负担会非常重。
其核心后处理逻辑位于GetResult函数中,可以清晰地分为三步。
1.1 解码与初步筛选
模型推理完成后,我们得到的是一个巨大的、一维的浮点数数组。对于YOLOv7来说,这个数组包含了多达25200个“候选框”的信息。第一步就是把这些原始数据“翻译”成我们能理解的格式。
- 遍历候选框:代码会遍历这25200个候选框。
- 计算最终置信度:每个候选框都包含一个“物体置信度”(Objectness Score)和80个类别的概率。代码将这两者相乘,得到一个最终置信度。
- 阈值筛选:设置一个置信度阈值(例如0.25)。只有最终置信度高于这个阈值的候选框,才被认为是有效的,并被保留下来。同时,代码还会将候选框的坐标从相对于模型输入尺寸(如640x640)转换回原始图片的尺寸。
// sampleYOLOV7.cpp -> GetResult()
// ...
vector <BoundBox> boxes;
for (size_t i = 0; i < modelOutputBoxNum; ++i) {
// ... 计算每个候选框的最大类别概率 ...
float classConfidence = classBuff[i * totalNumber + classConfidenceIndex];
if (classConfidence >= confidenceThreshold) { // 阈值筛选
// ... 计算最终置信度,并将框的信息存入 boxes ...
}
}
// ...
1.2 非极大值抑制(NMS)
经过初步筛选后,我们可能仍然有很多个检测框指向同一个物体。NMS(Non-Maximum Suppression)的目的就是“去重”,为每个物体只保留一个最好的框。
这是一个在CPU上计算相当密集的过程:
- 排序:将所有通过筛选的检测框按置信度从高到低排序。
- 循环去重:
a. 选出当前置信度最高的框A,将它作为最终结果之一。
b. 计算其他所有框与框A的重叠程度,即IoU(交并比)。
c. 如果某个框B与框A的IoU超过了设定的NMS阈值(例如0.45),就认为框B是多余的,将其丢弃。
d. 在剩下的框中,重复上述过程,直到所有框都被处理完毕。
// sampleYOLOV7.cpp -> GetResult()
vector <BoundBox> result;
std::sort(boxes.begin(), boxes.end(), sortScore); // 按分数排序
while (boxes.size() != 0) {
result.push_back(boxes[0]); // 添加分数最高的框到结果
size_t index = 1;
while (boxes.size() > index) {
// ... 计算 IoU ...
float iou = area / (boxMax.width * boxMax.height + boxCompare.width * boxCompare.height - area);
if (iou > NMSThreshold) { // 如果 IoU 大于阈值
boxes.erase(boxes.begin() + index); // 删除冗余的框
continue;
}
++index;
}
boxes.erase(boxes.begin());
}
1.3 结果绘制
最后一步就简单了。遍历经过NMS筛选后的最终结果,使用OpenCV等图形库,将检测框、类别和置信度绘制在原始图片上,并保存。
2. NPU硬件加速后处理:sampleYOLOV7NMSONNX 的奥秘
既然CPU处理NMS那么费劲,我们能不能把这个工作也交给擅长并行计算的NPU呢?sampleYOLOV7NMSONNX这个Python示例就给出了肯定的答案。它的核心思想是:将后处理过程本身也构建成一个模型。
2.1 两阶段推理
这个方案巧妙地将一次推理拆分成了两个阶段:
- 第一阶段(主干网络推理):运行YOLOv7的主体模型(
yolov7x.om)。但这个模型的输出不再是最终的检测结果,而是被“截断”了的、未经解码的三个原始特征图(Feature Map)。 - 第二阶段(后处理网络推理):运行一个专门用于后处理的小模型(
postprocess.om)。这个模型接收第一阶段输出的三个特征图作为输入,然后在NPU上完成所有的解码和NMS操作。
2.2 动态构建后处理ONNX模型
postprocess.om是如何来的呢?答案是通过postProcessOperator.py这个脚本动态生成的。它利用ONNX的API,在代码中创建了一个计算图,这个图主要由两个CANN内置的高性能算子构成:
YoloPreDetection:这个算子负责解码,将主干网络输出的原始特征图翻译成候选框的坐标、置信度和类别信息。YoloV5DetectionOutput:这是实现加速的关键。它在NPU硬件上高效地执行NMS算法,并直接输出最终筛选好的检测框。
# postProcessOperator.py
# ... 定义输入、输出和各种参数 ...
# 创建 YoloPreDetection 节点,对三个特征图分别进行解码
yolo_pre_node_0 = helper.make_node('YoloPreDetection', ...)
yolo_pre_node_1 = helper.make_node('YoloPreDetection', ...)
yolo_pre_node_2 = helper.make_node('YoloPreDetection', ...)
# 创建 YoloV5DetectionOutput 节点,执行NMS
yolo_detect_node = helper.make_node('YoloV5DetectionOutput',
inputs = [...] # 输入是解码后的所有信息
outputs = ['box_out', 'box_out_num'], # 直接输出最终结果
iou_threshold = 0.45, # NMS参数固化在模型中
score_threshold = 0.25, # 置信度阈值也固化在模型中
biases = anchor, # Anchor信息
name ='YoloV5DetectionOutput')
# ... 将计算图保存为 postprocess.onnx ...
2.3 简洁的后处理代码
在这种架构下,主程序的后处理代码变得异常简洁。它不再需要关心NMS的复杂实现,只需要将第一阶段的输出喂给第二阶段的模型,然后直接从第二阶段的输出中拿结果即可。
# sampleYOLOV7NMSONNX.py
# 第一阶段:主干网络推理
net.yolo_inference()
# 准备第二阶段的输入
net.postprocess_process_input()
# 第二阶段:后处理网络推理
net.postprocess_inference()
# 直接获取并绘制结果
net.postprocess_get_reslut(images_path)
在postprocess_get_reslut函数中,我们看不到任何NMS的计算,只有一个简单的循环,用于将硬件已经帮我们算好的结果绘制出来。通过这种方式,CPU被彻底解放,整个检测流程的性能得到了极大的提升。
3. 另一种检测思路:sampleCrowdCounting
最后,我们来看一个完全不同的“检测”任务——人群计数。与YOLOv7试图找到“每一个”物体不同,人群计数模型的目标是估计一个区域内的“物体总量”。
3.1 密度图(Density Map)
这类模型的输出通常不是边界框列表,而是一张“密度图”。这是一张灰度图,图上每个像素的亮度代表了原始图像对应区域的人群密集程度。像素值越高,代表该区域的人越多。
3.2 积分代替NMS
既然输出是密度图,那么它的后处理方式也完全不同。我们不再需要NMS,而是需要一种方法来从密度图中得出总人数。这个方法非常直观和巧妙:对整张密度图进行积分(即所有像素值求和)。
# sampleCrowdCounting.py -> get_result()
# 从模型输出中获取密度图
outputs, _ = self.result
output_image = outputs[0, 0]
# 关键一步:将密度图所有像素值求和,得到总人数
out_number = int(np.sum(outputs).item())
# ... 后续为可视化操作 ...
np.sum(outputs)这一行代码就完成了后处理的核心计算。它简单地将密度图张量中的所有元素值相加,就得到了估算的人群总数。
3.3 可视化
为了直观地展示结果,代码会将灰度的密度图转换成彩色的“热力图”(通常用COLORMAP_JET),并将其半透明地叠加在原始图像上。这样,我们就能一目了然地看到人群的分布情况,红色区域表示人流高度集中。
4. 总结与思考
通过这三个示例的层层深入,我们对“后处理”这个看似普通却至关重要的环节,应该有了全新的、立体的认识。它不再是模型推理结束后一个简单的收尾工作,而是决定整个应用性能和最终效果的关键一环。
我们来回顾一下这趟旅程:
-
起点:标准CPU后处理 (
sampleYOLOV7)- 方法:在CPU上解码、筛选、然后执行NMS。
- 特点:逻辑清晰,代码直观,是理解后处理原理的最佳起点。但它的缺点也很明显,NMS的循环计算在面对大量目标时,会成为性能的“重灾区”。
-
进阶:NPU硬件加速后处理 (
sampleYOLOV7NMSONNX)- 方法:将后处理(特别是NMS)本身也构建成一个独立的ONNX模型,让NPU来处理。
- 特点:这是一种“软硬结合”的思维。通过CANN内置的高性能算子,我们将CPU不擅长的密集型计算任务,转移到了专用的硬件上,从而将CPU解放出来,极大地提升了端到端的处理性能。这是在追求极致性能的场景下,非常值得借鉴的工程实践。
-
换道:基于密度图的后处理 (
sampleCrowdCounting)- 方法:完全抛弃边界框和NMS,通过对模型输出的“密度图”进行积分求和,来得到最终结果。
- 特点:这个案例告诉我们,解决问题的方法不止一种。针对特定任务(如人群计数、热力图分析等),后处理的思路可以完全跳出“检测框”的框架。选择合适的模型和与之匹配的后处理策略,往往能事半功倍。
理解并掌握这些不同的后处理技术,意味着你拥有了更多的“武器”来应对复杂的AI应用场景。在未来的项目开发中,你可以不再局限于单一的实现方式,而是能够根据实际的性能要求、硬件平台和任务目标,灵活地设计出更高效、更具针对性的解决方案。这正是从一个AI模型的使用者,向一个优秀的AI应用开发者转变的关键一步。
昇腾计算产业是基于昇腾系列(HUAWEI Ascend)处理器和基础软件构建的全栈 AI计算基础设施、行业应用及服务,https://devpress.csdn.net/organization/setting/general/146749包括昇腾系列处理器、系列硬件、CANN、AI计算框架、应用使能、开发工具链、管理运维工具、行业应用及服务等全产业链
更多推荐

所有评论(0)