
前言YOLO 系列在昇腾NPU上跑推理NMS、ROIAlign 这些后处理算子的性能经常拖后腿。ops-cv 仓是 CANN 的计算机视觉类算子库专门处理这些后处理计算。这篇文章拿 YOLOv8 做例子实战演示一遍这些算子怎么用。目标检测的流水线目标检测的典型流水线是Backbone → Neck → Head → NMS。Backbone 提特征Neck 做特征金字塔Head 出检测框和类别分数NMS 过滤重叠框。在昇腾NPU上跑这个流水线性能瓶颈往往不在 BackboneCNN 推理已经很成熟了而在于后处理。原因是 NMS 里面有一大堆排序和比较操作这些在 CPU 上跑很慢在 NPU 上跑又不太划算NPU 的矩阵乘很强但标量比较很弱。ops-cv 仓就是来解决这个问题的。它提供了 NMS、ROIAlign、BboxTransform 等后处理算子能在 NPU 上跑完整个检测流水线不用把结果回传 CPU。ops-cv 提供的关键算子ops-cv 仓的核心算子就三个但每个都不简单NMSNon-Maximum Suppression中文叫非极大值抑制作用是把重叠的检测框合并成一个。输入是一堆候选框和分数输出是过滤后的框。算法是先按分数排序然后从高到低挑框跟后面的框比 IoU超过阈值的就扔掉。这个过程看起来简单但排序和 IoU 计算都很耗时。ROIAlign来自 Mask R-CNN是一个从特征图中抠出 ROIRegion of Interest区域并做池化的操作。相比 ROI PoolingROIAlign 用双线性插值避免了量化误差精度更高。实现难点在于怎么高效地从特征图上取值怎么处理边界情况。BboxTransform把 anchor box 转换成最终的检测框。网络输出的 delta 需要跟 anchor box 做一个解码这个解码过程就是 BboxTransform。YOLOv8 在昇腾NPU上的部署流程YOLOv8 的输出有三个bbox检测框坐标、objectness目标分数、class_probs类别概率。在昇腾NPU上跑 YOLOv8 推理的完整流程是输入图像 - DVPP 预处理 - Resize/归一化 ↓ Backbone (CBS C2f C3) - 特征图 P3, P4, P5 ↓ Head (检测头) - 输出三个尺度的 feature ↓ 后处理 (这里用 ops-cv 的算子) ├── BboxTransform: 三个尺度的输出做解码 ├── Concat: 合并三个尺度的检测结果 └── NMS: 过滤重叠框 ↓ 输出检测结果重点在后面三步解码、合并、NMS。这三步在 CPU 上跑大概占 30% 的总延迟搬到 NPU 上能降到 5% 以下。关键代码示例先看 BboxTransform 算子怎么用。网络输出的是相对于 anchor 的偏移量要转换成真实的检测框坐标importtorch_npufromtorch_npu.contribimportnpu_ops# 假设网络输出的 bbox 是 (batch, num_anchors, 4)# 4 个值分别是 dx, dy, dw, dh相对 anchor 的偏移# anchor 是预设的先验框# bbox_transform 就是把偏移量解码成真实坐标# 网络输出bbox_deltatorch.randn(1,25200,4,dtypetorch.float16).npu()objectnesstorch.rand(1,25200,dtypetorch.float16).npu()class_probstorch.rand(1,25200,80,dtypetorch.float16).npu()# 先验框 (anchor)anchorstorch.tensor([[0,0,32,32],[0,0,64,64],# 简化的 anchor 示例# ... 更多 anchor],dtypetorch.float16).npu()# BboxTransform: 解码出真实的检测框坐标# 输出格式是 (x1, y1, x2, y2)bboxesnpu_ops.bbox_transform(anchors,# 先验框bbox_delta,# 网络预测的偏移量clip_borderTrue,# 是否截断到图像边界eps1e-6)# bboxes shape: (1, 25200, 4)NMS 算子是整个后处理的核心把重叠的框过滤掉# NMS: 非极大值抑制# 输入是检测框和分数输出是最终的检测结果# 合并 objectness 和 class_probs 得到最终分数scores(objectness.unsqueeze(-1)*class_probs).max(dim-1)[0]# scores shape: (1, 25200)# NMS 算子# 参数说明# - boxes: 检测框坐标 (x1, y1, x2, y2)# - scores: 检测分数# - max_num: 最多保留多少个框# - iou_threshold: IoU 阈值超过这个值就过滤掉# - score_threshold: 分数阈值低于这个值直接扔掉keep_indices,num_keptnpu_ops.nms(bboxes.squeeze(0),# (25200, 4)scores.squeeze(0),# (25200,)max_num100,# 最多保留 100 个框iou_threshold0.45,# IoU 阈值 0.45score_threshold0.25# 分数阈值 0.25)print(f保留的框数量:{num_kept})# 输出可能是: 保留的框数量: 35这里有个坑NMS 的输出 indices 是排序后的需要用num_kept来截取有效结果。如果num_kept 35但keep_indices可能包含 100 个元素因为 NMS 输出固定长度后 65 个是无效的。完整的 YOLOv8 后处理代码串起来是这样的defyolov8_postprocess(outputs,anchors,image_shape,conf_thresh0.25,iou_thresh0.45): YOLOv8 后处理完整流程 outputs: 网络输出列表 [(batch, 25200, 85), ...] anchors: 先验框 image_shape: 原始图像尺寸 (h, w) # 1. 解码三个尺度的输出decoded_boxes[]fori,outputinenumerate(outputs):bbox_deltaoutput[...,:4]scoresoutput[...,4:]# 每个尺度有自己的 anchorbboxnpu_ops.bbox_transform(anchors[i],bbox_delta,clip_borderTrue)decoded_boxes.append(bbox)# 2. 合并三个尺度all_boxestorch.cat(decoded_boxes,dim1)# (batch, total_anchors, 4)all_scorestorch.cat([o[...,4:].max(dim-1)[0]foroinoutputs],dim1)# 3. 逐样本做 NMSfinal_results[]batch_sizeall_boxes.shape[0]forbinrange(batch_size):boxesall_boxes[b]# (N, 4)scoresall_scores[b]# (N,)# 过滤低分框maskscoresconf_thresh boxesboxes[mask]scoresscores[mask]ifboxes.shape[0]0:final_results.append(None)continue# NMSindices,numnpu_ops.nms(boxes,scores,max_num100,iou_thresholdiou_thresh,score_thresholdconf_thresh)# 截取有效结果valid_boxesboxes[:num]valid_scoresscores[:num]final_results.append((valid_boxes,valid_scores))returnfinal_resultsDVPP 预处理昇腾NPU 有自己的硬件编解码模块 DVPPDigital Video Pre-Processor做图像预处理比 CPU 快得多。典型的流程是# DVPP 预处理解码 Resize 归一化# 输入是原始图像可以是 JPEG/PNG输出是 NPU 能吃的 tensor# 假设有一张图片的路径image_pathdog.jpg# 用 DVPP 解码 JPEG - YUV420# 用 DVPP Resize - 640x640# 用 DVPP 转成 RGB - tensor# CANN 8.0 的 DVPP 接口fromtorch_npu.npu.dvppimportdvpp_process# 输入图片路径列表输出归一化后的 tensorinput_tensordvpp_process([image_path],# 图片路径target_size(640,640),# 目标尺寸mean[0,0,0],# 归一化均值std[255,255,255]# 归一化标准差)# output shape: (1, 3, 640, 640), dtype: float32DVPP 预处理的延迟大概在 5-10ms比 CPU OpenCV 的 20-30ms 快一倍左右。关键是整个过程在昇腾芯片上完成数据不用在 CPU 和 NPU 之间搬来搬去。性能数据YOLOv8s 在昇腾 910 上的端到端性能阶段延迟 (ms)占比DVPP 预处理510%Backbone2550%Head1020%后处理 (ops-cv)1020%总计50100%可以看到后处理NMS BboxTransform占 20% 的延迟已经是比较优化的水平了。如果后处理在 CPU 上跑这个比例会升到 40% 甚至 50%。注意事项ops-cv 的 NMS 算子有几个点需要注意第一是输入格式。NMS 算子要求的 bbox 坐标格式是(x1, y1, x2, y2)而不是(cx, cy, w, h)。很多框架输出的是后者需要先转换。第二是batch 维度。NMS 算子一般不支持 batch 处理需要逐样本调用。如果 batch_size 很大循环调用会有额外开销。第三是阈值选择。conf_thresh 和 iou_thresh 这两个阈值对结果影响很大。conf_thresh 设高了会漏检设低了框太多 NMS 处理慢。iou_thresh 设高了框重叠设低了相近的物体容易被误杀。目标检测的后处理是昇腾NPU 优化的重点方向之一。ops-cv 提供的 NMS、ROIAlign、BboxTransform 这些算子已经帮开发者屏蔽了底层细节直接调用就能把整个检测流水线跑在 NPU 上。仓库地址https://atomgit.com/cann/ops-cv