Vision APIで物体検出した画像に四角い枠を描画する方法【Canvas】

[RUNTEQアドベントカレンダー2021](https://qiita.com/advent-calendar/2021/runteq) 9日目の記事です。

# はじめに
[たいじゅ](https://qiita.com/taiju_higashimachi)と申します。

この記事では**「Vision APIのレスポンスをもとに、四角い枠を描画する」**方法を解説します。
四角い枠とは、次の画像のように、物体が画像内のどこに検出されたかを示すものです。
![aaf9e72a4af0244a51893a499f2fbb3b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/641012/fd40de68-01c6-8b89-2200-4227862b6e5f.png)

検索してもピンポイントで解説されている記事が見つからず、また実際に私がサービスを開発する中で詰まってしまった内容です。
なので、Vision APIを使ってサービスづくりをしてみたいという方は、ぜひ最後までご覧ください。


**▼Vision APIを使って開発したサービス**
[身の回りにある「呪いのアイテム」を検出するサービスを未経験エンジニアが作ってみた【個人開発】](https://qiita.com/taiju365/items/f7c5387ade42bfb5d3cf)

# 目次
| 大見出し | 小見出し |
|:--|:--|
|[Vision APIを使うときの流れ](#visionapiを使うときの流れ)|
|[具体的なやり方](#具体的なやり方)|[1．座標データを取得する](#1座標データを取得する)|
||[2．Canvasで画像を表示する](#2canvasで画像を表示する)|
||[3．画像のサイズ問題を解決する](#3画像のサイズ問題を解決する)|
||[4．四角形を描画する](#4四角形を描画する)|
|[さいごに](#さいごに)||



# VisionAPIを使うときの流れ
Vision APIを使うときの流れを大きくわけると、次のようになります。

1．Vision APIにリクエストを送る
2．Vision APIからレスポンスが返ってくる
3．レスポンスのデータを使う

「1．Vision APIにリクエストを送る」に関しては、多くの記事で解説されているので、この記事では説明しません。
その代わりに、開発の際にとてもお世話になった記事のリンクを貼っておきます。

[【図解】個人サービスを例に、Web APIの概要とRailsでの使い方を一番分かりやすく説明する - Qiita](https://qiita.com/a0ki-github/items/d9ff56561504dd20a0f9)
[Google Cloud Vision API を使って「1時間（自称）」くらいで車のナンバーを隠してくれるサービスを作ってみた - Qiita](https://qiita.com/Toya-Onodera/items/de59a1f6f9029e3e91e4)

# 具体的なやり方

## 1．座標データを取得する
本題に入っていきますが、ここからは以下のようなレスポンスが取得できている状況を前提としてお話します。

```yaml
# 物体検出で「人」が1人検出された例

{
  "responses": [
    {
      "localizedObjectAnnotations": [
        {
          "mid": "/m/01g317",
          "name": "Person",
          "score": 0.79965854,
          "boundingPoly": {
            "normalizedVertices": [
              {
                "x": 0.10867997,
                "y": 0.18811147
              },
              {
                "x": 0.38708806,
                "y": 0.18811147
              },
              {
                "x": 0.38708806,
                "y": 0.98924774
              },
              {
                "x": 0.10867997,
                "y": 0.98924774
              }
            ]
          }
        }
      ]
    }
  ]
}

```

今回メインとなるのはレスポンスに含まれている座標データです。
座標データを簡単に説明すると、**リクエストで渡した画像の中の、どの範囲に物体が検出されたかの位置情報**です。

言葉だとわかりにくいので、[Vision APIのデモページ](https://cloud.google.com/vision/?hl=ja&utm_source=google&utm_medium=cpc&utm_campaign=japac-JP-all-ja-dr-bkws-all-all-trial-b-dr-1009882&utm_content=text-ad-none-none-DEV_c-CRE_540835448615-ADGP_Hybrid%20%7C%20BKWS%20-%20PHR%20%7C%20Txt%20~%20AI%20%26%20ML%20~%20Vision%20AI_Vision-API-en-KWID_43700065772974180-aud-970366092687%3Akwd-1228493113915&userloc_1009266-network_g&utm_term=KW_visionapi&gclid=Cj0KCQiAnaeNBhCUARIsABEee8URV6MMeglAoAeKV8uPi_Dpu27n75qjxj7Ab-W2aNprgYWqZT-M9l0aAq53EALw_wcB&gclsrc=aw.ds)を使って、実際に検出箇所に四角い枠が描画されたものを見てみましょう。

![6fba5b75c7337bb3d489e909401778c5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/641012/121fdde3-482a-1f7d-f5a8-67a97f6b8615.png)

※フリー素材の画像を使わせていただきました

今回の画像だと、「人」「衣類」の2つが検出されています。
座標データというのは、この画像に描画されている**四角形の各頂点の位置情報**です。

もう一度レスポンスを見てみると、`normalizedVertices`の配列の中に`x`と`y`の組み合わせが4つあります。
この`x`がx座標、つまり横軸での位置を表していて、`y`がy座標、つまり縦軸での位置を表しています。
そして組み合わせが4つあるので、上から順番にそれぞれ左上、右上、右下、左下の頂点座標となっています。

```yaml
{
  "responses": [
    {
      "localizedObjectAnnotations": [
        {
          "mid": "/m/01g317",
          "name": "Person",
          "score": 0.79965854,
          "boundingPoly": {
            "normalizedVertices": [
              # 左上
              {
                "x": 0.10867997,
                "y": 0.18811147
              },
              # 右上
              {
                "x": 0.38708806,
                "y": 0.18811147
              },
              # 右下
              {
                "x": 0.38708806,
                "y": 0.98924774
              },
              # 左下
              {
                "x": 0.10867997,
                "y": 0.98924774
              }
            ]
          }
        }
      ]
    }
  ]
}

```

つまり1つの四角形を描画するためには、左上のx座標、左上のy座標、というように8個の位置情報を取得すればよいわけです。
具体的には以下のコードで左上のx座標が取得できます。

```ruby
['responses'][0]['localizedObjectAnnotations'][0]["boundingPoly"]["normalizedVertices"][0]["x"]

#-> 0.10867997
```

こうして、四角形を描画するためには座標データが必要で、さらに取得方法もわかりました。
次は、この座標データを使って四角形を描画する方法を説明いたします。

※ここまで`x`や`y`のことを座標データであると説明してきましたが、それがそのまま座標を示しているわけではありません。
後ほど改めて説明いたしますが、実際に座標として変換するためには、`x`だったら画像の横幅と掛け算をして算出します。
そうすることで、〇〇pxというデータとして使用できるようになります。

### Canvasを使おう
座標のデータをもとに、四角形を描画する方法はいくつかあると思うのですが、今回は**Canvas**を使う方法を説明します。
CanvasであればJavaScriptだけで比較的カンタンに処理が記述できます。

それでは、私がサービスで使用してるコードを見てみましょう。
（実際にはRubyのタグや、繰り返し処理を使っているのですが、わかりやすいようにシンプルな形に書き換えました）

```html
<canvas id="canvas"></canvas>
```

```javascript
// 2．Canvasで画像を表示する
window.onload = function() {
  var img = new Image();
  var data = "";
  img.src = data;
  img.onload = function(){
    var canvas = document.querySelector("#canvas");
    var ctx = canvas.getContext("2d");

// 3．画像のサイズ問題を解決する
    var aspectRatio = img.naturalHeight / img.naturalWidth;
    canvas.width = 300;
    canvas.height = 300 * aspectRatio;
    ctx.drawImage(img, 0, 0, canvas.width, canvas.height);

// 4．四角形を描画する
    ctx.lineWidth = 3;
    ctx.strokeStyle = "rgb(190, 54, 75)";
    ctx.beginPath();
    ctx.moveTo(
      ['responses'][0]['localizedObjectAnnotations'][0]["boundingPoly"]["normalizedVertices"][0]["x"] * canvas.width,
      ['responses'][0]['localizedObjectAnnotations'][0]["boundingPoly"]["normalizedVertices"][0]["y"] * canvas.height);
    ctx.lineTo(
      ['responses'][0]['localizedObjectAnnotations'][0]["boundingPoly"]["normalizedVertices"][1]["x"] * canvas.width,
      ['responses'][0]['localizedObjectAnnotations'][0]["boundingPoly"]["normalizedVertices"][1]["y"] * canvas.height);
    ctx.lineTo(
      ['responses'][0]['localizedObjectAnnotations'][0]["boundingPoly"]["normalizedVertices"][2]["x"] * canvas.width,
      ['responses'][0]['localizedObjectAnnotations'][0]["boundingPoly"]["normalizedVertices"][2]["y"] * canvas.height);
    ctx.lineTo(
      ['responses'][0]['localizedObjectAnnotations'][0]["boundingPoly"]["normalizedVertices"][3]["x"] * canvas.width,
      ['responses'][0]['localizedObjectAnnotations'][0]["boundingPoly"]["normalizedVertices"][3]["y"] * canvas.height);
    ctx.closePath();
    ctx.stroke();
  };
};
```

いきなりこれを見てもわかりにくいと思うので、1つずつ分解して説明していきます。

## 2．Canvasで画像を表示する

```html
<canvas id="canvas"></canvas>
```

```javascript
window.onload = function() {
  var img = new Image();                            // img要素を作成
  var data = "画像のパス";                            // 四角形を描画したい画像のパスを指定
  img.src = data;
  img.onload = function(){                          // 画像に行う処理を記述する
    var canvas = document.querySelector("#canvas"); // 画像を表示するエリアを取得
    var ctx = canvas.getContext("2d");              // グラフィックを描くための2Dオブジェクトを生成
    // ・・・省略
  }
```

ここで行っていることは、HTMLでCanvasタグを記述し、JavaScriptでCanvas要素を指定して、処理を記述するための下準備です。
`ctx`は`context`の略で、四角形を描画するための透明な画像だと考えるとわかりやすいと思います。

ここで重要なのは、**JavaScriptで画像を読み込む**ということです。
JavaScriptで画像を読み込み、四角形が描画された画像と合体させて表示するところまでを行います。

## 3．画像のサイズ問題を解決する

```javascript
var aspectRatio = img.naturalHeight / img.naturalWidth; // 画像のアスペクト比を計算
canvas.width = 300;                                     // Canvasの横幅を指定
canvas.height = 300 * aspectRatio;                      // アスペクト比に合わせてCanvasの縦幅を指定
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);  // 範囲を指定して、イメージを描画
```

ここで重要なのは、**Vision APIにリクエストした画像をリサイズし、`ctx`内で描画する範囲を同じ大きさにすること**です。

私が作ったサービスでは、ユーザーがアップした画像に四角形を描画して表示します。
ですがユーザーがアップした画像サイズそのままだとデザインが崩れるので、横幅を300pxで固定しました。
じゃあ縦幅はどうやって指定するのかというと、ユーザーがアップした画像のアスペクト比（縦横の比率）を計算して、300pxに掛けて算出します。

そして`drawImage`で`ctx`内に描画するイメージの範囲を、リサイズ後の画像サイズと全く同じになるように指定します。
`drawImage`の説明は「[Canvasリファレンス](http://www.htmq.com/canvas/drawImage_s.shtml)」がわかりやすいので、こちらも見てみてください。

これでリサイズ後の画像と全く同じサイズの`ctx`に、四角形を描画する準備ができました。

## 4．四角形を描画する

```javascript
ctx.lineWidth = 3;                    // 線の太さを指定
ctx.strokeStyle = "rgb(190, 54, 75)"; // 線の色を指定
ctx.beginPath();                      // 四角形の座標を指定
ctx.moveTo(                             // 左上
  ['responses'][0]['localizedObjectAnnotations'][0]["boundingPoly"]["normalizedVertices"][0]["x"] * canvas.width,
  ['responses'][0]['localizedObjectAnnotations'][0]["boundingPoly"]["normalizedVertices"][0]["y"] * canvas.height);
ctx.lineTo(                             // 右上
  ['responses'][0]['localizedObjectAnnotations'][0]["boundingPoly"]["normalizedVertices"][1]["x"] * canvas.width,
  ['responses'][0]['localizedObjectAnnotations'][0]["boundingPoly"]["normalizedVertices"][1]["y"] * canvas.height);
ctx.lineTo(                             // 右下
  ['responses'][0]['localizedObjectAnnotations'][0]["boundingPoly"]["normalizedVertices"][2]["x"] * canvas.width,
  ['responses'][0]['localizedObjectAnnotations'][0]["boundingPoly"]["normalizedVertices"][2]["y"] * canvas.height);
ctx.lineTo(                             // 左下
  ['responses'][0]['localizedObjectAnnotations'][0]["boundingPoly"]["normalizedVertices"][3]["x"] * canvas.width,
  ['responses'][0]['localizedObjectAnnotations'][0]["boundingPoly"]["normalizedVertices"][3]["y"] * canvas.height);
ctx.closePath();                      // 座標指定を終了
ctx.stroke();                         // 指定された座標を直線でつなぎ描画する
```

ここで行っていることは、四角形の線の太さと色を指定、そして座標を指定して実際に四角形を描画する処理です。

ここで重要なのは、**座標の求め方**です。
Vision APIのレスポンスには`"x": 0.10867997`のようなデータ入っていますが、単位が`px`ではありません。
なので、この値をそのまま座標として使用しても、描画位置がズレてしまいます。

**レスポンスの数値を`px`に変換するためには、画像サイズと掛け算を行う必要があります。**
具体的には、`x`座標は画像の横幅と、`y`座標は画像の縦幅と掛け算を行います。

コードとしては次のものが当てはまります。

```ruby
['responses'][0]['localizedObjectAnnotations'][0]["boundingPoly"]["normalizedVertices"][0]["x"] * canvas.width

#-> 32.603991　(0.10867997 * 300の計算結果)
```

これで、無事に四角形を描画できるようになりましたので、実際に結果を見てみましょう。

![aaf9e72a4af0244a51893a499f2fbb3b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/641012/fd40de68-01c6-8b89-2200-4227862b6e5f.png)

Vision APIのデモページと四角形の位置もズレていませんし、色と太さがちゃんと指定したものになっています。

### 四角形を複数描画したいとき
検出した物体全てに四角形を描画したいときは、`ctx.beginPath`から`ctx.stroke`までの記述を追加します。
具体的には、レスポンスの`localizedObjectAnnotations`の配列を繰り返し処理する中に、`ctx.beginPath`から`ctx.stroke`までを記述すれば実現できます。

# さいごに
私が自分のサービスでVision APIを使ったときには、座標データを`px`単位にするには画像のサイズと掛け算をしないといけない、という情報が全然見つけられず発見するまでに1日かかってしまいました。

ですので、この情報で1人でも多くの方の時間を救えたら幸いです。

最後までご拝読いただき、ありがとうございます。