.NET做人脸识别并分类

前言

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

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

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

  1. fa3a7bfd807ccd6b17cf559ad584cbaa

使用方法

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

  1. string key = "fa3a7bfd807ccd6b17cf559ad584cbaa"; // 替换为你的key

  2. using var fc = new FaceClient(new ApiKeyServiceClientCredentials(key))

  3. {

  4. Endpoint = "https://southeastasia.api.cognitive.microsoft.com",

  5. };

然后识别一张照片:

  1. using var file = File.OpenRead(@"C:\Photos\DSC_996ICU.JPG");

  2. IList<DetectedFace> faces = await fc.Face.DetectWithStreamAsync(file);

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

  1. [

  2. {

  3. "FaceId": "9997b64e-6e62-4424-88b5-f4780d3767c6",

  4. "RecognitionModel": null,

  5. "FaceRectangle": {

  6. "Width": 174,

  7. "Height": 174,

  8. "Left": 62,

  9. "Top": 559

  10. },

  11. "FaceLandmarks": null,

  12. "FaceAttributes": null

  13. },

  14. {

  15. "FaceId": "8793b251-8cc8-45c5-ab68-e7c9064c4cfd",

  16. "RecognitionModel": null,

  17. "FaceRectangle": {

  18. "Width": 152,

  19. "Height": 152,

  20. "Left": 775,

  21. "Top": 580

  22. },

  23. "FaceLandmarks": null,

  24. "FaceAttributes": null

  25. }

  26. ]

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

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

  1. var faceIds = faces.Select(x => x.FaceId.Value).ToList();

  2. GroupResult reslut = await fc.Face.GroupAsync(faceIds);

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

  1. public class GroupResult

  2. {

  3. public IList<IList<Guid>> Groups

  4. {

  5. get;

  6. set;

  7. }

  8. public IList<Guid> MessyGroup

  9. {

  10. get;

  11. set;

  12. }

  13. // ...

  14. }

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

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

  1. void CopyGroup(string outputPath, GroupResult result, Dictionary<Guid, (string file, DetectedFace face)> faces)

  2. {

  3. foreach (var item in result.Groups

  4. .SelectMany((group, index) => group.Select(v => (faceId: v, index)))

  5. .Select(x => (info: faces[x.faceId], i: x.index + 1)).Dump())

  6. {

  7. string dir = Path.Combine(outputPath, item.i.ToString());

  8. Directory.CreateDirectory(dir);

  9. File.Copy(item.info.file, Path.Combine(dir, Path.GetFileName(item.info.file)), overwrite: true);

  10. }

  11. string messyFolder = Path.Combine(outputPath, "messy");

  12. Directory.CreateDirectory(messyFolder);

  13. foreach (var file in result.MessyGroup.Select(x => faces[x].file).Distinct())

  14. {

  15. File.Copy(file, Path.Combine(messyFolder, Path.GetFileName(file)), overwrite: true);

  16. }

  17. }

然后就能得到运行结果,如图,我传入了 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.

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

  1. byte[] CompressImage(string image, int edgeLimit = 1920)

  2. {

  3. using var bmp = Bitmap.FromFile(image);

  4. using var resized = (1.0 * Math.Max(bmp.Width, bmp.Height) / edgeLimit) switch

  5. {

  6. var x when x > 1 => new Bitmap(bmp, new Size((int)(bmp.Size.Width / x), (int)(bmp.Size.Height / x))),

  7. _ => bmp,

  8. };

  9. using var ms = new MemoryStream();

  10. resized.Save(ms, ImageFormat.Jpeg);

  11. return ms.ToArray();

  12. }

竖立的照片

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

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

  1. void HandleOrientation(Image image, PropertyItem[] propertyItems)

  2. {

  3. const int exifOrientationId = 0x112;

  4. PropertyItem orientationProp = propertyItems.FirstOrDefault(i => i.Id == exifOrientationId);

  5. if (orientationProp == null) return;

  6. int val = BitConverter.ToUInt16(orientationProp.Value, 0);

  7. RotateFlipType rotateFlipType = val switch

  8. {

  9. 2 => RotateFlipType.RotateNoneFlipX,

  10. 3 => RotateFlipType.Rotate180FlipNone,

  11. 4 => RotateFlipType.Rotate180FlipX,

  12. 5 => RotateFlipType.Rotate90FlipX,

  13. 6 => RotateFlipType.Rotate90FlipNone,

  14. 7 => RotateFlipType.Rotate270FlipX,

  15. 8 => RotateFlipType.Rotate270FlipNone,

  16. _ => RotateFlipType.RotateNoneFlipNone,

  17. };

  18. if (rotateFlipType != RotateFlipType.RotateNoneFlipNone)

  19. {

  20. image.RotateFlip(rotateFlipType);

  21. }

  22. }

