12_2:ビデオの操作 Creative Coding p5.js

アリソンパリッシュによる

p5.jsフレームワークは、スケッチでのビデオ再生制御を容易にします。

ビデオとWebブラウザ

コンピュータのビデオと言うと、通常はビデオファイル、つまり動画のエンコーディングされたデータを含むファイルに関する話です。ビデオの”エンコーディング”にはさまざまな形式があり、”コンテナ”にはさらに多くの形式があります。

ビデオの形式とコンテナは非常に多いので、これを正しく識別するソフトウェアの作成は大変な仕事です。特に、ビデオファイルを実際の画面上のイメージに効率よく変換する(“デコーディング”と呼ばれる処理)ソフトウェアの記述は、相当難しい作業なので、p5.jsは独自のビデオ再生コードを提供せずに、Webブラウザに組み込まれているビデオデコーディング機能に”便乗”します。

前のチュートリアルでは、p5.domライブラリの関数を使って、スライダーやボタン、入力領域などのコントロールを、p5.jsスケッチを含むページに追加しました。スケッチに簡単なビデオ再生を追加する過程はこれとよく似ています。p5.domライブラリのcreateVideo()関数を使って、ページにビデオ”コントロール”を追加するだけです。次のスケッチはその例です。

let vid;

function setup() {
    createCanvas(0, 0);
    vid = createVideo("iwaswrong.mp4");
    vid.elt.muted = true;
    vid.loop();
}

このサンプルを動作させるには、いくつかの作業が必要になります。まず再生するビデオファイルが必要です。ここで使用しているのは、サザーランドプロダクションの古典的な資本主義プロパガンダ映画「Destination Earth」からの短いクリップ動画です。また、p5.domライブラリのコピーも必要です(スケッチをp5アプリで作成している場合には、デフォルトで含まれていますが、通常のHTML/JSファイルで作成する場合には読み込む必要があります)。

最初に気づかれるのは、createCanvas()命令を0, 0のサイズで呼び出していることでしょう。これは、今見ているビデオはスケッチ内で再生されていないからです。ビデオは、スケッチの横にあるHTML <video>要素で再生されています。実を言うと、このスケッチコードは、ビデオタグ(createVideo()を使って)を作成し、ビデオタグの.loop()メソッドを呼び出しているだけです(.loop()メソッドはビデオを無限にループ再生します)。以降では、ビデオオブジェクトのそのほかのメソッドを見ていきます。


メモ

ビデオをファイルの情報は、Windows 10の場合、ビデオファイルの右クリック → コンテキストメニューの[プロパティ]で表示されるダイアログボックスの[詳細]タブで確認できます。

ビデオの再生は本来、ユーザーが自分の意志で開始するものなので、自動再生は避けるべきです。最近のChromeではこれが強化され、音声をオフにしないと自動再生が開始されません。
音声付き動画の自動再生がデフォルトで無効になる

上記コードのvid.elt.muted = trueは、これを回避するためのものです。eltはp5.Elementの基底にある元のHTML要素を参照するプロパティで、今の場合は<video>要素です。そのmutedプロパティをtrueに設定すると、Chromeのポリシーに準じることになります。

スケッチのコードで何が作成されているかは、ブラウザのデベロッパーツールの[Element]タブで確認できます。p5.jsのcreateCanvas(0, 0)によって作成されたキャンバスは下図の上の赤枠です。widthもheightも0なので、事実上ないのと同じです。その下の赤枠がp5.dom.jsのcreateVideo()が作成した<video>要素です。loop属性が”true”になっているのは、スケッチのvid.loop()によるものです。


形式に関する注意

上で述べたように、ビデオ形式の現状とそれに対するWebブラウザのサポートは混乱状態にあります。このチュートリアルでは、ほとんどのブラウザで幅広くサポートされている(場合によっては渋々)という理由から、.mp4ファイルを使用しています。Mozillaサイトには、どのブラウザがどの形式をサポートしているかに関する優れた概要が掲載されています。互換性を可能な限り保つために、p5.jsは、あるファイル形式が動作しなかった場合のために、複数のファイル形式が指定できるcreateVideo()命令の代替形式をサポートしています。

vid = createVideo(['vid.mp4', 'vid.webm']);

この場合createVideo()関数は、まずvid.mp4のロードを試行し、ブラウザがMP4形式をサポートしない場合に、vid.webmのロードを試行します。

