昇腾CANN ops-cv 仓:昇腾NPU上的目标检测算子实战

前言

YOLO 系列在昇腾NPU上跑推理,NMS、ROIAlign 这些后处理算子的性能经常拖后腿。ops-cv 仓是 CANN 的计算机视觉类算子库,专门处理这些后处理计算。这篇文章拿 YOLOv8 做例子,实战演示一遍这些算子怎么用。

目标检测的流水线

目标检测的典型流水线是:Backbone → Neck → Head → NMS。Backbone 提特征,Neck 做特征金字塔,Head 出检测框和类别分数,NMS 过滤重叠框。

在昇腾NPU上跑这个流水线,性能瓶颈往往不在 Backbone(CNN 推理已经很成熟了),而在于后处理。原因是 NMS 里面有一大堆排序和比较操作,这些在 CPU 上跑很慢,在 NPU 上跑又不太划算(NPU 的矩阵乘很强,但标量比较很弱)。

ops-cv 仓就是来解决这个问题的。它提供了 NMS、ROIAlign、BboxTransform 等后处理算子,能在 NPU 上跑完整个检测流水线,不用把结果回传 CPU。

ops-cv 提供的关键算子

ops-cv 仓的核心算子就三个,但每个都不简单:

NMS(Non-Maximum Suppression):中文叫非极大值抑制,作用是把重叠的检测框合并成一个。输入是一堆候选框和分数,输出是过滤后的框。算法是:先按分数排序,然后从高到低挑框,跟后面的框比 IoU,超过阈值的就扔掉。这个过程看起来简单,但排序和 IoU 计算都很耗时。

ROIAlign:来自 Mask R-CNN,是一个从特征图中抠出 ROI(Region of Interest)区域并做池化的操作。相比 ROI Pooling,ROIAlign 用双线性插值避免了量化误差,精度更高。实现难点在于:怎么高效地从特征图上取值,怎么处理边界情况。

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 的偏移量,要转换成真实的检测框坐标:

python 复制代码
import torch_npu
from torch_npu.contrib import npu_ops

# 假设网络输出的 bbox 是 (batch, num_anchors, 4)
# 4 个值分别是 dx, dy, dw, dh(相对 anchor 的偏移)
# anchor 是预设的先验框
# bbox_transform 就是把偏移量解码成真实坐标

# 网络输出
bbox_delta = torch.randn(1, 25200, 4, dtype=torch.float16).npu()
objectness = torch.rand(1, 25200, dtype=torch.float16).npu()
class_probs = torch.rand(1, 25200, 80, dtype=torch.float16).npu()

# 先验框 (anchor)
anchors = torch.tensor([
    [0, 0, 32, 32], [0, 0, 64, 64],  # 简化的 anchor 示例
    # ... 更多 anchor
], dtype=torch.float16).npu()

# BboxTransform: 解码出真实的检测框坐标
# 输出格式是 (x1, y1, x2, y2)
bboxes = npu_ops.bbox_transform(
    anchors,       # 先验框
    bbox_delta,    # 网络预测的偏移量
    clip_border=True,  # 是否截断到图像边界
    eps=1e-6
)
# bboxes shape: (1, 25200, 4)

NMS 算子是整个后处理的核心,把重叠的框过滤掉:

python 复制代码
# 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_kept = npu_ops.nms(
    bboxes.squeeze(0),     # (25200, 4)
    scores.squeeze(0),     # (25200,)
    max_num=100,           # 最多保留 100 个框
    iou_threshold=0.45,   # IoU 阈值 0.45
    score_threshold=0.25   # 分数阈值 0.25
)

print(f"保留的框数量: {num_kept}")
# 输出可能是: 保留的框数量: 35

这里有个坑:NMS 的输出 indices 是排序后的,需要用 num_kept 来截取有效结果。如果 num_kept = 35,但 keep_indices 可能包含 100 个元素(因为 NMS 输出固定长度),后 65 个是无效的。

完整的 YOLOv8 后处理代码串起来是这样的:

