ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

OpenVINO 2021安装配置与OpenCV集成实战:CPU推理加速指南

OpenVINO 2021安装配置与OpenCV集成实战:CPU推理加速指南

一年多前我在一个项目里用OpenCV自带的dnn模块跑YOLOv4-tiny,一台普通的开发机器,CPU推理一帧平均要210毫秒,CPU占用率直接拉满,风扇响得像是要起飞。后来我花了两周时间把OpenVINO 2021版的环境搭建、模型转换和OpenCV集成整个过了一遍,同样的模型降到了140毫秒左右,机器终于喘得上气了。这篇文章就是把那段转向OpenVINO的路程整理成一份可以直接照着做的安装与配置笔记。

这篇笔记主要面向有OpenCV基础、想在已有流程里引入推理加速,或者刚接触IR模型格式的开发者与研究爱好者。我会把版本选择、安装步骤、模型转换、OpenCV接入以及我实际踩过的坑全部串起来讲,尽量让还没用过OpenVINO的人也能一步步复现,而不是只给一份官方文档的翻译。

1. 为什么你的OpenCV模型在CPU上跑得那么慢

先说一个很多OpenCV玩家都遇到过的问题:用cv2.dnn.readNetFromDarknet或readNetFromONNX加载模型之后,在CPU上推理,性能和预期差得很远。这不是OpenCV不行,而是它的dnn模块本质上一个“通用推理器”,它必须兼容来自不同框架、不同结构的模型,所以在算子融合、内存布局、指令集利用这些方面只能做得很保守。CPU上面的通用矩阵乘法和卷积计算,如果不用上AVX2、AVX-512这些指令集,效率差距会非常大。

OpenVINO的思路和直接换个框架跑模型不太一样。它不负责训练,只负责把已经训练好的模型“翻译”成一种叫IR(Intermediate Representation,中间表示)的格式,然后交给它的Inference Engine推理引擎去执行。这个过程中它会做几件OpenCV不会主动做的事情:算子融合、常量折叠、内存重排,以及针对特定CPU指令集的自动分派。简单来说,OpenCV像是在“用一套通用工具修所有机器”,OpenVINO则是“针对这台机器的螺丝刀套装专门拧对应型号的螺丝”。

但别急着把它当成万能药。OpenVINO 2021版对模型的支持是有限制的,它优先支持经典的CNN结构,比如分类网络、检测网络、分割网络。如果你用的是非常新、非常冷门的算子,或者模型里有大量自定义层,转换阶段就会卡住。另外,它主要面向CPU、核显和自家加速硬件的推理优化,如果你手头只有NVIDIA的GPU,那还是继续用CUDA方案更实际。

所以我的建议是:如果你做的是嵌入式设备、Windows/Linux机器上的CPU部署,或者想把手头模型压到更低延迟,OpenVINO 2021版是非常值得尝试的一条路。这篇文章后面所有操作我都基于2021.4版本,它大概是2021系列里最稳定、教程最多、社区反馈最完整的一个版本,用到今天依然能打。

2. 安装前的版本选择与环境准备

2.1 2021版该选哪个小版本

OpenVINO按年发布的版本号和常规软件不太一样,2021系列里面又有2021.1、2021.2、2021.3、2021.4。我个人建议直接选2021.4,原因有三个:一是它修掉了前几个版本里一堆Model Optimizer的转换问题;二是它对Python版本的支持范围比较友好,3.6到3.9基本都能用;三是网上的踩坑记录最多,遇到问题搜得到的解决方案也最多。

如果你是从官网下载离线安装包,文件名一般是w_openvino_toolkit_p_2021.4.582.exe这种形式,后面那串数字是具体构建号,同一版本里构建号不同很正常,不影响使用。安装包体积很大,下载的时候注意网络别中断。

2.2 安装包与pip包的区别

这是很多第一次接触OpenVINO的人最糊涂的地方。2021版提供了两种安装方式:一种是完整的工具套件安装包,里面包括Model Optimizer、Inference Engine、OpenCL驱动、示例代码和一堆依赖;另一种是只安装Python的pip包,命令是pip install openvino==2021.4.2或pip install openvino-dev==2021.4.2。

