「見える！動きが見える！」JavaScriptで残像カメラを作ってみた

#俺にはおまえの動きが見えるぞ
　Webカメラの画像を、JavaScriptで処理して残像をつけてみました。ダンスや機か運動の様子を撮影して、動きを見られるようできたらと考えつくりました。フリーズさせるための「止まれ」ボタンもつけてみました。でも、できがいまいちでした。(-_-)
https://kaihatuiinkai.jp/afterimage/

「動きがブレて見える！」。動きがはっきり見えない。この教材はボツかな？
　それでは、作成ステップを説明します。データの流れとプログラム作動を確認してもらうため、ソースコードを全て貼り付けしました。長くなってしまうことをご了承願います。
　WebカメラをJavaScriptで画像表示させるための方法はこちらを参照願います。
https://qiita.com/mvm43236/items/398e92761440eb006271

#残像表示（画像重ね合わせ）
 Webカメラの画像を&lt;canvas&gt;に表示します。前の画像と重ね合わせをして、右の&lt;canvas&gt;に表示します。
作動例→  https://kaihatuiinkai.jp/afterimage/camera5.html

```camera5.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>残像表示（画像重ね合わせ）</title>
  <style>
  canvas, video{ border: 1px solid gray; }
  </style>
</head>
<body>
<h2>残像表示（画像重ね合わせ）</h2>
video　　　　　　　　　　　　　　　　　canvas1　　　　　　　　　　　　　　　　　canvas2（画像重ね合わせ）<br>
<video id="video" width="300" height="200"></video>
<canvas id="canvas1" width="300" height="200"></canvas>
<canvas id="canvas2" width="300" height="200"></canvas>
<script>
var imageDelay = 10;  // 表示遅延  1秒
window.onload = () => {
  const video  = document.querySelector("#video");
  const canvas1 = document.querySelector("#canvas1");
  const canvas2 = document.querySelector("#canvas2");
  // カメラ設定  //
  const constraints = {
    audio: false,
    video: {
      width: 300,
      height: 200,
      facingMode: "user"   // フロントカメラを利用する
      // facingMode: { exact: "environment" }  // リアカメラを利用する場合
    }
  };
  // カメラを<video>と同期 //
  navigator.mediaDevices.getUserMedia(constraints)
  .then( (stream) => {
    video.srcObject = stream;
    video.onloadedmetadata = (e) => {
      video.play();
    };
  })
  .catch( (err) => {
    console.log(err.name + ": " + err.message);
  });
    // canvas2 の準備
    var picture2 = canvas2.getContext("2d");
    // canvas2に画像を貼り付ける
    picture2.drawImage(video, 0, 0, canvas1.width, canvas1.height);
  // RGBの範囲
  function set255(value){
    if (value > 255) return 255;
    if (value < 0)   return 0;
    return value;
  }
  // インターバル処理
  var intervalID = setInterval(function(){
    // canvas1 の準備
    var picture1 = canvas1.getContext("2d");
    // canvas1に画像を貼り付ける
    picture1.drawImage(video, 0, 0, canvas1.width, canvas1.height);
    // イメージデータの取得
    image1 = picture1.getImageData(0,0,canvas1.width, canvas1.height);
    // canvas2 の準備
    var picture2 = canvas2.getContext("2d");
    // イメージデータの取得
    image2 = picture2.getImageData(0,0,canvas1.width, canvas1.height);
    var afterimage =picture2.createImageData(canvas2.width, canvas2.height);     
    // 画像合成
    var cnt = 0;
    var amount = 128 / 255;
    for (var y = 0; y < canvas1.height; y++) {
      for (var x = 0;x < canvas1.width; x++) {
        afterimage.data[(cnt*4)]   = set255(Math.round((1 - amount) * image1.data[(cnt*4)]   + (amount * image2.data[(cnt*4)])));
        afterimage.data[(cnt*4)+1] = set255(Math.round((1 - amount) * image1.data[(cnt*4)+1] + (amount * image2.data[(cnt*4)+1])));
        afterimage.data[(cnt*4)+2] = set255(Math.round((1 - amount) * image1.data[(cnt*4)+2] + (amount * image2.data[(cnt*4)+2]))); 
        afterimage.data[(cnt*4)+3] = 255; 
        cnt++;
      }
    }    
    // エラー回避処理
    try {
    // canvas2 に変数の画像を貼り付ける
      document.getElementById("canvas2").getContext('2d').putImageData(afterimage, 0, 0);
    }catch(e){
      // エラー時には何もしない
    };
  }, 300);
};
</script>
</body>
</html>
```

