前言

去年接了一个工业质检项目,模型用PyTorch写的,预处理用OpenCV跑在CPU上,推理跑在昇腾NPU上。结果预处理比推理还慢——图像缩放+色彩转换+归一化,CPU上跑8ms/张,NPU推理只要3ms/张。整个流水线的瓶颈卡在CPU预处理上,NPU闲着等数据。

后来把预处理搬到ops-cv上跑,同样的流水线在NPU上只要0.4ms/张,整体吞吐翻了6倍。这个差距让我重新审视了一个问题:ops-cv到底是什么?它跟OpenCV是什么关系?

这篇文章是我实际项目中对ops-cv的理解——它不是"跑在NPU上的OpenCV",而是一种完全不同的视觉计算范式。

认知纠偏:ops-cv ≠ NPU版OpenCV

很多人第一次听说ops-cv,第一反应是"哦,OpenCV的NPU版本"。这个理解是错的。

OpenCV是函数调用式的——你调一次cv2.resize(),它就处理一张图,结果返回到CPU内存。你再调cv2.cvtColor(),又是一次CPU内存读写。每一步都是"调函数→算→返回",中间结果在CPU内存里来回搬。

ops-cv是数据流驱动的——你搭一条流水线(Resize→ColorConvert→Normalize),然后把一批图丢进去,流水线在NPU内部从头跑到尾,中间结果不回CPU。这条流水线的底层硬件是DVPP(数字视觉预处理单元),它是昇腾NPU上的专用视觉计算硬件,跟Matrix单元(算矩阵乘的)和Vector单元(算逐元素运算的)并列。

算子不是你写在Python里的那段代码。 就像"翻炒"不是厨师口头描述的步骤,而是真正落在锅里的那套动作——ops-cv的算子不是Python函数调用,而是配置DVPP硬件流水线的参数。

ops-cv的算子体系

ops-cv的算子分两大类:Image类和ObjDetect类。

Image类:图像预处理算子

Image类算子是ops-cv的核心,覆盖了视觉模型预处理的所有常见操作:

算子 功能 DVPP硬件支持 典型用途
Resize 图像缩放 ImageNet预处理
Crop 图像裁剪 随机裁剪数据增强
ColorConvert 色彩空间转换(NV12→RGB/BGR) 视频流解码后转RGB
Normalize 均值方差归一化 ImageNet标准化
Pad 图像填充 目标检测batch对齐
Flip 水平/垂直翻转 ❌(Vector单元算) 数据增强

注意Resize、Crop、ColorConvert、Normalize都有DVPP硬件支持,意味着它们可以在DVPP流水线里串联执行,中间结果不回CPU。Flip没有DVPP支持,走Vector单元,需要单独一步。

ObjDetect类:目标检测后处理算子

ObjDetect类算子是目标检测模型的后处理部分:

算子 功能 典型用途
NMS 非极大值抑制 YOLO/SSD后处理
ROIAlign ROI特征对齐 Faster R-CNN
BBoxTransform 边界框变换 Faster R-CNN

这些算子走Vector单元,不走DVPP(DVPP只做图像预处理)。

DVPP流水线:为什么ops-cv能比OpenCV快20倍

DVPP是ops-cv性能碾压OpenCV的根本原因。理解DVPP的工作方式,才能理解ops-cv为什么快。

DVPP是什么?

DVPP(Digital Vision Pre-Processing)是昇腾NPU上的专用视觉预处理硬件单元,它跟Matrix单元(算GEMM的)和Vector单元(算逐元素运算的)并列,是NPU内部三个主要计算单元之一。

DVPP的架构:

DVPP 硬件单元
  ├─ VPC(Visual Pre-Processing Core)
  │   ├─ Resize引擎(硬件缩放,支持双线性/双三次插值)
  │   ├─ Crop引擎(硬件裁剪)
  │   ├─ ColorConvert引擎(硬件色彩空间转换)
  │   └─ Pad引擎(硬件填充)
  ├─ JPEGD(JPEG解码引擎)
  ├─ PNGD(PNG解码引擎)
  └─ VDEC(视频解码引擎,H.264/H.265)

DVPP流水线的工作方式

DVPP的VPC支持流水线模式——你把Resize→Crop→ColorConvert的参数配好,然后把图像数据送进去,VPC硬件自动完成这三步,中间结果留在片上SRAM,不写回HBM。

# DVPP流水线模式示例(ops-cv提供的高级API)
import torch
from ops_cv import DVPPPipeline

