我的three.js學習記錄(三)

此次的亮點不是three.js的3d部分,而是通過調用攝像頭然後通過攝像頭的圖像變化進行簡單的判斷後進行一些操作。上篇中我通過簡單的示例分析來學習three.js,這次是通過上一篇的一些代碼來與攝像頭判斷部分的代碼相互結合,弄一個新的東西,可以看下圖
示例

說明

這次的示例是我們可以通過一個攝像頭隔空控制我們屏幕中的視頻的播放。
原理其實也是很簡單,我們看到的攝像頭圖像其實是通過獲取到的圖像數據然後再通過canvas 畫上去的,這裏有兩層canvas 一層是我們的正常的攝像頭輸出,一層是我們的播放按鈕和暫停按鈕。然後還有一層canvas被我們隱藏了,這層隱藏的就是將我們的上次的圖像輸出記錄下來作爲緩存的作用。這層隱藏的canvas和圖像輸出的進行特定區域(按鈕區域)的RGB值的判斷,如果判斷到波動了一定的範圍,那麼我們就進行特定的操作。

相關
我們這裏除了需要用到我們three.js的相關知識還需要用到canvas和js調用攝像頭的工作,所以這裏我給出一些鏈接,希望有用
https://developer.mozilla.org/zh-CN/docs/Web/API/Navigator/getUserMedia
http://blog.csdn.net/qq_16559905/article/details/51743588
http://www.w3school.com.cn/tags/html_ref_canvas.asp
http://www.w3school.com.cn/tags/tag_canvas.asp

準備工作

通過以上的說明,我們現在開始工作
在準備工作中我們需要用到我的three.js學習記錄(一)我的three.js學習記錄(二)的一些東西,這裏就不一一列舉出來了。
首先,我們先來看一下html代碼

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>webcam_demo</title>
    <style>
        body {
            background-color: #000;
            color: #fff;
            margin: 0;
            overflow: hidden;
        }
    </style>
    <script src="js/three.js"></script>
    <script src="js/stats.min.js"></script>
    <script src="js/dat.gui.js"></script>
    <script src="js/Detector.js"></script>
    <script src="js/DDSLoader.js"></script>
    <script src="js/day1020.js"></script>
    <script src="js/OrbitControls.js"></script>
</head>
<body>
<!--加載我們要看的視頻-->
<video id="video" src="video/sintel.ogv" style="display: none; left: 15px; top: 75px;"></video>
<!--加載我們的攝像機的圖像-->
<video id="webcam" autoplay style="display: none; width: 320px; height: 240px;"></video>

<div id="canvasLayers" style="position: relative; left: 0; top: 0;">
    <!--畫攝像頭輸出圖像-->
    <canvas id="videoCanvas" width="320" height="240" style="z-index: 1; position: absolute; left:0; top:0; opacity:0.5;"></canvas>
    <!--畫出按鈕-->
    <canvas id="layer2"  width="320" height="240" style="z-index: 2; position: absolute; left:0; top:0; opacity:0.5;"></canvas>
</div>
<canvas id="blendCanvas" style="display: none; position: relative; left: 320px; top: 240px; width: 320px; height: 240px;"></canvas>

<!--加載攝像機,主要是將我們的攝像機獲取的圖像數據放入video#webcam中-->
<script>
    navigator.getUserMedia = navigator.getUserMedia ||
        navigator.webkitGetUserMedia ||
        navigator.mozGetUserMedia;

    if (navigator.getUserMedia) {
        navigator.getUserMedia({ audio: false, video: { width: 1280, height: 720 } },
            function(stream) {
                var video = document.querySelector('#webcam');
                video.srcObject = stream;
                video.onloadedmetadata = function(e) {
                    video.play();
                };
            },
            function(err) {
                console.log("當前錯誤:" + err.name);
            }
        );
    } else {
        console.log("設備不支持");
    }
</script>

<!--用作渲染器的容器-->
<div id="webgl" style="position: absolute; left: 0; top: 0;"></div>
<!--這裏的js文件是操作我們圖像處理部分的-->
<script src="js/webcam.js"></script>
<script type="text/javascript">
    threeStart();