#残像表示（差分抽出表示）
 Webカメラの画像を&lt;canvas&gt;に表示します。前の画像と比較して、下の&lt;canvas&gt;に差分を表示します。
作動例→  https://kaihatuiinkai.jp/afterimage/camera6.html

　動いた部分が表示されます。

```camera6.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>残像表示（差分抽出表示）</title>
  <style>
  canvas, video{ border: 1px solid gray; }
  </style>
</head>
<body>
<h2>残像表示（差分抽出表示）</h2>
video　　　　　　　　　　　　　　　　　canvas1　　　　　　　　　　　　　　　　　canvas2<br>
<video id="video" width="300" height="200"></video>
<canvas id="canvas1" width="300" height="200"></canvas>
<canvas id="canvas2" width="300" height="200"></canvas>
<br>canvas2-canvas1　差分抽出画像<br>
<canvas id="canvas3" width="300" height="200"></canvas>
<script>
var imageDelay = 10;  // 表示遅延  1秒
window.onload = () => {
  const video  = document.querySelector("#video");
  const canvas1 = document.querySelector("#canvas1");
  const canvas2 = document.querySelector("#canvas2");
  const canvas3 = document.querySelector("#canvas3");
  // カメラ設定  //
  const constraints = {
    audio: false,
    video: {
      width: 300,
      height: 200,
      facingMode: "user"   // フロントカメラを利用する
      // facingMode: { exact: "environment" }  // リアカメラを利用する場合
    }
  };
  // カメラを<video>と同期 //
  navigator.mediaDevices.getUserMedia(constraints)
  .then( (stream) => {
    video.srcObject = stream;
    video.onloadedmetadata = (e) => {
      video.play();
    };
  })
  .catch( (err) => {
    console.log(err.name + ": " + err.message);
  });
    // canvas2 の準備
    var picture2 = canvas2.getContext("2d");
    // canvas2に画像を貼り付ける
    picture2.drawImage(video, 0, 0, canvas1.width, canvas1.height);
  // RGBの範囲
  function set255(value){
    if (value > 255) return 255;
    if (value < 0)   return 0;
    return value;
  }
  // インターバル処理
  var intervalID = setInterval(function(){
    // canvas1 の準備
    var picture1 = canvas1.getContext("2d");
    // canvas1に画像を貼り付ける
    picture1.drawImage(video, 0, 0, canvas1.width, canvas1.height);
    // イメージデータの取得
    image1 = picture1.getImageData(0,0,canvas1.width, canvas1.height);
    // canvas2 の準備
    var picture2 = canvas2.getContext("2d");
    // イメージデータの取得
    image2 = picture2.getImageData(0,0,canvas1.width, canvas1.height);
    var afterimage =picture2.createImageData(canvas2.width, canvas2.height);     
    // 画像合成
    var cnt = 0;
    var difference = 15;
    var cValue = 255;
    for (var y = 0; y < canvas1.height; y++) {
      for (var x = 0;x < canvas1.width; x++) {
        if((image1.data[(cnt*4)]-image2.data[(cnt*4)] > difference)||((image1.data[(cnt*4)+1]-image2.data[(cnt*4)+1] > difference))||(image1.data[(cnt*4)+2]-image2.data[(cnt*4)+2] > difference)){
            afterimage.data[(cnt*4)]   = image1.data[(cnt*4)];
            afterimage.data[(cnt*4)+1] = image1.data[(cnt*4)+1];
            afterimage.data[(cnt*4)+2] = image1.data[(cnt*4)+2]; 
        }else{
            afterimage.data[(cnt*4)]   = cValue;
            afterimage.data[(cnt*4)+1] = cValue;
            afterimage.data[(cnt*4)+2] = cValue; 
        };
        afterimage.data[(cnt*4)+3] = 255; 
        cnt++;
      }
    }    
    // エラー回避処理
    try {
    // canvas2 に変数の画像を貼り付ける
      document.getElementById("canvas2").getContext('2d').putImageData(image1, 0, 0);
    // canvas3 の準備
    var picture3 = canvas3.getContext("2d");
    // canvas3 に変数の画像を貼り付ける
    document.getElementById("canvas3").getContext('2d').putImageData(afterimage, 0, 0);
    }catch(e){
      // エラー時には何もしない
    };
  }, 300);
};
</script>
</body>
</html>
```

