65.9K
CodeProject 正在变化。 阅读更多。
Home

连接设备上的面部生物识别身份验证

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (20投票s)

2016年7月22日

CPOL

51分钟阅读

viewsIcon

50888

downloadIcon

1588

在这篇文章中,我将引导您创建一个自己的中央枢纽,让您的连接设备能够使用面部识别系统来认证人员。

引言和背景

您可能见过许多安全系统,它们允许您只需看向摄像头即可进行身份验证(或在某些地区称为授权),以进入建筑物或在环境中执行任何操作。此类生物识别方法需要能够理解输入数据的硬件和软件,以及基于内部使用的算法和逻辑如何给出输出的软件。现在,我想极大地简化事情,以帮助您开始编程自己的软件框架,让您的连接设备能够实际在网络上执行面部识别。本文的目的是澄清对面部识别的理解,并尝试指导您了解如何构建这些编程框架并托管它们,以便可以在您的设备上提供相同的功能。您当然可以在一个硬件设备或一部手机上构建系统,但如果您需要连接多个设备并在所有设备上执行相同操作呢?在这些情况下,在每个设备上添加一个简单的程序然后维护它们并不是一个好主意。因此,在本指南中,我将向您展示如何构建一个服务器。该服务器将能够处理请求,处理发送的数据并生成响应。

您不需要在任何这些编程概念和领域拥有中级背景,因为我将假设您是初学者,并在我的帖子中介绍其中大部分。但是,您需要了解 C# 编程基础以及 ASP.NET Web 开发框架的工作原理。我将使用一个基于 ASP.NET 的 Web 服务器来支持我的基于 API 的面部识别系统。随着我们继续深入探讨各个部分,您将能够牢固掌握这里提出的想法。

注意:下面所有图片(除非尺寸较小)都大于 640 像素。您可以在新标签页中打开图片以查看其完整分辨率。

目录

  1. 引言和背景
    1. 开始之前需要了解的事项
  2. 开始面部识别
    1. 计算机视觉——父领域
    2. 概念解释
    3. 用于面部识别的算法
    4. 识别如何工作?
  3. 开发项目的服务器端
    1. 为什么要使用服务器?
    2. ASP.NET Web 应用程序开发
    3. 开发人脸检测和识别的辅助库
      1. 编写库的源代码
      2. 编写 Web 应用程序的源代码
      3. 编写 LoginKeysAuthenticationController
      4. ASP.NET 的 Startup.Auth.cs
      5. 编写 FacialAuthenticationController—API 控制器
      6. 托管应用程序
  4. 构建示例 HTTP 客户端
    1. 添加应用程序的源代码
      1. 构建 HomePage
      2. 构建 LoginPage
      3. 使用应用程序—演示
  5. 最后的寄语
    1. 关于性能的几点说明
    2. 关于安全的几点说明
    3. 参考文献

开始之前需要了解的事项

今天,我想分享一些内容,让您为即将学习到的内容做好准备。在这篇文章中,我们将学习一系列框架、概念和我们将为用户(或在许多情况下为客户)创建和提供的服务。这些多个平台将合并到一个单一的服务中,然后该服务将由多个客户端使用,而与它们的硬件、软件、操作系统或其他任何层无关。这样,我们将能够为连接的设备生成结果,而无论它们是如何开发、构建以及如何工作的。

因此,我们将在此涵盖的主题是

  1. 面部识别程序。
  2. 供连接设备通信的中心或服务器。
  3. 实现协议并生成设备连接的 API 和接口。
  4. 在实际设备上托管 API(我将使用一个服务器,您可以使用运行 Windows 10 IoT 或其他操作系统的连接设备)
  5. 通过设备使用 API。

然而,我只希望各位能够了解和使用 HTTP 协议。尽管本文将在各个阶段和部分中大量使用此协议,但我希望您能自行理解它,因为我不会在这里解释。请参阅参考部分,找到一些有助于入门 HTTP 协议的有用 URL。

开始面部识别

本文的重点将放在面部识别、用于面部识别的算法以及如何实现这一点。因此,本节的主要关注点是为您提供尽可能广泛、简明且易于理解的想法,以便您能牢固掌握整个面部识别概念。那么,让我们从它的起源开始……

计算机视觉——父领域

面部识别是计算机视觉领域的一个子领域。基本的算法和理论知识都围绕着计算机科学的计算机视觉领域。这个领域是什么,

Afzaal Ahmad Zeeshan 对计算机视觉的定义

计算机视觉是计算机科学的一个领域,其目标是使计算机系统能够通过图像处理技术操纵周围环境,以查找对象、跟踪其属性,并使用多种模式和算法识别对象。

我(故意)省略了这个概念的技术定义中涵盖的大部分方面,我这样做是因为那只会让大家感到困惑。如果您真的想知道计算机视觉是什么,请查阅课程书籍、维基百科文章或任何其他研究论文中提供的定义之一。当然,这个概念要广泛得多,但这并不是我们要讨论的。相反,我们感兴趣的是识别对象的方法。计算机视觉的工作方式(或倾向于工作的方式)就像人类视觉一样。已向生物学家寻求帮助,以使计算机能够像人类一样看待和感知周围的对象。然而,计算机并非完美。因此,计算机视觉利用了多个领域的专家来共同使计算机工作得更好、更有效。


图 1:计算机视觉和人工智能的云融合,以及大量的其他技术和研究领域。

计算机视觉在许多领域都有巨大的好处和常规用途,从智能家居一直到控制谁在周围走动以及谁应该被使用该程序的人员识别为(出于任何目的!)需要的人员的复杂安全程序。

现在,既然我们谈论计算机视觉,并且程序能够检测对象,我们就需要考虑如何“识别 已检测的对象”。就像人类视觉一样,我们的眼睛负责观察场景,然后我们的大脑在场景中检测到人脸,之后再尝试识别它认识的面孔。类似地,面部识别使用算法来

  1. 在图像帧中检测人脸(图像帧可以来自多个来源,如网络摄像头、图像文件、网络或任何其他资源)
    • 此步骤需要将图像帧加载到内存中。
    • 然后,专门的算法读取图像并研究对象的模式。
    • 如果模式与已知的模式(例如人脸的模式)相似,则将该图像位置保存为已检测,
      • 前额区域
      • 顶部的两只眼睛
      • 形状中心的鼻子
      • 嘴唇
      • 下巴
  2. 将数据识别为已知(或未知)
    • 我们在该过程中执行相同的操作。
    • 但是,一旦我们检测到对象,我们就只从图像中获取有关该对象的数据。
    • 我们需要一个数据库来存储我们“认识”的对象。计算机程序使用该对象数据库来训练模型集以供以后使用。
    • 收到请求后,计算机程序将继续将检测到的对象与数据库中的对象进行匹配。
    • 如果找到匹配项,则该对象被“识别”为已知对象。否则,它被声明为未知对象,并且可以采取进一步的步骤来记住该对象或忽略它。

这两件事将是我们要学习、实现和玩转的!别担心,我相信这些概念和事物此刻可能有点令人困惑,但随着我们在文章中继续,我将以更简单、更深入的方式解释这些概念,以便您能理解为实现本文主题中所述内容所采取的这些步骤。

概念解释

本文的目的是让设备能够知道谁在使用它们。此功能的用途非常广泛,从跟踪家中人员,了解谁在哪里,到家庭环境中的其他类似用途。它也可以应用于安全目的,跟踪某人何时进入建筑物以及何时离开建筑物。然而,我们将在本文中解释的目的相对简单且有趣。

我们希望在本文中构建一个非常简单但功能强大到足以支持您的连接设备并使它们能够知道谁在使用它们的功能。我们需要做的是:

  1. 创建一个识别面部的服务
    1. 该服务将使用 OpenCV 的 .NET 包装器 Emgu CV 作为计算机视觉功能的的基础框架。
    2. 该服务将被创建为使用发送给它的数据字节,然后服务将执行构建矩阵以及在数据上执行检测和识别算法的操作。
    3. OpenCV 和其他类似框架提供了用于检测和识别对象的模板和模型。我们将在我们的程序中使用它们,以使编程更简单、更容易。
  2. 在 Web 应用程序中托管该服务
    1. 我之所以这样做,是因为在每个设备上添加和管理功能并不是一个好主意。相反,使用 Web 服务将是提供相同功能集的最佳解决方案。
    2. Web 应用程序将全天在网络上运行,设备可以连接到它。
  3. 在实际服务器上托管 Web 应用程序
    1. 现在,问题在于,我们将使用 Web 服务器来允许网络上的设备连接。因此,我们将能够允许我们自己网络中的设备连接到一个“本地服务器”。
    2. 本地服务器的意思是我们将使用服务器的本地 IP 地址。当然,您可以创建一个可以从网络外部访问的服务器。
  4. 使用该服务
    1. 我们可以使用任何能够通过 HTTP 协议通信的设备。
    2. 几乎所有基于 IoT 的设备都带有 Web 界面,可以支持 HTTP 通信,因此每个设备都能够使用此服务。数据传输将基于原生的 HTTP 协议,并且序列化和反序列化将使用 JSON 数据模式进行数据交换。

