- ubuntu12.04环境下使用kvm ioctl接口实现最简单的虚拟机
- Ubuntu 通过无线网络安装Ubuntu Server启动系统后连接无线网络的方法
- 在Ubuntu上搭建网桥的方法
- ubuntu 虚拟机上网方式及相关配置详解
CFSDN坚持开源创造价值,我们致力于搭建一个资源共享平台,让每一个IT人在这里找到属于你的精彩世界.
这篇CFSDN的博客文章.NET做人脸识别并分类的实现示例由作者收集整理,如果你对这篇文章有兴趣,记得点赞哟.
在游乐场、玻璃天桥、滑雪场等娱乐场所,经常能看到有摄影师在拍照片,令这些经营者发愁的一件事就是照片太多了,客户在成千上万张照片中找到自己可不是件容易的事。在一次游玩等活动或家庭聚会也同理,太多了照片导致挑选十分困难.
还好有.NET,只需少量代码,即可轻松找到人脸并完成分类.
本文将使用Microsoft Azure云提供的认知服务(Cognitive Services)API来识别并进行人脸分类,可以免费使用,注册地址是:https://portal.azure.com。注册完成后,会得到两个密钥,通过这个密钥即可完成本文中的所有代码,这个密钥长这个样子(非真实密钥):
1
|
fa3a7bfd807ccd6b17cf559ad584cbaa
|
使用方法 。
首先安装NuGet包Microsoft.Azure.CognitiveServices.Vision.Face,目前最新版是2.5.0-preview.1,然后创建一个FaceClient:
1
2
3
4
5
|
string
key =
"fa3a7bfd807ccd6b17cf559ad584cbaa"
;
// 替换为你的key
using
var fc =
new
FaceClient(
new
ApiKeyServiceClientCredentials(key))
{
Endpoint =
"https://southeastasia.api.cognitive.microsoft.com"
,
};
|
然后识别一张照片:
1
2
|
using
var file = File.OpenRead(
@"C:\Photos\DSC_996ICU.JPG"
);
IList<DetectedFace> faces = await fc.Face.DetectWithStreamAsync(file);
|
其中返回的faces是一个IList结构,很显然一次可以识别出多个人脸,其中一个示例返回结果如下(已转换为JSON):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
[
{
"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进行分类:
1
2
|
var
faceIds = faces.Select(x => x.FaceId.Value).ToList();
GroupResult reslut = await fc.Face.GroupAsync(faceIds);
|
返回了一个GroupResult,其对象定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public
class
GroupResult
{
public
IList<IList<Guid>> Groups
{
get
;
set
;
}
public
IList<Guid> MessyGroup
{
get
;
set
;
}
// ...
}
|
包含了一个Groups对象和一个MessyGroup对象,其中Groups是一个数据的数据,用于存放人脸的分组,MessyGroup用于保存未能找到分组的FaceId.
有了这个,就可以通过一小段简短的代码,将不同的人脸组,分别复制对应的文件夹中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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的分辨率:
因此,如果图片太大,必须进行一定的压缩(当然如果图片太小,显然也没必要进行压缩了),使用.NET的Bitmap,并结合C# 8.0的switch expression,这个判断逻辑以及压缩代码可以一气呵成:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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信息并对照片做相应的旋转即可:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
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);
}
}
|
旋转后,我的照片如下:
这样竖拍的照片也能识别出来了.
并行速度 。
前文说过,一个文件夹可能会有成千上万个文件,一个个上传识别,速度可能慢了点,它的代码可能长这个样子:
1
2
3
4
5
6
7
8
9
10
|
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#/.NET的LINQ支持,只需加一行.AsParallel()即可完成:
1
2
3
4
5
6
7
8
9
10
11
|
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文件中:
1
2
3
4
5
6
7
8
9
10
11
|
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中添加一行代码即可:
1
2
3
4
5
6
7
8
9
10
11
12
|
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对象值,它也是压缩和旋转过的,如果不进行压缩和旋转,找到的脸的位置会完全不正确,因此需要将之前的计算过程重新演算一次:
1
2
3
4
5
6
7
8
9
10
11
12
|
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<T>那样重量级的东西,因此使用起来很方便.
这里我使用的是.Buffer(int)函数,它可以将IEnumerable<T>按指定的数量(如1000)进行分组,代码如下:
1
2
3
4
5
6
7
8
|
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 Conf的800多张照片做了分组,识别出了2000多张人脸,我将其中我的照片的前三张找出来,结果如下:
...... 。
总的来说,这个效果还挺不错,渣渣分辨率的照片的脸都被它找到了.
注意,不一定非得用Azure Cognitive Services来做人脸识别,国内还有阿里云等厂商也提供了人脸识别等服务,并提供了.NET接口,无非就是调用API,注意其限制,代码总体差不多.
另外,如有离线人脸识别需求,Luxand提供了还有离线版人脸识别SDK,名叫Luxand FaceSDK,同样提供了.NET接口。因为无需网络调用,其识别更快,匹配速度更是可达每秒5千万个人脸数据,精度也非常高,亲测好用,目前最新版是v7.1.0,授权昂贵(但百度有惊喜).
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家.
原文链接:https://www.cnblogs.com/sdflysha/p/20191122-dotnet-face-detection.html 。
最后此篇关于.NET做人脸识别并分类的实现示例的文章就讲到这里了,如果你想了解更多关于.NET做人脸识别并分类的实现示例的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
背景: 我最近一直在使用 JPA,我为相当大的关系数据库项目生成持久层的轻松程度给我留下了深刻的印象。 我们公司使用大量非 SQL 数据库,特别是面向列的数据库。我对可能对这些数据库使用 JPA 有一
我已经在我的 maven pom 中添加了这些构建配置,因为我希望将 Apache Solr 依赖项与 Jar 捆绑在一起。否则我得到了 SolarServerException: ClassNotF
interface ITurtle { void Fight(); void EatPizza(); } interface ILeonardo : ITurtle {
我希望可用于 Java 的对象/关系映射 (ORM) 工具之一能够满足这些要求: 使用 JPA 或 native SQL 查询获取大量行并将其作为实体对象返回。 允许在行(实体)中进行迭代,并在对当前
好像没有,因为我有实现From for 的代码, 我可以转换 A到 B与 .into() , 但同样的事情不适用于 Vec .into()一个Vec . 要么我搞砸了阻止实现派生的事情,要么这不应该发
在 C# 中,如果 A 实现 IX 并且 B 继承自 A ,是否必然遵循 B 实现 IX?如果是,是因为 LSP 吗?之间有什么区别吗: 1. Interface IX; Class A : IX;
就目前而言,这个问题不适合我们的问答形式。我们希望答案得到事实、引用资料或专业知识的支持,但这个问题可能会引发辩论、争论、投票或扩展讨论。如果您觉得这个问题可以改进并可能重新打开,visit the
我正在阅读标准haskell库的(^)的实现代码: (^) :: (Num a, Integral b) => a -> b -> a x0 ^ y0 | y0 a -> b ->a expo x0
我将把国际象棋游戏表示为 C++ 结构。我认为,最好的选择是树结构(因为在每个深度我们都有几个可能的移动)。 这是一个好的方法吗? struct TreeElement{ SomeMoveType
我正在为用户名数据库实现字符串匹配算法。我的方法采用现有的用户名数据库和用户想要的新用户名,然后检查用户名是否已被占用。如果采用该方法,则该方法应该返回带有数据库中未采用的数字的用户名。 例子: “贾
我正在尝试实现 Breadth-first search algorithm , 为了找到两个顶点之间的最短距离。我开发了一个 Queue 对象来保存和检索对象,并且我有一个二维数组来保存两个给定顶点
我目前正在 ika 中开发我的 Python 游戏,它使用 python 2.5 我决定为 AI 使用 A* 寻路。然而,我发现它对我的需要来说太慢了(3-4 个敌人可能会落后于游戏,但我想供应 4-
我正在寻找 Kademlia 的开源实现C/C++ 中的分布式哈希表。它必须是轻量级和跨平台的(win/linux/mac)。 它必须能够将信息发布到 DHT 并检索它。 最佳答案 OpenDHT是
我在一本书中读到这一行:-“当我们要求 C++ 实现运行程序时,它会通过调用此函数来实现。” 而且我想知道“C++ 实现”是什么意思或具体是什么。帮忙!? 最佳答案 “C++ 实现”是指编译器加上链接
我正在尝试使用分支定界的 C++ 实现这个背包问题。此网站上有一个 Java 版本:Implementing branch and bound for knapsack 我试图让我的 C++ 版本打印
在很多情况下,我需要在 C# 中访问合适的哈希算法,从重写 GetHashCode 到对数据执行快速比较/查找。 我发现 FNV 哈希是一种非常简单/好/快速的哈希算法。但是,我从未见过 C# 实现的
目录 LRU缓存替换策略 核心思想 不适用场景 算法基本实现 算法优化
1. 绪论 在前面文章中提到 空间直角坐标系相互转换 ,测绘坐标转换时,一般涉及到的情况是:两个直角坐标系的小角度转换。这个就是我们经常在测绘数据处理中,WGS-84坐标系、54北京坐标系
在软件开发过程中,有时候我们需要定时地检查数据库中的数据,并在发现新增数据时触发一个动作。为了实现这个需求,我们在 .Net 7 下进行一次简单的演示. PeriodicTimer .
二分查找 二分查找算法,说白了就是在有序的数组里面给予一个存在数组里面的值key,然后将其先和数组中间的比较,如果key大于中间值,进行下一次mid后面的比较,直到找到相等的,就可以得到它的位置。
我是一名优秀的程序员,十分优秀!