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

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

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

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

請確保牢記這些Key,因為接下來的程序中你將需要這些Key,如果忘記了,就登錄剛才的那個地址,在用戶中心里面可以看到這些Key,當然,你也可以在郵件中查找。
我們打開下載的文件,是一個zip格式的壓縮包,我們把它解壓。發現里面還有三個包,我們解壓其中名為Face_Detection的包。可以看到下面的目錄結構

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

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

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

打開圖片
加載圖片比較簡單,我們調用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);
}
檢測並標記人臉
終於到正題了,很興奮,對吧。不過還是沒有思路,因為我們不知道如何來調用那個引擎。這個時候我們必須參考samplecode,通過sampleCode我們可以得知,首先我們需要讀取圖片的內容到BMP格式,而且這個BMP格式必須為ASVL_PAF_RGB24_B8G8R8,標准的Image中的Bitmap就是這個格式,讀取bitmap中的所有圖像信息存入ASVLOFFSCREEN的offInput中,這時候SampleCode中的代碼是從文件中讀取的,我們要直接從Bitmap中讀取,這里面還是有一些不一樣的。我們首先來看一下這個讀取的代碼
private byte[] readBmp(Bitmap image, ref int width, ref int height, ref int pitch)
{//將Bitmap鎖定到系統內存中,獲得BitmapData
BitmapData data = image.LockBits(new Rectangle(0, 0, image.Width, image.Height), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
//位圖中第一個像素數據的地址。它也可以看成是位圖中的第一個掃描行
IntPtr ptr = data.Scan0;
//定義數組長度
int soureBitArrayLength = data.Height * Math.Abs(data.Stride);
byte[] sourceBitArray = new byte[soureBitArrayLength];
//將bitmap中的內容拷貝到ptr_bgr數組中
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;
}
有關這部分的內容,可以參考微軟關於BitmapData的注解。https://msdn.microsoft.com/zh-cn/library/system.drawing.imaging.bitmapdata.aspx
識別人臉
回到我們的這個方法,我們繼續人臉識別的過程首先,我們把獲取到的圖像信息存起來
byte[] imageData = readBmp(bitmap, ref width, ref height, ref pitch);
通過前面的過程,我們知道,我們的代碼中的傳入圖像的參數類型是ASVLOFFSCREEN指針。通過查看ASVLOFFSCREEN類型。我們可以發現,u32PixelArrayFormat為需要圖像的格式。這個是因為我們准備使用BMP位圖,因此我們直接使用ASVL_PAF_RGB24_B8G8R8格式通過查詢可知定義的值為513.
i32Width和i32Height則為識別圖像的大小。ppu8Plane為一個批向byte數組的指針數組,這里面會保存我們剛剛轉換后的圖片數據。而pi32Pitch則是為每一個圖像指定了pitch大小,在結構中,一次人臉識別工作,可以傳遞四幅圖片。
我們先來把byte[]數組轉化為C++識別的數組類型。
IntPtr imageDataPtr = Marshal.AllocHGlobal(imageData.Length);
Marshal.Copy(imageData, 0, imageDataPtr, imageData.Length);
接下來是根據剛才的分析,我們設置的ASVLOFFSCREEN的結構體類型
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;
由於方法中需要是的一個結構體的指針,因此,我們還需要調用Marshal. AllocHGlobal方法創建指針,並使用Marshal.StructureToPtr進行初始化。
IntPtr offInputPtr = Marshal.AllocHGlobal(Marshal.SizeOf(offInput));
Marshal.StructureToPtr(offInput, offInputPtr, false);
由於接口還需要一個結構體保存返回的人臉數據,我們來定義它
AFD_FSDK_FACERES faceRes = new AFD_FSDK_FACERES();
同人臉數據一樣,我們需要把這個結構體轉換為指針類型。
IntPtr faceResPtr = Marshal.AllocHGlobal(Marshal.SizeOf(faceRes));
這個是返回值,因此我們不需要對內容進行初始化。我們直接調用引擎
int detectResult = FaceDllImport.AFD_FSDK_StillImageFaceDetection(detectEngine, offInputPtr, ref faceResPtr);
如果成功返回detectResult會返回0,也就是0
這個時候,返回為0並不意味着找到了人臉,具體的人臉信息還需要在我們的AFD_FSDK_FACERES結構休中查找。
使用Marshal.PtrToStructure批獲得的指針類型轉化為結構體類型。
faceRes = (AFD_FSDK_FACERES) Marshal.PtrToStructure(faceResPtr, typeof(AFD_FSDK_FACERES));
根據前端的結構體定義部分的數據,我們可以發現其中AFD_FSDK_FACERES.nFace屬性為識別到的人臉的數目。faceRes.rcFace則為識別到的人臉的數據。nFace可以直接轉化為int。
標出識別到的人臉信息
AFD_FSDK_FACERES中的rcFace是一個結構體指針,因此我們使用Marshal.PtrToStructure將其轉化為結構體。
MRECT rect = (MRECT)Marshal.PtrToStructure(faceRes.rcFace , typeof(MRECT));
通過獲得這個rect信息,就可以得到我們需要的人臉的位置數據了,包括人臉矩形的在上角和右下角的坐標。然后我們就可以利用這些數據來重新創建一個位圖
Image image = CutFace(bitmap, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top);
將位圖顯示到圖片控件上
this.pictureBox2.Image = image;
然后我們想像Demo中的一樣,標出人臉的位置。我們就可以使用這樣的方法。
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;
}
點擊運行
現在你可以點擊運行你的項目了,如果沒有任何問題,你的將會看到下面的畫面。
如果出現問題,你需要根據返回的錯誤碼進行查找。
引擎初始化失敗
一般是APPID和APPKEY不對,你需要確保你到下載的地方申請了正確的APPID和KEY,並且注意平台是Windows平台的。初始化失敗可以通過返回值進行查看,他們官網上也會有一個錯誤代碼表。對照查表一般會解決問題。
找不到DLL
首先請保證你把DLL拷貝到對應的目錄下面,其次要確定設置輸出選項為拷貝到輸出目錄。
內存不能讀或者寫
這個是C++的尿性,也是C#程序員不多見的報錯,主要檢查相關參數是否傳入正確。還要注意,如果人臉檢測返回的值不為0,獲取到的人臉數目會是一個比較大的隨機數。這個時候如果用循環讀取,就會出現地址越界的情況。
最后來一張華仔的圖鎮樓

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