audiovisualcoding

これをやるのに必要なのはこれだけ、が欲しかったプログラミング初心者による覚え書き

【JavaScript】Webページ上で画像や映像をトリミングしたいとき

【実践したいこと】
何を:画像または映像を読み込み、その一部分だけを切り取る(トリミングする)まで (今回は、Webカメラから取得した映像(以下の記事を参照)を使う)
どこで:Webページ上

audiovisualcoding.hatenablog.com

【前提知識】
HTML属性とDOMプロパティ

【ソース】

▾index.html

<!DOCTYPE html>
<html>
    <body>
        <div id="canvasWrap"></div>
        <script src="main.js"></script>
    </body>
</html>

▾main.js

// 「Webページ上でWebカメラの映像を使いたいとき」のコードを使用 ==========
const video = document.createElement('video');
video.autoplay = true;
video.width = 720;
video.height = 480;

const media = navigator.mediaDevices;

const constraints = { audio: false, video: true };
media.getUserMedia(constraints)
.then(function(stream) {
    video.srcObject = stream;
});

// 本記事の内容ここから ===============================================
// 1-i Canvasオブジェクトを作成する
const canvas = document.createElement('canvas');
canvas.width = 360;
canvas.height = 240;
// 1-ii canvas要素にどういう手法で描画するかを決定する
ctx = canvas.getContext('2d');
// 1-iii canvas要素をHTMLに追加する
document.getElementById('canvasWrap').appendChild(canvas);

// 2-i 映像を切り取る関数を準備する
function trim() {
    ctx.drawImage(
        // image: 切り取る対象(元イメージ)
        video,
        // sx, sy: 元イメージをトリミングする起点の座標
        video.width / 2 - canvas.width / 2,
        video.height / 2 - canvas.height / 2, 
        // sw, sh: トリミングする大きさ
        canvas.width, canvas.height,
        // dx, dy: トリミングした画像を描画するキャンバス上の起点の座標 
        0, 0, 
        // dw, dh: 描画する大きさ
        canvas.width, canvas.height
    );
}

// 2-ii 映像の読み込みを待って、2-iの関数を実行する
video.addEventListener('play', function() {
    function loop() {
        trim();
        // 2-iii 映像が変わるのに従ってcanvas上の描画を更新する
        // 次の再描画で呼び出し
        requestAnimationFrame(loop);
    }
    // 初回呼び出し: アニメーション画面の更新を始める
    requestAnimationFrame(loop);
})

切り取った画像を描画するための領域を準備する

1-i Canvasオブジェクトを作成する

const canvas = document.createElement('canvas');

Canvasオブジェクトとは、HTMLの<canvas>要素に相当する、図形やアニメーションを描画するための四角い領域を表示するオブジェクトである。
描画は、JavaScriptを使ってピクセルを制御するという形で行われる。

あらかじめHTMLの文書内で<canvas>タグを入れておいてもいいが、今回は前記事と同じように、HTML要素を作成するdocument.createElement()メソッドを使って準備する。

Canvasオブジェクトが持つプロパティ

width, height (HTML属性)

canvas.width = 360;
canvas.height = 240;

描画領域(画像を切り抜きたい大きさ)の大きさを指定する。単位は px (ピクセル)。

1-ii canvas要素にどういう手法で描画するかを決定する

ctx = canvas.getContext('2d');

<canvas>に図形を描画する方法は「コンテキスト」と呼ばれ、大きく分けて二種類ある:

  • 2dコンテキスト … 2Dグラフィックを描画するのに向いている手法。「線を引く」、「色を塗りつぶす」など。

  • webglコンテキスト … 3Dグラフィックを描画するのに向いている手法。「立体的なポリゴンを扱う」など。

今回は、平面の画像の連続であるWebカメラからの映像を、別の平面の画像に出力するという目的なので、コンテキストは2dを選択する。*1

1-iii canvas要素をHTMLに追加する

<div id="canvasWrap"></div>

今回は、HTMLの方にあらかじめcanvasWrapというIDを持った<div>を準備しておいてある。

document.getElementById('canvasWrap').appendChild(canvas);

getElementById()で上記の<div>を探し出し、appendChild()によってその下の階層に<canvas>を追加する。

映像をトリミングして、キャンバスに描画する

2-i 映像を切り取る関数を準備する

