不少開發的童鞋都是隻身混江湖、夜宿城中村,若是居住的地方安保欠缺,那麼出門在外不免擔憂屋裏的財產安全。css
事實上世面上有不少高大上的防盜設備,但對於機智的前端童鞋來講,只要有一臺附帶攝像頭的電腦,就能夠簡單地實現一個防盜監控系統~html
純 JS 的「防盜」能力很大程度藉助於 H5 canvas 的力量,且很是有意思。若是你對 canvas 還不熟悉,能夠先點這裏閱讀個人系列教程。前端
step1. 調用攝像頭git
咱們須要先在瀏覽器上訪問和調用攝像頭,用來監控屋子裏的一舉一動。不一樣瀏覽器中調用攝像頭的 API 都略有出入,在這裏咱們以 chrome 作示例:github
<video width="640" height="480" autoplay></video> <script> var video = document.querySelector('video'); navigator.webkitGetUserMedia({ video: true }, success, error); function success(stream) { video.src = window.webkitURL.createObjectURL(stream); video.play(); } function error(err) { alert('video error: ' + err) } </script>
運行頁面後,瀏覽器出於安全性考慮,會詢問是否容許當前頁面訪問你的攝像頭設備,點擊「容許」後便能直接在 <video> 上看到攝像頭捕獲到的畫面了:web
step2. 捕獲 video 幀畫面ajax
光是開着攝像頭監視房間可沒有任何意義,瀏覽器不會幫你對監控畫面進行分析。因此這裏咱們得手動用腳本捕獲 video 上的幀畫面,用於在後續進行數據分析。chrome
從這裏開始我們就要藉助 canvas 力量了。在 Canvas入門(五)一文咱們介紹過 ctx.drawImage() 方法,經過它能夠捕獲 video 幀畫面並渲染到畫布上。canvas
咱們須要建立一個畫布,而後這麼寫:跨域
<video width="640" height="480" autoplay></video> <canvas width="640" height="480"></canvas> <script> var video = document.querySelector('video'); var canvas = document.querySelector('canvas'); // video捕獲攝像頭畫面 navigator.webkitGetUserMedia({ video: true }, success, error); function success(stream) { video.src = window.webkitURL.createObjectURL(stream); video.play(); } function error(err) { alert('video error: ' + err) } //canvas var context = canvas.getContext('2d'); setTimeout(function(){ //把當前視頻幀內容渲染到畫布上 context.drawImage(video, 0, 0, 640, 480); }, 5000); </script>
如上代碼所示,5秒後把視頻幀內容渲染到畫布上(下方右圖):
step3. 對捕獲的兩個幀畫面執行差別混合
在上面咱們提到過,要有效地識別某個場景,須要對視頻畫面進行數據分析。
那麼要怎麼識別我們的房子是否有人忽然闖入了呢?答案很簡單 —— 定時地捕獲 video 畫面,而後對比先後兩幀內容是否存在較大變化。
咱們先簡單地寫一個定時捕獲的方法,並將捕獲到的幀數據存起來:
//canvas var context = canvas.getContext('2d'); var preFrame, //前一幀 curFrame; //當前幀 //捕獲並保存幀內容 function captureAndSaveFrame(){ console.log(context); preFrame = curFrame; context.drawImage(video, 0, 0, 640, 480); curFrame = canvas.toDataURL; //轉爲base64並保存 } //定時捕獲 function timer(delta){ setTimeout(function(){ captureAndSaveFrame(); timer(delta) }, delta || 500); } timer();
如上代碼所示,畫布會每隔500毫秒捕獲並渲染一次 video 的幀內容(夭壽哇,作完這個動做不當心把餅乾灑了一地。。。\("▔□▔)/):
留意這裏咱們使用了 canvas.toDataURL 方法來保存幀畫面。
接着就是數據分析處理了,咱們能夠經過對比先後捕獲的幀畫面來判斷攝像頭是否監控到變化,那麼怎麼作呢?
熟悉設計的同窗確定經常使用一個圖層功能 —— 混合模式:
當有兩個圖層時,對頂層圖層設置「差值/Difference」的混合模式,能夠一目瞭然地看到兩個圖層的差別:
「圖A」是我去年在公司樓下拍的照片,而後我把它稍微調亮了一點點,並在上面畫了一個 X 和 O 獲得「圖B」。接着我把它們以「差值」模式混合在一塊兒,獲得了最右的這張圖。
「差值」模式原理:要混合圖層雙方的RGB值中每一個值分別進行比較,用高值減去低值做爲合成後的顏色,一般用白色圖層合成一圖像時,能夠獲得負片效果的反相圖像。用黑色的話不發生任何變化(黑色亮度最低,下層顏色減去最小顏色值0,結果和原來同樣),而用白色會獲得反相效果(下層顏色被減去,獲得補值),其它顏色則基於它們的亮度水平
在CSS3中,已經有 blend-mode 特性來支持這個有趣的混合模式,不過咱們發現,在主流瀏覽器上,canvas 的 globalCompositeOperation 接口也已經良好支持了圖像混合模式:
因而咱們再建多一個畫布來展現先後兩幀差別:
<video width="640" height="480" autoplay></video> <canvas width="640" height="480"></canvas> <canvas width="640" height="480"></canvas> <script> var video = document.querySelector('video'); var canvas = document.querySelectorAll('canvas')[0]; var canvasForDiff = document.querySelectorAll('canvas')[1]; // video捕獲攝像頭畫面 navigator.webkitGetUserMedia({ video: true }, success, error); function success(stream) { video.src = window.URL.createObjectURL(stream); video.play(); } function error(err) { alert('video error: ' + err) } //canvas var context = canvas.getContext('2d'), diffCtx = canvasForDiff.getContext('2d'); //將第二個畫布混合模式設爲「差別」 diffCtx.globalCompositeOperation = 'difference'; var preFrame, //前一幀 curFrame; //當前幀 //捕獲並保存幀內容 function captureAndSaveFrame(){ preFrame = curFrame; context.drawImage(video, 0, 0, 640, 480); curFrame = canvas.toDataURL(); //轉爲base64並保存 } //繪製base64圖像到畫布上 function drawImg(src, ctx){ ctx = ctx || diffCtx; var img = new Image(); img.src = src; ctx.drawImage(img, 0, 0, 640, 480); } //渲染先後兩幀差別 function renderDiff(){ if(!preFrame || !curFrame) return; diffCtx.clearRect(0, 0, 640, 480); drawImg(preFrame); drawImg(curFrame); } //定時捕獲 function timer(delta){ setTimeout(function(){ captureAndSaveFrame(); renderDiff(); timer(delta) }, delta || 500); } timer(); </script>
效果以下(夭壽啊,作完這個動做我又把雪碧灑在鍵盤上了。。。(#--)/ ):
能夠看到,當先後兩幀差別不大時,第三個畫布幾乎是黑乎乎的一片,只有當攝像頭捕獲到動做了,第三個畫布纔有明顯的高亮內容出現。
所以,咱們只須要對第三個畫布渲染後的圖像進行像素分析——判斷其高亮閾值是否達到某個指定預期:
var context = canvas.getContext('2d'), diffCtx = canvasForDiff.getContext('2d'); //將第二個畫布混合模式設爲「差別」 diffCtx.globalCompositeOperation = 'difference'; var preFrame, //前一幀 curFrame; //當前幀 var diffFrame; //存放差別幀的imageData //捕獲並保存幀內容 function captureAndSaveFrame(){ preFrame = curFrame; context.drawImage(video, 0, 0, 640, 480); curFrame = canvas.toDataURL(); //轉爲base64並保存 } //繪製base64圖像到畫布上 function drawImg(src, ctx){ ctx = ctx || diffCtx; var img = new Image(); img.src = src; ctx.drawImage(img, 0, 0, 640, 480); } //渲染先後兩幀差別 function renderDiff(){ if(!preFrame || !curFrame) return; diffCtx.clearRect(0, 0, 640, 480); drawImg(preFrame); drawImg(curFrame); diffFrame = diffCtx.getImageData( 0, 0, 640, 480 ); //捕獲差別幀的imageData對象 } //計算差別 function calcDiff(){ if(!diffFrame) return 0; var cache = arguments.callee, count = 0; cache.total = cache.total || 0; //整個畫布都是白色時全部像素的值的總和 for (var i = 0, l = diffFrame.width * diffFrame.height * 4; i < l; i += 4) { count += diffFrame.data[i] + diffFrame.data[i + 1] + diffFrame.data[i + 2]; if(!cache.isLoopEver){ //只需在第一次循環裏執行 cache.total += 255 * 3; //單個白色像素值 } } cache.isLoopEver = true; count *= 3; //亮度放大 //返回「差別畫布高亮部分像素總值」佔「畫布全亮狀況像素總值」的比例 return Number(count/cache.total).toFixed(2); } //定時捕獲 function timer(delta){ setTimeout(function(){ captureAndSaveFrame(); renderDiff(); setTimeout(function(){ console.log(calcDiff()); }, 10); timer(delta) }, delta || 500); } timer();
注意這裏咱們使用了 count *= 3 來放大差別高亮像素的亮度值,否則得出的數值實在過小了。咱們運行下頁面(圖片較大加載會有點慢):
通過試(xia)驗(bai),我的以爲若是 calcDiff() 返回的比值若是大於 0.20,那麼就能夠定性爲「一間空屋子,忽然有人闖進來」的狀況了。
step4. 上報異常圖片
當上述的計算髮現有情況時,須要有某種途徑通知咱們。有錢有精力的話能夠部署個郵件服務器,直接發郵件甚至短信通知到本身,but 本文走的吃吐少年路線,就不搞的那麼高端了。
那麼要如何簡單地實現異常圖片的上報呢?我暫且想到的是 —— 直接把問題圖片發送到某個站點中去。
這裏咱們選擇博客園的「日記」功能,它能夠隨意上傳相關內容。
p.s.,其實這裏本來是想直接把圖片傳到博客園相冊上的,惋惜POST請求的圖片實體要求走 file 格式,即沒法經過腳本更改文件的 input[type=file],轉 Blob 再上傳也沒用,只好做罷。
糾正上述p.s.內容~ 後續發現 formData.append 支持第三個參數做爲 filename 屬性,因此實際上是能夠轉 blob 上傳的。
咱們在管理後臺建立日記時,經過 Fiddler 抓包能夠看到其請求參數很是簡單:
從而能夠直接構造一個請求:
//異常圖片上傳處理 function submit(){ //ajax 提交form $.ajax({ url : 'http://i.cnblogs.com/EditDiary.aspx?opt=1', type : "POST", data : { '__VIEWSTATE': '', '__VIEWSTATEGENERATOR': '4773056F', 'Editor$Edit$txbTitle': '告警' + Date.now(), 'Editor$Edit$EditorBody': '<img src="' + curFrame + '" />', 'Editor$Edit$lkbPost': '保存' }, success: function(){ console.log('submit done') } }); }
固然若是請求頁面跟博客園域名不一樣,是沒法發送 cookie 致使請求跨域而失效,不過這個很好解決,直接修改 host 便可(怎麼修改就不介紹了,自行百度吧)。
我這邊改完 host,經過 http://i.cnblogs.com/h5monitor/final.html 的地址訪問頁面,發現攝像頭居然失效了~
經過谷歌的文檔能夠得知,這是爲了安全性考慮,非 HTTPS 的服務端請求都不能接入攝像頭。不過解決辦法也是有的,以 window 系統爲例,打開 cmd 命令行面板並定位到 chrome 安裝文件夾下,而後執行:
chrome --unsafely-treat-insecure-origin-as-secure="http://i.cnblogs.com/h5monitor/final.html" --user-data-dir=C:\testprofile
此舉將以沙箱模式打開一個獨立的 chrome 進程,並對指定的站點去掉安全限制。注意我們在新開的 chrome 中得從新登陸博客園。
這時候便能正常訪問攝像頭了,咱們對代碼作下處理,當差別檢測發現異常時,建立一份日記,最小間隔時間爲5秒(不事後來發現不必,由於博客園已經有作了時間限制,差很少10秒後才能發佈新的日記):
//定時捕獲 function timer(delta){ setTimeout(function(){ captureAndSaveFrame(); renderDiff(); if(calcDiff() > 0.2){ //監控到異常,發日誌 submit() } timer(delta) }, delta || 500); } setTimeout(timer, 60000 * 10); //設定打開頁面十分鐘後纔開始監控 //異常圖片上傳處理 function submit(){ var cache = arguments.callee, now = Date.now(); if(cache.reqTime && (now - cache.reqTime < 5000)) return; //日記建立最小間隔爲5秒 cache.reqTime = now; //ajax 提交form $.ajax({ url : 'http://i.cnblogs.com/EditDiary.aspx?opt=1', type : "POST", timeout : 5000, data : { '__VIEWSTATE': '', '__VIEWSTATEGENERATOR': '4773056F', 'Editor$Edit$txbTitle': '告警' + Date.now(), 'Editor$Edit$EditorBody': '<img src="' + curFrame + '" />', 'Editor$Edit$lkbPost': '保存' }, success: function(){ console.log('submit done') }, error: function(err){ cache.reqTime = 0; console.log('error: ' + err) } }); }
執行效果:
日記也是妥妥的出來了:
點開就能看到異常的那張圖片了:
要留意的是,博客園對日記發佈數量是有作每日額度限制來防刷的,達到限額的話會致使當天的隨筆和文章也沒法發佈,因此得謹慎使用:
不過這種形式僅能上報異常圖片,暫時沒法讓咱們及時收悉告警,有興趣的童鞋能夠試着再寫個 chrome 插件,定時去拉取日記列表作判斷,若是有新增日記則觸發頁面 alert。
另外咱們固然但願能直接對闖入者進行警告,這塊比較好辦 —— 搞個警示的音頻,在異常的時候觸發播放便可:
//播放音頻 function fireAlarm(){ audio.play() } //定時捕獲 function timer(delta){ setTimeout(function(){ captureAndSaveFrame(); if(preFrame && curFrame){ renderDiff(); if(calcDiff() > 0.2){ //監控到異常 //發日記 submit(); //播放音頻告警 fireAlarm(); } } timer(delta) }, delta || 500); } setTimeout(timer, 60000 * 10); //設定打開頁面十分鐘後纔開始監控
最後說一下,本文代碼均掛在個人github上,有興趣的童鞋能夠自助下載。共勉~