旋转后,我的照片如下: 

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

并行速度

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

  1. Dictionary<Guid, (string file, DetectedFace face)> faces = GetFiles(inFolder)

  2. .Select(file =>

  3. {

  4. byte[] bytes = CompressImage(file);

  5. var result = (file, faces: fc.Face.DetectWithStreamAsync(new MemoryStream(bytes)).GetAwaiter().GetResult());

  6. (result.faces.Count == 0 ? $"{file} not detect any face!!!" : $"{file} detected {result.faces.Count}.").Dump();

  7. return (file, faces: result.faces.ToList());

  8. })

  9. .SelectMany(x => x.faces.Select(face => (x.file, face)))

  10. .ToDictionary(x => x.face.FaceId.Value, x => (file: x.file, face: x.face));

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

  1. Dictionary<Guid, (string file, DetectedFace face)> faces = GetFiles(inFolder)

  2. .AsParallel() // 加的就是这行代码

  3. .Select(file =>

  4. {

  5. byte[] bytes = CompressImage(file);

  6. var result = (file, faces: fc.Face.DetectWithStreamAsync(new MemoryStream(bytes)).GetAwaiter().GetResult());

  7. (result.faces.Count == 0 ? $"{file} not detect any face!!!" : $"{file} detected {result.faces.Count}.").Dump();

  8. return (file, faces: result.faces.ToList());

  9. })

  10. .SelectMany(x => x.faces.Select(face => (x.file, face)))

  11. .ToDictionary(x => x.face.FaceId.Value, x => (file: x.file, face: x.face));

断点续传

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

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

  1. class Cache<T>

  2. {

  3. static string cacheFile = outFolder + @$"\cache-{typeof(T).Name}.json";

  4. Dictionary<string, T> cachingData;

  5. public Cache()

  6. {

  7. cachingData = File.Exists(cacheFile) switch

  8. {

  9. true => JsonSerializer.Deserialize<Dictionary<string, T>>(File.ReadAllBytes(cacheFile)),

  10. _ => new Dictionary<string, T>()

  11. };

  12. }

  13. public T GetOrCreate(string key, Func<T> fetchMethod)

  14. {

  15. if (cachingData.TryGetValue(key, out T cachedValue))

  16. {

  17. return cachedValue;

  18. }

  19. var realValue = fetchMethod();

  20. lock(this)

  21. {

  22. cachingData[key] = realValue;

  23. File.WriteAllBytes(cacheFile, JsonSerializer.SerializeToUtf8Bytes(cachingData, new JsonSerializerOptions

  24. {

  25. WriteIndented = true,

  26. }));

  27. return realValue;

  28. }

  29. }

  30. }

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

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

  1. var cache = new Cache<List<DetectedFace>>(); // 重点

  2. Dictionary<Guid, (string file, DetectedFace face)> faces = GetFiles(inFolder)

  3. .AsParallel()

  4. .Select(file => (file: file, faces: cache.GetOrCreate(file, () => // 重点

  5. {

  6. byte[] bytes = CompressImage(file);

  7. var result = (file, faces: fc.Face.DetectWithStreamAsync(new MemoryStream(bytes)).GetAwaiter().GetResult());

  8. (result.faces.Count == 0 ? $"{file} not detect any face!!!" : $"{file} detected {result.faces.Count}.").Dump();

  9. return result.faces.ToList();

  10. })))

  11. .SelectMany(x => x.faces.Select(face => (x.file, face)))

  12. .ToDictionary(x => x.face.FaceId.Value, x => (file: x.file, face: x.face));

将人脸框起来

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

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

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

  1. using var bmp = Bitmap.FromFile(item.info.file);

  2. HandleOrientation(bmp, bmp.PropertyItems);

  3. using (var g = Graphics.FromImage(bmp))

  4. {

  5. using var brush = new SolidBrush(Color.Red);

  6. using var pen = new Pen(brush, 5.0f);

  7. var rect = item.info.face.FaceRectangle;

  8. float scale = Math.Max(1.0f, (float)(1.0 * Math.Max(bmp.Width, bmp.Height) / 1920.0));

  9. g.ScaleTransform(scale, scale);

  10. g.DrawRectangle(pen, new Rectangle(rect.Left, rect.Top, rect.Width, rect.Height));

  11. }

  12. bmp.Save(Path.Combine(dir, Path.GetFileName(item.info.file)));

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

1000个脸的限制

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

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

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

  1. foreach (var buffer in faces

  2. .Buffer(1000)

  3. .Select((list, groupId) => (list, groupId))

  4. {

  5. GroupResult group = await fc.Face.GroupAsync(buffer.list.Select(x => x.Key).ToList());

  6. var folder = outFolder + @"\gid-" + buffer.groupId;

  7. CopyGroup(folder, group, faces);

  8. }

总结

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

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

 ......

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

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

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

微信不能留言,有想法的朋友,欢迎前往我的博客园进行评论、点赞:https://www.cnblogs.com/sdflysha/p/20191122-dotnet-face-detection.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/312894.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

.NET Core 3.0中用 Code-First 方式创建 gRPC 服务与客户端

.NET Core ❤ gRPC千呼万唤的 .NET Core 3.0 终于在 9 月份正式发布&#xff0c;在它的众多新特性中&#xff0c;除了性能得到了大大提高&#xff0c;比较受关注的应该是 ASP.NET Core 3.0 对 gRPC 的集成了。它的源码托管在 grpc-dotnet 这个 Github 库中&#xff0c;由微软 .…

dotnet Blazor 用 C# 控制界面行为

微软很久就在做 Blazor 但是我现在才开始创建一个测试项目&#xff0c;我想用 C# 去控制 HTML 界面。小伙伴也许会问现在前端不是烂大街么&#xff0c;为什么还需要 Blazor 来做。可能原因只有一个&#xff0c;就是可以使用 C# 写脚本&#xff0c;代码比较清真用 VisualStudio …

2019年该学习哪门语言?建议学习C#语言

世界上只有少数几种语言是多功能的&#xff0c;而没有一个像C#那样干净整洁。作者 | Arctek译者 | 谭开朗&#xff0c;责编 | 郭芮出品 | CSDN&#xff08;ID&#xff1a;CSDNnews&#xff09;以下为译文&#xff1a;最直接的答案是&#xff1a;值得。但我想你不是来找这样的答…

不一样的 SQL Server 日期格式化

不一样的 SQL Server 日期格式化Intro最近统计一些数据&#xff0c;需要按天/按小时/按分钟来统计&#xff0c;涉及到一些日期的格式化&#xff0c;网上看了一些文章大部分都是使用 CONVERT 来转换的&#xff0c;SQL Server 从 2012 开始增加了 FORMAT 方法&#xff0c;可以使用…

怕被政治烧到,RISC-V基金会决定迁址瑞士

由于政治影响&#xff0c;RISC-V 基金会决定迁址瑞士。FILE PHOTO: Technology on display at Huaweis headquarters in Shenzhen, Guangdong province, China May 29, 2019. REUTERS/Jason Lee去年 12 月份&#xff0c;RISC-V 基金会在一次会议上宣布&#xff0c;它将迁址到一…

进程和线程的状态

一、进程的基本状态 进程经常讨论的基本状态为&#xff1a;就绪状态&#xff08;Ready&#xff09;、运行状态&#xff08;Running&#xff09;、阻塞状态&#xff08;Blocked&#xff09;。此外&#xff0c;还包括不常讨论的创建和结束。 就绪状态&#xff1a;当进程已分配到除…

ASP.NET Core快速入门(第6章:ASP.NET Core MVC)--学习笔记

点击蓝字关注我们课程链接&#xff1a;http://video.jessetalk.cn/course/explore良心课程&#xff0c;大家一起来学习哈&#xff01;任务40&#xff1a;介绍1.Individual authentication 模板2.EF Core Migration3.Identity MVC&#xff1a;UI4.Identity MVC&#xff1a;EF I…

EF Core For MySql查询中使用DateTime.Now作为查询条件的一个小问题

背景最近一直忙于手上澳洲线上项目的整体迁移和升级的准备工作&#xff0c;导致博客和公众号停更。本周终于艰难的完成了任务&#xff0c;借此机会&#xff0c;总结一下项目中遇到的一些问题。EF Core 一直是我们团队中中小型项目常用的 ORM 框架&#xff0c;在使用 SQL Server…

进程的同步与互斥

现代操作系统采用多道程序设计机制&#xff0c;多个进程可以并发执行&#xff0c;CPU在进程之间来回切换&#xff0c;共享某些资源&#xff0c;提高了资源的利用率&#xff0c;但这也使得处理并发执行的多个进程之间的冲突和相互制约关系成为了一道难题。如果对并发进程的调度不…

缓存击穿/穿透/雪崩

缓存击穿/穿透/雪崩Intro使用缓存需要了解几个缓存问题&#xff0c;缓存击穿、缓存穿透以及缓存雪崩&#xff0c;需要了解它们产生的原因以及怎么避免&#xff0c;尤其是当你打算设计自己的缓存框架的时候需要考虑如何处理这些问题。缓存击穿一般的缓存系统&#xff0c;都是按照…

99%的人不知道搜索引擎的6个技巧

点击上方“dotNET全栈开发”&#xff0c;“设为星标”加“星标★”&#xff0c;每天11.50&#xff0c;好文必达全文约900字&#xff0c;预计阅读时间1分钟今天看了一期seo优化的视频&#xff0c;其中就有这么一篇关于百度搜索的几个小技巧&#xff0c;这里整理出来&#xff0c;…

用信号量解决进程的同步与互斥

转自&#xff1a;http://www.cnblogs.com/whatbeg/p/4435286.html 现代操作系统采用多道程序设计机制&#xff0c;多个进程可以并发执行&#xff0c;CPU在进程之间来回切换&#xff0c;共享某些资源&#xff0c;提高了资源的利用率&#xff0c;但这也使得处理并发执行的多个进程…

扎心了,程序员2017到2019经历了什么?

刷爆朋友圈的2017-2019到底是什么梗&#xff1f;只剩下33天了&#xff0c;就到2020年了最后一批90后&#xff0c;马上就要30了&#xff1f;一到年底&#xff0c;就会陷入回忆和比较中近几日&#xff0c;网友开始将2017年和2019年进行对比&#xff0c;不少人晒出了自己在17年和1…

【.NETCore 3】Ids4 ║ 统一角色管理(上)

前言书接上文&#xff0c;咱们在上周&#xff0c;通过一篇《思考》 性质的文章&#xff0c;和很多小伙伴简单的讨论了下&#xff0c;如何统一同步处理角色的问题&#xff0c;众说纷纭&#xff0c;这个我一会儿会在下文详细说到&#xff0c;而且我最终也定稿方案了。所以今天咱们…

.NET Core 3.0 使用Nswag生成Api文档和客户端代码

摘要在前后端分离、Restful API盛行的年代&#xff0c;完美的接口文档&#xff0c;成了交流的纽带。在项目中引入Swagger &#xff08;也称为OpenAPI&#xff09;&#xff0c;是种不错的选择&#xff0c;它可以让接口数据可视化。下文将会演示利用Nswag如何生成Api文档利用NSwa…

深入研究 Angular 和 ASP.NET Core 3.0

本文要点&#xff1a;可以把多个 Angular 应用程序集成到 ASP.NET 网站中把 Angular 代码打包成 Web 组件是引导 Angular 应用程序的好方法可以把用 Angular 编写的 Web 组件轻松地集成到 ASP.NET 视图中把 Angular 解决方案构造成 Angular 应用程序的集合以实现更好的代码重用…

操作系统内存管理--简单、页式、段式、段页式

一、内存管理的目的和功能 内存一直是计算机系统中宝贵而又紧俏的资源&#xff0c;内存能否被有效、合理地使用&#xff0c;将直接影响到操作系统的性能。此外&#xff0c;虽然物理内存的增长现在达到了N个GB&#xff0c;但比物理内存增长还快的是程序&#xff0c;所以无论物理…

网易裁员背后,芸芸众生,相煎何急

十一月初拖家带口去了上海&#xff0c;到了著名的城隍庙参观&#xff0c;无意中看到了一个仅出现在历史书上的古老物件“西洋镜”&#xff0c;仿佛跨越百年&#xff0c;来到那个如裹脚布般冗长而乏味的古老年代&#xff0c;看到了一群有一群卑微的小民在生活的裹挟之下&#xf…

.NET Core on K8S 学习与实践系列文章索引 (更新至20191126)

更新记录&#xff1a;-- 2019-11-26 增加Docker容器日志系列文章近期在学习Kubernetes&#xff0c;基于之前做笔记的习惯&#xff0c;已经写了一部分文章&#xff0c;因此给自己立一个flag&#xff1a;完成这个《.NET Core on K8S学习实践》系列文章&#xff01;这个系列会持续…

ASP.NET Core gRPC 使用 Consul 服务注册发现

一. 前言gRPC 在当前最常见的应用就是在微服务场景中&#xff0c;所以不可避免的会有服务注册与发现问题&#xff0c;我们使用gRPC实现的服务可以使用 Consul 或者 etcd 作为服务注册与发现中心&#xff0c;本文主要介绍Consul。二. Consul 介绍Consul是一种服务网络解决方案&a…