サンタの安全を考え足下を照らすプログラムを書いてみた

クリスマスも近づき子供達が待ちわびているサンタ。
月日も経ち僕は親になり息子もできて、毎年我が家にもサンタが来てくれるようになりました。
ただ、親として心配なのはサンタが無事にミッションをこなせるのか？という心配。
暗闇を抜き足差し足でやってくる時に、もし床に落ちてるレゴを踏んで「痛てっ！」と声が出てしまったら大惨事です。息子が起きてしまう！
そんな心配から「よし！サンタが来たら自動で見つけると足下をきっちり照らす装置を作らねば！」と思い作りました。
まだクリスマスまではまだですが、私はAdvent Calendarに書いてサンタさんに安心してもらうためにご報告します。

※ HTML5 Advent Calendar 2014 10日目の記事です。 http://qiita.com/advent-calendar/2014/html5

# 作ったもの
動作の動画
https://vine.co/v/OrUDezr9xgH

## 利用するソフト
ブラウザ
以上
## 利用するAPI
getUserMedia , WebAudioAPI
## 仮定
* サンタはとある飲料メーカーの陰謀により赤い服を着ている
* サンタにきちんとライトが追随して足下を照らしてあげることで床に落ちてるレゴに気付く

※サンタは実際に来ていないため赤いものとして有名なジバニャン様にご協力いただきました。お忙しいところありがとうございます。

## 構成
1. getUserMediaで取得した画像から赤に該当する部分だけを抽出
2. カメラ内で現在居る位置を特定
3. WebAudioAPIでライトがついたサーボモーターを制御する

