Hatena::ブログ(Diary)

Imaginable Reality

2016-01-08

OpenCVで画像の上に別な画像を貼り付ける (位置・サイズ指定あり)

たぶん車輪の再発明だろうし、OpenCV3.0ともなればいい加減このへんをなんとかしたAPIがありそうな気がするけど、探すより作った方が早かったので作った。反省はしてない。

f:id:kougaku-navi:20160109012903j:image

#include <opencv2/opencv.hpp>

// 画像を画像に貼り付ける関数
void paste(cv::Mat dst, cv::Mat src, int x, int y, int width, int height) {
	cv::Mat resized_img;
	cv::resize(src, resized_img, cv::Size(width, height));

	if (x >= dst.cols || y >= dst.rows) return;
	int w = (x >= 0) ? std::min(dst.cols - x, resized_img.cols) : std::min(std::max(resized_img.cols + x, 0), dst.cols);
	int h = (y >= 0) ? std::min(dst.rows - y, resized_img.rows) : std::min(std::max(resized_img.rows + y, 0), dst.rows);
	int u = (x >= 0) ? 0 : std::min(-x, resized_img.cols - 1);
	int v = (y >= 0) ? 0 : std::min(-y, resized_img.rows - 1);
	int px = std::max(x, 0);
	int py = std::max(y, 0);

	cv::Mat roi_dst = dst(cv::Rect(px, py, w, h));
	cv::Mat roi_resized = resized_img(cv::Rect(u, v, w, h));
	roi_resized.copyTo(roi_dst);
}

// 画像を画像に貼り付ける関数(サイズ指定を省略したバージョン)
void paste(cv::Mat dst, cv::Mat src, int x, int y) {
	paste(dst, src, x, y, src.rows, src.cols);
}

int main() {
	cv::Mat img1 = cv::imread("Lenna.jpg"); // 画像の読み込み
	cv::Mat img2 = cv::imread("cat.jpg");   // 画像の読み込み

	// img1にimg2を貼り付ける。位置 x=105, y=110, サイズ80x80
	paste(img1, img2, 105, 110, 80, 80);

	cv::namedWindow("result");   // ウィンドウの準備
	cv::imshow("result", img1);  // 画像の表示
	cv::waitKey();               // キー入力待ち
}

関連

2016-01-04

Processingで3Dグラフィックスを扱う上での注意点

 Processingのグラフィックスの描画系は内部でOpenGLを利用していますが、ProcessingのAPIは特有の癖があるのでOpenGLと同じ感覚でやっていると時々戸惑うことがあります。3DグラフィックスまわりのProcessing特有の仕様について、気をつけておくべき点をここにメモしておきます(Processing 2.2.1において動作確認)。


(2017/7/19)内容を加筆しました(主に行列のところ)

座標系

  • Processingは左手系。OpenGLは右手系。
  • 視点に対する各軸の向きは、x軸が右方向、y軸が下方向、z軸が手前方向となっている。

f:id:kougaku-navi:20160104103420p:image:w400

3Dグラフィックスで扱われる行列とその取得方法

  • Processingの内部で以下の行列が管理されている。
    • プロジェクション行列(projection)
    • モデルビュー行列(modelview)
    • モデルビュー行列の逆行列(modelviewInv)
    • ビュー行列(camera)
    • ビュー行列の逆行列(cameraInv)
    • プロジェクション行列とモデルビュー行列を乗算した行列(projmodelview)
  • ワールド座標系とローカル座標系のあいだのモデリング変換を表わすモデル行列は定義されていない。モデル行列が欲しい場合は、ビュー行列の逆行列とモデルビュー行列を乗算して求めればよい。

f:id:kougaku-navi:20170719152146p:image

  • プロジェクション行列やモデルビュー行列は、PAppletのメンバ変数である「g」から取得できる。「g」はPGraphicsクラスのオブジェクトだが、その派生クラスであるPGraphicsOpenGL(またはPGraphics3D)でキャストしてやると、modelviewやprojectionにアクセスできる。
  • 各行列への参照を取得できるので、行列の値を書き変えてジオメトリを直接コントロールすることもできる。
  • 行列の値を直接書き変える場合は、ペアになっている逆行列(modelviewの場合はmodelviewInv)のほうも更新したほうがよいようだ。
  • projectionまたはmodelviewを書き変えた場合は、projmodelviewを更新するために ((PGraphicsOpenGL)g).updateProjmodelview() を実行する必要があるようだ。
  • 行列のコピーが欲しい場合は、.get()で取得すればよい。

行列への「参照」が欲しい場合(内部のパラメータを直接書き変えたい場合)
// プロジェクション行列
PMatrix3D projection = ((PGraphicsOpenGL)g).projection;