python 复制代码
def yolov8_postprocess(outputs, anchors, image_shape, conf_thresh=0.25, iou_thresh=0.45):
    """
    YOLOv8 后处理完整流程
    outputs: 网络输出列表 [(batch, 25200, 85), ...]
    anchors: 先验框
    image_shape: 原始图像尺寸 (h, w)
    """
    # 1. 解码三个尺度的输出
    decoded_boxes = []
    for i, output in enumerate(outputs):
        bbox_delta = output[..., :4]
        scores = output[..., 4:]
        
        # 每个尺度有自己的 anchor
        bbox = npu_ops.bbox_transform(
            anchors[i], bbox_delta, clip_border=True
        )
        decoded_boxes.append(bbox)
    
    # 2. 合并三个尺度
    all_boxes = torch.cat(decoded_boxes, dim=1)  # (batch, total_anchors, 4)
    all_scores = torch.cat([o[..., 4:].max(dim=-1)[0] for o in outputs], dim=1)
    
    # 3. 逐样本做 NMS
    final_results = []
    batch_size = all_boxes.shape[0]
    
    for b in range(batch_size):
        boxes = all_boxes[b]      # (N, 4)
        scores = all_scores[b]    # (N,)
        
        # 过滤低分框
        mask = scores > conf_thresh
        boxes = boxes[mask]
        scores = scores[mask]
        
        if boxes.shape[0] == 0:
            final_results.append(None)
            continue
        
        # NMS
        indices, num = npu_ops.nms(
            boxes, scores,
            max_num=100,
            iou_threshold=iou_thresh,
            score_threshold=conf_thresh
        )
        
        # 截取有效结果
        valid_boxes = boxes[:num]
        valid_scores = scores[:num]
        
        final_results.append((valid_boxes, valid_scores))
    
    return final_results

DVPP 预处理

昇腾NPU 有自己的硬件编解码模块 DVPP(Digital Video Pre-Processor),做图像预处理比 CPU 快得多。典型的流程是:

python 复制代码
# DVPP 预处理:解码 + Resize + 归一化
# 输入是原始图像(可以是 JPEG/PNG),输出是 NPU 能吃的 tensor

# 假设有一张图片的路径
image_path = "dog.jpg"

# 用 DVPP 解码 JPEG -> YUV420
# 用 DVPP Resize -> 640x640
# 用 DVPP 转成 RGB -> tensor

# CANN 8.0+ 的 DVPP 接口
from torch_npu.npu.dvpp import dvpp_process

# 输入图片路径列表,输出归一化后的 tensor
input_tensor = dvpp_process(
    [image_path],          # 图片路径
    target_size=(640, 640), # 目标尺寸
    mean=[0, 0, 0],        # 归一化均值
    std=[255, 255, 255]    # 归一化标准差
)
# output shape: (1, 3, 640, 640), dtype: float32

DVPP 预处理的延迟大概在 5-10ms,比 CPU OpenCV 的 20-30ms 快一倍左右。关键是整个过程在昇腾芯片上完成,数据不用在 CPU 和 NPU 之间搬来搬去。

性能数据

YOLOv8s 在昇腾 910 上的端到端性能:

阶段 延迟 (ms) 占比
DVPP 预处理 5 10%
Backbone 25 50%
Head 10 20%
后处理 (ops-cv) 10 20%
总计 50 100%

可以看到后处理(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

相关推荐
互联圈运营观察2 小时前
Google I/O 2026之外,声网搞定弱网通话难题
人工智能
落日屿星辰2 小时前
ops-cv - 让计算机视觉“看得快“
人工智能·计算机视觉
数学建模导师2 小时前
2026电工杯A题电—氢—氨”耦合系统完整版解答含论文!
大数据·人工智能·数学建模
GEO从入门到精通2 小时前
GEO学习书籍或文章推荐哪本?
人工智能·学习
陌陌龙2 小时前
Sub2API 源码技术分析与搭建教程:把 AI 订阅变成可管理的 API 网关
人工智能
老虎海子2 小时前
从零入门 OpenAI Codex|登录、权限、终端、记忆配置全实操
人工智能·vscode·自然语言处理·chatgpt·个人开发·业界资讯
与芯同行2 小时前
TP9243S与TP9311双芯片:AI语音产品从采集到回放的完整解决方案
人工智能
若兰幽竹2 小时前
【大模型应用】抖音爆款视频深度分析系统:流水线式AI逆向拆解流量密码,精准预测播放量!
人工智能·python·音视频·抖音爆款分析
AI技术控2 小时前
NeuroH-TGL 论文解读:面向脑疾病诊断的神经异质性引导时序图学习方法
人工智能·语言模型·自然语言处理·langchain·nlp