第04回 射影変換

射影変換とは、任意の形の四角形から別の形の四角形にする変形のことをいう。
4つの頂点の座標の対応からその変換式を求めることができる。

今回のプログラムの最終的な機能

プログラムを実行すると下の図のように元画像と赤い枠が表示される。
赤枠の四隅の○をドラッグすると、それに合わせて枠の形が変わる。
画面上で右クリックすると、dataフォルダに の2つの画像が作られる。

枠の表示

概要

setup関数で PVector型の変数 c[0]~c[3] に枠の頂点の初期値は設定済み。
不規則な図形はrect関数では描けないので、Processingのシェイプ機能を使って描く (実は第03回にもこの機能で枠を描いている)。
さらに頂点に直径20ピクセルの円を描けば、上の実行例のような表示になる。

課題 1

ベースのプログラム (元画像が表示されるだけ)
PImage img;  // 元画像の変数
PVector[] c = new PVector[4]; // 選択範囲の端点
int dragging = -1; // ドラッグ中の点番号(0~3ならそれぞれの端点、-1ならドラッグ中でない)
// 射影変換の係数
float a0, a1, a2, b0, b1, b2, c1, c2;

void setup() {
  size(600, 450);
  img = loadImage("元.jpg");
  // 選択範囲の初期値を決める
  c[0] = new PVector(width*0.1, height*0.1); // 左上
  c[1] = new PVector(width*0.9, height*0.1); // 右上
  c[2] = new PVector(width*0.9, height*0.9); // 右下
  c[3] = new PVector(width*0.1, height*0.9); // 左下
  calcParameters();
}

void draw() { // 課題1で変更を加える
  background(0);
  image(img, 0, 0, width, height);// 画像を表示
  // 赤枠の描画
  noFill(); // 図形の中を塗りつぶさない設定にする
  strokeWeight(2);   // 図形の枠の太さを2ptにする
  stroke(255, 0, 0);
  // 端点がc[0]~c[3]の四角形を描く
}

// 画面の状態を画像として出力
void getScreenShot() {
  save("data/2選択範囲.jpg");
}

// 双一次補間で射影変換した画像を保存
void project() {
}

// 射影変換された出力画像の点fに対応する元画像の対応点の座標を返す
PVector getProjectedPosition(PVector f) {
  float x = (a1*f.x+b1*f.y+c1)/(a0*f.x+b0*f.y+1);
  float y = (a2*f.x+b2*f.y+c2)/(a0*f.x+b0*f.y+1);
  return new PVector(x, y);
}

// 以下3つの関数に課題2で変更を加える
// ボタンを押したときに呼び出される
void mousePressed() {
  // 左クリックなら円の範囲内の場合はその端点をドラッグ開始
  if (mouseButton == LEFT) {
  }
  // 右クリックなら実行画面のスクリーンショットと射影変換した画像を保存
  if (mouseButton == RIGHT) {
    getScreenShot(); // 実行画面のスクリーンショットを保存
    project();       // 双一次補間で射影変換した画像を保存
  }
}

// ドラッグ中に呼び出される(端点をつかんでいるときはカーソル位置まで端点を移動)
void mouseDragged() {
}

// ボタンを離したときに呼び出される(ドラッグ終了)
void mouseReleased() {
}

