C# 實現(xiàn)人臉識別一 (運用虹軟人臉識別引擎)

知識背景:

下載虹軟人臉識別引擎

下載地址: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)建工作順利完成。

一步一步,根據(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就認(rèn)為是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相當(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中提供的所有方法。

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是一個定義??梢栽趇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);

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é)中,會包含大量的細(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;

定義人臉識別引擎參數(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);

? ? ? ? ? ? }

檢測并標(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。

標(biāo)出識別到的人臉信息

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)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容