1. 程式人生 > >人臉識別ArcFace C#DEMO 開發應用全過程

人臉識別ArcFace C#DEMO 開發應用全過程

手上有一個專案,需要檢驗使用本程式的,是否本人!因為在程式使用前,我們都已經做過頭像現場採集,所以源頭呢是不成問題的,那麼人臉檢測,人臉比對,怎麼辦呢?度娘了下,目前流行的幾個人臉檢測,人臉比對核心,大多都是基於網際網路的,但我們的專案是基於本地伺服器,那就有點麻煩了,後來找到ArcFace.它的核心允許本地呼叫,那就好辦了,立刻去了官網,看論壇,下DEMO;我當時下的是這個:ArcFace C#DEMO

本以為可以一帆風順的就可以把專案搞定了,不想…噩夢才剛剛開始呢…且聽我細細道來:

首先說下我的呼叫邏輯; 專案裡有一個採集端(每個業務視窗),負責採集現場人像,並通過ArcFace人臉檢測,特徵提取,獲取到.dat比對源(ServiceFaceModels),然後存到資料庫(blob);

專案裡的應用端(使用者手機),隨機時間的呼叫攝像頭,採集到被比對圖片;並對該記錄進行標記;

專案時比對端(伺服器),定時詢問資料庫,哪些被標記記錄需要比對,然後通過資料庫記錄,找到該圖片,並通過ArcFace人臉檢測,特徵提取,獲取到.dat被比對源(LocalFaceModels) 然後將這兩個源在記憶體中進行比對,得分高於0.7的,就通過;

前兩端就不多說了,都是一些常規的操作.重點講下比對端(伺服器);

先說我做的第一個版本,做的是一個控制檯程式;

//首先定義了一個呼叫類; MatchUserFace;它裡邊包含了初始化,人臉檢測,特徵提取,人臉比對,以及一些輔助方法;

//然後在Program裡定義了一個委託,這個委託的作用,就是能夠讓我可以帶引數進去ArcFace的檢測與比對核心;

public delegate bool MatchHandler(string userid, string studyid, string photoid, string photopath);

//最後我的Program裡邊,就是做一個遞迴,去不斷的問資料庫拿被標誌需要進行核對的記錄,拿到圖片後,就進行比對; QueryDataFile(string upstate);

下邊這段就是在QueryDataFile();去實現非同步呼叫比對核心;

MatchHandler handler = new MatchHandler(MatchUserFace.GetAndMatchImage);
                        string Identification = string.Format("USERID:{0} STUDYID:{1} PHID:{2}", userid, studyid, photoid);
                        IAsyncResult result = handler.BeginInvoke(userid, studyid, photoid, pathstr, RecognizeEngine, DetectEngine, new AsyncCallback(CallbackFunc), Identification);

下邊這段就是非同步的結果回撥;

 static void CallbackFunc(IAsyncResult result)
        {
            MatchHandler handler = (MatchHandler)((AsyncResult)result).AsyncDelegate;
            bool match = handler.EndInvoke(result);
            string strmatch = string.Empty;
            if (match)
            {
                strmatch = " 比對結果:OK";
            }
            else
            {

                strmatch = " 比對結果:NO";
            }
            Console.WriteLine(result.AsyncState + strmatch);
            GC.Collect();
        }

寫好了,釋出到伺服器上,還想著中午吃個雞腿獎勵下自己;不想…釋出後不到兩小時,小弟來說:伺服器是不是出問題了,下邊所有業務視窗訪問速度嚴重延遲…立馬跑到機房去看,一看沒毛病呀,所有的服務都好好的,沒有卦死..再開啟資源監視器一看,靠…那個比對端一下吃3個多G的記憶體,而且還在不斷上升中…立馬停掉,然後再問小弟,下邊業務是否正常,他回覆正常了…那麼說,就是我寫的這個比對端有問題了!改!!!

第二個版本,

下了機房看程式碼…左看右看,沒有哪不對呀,一步步按步就班的…毫無頭緒時,就想,是不是伺服器記憶體不夠而已,打申請拿了64G回來.再開程式也是一樣吃的很緊,但是下邊業務視窗倒是不延時,看來記憶體增大還是有好處的…呵…;但是源頭問題還是沒解決,不行的呀!到了晚飯時,一道靈光拍進腦門,我看到程式碼裡我是每非同步呼叫一次,就初始化一次ArcFace的SDK.我就想,是不是這個原因導致呢?修改方法,去試試!! //把那個委託改成如下:

public delegate bool MatchHandler(string userid, string studyid,  string photoid, string photopath, IntPtr RecognizeEngine, IntPtr DetectEngine);

