第一个做网站的是谁,福永响应式网站建设,wordpress app登陆,芜湖市建设工程质量监督站网站摘要#xff1a;本文介绍了一种使用OpenCVSharp对摄像头中的绿幕视频进行实时“抠人像、替换背景”的方式#xff0c;对于项目中的算法进行了分析。本文中给出了简化OpenCVSharp中Mat、MatExpr等托管资源释放的方法。本文还介绍了“高效摄像头播放控件”以及和OpenCVSharp的性… 摘要本文介绍了一种使用OpenCVSharp对摄像头中的绿幕视频进行实时“抠人像、替换背景”的方式对于项目中的算法进行了分析。本文中给出了简化OpenCVSharp中Mat、MatExpr等托管资源释放的方法。本文还介绍了“高效摄像头播放控件”以及和OpenCVSharp的性能优化技术包括高效读写Mat数据、如何避免效率低的代码等。 一、为什么自己开发实时抠图软件由于工作的需要我需要一个能够对于摄像头中的人像进行实时地“扣除背景、替换背景并且把替换背景后的图片显示到窗口中”的功能。很多会议直播软件都有类似的功能比如Zoom、微软Teams等都有人像抠图功能但是他们的这些功能都只局限于在它们的软件内使用。我又试用了几个软件包括XSplit Vcam、抖音直播伴侣、OBS他们的功能都做的很优秀包括很多都还有不需要绿幕的智能抠图的功能非常强大但是他们都无法满足我的特殊要求。所以我需要自己开发这样一款软件。典型的人像抠图需要在被抠图的物体之后放上绿幕然后再通过程序把绿幕扣除掉这样人像就被保留下来了再把抠出来的人像绘制到新的背景图上即可。很多影视制作都是用类似这样的原理制作出来的。如图 1所示 [1]。图 1只要环境光线调整好了通过绿幕进行抠图是非常准确的不过这种方式的缺点就是对于场地的布置要求非常高。所以现在流行“无绿幕抠图”的功能也就是用人工智能的方法智能识别前景人像和背景然后智能的把前景人像识别出来。XSplit Vcam有这个功能而且可以把抠图的结果再模拟成一个虚拟摄像头进行输出属于民用领域中比较强悍的一款软件但是如果背景比较复杂的话XSplit Vcam移除背景的效果仍然不理想。我个人在计算机视觉方面特别是结合人工智能进行图像的智能处理方面研究很浅我不认为在时间有限的情况下能写出来一个比Vcam还要强大的软件因此我决定仍然用传统的绿幕形式来实现我想要的功能毕竟只要花几十块钱买一块绿幕即可。在开始讲解实现代码之前先展示一下软件的运行效果。图 2是相机采集的原始图像可以看到背后是一张绿幕而图 3则是软件运行后的效果而且是实时抠图的目前可以做到大约20FPS一秒钟约20帧。图 2没有抠绿幕图 3抠人像、替换背景 二、软件架构软件使用了OpenCV它是一个非常成熟、功能丰富的计算机视觉库。OpenCV支持C/C、Python、.NET、Java等主流的编程语言。在互联网上使用Python进行OpenCV开发的资料最多。由于个人不是很喜欢Python的语法所以这个软件我使用C#语言在.NET 5平台上进行开发。由于OpenCV在各个编程语言上用法大同小异因此这里用C#实现的代码改用其他编程语言也非常容易。.NET平台下有两个OpenCV的绑定库OpenCVSharp和Emgu CV。由于OpenCVSharp没有商业使用限制因此我这里使用OpenCVSharp。不过即使您使用的是Emgu CV这篇文章里的代码也是简单修改后就可以应用到Emgu CV中。 三、如何获得源代码由于抠绿幕替换背景的功能只是我的软件的一个模块整个软件暂时不方便开源所以我把抠绿幕替换背景这部分核心代码功能剥离到一个单独的开源项目中。项目开源地址https://github.com/yangzhongke/Zack.OpenCVSharp.Ext 代码中的“GreenScreenRemovalDemo.cs”就是最核心的代码也可以在项目页面底部的【GreenScreenRemovalDemo】中下载各个操作系统下的可执行文件其中的GreenScreenRemovalDemo就是主程序。以Windows为例运行GreenScreenRemovalDemo.exe就会出现如图 4所示的控制台图 4选择用演示视频还是摄像头 如果输入v就会自动播放一个内置的monster.mp4绿幕视频文件 [2]供没有绿幕环境的朋友进行体验程序会从视频文件中将绿幕剔除掉替换为自定义背景文件bg.png。如果在图 4这一步输入数字则会从指定编号的网络摄像头中读取画面进行抠图如果您的计算机中只有一个摄像头那么输入0即可。体验完毕在图形窗口内按任意键就会退出程序。 如下的图 5、图 6和图 7分辨就是绿幕视频、背景图以及合成图。图 5绿幕视频monster.mp4 图 6背景图bg.png(新西兰的伊甸山) 图 7替换背景后的合成图 四、核心原理图 8原始帧图片 图 8是从摄像头获取的一帧原始图片。首先调用我编写的RenderGreenScreenMask(src, matMask)方法把原始帧src转换为一张黑白图matMask做为遮罩。matMast中绿色部分渲染为黑色其他部分渲染为白色如图 9。RenderGreenScreenMask方法的主要代码如下 [3]private unsafe void RenderGreenScreenMask(Mat src,Mat matMask){ int rows src.Rows; int cols src.Cols; for (intx 0; x rows; x) { Vec3b*srcRow (Vec3b*)src.Ptr(x); byte*maskRow (byte*)matMask.Ptr(x); for(int y 0; y cols; y) { varpData srcRow y; byteblue pData-Item0; bytegreen pData-Item1; bytered pData-Item2; bytemax Math.Max(red, Math.Max(blue, green)); //ifthis pixel is some green, render the pixel with the same position on matMask asblack if(green max green 30) { *(maskRow y) 0; } else { *(maskRow y) 255;//render as white } } }}为了加速图片的像素点访问这里使用指针来操作。C#中可以使用指针操作内存这样可以大大加速程序的运行效率。因为环境光照的影响背景绿幕中的各个点颜色并不完全相同所以这里使用像素点的green max (blue,green,red) green 30是否为true来判断一个点是否是绿色30是一个阈值可以根据情况来调节识别效果这个阈值选的越大被认为是绿色的范围越窄。 图 9去掉绿色 接下来调用OpenCV的FindContoursAsArray()方法找到 图 9中的若干个轮廓信息。为了去掉一些绿幕中的褶皱或者光线问题造成的小面积干扰对于找到的轮廓信息需要删除掉面积较小的轮廓只保留面积较大的轮廓。使用C#中的LINQ操作可以轻松的完成这个筛选代码如下var contoursExternalForeground Cv2.FindContoursAsArray(matMask, RetrievalModes.External, ContourApproximationModes.ApproxNone) .Select(c new { contour c, Area (int)Cv2.ContourArea(c) }) .Where(c c.Area minBlockArea) .OrderByDescending(c c.Area).Take(5).Select(c c.contour); 这里的minBlockArea代表设定的一个“最小允许轮廓区域的面积”。接下来新建一个空的黑色Mat名字为matMaskForeground然后把上面得到的大轮廓区域绘制到这个matMaskForeground中并且内部填充为白色代码如下 matMaskForeground.DrawContours(contoursExternalForeground,-1, new Scalar(255), thickness:-1); matMaskForeground对应的图片内容如图 10。这样matMaskForeground中就只包含若干大面积轮廓了其他小面积的干扰都被排除了。 图 10找到最大几个闭合区域然后填充为白色 接下来要把图 9中的手臂、手、肩膀和脖子形成的那些大的镂空区域抠出来。因此把图 9和图 10做“异或”操作得到图 11这样的镂空区域。 图 11前两张图片做异或操作得到身体内部的镂空区域 因为眼镜中反射的屏幕中的绿光、或者衣服上的小的绿色可能会被识别为小的镂空区域可以看到图 11的右下角就有一些小白色区域因此再次使用FindContoursAsArray、DrawContours把 图 11中的小面积的区域排除掉。然后再把排除掉小面积轮廓的图 11和图 10做合并操作就得到图 12就是一个白色部分为身体区域而黑色部分为绿幕背景的的图片。图12把小镂空区域去掉并和身体遮罩做合并 接下来使用图 12做为遮罩对原始帧图像图 8进行背景透明处理得到图 13 这样的图片就是背景透明的图片了。主要代码如下public static void AddAlphaChannel(Mat src, Mat dst,Mat alpha){ using(ResourceTracker t new ResourceTracker()) { //splitis used for splitting the channels separately varbgr t.T(Cv2.Split(src)); varbgra new[] { bgr[0], bgr[1], bgr[2], alpha }; Cv2.Merge(bgra,dst); }} 其中src是原始帧图像dst是合并结果而alpha则是图 12这个透明遮罩。最后把背景透明的图 13绘制到我们自定义的背景图上就得到替换为背景图的图 14了。核心代码如下publicunsafe static void DrawOverlay(Mat bg, Mat overlay){ int colsOverlay overlay.Cols; int rowsOverlay overlay.Rows; for (int i 0; i rowsOverlay; i) { Vec3b* pBg (Vec3b*)bg.Ptr(i); Vec4b* pOverlay (Vec4b*)overlay.Ptr(i); for (int j 0; j colsOverlay; j) { Vec3b* pointBg pBg j; Vec4b*pointOverlay pOverlay j; if (pointOverlay-Item3! 0) { pointBg-Item0 pointOverlay-Item0; pointBg-Item1 pointOverlay-Item1; pointBg-Item2 pointOverlay-Item2; } } }} 其中参数bg就是原始帧图像图 8而overlay则是背景透明的图 13经过DrawOverlay方法绘制后bg的内容就变成了图 14然后就可以输出到界面上了。图 13背景透明图 图 14最终结果上面讲述的核心代码就位于GreenScreenRemovalDemo项目的ReplaceGreenScreenFilter类中。下面列出ReplaceGreenScreenFilter最主干的代码class ReplaceGreenScreenFilter{ private byte _greenScale 30; private double _minBlockPercent 0.01; private Mat _backgroundImage; public void SetBackgroundImage(Mat backgroundImage) { this._backgroundImage backgroundImage; } private unsafe void RenderGreenScreenMask(Mat src, MatmatMask) { int rows src.Rows; int cols src.Cols; for (int x 0; x rows; x) { Vec3b* srcRow (Vec3b*)src.Ptr(x); byte* maskRow (byte*)matMask.Ptr(x); for (int y 0; y cols; y) { var pData srcRow y; byte blue pData-Item0; byte green pData-Item1; byte red pData-Item2; byte max Math.Max(red, Math.Max(blue,green)); if (green max green this._greenScale) { *(maskRow y) 0; } else { *(maskRow y) 255;//render aswhite } } } } public void Apply(Mat src) { using (ResourceTracker t new ResourceTracker()) { Size srcSize src.Size(); Mat matMask t.NewMat(srcSize, MatType.CV_8UC1,new Scalar(0)); RenderGreenScreenMask(src, matMask); //the area is by integer instead of double, sothat it can improve the performance of comparision of areas int minBlockArea (int)(srcSize.Width *srcSize.Height * this.MinBlockPercent); var contoursExternalForeground Cv2.FindContoursAsArray(matMask, RetrievalModes.External,ContourApproximationModes.ApproxNone) .Select(c new { contour c, Area (int)Cv2.ContourArea(c) }) .Where(c c.Area minBlockArea) .OrderByDescending(c c.Area).Take(5).Select(c c.contour); //a new Mat used for rendering the selectedContours var matMaskForeground t.NewMat(srcSize,MatType.CV_8UC1, new Scalar(0)); //thickness: -1 means filling the inner space matMaskForeground.DrawContours(contoursExternalForeground,-1, new Scalar(255), thickness: -1); //matInternalHollow is the inner Hollow parts ofbody part. var matInternalHollow t.NewMat(srcSize,MatType.CV_8UC1, new Scalar(0)); Cv2.BitwiseXor(matMaskForeground, matMask,matInternalHollow); int minHollowArea (int)(minBlockArea *0.01);//the lower size limitation of InternalHollow is less than minBlockArea,because InternalHollows are smaller //find the Contours of Internal Hollow var contoursInternalHollow Cv2.FindContoursAsArray(matInternalHollow, RetrievalModes.External,ContourApproximationModes.ApproxNone) .Select(c new { contour c, Area Cv2.ContourArea(c) }) .Where(c c.Area minHollowArea) .OrderByDescending(c c.Area).Take(10).Select(c c.contour); //draw hollows foreach (var c in contoursInternalHollow) { matMaskForeground.FillConvexPoly(c, newScalar(0)); } var element t.T(Cv2.GetStructuringElement(MorphShapes.Cross,new Size(3, 3))); //smooth the edge of matMaskForeground Cv2.MorphologyEx(matMaskForeground,matMaskForeground, MorphTypes.Close, element, iterations: 6); var foreground t.NewMat(src.Size(),MatType.CV_8UC4, new Scalar(0)); ZackCVHelper.AddAlphaChannel(src, foreground,matMaskForeground); //resize the _backgroundImage to the same sizeof src Cv2.Resize(_backgroundImage, src, src.Size()); //draw foreground(people) on the backgroundimage ZackCVHelper.DrawOverlay(src, foreground); } }} 五、重要技术受限于篇幅这里不讲解OpenCV的基础知识这里只讲解项目中的一些重点技术以及OpenCVSharp使用过程中的一些需要注意的事项。由于我也是刚接触OpenCVSharp几天时间所以如果存在有问题的地方请各位指正。简化OpenCVSharp对象的释放在OpenCVSharp中Mat 和 MatExpr等类的对象拥有非托管资源因此需要调用Dispose()方法手动释放。更糟糕的是、-、*等运算符每次都会创建一个新的对象这些对象都需要释放否则就会有内存泄露。但是这些对象释放的代码看起来非常啰嗦。假设有如下Python中访问opencv的代码mat1 np.empty([100,100])mat3 255-mat1*0.8mats1 cv2.split(mat3)mat4cv2.merge(mats1[0],mats1[2],mats1[2]) 而在C#中同样的代码则像下面这样啰嗦using (Mat mat1 newMat(new Size(100, 100), MatType.CV_8UC3))using (Mat mat2 mat1* 0.8)using (Mat mat3 255-mat2){ Mat[] mats1 mat3.Split(); using (Mat mat4 new Mat()) { Cv2.Merge(new Mat[] { mats1[0], mats1[1], mats1[2] },mat4); } foreach(var m in mats1) { m.Dispose(); }} 因此我创建了一个ResourceTracker类用来管理OpenCV的资源。ResourceTracker类的T()方法用于把OpenCV对象加入跟踪记录。T()方法的实现很简单就是把被包裹的对象加入跟踪记录然后再把对象返回。T()方法的核心代码如下public Mat T(Mat obj){ if (obj null) { return obj; } trackedObjects.Add(obj); return obj;} public Mat[] T(Mat[]objs){ foreach (var obj in objs) { T(obj); } return objs;} ResourceTracker实现了IDisposable接口当ResourceTracker类的 Dispose()方法被调用后ResourceTracker跟踪的所有资源都会被释放。T()方法可以跟踪一个对象或者一个对象数组。而NewMat() 这个方法是T(new Mat(...)) 的一个简化。因为、-、*等运算符每次都会创建一个新的对象所以每步运算得到的对象都需要释放他们可以使用T()进行包裹。例如t.T(255 - t.T(picMat * 0.8)) 因此上面的啰嗦的C#代码可以简化成如下的样子using (ResourceTrackert new ResourceTracker()){ Mat mat1 t.NewMat(new Size(100, 100), MatType.CV_8UC3,newScalar(0)); Mat mat3 t.T(255-t.T(mat1*0.8)); Mat[] mats1 t.T(mat3.Split()); Mat mat4 t.NewMat(); Cv2.Merge(new Mat[] { mats1[0], mats1[1], mats1[2] }, mat4);} 在离开ResourceTracker的using代码块之后所有ResourceTracker对象管理的Mat、MatExpr等对象的资源都会被释放。这个ResourceTracker类我放到了Zack.OpenCVSharp.Ext这个NuGet包中可以通过如下NuGet命令安装Install-PackageZack.OpenCVSharp.Ext项目的源代码地址https://github.com/yangzhongke/Zack.OpenCVSharp.Ext 访问Mat中数据的高效方式OpenCVSharp中提供了很多访问Mat中数据的方法经过测试我发现At()方式最慢GetGenericIndexer也很慢因为他们都是完全通过托管代码的方式进行的性能必然打折扣。而直接访问内存的GetUnsafeGenericIndexer方式快了很多但是最快的方式还是使用mat.Ptr(x)并使用指针这种方式速度最快因为这种方式直接通过指针读写Mat的内存。使用这种方式的方法需要标记为unsafe并且项目要启用“允许不安全代码”。由于这种方式是直接读写内存所以一定要注意你的代码以免造成不正确的内存访问或者AccessViolation对指针操作不熟悉的读者可以阅读我出版的图书《零基础趣学C语言》作者杨中科人民邮电出版社因为C#中指针操作和C语言几乎一模一样。这种指针方式的参考代码请参考上面的RenderGreenScreenMask()、DrawOverlay()两个方法Zack.OpenCVSharp.Ext这个开源项目中np类的where方法还演示了C#泛型、指针操作以及lambda的结合使用。OpenCVSharp中Vec4b、Vec3b、byte等代表不同字节长度的内存单元一定要根据使用的Mat对象的通道数等来选择使用Vec4b、Vec3b、byte等使用不当不仅会影响性能而且还可能会造成数据混乱数据混乱的最直接的表现就是图片显示错乱、花屏。 CameraPlayer我的软件需要从摄像头采集图像并且显示到界面上而且在显示到界面上之前还要对图像进行“抠人像、替换背景”的操作。在最开始的时候我使用AForge.NET完成摄像头的图像采集和显示不过性能非常低。因为需要先把AForge.NET采集到的Bitmap转换为OpenCVSharp的Mat抠图处理完成后再把Mat转换回Bitmap显示到界面上。所以我就直接使用OpenCVSharp的VideoCapture类来完成摄像头图像的采集由于它采集到的帧图像直接用Mat表示省去了转换环节速度得到了很大的提升。我把从摄像头取数据以及显示到界面上的操作封装了一个CameraPlayer控件中同时提供了.NET Core和.NET Framework版的WinForm控件可以直接拿来用而且提供了SetFrameFilter(ActionMat frameFilterFunc)方法来允许设定一个委托从而在把帧图像的Mat绘制到界面前使用OpenCVSharp进行处理。CameraPlayer控件中图像采集、图像的处理和图像的显示是由不同线程负责各自并行处理所以性能非常高。我把这个CameraPlayer控件开源了具体用法请参考项目的文档。项目地址https://github.com/yangzhongke/Zack.CameraLib在开发CameraPlayer的时候我发现如果不设定VideoCapture的FourCC属性也就是视频的编码取一帧需要100ms而把FourCC属性设置为MJPG之后取一帧只要50ms。我不知道这是否和摄像头相关。因此如果你因为FourCC属性设置为MJPG之后读取图像的速度反而变慢了可以尝试修改一个不同的FourCC值。 谨慎使用可能造成性能问题的玩意儿在实现RenderGreenScreenMask()这个方法的时候其中有一步是用来“取blue、green、red三个值中的最大值”最开始的时候我使用.NET中的LINQ扩展方法实现newbyte[]{blue,green,red}.Max(); 但是发现改成byte max1 blue green ? blue : green; byte max max1red?max1:red;这种简单的方法计算之后每一帧的处理时间减少了50%。由于LINQ操作涉及到“创建集合对象、把数据放入集合对象、获取数据”这样的过程速度会比常规算法慢一些在普通的数据处理中这点性能差距可以忽略不计特别是在使用LINQ对数据库等进行操作的时候相对于耗时的IO操作来讲这点性能差别更是可以忽略不计。但是由于这里是在双层循环中使用而且执行的操作的速度非常快的内存读写所以就把性能差距放大了。因此在使用OpenCVSharp对图像进行处理的时候要谨慎使用这些可能会造成性能问题的高级玩意儿。 Mat内存的初始化在创建空的Mat对象的时候最好初始化Mat对象的内存数据就像在C语言中对于malloc拿到的内存空间最好用memset重置一样以免造成内存中旧的残留数据干扰我们的操作。比如new Mat(srcSize,MatType.CV_8UC1)这样创建的空白Mat中的内存可能是复用之前被释放的其他对象的内存数据是脏的除非你的下一步操作是把Mat的每一位都重新填充否则请使用Mat 构造函数的Scalar类型的参数来初始化内存参考代码如下new Mat(srcSize,MatType.CV_8UC1,new Scalar(0)) 六、未来工作在以后有时间的时候我可能会做如下这些工作。提升从摄像头取一帧的速度。因为我目前用的摄像头“罗技C920”标称的是FPS30所以理论上来讲取一帧的速度是33ms而目前我取一帧的速度是50ms我要研究一下是否能进一步提升取一帧图像的速度。除了我长得不好看这个不可控因素之外抠出来的图也是原图亮度以及边缘都还有待优化所以考虑增加美颜、瘦脸、亮肤、边缘优化等功能目前的人像抠图算法处理一帧需要大约20ms而从摄像头取一帧的速度是50ms因此还有30ms的额外时间可以用来做这些美化工作。用人工智能算法实现“无绿幕抠人像、去除背景”。完全自己实现这个无疑是比较难的。我发现一个很强大的开源项目MODNet它是一个pythontorch实现的使用神经网络做智能人像识别的库包含已经训练完成模型。而torch也有对应的.NET移植版所以理论上这是可以做到的。 七、结论使用OpenCVSharp的时候只要注意使用本文中介绍的高效访问内存的方式并且合理调用相关的函数可以非常高性能的进行图像的处理因此我开发的软件可以做到每一帧图像处理仅需大约20ms。借助于我开发的Zack.OpenCVSharp.Ext这个包中的ResourceTracker类可以让OpenCVSharp中的资源释放变得非常简单在几乎不用修改表达式、代码的基础上让资源能够及时得到释放避免内存泄漏。点击【阅读原文】查看项目的Github页面。