在上篇文章中我們提到了人臉識別的想法措嵌,并且下載到了虹軟免費的人臉識別的SDK翎冲,然后發(fā)現(xiàn)它是C++版本的,經(jīng)過了一番百度之后發(fā)現(xiàn)原來C#可以使用P/Invoke的方式來操作C++的DLL的沈堡,而且相當方便坡锡。我們今天就來實現(xiàn)它。
項目目標
我們希望先實現(xiàn)我們的簡單的Hello World功能院溺,從一張照片中檢測人臉是否存在楣嘁,我們稱之為靜態(tài)人臉檢測。我們希望程序能夠打開一張照片珍逸,告訴我們這張照片中是否有人臉逐虚,如果有,就需要識別并顯示出來谆膳,如果沒有叭爱,就提示照片中沒有人臉。
創(chuàng)建Demo項目
項目技術(shù)
我們使用C# 4.0版本漱病,IDE使用Visual Studio 2013涤伐,項目就用標準的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之類的槽唾,我們選擇默認的就可以了丧枪,1:N和1:1在人臉識別上是有差別的,但在人臉檢測功能上基本上沒有差異庞萍。
在下載完成的頁面上豪诲,會顯示你申請的APPID和SDK KEY的信息,如下所示
請確保牢記這些Key挂绰,因為接下來的程序中你將需要這些Key屎篱,如果忘記了,就登錄剛才的那個地址葵蒂,在用戶中心里面可以看到這些Key交播,當然,你也可以在郵件中查找践付。
我們打開下載的文件秦士,是一個zip格式的壓縮包,我們把它解壓永高。發(fā)現(xiàn)里面還有三個包隧土,我們解壓其中名為Face_Detection的包∶溃可以看到下面的目錄結(jié)構(gòu)
命名很清晰曹傀,我這里只需要簡單說一下。lib中的dll是要拷貝到你的運行目錄中的饲宛,doc中的PDF相當重要皆愉,是SDK的入門指南。samplecode和inc是供C++調(diào)用時候用到的參考源碼和頭文件。這些都是比較重要的幕庐。
現(xiàn)在久锥,讓我們把dll拖入到我們的應(yīng)用程序的bin目錄.在編輯選項時選擇始終復制這個文件到輸出目錄.
另外我們的SDK是32位系統(tǒng)的,所以我們還需要設(shè)置編譯選項為x86.
至此异剥,項目創(chuàng)建工作順利完成瑟由。
一步一步,根據(jù)人臉識別的SDK代碼示例來完善項目
現(xiàn)在我們回到上一章節(jié)的四個文件夾冤寿,我們打開doc文件夾歹苦。這里面的pdf文件是我們接下來課程的基礎(chǔ)。通讀一遍疚沐,發(fā)現(xiàn)4個函數(shù),3個結(jié)構(gòu)體潮模,然后2個枚舉亮蛔,兩個變量類型,還有一段示例代碼擎厢。我們來一步步定義它們.
自定義數(shù)據(jù)類型
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就認為是Mint32,對應(yīng)C#就是int宵蕉,全部的定義在inc文件夾afdcommdef.h頭文件中
定義結(jié)構(gòu)體
由于C并不是面向?qū)ο蟮恼Z言酝静,結(jié)構(gòu)體作為可以自定義的類型,在一定程度的代替了我們C#中的類和對象羡玛,我們來一步步定義這些結(jié)構(gòu)體别智。
AFD_FSDK_FACERES
這個結(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盔性。
MRECT
我們在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相當于int,MPChar相當于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构眯,枚舉比較簡單愕难。我們只需要把十六進制轉(zhuǎn)換為10進制就可以了。
根據(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)體是用來進行人臉識別的關(guān)鍵結(jié)構(gòu)猫缭,我當初就是在定義函數(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當中必須要在C#和C++之間傳遞數(shù)據(jù),有的時候责鳍,CLR或者說.NET能夠自動在兩種編程語言之間轉(zhuǎn)換數(shù)據(jù)碾褂,有的時候又不行,這時候就需要程序員來幫忙告訴.NET怎樣轉(zhuǎn)換數(shù)據(jù)了历葛。這個轉(zhuǎn)換的方式是指定MarshalAs屬性正塌。Marshal屬性相當難用嘀略,如何轉(zhuǎn)換是一個復雜的事情,這個時個我們需要請出微軟的神器乓诽。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)化為標準的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中提供的所有方法蹲缠。
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
);
我們來看一下它的參數(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是一個定義抹剩〕盘可以在inc文件 夾中的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ù)的默認約定缴罗。
- ThisCall 第一個參數(shù)是 this 指針,它存儲在寄存器 ECX 中祭埂。其他參數(shù)被推送到堆棧上面氓。此調(diào)用約定用于對從非托管 DLL 導出的類調(diào)用方法。
- Winapi 此成員實際上不是調(diào)用約定,而是使用了默認平臺調(diào)用約定舌界。例如掘譬,在 Windows 上默認為 StdCall,在 Windows CE.NET 上默認為 Cdecl禀横。
默認情況下屁药,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);
AFD_FSDK_GETVERSION
初始化之后的方法是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)圖片讀取和人臉識別功能
我們來實現(xiàn)我們的圖片讀取和人臉識別功能郎楼,這個章節(jié)中万伤,會包含大量的細節(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)存芜抒、復制非托管內(nèi)存塊珍策、將托管類型轉(zhuǎn)換為非托管類型笼吟,此外還提供了在與非托管代碼交互時使用的其他雜項方法票堵,我們將會在下面開發(fā)進程中頻繁使用這個類的多個方法。
例如:在定義一個指針類型變量時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;
定義人臉識別引擎參數(shù)
我們可以根據(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";
初始始化引擎內(nèi)存緩沖區(qū)
在示例代碼中,我們可以得到引擎在初始化時丙挽,需要指定緩沖區(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();
}
實現(xiàn)業(yè)務(wù)邏輯
接下來,我們找到我們的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);
}
檢測并標記人臉
終于到正題了段化,很興奮,對吧造成。不過還是沒有思路穗泵,因為我們不知道如何來調(diào)用那個引擎。這個時候我們必須參考samplecode,通過sampleCode我們可以得知谜疤,首先我們需要讀取圖片的內(nèi)容到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鎖定到系統(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為需要圖像的格式像啼。這個是因為我們準備使用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進行初始化脊串。
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)容進行初始化祟昭。我們直接調(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ù)了疾忍,包括人臉矩形的在上角和右下角的坐標。然后我們就可以利用這些數(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中的一樣床三,標出人臉的位置一罩。我們就可以使用這樣的方法。
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ù)返回的錯誤碼進行查找莲组。
引擎初始化失敗
一般是APPID和APPKEY不對,你需要確保你到下載的地方申請了正確的APPID和KEY暖夭,并且注意平臺是Windows平臺的锹杈。初始化失敗可以通過返回值進行查看撵孤,他們官網(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的文檔匾鸥,自己來進行模擬實現(xiàn)昂勉。