C#實現基於ffmpeg加虹軟的人臉識別demo及開發分享
對開發庫的C#封裝,遮蔽使用細節,可以快速安全的呼叫人臉識別相關API。具體見github地址。新增對.NET Core的支援,在Linux(Ubuntu下)測試通過。具體的使用例子和Demo詳解,參見部落格地址。
更新: 增加對V1.1兩個新功能的支援。
關於人臉識別 目前的人臉識別已經相對成熟,有各種收費免費的商業方案和開源方案,其中OpenCV很早就支援了人臉識別,在我選擇人臉識別開發庫時,也橫向對比了三種庫,包括線上識別的百度、開源的OpenCV和商業庫虹軟(中小型規模免費)。
百度的人臉識別,才上線不久,文件不太完善,之前聯絡百度,官方也給了我基於Android的Example,但是不太符合我的需求,一是照片需要上傳至百度伺服器(這個是最大的問題),其次,人臉的定位需要自行去實現(捕獲到人臉後上傳進行識別)。
OpenCV很早以前就用過,當時做人臉+車牌識別時,最先考慮的就是OpenCV,但是識別率在當時不算很高,後來是採用了一個電子科大的老師自行開發的識別庫(相對易用,識別率也還不錯),所以這次準備做時,沒有選擇OpenCV。
虹軟其實在無意間發現的,當時正在尋找開發庫,正在測試Python的一個方案,就發現有新聞說虹軟的識別庫全面開放並且可以免費使用,而且是離線識別,所以就下載嘗試了一下,發現識別率還不錯,所以就暫定了採用虹軟的識別方案。這裡主要就給大家分享一下開發過程當中的一些坑和使用心得,順便開源識別庫的C# Wrapper。
SDK的C# Wrapper 由於虹軟的庫是採用C++開發的,而我的應用程式採用的是C#,所以,需要對庫進行包裝,便於C#的呼叫,包裝的主要需求是可以在C#中快速方便的呼叫,無需考慮記憶體、指標等問題,並且具備一定的容錯性。Wrapper庫目前已經開源,大家可以到Github上進行下載,地址點選這裡。Wrapper庫基本上沒有什麼可以說的,無非是對PInvoke的包裝,只是裡面做了比較多的細節處理,遮蔽了呼叫細節,提供了相對高層的函式。有興趣的可以看看原始碼。 Wrapper庫的使用例子 基本使用
注意使用之前,在虹軟申請了新的Key後,需要同時更新libs下的三個dll檔案,key和sdk的版本是相關聯的,否則會丟擲異常。
人臉檢測(靜態圖片):
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) { 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上,有完整的FaceDemo例子,裡面主要實現了通過ffmpeg採集RTSP協議的影象(使用海康的攝像機),然後進行人臉匹配。在開發過程中遇到不少的坑。
人臉識別的首要工作就是捕獲攝像機視訊幀,這一塊上是坑的最久的,因為最開始採用的是OpenCV的包裝庫,Emgu.CV,在開發過程中,捕獲USB攝像頭時,倒是問題不大,沒有出現過異常。在捕獲RTSP視訊流時,會不定時的出現AccessviolationException異常,短則幾十分鐘,長則幾個小時,總之就是不穩定。在官方Github地址上,也提了Issue,他們給出的答覆是遮蔽的我業務邏輯,僅捕獲視訊流試試,結果問題依然,所以,我基本坑定了試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:[email protected]: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上顯示非常正常,清晰、流暢,加上識別邏輯後,開始出現花屏(捕獲到的影象花屏)、拖影、顯示延遲(至少會延遲10-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” 更新2:
原始碼中新增了對 .net core 2.0的支援,因為用到了GDI+相關函式,所以用的是CoreCompat/System.Drawing,所以在部署環境下需要安裝libgdiplus, apt-get intall libgdiplus。
另外,有關於視訊流的採集,除了使用FFMEPG和一些開源的開發庫外,也可以使用廠商的SDK,不過之前試過海康的SDK,那叫一個難用啊,所以大家自己選擇吧。
更新3: 虹軟SDK更新了新的功能,開發包同步更新,支援年