其实这只是这周的一个小作业,实现思路挺简单的,本以为很快就能做完,但万万没想到中间踩了这么多坑,最后花了接近10小时才实现基础功能,甚至连UI都还来不及设计……

既然这些坑我能精确踩中,其他人或许也很难避开。我就在这里浅谈一下这个小程序的技术实现以及容易踩的坑吧~

实现思路

我们知道,face++的api需要我们提供一个图片文件及相关参数,然后会返回给我们一个json对象。那么这个程序的核心问题就是要如何获取那个图片文件以及处理接收到的json对象了。

根据老师的要求,我们是要使用unity调用PC摄像头来获取那张图片。这部分有两种解决思路:每过一段时间动态截取,实现实时检测的效果;或是设计一个拍照按钮,由用户来手动截取。其实两者都可以实现,但考虑到网络延迟不好把控,以及我们是使用免费的api,不清楚有没有数量的限制,我个人还是选用了后者。

于是最终的实现思路便是:

调用摄像头→获取用户图像→调用api→接收返回的json对象→对检测到的每个人脸进行处理

OK,有了思路,一切就都变得简单了,接下来我们逐一进行完成。

具体实现

调用摄像头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
WebCamTexture tex;
IEnumerator Start()
{
//获取授权
yield return Application.RequestUserAuthorization(UserAuthorization.WebCam);
if (Application.HasUserAuthorization(UserAuthorization.WebCam))
{
WebCamDevice[] devices = WebCamTexture.devices;
deviceName = devices[0].name;
tex = new WebCamTexture(deviceName, 1600, 900, 12);
GetComponent<Renderer>().material.mainTexture = tex;
tex.Play();
}
}

这部分的重点在于 WebCamTexture() 函数与 Renderer 组件。 WebCamTexture() 函数提供四个参数,分别是作为摄像机名字的字符串(没啥用)、摄像机宽度、摄像机高度、以及摄像机的帧率。而 Renderer 组件,则要求我们将该脚本挂载到一个含有 Renderer 的节点上,这个 Renderer 可以是各种 Renderer ,比如 Mesh Renderer

获取用户图像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void Save()
{
_t2d = new Texture2D(tex.width, tex.height, TextureFormat.ARGB32, true);
//将WebCamTexture 的像素保存到texture2D中
_t2d.SetPixels(tex.GetPixels());
//t2d.ReadPixels(new Rect(200,200,200,200),0,0,false);
_t2d.Apply();

//编码
byte[] imageTytes = _t2d.EncodeToJPG();
//存储
File.WriteAllBytes(Application.dataPath + "/Resources/cameraShoot_02.jpg", imageTytes);
this.gameObject.SetActive(false);
}

这里最重要的是 Texture2D _t2dApplication.dataPath 。前者是一种图片的存储格式,可以直接存储摄像机当前帧的图像,也可以用来将图像显示在 Sprite 中。而 Application.dataPath 则对应当前项目的 Assets 文件夹,请务必保证存储路径上的每个文件夹都存在,否则会因为找不到文件夹而报错。

调用api

这部分在官方文档中有给出范例,我也是直接参考的,这里就稍微介绍一下。

官方文档中有给出一个用于post数据的类 HttpHelper4MultipartForm ,我一会放在文末的附录里,这个类的实现原理不太需要了解,大概知道怎么用就行。我们调用这个类的时候,最终需要调用 HttpHelper4MultipartForm.MultipartFormDataPost() 函数,这个函数要提供三个参数,api网址、用户代理(不用管)、参数字典。api网址在api中有给出,参数字典也只需参考api中的文档,比较重要的是图片文件的传输方式。由于我们要传输的是本地文件(刚才存储到本地的那个嘛),所以需要的是一段图片的二进制流,这部分都是调用函数,没啥好解释的,所以我直接给出一个函数实现吧。

1
2
3
4
5
6
7
8
9
private byte[] GetPictureData(string imagePath)
{
FileStream fs = new FileStream(imagePath, FileMode.Open);
byte[] byData = new byte[fs.Length];
fs.Read(byData, 0, byData.Length);
fs.Close();

return byData;
}

给函数传入文件路径参数,返回一串二进制流 byte[] fileImage = GetPictureData(Application.dataPath + "/Resources/cameraShoot_02.jpg"); 然后把这串二进制流传给 HttpHelper4MultipartForm.FileParameter() 函数(具体参数看下方的完整代码吧)处理,最后再一起加入参数字典。

最后,再用一个 HttpWebResponse 对象来接收api传回的response即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//参数字典
Dictionary<string, object> verifyPostParameters = new Dictionary<string, object>();
verifyPostParameters.Add("api_key", "你的api_key");
verifyPostParameters.Add("api_secret", "你的api_secret");
verifyPostParameters.Add("return_landmark", "1");
verifyPostParameters.Add("return_attributes", "gender,beauty,age和你想加的参数");
byte[] fileImage = GetPictureData(Application.dataPath + "/Resources/cameraShoot_02.jpg");
//添加图片参数
verifyPostParameters.Add("image_file", new HttpHelper4MultipartForm.FileParameter(fileImage, "1.jpg", "application/octet-stream")); //"1.jpg"换成啥名字都行,没实际用途
HttpWebResponse verifyResponse = HttpHelper4MultipartForm.MultipartFormDataPost("https://api-cn.faceplusplus.com/facepp/v3/detect", "", verifyPostParameters);