#残像表示（差分抽出、重ね合わせ）
 Webカメラの画像を&lt;canvas&gt;に表示します。前の画像と比較して、下の&lt;canvas&gt;に差分を重ね合わせて表示します。
作動例→  https://kaihatuiinkai.jp/afterimage/camera7.html

　動いた部分が影のようについてきます。
・カメラの前で顔を動かすと、シュールな顔になれます。
・仮○ライ○○の変身シーンの動きをすると、かっこよく見えます。
・カメラに向かって、拳を何度も降り出すと、ペガ○○流○拳、北○百○拳が再現できます。
・・・あれ？教材にならない。

```camera7.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>残像表示（差分抽出、重ね合わせ）</title>
  <style>
  canvas, video{ border: 1px solid gray; }
  </style>
</head>
<body>
<h2>残像表示（差分抽出、重ね合わせ）</h2>
video　　　　　　　　　　　　　　　　　canvas1　現在の画像　　　　　　　　　　canvas2　この画像をもとにして重ね合わせ<br>
<video id="video" width="300" height="200"></video>
<canvas id="canvas1" width="300" height="200"></canvas>
<canvas id="canvas2" width="300" height="200"></canvas>
<br>canvas2-canvas1　差分抽出、重ね合わせ画像<br>
<canvas id="canvas3" width="300" height="200"></canvas>
<script>
var imageMax = 30;   // 遅延最大時間　最大3秒
var imageArray = new Array(imageMax); // 画像保存用
var imageNumber = 0;  // 現在の画像番号
var imageDisplay = 0; // 表示番号
var imageDelay = 8;  // 表示遅延  1秒
// 読み込まれたら　作動します
window.onload = () => {
  const video  = document.querySelector("#video");
  const canvas1 = document.querySelector("#canvas1");
  const canvas2 = document.querySelector("#canvas2");
  const canvas3 = document.querySelector("#canvas3");
  // カメラ設定  //
  const constraints = {
    audio: false,
    video: {
      width: 300,
      height: 200,
      facingMode: "user"   // フロントカメラを利用する
      // facingMode: { exact: "environment" }  // リアカメラを利用する場合
    }
  };
  // カメラを<video>と同期 //
  navigator.mediaDevices.getUserMedia(constraints)
  .then( (stream) => {
    video.srcObject = stream;
    video.onloadedmetadata = (e) => {
      video.play();
    };
  })
  .catch( (err) => {
    console.log(err.name + ": " + err.message);
  });
    // canvas3 の準備
    var picture3 = canvas3.getContext("2d");
    // canvas2に画像を貼り付ける
    picture3.drawImage(video, 0, 0, canvas3.width, canvas3.height);
    // イメージデータの取得
    var afterimage =picture3.createImageData(canvas3.width, canvas3.height);
    // エラー回避のため、事前に画像を入れる
    for(var i= 0; i <= imageMax; i++){
      imageArray[i] = picture3.getImageData(0,0,canvas1.width, canvas1.height);
    };
  // インターバル処理
  var intervalID = setInterval(function(){
    // 画像記録番号
    imageNumber = imageNumber + 1;
    if(imageNumber >= imageMax){
      imageNumber =0;
    }
    // 表示番号
    imageDisplay = imageNumber - imageDelay;
    if(imageDisplay <0 ){
      imageDisplay = imageDisplay + imageMax; // マイナスの処理
    }
    // canvas1 の準備
    var picture1 = canvas1.getContext("2d");
    // canvas1に画像を貼り付ける
    picture1.drawImage(video, 0, 0, canvas1.width, canvas1.height);
    // イメージデータの取得
    imageArray[imageNumber] = picture1.getImageData(0,0,canvas1.width, canvas1.height);
    // canvas2 の準備
    var picture2 = canvas2.getContext("2d");
    try {
        // canvas2 に変数の画像を貼り付ける
        document.getElementById("canvas2").getContext('2d').putImageData(imageArray[imageDisplay], 0, 0);
    }catch(e){
        // エラー時には何もしない
    };
    // 画像合成
    imageDisplay = imageNumber - imageDelay;
    if(imageDisplay <0 ){
      imageDisplay = imageDisplay + imageMax; // マイナスの処理
    }
    afterimage = imageArray[imageDisplay];
    for(var i= 1; i <= imageDelay; i++){
      funcComposition(imageArray[setImageNumber(imageNumber - imageDelay + i)],imageArray[imageDisplay]);
    };
    // エラー回避処理
    try {
        // canvas3 の準備
        var picture3 = canvas3.getContext("2d");
        // canvas3 に変数の画像を貼り付ける
        document.getElementById("canvas3").getContext('2d').putImageData(afterimage, 0, 0);
    }catch(e){
        // エラー時には何もしない
    };
  }, 50);
// ImageNumberの範囲
function setImageNumber(value){
    if (value > imageMax) return (value - imageMax);
    if (value < 0)   return (value + imageMax);
    return value;
}
  function funcComposition(image1,image2){  // 画像合成
    var cnt = 0;
    var difference = 4;
    var cValue = 255;
    for (var y = 0; y < canvas1.height; y++) {
      for (var x = 0;x < canvas1.width; x++) {
        if((image1.data[(cnt*4)]-image2.data[(cnt*4)] > difference)||((image1.data[(cnt*4)+1]-image2.data[(cnt*4)+1] > difference))||(image1.data[(cnt*4)+2]-image2.data[(cnt*4)+2] > difference)){
            afterimage.data[(cnt*4)]   = image1.data[(cnt*4)];
            afterimage.data[(cnt*4)+1] = image1.data[(cnt*4)+1];
            afterimage.data[(cnt*4)+2] = image1.data[(cnt*4)+2]; 
        };
        afterimage.data[(cnt*4)+3] = 255; 
        cnt++;
      }
    }
  };
};
</script>
</body>
</html>
```

　以前、Windows用に画像重ね合わせソフト「多重露光」を作りました。
http://www1.iwate-ed.jp/tantou/joho/material/tajuurokou/index.html
.NETの機能を使って、Windowsネイティブのソフトで処理していたことが、今は、ブラウザで表示できることに進歩を感じます。ハードとソフトの進化ですね。
　1990年代、CPUがV30のPC9801で「気象衛星画像」のビットマップを1ドットごとに処理をして表示する理科の教材を作りました。でも、400×400を終えるのに1分30秒かかり、授業で使うことをあきらめました。→事前に処理して保存しておきました。それを今では、0.05秒で処理してしまう。コンピュータの能力の進化はすばらしいですね。