[JavaScript]canvasでバウンディングボックスの作成と描画

javascript初心者の勉強としてjavascript+python(flask)で[アノテーションツール](https://github.com/deepgreenAN/tracking_annotation)を作成しました．アノテーションツールとは深層学習などに用いる画像データのラベル付けを行うツールのことです．自分が作成したのはGUIフレームワークの代わりにブラウザを使ったような形でローカルを前提としたアプリケーションです．今回はjavascriptのcanvasを利用して，バウンディングボックスの作成と描画が以下のように行えるようにします．

<img src="https://dl.dropboxusercontent.com/s/z6p6l0usvcyxa2d/bounding_box.gif">


以下が，プログラム全体です．

```html
<!DOCTYPE html>
<html>
  <head>
    <title>canvas bbox tutorial</title>
    <style>
      #canvas {
        background: #666;
        width: 600px;
        height: 600px;
      }
    </style>
  </head>
  <body>
    <canvas id="canvas" width="600px" height="600px"></canvas>
    <script>
      class CanvasVox{
        constructor(canvas, ctx){
          this.canvas = canvas;
          this.ctx = ctx;
          this.upper_left_pos = {"x":0, "y":0};
          this.lowwer_right_pos = {"x":0, "y":0};
          this.is_search_upper_left = true;
          this.pixel_ratio_display_over_true_x = 1.0;
          this.pixel_ratio_display_over_true_y = 1.0;
          this.is_active = true;
          this.text = "1"
        }

        _onClick(e){
          var offsetX = e.offsetX; // =>要素左上からのx座標
          var offsetY = e.offsetY; // =>要素左上からのy座標

          if (this.is_search_upper_left) {
            this.upper_left_pos["x"] = Math.round(offsetX/this.pixel_ratio_display_over_true_x);
            this.upper_left_pos["y"] = Math.round(offsetY/this.pixel_ratio_display_over_true_y);
            this.is_search_upper_left = false;
          } else {
            this.lowwer_right_pos["x"] = Math.round(offsetX/this.pixel_ratio_display_over_true_x);
            this.lowwer_right_pos["y"] = Math.round(offsetY/this.pixel_ratio_display_over_true_y);
            this.is_search_upper_left = true;
          }
        }

        onClick(e){
          if (this.is_active) {
            this._onClick(e);
          }
        }

        clearCanvas(){
          this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
        }

        _draw(offsetX, offsetY){
          if (this.is_search_upper_left) {
            if (this.upper_left_pos["x"] != null && this.upper_left_pos["y"] != null && this.lowwer_right_pos["x"] != null && this.lowwer_right_pos["y"] != null) {  // 値が入って入るとき
              this.ctx.strokeStyle = "orange";
              this.ctx.strokeRect(
                Math.round(this.pixel_ratio_display_over_true_x*this.upper_left_pos["x"]),
                Math.round(this.pixel_ratio_display_over_true_y*this.upper_left_pos["y"]),
                Math.round(this.pixel_ratio_display_over_true_x*(this.lowwer_right_pos["x"]-this.upper_left_pos["x"])),
                Math.round(this.pixel_ratio_display_over_true_y*(this.lowwer_right_pos["y"]-this.upper_left_pos["y"]))
              );
              this.ctx.font = "30px monospace";
              this.ctx.fillStyle = "orange";
              this.ctx.fillText(
                this.text,
                Math.round(this.pixel_ratio_display_over_true_x*this.upper_left_pos["x"]),
                Math.round(this.pixel_ratio_display_over_true_y*this.upper_left_pos["y"])
                );
            }
          } else {
            if (offsetX != null && offsetY != null && this.upper_left_pos["x"] != null && this.upper_left_pos["y"] != null) {  // 値が入って入るとき
              this.ctx.strokeStyle = "blue";
              this.ctx.strokeRect(
                Math.round(this.pixel_ratio_display_over_true_x*this.upper_left_pos["x"]),
                Math.round(this.pixel_ratio_display_over_true_y*this.upper_left_pos["y"]),
                Math.round(offsetX-this.pixel_ratio_display_over_true_x*this.upper_left_pos["x"]),
                Math.round(offsetY-this.pixel_ratio_display_over_true_y*this.upper_left_pos["y"])
              );
              this.ctx.font = "30px monospace";
              this.ctx.fillStyle = "blue";
              this.ctx.fillText(
                this.text,
                Math.round(this.pixel_ratio_display_over_true_x*this.upper_left_pos["x"]),
                Math.round(this.pixel_ratio_display_over_true_y*this.upper_left_pos["y"])
                );
            }      
          }
        }

        draw(offsetX, offsetY) {
          if (this.is_active) {
            this._draw(offsetX, offsetY)
          }
        }

        activate() {
          this.is_active = true;
        }

        deactivate() {
          this.is_active = false;
        }
      }

      var canvas = document.getElementById("canvas");
      var ctx = canvas.getContext("2d")
      var canvas_box = new CanvasVox(canvas, ctx)

      onMove = (e) => {
        var offsetX = e.offsetX; // =>要素左上からのx座標
        var offsetY = e.offsetY; // =>要素左上からのy座標
        canvas_box.clearCanvas();
        canvas_box.draw(offsetX, offsetY);
      }

      canvas.onclick = (e) => {
        canvas_box.onClick(e);
        onMove(e);
      }
      canvas.onmousemove = onMove

    </script>
  </body>
</html>
```

まず，このプログラムのほとんどを占めるCanvasクラスについてみていきます．コンストラクタではcanvasオブジェクト，コンテキスト，矩形の左上点と右下点の位置，左上の探索を行っているかどうか，実際の画像サイズに対するcanvasサイズの比などをメンバ変数としています．実際の画像サイズに対するcanvasサイズの比は今回は変化しません．

```javascript
constructor(canvas, ctx){
  this.canvas = canvas;
  this.ctx = ctx;
  this.upper_left_pos = {"x":0, "y":0};
  this.lowwer_right_pos = {"x":0, "y":0};
  this.is_search_upper_left = true;
  this.pixel_ratio_display_over_true_x = 1.0;
  this.pixel_ratio_display_over_true_y = 1.0;
  this.is_active = true;
  this.text = "1"
}
```

canvas上で左クリックしたときに呼ばれるメソッドが`_onClick`です．左上探索時にクリックした場合に左上点を決定，右下探索時に右下点を決定します．決定した後に探索点を入れ替えます．

```javascript
_onClick(e){
  var offsetX = e.offsetX; // =>要素左上からのx座標
  var offsetY = e.offsetY; // =>要素左上からのy座標

  if (this.is_search_upper_left) {
    this.upper_left_pos["x"] = Math.round(offsetX/this.pixel_ratio_display_over_true_x);
    this.upper_left_pos["y"] = Math.round(offsetY/this.pixel_ratio_display_over_true_y);
    this.is_search_upper_left = false;
  } else {
    this.lowwer_right_pos["x"] = Math.round(offsetX/this.pixel_ratio_display_over_true_x);
    this.lowwer_right_pos["y"] = Math.round(offsetY/this.pixel_ratio_display_over_true_y);
    this.is_search_upper_left = true;
  }
}
```

実際に描画を行うのが`_draw`です．左上探索時つまり矩形が確定している場合に，確定した矩形とテキストをオレンジで描画します．一方右下探索時は引数のマウスの位置(`offsetX`, `offsetY`)を利用し，マウス位置を右下点とした青の矩形をテキストと共に描画します．また，一応nullか判定をしています．

```javascript
_draw(offsetX, offsetY){
  if (this.is_search_upper_left) {
    if (this.upper_left_pos["x"] != null && this.upper_left_pos["y"] != null && this.lowwer_right_pos["x"] != null && this.lowwer_right_pos["y"] != null) {  // 値が入って入るとき
      this.ctx.strokeStyle = "orange";
      this.ctx.strokeRect(
        Math.round(this.pixel_ratio_display_over_true_x*this.upper_left_pos["x"]),
        Math.round(this.pixel_ratio_display_over_true_y*this.upper_left_pos["y"]),
        Math.round(this.pixel_ratio_display_over_true_x*(this.lowwer_right_pos["x"]-this.upper_left_pos["x"])),
        Math.round(this.pixel_ratio_display_over_true_y*(this.lowwer_right_pos["y"]-this.upper_left_pos["y"]))
      );
      this.ctx.font = "30px monospace";
      this.ctx.fillStyle = "orange";
      this.ctx.fillText(
        this.text,
        Math.round(this.pixel_ratio_display_over_true_x*this.upper_left_pos["x"]),
        Math.round(this.pixel_ratio_display_over_true_y*this.upper_left_pos["y"])
        );
    }
  } else {
    if (offsetX != null && offsetY != null && this.upper_left_pos["x"] != null && this.upper_left_pos["y"] != null) {  // 値が入って入るとき
      this.ctx.strokeStyle = "blue";
      this.ctx.strokeRect(
        Math.round(this.pixel_ratio_display_over_true_x*this.upper_left_pos["x"]),
        Math.round(this.pixel_ratio_display_over_true_y*this.upper_left_pos["y"]),
        Math.round(offsetX-this.pixel_ratio_display_over_true_x*this.upper_left_pos["x"]),
        Math.round(offsetY-this.pixel_ratio_display_over_true_y*this.upper_left_pos["y"])
      );
      this.ctx.font = "30px monospace";
      this.ctx.fillStyle = "blue";
      this.ctx.fillText(
        this.text,
        Math.round(this.pixel_ratio_display_over_true_x*this.upper_left_pos["x"]),
        Math.round(this.pixel_ratio_display_over_true_y*this.upper_left_pos["y"])
        );
    }      
  }
}
```

以下がキャンバス要素の取得とイベントハンドラの登録です．`onMove`関数ではマウス位置の取得とキャンバスの初期化とバウンディングボックスの描画を行います．また，クリック時にも`onMove`関数が実行されるようにします．`onclick`,`onmousemove`プロパティに関数を代入してイベントハンドラを登録します．

```javascript
  var canvas = document.getElementById("canvas");
  var ctx = canvas.getContext("2d")
  var canvas_box = new CanvasVox(canvas, ctx)

  onMove = (e) => {
    var offsetX = e.offsetX; // =>要素左上からのx座標
    var offsetY = e.offsetY; // =>要素左上からのy座標
    canvas_box.clearCanvas();
    canvas_box.draw(offsetX, offsetY);
  }

  canvas.onclick = (e) => {
    canvas_box.onClick(e);
    onMove(e);
  }
  canvas.onmousemove = onMove
```

## まとめ
canvasを利用して矩形の作成と描画を行えるようにしました．次回はこれを応用して多角形の作成と描画をしてみます．