魚眼カメラモデル

この記事はレイトレAdventカレンダーの12月24日の記事になります。タイトルは魚眼カメラモデル(Fisheye Camera Model)です。

レイトレーシングではピンホールカメラモデル(Pinhole Camera Model)薄レンズモデル(Thin Lens Camera Model)といったカメラモデルがよく使われるのですが、魚眼カメラモデルを扱っている記事はあんまり見たことが無いので書いてみました。ピンホールカメラモデルのように、簡単な計算で魚眼カメラによるレイの方向を計算できる方法について、C++による実装例を交えて解説します。

カメラモデル

カメラモデルはレイトレーシングにおいて、最初のレイを決定する部分です。イメージセンサーの画素$(i, j)$に対応するレイを返す役割を持ちます。

画像の横幅を$w$、縦幅を$h$とします。画素$(i, j)$のイメージセンサー内における位置$(u, v)$を
$$(u, v) = (\frac{2i - w}{h}, \frac{2j - h}{h})$$
と定義しておきます。

$u, v$は$w = h$なら$-1$から$1$までの値を取ります。ピンホールカメラモデルではレイの方向$\vec{r}$をカメラの前方向を$\vec{c_f}$、カメラの横方向を$\vec{c_r}$、カメラの上方向を$\vec{c_u}$として
$$\vec{r} = normalize(\vec{c_f} + u\vec{c_r} + v\vec{c_u})$$
と計算することができました。

同様の計算で魚眼カメラを再現する方法について見ていきます。


魚眼カメラモデル

イメージセンサーの中心を$\vec{c_p}$、現在注目している画素のイメージセンサー中心からの距離を$l$、レイの方向$\vec{r}$を球面座標系で表したものを$(\theta, \phi)$とします。すると、ある関数$f(\theta)$を用いて
$$l = f(\theta)$$
とすることで、様々な射影方式のカメラを表現することができます。例えば通常のカメラでは中心射影方式というものを使っていますが、中心射影方式は焦点距離を$f$として、
$$l = f\tan{\theta}$$
と表されます。

魚眼カメラの射影方式には以下のようなものがあります。
  • 正射影方式 $l = f\sin{\theta}$
  • 等距離射影方式 $l = f\theta$
  • 立体射影方式 $l = 2f\tan{\frac{\theta}{2}}$
  • 等立体角射影方式 $l = 2f\sin{\frac{\theta}{2}}$
それぞれの射影方式についてはこちらのサイトで詳しく解説されています。

これらの数式からレイの方向$\vec{r}$を計算する方法について見ていきます。


$l = f(\theta)$からレイの方向を計算する

レイのカメラのローカル座標系上での方向$(x, y, z)$は、対応する球面座標系$(\theta, \phi)$が分かれば
\begin{align} x &= \cos{\phi}\sin{\theta} \\ y &= \cos{\theta}  \\ z &= \sin{\phi}\sin{\theta} \end{align}
として計算することができます。そこで球面座標系$(\theta, \phi)$を求める方法について考えます。

$\phi$については$(u, v)$を用いて
$$\phi = \tan^{-1}(\frac{v}{u})$$
と計算できます。

$\theta$については$l = f(\theta)$という関係から
$$\theta = f^{-1}(l)$$
として計算できます。ここで注意するのが$l$の値によっては対応する$\theta$が存在しないことがあり、その場合はレイの方向を計算することができません。

$l$については
$$l = \sqrt{u^2 + v^2}$$
と計算できます。

最後に$(x, y, z)$をワールド座標系での方向$\vec{r}$に変換します。
$$\vec{r} = normalize(y\vec{c_f} + x\vec{c_r} + z\vec{c_u})$$

この処理をC++で実装します。$(u, v)$に対応するレイを返す関数を

  • bool getRay(double u, double v, Ray& ray) const
とします。この関数は$(u, v)$を受け取り、対応するレイをrayに格納してtrueを返します。レイの方向が計算出来ない時はfalseを返します。

カメラの位置をcamPos、前方向をcamForward、横方向をcamRight、上方向をcamUp、焦点距離$f$をfとすると、コードは次のようになります。
bool getRay(double u, double v, Ray& ray) const {
  //lの計算
  double l = std::sqrt(u*u + v*v);

  //thetaの計算
  if(std::abs(l/f) > 1) return false;
  double theta = std::asin(l/f);
  //phiの計算
  double phi = std::atan2(v, u);
  if(phi < 0) phi += 2*M_PI;

  //(x, y, z)の計算
  double x = std::cos(phi)*std::sin(theta);
  double y = std::cos(theta);
  double z = std::sin(phi)*std::sin(theta);

  //レイの方向の計算
  Vec3 rayDir = normalize(y*camForward + x*camRight + z*camUp);
  ray = Ray(camPos, rayDir);

  return true;
};

$\theta$の計算については各射影方式で異なってきます。

正射影方式の場合$l = f\sin{\theta}$より
$$\theta = \sin^{-1}(\frac{l}{f})$$
となります。$|\frac{l}{f}| > 1$のとき$\theta$を計算することはできないので、falseを返すようにします。

等距離射影方式の場合$l = f\theta$より
$$\theta = \frac{l}{f}$$
となります。$|\theta| > \frac{\pi}{2}$のときは$\theta$の範囲を超えてしまうので、falseを返すようにします。立体射影方式と等立体角射影方式も同様に計算できます。


魚眼カメラモデルによるレンダリング

それぞれの射影方式でレンダリングした結果は以下のようになります。
正射影方式

等距離射影方式
立体射影方式
等立体角射影方式
それぞれの射影方式で写り方が結構違うのが分かります。この画像のレンダリングでは周辺減光も考慮しているので結構リアルに見えます。通常のカメラモデルに飽きたときに使ってみると楽しいかもしれません。




Comments