我的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

posted @ 2017-10-20 17:09  Lger  阅读(1228)  评论(0编辑  收藏  举报