C#实现⼈脸识别
关于⼈脸识别
⽬前的⼈脸识别已经相对成熟,有各种收费免费的商业⽅案和开源⽅案,其中OpenCV很早就⽀持了⼈脸识别,在我选择⼈脸识别开发库时,也横向对⽐了三种库,包括在线识别的百度、开源的OpenCV和商业库虹软(中⼩型规模免费)。
百度的⼈脸识别,才上线不久,⽂档不太完善,之前联系百度,官⽅也给了我基于Android的Example,但是不太符合我的需求,⼀是照⽚需要上传⾄百度服务器(这个是最⼤的问题),其次,⼈脸的定位需要⾃⾏去实现(捕获到⼈脸后上传进⾏识别)。
OpenCV很早以前就⽤过,当时做⼈脸+车牌识别时,最先考虑的就是OpenCV,但是识别率在当时不算很⾼,后来是采⽤了⼀个电⼦科⼤的⽼师⾃⾏开发的识别库(相对易⽤,识别率也还不错),所以这次准备做时,没有选择OpenCV。
虹软其实在⽆意间发现的,当时正在寻开发库,正在测试Python的⼀个,就发现有新闻说虹软的识别库全⾯开放并且可以免费使⽤,⽽且是离线识别,所以就下载尝试了⼀下,发现识别率还不错,所以就暂定了采⽤虹软的识别⽅案。这⾥主要就给⼤家分享⼀下开发过程当中的⼀些坑和使⽤⼼得,顺便开源识别库的C# Wrapper。
SDK的C# Wrapper
由于虹软的库是采⽤C++开发的,⽽我的应⽤程序采⽤的是C#,所以,需要对库进⾏包装,便于C#的调⽤,包装的主要需求是可以在C#中快速⽅便的调⽤,⽆需考虑内存、指针等问题,并且具备⼀定的容错性。Wrapper库⽬前已经开源,⼤家可以到Github上进⾏下载,。Wrapper库基本上没有什么可以说的,⽆⾮是对PInvoke的包装,只是⾥⾯做了⽐较多的细节处理,屏蔽了调⽤细节,提供了相对⾼层的函数。有兴趣的可以看看源代码。
Wrapper库的使⽤例⼦
基本使⽤
⼈脸检测(静态图⽚):
using (var detection = LocatorFactory.GetDetectionLocator("appId", "sdkKey"))
{
var image = Image.FromFile("test.jpg");
var bitmap = new Bitmap(image);
var result = detection.Detect(bitmap, out var locateResult);
//检测到位置信息在使⽤完毕后,需要释放资源,避免内存泄露
using (locateResult)
{
if (result == ErrorCode.Ok && locateResult.FaceCount > 0)rectangle函数opencv
{
using (var g = Graphics.FromImage(bitmap))
{
var face = locateResult.Faces[0].ToRectangle();
g.DrawRectangle(new Pen(Color.Chartreuse), face.X, face.Y, face.Width, face.Height);
}
bitmap.Save("output.jpg", ImageFormat.Jpeg);
}
}
}
⼈脸跟踪(⼈脸跟踪⼀般⽤于视频的连续帧识别,相较于检测,⼜更⾼的执⾏效率,这⾥⽤静态图⽚做例⼦,实际使⽤和检测没啥区别):using (var detection = LocatorFactory.GetTrackingLocator("appId", "sdkKey"))
{
var image = Image.FromFile("test.jpg");
var bitmap = new Bitmap(image);
var result = detection.Detect(bitmap, out var locateResult);
using (locateResult)
{
if (result == ErrorCode.Ok && locateResult.FaceCount > 0)
{
using (var g = Graphics.FromImage(bitmap))
{
var face = locateResult.Faces[0].ToRectangle();
g.DrawRectangle(new Pen(Color.Chartreuse), face.X, face.Y, face.Width, face.Height);
}
bitmap.Save("output.jpg", ImageFormat.Jpeg);
}
}
}
⼈脸对⽐:
using (var proccesor = new FaceProcessor("appid",
"locatorKey", "recognizeKey", true))
{
var image1 = Image.FromFile("test2.jpg");
var image2 = Image.FromFile("test.jpg");
var result1 = proccesor.LocateExtract(new Bitmap(image1));
var result2 = proccesor.LocateExtract(new Bitmap(image2));
//FaceProcessor是个整合包装类,集成了检测和识别,如果要单独使⽤识别,可以使⽤FaceRecognize类
//这⾥做演⽰,假设图⽚都只有⼀张脸
//可以将FeatureData持久化保存,这个即是⼈脸特征数据,⽤于后续的⼈脸匹配
//File.WriteAllBytes("XXX.data", feature.FeatureData);FeatureData会⾃动转型为byte数组
if ((result1 != null) & (result2 != null))
Console.WriteLine(proccesor.Match(result1[0].FeatureData, result2[0].FeatureData, true));
}
使⽤注意事项
LocateResult(检测结果)和Feature(⼈脸特征)都包含需要释放的内存资源,在使⽤完毕后,记得需要释放,否则会引起内存泄
露。FaceProcessor和FaceRecognize的Match函数,在完成⽐较后,可以⾃动释放,只需要最后两个参数指定为true即可,如果是⽤于⼈脸匹配(1:N),则可以采⽤默认参数,这种情况下,第⼀个参数指定的特征数据不会⾃动释放,⽤于循环和特征库的特征进⾏⽐对。
整合的完整例⼦
在Github上,有完整的例⼦,⾥⾯主要实现了通过ffmpeg采集RTSP协议的图像(使⽤海康的摄像机),然后进⾏⼈脸匹配。在开发过程中遇到不少的坑。
⼈脸识别的⾸要⼯作就是捕获摄像机视频帧,这⼀块上是坑的最久的,因为最开始采⽤的是OpenCV的包装库,Emgu.CV,在开发过程中,捕获USB摄像头时,倒是问题不⼤,没有出现过异常。在捕获RTSP视频流时,会不定时的出现AccessviolationException异常,短则⼏⼗分钟,长则⼏个⼩时,总之就是不稳定。在官⽅Github地址上,也提了,他们给出的答复是屏蔽的我业务逻辑,仅捕获视频流试试,结果问题依然,所以,我基本坑定了试Emgu.CV上⾯的问题。后来经过反复的实验,最终确定了选择ffmpeg。
ffmepg主要采⽤ProcessStartInfo进⾏调⽤,我采⽤的是NReco.VideoConverter(⼀个ffmpeg调⽤的包装,可以通过nuget搜索安装),虽然ffmpeg 解决了稳定性问题,但是实际开发时,也遇到了不少坑,其中,最主要的是NReco.VideoConverter没有任何⽂档和例⼦(实际有,需要75⼑购买),所以,⾃⼰研究了半天,如何捕获视频流并转换为Bitmap对象。只要实现这⼀步,后续就是调⽤Wrapper就⾏了。FaceDemo详解
上⾯说到了,通过ffmpeg捕获视频流并转换Bitmap是重点,所以,这⾥也主要介绍这⼀块。
⾸先是ffmpeg的调⽤参数:
var setting =
new ConvertSettings
{
CustomOutputArgs = "-an -r 15 -pix_fmt bgr24 -updatefirst 1"
}; //-s 1920x1080 -q:v 2 -b:v 64k
task = ffmpeg.ConvertLiveMedia("rtsp://admin:12qwaszxA@192.168.1.64:554/h264/ch1/main/av_stream", null,
outputStream, Format.raw_video, setting);
task.OutputDataReceived += DataReceived;
task.Start();
-an表⽰不捕获⾳频流,-r表⽰帧率,根据需求和实际设备调整此参数,-pix_fmt⽐较重要,⼀般情况下,
指定为bgr24不会有太⼤问题(还是看具体设备),之前就是⽤成了rgb24,结果捕获出来的图像,⼈都变成阿凡达了,颜⾊是反的。最后⼀个参数,坑的我差点放弃这个⽅案。本⾝,ffmpeg在调⽤时,需要指定⼀个⽂件名模板,捕获到的输出会按照模板⽣成⽂件,如果要将数据输出到控制台,则最后传⼊⼀
个-即可,最开始没有指定updatefirst,ffmpeg在捕获了第⼀帧后就抛出了异常,最后查了半天ffmpeg说明(完整参数说明⾮常多,输出到⽂本有1319KB),发现了这个参数,表⽰持续更新第⼀个⽂件。最后,在调⽤视频捕获是,需要指定输出格式,必须指定为Format.raw_video,实际上这个格式名称有些误导⼈,按道理将应该叫做raw_image,因为最终输出的是每帧原始的位图数据。
到此为⽌,还并没有解决视频流数据的捕获,因为⼜来⼀个坑,ProcessStartInfo的控制台缓冲区⼤⼩只有32768 bytes,即,每⼀次的输出,实际上并不是⼀个完整的位图数据。
//完整代码参加Github源代码
//代码⽚段1
private Bitmap _image;
private IntPtr _pImage;
{
_pImage = Marshal.AllocHGlobal(1920 * 1080 * 3);
_image = new Bitmap(1920, 1080, 1920 * 3, PixelFormat.Format24bppRgb, _pImage);
}
//代码⽚段2
private MemoryStream outputStream;
private void DataReceived(object sender, EventArgs e)
{
if (outputStream.Position == 6220800)
lock (_imageLock)
{
var data = outputStream.ToArray();
Marshal.Copy(data, 0, _pImage, data.Length);
outputStream.Seek(0, SeekOrigin.Begin);
}
}
花了不少时间摸索(不要看只有⼏⾏,⼈都整崩溃了),得出了上述代码。⾸先,我捕获的图像数据是24位的,并且图像⼤⼩是1080p的,所以,实际上,⼀个原始位图数据的⼤⼩为stride * height,即width * 3 * height,⼤⼩为6220800 bytes。所以,在判断了捕获数据到达这个⼤⼩后,就进⾏Bitmap转换处理,然后将MemoryStream的位置移动到最开始。需要注意的时,由于捕获到的是原始数据(不包含bmp的HeaderInfo),所以注意看Bitmap的构造⽅式,是通过⼀个指向原始数据位置的指针就⾏构造的,更新该图像时,也仅需要更新指针指向的位置数据即可,⽆需在建⽴新的Bitmap实例。
位图数据获取到了,就可以进⾏识别处理了,⾼⾼兴兴的加上了识别逻辑,但是现实总是充满了意外和惊喜,没错,坑⼜来了。没有加⼊识别逻辑的时候,捕获到的图像在PictureBox上显⽰⾮常正常,清晰、流畅,加上识别逻辑后,开始出现花屏(捕获到的图像花屏)、拖影、显⽰延迟(⾄少会延迟1
0-20秒以上)、程序卡顿,总之就是各种问题。最开始,我的识别逻辑写到DataReceived⽅法⾥⾯的,这个⽅法是运⾏于主线程外的另⼀个线程中的,其实按道理将,捕获、识别、显⽰位于⼀个线程中,应该是不会出现问题,我估计(不确定,没有去深⼊研究,如果谁知道实际原因,可以留⾔告诉我),是因为ffmpeg的原因,因为ffmpeg是单独的⼀个进程在跑,他的数据捕获是持续在进⾏的,⽽识别模块的处理时间⼤于每⼀帧的采集时间,所以,缓冲区中的数据没有得到及时处理,ffmpeg接收到的部分图像数据(⼤于32768的数据)被丢弃了,然后就出现了各种问题。最后,⼜是⼀次耗时不短的探索之旅。
private void Render()
{
while (_renderRunning)
{
if (_image == null)
continue;
Bitmap image;
lock (_imageLock)
{
image = (Bitmap) _image.Clone();
}
if (_shouldShot){
WriteFeature(image);
_shouldShot = false;
}
Verify(image);
if (videoImage.InvokeRequired)
videoImage.Invoke(new Action(() => { videoImage.Image = image; }));
else
videoImage.Image = image;
}
}
如上代码所述,我单独开了⼀个线程,⽤于图像的识别处理和显⽰,每次都从已捕获到的图像中克隆出新的Bitmap实例进⾏处理。这种⽅式的缺点在于,有可能会导致丢帧的现象,因为上⾯说到了,识别时间(如果检测到新的⼈脸,那么加上匹配,⼤约需要130ms左右)⼤于每帧时间,但是并不影响识别效果和需求的实现,基本丢弃的帧可以忽律。最后,运⾏,稳定了、完美了,实际也感觉不到丢帧。
Demo程序,我运⾏了⼤约4天左右,中间没有出现过任何异常和识别错误。
写在最后
虽然虹软官⽅表⽰,免费识别库适⽤于1000⼈脸库以下的识别,实际上,做⼀定的⼯作(⼯作量其实也不⼩),也是可以实现较⼤规模的⼈脸搜索滴。例如,采⽤多线程进⾏匹配,如果⼈脸库⼈脸数量
⼤于1000,则可以考虑每个线程分别进⾏处理,⼈脸特征数据做缓存(⼀个⼈脸的特征数据是22KB,对内存要求较⾼),以提升程序的识别搜索效率。或者⼈脸库特别⼤的情况下,可以采⽤分布式处理,⼈脸特征加载到Redis数据库当中,多个进程多个线程读取处理,每个线程上传⾃⼰的识别结果,然后主进程做结果合并判断⼯作,主要的挑战就在于多线程的⼯作分配⼀致性和对单点故障的容错性。
更新:
DEMO中的例⼦采⽤了IP Camera,⼀般情况下,⼤家可能⽤USB Camera居多,所以,更新了源代码,增加了USB Camera的例⼦,只需
要屏蔽掉IP Camara代码即可。
task = ffmpeg.ConvertLiveMedia("video=USB2.0 PC CAMERA", "dshow",
outputStream, Format.raw_video, setting);
需要注意的有以下⼏点:
设备名称可以通过控制⾯板或者ffmpeg的命令获取:ffmpeg -list_devices true -f dshow -i dummy
注意修改捕获的图像⼤⼩,⼀般USB摄像头是640*480,更新的代码增加了全局变量,可以直接修改。
如果要查询USB摄像头⽀持的分辨率,也可以通过ffmpeg命令:ffmpeg -list_options true -f dshow -i video="USB2.0 PC CAMERA"标签: , ,
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论