</script>

</body>
</html>

上面的html代碼主要是佈局還有將我們需要的視頻以及我們的攝像機需要的圖像數據加載進來,其他的處理部分則是調用了threeStart()來實現

開發

現在我們已經搭建好了一切我們需要的(上兩篇博客中已經有了,只是沿用上次的進行開發),現在我們來處理我們獲取到的圖像數據,然後進行判斷圖像的變化,如果變化的量超過了一定的值我們就進行特定的操作。

這裏不同於上一篇博客我的three.js學習記錄(二)的地方除了減少了一些東西外,我們的arimate()函數也做了一些變化,如下:

/**
 * 回調函數,重畫整個場景
 */
var isPlayTv = false;
function arimate() {

    if (isPlayTv && video.readyState === video.HAVE_ENOUGH_DATA) {
        if (texture) texture.needsUpdate = true;
        // video.play();
    }
    //將我們的攝像頭的圖像和按鈕圖片分別放入兩層canvas中
    renderWebcam();
    blender();
    checkUpdate(function (msg) {
        if (msg === 'play') {
            isPlayTv = true;
            video.play();
        } else {
            isPlayTv = false;
            video.pause();
        }
    });
    //渲染
    renderer.render(scene, camera);
    //fps狀態更新
    stats.update();
    //重新調用arimate
    requestAnimationFrame(arimate);
}

接下來我們來看看上面的三個函數,分別是renderWebcam()blender()checkUpdate(func)
這三個函數的代碼如下:

//我們從攝像機獲取的圖像數據就存放於video#webcam
var webcam = document.getElementById('webcam');
//畫出我們的攝像機圖像
var videoCanvas = document.getElementById('videoCanvas');
var videoContext = videoCanvas.getContext('2d');

//專門用於畫出按鈕(播放和暫停)
var layer2Canvas = document.getElementById('layer2');
var layer2Context = layer2Canvas.getContext('2d');

var blendCanvas = document.getElementById('blendCanvas');
//這裏主要是用於緩衝,儲存上一個視頻圖像與下一個視頻圖像之間的變化
var blendContext = blendCanvas.getContext('2d');

//這裏是加入我們的兩個按鈕(分別都是圖片)
var buttons = [];

var button1 = new Image();
button1.src ="img/play.png";
var buttonData2 = { name:"play", image:button1, x:320 - 64 - 20, y:10, w:32, h:32 };
buttons.push( buttonData2 );

var button2 = new Image();
button2.src ="img/pause.png";
var buttonData3 = { name:"pause", image:button2, x:320 - 32 - 10, y:10, w:32, h:32 };
buttons.push( buttonData3 );

// 這裏能將視頻反轉,缺一不可,是一個搭配
videoContext.translate(320, 0);
videoContext.scale(-1, 1);

// 設置背景顏色,如果沒有視頻輸出顯示該顏色
videoContext.fillStyle = '#005337';
videoContext.fillRect( 0, 0, videoCanvas.width, videoCanvas.height );

var lastImage;
/**
 * 功能:主要是拿我們當前的canvas#videoCanvas上下文中的圖像數據,與我們上次的數據lastImage作比較
 * 然後將我們比較後的數據的結果放入canvas#blendCanvas的上下文,當我們調用checkUpdate(func)
 * 就能調用canvas#blendCanvas的上下文的數據做判斷是否按鈕區域的rgb是否變化然後調用func
 */
