HMDを作る。 Processingを用いたレンズ歪み補正の筋肉実装でステレオカメラの映像を見たい


はじめに

はじめまして、Togaras1です。

今回、わけあってHMDを作ることになったのですが、映像に魚眼レンズのような歪みを与えるという効果をコーディングで実装している例は少なく、ただ難しい単語を羅列したり数式を掲載しただけで解説した気になっている論文などを読んでいるうちにイライラしてきたので実際に実装し、この広く知見を共有しようと思い、執筆に至りました。

環境

Processingとカメラを2台使います。
ハードウェアの関係からカメラ映像を片方反転させています。

そもそもVR映像はなぜ歪んでいるか?

HMDには至近距離にマウントされているディスプレイに人間の目が焦点を合わせられるように凸レンズがついています。

この凸レンズが焦点距離では糸巻き型というレンズ歪みを発生させるため、樽型というレンズ歪みでレンズ歪みを相殺する必要があります。
VR映像はそれゆえに歪んでいるのです。

詳しくは歪曲収差でググったり、すごく薄っぺらいですがWikiでも読んでください。

知ってると便利

参考サイトはこちらです。

参考にしていた論文とかはこの辺だったと思います。

まず、レンズの歪みは、レンズの中心点(光軸)からの距離に従って変化します。
図1のYは多くの式中で、像の高さと定義されています。


図1 レンズにおいての像の高さ

これは点対称に、中心からの距離で変化します。

なのでカメラ映像でも同様に、ある点の像の高さは中心からの距離から求まります。


図2 カメラ映像においての像の高さ

カメラキャリブレーションとか歪曲収差でいろいろ調べるとすごく難しいことが羅列されているように見えますが、結局なにが言いたいかと言うと、この像の高さが高ければ高いほど歪みを大きくさせる数式を頑張って定義しているだけなのです。

参考サイトにある式

$DistotionX = p_2(3x^2+y^2)+x(k_2(x^2+y^2)^2+k_1(x^2+y^2)+1)+2p_1xy $ (1)

$DistotionY = p_1(x^2+3y^2)+y(k_2(x^2+y^2)^2+k_1(x^2+y^2)+1)+2p_2xy $ (2)

ですが、これがまさに高さが高ければ高いほど歪みが大きくなるという数式です。
式中の$x$と$y$はカメラの中心点からの相対座標で、DistotionXとDistotionYはそれぞれ歪み効果を掛けたあとの座標です。

特に$(k_2(x^2+y^2)^2+k_1(x^2+y^2)+1)$部分はX方向の歪みにもY方向の歪みにも係数として与えられています。ここは、論文などでは多項式で表されてい、$k_1$、$k_2$を変えることにより、全体の歪み度合いが変わります。

また、$p_1$、$p_2$についてですが、これはそれぞれY方向、X方向に強く作用する歪み度合いです。
今回は使わないので省略しても大丈夫です。

実装

コードはgithubに上げたのでこれをみてって言えば早いのですが、一応実装について解説しておきます。

まず、描画についてですが、Processingの描画機能であるところの図形にテクスチャを貼り付けるという超強引な方法で実装しました。
ポリゴン数はコード中のnumでいじることができます。

コードでは、頂点座標を使って動的に生成しているというところ以外は数式の通り実装してあります。

...
  // 60行目
  // 頂点座標とUV座標の計算
  posX = new float[(num + 1) * (num + 1)];
  posY = new float[(num + 1) * (num + 1)];
  posU = new float[(num + 1) * (num + 1)];
  posV = new float[(num + 1) * (num + 1)];
  vertexes = new int[num * num * 4];

  for(int i = 0; i < posX.length; ++i){
    posX[i] = ((float)(i % (num + 1)) / (float)num);
    posU[i] = posX[i];
  }
  for(int i = 0; i < posY.length; ++i){
    posY[i] = ((float)(int)(i / (num + 1)) / (float)num);
    posV[i] = posY[i];
  }

  for(int i = 0, j = 0; i < num * num * 4; i+=4, ++j){
    vertexes[i] = j + (int)(j / num);
    vertexes[i + 1] = j + (int)(j / num) + 1;
    vertexes[i + 2] = j + (int)(j / num) + 2 + num;
    vertexes[i + 3] = j + (int)(j / num) + 1 + num;
  }
  // 頂点座標の計算ここまで

  // 歪み座標の計算ここから
  float k1 = -0.20;
  float k2 = 0.005;
  float p1 = 0;
  float p2 = 0;
  for(int i = 0; i < posX.length; ++i){
    posX[i] = (posX[i] - 0.5) * 2;
    posY[i] = (posY[i] - 0.5) * 2;
    posU[i] *= cameraWidth;
    posV[i] *= cameraHeight;

    float xx = pow(posX[i], 2);
    float yy = pow(posY[i], 2);

    float b = k2 * pow(xx + yy,2) + k1 * (xx + yy) + 1;
    float distX = p2 * (3 * xx + yy) + posX[i] * b + 2 * p1 * posX[i] * posY[i]; // 式1
    float distY = p1 * (3 * yy + xx) + posY[i] * b + 2 * p2 * posX[i] * posY[i]; // 式2

    posX[i] = lensCenterX + distX * lensWidth / 2;
    posY[i] = lensCenterY + distY * lensHeight / 2;
  }
...

省略しましたが、全貌はgithubで見てください。


図3 動作画面

尊くて・・・・、儚い・・・・。

以上です。

これをカードボード使って見ようとしたら瞳孔間距離の問題で焦点が合わなかったのでソフト側で適当に左の絵と右の絵の間の距離を詰めました。

まとめ

実装してみたら大したことなかった
今はUnityとかUE4とかでいくらでも既成品のHMDを使えるし、レンズ歪みも勝手にやってくれるのでコーディングで実装するという需要は謎だけど、存在するかわからない自作HMDerの助けになればいいと思う。

今回実装した手法だと割と計算機負荷は高いっぽいのでProcessingにOpenGLライブラリ積んで実装すれば早くなるかもしれないなとか考えながら床に就くことにする。