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

图像分类:使用 Azure Face API 认知服务进行人脸检测和识别

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (26投票s)

2019 年 5 月 27 日

CPOL

28分钟阅读

viewsIcon

38031

downloadIcon

1119

使用现代 AI 驱动的 Azure 认知服务进行人脸识别和检测。

目录

前言

本文是 CodeProject 图像分类挑战赛的投稿。比赛于 2019 年 5 月 1 日正式开始,并于 2019 年 6 月 28 日结束。

本文是一个完整的端到端教程,将解释如何使用基于现代 AI 的 Azure 认知服务(即 Azure 的 Face API 服务)进行人脸识别和人脸检测。

简介

随着科技领域以飞快的速度发展,我们发现自己被机器学习和人工智能包围。AI 基础技术正在蓬勃发展,编程语言已成为促进这些技术的媒介。Face API 使开发人员能够通过简单的 HTTP 调用轻松执行人脸识别、检测和情感分析。此功能可以直接集成到任何应用程序中,并可用于解决无数业务问题。在本文中,我们将详细介绍 Azure Face API 主题。我们将设置一个 Azure 账户,一个 Face API 账户,并在几秒钟内运行 Face API。我们将了解所有可用于熟悉 Face API 的 API。我们还将这些 HTTP 服务集成到一个应用程序中,该应用程序可以执行人脸检测、人脸分组、人脸识别以及从人脸组中查找相似人脸。您将看到如何轻松地检测图片中的人脸,例如获取其属性,或者检测到人脸的人是否在图片中戴着眼镜?此人是男性还是女性,此人的年龄是多少,以及面部毛发、眼睛、鼻子、嘴唇等属性。不仅如此,我们还获得了按需详细信息,以查找面部表情,这些表情表明此人是高兴、悲伤还是愤怒,以及检测到的情绪程度。在此处获取更多详细信息:https://azure.microsoft.com/en-in/services/cognitive-services/face/。总之,乐趣多多。

Azure

Azure 是 Microsoft 的一个云平台,提供大量与云计算相关的资源。其中一个资源是虚拟机。例如,只需点击几下,您就可以在几秒钟内创建一台您选择的、具有您选择的配置和操作系统的功能齐全的机器,并且您可以使用安全凭据从任何地方远程访问该机器,并做任何您想做的事情,例如托管您的网站、开发应用程序、为您的软件创建生产或测试环境等。让我们看看如何实现这一目标的逐步教程。

Azure Functions 和无服务器计算

谈到此处的定义

"Azure Functions 是托管在 Microsoft Azure 公有云上的无服务器计算服务。Azure Functions 以及一般的无服务器计算旨在加速和简化应用程序开发。"

关于 Azure Functions 的第一件事是它们在完全托管的环境中运行。这基本上意味着我们不必去创建或管理虚拟机。Microsoft 提供并预配我们 Azure Functions 所基于的所有底层硬件。其中一个好处是它为我们提供了高可靠性,因为我们不必手动管理底层基础设施。另一个好处是,我们不必担心去为底层基础设施应用安全补丁。Microsoft 将为我们处理此事。但是,我们有责任确保我们基于 Azure Functions 构建的系统得到适当的保护和管理。这个完全托管环境的另一个好处是它为我们提供了自动扩展。Azure Functions 平台将自动扩展我们的函数以应对传入请求量的变化。如果我们在 Consumption 计划上执行 Azure Functions,那么我们只会在函数执行时付费。Azure Functions 的另一个好处是我们需要编写更少的样板代码,其中一个原因是我们的 Azure Functions 可以轻松地与一系列 Azure 服务集成。例如,将我们的函数与 Azure Blob 存储集成就像在我们的 C# 代码上配置正确的属性一样简单。

Azure 账户设置

如果没有付费的 Azure 账户,可以利用 Azure 新账户提供 200 美元赠金的优惠。这意味着如果您是 Azure 新用户并想试用其免费试用版,您将获得 200 美元赠金,可用于探索 Azure。如果您是 Azure 新用户且没有账户,请按照我下一节描述的流程操作,或者直接登录您的门户。

  1. 打开 Azure 网站,即 azure.microsoft.com

  2. 点击免费开始以创建您的免费 Azure 账户并获得 200 美元赠金。

创建账户并领取 200 美元需要您的信用卡/借记卡仅用于验证目的,不会从您的卡中扣除任何金额。您可以利用此赠金和账户玩 30 天。您将看到注册页面,您可以在其中填写所有信息并逐步注册。成功注册后,您将看到门户链接,如下所示

点击门户,您将进入仪表板,即可使用/体验 Azure。

人脸 API

Azure AI 服务包含许多不同的服务。最受关注的服务可能是认知服务,它是 Microsoft 预构建的 AI。Face API 是认知服务的一个示例,因此它完全属于此类别。

Face API 有五个主要功能领域。最基本的操作是人脸检测。检测将提供人脸在图像中出现的精确位置,它还将提供有关检测到的人脸的元数据,例如年龄估算、性别以及众多人脸属性,例如面部毛发和此人是否戴眼镜。此处也检测情感元数据。识别就是我们所说的人脸识别。它可以识别特定的已知个体,前提是 Face API 已预先训练以识别该个体。验证是一种人脸识别,但在验证中,我们只是尝试将人脸与特定人员进行匹配,而不是一组人员。最好的例子是当您尝试使用人脸解锁计算机或手机时。人脸分组尝试将同一人的人脸分组,前提是提供了一组人脸。最后,查找相似功能将查找相似人脸,前提是提供了一组人脸。这将尝试查找完全相同的人,或者,根据您选择的模式,查找看起来相似的人脸。例如,如果您正在尝试查找您的名人替身,则将使用第二种模式。在本文中,我们将看到这些功能的实际应用。

在 Azure 门户上创建人脸 API