// 端点の位置から射影変換の係数を求める (課題3で変更を加える)
void calcParameters() {
  float w = img.width;
  float h = img.height;
  PVector[] co = new PVector[4]; // 画像サイズに換算した選択範囲の端点の座標
  for (int i=0; i<4; i++) {
    co[i] = new PVector(c[i].x*img.width/width, c[i].y*img.height/height);
  }
  // 仮の値
  a0 = 0;
  b0 = 0;
  a1 = 1;
  b1 = 0;
  c1 = 0;
  a2 = 0;
  b2 = 1;
  c2 = 0;
}
枠の初期座標を計算する部分 (setup関数) と、枠を描画する部分 (draw関数) を完成させるのがこの課題のゴール。
  1. Processingのエディタに上のサンプルプログラムのコードをコピー&ペーストする。
  2. 「img04」という名前で保存する。
  3. 適当に画像検索してサンプル用の画像 (「被写体としては長方形で、斜めに撮影されたために写真の中ではゆがんだ状態になっているもの」が含まれるもの。建物、看板など) を用意する。

  4. 悪い例1 (ほぼ正面から撮影されていて、プログラムの変形機能がちゃんと働いているか確認できない)


    悪い例2 (「長方形のはずの部分」はあるが、小さすぎて変換後にぼやけてしまう)


    悪い例3 (「長方形のはずの部分」がない)


  5. draw関数の「// 端点がc[0]~c[3]の四角形を描く」の下に以下のコードを追加して実行する。
  6. (setup関数でc[0]~c[3]には頂点の初期値を入れてあるので、これで長方形が表示されるはず)
      beginShape();
      for (int i=0; i<4; i++){
        vertex(c[i].x, c[i].y);
      }
      endShape(CLOSE);

  7. draw関数の「vertex(c[i].x, c[i].y);」の下に以下のコードを追加して実行する。
  8.     ellipse(c[i].x, c[i].y, 20, 20);
    (ellipse関数の第1, 2引数が中心の位置、第3, 4引数が横と縦の径をきめる)
    (ここまで書いたときの実行結果はこのようになるはず。まだドラッグ移動はできない)


  • この段階でも、右クリックすると実行画面が「2選択範囲.jpg」として保存されるが、これは確認用なのでまだ何もしなくてよい。
  • うまくいかない場合は、draw関数の最終状態とコードを見比べる。

枠を変形できるようにする

概要

Processingでは、以下のタイミングで自動的に実行される関数が存在する。これをうまく使えば、端点をドラッグして移動させる機能を実装できる。
  • マウスボタンを押した → mousePressed関数
  • マウスでドラッグした → mouseDragged関数
  • マウスボタンを離した → mouseReleased関数
四角形の端点の座標は c[0] ~ c[3] に格納しているので、「どれをドラッグしているか」を整数型変数 dragging に入れておけば、ドラッグ中に c[dragging] にマウスの座標を入れることで端点をその場所に移動させられる。

課題 2

  1. mousePressed関数の「if (mouseButton == LEFT) {」と、そのすぐ下の「}」の間に以下のコードを追加する。
  2.     for (int i=0; i<4; i++){ // 4つの端点についての繰り返し
          // 端点とカーソルの距離が10未満なら(カーソルがその円の中にあったら)
          if (dist(c[i].x, c[i].y, mouseX, mouseY) < 10){
            dragging = i; // その端点のドラッグを開始
          }
        }

  3. mouseDragged関数の中に以下のコードを追加する。
  4.   // ドラッグ中の端点の位置をカーソル位置に
      if (dragging>=0) {
        c[dragging].x = mouseX;
        c[dragging].y = mouseY;
      }

  5. mouseReleased関数の中に以下のコードを追加する。
  6.   dragging = -1; // ドラッグ終了
    

  7. 実行して端点をドラッグし、移動できることを確認する。

画像の変形 (双一次補間)

概要

一般に、どのような射影変換も次の式で表すことができる。
\(\begin{eqnarray} x'&=&\frac{a_1x+b_1y+c_1}{a_0x+b_0y+1}\cr y'&=&\frac{a_2x+b_2y+c_2}{a_0x+b_0y+1} \end{eqnarray}\)

この式の8個の係数\(a_0, b_0, a_1, b_1, c_1, a_2, b_2, c_2\) がわかれば変換式が確定する。
今回のプログラムの場合は、選択範囲の端点 \((x_0, y_0)\) ~ \((x_3, y_3)\) が元画像の端点に対応する。

これを上の変換式に入れると、以下の8つの関係が得られる。
\(\begin{eqnarray} x_0&=&c_1\cr y_0&=&c_2\cr x_1&=&\frac{a_1w+c_1}{a_0w+1}\cr y_1&=&\frac{a_2w+c_2}{a_0w+1}\cr x_2&=&\frac{a_1w+b_1h+c_1}{a_0w+b_0h+1}\cr y_2&=&\frac{a_2w+b_2h+c_2}{a_0w+b_0h+1}\cr x_3&=&\frac{b_1h+c_1}{b_0h+1}\cr y_3&=&\frac{b_2h+c_2}{b_0h+1} \end{eqnarray}\)