function trim() {
    ctx.drawImage(
        // image: 切り取る対象(元イメージ)
        video,
        // sx, sy: 元イメージをトリミングする起点の座標
        video.width / 2 - canvas.width / 2,
        video.height / 2 - canvas.height / 2, 
        // sw, sh: トリミングする大きさ
        canvas.width, canvas.height,
        // dx, dy: トリミングした画像を描画するキャンバス上の起点の座標 
        0, 0, 
        // dw, dh: 描画する大きさ
        canvas.width, canvas.height
    );
}

2dコンテキストが持っている道具(メソッド)のひとつに、drawImage()がある。

メソッド drawImage()

すでに読み込んである画像を受け取って、canvas上に描画する。
引数の数に応じて、原寸で描画したり(引数3つ)、大きさを変えて描画したり(引数5つ)できる。

今回は9つの引数を使って好きな部分だけ切り取る(トリミングする)のに利用する。
これについては、以下の記事が非常にわかりやすくまとめてくれている:

honttoni.blog74.fc2.com

第一引数 (image)
切り取る対象である元の画像のオブジェクトがここに入る。
すでに読み込んである画像である必要がある。
今回は、videoに流れている映像(画像の連続)が対象なので、これが入る。

第二・第三引数 (sx, sy)
元の画像のどこから切り取るか、座標で指定する。 [単位 px (ピクセル)]
座標は、画像の左上が原点(0, 0)、横(左→右)がx座標、縦(上→下)がy座標となっている。

第四・第五引数 (sw, sh)
元の画像をどのくらいの大きさで切り取るか指定する。 [単位 px (ピクセル)]
今回は<canvas>の大きさに合わせるので、それぞれのwidthとheightを入れている。

第六・第七引数 (dx, dy)
切り取った画像を、<canvas>のどこから描画するか、座標で指定する。 [単位 px (ピクセル)]
座標は、sx, syを決定したときと同じルールになっている。
<canvas>の左上端から表示させたいので、原点(0, 0)を指定する。

第八、第九引数 (dw, dh)
切り取った画像を、どのくらいの大きさで描画するかを指定する。 [単位 px (ピクセル)]
特に拡大・縮小をしないので、ここでも<canvas>のwidthとheightを入れる。

2-ii 映像の読み込みを待って、2-iの関数を実行する

video.addEventListener('play', function() {
    function loop() {
        trim(); ...

先に述べたように、drawImage()を実行するには、切り取り元の画像が用意されている(読み込まれている)状態である必要がある。
そのため、映像の読み込みを待ってから、2-iの関数を呼び出したい。

そこで、addEventListener()を使い、<video>に渡されている映像の再生が始まったら、2-iの関数を呼び出すようにする。
それならaddEventListener('play', trim);と書けば良いように思われるが、それでは最初に読み込んだ画像だけが描画されたままになり、動画にはならない。

2-iii 映像が変わるのに従ってcanvas上の描画を更新する

    ...
    function loop() {
        trim();
        // 次の再描画で呼び出し
        requestAnimationFrame(loop);
    }
    // 初回呼び出し: アニメーション画面の更新を始める
    requestAnimationFrame(loop);
})

メソッド requestAnimationFrame()

ブラウザにアニメーションを行うことを知らせ、画面の描画を更新する。
描画の更新頻度はブラウザーの負荷に合わせて60fps前後で変動する(タブがアクティブの場合)。

今回はloopという名前の関数*2を準備し、これ自身をループさせるようにした。これによって、映像が進むたびにtrim()が繰り返し実行され、<canvas>上の画像が映像に従って変わるようになる。

<video>の読み込みが始まったら実行させられるように、初回呼び出し用のrequestAnimationFrame()を記述しておくことも忘れない。

以上で、Webカメラからの映像の中心部分のみを切り取ったものを表示することができるようになった。

参考文献

developer.mozilla.org

memopad.bitter.jp

qiita.com

teratail.com

honttoni.blog74.fc2.com

memopad.bitter.jp

stackoverflow.com

developer.mozilla.org

leap-in.com

yomotsu.net

*1:ちなみに3Dグラフィックを扱いたいなら、webglコンテキストを使うより、ずっと易しいThree.jsというライブラリを使うべしという意見をよく見る

*2:特に決まった、伝統的な関数名があるわけではないらしい。他にもstepやanimateなど色々な名前がつけられるのが観測される