// モデルビュー行列
PMatrix3D modelview = ((PGraphicsOpenGL)g).modelview;

// モデルビュー行列の逆行列
PMatrix3D modelviewInv = ((PGraphicsOpenGL)g).modelviewInv;

// ビュー行列
PMatrix3D camera = ((PGraphicsOpenGL)g).camera;

// ビュー行列の逆行列
PMatrix3D cameraInv = ((PGraphicsOpenGL)g).cameraInv;

// プロジェクション行列とモデルビュー行列を乗算した行列
PMatrix3D projmodelview = ((PGraphicsOpenGL)g).projmodelview;


行列の「コピー」が欲しい場合
// プロジェクション行列
PMatrix3D projection = ((PGraphicsOpenGL)g).projection.get();

// モデルビュー行列
PMatrix3D modelview = ((PGraphicsOpenGL)g).modelview.get();

// モデルビュー行列の逆行列
PMatrix3D modelviewInv = ((PGraphicsOpenGL)g).modelviewInv.get();

// ビュー行列
PMatrix3D camera = ((PGraphicsOpenGL)g).camera.get();

// ビュー行列の逆行列
PMatrix3D cameraInv = ((PGraphicsOpenGL)g).cameraInv.get();

// プロジェクション行列とモデルビュー行列を乗算した行列
PMatrix3D projmodelview = ((PGraphicsOpenGL)g).projmodelview.get();

デフォルトの視点位置

  • デフォルトではワールド座標系≠カメラ座標系であることに注意。
  • デフォルトの視点位置は以下の通り。
    • 目の位置:( width/2, height/2, (height/2)/tan(PI/6) )
    • 注視点の位置:( width/2, height/2, 0 )
  • これは、画面の左上隅が(0, 0, 0)、右下隅が(width, height, 0)にそれぞれ対応するような視点の配置になっている。ただし、これは透視投影の視野角がデフォルトの60度であることを考慮した計算のため、perspective()などで視野角が変更されるとこの関係はくずれる。

f:id:kougaku-navi:20160104104715p:image:w600

毎フレームの行列の初期化

  • 毎フレーム、draw()開始時に、モデルビュー行列が自動的に初期化される。
  • より具体的に言うと、モデルビュー行列(modelview)がビュー行列(camera)で初期化される(同じ値になる)。
  • デフォルトでは、camera()にデフォルトパラメータを与えた時の行列(すなわち上図のカメラ配置にするための行列)で毎フレーム初期化される。
    • 具体的には、以下の形になっている。ここでwidthとheightは画面サイズ。
 1    0    0       -width/2
 0    1    0       -height/2
 0    0    1    -(height/2)/tan(PI/6)
 0    0    0          1
  • resetMatrix()を実行すると、それ以降、毎フレーム「単位行列」で初期化されるようになる。
  • camera()を実行すると、それ以降、毎フレームそのcamera()で指定したパラメータによって決まるビュー行列で初期化されるようになる。
  • ただし、resetMatrix()やcamera()を実行している箇所をpushMatrix()・popMatrix()で囲んでいた場合はこの限りではない。
  • この挙動については、内部でビューイング変換(ビュー行列)が常に保持されていて、draw()開始時にモデリング変換(モデル行列)のみを毎回単位行列にリセットしている、と考えればよい。
  • draw()の冒頭で毎回resetMatrix()やcamera()を実行している場合は特に気にする必要はないが、draw()の外や特定のタイミングでそれらを実行する場合は留意したい。

camera()の挙動

  • camera()はワールド座標系に対する視点の位置・姿勢(=ビューイング変換)を設定する関数である。
  • camera()を実行すると、内部で保有している5つの行列が更新される(modelview、modelviewInv、camera、cameraInv、projmodelview)
  • camera()を実行すると、パラメータで指定された視点になるようにビュー行列(camera)が設定され、モデル行列が単位行列に初期化される。その結果としてモデルビュー行列(modelview)はビュー行列に等しくなる。
  • camera()を実行すると、それ以降のフレームにおいて、draw()開始時のモデルビュー行列の初期化に影響をもたらす(camera()で設定されたビュー行列で初期化されるようになる)。
  • 上方向ベクトル(upX, upY, upZ)は(downX, downY, downZ)じゃないの?という疑念がある。Y軸が下に向いているのに(0, 1, 0)で上方向を表わすのは気持ち悪い。フォーラムでも同様の指摘がある。

resetMatrix()の挙動

  • resetMatrix()を実行すると、modelview、modelviewInv、camera、cameraInvが単位行列に初期化され、projmodelviewがprojectionと同じ値になる。
  • resetMatrix()を実行すると、それ以降のフレームにおいて、draw()開始時のモデルビュー行列は単位行列になる(ビュー行列も単位行列になってしまっているから)。

