知識背景:
下載虹軟人臉識別引擎
下載地址:http://ai.arcsoft.com.cn/product/arcface.html
目前虹軟人臉識別引擎有3個平臺,其中Windows與iOS是基于C++開發(fā)的,本文都是基于Windows版本下用C#實現(xiàn)人臉識別的,請大家注意下載的平臺
如何用C#調(diào)用C++的庫
那么,如何使用C#調(diào)用C++的庫呢,C#提供了兩種技術(shù)調(diào)用C++的DLL
靜態(tài)調(diào)用(DCOM+)
動態(tài)調(diào)用(P/Invoke)
我們可以將C或者C++的函數(shù)封裝成COM組件,在C#中調(diào)用時比較方便,但是COM組件需要注冊,而且多次注冊可能也會導(dǎo)致一些問題,同時在處理C或者C++的類型與COM組件的類型轉(zhuǎn)換的時候也可能有些麻煩
采用動態(tài)的方式就是直接用C#調(diào)用C或者C++已經(jīng)寫好的動態(tài)鏈接庫,這幾種方式相對而言,P/Invoke要方便一些
** 因此我們選擇P/Invoke的方式**
** P/Invoke是什么*
P/Invoke的全稱是Platform Invoke (平臺調(diào)用) 它實際上是一種函數(shù)調(diào)用機制,通過P/Invoke我們就可以調(diào)用非托管DLL中的函數(shù) ,實際上很多NET基類庫中定義的類 型內(nèi)部部調(diào)用了從Kernel32.dll,User32.dll,gdi32.dll等非托管DLL中導(dǎo)出的函數(shù)。
來看一個簡單的例子
[DllImportAttribute("user32.dll", EntryPoint = "SetCursorPos")] [return: MarshalAsAttribute(UnmanagedType.Bool)] //可寫可不寫,定義如何封送返回參數(shù) public static extern bool SetCursorPos(int X, int Y);
這段代碼的目的就是調(diào)用系統(tǒng)中獲取鼠標(biāo)參數(shù)的方法。
P/INVOKE的過程
關(guān)于P/Invoke的過程,我找到了MSDN上的一張圖,如下所示。

在使用P/Invoke調(diào)用C/C++方法時,會依次執(zhí)行以下操作
1 查找包含該函數(shù)的非托管DLL
2 將該非托管DLL加載到內(nèi)存中
3 查找函數(shù)在內(nèi)存中的地址并將其參數(shù)按照函數(shù)的調(diào)用約定壓棧
4 將控制權(quán)轉(zhuǎn)移給非托管函數(shù)
注意:只在第一次調(diào)用函數(shù)時,才會查找和加載非托管DLL并查找函數(shù)在內(nèi)存中的地址。當(dāng)非托管函數(shù)產(chǎn)生異常時,P/Invoke會將異常傳遞給托管調(diào)用方
看起來很復(fù)雜,但使用起來卻很簡單,只需要在C#中重新聲明函數(shù)的定義就可以了,然后可以像其它函數(shù)一樣調(diào)用。
注意:只在第一次調(diào)用函數(shù)時,才會查找和加載非托管DLL并查找函數(shù)在內(nèi)存中的地址。當(dāng)非托管函數(shù)產(chǎn)生異常時,P/Invoke會將異常傳遞給托管調(diào)用方
看起來很復(fù)雜,但使用起來卻很簡單,只需要在C#中重新聲明函數(shù)的定義就可以了,然后可以像其它函數(shù)一樣調(diào)用。
第一步: 實現(xiàn)人臉檢測
我們希望先實現(xiàn)我們的簡單的Hello World功能,從一張照片中檢測人臉是否存在,我們稱之為靜態(tài)人臉檢測。我們希望程序能夠打開一張照片,告訴我們這張照片中是否有人臉,如果有,就需要識別并顯示出來,如果沒有,就提示照片中沒有人臉。
創(chuàng)建Demo項目
項目技術(shù)
我們使用C# 4.0版本,IDE使用Visual Studio 2013,項目就用標(biāo)準(zhǔn)的Winform項目。
建立項目
我們打開Visual Studio,選擇C#語言,建立Winfrom項目,項目名稱為FaceDetectDemo,路徑隨便選。立項后,項目結(jié)構(gòu)如圖所示:

上圖中的AFD和dll文件夾我們后面就會用到,剛建項目時是沒有這兩個文件夾的。
建立視圖
通過設(shè)計器和工具箱,我們可以建立我們的視圖界面,包括一個按鈕兩個PictureBox.
大的那個我們用來顯示完整的圖片,小的用來顯示識別到的人臉信息。
我們把大PicturesBox的那個命名為pictureBox1,小的命名為pictureBox2,然后設(shè)置兩個的SizeMode均為Zoom, 以方便我們自動顯示照片。

