使用JavaScript實現(xiàn)Motion Detection
一、知識必備
初、中級水平的JavaScript
初、中級水平的HTML5
對jQuery有基本了解
由于使用getUserMdedia和Audio API,Demo需要使用Chrome Canary,并通過本機Web服務器運行。
第三方必備產(chǎn)品
Chrome Canary
本機Web服務器,例:Xampp(Mac,Windows,Linux)Mamp(Mac)
Webm格式文件的視頻解碼器,例:
Online ConVert Video converter to convert to the WebM format (VP8)
Mp4格式文件的視頻解碼器,例:
Ogg格式文件的視頻解碼器,例:
二、示例文件
javascript_motion_detection_soundstep.zip
我會在本篇文章中,跟大家討論一下如何在JavaScript中使用webcam stream追蹤用戶的操作。Demo中展示了從用戶的網(wǎng)絡(luò)攝像頭中搜集到的一段視頻,用戶可以實時地在使用HTML開發(fā)的木琴上彈奏,就像彈奏真正的樂器一樣。這個Demo是使用HTML5中的兩個“仍需改進”的接口開發(fā)的,它們是:getUserMedia API,用來調(diào)用用戶的網(wǎng)絡(luò)攝像頭,和Audio API,用來彈奏木琴。
同時,我也給大家展示了如何使用混合模式來捕獲用戶的操作。幾乎所有提供了圖片處理接口的語言都提供了混合模式。這里引用Wikipedia評論混合模式的一句話,“數(shù)字圖像編輯中的混合模式通常用來判斷兩個圖層是否能夠融合在一起。”JavaScript本身不支持混合模式,而混合模式不過是對像素進行的數(shù)學運算,因此我創(chuàng)建了一個混合模式“difference”。
我制作了一個Demo,用來展示web技術(shù)的的發(fā)展方向。JavaScript和HTML5將會為新型的交互式Web應用提供接口。這是多么激動人心的一件事?。?/p>
HTML5 getUserMedia API
一直到我寫這篇文章的時候,getUserMedia API仍在完善中。W3C第5次發(fā)布的HTML,新增了相當多的接口,用于訪問本機硬件。navigator.getUserMediaAPI()接口提供了一些工具,使網(wǎng)站能夠捕獲音頻和視頻。
一些技術(shù)博客中有很多關(guān)于如何使用這些接口的文章,所以,這里我不再贅述。如果你想學習更多相關(guān)的內(nèi)容,文章的最后我給大家提供了一些有關(guān)getUserMedia的鏈接。下面,我們一起來看一下我是如何使用該接口做出示例中的內(nèi)容的。
目前,getUserMedia API只能在Opera 12和Chrome Canary中使用,并且兩款瀏覽器都沒有公開發(fā)布。針對該示例,你只能使用Chrome Canary,因為該瀏覽器允許使用AudioContext彈奏木琴。Opera暫還不支持AudioContext。
Chrome Canary 完成安裝并啟動,然后啟用API。在地址欄中輸入:about:flags。在啟用Media
圖1. 啟用Media Stream
最后,由于安全限制,攝像機不允許以本地文件:///的形式進行訪問,你必須使用自己的本機web服務器運行示例文件。
現(xiàn)在萬事俱備,只需添加一個視頻標簽用來播放攝像頭流媒體文件。使用JavaScript把接收到的流媒體文件添加到視頻標簽的src屬性中。
- <video id=”webcam” autoplay width=”640″ height=”480″></video>
在JavaSript代碼段中,首先要做兩項準備工作,第一項是確保用戶的瀏覽器能夠使用getUserMedia API。
- function hasGetUserMedia() { return !!(navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia); }
第二項工作就是嘗試從用戶的網(wǎng)絡(luò)攝像頭中獲取流媒體文件。
注:在這篇文章和示例中,我使用的是jQuery框架,但是你可以根據(jù)自己的需要選擇其它框架,例如,本地文件.querySelector
- var webcamError = function(e) {
- alert(‘Webcam error!’, e);
- };
- var video = $(‘#webcam’)[0];
- if (navigator.getUserMedia) {
- navigator.getUserMedia({audio: true, video: true}, function(stream) { video.src = stream;
- }, webcamError); }
- else if (navigator.webkitGetUserMedia) { navigator.webkitGetUserMedia(‘audio, video’, function(stream) { video.src = window.webkitURL.createObjectURL(stream);
- }, webcamError);
- } else {
- //video.src = ’video.webm’; // fallback.
- }
現(xiàn)在你已經(jīng)可以在HTML頁面中展示網(wǎng)絡(luò)攝像頭流媒體文件了。在下一部分中,我將為大家介紹總體結(jié)構(gòu)以及示例中用到的一些資源。
如果你希望在自己的程序中使用getUserMedia,我推薦以下Addy Osmani的作品。Addy使用Flash fallback創(chuàng)建了getUserMedia API的shim。
Blend mode difference
接下來,我給大家展示的是使用blend mode difference來檢測用戶的操作。
把兩張圖片融合在一起就是對像素值進行加減計算。在Wikipedia的文章中有關(guān)混合模式的部分對difference進行了描述。“兩個圖層相減取其絕對值作為Difference,任何圖片和黑片進行融合,所有顏色的值都為0.”
為了實現(xiàn)這個操作,首先需要兩張圖片。代碼反復計算圖片的每一個像素,第一張圖片的色彩通道值減去第二上的色彩通道值。
比如說,兩張紅色的圖片的每個像素中的色彩通道值為:
-紅色:255(0xFF)
-綠色:0
-藍色:0
下面的操作表示從這些圖片減去這些顏色的值:
-紅色:255-255=0
-綠色:0-0=0
-藍色:0-0=0
換句話說,把blend mode difference應用于兩張相同的圖片會產(chǎn)生一張黑色的圖片。下面我們來討論一下blend mode difference的實用性和圖片的獲取途徑。
我們逐步地完成這個過程,首先設(shè)定一個時間間隔,使用canvas元素畫出webcam stream中的一張圖片,如示例中所示,我設(shè)定為1分鐘畫60張圖片,這個數(shù)量遠遠超過你所需要的。當前顯示在webcam stream中的圖片是你要融合的圖片中的第一張。
第二張圖片是從webcam中捕獲的另外一張圖片?,F(xiàn)在就有了兩張圖片,要做的就是對它們的像素值進行差值計算。這意味著,如果兩張照片是相同的—換句話說,如果用戶沒有任何操作—運算結(jié)果會是一張黑色的圖片。
當用戶開始操作,奇妙的事情發(fā)生了。當前時間間隔獲取到的圖片與在之前的時間間隔中獲取到圖片相比,有細微的變化。如果對不同的像素值進行差值計算,一些顏色出現(xiàn)了。這意味著這兩幀圖畫之間發(fā)生了變化。
圖2.融合后的兩張圖片
Motion detection這個過程就完成了。最后一步就是反復計算融合后的圖片中的所有像素,是看否存在不為黑色的像素。
必備資源
為了運行這個Demo,還需要一般網(wǎng)站的必備元素。首先,你需要一個包含了canvas和video tags 元素的HTML頁面,還需要JavaScript來運行這個應用程序。
然后,還需要一張木琴的圖片,最好是透明背景(png格式)。另外,還需要木琴每個按鍵的圖片,每一張圖片都需要有輕微的變化。這樣使用rollover 效果,這些圖片就可以給用戶視覺上的反饋,彈奏的琴鍵會高亮顯示。
我們還需要一個音頻文件,包含了每一個音符的聲音,mp3文件即可。如果彈奏木琴沒有聲音就會沒有任何樂趣可言了。
最后,這時候,需要新創(chuàng)建一個視頻,把它解碼成MP4格式,下一節(jié),我將為大家介紹一些用來解碼視頻的工具,你可以在Demo的zip文件中找到所有的資源。
解碼HTML5視頻文件
把視頻Demo后備文件解碼成用來展示HTML5視頻文件常見的三種格式。
在把文件解碼成Webm格式的過程中,我遇到了點麻煩,我選擇的解碼工具不允許選擇碼率,而碼率恰恰決定了視頻的大小和質(zhì)量,通常被稱作kbps(千比特每秒)。我不再使用Online ConVert Video轉(zhuǎn)換器來轉(zhuǎn)換WebM格式(VP8),這個問題迎刃而解:
MP4格式:你可以使用Adobe Creative Suite中的工具,例如Adobe Media Encoder或者After Effects?;蛘吣憧梢允褂妹赓M的視頻轉(zhuǎn)換軟件,如Handbrake。
Ogg格式:我通常使用一些工具一次性轉(zhuǎn)換所有格式。我對解碼后生產(chǎn)webm和MP4格式視頻的質(zhì)量感覺不是很滿意,雖然我沒辦法更改輸出的質(zhì)量,而Ogg格式的視頻的質(zhì)量比較樂觀。你可以使用Easy HTNL5 Video或者Miro等轉(zhuǎn)換器。
Demo中用到的HTML代碼段
這是開始JavaScript編碼之前的最后一步。添加HTML標簽,下面是需要用到的HTML標簽(我不會列出我用到的所有內(nèi)容,比如說視頻Demo中的fallback代碼,你可以在資源中下載得到。)
這里需要一個簡單的視頻標簽,用來接收用戶的webcam feed。不要忘記設(shè)計自動播放屬性,如果忘記了,stream會暫停在接收到的第一幀畫面上。
- <video id=”webcam” autoplay width=”640″ height=”480″></video>
視頻標簽不會顯示出來,它的CSS顯示樣式被設(shè)置成none,另外,添加一個canvas元素,用來繪制webcam stream。
- <canvas id=”canvas-source” width=”640″ height=”480″></canvas>
這時還需要另外添加一個canvas用來顯示motion detection中實時的變動。
- <canvas id=”canvas-blended” width=”640″ height=”480″></canvas>
下面添加一個DIV標簽,把木琴的圖片放入其中。把木琴放到webcam的上部:用戶可以身入其境地彈奏木琴。把音符放在木琴的上方,將其屬性設(shè)置為隱藏,當音符被觸發(fā)的時候,將會顯示在rollover中。
- <div id=”xylo”>
- <div id=”back”><img id=”xyloback” src=”images/xylo.png”/></div> <div id=”front”> <img id=”note0″ src=”images/note1.png”/> <img id=”note1″ src=”images/note2.png”/> <img id=”note2″ src=”images/note3.png”/> <img id=”note3″ src=”images/note4.png”/> <img id=”note4″ src=”images/note5.png”/> <img id=”note5″ src=”images/note6.png”/> <img id=”note6″ src=”images/note7.png”/> <img id=”note7″ src=”images/note8.png”/> </div> </div>
JavaScript motion detection
使用以下JavaScript代碼段完成Demo
確認應用是否可以使用getUserMedia API。
確認已接收webcam stream
載入木琴琴譜的音頻
啟動時間間隔,并調(diào)用Update函數(shù)
在每一個時間間隔中,在canvas中繪制一張webcam feed。
在每一個時間間隔中,把當前的webcam中的圖片和之前的一張融合在一起。
在每一個時間間隔中,在canvas中繪制出融合后的圖片。
在每一個時間間隔中,在木琴的琴譜中檢查像素的顏色值。
在每一個時間間隔中,如果發(fā)現(xiàn)用戶進行操作,彈奏出特定的木琴音符。
第一步:聲明變量
聲明一些用于存儲drawing和motion detection過程中使用到的變量。首先,需要兩個對canvas元素的引用,一個用來存儲每個canvas上下文
的變量,一個用來存儲已繪制的webcam stream。也需要存儲每個音符x軸線的位置,彈奏的樂譜,和一些與聲音相關(guān)的變量。
- var notesPos = [0, 82, 159, 238, 313, 390, 468, 544];
- var timeOut, lastImageData;
- var canvasSource = $(“#canvas-source”)[0];
- var canvasBlended = $(“#canvas-blended”)[0];
- var contextSource = canvasSource.getContext(’2d’);
- var contextBlended = canvasBlended.getContext(’2d’);
- var soundContext, bufferLoader;
- var notes = [];
同時還需要倒置webcam stream的軸線,讓用戶感覺自己是在鏡子前。這使得他們的彈奏更順暢。下面是完成該功能需要的代碼段:
contextSource.translate(canvasSource.width, 0); contextSource.scale(-1, 1);
第二步:升級和視頻繪制
創(chuàng)建一個名為Update的函數(shù),并設(shè)定一秒鐘執(zhí)行60次,其中調(diào)用其它一些函數(shù)。
- function update() {
- drawVideo();
- blend();
- checkAreas();
- timeOut = setTimeout(update, 1000/60);
- }
把視頻繪制到canvas中非常簡單,只有一行代碼
- function drawVideo() {
- contextSource.drawImage(video, 0, 0, video.width, video.height); }
第三步:創(chuàng)建blend mode difference
創(chuàng)建一個幫助函數(shù),確保像素相減得到的值務必為正值。你可以使用內(nèi)置函數(shù)Math.abs,不過我用二進制運算符寫了一個差不多的函數(shù).大部分情況下,使用二進制運算結(jié)構(gòu)能夠提升性能,你不一定要了解其中的細節(jié),只要使用以下的代碼即可:
- function fastAbs(value) {
- // equivalent to Math.abs(); return (value ^ (value >> 31)) - (value >> 31);
- }
下面我們寫一個blend mode difference函數(shù),這個函數(shù)包含三個形參:
一個二維數(shù)組用來存儲減法得到的結(jié)果
一個二維數(shù)組存儲當前webcam stream中圖片的像素
一個二維數(shù)組存儲之前webcam stream中圖片的像素
存儲像素的數(shù)組是二維的,并且還包含了Red,Green,Blue和Alpha等通道值.
· pixels[0] = red value
· pixels[1] = green value
· pixels[2] = blue value
· pixels[3] = alpha value
· pixels[4] = red value
· pixels[5] = green value
等等……
如示例中所示,webcam stream的寬為640像素,高為480像素。因此,數(shù)組的大小為640*480*4=1228000.
循環(huán)處理這些像素數(shù)組的最好方式是把值擴大4倍(紅、綠、藍、Alpha),這意味著現(xiàn)在只需要處理 307,200 次–情況更加樂觀一些。
- function difference(target, data1, data2) {
- var i = 0;
- while (i < (data1.length / 4)) {
- var red = data1[i*4];
- var green = data1[i*4+1];
- var blue = data1[i*4+2]; var alpha = data1[i*4+3]; ++i;
- }
- }
現(xiàn)在你可以對圖片中的像素進行減法運算—從效果上來看,會有大不同—如果顏色通道值已經(jīng)為0,停止減法運行,并自動設(shè)置Alpha的值為255(0xFF)。下面是已經(jīng)完成的blend mode difference函數(shù)(可以進行自主優(yōu)化):
function difference(target, data1, data2) { // blend mode difference if (data1.length != data2.length) return null; var i = 0; while (i < (data1.length * 0.25)) { target[4*i] = data1[4*i] == 0 ? 0 : fastAbs(data1[4*i] - data2[4*i]); target[4*i+1] = data1[4*i+1] == 0 ? 0 : fastAbs(data1[4*i+1] - data2[4*i+1]); target[4*i+2] = data1[4*i+2] == 0 ? 0 : fastAbs(data1[4*i+2] - data2[4*i+2]); target[4*i+3] = 0xFF; ++i; } }
我在Demo中使用的是一個稍微不同的版本,以取得更好的準確性。我寫了一個threshold函數(shù),應用于顏色值。當顏色值低于臨界值的時候,該方法把顏色像素值更改為黑色,而高于臨界值時,將顏色像素值更改為白色。你可以使用以下的代碼段完成該功能。
- function threshold(value) {
- return (value > 0×15) ? 0xFF : 0;
- }
同樣我也計算了三種顏色通道的平均值,它們在圖片中的像素為黑色或白色 。
- function differenceAccuracy(target, data1, data2)
- { if (data1.length != data2.length) return null;
- var i = 0;
- while (i < (data1.length * 0.25)) {
- var average1 = (data1[4*i] + data1[4*i+1] + data1[4*i+2]) / 3;
- var average2 = (data2[4*i] + data2[4*i+1] + data2[4*i+2]) / 3;
- var diff = threshold(fastAbs(average1 - average2)); target[4*i] = diff;
- target[4*i+1] = diff;
- target[4*i+2] = diff; target[4*i+3] = 0xFF; ++i; }
- }
結(jié)果如下面的黑白圖片所示:
圖3.blend mode difference的結(jié)果以黑白圖片顯示
第4步:blend canvas
下面我們創(chuàng)建一個函數(shù),用來融合圖片,你只需要把正確的值傳給它:像素數(shù)組。
JavaScript的繪圖API提供了一個方法,。這個對象包含了很多有用的屬性,比如寬度和高度,同樣還有數(shù)據(jù)屬性,正是你需要用到的像素數(shù)組。同樣,創(chuàng)建一個空的ImageData示例,用來存儲結(jié)果,并把當前的webcam圖片存儲下來,以便重復使用。下面是在canvas中融合并繪制結(jié)果所用到的代碼段:
- function blend() {
- var width = canvasSource.width;
- var height = canvasSource.height;
- // get webcam image data
- var sourceData = contextSource.getImageData(0, 0, width, height);
- // create an image if the previous image doesn’t exist
- if (!lastImageData) lastImageData = contextSource.getImageData(0, 0, width, height);
- // create a ImageData instance to receive the blended result
- var blendedData = contextSource.createImageData(width, height);
- // blend the 2 images
- differenceAccuracy(blendedData.data, sourceData.data, lastImageData.data);
- // draw the result in a canvas contextBlended.putImageData(blendedData, 0, 0);
- // store the current webcam image lastImageData = sourceData;
- }
第5步:search for pixel
這是完成Demo的最后一步,這就是motion detection,這里用到上一節(jié)我們創(chuàng)建的融合的圖片。
在準備階段,放入了8張木琴音符的圖片。用這些音符的位置和大小作融合后圖像的像素。然后,重復找出白色的像素。
在重復的過程中,計算出顏色通道的平均值,使用變量存儲計算結(jié)果。重復過程完成之后,計算出整張圖片的像素的總平均值。
通過設(shè)定臨界值10來避免噪音和一些細微的操作,如果你發(fā)現(xiàn)大于10的數(shù)值,則應當考慮到在最后一張圖像之后,用戶進行了操作。這就是運動追蹤。
然后,播放出相對應的音符,并展示出note rollover。此處用到的函數(shù)如下所示:
- function checkAreas() {
- // loop over the note areas
- for (var r=0; r<8; ++r)
- {
- // get the pixels in a note area from the blended image
- var blendedData = contextBlended.getImageData
- ( notes[r].area.x,
- notes[r].area.y,
- notes[r].area.width,
- notes[r].area.height);
- var i = 0;
- var average = 0;
- // loop over the pixels
- while (i < (blendedData.data.length / 4)) {
- // make an average between the color channel
- average += (blendedData.data[i*4] + blendedData.data[i*4+1] + blendedData.data[i*4+2]) / 3;
- ++i; }
- // calculate an average between of the color values of the note area
- average = Math.round(average / (blendedData.data.length / 4));
- if (average > 10) {
- // over a small limit, consider that a movement is detected
- // play a note and show a visual feedback to the user
- playSound(notes[r]);
- notes[r].visual.style.display = ”block”;
- $(notes[r].visual).fadeOut(); } }
相關(guān)鏈接
我希望這篇文章可以帶給你更多的靈感,開發(fā)出基于視頻的交互程序,
通過以下的Chrome和Opera瀏覽器的開發(fā)和W3C網(wǎng)站上有關(guān)API的草案,你可以學到更多有關(guān)getUserMedia接口的知識。
如果你想找到更多關(guān)于motion detection或者基于視頻的應用的靈感,我推薦你遵照以下一些Flash開發(fā)人員和motion設(shè)計者使用After Effects得出的意見。現(xiàn)在Java Script和HTML勢頭正勁,開發(fā)出了很多新的功能。要學習的內(nèi)容有很多,你可以參考以下資源:
· GetUserMedia article to get started
· 優(yōu)秀的開發(fā)者博客,如何找到靈感以及寫出更好的代碼:
· Motion 設(shè)計者的博客,Video Copilot