【新規事業】SkyWayでモザイク加工したカメラ映像を送ってみるよ（前編）【プロト開発】

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/b6d31427-3ef7-8a1a-d68e-896254c9be43.png)

# はじめに
株式会社マイスター・ギルド新規事業部のウサギーです。
弊社新規事業部では、新規サービスの立ち上げを目指して
日々、アイディアの検証やプロトタイプの作成を行っています。

アイディアの検証のために、先日作成した[SkyWay*の1対1通信アプリ](https://qiita.com/Shinkijigyo_no_Usagi/items/1d4e1cccbb6de9aa4526)に自分たちの機能を載せていこう！となる中で

**①カメラ映像に加工すること**
**②加工した映像をSkyWayを通じて送ること**

が必要になりました。

*[SkyWay](https://webrtc.ecl.ntt.com/)…NTTコミュニケーションズが提供する、ビデオ・音声通話を簡単に実装できるSDKとAPIです

# この記事の位置づけ
普段からこういったコードをバリバリと書いている方なら簡単な内容だと思いますが、私のJavaScript歴はまだ数か月。
迷いながらの開発だったので作業の軌跡やコードを記録しておこうと思います。
誰かのためになったらいいな～

今回は上記のうち「**①カメラ映像に加工すること**」についての記事になります。

実際は②の調査が先だったのですが
手順が分かりやすいように**①カメラ映像に加工すること**を先に記事にしてみました。
②についても後日記事にする予定です。

ちなみに、この出来事のちょっと先の内容になります。
興味がある方はこちらの記事もチェックしてみてくださいね。

https://qiita.com/Shinkijigyo_no_Hitsuji/items/9c067d4d0b51410fa3e2


# あらすじ

[前回の記事](https://qiita.com/Shinkijigyo_no_Usagi/items/1d4e1cccbb6de9aa4526)でカメラ映像を使って1対1通信はできました。
これね↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/8dcd3517-b88f-f7b2-9d2c-4e19e33f57ef.png)

次のステップとして、このカメラ映像にいろいろ加工したものを送ってみたーい！となりました。
[過去の経験](https://qiita.com/Shinkijigyo_no_Usagi/items/a72c374e4d27f4b1a431)から、「カメラの映像にあれやこれやするのはCanvasを使う」と言うことだけは学んでいます。
早速、Canvasを使ってあれやこれやしていこう！


ということで「モザイク加工」をやってみました。

# 実現方法のイメージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/145f70cc-cf18-7936-803a-6448352532d1.png)
**（上）canvas：カメラ映像をコピーしてモザイク加工**
**（下）video：カメラ映像**

こんな感じを目指して実装します。

# 準備編
### canvasの用意
HTMLでcanvasを宣言
```diff_HTML:index.html
　<video id="my-video" width="360px" height="270px" autoplay muted playsinline></video>
+ <canvas id="my-canvas"></canvas>
```
canvasを使う準備
（canvas要素を示すElementオブジェクトとcanvasの描画コンテキストを取得）
```JS:main.js
const myCvs = document.getElementById('my-canvas');
const myCtx = myCvs.getContext('2d');
```



現状だとこんな感じ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/62f6f2aa-e874-efc6-251e-b81bccce12c4.png)
表示位置が分かりやすいように、canvasに枠を付けています。

### 大きさをそろえる
前述の表示ではCanvasはデフォルトサイズ
width:300px、 height:150px　のまま。
これを、カメラ映像と同じ大きさにします。

わかりやすくするために、カメラ映像の解像度も表示サイズも360*270にします。
カメラ映像の解像度は、getUserMedia取得時に指定できます。
（表示サイズはindex.htmlでの要素宣言時に指定済みです。）
```JS:main.js
navigator.mediaDevices.getUserMedia({ video:{ width: 360, height: 270 }, audio: true })
```
Canvasの方も、オブジェクトサイズ、表示サイズともに360*270に指定します。

```JS:main.js
const CVS_WIDTH = 360
const CVS_HEIGHT = 270
myCvs.width = CVS_WIDTH;
myCvs.height = CVS_HEIGHT;
myCvs.style.width = `${CVS_WIDTH}px`;
myCvs.style.height = `${CVS_HEIGHT}px`;
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/624bca8e-8f77-8608-d3a5-5fd661e5e722.png)

# カメラ映像に加工する

### モザイクをかける
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/50e19874-30cf-2a0c-5665-7c35488a78fb.png)
```JS:main.js
const MOSAIC_SIZE= 20;
const mosaicMyCvs = () => {
    myCtx.clearRect(0, 0, 360, 270);
    const mosaicCvs = document.createElement('canvas');
    const mosaicCtx = mosaicCvs.getContext('2d');
    mosaicCvs.width = CVS_WIDTH ;
    mosaicCvs.height = CVS_HEIGHT ;
    mosaicCvs.style.width = `${CVS_WIDTH}px`;
    mosaicCvs.style.height = `${CVS_HEIGHT}px`;
    mosaicCtx.drawImage(myVideo, 0, 0);
    const imageData = mosaicCtx.getImageData(0, 0, mosaicCvs.width, mosaicCvs.height);
    //モザイクサイズ単位でループ
    for (let y = 0; y < myCvs.height; y = y + MOSAIC_SIZE) {
        for (let x = 0; x < myCvs.width; x = x + MOSAIC_SIZE) {
            // 該当ピクセルの色情報を取得
            const cR = imageData.data[(y * myCvs.width + x) * 4];
            const cG = imageData.data[(y * myCvs.width + x) * 4 + 1];
            const cB = imageData.data[(y * myCvs.width + x) * 4 + 2];
            // モザイクサイズの正方形を描画
            myCtx.fillStyle = `rgb(${cR},${cG},${cB})`;
            myCtx.fillRect(x, y, x + MOSAIC_SIZE, y + MOSAIC_SIZE);
        }
    }
}
```
videoが読み込まれた後でこの処理（モザイク）を実行
```diff_Javascript:main.js
navigator.mediaDevices.getUserMedia({ video:{ width: 360, height: 270 }, audio: true })
    .then(stream => {
        myVideo.srcObject = stream;
        myVideo.play();
        localStream = stream;
+       myVideo.onloadeddata = () => {
+           mosaicMyCvs();
+       }
    }).catch(error => {
        console.error('mediaDevice.getUserMedia() error:', error);
});
```

画像を処理している部分ですが、ピクセルデータには順番に各画素の値が並んでいます。
どんな順に並んでいるかと言うと・・・

こう！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/21c1ccb3-603e-7f56-53ff-ce57b4c24a91.png)

この１つ１つが「１画素」を表し、１画素ごとにR,G,B,Aの値が入っています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/a07e112d-4535-4be2-e49e-5159bba38db4.png)

ｍはheight、ｎはwidthの画素数です。
「n個のセット」が「m個並んでいる」ので、画素を走査するために2重のfor文を使います。

`for (let y = 0; y < myCvs.height; y = y + MOSAIC_SIZE) {`
　　`for (let x = 0; x < myCvs.width; x = x + MOSAIC_SIZE) {`

今回はモザイク処理なので、変化式がポイントです。
**+ mosaic_size**とすることで、「モザイクの大きさ単位で」画素をチェックしていきます。
1ピクセルごとに確認するようなときは+1で大丈夫です◎

1画素＝4データなので、該当ピクセルの色情報を取得するときに**× 4**をしています。

`const cR = imageData.data[(y * myCvs.width + x) * 4]; `　Rの値は先頭
`const cG = imageData.data[(y * myCvs.width + x) * 4 + 1];`　Gの値はRの次なので+1
`const cB = imageData.data[(y * myCvs.width + x) * 4 + 2];`　Bの値はRの次の次なので+2
※Aは透明度の数値なので今回は使いません


やっていることはこんな感じ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/0896591a-8b01-6a9f-23df-47d88e2151d8.png)


### モザイク加工もアニメーションする
上記のままではモザイクは1度描画しているだけなので、定期的に描画しなおすようにします。
```diff_Javascript:main.js
navigator.mediaDevices.getUserMedia({ video:{ width: 360, height: 270 }, audio: true })
    .then(stream => {
        myVideo.srcObject = stream;
        myVideo.play();
        localStream = stream;
        myVideo.onloadeddata = () => {
+           setInterval(() => {
                mosaicMyCvs();
+           }, 1000 / 30);
        }
    }).catch(error => {
        console.error('mediaDevice.getUserMedia() error:', error);
});
```
`setInterval(func, delay)`は**delay**の間隔で**func**を繰り返し呼びだすことができます。
delayはミリ秒 (1/1000 秒) 単位で指定します。
例えば、1秒ごとに呼び出したいときは「1000」を与えます。

今回は使用しているwebカメラと同じ30fps（1秒間に30回のペースで呼び出す）にしたいので、「1000/30」ミリ秒を指定しました。

動画になってるの伝わりますかね？
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/0ac0538a-6074-de72-d9bb-b502e9f7b52f.png)

# 整える
### ON/OFF切り替え
モザイク加工のON/OFFをボタンで切り替えられるようにします
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/2aa80048-6318-8a5b-7c15-1c4eb7030d38.png)
```diff_HTML:index.html
<div id="my-setting" >
    <input type="button" id="mosaic-btn" value="モザイク">
    <span id="mosaic-enabled">OFF</span> <!-- 初期値はOFF --> 
</div>
```

```diff_Javascript:main.js
let isMosaicEnabled = false;
document.getElementById('mosaic-btn').onclick = () => {
    if (isMosaicEnabled) {
        isMosaicEnabled = false;
        document.getElementById('mosaic-enabled').textContent = "OFF";
    } else {
        isMosaicEnabled = true;
        document.getElementById('mosaic-enabled').textContent = "ON";
    }
}
```
```diff_Javascript:main.js
+ if(isMosaicEnabled){
     mosaicMyCvs();
+ }
```

これで、**isMosaicEnabled = true**のときはmosaicMyCvs()が実行され、モザイク動画が生成されます！

### モザイクOFF時のCanvas表示

このままだと、**isMosaicEnabled = false**のときはCanvasには直前に描画されたモザイク画像が表示されちゃいます。
それはイケて無いのでカメラ映像をそのままコピーして表示する関数を作り、
**isMosaicEnabled = false**のときはそちらを実行することにします。

```diff_Javascript:main.js
const copyToMyCvs = () => {
    myCtx.drawImage(myVideo, 0, 0, 360, 270);
}
```

```diff_Javascript:main.js
if(isMosaicEnabled){
    mosaicMyCvs();
+ }else{
+     copyToMyCvs();
}
```

### カメラ映像と重ねる
最後に、カメラ映像の上にCanvasを乗っけます。

```css
#my-video{
    position:absolute;
    top:140px;
    left:10px;
    z-index:1;
}
#my-canvas{
    position:absolute;
    top:140px;
    left:10px;
    z-index:2;
}
#my-setting{
    position:absolute;
    top:420px;
    left:10px;
    z-index:3;
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/96de9e23-ad4e-4920-7790-cddc683ae650.png)


無事、モザイク加工ができました！

# コード全体
<details><summary>index.html</summary><div>

```HTML:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>1対1ビデオ通話</title>
    <style type="text/css">
        .container{
            width: 900px;
        }
        .main-wrapper{
            height: 600px;
        }
        .my-section{
            float: left;
            width: 50%;
            height: 100%;
        }
        .their-section{
            float: right;
            width: 50%;
            height: 100%;
        }
        #my-video{
            position:absolute;
            top:140px;
            left:10px;
            z-index:1;
        }
        #my-canvas{
            position:absolute;
            top:140px;
            left:10px;
            z-index:2;
        }
        #my-setting{
            position:absolute;
            top:420px;
            left:10px;
            z-index:3;
        }
    </style>
</head>
<body>
    <h1>1対1ビデオ通話のサンプル</h1>
    <div class="container">
        <div class="my-section">
            <p class="my-id-label">自分のPeerID: <span id="my-id"></span></p>
            <video id="my-video" width="360px" height="270px" autoplay muted playsinline></video>
            <canvas id="my-canvas"></canvas>
            <div id="my-setting" >
                <input type="button" id="mosaic-btn" value="モザイク">
                <span id="mosaic-enabled">OFF</span>
            </div>
        </div>
        <div class="their-section">
            <p>
                <label class="call-id-form-label" for="their-id">相手のPeerID: </label>
                <input id="their-id" class="call-id-form">
                <button type="button" id="call-btn">発信</button>
            </p>
            <video id="their-video"  width="360px" height="270px" autoplay muted playsinline></video>    
        </div>
    </div>
    <script src="//cdn.webrtc.ecl.ntt.com/skyway-latest.js"></script>
    <script src="./main.js"></script>
</body>
</html>
```
</div></details>
<details><summary>main.js</summary><div>

```js:main.js
let localStream;
const myVideo = document.getElementById('my-video');
const theirVideo = document.getElementById('their-video');

const CVS_WIDTH = 360
const CVS_HEIGHT = 270

const myCvs = document.getElementById('my-canvas');
const myCtx = myCvs.getContext('2d');
myCvs.width = CVS_WIDTH;
myCvs.height = CVS_HEIGHT;
myCvs.style.width = `${CVS_WIDTH}px`;
myCvs.style.height = `${CVS_HEIGHT}px`;

navigator.mediaDevices.getUserMedia({ video:{ width: 360, height: 270 }, audio: true })
    .then(stream => {
        myVideo.srcObject = stream;
        myVideo.play();
        localStream = stream;
        myVideo.onloadeddata = () => {
            setInterval(() => {
                if(isMosaicEnabled){
                    mosaicMyCvs();
                }else{
                    copyToMyCvs();
                }
            }, 1000 / 30);
        }
    }).catch(error => {
        console.error('mediaDevice.getUserMedia() error:', error);
});

const MOSAIC_SIZE = 20;
const mosaicMyCvs = () => {
    myCtx.clearRect(0, 0, 360, 270);
    const mosaicCvs = document.createElement('canvas');
    const mosaicCtx = mosaicCvs.getContext('2d');
    mosaicCvs.width = CVS_WIDTH ;
    mosaicCvs.height = CVS_HEIGHT ;
    mosaicCvs.style.width = `${CVS_WIDTH}px`;
    mosaicCvs.style.height = `${CVS_HEIGHT}px`;
    mosaicCtx.drawImage(myVideo, 0, 0);
    const imageData = mosaicCtx.getImageData(0, 0, mosaicCvs.width, mosaicCvs.height);
    //モザイクサイズ単位でループ
    for (let y = 0; y < myCvs.height; y = y + MOSAIC_SIZE) {
        for (let x = 0; x < myCvs.width; x = x + MOSAIC_SIZE) {
            // 該当ピクセルの色情報を取得
            const cR = imageData.data[(y * myCvs.width + x) * 4];
            const cG = imageData.data[(y * myCvs.width + x) * 4 + 1];
            const cB = imageData.data[(y * myCvs.width + x) * 4 + 2];
            // モザイクサイズの正方形を描画
            myCtx.fillStyle = `rgb(${cR},${cG},${cB})`;
            myCtx.fillRect(x, y, x + MOSAIC_SIZE, y + MOSAIC_SIZE);
        }
    }
}

const copyToMyCvs = () => {
    myCtx.drawImage(myVideo, 0, 0, 360, 270);
}

let isMosaicEnabled = false;
document.getElementById('mosaic-btn').onclick = () => {
    if (isMosaicEnabled) {
        isMosaicEnabled = false;
        document.getElementById('mosaic-enabled').textContent = "OFF";
    } else {
        isMosaicEnabled = true;
        document.getElementById('mosaic-enabled').textContent = "ON";
    }
}

const peer = new Peer({
    key: window.__SKYWAY_KEY__,
    debug: 3
});

peer.on('open', () => {
    document.getElementById('my-id').textContent = peer.id;
});

document.getElementById('call-btn').onclick = () => {
    const theirID = document.getElementById('their-id').value;
    const mediaConnection = peer.call(theirID, localStream);
    setEventListener(mediaConnection);
};

const setEventListener = mediaConnection => {
    mediaConnection.on('stream', stream => {
        theirVideo.srcObject = stream;
        theirVideo.play();
    });
}

peer.on('call', mediaConnection => {
    mediaConnection.answer(localStream);
    setEventListener(mediaConnection);
});
```
</div></details>
お手元で試してみたい方は、上記のコードをコピペしてみてくださいねー

# おわりに
無事、カメラ映像にモザイクをかけることが出来ました。
次の記事ではこのモザイク加工済みの映像を相手側に送りたいと思います。