作ろう!自作ビデオエフェクト

数年前にビデオエフェクトと言えば、専用のビデオエフェクタくらいしか 手に入りませんでした。徐々にノンリニア編集ができるようになり、 エフェクタ内蔵の編集ボードも出てきましたが、既に用意されている エフェクトを選んで使うことしかできず、「自分でエフェクトを作る」 ことは不可能でした。

従来ビデオエフェクトは処理する量が非常に多く、専用のハードウェア を必要としました。PCのCPUで処理するなんて無理な話でした。ところが、 ムーアの法則に従いCPUの処理能力が向上し、「C言語で書けば」実時間で かなり複雑なエフェクトも可能になってきています。 さらに、EffecTVのような自分でエフェクトを作れるソフトまで出てきました。

残念ながら現在のEffecTVには、ドキュメントが満足に用意されていない ため、プログラミングに慣れていない人が自分でエフェクトを作るのは かなり厳しい状況です。そこでそのような人向けの説明を書こうと思います。

EffecTVの動作環境

EffecTVは、福地健太郎さんが中心になって開発しているビデオエフェクトソフトです。 Linux上で動作し、オープンソースとして開発されています。 したがって、ソースを読むことができ、自由に改造できます。 ただし、GPL2.0のライセンスをとっていますので、改造した場合は ソースを公開して欲しいと要望があった場合には断われません。

なお、EffecTVでビデオエフェクトを楽しむには、Video on Linuxに 対応したビデオキャプチャー機器が必要です。ビデオキャプチャーカード やUSB接続やIEEE1394接続のWebカメラが相当します。USBのWebカメラの 一部が対応していますが、キャプチャ速度が遅いのでお勧めできません。 お勧めは、Bt848/878というチップを使ったキャプチャーカードに ビデオカメラをつなぐ方法です。Bt848/878のカードは、最近ほとんど 見かけなくなりました。今は玄人指向のBT878A-STVPCI2が唯一です。 秋葉原で5000円前後で買えます。これなら、最近のLinuxであれば、 挿すだけで即認識です!

EffecTVのインストールと実行

EffecTVの日本語のページは、こちらです。 ここのダウンロードのページからソースをダウンロードします。 tar xzvf effectv-.tar.gz などとして展開します。effectv-といったディレクトリ ができます。そのディレクトリの中で、make コマンドを打てば、コンパイルされます。ただし、それ以前に必要なソフトが ありますので、EffecTVのページを読んで確認&インストールしてください。 成功すればそのディレクトリの中に effectv という実行ファイル ができます。./effectvなどとして実行してみてください。

※ 最終的には make install で正式にインストールします。

玄人指向のBt878カードを使った場合、オプションなしだと チューナーの映像が映ります。./effectv -channel 1 とすると、コンポジット(RCAコネクタ)入力が映ります。 ./effectv -channel 2なら、S入力が映ります。

また、複数のビデオキャプチャー機器を接続している場合は、順番に /dev/video0,/dev/video1,/dev/video2 ... といった具合にデバイス名が付きます。 例えば video1 を映したい時は、./effectv -device /dev/video1とします。

映すウインドウの大きさを変えたい時は、-size オプションを使います。 例えば、640×480にしたい時は、./effectv -size 640x480 とします。

カーソルキーの上下で、エフェクトの種類を切り替えられます。 既に数十種類が用意されていますので、切り替えてみて遊んでみましょう。

自分のエフェクトを作ってみる

まずは非常に簡単なエフェクトを作りましょう。

エフェクトは effects ディレクトリの中でCプログラムで書かれています。 その中に dumb.c というプログラムがあります。これは、入力した映像を そのまま表示するものです。まずは、これをいじってみましょう。

dumb.c の一番下にある、draw()という関数で、入力した映像を 処理しています。ここで行なっている処理は、入力映像(ビデオバッファ) の内容を処理して、出力映像(スクリーンバッファ)を作るということを しています。その後自動的にスクリーンバッファの内容を画面に表示 してくれます。

以上の実際の処理は、

		memcpy(dest, src, video_area * sizeof(RGB32));
