.NET做人脸识别并分类的实现示例

在游乐场、玻璃天桥、滑雪场等娱乐场所,经常能看到有摄影师在拍照片,令这些经营者发愁的一件事就是照片太多了,客户在成千上万张照片中找到自己可不是件容易的事。在一次游玩等活动或家庭聚会也同理,太多了照片导致挑选十分困难。

还好有.NET,只需少量代码,即可轻松找到人脸并完成分类。

本文将使用Microsoft Azure云提供的认知服务Cognitive ServicesAPI来识别并进行人脸分类,可以免费使用,注册地址是:https://portal.azure.com。注册完成后,会得到两个密钥,通过这个密钥即可完成本文中的所有代码,这个密钥长这个样子(非真实密钥):

fa3a7bfd807ccd6b17cf559ad584cbaa

使用方法

首先安装NuGetMicrosoft.Azure.CognitiveServices.Vision.Face,目前最新版是2.5.0-preview.1,然后创建一个FaceClient

string key = "fa3a7bfd807ccd6b17cf559ad584cbaa"; // 替换为你的key
using var fc = new FaceClient(new ApiKeyServiceClientCredentials(key))
{
  Endpoint = "https://southeastasia.api.cognitive.microsoft.com",
};

然后识别一张照片:

using var file = File.OpenRead(@"C:\Photos\DSC_996ICU.JPG");
IList<DetectedFace> faces = await fc.Face.DetectWithStreamAsync(file);

其中返回的faces是一个IList结构,很显然一次可以识别出多个人脸,其中一个示例返回结果如下(已转换为JSON):

[
  {
   "FaceId": "9997b64e-6e62-4424-88b5-f4780d3767c6",
   "RecognitionModel": null,
   "FaceRectangle": {
    "Width": 174,
    "Height": 174,
    "Left": 62,
    "Top": 559
   },
   "FaceLandmarks": null,
   "FaceAttributes": null
  },
  {
   "FaceId": "8793b251-8cc8-45c5-ab68-e7c9064c4cfd",
   "RecognitionModel": null,
   "FaceRectangle": {
    "Width": 152,
    "Height": 152,
    "Left": 775,
    "Top": 580
   },
   "FaceLandmarks": null,
   "FaceAttributes": null
  }
 ]

可见,该照片返回了两个DetectedFace对象,它用FaceId保存了其Id,用于后续的识别,用FaceRectangle保存了其人脸的位置信息,可供对其做进一步操作。RecognitionModelFaceLandmarksFaceAttributes是一些额外属性,包括识别性别年龄表情等信息,默认不识别,如下图API所示,可以通过各种参数配置,非常好玩,有兴趣的可以试试:

最后,通过.GroupAsync来将之前识别出的多个faceId进行分类:

var faceIds = faces.Select(x => x.FaceId.Value).ToList();
GroupResult reslut = await fc.Face.GroupAsync(faceIds);

返回了一个GroupResult,其对象定义如下:

public class GroupResult
{
  public IList<IList<Guid>> Groups
  {
    get;
    set;
  }

  public IList<Guid> MessyGroup
  {
    get;
    set;
  }

  // ...
}

包含了一个Groups对象和一个MessyGroup对象,其中Groups是一个数据的数据,用于存放人脸的分组,MessyGroup用于保存未能找到分组的FaceId

有了这个,就可以通过一小段简短的代码,将不同的人脸组,分别复制对应的文件夹中:

void CopyGroup(string outputPath, GroupResult result, Dictionary<Guid, (string file, DetectedFace face)> faces)
{
  foreach (var item in result.Groups
    .SelectMany((group, index) => group.Select(v => (faceId: v, index)))
    .Select(x => (info: faces[x.faceId], i: x.index + 1)).Dump())
  {
    string dir = Path.Combine(outputPath, item.i.ToString());
    Directory.CreateDirectory(dir);
    File.Copy(item.info.file, Path.Combine(dir, Path.GetFileName(item.info.file)), overwrite: true);
  }

  string messyFolder = Path.Combine(outputPath, "messy");
  Directory.CreateDirectory(messyFolder);
  foreach (var file in result.MessyGroup.Select(x => faces[x].file).Distinct())
  {
    File.Copy(file, Path.Combine(messyFolder, Path.GetFileName(file)), overwrite: true);
  }
}

然后就能得到运行结果,如图,我传入了102张照片,输出了15个分组和一个“未找到队友”的分组:

还能有什么问题?