スケッチに自分のビデオを含めたい場合には、MP4として保存し、可能ならWebM形式でも保存するのがよいでしょう。この記事は少し古いものの、HTML5用ビデオメディアの作成と、よくある問題の処理に関する優れた経験則を与えてくれます。さらに本格的に取り組むのであれば、ffmpegについて学ぶことになるでしょう。ffmpegはビデオを異なる形式に変換する万能ツールです。

ビデオオブジェクトの制御

ビデオ要素では、ただページに埋め込むだけでなく、もっと多くのことが行えます。次のサンプルでは、再生中のビデオの情報を取得するスケッチの作成方法と、マウスクリックによるビデオの一時停止と一時停止解除の方法を見ていきます。

let vid;
// ビデオの長さ(全再生時間、ランニングタイム)
let duration;
// 現在再生中かどうかを追跡する
let playing = false;

function setup() {
  createCanvas(400, 100);
  // ビデオファイルを読み込み、終了したらviodeLoadedコールバック関数を呼び出す
  vid = createVideo("iwaswrong.mp4", videoLoaded);

}

// ビデオファイルの読み込みが終わったら呼び出される
function videoLoaded() {
  // ビデオの縦横サイズを設定
  vid.size(400, 300);
  // ビデオを長さを取得
  duration = vid.duration();
}

function draw() {
  background(50);
  // ビデオの現在の再生位置を、幅を100%としたときの割合で示す

  // 現在の再生位置(秒数) / ビデオの長さ(秒数) = 進捗度
  const completion = vid.time() / duration;
  ellipse(completion * width, 50, 20, 20);
}

function mousePressed() {
  // 再生中でないなら
  if (!playing) {
    // ビデオを再生
    vid.play();
    // マウスプレスされたX位置に相当する位置にシーク
    vid.time((mouseX / width) * duration);
    playing = true;
  }
  // 再生中なら
  else {
    // 一時停止
    vid.pause();
    playing = false;
  }
}

このサンプルでは、新しい事柄がいくつも登場しているので、順番に説明しましょう。

  • ビデオオブジェクトの.size()メソッドは、ビデオオブジェクトの縦横サイズを制御する。ここでは少し横長にしている(元のサイズは320 : 240なので、4:3を維持したサイズに設定している)
  • .time()メソッドはビデオの経過秒数を返し、.duration()メソッドはビデオの合計秒数を返す。経過時間を合計秒数で割ると、完了パーセント(全体の何%まで再生が進んでいるか)が得られる(0から1までの数値で、0が開始、1が終了)
  • ビデオオブジェクトの.play()メソッドはビデオを再生し、.pause()は再生を一時停止する。ビデオオブジェクトには、現在ビデオが再生中かどうかを教えてくれるメソッドがないので、自分で追跡する必要がある。このスケッチでは、playingという名前のブーリアン型変数で、それを行っている。
  • .time()メソッドにパラメータを渡して呼び出すと、ビデオの与えられた位置(秒単位)にシーク(目的の場所を探してそこに移動)する。このスケッチでは、ビデオの長さに、マウス位置とスケッチの幅の割合を掛けている。これにより、ビデオの再生位置を、マウスのX位置に比例させる効果が生まれる。

メモ

draw()とmousePressed()内には「比」で考える必要のあるコードが含まれています。こうした比の問題を一気に解決してくれる便利な関数はJavaScriptにもp5.jsにもなく、比例式を使って解くことになります。

const completion = vid.time() / duration;
ellipse(completion * width, 50, 20, 20);

この2行は、ビデオの今の進捗度(全体を100%としたとき今何%まで再生が進んでいるか)を、スケッチの幅(100px)を100%としたときの円の位置で示します。これは、次の比例式で表すことができます。

time() : duration = X : width
(time()とdurationの比は、X(求めたい値)とスケッチの幅の比に等しい)

比例式の“内項の積=外項の積”を使うと、Xは次の計算で求まります。

duration * X = time() * width
X = time() * width / duration

mousePressed()内の次のコードも、同様です。

vid.time((mouseX / width) * duration);

X : duration = mouseX : width
X * width = duration * mouseX
X = duration * mouseX / width

またビデオの再生は、下図にように、ビデオ全体の長さ(duration、秒数)の線上を(今はすたれてしまったビデオデッキの)再生ヘッドが右に進むと考えると分かりやすいです。time()は今の再生ヘッドの位置を秒数で返し、time()に秒数を指定すると、再生ヘッドがその位置に移動(シーク)します。


