Vonage Video API を使った基本のビデオチャットをもとに p5.js ＋ ml5.js を使った AI・機械学習の仕組みを追加してみる【Vonage】

この記事は、「[Vonage Advent Calendar 2023](https://qiita.com/advent-calendar/2023/vonage)」の 24日目の記事です。

## 今回の内容
今回の記事の内容は、「コミュニケーションAPIを使ってみよう、Vonageのことなら何でも共有しよう！」というアドベントカレンダーのトピックになっている「Vonage」に関する記事です。

今回、Vonage Video API を初めて使うので、公式チュートリアル・公式サンプルをもとに基本的な実装を試してみつつ、そこからさらに自分なりの実装を加える、という流れで進めました。

具体的には、以下の内容を段階的に進めていきました。

1. 公式チュートリアル「Basic video chat」を参照して基本のビデオチャットの実装を試す
1. 「Publish-Canvas」の公式サンプルを参考にして、上記のビデオチャットのビデオソースが Canvasに描画した内容になるよう変更する
1. 上記の Canvas描画に p5.js を使い、なおかつ AI・機械学習のライブラリの ml5.js も組み合わせて、ビデオ映像に対する手のキーポイント検出（Handpose）を実装してみる
1. 上記の手のキーポイント検出を行った結果を使って、挙手の動作に連動したアニメーション表示を実装してみる

## 開発の準備（公式チュートリアル「Basic video chat」を試してみる）
### アカウントの準備
前提として、Vonage Video API (tokbox) アカウントの作成が必要です。

[Video API のページ](https://www.vonagebusiness.jp/communications-apis/video/)からアカウントを作成してください。

### 開発のための準備
アカウントを作成済みの状態になったら、次は開発するための準備です。

#### 「Basic video chat」の公式チュートリアルと GitHub のリポジトリ
今回は、自分がよく使っている JavaScript で開発してみようと思います。
公式チュートリアルに、以下の「Basic video chat」という内容があったので、これを使ってみることにします。

●Creating a video chat app
　https://developer.vonage.com/ja/tutorials/basic-video-chat/introduction/javascript

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/85edee78-cfca-d7c2-77df-c94e218eaa5a.png)

次のページへ進むと、GitHub のリポジトリへのリンクが紹介されていました。
そのリポジトリ内で、今回の「Basic video chat」に関するものは、以下が該当するようでした。

●opentok-web-samples/Basic Video Chat at main · opentok/opentok-web-samples · GitHub
　https://github.com/opentok/opentok-web-samples/tree/main/Basic%20Video%20Chat

#### 「Basic video chat」のチュートリアルに必要な環境
また、チュートリアルを進めていくと、必要な環境なども書かれています。
具体的には以下のとおりです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/1df395de-9fd3-f81b-1b62-2d993e7b6345.png)

ここには 4つの内容が書かれており、「Video API のアカウント」「Webカメラ・マイク」「Chrome Firefox などのブラウザ（※ サポートされているものは [⇒こちら](https://developer.vonage.com/ja/video/client-sdks/web/overview#browser-support)）」「コードエディタ」が必要になるようです。

## 「Basic video chat」の開発を進めてみる
それでは、引き続きチュートリアルを使って開発を進めていきます。

### プロジェクト構成と htmlファイルの内容
チュートリアルの次のページに、プロジェクト構成の情報がでてきます。
ファイルとして HTML・JavaScript・CSS の 3つを使うようです。フォルダ・ファイルの構成は、好みに応じて変更しても良いかと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/f6ee10fb-bbb9-c935-a6ca-ce8fd9d6de8d.png)

また、HTML の内容について、以下が書かれていました。

```html:index.html
<html>
<head>
    <title> Vonage Getting Started </title>
    <link href="css/app.css" rel="stylesheet" type="text/css">
    <script src="https://unpkg.com/@vonage/video-client@2/dist/js/opentok.js"></script>
</head>
<body>

    <div id="videos">
        <div id="subscriber"></div>
        <div id="publisher"></div>
    </div>

    <script type="text/javascript" src="js/app.js"></script>
</body>
</html>
```

自分は、ファイル構成に関して、HTML・JavaScript・CSS を同じ階層に置く形にしました。
そのため、HTML の内容を少し修正し、以下としました。