//然後初始化SDK放到了Program裡做:

string appId = "4yHjnxK94FCK6L7HaJieWawSLubnANXXXXX";
            string sdkFDKey = "7S6Xp4mtroLnjTt7qDYnd2dqHXXXXX";
            string sdkFRKey = "7S6Xp4mtroLnjTt7qDYnd2dxSgXXXXX";
            int retCode = AFDFunction.AFD_FSDK_InitialFaceEngine(appId, sdkFDKey, pMem, detectSize, ref DetectEngine, 5, nScale, nMaxFaceNum);
            int retCode2 = AFRFunction.AFR_FSDK_InitialEngine(appId, sdkFRKey, pMemRecongnize, detectSize, ref RecognizeEngine); 

//最後把非同步呼叫的方法改成如下:

  MatchHandler handler = new MatchHandler(MatchUserFace.GetAndMatchImage);
                        string Identification = string.Format("USERID:{0} STUDYID:{1} PHID:{2}", userid, studyid, photoid);
                        IAsyncResult result = handler.BeginInvoke(userid, studyid, photoid, pathstr, RecognizeEngine, DetectEngine, new AsyncCallback(CallbackFunc), Identification);

再次釋出到伺服器.然後再到資源監視器去看,喲…執行緒數不高了而且增長的還不快…好開心!!以為搞好了;就回宿舍睡覺去了!!不想…睡得迷糊的時候,我們的客服小妹妹的電話就打到我這了,我說什麼事,她說現在大面積反映使用者比對不了?what?我說不可能吧,是不是當地電信故障呀?我自己拿手機試了下,真的不行呀!!!快速趕回辦公室遠端看了下伺服器,我的乖乖…比對端卦了!!!我再看日誌,日誌沒有捕捉到程式異常,只是捕到了個:Value cannot be null.Parameter name: source;我吃你大米了,我刨你家玉米地了,為啥要這麼對我!重啟比對端,然後都可以正常運作了…我決定在這監視這個比對端,在資源監視器我到是發現了一個:w3wp.exe它在不斷的漲記憶體(這是要劃重點的)想想這可已經是深夜了.果不出其然,運行了大概兩個多小時後,程式又卦了.我的乖乖,為啥會這樣呢,一時半會也想不出辦法呀!我也總不能呆在伺服器旁它停了,我就重啟吧! 第二天致電虹軟,反映了程式會執行一段時間就會卦掉,虹軟這邊也提出了很多寶貴意見,