function blender() {
    var width = videoCanvas.width;
    var height = videoCanvas.height;

    //獲取攝像機視頻中的圖像資源信息,包括了rgba,是一個數組,數組大小是像素的4倍(rgba)
    //r = temp[0]; g = temp[1]; b = temp[2]; a = temp[3];
    var source = videoContext.getImageData(0, 0, width, height);
    //創建一個跟視頻cavas一樣大小的圖像數據區
    var blend = videoContext.createImageData(width, height);
    //如果沒有上次的數據則置入本次的圖像數據
    if (!lastImage)lastImage = videoContext.getImageData(0, 0, width, height);
    //判斷我們rgb的值有沒有變化
    differenceAccuracy(blend.data, source.data, lastImage.data);
    //將我們判斷後的數據blend放入blendCanvas上下文
    blendContext.putImageData(blend, 0, 0);
    //將我們上次的數據置爲本次
    lastImage = source;

    /**
     * 混合源rgb值和前rgb值,得到當前像素點是否發生改變 改變用1表示,不改變用0表示
     * @param targetData 轉換的目標rgba數組
     * @param sourceData 源,即當前的視頻圖像rgba數組
     * @param lastData 上一個圖像數組
     */
    function differenceAccuracy(targetData, sourceData, lastData) {
        if (sourceData.length !== lastData.length) return null;
        var i = 0;
        //這裏sourceData.length * 0.25只是獲取圖像的1/4
        //這裏用一維數組獲取數據是因爲整個圖像rbga二維值都使用一維數組
        while (i < (sourceData.length * 0.25))
        {
            //這裏每隔4個像素點獲取一個像素rgba值
            var average1 = (sourceData[4*i] + sourceData[4*i+1] + sourceData[4*i+2]) / 3;
            var average2 = (lastData[4*i] + lastData[4*i+1] + lastData[4*i+2]) / 3;
            //算出我們的上一個和當前的圖像數據的值是否超過一個規定的值(可以理解爲對變化的敏感度)
            //如果是則將diff置爲0xFF,否0
            var diff = threshold(Math.abs(average1 - average2));
            
            //將算出的值放入targetData
            targetData[4*i]   = diff;
            targetData[4*i+1] = diff;
            targetData[4*i+2] = diff;
            targetData[4*i+3] = 0xFF;
            ++i;
        }

        function threshold(value)
        {
            return (value > 0x15) ? 0xFF : 0;
        }
    }
}

function renderWebcam() {
    if ( webcam.readyState === webcam.HAVE_ENOUGH_DATA ){
        //將我們video#webcam的圖像數據使用videoCanvas畫出來
        videoContext.drawImage(webcam, 0, 0, videoCanvas.width, videoCanvas.height);
        //畫出我們的圖片按鈕播放和暫停
        for ( var i = 0; i < buttons.length; i++ )
            layer2Context.drawImage( buttons[i].image, buttons[i].x, buttons[i].y, buttons[i].w, buttons[i].h );
    }
}

/**
 * 判斷canvas#blendCanvas的上下文數據總體上是否變化,如果是則調用func
 * @param func 回調
 */
function checkUpdate(func) {
    //我們這裏是循環按鈕的個數,我們這裏有兩個按鈕,有可能兩個按鈕都有變化
    for (var i = 0; i < buttons.length; i++){
        var data = blendContext.getImageData(buttons[i].x, buttons[i].y, buttons[i].w, buttons[i].h).data;
        //儲存當前區域的countPixels數量的rgb相加的總值
        var sum = 0;
        //countPixels是我們區域中所有的像素點的1/4
        var countPixels = data.length * 0.25;
        for (var j = 0; j < countPixels; j++){
            //因爲我們countPixels所有的像素點的1/4,所以每一次需要*4
            sum += data[4*j] + data[4*j+1] + data[4*j+2];
        }
        //做出平均值
        var average = Math.round((sum / (3 * countPixels)));
        //如果平均值大於某個值則判斷爲變化了,就調用func將我們按鈕區域的名字傳過去
        if (average > 50){
            func(buttons[i].name);
        }
    }
}

上面的代碼就是我們處理圖像的核心,通過以上的代碼可以判斷我們的按鈕區域的rgb值是否在總體上變化,如果變化就進行調用func,這裏也就進入尾聲了

總結

本次的調用攝像頭來進行隔空的操作需要感謝http://stemkoski.github.io/本鏈接提供的東西,裏面有很多操作,可以供我們學習,這篇博客個人感覺寫的不是很好,畢竟思路還是沒有理清晰,可能是因爲對於代碼的不完全理解吧,希望海涵

以上代碼已經上傳Github

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章