C#實(shí)現(xiàn)基于ffmpeg加虹軟的人臉識(shí)別的示例
關(guān)于人臉識(shí)別
目前的人臉識(shí)別已經(jīng)相對(duì)成熟,有各種收費(fèi)免費(fèi)的商業(yè)方案和開源方案,其中OpenCV很早就支持了人臉識(shí)別,在我選擇人臉識(shí)別開發(fā)庫時(shí),也橫向?qū)Ρ攘巳N庫,包括在線識(shí)別的百度、開源的OpenCV和商業(yè)庫虹軟(中小型規(guī)模免費(fèi))。
百度的人臉識(shí)別,才上線不久,文檔不太完善,之前聯(lián)系百度,官方也給了我基于Android的Example,但是不太符合我的需求,一是照片需要上傳至百度服務(wù)器(這個(gè)是最大的問題),其次,人臉的定位需要自行去實(shí)現(xiàn)(捕獲到人臉后上傳進(jìn)行識(shí)別)。
OpenCV很早以前就用過,當(dāng)時(shí)做人臉+車牌識(shí)別時(shí),最先考慮的就是OpenCV,但是識(shí)別率在當(dāng)時(shí)不算很高,后來是采用了一個(gè)電子科大的老師自行開發(fā)的識(shí)別庫(相對(duì)易用,識(shí)別率也還不錯(cuò)),所以這次準(zhǔn)備做時(shí),沒有選擇OpenCV。
虹軟其實(shí)在無意間發(fā)現(xiàn)的,當(dāng)時(shí)正在尋找開發(fā)庫,正在測(cè)試Python的一個(gè)方案,就發(fā)現(xiàn)有新聞?wù)f虹軟的識(shí)別庫全面開放并且可以免費(fèi)使用,而且是離線識(shí)別,所以就下載嘗試了一下,發(fā)現(xiàn)識(shí)別率還不錯(cuò),所以就暫定了采用虹軟的識(shí)別方案。這里主要就給大家分享一下開發(fā)過程當(dāng)中的一些坑和使用心得,順便開源識(shí)別庫的C# Wrapper。
SDK的C# Wrapper
由于虹軟的庫是采用C++開發(fā)的,而我的應(yīng)用程序采用的是C#,所以,需要對(duì)庫進(jìn)行包裝,便于C#的調(diào)用,包裝的主要需求是可以在C#中快速方便的調(diào)用,無需考慮內(nèi)存、指針等問題,并且具備一定的容錯(cuò)性。Wrapper庫目前已經(jīng)開源,大家可以到Github上進(jìn)行下載,地址點(diǎn)擊這里。Wrapper庫基本上沒有什么可以說的,無非是對(duì)PInvoke的包裝,只是里面做了比較多的細(xì)節(jié)處理,屏蔽了調(diào)用細(xì)節(jié),提供了相對(duì)高層的函數(shù)。有興趣的可以看看源代碼。
Wrapper庫的使用例子
基本使用
人臉檢測(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è)沒啥區(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è)整合包裝類,集成了檢測(cè)和識(shí)別,如果要單獨(dú)使用識(shí)別,可以使用FaceRecognize類 //這里做演示,假設(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)和特征庫的特征進(jìn)行比對(duì)。
整合的完整例子
在Github上,有完整的FaceDemo例子,里面主要實(shí)現(xiàn)了通過ffmpeg采集RTSP協(xié)議的圖像(使用海康的攝像機(jī)),然后進(jìn)行人臉匹配。在開發(fā)過程中遇到不少的坑。
人臉識(shí)別的首要工作就是捕獲攝像機(jī)視頻幀,這一塊上是坑的最久的,因?yàn)樽铋_始采用的是OpenCV的包裝庫,Emgu.CV,在開發(fā)過程中,捕獲USB攝像頭時(shí),倒是問題不大,沒有出現(xiàn)過異常。在捕獲RTSP視頻流時(shí),會(huì)不定時(shí)的出現(xiàn)AccessviolationException異常,短則幾十分鐘,長(zhǎng)則幾個(gè)小時(shí),總之就是不穩(wěn)定。在官方Github地址上,也提了Issue,他們給出的答復(fù)是屏蔽的我業(yè)務(wù)邏輯,僅捕獲視頻流試試,結(jié)果問題依然,所以,我基本坑定了試Emgu.CV上面的問題。后來經(jīng)過反復(fù)的實(shí)驗(yàn),最終確定了選擇ffmpeg。
ffmepg主要采用ProcessStartInfo進(jìn)行調(diào)用,我采用的是NReco.VideoConverter(一個(gè)ffmpeg調(diào)用的包裝,可以通過nuget搜索安裝),雖然ffmpeg解決了穩(wěn)定性問題,但是實(shí)際開發(fā)時(shí),也遇到了不少坑,其中,最主要的是NReco.VideoConverter沒有任何文檔和例子(實(shí)際有,需要75刀購(gòu)買),所以,自己研究了半天,如何捕獲視頻流并轉(zhuǎn)換為Bitmap對(duì)象。只要實(shí)現(xiàn)這一步,后續(xù)就是調(diào)用Wrapper就行了。
FaceDemo詳解
上面說到了,通過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/h264/ch1/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ì)有太大問題(還是看具體設(shè)備),之前就是用成了rgb24,結(jié)果捕獲出來的圖像,人都變成阿凡達(dá)了,顏色是反的。最后一個(gè)參數(shù),坑的我差點(diǎn)放棄這個(gè)方案。本身,ffmpeg在調(diào)用時(shí),需要指定一個(gè)文件名模板,捕獲到的輸出會(huì)按照模板生成文件,如果要將數(shù)據(jù)輸出到控制臺(tái),則最后傳入一個(gè)-即可,最開始沒有指定updatefirst,ffmpeg在捕獲了第一幀后就拋出了異常,最后查了半天ffmpeg說明(完整參數(shù)說明非常多,輸出到文本有1319KB),發(fā)現(xiàn)了這個(gè)參數(shù),表示持續(xù)更新第一個(gè)文件。最后,在調(diào)用視頻捕獲是,需要指定輸出格式,必須指定為Format.raw_video,實(shí)際上這個(gè)格式名稱有些誤導(dǎo)人,按道理將應(yīng)該叫做raw_image,因?yàn)樽罱K輸出的是每幀原始的位圖數(shù)據(jù)。
到此為止,還并沒有解決視頻流數(shù)據(jù)的捕獲,因?yàn)橛謥硪粋€(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)到最開始。需要注意的時(shí),由于捕獲到的是原始數(shù)據(jù)(不包含bmp的HeaderInfo),所以注意看Bitmap的構(gòu)造方式,是通過一個(gè)指向原始數(shù)據(jù)位置的指針就行構(gòu)造的,更新該圖像時(shí),也僅需要更新指針指向的位置數(shù)據(jù)即可,無需在建立新的Bitmap實(shí)例。
位圖數(shù)據(jù)獲取到了,就可以進(jìn)行識(shí)別處理了,高高興興的加上了識(shí)別邏輯,但是現(xiàn)實(shí)總是充滿了意外和驚喜,沒錯(cuò),坑又來了。沒有加入識(shí)別邏輯的時(shí)候,捕獲到的圖像在PictureBox上顯示非常正常,清晰、流暢,加上識(shí)別邏輯后,開始出現(xiàn)花屏(捕獲到的圖像花屏)、拖影、顯示延遲(至少會(huì)延遲10-20秒以上)、程序卡頓,總之就是各種問題。最開始,我的識(shí)別邏輯寫到DataReceived方法里面的,這個(gè)方法是運(yùn)行于主線程外的另一個(gè)線程中的,其實(shí)按道理將,捕獲、識(shí)別、顯示位于一個(gè)線程中,應(yīng)該是不會(huì)出現(xiàn)問題,我估計(jì)(不確定,沒有去深入研究,如果誰知道實(shí)際原因,可以留言告訴我),是因?yàn)閒fmpeg的原因,因?yàn)閒fmpeg是單獨(dú)的一個(gè)進(jìn)程在跑,他的數(shù)據(jù)捕獲是持續(xù)在進(jìn)行的,而識(shí)別模塊的處理時(shí)間大于每一幀的采集時(shí)間,所以,緩沖區(qū)中的數(shù)據(jù)沒有得到及時(shí)處理,ffmpeg接收到的部分圖像數(shù)據(jù)(大于32768的數(shù)據(jù))被丟棄了,然后就出現(xià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ú)開了一個(gè)線程,用于圖像的識(shí)別處理和顯示,每次都從已捕獲到的圖像中克隆出新的Bitmap實(shí)例進(jìn)行處理。這種方式的缺點(diǎn)在于,有可能會(huì)導(dǎo)致丟幀的現(xiàn)象,因?yàn)樯厦嬲f到了,識(shí)別時(shí)間(如果檢測(cè)到新的人臉,那么加上匹配,大約需要130ms左右)大于每幀時(shí)間,但是并不影響識(shí)別效果和需求的實(shí)現(xiàn),基本丟棄的幀可以忽律。最后,運(yùn)行,穩(wěn)定了、完美了,實(shí)際也感覺不到丟幀。
Demo程序,我運(yùn)行了大約4天左右,中間沒有出現(xiàn)過任何異常和識(shí)別錯(cuò)誤。
寫在最后
雖然虹軟官方表示,免費(fèi)識(shí)別庫適用于1000人臉庫以下的識(shí)別,實(shí)際上,做一定的工作(工作量其實(shí)也不?。?,也是可以實(shí)現(xiàn)較大規(guī)模的人臉?biāo)阉鞯?。例如,采用多線程進(jìn)行匹配,如果人臉庫人臉數(shù)量大于1000,則可以考慮每個(gè)線程分別進(jìn)行處理,人臉特征數(shù)據(jù)做緩存(一個(gè)人臉的特征數(shù)據(jù)是22KB,對(duì)內(nèi)存要求較高),以提升程序的識(shí)別搜索效率?;蛘呷四槑焯貏e大的情況下,可以采用分布式處理,人臉特征加載到Redis數(shù)據(jù)庫當(dāng)中,多個(gè)進(jìn)程多個(gè)線程讀取處理,每個(gè)線程上傳自己的識(shí)別結(jié)果,然后主進(jìn)程做結(jié)果合并判斷工作,主要的挑戰(zhàn)就在于多線程的工作分配一致性和對(duì)單點(diǎn)故障的容錯(cuò)性。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持我們。
上一篇:C#開發(fā)之微信小程序發(fā)送模板消息功能
欄 目:C#教程
下一篇:C#調(diào)用AForge實(shí)現(xiàn)攝像頭錄像的示例代碼
本文標(biāo)題:C#實(shí)現(xiàn)基于ffmpeg加虹軟的人臉識(shí)別的示例
本文地址:http://mengdiqiu.com.cn/a1/C_jiaocheng/5470.html
您可能感興趣的文章
- 01-10C#實(shí)現(xiàn)txt定位指定行完整實(shí)例
- 01-10WinForm實(shí)現(xiàn)仿視頻播放器左下角滾動(dòng)新聞效果的方法
- 01-10C#實(shí)現(xiàn)清空回收站的方法
- 01-10C#實(shí)現(xiàn)讀取注冊(cè)表監(jiān)控當(dāng)前操作系統(tǒng)已安裝軟件變化的方法
- 01-10C#實(shí)現(xiàn)多線程下載文件的方法
- 01-10C#實(shí)現(xiàn)Winform中打開網(wǎng)頁頁面的方法
- 01-10C#實(shí)現(xiàn)遠(yuǎn)程關(guān)閉計(jì)算機(jī)或重啟計(jì)算機(jī)的方法
- 01-10C#自定義簽名章實(shí)現(xiàn)方法
- 01-10C#文件斷點(diǎn)續(xù)傳實(shí)現(xiàn)方法
- 01-10winform實(shí)現(xiàn)創(chuàng)建最前端窗體的方法


