.NET做人脸识别并分类的实现示例 |
|
在游乐场、玻璃天桥、滑雪场等娱乐场所,经常能看到有摄影师在拍照片,令这些经营者发愁的一件事就是照片太多了,客户在成千上万张照片中找到自己可不是件容易的事 。在一次游玩等活动或家庭聚会也同理,太多了照片导致挑选十分困难 。 还好有 本文将使用 fa3a7bfd807ccd6b17cf559ad584cbaa 使用方法 首先安装
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:PhotosDSC_996ICU.JPG"); IList<DetectedFace> faces = await fc.Face.DetectWithStreamAsync(file); 其中返回的
[
{
"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
}
]
可见,该照片返回了两个
最后,通过 var faceIds = faces.Select(x => x.FaceId.Value).ToList(); GroupResult reslut = await fc.Face.GroupAsync(faceIds); 返回了一个
public class GroupResult
{
public IList<IList<Guid>> Groups
{
get;
set;
}
public IList<Guid> MessyGroup
{
get;
set;
}
// ...
}
包含了一个 有了这个,就可以通过一小段简短的代码,将不同的人脸组,分别复制对应的文件夹中:
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);
}
}
然后就能得到运行结果,如图,我传入了
还能有什么问题? 就两个 图片太大,需要压缩 毕竟要把图片上传到云服务中,如果上传网速不佳,流量会挺大,而且现在的手机、单反、微单都能轻松达到好几千万像素, 二来……其实
因此,如果图片太大,必须进行一定的压缩(当然如果图片太小,显然也没必要进行压缩了),使用
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();
}
竖立的照片 相机一般都是
还好照片在拍摄后,都会保留
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));
要想把速度变化,可以启用并行上传,有了
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));
断点续传 也如上文所说,有成千上万张照片,如果一旦网络传输异常,或者打翻了桌子上的咖啡(谁知道呢?)……或者完全一切正常,只是想再做一些其它的分析,所有东西又要重新开始 。我们可以加入下载中常说的“断点续传”机制 。 其实就是一个缓存,记录每个文件读取的结果,然后下次运行时先从缓存中读取即可,缓存到一个
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));
注意代码下方有一个 使用时,只需只需在
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));
将人脸框起来 照片太多,如果活动很大,或者合影中有好几十个人,分出来的组,将长这个样子:
完全不知道自己的脸在哪,因此需要将检测到的脸框起来 。 注意框起来的过程,也很有技巧,回忆一下,上传时的照片本来就是压缩和旋转过的,因此返回的
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个脸的限制
分组最简单的方法,就是使用 这里我使用的是
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);
}
总结 文中用到的完整代码,全部上传了到我的博客数据 这个月我参加了上海的
总的来说,这个效果还挺不错,渣渣分辨率的照片的脸都被它找到了😂 。 注意,不一定非得用 另外,如有离线人脸识别需求, 以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家 。 |