來看看機智的前端童鞋怎麼防盜

不少開發的童鞋都是隻身混江湖、夜宿城中村,若是居住的地方安保欠缺,那麼出門在外不免擔憂屋裏的財產安全。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上,有興趣的童鞋能夠自助下載。共勉~

相關文章
相關標籤/搜索