次のスケッチは2秒おきに、ビデオのランダムな位置にシークし、ランダムにボリュームを調節します。

let vid;
let playing = false;
let duration;
function setup() {
  createCanvas(400, 100);
  vid = createVideo("iwaswrong.mp4", videoLoaded);
}

function videoLoaded() {
  vid.size(400, 300);
  duration = vid.duration();
  // vid.volume()は1
  background(vid.volume() * 255);
}

function mousePressed() {
  if (!playing) {
    vid.play();
    playing = true;
  } else {
    vid.pause();
    playing = false;
  }
}

function draw() {
  if (playing) {
    // 現在の音量(0から1の間)に255を掛けた数値を背景色(グレーの濃淡)にする
    background(vid.volume() * 255);
    // スタートしてからの再生フレーム数が120で割れたら
    if (frameCount % 120 == 0) {
      // ビデオの長さ内のランダムな位置にシーク
      vid.time(random() * duration - 2);
      // 音量をランダムに設定
      vid.volume(random());
    }
    ellipse((vid.time() / duration) * width,
      50, 20, 20);
  }
}

ビデオオブジェクトの.volume()メソッドはビデオのボリュームを設定します(0が無音で、1がフルボリューム)。0から1の間の数値のパラメータをつけて呼び出します。パラメータをつけないと現在のボリューム値を返します。

p5.MediaElementのリファレンスには、ビデオオブジェクトで呼び出すことができるメソッドの全リストを読むことができます。

ビデオのピクセルデータの操作

ビデオの再生はスマートフォンでもできるので、面白くありません。そこで、もっと興味が湧きそうな、ビデオからピクセルデータを得る方法を見ていきます。draw()関数では、ビデオの.loadPixels()メソッドが呼び出せます。このメソッドを使用すると、ビデオの.pixels属性が使用できるようになり、そのビデオの現在のフレーム内の特定の位置のカラーにアクセスできるようになります。次のスケッチはその例です。


注意

本記事の翻訳時現在、p5.dom.jsのバージョン0.7.2と0.7.3はバグがあるようで、次のサンプルを再生するには、0.7.1を読み込む必要があります。

<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.1/addons/p5.dom.min.js"></script>

let vid;
let playing = false;
const step = 8;

function setup() {
    createCanvas(320, 240);
    vid = createVideo("iwaswrong.mp4", videoLoaded);
    createP('画面のクリックで再生/一時停止');
}

function videoLoaded() {
    vid.loop();
    vid.size(320, 240);
    // ビデオは隠す
    //vid.hide();

    // うるさいので音は消す
    vid.elt.muted = true;
    noStroke();
}

function mousePressed() {
    if (!playing) {
        vid.play();
        playing = true;
        // 確認用
        //vid.loadPixels();
        //console.log(vid.pixels)
    }
    else {
        vid.pause();
        playing = false;
    }
}

function draw() {
    if (playing) {
        background(0);
        // この時点でのビデオのピクセルを読み込む
        vid.loadPixels();

        // ビデオとキャンバスのサイズは同じ(320 x 240)
        // キャンバスの幅と高さ分を繰り返し処理し、
        // オフセットのカラーでモザイクを描く
        for (let y = 0; y < height; y += step) {
            for (let x = 0; x < width; x += step) {
                const offset = ((y * width) + x) * 4;
                fill(vid.pixels[offset],
                    vid.pixels[offset + 1],
                    vid.pixels[offset + 2]);
                rect(x, y, step, step);
            }
        }
    }
}

このサンプルはスーパートリッキーです。それは、ほとんどが扱いが面倒な.pixelsに起因します。.pixelsとは何なのでしょう? ((y * width) + x) * 4 は一体何を意味しているのでしょう? .pixelsは最高のパフォーマンスを発揮するよう設計された配列で、一次元です(一次元の配列は、ほかの配列を要素に含む配列よりも高速でメモリ効率も優れています)。

以下はその仕組みです。.pixelsのピクセルデータは、各ピクセルの赤、緑、青、アルファの値が個々の項目として保持されるように並んでいます([r0,g0,b0,a0,r1,g1,b1,a1,…])。最初の4つの項目(r0,g0,b0,a0)は、ビデオの0, 0の位置にあるピクセルのRGBA値で、2つめの4つの項目のセット(r1,g1,b1,a1)は、1,0の位置にあるピクセルのRGBA値で、その隣の4つは2.0の位置にあるピクセルのRGBA値です。画面のピクセルの行が終わると、ピクセルデータは0,1から再スタートします。下図はその概念を示したものです。

