もうすぐ年末だ
どうしよう。もうすぐ年末だ。いろいろやばい。やばいやばいやばい。危機感は無いわけではないけどマイペースをどうしても崩せなくて、いろんなことを先送りしています。そういえば、ようやく寒くなってきて、年末っぽい雰囲気が日に日に増しています。と思ったらいきなり風邪を引いてしまい、今日はなかなかお聞き苦しい講義になってしまいました。Winged Edge Data Structure の説明をしていて「右ウィング、左ウィング」とか言った後「北ウィング、南ウィングじゃないですよ、中森明菜は知らないと思いますけど」みたいなことを口走ってしまいましたが、やっぱり通じませんでした。
CG の研究テーマ募集
学生時代は画像処理 (CV) や人工知能 (AI) なんかの研究室に所属していたんですけど、CV や AI というのはやっぱり難しくて、結局自分は CG に転向してしまったのですね。そのせいか今でも CV ってのには苦手意識があります。ですが、立場が逆になって学生さんに研究テーマを考えてもらおうとすると今度は CG では面白い応用が見つけられず、ゲーム→インタラクション→画像入力という流れで、逆に CV っぽいテーマになってしまうことがあります。どうも CG に関して自分が思いつくテーマはパッとしないので、いつも制作現場の人が何を考えているのか知りたいと思っています。教えてください。
視差画像生成
ということで、ある学生さんが卒研で距離画像を扱うということを考えられまして、最初は Kinect を使えばなんとかなるじゃろと思ってはいたのですが、二台ある Kinect は両方とも他の学生さんが使っていたりしたので、Web カメラだけでやってみるかという雰囲気になってました。それでモタモタしているうちに学生さんが「面積相関法でやってみる」とか言い出して、この方法で距離画像の元になる視差画像1が作れることを教えてくれました。話を聞いたら GPU で簡単にできそうに思えたので、教員がやっちゃいかんとは思いながら (そんな大した話でもないし、もしかしたら OpenCV に入ってるかもしれないし←ほんま何も知らん奴), 学生さんが実装する前に自分で試してみようと思ってプログラムを書いてみました。
面積相関法
面積相関法自体の説明は他に譲りたい (というか私は良く知らない) と思いますが、とりあえず左右の画像に写っている同じ形状が、左右の画像でどれだけ水平方向にずれているかということを求めれば良いようです。このずれの大きさ(視差)は距離に反比例します。
そこで、表示領域全体を覆う一枚のポリゴンに、Web カメラでキャプチャした左右の画像二枚をマルチテクスチャの機能を使って重ねてマッピングして描画します。このとき、画像をそのまま貼付ける代わりに、描画するフラグメントに対応する二つのテクスチャ上の小領域の画素値の距離 (ノルム) を求めます。そしてその距離を gl_FragDepth に代入してデプスバッファに書き込みます。
こうして、マッピングするテクスチャの一方をずらしながらポリゴンを描き、ずれの量を gl_FragColor に代入して画素の色にしてしまえば、デプスバッファによる隠面消去により、もっともノルムが小さい時のずれの量がカラーバッファに残ります。理屈の上では、これで視差画像が得られるはずです。

ノルムを求める小領域 (ウィンドウ) 内のテクスチャのサンプリングは、正規分布を使って中央を重点的に行う方法と、矩形領域を均等に行う方法を試してみました。GPU だとテクスチャの画素間を補間してくれるので、配列に格納された画像と違って乱数でサンプリングしても許されそうな気がするので楽です。以下はフラグメントシェーダの内容です。
#version 120
#extension GL_EXT_gpu_shader4 : enable
const float MAXPARALLAX = 200.0; // ずれ (視差) の量の最大値
const int TABLESIZE = 16; // 乱数テーブルの大きさ
const float TEXWIDTH = 1024.0; // テクスチャの幅
const float MATCHSIZE = 20.0 / TEXWIDTH; // マッチング領域の大きさ
uniform sampler2D lmap; // 左側カメラから得たテクスチャ
uniform sampler2D rmap; // 右側カメラから得たテクスチャ
uniform float parallax; // 左右のカメラの画像のずれの量
uniform vec2 table[TABLESIZE]; // 乱数テーブル
noperspective varying vec2 tc; // テクスチャ座標
void main(void)
{
vec3 d = vec3(0.0);
for (int i = 0; i < TABLESIZE; ++i) {
vec2 t = tc + table[i] * MATCHSIZE; // ウィンドウ内をランダムにサンプリングする
vec2 p = vec2(parallax / TEXWIDTH, 0.0); // ずれの量をテクスチャ座標に換算する
vec3 c = (texture2D(lmap, t) - texture2D(rmap, t - p)).rgb;
d += c * c; // 画素値の RGB ごとに二乗して合計する(絶対値でもいい鴨)
}
gl_FragDepth = sqrt((d.r + d.g + d.b) / (3.0 * float(TABLESIZE))); // 画素値のノルム
gl_FragColor = vec4(vec3(parallax) / MAXPARALLAX, 1.0);
}
結果
得られた結果はなかなかひどいもので、まあ CV を知らない素人が適当に、キャリブレーションも何もせずに (そういや二台のカメラのピントすら合わせなかった) やってるってことで多めに見てください。一応近いものは明るく、遠いものは暗く写るようですけど。

撮影環境はこんな感じです。

いい加減なやり方だとはいえ、さすがに品質が悪いですし、不安定です。左右のカメラのノイズの出方の違いも結構影響します。さすがにこれでは使える気がしないのですが、ちゃんとキャリブレーションしたり前処理でフィルタリングしたりすれば、少しはまともになるんでしょうか。わかりません。また、当然ながら、何も模様がないところは視差が測れません。「やっぱり赤外線でパターンを照射するかな」「それ Kinect」と一人ボケツッコミをしてしまいました。
プログラム
プログラムは Mac OS 10.7.2, Xcode 3 上で作りました。Mac Ports で入れた OpenCV 2.3.1a を使っています (この間まで fink を使ってましたけど、いろいろあって乗り換えました)。
新規性
別の学生さん (3年生) が位置センサとグローブを使って「かめはめ波」のデモを作ると頑張ってるんですが (なんか一週間くらいでヘッドトラッキングぽいことができててびっくり),「Kinect でやってる人いるよ」と指摘されました。それくらい知ってたけど、やってみたかったんだから別にええじゃんと思います。やってみた後で、そっから先を考えたらいいと考えてるんですけど、それは無駄ですかね。今回の視差画像ネタも、単に私がやってみたかっただけなんですけど、こういう価値のないことばかりやってることを許してくれなくなりそうな昨今の雰囲気が、私にはちょっとしんどい。
-
最初これを「距離画像」と書いてましたけど、これは「視差画像」だと教わりました ↩