さらなる探究

2016-01-02

Processingで画面座標に対応する空間座標を計算する

 3Dのアプリケーションにおいて、画面をクリックして空間中のオブジェクトの表示位置を指定するときなど、画面の2次元座標に対応する3次元空間座標が欲しいときがあります。こんなとき、どんな計算を行えば良いかについて解説します。コードはProcessing 2.2.1にて動作を確認。


f:id:kougaku-navi:20160103001656g:image

画面位置に対応するローカル座標

 まず本題に入る前に、3次元空間中の点がウィンドウ上に描画されるまでの過程を理解しておく必要があります。ローカル座標系におかれたCG物体は、モデリング変換、ビューイング変換、投影変換、ビューポート変換を経て画面に描画されます。


f:id:kougaku-navi:20160101191731p:image:w600


 モデリング変換を行う行列をMm、ビューイング変換を行う行列をMv、投影変換を行う行列(プロジェクション行列)をP、ビューポート変換を行う行列をUと置きます。それぞれ4x4の行列です。このうち、モデリング変換とビューイング変換はモデルビュー行列という1つの行列で表現されます。一連の変換過程をまとめて数式で表現すると以下のようになります。

f:id:kougaku-navi:20160101192705p:image:w550

求めたいのはウィンドウ座標系からローカル座標系への変換なので、この式の逆変換を考えます。

f:id:kougaku-navi:20160102212628p:image:w400

 ここで、プロジェクション行列Pとモデルビュー行列Mはプログラム的に取得可能な行列です。ビューポート変換行列Uは取得できない(?)ようなので、数式から計算することにします。ビューポート変換行列Uは以下のようになります(ウィンドウ座標系において左上隅に原点を置いた場合)。

f:id:kougaku-navi:20160102212629p:image:w600


 以上のロジックを踏まえて、ウィンドウ座標系からローカル座標系への座標値の変換を実装した例が以下のコードです。実行すると、マウスカーソル上に黒い点(球)が表示されます。

void setup() {
  size( 400, 300, P3D);
}

void draw() {
  background(200);

  // カーソルの位置に対応する空間座標を計算する
  PVector result = unProject(mouseX, mouseY, -1);
  
  // カーソルの3次元位置に球を表示
  translate( result.x, result.y, result.z );
  sphere(2);
}

// ウィンドウ座標系からローカル座標系への変換(逆投影)を行う関数
PVector unProject(float winX, float winY, float winZ) {
  PMatrix3D mat = getMatrixLocalToWindow();  
  mat.invert();
  
  float[] in = {winX, winY, winZ, 1.0f};
  float[] out = new float[4];
  mat.mult(in, out);  // Do not use PMatrix3D.mult(PVector, PVector)
  
  if (out[3] == 0 ) {
    return null;
  }
  
  PVector result = new PVector(out[0]/out[3], out[1]/out[3], out[2]/out[3]);  
  return result;
}

// ローカル座標系からウィンドウ座標系への変換行列を計算する関数
PMatrix3D getMatrixLocalToWindow() {
  PMatrix3D projection = ((PGraphics3D)g).projection; // プロジェクション行列
  PMatrix3D modelview = ((PGraphics3D)g).modelview;   // モデルビュー行列
  
  // ビューポート変換行列
  PMatrix3D viewport = new PMatrix3D();
  viewport.m00 = viewport.m03 = width/2;
  viewport.m11 = -height/2;
  viewport.m13 =  height/2;

  // ローカル座標系からウィンドウ座標系への変換行列を計算  
  viewport.apply(projection);
  viewport.apply(modelview);
  return viewport;
}

f:id:kougaku-navi:20160102215234g:image

 上記コードにおいて、球(sphere)は2次元的に配置しているのではなく、ウィンドウ座標系で表現されるマウスカーソル座標をローカル座標系に変換してから配置しています。その計算をやっているのがunProject()という関数です。ウィンドウ座標は(winX, winY, winZ)の3つで表現されます。winZは奥行きを表わしており、-1.0にすると視錘台の手前の面(near clipping plane)、1.0にすると視錘台の奥の面(far clipping plane)の位置に対応します。


f:id:kougaku-navi:20160102155510p:image:w500


 ここでやっている計算内容は、OpenGLのgluUnProject()という関数とほぼ同じです(Y軸の向きだけ違う)。ProcessingからgluUnProject()を直接呼ぶほうが賢いかもしれませんが、Processing 2.2.1におけるGLUへのアクセス方法がわからなかったので現状こうしています。OpenGLまわりは、Processingのバージョンによって仕様が異なるのでややこしいんですよね(古いコードが動かなかったり)。


