第05回 射影変換


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

はじめに、これから作るプログラムが完成した時点の機能をざっと見ておく。


選択範囲の赤枠・赤丸を表示する

まずは選択範囲が見えるようにする。


課題 1

    ベースのプログラム (元画像が表示されるだけ)
    PImage img;  // 元画像の変数
    
    PVector[] co = new PVector[4]; // 選択範囲の端点
    int dragging = -1; // ドラッグ中の点番号(0~3ならそれぞれの端点、-1ならドラッグ中でない)
    // 射影変換の係数
    float[] a = new float[3];
    float[] b = new float[3];
    float[] c = new float[3];
    
    void setup() {
      size(800, 600);
      img = loadImage("1元.jpg");
      // 選択範囲の初期値を決める
      co[0] = new PVector(width*0.1, height*0.1); // 左上
      co[1] = new PVector(width*0.9, height*0.1); // 右上
      co[2] = new PVector(width*0.9, height*0.9); // 右下
      co[3] = new PVector(width*0.1, height*0.9); // 左下
      calcParameters();
    }
    
    void draw() {
      background(0);
      image(img, 0, 0, width, height);// 画像を表示
      // 赤枠の描画
      noFill(); // 図形の中を塗りつぶさない設定にする
      strokeWeight(2);   // 図形の枠の太さを2ptにする
      stroke(255, 0, 0);
      // 端点がco[0]~co[3]の四角形を描く
      // 赤丸の表示
      // co[0]~co[3]を中心とした、直径20ピクセルの円を描く
    }
    
    // 画面の状態を画像として出力
    void getScreenShot() {
      save("data/2選択範囲.jpg");
    }
    
    // 双一次補間で射影変換した画像を保存
    void project() {
    }
    
    // 射影変換された出力画像の点fに対応する元画像の対応点の座標を返す
    PVector getProjectedPosition(PVector f) {
      float x = (a[1]*f.x+b[1]*f.y+c[1])/(a[0]*f.x+b[0]*f.y+c[0]);
      float y = (a[2]*f.x+b[2]*f.y+c[2])/(a[0]*f.x+b[0]*f.y+c[0]);
      return new PVector(x, y);
    }
    
    void mousePressed() {
      // 左クリックなら円の範囲内の場合はその端点をドラッグ開始
      if (mouseButton == LEFT) {
      }
      // 右クリックなら実行画面のスクリーンショットと射影変換した画像を保存
      if (mouseButton == RIGHT) {
        getScreenShot(); // 実行画面のスクリーンショットを保存
        project();       // 双一次補間で射影変換した画像を保存
      }
    }
    
    // ドラッグ中に呼び出される(端点をつかんでいるときはカーソル位置まで端点を移動)
    void mouseDragged() {
      if (dragging>=0) {
      }
    }
    
    // ボタンを離したときに呼び出される(ドラッグ終了)
    void mouseReleased() {
    }
    
    // 端点の位置から射影変換の係数を求める
    void calcParameters() {
      float w = img.width;
      float h = img.height;
      PVector[] ico = new PVector[4]; // 画像サイズに換算した選択範囲の端点の座標
      for (int i=0; i<4; i++) {
        ico[i] = new PVector(co[i].x*img.width/width, co[i].y*img.height/height);
      }
      c[0] = 1;
      c[1] = ico[0].x;
      c[2] = ico[0].y;
      b[0] = (ico[1].x+ico[3].x-ico[0].x-ico[2].x)*(ico[2].y-ico[1].y);
      b[0]-= (ico[1].y+ico[3].y-ico[0].y-ico[2].y)*(ico[2].x-ico[1].x);
      b[0]/= h*((ico[2].x-ico[3].x)*(ico[2].y-ico[1].y)-(ico[2].y-ico[3].y)*(ico[2].x-ico[1].x));
      a[0] = (ico[1].x+ico[3].x-ico[0].x-ico[2].x)*(ico[2].y-ico[3].y);
      a[0]-= (ico[1].y+ico[3].y-ico[0].y-ico[2].y)*(ico[2].x-ico[3].x);
      a[0]/= w*((ico[2].x-ico[1].x)*(ico[2].y-ico[3].y)-(ico[2].y-ico[1].y)*(ico[2].x-ico[3].x));
      a[1] = a[0]*ico[1].x+(ico[1].x-ico[0].x)/w;
      a[2] = a[0]*ico[1].y+(ico[1].y-ico[0].y)/w;
      b[1] = b[0]*ico[3].x+(ico[3].x-ico[0].x)/h;
      b[2] = b[0]*ico[3].y+(ico[3].y-ico[0].y)/h;
    }
    

  1. Processingのエディタに上のサンプルプログラムのコードをコピー&ペーストする。
  2. 「img05」という名前で保存する。
  3. 適当に画像検索してサンプル用の画像(被写体自身としては長方形で、写真上では歪んだ状態になっている部分を含むもの。建物・看板など)を用意する。
  4. 画像の形式に応じて以下の変更を加える。
  5. 「1元.jpg」をProcessingのウインドウにドラッグ&ドロップする。
  6. draw関数の「// 端点がco[0]~co[3]の長方形を描く」の下に、対応するコードを追加する。

  7. 実行して、画面より一回り小さい赤枠が表示されることを確認する。

  8. draw関数の「// co[0]~co[3]を中心とした、直径20ピクセルの円を描く」の下に、対応するコードを追加する。

  9. 実行して、赤枠の4隅に赤丸が表示されることを確認する。


  10. この時点で存在するファイル
    ※ うまくいかないときはdraw関数の最終状態とコードを見比べる。