//将response转换为字符串
var res = verifyResponse.GetResponseStream();
StreamReader sr = new StreamReader(res, Encoding.ASCII);
string result = sr.ReadToEnd();

这部分可能对于没有网络经验的朋友不太友好,不过没事,尽量理解吧,毕竟超出所学知识了。

接收json对象

好了,现在我们可以把 result 对象log一下,不出意外的话应该可以看到和api文档中介绍的一样的字符串。

关于log

在unity中,不可使用 Console.WriteLine() 进行log,要使用 print() 或者 Debug.Log()

现在的唯一问题是:把字符串对象转换成json对象。

由于unity中自带的json处理方法较为麻烦,我选择了引用外部的库: Litjson ,下载地址我会放在附录中(随便找的网盘链接,过期了或者不能用的话就私聊找我要吧)。

把这个dll下载下来后,在 Assets 文件夹下创建一个 Plugins 文件夹,然后把dll文件拖进去即可。

接下来在需要使用这个库的脚本前几行加上 using LitJson; ,就可以使用这个库了。由于这个库的使用较为简单,会json就能看懂,我就只放代码,不做过多解释了:

1
2
3
4
5
6
7
8
9
10
//解析为json
JsonData jsonData = JsonMapper.ToObject(result);
//识别到人脸
if (jsonData["face_num"].ToString() != "0")
{
foreach (JsonData face in jsonData["faces"])
{
//对每个face单独处理
}
}

信息处理

有了数据,最后一步自然是进行数据的输出,那些性别年龄颜值之类的字符串数据就没啥好说的了,大家应该都有自己的处理方式。这边稍微介绍一下怎么将人脸矩形(face_rectangle)在场景中标注出来。要在画布上画一个矩形,一般是使用unity的 Line Renderer 组件,组件的使用交给大家自己研究,我介绍一下代码中的调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
LineRenderer lineRenderer = faceRectNode.GetComponent<LineRenderer>();
float t2d_w = _t2d.width;
float t2d_h = _t2d.height;
float top = 略
float bottom = 略
float left = 略
float right = 略

Vector3 v0 = new Vector3(left, top, -2f);
Vector3 v1 = new Vector3(right, top, -2f);
Vector3 v2 = new Vector3(right, bottom, -2f);
Vector3 v3 = new Vector3(left, bottom, -2f);

lineRenderer.positionCount = 5;

lineRenderer.SetPosition(0, v0);
lineRenderer.SetPosition(1, v1);
lineRenderer.SetPosition(2, v2);
lineRenderer.SetPosition(3, v3);
lineRenderer.SetPosition(4, v0);

由于矩形有四条边,所以我们需要画四条线,对应的就是五个点(第五个点和第一个点重叠)。可以通过 SetPosition() 函数来设置点的位置,只需提供两个参数,前一个为点的索引,后一个为一个 Vector3 的坐标参数。

额外功能

至此,我们已经能将大部分功能都实现了,剩下的细枝末节只需慢慢打磨,实际上没什么难度。

但我打磨完之后,突然突发奇想,能不能再加一个美颜功能,来丰富一下这个程序?

好巧不巧,face++也提供了美颜的api接口,唯一的缺点就是相应时间比较长(毕竟传回的是图片嘛),需要我们在程序中 Thread.Sleep() 一下,等待数据接收。这部分的api调用其实和人脸检测部分的没什么区别,只需照本宣科地修改参数即可。但问题出在接收数据的时候,服务器传回来的是一段 Base64 的字符串,那没办法,我们只好再写个函数将这段字符串编码为 Texture2D 对象了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private Texture2D Base64StringToTexture(string base64Str)
{
try
{
//将base64头部信息替换
base64Str = base64Str.Replace("data:image/png;base64,", "").Replace("data:image/jgp;base64,", "")
.Replace("data:image/jpg;base64,", "").Replace("data:image/jpeg;base64,", "");
byte[] bytes = Convert.FromBase64String(base64Str);
Texture2D texture = new Texture2D(10, 10);
texture.LoadImage(bytes);
return texture;
}
catch (Exception ex)
{
throw ex;
}
}

这部分写完后,时间也到了23:30分,我也该去睡觉了,这篇教程也就到这里结束了,希望大家都能顺利完成这次作业~

晚安💤

附录