```html:index.html
<html>
<head>
    <title> Vonage Getting Started </title>
    <link href="app.css" rel="stylesheet" type="text/css">
    <script src="https://unpkg.com/@vonage/video-client@2/dist/js/opentok.js"></script>
</head>
<body>

    <div id="videos">
        <div id="subscriber"></div>
        <div id="publisher"></div>
    </div>

    <script type="text/javascript" src="app.js"></script>
</body>
</html>
```

### 認証用の情報
さらにチュートリアルを次に進めると、認証用の情報の話が出てきました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/ca82f923-1f84-fabf-37ce-1adf82820c18.png)

ビデオチャットの実装には「apiKey、sessionId、token」の 3つが必要になるようです。サンプルでは、JavaScript の内容（app.js の内容）の冒頭部分に、それらを直接記載するやり方をとっていました。

以後、以下の内容のように「"YOUR_APP_ID"、"YOUR_SESSION_ID"、"YOUR_TOKEN"」という部分が出てきた時は、それをご自身の認証用の情報に書きかえてください。（※ 以下の内容に関し、サンプルでは var となっていた部分を constにしています）。

```javascript:app.js
// replace these values with those generated in your Video API account
const apiKey = "YOUR_APP_ID";
const sessionId = "YOUR_SESSION_ID";
const token = "YOUR_TOKEN";

// (optional) add server code here
initializeSession();
```

#### 認証用の情報生成の流れ
apiKey、sessionId、token の 3つを取得する方法について見ていきます。
その方法は、以下で説明されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/f05065e6-b51b-d185-1b64-2e9af7afe9d3.png)

ここで書かれた内容などに従って、情報を取得していきます。
この後のそのおおまかな手順は、以下の「Vonage Video APIアカウント」のページで、プロジェクトを作成し、その作成したプロジェクトのページで必要な情報を生成する流れとなるようです。

●Vonage Video API Account
　https://tokbox.com/account/#/

#### プロジェクトを作成する
それでは、「Vonage Video APIアカウント」のページの左側のメニューで「Create New Project」を選択します。そして、その後に出てくる以下で「Create Custom　Project」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/7f017d96-d8b9-2070-826b-29834c226ac6.png)

何らか適当なプロジェクト名を入力します。コーデックは必要に応じて変更してください（※ 自分はデフォルトの VP8 のままにしました）。
そして Create ボタンを押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/a9a8da20-480c-1015-7476-3bbeee37d96a.png)

そうするとプロジェクトが作成され、API KEY と SECRET が画面上に表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/2e525d7f-a8ae-ff66-5475-86783322f041.png)

この API KEY・SECRET はプロジェクト作成後の画面上部でも確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/6bc968ae-49ee-5602-be84-bd321055d39e.png)

この後で必要になるのは、API KEY のほうだけです。

#### 「Session ID」と「token」を生成する
API KEY は作成できたので、「Session ID」と「token」も生成していきます。
それは、プロジェクトのページの中で行えるようです。

それでは、プロジェクトのページの画面を下に進み、「Project Tools」という項目まで進んでください。

ここで「Create Session ID」というボタンを押すことで、「Session ID」を作成できます。Session ID を作成してから、次へ進んでください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/9d56e07f-227a-594a-31ca-c434816ff7f2.png)

さらにその下の画面では「token」をも作成できます。

先ほど作成した Session ID を使い、Role・Expiration Time を適宜変更するなどして、「Generate token」ボタンを押します。
（自分は、Role はデフォルトのままとして、Expiration Time は期間を 24時間などデフォルトよりも長くするようにしました）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/344bed41-837b-82a4-329d-e1965ecbf0c3.png)

これで「token」が生成されます。

ここまでの手順で、app.js の `"YOUR_APP_ID"` 、 `"YOUR_SESSION_ID"` 、 `"YOUR_TOKEN"` の部分に入力するべき情報 3つがそろいました。

### Publisher を準備する
さらに、チュートリアルのページを、次へ進めていきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/75ccec8b-95b2-019b-00c3-dc3e5eb36253.png)

ここでは Publisher を作成していきます。
先ほどの app.js に処理を追加します。追加する部分は以下のとおりです。