# 1. 搭建流水线(配置参数,不是执行计算)
pipe = DVPPPipeline()
pipe.resize(target_size=(224, 224), interpolation="bilinear")
pipe.color_convert(src_fmt="nv12", dst_fmt="rgb")
pipe.normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])

# 2. 执行流水线(一批图一起过,中间不回CPU)
#    输入:NV12格式的原始图像(H.264解码后的格式)
#    输出:归一化后的RGB tensor(直接喂给模型)
images_nv12 = load_video_frames()  # [N, H, W, 1.5] NV12格式
output = pipe(images_nv12)  # [N, 3, 224, 224] 归一化RGB

关键:pipe(images_nv12)这一步,图像数据在DVPP内部经历了Resize→ColorConvert→Normalize三步,中间结果不离开DVPP的片上SRAM,不写HBM,不回CPU。

对比OpenCV:为什么慢?

OpenCV的做法:

import cv2
import numpy as np

# 每一步都是CPU计算 + 内存读写
img = cv2.imread("test.jpg")          # 1. 读图(磁盘→CPU内存)
img = cv2.resize(img, (224, 224))     # 2. 缩放(CPU计算,读+写CPU内存)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)  # 3. 色彩转换(CPU计算,读+写CPU内存)
img = img.astype(np.float32) / 255.0  # 4. 归一化(CPU计算,读+写CPU内存)
img = (img - mean) / std              # 5. 标准化(CPU计算,读+写CPU内存)

# 6. 搬到NPU(CPU内存→HBM,DMA搬运,~0.3ms)
tensor = torch.from_numpy(img).npu()

问题:步骤2-5每一步都要读+写CPU内存,4步就是8次内存访问。而且第6步要把数据从CPU搬到NPU,又有DMA搬运开销。

DVPP的做法:

图像数据(已经在NPU HBM上,来自视频解码器)
  → DVPP Resize(片上SRAM,0次HBM读写)
  → DVPP ColorConvert(片上SRAM,0次HBM读写)
  → DVPP Normalize(片上SRAM,0次HBM读写)
  → 写回HBM(1次HBM写入)
  → 直接喂给模型(0次CPU交互)

ops-cv的DVPP流水线,4步预处理只写1次HBM、0次CPU交互。OpenCV的4步预处理,写8次CPU内存+1次DMA搬运。这就是20倍性能差距的来源。

实战:用ops-cv替换OpenCV预处理

下面是我在工业质检项目中,用ops-cv替换OpenCV预处理的完整代码。

原始方案:OpenCV预处理 + NPU推理

import cv2
import numpy as np
import torch
from torchvision import transforms

# OpenCV预处理(CPU上跑,慢!)
def preprocess_opencv(img_path):
    img = cv2.imread(img_path)  # CPU读图
    img = cv2.resize(img, (224, 224))  # CPU缩放
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)  # CPU色彩转换
    img = img.astype(np.float32) / 255.0
    img = (img - np.array([0.485, 0.456, 0.406])) / np.array([0.229, 0.224, 0.225])
    return img

# 推理流水线
model = torch.jit.load("resnet50.pt").npu()
model.eval()

img = preprocess_opencv("test.jpg")  # CPU预处理,~8ms
tensor = torch.from_numpy(img).permute(2,0,1).unsqueeze(0).npu()  # CPU→NPU搬运,~0.3ms
with torch.no_grad():
    output = model(tensor)  # NPU推理,~3ms
# 总耗时:8 + 0.3 + 3 = 11.3ms

优化方案:ops-cv DVPP流水线 + NPU推理

import torch
from ops_cv import DVPPPipeline, JPEGDecoder

# 1. 搭建DVPP流水线(只搭一次,复用)
pipe = DVPPPipeline()
pipe.resize(target_size=(224, 224), interpolation="bilinear")
pipe.color_convert(src_fmt="nv12", dst_fmt="rgb")
pipe.normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])

# 2. JPEG解码器(用DVPP的JPEGD引擎,不走CPU)
jpeg_dec = JPEGDecoder()

# 3. 推理流水线
model = torch.jit.load("resnet50.pt").npu()
model.eval()

# 4. 单张推理
with open("test.jpg", "rb") as f:
    jpeg_data = f.read()
jpeg_data_npu = torch.frombuffer(jpeg_data, dtype=torch.uint8).npu()  # 只搬JPEG原始字节

# DVPP解码+预处理,全程在NPU上,0次CPU交互
nv12_img = jpeg_dec(jpeg_data_npu)  # DVPP JPEG解码,~0.1ms
tensor = pipe(nv12_img)  # DVPP流水线(Resize+ColorConvert+Normalize),~0.3ms

with torch.no_grad():
    output = model(tensor)  # NPU推理,~3ms
# 总耗时:0.1 + 0.3 + 3 = 3.4ms(比OpenCV方案快3.3x)

批量推理性能对比

方案 单张耗时 批量吞吐(batch=32) 预处理瓶颈
OpenCV预处理 + NPU推理 11.3 ms 94 张/秒 CPU预处理
ops-cv DVPP + NPU推理 3.4 ms 580 张/秒 无(全在NPU上)

批量场景下差距更大(6.2x),因为DVPP流水线可以并行处理多张图(VPC有多个硬件通道),而OpenCV只能在CPU上串行处理。

踩坑实录

坑1:Resize不支持任意缩放比例

问题:DVPP的Resize引擎只支持特定缩放因子(硬件限制),不是任意比例都能做。比如从1920×1080缩放到224×224,缩放因子是8.57:1,这个比例DVPP不支持。

解决方案:先Crop到最近的整数倍尺寸,再Resize:

pipe = DVPPPipeline()
# 先裁剪到2240×2240(8:1整数倍),再缩放到224×224
pipe.crop(top=0, left=340, height=2240, width=2240)  # 裁剪
pipe.resize(target_size=(224, 224), interpolation="bilinear")  # 缩放(10:1,DVPP支持)

坑2:ColorConvert的输入必须是NV12/NV21格式

问题:DVPP的ColorConvert引擎只接受NV12或NV21格式的输入(这是视频解码器的输出格式),不接受BGR/RGB。如果你用OpenCV读图(输出BGR),要先转成NV12才能进DVPP流水线。

解决方案:用DVPP的JPEGDecoder解码(输出NV12),不要用OpenCV的imread(输出BGR):

# ❌ 错误写法(OpenCV读图输出BGR,DVPP不接受)
img = cv2.imread("test.jpg")  # BGR格式
tensor = pipe(img)  # 报错!DVPP只接受NV12/NV21

# ✅ 正确写法(DVPP JPEG解码输出NV12,直接进流水线)
jpeg_dec = JPEGDecoder()
nv12 = jpeg_dec(jpeg_bytes_npu)  # NV12格式
tensor = pipe(nv12)  # 正常

坑3:DVPP流水线的输入图像宽高必须是2的倍数

问题:DVPP硬件要求输入图像的宽度和高度都是2的倍数(NV12格式的YUV420采样要求)。如果你的图像尺寸是奇数(比如1921×1081),会报错。

解决方案:先Pad到偶数尺寸:

pipe = DVPPPipeline()
pipe.pad(target_height=1082, target_width=1922, pad_value=0)  # 补1个像素
pipe.resize(target_size=(224, 224))

ops-cv在CANN架构中的位置

ops-cv位于CANN五层架构的第2层(昇腾计算服务层),属于AOL算子库的一部分:

第2层:昇腾计算服务层
  ├─ AOL 算子库
  │   ├─ NN算子(ops-nn)
  │   ├─ BLAS算子(ops-blas)
  │   ├─ CV算子(ops-cv)← 你在这里
  │   ├─ FFT算子(ops-fft)
  │   ├─ DVPP算子(ops-cv底层调用DVPP)
  │   └─ 融合算子
  ├─ AOE 调优引擎
  └─ Framework Adaptor

ops-cv跟其他组件的关系:

  • ops-cv ←→ DVPP:ops-cv的Image类算子底层调用DVPP硬件
  • ops-cv ←→ cann-recipes-infer:推理食谱里用ops-cv做预处理
  • ops-cv ←→ ops-nn:目标检测后处理(NMS等)也可以用ops-nn的算子

结尾

ops-cv的价值不在于"跟OpenCV功能一样",而在于"跟NPU推理无缝衔接,零拷贝"。OpenCV做预处理,数据要在CPU和NPU之间来回搬,预处理成了瓶颈。ops-cv做预处理,数据从头到尾在NPU上流转,DVPP流水线把预处理和推理串成一条链,吞吐翻几倍是自然的。

如果你在做视觉模型的NPU部署,尤其是推理场景(工业质检、安防监控、自动驾驶),建议把你的OpenCV预处理替换成ops-cv的DVPP流水线。光看文档是感受不到区别的,改完代码跑一把,看吞吐从94张/秒涨到580张/秒的那一刻,你就明白ops-cv为什么存在了。

https://atomgit.com/cann/ops-cv

Logo

鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。

更多推荐