编程去除背景绿幕抠图,基于.NET+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.8

mats1 = cv2.split(mat3)

mat4=cv2.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(Action<Mat> 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 =max1>red?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”标称的是FPS=30,所以理论上来讲,取一帧的速度是33ms,而目前我取一帧的速度是50ms,我要研究一下是否能进一步提升取一帧图像的速度。

  • 除了我长得不好看这个不可控因素之外,抠出来的图也是原图,亮度以及边缘都还有待优化,所以考虑增加美颜、瘦脸、亮肤、边缘优化等功能,目前的人像抠图算法处理一帧需要大约20ms,而从摄像头取一帧的速度是50ms,因此还有30ms的额外时间可以用来做这些美化工作。

  • 用人工智能算法实现“无绿幕抠人像、去除背景”。完全自己实现这个无疑是比较难的。我发现一个很强大的开源项目MODNet,它是一个python+torch实现的使用神经网络做智能人像识别的库,包含已经训练完成模型。而torch也有对应的.NET移植版,所以理论上这是可以做到的。

 

七、结论

使用OpenCVSharp的时候,只要注意使用本文中介绍的高效访问内存的方式,并且合理调用相关的函数,可以非常高性能的进行图像的处理,因此我开发的软件可以做到每一帧图像处理仅需大约20ms。借助于我开发的Zack.OpenCVSharp.Ext这个包中的ResourceTracker类,可以让OpenCVSharp中的资源释放变得非常简单,在几乎不用修改表达式、代码的基础上,让资源能够及时得到释放,避免内存泄漏。

点击【阅读原文】查看项目的Github页面。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/306251.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

.NET 云原生架构师训练营(模块二 基础巩固 依赖注入)--学习笔记

2.2.1 核心模块--依赖注入什么是依赖注入.NET Core DI 生命周期服务设计服务范围检查ASP.NET Core 依赖注入&#xff1a;https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/dependency-injection?viewaspnetcore-5.0什么是依赖注入Dependency injection 依赖注入Inv…

vue ref 绑定的事件需要移除吗_Vue易遗忘的基础复习(二)

数据请求Vue-resource请求在Vue2.0之后已经被舍弃2. fetch请求因为传统 Ajax &#xff08;指 XMLHttpRequest&#xff09;存在一些令人头疼的问题&#xff1a;配置和调用方式非常混乱&#xff0c;而且基于事件的异步模型写起来也没有现代的 Promise&#xff0c;generator/yield…

如何在 ASP.NET Core 中使用 API 分析器

ASP.NET Core 2.2 引入了 API 分析器&#xff0c;它有利于提高 API 的文档化&#xff0c;API分析器 可以应用在任何带有 ApiController 特性的 Controller 上&#xff0c;本篇就和大家一起讨论下。安装 API 分析器 如果你使用的是 ASP.NET Core 2.2 的话&#xff0c;用 Visual …

.net mysql字符串截取_【MySQL】字符串截取之SUBSTRING_INDEX和【MySQL】字符串四则运算...

substring_index(str,delim,count)str:要处理的字符串delim:分隔符count:计数例子&#xff1a;strwww.google.com1.count是正数&#xff0c;那么就是从左往右数&#xff0c;第N个分隔符的左边的全部内容SELECT SUBSTRING_INDEX(www.google.com,.,1);结果是&#xff1a;wwwSELEC…

用C#+Selenium+ChromeDriver 爬取网页,完美模拟真实的用户浏览行为

背景Selenium是一个用于Web应用程序测试的工具。Selenium测试直接运行在浏览器中&#xff0c;就像真正的用户在操作一样。而对于爬虫来说&#xff0c;使用Selenium操控浏览器来爬取网上的数据那么肯定是爬虫中的杀手武器。这里&#xff0c;我将介绍selenium 谷歌浏览器的一般使…

ASP.NET Core ActionFilter引发的一个EF异常

最近在使用ASP.NET Core的时候出现了一个奇怪的问题。在一个Controller上使用了一个ActionFilter之后经常出现EF报错。InvalidOperationException: A second operation started on this context before a previous operation completed. Any instance members are not guarante…

BCVP开发者说第5期:QuartzCore.Blazor

沉静岁月&#xff0c;淡忘流年1项目简介QuartzCore.BlazorQuartzCore.Blazor 是一个基于 .Net5 开发的轻量级 Quartz 作业配置中心&#xff0c;实践应用 Ant Design Blazor 和 FreeSql 两个技术&#xff0c; 对这两个技术感兴趣的小伙伴可以加我一起学习讨论哦&#xff0c;对有…

mysql update返回_MySQL中,当update修改数据与原数据相同时会再次执行吗?

本文同步Java知音社区&#xff0c;专注于Java作者&#xff1a;powdbahttps://yq.aliyun.com/articles/694162一、背景本文主要测试MySQL执行update语句时&#xff0c;针对与原数据&#xff08;即未修改&#xff09;相同的update语句会在MySQL内部重新执行吗&#xff1f;二、测试…

.NET 云原生架构师训练营(模块二 基础巩固 日志)--学习笔记

2.2.2 核心模块--日志ILogger 的使用日志的 ID日志的分类日志的级别LoggerProvider日志的最佳实践.NET Core 和 ASP.NET Core 中的日志记录&#xff1a;https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/logging/?viewaspnetcore-5.0ILogger 的使用在 Get 方法中添…

mysql数据库设计三大范式_了解数据库设计三大范式

数据库设计范式什么是范式&#xff1a;简言之就是&#xff0c;数据库设计对数据的存储性能&#xff0c;还有开发人员对数据的操作都有莫大的关系。所以建立科学的&#xff0c;规范的的数据库是需要满足一些规范的来优化数据数据存储方式。在关系型数据库中这些规范就可以称为范…

使用BeetleX网关部署第三方Web服务

BeetleX的http/ws网关在早期版本可以启动和管理第三方Web服务进程&#xff0c;在最新的1.5版本中引入了文件管理功能&#xff0c;通过这一功能可以对第三方Web服务进行发布管理。加入文件管理后BeetleX的新版本网关服务可以理解为一个简单化的IIS&#xff0c;但它的不同之处是可…

2020.NET开发者大会大会线上同步直播,以及参会秘籍

2020.NET开发者大会马上就要开幕啦&#xff01;你都做好参会准备没有&#xff1f;本届峰会线上分享将在思否和CSDN两个媒体平台同步进行&#xff0c;大家可以选择适合自己的方式在线参与互动哦&#xff01;访问下方链接&#xff0c;可以直接加入直播&#xff1a;思否直播观看地…

C# 9.0中引入的新特性init和record的使用思考

.NET 5.0已经发布&#xff0c;C# 9.0也为我们带来了许多新特性&#xff0c;其中最让我印象深刻的就是init和record type&#xff0c;很多文章已经把这两个新特性讨论的差不多了&#xff0c;本文不再详细讨论&#xff0c;而是通过使用角度来思考这两个特性。initinit是C# 9.0中引…

使用 .NET Core 中的 EventCounters 衡量性能

背景对于每隔几毫秒发生的事件&#xff0c;最好使每个事件的开销较低&#xff08;小于一毫秒&#xff09;。 否则&#xff0c;对性能的影响将很大。 记录事件意味着你将向磁盘写入内容。 如果磁盘不够快&#xff0c;你将丢失事件。 你需要一个解决方案&#xff0c;而不是记录事…

基于 C# 的 ETL 大数据并行编程

作者&#xff1a;James Spinella译者&#xff1a;精致码农原文&#xff1a;https://bit.ly/3nGQu4J并行编程在历史上一直是软件开发中比较小众和复杂的环节&#xff0c;往往不值得头疼。但编写并行化应用只会越来越简单&#xff0c;一个应用同时利用设备 CPU 上的多个内核&…

一个小技巧助您减少if语句的状态判断

在进行项目的开发的过程中&#xff0c; if 语句是少不了的&#xff0c;但我们始终要有一颗消灭 if / else 语句的心。为了消灭if / else 我们引入了 短路器 的概念。短路器 有时候的确能精简我们的代码&#xff0c;但还不够&#xff0c;因此我参考了一个方法来继续消灭一部分 断…

抢先看:笔者亲历的2020年中国.NET开发者大会活动纪实

编者&#xff1a;2020年中国.NET开发者大会第一天活动已经结束&#xff0c;可以通过https://codechina.csdn.net/lives 会看。第二天的Workshop 也有直播哦。12020年12月19日的苏州工业园区&#xff0c;天公作美&#xff0c;阳光明媚&#xff0c;气象迷人&#xff0c;正是一个搞…

python魔术方法由谁定义_Python的魔术方法

魔术方法就是在定义的类中定义一些”不一般”的方法&#xff0c;使类的使用更方便、完善、健壮&#xff0c;是python特有的方法&#xff0c;一般都是前后包含两个下划线__的方法称为魔术方法&#xff0c;例如__new__。基本魔术方法有哪些__new__&#xff1a;是在一个对象实例化…

Swagger在header中添加token

概述平常做项目使用mvcwebapi&#xff0c;采取前后端分离的方式&#xff0c;后台提供API接口给前端开发人员。这个过程中遇到一个问题后台开发人员怎么提供接口说明文档给前端开发人员。为了解决这个问题&#xff0c;项目中引用swagger&#xff08;我比较喜欢戏称为“丝袜哥”&…

如何在 C# 中使用 数据注解

数据注解 是一种可以应用到 类 或者 类成员上用来指定类之间关系的一种 Attribute&#xff0c;它的应用场景比较多&#xff0c;可用来描述 UI 上如何进行数据展示&#xff0c;还可以用来做类属性的规则验证&#xff0c;这篇文章就来讨论为什么 注解 值得你去学习&#xff0c;以…