下載需要的SDK
這里我們需要虹軟提供的SDK中的DLL,如果你還沒有下載它,那么現(xiàn)在就是下載的時候了。訪問地址http://www.arcsoft.com.cn/ai/arcface.html在明顯的地方找到WIndows版本,填寫基本的資料后就可以下載了。
下載的時候有一個版本選擇,1:1,1:N之類的,我們選擇默認(rèn)的就可以了,1:N和1:1在人臉識別上是有差別的,但在人臉檢測功能上基本上沒有差異。
在下載完成的頁面上,會顯示你申請的APPID和SDK KEY的信息,如下所示

請確保牢記這些Key,因為接下來的程序中你將需要這些Key,如果忘記了,就登錄剛才的那個地址,在用戶中心里面可以看到這些Key,當(dāng)然,你也可以在郵件中查找。
我們打開下載的文件,是一個zip格式的壓縮包,我們把它解壓。發(fā)現(xiàn)里面還有三個包,我們解壓其中名為Face_Detection的包??梢钥吹较旅娴哪夸浗Y(jié)構(gòu)

命名很清晰,我這里只需要簡單說一下。lib中的dll是要拷貝到你的運行目錄中的,doc中的PDF相當(dāng)重要,是SDK的入門指南。samplecode和inc是供C++調(diào)用時候用到的參考源碼和頭文件。這些都是比較重要的。
現(xiàn)在,讓我們把dll拖入到我們的應(yīng)用程序的bin目錄.在編輯選項時選擇始終復(fù)制這個文件到輸出目錄.
另外我們的SDK是32位系統(tǒng)的,所以我們還需要設(shè)置編譯選項為x86.