HttpHelper4MultipartForm

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
public static class HttpHelper4MultipartForm
{
public class FileParameter
{
public byte[] File
{
get;
set;
}

public string FileName
{
get;
set;
}

public string ContentType
{
get;
set;
}

public FileParameter(byte[] file) : this(file, null)
{
}

public FileParameter(byte[] file, string filename) : this(file, filename, null)
{
}

public FileParameter(byte[] file, string filename, string contenttype)
{
this.File = file;
this.FileName = filename;
this.ContentType = contenttype;
}
}
private static readonly Encoding encoding = Encoding.UTF8;
/// <summary>
/// MultipartForm请求
/// </summary>
/// <param name="postUrl">服务地址</param>
/// <param name="userAgent"></param>
/// <param name="postParameters">参数</param>
/// <returns></returns>
public static HttpWebResponse MultipartFormDataPost(string postUrl, string userAgent, Dictionary<string, object> postParameters)
{
string text = string.Format("----------{0:N}", Guid.NewGuid());
string contentType = "multipart/form-data; boundary=" + text;//multipart类型
byte[] multipartFormData = HttpHelper4MultipartForm.GetMultipartFormData(postParameters, text);
return HttpHelper4MultipartForm.PostForm(postUrl, userAgent, contentType, multipartFormData);
}

private static HttpWebResponse PostForm(string postUrl, string userAgent, string contentType, byte[] formData)
{
HttpWebRequest httpWebRequest = WebRequest.Create(postUrl) as HttpWebRequest;
if (httpWebRequest == null)
{
throw new NullReferenceException("request is not a http request");
}

httpWebRequest.Method = "POST";//post方式
httpWebRequest.SendChunked = false;
httpWebRequest.KeepAlive = true;
httpWebRequest.Proxy = null;
httpWebRequest.Timeout = Timeout.Infinite;
httpWebRequest.ReadWriteTimeout = Timeout.Infinite;
httpWebRequest.AllowWriteStreamBuffering = false;
httpWebRequest.ProtocolVersion = HttpVersion.Version11;
httpWebRequest.ContentType = contentType;
httpWebRequest.CookieContainer = new CookieContainer();
httpWebRequest.ContentLength = (long)formData.Length;

try
{
using (Stream requestStream = httpWebRequest.GetRequestStream())
{
int bufferSize = 4096;
int position = 0;
while (position < formData.Length)
{
bufferSize = Math.Min(bufferSize, formData.Length - position);
byte[] data = new byte[bufferSize];
Array.Copy(formData, position, data, 0, bufferSize);
requestStream.Write(data, 0, data.Length);
position += data.Length;
}
requestStream.Close();
}
}
catch (Exception ex)
{
return null;
}

HttpWebResponse result;
try
{
result = (httpWebRequest.GetResponse() as HttpWebResponse);
}
catch (WebException arg_9C_0)
{
result = (arg_9C_0.Response as HttpWebResponse);
}
return result;
}

public static bool CheckValidationResult(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors)
{
return true;
}
/// <summary>
/// 从表单中获取数据
/// </summary>
/// <param name="postParameters"></param>
/// <param name="boundary"></param>
/// <returns></returns>
private static byte[] GetMultipartFormData(Dictionary<string, object> postParameters, string boundary)
{
Stream stream = new MemoryStream();
bool flag = false;
foreach (KeyValuePair<string, object> current in postParameters)
{
if (flag)
{
stream.Write(HttpHelper4MultipartForm.encoding.GetBytes("\r\n"), 0, HttpHelper4MultipartForm.encoding.GetByteCount("\r\n"));
}
flag = true;
if (current.Value is HttpHelper4MultipartForm.FileParameter)
{
HttpHelper4MultipartForm.FileParameter fileParameter = (HttpHelper4MultipartForm.FileParameter)current.Value;
string s = string.Format("--{0}\r\nContent-Disposition: form-data; name=\"{1}\"; filename=\"{2}\"\r\nContent-Type: {3}\r\n\r\n", new object[]
{
boundary,
current.Key,
fileParameter.FileName ?? current.Key,
fileParameter.ContentType ?? "application/octet-stream"
});
stream.Write(HttpHelper4MultipartForm.encoding.GetBytes(s), 0, HttpHelper4MultipartForm.encoding.GetByteCount(s));
stream.Write(fileParameter.File, 0, fileParameter.File.Length);
}
else
{
string s2 = string.Format("--{0}\r\nContent-Disposition: form-data; name=\"{1}\"\r\n\r\n{2}", boundary, current.Key, current.Value);
stream.Write(HttpHelper4MultipartForm.encoding.GetBytes(s2), 0, HttpHelper4MultipartForm.encoding.GetByteCount(s2));
}
}
string s3 = "\r\n--" + boundary + "--\r\n";
stream.Write(HttpHelper4MultipartForm.encoding.GetBytes(s3), 0, HttpHelper4MultipartForm.encoding.GetByteCount(s3));
stream.Position = 0L;
byte[] array = new byte[stream.Length];
stream.Read(array, 0, array.Length);
stream.Close();
return array;
}
}

Litjson

百度云
链接:https://pan.baidu.com/s/1BmmqLJ5asX2DSDxfR29KLQ
提取码:80gl