在本节中,我们将在 Azure 门户上启动并运行人脸 API。我们将逐步进行,以免遗漏任何细节。

  1. 登录 Azure 门户并点击“创建资源”。您可以在左侧面板中找到此选项。

  2. 在屏幕上,选择“AI + 机器学习”,您将看到 Azure 提供的所有机器学习服务。在那里,我们看到一个名为“人脸”的服务。选择该选项。

  3. 点击该“人脸”选项后,系统将要求您创建 Face API,这正是我们想要的。提供必要的详细信息,例如您的 API 名称(提供您方便记忆的任何名称。我将我的 API 命名为“face-api”)。提供订阅详细信息、位置。在我的例子中,我选择了印度中部。选择定价层,您可以通过导航到以下图像中显示的链接“查看完整的定价详细信息”来获取定价层列表。我选择了 F0。选择一个资源组或创建一个新的资源组。资源组仅用于对资源进行逻辑分离。例如,我将我的资源组命名为“azure-machine learning”,因为我想将所有与 AI 或机器学习相关的应用程序和服务都放在此组中。

  4. 填写所有详细信息并提交信息后,服务部署需要一些时间(几秒钟),您将收到部署完成的消息,并显示一个名为“转到资源”的按钮,该按钮将导航到已创建的 Face API。您也可以通过点击主门户页面上的资源链接或仪表板,转到已创建的 Face API。

  5. 仪表板,当您进入已创建的 Face API 资源时,您可以找到终结点,如下图所示。将终结点复制到记事本中。我们很快就会在测试 API 时用到它。

  6. 在同一屏幕的“资源管理”下的左侧面板中,您可以找到安全密钥。也将其复制并粘贴到记事本中。我们也会用到它。

测试人脸 API

由于 API 是一个端点,并且行为类似于 REST 服务,因此我们可以使用 Postman 来测试 API。打开 Postman,如果您的计算机上未安装,请安装。您可以在此处获取最新版本的应用程序。

检测调用

我们将从对 API 进行检测调用开始。您可以在此处找到所有端点详细信息,该链接解释了可以对 API 端点进行的所有调用。这些调用包括查找相似人脸、从图像中检测人脸、对人脸进行分组等等。我们将逐一详细介绍。让我们从检测开始。

  1. Postman 应用程序启动后,我们就可以测试 API 了。将我们保存在记事本中的端点复制并粘贴到 Postman 请求中。确保请求是 POST 请求。在端点末尾追加“detect”,因为它是一个检测调用,如下图所示。在Headers 部分,我们必须添加几个标头,Content-Type,我们将其设置为 application/JSON。另一个用于身份验证的标头是 Ocp-Apim-Subscription-Key,这是 Face API 用于身份验证的标头。现在我们需要在此处粘贴我们的密钥。获取我们在上一步中复制的密钥并将其粘贴到 Ocp-Apim-Subscription-Key 键的值中。

  2. 正文选项卡(即 Postman 中标头旁边的选项卡)中,选择“原始”作为内容类型,并在正文部分提供一个包含一组图片 URL 的 JSON。我使用了一个公共服务器位置来放置我的图像。您可以使用 Azure Blob 存储或任何其他云提供商来存储您的图像。确保您的图像是公开可访问的,即使不是,您也可以在测试 API 时将图像作为上传发送。您只需将数据类型从原始更改为二进制并上传您的图像。请求内容类型为 application/octet-stream

    以下是我在公共服务器上使用的一张包含我朋友和家人的脸的图片。

  3. 按下蓝色的发送按钮以发出请求并测试 API。我们看到在几秒钟内,我们收到了 API 的响应。这首先证明我们的 API 已启动并正在运行,其次,它也正在工作。我们收到的响应是一个 JSON,它为我们提供了图片中每个检测到的人脸的信息以及人脸的属性,例如以 faceRectangle 属性形式的尺寸,并且每个人脸都唯一地检测到并具有唯一的 faceId

在以下认知服务 URL 上,您将看到除了检测之外,还可以对 API 进行的所有调用。URL:https://centralindia.dev.cognitive.microsoft.com/docs/services/563879b61984550e40cbbe8d/operations/563879b61984550f30395236

向下滚动以检查您可以随检测调用本身发送的所有查询参数,以获取有关人脸的更多信息。

人脸特征点

“一个包含 27 个点的人脸特征点数组,指向人脸组件的重要位置。要返回此项,需要将 'returnFaceLandmarks' 参数设置为 true。”

人脸特征点是一个查询字符串参数,可以与检测调用一起发送,以获取人脸组件(如眼睛、鼻子、嘴唇等)的位置。只需在您的检测调用中添加一个名为“returnFaceLandmarks”的查询字符串参数,并将其值设置为 true,如下所示。当您点击发送时,您将收到一个包含更详细 JSON 的响应,告诉您每个面部组件(如瞳孔、眉毛、嘴巴、鼻子等)的位置。尽情享受吧。

人脸属性

从 Azure 文档链接获取以下详细信息:https://centralindia.dev.cognitive.microsoft.com/docs/services/563879b61984550e40cbbe8d/operations/563879b61984550f30395236

“人脸属性

  • age:一个估算的“视觉年龄”数字(以年为单位)。它表示一个人看起来的年龄,而不是实际的生物学年龄
  • gender:男性或女性
  • smile:微笑强度,一个介于 [0,1] 之间的数字
  • facialHair:返回三个面部毛发区域的长度:胡子、胡须和鬓角。长度是一个介于 [0,1] 之间的数字。0 表示该区域没有面部毛发,1 表示该区域有长或非常浓密的面部毛发。
  • headPose:用于人脸方向的 3D 滚转/偏航/俯仰角
  • glasses:眼镜类型 - 值包括 'NoGlasses'、'ReadingGlasses'、'Sunglasses'、'SwimmingGoggles'
  • emotion:情绪强度,包括中性、愤怒、蔑视、厌恶、恐惧、快乐、悲伤和惊讶
  • hair:一组头发值,指示头发是否可见、秃头以及头发可见时的发色。
  • makeup:眼睛、嘴唇区域是否化妆。
  • accessories:人脸周围的配件,包括“头饰”、“眼镜”和“口罩”。空数组表示未检测到配件。请注意,这是在检测到人脸之后。大口罩可能导致无法检测到人脸。
  • blur:人脸是否模糊。级别返回“低”、“中”或“高”。值返回一个介于 [0,1] 之间的数字,数字越大越模糊。
  • exposure:人脸曝光级别。级别返回“GoodExposure”、“OverExposure”或“UnderExposure”。
  • noise:人脸像素的噪点级别。级别返回“Low”、“Medium”和“High”。值返回一个介于 [0,1] 之间的数字,数字越大噪点越多”

人脸属性也是一个查询字符串参数,可以与检测调用一起发送。它返回上面列表中给出​​的属性值。例如,如果您说 returnFaceAttributes = age,它将返回检测到的人脸的年龄。