1.先著眼把捕捉到的那個錯誤,查出來,看看是否處理好了,程式還會不會卦;那我就在程式裡增加了日誌列印,還真就發現了幾個在DEMO裡沒有處理到的問題:

  1. 每個Marshal.AllocHGlobal,用完以後,一定要釋放;
  2. AFD_FSDK_StillImageFaceDetection;AFR_FSDK_ExtractFRFeature;這兩個函式要判斷返回值是否等於0; 所以 MatchUserFace 呼叫類我作了如下修改
    private static byte[] detectAndExtractFeature(Image imageParam, out Image facerect,
            IntPtr RecognizeEngine, IntPtr DetectEngine)
        {
            byte[] feature = null; facerect = null;

            try
            {
                int width = 0; int height = 0; int pitch = 0;
                Bitmap bitmap = new Bitmap(imageParam);
                byte[] imageData = getBGR(bitmap, ref width, ref height, ref pitch);
                IntPtr imageDataPtr = Marshal.AllocHGlobal(imageData.Length);
                Marshal.Copy(imageData, 0, imageDataPtr, imageData.Length);

                ASVLOFFSCREEN offInput = new ASVLOFFSCREEN();
                offInput.u32PixelArrayFormat = 513;
                offInput.ppu8Plane = new IntPtr[4];
                offInput.ppu8Plane[0] = imageDataPtr;
                offInput.i32Width = width;
                offInput.i32Height = height;
                offInput.pi32Pitch = new int[4];
                offInput.pi32Pitch[0] = pitch;
                AFD_FSDK_FACERES faceRes = new AFD_FSDK_FACERES();
                IntPtr offInputPtr = Marshal.AllocHGlobal(Marshal.SizeOf(offInput));
                Marshal.StructureToPtr(offInput, offInputPtr, false);
                IntPtr faceResPtr = Marshal.AllocHGlobal(Marshal.SizeOf(faceRes));

                //人臉檢測
                int detectResult = AFDFunction.AFD_FSDK_StillImageFaceDetection(DetectEngine, offInputPtr, ref faceResPtr);
                if (detectResult == 0)
                {
                    try
                    {
                        object obj = Marshal.PtrToStructure(faceResPtr, typeof(AFD_FSDK_FACERES));
                        faceRes = (AFD_FSDK_FACERES)obj;
                        for (int i = 0; i < faceRes.nFace; i++)
                        {
                            MRECT rect = (MRECT)Marshal.PtrToStructure(faceRes.rcFace + Marshal.SizeOf(typeof(MRECT)) * i, typeof(MRECT));
                            int orient = (int)Marshal.PtrToStructure(faceRes.lfaceOrient + Marshal.SizeOf(typeof(int)) * i, typeof(int));
                            if (i == 0)
                            {
                                facerect = CutFace(bitmap, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top);
                            }
                        }
                    }
                    catch (Exception ex)
                    {

                        LogNetWriter.Error("人臉檢測時出錯:" + ex.Message);
                    }

                }


                if (faceRes.nFace > 0)
                {
                    try
                    {
                        AFR_FSDK_FaceInput faceResult = new AFR_FSDK_FaceInput();
                        int orient = (int)Marshal.PtrToStructure(faceRes.lfaceOrient, typeof(int));
                        faceResult.lOrient = orient;
                        faceResult.rcFace = new MRECT();
                        MRECT rect = (MRECT)Marshal.PtrToStructure(faceRes.rcFace, typeof(MRECT));
                        faceResult.rcFace = rect;
                        IntPtr faceResultPtr = Marshal.AllocHGlobal(Marshal.SizeOf(faceResult));
                        Marshal.StructureToPtr(faceResult, faceResultPtr, false);

                        AFR_FSDK_FaceModel localFaceModels = new AFR_FSDK_FaceModel();
                        IntPtr localFaceModelsPtr = Marshal.AllocHGlobal(Marshal.SizeOf(localFaceModels));
                        int extractResult = AFRFunction.AFR_FSDK_ExtractFRFeature(RecognizeEngine, offInputPtr, faceResultPtr, localFaceModelsPtr);
                        if (extractResult == 0)
                        {
                            Marshal.FreeHGlobal(faceResultPtr);
                            Marshal.FreeHGlobal(offInputPtr);

                            object objFeature = Marshal.PtrToStructure(localFaceModelsPtr, typeof(AFR_FSDK_FaceModel));

                            Marshal.FreeHGlobal(localFaceModelsPtr);

                            localFaceModels = (AFR_FSDK_FaceModel)objFeature;
                            feature = new byte[localFaceModels.lFeatureSize];
                            Marshal.Copy(localFaceModels.pbFeature, feature, 0, localFaceModels.lFeatureSize);

                            localFaceModels = new AFR_FSDK_FaceModel();
                        }
                    }
                    catch (Exception ex)
                    {

                        LogNetWriter.Error("提取特徵時出錯:" + ex.Message);
                    }



                }

                bitmap.Dispose();
                imageData = null;
                Marshal.FreeHGlobal(imageDataPtr);
                //Marshal.FreeHGlobal(faceResPtr);
                offInput = new ASVLOFFSCREEN();
                faceRes = new AFD_FSDK_FACERES();
            }
            catch (Exception ex)
            {
                LogNetWriter.Error("識別人臉並提取人臉特徵出錯:" + ex.Message);
            }
            return feature;
        } 

當然了,比對的時候也作了一些修改,就是當比對完了以後,就做了指標釋放;

  Marshal.FreeHGlobal(firstFeaturePtr);
                                        Marshal.FreeHGlobal(secondFeaturePtr);
                                        Marshal.FreeHGlobal(firstPtr);
                                        Marshal.FreeHGlobal(secondPtr);

經過這一次修改後,再發布到伺服器,喲…不錯哦..執行的時間久了…但還是會卦,而且那個w3wp.exe還是會不斷的拉記憶體;這個版本的執行時間可以達到4小左右了;我就想總得有個解決辦法吧;再次致電虹軟,再次反映這個問題,虹軟這邊給我的建議就是不要去進行多執行緒,我想想也對,要把邏輯簡單化,我就把識別核心打包成一個EXE.然後在Program裡呼叫這個EXE.意思就是每當我有需要識別的圖片,我就調一個EXE.然後EXE處理完以後,就自我釋放了… 於是我改了第三版:

 //這裡就是一條執行緒在做處理
                        string strmatch = string.Empty;
                        ControlExeClass _ControlExeClass = new Model.ControlExeClass();
                        //這個方法是調一個EXE,EXE的內容是:ControlExeClass.cs;
                        //做的任務就是把圖片進行人臉檢測,人臉特徵提取,人臉識別;
                        bool bo = _ControlExeClass.ControlExe(userid, studyid, photoid, pathstr);
                        if (bo)
                        {
                            iCheck_OK++;
                            label5.Text = iCheck_OK.ToString();
                            strmatch = "  比對結果:OK";
                        }
                        else
                        {

                            strmatch = "  比對結果:NO";
                        }
                        string dates = "   比對時間:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");

                        textBox1.Text += "  USERID:" + userid + "  STUDYID:" + studyid + "  PHID:" + photoid + strmatch + dates + Environment.NewLine;