```javascript:app.js
// Handling all of our errors here by alerting them
function handleError(error) {
  if (error) {
    alert(error.message);
  }
}

function initializeSession() {
  const session = OT.initSession(apiKey, sessionId);

  // Subscribe to a newly created stream

  // Create a publisher
  const publisher = OT.initPublisher('publisher', {
    insertMode: 'append',
    width: '100%',
    height: '100%'
  }, handleError);

  // Connect to the session
  session.connect(token, function(error) {
    // If the connection is successful, publish to the session
    if (error) {
      handleError(error);
    } else {
      session.publish(publisher, handleError);
    }
  });
}
```

### Subscriber を準備する
次は Subscriber を準備します。
Publisher が配信側、Subscriber は受信側という位置付けになるかと思います。

先ほどのコードの、 `function initializeSession() {}` の最後の部分に、以下を追加します。

```javascript
session.on('streamCreated', function(event) {
  session.subscribe(event.stream, 'subscriber', {
    insertMode: 'append',
    width: '100%',
    height: '100%'
  }, handleError);
});
```

### コード全体
ここまでの手順を進めた結果、できあがった JavaScript のコードは、以下のとおりです。
※ `"YOUR_APP_ID"` 、 `"YOUR_SESSION_ID"` 、 `"YOUR_TOKEN"` は、ご自身が用意したものに置きかえてください

```javascript:app.js
const apiKey = "YOUR_APP_ID";
const sessionId = "YOUR_SESSION_ID";
const token = "YOUR_TOKEN";

initializeSession();

function handleError(error) {
  if (error) {
    alert(error.message);
  }
}

function initializeSession() {
  const session = OT.initSession(apiKey, sessionId);

  const publisher = OT.initPublisher('publisher', {
    insertMode: 'append',
    width: '100%',
    height: '100%'
  }, handleError);

  session.connect(token, function(error) {
    if (error) {
      handleError(error);
    } else {
      session.publish(publisher, handleError);
    }
  });

  session.on('streamCreated', function(event) {
    session.subscribe(event.stream, 'subscriber', {
      insertMode: 'append',
      width: '100%',
      height: '100%'
    }, handleError);
  });
}
```

### CSS の準備
次に app.css を準備します。内容は以下のとおりです。

```css:app.css
body, html {
    background-color: gray;
    height: 100%;
}

#videos {
    position: relative;
    width: 100%;
    height: 100%;
    margin-left: auto;
    margin-right: auto;
}

#subscriber {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    z-index: 10;
}

#publisher {
    position: absolute;
    width: 360px;
    height: 240px;
    bottom: 10px;
    left: 10px;
    z-index: 100;
    border: 3px solid white;
    border-radius: 3px;
}
```

ここで作った CSS の内容は、これ以後も同じものを使います。

### 動作確認
ここまでで、HTML・JavaScript・CSS の全てがそろったので、動作確認を行います。

ブラウザのウィンドウを 2つ開き、それぞれで index.html を開きます。
そうすると、以下のように 2つのウィンドウを、それぞれ送信側・受信側としたビデオチャットが動くのを確認できました（1台の PC で開いたブラウザのウィンドウ　2つが、同じ入力映像を使っている形になるため、見た目がややこしいですが...）。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/942342cf-13bf-dac1-a946-003ba75c1273.png)

ここまで進めたみて、「Vonage Video API を初めて使ったけど、簡単にビデオチャットを作成できてしまった...」と感じました。コードもそれほど長くない内容で、こんなにあっさりできてしまったのは驚きでした。

## 「Publish-Canvas」のサンプル
ひとまず、基本のビデオチャットは無事に完成しました。

さらに何か試してみようかと思い、公式の GitHubリポジトリの JavaScriptサンプルを見てみました。
そうすると、個人的にすごく気になるものがありました。それは、以下の「Publish-Canvas」のサンプルです。

●opentok-web-samples/Publish-Canvas at main · opentok/opentok-web-samples · GitHub
　https://github.com/opentok/opentok-web-samples/tree/main/Publish-Canvas

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/140ab4fd-743d-c691-fc4e-6c9b5f88b028.png)