以下图片可以说明这一点,


图 2:我们项目的构想。大屏幕显示器并非必需,它只在您要创建账户时才需要。但您也可以通过手机浏览器完成。

我们大部分时间将花在本文的服务器端,因此让我们开始服务器端编程,并构建将被连接到该服务器的客户端通过 HTTP 协议使用的服务。但首先,让我们看一下将在示例中使用的算法。在接下来的部分(在开始编程之前),我们将研究一些将有助于我们进行文章下一部分的构想和概念理论。

用于面部识别的算法

面部识别有许多算法,例如 EigenFaces、FisherFaces 等。它们都非常有帮助且易于学习。大多数在线教程已经使用 EigenFaces 算法向读者教授面部识别。在这篇文章中,我将使用 LBPH 算法,即局部二值模式,这是一种强大的纹理分类算法,在许多面部识别情况(例如不同的光照条件)下都很有用。该算法的概念超出了本指南的范围,因此我建议您参考OpenCV 的教程,我发现他们的教程非常简单易懂。

此外,它们还强调了这三种算法的区别以及何时应考虑使用它们。请注意,OpenCV 包含所有三种算法的定义和引擎,您可以选择任何一种。但请记住,您不能在运行时更改其用法,您应该考虑使用哪种算法,然后为此特定算法设计和准备模型。在本文中,您将学习如何使用 LBPH 算法以及如何为此训练模型。

识别如何工作?

基本上,图像处理依赖于矩阵,检测到的对象,然后检查几何图形中的模式并与已知的模式进行匹配;例如人脸。识别是对图像执行的,以确定与特定形状、几何形状或区域匹配的对象。特殊的分类器经过训练以从图像中获取结果,Haar Cascade 就是其中一种分类器。

程序旨在提取对象的属性,例如

  1. 人眼区域的宽度;或者 whatever that is called。
  2. 鼻子的长度。
  3. 下巴的面积。
  4. 脸部几何形状,例如脸部的宽度和高度。

例如,看看下图,看看脸部的属性如何根据它们的位置和其他东西轻松区分。请注意,在此处我们只是检测人脸,而不是识别。在检测阶段,我们不关心鼻子应该有多远,眼睛应该多大等等。


图 3:我的脸,用我脸部的重要特征(大致)由面部识别软件看到的标记。

我当然自己掩盖了脸部,算法会以一种截然不同的方式检测人脸,并且这些属性的区域在我自己的情况下一定有所不同。但您明白了,人脸是如何在图像中被检测到的。

到目前为止,已经完成了检测,检测只是为了检测人脸在哪里,计算机程序不知道那个人是谁。因此,您将存储人员的姓名以及图像。创建一个人员数据库,然后记录每个面孔以及该面孔人员的姓名,这是一种更好的方法。我们使用的算法配备了确定人员是否在数据库中的代码行。这是通过将当前检测到的面部的属性与数据库中已有的属性进行匹配来完成的。如果区域匹配,则该人员被“识别”为数据库中的人员,并返回该人员的姓名。否则,该人员被声明为未知。

算法还检测面部是否 closely 匹配,或者它们之间是否有很大的距离。距离也用于确定在确定面部时应保留的阈值。阈值设置得越低,匹配就越完美,但即使是很小的角度也可能导致结果产生巨大的变化和差异。然而,较大的阈值将导致匹配,但错误率非常高。因此,您需要找到一个能最小化错误率的模型。此过程的示例可以在下图看到,


图 4:说明两张图片如何相互映射以及它们的距离如何根据面部特征在图像中的位置来测量。

人脸的旋转角度不同,因此它们在图像中的几何形状也不同。一个高度复杂的人脸识别程序将能够判断这是谁,否则由于两个人脸之间的距离,它将无法识别该人。计算机严重依赖图像或图像中的对象创建的矩阵,它没有像人类那样完美的识别系统。因此,在深入研究识别系统之前,您需要先了解这些内容。

开发人员提示

如您所见,人脸检测在很大程度上取决于光照、人脸特征、它们在图像中的清晰度、图像本身的清晰度等。请注意,我的胡须如何可能影响检测我嘴唇的准确性,请注意由于眼睛的大小和眉毛(如果算法关心它们),眼睛区域的大小不同。始终等化直方图,确保图像质量良好,确保从图像中去除噪声,这是一个好主意。这些可以帮助提高程序的准确性。

开发项目的服务器端

在前面的部分中,您已经了解了面部识别软件和程序的理论概念及其工作原理,以便您可以理解我们将要创建的内容。在本节中,我们将实际编写程序,这些程序在运行时实际执行这些操作并在服务器上生成输出。我将使用 ASP.NET Web 应用程序来实际构建服务器并运行它们。但在我继续托管部分之前,我想先开始构建过程。所以,让我们开始编程。

为什么要使用服务器?

市场上的每个 IoT 设备都内置了功能,并支持自身大量的多功能性,因此您根本不需要编写任何软件。但是,如果您要购买一个不包含此功能的设备呢?您当然可以为此编写软件,但如果您要购买另一个产品,如智能电视、智能手机或任何其他 Arduino 设备等呢?在这些情况下,为每个框架编写软件,部署软件,然后在每次出现问题时维护每个软件都不会有效。这也不是一个好主意。

在这些情况下,IoT 应该使用 Web 服务,因为所有它需要和做的就是将源代码保留在中央枢纽上,其余设备可以连接到该服务器来使用资源、共享数据并响应用户请求。我将在本文中使用这一点,您将看到它的用处。与为每个设备编写软件相比,安装服务器的一些好处是:

  1. 您只需要编写一次程序和源代码。
  2. 服务器可供所有设备访问,设备只需拥有 Web 界面即可通过 HTTP 协议进行通信。
  3. 其他设备不会造成问题,您只需安装一个最小的程序来使用 API。
  4. 如果出现任何错误,您只需调试服务器端的代码,这非常容易且耗时少。修复后,任何设备都可以使用该服务。

但是,在服务器-客户端架构中也会有一些麻烦,例如,如果服务器宕机,所有客户端也会宕机。但这在您能够解决问题时没什么意义。

ASP.NET Web 应用程序开发

最后,开始编码。我们现在需要理解我们如何开始设计我们的应用程序。我们在这里需要构建的是一个身份验证系统,允许用户使用他们的面部几何形状进行身份验证。这需要一个身份验证系统,该系统能够处理面部身份验证以及使用经典方式(用户名和密码)执行的基本身份验证。我可以使用 ASP.NET 的 Identity 进程,这确实很简单,但相反,我使用了自己两年前编写的库,LoginKeys。我想警告您,我使用这个库是为了简单起见,我写的库效率不高,并且有很多我正在努力修复的问题,并且很快就会将该库发布到 NuGet 包中。在此之前,请勿使用该库。该系统非常简单,它包含了基本身份验证系统所需的一切,并且还包含额外的字段来支持面部身份验证。

public class Account
{
    public string Email { get; set; }
    public string Password { get; set; }
    public DateTime CreateDate { get; set; }
    public int UserId { get; set; }
    public bool RequireEmailVerification { get; set; }
    public string AuthenticationToken { get; set; }
    public DateTime TokenExpires { get; set; }
    public bool IsVerified { get; set; }
    public bool IsLocked { get; set; }
    public DateTime AccountLockedSince { get; set; }
    public DateTime PasswordResetDate { get; set; }
    public string PasswordResetToken { get; set; }
    public int TotalPasswordFailure { get; set; }

    // Addition to the library
    public string FacesName { get; set; }
    public string Name { get; set; }
    public string FacialAuthenticationToken { get; set; }

    // Security consideration
    public string SecurityQuestion { get; set; }
    public string SecurityAnswerHash { get; set; } // Answers are like passwords, hash them.
}

在上面的类中,字段定义在库本身中,我必须另外添加这些字段来支持我的框架的面部身份验证。下图显示了其类图以及实际创建用户帐户、分配用户凭据和令牌等的服务。


图 5:身份验证系统对象的类图。

到目前为止,程序非常简单明了。此外,我使用 JSON 格式进行数据交换以及存储用户帐户和其他内容的数据。JSON 在日常编程中非常有用,因为它非常轻量级,并且在网络上使用时因其紧凑的尺寸非常有意义。

看看上面的类,您可以看到过程是如何管理的,以及这些内容如何协同工作以创建身份验证系统。您可以看到,帐户类中的令牌将用于客户端,而不是将 UserIds 等实际传递给客户端。这确实很有帮助,在许多情况下与 OAuth 的工作方式相似。您的应用程序将获得令牌,这些令牌将代表您执行操作。这里将执行相同的事情,您的令牌将被使用——令牌可以在应用程序中的任何地方,以任何您希望的方式使用,我不会对此争论。到目前为止,我们已经开发了身份验证系统,请注意,我们使用了一个现有的(但存在一些性能问题)库,并对其进行了移植,以满足我们的一些需求,以便我们可以根据用户的面部几何形状进行身份验证。在下一节中,我们将看到如何在 ASP.NET Web 应用程序中实际执行用户面部识别。

开发人脸检测和识别的辅助库

开始之前,您需要安装适用于 .NET 框架的 OpenCV 包装器。EmguCV 是一个免费的计算机视觉库,实际上它主要是 OpenCV 开源计算机视觉库的一个包装器。您可以免费获取该库并安装它,然后才能在您的应用程序代码示例中编写任何代码、实现任何接口和 API。所以,请访问此处下载并安装 Emgu CV,它们将指导您完成整个过程,然后您可以回到这里继续。

现在您已经安装了 OpenCV 的包装器,您可以继续处理实际执行对象(在本例中为面部)检测和识别的程序和服务的编程方面。我将假定您已创建了一个新项目。如果您尚未创建带有 ASP.NET Web 应用程序模板的新项目,请遵循说明。

在 Visual Studio 中,选择:

  1. 文件
  2. 新建 → 项目
  3. 从提供的模板中选择“ASP.NET Web 应用程序”。
  4. 输入项目的详细信息,例如项目名称、目录等。
  5. 点击完成以创建项目。

完成后,我们将在同一个页面上,现在可以继续添加我们将要在我们的项目中使用到的库和包了。

向项目中添加必需的组件

由于我们使用的是第三方库,因此我们需要能够将库添加到项目的可执行目录中。此框架的许多组件都在本地环境中执行,而这正是主要问题所在。但是,提供了托管的包装器,您可以将其用于您的目的。首先,让我们包含托管的包装器到项目中。


图 6:库所在的目录。

您需要将这两个托管库包含到您的项目中。它们很容易包含,因为它们的结构是 .NET 库,而我们要包含的另一个库是本地库,而不是托管库。

如果运行的项目,Emgu CV 会抱怨 TypeInitializerException。这是因为库知道类型存在,但不知道如何实际初始化对象。这正是 OpenCV 在包装器库后台所做的。因此,您还需要引用 OpenCV 库才能实际执行操作,因为 Emgu CV 实际上会将内容传输到 OpenCV 库。

OpenCV 包装器有两个版本,取决于您使用的体系结构。我将包含基于 x86 架构的包装器。即,


图 7cvextern.dll 需要在应用程序项目 **bin** 文件夹中可用。

请注意,我说它们不能作为托管库包含到您的项目中。


图 8:当您尝试将必需的库添加到项目时,会出现错误消息;因为它不是托管库。

要将其实际包含到您的项目中,您应该做的是将其复制并粘贴到应用程序的“bin”文件夹中。这是一个快速技巧,它将允许您在运行时实际使用该库,因为错误发生在运行时;而不是编译时。您应该注意要包含哪个版本的库,它们的名称相似,但它们所在的目录不同。


图 9:粘贴库有效。此图片显示库已复制到 **bin** 文件夹。

之后,您的库就设置好了,您可以开始编程组件并使用可用的功能。

提示:最好检查框架本身是否有效且完美地工作,没有任何错误。为此,您应该实际启动并尝试 Emgu CV 库本身提供的应用程序。它们位于目录“C:\Emgu\emgucv-windesktop 3.1.0.2282\bin”(如果您没有更改安装目录的位置)。选择其中任何一个并运行它们。这将让您知道框架是否已成功安装。

编写库的源代码

现在,我们将继续编写库本身。为此,首先,创建一个我们将用作执行功能的库类的新类。我称之为 FacialAuthenticationHelper。我将在该类中编写代码以实际执行功能,这将为我们提供一种非常简单的调用功能的方式,并允许我们在想要修改代码以供将来使用时进行编辑。

1. 人脸检测部分

首先,让我们看看检测部分做了什么,

#region Face detection
public static void Detect(Mat image, List<Rectangle> faces, out long detectionTime)
{
    Stopwatch watch;

    //Read the HaarCascade objects
    using (CascadeClassifier face = new CascadeClassifier(FacesHaarCascadeFile))
    {
        watch = Stopwatch.StartNew();
        using (UMat ugray = new UMat())
        {
            // Convert the image to grayscale. 
            CvInvoke.CvtColor(image, ugray, Emgu.CV.CvEnum.ColorConversion.Bgr2Gray);

            // Histogram equalize to clarify the image. 
            CvInvoke.EqualizeHist(ugray, ugray);

            // Detect the faces and return their areas as rectangles.
            Rectangle[] facesDetected = face.DetectMultiScale(
               ugray,
               1.1,
               10,
               new Size(20, 20));

            faces.AddRange(facesDetected);
        }
        watch.Stop();
    }

    // Just for sake of debugging.
    detectionTime = watch.ElapsedMilliseconds;
}
#endregion

这段代码,如果您能看到,它将在图像(Mat 类型)中执行人脸检测,然后将其位置存储在将返回的 faces(List<Rectangle> 类型)变量中。现在,首先,看看初始的 using 块,该块初始化一个使用 Haar Cascade 分类器来实际检测人脸的新分类器。Emgu CV 附带了许多可以帮助检测各种形状对象的分类器。


图 10:API 本身提供的多个 Haar Cascade 分类器。

我们只对 haarcascarde_frontalface_default.xml 文件感兴趣。它允许我们在图像中检测人脸,然后稍后执行我们想要的操作。在实际示例中,您将看到这些程序如何允许我们检测人脸并仅存储图像中的面部内容。

仅用于示例演示,它看起来像这样(Windows Forms 中的示例),


图 11:一个在 Windows Forms 应用程序中显示的人脸检测和从图像中提取的示例。

请注意,右上角的人脸只是在捕获的图像中检测到的人脸。这是我们稍后将在识别过程中使用的图像。那么,让我们继续下一步,编写识别过程的代码。

2. 人脸识别部分

这一部分也非常简单,简洁,但需要大量的模型生成,一些复杂的功能,如预测和检查阈值。区别在于,在检测过程中,您只需检测图像中是否存在对象,而在本过程中,您需要说明该对象是否是您正在寻找的对象!在这种情况下,您需要关心图像对象的细节,例如眼睛、嘴唇、鼻子尺寸等。幸运的是,我们有引擎,我们只需要训练模型,我们只需要执行所需的操作,而无需担心过程的底层细节。

#region Face recognition
private static FaceRecognizer recognizer { get; set; }
public static FaceRecognizerType type { get; set; }

public static void UseEigen()
{
    recognizer = new EigenFaceRecognizer();
    type = FaceRecognizerType.EigenFaces;
}

public static void UseEigen(int numComponents, double threshold)
{
    recognizer = new EigenFaceRecognizer(numComponents, threshold);
    type = FaceRecognizerType.EigenFaces;
}

public static void UseFisher(int numComponents, double threshold)
{
    recognizer = new FisherFaceRecognizer(numComponents, threshold);
    type = FaceRecognizerType.FisherFaces;
}

public static void UseLBPH(int radius, int neighbors, int grid_x, int grid_y, double threshold)
{
    recognizer = new LBPHFaceRecognizer(radius, neighbors, grid_x, grid_y, threshold);
    type = FaceRecognizerType.EigenFaces;
}

public static void Load()
{
    recognizer.Load(TrainedEngineFile);
}

// Train
public static void Train(List<Image<Gray, byte>> images, List<int> labels)
{
    recognizer.Train(images.ToArray(), labels.ToArray());
}

// Recognize the person
public static int Recognize(Image<Gray, byte> image)
{
    int label = -1;

    var result = recognizer.Predict(image);
    label = result.Label;

    return label;
}
#endregion

这是执行识别的代码,请注意,我还添加了另外两种方法,但我不会使用那些算法。我想考虑的功能是:

  1. Load (加载)
    • Load 函数实际上从文件中加载已训练好的模型。它使用该训练模型准备识别引擎。
  2. Train (训练)
    • 此函数使用模型训练引擎。它基于所使用的算法训练识别引擎。内部实现已被抽象化,但当您实际执行它们时可以看到差异。
    • 请注意,我们将使用 LBPH 算法,因此我们需要至少 2 个人才能让 LBPH 算法工作。否则,它将生成类似“LBPH 期望 2 个类”的错误。其中类是两个人。
  3. Recognize (识别)
    • 此函数执行面部预测,它会预测带有标签的面部。然后可以使用该标签从数据库中查找人员。我们已经有一个系统,其中保存了我们的 UserIds 和人员姓名。我们现在只需要系统将图像保存在一个目录中,集中起来。您很快就会学到这一点。

这些函数将执行识别。然后我们可以管理我们想要的引擎在预测姓名时放松的阈值。

3. 杂项

就像辅助类需要一样,也添加了以下属性和枚举列表。

#region Files and other settings
public static string FacesHaarCascadeFile = HttpContext.Current.Server.MapPath("~/App_Data/haarcascade_frontalface_default.xml");
public static string TrainedEngineFile = HttpContext.Current.Server.MapPath("~/LoginKeys_Data/recognitionEngineTrainedData.yml");
#endregion

#region Enum
public enum FaceRecognizerType
{
    EigenFaces, FisherFaces, LBPH
}
#endregion

到目前为止,我们已经成功创建了项目的库部分。在下一节中,我们将继续添加消耗该库的源代码以及如何在 ASP.NET Web 应用程序中编写 API。

编写 Web 应用程序的源代码

尽管该项目是为互联网上的物联网连接设备而构建的,但为了简单起见,我创建了一个用于添加图片和创建帐户的在线模块。它使用简单的 HTML 文档和一些编程来创建帐户,以实际存储他们的数据。我们要做的就是

  1. 在创建帐户时,还要创建一个新目录来存储该人的面部位图。
  2. 这样,我们就可以区分系统中周围人员的图像。
  3. 我们还希望他们的令牌和其他面向识别的属性和字段自动生成。大部分部分是使用 Guid.NewGuid() 函数生成的,该函数会生成一个唯一的字符串。我们可以稍后为用户创建额外的字符串作为令牌。
  4. 请注意,我们还希望允许用户摆脱需要身份验证的用户名和密码。我们希望他们能够通过仅使用面部指标进行身份验证来实际执行操作。

内容将如下。

对帐户创建的修改

以下代码将为我们的用户创建帐户,请注意,我们只填写了帐户所需的字段。我们将大部分内容留给用户以后更新。

// Create a new user and append it to the list.
Account account = new Account() { Name = name, Email = email, Password = Crypto.SHA256(password) };
account.FacesName = name.ToLower().Replace(" ", "");
account.FacialAuthenticationToken = Guid.NewGuid().ToString().Replace("-", ""); // Unique token

account.UserId = Account.GetID();
accounts.Add(account);
                            
// Create the folder too.
Directory.CreateDirectory(System.Web.HttpContext.Current.Server.MapPath($"~/LoginKeys_Data/FaceBitmaps/{account.FacesName}"));

string accountList = JsonConvert.SerializeObject(accounts);
File.WriteAllText(AccountFile, accountList);

其余由 LoginKeys 库和 JSON 库 Newtonsoft.Json 处理。我不会深入研究,您可以自己下载包进行尝试。只有这一部分内容得到了更新,现在主要的代码编写在实际执行帐户管理的控制器中。

编写 LoginKeysAuthenticationController

此控制器控制您的帐户如何创建、在何处创建以及内容如何更新。在本节中,我们将学习并研究网络上面部识别的许多“重要”方面。例如

  1. 显示帐户详细信息;令牌、电子邮件等。
  2. 允许用户执行面部检测和面部指标存储。您将看到我们将如何使用我们刚刚创建的库 FacialAuthenticationHelper 来存储面部。
  3. 在上传面部时自动触发引擎的训练。

此控制器仅用于项目的 Web 浏览器端,而在其他情况下,您将使用 API 控制器通过 HTTP 协议进行通信。您会发现构建之前的库和控件是最好的选择,因为它们在后续阶段将极大地帮助我们。

我将从向您展示控制器操作及其视图的代码开始。它们非常容易理解,我非常有信心您能很快理解。它们只是表单提交等,这里没有什么高深的学问。

1. 注册操作

服务器上运行的源代码如下:

public ActionResult Register()
{
    if (System.Web.HttpContext.Current.Request.HttpMethod == "POST")
    {
        // Get the details
        var requestHandle = System.Web.HttpContext.Current.Request;
        var email = requestHandle["email"];
        var name = requestHandle["name"];
        var password = requestHandle["password"];

        // Create the account
        if (LoginService.CreateAccount(name, email, password))
        {
            Response.Redirect("~/?message=account-created");
        }
        else
        {
            ViewBag.ErrorMessage = "Error creating the account.";
        }
    }
    return View();
}

HTML 内容如下:

@{
    ViewBag.Title = "Register";
}

<h2>Register</h2>

@if(ViewBag.ErrorMessage != null)
{
    <h4 style="color: #e06161">Error</h4>
    <p>@ViewBag.ErrorMessage</p>
}

<script>
    function checkPasswords() {
        // Script to handle the Password combination
        var password = document.getElementById("password");
        var cpassword = document.getElementById("confirmpassword");
        var landing = document.getElementById("landing");

        // Check their properties
        if (password.value != cpassword.value) {
            document.getElementById("lblControl").style.color = "#f00";
            return; // Just terminate
        } else {
            document.getElementById("lblControl").style.color = "#0f0";
        }
        return true;
    }
</script>

<p>Use <code>LoginKeys</code> API to create a new account. Enter the details to register.</p>
<p id="landing"></p>
<form method="post">
    <label for="name">Name</label>
    <input type="text" class="form-control" id="name" name="name" /><br />
    <label for="email">Email</label>
    <input type="text" class="form-control" id="email" name="email" /><br />
    <label for="password">Password</label>
    <input type="password" class="form-control" id="password" name="password" /><br />
    <label for="confirmpassword" id="lblControl">Confirm Password</label>
    <input type="password" class="form-control" onkeyup="checkPasswords()" id="confirmpassword" name="cpassword" /><br />
    <input type="submit" class="form-control" onsubmit="checkPasswords()" value="Register" />
</form>

<p>Already have an account? <a href="~/LoginKeysAuthentication/Login">Login in here</a>.</p>

如您所见,这里没有什么非常严肃的事情。它只是一个普通的 HTML 表单,会被提交。大部分处理由 LoginKeys 库本身完成。

2. 登录操作

服务器代码如下:

public ActionResult Login()
{
    if (System.Web.HttpContext.Current.Request.HttpMethod == "POST")
    {
        // Get the details
        var requestHandle = System.Web.HttpContext.Current.Request;
        var email = requestHandle["email"];
        var password = requestHandle["password"];
        bool rememberMe = requestHandle["rememberMe"] == "false" ? false : true;

        // Create the account
        if (LoginService.Login(email, password, rememberMe))
        {
            Response.Redirect("~/");
        }
        else
        {
            ViewBag.ErrorMessage = "Error creating the account.";
        }
    }
    return View();
}

视图的 HTML 内容如下:

@{
    ViewBag.Title = "Login";
}

<h2>Login</h2>

@if (ViewBag.ErrorMessage != null)
{
    <h4 style="color: #e06161">Error</h4>
    <p>@ViewBag.ErrorMessage</p>
}

<p>Use <code>LoginKeys</code> API to login to your account.</p>
<form method="post">
    <label for="email">Email</label>
    <input type="text" class="form-control" id="email" name="email" /><br />
    <label for="password">Password</label>
    <input type="password" class="form-control" id="password" name="password" /><br />
    <label for="rememberMe">Remember me</label>
    <input type="checkbox" id="rememberMe" name="rememberMe" /><br />
    <input type="submit" class="form-control" value="Login" />
</form>

<p>Don't have an account? <a href="~/LoginKeysAuthentication/Register">Register here</a>.</p>
3. 显示个人资料

在继续向用户个人资料添加面部信息之前,我想展示一下我用来实际显示当前活动帐户个人资料的视图。

@using LoginKeys;

@{
    ViewBag.Title = "Profile";

    var account = LoginService.GetAllAccounts().Where(x => x.UserId == LoginService.ActiveID()).FirstOrDefault();
}

<h2>Profile</h2>

<p>This is a web page where you get to see your profile settings. Editing portal has not been implemented yet.</p>
<ul style="list-style: none;">
    <li>Name: @account.Name</li>
    <li>Email: @account.Email</li>
    <li>FaceName (folder): @account.FacesName</li>
    <li>Facial Authentication Token: @account.FacialAuthenticationToken</li>
</ul>
<p>To enable facial authentication, go <a href="~/LoginKeysAuthentication/AddFaces">here</a>.</p>

这将生成并显示个人资料。如建议的,它显示了文件夹的名称,还显示了用户用于身份验证的令牌。

4. 向系统中添加面部

最后,控制器最重要的部分是实际将面部添加到系统的功能。这需要由服务检测面部,然后将其作为位图保存到用户目录。工作方式类似于数据库,但我不喜欢将图像以字节格式保存在数据库本身中。相反,我建议将图像(和其他多媒体文件)保存在文件系统中,只将它们的名称(如果需要)保存在数据库中。

代码有点长,由于我在此处执行多项任务,例如添加安全问题、上传文件或仅显示视图(在 HTTP GET 的情况下),代码需要很长。

public ActionResult AddFaces()
{
    if (System.Web.HttpContext.Current.Request.HttpMethod == "POST")
    {
        if (Request.Form["question"] != null)
        {
            var question = Request.Form["question"];
            var answer = Request.Form["answer"];

            // Save the question and answer
            var accounts = LoginService.GetAllAccounts().Where(x => x.UserId == LoginService.ActiveID()).ToList();
            var account = accounts.FirstOrDefault();
            account.SecurityQuestion = question;
            account.SecurityAnswerHash = Crypto.SHA256(answer); // Secure the answer too.

            // Get the entire list.
            accounts = LoginService.GetAllAccounts();
            accounts.RemoveAt(accounts.IndexOf(account));
            accounts.Add(account);
            LoginService.SaveAccounts(accounts);
        }

        // Get the image and save it.
        var files = Request.Files;
        if (files.Count > 0)
        {
            // Files uploaded
            for (int i = 0; i < files.Count; i++)
            {
                // Get the file
                var file = files[i];

                // Get the facesname
                var facesname = LoginService.GetAllAccounts().Where(x => x.UserId == LoginService.ActiveID()).FirstOrDefault().FacesName;
                var bitmapName = FacesDbHelper.AddBitmap(facesname, file);

                try
                {
                    // Detect the face; Start the process
                    Bitmap map = (Bitmap)Image.FromFile(bitmapName);
                    List<Rectangle> faces = new List<Rectangle>();

                    Image<Bgr, byte> image = new Image<Bgr, byte>(map);
                    map.Dispose(); // Clear the object
                    map = null;

                    Mat matrix = image.Mat; // Get the matrix
                    long detectionTime = -1; // If required.

                    // Process
                    FacialAuthenticationHelper.Detect(matrix, faces, out detectionTime);

                    if (faces.Count == 0)
                    {
                        ViewBag.ErrorMessage = "No faces found.";
                        System.IO.File.Delete(bitmapName);
                        return View();
                    }

                    if (faces.Count > 1)
                    {
                        ViewBag.ErrorMessage = "Multiple faces were detected.";
                        System.IO.File.Delete(bitmapName);
                        return View();
                    }

                    var face = faces[0]; // Get the face
                    var faceImage = image.Copy(face).Convert<Gray, byte>().Resize(100, 100, Emgu.CV.CvEnum.Inter.Cubic);

                    // Save the face bitmap only now.
                    new Bitmap(faceImage.Bitmap, 100, 100).Save(bitmapName);
                    // faceImage.Bitmap.Save(bitmapName);
                    ViewBag.Success = "Face added.";

                    var imageName = bitmapName.Split('\\')[bitmapName.Split('\\').Length - 1];
                    var imageUrl = $"LoginKeys_Data/FaceBitmaps/{facesname}/{imageName}";
                    ViewBag.BitmapLink = imageUrl;
                }
                catch (Exception e)
                {
                    ViewBag.ErrorMessageCV = e.InnerException.Message;
                }
            }
        }
        else
        {
            ViewBag.ErrorMessage = "No files were uploaded.";
        }
    }
    return View();
}

我只讨论人脸检测,因为安全部分稍后将讨论问题和答案部分。在此阶段,您可以看到应用程序将检查上传到服务器的文件以及图像中是否存在人脸。请注意,该条件已检查:

  1. 如果图像中没有人脸。则无需保存任何内容。
  2. 如果有多个面孔,哪个属于用户?
  3. 如果检测到一个度量作为面部,则将其保存到目录中。这将有助于我们稍后用面部训练引擎。

在其他情况下,我们也会进行一些清理。看起来是这样的,并且可以看到系统还显示上传到系统的一些额外图像。


图 12:个人资料页面。


图 13:Web 应用程序上的面部身份验证页面。

它将使用我的这两张照片(以及网络上其他用户提供的照片)来训练模型。您可以看到底部的“训练”按钮,它会触发训练引擎的请求。

// Train is an action, which returns a string instead of ActionResult
public string Train()
{
    return _Train();
}

// Helper function. 
public static string _Train()
{
    List<Image<Gray, byte>> images = new List<Image<Gray, byte>>();
    List<int> ids = new List<int>();

    FacesDbHelper.GetBitmaps("*").ForEach((bitmapFile) =>
    {
        string facesname = bitmapFile.Split('\\')[bitmapFile.Split('\\').Length - 2];
        int id = LoginService.GetAllAccounts().Where(x => x.FacesName == facesname).FirstOrDefault().UserId;
        Image<Gray, byte> image = new Image<Gray, byte>(new Bitmap(bitmapFile));

        images.Add(image);
        ids.Add(id); // Bitmap must use the ID of the person.
    });

    try
    {
        FacialAuthenticationHelper.UseLBPH(1, 8, 8, 8, 100);
        FacialAuthenticationHelper.Train(images, ids); // It will also load the file.
        return "true";
    }
    catch { return "false"; }
}

我们使用 Ajax 请求触发此操作,

<script>
    $(document).ready(function () {
        $('#refresh').click(function () {
            event.preventDefault();

            // Send the request.
            $.ajax({
                url: '/loginkeysauthentication/train',
                success: function (data) {
                    data = "" + data;
                    if (data.toLowerCase() == 'true') {
                        $('#trainResult').text('Engine has been trained with your latest images.');
                    } else {
                        $('#trainResult').text('There was problem training the engine.');
                    }
                }
            });
        });
    });
</script>

如果出现任何错误,这将导致错误,否则它将告知您一切顺利。

这就是项目的服务器端编程。到此为止,我们已经设置了允许用户创建帐户并为其个人资料设置目录的控制器。系统还维护许多内容,例如处理来自客户端的触发多个操作的请求;例如此训练操作。还有其他需要检查的事项,我已将所有这些添加到 ASP.NET Web 应用程序的 startup 类中。

ASP.NET 的 Startup.Auth.cs

由于我们的应用程序非常复杂,需要随时可用。我们希望确保一切都在位,如果不在,则应创建并放置在相应位置。我编写了执行内容检查并创建资源(如果不存在)的代码。然后我在这个 Startup 类中触发了它们。

// Start our service too.
LoginService.Start();
FacesDbHelper.Setup();
LoginKeysAuthenticationController._Train(); // Train the model

// LoginService.Start function
public static void Start()
{
    // Take care of the directories and files and their content.
    Directory.CreateDirectory(HttpContext.Current.Server.MapPath("~/App_Data"));
    Directory.CreateDirectory(HttpContext.Current.Server.MapPath("~/App_Data/LoginService"));

    if (!File.Exists(AccountFile))
    {
        File.Create(AccountFile).Close();
        File.WriteAllText(AccountFile, "[]");
    }
    if (!File.Exists(GroupsFile))
    {
        File.Create(GroupsFile).Close();
        File.WriteAllText(GroupsFile, "[]");
    }
    if (!File.Exists(UserInGroupsFile))
    {
        File.Create(UserInGroupsFile).Close();
        File.WriteAllText(UserInGroupsFile, "[]");
    }

    // Read the file
    var accounts = JsonConvert.DeserializeObject<List<Account>>(File.ReadAllText(AccountFile));
}

// FacesDbHelper.Setup function
// A few settings
public static void Setup()
{
    // Create the main directories.
    if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); }

    // Create the folders for the users too
    var accounts = LoginService.GetAllAccounts();
    foreach (var account in accounts)
    {
        if (!Directory.Exists(directory + "/" + account.FacesName)) { Directory.CreateDirectory(directory + "/" + account.FacesName); }
    }
}

// You have seen the final function above.

此时,我们的系统已设置完毕,我们现在可以继续进行创建 API 控制器并随后通过我们的设备使用该服务的下一阶段了。

编写 FacialAuthenticationController—API 控制器

现在我们的服务已启动并运行,我们只需要将其公开给 HTTP API 控制器接口,以便我们的连接设备可以自己使用该服务,而无需仅使用 Web 浏览器。

ASP.NET 中的 Web API 对想要支持跨平台但为使用该服务的客户端提供原生体验的开发人员非常有帮助。尽管我将在本地网络上使用该 API,因为我为此目的设置的服务器是一个本地服务器,但这绝对可以在更高级别的应用中使用,例如可以跨全球访问的企业 Web 应用程序服务器。ASP.NET 支持这一点,您的服务将在互联网上的任何地方被使用,所有客户端将使用原生的 UI 和 UX 体验,而不是遵循旧的 HTML、CSS 和 JavaScript 方式来为网络应用程序编程客户端。

识别过程

默认情况下,控制器进行的初始请求是 HTTP POST 请求,并在其中附带图像。需要注意的一点是,HTML 会自行完成所有这些操作,浏览器也会自行完成所有工作。然而,在 API 的情况下,我们没有那么多的简洁性。这里的概念有点复杂。

POST 请求处理程序如下:

public string Post()
{
    int id = -1;
    Bitmap map = null;

    string root = HttpContext.Current.Server.MapPath("~/App_Data");
    var provider = new MultipartFormDataStreamProvider(root);

    // Read the form data.
    Request.Content.ReadAsMultipartAsync(provider);
    using (var memStream = new MemoryStream(provider.Contents[0].ReadAsByteArrayAsync().Result))
    {
        map = new Bitmap(memStream);
    }

    try
    {
        List<Rectangle> faces = new List<Rectangle>();

        Image<Bgr, byte> image = new Image<Bgr, byte>(map);
        map.Dispose(); // Clear the object
        map = null;

        Mat matrix = image.Mat; // Get the matrix
        long detectionTime = -1; // If required.

        // Process
        FacialAuthenticationHelper.Detect(matrix, faces, out detectionTime);

        // We do not need multiple faces or no faces.
        if (faces.Count == 0 || faces.Count > 1)
        {
            return "-2";
        }

        var face = faces[0]; // Get the face
        var faceImage = image.Copy(face).Convert<Gray, byte>().Resize(100, 100, 
                                                                      Emgu.CV.CvEnum.Inter.Cubic);

        // Recognize
        id = FacialAuthenticationHelper.Recognize(faceImage); // Pass the image
    }
    catch { }

    Account account = null;
    try
    {
        account = LoginService.GetAllAccounts().Where(x => x.UserId == id).FirstOrDefault();
    }
    catch { }

    if (account == null) { return "null"; }
    var tempAccount = new Account 
                     {
                         UserId = account.UserId, 
                         Name = account.Name, 
                         SecurityQuestion = account.SecurityQuestion 
                     };
    return JsonConvert.SerializeObject(tempAccount);
}

您将使用一个客户端来消费此内容,该客户端将通过 HttpClient 对象(或其他任何可以发送 HTTP 请求的 WebClient 或其他类似对象)发送请求。我们将看到如何做到这一点,但首先让我们理解这里正在进行的流程。上面的代码从请求中获取图像,然后对其执行相同的函数以检测面部并识别它们。看看这里是如何使用辅助功能的,FacialAuthenticationHelper.Recognize(faceImage); 然后结果将返回 null 或帐户详细信息。

然后,我们将稍后使用该用户帐户详细信息来获取正在使用的帐户的问题和答案。设备本身是否要进一步验证用户取决于设备本身。我将在这里展示这一点,但在下一节安全部分,我将演示为什么需要这样做。

以下代码返回多个属性,因为客户端设备需要它们,这使得客户端可以根据需要请求更多数据。

[System.Web.Http.Route("api/facialauthentication/{id}/{property}")]
public string Get(int id, string property)
{
    var accounts = LoginService.GetAllAccounts().Where(x => x.UserId == id);
    if (accounts == null) { return "null"; }

    var account = accounts.FirstOrDefault();
    switch (property.ToLower())
    {
        case "name":
            return account.Name;
        case "id":
            return account.UserId.ToString();
        case "question":
            return account.SecurityQuestion;
        case "answer":
            return account.SecurityAnswerHash;
        case "token":
            return account.FacialAuthenticationToken;
        default:
            return "UnknownProperty";
    }
}

我们最初会请求当前用户的问题,这将根据设置的 ID 返回。它还控制用户是否设置了问题,如果有问题,则返回问题本身,否则返回 null 字符串;包含 null 的字符串

最后,要从服务器获取将在整个系统用于身份验证的令牌,我们可以使用 API 中的以下路由操作:

[System.Web.Http.Route("api/facialauthentication/{question}/{id}/{answer}")]
public string Get(string question, int id, string answer)
{
    // Perform the same check as in the case for a password. 
    if (Crypto.SHA256(answer) == Get(id, "answer"))
    {
        // Return the authentication token
        return Get(id, "token");
    }
    return "error";
}

此操作将简单地返回用户的令牌(参见上面的 HTTP GET 处理程序),否则将返回错误。所有这些都可以在客户端程序的一个页面上实现。在客户端,您可以在一个页面上执行所有这些检查,并且客户端将在 15 秒内获得身份验证并运行,前提是它能及时提供数据。

托管应用程序

我不会花太多时间解释如何托管应用程序,我已经写了一篇关于在 IIS 服务器上托管 ASP.NET Web 应用程序基础知识的文章。您应该阅读我那篇涵盖了整个步骤基础知识的文章,其中还解释了如何实际托管您自己的应用程序。我在这里也这样做,事实上,这个应用程序将托管在同一 IP 地址上,该 IP 地址在之前的文章中使用过。更多请阅读:使用 IIS 和 ASP.NET 设置家庭服务器的简单指南。现在继续本文的主要部分。到此为止,您的服务器端编程已完成,您的 API 已准备好被使用,所以让我们开始构建可以使用这些资源的客户端。

构建示例 HTTP 客户端

我还要借此机会解释如何构建 HTTP 客户端以在您自己的客户端中实际执行 HTTP 通信功能。在 .NET 客户端中,HttpClient 代码可以以类似的方式工作,并且可以进行代码重用。在其他环境中,您只需搜索允许基于 HTTP 的通信的对象。

我将使用一个基于 Windows 10 的应用程序作为此应用程序的客户端,也可以使用其他应用程序、设备、框架,但为了简单起见,我将使用一个 UWP 应用程序。我将创建两个页面,一个是主页,在这里加载内容,另一个是登录页,用户可以在此登录,通过摄像头捕捉他们的图像。这将演示过程的简单、容易和直接。

首先,创建一个新的 Windows 10(通用应用程序),您可以选择空白应用程序作为模板,因为我们将从头开始编写应用程序中的所有内容。按照相同的选项,在文件菜单下创建一个新项目。

添加应用程序的源代码

最初,提供了 MainPage,但我们要更改它。我们要在这里更改的是,我们希望其他页面与此 MainPage 动态通信,并更改应用程序的内容。MainPage 的内容已更新为以下内容,以允许用户导航应用程序的功能。

<SplitView Name="myMenu" DisplayMode="CompactOverlay"  IsPaneOpen="False" 
               CompactPaneLength="50" OpenPaneLength="150">
    <SplitView.Pane>
        <StackPanel>
            <Button x:Name="HamburgerButton" FontFamily="Segoe MDL2 Assets" Content="&#xE700;"
                    Width="50" Height="50" Background="Transparent" Click="HamburgerButton_Click"/>
            <StackPanel Orientation="Horizontal">
                <Button x:Name="homeBtn" FontFamily="Segoe MDL2 Assets" Content="&#xE80F;"
                   Width="50" Height="50" Background="Transparent" Click="homeBtn_Click" />
                <TextBlock Text="Home" FontSize="18" VerticalAlignment="Center" />
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <Button x:Name="loginBtn" FontFamily="Segoe MDL2 Assets" Content="&#xE722;"
                       Width="50" Height="50" Background="Transparent" Click="loginBtn_Click" />
                <TextBlock Text="Login" FontSize="18" VerticalAlignment="Center" />
            </StackPanel>
        </StackPanel>
    </SplitView.Pane>
    <SplitView.Content>
        <Frame Name="mainFrame" />
    </SplitView.Content>
</SplitView>

这将允许我们让用户在菜单中导航,并实际选择应用程序的不同区域。后端执行的 C# 程序的源代码如下:

public sealed partial class MainPage : Page
{
    public static string resultFromAPI { get; set; }
    public static Frame rootFrame { get; set; }

    public MainPage()
    {
        this.InitializeComponent();
        rootFrame = mainFrame;
    }

    private void HamburgerButton_Click(object sender, RoutedEventArgs e)
    {
        myMenu.IsPaneOpen = !myMenu.IsPaneOpen;
    }

    private void loginBtn_Click(object sender, RoutedEventArgs e)
    {
        // Add the code to load the page for login.
        mainFrame.Navigate(typeof(LoginPage));
    }

    private void homeBtn_Click(object sender, RoutedEventArgs e)
    {
        // Add the code to load the page for main action, home page.
        mainFrame.Navigate(typeof(HomePage));
    }

    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
        mainFrame.Navigate(typeof(HomePage));
        base.OnNavigatedTo(e);
    }
}

1. 构建 HomePage

以下示例演示了如何为应用程序创建主页。以后可以从代码本身更新。

<StackPanel>
    <TextBlock Text="Home Page" FontSize="40" HorizontalAlignment="Center" />
    <StackPanel>
        <TextBlock Text="This application is a demo application for the facial authentication project that I have created. In this applicatin, I will demonstrate how your applications will be able to consume the ASP.NET's Web API based facial authentication service." TextWrapping="Wrap" />
        <TextBlock Text="To get started, just select the login menu from the split view and get started." />
        <TextBlock>
                    <Run FontWeight="Bold">Note</Run>: The service is subject to the API being consumed. If the API is not active, or not available, then the application cannot be used.
        </TextBlock>
    </StackPanel>
    <StackPanel Name="askQuestionPanel" Visibility="Collapsed">
        <TextBlock Name="question" />
        <TextBox Name="answer" Margin="0, 15" />
        <Button HorizontalAlignment="Center" Name="authorizeBtn" Click="authorizeBtn_Click">Authorize</Button>
    </StackPanel>
    <StackPanel Visibility="Collapsed" Name="loggedInPanel">
        <TextBlock Name="loginStatus" />
        <TextBlock Name="loginId" />
        <TextBlock Name="loginName" />
        <TextBlock Name="loginToken" />
    </StackPanel>
</StackPanel>

处理此处内容的程序是:

public sealed partial class HomePage : Page
{
    // Properties
    private static bool authorized { get; set; }
    private static string errorMessage { get; set; }

    // User properties
    private int Id { get; set; }
    private new string Name { get; set; }
    private string Question { get; set; }
    private static string Token { get; set; }

    public HomePage()
    {
        this.InitializeComponent();
    }

    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
        base.OnNavigatedTo(e);

        // See whether to load the page, or whether to show something else. 
        string result = MainPage.resultFromAPI;
        if (result == null)
        {
            return;
        }
        else if (result == "-2")
        {
            errorMessage = "Image was not correct.";
        }
        else if (result == "null")
        {
            errorMessage = "No face was detected.";
        }
        else
        {
            // Get the ID and Name.
            dynamic obj = JsonConvert.DeserializeObject(result);
            string _id = obj.UserId;
            Id = Convert.ToInt32(_id);
            Name = obj.Name;
            Question = obj.SecurityQuestion;

            // Set the token to request_question and fetch the token later by passing answer.
            Token = "request_question";
        }

        // If the token is null, try to get it.
        if (Token == "request_question")
        {
            askQuestionPanel.Visibility = Visibility.Visible;
            question.Text = $"You are required to answer a security question to get authorized as the identified person, What is the answer for &quot;{Question}&quot;?";
        }
        else
        {
            if (authorized)
            {
                // User is authorized
                loggedInPanel.Visibility = Visibility.Visible;
                loginId.Text = Id.ToString();
                loginName.Text = Name;
                loginStatus.Text = "Logged in using Facial Authorization";
                loginToken.Text = Token;
            }
        }
    }

    async private void authorizeBtn_Click(object sender, RoutedEventArgs e)
    {
        using (var client = new HttpClient())
        {
            client.BaseAddress = new Uri("https://:52138");
            var result = await client.GetAsync($"/api/facialauthentication/question/{Id}/{answer.Text}");
            var content = JsonConvert.DeserializeObject<string>(await result.Content.ReadAsStringAsync());

            if (content == "error")
            {
                // Set the result to null
                MainPage.resultFromAPI = null;

                await new MessageDialog("Incorrect answer provided.").ShowAsync();
            }
            else
            {
                Token = content;
                MainPage.rootFrame.Navigate(this.GetType());
                authorized = true;
            }
        }
    }
}

我还会给您一点解释,因为我将继续实际使用该应用程序进行演示。到目前为止,请仅将其视为这样。但正如您在代码中所见,我实际上在这里使用了一些安全结构,为用户提供 UI,以显示他们是否已通过身份验证、未通过或任何其他结果。

2. 构建 LoginPage

此项目的第二部分是创建登录页面,用户可以在其中对着他们的摄像头微笑,并通过访问令牌来验证自己,该令牌以后可以在系统中用于自己的用途。现在,让我们看看这个页面的代码:

<StackPanel>
    <TextBlock Text="Facial authentication API client" HorizontalAlignment="Center" FontSize="35" />
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="To get started, press &quot;start&quot;." Margin="0, 5, 0, 0" />
        <Button Content="Start" Margin="10, 0, 0, 0" Click="Button_Click"/>
    </StackPanel>
    <CaptureElement Name="mediaElement" Height="500" Width="700" FlowDirection="RightToLeft" />
    <Button Name="authenticate" Click="authenticate_Click" Content="Authenticate" Visibility="Collapsed" />
</StackPanel>

此页面的 C# 代码如下:

public sealed partial class LoginPage : Page
{
    private MediaCapture camera { get; set; }

    public LoginPage()
    {
        this.InitializeComponent();
    }

    private async void Button_Click(object sender, RoutedEventArgs e)
    {
        camera = new MediaCapture();
        await camera.InitializeAsync();
        mediaElement.Source = camera;

        await camera.StartPreviewAsync(); // Get the preview of the user

        // Show the button, too.
        authenticate.Visibility = Visibility.Visible;
    }

    private async void authenticate_Click(object sender, RoutedEventArgs e)
    {
        // Send a request to authenticate the user.
        using (var client = new HttpClient())
        {
            client.BaseAddress = new Uri("http://192.168.1.10");

            // Create and publish the content
            using (var content = new MultipartFormDataContent())
            {
                // Add content
                ImageEncodingProperties imgFormat = ImageEncodingProperties.CreateJpeg();

                // create storage file in local app storage
                StorageFile file = await ApplicationData.Current.LocalFolder.CreateFileAsync(
                                                    "CapturedPhoto.jpg",
                                                    CreationCollisionOption.GenerateUniqueName);

                // take photo
                await camera.CapturePhotoToStorageFileAsync(imgFormat, file);

                byte[] bytes = null;

                // It may block the thread and cause WinRT to throw an exception, 
                // So run it on a separate thread using Task.Run.
                await Task.Run(async () =>
                {
                    bytes = File.ReadAllBytes(file.Path); // Read the bytes
                });

                // imagePreview is a <Image> object defined in XAML
                content.Add(
                        new ByteArrayContent(bytes), "imageFile"
                    );

                // Delete the file now,
                await file.DeleteAsync();

                // Get the result now
                string result = null;
                try
                {
                    var postResult = await client.PostAsync("/api/facialauthentication", content);
                    var responseString = postResult.Content.ReadAsStringAsync().Result;
                    result = JsonConvert.DeserializeObject<string>(responseString);

                    MainPage.resultFromAPI = result;
                }
                catch (Exception error)
                {
                    // Show the error mesage.
                    await new MessageDialog(error.Message + "\n\n" + result, "Cannot connect").ShowAsync();
                }

                // Navigate back
                MainPage.rootFrame.Navigate(typeof(HomePage));
            }
        }
    }
}

此页面实际上会点击前面人的图像。然后将图像通过网络发送到服务器进行处理,并生成结果,以验证用户或向用户发送错误消息。我认为已经完成了足够的编码,现在是时候继续实际使用应用程序了。

使用应用程序—演示

我终于完成了本指南的编程部分,现在让我们看看这个东西在实际应用程序环境中是如何工作的。当然,如果您愿意,可以跳过本节,因为它只是对本文已分享内容的演示。因此,让我们看看它是如何工作的,为了不浪费您宝贵的时间,我将使本节主题非常简短,只进行视觉演示。其他内容将在后面的部分中涵盖,届时我将为您提供一些关于以后在生产应用程序中使用它的技巧。

注意:此时您必须确保 API 处于活动状态并正在运行。我使用了 192.168.1.10 连接到 API,您可以(也应该!)将 IP 地址更改为您的服务所在的位置,可以是 localhost 地址。否则,应用程序将不显示任何内容,在许多情况下会意外终止。