という一行だけです。これはメモリのある領域を一度にコピーしている のですが、これでは分かりにくいので、1画素ずつコピーしてみます。 上に示した行を以下に置きかえます。
	int x, y;
	for (y = 0; y < video_height; y++) {
		for (x = 0; x < video_width; x++) {
			*(dest+x+y*video_width) = *(src+x+y*video_width);
		}
	}
二重ループでxとyを1つずつ増やしていきます。 メモリ上で1画素は32bit、つまり4byteを取ります。それが横に順番に 並びます。一行の右端までいくと、その後に下の行が続きます。 したがって、(x,y)の場所をポインタで示すと、上のように *(src+x+y*video_width)となるわけです。 なお、video_widthは横の画素数、video_heightは縦の画素数です。

ちなみにCのポインタに慣れていれば、上のプログラムは例えば以下のように書くでしょう。

	int x, y;
	for (y = 0; y < video_height; y++) {
		for (x = 0; x < video_width; x++) {
			*dest = *src;
			dest++;
			src++;
		}
	}

以上を理解すると以下のようなプログラムが書けます。

/* 上下を反転する */
	int x, y;
	for (y = 0; y < video_height; y++) {
		for (x = 0; x < video_width; x++) {
			*(dest+x+y*video_width)
			 = *(src+x+(video_height-1-y)*video_width);
		}
	}
/* 左右を反転する */
	int x, y;
	for (y = 0; y < video_height; y++) {
		for (x = 0; x < video_width; x++) {
			*(dest+x+y*video_width)
			 = *(src+(video_width-1-x)+y*video_width);
		}
	}
/* 横を2倍に広げる */
	int x, y;
	for (y = 0; y < video_height; y++) {
		for (x = 0; x < video_width; x++) {
			*(dest+x+y*video_width)
			 = *(src+(x/2)+y*video_width);
		}
	}
/* 横を1/2に縮める */
	int x, y;
	for (y = 0; y < video_height; y++) {
		for (x = 0; x < video_width; x++) {
			*(dest+x+y*video_width)
			 = *(src+x*2+y*video_width);
		}
	}
最後の1/2に縮めるプログラムは、かなり荒っぽいことをしています。 画面の右半分は、yよりも一行下の内容を描画しています。 少しまともにするには、x*2を (x*2)%video_width に変えると良いでしょう。

色を変えてみる

次に画素のデータがどうなっているかを見ます。

1つの画素は32bit(RGB32型)で表わされますが、これは8bitごとに意味を持ちます。 最初の8bitは使われていません。未使用です。次の8bitは赤(red)の値、 続いての8bitが緑(green)の値、最後の8bitが青(blue)の値です。 いわゆるRGB値です。それぞれ8bitですので、RGBごとに0〜255の範囲で 値を取ります。

したがって、RGBの各値を取り出すには以下のようにします。

	int x, y;
	RGB32 p, r, g, b;
	for (y = 0; y < video_height; y++) {
		for (x = 0; x < video_width; x++) {
			p = *(src+x+y*video_width);
			r = (p & 0x00ff0000) >> 16;
			g = (p & 0x0000ff00) >> 8;
			b = (p & 0x000000ff);
                        *(dest+x+y*video_width) = r << 16 | g << 8 | b;
		}
	}
