C#人脸识别入门篇(Step by step 人脸识别)
来源:互联网 发布:广汽丰田一汽丰田 知乎 编辑:程序博客网 时间:2024/04/28 18:47
C#人脸识别入门篇-Step ByStep人脸识别
引言
如今,基于人脸的技术和话题可以说是炙手可热,基于大数据和人工智能的人脸识别更是突破了我们的想象力的极限,如果应用中不能集成人脸识别,那就太跟不上潮流了。人脸识别是一个算法密集型的项目,如果自行开发,需要很深厚的数学功底和算法底蕴,成本较高,我一个做C#的,自问没有那么高的水平能够写出那么复杂的算法,即使能,我们的算法能和其它公司相比吗。不过好在现在是一个互联网时代,自己开发不行,那么使用其它现成的人脸识别引擎可行吗?答案当然是可行的。
本系列文章就将先从静态图片的人脸检测开始,逐步讲解C#是如何进行人脸识别的。共分为以下四篇
1.人脸识别入门—静态照片人脸检测
2.人脸识别入门—基于视频的人脸检测
3.人脸识别入门—人脸识别初应用
4.人脸识别入门—模拟简单的门禁系统应用
在开始之前,我们先来了解一些人脸识别的集成方式和基础知识,为下面的课程做准备。
选择人脸识别引擎的心路历程
通过搜索引擎,可以大致确定集成人脸识别的可选方式有以下几种
1. 集成WebAPI
目前以百度云,腾讯云为首的互联网公司提供了基于WEBAPI的集成方式,可以通过HTTP的方式提交识别请求,识别结果通过JSON串的方式返回。基于HTTP的方式识别人脸是比较慢的,慢的原因在于IO性能,相对来讲,离线版本的API则能够充分利用本机的机器资源,不用往返于所谓的算法云服务器,直接在本地就能完成人脸识别和标记工作。
2. 集成SDK
另一种是基于SDK的本地识别方式。本地识别的优点是速度快,集成度高。而且,作为C#,我们还可以搭建自己的云识别平台。 本人也尝试过主流开源的人脸识别SDK、如OpenCV、Dlib、Seetaface等等,但经过试用,都不太理想。最近在今日头条上看到有个商业公司免费提供人脸识别SDK,就想着去下载尝试试用一下。
下载引擎发现只C++
想到就要做到,于是赶紧打开电脑下载了SDK,吐槽下今日头条,做新闻不放链接太不厚道了。只能百度了,链接在这里http://www.arcsoft.com.cn/ai/arcface.html。
下载后,发现SDK包括了人脸识别,人脸检测,人脸跟踪所有的API。不过美中不足的是,这SDK竟然只有C++版本的,Windows版本不出C#,这虹软有点不近人情啊。不过伤心归伤心,活还得做,没有C#,那我们就拿C++的包裹出C#来用。其实有了C++就等于有了C#,因为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的全称是Platform Invoke (平台调用) 它实际上是一种函数调用机制,通过P/Invoke我们就可以调用非托管DLL中的函数 ,实际上很多NET基类库中定义的类 型内部部调用了从Kernel32.dll,User32.dll,gdi32.dll等非托管DLL中导出的函数。
看一个最简单的例子
[DllImportAttribute("user32.dll", EntryPoint ="SetCursorPos")]
[return: MarshalAsAttribute(UnmanagedType.Bool)] //可写可不写,定义如何封送返回参数
public staticextern bool SetCursorPos(intX,int Y);
3) P/Invoke的过程
关于P/Invoke的过程,我找到了MSDN上的一张图,如下所示。
在使用P/Invoke调用C/C++方法时,会依次执行以下操作
1 查找包含该函数的非托管DLL
2 将该非托管DLL加载到内存中
3 查找函数在内存中的地址并将其参数按照函数的调用约定压栈
4 将控制权转移给非托管函数
注意:只在第一次调用函数时,才会查找和加载非托管DLL并查找函数在内存中的地址。当非托管函数产生异常时,P/Invoke会将异常传递给托管调用方
看起来很复杂,但使用起来却很简单,只需要在C#中重新声明函数的定义就可以了,然后可以像其它函数一样调用。
但是,这里面最棘手的问题是处理要调用函数的签名,还有最难处理的指针和内存问题。我们需要根据SDK的提供方的函数说明来定义函数的签名。C#的自动内存管理机制让我们几乎忘记了C++中很司空见惯的琐碎细节,如果你以前没有玩过P/Invoke,也没有关系,我会给大家尽可能的讲解这里面需要注意的细节。
人脸识别入门—静态照片人脸检测
现在开始我们的项目,基于静态照片的人脸检测。我们希望我们的程序能够打开一张照片,告诉我们这张照片中是否有人脸,如果有,就需要识别并显示出来,如果没有,就提示照片中没有人脸。
我们的项目使用WinForm,C#版本采用的是4.0版本,IDE我使用的是VS2013的英文版。
建立项目
这一步比较简单,我们使用WinFrom的项目,我们使用.net 2.0或者4.0都可以,直接下一步就可以搞定。
上图中的AFD和dll文件夹我们后面就会用到,刚建项目时是没有这两个文件夹的。
建立视图
通过设计器和工具箱,我们可以建立我们的视图界面,包括一个按钮两个PictureBox.
大的那个我们用来显示完整的图片,小的用来显示识别到的人脸信息。
我们把大PicturesBox的那个命名为pictureBox1,小的命名为pictureBox2,然后设置两个的SizeMode均为Zoom, 以方便我们自动显示照片。
加载DLL
好了,如果刚才你还没有下载需要用的到SDK,那么在接下来之前,你需要下载它了。访问http://www.arcsoft.com.cn/ai/arcface.html。在这个页面找到
我们下载Windows版本的。接下来会要求你登录,注册,选择行业blabla~~.一切结束后,会告诉你申请成功,并且给出下载链接和SDK的Key。
请确保牢记这些Key,因为接下来的程序中你将需要这些Key,如果忘记了,就登录刚才的那个地址,在用户中心里面可以看到这些Key,当然,你也可以在邮件中查找。BTW,现在我们打开下载的包。
使用解压工具打开后,我们可以看到三个包。
我们选择Face_Detection这个包,解压之。
这里面的文件夹对我们比较重要。首先,我们需要把lib拖到我们的项目中。然后在选中这个dll文件,在属性设置中我们把它设置为始终拷贝。
由于我们的SDK是32位版本的,因此我们需要设置编译版本为x86
至此,创建项目工作正式完成。
一步一步,根据人脸识别的SDK代码示例来完善项目
现在我们回到上一章节的四个文件夹,我们打开doc文件夹。这里面的pdf文件是我们接下来课程的基础。通读一遍,发现4个函数,3个结构体,然后2个枚举,两个变量类型,还有一段示例代码。我们来一步步定义它们
基本类型
C/C++ 可以定义自己的类型,打开SDK文档可以发现,这里面几乎没有我们熟悉的int,long,char*这些类型,取而代之是的Mint以及一些其它AFD开头的类型,SDK文档开篇引入了两个基础类型。
typedef MInt32AFD_FSDK_OrientPriority;
typedef MInt32AFD_FSDK_OrientCode;
所有基本类型在平台库中有定义。定义规则是在ANSIC 中的基本类型前加上字母“M”同时将类型的第一个字母改成大写。例如“long” 被定义成“MLong”
这段比较明朗了,就是说凡是下面遇到AFD_FSDK_OrientPriority就认为是Mint32,对应C#就是int,全部的定义在inc文件夹afdcommdef.h头文件中
定义结构体。
由于C并不是面向对象的语言,结构体作为可以自定义的类型,在一定程度的代替了我们C#中的类和对象,我们来一步步定义这些结构体。
AFD_FSDK_FACERES
这个结构体是用来存储脸部信息的,我们可以从文档中得到它的定义如下:
typedefstruct{
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#格式为:
我们根据刚才所说的定义规则,换算成C#语言的定义如下:
类功能:定义检测到的脸部信息结构体
public struct AFD_FSDK_FACERES
{
public int nFace;
public IntPtrrcFace;
public IntPtrlfaceOrient;
}
注意:nface虽然C++中是long,但对应到C#中可不long,而是int.在32位程序中int和long占用的内存大小都是4Byte=32bit,其表示的大小都是:-2147483648~2147483647。
MRECT
我们继续关注rcFace,首先注意到MRECT,这个类型在文档中没有。但它确实是一个自定义类型。
我们在inc文件下面的amcomdef.h下面找到了它的定义。
typedef struct__tag_rect
{
MInt32 left;
MInt32 top;
MInt32 right;
MInt32 bottom;
} MRECT,*PMRECT;
我们先来定义这个MRECT类型,这个类型比较简单,C#版定义如下:
public struct MRECT
{
public int left;
public int top;
public int right;
public int bottom;
}
AFD_FSDK_Version
接下来的类型是Version也就是我们的版本,来看一下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 structAFD_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 enumAFD_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:像素数组的格式。我们可以根据SDK中的文档得到,SDK支持的格式为:
ASVL_PAF_I420
8-bit Y层,之后是8-bit的2x2 采样的U层和V层
ASVL_PAF_YUYV
Y0, U0, Y1, V0
ASVL_PAF_RGB24_B8G8R8
BGR24, B8G8R8
ppu8Plane[4]为一个指针数组,pi32Pitch[4]为一整形数组,根据我们前面的经验,我们同样定义这个结构体的C#版本如下:
public struct ASVLOFFSCREEN
{
public intu32PixelArrayFormat;
public int i32Width;
public inti32Height;
public IntPtr[] ppu8Plane;
public int[]pi32Pitch;
}
数组的定义没有我们想象中的那么简单。在C++中定义数组的时候,是指定了数组的长度的,而C#中定义数组时,是不指定长度的。这只是一个问题,另一个问题是因为C#的数据和C++的数据布局方式有很大的不同,在P/Invoke和COM Interop当中必须要在C#和C++之间传递数据,有的时候,CLR或者说.NET能够自动在两种编程语言之间转换数据,有的时候又不行,这时候就需要程序员来帮忙告诉.NET怎样转换数据了。这个转换的方式是指定MarshalAs属性。Marshal属性相当难用,如何转换是一个复杂的事情,这个时个我们需要请出微软的神器。P/InvokeInterop Assistant,你可以去下面的链接下载这个神器 http://download.microsoft.com/download/f/2/7/f279e71e-efb0-4155-873d-5554a0608523/CLRInsideOut2008_01.exe
我们把定义贴到这个工具中。注意把类型转换为标准的C类型。关于自定义类型M开头的与标准类型间的转换,可以参考amcomdef.h头文件
我们得到了正确的C#的定义
public structASVLOFFSCREEN
{
public intu32PixelArrayFormat;
public inti32Width;
public inti32Height;
[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函数
查看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]
其返回值MRESULT,成功返回MOK,失败返回MRCode,MOK是一个int型的值为0,MRCOde是一个定义。可以在inc文件夹中的merror.h中找到。
此函数的定义如下:
[DllImport("libarcsoft_fsdk_face_detection.dll", EntryPoint ="AFD_FSDK_InitialFaceEngine", CallingConvention =CallingConvention.Cdecl)]
public staticextern int AFD_FSDK_InitialFaceEngine(string appId,string sdkKey, IntPtr pMem, int lMemSize, ref IntPtr pEngine, int iOrientPriority, int nScale, int nMaxFaceNum);
此定义中的CallingConvention.Cdecl是我们在C#调用非托管的.dll文件时指定的调用方式,
Cdecl
调用方清理堆栈。这使您能够调用具有 varargs 的函数(如 Printf),使之可用于接受可变数目的参数的方法。
FastCall
不支持此调用约定。
StdCall
被调用方清理堆栈。这是使用平台 invoke 调用非托管函数的默认约定。
ThisCall
第一个参数是 this 指针,它存储在寄存器 ECX 中。其他参数被推送到堆栈上。此调用约定用于对从非托管 DLL 导出的类调用方法。
Winapi
此成员实际上不是调用约定,而是使用了默认平台调用约定。例如,在 Windows 上默认为 StdCall,在 Windows CE.NET 上默认为 Cdecl。
默认情况下,C和C++使用的Cdecl调用,因此我们在调用DLL时指定这个值就可以。
AFD_FSDK_StillImageFaceDetection
第二个接口是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
);
描述
获取SDK版本信息
参数 hEngine
[in]
引擎handle
这个方法比较简单,参数就是Engine的引用,其返回值为Version结构体,我们在最初的时候已经定义完成。
实现图片读取和人脸识别功能
我们来实现我们的图片读取和人脸识别功能,这个章节中,会包含大量的细节及互操作的内容。
基础知识介绍
其实关于P/Invoke的操作我们前面的代码已经讲解了很多。也基本把我们用到的结构体和函数定义出来,我们知道指针映射为IntPtr,引用类变量映射为IntPtr,char *可以映摄为字符串。那么,结构体,和数组如果从IntPtr中取数据呢,我们需要使用的一个类叫Marshal,其命名和我们前面的Attribute MarshalAs非常像。我们来看一下这个类。我们来看一下MSDN上的介绍
https://msdn.microsoft.com/zh-cn/library/system.runtime.interopservices.marshal(v=vs.110).aspx
它提供了一个方法集合,这些方法用于分配非托管内存、复制非托管内存块、将托管类型转换为非托管类型,此外还提供了在与非托管代码交互时使用的其他杂项方法,我们将会在下面开发进程中频繁使用这个类的多个方法。
例如:在定义一个指针类型变量时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, outdetectEngine, (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中的图片,添加上识别的红框,完成人脸检测的效果。
1.加载图片
加载图片比较简单,我们调用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);
}
2.检测并标记人脸
终于到正题了,很兴奋,对吧。不过还是没有思路,因为我们不知道如何来调用那个引擎。这个时候我们必须参考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, refint pitch)
{//将Bitmap锁定到系统内存中,获得BitmapData
BitmapData data = image.LockBits(newRectangle(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 =newbyte[bgr_len];
for (int i = 0; i < height; ++i)
{
Array.Copy(sourceBitArray, i * pitch, destBitArray, i * line, line);
}
pitch = line;
image.UnlockBits(data);
returndestBitArray;
}
这段代码中最难理解提我高亮的这段,要理解这段代码,就必须先来了解人脸识别引擎所使用的24bppRgb格式,首先它是一种位图存储格式,每个像素使用3字节存储,因为是Format24bppRgb格式(每像素3字节。在以下的讨论中,除非特别说明,否则Bitmap都被认为是24位RGB)的。比如我们有一个6*6大小的位图,它的每一行需要6*3=18个字节存储,但是对于BitmapData,虽然BitmapData.Width还是等于Bitmap.Width,但大概是出于显示性能的考虑,每行的实际的字节数将变成大于等于它的那个离它最近的4的整倍数,此时的实际字节数就是Stride.就此例而言,18不是4的整倍数,而比18大的离18最近的4的倍数是20,所以这个BitmapData.Stride = 20。由于我们的处理引擎是C++,我们得转化为C++的数据格式,不可能直接用BitmapData的byte数组,而是需要进行一定的转换,这个转换的目的就是把Stride的宽裕部分去掉。我们的这段readbmp代码就是为了解决这个问题的。
有关这部分的内容,可以参考微软关于BitmapData的注解。https://msdn.microsoft.com/zh-cn/library/system.drawing.imaging.bitmapdata.aspx
3.识别人脸
回到我们的这个方法,我们继续人脸识别的过程首先,我们把获取到的图像信息存起来
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。
4.标出识别到的人脸信息
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 staticBitmap 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;
}
5.点击运行。
现在你可以点击运行你的项目了,如果没有任何问题,你的将会看到下面的画面。
如果出现问题,你需要根据返回的错误码进行查抄。
引擎初始化失败
一般是APPID和APPKEY不对,你需要确保你到下载的地方申请了正确的APPID和KEY,并且注意平台是Windows平台的。初始化失败可以通过返回值进行查看,他们官网上也会有一个错误代码表。对照查表一般会解决问题。
找不到DLL
首先请保证你把DLL拷贝到对应的目录下面,其次要确定设置输出选项为拷贝到输出目录。
内存不能读或者写
这个是C++的尿性,也是C#程序员不多见的报错,主要检查相关参数是否传入正确。还要注意,如果人脸检测返回的值不为0,获取到的人脸数目会是一个比较大的随机数。这个时候如果用循环读取,就会出现地址越界的情况。
最后来一张华仔的图镇楼
后续
今天我们只是讲解了一下人脸识别的最简单的Demo,我们下一节从获取两张人脸的相似度来入手讲解如何识别不同的人的,欢迎继续关注。如果你已经了解了本博客的内容,你可以打开FR的文档,自己来进行模拟实现。相信,你会成功的。
- C#人脸识别入门篇-STEP BY STEP人脸识别--入门篇
- C#人脸识别入门篇(Step by step 人脸识别)
- C#人脸识别入门篇-STEP BY STEP人脸识别—静态照片人脸检测
- MNIST 数字识别和数据持久化--step by step 入门TensorFlow(三)
- ClearCase使用入门--step by step(管理篇)
- C#人脸识别入门篇--提取人脸特征值及人脸识别
- C# 人脸识别
- cetia4入门step by step(1)
- step by step入门LAMP配置
- Step by Step: Event handling in C#
- Microsoft Visual C# 2005 Step by Step
- c#做外挂 step by step
- 深入浅出C#2008从入门到提高Step By Step(二)C#基本概念
- 深入浅出C#2008从入门到提高Step By Step(一)VS2008的安装
- 深入浅出C#2008从入门到提高Step By Step(六)Windows应用程序实例(1)
- 深入浅出C#2008从入门到提高Step By Step(六)Windows应用程序实例(1)
- 深入浅出C#2008从入门到提高Step By Step(六)Windows应用程序实例(2)
- 深入浅出C#2008从入门到提高Step By Step(六)Windows应用程序实例(2)
- 微信 request请求解析验证 与 response 返回消息组装 工具类
- 表单验证
- 关于在开发过程中 跳转页面被拦截问题
- Piggy-Bank(hdu-1114)(完全背包)
- win10 edge浏览器去掉自动识别电话号码功能
- C#人脸识别入门篇(Step by step 人脸识别)
- 运用触发器定位不知源的weblogic
- android UiAutomator写一个给微信朋友圈所有动态点赞的用例
- NIO与I/O的区别
- java动态代理原理及解析
- Log4net使用探究
- Xlistview的依赖
- LeetCode刷题之路(一)——easy的开始
- 世界数据中心新闻摘要