上個月在網上看到一個用web實現簡單AR效果的文章,然后自己一路折騰,最后折騰出來一個 Asp.net+WebSocket+Emgucv實時人臉識別的東西,網上也有不少相關資料,有用winform的也有asp.net的。其實人臉識別技術早就成熟了,就是沒機會接觸這方面。百度了一下 找到好多,JqueryFaceDetection,face++,face core,opencv,emgucv等等,這些我都折騰了一遍,並不能很好的滿足我的需求,我就是想像手機QQ里邊的拍照的時候能識別到人臉並且對圖像做一些處理。后來找到了一個用winform+emgucv實現的例子,我就想着怎么給弄web上。后來又看到一篇用websocket實現的例子,就結合了一下。
我自己做的這個有相當多的代碼都是網上的直接拿來用了,對我來說,websocket和emgucv這兩個東西都是第一次接觸,有不少的坑,尤其這個emgucv!!,各個版本差別巨大,從2.4到3.2這幾個版本我幾乎都下載過,最終是用的3.1的。好了,下面進入正題,源碼我已經放在github了,https://github.com/13005463562/FaceWeb 。其中NewFaceWeb是web端,NewFace是服務端。想試一下效果的可以戳這里(要用火狐瀏覽器,谷歌太坑,強制要用https才能打開攝像頭,其他瀏覽器還存在兼容性問題,其實一些手機瀏覽器UC或者火狐也行,但是我不會調樣式。:( ,對於沒有錄入姓名的人呢,只能出現一個方框,可以點截圖(等你的臉出現方框的時候截圖),然后錄入你的姓名,就可以把你的名字也識別出來。
一.整體介紹
首先下載emgucv3.1 ,我下載的是第一個297M那個。下載之后解壓,需要用到bin下的x64文件夾,注意不是根目錄下的x64。 Emgu.CV.Example 里邊有一些關於emgucv的例子,都是按照那個寫的代碼,可以看看。
在前端利用canvas獲取攝像頭的圖像信息,通過websocket把每一幀數據傳到服務端,服務端拿到的是byte[]數據,要轉換成需要的格式再識別到你的臉,然后去人臉訓練庫中比較,找出最像你的那個樣本的姓名(相似度太低則為空),最后把你的臉的位置(左上角坐標和寬高)和姓名返回前端。前端拿到返回數據,在canvas上畫出方框和姓名,ok,完事。
二.前端實現
首先是html代碼,使用H5中的video和canvas:
<div>
<div id='frame' style="position:relative;">
<video style='position:absolute;top:0px;left:0px;z-index:2;' id="live" width="320" height="240" autoplay></video>
<canvas style='position:absolute;top:242px;left:0px; z-index:170;' width="320" id="canvasFace" height="240"></canvas>
<canvas style='position:absolute;top:242px;left:0px; z-index:11;' width="320" id="canvas" height="240"></canvas>
</div>
</div>
接着放js代碼(從別人那搬來的=-=), 先是要打開攝像頭,打開成功了就開啟websocket,把一幀圖像數據轉成base64形式順便壓縮一下,壓縮很重要,在本機測無所謂,但要放服務器網絡延遲太高,每次前后台交互一兩秒。。。壓縮比0.5即可把延遲降低到300-400毫秒,這樣就很流暢啦.
$(function () {
var video = $('#live').get()[0],
canvas = $('#canvas'),
ctx = canvas.get()[0].getContext('2d'),
canvasFace = $('#canvasFace'),
//canvasFace1 = document.getElementById("canvasFace");
ctx2 = canvasFace.get()[0].getContext('2d'),
canSend = true;
if (navigator.getUserMedia) { // Standard
navigator.getUserMedia({ "video": true }, function (stream) {
video.src = webkitURL.createObjectURL(stream);
// video.play();
startWS();
}, errBack);
} else if (navigator.webkitGetUserMedia) { // WebKit-prefixed
navigator.webkitGetUserMedia({ "video": true }, function (stream) {
video.src = window.webkitURL.createObjectURL(stream);
// video.play();
startWS();
}, errBack);
}
else if (navigator.mozGetUserMedia) { // Firefox-prefixed
navigator.mozGetUserMedia({ "video": true }, function (stream) {
video.src = window.URL.createObjectURL(stream);
//video.play();
startWS();
}, errBack);
};
function errBack() {
console.log('err');
}
var _draw = function (pArr) {
canvasFace[0].height = canvasFace[0].height;//重設height以清除畫布
ctx2.strokeStyle = "#EEEE00";
ctx2.fillStyle = 'rgba(0,0,0,0.0)';
ctx2.lineWidth = 2;
//設置字體樣式
ctx2.font = "30px Courier New";
//設置字體填充顏色
ctx2.fillStyle = "red";
//ctx2.clearRect(0, 0, 320, 240);
if (pArr == "[]") {
return;
}
var obj = $.parseJSON(pArr);
for (var i = 0, l = obj.length; i < l; i++) {
var left = obj[i].X; //左上角x坐標
var top = obj[i].Y;//左上角y坐標
var width = obj[i].W; //寬
var height = obj[i].H;//高
var name = obj[i].N;//姓名
//畫方框
ctx2.moveTo(left, top);
ctx2.lineTo(left + width, top);
ctx2.lineTo(left + width, top + height);
ctx2.lineTo(left, top + height);
ctx2.lineTo(left, top);
ctx2.stroke();
//從坐標點(50,50)開始繪制姓名
ctx2.fillText(name, left - 30, top - 30);
}
};
var startWS = function () {
var ws = new WebSocket("ws://119.23.237.231:8082/Handler/GetFacePosition.ashx");
ws.onopen = function () {
console.log('Opened WS!');
};
ws.onmessage = function (msg) {
_draw(msg.data);
canSend = true;
//記錄每次連接的時間
//var timestamp = new Date().getTime();
//console.log("end=" + timestamp);
};
ws.onclose = function (msg) {
console.log('socket close!');
};
var timer = setInterval(function () {
ctx.drawImage(video, 0, 0, 320, 240);
if (ws.readyState == WebSocket.OPEN && canSend) {
canSend = false;
var data = canvas.get()[0].toDataURL('image/jpeg', 0.5), //把畫布轉base64 壓縮比例0.5
newblob = dataURItoBlob(data);
ws.send(newblob);
//ws.send("123");
}
}, 60);
};
});
function dataURItoBlob(dataURI) {
var byteString = atob(dataURI.split(',')[1]),
mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0],
ab = new ArrayBuffer(byteString.length),
ia = new Uint8Array(ab);
for (var i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
return new Blob([ab], { type: mimeString });
}
前端大概就這樣子了,發送數據,接收數據,畫圖。仔細看一下,挺簡單的。
二.服務端實現
服務端相對要復雜點了,我就大致講一下怎么處理的,說說遇到的一些坑,詳細的實現看源碼就行了。
我用的asp.net MVC,需要引用emgucv的一些dll,Emgu.CV.UI,Emgu.CV.World,ZedGraph ,這些在下載的emgucv中bin目錄下都能找到,找不到就是版本下載錯了。
首先當然是接收數據,用ashx實現的,rootPath是根目錄路徑,到時候需要把人臉樣本(也就是你錄入的臉的圖像)文件夾放在項目根目錄,還有一個人臉分類器的xml文件,也放在根目錄。在調用emgucv的方法時會用到。
private static string rootPath;
private int _maxBufferSize = 256 * 1024;
public void ProcessRequest(HttpContext context)
{
if (context.IsWebSocketRequest)
{
rootPath = context.Request.PhysicalApplicationPath;
context.AcceptWebSocketRequest(ProcessWSChat);
}
}
接着是實現websocket的代碼,我就不多說了,還是搬代碼:
private async Task ProcessWSChat(AspNetWebSocketContext context)
{
try
{
WebSocket socket = context.WebSocket;
byte[] receiveBuffer = new byte[_maxBufferSize];
ArraySegment<byte> buffer = new ArraySegment<byte>(receiveBuffer);
while (socket.State == WebSocketState.Open)
{
WebSocketReceiveResult result = await socket.ReceiveAsync(buffer, CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Close)
{
await socket.CloseAsync(
result.CloseStatus.GetValueOrDefault(),
result.CloseStatusDescription,
CancellationToken.None);
break;
}
int offset = result.Count;
while (result.EndOfMessage == false)
{
result = await socket.ReceiveAsync(new ArraySegment<byte>(receiveBuffer, offset, _maxBufferSize - offset), CancellationToken.None);
offset += result.Count;
}
if (result.MessageType == WebSocketMessageType.Binary && offset != 0)
{
ArraySegment<byte> newbuff = new ArraySegment<byte>(Encoding.UTF8.GetBytes(FaceDetectionDetail(receiveBuffer, offset)));
await socket.SendAsync(newbuff, WebSocketMessageType.Text, true, CancellationToken.None);
}
}
}
catch (Exception e)
{
var err = e.Message;
Com.Other.AddLog(err);
}
}
然后是調方法得到人臉數據,可以是多個臉,這里的把byte[]轉Mat可是費了我好大功夫,最開始找不到簡單的方法,只能傻乎乎生成圖片到本地再去讀取,效率低下,最終是在一個英語網站(講真。。英語水平太低,都是蒙的)里邊找到這個方法:
private static string FaceDetectionDetail(byte[] data, int plength)
{
StringBuilder sb = new StringBuilder();
sb.Append("[");
//把byte[]轉成mat 找了好久找到的方法
Image img =Com.Other. GetImageByBytes(data);
Bitmap bmpImage = new Bitmap(img);
Emgu.CV.Image<Bgr, Byte> currentFrame = new Emgu.CV.Image<Bgr, Byte>(bmpImage);
Mat invert = new Mat();
CvInvoke.BitwiseAnd(currentFrame, currentFrame, invert);
if (invert != null)
{
Com.KingFaceDetect.faceDetectedObj faces = Run1(invert); //得到識別到的臉
for (int i = 0; i < faces.facesRectangle.Count; i++)
{
sb.AppendFormat("{{\"X\":{0},\"Y\":{1},\"W\":{2},\"H\":{3},\"N\":\"{4}\"}},", faces.facesRectangle[i].X, faces.facesRectangle[i].Y, faces.facesRectangle[i].Width, faces.facesRectangle[i].Height, faces.names[i]);
}
if (sb[sb.Length - 1] == ',')
{
sb.Remove(sb.Length - 1, 1);
}
}
sb.Append("]");
GC.Collect();
//AddLog((System.Environment.TickCount - aa).ToString()); //單位毫秒
return sb.ToString();
}
再來看一下Run1這個方法,返回值是一個faceDetectedObj類型的,這是自己封裝的一個類KingFaceDetect中的東西,它包含了識別的的臉部的坐標和這個人的姓名,從之前提到的winform版本中提出來的,基本沒改。可以看到這里用了一個Application,因為在創建KingFaceDetect的時候會去加載人臉樣本庫,比較耗內存把,第一次沒用全局,然后服務器都被搞崩了。
static Com.KingFaceDetect.faceDetectedObj Run1(Mat image)
{
if (HttpContext.Current.Application["detect"] == null)
{
HttpContext.Current.Application["detect"] = new Com.KingFaceDetect(); //存入全局 否則好像會報內存錯誤
}
Com.KingFaceDetect detect = (Com.KingFaceDetect)HttpContext.Current.Application["detect"];
Com.KingFaceDetect.faceDetectedObj resut = detect.faceRecognize(image);
return resut;
}
接下來就是這個核心的類了,KingFaceDetect ,里邊都有注釋,懶得講。。。。直接搬上來:,,在對比訓練庫得到姓名那一步,有個Distance,值越小越可能是同一個人,我自己改了下,大於4000就當沒有,姓名返回“”。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Emgu.CV;
using Emgu.CV.CvEnum;
using Emgu.CV.Structure;
using Emgu.Util;
using Emgu.CV.Cuda;
using System.Diagnostics;
using Emgu.CV.UI;
using System.Drawing;
using System.IO;
namespace NewFace.Com
{
class KingFaceDetect
{
private string FaceSamplesPath =System.Web.HttpContext.Current. Server.MapPath("~/") + "\\trainedFaces"; //這個是訓練庫文件夾 需要手動復制到項目根目錄下
private CascadeClassifier faceClassifier = new CascadeClassifier(System.Web.HttpContext.Current. Server.MapPath("~/")+"\\haarcascade_frontalface_default.xml"); //這個文件也放根目錄
TrainedFaceRecognizer tfr;
public KingFaceDetect()
{
SetTrainedFaceRecognizer(FaceRecognizerType.EigenFaceRecognizer);
}
/// <summary>
/// 獲取已保存的所有樣本文件
/// </summary>
/// <returns></returns>
public TrainedFileList SetSampleFacesList()
{
TrainedFileList tf = new TrainedFileList();
DirectoryInfo di = new DirectoryInfo(FaceSamplesPath);
int i = 0;
foreach (FileInfo fi in di.GetFiles())
{
tf.trainedImages.Add(new Image<Gray, byte>(fi.FullName));
tf.trainedLabelOrder.Add(i);
tf.trainedFileName.Add(fi.Name.Split('_')[0]);
i++;
}
return tf;
}
/// <summary>
/// 訓練人臉識別器
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public TrainedFaceRecognizer SetTrainedFaceRecognizer(FaceRecognizerType type)
{
tfr = new TrainedFaceRecognizer();
tfr.trainedFileList = SetSampleFacesList();
switch (type)
{
case FaceRecognizerType.EigenFaceRecognizer:
tfr.faceRecognizer = new Emgu.CV.Face.EigenFaceRecognizer(80, double.PositiveInfinity);
break;
case FaceRecognizerType.FisherFaceRecognizer:
tfr.faceRecognizer = new Emgu.CV.Face.FisherFaceRecognizer(80, 3500);
break;
case FaceRecognizerType.LBPHFaceRecognizer:
tfr.faceRecognizer = new Emgu.CV.Face.LBPHFaceRecognizer(1, 8, 8, 8, 100);
break;
}
tfr.faceRecognizer.Train(tfr.trainedFileList.trainedImages.ToArray(), tfr.trainedFileList.trainedLabelOrder.ToArray());
return tfr;
}
/// <summary>
/// 獲取制定圖片,識別出的人臉矩形框
/// </summary>
/// <param name="emguImage"></param>
/// <returns></returns>
public faceDetectedObj GetFaceRectangle(Mat emguImage)
{
faceDetectedObj fdo = new faceDetectedObj();
fdo.originalImg = emguImage;
List<Rectangle> faces = new List<Rectangle>();
try
{
using (UMat ugray = new UMat())
{
CvInvoke.CvtColor(emguImage, ugray, Emgu.CV.CvEnum.ColorConversion.Bgr2Gray);//灰度化圖片
CvInvoke.EqualizeHist(ugray, ugray);//均衡化灰度圖片
Rectangle[] facesDetected = faceClassifier.DetectMultiScale(ugray, 1.1, 10, new Size(20, 20));
faces.AddRange(facesDetected);
}
}
catch (Exception ex)
{
}
fdo.facesRectangle = faces;
return fdo;
}
/// <summary>
/// 人臉識別
/// </summary>
/// <param name="emguImage"></param>
/// <returns></returns>
public faceDetectedObj faceRecognize(Mat emguImage)
{
faceDetectedObj fdo = GetFaceRectangle(emguImage);
Image<Gray, byte> tempImg = fdo.originalImg.ToImage<Gray, byte>();
#region 給識別出的所有人臉畫矩形框
using (Graphics g = Graphics.FromImage(fdo.originalImg.Bitmap))
{
foreach (Rectangle face in fdo.facesRectangle)
{
Image<Gray, byte> GrayFace = tempImg.Copy(face).Resize(100, 100, Emgu.CV.CvEnum.Inter.Cubic);
GrayFace._EqualizeHist();//得到均衡化人臉的灰度圖像
#region 得到匹配姓名
Emgu.CV.Face.FaceRecognizer.PredictionResult pr = tfr.faceRecognizer.Predict(GrayFace);
string name = "";
//Distance越小表示 越可能是同一個人
if (pr.Distance <4000)
{
name = tfr.trainedFileList.trainedFileName[pr.Label].ToString();
}
#endregion
fdo.names.Add(name);
}
}
#endregion
return fdo;
}
#region 自定義類及訪問類型
public class TrainedFileList
{
public List<Image<Gray, byte>> trainedImages = new List<Image<Gray, byte>>();
public List<int> trainedLabelOrder = new List<int>();
public List<string> trainedFileName = new List<string>();
}
public class TrainedFaceRecognizer
{
public Emgu.CV.Face.FaceRecognizer faceRecognizer;
public TrainedFileList trainedFileList;
}
public class faceDetectedObj
{
public Mat originalImg;
public List<Rectangle> facesRectangle;
public List<string> names = new List<string>();
}
public enum FaceRecognizerType
{
EigenFaceRecognizer = 0,
FisherFaceRecognizer = 1,
LBPHFaceRecognizer = 2,
};
#endregion
}
}
OK,核心代碼都齊了,但是你想點擊Debug來跑一個那還不行,,你會發現在調用emgucv的時候會報錯:
“Emgu.CV.CvInvoke”的類型初始值設定項引發異常 !!!!!!!!!
就是這個異常,幾乎伴隨整個項目,關於這個異常,稍后我再總結一下。在代碼都完事的時候在vs上跑不起來,很傷心啊,,很絕望,,想了好久好久,會不會是vs根本就沒把x64文件夾下的dll加載起來?,把項目發布到iis上跑了一下,居然成功了!別提我有多雞凍了。所以呢,就不在vs上調試了,直接放服務器上跑,在慢慢調試。下面是發布后的樣子:

二.總結
1.對於上邊提到的那個異常,首先是和.net版本有關,當時我先整的winform版的人臉識別,用的.net4.5,就報那個異常,一直降級降到3.5才ok。但是在寫web服務端的時候,用的.net4.5卻又完全沒問題。我也很蒙。還有一個原因就是之前提到的x64文件夾,要把整個文件夾放到應用程序的bin目錄下(把整個文件夾放進去就行,不要把里邊的dll復制出來到bin下),大概700多M。
2.emgucv各個版本差別較大,在這個版本能用的代碼,到其他版本可能根本用不了。
暫時先這些吧,有什么疏忽的以后再補上。本來還想用Xamarin.Android做個安卓app的,但是。。。好難啊,就一個socket就遇到了麻煩。有懂Xamarin的大神能指點指點嗎?