上記サンプルでは、2つのループが0から幅と高さ(未満)までを繰り返し処理します。式((y * width) + x) * 4は、位置 x,y のピクセルのカラーに対応する4つの値のオフセットです。その後は塗り色の設定で、赤の値はvid.pixels[offset]を評価した値に、緑の値はvid.pixels[offset + 1]を評価した値に、青の値はvid.pixels[offset + 2]を評価した値に設定しています。また、こうした数学計算を単純にするために、スケッチのサイズをビデオのサイズに合わせている点にも注目してください。


メモ

まず、loadPixels()は2種類あります。ここで使っているのはp5.ImageのloadPixels()メソッドで、そのイメージ(今の場合はビデオ)のピクセルデータを、そのイメージ(ビデオ)のpixels属性に読み込みます。もう1つはloadPixels()関数で、表示ウィンドウ(キャンバス)のピクセルデータをpixels配列に読み込みます。両方とも働きはよく似ていますが、ここではビデオのピクセルを読み取りたいので、ビデオからloadPixels()メソッドを呼び出す必要があります。

loadPixels()メソッドの呼び出しによって、その時点でのビデオのフレームのピクセルがビデオオブジェクトのpixelsに読み込まれます。このpixelsは次のような配列です。

Uint8ClampedArray(307200)という表示は、この配列がUint8ClampedArrayというタイプの配列で、要素を307,200個持っている、ということを表しています。Uint8ClampedArrayはJavaScriptで型付き配列と呼ばれる配列で、0から255の範囲に収めた整数を要素に持ちます。

使用しているビデオのサイズは320 x 420なので、320 x 420 = 76,800個のピクセルがあります。その各ピクセルはRGBA値を1つずつ持っています。たとえば上図では1つめのピクセルは0,0,0,255、2つめのピクセルは12,22,28,255です。1つのピクセルが4つの値を持っているので、全体としては76,800 x 4 = 307,200個の数値になります。これが Uint8ClampedArray(307200)に表示されていたpixels配列の要素数です。pixels配列には、上図のように、0や12や255といった整数が,で区切られてずらっと並んでいます。

とはいえこの数値は漫然と並んでいるわけではなく、ビデオの左上隅のピクセルを開始点とするピクセルデータに対応しています。具体的に言うと、pixels配列の最初の4つ(r0,g0,b0,a0)がビデオの左上隅のピクセル値を表し、その次の4つ(r1,g1,b1,a1)がビデオの左上隅のピクセルから1ピクセル右のピクセル値を表します。このようにpixels配列の4つの数値のセットが、ビデオの左上隅から横方向へ1ピクセルずつ進んだピクセルの値を表します。

そしてピクセル表現(r319,g319,b319,a319)がビデオの右上隅まで到達すると、その次の4つの数値(r320,g320,b320,a320)は、ちょうどそこで”改行”するかのように、ビデオの左上隅から下に1ピクセル進んだピクセルの値を表します。その後はまた同様に、次の4つの数値が1ピクセル右のピクセルの値を表します。

pixels配列の中の数値は,で区切られて並んでいるだけですが、上記の”改行”によって、行と列の考えを当てはめることができます。行はビデオの幅分のピクセルを表す、今の場合なら320 x 4個の並びです。数値は4個で1セットなので、320列と見なすことができます。320 x 240のビデオには、この行が240個あります。

const offset = ((y * width) + x) * 4; について

(y * width) + x は1次元配列を、2次元配列(行列)のように扱いたいときに使用される式です(掛け算は足し算より優先されるので、このかっこは必要ありませんが、かっこがある方が読みやすくなります)。この式は、非常に長い一次元の配列pixelsを「右に、行数分進んで残りの列数分進んだ要素を参照」します。

例として、aからtのアルファベット文字20個を要素として持つ一次元配列のarrayATがあり、これを5行4列の行列のように扱いたい場合を見ていきます。

const arrayAT = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
  'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't'];
