Web技術(shù)實現(xiàn)移動監(jiān)測的介紹
Web技術(shù)實現(xiàn)移動監(jiān)測的介紹
由上述引用語句可得出“移動監(jiān)測”需要以下要素:
一個擁有攝像頭的計算機(jī)用于判斷移動的算法移動后的處理
注:本文涉及的所有案例均基于 PC/Mac 較新版本的 Chrome / Firefox 瀏覽器,部分案例需配合攝像頭完成,所有截圖均保存在本地。
對方不想和你說話,并向你扔來一個鏈接:
綜合案例
該案例有以下兩個功能:
拍好 POST 后的 1 秒會進(jìn)行拍照靜止 1 秒后音樂會停止,產(chǎn)生移動會恢復(fù)播放狀態(tài)
上述案例也許并不能直接體現(xiàn)出『移動監(jiān)測』的實際效果和原理,下面再看看這個案例。
像素差異
案例的左側(cè)是視頻源,而右側(cè)則是移動后的像素處理(像素化、判斷移動和只保留綠色等)。
因為是基于 Web 技術(shù),所以視頻源采用 WebRTC,像素處理則采用 Canvas。
視頻源
不依賴 Flash 或 Silverlight,我們使用 WebRTC (Web Real-Time Communications) 中的 navigator.getUserMedia() API,該 API 允許 Web 應(yīng)用獲取用戶的攝像頭與麥克風(fēng)流(stream)。
示例代碼如下:
<!-- 若不加 autoplay,則會停留在第一幀 --> <video id="video" autoplay></video> // 具體參數(shù)含義可看相關(guān)文檔。 const constraints = { audio: false, video: { width: 640, height: 480 } } navigator.mediaDevices.getUserMedia(constraints) .then(stream => { // 將視頻源展示在 video 中 video.srcObject = stream }) .catch(err => { console.log(err) })
對于兼容性問題,Safari 11 開始支持 WebRTC 了。具體可查看 caniuse。
像素處理
在得到視頻源后,我們就有了判斷物體是否移動的素材。當(dāng)然,這里并沒有采用什么高深的識別算法,只是利用連續(xù)兩幀截圖的像素差異來判斷物體是否發(fā)生移動(嚴(yán)格來說,是畫面的變化)。
截圖
獲取視頻源截圖的示例代碼:
const video = document.getElementById('video') const canvas = document.createElement('canvas') const ctx = canvas.getContext('2d') canvas.width = 640 canvas.height = 480 // 獲取視頻中的一幀 function capture () { ctx.drawImage(video, 0, 0, canvas.width, canvas.height) // ...其它操作 }
得出截圖間的差異
對于兩張圖的像素差異,在 凹凸實驗室 的 《“等一下,我碰!”——常見的2D碰撞檢測》 這篇博文中所提及的“像素檢測”碰撞算法是解決辦法之一。該算法是通過遍歷兩個離屏畫布(offscreen canvas)同一位置的像素點的透明度是否同時大于 0,來判斷碰撞與否。當(dāng)然,這里要改為『同一位置的像素點是否不同(或差異小于某閾值)』來判斷移動與否。
但上述方式稍顯麻煩和低效,這里我們采用 ctx.globalCompositeOperation = 'difference'
指定畫布新增元素(即第二張截圖與第一張截圖)的合成方式,得出兩張截圖的差異部分。
示例代碼:
function diffTwoImage () { // 設(shè)置新增元素的合成方式 ctx.globalCompositeOperation = 'difference' // 清除畫布 ctx.clearRect(0, 0, canvas.width, canvas.height) // 假設(shè)兩張圖像尺寸相等 ctx.drawImage(firstImg, 0, 0) ctx.drawImage(secondImg, 0, 0) }
兩張圖的差異
體驗上述案例后,是否有種當(dāng)年玩“QQ游戲《大家來找茬》”的感覺。另外,這個案例可能還適用于以下兩種情況:
- 當(dāng)你不知道設(shè)計師前后兩次給你的設(shè)計稿有何差異時
- 想查看兩個瀏覽器對同一個網(wǎng)頁的渲染有何差異時何時為一個“動作”
由上述“兩張圖像差異”的案例中可得:黑色代表該位置上的像素未發(fā)生改變,而像素越明亮則代表該點的“動作”越大。因此,當(dāng)連續(xù)兩幀截圖合成后有明亮的像素存在時,即為一個“動作”的產(chǎn)生。但為了讓程序不那么“敏感”,我們可以設(shè)定一個閾值。當(dāng)明亮像素的個數(shù)大于該閾值時,才認(rèn)為產(chǎn)生了一個“動作”。當(dāng)然,我們也可以剔除“不足夠明亮”的像素,以盡可能避免外界環(huán)境(如燈光等)的影響。
想要獲取 Canvas 的像素信息,需要通過 ctx.getImageData(sx, sy, sw, sh)
,該 API 會返回你所指定畫布區(qū)域的像素對象。該對象包含 data
、width
、height
。其中 data
是一個含有每個像素點 RGBA 信息的一維數(shù)組,如下圖所示。
含有 RGBA 信息的一維數(shù)組
獲取到特定區(qū)域的像素后,我們就能對每個像素進(jìn)行處理(如各種濾鏡效果)。處理完后,則可通過 ctx.putImageData()
將其渲染在指定的 Canvas 上。
擴(kuò)展:由于 Canvas 目前沒有提供“歷史記錄”的功能,如需實現(xiàn)“返回上一步”操作,則可通過 getImageData 保存上一步操作,當(dāng)需要時則可通過 putImageData 進(jìn)行復(fù)原。
示例代碼:
let imageScore = 0 const rgba = imageData.data for (let i = 0; i < rgba.length; i += 4) { const r = rgba[i] / 3 const g = rgba[i + 1] / 3 const b = rgba[i + 2] / 3 const pixelScore = r + g + b // 如果該像素足夠明亮 if (pixelScore >= PIXEL_SCORE_THRESHOLD) { imageScore++ } } // 如果明亮的像素數(shù)量滿足一定條件 if (imageScore >= IMAGE_SCORE_THRESHOLD) { // 產(chǎn)生了移動 }
在上述案例中,你也許會注意到畫面是『綠色』的。其實,我們只需將每個像素的紅和藍(lán)設(shè)置為 0,即將 RGBA 的 r = 0; b = 0
即可。這樣就會像電影的某些鏡頭一樣,增加了科技感和神秘感。
const rgba = imageData.data for (let i = 0; i < rgba.length; i += 4) { rgba[i] = 0 // red rgba[i + 2] = 0 // blue } ctx.putImageData(imageData, 0, 0)
將 RGBA 中的 R 和 B 置為 0
跟蹤“移動物體”
有了明亮的像素后,我們就要找出其 x 坐標(biāo)的最小值與 y 坐標(biāo)的最小值,以表示跟蹤矩形的左上角。同理,x 坐標(biāo)的最大值與 y 坐標(biāo)的最大值則表示跟蹤矩形的右下角。至此,我們就能繪制出一個能包圍所有明亮像素的矩形,從而實現(xiàn)跟蹤移動物體的效果。
找出跟蹤矩形的左上角和右下角
示例代碼:
function processDiff (imageData) { const rgba = imageData.data let score = 0 let pixelScore = 0 let motionBox = 0 // 遍歷整個 canvas 的像素,以找出明亮的點 for (let i = 0; i < rgba.length; i += 4) { pixelScore = (rgba[i] + rgba[i+1] + rgba[i+2]) / 3 // 若該像素足夠明亮 if (pixelScore >= 80) { score++ coord = calcCoord(i) motionBox = calcMotionBox(montionBox, coord.x, coord.y) } } return { score, motionBox } } // 得到左上角和右下角兩個坐標(biāo)值 function calcMotionBox (curMotionBox, x, y) { const motionBox = curMotionBox || { x: { min: coord.x, max: x }, y: { min: coord.y, max: y } } motionBox.x.min = Math.min(motionBox.x.min, x) motionBox.x.max = Math.max(motionBox.x.max, x) motionBox.y.min = Math.min(motionBox.y.min, y) motionBox.y.max = Math.max(motionBox.y.max, y) return motionBox } // imageData.data 是一個含有每個像素點 rgba 信息的一維數(shù)組。 // 該函數(shù)是將上述一維數(shù)組的任意下標(biāo)轉(zhuǎn)為 (x,y) 二維坐標(biāo)。 function calcCoord(i) { return { x: (i / 4) % diffWidth, y: Math.floor((i / 4) / diffWidth) } }
在得到跟蹤矩形的左上角和右下角的坐標(biāo)值后,通過 ctx.strokeRect(x, y, width, height)
API 繪制出矩形即可。
ctx.lineWidth = 6 ctx.strokeRect( diff.motionBox.x.min + 0.5, diff.motionBox.y.min + 0.5, diff.motionBox.x.max - diff.motionBox.x.min, diff.motionBox.y.max - diff.motionBox.y.min )
這是理想效果,實際效果請打開 體驗鏈接
擴(kuò)展:為什么上述繪制矩形的代碼中的
x、y
要加0.5
呢?一圖勝千言:
性能縮小尺寸
在上一個章節(jié)提到,我們需要通過對 Canvas 每個像素進(jìn)行處理,假設(shè) Canvas 的寬為 640
,高為 480
,那么就需要遍歷 640 * 480 = 307200
個像素。而在監(jiān)測效果可接受的前提下,我們可以將需要進(jìn)行像素處理的 Canvas 縮小尺寸,如縮小 10 倍。這樣需要遍歷的像素數(shù)量就降低 100
倍,從而提升性能。
示例代碼:
const motionCanvas // 展示給用戶看 const backgroundCanvas // offscreen canvas 背后處理數(shù)據(jù) motionCanvas.width = 640 motionCanvas.height = 480 backgroundCanvas.width = 64 backgroundCanvas.height = 48
尺寸縮小 10 倍
定時器
我們都知道,當(dāng)游戲以『每秒60幀』運行時才能保證一定的體驗。但對于我們目前的案例來說,幀率并不是我們追求的第一位。因此,每 100 毫秒(具體數(shù)值取決于實際情況)取當(dāng)前幀與前一幀進(jìn)行比較即可。
另外,因為我們的動作一般具有連貫性,所以可取該連貫動作中幅度最大的(即“分?jǐn)?shù)”最高)或最后一幀動作進(jìn)行處理即可(如存儲到本地或分享到朋友圈)。
延伸
至此,用 Web 技術(shù)實現(xiàn)簡易的“移動監(jiān)測”效果已基本講述完畢。由于算法、設(shè)備等因素的限制,該效果只能以 2D 畫面為基礎(chǔ)來判斷物體是否發(fā)生“移動”。而微軟的 Xbox、索尼的 PS、任天堂的 Wii 等游戲設(shè)備上的體感游戲則依賴于硬件。以微軟的 Kinect 為例,它為開發(fā)者提供了可跟蹤最多六個完整骨骼和每人 25 個關(guān)節(jié)等強大功能。利用這些詳細(xì)的人體參數(shù),我們就能實現(xiàn)各種隔空的『手勢操作』,如畫圈圈詛咒某人。
下面幾個是通過 Web 使用 Kinect 的庫:
- DepthJS:以瀏覽器插件形式提供數(shù)據(jù)訪問。
- Node-Kinect2: 以 Nodejs 搭建服務(wù)器端,提供數(shù)據(jù)比較完整,實例較多。
- ZigFu:支持 H5、U3D、Flash,API較為完整。
- Kinect-HTML5:Kinect-HTML5 用 C# 搭建服務(wù)端,提供色彩數(shù)據(jù)、深度數(shù)據(jù)和骨骼數(shù)據(jù)。
通過 Node-Kinect2 獲取骨骼數(shù)據(jù)
文章至此就真的要結(jié)束了,如果你想知道更多玩法,請關(guān)注 凹凸實驗室。同時,也希望大家發(fā)掘更多玩法。
參考資料
MOTION DETECTION WITH JAVASCRIPT
相關(guān)文章
JavaScript中Set和Map數(shù)據(jù)結(jié)構(gòu)使用場景詳解
這篇文章主要為大家介紹了JavaScript中Set和Map數(shù)據(jù)結(jié)構(gòu)使用場景詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06js自動閉合html標(biāo)簽(自動補全html標(biāo)記)
假如我有一個DIV,如果沒有閉合后面的樣式都會亂了,這樣的代碼可能會影響后面的樣式,我希望用JS去自動閉合這種沒有閉合的標(biāo)簽2012-10-10JavaScript實現(xiàn)維吉尼亞(Vigenere)密碼算法實例
Vigenere密碼就是一種傳統(tǒng)加密技術(shù),它是多表代換密碼,能夠有效改進(jìn)單表代換密碼的詞頻分布特征問題,本文用JavaScript實現(xiàn)維吉尼亞(Vigenere)密碼算法2013-11-11JavaScript實現(xiàn)shuffle數(shù)組洗牌操作示例
這篇文章主要介紹了JavaScript實現(xiàn)shuffle數(shù)組洗牌操作,結(jié)合實例形式分析了javascript數(shù)組的定義、構(gòu)造、排序等相關(guān)操作技巧,需要的朋友可以參考下2019-01-01js遍歷、動態(tài)的添加數(shù)據(jù)的小例子
js遍歷、動態(tài)的添加數(shù)據(jù)的小例子,需要的朋友可以參考一下2013-06-06javascript實現(xiàn)鼠標(biāo)移到Image上方時顯示文字效果的方法
這篇文章主要介紹了javascript實現(xiàn)鼠標(biāo)移到Image上方時顯示文字效果的方法,涉及javascript鼠標(biāo)事件及圖文屬性動態(tài)設(shè)置的相關(guān)技巧,可用于為圖片增加文字提示效果,需要的朋友可以參考下2015-08-08