選択範囲をドラッグできるようにする

課題 2

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


  3. mouseDragged関数の「if (dragging>=0) {」と、そのすぐ下の「}」の間に以下のコードを追加する。
  4.     co[dragging].x = mouseX; // ドラッグ中の端点の横位置をカーソルの横位置に変更
        co[dragging].y = mouseY; // ドラッグ中の端点の縦位置をカーソルの縦位置に変更
    


  5. 実行して端点をドラッグしてみる。
  6. ※ 端点がドラッグ移動できるはず。
    ※ 一見問題なく動作するように見えるが、どれかをドラッグしたあとで赤丸以外の所からドラッグすると、前に動かした端点がカーソル位置にワープしてしまう。

  7. mouseReleased関数の中に以下のコードを追加する。
  8.   dragging = -1; // ドラッグ終了
    
    ※ これを入れることで、ボタンを離すと「ドラッグ中の端点の番号」がリセットされる。

  9. 実行して端点をドラッグしてみる。
  10. ※ 今度は赤丸以外の所からドラッグしても、なにも起こらなくなるはず。

  11. 適当に赤丸をドラッグして赤枠を変形させてから右クリックする。


  12. この時点で存在するファイル
    ※ うまくいかないときは最終状態のmousePressed関数、mouseDragged関数、mouseReleased関数とコードを見比べる。

射影変換した画像を出力する

課題 3

  1. 第04回の課題の最終状態のプログラムのskew関数の中のコードを、今のプログラムのproject関数の中にコピー&ペーストする。
  2. ※ 今回のプログラムにはgetSkewedPosition関数や引数「s」がないので、そのままではエラーになる。

  3. project関数の中の「PVector ij」を求める部分を以下のように書き換える。
  4.       PVector ij = getSkewedPosition(new PVector(cx, cy), new PVector(i, j), s);
    
          PVector ij = getProjectedPosition(new PVector(i, j));
    
    ※ getProjectedPosition関数は「射影変換前の対応点」を求める自前の関数。今日の説明の最初の変換式を使ったもの。


  5. project関数から以下の2行を削除する。
  6.   float cx = w/2;
      float cy = h/2;
    
    ※ 前回はこれらを中心の位置として使ったが、射影変換では中心の位置は不要なので。

  7. その場所に以下のコードを追加する。
  8.   calcParameters();
    
    ※ calcParametersは、選択範囲の4隅の位置の情報から射影変換の係数を計算する自前の関数。それぞれのピクセルの対応点を調べる前にこれを呼び出して係数を更新する。

  9. project関数の最後のファイル出力のところを以下のように変更する。
  10.   if (s.equals("横")) {
        outputImg.save("data/2スキューX.jpg");
      } else {
        outputImg.save("data/3スキューY.jpg");
      }
    
      outputImg.save("data/3射影変換.jpg");
    
    ※ 今回は縦横の区別はないので場合分けは不要。

  11. 実行して端点をドラッグして本来長方形のはずの部分が選択されている状態にしてから右クリックする。


  12. この時点で存在するファイル→提出ファイル
    ※ うまくいかないときは最終状態のproject関数とコードを見比べる。

提出

理解度確認テスト予告



戻る inserted by FC2 system