C#加神经网络实现视频换背景
机器配置:
1.cpu:i3-2375M
2.操作系统:win 7 64位旗舰版 SP1
程序运行环境:
Microsoft .NET Framework 4.8(wpf运行时)
Microsoft Visual C++2015-2022 Redistributable(x86)-14.40.33810(onnx运行时)
依赖:
Microsoft.ML.OnnxRuntime 1.11(备注1)
难点:
1.网上用C#实现的类似功能的项目比较少,大部分都是用的python,onnx格式的模型也不多,或者需要把其他格式的模型转成onnx。
2.运行性能比较低下,如果想要实现比较流畅的效果,需要一秒25帧,也就意味着40毫秒内就要完成推理到图像合成,而目前以上面列出的机器配置加上下文的优化后,平均每帧图像的处理时间是755毫秒
效果:实现类似视频会议换背景功能。
原理:若给定一张宽为w像素高为h像素的含人像图片pic,神经网络模型将会估计图片的每一个像素pic[i,j]为背景像素的概率,
若大于0.5说明这个像素过半概率是背景像素,小于0.5说明不到一半的概率属于背景,也就是这个像素属于人像像素;
而图片总共含有w×h个像素,因此最终判断的结果也将会是一个w×h大小的二维小数数组Alpha,其中的元素Alpha[i,j]存放着像素pic[i,j]为背景像素的概率。
完整项目源码:https://gitee.com/ReadmeHo/back-ground-change.git
程序主要结构:
1.ModelWorker类,封装了Onnx神经网络模型的初始化与推理方法。
2.ModelHelper类,把图像转为张量,根据神经网络的推理结果抠出人像前景并和自定义背景合成新图像返回。
3.Handle类,组合ModelWorker和ModelHelper进行工作。
主要具体实现:
神经网络模型(备注2)接受一个1×3×h×w的四维张量作为输入,
所以主要思路是把BitmapData(备注3)转换成元素类型为float的1×3×h×w的四维张量,张量中每一个数的范围为-1到1。
神经网络每次判断一张图片,因此第一维是1,图像的每个像素具有rgb三个通道,因此第二维是3,
而第三维和第四维分别为图像的高h和宽w。
因为像素的rgb各使用一个字节存储,所以每个通道会有0到255一共256个可能的整数取值,而神经网络允许的每个张量元素输入范围是-1到1,因此还需要归一化操作把整数的通道值映射成-1到1之间的小数
public static Tensor<float> BitmapDataToTensor(BitmapData bitmapData)
{
int row = bitmapData.Height;
int col = bitmapData.Width;
Tensor<float> re = new DenseTensor<float>(new[] { 1, 3, row, col });
unsafe
{
byte* ptr = (byte*)bitmapData.Scan0;
Parallel.For(0, row, i =>
{
int line = bitmapData.Stride * i;
Parallel.For(0, col, j =>
{
int position = line + j + (j << 1);
byte* ptr2 = ptr + position;
re[0, 0, i, j] = normalizationCache[ptr2[2]];
re[0, 1, i, j] = normalizationCache[ptr2[1]];
re[0, 2, i, j] = normalizationCache[ptr2[0]];
});
});
}
return re;
}
输入是bitmapData(摄像头采集到的实时带人像图像),bgData(自定义背景图片)
抠图并把前景和背景合成新图像输出,神经网络模型会输出一个1×1×宽×高的四维float张量Alpha,因为前两维的长度都是1,所以可以看做是二维数组。
根据上文原理中的解释可知,程序将会根据Alpha数组的每个元素的值来区分bitmapData的对应相同坐标像素是否属于人像,若属于则直接把这个像素覆盖bgData对应坐标的像素,也就是把同时实现抠图和合成。
public static void Composition(BitmapData bitmapData, BitmapData bgData, Tensor<float> Alpha)
{
unsafe
{
byte* iptr2 = (byte*)bitmapData.Scan0;
byte* iptr3 = (byte*)bgData.Scan0;
Parallel.For(0, bitmapData.Height, i =>
{
int line = bitmapData.Stride * i;
Parallel.For(0, bitmapData.Width, j =>
{
if (Alpha[0, 0, i, j] < 0.5)
{
int position = line + j + (j << 1);
byte* ptr2 = iptr2 + position;
byte* ptr3 = iptr3 + position;
ptr3[0] = ptr2[0];
ptr3[1] = ptr2[1];
ptr3[2] = ptr2[2];
}
});
});
}
}
计算加速
为了尽可能减少不必要的计算,以及最大程度挖掘硬件性能,需要采用多个办法实现加速
1.最直接的,尽量使用小的图像,以归一化操作为例,1280×720的图片所需计算量是640×360的图片的四倍。
2.使用硬编码数组缓存归一化数值,归一化时,一个w×h的图片需要把所有像素的所有通道的值由0到255映射到-1到1的范围内,那么就需要w×h×3次浮点运算,因为rgb的值只有256种可能取值,所以用一个长度为256的float数组normalizationCache存放事先计算好的数值,以rgb的值作为数组下标索引,这样图像归一化的时候就可以直接查数组而不用进行浮点运算。
3.使用移位代替乘法,因为移位操作和加法操作比乘法操作快很多,比如j * 3这个计算,改为j + (j << 1),在遍历一个图片所有像素
的时候,需要进行w×h次乘法操作,通过把乘法改为一个移位加一个加法可以节省出几十毫秒的时间。
4.使用指针直接访问内存中的图片像素,而不是通过bitmap的访问像素方法。
6.使用Parallel.For代替for循环。
5.启用onnx运行时的多线程和切换到并行模式
SessionOptions o = new SessionOptions();
o.InterOpNumThreads = 4;//不同操作之间线程数
o.IntraOpNumThreads = 4;//每个操作线程数
o.ExecutionMode = ExecutionMode.ORT_PARALLEL;//并行模式
6.使用流水线加速,每一帧图像都需要进行1.转换为张量,2.推理,3.前后景合成这三个步骤,则根据流水线的工作方式,最多可以用三个线程同时进行三个步骤,例如第一帧进行步骤3时,同时第二帧进行步骤2,同时第三帧进行步骤1。
注意事项:
1.背景图像长宽要和摄像头录像长宽一样
相关链接:
1.模型下载地址:https://bj.bcebos.com/paddle2onnx/model_zoo/ppseg_lite_portrait_398x224_with_softmax.onnx
2.onnx文档:https://onnxruntime.ai/docs/
备注:
1.Microsoft.ML.OnnxRuntime 1.11是经过我测试可以在win 7上运行的最高版本,有的win7 有可能会报错,如果用1.1.2版本目前在我用过的所有win7 机器都能跑。win10或者更新的系统则可以试试更新的Microsoft.ML.OnnxRuntime版本
2.此项目使用的是ppseg_lite_portrait_398x224_with_softmax.onnx这个模型,这个模型是暂时我能找到的最快的人像识别抠图模型。
3.为什么是用BitmapData而不是Bitmap请看加速计算部分