画素のデータからr,g,bを求める式(例: r = ([ & 0x00ff0000) >> 16) と r,g,b から画素のデータを求める式( r << 16 | g << 8 | b )に注目してください。 完全に理解するには、ビット演算子(&,|)とシフト演算子(>>, <<)を理解する必要があります。

以下に応用例を示します。forのループの中だけを示します。

// R(赤)とG(緑)を入れ替える
	p = *(src+x+y*video_width);
	r = (p & 0x00ff0000) >> 16;
	g = (p & 0x0000ff00) >> 8;
	b = (p & 0x000000ff);
        *(dest+x+y*video_width) = g << 16 | r << 8 | b;
// 色を全部反転する
	p = *(src+x+y*video_width);
	r = (p & 0x00ff0000) >> 16;
	g = (p & 0x0000ff00) >> 8;
	b = (p & 0x000000ff);
        *(dest+x+y*video_width) = (255-r) << 16 | (255-g) << 8 | (255-b);
// 色を全部反転する(賢いやり方) ~はビット反転する
	p = *(src+x+y*video_width);
        *(dest+x+y*video_width) = ~p;
// 色数を減らす(ソラリゼーション)
	p = *(src+x+y*video_width);
	r = (p & 0x00ff0000) >> 16;
	g = (p & 0x0000ff00) >> 8;
	b = (p & 0x000000ff);
	r = r/16*16; g = g/16*16; b = b/16*16;
        *(dest+x+y*video_width) = g << 16 | r << 8 | b;

前の映像を残して使う

ここまでは、ビデオ映像をキャプチャしたものを即時(リアルタイム)に 映していました。Effectvに用意されているエフェクトには、少し前の 時間の映像を表示したり、エフェクトを開始した時の画像を使っているものがあります。 それらは、ある時キャプチャした画像(静止画)を別の場所にバッファとして残してあるのです。

バッファを作る

まず、バッファを作ってみることから始めます。基本的には画像データを格納するメモリ領域を確保すればできます。 単純に配列を作るだけでも作ることができますが、画像のサイズが変わるとC言語では記述できません。 C言語で自由の大きさの領域を確保するには動的割当てと呼ばれる処理を行います。 一般にメモリを動的に割当てるには、malloc関数を使用します。

RGB32* buf;
buf = malloc(video_width*video_height*sizeof(RGB32));

上の例は、静止画一枚分のメモリ領域を動的に割当て(確保し)て、その先頭アドレスを bufに代入しています。mallocの引数は確保するメモリ領域のバイト数です。 つまり、静止画一枚分は、横のドット数(video_width)×縦のドット数(video_height)×1画素当りのバイト数(sizeof(RGB32))となります。

バッファはエフェクトが始まる時に1回だけ作成します。つまり、 start()内に書きます。 今までエフェクトのプログラムを書いていた draw()内に書いてはいけません。

// ここからは、先頭の static ... が並んでいる所に書く
static RGB32* buf; 
// ここまで

int start()
{
	state = 1;
	buf = malloc(video_area*sizeof(RGB32));
	return 0;
}
draw()内でこれを使って見ましょう。draw()の最初のコードでは、
		memcpy(dest, src, video_area * sizeof(RGB32));
として、ビデオキャプチャーした領域を、画面領域にコピーして描画しました。 これを ビデオキャプチャー領域からバッファにコピー、 そしてバッファから画面領域にコピーしましょう。ワンクッション置くわけです。 以下のように書き変えます。
                memcpy(buf, src, video_area * sizeof(RGB32));
                memcpy(dest, buf, video_area * sizeof(RGB32));
これを実行してみると、単にカメラの映像が映っているだけでおもしろく ありません。そこで、1つ前にキャプチャした画像と現在キャプチャした 画像の差を表示してみましょう。ここでは、RGBのうちBの値の差が10以上 あったら白、それ以外は黒にします。
                RGB32 b1, b2;
                int x, y;
                for (y = 0; y < video_height; y++) {
                   for (x = 0; x < video_width; x++) {
                      b1 = *(src+x+video_width*y) & 0xff;
                      b2 = *(buf+x+video_width*y) & 0xff;
                      if (abs(b1-b2) >= 10) { 
                          *(dest+x+video_width*y) = 0xffffff;
                      } else {
                          *(dest+x+video_width*y) = 0x000000;
		      }
                   }
                }
		memcpy(buf, src, video_area*sizeof(RGB32));

b1にキャプチャ画像の画素(x,y)のB値を、b2にバッファの画素(x,y)のB値を 代入し、abs(b1-b2) >= 10 で差の絶対値が10以上なら、画面の画素(x,y)を 白(0xffffff)、それ以外なら黒(0x000000)としています。 以上の処理を行なった後に キャプチャ画像をバッファにコピーしている ことに注意してください。この順にしないと、差を出す前にバッファが 現在の画像になってしまい、キャプチャ画像とバッファが同じ内容になってしまいます。

1つ前の画像との違いを処理するエフェクトには実にたくさんの種類があります。 基本的には、上記の2重ループの中身を変えていけば良いのです。 色々なエフェクトを作ってみて能力を高めてみてください。

以下、執筆予定...

バッファをたくさん使う

ohmi@rsch.tuis.ac.jp
Copyright (c) 2003-2005 Yoshihiro OHMI All rights reserved.