使用 Azure Face API 和 C# 进行人脸检测和识别





5.00/5 (6投票s)
在本文中,我们将分析 Microsoft Azure 认知服务提供的一些功能,特别是认知服务中专门用于人脸识别的部分(Face API)。文章结尾,读者将能够开发一个简单的 C# 应用程序来检测人脸。
范围
在本文中,我们将分析 Microsoft Azure 认知服务提供的一些功能,特别是认知服务中专门用于人脸识别的部分(Face API)。文章结尾,读者将能够开发一个简单的 C# 应用程序来检测图像中的人脸,并训练该网络服务以识别照片中之前未见过的人。
什么是认知服务?
正如 Azure 认知服务欢迎页面 所述,“Microsoft 认知服务(前身为 Project Oxford)是一套 API、SDK 和服务,可供开发人员使用,让他们的应用程序更智能、更具吸引力且更易于发现。Microsoft 认知服务扩展了 Microsoft 不断发展的机器学习 API 产品组合,使开发人员能够轻松地将智能功能(如情感和视频检测;面部、语音和视觉识别;以及语音和语言理解)添加到他们的应用程序中。我们的愿景是,通过日益能够看、听、说、理解甚至开始推理的系统,实现更个性化的计算体验和增强的生产力。”
在这些服务中,我们将在此处介绍 Microsoft Face API,“一种基于云的服务,提供最先进的面部算法。Face API 具有两个主要功能:带有属性的面部检测和面部识别”(认知服务 Face API 概述)。我们将在文章后续部分介绍这两个功能,并在开发示例解决方案时更深入地探讨它们。
在 Azure 上创建认知服务帐户
访问 https://portal.azure.com/。您需要一个 Azure 帐户才能登录。
登录后,在搜索栏中搜索“认知服务”。
现在,点击“添加”以创建新服务。
向导会要求您输入帐户名称、要使用的 API 类型(在本例中为 Face API)、服务位置以及要使用的定价层。在此示例中,我们将使用 F0,它提供免费服务,每分钟限制 20 次调用,每月 30K 次调用,最多 1000 个唯一面孔。输入所需详细信息,然后按“创建”开始创建过程。您还可以勾选“固定到仪表板”,以便在仪表板中为服务创建有用的链接。
在适当的时间后(通常很短),服务将部署并准备就绪。
通过单击它,可以访问其属性,其中包含一个基本信息 - 服务密钥 - 用于在外部环境(如本地 .NET 应用程序)中使用该服务。
通过单击相应的链接,我们可以看到分配给服务的密钥。每个服务都会创建两个密钥,并且可以随时重新生成。这些 32 个十六进制值将在我们稍后编写的 .NET 应用程序中使用,以连接到服务并执行请求操作。
这涵盖了 Azure 认知服务 Face API 的配置部分。现在一切都已设置好,我们可以开始开发我们的解决方案了。
一个简单的 C# 解决方案
通过 C# 和认知服务,以下目标是创建一个程序,该程序可以处理上传的图像并在其中搜索人脸。不仅如此,每张人脸都应该公开一些附加信息,例如所检测人员的年龄、性别和情绪。我们希望突出 Face API 用于跟踪人脸的 27 个特征。最后,给定一组图像,我们将看到如何训练 Face API 服务以在从未向服务公开过的照片中识别特定人物。
对于第一部分,我们将使用 Winforms,并获得以下结果:
设置环境
在 Visual Studio 中创建一个新项目(Ctrl+N),选择“Visual C”然后选择“Winforms 应用程序”,在为其命名后,继续保存。这将允许我们安装使用 Face API 所需的 NuGet 包。
接下来,我们继续安装 Newtonsoft.Json NuGet 包,然后安装 Microsoft.ProjectOxford.Face(该包将使用 Newtonsoft.Json 与 Web 服务进行通信)。
现在我们可以开始编写代码了。
连接并查询 Web 服务
基于 Web 服务器的 Face API 程序最重要的部分是声明一个 IFaceServiceClient 类型的变量:这允许连接和身份验证到远程终结点。可以使用以下代码轻松声明它:
private readonly IFaceServiceClient faceServiceClient = new FaceServiceClient("<PLACE_YOUR_SERVICE_KEY_HERE>");
您将使用的密钥可以是上面看到的两个密钥之一,也可以是在 Azure 门户创建的密钥。
假设我们有一张我们想分析的图片,我们首先将其作为流读取,然后以这种形式传递给 IFaceServiceClient 的方法 DetectAsync。
DetectAsync 调用可以根据我们的需求和我们想要获取的信息进行自定义。在以下代码片段中,我们将看到如何将图像打开为流,然后调用该方法。
第一个参数是图像流。第二个参数是请求返回 faceId,第三个查询 face landmarks。最后一个是 FaceAttributeType 的数组,用于检索我们希望检查的那些属性。在此代码片段中,我们请求 Id、landmarks 以及对图像中每个人脸的性别、年龄和情绪的分析。
我们将返回的是这些信息的数组。
private async Task<Face[]> UploadAndDetectFaces(string imageFilePath) { try { using (Stream imageFileStream = File.OpenRead(imageFilePath)) { var faces = await faceServiceClient.DetectAsync(imageFileStream, true, true, new FaceAttributeType[] { FaceAttributeType.Gender, FaceAttributeType.Age, FaceAttributeType.Emotion }); return faces.ToArray(); } } catch (Exception ex) { MessageBox.Show(ex.Message); return new Face[0]; } }
检索到这些信息后,如何使用它们取决于需求/偏好。在附带的示例中,我们解析返回的数组以图形方式显示每个读取的属性。让我们看看与上传图像的按钮点击相关的以下代码片段:
private async void btnProcess_Click(object sender, EventArgs e) { Face[] faces = await UploadAndDetectFaces(_imagePath); if (faces.Length > 0) { var faceBitmap = new Bitmap(imgBox.Image); using (var g = Graphics.FromImage(faceBitmap)) { // Alpha-black rectangle on entire image g.FillRectangle(new SolidBrush(Color.FromArgb(200, 0, 0, 0)), g.ClipBounds); var br = new SolidBrush(Color.FromArgb(200, Color.LightGreen)); // Loop each face recognized foreach (var face in faces) { var fr = face.FaceRectangle; var fa = face.FaceAttributes; // Get original face image (color) to overlap the grayed image var faceRect = new Rectangle(fr.Left, fr.Top, fr.Width, fr.Height); g.DrawImage(imgBox.Image, faceRect, faceRect, GraphicsUnit.Pixel); g.DrawRectangle(Pens.LightGreen, faceRect); // Loop face.FaceLandmarks properties for drawing landmark spots var pts = new List<Point>(); Type type = face.FaceLandmarks.GetType(); foreach (PropertyInfo property in type.GetProperties()) { g.DrawRectangle(Pens.LightGreen, GetRectangle((FeatureCoordinate)property.GetValue(face.FaceLandmarks, null))); } // Calculate where to position the detail rectangle int rectTop = fr.Top + fr.Height + 10; if (rectTop + 45 > faceBitmap.Height) rectTop = fr.Top - 30; // Draw detail rectangle and write face informations g.FillRectangle(br, fr.Left - 10, rectTop, fr.Width < 120 ? 120 : fr.Width + 20, 25); g.DrawString(string.Format("{0:0.0} / {1} / {2}", fa.Age, fa.Gender, fa.Emotion.ToRankedList().OrderByDescending(x => x.Value).First().Key), this.Font, Brushes.Black, fr.Left - 8, rectTop + 4); } } imgBox.Image = faceBitmap; } }
如您所见,这段代码没有特别的难点,可以简化为对每个感兴趣元素的简单图形渲染。调用 UploadAndDetectFaces 方法后,会循环遍历得到的数组。原始图像会被黑色覆盖,以更好地突出检测到的人脸,并用矩形框起来,该矩形将单独用绿色绘制。每个 landmark(即 Web 服务用于确定可以定义为人脸的每个点)也以同样的方式绘制,用绿色 2x2 矩形。最后,我们绘制一个矩形,其中包含读取的属性:性别、年龄和情绪。
Emotion API beta 会为一组预定义的情绪(如“快乐”、“悲伤”、“中性”等)分配一个介于 0 和 1 之间的值。
在我们的示例中,通过 LINQ 函数
fa.Emotion.ToRankedList().OrderByDescending(x => x.Value).First().Key
我们将读取占主导地位的情绪,即获得最高值的那个。该方法缺乏精确性,仅作为示例,不声称完美。
使用训练好的 Web 服务识别面孔
Face API 认知服务允许定义人物组,当我们需要训练服务以识别人物时,此功能非常有用。
Azure 需要知道一个名称来分配给特定面孔,以及一定数量的照片(其中包含该面孔)才能学习面孔的细节,并能够理解特定面孔何时出现在新照片中。
要定义一个人物组,我们可以使用以下指令:
await faceServiceClient.CreatePersonGroupAsync(_groupId, _groupName);
其中 _groupId 定义了与组关联的唯一 ID,而 _groupName 仅仅是一个描述符(例如,我们可以有一个组名“我的朋友列表”,组 ID 为“myfriends”)。
假设我们有一个文件夹列表,每个文件夹都以一个人的名字命名,并且包含每个人的一组照片。
我们可以使用类似这样的代码片段将这些图像与组中的给定成员关联起来:
CreatePersonResult person = await faceServiceClient.CreatePersonAsync(_groupId, "Person 1"); foreach (string imagePath in Directory.GetFiles("<PATH_TO_PHOTOS_OF_PERSON_1>")) { using (Stream s = File.OpenRead(imagePath)) { await faceServiceClient.AddPersonFaceAsync(_groupId, person.PersonId, s); } }
如您所见,我们查询 Web 服务以创建一个与给定组相关联的新人物(方法 CreatePersonAsync),然后使用 AddPersonFaceAsync 方法将图像流馈送到服务,并将每个流绑定到创建的人物。通过这种方式,我们可以填充一个需要识别的个体列表。
完成之后,我们可以继续训练服务。这可以通过一条指令完成,但下面的代码片段更复杂一些,因为我们不仅想开始训练,还想检索训练状态,等待其完成,并在完成后通知用户。
await faceServiceClient.TrainPersonGroupAsync(_groupId); TrainingStatus trainingStatus = null; while (true) { trainingStatus = await faceServiceClient.GetPersonGroupTrainingStatusAsync(_groupId); if (trainingStatus.Status != Status.Running) { break; } await Task.Delay(1000); } MessageBox.Show("Training successfully completed");
第一行在给定的 groupId 上启动训练。一个无限循环会检查当前的训练状态(方法 GetPersonGroupTrainingStatusAsync),如果读到的状态与“running”不同,则循环将退出,因为训练已完成。
当一个组被创建、填充并成功训练后,识别就非常简单了。
考虑以下代码:
Face[] faces = await UploadAndDetectFaces(_imagePath); var faceIds = faces.Select(face => face.FaceId).ToArray(); foreach (var identifyResult in await faceServiceClient.IdentifyAsync(_groupId, faceIds)) { if (identifyResult.Candidates.Length != 0) { var candidateId = identifyResult.Candidates[0].PersonId; var person = await faceServiceClient.GetPersonAsync(_groupId, candidateId); // user identificated: person.name is the associated name } else { // user not recognized } }
如上所述,我们需要上传我们希望处理的图片,并使用我们的 UploadAndDetectFaces 函数。
然后,解析从给定组的异步方法 IdentifyAsync 收到的结果,我们可以简单地循环候选人,以查看哪些面孔已被服务识别。
同样,开发者如何处理结果由他们决定。在我们的示例中,已识别人物的姓名被图形化地写在图像上,并插入到 ListBox 中。
源代码
本文使用的源代码可在此处免费下载:https://code.msdn.microsoft.com/Face-Detection-and-5ccb2fcb/
演示项目信息
可下载的 C# 项目允许实验上面看到的所有功能和可能性。以下是对每个项目组件的简要参考,以及与每个组件关联的完整源代码。
btnBrowse 用于加载要处理的特定图像。
private void btnBrowse_Click(object sender, EventArgs e) { using(var od = new OpenFileDialog()) { od.Filter = "All files(*.*)|*.*"; if (od.ShowDialog() == DialogResult.OK) { _imagePath = od.FileName; imgBox.Load(_imagePath); } } }
btnProcess 调用 UploadAndDetectFaces 方法,然后 - 检索结果 - 修改 imgBox 的图形内容,以显示人脸矩形、landmarks 和检测到的信息。
private async void btnProcess_Click(object sender, EventArgs e) { Face[] faces = await UploadAndDetectFaces(_imagePath); if (faces.Length > 0) { var faceBitmap = new Bitmap(imgBox.Image); using (var g = Graphics.FromImage(faceBitmap)) { // Alpha-black rectangle on entire image g.FillRectangle(new SolidBrush(Color.FromArgb(200, 0, 0, 0)), g.ClipBounds); var br = new SolidBrush(Color.FromArgb(200, Color.LightGreen)); // Loop each face recognized foreach (var face in faces) { var fr = face.FaceRectangle; var fa = face.FaceAttributes; // Get original face image (color) to overlap the grayed image var faceRect = new Rectangle(fr.Left, fr.Top, fr.Width, fr.Height); g.DrawImage(imgBox.Image, faceRect, faceRect, GraphicsUnit.Pixel); g.DrawRectangle(Pens.LightGreen, faceRect); // Loop face.FaceLandmarks properties for drawing landmark spots var pts = new List<Point>(); Type type = face.FaceLandmarks.GetType(); foreach (PropertyInfo property in type.GetProperties()) { g.DrawRectangle(Pens.LightGreen, GetRectangle((FeatureCoordinate)property.GetValue(face.FaceLandmarks, null))); } // Calculate where to position the detail rectangle int rectTop = fr.Top + fr.Height + 10; if (rectTop + 45 > faceBitmap.Height) rectTop = fr.Top - 30; // Draw detail rectangle and write face informations g.FillRectangle(br, fr.Left - 10, rectTop, fr.Width < 120 ? 120 : fr.Width + 20, 25); g.DrawString(string.Format("{0:0.0} / {1} / {2}", fa.Age, fa.Gender, fa.Emotion.ToRankedList().OrderByDescending(x => x.Value).First().Key), this.Font, Brushes.Black, fr.Left - 8, rectTop + 4); } } imgBox.Image = faceBitmap; } }
btnAddUser 将输入的字符串添加到要识别的人物列表中。组创建和训练按说明进行:用户首先添加所有需要识别的人名。这些名称对应于 txtImageFolder.Text 的子文件夹名称。当按下 btnCreateGroup 时,程序根据 txtGroupName 中输入的名称创建组,然后循环遍历 txtImageFolder 的子文件夹,为每个用户查找。对于每个文件,它会将照片上传到服务器,并将其与 groupId / personId 相关联。
private void btnAddUser_Click(object sender, EventArgs e) { if (txtNewUser.Text == "") return; listUsers.Items.Add(txtNewUser.Text); } private async void btnCreateGroup_Click(object sender, EventArgs e) { try { _groupId = txtGroupName.Text.ToLower().Replace(" ", ""); try { await faceServiceClient.DeletePersonGroupAsync(_groupId); } catch { } await faceServiceClient.CreatePersonGroupAsync(_groupId, txtGroupName.Text); foreach (var u in listUsers.Items) { CreatePersonResult person = await faceServiceClient.CreatePersonAsync(_groupId, u.ToString()); foreach (string imagePath in Directory.GetFiles(txtImageFolder.Text + "\\" + u.ToString())) { using (Stream s = File.OpenRead(imagePath)) { await faceServiceClient.AddPersonFaceAsync(_groupId, person.PersonId, s); } } await Task.Delay(1000); } MessageBox.Show("Group successfully created"); } catch (Exception ex) { MessageBox.Show(ex.ToString()); } }
btnTrain 启动训练过程。
private async void btnTrain_Click(object sender, EventArgs e) { try { await faceServiceClient.TrainPersonGroupAsync(_groupId); TrainingStatus trainingStatus = null; while (true) { trainingStatus = await faceServiceClient.GetPersonGroupTrainingStatusAsync(_groupId); if (trainingStatus.Status != Status.Running) { break; } await Task.Delay(1000); } MessageBox.Show("Training successfully completed"); } catch (Exception ex) { MessageBox.Show(ex.Message); } }
而 btnIdentify 则对给定照片中的人脸进行识别,将检测到的人物名称添加到图像(在正确的人脸位置)以及名称列表中。
private async void btnIdentify_Click(object sender, EventArgs e) { try { Face[] faces = await UploadAndDetectFaces(_imagePath); var faceIds = faces.Select(face => face.FaceId).ToArray(); var faceBitmap = new Bitmap(imgBox.Image); idList.Items.Clear(); using (var g = Graphics.FromImage(faceBitmap)) { foreach (var identifyResult in await faceServiceClient.IdentifyAsync(_groupId, faceIds)) { if (identifyResult.Candidates.Length != 0) { var candidateId = identifyResult.Candidates[0].PersonId; var person = await faceServiceClient.GetPersonAsync(_groupId, candidateId); // Writes name above face rectangle var x = faces.FirstOrDefault(y => y.FaceId == identifyResult.FaceId); if (x != null) { g.DrawString(person.Name, this.Font, Brushes.White, x.FaceRectangle.Left, x.FaceRectangle.Top + x.FaceRectangle.Height + 15); } idList.Items.Add(person.Name); } else { idList.Items.Add("< Unknown person >"); } } } imgBox.Image = faceBitmap; MessageBox.Show("Identification successfully completed"); } catch (Exception ex) { MessageBox.Show(ex.Message); } }