ビデオチャットの入力映像として、Webカメラの映像ではなく、Canvas内の描画内容を利用できるもののようでした。

Canvas を対象にできるというのを見て、自分がよく使っている（そして Qiita によく記事を書いている）「描画系のライブラリ p5.js」を組み合わせて何かできそう、と思いました。
これまで p5.js を使っていろいろな描画系の作品などを試作してきたので、そういったリソースをうまく活用できるかもしれません。

そこで、この Canvas内の描画を利用するやり方を探ってみることにしました。
ポイントになりそうな部分は、JavaScript の処理になるように思われたので、その部分を見てみます。

### JavaScript の処理のポイントになりそうな部分を見てみる
以下が JavaScript のコード全体です。

```javascript:app.js
/* global OT API_KEY TOKEN SESSION_ID SAMPLE_SERVER_BASE_URL */

let apiKey;
let sessionId;
let token;

function handleError(error) {
  if (error) {
    console.error(error);
  }
}

function initializeSession() {
  const session = OT.initSession(apiKey, sessionId);

  // Subscribe to a newly created stream
  session.on('streamCreated', (event) => {
    const subscriberOptions = {
      insertMode: 'append',
      width: '100%',
      height: '100%'
    };
    session.subscribe(event.stream, 'subscriber', subscriberOptions, handleError);
  });

  session.on('sessionDisconnected', (event) => {
    console.log('You were disconnected from the session.', event.reason);
  });

  const randomColour = () => {
    return Math.round(Math.random() * 255);
  };

  const canvas = document.createElement('canvas');
  canvas.width = 640;
  canvas.height = 480;
  const ctx = canvas.getContext('2d');

  // Draw a random colour in the Canvas every 3 seconds
  const interval = setInterval(() => {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = `rgb(${randomColour()}, ${randomColour()}, ${randomColour()})`;
    ctx.fillRect(0, 0, canvas.width, canvas.height);
  }, 3000);

  // initialize the publisher
  const publisherOptions = {
    insertMode: 'append',
    width: '100%',
    height: '100%',
    videoSource: canvas.captureStream(3).getVideoTracks()[0] // Use canvas.captureStream at 3 fps and pass the video track to the Publisher
  };

  const publisher = OT.initPublisher('publisher', publisherOptions, (error) => {
    if (error) {
      clearInterval(interval);
      handleError(error);
      alert(error.message);
    }
  });

  publisher.on('destroyed', () => {
    clearInterval(interval);
  });

  // Connect to the session
  session.connect(token, (error) => {
    if (error) {
      handleError(error);
    } else {
      // If the connection is successful, publish the publisher to the session
      session.publish(publisher, handleError);
    }
  });
}

// See the config.js file.
if (API_KEY && TOKEN && SESSION_ID) {
  apiKey = API_KEY;
  sessionId = SESSION_ID;
  token = TOKEN;
  initializeSession();
} else if (SAMPLE_SERVER_BASE_URL) {
  // Make a GET request to get the OpenTok API key, session ID, and token from the server
  fetch(SAMPLE_SERVER_BASE_URL + '/session')
      .then((response) => response.json())
      .then((json) => {
        apiKey = json.apiKey;
        sessionId = json.sessionId;
        token = json.token;
        // Initialize an OpenTok Session object
        initializeSession();
      }).catch((error) => {
    handleError(error);
    alert('Failed to get opentok sessionId and token. Make sure you have updated the config.js file.');
  });
}
```

コードを見た感じだと、以下の Canvas を videoSource に紐付ける部分がポイントになりそうでした（Canvas描画に関係する処理も、必要にはなってきますが）。

```javascript
  const publisherOptions = {
    insertMode: 'append',
    width: '100%',
    height: '100%',
    videoSource: canvas.captureStream(3).getVideoTracks()[0] // Use canvas.captureStream at 3 fps and pass the video track to the Publisher
  };
```

### 「Basic video chat」に Canvas関連の処理を追加してみる
上記で見てきた限りだと、「Basic video chat」に Canvas関連の処理を足すということをやろうとした時に、行うべき内容は2つありそうです。それは、「Publisher のビデオソースを Canvas にする」というところと、「Canvas描画の部分を足す」という対応です。

