4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-07-02

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

以下が,プログラム全体です.

<!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サイズの比は今回は変化しません.

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です.左上探索時にクリックした場合に左上点を決定,右下探索時に右下点を決定します.決定した後に探索点を入れ替えます.

_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か判定をしています.

_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プロパティに関数を代入してイベントハンドラを登録します.

  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を利用して矩形の作成と描画を行えるようにしました.次回はこれを応用して多角形の作成と描画をしてみます.

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?