p5.js と ml5.js の組み合わせでブラウザ上でのリアルタイム物体検出を試す（COCO-SSD を利用）

この記事は、以下のツイートで動画・画像をのせていた、ブラウザ上でのリアルタイム物体検出の話です。

https://twitter.com/youtoy/status/1614483055703658496

https://twitter.com/youtoy/status/1614516076032978944

上記の内容は、以下の OpenProcessing のスケッチにアクセスすると、実際に試していただくことができます。

●ml5.js で物体検出（COCO-SSD を利用） - OpenProcessing
　https://openprocessing.org/sketch/1795350

## 実装した内容の話
今回の内容は、p5.js と ml5.js の 2つを組み合わせて作っています。その実装内容や、実装に関する補足を、この後に書いていきます。

### 実装の際に参照した情報
実装時には、以下の ml5.js公式のドキュメントを見て実装しました。

●ObjectDetector
　https://learn.ml5js.org/#/reference/object-detector
![ObjectDetector](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/d3b38a3c-cf8a-eb0a-1d8d-fafc02a00af5.png)

#### ml5.js による物体検出の概要
ObjectDetector に関する公式ドキュメントのクイックスタートで、実装の概要が書かれています。
![ObjectDetectorのクイックスタート](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/476eecf8-0110-d2c2-c5b5-156793ced061.png)

大まかには、以下の処理となるようです。

1. カメラから画像を取り込む
1. 物体検出用の機械学習モデルを読み込む
1. 上記の 2つの準備がそろったら、物体検出の処理を行い、結果を得る

そして、得られた検出結果を画面上に描画する処理も行います。これについては、p5.js を使った文字・矩形の描画を行います。