就两个API调用而已,代码一把梭,感觉太简单了?其实不然,还会有很多问题。

图片太大,需要压缩

毕竟要把图片上传到云服务中,如果上传网速不佳,流量会挺大,而且现在的手机、单反、微单都能轻松达到好几千万像素,jpg大小轻松上10MB,如果不压缩就上传,一来流量和速度遭不住。

二来……其实Azure也不支持,文档(https://docs.microsoft.com/en-us/rest/api/cognitiveservices/face/face/detectwithstream)显示,最大仅支持6MB的图片,且图片大小应不大于1920x1080的分辨率:

  • JPEG, PNG, GIF (the first frame), and BMP format are supported. The allowed image file size is from 1KB to 6MB.
  • The minimum detectable face size is 36x36 pixels in an image no larger than 1920x1080 pixels. Images with dimensions higher than 1920x1080 pixels will need a proportionally larger minimum face size.

因此,如果图片太大,必须进行一定的压缩(当然如果图片太小,显然也没必要进行压缩了),使用.NETBitmap,并结合C# 8.0switch expression,这个判断逻辑以及压缩代码可以一气呵成:

byte[] CompressImage(string image, int edgeLimit = 1920)
{
  using var bmp = Bitmap.FromFile(image);

  using var resized = (1.0 * Math.Max(bmp.Width, bmp.Height) / edgeLimit) switch
  {
    var x when x > 1 => new Bitmap(bmp, new Size((int)(bmp.Size.Width / x), (int)(bmp.Size.Height / x))),
    _ => bmp,
  };

  using var ms = new MemoryStream();
  resized.Save(ms, ImageFormat.Jpeg);
  return ms.ToArray();
}

竖立的照片

相机一般都是3:2的传感器,拍出来的照片一般都是横向的。但偶尔寻求一些构图的时候,我们也会选择纵向构图。虽然现在许多API都支持正负30度的侧脸,但竖着的脸API基本都是不支持的,如下图(实在找不到可以授权使用照片的模特了😂):

还好照片在拍摄后,都会保留exif信息,只需读取exif信息并对照片做相应的旋转即可:

void HandleOrientation(Image image, PropertyItem[] propertyItems)
{
  const int exifOrientationId = 0x112;
  PropertyItem orientationProp = propertyItems.FirstOrDefault(i => i.Id == exifOrientationId);

  if (orientationProp == null) return;

  int val = BitConverter.ToUInt16(orientationProp.Value, 0);
  RotateFlipType rotateFlipType = val switch
  {
    2 => RotateFlipType.RotateNoneFlipX,
    3 => RotateFlipType.Rotate180FlipNone,
    4 => RotateFlipType.Rotate180FlipX,
    5 => RotateFlipType.Rotate90FlipX,
    6 => RotateFlipType.Rotate90FlipNone,
    7 => RotateFlipType.Rotate270FlipX,
    8 => RotateFlipType.Rotate270FlipNone,
    _ => RotateFlipType.RotateNoneFlipNone,
  };

  if (rotateFlipType != RotateFlipType.RotateNoneFlipNone)
  {
    image.RotateFlip(rotateFlipType);
  }
}

旋转后,我的照片如下:

这样竖拍的照片也能识别出来了。

并行速度

前文说过,一个文件夹可能会有成千上万个文件,一个个上传识别,速度可能慢了点,它的代码可能长这个样子:

Dictionary<Guid, (string file, DetectedFace face)> faces = GetFiles(inFolder)
 .Select(file =>
 {
  byte[] bytes = CompressImage(file);
  var result = (file, faces: fc.Face.DetectWithStreamAsync(new MemoryStream(bytes)).GetAwaiter().GetResult());
  (result.faces.Count == 0 ? $"{file} not detect any face!!!" : $"{file} detected {result.faces.Count}.").Dump();
  return (file, faces: result.faces.ToList());
 })
 .SelectMany(x => x.faces.Select(face => (x.file, face)))
 .ToDictionary(x => x.face.FaceId.Value, x => (file: x.file, face: x.face));

要想把速度变化,可以启用并行上传,有了C#/.NETLINQ支持,只需加一行.AsParallel()即可完成:

Dictionary<Guid, (string file, DetectedFace face)> faces = GetFiles(inFolder)
 .AsParallel() // 加的就是这行代码
 .Select(file =>
 {
  byte[] bytes = CompressImage(file);
  var result = (file, faces: fc.Face.DetectWithStreamAsync(new MemoryStream(bytes)).GetAwaiter().GetResult());
  (result.faces.Count == 0 ? $"{file} not detect any face!!!" : $"{file} detected {result.faces.Count}.").Dump();
  return (file, faces: result.faces.ToList());
 })
 .SelectMany(x => x.faces.Select(face => (x.file, face)))
 .ToDictionary(x => x.face.FaceId.Value, x => (file: x.file, face: x.face));

断点续传

也如上文所说,有成千上万张照片,如果一旦网络传输异常,或者打翻了桌子上的咖啡(谁知道呢?)……或者完全一切正常,只是想再做一些其它的分析,所有东西又要重新开始。我们可以加入下载中常说的“断点续传”机制。

其实就是一个缓存,记录每个文件读取的结果,然后下次运行时先从缓存中读取即可,缓存到一个json文件中:

Dictionary<Guid, (string file, DetectedFace face)> faces = GetFiles(inFolder)
 .AsParallel() // 加的就是这行代码
 .Select(file =>
 {
  byte[] bytes = CompressImage(file);
  var result = (file, faces: fc.Face.DetectWithStreamAsync(new MemoryStream(bytes)).GetAwaiter().GetResult());
  (result.faces.Count == 0 ? $"{file} not detect any face!!!" : $"{file} detected {result.faces.Count}.").Dump();
  return (file, faces: result.faces.ToList());
 })
 .SelectMany(x => x.faces.Select(face => (x.file, face)))
 .ToDictionary(x => x.face.FaceId.Value, x => (file: x.file, face: x.face));

注意代码下方有一个lock关键字,是为了保证多线程下载时的线程安全。

使用时,只需只需在Select中添加一行代码即可:

var cache = new Cache<List<DetectedFace>>(); // 重点
Dictionary<Guid, (string file, DetectedFace face)> faces = GetFiles(inFolder)
 .AsParallel()
 .Select(file => (file: file, faces: cache.GetOrCreate(file, () => // 重点
 {
  byte[] bytes = CompressImage(file);
  var result = (file, faces: fc.Face.DetectWithStreamAsync(new MemoryStream(bytes)).GetAwaiter().GetResult());
  (result.faces.Count == 0 ? $"{file} not detect any face!!!" : $"{file} detected {result.faces.Count}.").Dump();
  return result.faces.ToList();
 })))
 .SelectMany(x => x.faces.Select(face => (x.file, face)))
 .ToDictionary(x => x.face.FaceId.Value, x => (file: x.file, face: x.face));

将人脸框起来

照片太多,如果活动很大,或者合影中有好几十个人,分出来的组,将长这个样子:

完全不知道自己的脸在哪,因此需要将检测到的脸框起来。

注意框起来的过程,也很有技巧,回忆一下,上传时的照片本来就是压缩和旋转过的,因此返回的DetectedFace对象值,它也是压缩和旋转过的,如果不进行压缩和旋转,找到的脸的位置会完全不正确,因此需要将之前的计算过程重新演算一次:

using var bmp = Bitmap.FromFile(item.info.file);
HandleOrientation(bmp, bmp.PropertyItems);
using (var g = Graphics.FromImage(bmp))
{
 using var brush = new SolidBrush(Color.Red);
 using var pen = new Pen(brush, 5.0f);
 var rect = item.info.face.FaceRectangle;
 float scale = Math.Max(1.0f, (float)(1.0 * Math.Max(bmp.Width, bmp.Height) / 1920.0));
 g.ScaleTransform(scale, scale);
 g.DrawRectangle(pen, new Rectangle(rect.Left, rect.Top, rect.Width, rect.Height));
}
bmp.Save(Path.Combine(dir, Path.GetFileName(item.info.file)));

使用我上面的那张照片,检测结果如下(有点像相机对焦时人脸识别的感觉):

1000个脸的限制

.GroupAsync方法一次只能检测1000FaceId,而上次活动800多张照片中有超过2000FaceId,因此需要做一些必要的分组。

分组最简单的方法,就是使用System.Interactive包,它提供了Rx.NET那样方便快捷的API(这些APILINQ中未提供),但又不需要引入Observable<T>那样重量级的东西,因此使用起来很方便。

这里我使用的是.Buffer(int)函数,它可以将IEnumerable<T>按指定的数量(如1000)进行分组,代码如下:

foreach (var buffer in faces
 .Buffer(1000)
 .Select((list, groupId) => (list, groupId))
{
 GroupResult group = await fc.Face.GroupAsync(buffer.list.Select(x => x.Key).ToList());
 var folder = outFolder + @"\gid-" + buffer.groupId;
 CopyGroup(folder, group, faces);
}

总结

文中用到的完整代码,全部上传了到我的博客数据Github,只要输入图片和key,即可直接使用和运行:
https://github.com/sdcb/blog-data/tree/master/2019/20191122-dotnet-face-detection

这个月我参加了上海的.NET Conf,我上述代码对.NET Conf800多张照片做了分组,识别出了2000多张人脸,我将其中我的照片的前三张找出来,结果如下:


......

总的来说,这个效果还挺不错,渣渣分辨率的照片的脸都被它找到了😂。

注意,不一定非得用Azure Cognitive Services来做人脸识别,国内还有阿里云等厂商也提供了人脸识别等服务,并提供了.NET接口,无非就是调用API,注意其限制,代码总体差不多。

另外,如有离线人脸识别需求,Luxand提供了还有离线版人脸识别SDK,名叫Luxand FaceSDK,同样提供了.NET接口。因为无需网络调用,其识别更快,匹配速度更是可达每秒5千万个人脸数据,精度也非常高,亲测好用,目前最新版是v7.1.0,授权昂贵(但百度有惊喜)。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • python3+dlib实现人脸识别和情绪分析

    一.介绍 我想做的是基于人脸识别的表情(情绪)分析.看到网上也是有很多的开源库提供使用,为开发提供了很大的方便.我选择目前用的比较多的dlib库进行人脸识别与特征标定.使用python也缩短了开发周期. 官网对于dlib的介绍是:Dlib包含广泛的机器学习算法.所有的设计都是高度模块化的,快速执行,并且通过一个干净而现代的C ++ API,使用起来非常简单.它用于各种应用,包括机器人技术,嵌入式设备,手机和大型高性能计算环境. 虽然应用都比较高大上,但是自己在PC上做个情绪分析的小软件还是挺有意

  • python实现人脸识别经典算法(一) 特征脸法

    近来想要做一做人脸识别相关的内容,主要是想集成一个系统,看到opencv已经集成了三种性能较好的算法,但是还是想自己动手试一下,毕竟算法都比较初级. 操作环境:python2.7 第三方库:opencv for python.numpy 第一种比较经典的算法就是特征脸法,本质上其实就是PCA降维,这种算法的基本思路是,把二维的图像先灰度化,转化为一通道的图像,之后再把它首尾相接转化为一个列向量,假设图像大小是20*20的,那么这个向量就是400维,理论上讲组织成一个向量,就可以应用任何机器学习算

  • 微信小程序实现人脸识别

    本文为大家分享了微信小程序人脸识别的具体代码,供大家参考,具体内容如下 首先,我们要有开发者工具,今天所说的是后端和前端联合起来实现的. 在PHP的控制器中写一个upload方法,代码如下: public function upload($id=''){ if(empty($id)){ return false; } $no = M("student")->where("id={$id}")->getField('no'); $dir = ".

  • 基于OpenCV的PHP图像人脸识别技术

    openCV是一个开源的用C/C++开发的计算机图形图像库,非常强大,研究资料很齐全.本文重点是介绍如何使用php来调用其中的局部的功能.人脸侦查技术只是openCV一个应用分支. 1.安装 从源代码编译成一个动态的so文件. 1.1.安装 OpenCV (OpenCV 1.0.0) 下载地址:http://sourceforge.net/project/showfiles.php?group_id=22870&package_id=16948 #tar xvzf OpenCV-1.0.0.ta

  • Android camera实时预览 实时处理,人脸识别示例

    Android camera实时预览 实时处理,面部认证. 预览操作是网友共享的代码,我在继承SurfaceView 的CameraSurfaceView 中加入了帧监听事件,每次预览监听前五个数据帧,在处理做一个面部识别. 先看目录关系 自定义控件CameraSurfaceView.java 自定义接口方法CameraInterface.java CameraActivity预览界面. CameraSurfaceView.Java package com.centaur.camera.prev

  • JavaScript人脸识别技术及脸部识别JavaScript类库Tracking.js

    我一直对人工智能识别技术非常感兴趣,因为我无法想象这究竟是一种什么样的算法,什么样的分析过程.无论是声音识别.人脸识别或其它种识别,人们的外貌.说话的方式都是如此不同,一种图片你可以用不同的方式.从不同的角度拍摄,我不能理解这些识别技术是如何做到的.有个叫做"面具"的游戏也使用了这种识别技术,我想对于脸部识别技术也应该研究一下.Facebook使用了这种技术,在手势控制中也能用到它,所以,你网站上也会有应用的地方. 我找到的一个可以用于人脸识别的JavaScript程序包是Face D

  • 详解如何用OpenCV + Python 实现人脸识别

    下午的时候,配好了OpenCV的Python环境,OpenCV的Python环境搭建.于是迫不及待的想体验一下opencv的人脸识别,如下文. 必备知识 Haar-like 通俗的来讲,就是作为人脸特征即可. Haar特征值反映了图像的灰度变化情况.例如:脸部的一些特征能由矩形特征简单的描述,如:眼睛要比脸颊颜色要深,鼻梁两侧比鼻梁颜色要深,嘴巴比周围颜色要深等. opencv api 要想使用opencv,就必须先知道其能干什么,怎么做.于是API的重要性便体现出来了.就本例而言,使用到的函数

  • 人脸识别经典算法一 特征脸方法(Eigenface)

    这篇文章是撸主要介绍人脸识别经典方法的第一篇,后续会有其他方法更新.特征脸方法基本是将人脸识别推向真正可用的第一种方法,了解一下还是很有必要的.特征脸用到的理论基础PCA在另一篇博客里:特征脸(Eigenface)理论基础-PCA(主成分分析法).本文的参考资料附在最后了^_^ 步骤一:获取包含M张人脸图像的集合S.在我们的例子里有25张人脸图像(虽然是25个不同人的人脸的图像,但是看着怎么不像呢,难道我有脸盲症么),如下图所示哦.每张图像可以转换成一个N维的向量(是的,没错,一个像素一个像素的

  • opencv 做人脸识别 opencv 人脸匹配分析

    机器学习 机器学习的目的是把数据转换成信息. 机器学习通过从数据里提取规则或模式来把数据转成信息. 人脸识别 人脸识别通过级联分类器对特征的分级筛选来确定是否是人脸. 每个节点的正确识别率很高,但正确拒绝率很低. 任一节点判断没有人脸特征则结束运算,宣布不是人脸. 全部节点通过,则宣布是人脸. 工业上,常用人脸识别技术来识别物体. 对图片进行识别 复制代码 代码如下: #include "opencv2/core/core.hpp" #include "opencv2/obj

  • Python3结合Dlib实现人脸识别和剪切

    0.引言 利用python开发,借助Dlib库进行人脸识别,然后将检测到的人脸剪切下来,依次排序显示在新的图像上: 实现的效果如下图所示,将图1原图中的6张人脸检测出来,然后剪切下来,在图像窗口中依次输出显示人脸: 实现比较简单,代码量也比较少,适合入门或者兴趣学习. 图1 原图和处理后得到的图像窗口 1.开发环境 python: 3.6.3 dlib: 19.7 OpenCv, numpy import dlib # 人脸识别的库dlib import numpy as np # 数据处理的库

  • PHP使用Face++接口开发微信公众平台人脸识别系统的方法

    本文实例讲述了PHP使用Face++接口开发微信公众平台人脸识别系统的方法.分享给大家供大家参考.具体如下: 效果图如下: 具体步骤如下: 首先,先登录Face++的官网注册账号:官网链接 注册之后会获取到api_secret和api_key,这些在调用接口的时候需要用到. 然后接下来的就是使用PHP脚本调用API了. 在使用PHP开发微信公共平台的时候,推荐使用Github上的一款不错的框架:wechat-php-sdk 对于微信的常用接口做了一些封装,核心文件wechat.class.php

  • android实现人脸识别技术的示例代码

    1.前沿 人工智能时代快速来临,其中人脸识别是当前比较热门的技术,在国内也越来越多的运用,例如刷脸打卡,刷脸APP,身份识别,人脸门禁等.当前的人脸识别技术分为WEBAPI和SDK调用两种方式,WEBAPI需要实时联网,SDK调用可以离线使用. 本次使用的虹软提供的人脸识别的SDK,此SDK也可根据不同应用场景设计,针对性强.包括人脸检测.人脸跟踪.人脸识别,即使在离线环境下也可正常运行. 虹软公司是一家具有硅谷背景的图像处理公司,除了人脸技术以外,还有多项图像及视频处理技术.他们的双摄像头处理

随机推荐