TIGER-TANIOKAです。

WebGLで動体検知やります。

今回はWEBカメラを使用します。
WebRTC(WebRealTimeCommunication)というものを使用します。

WebRTCとは、WebRealTimeCommunicationの略です。
WebRTCを使用することにより、カメラ、マイクから動画、音声を取り込むことが可能になります。

ではやってきます〜

1.HTML側にVIDEOタグとCANVASタグ配置します。

video(style='float:left;display:none;' , id='webcam' , width='900' , height='500' , autoplay)
canvas(style='float:left;',id='cvs',width='900' , height='500')

2.Navigator.getUserMediaを使用する。

getUserMedia(API)とはPCに接続されたウェブカムやマイクなどの情報を返してくれるAPIです。

navigator.getUserMedia = (
  navigator.getUserMedia ||
  navigator.webkitGetUserMedia ||
  navigator.mozGetUserMedia
  );

if(navigator.getUserMedia){
  navigator.getUserMedia({
    'video' : true, //動画を利用
    'audio' : false //今回音はなし
  },
  successHandler, //LocalMediaStream オブジェクトが取得できた場合、呼び出されるコールバック関数。
  errorHandler //呼び出しが失敗した際に実行されるコールバック関数
  );
}else{
  window.alert('error'); //getUserMediaがサポートされていない場合
}





コールバック///////////////
function successHandler(_stream){//LocalMediaStreamを取得
  var _url = window.URL || window.webkitURL;//_url
  video.src = _url.createObjectURL(_stream);
}

function errorHandler(){
  win.alert('error')
}

これでvideoタグにwebcamでとってる動画が映りました。 alt

3.シェーダーで四角形を描画

cpu(js側)で四角形の座標、uniform変数等用意してシェーダー側で描画するプログラムかきます。
※多少はしょります。

js側

var CreateProgram = function(_vs , _fs , _gl){
  var _prg = _gl.createProgram();
  // プログラムオブジェクトにシェーダを追加
  _gl.attachShader(_prg , _vs);
  _gl.attachShader(_prg , _fs);
  // プログラムオブジェクトをリンク
  _gl.linkProgram(_prg);
  // シェーダーのリンクが上手くいったかチェック
  if(_gl.getProgramParameter(_prg, _gl.LINK_STATUS)){
    _gl.useProgram(_prg);
    return _prg;
  }else{
    alert('programError' + _gl.getProgramInfoLog(_prg));
  }
}

var SetAttribute = function(_vbo , _attL ,  _attS , _gl){
  _gl.bindBuffer(_gl.ARRAY_BUFFER , _vbo);
  // attribute属性を有効にする
  _gl.enableVertexAttribArray(_attL);
  // attribute属性を登録
  _gl.vertexAttribPointer(_attL , _attS , _gl.FLOAT , false, 0  , 0);
}

var CreateVbo = function(_data , _gl){
  var _vbo = _gl.createBuffer();
  _gl.bindBuffer(_gl.ARRAY_BUFFER , _vbo);
  _gl.bufferData(_gl.ARRAY_BUFFER , new Float32Array(_data) , _gl.DYNAMIC_DRAW);
  _gl.bindBuffer(_gl.ARRAY_BUFFER , null);
  return _vbo;
}

var vertex = [
  -1.0, 1.0, 0.0,
  1.0, 1.0, 0.0,
  1.0, -1.0, 0.0,
  -1.0, 1.0, 0.0,
  1.0, -1.0, 0.0,
  -1.0, -1.0, 0.0
];


function draw(){
  var tmpMtx = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
  gl.clearColor(1.0, 1.0, 1.0, 1.0);
  gl.clearDepth(1.0);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  gl.uniformMatrix4fv(uniLocations['matrix'] , false , tmpMtx);
  gl.drawArrays(gl.TRIANGLES, 0 , 6);
  gl.flush();
}




〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜




  cvs = doc.getElementById('cvs');
  gl = cvs.getContext('webgl') || cvs.getContext('experimental-webgl');
  video = doc.getElementById('webcam');

  vtxShader = CreateShader('vs' , gl);
  frgShader = CreateShader('fs' , gl);

  prg = CreateProgram(vtxShader , frgShader, gl);


  uniLocations['matrix'] = gl.getUniformLocation(prg , 'mvpMatrix');
  attLocations['position'] = gl.getAttribLocation(prg , 'position');
  strids['position'] = 3;
  vbos['position'] = CreateVbo(vertex , gl);
  SetAttribute(vbos['position'] , attLocations['position'] , strids['position'] , gl);


  setInerval(draw , 40 / 1000);

シェーダー側

script(id='vs' , type='x-shader/x-vertex').
  attribute vec3 position;
  uniform mat4 mvpMatrix;

  void main(){
    gl_Position = mvpMatrix * vec4(position.x ,position.y, 0.0, 1.0);
  }


script(id='fs' , type='x-shader/x-fragment').
  precision mediump float;
  void main(){
    gl_FragColor = vec4(0.0 , 0.0 , 0.0 , 1.0);
  }

黒い四角形が描画ができたら
次はこのエリアに先ほどのウェブカメラの映像を描画します。

4.シェーダー側に動画をテクスチャとして渡す。

ウェブカメラでとってる動画をテクスチャ化
シェーダーに渡します。

js側

function canPlay(){
  createTexture('video' , video);
  setInterval(draw , FRAME_RATE / 1000);
}

function successHandler(_stream){//LocalMediaStreamを取得
  var _url = window.URL || window.webkitURL;//_url


  video.addEventListener('canplay' , canPlay);

  video.src = _url.createObjectURL(_stream);
}



function createTexture(_name , _texSrc){
  var _tex = gl.createTexture();//テクスチャ生成
  gl.bindTexture(gl.TEXTURE_2D , _tex);//バインド
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);//反転
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA,gl.UNSIGNED_BYTE, _texSrc);//テクスチャに画像データを紐付け

  //テクスチャが縮小される際のパラメータ
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);



  //テクスチャが縮小される際のパラメータ
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

  gl.bindTexture(gl.TEXTURE_2D, null); //バインド解除
  textures[_name] = _tex;

}

function draw(){
  var tmpMtx = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
  gl.clearColor(1.0, 1.0, 1.0, 1.0);
  gl.clearDepth(1.0);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  gl.uniformMatrix4fv(uniLocations['matrix'] , false , tmpMtx);


  gl.uniform2f(uniLocations['screen'] , win.innerWidth , win.innerHeight);
  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D, textures['video']);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA,gl.UNSIGNED_BYTE, video);
  gl.uniform1i(uniLocations['video'] , 0);


  gl.drawArrays(gl.TRIANGLES, 0 , 6);
  gl.flush();
}




〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜




  cvs = doc.getElementById('cvs');
  gl = cvs.getContext('webgl') || cvs.getContext('experimental-webgl');
  video = doc.getElementById('webcam');

  vtxShader = CreateShader('vs' , gl);
  frgShader = CreateShader('fs' , gl);

  prg = CreateProgram(vtxShader , frgShader, gl);


  uniLocations['matrix'] = gl.getUniformLocation(prg , 'mvpMatrix');
  attLocations['position'] = gl.getAttribLocation(prg , 'position');
  strids['position'] = 3;
  vbos['position'] = CreateVbo(vertex , gl);
  SetAttribute(vbos['position'] , attLocations['position'] , strids['position'] , gl);



  uniLocations['video'] = gl.getUniformLocation(prg , 'videoTex');
  uniLocations['screen'] = gl.getUniformLocation(prg , 'screen');

シェーダー側

script(id='vs' , type='x-shader/x-vertex').
  attribute vec3 position;
  uniform mat4 mvpMatrix;

  void main(){
    gl_Position = mvpMatrix * vec4(position.x ,position.y, 0.0, 1.0);
  }


script(id='fs' , type='x-shader/x-fragment').
  precision mediump float;
  uniform sampler2D videoTex;
  uniform vec2 screen;
  vec4 videoTexC;

  void main(){
    float texColorX = gl_FragCoord.x / screen.x;
    float texColorY = gl_FragCoord.y / screen.y;
    videoTexC = texture2D(videoTex, vec2(texColorX , texColorY));
    gl_FragColor = videoTexC;
  }