画面座標に対応する3次元平面上の点

 画面をクリックして空間中のオブジェクトの表示位置を指定する方法について考えてみます。画面上の点に対応する3次元空間中の点は、画面上の点と焦点を結ぶ直線上に無数に存在します。そこで、3次元空間内に地面を作り、画面上の点に対応する地面上の点を求めることにします。これを図で描くと以下のようになります。


f:id:kougaku-navi:20160102193117p:image:w600


 ワールド座標系で図示していますが、これをローカル座標系(現在の座標系)と読み替えても同じです。上図のようにパラメータを定義したとき、画面上の点に対応する地面上の点pは次式で与えられます。

f:id:kougaku-navi:20160102193118p:image:w300

 画面上の点に対応する3次元座標wはさきほどのunProject()を使って計算された座標値です。目の位置eはモデルビュー行列の逆行列の4列目の成分から得ることができます。平面上の任意の点fと平面の方向を表わす法線ベクトルnは、床をどう配置したかの情報に基づいて与えてください。

 これをプログラムで実装すると以下のようになります。

void setup() {
  size( 500, 500, P3D);
}

void draw() {
  background(200);

  // 視点の位置を設定
  camera( 1160, -1960, 1730, 890, -1200, 1200, 0, 1, 0 );

  // カーソル位置に対応する床面上の座標を計算
  PVector floorPos = new PVector( 500, 300, 100 ); // 床の座標
  PVector floorDir = new PVector( 0, -1, 0 );      // 床の法線ベクトル
  PVector mousePos = getUnProjectedPointOnFloor( mouseX, mouseY, floorPos, floorDir );
  
  // 床
  pushMatrix();
    translate( floorPos.x, floorPos.y, floorPos.z );
    fill(255);
    box( 2000, 1, 2000 );
  popMatrix();

  // カーソル位置に立方体を描画
  pushMatrix();
    translate( mousePos.x, mousePos.y, mousePos.z );
    fill(255, 0, 0);
    box(200);
  popMatrix();
}

// 画面座標に対応する床面上の座標を計算する関数
PVector getUnProjectedPointOnFloor(float screen_x, float screen_y, PVector floorPosition, PVector floorDirection) {

  PVector f = floorPosition.get(); // 床の位置
  PVector n = floorDirection.get(); // 床の方向(法線ベクトル)
  PVector w = unProject(screen_x, screen_y, -1.0); // 画面上の点に対応する3次元座標
  PVector e = getEyePosition(); // 視点位置

  // 交点の計算  
  f.sub(e);
  w.sub(e);
  w.mult( n.dot(f)/n.dot(w) );
  w.add(e);

  return w;
}

// 現在の座標系における視点の位置を取得する関数
PVector getEyePosition() {
  PMatrix3D mat = (PMatrix3D)getMatrix(); // モデルビュー行列を取得
  mat.invert();
  return new PVector( mat.m03, mat.m13, mat.m23 );
}

// ウィンドウ座標系からローカル座標系への変換(逆投影)を行う関数
PVector unProject(float winX, float winY, float winZ) {
  PMatrix3D mat = getMatrixLocalToWindow();  
  mat.invert();
  
  float[] in = {winX, winY, winZ, 1.0f};
  float[] out = new float[4];
  mat.mult(in, out);  // Do not use PMatrix3D.mult(PVector, PVector)
  
  if (out[3] == 0 ) {
    return null;
  }
  
  PVector result = new PVector(out[0]/out[3], out[1]/out[3], out[2]/out[3]);  
  return result;
}

// ローカル座標系からウィンドウ座標系への変換行列を計算する関数
PMatrix3D getMatrixLocalToWindow() {
  PMatrix3D projection = ((PGraphics3D)g).projection; // プロジェクション行列
  PMatrix3D modelview = ((PGraphics3D)g).modelview;   // モデルビュー行列
  
  // ビューポート変換行列
  PMatrix3D viewport = new PMatrix3D();
  viewport.m00 = viewport.m03 = width/2;
  viewport.m11 = -height/2;
  viewport.m13 =  height/2;

  // ローカル座標系からウィンドウ座標系への変換行列を計算  
  viewport.apply(projection);
  viewport.apply(modelview);
  return viewport;
}

 実行結果は以下のようになります。マウスカーソルの位置に対応する平面上の座標が計算できていることがわかります。

f:id:kougaku-navi:20160102194838g:image:w350


まとめ

 CGが画面に投影されるまでの変換過程を理解していると、数学的にいろいろできますね。いじょ。


追記

 ここで解説したテクニックを使って、忍者くんをクリックした場所に歩かせるサンプルを作りました。こちらもあわせてごらんください。

f:id:kougaku-navi:20160103001656g:image


参考ページ

関連する記事