至此,項目創(chuàng)建工作順利完成。
現(xiàn)在我們回到上一章節(jié)的四個文件夾,我們打開doc文件夾。這里面的pdf文件是我們接下來課程的基礎(chǔ)。通讀一遍,發(fā)現(xiàn)4個函數(shù),3個結(jié)構(gòu)體,然后2個枚舉,兩個變量類型,還有一段示例代碼。我們來一步步定義它們.
C/C++ 可以定義自己的類型,打開SDK文檔可以發(fā)現(xiàn),這里面幾乎沒有我們熟悉的int,long,char*這些類型,取而代之是的Mint以及一些其它AFD開頭的類型,SDK文檔開篇引入了兩個基礎(chǔ)類型。
typedef MInt32 AFD_FSDK_OrientPriority;
typedef MInt32 AFD_FSDK_OrientCode;
所有基本類型在平臺庫中有定義。
定義規(guī)則是在ANSIC 中的基本類型前加上字母“M”同時將類型的第一個字母改成大寫。
例如“l(fā)ong” 被定義成“MLong”
具體到上面的代碼,它的意思是在項目中遇到AFD_FSDK_OrientPriority就認(rèn)為是Mint32,對應(yīng)C#就是int,全部的定義在inc文件夾afdcommdef.h頭文件中
由于C并不是面向?qū)ο蟮恼Z言,結(jié)構(gòu)體作為可以自定義的類型,在一定程度的代替了我們C#中的類和對象,我們來一步步定義這些結(jié)構(gòu)體。
這個結(jié)構(gòu)體是用來存儲臉部信息的,我們可以從文檔中得到它的定義如下:
typedef struct{
MRECT * rcFace;
MLong nFace;
AFD_FSDK_OrientCode * lfaceOrient;
} AFD_FSDK_FACERES, * LPAFD_FSDK_FACERES;
根據(jù)我們上一節(jié)中的內(nèi)容,可以知道這個MLong類似于long,rcFace和lfaceOrient則是兩個指針。那么在C#中如何使用指針呢,直接用unsafe code肯定是可以的,不過這里我們使用IntPtr.
IntPtr的簡介
IntPtr用于表示指針或句柄的平臺特定類型。這個其實說出了這樣兩個事實,IntPtr 可以用來表示指針或句柄、它是一個平臺特定類型,它主要用在兩個地方:
(1)C#調(diào)用WIN32 API時
(2)C#調(diào)用C/C++寫的DLL時(其實和1相同,只是這個一般是我們在和他人合作開發(fā)時經(jīng)常用到)
我們可以這樣子理解,IntPtr就可以互換C++中的指針
我們根據(jù)剛才所說的定義規(guī)則,換算成C#語言的定義如下:
public struct AFD_FSDK_FACERES
{
public int nFace;
public IntPtr rcFace;
public IntPtr lfaceOrient;
}
注意:nface雖然C++中是long,但對應(yīng)到C#中可不long,而是int.在32位程序中int和long占用的內(nèi)存大小都是4Byte=32bit,其表示的大小都是:-2147483648~2147483647。
我們在SDK文檔中注意到rcFace的類型是MRect* 這里的* 說明這是一個指針類型,因此我們在定義這個類的時候使用了IntPtr,但是MRect是一個結(jié)構(gòu)體,我們可在inc文件夾下面的amcomdef.h下面找到了它的定義.
typedef struct __tag_rect
{
? ? MInt32 left;
? ? MInt32 top;
? ? MInt32 right;
? ? MInt32 bottom;
} MRECT, *PMRECT;
這個類型比較簡單,C#版定義如下:
public struct MRECT
? ? {
? ? ? ? public int left;
? ? ? ? public int top;
? ? ? ? public int right;
? ? ? ? public int bottom;
? ? }
AFD_FSDK_VERSION
這個結(jié)構(gòu)體定義的是我們API的版本信息,同樣的我們來查看一下它的SDK的定義
typedef struct
{
MInt32 lCodebase;
MInt32 lMajor;
MInt32 lMinor;
MInt32 lBuild;
MPChar Version;
MPChar BuildDate;
MPChar CopyRight;
} AFD_FSDK_Version;
根據(jù)SDK開始約定,我們可以知道Mint32相當(dāng)于int,MPChar相當(dāng)于char*,這些自定義的變量類型可以在inc/comdef.h中查找,因此我們的對應(yīng)的C#版本如下:
? //定義FD的版本號
? public struct AFD_FSDK_Version
? ? {
? ? ? ? public int lCodebase;
? ? ? ? public int lMajor;
? ? ? ? public int lMinor;
? ? ? ? public int lBuild;
? ? ? ? public IntPtr Version;
? ? ? ? public IntPtr BuildDate;
? ? ? ? public IntPtr CopyRight;
? ? }
AFD_FSDK_ORIENTCODE
接下來我們來定義枚舉,這里面用到的枚舉有以下兩個:AFD_FSDK_OrientPriority和AFD_FSDK_OrientCode,枚舉比較簡單。我們只需要把十六進(jìn)制轉(zhuǎn)換為10進(jìn)制就可以了。
根據(jù)SDK文檔,我們需要定義的類型如下:
//定義人臉檢查結(jié)果中人臉的角度
public enum AFD_FSDK_OrientCode
? ? {
? ? ? ? AFD_FSDK_FOC_0 = 1,
? ? ? ? AFD_FSDK_FOC_90 = 2,
? ? ? ? AFD_FSDK_FOC_270 = 3,
? ? ? ? AFD_FSDK_FOC_180 = 4,
? ? ? ? AFD_FSDK_FOC_30 = 5,
? ? ? ? AFD_FSDK_FOC_60 = 6,
? ? ? ? AFD_FSDK_FOC_120 = 7,
? ? ? ? AFD_FSDK_FOC_150 = 8,
? ? ? ? AFD_FSDK_FOC_210 = 9,
? ? ? ? AFD_FSDK_FOC_240 = 10,
? ? ? ? AFD_FSDK_FOC_300 = 11,
? ? ? ? AFD_FSDK_FOC_330 = 12
AFD_FSDK_ORIENTPRIORITY
? public enum AFD_FSDK_OrientPriority
? ? {
? ? ? ? AFD_FSDK_OPF_0_ONLY=1,
? ? ? ? AFD_FSDK_OPF_90_ONLY=2,
? ? ? ? AFD_FSDK_OPF_270_ONLY=3,
? ? ? ? AFD_FSDK_OPF_180_ONLY=4,
? ? ? ? AFD_FSDK_OPF_0_HIGHER_EXT=5
? ? }
ASVLOFFSCREEN
這個結(jié)構(gòu)體是用來進(jìn)行人臉識別的關(guān)鍵結(jié)構(gòu),我當(dāng)初就是在定義函數(shù)時才發(fā)現(xiàn)這個沒有。又跑回來重新定義的。這個在SDK文檔中沒有,但是我們在示例代碼中能夠看到。我看來看看一下LPASVLOFFSCREEN的定義。在我們SDK的inc文件夾中,我們找到了一個名為asvloffscreen.h的文件。我們把文件打開,可以發(fā)現(xiàn)里面的主要定義
typedef struct __tag_ASVL_OFFSCREEN
{
? ? MUInt32 u32PixelArrayFormat;
? ? MInt32? i32Width;
? ? MInt32? i32Height;
? ? MUInt8* ppu8Plane[4];
? ? MInt32? pi32Pitch[4];
}ASVLOFFSCREEN, *LPASVLOFFSCREEN;
u32PixelArrayFormat:像素數(shù)組的格式
ppu8Plane[4]為一個指針數(shù)組
pi32Pitch[4]為一整形數(shù)組
如何定義數(shù)組
數(shù)組的定義沒有我們想象中的那么簡單。在C++中定義數(shù)組的時候,是指定了數(shù)組的長度的,而C#中定義數(shù)組時,是不指定長度的。這只是一個問題,另一個問題是因為C#的數(shù)據(jù)和C++的數(shù)據(jù)布局方式有很大的不同,在P/Invoke和COM Interop當(dāng)中必須要在C#和C++之間傳遞數(shù)據(jù),有的時候,CLR或者說.NET能夠自動在兩種編程語言之間轉(zhuǎn)換數(shù)據(jù),有的時候又不行,這時候就需要程序員來幫忙告訴.NET怎樣轉(zhuǎn)換數(shù)據(jù)了。這個轉(zhuǎn)換的方式是指定MarshalAs屬性。Marshal屬性相當(dāng)難用,如何轉(zhuǎn)換是一個復(fù)雜的事情,這個時個我們需要請出微軟的神器。P/Invoke Interop Assistant,你可以去下面的鏈接下載這個神器http://download.microsoft.com/download/f/2/7/f279e71e-efb0-4155-873d-5554a0608523/CLRInsideOut2008_01.exe
通過P/Invoke Interop Assistant的幫忙,我們可以知道應(yīng)該這樣子定義這個結(jié)構(gòu)體。

使用這個工具時,需要注意的是要把我們結(jié)構(gòu)體中的類型轉(zhuǎn)化為標(biāo)準(zhǔn)的C類型,我們可以在inc的amcomdef.h頭文件中找到它們的轉(zhuǎn)換定義。
我們來看一下最終的這個結(jié)構(gòu)體的定義
public struct ASVLOFFSCREEN
? ? {
? ? ? ? public int u32PixelArrayFormat;
? ? ? ? public int i32Width;
? ? ? ? public int i32Height;
? ? ? ? [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4, ArraySubType = System.Runtime.InteropServices.UnmanagedType.SysUInt)]
? ? ? ? public System.IntPtr[] ppu8Plane;
? ? ? ? [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4, ArraySubType = System.Runtime.InteropServices.UnmanagedType.I4)]
? ? ? ? public int[] pi32Pitch;
}
現(xiàn)在你可以用這個工具來定義我們接來所需要用到的所有數(shù)據(jù)結(jié)構(gòu),也可以用來定義API函數(shù)。
定義API函數(shù)
查看SDK文檔,可以看到FD共提供了3個方法。我們定義一個類來包含這些方法
新建AFD文件夾,定義AFDFunction類,里面包含SDK中提供的所有方法。
我們先來看一下第一個方法,初始化SDK引擎,在SDK文檔中可以看到它的原型定義如下:
原型
MRESULT AFD_FSDK_InitialFaceEngine(
MPChar AppId,
MPChar SDKKey,
MByte *pMem,
MInt32 lMemSize,
MHandle *pEngine,
AFD_FSDK_OrientPriority iOrientPriority,
MInt32 nScale,
MInt32 nMaxFaceNum
);
我們來看一下它的參數(shù)列表
AppId [in] 用戶申請SDK時獲取的App Id
SDKKey [in] 用戶申請SDK時獲取的SDK Key
pMem [in] 分配給引擎使用的內(nèi)存地址
lMemSize [in] 分配給引擎使用的內(nèi)存大小
pEngine [out] 引擎handle
iOrientPriority [in] 期望的臉部檢測角度范圍
nScale [in] 用于數(shù)值表示的最小人臉尺寸 有效值范圍[2,50] 推薦值 16。該尺寸是人臉相對于所在圖片的長邊的占比。例如,如果用戶想檢測到的最小人臉尺寸是圖片長度的1/8,那么這個nScale就應(yīng)該設(shè)置為8
nMaxFaceNum [in] 用戶期望引擎最多能檢測出的人臉數(shù) 有效值范圍[1,50]
如果成功返回MOK,失敗返回MRCode,MOK是一個int型的值為0,MRCOde是一個定義??梢栽趇nc文件 夾中的merror.h中找到。
通過剛才提供的神器,我們可以定義這個函數(shù)如下:
[DllImport("libarcsoft_fsdk_face_detection.dll", EntryPoint = "AFD_FSDK_InitialFaceEngine", CallingConvention = CallingConvention.Cdecl)]
public static extern int AFD_FSDK_InitialFaceEngine(string appId, string sdkKey, IntPtr pMem, int lMemSize, ref IntPtr pEngine, int iOrientPriority, int nScale, int nMaxFaceNum);
CallingConvertion這個屬性用于定義C++函數(shù)調(diào)用的方式。
Cdecl 調(diào)用方清理堆棧。這使您能夠調(diào)用具有 varargs 的函數(shù)(如 Printf),使之可用于接受可變數(shù)目的參數(shù)的方法。
FastCall 不支持此調(diào)用約定。
StdCall 被調(diào)用方清理堆棧。這是使用平臺 invoke 調(diào)用非托管函數(shù)的默認(rèn)約定。
ThisCall 第一個參數(shù)是 this 指針,它存儲在寄存器 ECX 中。其他參數(shù)被推送到堆棧上。此調(diào)用約定用于對從非托管 DLL 導(dǎo)出的類調(diào)用方法。
Winapi 此成員實際上不是調(diào)用約定,而是使用了默認(rèn)平臺調(diào)用約定。例如,在 Windows 上默認(rèn)為 StdCall,在 Windowshttp://CE.NET上默認(rèn)為 Cdecl。
默認(rèn)情況下,C和C++使用的Cdecl調(diào)用,因此我們在調(diào)用DLL時指定這個值就可以。
AFD_FSDK_STILLIMAGEFACEDETECTION
這個方法是我們的核心方法,它的功能如我們所料,就是通過讀取輸入的圖像,檢測是否存在人臉內(nèi)容并輸出人臉的結(jié)果信息。我們來看一下基礎(chǔ)定義。
MRESULT AFD_FSDK_StillImageFaceDetection(
MHandle hEngine,
LPASVLOFFSCREEN pImgData,
LPAFD_FSDK_FACERES pFaceRes
);
hEngine [in] 引擎handle
pImgData [in] 待檢測的圖像信息
pFaceRes [out] 人臉檢測結(jié)果
和初始化類似,第一個參數(shù)是hEngine引用,第二個參數(shù)pImgData是要檢測的圖形信息,第三個參數(shù)pFaceRes是一個輸出參數(shù),獲取人臉的檢測結(jié)果。需要注意的是里面的參數(shù)類型,第一個MHandle對應(yīng)的是引擎的引用,這個沒有問題,第二個是LPASVLOFFSCREEN 它是指向ASVLOFFSCREEN的一個結(jié)構(gòu)體指針,同樣LPAFD_FSDK_FACERES也是一個指針,我們知道指針對應(yīng)的都是IntPtr,定義如下:
[DllImport("libarcsoft_fsdk_face_detection.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern int AFD_FSDK_StillImageFaceDetection(IntPtr pEngine, IntPtr pImgData, ref IntPtr pFaceRes);
初始化之后的方法是GetVersion,功能就是獲取SDK的版本信息。
原型
const AFD_FSDK_Version * AFD_FSDK_GetVersion(
MHandle hEngine
);
這個方法比較簡單,參數(shù)就是Engine的引用,其返回值為Version結(jié)構(gòu)體,我們在最初的時候已經(jīng)定義完成。
[DllImport("libarcsoft_fsdk_face_detection.dll", CallingConvention = CallingConvention.Cdecl)]
? ? ? ? public static extern int AFD_FSDK_StillImageFaceDetection(IntPtr pEngine, IntPtr pImgData, ref IntPtr pFaceRes);
[DllImport("libarcsoft_fsdk_face_detection.dll", CallingConvention = CallingConvention.Cdecl)]
? ? ? ? public static extern int AFD_FSDK_UninitialFaceEngine(IntPtr pEngine);
至此,我們的基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)已創(chuàng)建完畢.
我們來實現(xiàn)我們的圖片讀取和人臉識別功能,這個章節(jié)中,會包含大量的細(xì)節(jié)及互操作的內(nèi)容。
基礎(chǔ)知識介紹
其實關(guān)于P/Invoke的操作我們前面的代碼已經(jīng)講解了很多。也基本把我們用到的結(jié)構(gòu)體和函數(shù)定義出來,我們知道
指針映射為IntPtr,
引用類變量映射為IntPtr,
char *可以映攝為字符串
結(jié)構(gòu)體,和數(shù)組如果從IntPtr中取數(shù)據(jù)呢,我們需要使用的一個類叫Marshal
我們來看一下MSDN上的介紹
https://msdn.microsoft.com/zh-cn/library/system.runtime.interopservices.marshal(v=vs.110).aspx
Marshal類提供了一個方法集合,這些方法用于分配非托管內(nèi)存、復(fù)制非托管內(nèi)存塊、將托管類型轉(zhuǎn)換為非托管類型,此外還提供了在與非托管代碼交互時使用的其他雜項方法,我們將會在下面開發(fā)進(jìn)程中頻繁使用這個類的多個方法。
例如:在定義一個指針類型變量時IntPtr,我們需要使用Marshal.AllocHGlobal為其分配內(nèi)存,得到IntPtr變量,在分配內(nèi)存時,我們需要使用Marshal.SizeOf計算需要分配的內(nèi)存的大小。然后調(diào)用Marshal.
StructureToPtr為變量賦值
讓我們帶著這些概念開始我們下面的內(nèi)容。
根據(jù)我們的SDK說明文檔,在使用引擎之前需要先初始化。出于簡單我就把初始化代碼的部分放在Form1的構(gòu)造函數(shù)內(nèi)。而把引擎作為類的實例變量定義。
我們在構(gòu)造函數(shù)中添加初始化的代碼。
定義人臉識別引擎
IntPtr detectEngine = IntPtr.Zero;
我們可以根據(jù)sampleCode定義我們?nèi)四樧R別所需要的參數(shù)
首先,定義Engine運行需要的內(nèi)存,寬容度,人臉的數(shù)目以及有效的人臉角度。
int detectSize = 40 * 1024 * 1024;
int nScale = 50;
int nMaxFaceNum = 10;
string appId = "你申請到的APPID";
string sdkFDKey = "你申請到的FDKEY";
在示例代碼中,我們可以得到引擎在初始化時,需要指定緩沖區(qū)。
在C#中,可以使用
pMem = Marshal.AllocHGlobal(detectSize);
針對人臉角度的檢測范圍,直接傳遞為AFD_FSDK_OrientPriority.AFD_FSDK_OPF_0_HIGHER_EXT,
變量定義完成后,我們就可以調(diào)用我們的初始化方法了。返回值為int類型,通過返回的類型,可以得知是否能夠調(diào)用成功。
int retCode = AFDFunction.AFD_FSDK_InitialFaceEngine(appId, sdkFDKey, pMem, detectSize, out detectEngine, (int)AFD_FSDK_OrientPriority.AFD_FSDK_OPF_0_HIGHER_EXT, nScale, nMaxFaceNum);
? ? if (retCode != 0)
? ? ? {
? ? ? ? ? MessageBox.Show("引擎初始化失敗:錯誤碼為:" + retCode);
? ? ? ? ? this.Close();
? ? ? }
接下來,我們找到我們的btnLoadImage方法,在這里填寫我們的業(yè)務(wù)處理邏輯。
1.讀取一個jpg的文件,并加載的pictureBox1中顯示出來,
2.然后調(diào)用我們的引擎的AFD_FSDK_StillImageFaceDetection方法,檢查出人臉的位置。
3最后我們利用GDI+,把檢測到的人臉部分提取出位置顯示到PictureBox2中,
4.把pictureBox1中的圖片,添加上識別的紅框,完成人臉檢測的效果。

