本篇文章为大家展示了使用.NET怎么实现一个人脸识别功能,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。
专注于为中小企业提供网站建设、做网站服务,电脑端+手机端+微信端的三站合一,更高效的管理,为中小企业壶关免费做网站提供优质的服务。我们立足成都,凝聚了一批互联网行业人才,有力地推动了千余家企业的稳健成长,帮助中小企业通过网站建设实现规模扩充和转变。使用方法
首先安装NuGet
包Microsoft.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"); IListfaces = 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
保存了其人脸的位置信息,可供对其做进一步操作。RecognitionModel
、FaceLandmarks
、FaceAttributes
是一些额外属性,包括识别性别
、年龄
、表情
等信息,默认不识别,如下图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> Groups { get; set; } public IList MessyGroup { get; set; } // ... }
包含了一个Groups
对象和一个MessyGroup
对象,其中Groups
是一个数据的数据,用于存放人脸的分组,MessyGroup
用于保存未能找到分组的FaceId
。
有了这个,就可以通过一小段简短的代码,将不同的人脸组,分别复制对应的文件夹中:
void CopyGroup(string outputPath, GroupResult result, Dictionaryfaces) { 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.
因此,如果图片太大,必须进行一定的压缩(当然如果图片太小,显然也没必要进行压缩了),使用.NET
的Bitmap
,并结合C# 8.0
的switch 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); } }
旋转后,我的照片如下:
这样竖拍的照片也能识别出来了。
并行速度
前文说过,一个文件夹可能会有成千上万个文件,一个个上传识别,速度可能慢了点,它的代码可能长这个样子:
Dictionaryfaces = 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#
/.NET
的LINQ
支持,只需加一行.AsParallel()
即可完成:
Dictionaryfaces = 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
文件中:
Dictionaryfaces = 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>(); // 重点 Dictionary
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
方法一次只能检测1000
个FaceId
,而上次活动800
多张照片中有超过2000
个FaceId
,因此需要做一些必要的分组。
分组最简单的方法,就是使用System.Interactive
包,它提供了Rx.NET
那样方便快捷的API
(这些API
在LINQ
中未提供),但又不需要引入Observable
那样重量级的东西,因此使用起来很方便。
这里我使用的是.Buffer(int)
函数,它可以将IEnumerable
按指定的数量(如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); }
上述内容就是使用.NET怎么实现一个人脸识别功能,你们学到知识或技能了吗?如果还想学到更多技能或者丰富自己的知识储备,欢迎关注创新互联行业资讯频道。