一、知识必备
初、中级水平的JavaScript
初、中级水平的HTML5
对jQuery有基本了解
由于使用getUserMdedia和Audio API,Demo需要使用Chrome Canary,并通过本机Web服务器运行。
第三方必备产品
Chrome Canary
本机Web服务器,例:Xampp(Mac,Windows,Linux)Mamp(Mac)
Webm格式文件的视频解码器,例:
Online ConVert Video converter to convert to the WebM format (VP8)
Mp4格式文件的视频解码器,例:
Ogg格式文件的视频解码器,例:
二、示例文件
javascript_motion_detection_soundstep.zip
我会在本篇文章中,跟大家讨论一下如何在JavaScript中使用webcam stream追踪用户的操作。Demo中展示了从用户的网络摄像头中搜集到的一段视频,用户可以实时地在使用HTML开发的木琴上弹奏,就像弹奏真正的乐器一样。这个Demo是使用HTML5中的两个“仍需改进”的接口开发的,它们是:getUserMedia API,用来调用用户的网络摄像头,和Audio API,用来弹奏木琴。
同时,我也给大家展示了如何使用混合模式来捕获用户的操作。几乎所有提供了图片处理接口的语言都提供了混合模式。这里引用Wikipedia评论混合模式的一句话,“数字图像编辑中的混合模式通常用来判断两个图层是否能够融合在一起。”JavaScript本身不支持混合模式,而混合模式不过是对像素进行的数学运算,因此我创建了一个混合模式“difference”。
我制作了一个Demo,用来展示web技术的的发展方向。JavaScript和HTML5将会为新型的交互式Web应用提供接口。这是多么激动人心的一件事啊!
HTML5 getUserMedia API
一直到我写这篇文章的时候,getUserMedia API仍在完善中。W3C第5次发布的HTML,新增了相当多的接口,用于访问本机硬件。navigator.getUserMediaAPI()接口提供了一些工具,使网站能够捕获音频和视频。
一些技术博客中有很多关于如何使用这些接口的文章,所以,这里我不再赘述。如果你想学习更多相关的内容,文章的最后我给大家提供了一些有关getUserMedia的链接。下面,我们一起来看一下我是如何使用该接口做出示例中的内容的。
目前,getUserMedia API只能在Opera 12和Chrome Canary中使用,并且两款浏览器都没有公开发布。针对该示例,你只能使用Chrome Canary,因为该浏览器允许使用AudioContext弹奏木琴。Opera暂还不支持AudioContext。
Chrome Canary 完成安装并启动,然后启用API。在地址栏中输入:about:flags。在启用Media
图1. 启用Media Stream
最后,由于安全限制,摄像机不允许以本地文件:///的形式进行访问,你必须使用自己的本机web服务器运行示例文件。
现在万事俱备,只需添加一个视频标签用来播放摄像头流媒体文件。使用JavaScript把接收到的流媒体文件添加到视频标签的src属性中。
- <video id=”webcam” autoplay width=”640″ height=”480″></video>
在JavaSript代码段中,首先要做两项准备工作,第一项是确保用户的浏览器能够使用getUserMedia API。
- function hasGetUserMedia() { return !!(navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia); }
第二项工作就是尝试从用户的网络摄像头中获取流媒体文件。
注:在这篇文章和示例中,我使用的是jQuery框架,但是你可以根据自己的需要选择其它框架,例如,本地文件.querySelector
- var webcamError = function(e) {
- alert(‘Webcam error!’, e);
- };
- var video = $(‘#webcam’)[0];
- if (navigator.getUserMedia) {
- navigator.getUserMedia({audio: true, video: true}, function(stream) { video.src = stream;
- }, webcamError); }
- else if (navigator.webkitGetUserMedia) { navigator.webkitGetUserMedia(‘audio, video’, function(stream) { video.src = window.webkitURL.createObjectURL(stream);
- }, webcamError);
- } else {
- //video.src = ’video.webm’; // fallback.
- }
现在你已经可以在HTML页面中展示网络摄像头流媒体文件了。在下一部分中,我将为大家介绍总体结构以及示例中用到的一些资源。
如果你希望在自己的程序中使用getUserMedia,我推荐以下Addy Osmani的作品。Addy使用Flash fallback创建了getUserMedia API的shim。
Blend mode difference
接下来,我给大家展示的是使用blend mode difference来检测用户的操作。
把两张图片融合在一起就是对像素值进行加减计算。在Wikipedia的文章中有关混合模式的部分对difference进行了描述。“两个图层相减取其绝对值作为Difference,任何图片和黑片进行融合,所有颜色的值都为0.”
为了实现这个操作,首先需要两张图片。代码反复计算图片的每一个像素,第一张图片的色彩通道值减去第二上的色彩通道值。
比如说,两张红色的图片的每个像素中的色彩通道值为:
-红色:255(0xFF)
-绿色:0
-蓝色:0
下面的操作表示从这些图片减去这些颜色的值:
-红色:255-255=0
-绿色:0-0=0
-蓝色:0-0=0
换句话说,把blend mode difference应用于两张相同的图片会产生一张黑色的图片。下面我们来讨论一下blend mode difference的实用性和图片的获取途径。
我们逐步地完成这个过程,首先设定一个时间间隔,使用canvas元素画出webcam stream中的一张图片,如示例中所示,我设定为1分钟画60张图片,这个数量远远超过你所需要的。当前显示在webcam stream中的图片是你要融合的图片中的第一张。
第二张图片是从webcam中捕获的另外一张图片。现在就有了两张图片,要做的就是对它们的像素值进行差值计算。这意味着,如果两张照片是相同的—换句话说,如果用户没有任何操作—运算结果会是一张黑色的图片。
当用户开始操作,奇妙的事情发生了。当前时间间隔获取到的图片与在之前的时间间隔中获取到图片相比,有细微的变化。如果对不同的像素值进行差值计算,一些颜色出现了。这意味着这两帧图画之间发生了变化。
图2.融合后的两张图片
Motion detection这个过程就完成了。最后一步就是反复计算融合后的图片中的所有像素,是看否存在不为黑色的像素。
必备资源
为了运行这个Demo,还需要一般网站的必备元素。首先,你需要一个包含了canvas和video tags 元素的HTML页面,还需要JavaScript来运行这个应用程序。
然后,还需要一张木琴的图片,最好是透明背景(png格式)。另外,还需要木琴每个按键的图片,每一张图片都需要有轻微的变化。这样使用rollover 效果,这些图片就可以给用户视觉上的反馈,弹奏的琴键会高亮显示。
我们还需要一个音频文件,包含了每一个音符的声音,mp3文件即可。如果弹奏木琴没有声音就会没有任何乐趣可言了。
最后,这时候,需要新创建一个视频,把它解码成MP4格式,下一节,我将为大家介绍一些用来解码视频的工具,你可以在Demo的zip文件中找到所有的资源。
解码HTML5视频文件
把视频Demo后备文件解码成用来展示HTML5视频文件常见的三种格式。
在把文件解码成Webm格式的过程中,我遇到了点麻烦,我选择的解码工具不允许选择码率,而码率恰恰决定了视频的大小和质量,通常被称作kbps(千比特每秒)。我不再使用Online ConVert Video转换器来转换WebM格式(VP8),这个问题迎刃而解:
MP4格式:你可以使用Adobe Creative Suite中的工具,例如Adobe Media Encoder或者After Effects。或者你可以使用免费的视频转换软件,如Handbrake。
Ogg格式:我通常使用一些工具一次性转换所有格式。我对解码后生产webm和MP4格式视频的质量感觉不是很满意,虽然我没办法更改输出的质量,而Ogg格式的视频的质量比较乐观。你可以使用Easy HTNL5 Video或者Miro等转换器。
Demo中用到的HTML代码段
这是开始JavaScript编码之前的最后一步。添加HTML标签,下面是需要用到的HTML标签(我不会列出我用到的所有内容,比如说视频Demo中的fallback代码,你可以在资源中下载得到。)
这里需要一个简单的视频标签,用来接收用户的webcam feed。不要忘记设计自动播放属性,如果忘记了,stream会暂停在接收到的第一帧画面上。
- <video id=”webcam” autoplay width=”640″ height=”480″></video>
视频标签不会显示出来,它的CSS显示样式被设置成none,另外,添加一个canvas元素,用来绘制webcam stream。
- <canvas id=”canvas-source” width=”640″ height=”480″></canvas>
这时还需要另外添加一个canvas用来显示motion detection中实时的变动。
- <canvas id=”canvas-blended” width=”640″ height=”480″></canvas>
下面添加一个DIV标签,把木琴的图片放入其中。把木琴放到webcam的上部:用户可以身入其境地弹奏木琴。把音符放在木琴的上方,将其属性设置为隐藏,当音符被触发的时候,将会显示在rollover中。
- <div id=”xylo”>
- <div id=”back”><img id=”xyloback” src=”images/xylo.png”/></div> <div id=”front”> <img id=”note0″ src=”images/note1.png”/> <img id=”note1″ src=”images/note2.png”/> <img id=”note2″ src=”images/note3.png”/> <img id=”note3″ src=”images/note4.png”/> <img id=”note4″ src=”images/note5.png”/> <img id=”note5″ src=”images/note6.png”/> <img id=”note6″ src=”images/note7.png”/> <img id=”note7″ src=”images/note8.png”/> </div> </div>
JavaScript motion detection
使用以下JavaScript代码段完成Demo
确认应用是否可以使用getUserMedia API。
确认已接收webcam stream
载入木琴琴谱的音频
启动时间间隔,并调用Update函数
在每一个时间间隔中,在canvas中绘制一张webcam feed。
在每一个时间间隔中,把当前的webcam中的图片和之前的一张融合在一起。
在每一个时间间隔中,在canvas中绘制出融合后的图片。
在每一个时间间隔中,在木琴的琴谱中检查像素的颜色值。
在每一个时间间隔中,如果发现用户进行操作,弹奏出特定的木琴音符。
第一步:声明变量
声明一些用于存储drawing和motion detection过程中使用到的变量。首先,需要两个对canvas元素的引用,一个用来存储每个canvas上下文
的变量,一个用来存储已绘制的webcam stream。也需要存储每个音符x轴线的位置,弹奏的乐谱,和一些与声音相关的变量。
- var notesPos = [0, 82, 159, 238, 313, 390, 468, 544];
- var timeOut, lastImageData;
- var canvasSource = $(“#canvas-source”)[0];
- var canvasBlended = $(“#canvas-blended”)[0];
- var contextSource = canvasSource.getContext(’2d’);
- var contextBlended = canvasBlended.getContext(’2d’);
- var soundContext, bufferLoader;
- var notes = [];
同时还需要倒置webcam stream的轴线,让用户感觉自己是在镜子前。这使得他们的弹奏更顺畅。下面是完成该功能需要的代码段:
contextSource.translate(canvasSource.width, 0); contextSource.scale(-1, 1);
第二步:升级和视频绘制
创建一个名为Update的函数,并设定一秒钟执行60次,其中调用其它一些函数。
- function update() {
- drawVideo();
- blend();
- checkAreas();
- timeOut = setTimeout(update, 1000/60);
- }
把视频绘制到canvas中非常简单,只有一行代码
- function drawVideo() {
- contextSource.drawImage(video, 0, 0, video.width, video.height); }
第三步:创建blend mode difference
创建一个帮助函数,确保像素相减得到的值务必为正值。你可以使用内置函数Math.abs,不过我用二进制运算符写了一个差不多的函数.大部分情况下,使用二进制运算结构能够提升性能,你不一定要了解其中的细节,只要使用以下的代码即可:
- function fastAbs(value) {
- // equivalent to Math.abs(); return (value ^ (value >> 31)) - (value >> 31);
- }
下面我们写一个blend mode difference函数,这个函数包含三个形参:
一个二维数组用来存储减法得到的结果
一个二维数组存储当前webcam stream中图片的像素
一个二维数组存储之前webcam stream中图片的像素
存储像素的数组是二维的,并且还包含了Red,Green,Blue和Alpha等通道值.
· pixels[0] = red value
· pixels[1] = green value
· pixels[2] = blue value
· pixels[3] = alpha value
· pixels[4] = red value
· pixels[5] = green value
等等……
如示例中所示,webcam stream的宽为640像素,高为480像素。因此,数组的大小为640*480*4=1228000.
循环处理这些像素数组的最好方式是把值扩大4倍(红、绿、蓝、Alpha),这意味着现在只需要处理 307,200 次–情况更加乐观一些。
- function difference(target, data1, data2) {
- var i = 0;
- while (i < (data1.length / 4)) {
- var red = data1[i*4];
- var green = data1[i*4+1];
- var blue = data1[i*4+2]; var alpha = data1[i*4+3]; ++i;
- }
- }
现在你可以对图片中的像素进行减法运算—从效果上来看,会有大不同—如果颜色通道值已经为0,停止减法运行,并自动设置Alpha的值为255(0xFF)。下面是已经完成的blend mode difference函数(可以进行自主优化):
function difference(target, data1, data2) { // blend mode difference if (data1.length != data2.length) return null; var i = 0; while (i < (data1.length * 0.25)) { target[4*i] = data1[4*i] == 0 ? 0 : fastAbs(data1[4*i] - data2[4*i]); target[4*i+1] = data1[4*i+1] == 0 ? 0 : fastAbs(data1[4*i+1] - data2[4*i+1]); target[4*i+2] = data1[4*i+2] == 0 ? 0 : fastAbs(data1[4*i+2] - data2[4*i+2]); target[4*i+3] = 0xFF; ++i; } }
我在Demo中使用的是一个稍微不同的版本,以取得更好的准确性。我写了一个threshold函数,应用于颜色值。当颜色值低于临界值的时候,该方法把颜色像素值更改为黑色,而高于临界值时,将颜色像素值更改为白色。你可以使用以下的代码段完成该功能。
- function threshold(value) {
- return (value > 0×15) ? 0xFF : 0;
- }
同样我也计算了三种颜色通道的平均值,它们在图片中的像素为黑色或白色 。
- function differenceAccuracy(target, data1, data2)
- { if (data1.length != data2.length) return null;
- var i = 0;
- while (i < (data1.length * 0.25)) {
- var average1 = (data1[4*i] + data1[4*i+1] + data1[4*i+2]) / 3;
- var average2 = (data2[4*i] + data2[4*i+1] + data2[4*i+2]) / 3;
- var diff = threshold(fastAbs(average1 - average2)); target[4*i] = diff;
- target[4*i+1] = diff;
- target[4*i+2] = diff; target[4*i+3] = 0xFF; ++i; }
- }
结果如下面的黑白图片所示:
图3.blend mode difference的结果以黑白图片显示
第4步:blend canvas
下面我们创建一个函数,用来融合图片,你只需要把正确的值传给它:像素数组。
JavaScript的绘图API提供了一个方法,。这个对象包含了很多有用的属性,比如宽度和高度,同样还有数据属性,正是你需要用到的像素数组。同样,创建一个空的ImageData示例,用来存储结果,并把当前的webcam图片存储下来,以便重复使用。下面是在canvas中融合并绘制结果所用到的代码段:
- function blend() {
- var width = canvasSource.width;
- var height = canvasSource.height;
- // get webcam image data
- var sourceData = contextSource.getImageData(0, 0, width, height);
- // create an image if the previous image doesn’t exist
- if (!lastImageData) lastImageData = contextSource.getImageData(0, 0, width, height);
- // create a ImageData instance to receive the blended result
- var blendedData = contextSource.createImageData(width, height);
- // blend the 2 images
- differenceAccuracy(blendedData.data, sourceData.data, lastImageData.data);
- // draw the result in a canvas contextBlended.putImageData(blendedData, 0, 0);
- // store the current webcam image lastImageData = sourceData;
- }
第5步:search for pixel
这是完成Demo的最后一步,这就是motion detection,这里用到上一节我们创建的融合的图片。
在准备阶段,放入了8张木琴音符的图片。用这些音符的位置和大小作融合后图像的像素。然后,重复找出白色的像素。
在重复的过程中,计算出颜色通道的平均值,使用变量存储计算结果。重复过程完成之后,计算出整张图片的像素的总平均值。
通过设定临界值10来避免噪音和一些细微的操作,如果你发现大于10的数值,则应当考虑到在最后一张图像之后,用户进行了操作。这就是运动追踪。
然后,播放出相对应的音符,并展示出note rollover。此处用到的函数如下所示:
- function checkAreas() {
- // loop over the note areas
- for (var r=0; r<8; ++r)
- {
- // get the pixels in a note area from the blended image
- var blendedData = contextBlended.getImageData
- ( notes[r].area.x,
- notes[r].area.y,
- notes[r].area.width,
- notes[r].area.height);
- var i = 0;
- var average = 0;
- // loop over the pixels
- while (i < (blendedData.data.length / 4)) {
- // make an average between the color channel
- average += (blendedData.data[i*4] + blendedData.data[i*4+1] + blendedData.data[i*4+2]) / 3;
- ++i; }
- // calculate an average between of the color values of the note area
- average = Math.round(average / (blendedData.data.length / 4));
- if (average > 10) {
- // over a small limit, consider that a movement is detected
- // play a note and show a visual feedback to the user
- playSound(notes[r]);
- notes[r].visual.style.display = ”block”;
- $(notes[r].visual).fadeOut(); } }
相关链接
我希望这篇文章可以带给你更多的灵感,开发出基于视频的交互程序,
通过以下的Chrome和Opera浏览器的开发和W3C网站上有关API的草案,你可以学到更多有关getUserMedia接口的知识。
如果你想找到更多关于motion detection或者基于视频的应用的灵感,我推荐你遵照以下一些Flash开发人员和motion设计者使用After Effects得出的意见。现在Java Script和HTML势头正劲,开发出了很多新的功能。要学习的内容有很多,你可以参考以下资源:
· GetUserMedia article to get started
· 优秀的开发者博客,如何找到灵感以及写出更好的代码:
· Motion 设计者的博客,Video Copilot