在上篇文章中我們提到了人臉識(shí)別的想法,并且下載到了虹軟免費(fèi)的人臉識(shí)別的SDK,然后發(fā)現(xiàn)它是C++版本的,經(jīng)過了一番百度之后發(fā)現(xiàn)原來C#可以使用P/Invoke的方式來操作C++的DLL的,而且相當(dāng)方便。我們今天就來實(shí)現(xiàn)它。
項(xiàng)目目標(biāo)
我們希望先實(shí)現(xiàn)我們的簡(jiǎn)單的Hello World功能,從一張照片中檢測(cè)人臉是否存在,我們稱之為靜態(tài)人臉檢測(cè)。我們希望程序能夠打開一張照片,告訴我們這張照片中是否有人臉,如果有,就需要識(shí)別并顯示出來,如果沒有,就提示照片中沒有人臉。
創(chuàng)建Demo項(xiàng)目
項(xiàng)目技術(shù)
我們使用C# 4.0版本,IDE使用Visual Studio 2013,項(xiàng)目就用標(biāo)準(zhǔn)的Winform項(xiàng)目。
建立項(xiàng)目
我們打開Visual Studio,選擇C#語言,建立Winfrom項(xiàng)目,項(xiàng)目名稱為FaceDetectDemo,路徑隨便選。立項(xiàng)后,項(xiàng)目結(jié)構(gòu)如圖所示:
上圖中的AFD和dll文件夾我們后面就會(huì)用到,剛建項(xiàng)目時(shí)是沒有這兩個(gè)文件夾的。
建立視圖
通過設(shè)計(jì)器和工具箱,我們可以建立我們的視圖界面,包括一個(gè)按鈕兩個(gè)PictureBox.
大的那個(gè)我們用來顯示完整的圖片,小的用來顯示識(shí)別到的人臉信息。
我們把大PicturesBox的那個(gè)命名為pictureBox1,小的命名為pictureBox2,然后設(shè)置兩個(gè)的SizeMode均為Zoom, 以方便我們自動(dòng)顯示照片。
下載需要的SDK
這里我們需要虹軟提供的SDK中的DLL,如果你還沒有下載它,那么現(xiàn)在就是下載的時(shí)候了。訪問地址http://www.arcsoft.com.cn/ai/arcface.html 在明顯的地方找到WIndows版本,填寫基本的資料后就可以下載了。
下載的時(shí)候有一個(gè)版本選擇,1:1,1:N之類的,我們選擇默認(rèn)的就可以了,1:N和1:1在人臉識(shí)別上是有差別的,但在人臉檢測(cè)功能上基本上沒有差異。
在下載完成的頁面上,會(huì)顯示你申請(qǐng)的APPID和SDK KEY的信息,如下所示
請(qǐng)確保牢記這些Key,因?yàn)榻酉聛淼某绦蛑心銓⑿枰@些Key,如果忘記了,就登錄剛才的那個(gè)地址,在用戶中心里面可以看到這些Key,當(dāng)然,你也可以在郵件中查找。
我們打開下載的文件,是一個(gè)zip格式的壓縮包,我們把它解壓。發(fā)現(xiàn)里面還有三個(gè)包,我們解壓其中名為Face_Detection的包??梢钥吹较旅娴哪夸浗Y(jié)構(gòu)
命名很清晰,我這里只需要簡(jiǎn)單說一下。lib中的dll是要拷貝到你的運(yùn)行目錄中的,doc中的PDF相當(dāng)重要,是SDK的入門指南。samplecode和inc是供C++調(diào)用時(shí)候用到的參考源碼和頭文件。這些都是比較重要的。
現(xiàn)在,讓我們把dll拖入到我們的應(yīng)用程序的bin目錄.在編輯選項(xiàng)時(shí)選擇始終復(fù)制這個(gè)文件到輸出目錄.
另外我們的SDK是32位系統(tǒng)的,所以我們還需要設(shè)置編譯選項(xiàng)為x86.
至此,項(xiàng)目創(chuàng)建工作順利完成。
一步一步,根據(jù)人臉識(shí)別的SDK代碼示例來完善項(xiàng)目
現(xiàn)在我們回到上一章節(jié)的四個(gè)文件夾,我們打開doc文件夾。這里面的pdf文件是我們接下來課程的基礎(chǔ)。通讀一遍,發(fā)現(xiàn)4個(gè)函數(shù),3個(gè)結(jié)構(gòu)體,然后2個(gè)枚舉,兩個(gè)變量類型,還有一段示例代碼。我們來一步步定義它們.
自定義數(shù)據(jù)類型
C/C++ 可以定義自己的類型,打開SDK文檔可以發(fā)現(xiàn),這里面幾乎沒有我們熟悉的int,long,char*這些類型,取而代之是的Mint以及一些其它AFD開頭的類型,SDK文檔開篇引入了兩個(gè)基礎(chǔ)類型。
typedef MInt32 AFD_FSDK_OrientPriority;
typedef MInt32 AFD_FSDK_OrientCode;
所有基本類型在平臺(tái)庫中有定義。
定義規(guī)則是在ANSIC 中的基本類型前加上字母“M”同時(shí)將類型的第一個(gè)字母改成大寫。
例如“l(fā)ong” 被定義成“MLong”
具體到上面的代碼,它的意思是在項(xiàng)目中遇到AFD_FSDK_OrientPriority就認(rèn)為是Mint32,對(duì)應(yīng)C#就是int,全部的定義在inc文件夾afdcommdef.h頭文件中
定義結(jié)構(gòu)體
由于C并不是面向?qū)ο蟮恼Z言,結(jié)構(gòu)體作為可以自定義的類型,在一定程度的代替了我們C#中的類和對(duì)象,我們來一步步定義這些結(jié)構(gòu)體。
AFD_FSDK_FACERES
這個(gè)結(jié)構(gòu)體是用來存儲(chǔ)臉部信息的,我們可以從文檔中得到它的定義如下:
typedef struct{
MRECT * rcFace;
MLong nFace;
AFD_FSDK_OrientCode * lfaceOrient;
} AFD_FSDK_FACERES, * LPAFD_FSDK_FACERES;
根據(jù)我們上一節(jié)中的內(nèi)容,可以知道這個(gè)MLong類似于long,rcFace和lfaceOrient則是兩個(gè)指針。那么在C#中如何使用指針呢,直接用unsafe code肯定是可以的,不過這里我們使用IntPtr.
IntPtr的簡(jiǎn)介
IntPtr用于表示指針或句柄的平臺(tái)特定類型。這個(gè)其實(shí)說出了這樣兩個(gè)事實(shí),IntPtr 可以用來表示指針或句柄、它是一個(gè)平臺(tái)特定類型,它主要用在兩個(gè)地方:
(1)C#調(diào)用WIN32 API時(shí)
(2)C#調(diào)用C/C++寫的DLL時(shí)(其實(shí)和1相同,只是這個(gè)一般是我們?cè)诤退撕献鏖_發(fā)時(shí)經(jīng)常用到)
我們可以這樣子理解,IntPtr就可以互換C++中的指針
我們根據(jù)剛才所說的定義規(guī)則,換算成C#語言的定義如下:
public struct AFD_FSDK_FACERES
{
public int nFace;
public IntPtr rcFace;
public IntPtr lfaceOrient;
}
注意:nface雖然C++中是long,但對(duì)應(yīng)到C#中可不long,而是int.在32位程序中int和long占用的內(nèi)存大小都是4Byte=32bit,其表示的大小都是:-2147483648~2147483647。
MRECT
我們?cè)赟DK文檔中注意到rcFace的類型是MRect* 這里的* 說明這是一個(gè)指針類型,因此我們?cè)诙x這個(gè)類的時(shí)候使用了IntPtr,但是MRect是一個(gè)結(jié)構(gòu)體,我們可在inc文件夾下面的amcomdef.h下面找到了它的定義.
typedef struct __tag_rect
{
MInt32 left;
MInt32 top;
MInt32 right;
MInt32 bottom;
} MRECT, *PMRECT;
這個(gè)類型比較簡(jiǎn)單,C#版定義如下:
public struct MRECT
{
public int left;
public int top;
public int right;
public int bottom;
}
AFD_FSDK_VERSION
這個(gè)結(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中查找,因此我們的對(duì)應(yīng)的C#版本如下:
//定義FD的版本號(hào)
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
接下來我們來定義枚舉,這里面用到的枚舉有以下兩個(gè):AFD_FSDK_OrientPriority和AFD_FSDK_OrientCode,枚舉比較簡(jiǎn)單。我們只需要把十六進(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
定義臉部角度的檢測(cè)范圍
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
這個(gè)結(jié)構(gòu)體是用來進(jìn)行人臉識(shí)別的關(guān)鍵結(jié)構(gòu),我當(dāng)初就是在定義函數(shù)時(shí)才發(fā)現(xiàn)這個(gè)沒有。又跑回來重新定義的。這個(gè)在SDK文檔中沒有,但是我們?cè)谑纠a中能夠看到。我看來看看一下LPASVLOFFSCREEN的定義。在我們SDK的inc文件夾中,我們找到了一個(gè)名為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:像素?cái)?shù)組的格式
ppu8Plane[4]為一個(gè)指針數(shù)組
pi32Pitch[4]為一整形數(shù)組
如何定義數(shù)組
數(shù)組的定義沒有我們想象中的那么簡(jiǎn)單。在C++中定義數(shù)組的時(shí)候,是指定了數(shù)組的長(zhǎng)度的,而C#中定義數(shù)組時(shí),是不指定長(zhǎng)度的。這只是一個(gè)問題,另一個(gè)問題是因?yàn)镃#的數(shù)據(jù)和C++的數(shù)據(jù)布局方式有很大的不同,在P/Invoke和COM Interop當(dāng)中必須要在C#和C++之間傳遞數(shù)據(jù),有的時(shí)候,CLR或者說.NET能夠自動(dòng)在兩種編程語言之間轉(zhuǎn)換數(shù)據(jù),有的時(shí)候又不行,這時(shí)候就需要程序員來幫忙告訴.NET怎樣轉(zhuǎn)換數(shù)據(jù)了。這個(gè)轉(zhuǎn)換的方式是指定MarshalAs屬性。Marshal屬性相當(dāng)難用,如何轉(zhuǎn)換是一個(gè)復(fù)雜的事情,這個(gè)時(shí)個(gè)我們需要請(qǐng)出微軟的神器。P/Invoke Interop Assistant,你可以去下面的鏈接下載這個(gè)神器 http://download.microsoft.com/download/f/2/7/f279e71e-efb0-4155-873d-5554a0608523/CLRInsideOut2008_01.exe
通過P/Invoke Interop Assistant的幫忙,我們可以知道應(yīng)該這樣子定義這個(gè)結(jié)構(gòu)體。
使用這個(gè)工具時(shí),需要注意的是要把我們結(jié)構(gòu)體中的類型轉(zhuǎn)化為標(biāo)準(zhǔn)的C類型,我們可以在inc的amcomdef.h頭文件中找到它們的轉(zhuǎn)換定義。
我們來看一下最終的這個(gè)結(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)在你可以用這個(gè)工具來定義我們接來所需要用到的所有數(shù)據(jù)結(jié)構(gòu),也可以用來定義API函數(shù)。
定義API函數(shù)
查看SDK文檔,可以看到FD共提供了3個(gè)方法。我們定義一個(gè)類來包含這些方法
新建AFD文件夾,定義AFDFunction類,里面包含SDK中提供的所有方法。
AFD_FSDK_INITIALFACEENGINE
我們先來看一下第一個(gè)方法,初始化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] 用戶申請(qǐng)SDK時(shí)獲取的App Id
- SDKKey [in] 用戶申請(qǐng)SDK時(shí)獲取的SDK Key
- pMem [in] 分配給引擎使用的內(nèi)存地址
- lMemSize [in] 分配給引擎使用的內(nèi)存大小
- pEngine [out] 引擎handle
- iOrientPriority [in] 期望的臉部檢測(cè)角度范圍
- nScale [in] 用于數(shù)值表示的最小人臉尺寸 有效值范圍[2,50] 推薦值 16。該尺寸是人臉相對(duì)于所在圖片的長(zhǎng)邊的占比。例如,如果用戶想檢測(cè)到的最小人臉尺寸是圖片長(zhǎng)度的1/8,那么這個(gè)nScale就應(yīng)該設(shè)置為8
- nMaxFaceNum [in] 用戶期望引擎最多能檢測(cè)出的人臉數(shù) 有效值范圍[1,50]
如果成功返回MOK,失敗返回MRCode,MOK是一個(gè)int型的值為0,MRCOde是一個(gè)定義。可以在inc文件 夾中的merror.h中找到。
通過剛才提供的神器,我們可以定義這個(gè)函數(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這個(gè)屬性用于定義C++函數(shù)調(diào)用的方式。
- Cdecl 調(diào)用方清理堆棧。這使您能夠調(diào)用具有 varargs 的函數(shù)(如 Printf),使之可用于接受可變數(shù)目的參數(shù)的方法。
- FastCall 不支持此調(diào)用約定。
- StdCall 被調(diào)用方清理堆棧。這是使用平臺(tái) invoke 調(diào)用非托管函數(shù)的默認(rèn)約定。
- ThisCall 第一個(gè)參數(shù)是 this 指針,它存儲(chǔ)在寄存器 ECX 中。其他參數(shù)被推送到堆棧上。此調(diào)用約定用于對(duì)從非托管 DLL 導(dǎo)出的類調(diào)用方法。
- Winapi 此成員實(shí)際上不是調(diào)用約定,而是使用了默認(rèn)平臺(tái)調(diào)用約定。例如,在 Windows 上默認(rèn)為 StdCall,在 Windows CE.NET 上默認(rèn)為 Cdecl。
默認(rèn)情況下,C和C++使用的Cdecl調(diào)用,因此我們?cè)谡{(diào)用DLL時(shí)指定這個(gè)值就可以。
AFD_FSDK_STILLIMAGEFACEDETECTION
這個(gè)方法是我們的核心方法,它的功能如我們所料,就是通過讀取輸入的圖像,檢測(cè)是否存在人臉內(nèi)容并輸出人臉的結(jié)果信息。我們來看一下基礎(chǔ)定義。
MRESULT AFD_FSDK_StillImageFaceDetection(
MHandle hEngine,
LPASVLOFFSCREEN pImgData,
LPAFD_FSDK_FACERES pFaceRes
);
hEngine [in] 引擎handle
pImgData [in] 待檢測(cè)的圖像信息
pFaceRes [out] 人臉檢測(cè)結(jié)果
和初始化類似,第一個(gè)參數(shù)是hEngine引用,第二個(gè)參數(shù)pImgData是要檢測(cè)的圖形信息,第三個(gè)參數(shù)pFaceRes是一個(gè)輸出參數(shù),獲取人臉的檢測(cè)結(jié)果。需要注意的是里面的參數(shù)類型,第一個(gè)MHandle對(duì)應(yīng)的是引擎的引用,這個(gè)沒有問題,第二個(gè)是LPASVLOFFSCREEN 它是指向ASVLOFFSCREEN的一個(gè)結(jié)構(gòu)體指針,同樣LPAFD_FSDK_FACERES也是一個(gè)指針,我們知道指針對(duì)應(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);
AFD_FSDK_GETVERSION
初始化之后的方法是GetVersion,功能就是獲取SDK的版本信息。
原型
const AFD_FSDK_Version * AFD_FSDK_GetVersion(
MHandle hEngine
);
這個(gè)方法比較簡(jiǎn)單,參數(shù)就是Engine的引用,其返回值為Version結(jié)構(gòu)體,我們?cè)谧畛醯臅r(shí)候已經(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)建完畢.
實(shí)現(xiàn)圖片讀取和人臉識(shí)別功能
我們來實(shí)現(xiàn)我們的圖片讀取和人臉識(shí)別功能,這個(gè)章節(jié)中,會(huì)包含大量的細(xì)節(jié)及互操作的內(nèi)容。
基礎(chǔ)知識(shí)介紹
其實(shí)關(guān)于P/Invoke的操作我們前面的代碼已經(jīng)講解了很多。也基本把我們用到的結(jié)構(gòu)體和函數(shù)定義出來,我們知道
指針映射為IntPtr,
引用類變量映射為IntPtr,
char *可以映攝為字符串
結(jié)構(gòu)體,和數(shù)組如果從IntPtr中取數(shù)據(jù)呢,我們需要使用的一個(gè)類叫Marshal
我們來看一下MSDN上的介紹
https://msdn.microsoft.com/zh-cn/library/system.runtime.interopservices.marshal(v=vs.110).aspx
Marshal類提供了一個(gè)方法集合,這些方法用于分配非托管內(nèi)存、復(fù)制非托管內(nèi)存塊、將托管類型轉(zhuǎn)換為非托管類型,此外還提供了在與非托管代碼交互時(shí)使用的其他雜項(xiàng)方法,我們將會(huì)在下面開發(fā)進(jìn)程中頻繁使用這個(gè)類的多個(gè)方法。
例如:在定義一個(gè)指針類型變量時(shí)IntPtr,我們需要使用Marshal.AllocHGlobal為其分配內(nèi)存,得到IntPtr變量,在分配內(nèi)存時(shí),我們需要使用Marshal.SizeOf計(jì)算需要分配的內(nèi)存的大小。然后調(diào)用Marshal.
StructureToPtr為變量賦值
讓我們帶著這些概念開始我們下面的內(nèi)容。
初始化引擎
根據(jù)我們的SDK說明文檔,在使用引擎之前需要先初始化。出于簡(jiǎn)單我就把初始化代碼的部分放在Form1的構(gòu)造函數(shù)內(nèi)。而把引擎作為類的實(shí)例變量定義。
我們?cè)跇?gòu)造函數(shù)中添加初始化的代碼。
定義人臉識(shí)別引擎
IntPtr detectEngine = IntPtr.Zero;
定義人臉識(shí)別引擎參數(shù)
我們可以根據(jù)sampleCode定義我們?nèi)四樧R(shí)別所需要的參數(shù)
首先,定義Engine運(yùn)行需要的內(nèi)存,寬容度,人臉的數(shù)目以及有效的人臉角度。
int detectSize = 40 * 1024 * 1024;
int nScale = 50;
int nMaxFaceNum = 10;
string appId = "你申請(qǐng)到的APPID";
string sdkFDKey = "你申請(qǐng)到的FDKEY";
初始始化引擎內(nèi)存緩沖區(qū)
在示例代碼中,我們可以得到引擎在初始化時(shí),需要指定緩沖區(qū)。
在C#中,可以使用
pMem = Marshal.AllocHGlobal(detectSize);
初始始化引擎
針對(duì)人臉角度的檢測(cè)范圍,直接傳遞為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("引擎初始化失敗:錯(cuò)誤碼為:" + retCode);
this.Close();
}
實(shí)現(xiàn)業(yè)務(wù)邏輯
接下來,我們找到我們的btnLoadImage方法,在這里填寫我們的業(yè)務(wù)處理邏輯。
1.讀取一個(gè)jpg的文件,并加載的pictureBox1中顯示出來,
2.然后調(diào)用我們的引擎的AFD_FSDK_StillImageFaceDetection方法,檢查出人臉的位置。
3最后我們利用GDI+,把檢測(cè)到的人臉部分提取出位置顯示到PictureBox2中,
4.把pictureBox1中的圖片,添加上識(shí)別的紅框,完成人臉檢測(cè)的效果。
打開圖片
加載圖片比較簡(jiǎn)單,我們調(diào)用OpenFileDialog方法,打開一個(gè)圖片文件,并顯示到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);
}
檢測(cè)并標(biāo)記人臉
終于到正題了,很興奮,對(duì)吧。不過還是沒有思路,因?yàn)槲覀儾恢廊绾蝸碚{(diào)用那個(gè)引擎。這個(gè)時(shí)候我們必須參考samplecode,通過sampleCode我們可以得知,首先我們需要讀取圖片的內(nèi)容到BMP格式,而且這個(gè)BMP格式必須為ASVL_PAF_RGB24_B8G8R8,標(biāo)準(zhǔn)的Image中的Bitmap就是這個(gè)格式,讀取bitmap中的所有圖像信息存入ASVLOFFSCREEN的offInput中,這時(shí)候SampleCode中的代碼是從文件中讀取的,我們要直接從Bitmap中讀取,這里面還是有一些不一樣的。我們首先來看一下這個(gè)讀取的代碼
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);
//位圖中第一個(gè)像素?cái)?shù)據(jù)的地址。它也可以看成是位圖中的第一個(gè)掃描行
IntPtr ptr = data.Scan0;
//定義數(shù)組長(zhǎng)度
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
識(shí)別人臉
回到我們的這個(gè)方法,我們繼續(xù)人臉識(shí)別的過程首先,我們把獲取到的圖像信息存起來
byte[] imageData = readBmp(bitmap, ref width, ref height, ref pitch);
通過前面的過程,我們知道,我們的代碼中的傳入圖像的參數(shù)類型是ASVLOFFSCREEN指針。通過查看ASVLOFFSCREEN類型。我們可以發(fā)現(xiàn),u32PixelArrayFormat為需要圖像的格式。這個(gè)是因?yàn)槲覀儨?zhǔn)備使用BMP位圖,因此我們直接使用ASVL_PAF_RGB24_B8G8R8格式通過查詢可知定義的值為513.
i32Width和i32Height則為識(shí)別圖像的大小。ppu8Plane為一個(gè)批向byte數(shù)組的指針數(shù)組,這里面會(huì)保存我們剛剛轉(zhuǎn)換后的圖片數(shù)據(jù)。而pi32Pitch則是為每一個(gè)圖像指定了pitch大小,在結(jié)構(gòu)中,一次人臉識(shí)別工作,可以傳遞四幅圖片。
我們先來把byte[]數(shù)組轉(zhuǎn)化為C++識(shí)別的數(shù)組類型。
IntPtr imageDataPtr = Marshal.AllocHGlobal(imageData.Length);
Marshal.Copy(imageData, 0, imageDataPtr, imageData.Length);
接下來是根據(jù)剛才的分析,我們?cè)O(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;
由于方法中需要是的一個(gè)結(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);
由于接口還需要一個(gè)結(jié)構(gòu)體保存返回的人臉數(shù)據(jù),我們來定義它
AFD_FSDK_FACERES faceRes = new AFD_FSDK_FACERES();
同人臉數(shù)據(jù)一樣,我們需要把這個(gè)結(jié)構(gòu)體轉(zhuǎn)換為指針類型。
IntPtr faceResPtr = Marshal.AllocHGlobal(Marshal.SizeOf(faceRes));
這個(gè)是返回值,因此我們不需要對(duì)內(nèi)容進(jìn)行初始化。我們直接調(diào)用引擎
int detectResult = FaceDllImport.AFD_FSDK_StillImageFaceDetection(detectEngine, offInputPtr, ref faceResPtr);
如果成功返回detectResult會(huì)返回0,也就是0
這個(gè)時(shí)候,返回為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í)別到的人臉的數(shù)目。faceRes.rcFace則為識(shí)別到的人臉的數(shù)據(jù)。nFace可以直接轉(zhuǎn)化為int。
標(biāo)出識(shí)別到的人臉信息
AFD_FSDK_FACERES中的rcFace是一個(gè)結(jié)構(gòu)體指針,因此我們使用Marshal.PtrToStructure將其轉(zhuǎn)化為結(jié)構(gòu)體。
MRECT rect = (MRECT)Marshal.PtrToStructure(faceRes.rcFace , typeof(MRECT));
通過獲得這個(gè)rect信息,就可以得到我們需要的人臉的位置數(shù)據(jù)了,包括人臉矩形的在上角和右下角的坐標(biāo)。然后我們就可以利用這些數(shù)據(jù)來重新創(chuàng)建一個(gè)位圖
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#方法比較簡(jiǎn)單,純屬C#代碼,比較簡(jiǎn)單
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;
}
點(diǎn)擊運(yùn)行
現(xiàn)在你可以點(diǎn)擊運(yùn)行你的項(xiàng)目了,如果沒有任何問題,你的將會(huì)看到下面的畫面。
如果出現(xiàn)問題,你需要根據(jù)返回的錯(cuò)誤碼進(jìn)行查找。
引擎初始化失敗
一般是APPID和APPKEY不對(duì),你需要確保你到下載的地方申請(qǐng)了正確的APPID和KEY,并且注意平臺(tái)是Windows平臺(tái)的。初始化失敗可以通過返回值進(jìn)行查看,他們官網(wǎng)上也會(huì)有一個(gè)錯(cuò)誤代碼表。對(duì)照查表一般會(huì)解決問題。
找不到DLL
首先請(qǐng)保證你把DLL拷貝到對(duì)應(yīng)的目錄下面,其次要確定設(shè)置輸出選項(xiàng)為拷貝到輸出目錄。
內(nèi)存不能讀或者寫
這個(gè)是C++的尿性,也是C#程序員不多見的報(bào)錯(cuò),主要檢查相關(guān)參數(shù)是否傳入正確。還要注意,如果人臉檢測(cè)返回的值不為0,獲取到的人臉數(shù)目會(huì)是一個(gè)比較大的隨機(jī)數(shù)。這個(gè)時(shí)候如果用循環(huán)讀取,就會(huì)出現(xiàn)地址越界的情況。
最后來一張華仔的圖鎮(zhèn)樓
今天我們只是講解了一下人臉識(shí)別的最簡(jiǎn)單的Demo,我們下一節(jié)從獲取兩張人臉的相似度來入手講解如何識(shí)別不同的人的,歡迎繼續(xù)關(guān)注。如果你已經(jīng)了解了本博客的內(nèi)容,你可以打開FR的文檔,自己來進(jìn)行模擬實(shí)現(xiàn)。