これを \(a_0, b_0, a_1, b_1, c_1, a_2, b_2, c_2\) について解くと以下のようになる。プログラムではこの係数を使って出力画像の点に対応する元画像の座標を求める。あとはこれまでと同様にして双一次補間でその点の色を決めるだけ。
\(\begin{eqnarray} a_0&=&\frac{(x_1+x_3-x_0-x_2)(y_2-y_3)-(y_1+y_3-y_0-y_2)(x_2-x_3)} {w\left\{(x_2-x_1)(y_2-y_3)-(y_2-y_1)(x_2-x_3)\right\}}\cr b_0&=&\frac{(x_1+x_3-x_0-x_2)(y_2-y_1)-(y_1+y_3-y_0-y_2)(x_2-x_1)} {h\left\{(x_2-x_3)(y_2-y_1)-(y_2-y_3)(x_2-x_1)\right\}}\cr a_1&=&a_0x_1+\frac{x_1-x_0}{w}\cr b_1&=&b_0x_3+\frac{x_3-x_0}{h}\cr c_1&=&x_0\cr a_2&=&a_0y_1+\frac{y_1-y_0}{w}\cr b_2&=&b_0y_3+\frac{y_3-y_0}{h}\cr c_2&=&y_0\cr \end{eqnarray}\)

課題 3

  1. (いまは空っぽになっている) project関数の中に、前々回 (第02回) の完成状態のプログラムのbilinear関数の中のコードをコピー&ペーストする。
  2. project関数のファイル出力の部分を以下のように書き換える。
  3.   outputImg.save("data/3双一次補間.jpg");
      outputImg.save("data/3射影変換.jpg");

  4. project関数の「// 出力画像の(i, j)の位置に対応する元画像の位置(i_, j_)を求める」の下の行を以下のように書き換える。
  5.       PVector ij = getRotatedPosition(new PVector(cx, cy), new PVector(i, j), 1/scale, angle);
          PVector ij = getProjectedPosition(new PVector(i, j));

  6. project関数の中を以下のように変更する。
  7.   float cx = w/2; // 画像の中心の横位置
      float cy = h/2; // 画像の中心の縦位置
      calcParameters();
    • cx, cyは第02回の「回転」の中心にあたるもの。今回は必要ない。
    • calcParameters関数は、このあと完成させる「射影変換前の対応座標」を返す関数。

  8. calcParameters関数のコメント文「// 仮の値」を削除し、その下の8行を射影変換に応じたものに書き換える。
    • 「概要」の最後の結果を参考にする。
    • \(x_0\) → co[0].x, \(x_1\) → co[1].x, ..., \(y_0\) → co[0].y, \(y_1\) → co[1].y,... のように読み替える。

  9. 実行し、「本来長方形のはずのもの」にぴったり合うように赤枠を変形させてから右クリックする。
    • dataフォルダに「3射影変換.jpg」が作られる。
    • (「2選択範囲.jpg」も更新される)


    • このページの一番上の図のように実行画面とdataフォルダの中をならべて表示させ、ファイルのサムネイルを確認しながら位置を調整すればきれいに合わせられる (プログラムを終了させないかぎり、右クリックで何度でも出力し直せる)。
    • 「3射影変換.jpg」は、歪みが完全に取り除かれた (正面から撮影したような) 状態になるはず。
    • 赤枠の位置を決めるときは「選びたいモノを囲むように選択」ではなく「選びたいモノだけを選択」する。
    • こうではなく


      こう。
この時点で存在するファイル → 提出ファイル
  • 元.jpg
  • 2選択範囲.jpg
  • 3射影変換.jpg

提出

理解度確認テスト予告

次回の授業の初めに第01~04回の内容についての理解度確認テストを行う。

戻る

inserted by FC2 system