加載圖片比較簡單,我們調(diào)用OpenFileDialog方法,打開一個圖片文件,并顯示到pictureBox1中
OpenFileDialog openFile = new OpenFileDialog();
openFile.Filter = "圖片文件|*.bmp;*.jpg;*.jpeg;*.png|所有文件|*.*;";
openFile.Multiselect = false;
openFile.FileName = "";
if (openFile.ShowDialog() == DialogResult.OK)
? ? ? ? ? ? {? Image image = Image.FromFile(openFile.FileName);
? ? ? ? ? ? ? ? this.pictureBox1.Image = new Bitmap(image);
//TODO:完成下面的方法
? ? ? ? ? ? ? ? ? ? checkAndMarkFace(this.pictureBox1.Image);
? ? ? ? ? ? }
檢測并標(biāo)記人臉
終于到正題了,很興奮,對吧。不過還是沒有思路,因為我們不知道如何來調(diào)用那個引擎。這個時候我們必須參考samplecode,通過sampleCode我們可以得知,首先我們需要讀取圖片的內(nèi)容到BMP格式,而且這個BMP格式必須為ASVL_PAF_RGB24_B8G8R8,標(biāo)準(zhǔn)的Image中的Bitmap就是這個格式,讀取bitmap中的所有圖像信息存入ASVLOFFSCREEN的offInput中,這時候SampleCode中的代碼是從文件中讀取的,我們要直接從Bitmap中讀取,這里面還是有一些不一樣的。我們首先來看一下這個讀取的代碼
private byte[] readBmp(Bitmap image, ref int width, ref int height, ref int pitch)
{//將Bitmap鎖定到系統(tǒng)內(nèi)存中,獲得BitmapData
BitmapData data = image.LockBits(new Rectangle(0, 0, image.Width, image.Height), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);? ? ? ? ? ?
//位圖中第一個像素數(shù)據(jù)的地址。它也可以看成是位圖中的第一個掃描行
IntPtr ptr = data.Scan0;
//定義數(shù)組長度
int soureBitArrayLength = data.Height * Math.Abs(data.Stride);
byte[] sourceBitArray = new byte[soureBitArrayLength];
//將bitmap中的內(nèi)容拷貝到ptr_bgr數(shù)組中
Marshal.Copy(ptr, sourceBitArray, 0, soureBitArrayLength);? ? ? ? ? ? width = data.Width;
height = data.Height;
pitch = Math.Abs(data.Stride);
? int line = width * 3;
? int bgr_len = line * height;
? byte[] destBitArray = new byte[bgr_len];
for (int i = 0; i < height; ++i)
{
? Array.Copy(sourceBitArray, i * pitch, destBitArray, i * line, line);
}
pitch = line;
image.UnlockBits(data);
return destBitArray;
}
有關(guān)這部分的內(nèi)容,可以參考微軟關(guān)于BitmapData的注解。https://msdn.microsoft.com/zh-cn/library/system.drawing.imaging.bitmapdata.aspx
回到我們的這個方法,我們繼續(xù)人臉識別的過程首先,我們把獲取到的圖像信息存起來
byte[] imageData = readBmp(bitmap, ref width, ref height, ref pitch);
通過前面的過程,我們知道,我們的代碼中的傳入圖像的參數(shù)類型是ASVLOFFSCREEN指針。通過查看ASVLOFFSCREEN類型。我們可以發(fā)現(xiàn),u32PixelArrayFormat為需要圖像的格式。這個是因為我們準(zhǔn)備使用BMP位圖,因此我們直接使用ASVL_PAF_RGB24_B8G8R8格式通過查詢可知定義的值為513.
i32Width和i32Height則為識別圖像的大小。ppu8Plane為一個批向byte數(shù)組的指針數(shù)組,這里面會保存我們剛剛轉(zhuǎn)換后的圖片數(shù)據(jù)。而pi32Pitch則是為每一個圖像指定了pitch大小,在結(jié)構(gòu)中,一次人臉識別工作,可以傳遞四幅圖片。
我們先來把byte[]數(shù)組轉(zhuǎn)化為C++識別的數(shù)組類型。
IntPtr imageDataPtr = Marshal.AllocHGlobal(imageData.Length);
Marshal.Copy(imageData, 0, imageDataPtr, imageData.Length);
接下來是根據(jù)剛才的分析,我們設(shè)置的ASVLOFFSCREEN的結(jié)構(gòu)體類型
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;
由于方法中需要是的一個結(jié)構(gòu)體的指針,因此,我們還需要調(diào)用Marshal. AllocHGlobal方法創(chuàng)建指針,并使用Marshal.StructureToPtr進(jìn)行初始化。
IntPtr offInputPtr = Marshal.AllocHGlobal(Marshal.SizeOf(offInput));
Marshal.StructureToPtr(offInput, offInputPtr, false);
由于接口還需要一個結(jié)構(gòu)體保存返回的人臉數(shù)據(jù),我們來定義它
AFD_FSDK_FACERES faceRes = new AFD_FSDK_FACERES();
同人臉數(shù)據(jù)一樣,我們需要把這個結(jié)構(gòu)體轉(zhuǎn)換為指針類型。
IntPtr faceResPtr = Marshal.AllocHGlobal(Marshal.SizeOf(faceRes));
這個是返回值,因此我們不需要對內(nèi)容進(jìn)行初始化。我們直接調(diào)用引擎
int detectResult = FaceDllImport.AFD_FSDK_StillImageFaceDetection(detectEngine, offInputPtr, ref faceResPtr);
如果成功返回detectResult會返回0,也就是0
這個時候,返回為0并不意味著找到了人臉,具體的人臉信息還需要在我們的AFD_FSDK_FACERES結(jié)構(gòu)休中查找。
使用Marshal.PtrToStructure批獲得的指針類型轉(zhuǎn)化為結(jié)構(gòu)體類型。
faceRes = (AFD_FSDK_FACERES) Marshal.PtrToStructure(faceResPtr, typeof(AFD_FSDK_FACERES));
根據(jù)前端的結(jié)構(gòu)體定義部分的數(shù)據(jù),我們可以發(fā)現(xiàn)其中AFD_FSDK_FACERES.nFace屬性為識別到的人臉的數(shù)目。faceRes.rcFace則為識別到的人臉的數(shù)據(jù)。nFace可以直接轉(zhuǎn)化為int。
AFD_FSDK_FACERES中的rcFace是一個結(jié)構(gòu)體指針,因此我們使用Marshal.PtrToStructure將其轉(zhuǎn)化為結(jié)構(gòu)體。
MRECT rect = (MRECT)Marshal.PtrToStructure(faceRes.rcFace , typeof(MRECT));
通過獲得這個rect信息,就可以得到我們需要的人臉的位置數(shù)據(jù)了,包括人臉矩形的在上角和右下角的坐標(biāo)。然后我們就可以利用這些數(shù)據(jù)來重新創(chuàng)建一個位圖
Image image = CutFace(bitmap, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top);
將位圖顯示到圖片控件上
this.pictureBox2.Image = image;
然后我們想像Demo中的一樣,標(biāo)出人臉的位置。我們就可以使用這樣的方法。
this.pictureBox1.Image=? DrawRectangleInPicture(pictureBox1.Image, new Point(rect.left, rect.top), new Point(rect.right, rect.bottom), Color.Red, 2, DashStyle.Dash);
來看一下這里面用到的兩上C#方法比較簡單,純屬C#代碼,比較簡單
public static Bitmap CutFace(Bitmap srcImage, int StartX, int StartY, int iWidth, int iHeight)
? ? ? ? {
? ? ? ? ? ? if (srcImage == null)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? return null;
? ? ? ? ? ? }
? ? ? ? ? ? int w = srcImage.Width;
? ? ? ? ? ? int h = srcImage.Height;
? ? ? ? ? ? if (StartX >= w || StartY >= h)
? ? ? ? ? ? {
return null;
? ? ? ? ? ? }
? ? ? ? ? ? if (StartX + iWidth > w)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? iWidth = w - StartX;
? ? ? ? ? ? }
? ? ? ? ? ? if (StartY + iHeight > h)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? iHeight = h - StartY;
? ? ? ? ? ? }
? ? ? ? ? ? try
? ? ? ? ? ? {
? ? ? ? ? ? ? ? Bitmap bmpOut = new Bitmap(iWidth, iHeight, PixelFormat.Format24bppRgb);
? ? ? ? ? ? ? ? Graphics g = Graphics.FromImage(bmpOut);
? ? ? ? ? ? ? ? g.DrawImage(srcImage, new Rectangle(0, 0, iWidth, iHeight), new Rectangle(StartX, StartY, iWidth, iHeight), GraphicsUnit.Pixel);
? ? ? ? ? ? ? ? g.Dispose();
? ? ? ? ? ? ? ? return bmpOut;
? ? ? ? ? ? }
? ? ? ? ? ? catch
? ? ? ? ? ? {
return null;
? ? ? ? ? ? }
? ? ? ? }
private? Image DrawRectangleInPicture(Image bmp, Point p0, Point p1, Color RectColor, int LineWidth, DashStyle ds)
{
if (bmp == null) return null;
Graphics g = Graphics.FromImage(bmp);
Brush brush = new SolidBrush(RectColor);
Pen pen = new Pen(brush, LineWidth);
pen.DashStyle = ds;
g.DrawRectangle(pen, new Rectangle(p0.X, p0.Y, Math.Abs(p0.X - p1.X), Math.Abs(p0.Y - p1.Y)));
g.Dispose();
return bmp;
}
現(xiàn)在你可以點擊運行你的項目了,如果沒有任何問題,你的將會看到下面的畫面。
如果出現(xiàn)問題,你需要根據(jù)返回的錯誤碼進(jìn)行查找。
引擎初始化失敗
一般是APPID和APPKEY不對,你需要確保你到下載的地方申請了正確的APPID和KEY,并且注意平臺是Windows平臺的。初始化失敗可以通過返回值進(jìn)行查看,他們官網(wǎng)上也會有一個錯誤代碼表。對照查表一般會解決問題。
找不到DLL
首先請保證你把DLL拷貝到對應(yīng)的目錄下面,其次要確定設(shè)置輸出選項為拷貝到輸出目錄。
內(nèi)存不能讀或者寫
這個是C++的尿性,也是C#程序員不多見的報錯,主要檢查相關(guān)參數(shù)是否傳入正確。還要注意,如果人臉檢測返回的值不為0,獲取到的人臉數(shù)目會是一個比較大的隨機數(shù)。這個時候如果用循環(huán)讀取,就會出現(xiàn)地址越界的情況。
最后來一張華仔的圖鎮(zhèn)樓

今天我們只是講解了一下人臉識別的最簡單的Demo,我們下一節(jié)從獲取兩張人臉的相似度來入手講解如何識別不同的人的,歡迎繼續(xù)關(guān)注。如果你已經(jīng)了解了本博客的內(nèi)容,你可以打開FR的文檔,自己來進(jìn)行模擬實現(xiàn)。