5.シェーダー側でエッジを検出

モノクロ画像にして前フレームとの差分を検出

js側

jsのほうで3フレーム前までのテクスチャをつくり、
シェーダーにわたします。

function canPlay(){
  createTexture('video' , video);
  createTexture('frame0');
  createTexture('frame1');
  createTexture('frame2');
  setInterval(draw , 40 / 1000);
}


function createTexture(_name , _texSrc){

  var _tex = gl.createTexture();//テクスチャ生成
  gl.bindTexture(gl.TEXTURE_2D , _tex);//バインド
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);//反転


  if(!_texSrc){
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, showWidth, showHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);

  }else{
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA,gl.UNSIGNED_BYTE, _texSrc);
  }


  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA,gl.UNSIGNED_BYTE, _texSrc);//テクスチャに画像データを紐付け

  //テクスチャが縮小される際のパラメータ
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);


  //テクスチャが縮小される際のパラメータ
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

  gl.bindTexture(gl.TEXTURE_2D, null); //バインド解除
  textures[_name] = _tex;

}



function draw(){
  var prevFrameCnt = count - 2;
  if(prevFrameCnt < 0) prevFrameCnt = prevFrameCnt + 3;


  var tmpMtx = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
  gl.clearColor(1.0,  1.0, 1.0, 1.0);
  gl.clearDepth(1.0);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

  gl.uniformMatrix4fv(uniLocations['matrix'] , false , tmpMtx);
  gl.uniform2f(uniLocations['screen'] , width , height);

  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D, textures['video']);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA,gl.UNSIGNED_BYTE, video);
  gl.uniform1i(uniLocations['video'] , 0);


  if(textures['frame' + prevFrameCnt]){
    gl.activeTexture(gl.TEXTURE1);
    gl.bindTexture(gl.TEXTURE_2D, textures['frame' + prevFrameCnt]);
    gl.uniform1i(uniLocations['prevFrameVideo'] , 1);
  }



  gl.drawArrays(gl.TRIANGLES, 0 , 6);
  gl.flush();


  count++;
  if(count == 3) count = 0;
  //前フレームのテクスチャ格納
  gl.bindTexture(gl.TEXTURE_2D , textures['frame' + count]);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA,gl.UNSIGNED_BYTE, video);

}

シェーダー側

輪郭を検出します。

script(id='fs' , type='x-shader/x-fragment').
  precision mediump float;
  uniform sampler2D videoTex;
  uniform vec2 screen;
  uniform sampler2D prevFrameVideo;
  uniform sampler2D afterImage;
  vec4 videoTexC;
  vec4 prevVideoTexC;
  vec4 afterImageTexC;


//差分抽出
  vec4 getEdge(vec4 _color1 ,vec4 _color2){
    float _glay = (_color1[0] - _color2[0]);
    vec4 _diff = vec4(_glay , _glay , _glay , 1.0);
    return _diff;
  }


//グレースケールに変換
  vec4 toGlay(vec4 _color){
    float _glay = (_color[0] * 0.3) + (_color[1] * 0.59) + (_color[2] * 0.11);
    return vec4(vec3(_glay), 1.0);
  }


  void main(){

  //座標正規化
    float texColorX = gl_FragCoord.x / screen.x;
    float texColorY = gl_FragCoord.y / screen.y;

    videoTexC = toGlay(texture2D(videoTex, vec2(texColorX , texColorY)));
    prevVideoTexC = toGlay(texture2D(prevFrameVideo, vec2(texColorX , texColorY)));

    vec4 edgeC = getEdge(videoTexC , prevVideoTexC) * 4.0;
    gl_FragColor = edgeC;
  }

うまく動体検知できました。

alt

これに色をつけちょっとゆらゆらエフェクトをつけてみると
まぁ素敵。

alt

フラグメントシェーダーおもしろいですね。
今回はこのあたりで。