前言

​ 我一直都知道我学的c++和工程c++不一样,所以知道这篇文章之前我连库都不知道如何安装,毕竟刷算法题不需要也不能用额外的库。我也花了很大精力从舒适区走出来,终于成功c++部署modnet|RVM上线,我感觉收获了很多。对比之下用python真简单

注意,本文章并不是教程,更多是避坑记录,面向已经阅读过网上多数教程后需要debug的群体。

#无特殊说明默认是在cpu i5上的结果,否则以特殊说明的型号为准
win 10x64 cpu i5

vs2019
opencv 4.5.5
onnx 1.9.1

由于比赛原因,我近一个月后这篇文章才发布。

环境安装

VisualStudio 2019

​ 有人肯定会说为什么不用vscode啊巴拉巴拉。一个主打轻量级去做这种项目免不了安装很多插件这么多插件哪天一个不支持了就gg。所以还是vs更方便,当然各有各的喜好。

​ vs正常安装下载,然后把 xx\VisualStudio\VC\Tools\MSVC\14.29.30133\bin\Hostx64\x64路径放到环境变量即可。

这里大概说一下bin,include,lib文件夹的作用,后面安装库文件的时候就会明白很多,而不是一头雾水:

  • include:.h结尾,主要是函数声明,可以放入子文件夹当然如果这样引用也需要带文件夹。如果运行程序include报错多半是找不到这个路径。
  • bin:.dll结尾,动态链接文件。
  • lib:.lib结尾,静态链接文件。

c++编译过程图可以参考这篇

opencv

​ 这个网上有很多,都是安装后把有include的路径加入环境变量,链接库加入链接什么的。但是根据上面的理解,我们可以发现vs已经链接了xx\VisualStudio\VC\Tools\MSVC\14.29.30133\include、xx\VisualStudio\VC\Tools\MSVC\14.29.30133\lib\x64。我们可以把编译好的opencv里的\include\opencv,lib\xxx,bin\xxx放入vs的对应文件夹即可。同理,其他库比如gflags也是一样,编译好丢进去就行了就是怕冲突不好删除,就不用每次一个库占一个文件夹还要添加环境变量。

​ 不过这样做就需要项目部署时把依赖重新全部丢进去。

onnxruntime

​ 为避免踩坑可以看这篇csdn博客,我最开始还以为像其他库一样需要编译,结果直接可以用vs自带的包管理软件nuget直接下载。编译报一堆错误弄半天,吐血

去下载安装包https://www.nuget.org/ (建议不要下载最新)。然后打开vs2019,工具->NuGet程序包管理器->程序包管理控制平台:

Install-Package microsoft.ml.onnxruntime.1.xx.x -Source your nupkg dir

python 导出onnx

pip install onnx
pip install onnxmltool

这个具体如何从pth导出onnx还是要参考官网给的例子,网络不同细节也不同。

ModNet onnxruntimeC++部署

读取图像并预处理

​ 输入网络肯定是要预处理的图像,用image watch可以边调试边查看图片,同时我发现比较小的matting网络normalized可能效果会更差,因为相当于就是一个floor full,normalized后颜色更不好区分了,这个需要自己微调。image watch真好用

vector<cv::Mat> HumanSeg::preprocess(cv::Mat &image) 
{

image_h = image.rows;
image_w = image.cols;
int rw, rh;
/*
if (image_w > image_h)
{
rh = 512;
rw = (image_w*1.0 / image_h) *refsize;
}
else
{
rw = 512;
rh = (image_h *1.0/ image_w) * refsize;
}
rh -= rh % 32;
rw -= rw % 32;
*/

// 图像还是出bug了,有时间能改就改
rh = rw = refsize;
cv::Mat resized_image,resized_image_float,normalized_image;
cv::cvtColor(image, image, cv::ColorConversionCodes::COLOR_BGR2RGB);
cv::resize(image, resized_image, cv::Size(rw,rh),0,0,cv::INTER_AREA); //41ms

resized_image.convertTo(resized_image_float, CV_32F,1.0/255); //14 ms
normalized_image = normalize(resized_image_float); // 16ms
input_node_dims = { 1,3,rh,rw };
return { resized_image,normalized_image };
}

onnx网络运行并输出

  • 首先要将hwc格式转为bchw格式才能输入网络(blob函数)
  • 输出的是float数组指针类型,需要按对应点转换为矩阵这个我也不怎么明白怎么方便转换
  • 转换之后要转换为uint8类才能参与后面的运算。
cv::Mat blob = cv::dnn::blobFromImage(normalized_image, 1, cv::Size((512),(512)), cv::Scalar(0, 0, 0), false, true);


// create input tensor
auto memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);

input_tensors.emplace_back(Ort::Value::CreateTensor<float>(memory_info, blob.ptr<float>(), blob.total(), input_node_dims.data(), input_node_dims.size()));

// 798ms
std::vector<Ort::Value> output_tensors = session.Run(
Ort::RunOptions{ nullptr },
input_node_names.data(),
input_tensors.data(),
input_node_names.size(),
out_node_names.data(),
out_node_names.size()
);
// h,w,3

float* floatarr = output_tensors[0].GetTensorMutableData<float>();