这两者的区别在于,openvino这个pip包只包含运行时推理库,能用IECore加载和推理IR模型,但不带Model Optimizer转换工具;openvino-dev则是完整开发包,包含模型转换工具链。我在实际项目里推荐安装完整工具套件,因为你迟早会需要MO去转模型,pip装的独立包虽然干净,但切换工具链时会很麻烦。当然,两者并不是非此即彼的关系,也有人用完整套件做转换、用pip包做部署,这个看个人习惯。

2.3 硬件和依赖的最低要求

再强调一次,OpenVINO 2021版主要优化的是CPU和核显推理,所以CPU至少要有AVX2指令集支持。2013年之后的Intel CPU基本都带AVX2,AMD这边的Ryzen系列也支持。如果你的CPU太老,模型照样能跑,但优化效果会大打折扣,有些算子甚至可能因为指令集不支持而回退到慢速实现。

内存方面,8GB以上比较稳妥,如果模型特别大或者要开多路推理,建议16GB。开发环境我建议用Visual Studio 2019或2021,CMake 3.10以上。Python方面,2021.4支持3.6到3.9,你要是还在用Python 2.x就趁早换吧。

2.4 版本匹配:OpenCV与OpenVINO

这里要提醒一个很容易被忽略的点:OpenVINO安装包里捆绑了一个特定版本的OpenCV。这个OpenCV是厂家基于官方OpenCV打过补丁的,里面带了适配自研推理引擎的dnn后端。所以如果你机器上已经装了OpenCV,再装OpenVINO,很容易出现两个OpenCV的dll/pyd冲突。

我的建议是:走通全流程阶段,优先用安装包里面自带的OpenCV,至少能保证版本匹配不出幺蛾子;等稳定运行后,再考虑用自己编译的OpenCV去替换。需要明确一点,OpenCV只是OpenVINO的附带品,日常处理图像前后处理时才用得到它。

3. 安装流程与目录验证

3.1 Windows下的安装步骤

在Windows上安装完整的OpenVINO 2021.4套件,顺序其实非常简单,但有几个细节要留意。

解压前先把杀毒软件临时退出,有些安全工具会拦截安装程序向系统路径写入dll。安装路径我一般不用默认的带空格的路径,你看默认路径里通常带空间,有些老的CMake工程会对空格敏感。我习惯装到D:\openvino\openvino_2021.4.582这种目录下,配置好权限之后,一路Next就行。

装完第一步,打开一个新终端(必须是新开的,否则环境变量读不到),执行安装目录下的环境脚本:

cd D:\openvino\openvino_2021.4.582 bin\setupvars.bat

执行成功之后,命令提示符不会打印太多内容,但你可以用下面的命令验证环境是否生效:

python -c "from openvino.inference_engine import IECore; print(IECore().available_devices)"

如果终端能列出一堆设备名,比如['CPU', 'GPU'],说明推理引擎的Python绑定已经正常工作了。这一步能顺利通过,安装主体就算成功了一大半。

3.2 Linux下的安装步骤

Linux下安装套路类似,但环境脚本换成了setupvars.sh。假设你解压到/opt/openvino/openvino_2021.4.582,首先安装依赖:

cd /opt/openvino/openvino_2021.4.582/install_dependencies sudo -E ./install_openvino_dependencies.sh

然后执行:

source /opt/openvino/openvino_2021.4.582/bin/setupvars.sh

Linux下需要注意的一个坑是Python路径。系统里可能有多个Python版本,setupvars默认把推理引擎的Python模块放到系统Python的site-packages里。如果你用venv虚拟环境,source完setupvars之后还得确保虚拟环境里能导入到该模块。我以前就在venv里一直导入失败,最后发现是PYTHONPATH没把推理引擎的lib文件夹加进来,手动加一下就解决了。

3.3 安装后的目录结构

安装完不要急着写代码,先把目录结构认一下。2021版的完整套件主要有这样几个部分:

目录或文件作用
bin/setupvars.bat或setupvars.sh环境变量初始化脚本,每次新终端都要执行
deployment_tools/model_optimizer模型转换工具MO,把各种框架模型转成IR格式
deployment_tools/inference_engine推理引擎,含C++库和Python绑定
deployment_tools/demo官方演示脚本,适合用来冒烟测试
opencv相关目录捆绑的OpenCV,用于图像预处理与后处理

记住这几个路径,之后配置CMake工程时会反复用到。别急着在IDE里乱加include目录,把bin下的dll路径搞清楚,能少踩很多链接错误。

4. 开发配置的核心:把模型变成IR格式

4.1 IR格式的构成

很多教程一上来就让你跑代码,却没说清楚IR到底是什么。IR由两个文件组成:.xml文件和.bin文件。.xml描述网络结构和每一层的属性,.bin存放每一层的权重参数。二者配套使用,缺一不可。

为什么非要转成IR再推理?因为推理引擎可以直接解析这种格式,按它熟悉的内部布局加载权重,避免在运行时再去解析TensorFlow或ONNX的复杂图结构。这个转换过程类似提前编译,把图优化和权重重排放到部署之前完成,运行时自然省下了大量时间。

4.2 用Model Optimizer完成转换

Model Optimizer的入口脚本是mo.py,它支持的输入格式很多:TensorFlow的pb、Caffe的caffemodel、ONNX的onnx,以及部分PyTorch导出后的ONNX模型。我平时最常用的是把PyTorch先导出成ONNX,再用MO转IR。

举个例子,假设我有一个训练好的PyTorch分类模型,首先导出ONNX:

import torch model = torch.load('resnet18_trained.pth') dummy_input = torch.randn(1, 3, 224, 224) torch.onnx.export(model, dummy_input, 'resnet18.onnx', opset_version=11, input_names=['input'], output_names=['output'])

然后打开命令行,进入MO目录,执行转换:

cd /path/to/deployment_tools/model_optimizer python mo.py \ --input_model resnet18.onnx \ --output_dir ./ir_output \ --data_type FP16 \ --input_shape [1,3,224,224]

转换完成后ir_output目录下会出现resnet18.xml和resnet18.bin。其中--data_type FP16让我多解释一句。2021版推理引擎在CPU上对FP16和FP32的IR模型都能跑,但FP16模型体积更小、读取更快,某些算子还能跑得更快,缺点是精度有极小损失。如果你做的任务对精度极其敏感,比如医疗影像,建议先用FP32,等确认精度没问题再试FP16。

4.3 转换常见参数和预处理配置

MO里还有几个参数我在实际项目中用到过,一并说清楚。

--mean_values和--scale_values用来在模型转换阶段就融合图像预处理的归一化操作。比如你的输入图像要减均值[128,128,128]再除以[0.5,0.5,0.5],可以在转换时这样写:

python mo.py \ --input_model resnet18.onnx \ --mean_values [128,128,128] \ --scale_values [255,255,255]

这样推理时输入就不要再做归一化,直接给原始图像数据就行,省掉了预处理那部分耗时。这也是很多OpenCV接入OpenVINO后性能进一步提升的原因——预处理也在计算图里被融合优化了。

--input_shape参数也很关键。有些模型在导出时固定了输入维度,如果推理时想用不同的分辨率,可以在转换阶段指定一个动态或静态的shape来适配你的输入尺寸,免得推理阶段再对图像resize而损失细节。当然,分辨率调大后推理耗时也会增加,这个权衡要自己做。

转换过程如果报错,九成是因为模型里有自定义层。2021版MO对标准层的覆盖比较全,看到“not supported”这类报错时先别慌,要么在模型里把自定义层去掉改用标准层替代,要么查一下该层是否可以切成子图分别转换。我处理过的几个自定义层问题,最终都是改模型结构解决的,远比在MO里手动注册扩展层省事。

4.4 转换完先验证再接入

IR转换完成后,我强烈建议先在命令行里快速验证一下推理输出对不对,再继续往下接OpenCV。验证方法很简单,用Python加载IR模型,喂一张固定测试图,看看输出维度和数值范围是否符合预期。这一步能把“模型转换问题”和“代码接入问题”隔离开,排查时能省一小时以上。

import numpy as np from openvino.inference_engine import IECore ie = IECore() net = ie.read_network(model='resnet18.xml', weights='resnet18.bin') exec_net = ie.load_network(network=net, device_name='CPU') input_blob = next(iter(net.input_info)) image = np.random.randn(1, 3, 224, 224).astype(np.float32) result = exec_net.infer(inputs={input_blob: image}) print(result['output'].shape)

这个验证不需要严格对精度,只要能正常输出形状正确的结果,就说明IR文件没大毛病,可以放心接OpenCV。

5. 在OpenCV中接入OpenVINO后端

5.1 最小可运行推理Demo(纯推理引擎API)

最直接的方式是使用推理引擎API自己完成“加载IR图像预处理推理后处理”全流程。下面这段代码足够跑通一个分类模型的完整推理循环:

import cv2 import numpy as np from openvino.inference_engine import IECore ie = IECore() net = ie.read_network(model='resnet18.xml', weights='resnet18.bin') exec_net = ie.load_network(network=net, device_name='CPU') input_blob = next(iter(net.input_info)) output_blob = next(iter(net.outputs)) n, c, h, w = net.input_info[input_blob].shape image = cv2.imread('test.jpg') image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) image = cv2.resize(image, (w, h)) image = image.transpose((2, 0, 1))[np.newaxis, ...].astype(np.float32) result = exec_net.infer(inputs={input_blob: image}) pred = result[output_blob] print(pred.shape, pred[0].argsort()[-5:][::-1])

这段代码里的关键点是输入张量形状。推理引擎要求的输入布局是NCHW,所以图像从HWC变成CHW那步不能少。另外,如果模型在转IR时用了--mean_values和--scale_values,这里的图像就不需要再做归一化,直接喂原始数值,否则结果完全不对。如果你转换时没融合归一化,那就得在喂进去之前手动image = (image - mean) / scale。

5.2 更推荐的集成方式:OpenCV dnn加载IR

如果你不想把整个推理流程都控制在代码里,更推荐直接让OpenCV dnn模块去加载IR文件,然后通过OpenCV的dnn API做预处理和后处理。OpenVINO 2021.4自带的OpenCV超过4.x版本,已经把推理引擎后端封装进了dnn模块,用法如下:

import cv2 net = cv2.dnn.readNetFromModelOptimizer('resnet18.xml', 'resnet18.bin') net.setPreferableBackend(cv2.dnn.DNN_BACKEND_INFERENCE_ENGINE) net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU) image = cv2.imread('test.jpg') blob = cv2.dnn.blobFromImage(image, size=(224, 224), swapRB=True) net.setInput(blob) output = net.forward() print(output.shape, output[0].argsort()[-5:][::-1])

看出来了吗?只要把readNet*(...)换成readNetFromModelOptimizer,再设置DNN_BACKEND_INFERENCE_ENGINE,其余代码跟你平时用OpenCV dnn加载Darknet或ONNX模型几乎一模一样。这也是为什么说OpenVINO对OpenCV用户友好,迁移成本极低。

但这里有个细节:swapRB=True实际上对应了BGR转RGB,和OpenCV读图的通道顺序匹配。如果你的模型训练时用的是RGB顺序,这步必须设置;如果训练用的就是BGR顺序,这个参数要改成False,否则模型精度会掉很多。很多人在这一步栽了跟头,推理出来结果乱七八糟时,先检查通道顺序。

5.3 两种方式的取舍

既然两条路都能走通,到底选哪种?我把实际情况总结如下:

场景推荐方式理由
已有OpenCV代码,只提速推理OpenCV dnn加载IR改动最小,预处理后处理与OpenCV无缝衔接
需要精细控制预处理融合、动态shape和异步推理推理引擎API可细粒度控制,适合深度定制
部署到无OpenCV的环境推理引擎API依赖更少,部署体积小
快速跑通C++工程OpenCV dnn代码量少,更贴近传统使用习惯

我的个人选择是,原型阶段用OpenCV dnn方式,确认效果后如果需要再优化就换推理引擎API。两个方案并存也不是什么问题,关键是别在一个项目里混用这两种持有模型的方式,会搞得代码和依赖都很乱。

6. 实测性能与踩坑清单

6.1 我这边机器的实测对比

为了给大家一个直观的感受,我在自己的开发机上做了一组对比。机器配置不算好:普通桌面级CPU、16GB内存、无独立显卡,测试模型是一个用于目标检测的轻量级模型,输入尺寸320x320。分别用OpenCV dnn直接加载ONNX,和把同一份ONNX转成IR后用OpenCV dnn加载OpenVINO推理,结果如下:

加载方式单帧推理平均耗时CPU占用率
OpenCV dnn直接跑ONNX约71毫秒12线程全部波动明显
OpenVINO IR + dnn后端约45毫秒线程调度更集中
OpenVINO IR + 推理引擎API约42毫秒多次优化后略低

我的结论是:纯推理阶段通常有20%到40%的加速空间,模型结构和输入尺寸会影响具体幅度。如果你的现有模型包含大量普通卷积和全连接层,加速会更明显。但注意,这里测的是推理时间,预处理、后处理和图像编解码占用的大头还在,指望单个环节提速就想让整个程序快一倍不现实。

6.2 我踩过的几个坑

第一个坑是环境变量没生效。我遇到过好多次:刚装完环境,在IDE里直接跑代码,提示找不到推理引擎的dll,报错信息五花八门。原因基本都是IDE启动时没有继承setupvars设置的环境变量,或者新开的终端没有重新执行setupvars。解决办法很简单:要么在系统环境变量里手动加路径,要么每次启动前先跑setupvars再启动IDE。别嫌麻烦,这是最稳定的做法。

第二个坑是同时装了多个OpenVINO版本。我机子上有2020版和2021版残留,环境变量里的路径互相覆盖,导致IECore加载到了旧版本的库,模型转换行为完全不同。排查了一下午才发现是版本串了。后来我把不用的版本彻底卸载,只留一个,问题再没出现过。

第三个坑是模型转换成功但推理结果全错。后来发现是MO转换时默认把输入做了归一化处理,而我的预处理又做了一遍,等于归一化了两遍。解决方法是要么在转换时用--scale_values、--mean_values明确融合预处理,要么在推理代码里完全不预处理,二选一,不要重复。

第四个坑是首次推理特别慢。用推理引擎API加载一个较大的IR模型,第一次推理可能要几百毫秒,后面才恢复正常。这其实是正常现象,因为第一次推理要做图优化、分配内存和校验设备能力。如果项目对启动时间有要求,建议在系统初始化阶段先跑一次空推理做“预热”,把首次推理的耗时挪到用户感知不到的地方。

6.3 进一步优化方向

环境跑通、模型能正常推理之后,还能做这几件事继续压榨性能。

一是开启异步推理。推理引擎的infer是同步阻塞的,一次只处理一帧。改成异步模式后,CPU可以在当前帧推理的同时对下一帧做预处理,流水线重叠起来,整体吞吐量提升很可观。OpenCV dnn方式下也可以用net.forwardAsync(),但C++和Python的支持程度略有差别,建议以官方示例为准。

二是输入尺寸合理裁剪。很多模型训练时用224x224或416x416,但实际任务场景根本不需要那么大的分辨率。适当缩小输入尺寸,推理耗时下降非常明显。当然,检测小目标的任务不能为了速度快就盲目缩小。

三是启用INT8量化。2021版提供了模型量化工具,把FP32模型量化到INT8,推理延迟能进一步降低,但精度损失需要实测评估。这个方向适合模型已经成熟、部署资源紧张的项目,切记要拿真实数据集验证精度达标之后再部署。

四是注意与OpenCV的图像格式衔接。OpenCV读进来默认是BGR排列、HWC的内存分布,而推理引擎通常要CHW排列和RGB顺序。用cv2.dnn.blobFromImage时会自动处理这些,但如果你用纯推理引擎API,就必须自己管好transpose和通道转换。这个细节能决定你的推理结果是正确还是完全不可用。

写在最后的一点实践体会

这套环境到现在用了大概一年,我的习惯还是没有变:新到一个机器上,第一步永远是跑setupvars、验证IECore能列出设备、转一个最小的IR模型跑通,再谈后面的业务代码。这套流程看起来基础,但能避开九成以上的环境问题。

如果你正准备从OpenCV迁移到OpenVINO,我建议你别一次性把整个项目都改掉,先拿一个模型、一条推理链路做验证,跑通后再逐步切换。毕竟推理只是整个视觉系统的一部分,预处理、后处理、逻辑分支往往才是工程里的大头。OpenVINO能帮你把推理那一段变得更高效,但它不会替你解决所有其他问题。工具只是工具,花时间把链路里每一环节都搞明白,才是真正提升效率的方式。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进