应用程序的初始页面看起来像这样,请注意我们已经显示了主页(应用程序的未授权状态)。


图 14:客户端应用程序的主页。

您当然可以更改主页,显示一些未经授权的内容、公共新闻等,但我们希望保持简单明了,并主要展示与我们主题相关的内容。因此,我创建了一个主页,应用程序在此指导用户继续使用登录页。

我们需要提供我们的图像,我们可以从网络摄像头获取图像,而在另一页上,我做同样的事情,我访问摄像头,拍照并将其发送到服务器进行身份验证。以下是服务器所做的工作以及整个面部身份验证(或授权)过程的样子:


图 15:应用程序中使用的摄像头服务,用于拍摄样本照片上传到服务器以执行面部识别。

设备摄像头将用于将图像发布到服务器,将从保存的文件捕获图像字节,然后将它们发送到服务器。设备本身不必知道服务器在做什么以及它在哪里,它拥有服务器的 IP 地址,只会将内容上传到那里。然后服务器将响应,并指导应用程序动态适应应用程序的标准。


图 16:返回一个问题以供回答,作为安全措施。

应用程序将要求用户回答安全问题,安全问题是一种安全层,因为您知道,总有人试图访问他们不应访问的内容。


图 17:正在回答问题。

安全问题的答案是明文,您应该将其转换为密码,我想展示的是字符串将以明文 Unicode 形式传输,而不是在此处进行哈希处理。服务器将为我们进行哈希处理,

Crypto.SHA256(answer);

这样,我们只需上传内容,而无需担心使用的哈希方法和其他类似内容。


图 18:图像显示了在将正确答案发布到服务器后 HTTP 服务提供的结果。

这个图片应该用项目符号列表解释

  1. 这里的第一个 TextBlock 显示用户已通过身份验证,提供了
    • 面部几何形状
    • 安全问题的答案
    • 获取了面部身份验证令牌,准备使用该服务。
    • 附言:请忽略那里的“Authorization”,我当时很困惑该用哪个。
  2. 这个有一个 UserID,仅用于演示。您不必在这里显示任何内容。
  3. 这是帐户的名称。
  4. 这是令牌,可用于代表其用户名和密码在系统中进行身份验证和授权。我将在后面的部分讨论其优点。

这样,您就可以使用您拥有的相同资源来验证您的帐户,添加一些 Emgu CV 库来利用计算机视觉的力量,您就完成了!如果您想看看当面部未被识别时会发生什么,什么都不会发生。实际操作 UI 的代码如下:

if(authorized)
{
    // User is authorized
    loggedInPanel.Visibility = Visibility.Visible;
    loginId.Text = Id.ToString();
    loginName.Text = Name;
    loginStatus.Text = "Logged in using Facial Authorization";
    loginToken.Text = Token;
}

这里没有 else 块,您可以根据需要随意添加一个。然后,它将显示一个错误消息,说明用户未通过身份验证,用户未知,或者是否要添加新用户?无论您想对应用程序做什么,都会很棒。

最后的寄语

我希望您喜欢构建使用面部几何形状进行用户身份验证的系统。我学到了很多关于计算机视觉的知识,关于面部身份验证的核心编程,以及更多。说真的,如果您仍然认为我遗漏了什么,请告诉我,我很乐意将其添加到这篇文章中。然而,现在是一些我给读者的一些技巧以及一些我没有但应该让这个过程变得更简单、更容易和更安全的事情。

关于性能的几点说明

这个框架需要一个非常快速的架构和系统才能正常工作。即使是一个小图像也会产生一个巨大的矩阵(虽然只是二维的),因此会导致 CPU 花费一些时间来生成结果。这肯定会给您带来一些停机时间,因为 CPU 通常会忙于处理,而用于生成客户端响应的时间会更少。

有很多方法可以升级和提高执行所有计算机视觉相关任务的服务的性能。

  1. 更改服务的配置。
    • API 的函数允许您传递用于调整引擎的值。您可以随时更改它们的值。
    • 某些值以高计算能力(和时间)为代价提供最佳结果。
    • 某些值提供最佳性能,快速结果,但效率不高,也不够完美。
  2. 升级使用的硬件。
    • 有时,调整 API 并不能带来性能方面的任何更新。在这种情况下,您需要升级用作客户端服务器的机器的硬件。在大多数情况下,硬件不足以跟上生成请求响应的速度。
    • CPU 可能足够强大,但在大多数情况下,尤其是在您要执行图像处理时,GPU 会非常有帮助。您可以将 GPU 添加到您的计算机,然后将处理转发到您的 GPU。
  3. 充分利用您的硬件。
    • 大多数硬件驱动程序都可以更新,以充分利用您的设备。
    • GPU 驱动程序可以更新,如果您使用的是 Nvidia 的 GPU 技术,那么安装 CUDA 驱动程序是推荐的步骤。
    • 如果您的 CPU 有多个核心,那么您可以使用并行处理来充分利用 CPU 的能力,并在并行环境中处理复杂的函数。
    • 简单的线程也可以使用,但这并不能减少处理所需的时间。它只会让您的应用程序或服务对用户操作等做出响应。

可以执行这些操作以获得更好的性能。请注意,它们不能保证在所有时间都 100% 工作。有时最好根本不使用它们。例如,并行处理,因为它可能导致死锁(如果使用不当)。尝试所有这些并查看哪种最适合您仍然是一个好主意。我在家庭网络设置中使用双核处理器,并且由于用户限制在 4-10 人,因此性能足够,平均响应时间约为 4 秒。我当然可以提高性能以将等待时间缩短到约 2 秒,但这对于家庭网络来说不算什么,因为没有客户端在等待响应,因此添加昂贵的 GPU 来获得额外的 2 秒时间就有点过分了。

关于安全的几点说明

由于这是一个身份验证系统,“安全性必须被赋予极高的优先级”。例如,计算机不知道一个人的脸是否真的是一个人的脸,或者是一张照片以一定距离对着摄像头。谁知道呢?计算机无法从二维空间中分辨出来。这个概念的安全方面非常简单、直接,但需要大量的复杂理解才能实际工作。在我看来,可以通过以下方式改进身份验证系统的准确性和安全性。

  1. 降低引擎允许图像之间存在距离的阈值。
    • 通常,会提供一个平均距离,并允许较少数量的图像。
    • 您应该考虑增加使用的图像数量,这将提高您引擎的准确性(约 5-10 张图像)。这样,您可以降低阈值以获得更准确的结果,并注意到何时有人是未知的
    • 阈值越高,结果越差(可以说总是提供匹配!)。
  2. 使用能够确定图像深度(其 z 索引)的摄像头。
    • 考虑使用 3D 空间而不是 2D 空间中的图像。
    • 有人提出,与二维空间图像相比,三维空间图像可以获得更高的准确性结果。还有一篇论文发表,其中提供了概述;可惜由于价格昂贵,我无法阅读它
    • 三维空间相机将独立于空间几何、背景噪声、空间差异和亮度。三维图像将为面部几何分析和执行面部识别提供出色的结果。
    • 二维算法基于空间信息,例如亮度、噪声、面部属性等。这就是为什么大多数算法在许多方面和情况下都不同。在大多数情况下,您应该考虑使用适合您的服务环境以及用户将如何使用的算法。LBPH 是一个有用的算法,但 Eigen 算法是简单的算法。另一方面,Fisher 算法与 Eigen 面部一样简单,但它是一个非常准确的算法,并且能够与 LBPH 媲美。但请记住,这完全取决于服务的用途。
  3. 最重要的是,实施双向安全机制
    • 查看本文,您会发现尽管面部已被识别,但仍未授予令牌。令牌应用于 API 上的用户身份验证,而不是使用其 UserIds 和密码。
    • 您可以考虑使用双向因素对用户进行身份验证的其他方法。例如,将 PIN 码发送到他们的移动设备或任何连接的设备。
    • 如果您不考虑添加双向安全,您的帐户将始终对黑客开放,其中最简单的一种方法就是向需要获得授权的摄像头显示某个人的图像。

有人还录制了一段关于此比较的视频,比较哪种方法在大多数情况下(对于二维空间图像)具有更高的准确性,您可以在此处观看该视频,https://www.youtube.com/watch?v=x8W_htbct3U

参考文献

如前所述,我将分享我认为最有可能帮助您学习过程的参考资料。以下是一些,如果您认为还有其他链接可以帮助读者,请告诉我,我会将它们包含在此处。

  1. 概念参考
  2. OpenCV 参考
  3. 网络参考
  4. 统计概念参考

好了,本指南就到这里。下次再见。 :-)

© . All rights reserved.