/* 5 x 4 の行列と見なしたい
  a b c d e
  f g h i j
  k l m n o
  p q r s t

行列のように扱いたいというのは、(0, 0)でaを、(3, 2)でnを得るというように、行列の行番号と列番号を使って、要素にアクセスしたいということです。 width * y + x はこのとき使用できます。

const width = 5;

function getArrayElementFromXY(x, y) {
    return width * y + x; // 右に、行数分進んで、残りの列数分進む
}

// (0,0)の要素の値を得る
console.log(arrayAT[getArrayElementFromXY(0, 0)]); // a

// (3,2)の要素の値を得る
console.log(arrayAT[getArrayElementFromXY(3, 2)]) // n

((y * width) + x) に4を掛けているのは、pixels配列の数値は4つで1セットなので、セットの先頭のr値に飛ぶ必要があるからです。次のコードはそのテストです。

const arrayA = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'];
const arrayB = ['r', 'g', 'b', 'a'];

const array4_3 = [];

for (let i = 0; i < arrayA.length; i++) {
    for (let j = 0; j < arrayB.length; j++) {
        array4_3.push(arrayB[j] + arrayA[i])
    }
}

// 4行3列の一次元配列と見なす
console.log(array4_3);

const width = 4;
const height = 3
const step = 1;

for (let y = 0; y < height; y += step) {
    for (let x = 0; x < width; x += step) {
        const offset = (width * y + x) * 4;
        console.log('offsetR :' + array4_3[offset]); // r0 -> r11
        //console.log('offsetG :' + array4_3[offset + 1]);    // g0 -> g11
        // console.log('offsetB :' + array4_3[offset + 2]);    // b0 -> b11
        //console.log('offsetA :' + array4_3[offset + 3]);      // a0 -> a11
    }
}

次も同様のサンプルですが、ここではXとY方向に別々の”ステップ”を使用し、ピクセルの緑の値を使って矩形のサイズを制御しています。

let vid;
let playing = false;

function setup() {
    createCanvas(320, 240);
    vid = createVideo("iwaswrong.mp4", videoLoaded);
    createP('画面のクリックで再生/一時停止');
}

function videoLoaded() {
    vid.loop();
    vid.size(320, 240);
    // ビデオは隠す
    //vid.hide();
    // うるさいので音は消す
    vid.elt.muted = true;
    noStroke();
    rectMode(CENTER);
}

function mousePressed() {
    if (!playing) {
        vid.play();
        playing = true;
    }
    else {
        vid.pause();
        playing = false;
    }
}

function draw() {
    if (playing) {
        background(50);
        fill(255);
        // この時点でのビデオのピクセルを読み込む
        vid.loadPixels();
        for (let y = 0; y < height; y += 10) {
            for (let x = 0; x < width; x += 5) {
                const offset = ((y * width) + x) * 4;
                rect(x, y, 10, 10 * (vid.pixels[offset + 1] / 255));
            }
        }
    }
}

Webカメラのビデオのキャプチャ(捕捉)

HTML5のおかげで、ユーザーのWebカメラからのビデオを簡単に捕捉し、スケッチに含めることができます。次はそのシンプルな例です。

let cap;

function setup() {
    createCanvas(400, 400);
    cap = createCapture(VIDEO);
    cap.hide();
    imageMode(CENTER);
}

function draw() {
    background(50);
    image(cap, mouseX, mouseY, 160, 120);
}

このスケッチを実行すると、サーバーからカメラの使用を許可するよう求められます。ここで許可しないと、このサンプルは動作しません。

createCapture(VIDEO)関数は、”キャプチャ”オブジェクトを返します。その動作はビデオオブジェクトとよく似ています。これをimage()関数内で使用すると、キャプチャしたビデオのイメージを画面に描画することができます。

キャプチャオブジェクトはまた.loadPixels()メソッドもサポートしています。.loadPixels()を利用すると、次の摩訶不思議なビデオの鏡が作成できます。

let cap;

function setup() {
    createCanvas(400, 400);
    cap = createCapture(VIDEO);
    cap.hide();
    rectMode(CENTER);
    noStroke();
}

function draw() {
    background(50);
    fill(255);
    cap.loadPixels();
    for (var cy = 0; cy < cap.height; cy += 10) {
        for (var cx = 0; cx < cap.width; cx += 5) {
            var offset = ((cy * cap.width) + cx) * 4;
            var xpos = (cx / cap.width) * width;
            var ypos = (cy / cap.height) * height;
            rect(xpos, ypos, 10,
                10 * (cap.pixels[offset + 1] / 255));
        }
    }
}

この例では、ビデオキャプチャのサイズがスケッチのサイズより大きいので、キャプチャからピクセルデータを得てそのデータを画面の正しい位置に描画するには、結構な計算が必要になります。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA