關(guān)于人臉識(shí)別
創(chuàng)新互聯(lián)專(zhuān)業(yè)為企業(yè)提供渭濱網(wǎng)站建設(shè)、渭濱做網(wǎng)站、渭濱網(wǎng)站設(shè)計(jì)、渭濱網(wǎng)站制作等企業(yè)網(wǎng)站建設(shè)、網(wǎng)頁(yè)設(shè)計(jì)與制作、渭濱企業(yè)網(wǎng)站模板建站服務(wù),10余年渭濱做網(wǎng)站經(jīng)驗(yàn),不只是建網(wǎng)站,更提供有價(jià)值的思路和整體網(wǎng)絡(luò)服務(wù)。
目前的人臉識(shí)別已經(jīng)相對(duì)成熟,有各種收費(fèi)免費(fèi)的商業(yè)方案和開(kāi)源方案,其中OpenCV很早就支持了人臉識(shí)別,在我選擇人臉識(shí)別開(kāi)發(fā)庫(kù)時(shí),也橫向?qū)Ρ攘巳N庫(kù),包括在線(xiàn)識(shí)別的百度、開(kāi)源的OpenCV和商業(yè)庫(kù)虹軟(中小型規(guī)模免費(fèi))。
百度的人臉識(shí)別,才上線(xiàn)不久,文檔不太完善,之前聯(lián)系百度,官方也給了我基于A(yíng)ndroid的Example,但是不太符合我的需求,一是照片需要上傳至百度服務(wù)器(這個(gè)是最大的問(wèn)題),其次,人臉的定位需要自行去實(shí)現(xiàn)(捕獲到人臉后上傳進(jìn)行識(shí)別)。
OpenCV很早以前就用過(guò),當(dāng)時(shí)做人臉+車(chē)牌識(shí)別時(shí),最先考慮的就是OpenCV,但是識(shí)別率在當(dāng)時(shí)不算很高,后來(lái)是采用了一個(gè)電子科大的老師自行開(kāi)發(fā)的識(shí)別庫(kù)(相對(duì)易用,識(shí)別率也還不錯(cuò)),所以這次準(zhǔn)備做時(shí),沒(méi)有選擇OpenCV。
虹軟其實(shí)在無(wú)意間發(fā)現(xiàn)的,當(dāng)時(shí)正在尋找開(kāi)發(fā)庫(kù),正在測(cè)試Python的一個(gè)方案,就發(fā)現(xiàn)有新聞?wù)f虹軟的識(shí)別庫(kù)全面開(kāi)放并且可以免費(fèi)使用,而且是離線(xiàn)識(shí)別,所以就下載嘗試了一下,發(fā)現(xiàn)識(shí)別率還不錯(cuò),所以就暫定了采用虹軟的識(shí)別方案。這里主要就給大家分享一下開(kāi)發(fā)過(guò)程當(dāng)中的一些坑和使用心得,順便開(kāi)源識(shí)別庫(kù)的C# Wrapper。
SDK的C# Wrapper
由于虹軟的庫(kù)是采用C++開(kāi)發(fā)的,而我的應(yīng)用程序采用的是C#,所以,需要對(duì)庫(kù)進(jìn)行包裝,便于C#的調(diào)用,包裝的主要需求是可以在C#中快速方便的調(diào)用,無(wú)需考慮內(nèi)存、指針等問(wèn)題,并且具備一定的容錯(cuò)性。Wrapper庫(kù)目前已經(jīng)開(kāi)源,大家可以到Github上進(jìn)行下載,地址點(diǎn)擊這里。Wrapper庫(kù)基本上沒(méi)有什么可以說(shuō)的,無(wú)非是對(duì)PInvoke的包裝,只是里面做了比較多的細(xì)節(jié)處理,屏蔽了調(diào)用細(xì)節(jié),提供了相對(duì)高層的函數(shù)。有興趣的可以看看源代碼。
Wrapper庫(kù)的使用例子
基本使用
人臉檢測(cè)(靜態(tài)圖片):
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); //檢測(cè)到位置信息在使用完畢后,需要釋放資源,避免內(nèi)存泄露 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); } } }
人臉跟蹤(人臉跟蹤一般用于視頻的連續(xù)幀識(shí)別,相較于檢測(cè),又更高的執(zhí)行效率,這里用靜態(tài)圖片做例子,實(shí)際使用和檢測(cè)沒(méi)啥區(qū)別):
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); } } }
人臉對(duì)比:
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是個(gè)整合包裝類(lèi),集成了檢測(cè)和識(shí)別,如果要單獨(dú)使用識(shí)別,可以使用FaceRecognize類(lèi) //這里做演示,假設(shè)圖片都只有一張臉 //可以將FeatureData持久化保存,這個(gè)即是人臉特征數(shù)據(jù),用于后續(xù)的人臉匹配 //File.WriteAllBytes("XXX.data", feature.FeatureData);FeatureData會(huì)自動(dòng)轉(zhuǎn)型為byte數(shù)組 if ((result1 != null) & (result2 != null)) Console.WriteLine(proccesor.Match(result1[0].FeatureData, result2[0].FeatureData, true)); }
使用注意事項(xiàng)
LocateResult(檢測(cè)結(jié)果)和Feature(人臉特征)都包含需要釋放的內(nèi)存資源,在使用完畢后,記得需要釋放,否則會(huì)引起內(nèi)存泄露。FaceProcessor和FaceRecognize的Match函數(shù),在完成比較后,可以自動(dòng)釋放,只需要最后兩個(gè)參數(shù)指定為true即可,如果是用于人臉匹配(1:N),則可以采用默認(rèn)參數(shù),這種情況下,第一個(gè)參數(shù)指定的特征數(shù)據(jù)不會(huì)自動(dòng)釋放,用于循環(huán)和特征庫(kù)的特征進(jìn)行比對(duì)。
整合的完整例子
在Github上,有完整的FaceDemo例子,里面主要實(shí)現(xiàn)了通過(guò)ffmpeg采集RTSP協(xié)議的圖像(使用??档臄z像機(jī)),然后進(jìn)行人臉匹配。在開(kāi)發(fā)過(guò)程中遇到不少的坑。
人臉識(shí)別的首要工作就是捕獲攝像機(jī)視頻幀,這一塊上是坑的最久的,因?yàn)樽铋_(kāi)始采用的是OpenCV的包裝庫(kù),Emgu.CV,在開(kāi)發(fā)過(guò)程中,捕獲USB攝像頭時(shí),倒是問(wèn)題不大,沒(méi)有出現(xiàn)過(guò)異常。在捕獲RTSP視頻流時(shí),會(huì)不定時(shí)的出現(xiàn)AccessviolationException異常,短則幾十分鐘,長(zhǎng)則幾個(gè)小時(shí),總之就是不穩(wěn)定。在官方Github地址上,也提了Issue,他們給出的答復(fù)是屏蔽的我業(yè)務(wù)邏輯,僅捕獲視頻流試試,結(jié)果問(wèn)題依然,所以,我基本坑定了試Emgu.CV上面的問(wèn)題。后來(lái)經(jīng)過(guò)反復(fù)的實(shí)驗(yàn),最終確定了選擇ffmpeg。
ffmepg主要采用ProcessStartInfo進(jìn)行調(diào)用,我采用的是NReco.VideoConverter(一個(gè)ffmpeg調(diào)用的包裝,可以通過(guò)nuget搜索安裝),雖然ffmpeg解決了穩(wěn)定性問(wèn)題,但是實(shí)際開(kāi)發(fā)時(shí),也遇到了不少坑,其中,最主要的是NReco.VideoConverter沒(méi)有任何文檔和例子(實(shí)際有,需要75刀購(gòu)買(mǎi)),所以,自己研究了半天,如何捕獲視頻流并轉(zhuǎn)換為Bitmap對(duì)象。只要實(shí)現(xiàn)這一步,后續(xù)就是調(diào)用Wrapper就行了。
FaceDemo詳解
上面說(shuō)到了,通過(guò)ffmpeg捕獲視頻流并轉(zhuǎn)換Bitmap是重點(diǎn),所以,這里也主要介紹這一塊。
首先是ffmpeg的調(diào)用參數(shù):
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/h364/ch2/main/av_stream", null, outputStream, Format.raw_video, setting); task.OutputDataReceived += DataReceived; task.Start();
-an表示不捕獲音頻流,-r表示幀率,根據(jù)需求和實(shí)際設(shè)備調(diào)整此參數(shù),-pix_fmt比較重要,一般情況下,指定為bgr24不會(huì)有太大問(wèn)題(還是看具體設(shè)備),之前就是用成了rgb24,結(jié)果捕獲出來(lái)的圖像,人都變成阿凡達(dá)了,顏色是反的。最后一個(gè)參數(shù),坑的我差點(diǎn)放棄這個(gè)方案。本身,ffmpeg在調(diào)用時(shí),需要指定一個(gè)文件名模板,捕獲到的輸出會(huì)按照模板生成文件,如果要將數(shù)據(jù)輸出到控制臺(tái),則最后傳入一個(gè)-即可,最開(kāi)始沒(méi)有指定updatefirst,ffmpeg在捕獲了第一幀后就拋出了異常,最后查了半天ffmpeg說(shuō)明(完整參數(shù)說(shuō)明非常多,輸出到文本有1319KB),發(fā)現(xiàn)了這個(gè)參數(shù),表示持續(xù)更新第一個(gè)文件。最后,在調(diào)用視頻捕獲是,需要指定輸出格式,必須指定為Format.raw_video,實(shí)際上這個(gè)格式名稱(chēng)有些誤導(dǎo)人,按道理將應(yīng)該叫做raw_image,因?yàn)樽罱K輸出的是每幀原始的位圖數(shù)據(jù)。
到此為止,還并沒(méi)有解決視頻流數(shù)據(jù)的捕獲,因?yàn)橛謥?lái)一個(gè)坑,ProcessStartInfo的控制臺(tái)緩沖區(qū)大小只有32768 bytes,即,每一次的輸出,實(shí)際上并不是一個(gè)完整的位圖數(shù)據(jù)。
//完整代碼參加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); } }
花了不少時(shí)間摸索(不要看只有幾行,人都整崩潰了),得出了上述代碼。首先,我捕獲的圖像數(shù)據(jù)是24位的,并且圖像大小是1080p的,所以,實(shí)際上,一個(gè)原始位圖數(shù)據(jù)的大小為stride * height,即width * 3 * height,大小為6220800 bytes。所以,在判斷了捕獲數(shù)據(jù)到達(dá)這個(gè)大小后,就進(jìn)行Bitmap轉(zhuǎn)換處理,然后將MemoryStream的位置移動(dòng)到最開(kāi)始。需要注意的時(shí),由于捕獲到的是原始數(shù)據(jù)(不包含bmp的HeaderInfo),所以注意看Bitmap的構(gòu)造方式,是通過(guò)一個(gè)指向原始數(shù)據(jù)位置的指針就行構(gòu)造的,更新該圖像時(shí),也僅需要更新指針指向的位置數(shù)據(jù)即可,無(wú)需在建立新的Bitmap實(shí)例。
位圖數(shù)據(jù)獲取到了,就可以進(jìn)行識(shí)別處理了,高高興興的加上了識(shí)別邏輯,但是現(xiàn)實(shí)總是充滿(mǎn)了意外和驚喜,沒(méi)錯(cuò),坑又來(lái)了。沒(méi)有加入識(shí)別邏輯的時(shí)候,捕獲到的圖像在PictureBox上顯示非常正常,清晰、流暢,加上識(shí)別邏輯后,開(kāi)始出現(xiàn)花屏(捕獲到的圖像花屏)、拖影、顯示延遲(至少會(huì)延遲10-20秒以上)、程序卡頓,總之就是各種問(wèn)題。最開(kāi)始,我的識(shí)別邏輯寫(xiě)到DataReceived方法里面的,這個(gè)方法是運(yùn)行于主線(xiàn)程外的另一個(gè)線(xiàn)程中的,其實(shí)按道理將,捕獲、識(shí)別、顯示位于一個(gè)線(xiàn)程中,應(yīng)該是不會(huì)出現(xiàn)問(wèn)題,我估計(jì)(不確定,沒(méi)有去深入研究,如果誰(shuí)知道實(shí)際原因,可以留言告訴我),是因?yàn)閒fmpeg的原因,因?yàn)閒fmpeg是單獨(dú)的一個(gè)進(jìn)程在跑,他的數(shù)據(jù)捕獲是持續(xù)在進(jìn)行的,而識(shí)別模塊的處理時(shí)間大于每一幀的采集時(shí)間,所以,緩沖區(qū)中的數(shù)據(jù)沒(méi)有得到及時(shí)處理,ffmpeg接收到的部分圖像數(shù)據(jù)(大于32768的數(shù)據(jù))被丟棄了,然后就出現(xiàn)了各種問(wèn)題。最后,又是一次耗時(shí)不短的探索之旅。
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; } }
如上代碼所述,我單獨(dú)開(kāi)了一個(gè)線(xiàn)程,用于圖像的識(shí)別處理和顯示,每次都從已捕獲到的圖像中克隆出新的Bitmap實(shí)例進(jìn)行處理。這種方式的缺點(diǎn)在于,有可能會(huì)導(dǎo)致丟幀的現(xiàn)象,因?yàn)樯厦嬲f(shuō)到了,識(shí)別時(shí)間(如果檢測(cè)到新的人臉,那么加上匹配,大約需要130ms左右)大于每幀時(shí)間,但是并不影響識(shí)別效果和需求的實(shí)現(xiàn),基本丟棄的幀可以忽律。最后,運(yùn)行,穩(wěn)定了、完美了,實(shí)際也感覺(jué)不到丟幀。
Demo程序,我運(yùn)行了大約4天左右,中間沒(méi)有出現(xiàn)過(guò)任何異常和識(shí)別錯(cuò)誤。
寫(xiě)在最后
雖然虹軟官方表示,免費(fèi)識(shí)別庫(kù)適用于1000人臉庫(kù)以下的識(shí)別,實(shí)際上,做一定的工作(工作量其實(shí)也不?。?,也是可以實(shí)現(xiàn)較大規(guī)模的人臉?biāo)阉鞯?。例如,采用多線(xiàn)程進(jìn)行匹配,如果人臉庫(kù)人臉數(shù)量大于1000,則可以考慮每個(gè)線(xiàn)程分別進(jìn)行處理,人臉特征數(shù)據(jù)做緩存(一個(gè)人臉的特征數(shù)據(jù)是22KB,對(duì)內(nèi)存要求較高),以提升程序的識(shí)別搜索效率?;蛘呷四槑?kù)特別大的情況下,可以采用分布式處理,人臉特征加載到redis數(shù)據(jù)庫(kù)當(dāng)中,多個(gè)進(jìn)程多個(gè)線(xiàn)程讀取處理,每個(gè)線(xiàn)程上傳自己的識(shí)別結(jié)果,然后主進(jìn)程做結(jié)果合并判斷工作,主要的挑戰(zhàn)就在于多線(xiàn)程的工作分配一致性和對(duì)單點(diǎn)故障的容錯(cuò)性。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持創(chuàng)新互聯(lián)。