実際に、先ほど作成した「Basic video chat」の app.js に手を加えて試してみます。
元の JavaScript のコードを、以下のようにしました。

```javascript:app2.js
const apiKey = "YOUR_APP_ID";
const sessionId = "YOUR_SESSION_ID";
const token = "YOUR_TOKEN";

initializeSession();

function handleError(error) {
  if (error) {
    alert(error.message);
  }
}

function initializeSession() {
  const randomColour = () => {
    return Math.round(Math.random() * 255);
  };
  
  const canvas = document.createElement("canvas");
  canvas.width = 640;
  canvas.height = 480;
  const ctx = canvas.getContext("2d");
  const interval = setInterval(() => {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = `rgb(${randomColour()}, ${randomColour()}, ${randomColour()})`;
    ctx.fillRect(0, 0, canvas.width, canvas.height);
  }, 3000);

  const session = OT.initSession(apiKey, sessionId);

  const publisher = OT.initPublisher(
    "publisher",
    {
      insertMode: "append",
      width: "100%",
      height: "100%",
      videoSource: canvas.captureStream(3).getVideoTracks()[0], // Use canvas.captureStream at 3 fps and pass the video track to the Publisher
    },
    handleError
  );

  session.connect(token, function (error) {
    if (error) {
      handleError(error);
    } else {
      session.publish(publisher, handleError);
    }
  });

  session.on("streamCreated", function (event) {
    session.subscribe(
      event.stream,
      "subscriber",
      {
        insertMode: "append",
        width: "100%",
        height: "100%",
      },
      handleError
    );
  });
}
```

ここで掲載した内容と、先ほど「Basic video chat」用に作成していた内容との主な差分は 2つです。
1つは `function initializeSession() {}` の冒頭 13行くらいに Canvas関連の処理を加えたのと、もう 1つは `OT.initPublisher()` の中で `videoSource: canvas.captureStream(3).getVideoTracks()[0]` を加えたところです。

この内容で動作確認をしてみたところ、以下のとおり Canvas で描画した内容（色付の矩形）を送りあうという動作を実現できているのが確認できました。

<iframe width="560" height="315" src="https://www.youtube.com/embed/5A-_NURKpSE?si=G1ys54VVzfvJ7aSw" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

## 「AI・機械学習の仕組み」を組み込んでみる
ひとまず、Canvas に描画した色付きの矩形を送りあう、という動作は実現できました。

さらに、Canvas上でのビデオ映像の表示を行う処理と、その映像に対する「AI・機械学習を使った処理」を加えてみます。

そのために、以下の 2つのライブラリを使います。

- 追加で使うもの
   - 描画ライブラリの p5.js
   - AI・機械学習ライブラリの ml5.js

### 参照する公式の情報
それでは、p5.js と ml5.js を使った処理を実装していきます。

ml5.js で使える、AI・機械学習を使ったカメラ映像に対する処理はいくつかありますが、今回は手を対象とした「Handpose」を使うことにます。

それでは p5.js・ml5.js のそれぞれについて、参照すると良さそうな公式サンプルを見て、ビデオチャットのコードに追加実装を行っていきます。

●examples | p5.js： Video Capture
　https://p5js.org/examples/dom-video-capture.html

●Handpose
　https://learn.ml5js.org/#/reference/handpose

### コードに手を加える
それでは、上で作成してきた内容のうち HTML と JavaScript のファイルに手を加えていきます。
（※ CSS は最初に用意したものを、そのまま利用します）

#### HTML の内容
HTML は、p5.js と ml5.js のライブラリを読み込む処理を追加するだけです。
具体的には以下のとおりです（※ 6行目と 7行目が追加した部分です）。

```html
<html>
<head>
    <title> Vonage Getting Started </title>
    <link href="app.css" rel="stylesheet" type="text/css">
    <script src="https://unpkg.com/@vonage/video-client@2/dist/js/opentok.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/p5.js"></script>
    <script src="https://unpkg.com/ml5@latest/dist/ml5.min.js" type="text/javascript"></script>
</head>
<body>

    <div id="videos">
        <div id="subscriber"></div>
        <div id="publisher"></div>
    </div>

    <script type="text/javascript" src="app5.js"></script>
</body>
</html>
```

