其实这只是这周的一个小作业,实现思路挺简单的,本以为很快就能做完,但万万没想到中间踩了这么多坑,最后花了接近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 ); _t2d.SetPixels(tex.GetPixels()); _t2d.Apply(); byte [] imageTytes = _t2d.EncodeToJPG(); File.WriteAllBytes(Application.dataPath + "/Resources/cameraShoot_02.jpg" , imageTytes); this .gameObject.SetActive(false ); }
这里最重要的是 Texture2D _t2d
与 Application.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" )); HttpWebResponse verifyResponse = HttpHelper4MultipartForm.MultipartFormDataPost("https://api-cn.faceplusplus.com/facepp/v3/detect" , "" , verifyPostParameters); 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 JsonData jsonData = JsonMapper.ToObject(result); if (jsonData["face_num" ].ToString() != "0" ){ foreach (JsonData face in jsonData["faces" ]) { } }
信息处理
有了数据,最后一步自然是进行数据的输出,那些性别年龄颜值之类的字符串数据就没啥好说的了,大家应该都有自己的处理方式。这边稍微介绍一下怎么将人脸矩形(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 { 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分,我也该去睡觉了,这篇教程也就到这里结束了,希望大家都能顺利完成这次作业~
晚安💤
附录
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; 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; 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" ; 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 ; } 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