かみのメモ

コンピュータビジョン・プログラムな話題中心の勉強メモ(記事一覧は https://kamino.hatenablog.com/archive へ)

OpenCVで取得したカメラパラメータをUnityで使う

今回はOpenCVで取得したカメラパラメータ(内部パラメータ・外部パラメータ)をUnityのカメラオブジェクトに反映させる方法についてまとめます。{}

プロジェクションマッピングとかARをするときによくやる手順ですので、誰かの参考になれば嬉しいです。

ちなみに、カメラパラメータとそのキャリブレーション方法についてはこちらの記事で詳しく解説しています↓

kamino.hatenablog.com

またこの記事はUnityからOpenCVの関数を呼び出す話とは別物です。 別記事でUnityからC++を呼び出すテクニックを紹介しているので、UnityでOpenCVの関数を使いたいという方はこちらを参照してみてください。

kamino.hatenablog.com

もくじ

1. 作成したUnityスクリプト

github.com

最初に今回作成したC#スクリプトを晒しておきます。 「解説とかいらないよ」って人は、このスクリプトをカメラオブジェクトにアタッチして使ってください。

OpenCVCameraParamsが内部パラメータを反映させるスクリプトOpenCVTranslateが親オブジェクトからの相対位置を外部パラメータの通りに設定するスクリプトです。

f:id:kamino-dev:20181102153011p:plain

2. 概要

まずは、カメラパラメータの話と、OpenCVとUnityでは座標系が違うよという話をおさらいしておきます。

2.1. OpenCVのカメラパラメータ

OpenCVで取得できるカメラパラメータは内部パラメータ(カメラ行列)・歪みパラメータ・外部パラメータの3種類に分けられます(カメラ行列と歪みパラメータをまとめて内部パラメータとすることもあります)。 各パラメータは次の要素を含んでいます。

  • 内部パラメータ(intrinsic parameters)
  • 歪みパラメータ(distortion parameters)
  • 外部パラメータ(extrinsic parameters)
    • 回転(rotation)
    • 並進移動(translation)

この記事では、この内の内部パラメータ・外部パラメータをUnityに反映させる方法を紹介します。 歪みパラメータを再現するにはShaderを自前で実装する必要があるので今回は据え置きです(後日、別記事で書くかもしれません)。

2.2. OpenCVとUnityの座標系の違い

OpenCVとUnityを連携させる上で、座標系に関していくつか面倒な問題があります。

1つ目は、OpenCVが右手座標系であるのに対し、Unityが左手座標系である点です。 右手座標系と左手座標系は軸のどれか1本を反転させた関係にあります。 また、右手座標系における回転は軸の正方向を向いて時計回りを正としますが、左手座標系では反時計回りを正とします。 そのため、座標値をそのまま移植するとOpenCVのシーンとUnityのシーンが鏡対称になってしまいます。

2つ目は、回転・並進移動の表現方法の問題です。 OpenCVでは座標系の関係を同次座標系で記述します。 一方、Unityでは内部的には同次座標系を使っているのでしょうが、表面的にはtransform.rotationtransform.positionを使用する仕様になっています。

両者の違いですが、OpenCVでは回転ベクトルもしくは回転行列を用いて回転を記述する一方、Unityでは主にクォータニオンを用いて回転を記述します。 また後述しますが、OpenCVでは回転→並進移動の順に処理が行われるのに対し、Unityでは並進移動→回転の順に処理が行われます。

とまあ、このような違いを同時に考慮しながら実装しなければならない、ということになります。 左手座標系とか滅べばいいのに。

3. 内部パラメータを反映させる

それでは、内部パラメータについて考えてみます。

Unityのカメラはデフォルトで透視投影モデル(perspective projection model)になっていますが、残念ながら光学中心を動かしたりX/Y方向の画角を別々に設定したりすることはできません。 そこでCamera.projectionMatrixを直接設定してやることにします。 Camera.projectionMatrixOpenGL形式の投影行列を受け入れてくれるらしく、座標系も右手系準拠でよいようです。

ということで、OpenCVの内部パラメータを反映した透視投影行列を作ってみます。 よく見る透視投影行列はnearfartopbottomrightleftの6つのパラメータで定義するタイプですが、これをnearfarfxfycxcywidthheightを用いた表現に置き換えます。

$$ \begin{bmatrix} \frac{2f_x}{w} & 0 & 1-\frac{2c_x}{w} & 0 \\ 0 & \frac{2f_y}{h} & -1+\frac{2c_y}{h} & 0 \\ 0 & 0 & -\frac{f + n}{f-n} & -\frac{fn}{f-n} \\ 0 & 0 & -1 & 0 \end{bmatrix} $$

OpenGLでは上がY軸正方向ですが、OpenCVでは下がY軸正方向になるので2行3列目の符号が反転しています。

コードにするとこんなかんじです。

Matrix4x4 PerspectiveMat() {
    var m = new Matrix4x4();
    m[0, 0] = 2 * Fx / ImageWidth;
    m[0, 1] = 0;
    m[0, 2] = 1 - 2 * Cx / ImageWidth;
    m[0, 3] = 0;

    m[1, 0] = 0;
    m[1, 1] = 2 * Fy / ImageHeight;
    m[1, 2] = -1 + 2 * Cy / ImageHeight;
    m[1, 3] = 0;

    m[2, 0] = 0;
    m[2, 1] = 0;
    m[2, 2] = -(Far + Near) / (Far - Near);
    m[2, 3] = -2 * Far * Near / (Far - Near);

    m[3, 0] = 0;
    m[3, 1] = 0;
    m[3, 2] = -1;
    m[3, 3] = 0;
    return m;
}

また透視投影行列で画像サイズを固定すると、ウィンドウサイズを変更したときにアスペクト比がおかしくなってしまいます。 そこで以下のようにゲーム画面のアスペクト比を固定するためのコードを加えておきます。

float screenAspect = (float)Screen.height / Screen.width;
float aspect = (float)ImageHeight / ImageWidth;
if (screenAspect < aspect) {
   float scale = (float)ImageHeight / Screen.height;
   float camWidth = ImageWidth / (Screen.width * scale);
   cam.rect = new Rect((1 - camWidth) / 2, 0, camWidth, 1);
} else {
   float scale = (float)ImageWidth / Screen.width;
   float camHeight = (float)ImageHeight / (Screen.height * scale);
   cam.rect = new Rect(0, (1 - camHeight) / 2, 1, camHeight);
}

以上で、キャリブレートしたカメラと同じ画角・縦横比を持つカメラをUnity上に作ることができました。

4. 外部パラメータを反映させる

次に外部パラメータについて考えてみます。

外部パラメータを決めるためには、OpenCVとUnityの間でどの座標軸が反転していることにするかを決める必要があります。 OpenCVとUnityは共通して「カメラの向きはZ軸の正方向」と定義していますので、Z軸を反転させてしまうとカメラを180°回す操作が必要になって面倒です。 またX軸とY軸はどちらでもいいのですが、座標系の扱いを間違えたときに左右が逆になるより上下逆さまになる方が間違いに気付きやすいです。 ということで、ここではY軸を反転させることにします。 つまりOpenCV上の点(1, 2, 3)はUnity上の(1, -2, 3)と対応していることにします。

4.1. 回転

まずは回転成分について。

OpenCVでは座標系の回転を回転ベクトルないし回転行列で出力させることができますが、ここでは回転ベクトルを利用します。

OpenCVの回転ベクトルは変換後の座標系から変換前の座標系への回転を「ベクトル軸vを中心にベクトルの長さ|v|分の角度だけ右回転させる」という形式で記述したものです。 そして回転ベクトルの面白い性質なのですが、この定義に従ったとき変換前後のどちらの座標系で見ても回転ベクトルの成分は同じ値を取ります。 そのため、変換前の座標系から変換後の座標系への回転は「ベクトル軸vを中心にベクトルの長さ|v|分の角度だけ左回転させる」という風に表現できます。

それでは、この回転ベクトルをUnityに移植しましょう。 OpenCVで取得した回転ベクトルをv = (v_x, v_y, v_z)とします。

Unityではtransform.localRotation親オブジェクトから自身の座標系への回転クォータニオン形式で入力することでオブジェクトの回転を定義できます。 そしてUnityではQuaternion.AngleAxis()で回転ベクトルをクォータニオン表記に変換できるので、この関数を利用します。

まずUnityは左手系ですので、回転ベクトルはUnity上で(v_x, -v_y, v_z)と表現されます。 次に回転量ですが、ここで表現したいのは親オブジェクトから自身の座標系への回転なので|v|だけ左回転させればよいことになります。 左手系では左回転を正とするので、結局 +|v| だけ回せばよいことになります。

これをコードに書き起こすと次のようになります。

var rod = new Vector3(RotationX, -RotationY, RotationZ);
this.transform.localRotation = Quaternion.AngleAxis(rod.magnitude * 180 / Mathf.PI, rod);

回転についてはこれでOKです。

ちなみに三次元空間における回転の表現方法についてはこんな記事も書いているので参考にしてください→回転ベクトル・回転行列・クォータニオン・オイラー角についてまとめてみた - かみのメモ

4.2. 並進移動

最後に並進移動について。

OpenCVから取得できる並進ベクトルは、変換後の座標系原点から変換前の座標系原点までの距離回転後の座標系において表現したものです。 一方Unityは、親オブジェクトの座標系でlocalPositionに移動させたあとにlocalRotationの分だけ回転させます。 そのため、取得した並進ベクトルを変換前の座標系での表現に直してから方向を反転させてlocalPositionに代入してやる必要があります。

これをコードに書き起こすと次のようになります。 Y軸が反転しているのでy成分だけ+になっている点に注意してください。

var xdir = this.transform.localRotation * Vector3.right;
var ydir = this.transform.localRotation * Vector3.up;
var zdir = this.transform.localRotation * Vector3.forward;
this.transform.localPosition = -xdir * TranslationX + ydir * TranslationY - zdir * TranslationZ;

以上をまとめたのが、冒頭のGitHubのリンクのソースコードです。

CGの座標系はなかなか面倒ですね。