同样,您可以询问性别、情感,检查一个人是否戴眼镜,是否微笑。通过认知服务 AI 技术,您可以从图像中获取所有这些面部属性。

人脸 API SDK

C# 为 Face API 提供了丰富的 SDK,您可以使用它编写 C# 代码并执行所有端点操作。让我们一步步看看如何实现。

  1. 打开您的 Visual Studio。我正在使用 VS 2017 Professional 版,创建一个控制台应用程序,将其命名为 FaceApiSdk 或您选择的任何名称。

  2. 在 Visual Studio 中右键单击项目并添加一个名为 Microsoft.ProjectOxford.Face.DotNetStandard 的 Nuget 包。

  3. 完成后,我们可以按如下方式在类中添加代码
    using Microsoft.ProjectOxford.Face;
    using System;
    using System.Threading.Tasks;
    
    namespace FaceApiSdk
    {
        class Program
        {        
            static async Task Main(string[] args)
            {
                IFaceServiceClient faceServiceClient = new FaceServiceClient
                                                       ("<put your key here>", 
                    "https://centralindia.api.cognitive.microsoft.com/face/v1.0");
                var detectedFaces = await faceServiceClient.DetectAsync
                ("https://codeproject.org.cn/script/Membership/Uploads/
                  7869570/Faces.png");
                foreach (var detectedFace in detectedFaces)
                {
                    Console.WriteLine($"{detectedFace.FaceId}");
                }
            }
        }
    }

    如果您使用的 .NET Framework 版本低于 4.7,请阅读本文以启用 async Main 方法。

    在上述代码中,我们首先创建 IFaceServiceClient 的实例,并在构造函数中以参数形式提供密钥和 FaceAPI URL。然后我们等待该实例的 DetectAsync 方法,并将我们想要检测人脸的图像 URL 作为参数传递给 DetectAsync 方法。然后我们写入返回的所有人脸的 face ID。

    编译代码并按 F5 运行。在下图中,显示了输出,我们看到所有检测到的人脸的 face ID 都已返回。

  4. 是时候测试人脸属性了。添加一个名为 faceAttributes 的数组,说明您希望在响应中返回哪些属性。在 DetectAsync 方法中,将其作为参数传递给 returnFaceAttributes,如下所示

代码

using Microsoft.ProjectOxford.Face;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace FaceApiSdk
{
    class Program
    {
        /// <summary>
        /// Go through the following article to enable async Main method 
        /// if you work on .Net Framework lesser than 4.7: 
        /// https://www.c-sharpcorner.com/article/
        /// enabling-c-sharp-7-compilation-with-visual-studio-2017/
        /// </summary>
        /// <param name="args"></param>
        /// <returns></returns>
        static async Task Main(string[] args)
        {
            IFaceServiceClient faceServiceClient = new FaceServiceClient
                                                   ("<Provide your key here>",
                "https://centralindia.api.cognitive.microsoft.com/face/v1.0");

            var faceAttributes = new[] 
                { FaceAttributeType.Emotion, FaceAttributeType.Age };

            var detectedFaces = await faceServiceClient.DetectAsync
            ("https://codeproject.org.cn/script/Membership/Uploads/7869570/Faces.png",
                returnFaceAttributes:faceAttributes);

            foreach (var detectedFace in detectedFaces)
            {
                Console.WriteLine($"{detectedFace.FaceId}");
                Console.WriteLine($"Age = {detectedFace.FaceAttributes.Age}, 
                Happiness = {detectedFace.FaceAttributes.Emotion.Happiness}");
            }
            Console.ReadLine();
        }
    }
}

输出:我们看到以下输出,其中也返回了我们请求的人脸属性。Happiness = 0.983 表示 API 认为一个人开心的程度。因此,0.983 意味着一个人在图片中微笑或大笑的可能性很大。

人脸识别

在上一节中,我们讨论了人脸检测。在本节中,我们将重点介绍人脸识别,并检查人脸 API 识别人员人脸的能力。在本节中,我们将了解可以编写哪些服务来执行人脸识别。我们将使用 Postman 测试这些服务,在下一节中,我们将介绍一个实时人脸识别应用程序。

在我们详细查看各个操作之前,我们首先了解一切如何协同工作的整体结构和工作流程非常重要。我们需要做的第一件事是创建一个人员组。Face API 为我们提供了所有 CRUD 操作。它为我们提供了所有这些用于管理人组的操作。一旦我们创建了人员组,我们就可以向该人员组添加n人员。在我们将人员添加到人员组之后,我们再向每个人员添加n人脸。一旦我们向人员组添加了我们想要的尽可能多的人员人脸,我们就会调用Face API 方法来训练我们的人员组。一旦我们的人员组经过训练,我们就可以进行人脸识别了。人脸识别总是从人脸检测开始。您已经看到了人脸检测。关键结果是我们在人脸检测中为每个人脸获得一个唯一的faceId。一旦我们调用检测,我们就可以最终使用人脸检测调用的结果以及我们想要用于人脸识别的人员组来调用人脸识别。让我们一步一步地完成。

创建人员组

我已经在 Postman 中设置了一些设置,以保存我的基本 URL (即 https://centralindia.api.cognitive.microsoft.com/face/v1.0) 和密钥。因此,我将分别使用关键字 {{base-url}}{{face-api-key}} 作为 URL 和密钥。您可以使用您的基本 URL 和密钥来代替我的关键字。

在 Postman 中,对附加到基本 URL 的 persongroups/1 操作发出拉取请求,如下所示。在正文中,提供原始 JSON,其中包含人员组的名称。例如,在我的情况下是“family”,以及作为人员组描述的 userData。JSON 应如下所示

{
	"name": "family",
	"userData": "family person group"
}

在标头部分,提供两个键。例如,Ocp-Apim-Subscription-KeyContent-Type,它们的值分别是您的 API 密钥和 application/JSON。点击发送按钮。点击发送按钮后,您会收到 200 响应,即 OK,这意味着人员组已创建。

获取人员组

我们可以通过对 API https://centralindia.api.cognitive.microsoft.com/face/v1.0/persongroups/1 进行 get 调用来检查已创建的人员组。

您将获得已创建的人员组,其中包含人员组 ID、名称和用户数据。在下一步中,我们将向该人员组添加一些人员。

创建人员

在已创建的人员组中,将 URL 附加“persons”,并在正文部分提供函数的名称和用户数据,如下所示

{
	"name" : "Akhil Mittal",
	"userData" : "Author"
}

保持标头不变(即,提供密钥和内容类型),然后点击发送按钮。确保 HTTP 动词是 Post。我们收到一个响应,其中包含在人员组 1 下创建的人员,该人员具有人员 ID。

获取人员

您可以通过我们上一步收到的个人 ID 来获取该个人。向 API 发出 Get 调用,如下所示:https://centralindia.api.cognitive.microsoft.com/face/v1.0/persongroups/1/persons<personid>

我们得到了包含 personIdnameuserData 的 JSON 响应,以及一个名为 persistedFaceIds 数组的附加字段。由于我们尚未为此人创建任何面孔,因此该字段为空。

获取所有人员

如果您希望获取为该特定人员组创建的所有人员的列表,只需向 API https://centralindia.api.cognitive.microsoft.com/face/v1.0/persongroups/1/persons/ 发出 Get 调用即可

这将为您获取人员组中所有已创建的人员的 JSON 响应,其 Id1

为人员创建人脸

是时候为这个人创建面孔了。例如,为已创建的人创建持久面孔。由于您拥有已创建的人的 ID,我们希望为其创建持久面孔。我们将向 API 发出调用,如下所示:https://centralindia.api.cognitive.microsoft.com/face/v1.0/persongroups/1/persons/<personId>/persistedFaces

正文标签中,提供一个包含该人物脸部 URL(即需要创建人脸的人物图像)的原始 JSON。

{
	"url": "https://codeproject.org.cn/script/Membership/Uploads/7869570/Akhil.png"
}

以下是给出 URL 的此人的图像

现在,使用这些配置发出 POST 请求,并确保您的标头数据已定义密钥和内容类型。点击发送按钮。一旦我们收到响应,我们看到响应包含一个包含“persistedFaceId”的 JSON。这意味着此人的面部已创建并返回了相应的 Id

现在,如果您再次对该人员发出get请求,您还将获得该人员面部的持久化面部 ID 属性值,如下所示。

训练人员组

现在是时候训练人员组了。由于我们已经获得了人员组、人员和他的人脸,我们将训练我们的模型,然后对该人员执行人脸识别。只需将 train 附加到 persongroups/<id> URL,然后发出 POST 请求,如下所示:https://centralindia.api.cognitive.microsoft.com/face/v1.0/persongroups/1/persons/train

完成后,我们将收到响应 200,即 Accepted

检查训练状态

您可以通过向 URL https://centralindia.api.cognitive.microsoft.com/face/v1.0/persongroups/1/persons/training 发送 Get 请求来检查训练状态。

我们在下面的图像中看到,我们获得的训练状态是成功的。这意味着人员组现在已训练好,可以执行人脸识别操作。

让我们在下一步中完成它。

识别

我已将以下图像上传到包含我朋友和我自己的公共 URL。

  1. 我们像最初一样对上传的图像进行检测调用,当我们收到响应时,我们看到返回了两个面部的 JSON,一个是我朋友的,另一个是我的。

  2. 现在,由于我们的人员组已经过训练,我们可以向基本 URL 发送识别请求。在请求体中,提供包含人员组 ID 和我们在上次操作中检测到的面部的 JSON。
    {
    	"personGroupId" : "1",
    	"faceIds" : [
    		             "5a45a46a-6327-499e-8442-0fb404f4e426",
    	                         "c88546a1-543c-4497-ab9c-df4e055820cd"
                                    ]
    }

一旦我们点击发送按钮并发出 POST 请求,我们就会收到两个提供的人脸的响应,但只有一个新人脸有候选人,这些候选人会告诉personId以及识别出的人是否在人员组中的置信度。它似乎正在工作。在这里,我们请求识别两个人,但只收到一个人的响应,那是我的脸,因为我的脸已经存在于已创建的人员组的人员列表中,并且我们的模型已经训练好来处理它。这里的响应返回了置信度值,这意味着这是 API 识别出的人存在于人员组中的置信度。

在下一节中,我们将通过一个运行中的应用程序,其中包含所有这些操作以及操作的视觉视图。

人脸分类应用程序

获取代码

我已经创建了该应用程序,您可以从下载的源代码或 Git URL 获取:https://github.com/akhilmittal/Face-API。在此应用程序中,我们将执行创建人员组和人员、检测和识别人脸、验证人脸、对人脸进行分组以及查找相似人脸(即看起来相似的人脸)等操作。

  1. 前往 Git URL 并点击克隆或下载按钮以获取代码的 Git URL。复制该 URL。

  2. 打开命令提示符,但在此之前请确保您的计算机上已安装 Git。移动到您要获取源代码的目录,然后在命令提示符下执行 git clone <git URL> 操作。

  3. 克隆完成后,是时候打开应用程序了。我正在 VS Code 中打开它。因此,如果您正在使用 VS Code,只需从命令提示符进入获取的代码目录,然后键入命令“code .”。这将使用获取的代码打开 VS Code。

    以下是 VS Code 打开解决方案后的界面。

  4. 在开始之前,让我们安装软件包。在命令窗口中,键入 npm install 以安装应用程序所需的所有软件包。

设置代码

  1. 代码下载、在代码编辑器中打开并安装好包后,我们就可以继续查看其中包含的内容了。打开 face-api-service.service.ts 文件,并在文件的顶部,为 baseURL 变量提供您的基本 URL,如下所示

  2. 同样,在该文件的末尾,在 Ocp-Apim-Subscription-Key 字段中提供您的 API 密钥。

该文件的代码如下

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/observable/forkJoin';
import 'rxjs/add/observable/of';

@Injectable()
export class FaceApiService {

  private baseUrl = 'https://centralindia.api.cognitive.microsoft.com/face/v1.0';

  constructor(private http: HttpClient) { }

  // ***** Person Group Operations *****

  getPersonGroups() {
    return this.http.get<any[]>(`${this.baseUrl}/persongroups`, httpOptions);
  }

  createPersonGroup(personGroup) {
    return this.http.put<any[]>
    (`${this.baseUrl}/persongroups/${personGroup.personGroupId}`, 
    personGroup, httpOptions);
  }

  deletePersonGroup(personGroupId) {
    return this.http.delete
    (`${this.baseUrl}/persongroups/${personGroupId}`, httpOptions);
  }

  trainPersonGroup(personGroupId) {
    return this.http.post<any[]>(`${this.baseUrl}/persongroups/
    ${personGroupId}/train`, null, httpOptions);
  }

  getPersonGroupTrainingStatus(personGroupId) {
    return this.http.get<any>(`${this.baseUrl}/persongroups/
    ${personGroupId}/training`, httpOptions);
  }

  // ***** Persons Operations *****

  getPersonsByGroup(personGroupId) {
    return this.http.get<any[]>(`${this.baseUrl}/persongroups/
    ${personGroupId}/persons`, httpOptions);    
  }

  getPerson(personGroupId, personId) {
    return this.http.get<any[]>(`${this.baseUrl}/persongroups/
    ${personGroupId}/persons/${personId}`, httpOptions);    
  }

  // ***** Person Operations *****

  createPerson(personGroupId, person) {
    return this.http.post<any>(`${this.baseUrl}/persongroups/
    ${personGroupId}/persons`, person, httpOptions);    
  }

  deletePerson(personGroupId, personId) {
    return this.http.delete<any[]>(`${this.baseUrl}/persongroups/
    ${personGroupId}/persons/${personId}`, httpOptions);    
  }

  // ***** Person Face Operations *****/

  getPersonFaces(personGroupId, personId) {
    return this.http.get<any>(`${this.baseUrl}/persongroups/
    ${personGroupId}/persons/${personId}`, httpOptions).flatMap(person => {
      let obsList = [];
      if (person.persistedFaceIds.length) {
        for (const faceId of person.persistedFaceIds) {
          obsList.push(this.getPersonFace(personGroupId, personId, faceId));
        }
        return Observable.forkJoin(obsList);
      } else {
        return Observable.of([]);
      }
    });
  }

  getPersonFace(personGroupId, personId, faceId) {
    return this.http.get(`${this.baseUrl}/persongroups/
    ${personGroupId}/persons/${personId}/persistedfaces/${faceId}`, httpOptions);
  }

  addPersonFace(personGroupId, personId, url) {
    return this.http.post<any>(`${this.baseUrl}/persongroups/
    ${personGroupId}/persons/${personId}/persistedfaces?userData=${url}`, 
    { url: url}, httpOptions);
  }

  deletePersonFace(personGroupId, personId, faceId) {
    return this.http.delete(`${this.baseUrl}/persongroups/
    ${personGroupId}/persons/${personId}/persistedfaces/${faceId}`, httpOptions);
  }

  // ***** Face List Operations *****

  createFaceList(faceListId) {
    return this.http.put(`${this.baseUrl}/facelists/${faceListId}`, 
    { name: faceListId }, httpOptions);
  }

  addFace(faceListId, url) {
    return this.http.post(`${this.baseUrl}/facelists/${faceListId}/persistedFaces`, 
    { url: url }, httpOptions);
  }

  // ***** Face Operations *****

  detect(url) {
    return this.http.post<any[]>(`${this.baseUrl}/detect?returnFaceLandmarks=false&
    returnFaceAttributes=age,gender,smile,glasses,emotion,facialHair`, { url: url }, 
    httpOptions);
  }

  identify(personGroupId, faceIds) {
    let request = {
      personGroupId: personGroupId,
      faceIds: faceIds,
      confidenceThreshold: 0.4
    };
    return this.http.post<any[]>(`${this.baseUrl}/identify`, request, httpOptions);
  }

  group(faceIds) {
    return this.http.post<any>(`${this.baseUrl}/group`, 
    { faceIds: faceIds }, httpOptions);
  }

  findSimilar(faceListId, faceId) {
    let request = { faceId: faceId, faceListId: faceListId };
    return this.http.post<any>(`${this.baseUrl}/findsimilars`, request, httpOptions);
  }
}

// private (non-exported)

const httpOptions = {
  headers: new HttpHeaders({
    'Content-Type': 'application/json',
    'Ocp-Apim-Subscription-Key': '<key>'
  })
};

如果您仔细查看代码,我们会发现之前执行的所有人脸操作都在这里定义。它们只需从我们在应用程序中使用的 UI 中调用即可。

编译并运行应用程序

这是一个 Angular 应用程序,因此您也可以从 VS Code 终端或命令窗口运行该应用程序。我正在从命令窗口运行它。因此,在命令窗口中键入 ng serve 命令并按 Enter

一旦编译完成并且服务器正在运行,您将获得应用程序的 URL。在我的情况下,它在 localhost 4200 端口上运行。

复制该 URL 并在浏览器中打开。我们看到应用程序正在运行。此应用程序在主页上有设置人脸检测按钮。

创建人员组

  1. 点击设置并添加一个人员组。请注意,用户界面在后台绑定到所有 API 调用。我们已经探索了所有用于创建人员、组和人脸的 API 调用。因此,只需浏览应用程序代码以探索文件并查看它们如何绑定以调用 API。
  2. 添加一个人员组并将其命名为 family

  3. 一旦人员组已创建并显示,请向该人员添加人员

创建人员

添加人员后,向该人员添加新人脸

添加人脸

在“添加人脸”弹出窗口中,提供人员人脸图像 URL,然后点击保存。它将显示以下图像

同样,向该人员组添加更多人员。例如,我添加了Akhil MittalArshUdeep 作为人员。我为Akhil Mittal添加了三张人脸。

为 Arsh 添加了四张面孔。

为 Udeep 添加了三张面孔。

训练人员组

现在,如果您还记得,在添加人员组、人员和人脸之后,我们做的下一件事是训练模型。因此,点击训练模型,它在后台绑定到训练 API 端点,它将训练我们的人员组模型,并使其准备好进行检测和识别。一旦您点击“训练模型”按钮,您将看到“训练已启动”消息。

训练完成后,如果您按下“检查训练状态”按钮,您将看到“训练成功”消息。

所有操作的代码

configuration.component.ts 中,我们定义了所有执行这些操作的组件。

import { Component, OnInit } from '@angular/core';
import { FaceApiService } from '../services/face-api-service.service';
import { InputBoxService } from '../input-box/input-box.service';
import * as _ from 'lodash';
import { ToasterService } from 'angular2-toaster';

@Component({
  selector: 'app-configuration',
  templateUrl: './configuration.component.html',
  styleUrls: ['./configuration.component.css']
})
export class ConfigurationComponent implements OnInit {
  public loading = false;
  public personFaces = [];
  public personGroups = [];
  public personList = [];
  public selectedGroupId = '';
  public selectedPerson: any;

  constructor(private faceApi: FaceApiService, 
  private inputBox: InputBoxService, private toastr: ToasterService) { }

  ngOnInit() {
    this.faceApi.getPersonGroups().subscribe(data => this.personGroups = data);
  }

  addPersonGroup(){
    this.inputBox.show('Add Person Group', 'Person Group Name:').then(result => { 
      let newPersonGroup = { personGroupId: _.kebabCase(result), name: result };
      this.faceApi.createPersonGroup(newPersonGroup).subscribe(data => {
        this.personGroups.push(newPersonGroup);
        this.selectedGroupId = newPersonGroup.personGroupId;
        this.onGroupsChange();
      });
    });
  }

  deletePersonGroup() {
    this.faceApi.deletePersonGroup(this.selectedGroupId).subscribe(() => {
      _.remove(this.personGroups, x => x.personGroupId === this.selectedGroupId);
      this.selectedGroupId = '';
    });
  }

  onGroupsChange() {
    if (this.selectedGroupId) {
      this.loading = true;
      this.faceApi.getPersonsByGroup(this.selectedGroupId).subscribe(data => {
        this.personList = data; 
        this.selectedPerson = null;
        this.personFaces = [];
        this.loading = false; 
      });
    }
  }

  personClick(person) {
    this.selectedPerson = person;
    this.faceApi.getPersonFaces(this.selectedGroupId, 
    this.selectedPerson.personId).subscribe(data => { 
      this.personFaces = data;
    });
  }

  addPerson() {
    this.inputBox.show('Add Person', 'Person Name:').then(result => {
      let newPerson: any = { name: result };
      this.faceApi.createPerson(this.selectedGroupId, 
      { name: result }).subscribe(data => {
        newPerson.personId = data.personId;
        this.personList.push(newPerson);
        this.selectedPerson = newPerson;
      });
    });
  }

  deletePerson(personId) {
    this.faceApi.deletePerson(this.selectedGroupId, 
    this.selectedPerson.personId).subscribe(() => {
      _.remove(this.personList, x => x.personId === this.selectedPerson.personId);
      this.selectedPerson = null;
    });
  }

  addPersonFace() {
    this.inputBox.show('Add Face', 'URL:').then(result => {
      this.faceApi.addPersonFace(this.selectedGroupId, 
      this.selectedPerson.personId, result).subscribe(data => {
        let newFace = { persistedFaceId: data.persistedFaceId, userData: result };
        this.personFaces.push(newFace);
      });
    });
  }

  deletePersonFace(persistedFaceId) {
    this.faceApi.deletePersonFace(this.selectedGroupId, 
    this.selectedPerson.personId, persistedFaceId).subscribe(() => {
      _.remove(this.personFaces, x => x.persistedFaceId === persistedFaceId);
    });
  }

  trainPersonGroup() {
    this.loading = true;
    this.faceApi.trainPersonGroup(this.selectedGroupId).subscribe(() => {
      this.toastr.pop('info', 'Training Initiated', 
                      'Training has been initiated...');
      this.loading = false;
    });
  }

  getGroupTrainingStatus() {
    this.loading = true;
    this.faceApi.getPersonGroupTrainingStatus
    (this.selectedGroupId).subscribe(result => {
      switch (result.status) {
        case 'succeeded':
          this.toastr.pop('success', 'Training Succeeded');
          break;
        case 'running':
          this.toastr.pop
          ('info', 'Training still in progress...', 'Check back later');
          break;
        case 'failed':
          this.toastr.pop('error', 'Error during Training', result.message);
          break;
        default:
          break;
      }
      this.loading = false;
    });
  }
}

这些组件跟踪所有 ID,并在按钮点击时执行必要的 ID 操作。

人脸检测

在应用程序的右上角,您可以找到人脸识别选项卡,其中包含人脸检测人脸分组相似人脸子菜单。点击人脸检测

选择人脸检测选项将打开屏幕,提供需要检测人脸的图像。将图像 URL 放入该图像 URL 文本框中,然后点击检测。请注意,我使用了最初用于通过 API 检测人脸的相同图像。这次,再次进行了相同的 API 调用,我们看到检测到的人脸用黄色方框表示。

以下是 src->app->face-tester 文件夹下 face-tester.component.html 的代码。

<div class="container">
  <ngx-loading [show]="loading" [config]="{ backdropBorderRadius: '14px' }">
  </ngx-loading>

  <div class="card">
    <h3 class="card-header">Test Faces</h3>
    <div class="card-body">

      <div class="form-group">
        <label>Person Group</label>
        <select [(ngModel)]="selectedGroupId" 
         name="personGroups" class="form-control">
          <option value="">(Select)</option>
          <option *ngFor="let group of personGroups" [value]="group.personGroupId">
            {{group.name}} ({{group.personGroupId}})
          </option>
        </select>
      </div>
      <div class="form-group">
        <label>Image URL:</label>
        <input type="text" class="form-control" 
         name="groupName" [(ngModel)]="imageUrl">
      </div>

      <button class="btn btn-primary mr-sm-2" (click)="detect()">Detect</button>
      <button class="btn btn-primary" (click)="identify()">Identify</button>

      <hr/>

      <div *ngIf="selectedFace" class="text-primary">
        <pre class="text-primary">{{selectedFace | json}}</pre>
      </div>
      <div *ngIf="selectedFace && selectedFace.identifiedPerson">
        <ngb-alert>
          Subject Identified: {{selectedFace.name}}
        </ngb-alert>
      </div>
    </div>
  </div>

  <div class="card">

    <div class="mainImgContainer" *ngIf="imageUrl">
      <img #mainImg class="card-img main-img" [src]="imageUrl" 
      (load)="imageLoaded($event)" />

      <div [ngClass]="{'face-box-green': item.identifiedPerson, 
      'face-box-yellow': !item.identifiedPerson}" *ngFor="let item of detectedFaces"
        (click)="faceClicked(item)" [style.top.px]="item.faceRectangle.top * 
        multiplier" [style.left.px]="item.faceRectangle.left * multiplier"
        [style.height.px]="item.faceRectangle.height * multiplier" 
        [style.width.px]="item.faceRectangle.width * multiplier"></div>

    </div>

  </div>

</div>

face-tester.component.ts 的代码如下

import { Component, OnInit, ViewChild } from '@angular/core';
import { FaceApiService } from '../services/face-api-service.service';
import * as _ from 'lodash';
import { forkJoin } from 'rxjs/observable/forkJoin';

@Component({
  selector: 'app-face-tester',
  templateUrl: './face-tester.component.html',
  styleUrls: ['./face-tester.component.css']
})
export class FaceTesterComponent implements OnInit {
  loading = false;
  public detectedFaces: any;
  public identifiedPersons = [];
  public imageUrl: string;
  public multiplier: number;
  public personGroups = [];
  public selectedFace: any;
  public selectedGroupId = '';
  @ViewChild('mainImg') mainImg;

  constructor(private faceApi: FaceApiService) { }

  ngOnInit() {
    this.loading = true;
    this.faceApi.getPersonGroups().subscribe(data => {
      this.personGroups = data;
      this.loading = false;
    });
  }

  detect() {
    this.loading = true;
    this.faceApi.detect(this.imageUrl).subscribe(data => {
      this.detectedFaces = data;
      console.log('**detect results', this.detectedFaces);
      this.loading = false;
    });
  }

  faceClicked(face) {
    this.selectedFace = face;
    if (this.selectedFace.identifiedPersonId) {
      let identifiedPerson = _.find(this.identifiedPersons, 
      { 'personId': face.identifiedPersonId });
      this.selectedFace.name = identifiedPerson.name;
    }
  }

  identify() {
    let faceIds = _.map(this.detectedFaces, 'faceId');
    this.loading = true;

    //NOTE: for Production app, max groups of 10
    this.faceApi.identify(this.selectedGroupId, faceIds).subscribe(identifiedFaces => {
      console.log('**identify results', identifiedFaces);
      let obsList = [];

      _.forEach(identifiedFaces, identifiedFace => {
        if (identifiedFace.candidates.length > 0) {
          let detectedFace = _.find(this.detectedFaces, 
                             { faceId: identifiedFace.faceId });
          detectedFace.identifiedPerson = true;
          detectedFace.identifiedPersonId = identifiedFace.candidates[0].personId;
          detectedFace.identifiedPersonConfidence = 
                       identifiedFace.candidates[0].confidence;
          obsList.push(this.faceApi.getPerson
                      (this.selectedGroupId, identifiedFace.candidates[0].personId));
        }
      });

      // Call getPerson() for each identified face
      forkJoin(obsList).subscribe(results => {
        this.identifiedPersons = results;
        this.loading = false;
      });
    });
  }

  imageLoaded($event) {
    this.selectedFace = null;
    this.detectedFaces = [];
    let img = this.mainImg.nativeElement;
    this.multiplier = img.clientWidth / img.naturalWidth;
  }
}

此代码获取给定图像上检测到的人脸,并在图像上放置一个黄色方框。当您点击人脸时,它会显示该人脸的 JSON。

再次执行另一次检测以确保其正常工作。我又上传了一张我朋友和我的合影。点击检测,我们看到两张面孔上都有两个黄色方框。这次,也选择我们之前创建的人员组(即家庭人员组)。请注意,我们现在正在检测我的图像和我朋友的图像,他们已经作为人员添加到人员组中,并且我们之前也训练过我们的人员组。因此,在检测时我们会得到两个黄色方框。

人脸识别

现在,由于这些人是人员组的一部分,理想情况下,他们应该是可识别的。点击识别,它会向 API 发送识别调用。一旦我们收到响应,我们看到黄色方框变成了绿色,这意味着识别已完成并成功。

通过点击人脸进行交叉验证,我们看到与已识别的人脸对应的 JSON。因此,第一个主题被识别为“Udeep”。

第二个识别为“Akhil”。这些人脸之所以被识别,是因为它们在人员组中已经有条目,并且在训练时它们的人脸已经在人员组中。

人脸分组

让我们执行人脸分组操作。我们将提供几个用换行符分隔的 URL,并执行分组。这些图像 URL 是 Udeep、Arsh 和 Akhil 的几张图像。理想情况下,分组应该以将相似图像组合在一起并显示的方式进行。

一旦发出分组请求,我们看到图像按人物分组。例如,在提供的十一个用于分组的 URL 中,为我识别的人脸有五个,为 Arsh 识别的人脸有三个,为 Udeep 识别的人脸有三个。它完美地工作了。请注意,对于我的图像,它还从提供的图像中识别出了人群中的我的脸。

face-grouping.component.html 的代码如下

<div class="container">
  <ngx-loading [show]="loading" [config]="{ backdropBorderRadius: '14px' }">
  </ngx-loading>

  <div class="card">
    <h3 class="card-header">Face Grouping</h3>
    <div class="card-body">

      <textarea rows="8" cols="80" [(ngModel)]="imageUrls">
      </textarea>

      <hr/>

      <button class="btn btn-primary" (click)="executeGrouping()">
       Execute Grouping</button>

      <div *ngFor="let group of groupingResults.groups">
        <h3>Group</h3>
        <div class="row">
          <div class="col-md-3" *ngFor="let face of group">
            <div class="card text-center">
              <div class="card-body card-block-img-container">
                <span class="img-container">
                  <img class="img-person-face img-thumnail" 
                  [src]="getUrlForFace(face)" height="140" width="140" />
                </span>
              </div>
            </div>
          </div>
        </div>
      </div>

      <div *ngIf="groupingResults.messyGroup">
          <h3>Mixed Group</h3>
          <div class="row">
            <div class="col-md-3" *ngFor="let face of groupingResults.messyGroup">
              <div class="card text-center">
                <div class="card-body card-block-img-container">
                  <span class="img-container">
                    <img class="img-person-face img-thumnail" 
                    [src]="getUrlForFace(face)" height="140" width="140" />
                  </span>
                </div>
              </div>
            </div>
          </div>
        </div>

    </div>
  </div>
</div>

face-grouping.component.ts 的代码如下

import { Component, OnInit } from '@angular/core';
import * as _ from 'lodash';
import { FaceApiService } from '../services/face-api-service.service';
import { forkJoin } from 'rxjs/observable/forkJoin';

@Component({
  selector: 'app-face-grouping',
  templateUrl: './face-grouping.component.html',
  styleUrls: ['./face-grouping.component.css']
})
export class FaceGroupingComponent implements OnInit {
  public imageUrls: string[];
  public faces: any[];
  public groupingResults: any = {};
  public loading = false;

  constructor(private faceApi: FaceApiService) { }

  ngOnInit() { }

  executeGrouping() {
    let urls = _.split(this.imageUrls, '\n'); 

    let detectList = [];
    _.forEach(urls, url => {
      if (url){
        detectList.push(this.faceApi.detect(url));
      }
    });

    this.loading = true;
    forkJoin(detectList).subscribe(detectResults => {
      this.faces = [];
      _.forEach(detectResults, (value, index) => 
      this.faces.push({ url: urls[index], faceId: value[0].faceId} ));
      let faceIds = _.map(this.faces, 'faceId');

      this.faceApi.group(faceIds).subscribe(data => {
        this.groupingResults = data;
        this.loading = false;
      });
    });
  }

  getUrlForFace(faceId) {
    var face = _.find(this.faces, { faceId: faceId });
    return face.url;
  }
}

查找相似人脸

在本应用程序模块中,我们将尝试从提供的图像 URL 组中查找相似人脸。我们将提供一些图像 URL,从中我们需要查找人脸,以及一个我们想要查找相似人脸的 URL。例如,以下是我想要从人脸组中查找相似人脸的图像 URL。

现在,在查找相似人脸界面中,为要查找相似人脸的人员提供换行符分隔的相同或其他人脸的图像 URL,然后在下一个框中输入要匹配的人员的 URL。点击“查找相似人脸”按钮,它将为您提供匹配的人脸。如果人脸不匹配,则不返回任何内容。

您可以在图像中所示位置找到查找相似组件。

find-similar.component.ts 的代码如下

import { Component, OnInit } from '@angular/core';
import { FaceApiService } from '../services/face-api-service.service';
import * as _ from 'lodash';
import { forkJoin } from 'rxjs/observable/forkJoin';

@Component({
  selector: 'app-find-similar',
  templateUrl: './find-similar.component.html',
  styleUrls: ['./find-similar.component.css']
})
export class FindSimilarComponent implements OnInit {
  public faces: any[];
  public loading = false;
  public imageUrls: string[];
  public queryFace: string = 
  'https://codeproject.org.cn/script/Membership/Uploads/7869570/Akhil_5.png';
  public findSimilarResults: any[];

  constructor(private faceApi: FaceApiService) { }

  ngOnInit() { }

  findSimilar() {
    this.loading = true;

    // 1. First create a face list with all the imageUrls
    let faceListId = (new Date()).getTime().toString(); // comically naive, 
                                            // but this is just for demo
    this.faceApi.createFaceList(faceListId).subscribe(() => {

      // 2. Now add all faces to face list
      let facesSubscribableList = [];
      let urls = _.split(this.imageUrls, '\n');
      _.forEach(urls, url => {
        if (url) {
          facesSubscribableList.push(this.faceApi.addFace(faceListId, url));
        }
      });

      forkJoin(facesSubscribableList).subscribe(results => {
        this.faces = [];
        _.forEach(results, (value, index) => this.faces.push
                 ({ url: urls[index], faceId: value.persistedFaceId }));

        // 3. Call Detect on query face so we can establish a faceId 
        this.faceApi.detect(this.queryFace).subscribe(queryFaceDetectResult => {
          let queryFaceId = queryFaceDetectResult[0].faceId;

          // 4. Call Find Similar with the query face and the face list
          this.faceApi.findSimilar(faceListId, queryFaceId).subscribe(finalResults => {
            console.log('**findsimilar Results', finalResults);
            this.findSimilarResults = finalResults;
            this.loading = false;
          });
        });
      });
    });
  }

  getUrlForFace(faceId) {
    var face = _.find(this.faces, { faceId: faceId });
    return face.url;
  }
}

find-similar.component.html 的代码如下

<div class="container">
  <ngx-loading [show]="loading" [config]="{ backdropBorderRadius: '14px' }">
  </ngx-loading>

  <div class="card">
    <h3 class="card-header">Find Similar</h3>
    <div class="card-body">

      <textarea rows="8" cols="80" [(ngModel)]="imageUrls">
        </textarea>

      <input type="text" class="form-control" placeholder="Query Face" 
      [(ngModel)]="queryFace" />

      <hr/>

      <button class="btn btn-primary" (click)="findSimilar()">Find Similar</button>

      <div *ngIf="queryFace">
        <h3>Query Face</h3>
        <div class="row">
          <div class="col-md-3">
            <div class="card text-center">
              <div class="card-body card-block-img-container">
                <span class="img-container">
                  <img class="img-person-face img-thumnail" 
                  [src]="queryFace" height="140" width="140" />
                </span>
              </div>
            </div>
          </div>
        </div>
      </div>

      <div *ngIf="findSimilarResults">
        <h3>Find Similar Results</h3>
        <div class="row">
          <div class="col-md-3" *ngFor="let face of findSimilarResults">
            <div class="card text-center">
              <div class="card-body card-block-img-container">
                <span class="img-container">
                  <img class="img-person-face img-thumnail" 
                  [src]="getUrlForFace(face.persistedFaceId)" 
                   height="140" width="140" />
                </span>
                <hr/>
                <span>Confidence: {{face.confidence}}</span>                
              </div>
            </div>
          </div>
        </div>
      </div>

    </div>
  </div>
</div>

总结

这是一篇端到端的文章,旨在展示 Azure 人脸 API(即 Azure 认知服务之一)的功能。该 API 非常智能和强大,可以利用人工智能和机器学习功能并执行操作。我们详细了解了如何创建 Azure 帐户,如何创建 Face API 并使其启动和运行。我们看到了如何在 Face API 上为人员组、人员和人脸执行 CRUD 操作。API 不仅能进行检测,还能执行诸如提供检测到的人脸的面部属性、从训练模型中识别人脸、分组和查找相似人脸等操作。我希望这很有趣。

参考文献

  1. https://github.com/smichelotti/ps-face-api-explorer
  2. https://nuget.net.cn/packages/Microsoft.Azure.CognitiveServices.Vision.Face/
  3. https://azure.microsoft.com/en-in/services/cognitive-services/face/
  4. https://centralindia.dev.cognitive.microsoft.com/docs/services/563879b61984550e40cbbe8d/operations/563879b61984550f30395236

代码

  1. SDK 代码:https://github.com/akhilmittal/Face-API-SDK
  2. 图像分类应用程序:https://github.com/akhilmittal/Face-API

历史

  • 2019 年 5 月 27 日:初始版本
© . All rights reserved.