## アプリケーション
![IMG_2776.jpg](https://qiita-image-store.s3.amazonaws.com/0/4221/eeeb312e-836c-caa4-0efa-64a47fd223fe.jpeg)

この画面でわかるように赤いものを認識し位置を見つけています。
取得した原理としては
* getUserMediaで取得した画像のデータをRGBの数値に変換
* 一定のしきい値でRの部分の数値だけが高いものを抽出
* カメラの露出が変わる場合などがあるので３パターンのしきい値で行う
* 一定量連続(今回は10px)した場所には何か物体があるという事を判断
* 画面全体の物体の位置からどこにライトを照らせばよいか計算
というものです。
これでサンタが入ってきたというのが判断できます。

    navigator.getUserMedia({video: true, audio: false},
      function(stream) {
        var video=document.getElementById('capvideo');
        video.src = window.URL.createObjectURL(stream);
        video.play();
        // チェックする露出パターン
        var ckSteps=[[64,64,96],[96,96,128],[128,128,160]];

        // object
        var buffer = document.createElement('canvas');
        var bufferContext = buffer.getContext('2d');

        var disList=[];

        for(var i=0;i<ckSteps.length;i++){
          var newCanvas=document.createElement('canvas');
          $("#canvasArea").append($("<span>"+ckSteps[i]+"</span>"));
          $("#canvasArea").append(newCanvas);
          disList.push(newCanvas);
        }


        // サーチするサイズ
        var xSize=10;
        var ySize=18;

        var render = function() {
          requestAnimationFrame(render);

          var width = video.videoWidth;
          var height = video.videoHeight;
          if (width == 0 || height == 0) { return;}
          buffer.width = width;
          buffer.height = height;
          bufferContext.drawImage(video, 0, 0);

          for(var i=0;i<disList.length;i++){
            disList[i].width=width;
            disList[i].height=height;
          }
          
          var src = bufferContext.getImageData(0, 0, width, height); // カメラ画像のデータ
          var dest = bufferContext.createImageData(buffer.width, buffer.height); // 空のデータ（サイズはカメラ画像と一緒）
          
          var xRlist=[];
          var xGlist=[];
          var xBlist=[];

          var xRcount=0;
          var xGcount=0;
          var xBcount=0;

          var xRpos=-1;
          var xGpos=-1;
          var xBpos=-1;

          // 露出パターン分だけ繰り返し
          for(var step=0;step<ckSteps.length;step++){
            var isRskip=false;
            var isGskip=false;
            var isBskip=false;

            for (var i = 0; i < src.data.length; i += 4) {
              
              var rNum=0;
              var gNum=0;
              var bNum=0;
              var rTmpNum=0;
              var gTmpNum=0;
              var bTmpNum=0;

              // 次の行
              if(xRcount!=0 && i%(width*4)==0){
                // if(xRcount>xSize){
                  // 本来は上下関係をチェックしないとだが手抜き
                  // 諸事情により枠にかぶりは一旦無視
                  //xList.push(width-Math.cel(xRcount/2));
                // }
                xRcount=0;
                xGcount=0;
                xBcount=0;
              }

              // r
              if (src.data[i] > ckSteps[step][0]) {
                xRcount++;
                rTmpNum=255;
              } else {
                rTmpNum=0;
              }
              // g
              if (src.data[i+1] > ckSteps[step][1]) {
                xGcount++;
                gTmpNum=255;
              } else {
                gTmpNum=0;
              }
              // b
              if (src.data[i+2] > ckSteps[step][2]) {
                xBcount++;
                bTmpNum=255;
              } else {
                bTmpNum=0;
              }

              // r
              if(rTmpNum==255 && ( gTmpNum==255 || bTmpNum==255 ) ){
                rNum=0;
              }else{
                rNum=rTmpNum;
              }
              // g
              if(gTmpNum==255 && (rTmpNum==255 || bTmpNum==255) ){
                gNum=0;
              }else{
                gNum=gTmpNum;
              }
              // b
              if(bTmpNum==255 && (gTmpNum==255 || rTmpNum==255) ){
                bNum=0;
              }else{
                bNum=bTmpNum;
              }

              dest.data[i]=rNum;
              dest.data[i+1]=gNum;
              dest.data[i+2]=bNum;


              // 対象外で前が対象物だった時
              if(rNum==0 && xRcount!=0){
                if(xRcount>xSize){
                  // 本来は上下関係をチェックしないとだが手抜き
                  // 発見した位置を記録
                  // 誤差を考えとりあえず1行前にあったかだけチェック
                  var hitNum=0;
                  for(var j=0;j<ySize;j++){
                    var num=i-( width*j-Math.floor(xRcount/2) )*4;
                    if(num>0 && dest.data[num]==255){
                      hitNum++;
                    }
                  }
                  // 60%あった場合はOKとする
                  if(hitNum/ySize>0.6){
                    xRlist.push( (i%(width*4)-Math.floor(xRcount/2))/4 );                    
                  }
                }else{
                  for(var j=1;j<=xRcount;j++){
                    dest.data[i-(j*4)]=0;
                  }
                }
                xRcount=0;
              }
              if(gNum==0 && xGcount!=0){
                if(xGcount>xSize){
                  // 本来は上下関係をチェックしないとだが手抜き
                  // 発見した位置を記録
                  // 誤差を考えとりあえず1行前にあったかだけチェック
                  var hitNum=0;
                  for(var j=0;j<ySize;j++){
                    var num=i+1-( width*j-Math.floor(xGcount/2) )*4;
                    if(num>0 && dest.data[num]==255){
                      hitNum++;
                    }
                  }
                  // 60%あった場合はOKとする
                  if(hitNum/ySize>0.6){
                    xGlist.push( (i%(width*4)+1-Math.floor(xGcount/2))/4 );                    
                  }
                }else{
                  for(var j=1;j<=xGcount;j++){
                    dest.data[i-(j*4)]=0;
                  }
                }
                xGcount=0;
              }
              if(bNum==0 && xBcount!=0){
                if(xBcount>xSize){

                  // 本来は上下関係をチェックしないとだが手抜き
                  // 発見した位置を記録
                  // 誤差を考えとりあえず1行前にあったかだけチェック
                  var hitNum=0;
                  for(var j=0;j<ySize;j++){
                    var num=i+2-( width*j-Math.floor(xBcount/2) )*4;
                    if(num>0 && dest.data[num]==255){
                      hitNum++;
                    }
                  }
                  // 60%あった場合はOKとする
                  if(hitNum/ySize>0.6){
                    xBlist.push( (i%(width*4)+2-Math.floor(xBcount/2))/4 );                    
                  }
                }else{
                  for(var j=1;j<=xBcount;j++){
                    dest.data[i-(j*4)]=0;
                  }
                }
                xBcount=0;
              }

              dest.data[i+3] = 255;
            }
            disList[step].getContext('2d').putImageData(dest, 0, 0);

            // 赤
            if(!isRskip && xRlist.length>0){
              xRlist.sort(function(a, b){
                return a - b;
              });
              // 前後1割は誤差として捨てる
              var tNum=Math.floor(xRlist.length/10);
              var xNum=0;
              for(var i=tNum;i<xRlist.length-tNum;i++){
                xNum+=xRlist[i];
              }
              var xTmpPos=xNum/(xRlist.length-(tNum)*2);
              if(xRpos==-1){
                // 発見したので一旦終了
                xRpos=xTmpPos;
                isRskip=true;
              }
            }

            // 緑
            if(!isGskip && xGlist.length>0){
              xGlist.sort(function(a, b){
                return a - b;
              });
              // 前後1割は誤差として捨てる
              var tNum=Math.floor(xGlist.length/10);
              var xNum=0;
              for(var i=tNum;i<xGlist.length-tNum;i++){
                xNum+=xGlist[i];
              }
              var xTmpPos=xNum/(xGlist.length-(tNum)*2);
              if(xGpos==-1){
                // 発見したので一旦終了
                xGpos=xTmpPos;
                isGskip=true;
              }
            }

            // 青
            if(!isBskip && xBlist.length>0){
              xBlist.sort(function(a, b){
                return a - b;
              });
              // 前後1割は誤差として捨てる
              var tNum=Math.floor(xBlist.length/10);
              var xNum=0;
              for(var i=tNum;i<xBlist.length-tNum;i++){
                xNum+=xBlist[i];
              }
              var xTmpPos=xNum/(xBlist.length-(tNum)*2);
              if(xBpos==-1){
                // 発見したので一旦終了
                xBpos=xTmpPos;
                isBskip=true;
              }
            }

          }

          // 処理終了

          audioServo.moveServo1(xRpos!=-1? 100-(xRpos/width)*100 : 50);

        };
        render();

      },
      function(err) { // for error case
        console.log(err);
      }
    );

あとは僕がせっせと作ってるライブラリの[audioServo](https://github.com/wakasamasao/audioServo/)を用いてイヤホンジャックに制御する[サーボモーター](http://prod.kyohritsu.com/WR-S2ESi.html)を制御します。
そのサーボモーターの先にライトを付ける事で完了です。

ね。簡単でしょ？

## DEMO
https://dl.dropboxusercontent.com/u/24915023/music/audioSurvo/camera_v1.html

これで我が家にきたサンタさんは僕の為に[RICOH PJ WX4141](https://www.ricoh.co.jp/projector/pj/wx4141/)を持ってきてくれる。
安心して寝よっと。