### JavaScript で実装した内容 【最初のバージョン】
:::note alert
【追記】 その後、公式ドキュメント・サンプルを見直したところ、この最初のバージョンの実装（detect()周りの部分）は修正したほうが良い事が分かりました。
修正後の内容は、[この記事の最後の「【追記】 実装した内容の修正版」という部分](https://qiita.com/youtoy/items/57623faa9f39f4a3a64b#%E8%BF%BD%E8%A8%98-%E5%AE%9F%E8%A3%85%E3%81%97%E3%81%9F%E5%86%85%E5%AE%B9%E3%81%AE%E4%BF%AE%E6%AD%A3%E7%89%88)に掲載します。
:::

ここで、個別の話に入っていく前に、JavaScript で実装した内容を掲載します。

以下の内容は、p5.js Web Editor・OpenProcessing といった、オンラインで p5.js のプログラムを開発・実行できる環境で、「sketch.js」というファイル名で用意される JavaScript のプログラムの部分に書いた内容です。

:::note warn
これと合わせて、同環境で用意される HTML・CSS に関し、HTML に関する部分は少し操作などを行います。
具体的には、ml5.js のライブラリを読み込むための設定です（p5.js Web Editor では、HTMLファイルで、ml5.js を CDN から読み込む行を追加しており、OpenProcessing ではライブラリの有効／無効を切り替えるトグルボタンで、ml5.js を有効化しました）。
:::

```javascript
let capture;
let objectDetector,
  isModelLoaded = false;
let objects;

function setup() {
  createCanvas(640, 480);

  objectDetector = ml5.objectDetector("cocossd", {}, function () {
    isModelLoaded = true;
    console.log("Model Loaded!");
  });

  capture = createCapture(VIDEO);
  capture.size(640, 480);
  capture.hide();
}

function draw() {
  if (capture) {
    const img = capture.get();
    image(img, 0, 0);

    if (isModelLoaded) {
      objectDetector.detect(capture, (err, results) => {
        // console.log(results);
        objects = results;
      });
    }
  }

  if (objects) {
    for (object of objects) {
      textSize(24);
      noStroke();
      fill(0, 255, 0);
      text(object.label, object.x + 5, object.y + 25);
			
      noFill();
      strokeWeight(4);
      stroke(0, 255, 0);
      rect(object.x, object.y, object.width, object.height);
    }
  }
}
```

### 実装内容の補足
ここから、実装内容について補足します。

#### カメラからの画像の読み込み
今回、カメラからの画像の読み込みは、 p5.js の関数を用いています。

具体的には [createCapture()](https://p5js.org/reference/#/p5/createCapture) を用いた行です。その後、 `capture` という変数を用いている部分がありますが、これらの部分はビデオのサイズ指定・ビデオ要素を不可視にするといったことを行っています。

また、以下の部分でカメラから映像が得られているかどうかの判定や、p5.js のキャンバス上へのカメラ画像の描画を行っています。

```javascript
  if (capture) {
    const img = capture.get();
    image(img, 0, 0);

    ...
  }
```

#### 機械学習モデルの読み込み
物体検出用の機械学習モデルの読み込みは、以下の部分です。

```javascript
  objectDetector = ml5.objectDetector("cocossd", {}, function () {
    isModelLoaded = true;
    console.log("Model Loaded!");
  });
```

公式のクイックスタートのサンプルでは、モデル読み込み後の関数を分けていましたが、ここではそれをまとめて書いています。それと、 `isModelLoaded` という変数で、モデルの読み込みが完了しているかどうかの状態を管理しています。

#### 物体検出の結果の取得
物体検出の処理を行う部分は、以下です。

```javascript
   if (isModelLoaded) {
      objectDetector.detect(capture, (err, results) => {
        // console.log(results);
        objects = results;
      });
    }
```

モデルの読み込みが完了していないと物体検出の処理が行えないため、先ほど触れていた、モデルの読み込みが完了しているかどうかの状態管理をする変数を使い、読み込みが完了しているかどうかを判定しています。
そして、物体検出処理にカメラ画像をわたし、その画像に対して行われた物体検出の結果を `objects` という変数で参照できるようにしています。

#### 物体検出の結果の描画
最後に、物体検出の結果の描画の部分の説明です。

ここでは、上記の `objects` という変数を使い、物体検出結果が参照可能であるかどうかを判定しています。そして、物体検出の結果が参照可能であった場合に、物体検出で得られた情報を画面に描画します。

```javascript
if (objects) {
    for (object of objects) {
      textSize(24);
      noStroke();
      fill(0, 255, 0);
      text(object.label, object.x + 5, object.y + 25);
			
      noFill();
      strokeWeight(4);
      stroke(0, 255, 0);
      rect(object.x, object.y, object.width, object.height);
    }
  }
```

物体検出で得られた結果の情報は、複数の物体に関する情報が含まれる場合もあります。そのため、for...of を使って得られた結果ごとに処理を行うようにしています。

描画処理については、それら各情報に関し、「得られたラベル（物体の名前）」と「物体を検出した矩形領域」とを描画します。

このような処理により、冒頭に掲載した物体検出の結果の描画が行えます。

### 【追記】 実装した内容の修正版
公式の情報を見直してみると、[p5.js Web Editor上の「ObjectDetector_COCOSSD_Video」という公式サンプル](https://editor.p5js.org/ml5/sketches/ObjectDetector_COCOSSD_Video) がありました。

そのサンプルの実装を見ると、detect() の処理は、検出結果を得るごとに再実行する形が良さそうです。それで、公式サンプルを少し修正した、以下のバージョンを作りました。

```javascript
let capture;
let detector,
  detections = [];

function setup() {
  createCanvas(640, 480);

  capture = createCapture(VIDEO);
  capture.size(width, height);
  capture.hide();

  detector = ml5.objectDetector("cocossd", function () {
    console.log("Model Loaded!");
    detector.detect(capture, gotDetections);
  });
}

function gotDetections(error, results) {
  if (error) {
    console.error(error);
  }
  detections = results;
  detector.detect(capture, gotDetections);
}

function draw() {
  image(capture.get(), 0, 0);

  for (object of detections) {
    const { x, y, width: w, height: h, label } = object;

    stroke(0, 255, 0);
    strokeWeight(4);
    noFill();
    rect(x, y, w, h);

    noStroke();
    fill(255);
    textSize(24);
    text(label, x + 10, y + 24);
  }
}
```