上個月因為出差的關系,斷更了很久,為了補償大家長久的等待,送上一個新的系列,之前幾個系列也會抽空繼續更新。
大概半年多前吧,因為工作需要,我開始研究圖像識別技術。OpenCV在這方面已經有了很多技術積累,在html5領域也很早就有了這方面的Demo。但是一番學習下來,我發現基本上這方面的文章大都比較零散片面,而且很多關鍵的代碼可能已經老化不能正常使用了。所以這個系列的文章中,我將對html5與EmguCV的整體開發過程做一個整理,逐步介紹怎么使用html5技術和EmguCV類庫實現各種看上去高大上的識別技術。
本文,我會以人臉識別為例,引入html+EmguCV的基本架構(如下圖)。
前端沒有問題,在瀏覽器中用html5技術調用攝像頭,使用video和canvas標簽配合各種dom進行渲染。值得一提的是,因為這里有大量的圖像數據交互傳遞,所以需要建立websocket來與后端服務器進行交互。
后端的話,其實我開始使用的是PHP技術,但是發現openCV的安裝略糾結,於是乎轉投微軟陣營。這里我使用了framework4.5+EmguCV,微軟在frameworks4.5中已經集成了websocket的服務端套字,我們可以很方便地使用它,差不多就和之前版本中寫Ajax的處理文件一樣方便。關於EmguCV,其實就是OpenCV在c#中的再封裝,可以訪問OpenCV相關網站獲取更多信息。
接下來,我們快速地瀏覽下關鍵代碼。
html部分:
<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>
這里主要起作用的DOM是1個video標簽和2個Canvas標簽。Video標簽主要用來獲取攝像頭的數據流,兩個Canvas標簽分別用來繪制Video中的內容和計算出來的頭像的位置。
Javascript部分:
1 $(function(){ 2 var video = $('#live').get()[0], 3 canvas = $('#canvas'), 4 ctx=canvas.get()[0].getContext('2d'), 5 canvasFace =$('#canvasFace'), 6 ctx2= canvasFace.get()[0].getContext('2d'), 7 canSend=true; 8 9 ctx2.strokeStyle="#EEEE00"; 10 ctx2.fillStyle='rgba(0,0,0,0.0)'; 11 ctx2.lineWidth=3; 12 13 navigator.webkitGetUserMedia({ "video": true },function(stream){ 14 video.src = webkitURL.createObjectURL(stream); 15 startWS(); 16 },function(err){ 17 console.log('err'); 18 }); 19 20 //x,y,w,h 21 var _draw =function(pArr){ 22 var _obj = $.fromJson(pArr); 23 24 ctx2.clearRect(0,0,320,240); 25 26 if($.isArray(_obj)){ 27 for(var i=0,l=_obj.length;i<l;i++ ){ 28 ctx2.strokeRect(_obj[i].X,_obj[i].Y,_obj[i].W,_obj[i].H); 29 } 30 } 31 }; 32 33 var startWS=function(){ 34 var ws = new WebSocket("ws://10.168.1.1/Cloud/WSHandler.ashx"); 35 ws.onopen = function(){ 36 console.log('Opened WS!'); 37 }; 38 ws.onmessage=function(msg){ 39 _draw(msg.data); 40 canSend = true; 41 }; 42 ws.onclose=function(msg){ 43 console.log('socket close!'); 44 }; 45 var timer = setInterval(function(){ 46 ctx.drawImage(video,0,0,320,240); 47 if(ws.readyState == WebSocket.OPEN && canSend){ 48 canSend = false; 49 var data =canvas.get()[0].toDataURL('image/jpeg',1.0), 50 newblob = dataURItoBlob(data); 51 ws.send(newblob); 52 } 53 },60); 54 }; 55 });
這段JS代碼中,大家需要注意替換ws文件的地址。至於Canvas繪圖,websocket,Camera調用等細節,在后續文章中會有詳解。
可以看到websocket在向服務器提交數據時候,需要對DataURL的數據進行封裝,下面就附上這個函數(與早期版本不同)。
dataURItoBlob函數:
1 function dataURItoBlob(dataURI) { 2 var byteString = atob(dataURI.split(',')[1]), 3 mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0], 4 ab = new ArrayBuffer(byteString.length), 5 ia = new Uint8Array(ab); 6 for (var i = 0; i < byteString.length; i++) { 7 ia[i] = byteString.charCodeAt(i); 8 } 9 return new Blob([ab],{type: mimeString}); 10 }
前端的代碼大致就這樣了,后端Coding前,先大致說下怎么部署EmguCV。假設我們把解壓好的EmguCV文件夾拷貝到了C盤,那么環境變量Path為C:\Emgu\emgucv-windows-universal-cuda 2.9.0.1922\bin;在新建項目的時候,還需要把用到的DLL等文件拷貝到項目的輸出目錄。
后端代碼:
using System; using System.Collections.Generic; using System.Linq; using System.Net.WebSockets; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Web; using System.Web.WebSockets; using Emgu.CV; using Emgu.CV.Structure; using Emgu.Util; using Emgu.CV.CvEnum; using Emgu.CV.GPU; using System.IO; using System.Drawing; using System.Drawing.Imaging; namespace Cloud { public class WSHandler : IHttpHandler { private static HaarCascade haar; private static string hasLocation; private static string phy; private int _maxBufferSize = 256 * 1024; public void ProcessRequest(HttpContext context) { if (context.IsWebSocketRequest) { phy = context.Request.PhysicalApplicationPath; hasLocation = context.Request.PhysicalApplicationPath + "haarcascade_frontalface_alt2.xml"; context.AcceptWebSocketRequest(ProcessWSChat); } } private async Task ProcessWSChat(AspNetWebSocketContext context) { try { WebSocket socket = context.WebSocket; haar = new HaarCascade(hasLocation); 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); //.ConfigureAwait(continueOnCapturedContext: false); 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(FaceDetection(receiveBuffer, offset))); await socket.SendAsync(newbuff, WebSocketMessageType.Text, true, CancellationToken.None); } } } catch (Exception e) { var err = e.Message; } } private static string FaceDetection(byte[] data,int plength) { StringBuilder sb = new StringBuilder(); sb.Append("["); Image<Bgr, byte> nextFrame = new Image<Bgr, byte>(ByteToBitmap(data, plength)); if (nextFrame != null) { Image<Gray, Byte> grayframe = nextFrame.Convert<Gray, Byte>(); var faces = grayframe.DetectHaarCascade( haar, 1.4, 4, HAAR_DETECTION_TYPE.DO_CANNY_PRUNING, new Size(nextFrame.Width / 8, nextFrame.Height / 8) )[0]; foreach (var face in faces) { sb.AppendFormat("{{X:{0},Y:{1},W:{2},H:{3}}},",face.rect.X, face.rect.Y,face.rect.Width,face.rect.Height); } if (sb[sb.Length - 1] == ',') { sb.Remove(sb.Length-1,1); } } sb.Append("]"); return sb.ToString(); } private int _ii = 0; private static byte[] BitmapToByte(Bitmap b) { MemoryStream ms = new MemoryStream(); //b.Save(ms, System.Drawing.Imaging.ImageFormat.Bmp); byte[] bytes = ms.GetBuffer(); ms.Close(); return bytes; } private static Bitmap ByteToBitmap(byte[] datas,int pLength) { MemoryStream ms1 = new MemoryStream(datas, 0, pLength); Bitmap bm = (Bitmap)Bitmap.FromStream(ms1); // bm.Save(phy + "test", ImageFormat.Bmp); ms1.Close(); return bm; } public bool IsReusable { get { return false; } } } }
這里有幾點需要注意:
1)因為我們websocket傳輸的是圖片,數據流較大,一般需要輪訓多次接收。
2)如果你用的圖片較大,還需要修改接收圖片的數組的尺寸。
3)你需要一個xml文件用來人臉識別,EmguCV已經內置,也可以自己去網上找。
人臉識別Demo的關鍵代碼基本就都在這里了,關於各個技術的實現細節,我會在之后的同系列文章中一一闡述。
轉發請注明出處 http://www.cnblogs.com/Arthus/p/3804037.html