#### JavaScript の内容
次に、JavaScript のコードに手を加えます。以下が最終版のコードです。

主な変更点は、p5.js の定番の処理となっている「setup()」と「draw()」の部分です。ちなみに、「setup()」は最初に 1回のみ実行される処理で、「draw()」は一定の時間間隔で繰り返される処理です。

※ `"YOUR_APP_ID"` 、 `"YOUR_SESSION_ID"` 、 `"YOUR_TOKEN"` は、ご自身が用意したものに置きかえてください

```javascript
const apiKey = "YOUR_APP_ID";
const sessionId = "YOUR_SESSION_ID";
const token = "YOUR_TOKEN";

let p5canvas;
let handpose;
let video;
let predictions = [];

function setup() {
  p5canvas = createCanvas(640, 480);
  video = createCapture(VIDEO);
  video.size(width, height);

  handpose = ml5.handpose(video, function () {
    console.log("Model ready!");
  });

  handpose.on("predict", (results) => {
    predictions = results;
  });

  video.hide();

  initializeSession();
}

function draw() {
  let img = video.get();

  push();
  image(img, 0, 0);
  pop();

  for (let i = 0; i < predictions.length; i += 1) {
    const prediction = predictions[i];
    for (let j = 0; j < prediction.landmarks.length; j += 1) {
      const keypoint = prediction.landmarks[j];
      fill(0, 255, 0);
      noStroke();
      ellipse(keypoint[0], keypoint[1], 10, 10);
    }
  }
}

function handleError(error) {
  if (error) {
    alert(error.message);
  }
}

function initializeSession() {
  let canvas = p5canvas.elt;

  const session = OT.initSession(apiKey, sessionId);

  const publisher = OT.initPublisher(
    "publisher",
    {
      insertMode: "append",
      width: "100%",
      height: "100%",
      videoSource: canvas.captureStream(3).getVideoTracks()[0],
    },
    handleError
  );

  session.connect(token, function (error) {
    if (error) {
      handleError(error);
    } else {
      session.publish(publisher, handleError);
    }
  });

  session.on("streamCreated", function (event) {
    session.subscribe(
      event.stream,
      "subscriber",
      {
        insertMode: "append",
        width: "100%",
        height: "100%",
      },
      handleError
    );
  });
}
```

### 動作確認
それでは、作成したものの動作確認を行います。

今回使った Handpose は、手の複数箇所のキーポイントを検出できます。
例えば、各指の指先や、第一関節や第二関節の部分、その他に手首の根元などです。

以下をご覧いただくと、そういった手のキーポイントの上に、緑の丸が重畳描画されているのが分かります。

<iframe width="560" height="315" src="https://www.youtube.com/embed/9pq7xgqMeI4?si=0HVf3wq3izHcuGcS" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

## 「AI・機械学習の仕組み」を組み込んでみる 〜その2〜
上で行った内容は、ビデオ画像からの手のキーポイント検出を行うところまででした。

ここからさらに、手のキーポイント検出を行った結果を使って、それに応じた何らかの表示をするような追加実装をやってみます。具体的には、「ビデオにうつっている人が挙手をしたら、その時に挙手していることを強調して気づいてもらいやすくするような、アニメーションを表示をする」という内容です。

### 動作させた結果
まずは、どのようなものができあがるか、結果を先にご覧いただこうと思います。

以下を見ていただくと、手を挙げた時に、画面に向かって左上のところに絵文字を使ったアニメーションが表示されているのが分かります。これについて、実装内容とその補足を書いていきます。

<iframe width="560" height="315" src="https://www.youtube.com/embed/cTaGa91WTzo?si=8ov-AOqlj7zXd_U2" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

### 実装した内容
#### HTML と CSS の内容について
今回の内容に関して、1つ前の内容から変更を加えているのは、JavaScript のファイルのみです。

つまり、HTML は　1つ前のものと同じものを用い、CSS の内容は最初から使っているものを用います。

#### JavaScript の内容
それでは、JavaScript の追加実装を行った内容を、以下に掲載します。

※ `"YOUR_APP_ID"` 、 `"YOUR_SESSION_ID"` 、 `"YOUR_TOKEN"` は、ご自身が用意したものに置きかえてください

