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