cv::Mat mask, mask_1f;
mask_1f = cv::Mat1f(input_node_dims[2],input_node_dims[3], floatarr);
mask_1f *= 255;
mask_1f.convertTo(mask, CV_8U);

resize并添加alpha通道

要注意,网上最常见的用split然后push_back alpha通道 再merge的方法但这是有弊端的,因为输出的是四通道图片,opencv::imwrite和视频流都无法输出。

cv::cvtcolor的bgra2bgr参数 是直接丢去alpha通道,而不是智能转换。

cv::Mat add_alpha(cv::Mat src,cv::Mat alpha)
vector<cv::Mat> srcChannels;
cv::split(src, srcChannels);
dstChannels.push_back(srcChannels[0]);
dstChannels.push_back(srcChannels[1]);
dstChannels.push_back(srcChannels[2]);
//添加透明度通道
dstChannels.push_back(alpha);
//合并通道
cv::merge(dstChannels, dst);
return dst

如果用矩阵乘法转三通道也是可以的。

raw_image.convertTo(fimg,CV_32FC1);
cv::Mat rest =cv::Scalar(1.)-pmat;
std::vector<cv::Mat1f> channels;
cv::split(fimg,channels);
// 都是255就是黑色背景,这个参数是绿色背景
cv::Mat mbmat = channels[0].mul(pmat) + rest.mul(cv::Scalar(153.));
cv::Mat mgmat = channels[1].mul(pmat) + rest.mul(cv::Scalar(255.));
cv::Mat mrmat = channels[2].mul(pmat) + rest.mul(cv::Scalar(120.));
vector<cv::Mat> merge_channel_mats;
merge_channel_mats.push_back(mbmat);
merge_channel_mats.push_back(mgmat);
merge_channel_mats.push_back(mrmat);

用位与运算是可以的,如果边缘非常模糊还可以通过调节阈值来选择mask来除去。位运算这个思路很巧妙

cv::threshold(mask, mask_thresh, 250, 255, cv::THRESH_TOZERO);
cv::bitwise_and(src, src, dst, alpha);
cv::resize(pre_image, without_bg, cv::Size(image_w, image_h), 0, 0, cv::INTER_AREA);// 123ms

注意先后顺序,如果后resize则运算量会小一点,同时锯齿现象也会小取决于插值方法

完整代码

感兴趣可以去我的github仓库

onnx加速

Fp32转Fp16

众所周知,Fp16会更快,训练的时候用混合精度训练也会更快

但是注意自己的精度是否支持,有的显卡是不支持fp16或int8的,通常来说fp32转fp16各方面效率都会快两倍,但是只支持gpu,cup可能效果还会差。

多线程运行

我一直都在找onnxruntime的多线程,发现真的只有那一个参数有明显改变。其他参数我试过没有用,估计默认的就是开了。

// 就这一个有用
session_options.SetIntraOpNumThreads(num_threads);

//这个开了和没开一样
//session_options.SetInterOpNumThreads(num_threads);
// 如果开parallel会变慢
//session_options.SetExecutionMode(ExecutionMode::ORT_PARALLEL);

果然最后还是和别人一样没有复现出来ModNet(700ms),论文夹带私货太严重了。

Robust Video Matting

​ 这篇论文比modnet友好很多,没有藏着掖着,该开源的都开源了,但是能看的c++部署还是一如既往的少只有这一个issuse。而lite.ai.toolkit又”高度”耦合化,单独使用还挺麻烦的,所以我就借鉴了一下代码,然后自己写的个cpp。部署后用cpu i5跑FP32是300ms/frame的样子。更换好点的cpu可以到13fps

openvino 部署

​ 看网上都说这个框架比onnx还能加速,但是用python实测发现IR效果和直接用pth权重推理速度相同,猜测是没有起到加速作用,就没有使用c++部署。

onnxruntime c++部署

同见github

优化&&总结

rvm原始代码是将图像缩放到0~1即不做任何处理直接输入,输出也是直接通过alpha进行合成,下面我将图像进行其他简单的预处理:

  • coco数据集常数进行normalization:预处理后的边缘会变的很明显,噪声很多,效果也变差很多(出现多余的目标)。

  • 缩放到[-1,1]:效果比上一个的稍好,但是人物边缘会更模糊但是检测的精度有时会更高。

  • 对alpha通过阈值筛选,减少边缘模糊:

    ​ 将阈值设为0.32左右可以有效减少边缘模糊(不同数据集上要自己进行微调)

  • 修改输出节点仅为pha:

    ​ 因为应用中不需要fgr等,源码不仅仅输出alpha,还输出了fgr,r1o等层,我们更改输出仅为alpha,代码量减少。

  • 实践证明fp32和fp16在仅在cpu上运行是没有优化的推理时间相同。

  • 也尝试了使用Int8量化,但是网络结构包含大量四则运算,故需要频繁quant和dequant,并没有起到优化作用速度反而下降了两倍。参考到这篇文章也是没有量化成功,可见并非所有网络都能量化成功和达到理想的效果。

    onnxfp32转int8pth的fp32转int8更方便,用pth先转int8比较麻烦,还要改网络等,onnx只需要一行代码,所以个人建议先转fp32 onnx再转int8更简单(虽然不知道原理)而且用onnx的int8速度只会下降一点。