然後那個EXE就是沿用MatchUserFace呼叫類,在EXE的主執行緒裡完成呼叫;還別說,用了這個方法後,記憶體不拉昇了,而且w3wp.exe上漲,也只是在EXE工作的一剎那上來,EXE幹完活後,它就會生成一個新的w3wp.exe,舊的w3wp.exe那個會被登出掉…譁…想想就開心,終於如願解決了問題,但….當一個人覺得越順利時,往往大麻煩就會來了.正如我覺得上天不會對我那麼好一樣,運行了大概一天後,程式還是卦了.蒼天呀,大地呀,我到底做錯了什麼…. 正在我一籌莫展時,我就老記恨這個w3wp.exe,到底是什麼東東,好,度娘下徹底瞭解下它. 度娘是這麼形容它的: w3wp.exe是在IIS(因特網資訊伺服器)與應用程式池相關聯的一個程序,如果你有多個應用程式池,就會有對應的多個w3wp.exe的程序例項執行。這個程序用來分配大量的系統資源。 好,既然說我的IIS裡的應用程式池,那我就對我的應用程式池進行固定記憶體回收不就好了嘛;我就對執行緒池做了一個固定記憶體回收,當達到400000KB時就做一次回收. 這一下設定做下去後,的確是立竿見影的,當EXE工作時w3wp.exe就從來沒高過400000KB;我想這一下應該徹底解決了吧;可是….程式還是卦了….我是真的不得上天倦顧呀… 一連幾天毫無頭緒,鬍子長一臉了,也沒心思刮,領導這邊還想刮我骨頭呢…唉…上下壓力都好大呀.搞得我肚子也不舒服,就去廁所蹲了個坑,還別說,這個坑,含金量特高.又一道靈光打進了我的腦門,我想呀,是不是我的遞迴出現了問題呢???我就回去看了下程式碼,我的遞迴邏輯是沒有問題的呀,一步步有板有眼,這是怎麼回事呢,我又度娘了下,關於C#的遞迴,是這麼形容的:一個演算法中,由於遞迴呼叫次數過多,堆疊是會溢位。遞迴使用的記憶體大小累計達4G,系統就會進行記憶體回收. 至於何時收,怎麼收,就是windows的事情了.乖乖…既然有這麼一個限定,我不用不就好了嘛,我就用死詢還不好嗎? 所以第4版修改如下:

 private static void CycleData()
        {
            while (true)
            {
                if (_DoWork)
                {
                    break;
                }
                else
                {
                    QueryDataFile("U");
                    Thread.Sleep(1500);
                }

                Thread.Sleep(2000);
            }

        }

至此所有問題解決!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!哦…..忘說了,我後來沒有單獨呼叫EXE這種方法了,改成了第一版的控制檯程式,其結果是一樣的;

現在…呵呵…我可是十分輕鬆著座在大班椅上,喝著奶茶,身邊座著小祕,我說,她打的這篇文章…呵….開玩笑了,文章裡每個字都是我自己親手敲的,同時也十分感謝虹軟能提供這麼優秀的SDK供我使用,更要感謝虹軟的技術支援,給我莫大的幫助; 最後總結幾點:

1.SDK可以只初始化一次,然後ref傳參進結構體,就可以一直用下去;

2.每個Marshal.AllocHGlobal,用完以後,一定要釋放;

3.可以非同步回撥進行;

4.AFRFunction.AFR_FSDK_ExtractFRFeature; AFDFunction.AFD_FSDK_StillImageFaceDetection; 這兩個函式要判斷返回值是否等於0;

5.最最最重要一點,嚴禁使用遞迴去呼叫;寧願用死詢代替;(因為這個就是導致我程式死掉的主因),因為遞迴要是深度太大,而且次數過多,累計記憶體使用達4G以上,系統就會做一次執行緒與記憶體回收,至於怎麼收,何時收就是不定時的,所以一定不要用遞迴,這個是我在C#官方看到對於遞迴的解釋;

6.如果是使用windows伺服器進行虹軟SDK的;建議IIS執行緒池做一個固定記憶體回收機制; 最後上傳一下幾個示例片段吧,因為箇中涉及到一些資料庫操作,我整個工程就不上傳了