ビデオチャット時代のプライバシーと表現力の調和を兼ね備えたWebアプリを作ってみた

#概要
　ここ数か月でオンラインセミナーやオンラインイベントが急激に増えていますね。その中であるあるなのが、画面真っ黒状態。何かというと、参加者がカメラオフにしていて参加者一覧画面が真っ黒に染まる事です。

　これは主催や運営側として、参加者の感情を読み取りづらかったりつ辛い部分があるとのことです。そこで、少しでも華やかになりつつプライバシー保護を考慮したビデオチャットのプロトタイプを作ってみました！

　どうしたかというと、相手の画像を「画風変換にかける」です。以下が実際のようです。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">新時代のビデオチャットは画面を真っ暗にするのではなく、画風変換なんてのはいかがでしょう？<br>これならある程度プライバシー保護されつつ、感情や身振り手振りも伝わりやすいと考え作ってみました！<br>※サーバーを消したので、今はつながりません。<a href="https://twitter.com/hashtag/protoout?src=hash&amp;ref_src=twsrc%5Etfw">#protoout</a> <a href="https://t.co/PhJNEhOtUX">pic.twitter.com/PhJNEhOtUX</a></p>&mdash; まえぷー@出窓菜園ティスト (@kmaepu) <a href="https://twitter.com/kmaepu/status/1271078245115412485?ref_src=twsrc%5Etfw">June 11, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

**けっこうホラーですね**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183282/e170e9ac-c496-21d1-d7c5-c638335a246f.png)


#技術解説
##開発環境
- ホストマシン： Windows10 x64
- エディタ：Visual Studio Code
- ブラウザ：Google Chrome
- ライブラリ；p5.js 、　ml5.js

##構成
システム構成は次のようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183282/d2ede4a3-3f76-b23c-286e-6c96c7489322.png)

ビデオチャットにはWebRTCを手軽に利用できるSkywayで行っています。

##画風変換について
 
　画風変換はml5.jsの「**styleTransfer**」のサンプルを利用しています。このライブラリは、特定の画風を機械学習させておき、入力された画像に画風を加えて出力します。
　[こちら](https://ml5js.org/reference/api-StyleTransfer/)が公式のサンプルコードです。

##ソースコード
各所にコメントを載せていますので、styleTrasnferってこう使うんだなと思っていただければ幸いです。


```html:index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>kmaepu-waiwai chat</title>
  </head>
  <body>
    <div id="video-area"></div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.0.0/p5.min.js"></script>
    <script src="https://unpkg.com/ml5@0.4.3/dist/ml5.min.js"></script>
    <script src="https://cdn.webrtc.ecl.ntt.com/skyway-latest.js"></script>
    <script src="main.js"></script>
  </body>
</html>
```

Skywayを利用するには認証が必要なので、アクセスキーを入手したら「skyway KEY」を差し替える必要があります。

```javascript:main.js
let capture;
let theirVideo;
let style;
let mystyle;
let resultImg;
let myresultImg;
let img;

function setup() {
  createCanvas(640, 200); //canvas作成

  capture = createCapture({
    video: { width: 640, height: 480 },
    audio: false,
  });
  capture.hide(); //キャンバスで描くので非表示
  capture.size(160, 120); //重いので小さく

  resultImg = createImg("");
  resultImg.hide(); //キャンバスで描くので非表示

  myresultImg = createImg("");
  myresultImg.hide(); //キャンバスで描くので非表示

  //自分のにかけるときはここ
  mystyle = ml5.styleTransfer("models/wave", capture, function () {
     mystartTransfer();
    });

  // skywayのインスタンスを作成
  let peer = new Peer({
    key: "skyway KEY",
  });
  // skywayでドメインを許可していれば実行される
  peer.on("open", () => {
    console.log("open! id=" + peer.id);
    createP("Your id: " + peer.id);
  });

  // id入力タグの生成
  let idInput = createInput("");

  // 送信ボタンの生成
  createButton("Call").mousePressed(() => {
    // ボタンが押されたら
    const callId = idInput.value(); //id入力欄の値を取得
    console.log("call! id=" + peer.id);
    const call = peer.call(callId, capture.elt.srcObject); //id先を呼び出し
    addVideo(call);
  });

  // // 相手から呼び出された実行される
  peer.on("call", (call) => {
    console.log("be called!");
    call.answer(capture.elt.srcObject); //呼び出し相手に対して返す
    addVideo(call);
  });

  // 相手の映像を追加処理
  function addVideo(call) {
    call.on("stream", (theirStream) => {
      console.log("stream!");
      //相手のビデオを作成
      theirVideo = createVideo();
      theirVideo.elt.autoplay = true;
      theirVideo.elt.srcObject = theirStream;
      theirVideo.size(160, 120); //重いので小さく
      theirVideo.hide(); //キャンバスで描くので非表示

      //相手側のビデオ映像に対してstyle変換
      style = ml5.styleTransfer("models/udnie", theirVideo, function () {
        print("loaded");
        startTransfer();
      });
    });
  }
}

function draw() {
  image(myresultImg, 0, 0);
  image(resultImg, 160, 0);
}

function startTransfer() {
  style.transfer(function (err, res) {
    resultImg.attribute("src", res.src);
    startTransfer();
  });
}

function mystartTransfer() {
  mystyle.transfer(function (err, res) {
    myresultImg.attribute("src", res.src);
    mystartTransfer();
  });
}
```

#おわりに
　ここまで作るのは、javascript初心者の私には酷でした。そんな私をサポートして下さったProtoout関連の方、誠にありがとうございました。

余談ですが、スマートフォンのようにカメラが2つある場合は動きませんでした。背面カメラとインカメラがあるためです。
[この記事](https://qiita.com/shiba_mitue/items/6b6b6c1482c8815226de)参考にできそうですが、自分には力及ばずという結果でした。気になる方は試してみてはいかがでしょうか。

#参考
[p5.js](https://p5js.jp/reference/)
[ml5.js](https://examples.ml5js.org/)
[6_1：イメージ p5.js JavaScript](https://himco.jp/2019/02/18/6_1%EF%BC%9A%E3%82%A4%E3%83%A1%E3%83%BC%E3%82%B8-p5-js-javascript/)
[skyway javascript SDK](https://webrtc.ecl.ntt.com/api-reference/javascript.html#%E3%82%AF%E3%83%A9%E3%82%B9%E4%B8%80%E8%A6%A7)
[koji/fast_style_transfer_in_p5ML](https://github.com/koji/fast_style_transfer_in_p5ML)
[6_1：画風変換(Style Transfer) ml5.js JavaScript](https://himco.jp/2019/01/03/5_1%EF%BC%9A%E7%94%BB%E9%A2%A8%E5%A4%89%E6%8F%9Bstyle-transfer/)
[styleTransfer](https://ml5js.org/reference/api-StyleTransfer/)
[guthub styleTransfer](https://github.com/ml5js/ml5-examples/tree/release/p5js/StyleTransfer/StyleTransfer_Image)