```javascript:app3.js
const apiKey = "YOUR_APP_ID";
const sessionId = "YOUR_SESSION_ID";
const token = "YOUR_TOKEN";

let p5canvas;
let video;
let handpose,
  predictions = [];

function setup() {
  p5canvas = createCanvas(640, 480);
  p5canvas.hide();
  video = createCapture(VIDEO);
  video.size(width, height);
  video.hide();

  handpose = ml5.handpose(video, function () {
    console.log("Model ready!");
  });

  handpose.on("predict", (results) => {
    predictions = results;
  });

  initializeSession();
}

function draw() {
  let img = video.get();
  image(img, 0, 0);

  for (let i = 0; i < predictions.length; i += 1) {
    const prediction = predictions[i];
    for (let j = 0; j < prediction.landmarks.length; j += 1) {
      const keypoint = prediction.landmarks[j];
      const x = keypoint[0],
        y = keypoint[1];

      if (j === 8 && y < height / 2) {
        textSize(200 + 10 * cos(frameCount));
        text("✋", 50, 200);
      }
      fill(j === 8 ? [255, 100, 0, 128] : [0, 255, 0, 64]);
      noStroke();
      ellipse(x, y, 10, 10);
    }
  }
}

function handleError(error) {
  if (error) {
    alert(error.message);
  }
}

function initializeSession() {
  let canvas = p5canvas.elt;

  const session = OT.initSession(apiKey, sessionId);

  const publisher = OT.initPublisher(
    "publisher",
    {
      insertMode: "append",
      width: "100%",
      height: "100%",
      videoSource: canvas.captureStream(3).getVideoTracks()[0],
    },
    handleError
  );

  session.connect(token, function (error) {
    if (error) {
      handleError(error);
    } else {
      session.publish(publisher, handleError);
    }
  });

  session.on("streamCreated", function (event) {
    session.subscribe(
      event.stream,
      "subscriber",
      {
        insertMode: "append",
        width: "100%",
        height: "100%",
      },
      handleError
    );
  });
}
```

主な変更点は `draw()` の中の部分です。

追加で実装した内容は、「挙手をした際にアニメーションを表示する」という内容だと書いていました。
そのポイントになる部分は以下です。

```javascript
      if (j === 8 && y < height / 2) {
        textSize(200 + 10 * cos(frameCount));
        text("✋", 50, 200);
      }
```

ここでは、実装内容を簡易な処理にするために、挙手の動作のジェスチャー認識をするのではなく、シンプルに手のキーポイント検出結果の y座標を用いた処理を行っています。具体的には、人差し指の先の位置が、画面の上半分になった時に、アニメーションを表示させるという内容です。

また、アニメーションでも少し自分なりの工夫をしました。
「✋」の絵文字を表示させているのですが、その絵文字を単純に表示するだけだと、少し目立ちにくいかもしれないと思いました。そのため、その絵文字の表示サイズを少し時間で変動させました。

その処理として、 `10 * cos(frameCount)` という三角関数を使った「-10 から 10 の間で周期変動する値」を作り、その変動する値を絵文字のサイズ「200」に加算しました。

このようにして、「挙手をしたら、それに連動したアニメーションを表示する」という内容を実現できました。

## おわりに
今回、Vonage Video API を使った基本のビデオチャットを初めて作ってみて、それにさらに手を加えてみました。

具体的には、ビデオチャットのビデオソースを Canvasにする形とし、その Canvas描画を p5.js を使った処理で行いました。さらに、その p5.js での処理に、ml5.js を使った AI・機械学習の仕組みを追加しました。

今回、Vonage Video API を初めて使ってみて、上記の内容を素早く作ることができ、とても便利な API だと感じました。

## 【2024/1/20 追記】 Vonage Advent Calendar 2023 の優秀賞を受賞
[Vonage Advent Calendar 2023](https://qiita.com/advent-calendar/2023/vonage) の優秀賞をいただけました！

●Qiita Advent Calendar 2023 プレゼントカレンダー 結果発表！ - Qiita Blog
https://blog.qiita.com/adventcalendar-2023-presents-winners/
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/b4175ea5-d98c-7e3c-d0be-5963b8f4b758.png)