C# 實現基於ffmpeg加虹軟的人臉識別

關於人臉識別數據庫

目前的人臉識別已經相對成熟,有各類收費免費的商業方案和開源方案,其中OpenCV很早就支持了人臉識別,在我選擇人臉數組

識別開發庫時,也橫向對比了三種庫,包括在線識別的百度、開源的OpenCV和商業庫虹軟(中小型規模免費)。緩存

百度的人臉識別,才上線不久,文檔不太完善,以前聯繫百度,官方也給了我基於Android的Example,可是不太符合個人需服務器

求,一是照片須要上傳至百度服務器(這個是最大的問題),其次,人臉的定位須要自行去實現(捕獲到人臉後上傳進行識別)。多線程

OpenCV很早之前就用過,當時作人臉+車牌識別時,最早考慮的就是OpenCV,可是識別率在當時不算很高,後來是採用了app

一個電子科大的老師自行開發的識別庫(相對易用,識別率也還不錯),因此此次準備作時,沒有選擇OpenCV。分佈式

虹軟其實在無心間發現的,當時正在尋找開發庫,正在測試Python的一個方案,就發現有新聞說虹軟的識別庫全面開放而且ide

能夠無償使用,並且是離線識別,因此就下載嘗試了一下,發現識別率還不錯,因此就暫定了採用虹軟的識別方案。這裏主函數

要就給你們分享一下開發過程中的一些坑和使用心得,順便開源識別庫的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: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上顯示很是正常,清晰、流暢,加上識別邏輯後,開始出現花屏

(捕獲到的圖像花屏)、拖影、顯示延遲(至少會延遲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數據庫當中,多個進程多個線程讀取處理,每一個線程上傳本身的識

別結果,而後主進程作結果合併判斷工做,主要的挑戰就在於多線程的工做分配一致性和對單點故障的容錯性。

更新1:

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更新了新的功能,開發包同步更新,支持年齡和性別的評估

相關文章
相關標籤/搜索