閱讀排行
- 1C語言 while語句的用法詳解
- 2java 實(shí)現(xiàn)簡(jiǎn)單圣誕樹的示例代碼(圣誕
- 3利用C語言實(shí)現(xiàn)“百馬百擔(dān)”問題方法
- 4C語言中計(jì)算正弦的相關(guān)函數(shù)總結(jié)
- 5c語言計(jì)算三角形面積代碼
- 6什么是 WSH(腳本宿主)的詳細(xì)解釋
- 7C++ 中隨機(jī)函數(shù)random函數(shù)的使用方法
- 8正則表達(dá)式匹配各種特殊字符
- 9C語言十進(jìn)制轉(zhuǎn)二進(jìn)制代碼實(shí)例
- 10C語言查找數(shù)組里數(shù)字重復(fù)次數(shù)的方法
本欄相關(guān)
- 01-10C#通過反射獲取當(dāng)前工程中所有窗體并
- 01-10關(guān)于ASP網(wǎng)頁無法打開的解決方案
- 01-10WinForm限制窗體不能移到屏幕外的方法
- 01-10WinForm繪制圓角的方法
- 01-10C#實(shí)現(xiàn)txt定位指定行完整實(shí)例
- 01-10WinForm實(shí)現(xiàn)仿視頻播放器左下角滾動(dòng)新
- 01-10C#停止線程的方法
- 01-10C#實(shí)現(xiàn)清空回收站的方法
- 01-10C#通過重寫Panel改變邊框顏色與寬度的
- 01-10C#實(shí)現(xiàn)讀取注冊(cè)表監(jiān)控當(dāng)前操作系統(tǒng)已
隨機(jī)閱讀
- 01-10C#中split用法實(shí)例總結(jié)
- 01-10使用C語言求解撲克牌的順子及n個(gè)骰子
- 08-05織夢(mèng)dedecms什么時(shí)候用欄目交叉功能?
- 04-02jquery與jsp,用jquery
- 08-05dedecms(織夢(mèng))副欄目數(shù)量限制代碼修改
- 01-11Mac OSX 打開原生自帶讀寫NTFS功能(圖文
- 01-10delphi制作wav文件的方法
- 01-10SublimeText編譯C開發(fā)環(huán)境設(shè)置
- 01-11ajax實(shí)現(xiàn)頁面的局部加載
- 08-05DEDE織夢(mèng)data目錄下的sessions文件夾有什