.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,一经查实,立即删除!

相关文章

Java连接Mysql数据库警告:Establishing SSL connection without server's identity verification is not recommend

Java使用mysql-jdbc连接MySQL出现如下警告&#xff1a; Establishing SSL connection without servers identity verification is not recommended. According to MySQL 5.5.45, 5.6.26 and 5.7.6 requirements SSL connection must be established by default if explicit opt…

.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;由微软 .…

Spring集成Mybatis错误Result Maps collection already contains value for XXX

Spring在集成Mybatis出现如下错误&#xff1a; SpringResult Maps collection already contains value for com.guowei.maven.framework.dao.UserMapper.resultUser at org.mybatis.spring.SqlSessionFactoryBean.buildSqlSessionFactory(SqlSessionFactoryBean.java:468) at o…

dotnet Blazor 用 C# 控制界面行为

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

Spring集成Mybatis配置映射文件方法详解

Spring ORM模块集成Mybatis使用到了mybatis-spring&#xff0c;在配置mybatis映射文件的时候&#xff0c;一般不直接在Mybatis的配置文件里进行配置&#xff0c;而会在Spring的配置文件里使用MapperScannerConfigurer来配置。MapperScannerConfigurer会自动扫描basePackage指定…

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

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

Spring连接mysql数据库错误:Cannot load JDBC driver class '${driver}'

在用Spring使用连接mysql数据库时出现如下错误&#xff1a; Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Could not get JDBC Connection; nested exception isjava.sql.SQLException: Cannot load JDBC driver class ${driver} 错误详细信息如下&…

不一样的 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…

进程间通信的方式总结

进程间通信就是在不同进程之间传播或交换信息。 进程间通信的目的如下&#xff1a; ①数据传输&#xff1a;一个进程需要将它的数据发送给另一个进程&#xff0c;发送的数据量在一个字节到几兆字节之间。 ②共享数据&#xff1a;多个进程想要操作共享数据&#xff0c;一个进程对…

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

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

操作系统进程(作业)调度常见算法详解

一、进程调度的原因 在操作系统中&#xff0c;由于进程综述多于处理机&#xff0c;它们必然竞争处理机。为了充分利用计算机系统中的CPU资源&#xff0c;让计算机系统能够多快好省地完成我们让它做的各种任务&#xff0c;所以需要进行进程调度。 二、进程调度的定义 进程调度&a…

Orleans 3.0 为我们带来了什么

原文&#xff1a;https://devblogs.microsoft.com/dotnet/orleans-3-0/作者&#xff1a;Reuben Bond&#xff0c;Orleans首席软件开发工程师翻译&#xff1a;艾心这是一篇来自Orleans团队的客座文章&#xff0c;Orleans是一个使用.NET创建分布式应用的跨平台框架。获取更多信息…

进程的同步与互斥

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

缓存击穿/穿透/雪崩

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

临界区、互斥量、信号量、事件的区别

四种进程或线程同步互斥的控制方法&#xff1a; 1、临界区:通过对多线程的串行化来访问公共资源或一段代码&#xff0c;速度快&#xff0c;适合控制数据访问。 2、互斥量:为协调共同对一个共享资源的单独访问而设计的。 3、信号量:为控制一个具有有限数量用户资源而设计。 4…

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;但这也使得处理并发执行的多个进程…