第04回 スキュー

画像をゴムの膜に印刷し、上下に入れた棒をずらして歪ませるような変換をX方向のスキュー、左右をつかんで上下にずらす変換をY方向のスキューという。

X方向のスキューY方向のスキュー
変換式 X' = X + skewX * Y (skewXは定数)
 Y' = Y
 X' = X
 Y' = Y + skewY * X (skewYは定数)

課題1, 2でつくるプログラムの最終的な機能

はじめに、これから作るプログラムが完成した時点の機能をざっと見ておく (今回は横スキュー、縦スキューを行うので、2つのプログラムを作る。課題1, 2で横スキューのプログラムを完成させ、課題3ではそれを別名保存して書き換えて縦スキューのプログラムにする)。


選択範囲の枠を表示する(横スキュー)

まずは上の図のような赤枠を表示したい。

  1. 歪みがないときの枠の4隅の座標
  2. このプログラムでは、選択範囲の大きさは画像全体に対してscale倍にする(scaleの初期値は0.8)。
    また、計算を簡単にするために、座標の基準点は画像の中心とする。

    選択範囲の幅と高さをw, hとすると、歪みがなければ画像の4隅の座標は
    になる。このプログラムでは、これらの頂点の座標をPVector型の配列「c」に入れている。


  3. スキューしたときの枠の4隅の座標
  4. X方向のスキューでは、ずれ幅は「skewX」×「Y座標」にあたる。これをすべての頂点のX座標に加える。

  5. 操作系
  6. キーボードのキーを押すと、keyPressed関数が実行される。このとき、押したキーが「1」「2」ならそれぞれscaleを大きく、小さくし、方向キーの「右」「左」ならskewXを大きく、小さくすればよい。

課題 1

    ベースのプログラム
    PImage img;  // 元画像の変数
    float scale; // 元画像に対するスキュー抽出部分のサイズ比
    float skewX; // 横スキューの比率
    
    void setup() {
      size(800, 600);
      img = loadImage("1元.jpg");
      imageMode(CENTER); // 画像描画のときに中心で位置指定する設定に変える
      scale = 0.8;
      skewX = 0;
    }
    
    void draw() {
      background(0);
      translate(width/2, height/2); // 基準位置を画面中央にする
      image(img, 0, 0, width, height);// 画像を表示
      noFill(); // 図形の中を塗りつぶさない設定にする
      strokeWeight(2);   // 図形の枠の太さを2ptにする
      float w = width * scale;
      float h = height * scale;
      // 枠の右下、左下、左上、右上の座標
      PVector[] c = new PVector[4];
      c[0] = new PVector(0, 0); // 右下
      c[1] = new PVector(0, 0); // 左下
      c[2] = new PVector(0, 0); // 左上
      c[3] = new PVector(0, 0); // 右上
      stroke(255, 0, 0); // 線の色を赤にする
      // 枠の描画
      beginShape();
      for (int i=0; i<4; i++) {
        // skewXに応じて頂点のx座標を変える
        vertex(c[i].x, c[i].y); // 枠の頂点
      }
      endShape(CLOSE);
    }
    
    // 画面の状態を画像として出力
    void getScreenShot() {
      save("data/2選択範囲X.jpg");
    }
    
    // 双一次補間でスキューした画像を保存
    void skew() {
    }
    
    // cを中心として画像を1/scale倍にして、横にスキューしたときのベクトルfの移動先のベクトルを返す
    PVector getSkewedPosition(PVector c, PVector f) {
      f.mult(scale);
      f.x += img.width * (1-scale)/2;
      f.y += img.height * (1-scale)/2;
      float r = skewX*height/width*img.width/img.height;
      f.x += (f.y - c.y)*r;
      return f;
    }
    
    void mousePressed() {
      getScreenShot(); // 実行画面のスクリーンショットを保存
      skew();          // 双一次補間で横スキューした画像を保存
    }
    
    void keyPressed() {
    }
    

  1. Processingのエディタに上のサンプルプログラムのコードをコピー&ペーストする。
  2. 「img04_1」という名前で保存する。
  3. 適当に画像検索してサンプル用の画像(例えば建物など、どのように歪んだかがわかりやすいもの)を用意する。
  4. 画像の形式に応じて以下の変更を加える。
  5. 「1元.jpg」をProcessingのウインドウにドラッグ&ドロップする。
  6. draw関数の以下の部分を、それぞれ右下、左下、左上、右上の頂点に対応する座標になるように書き換える (上の説明の「1.」参照)。
  7.   c[0] = new PVector(0, 0); // 右下
      c[1] = new PVector(0, 0); // 左下
      c[2] = new PVector(0, 0); // 左上
      c[3] = new PVector(0, 0); // 右上
    
    変更してから実行すると、画面より一回り小さい赤枠が表示されるはず。

  8. draw関数の「// skewXに応じて頂点のx座標を変える」の左に以下のコードを追加する。
  9.     c[i].x += skewX * c[i].y;
    

  10. (プログラムの一番下にある)keyPressed関数の中に以下のコードを追加する。
  11.   if (keyCode==RIGHT){
        skewX += 0.01;
      }
    

  12. プログラムを実行して、画面をクリックしてから右方向キーを押し、図の向きに赤枠が歪むことを確認する。


  13. さらにコードを追加し、左方向キーで逆向きに赤枠が歪む機能を追加する。
  14. keyPressed関数の中に以下のコードを追加する。
  15.   if (key=='1'){
        scale += 0.01;
      }
    

  16. プログラムを実行して、画面をクリックしてから1キーを押し、赤枠が全体的に大きくなることを確認する。
  17. さらにコードを追加し、2キーで赤枠が小さくなる機能を追加する。


  18. この時点で存在するファイル
    ※ このあとの課題を進めるにつれて「1選択範囲.jpg」は更新される。この段階では提出用のバックアップなどはしないこと。
    ※ うまくいかないときはdraw関数の最終状態keyPressed関数の最終状態とコードを見比べる。

X方向のスキュー

課題 2

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

  3. skew関数の中の「PVector ij」を求める部分を以下のように書き換える。
  4.       PVector ij = getRotatedPosition(new PVector(cx, cy), new PVector(i, j), 1/scale, angle);
    
          PVector ij = getSkewedPosition(new PVector(cx, cy), new PVector(i, j));
    
    ※ getSkewedPosition関数は「拡大+スキュー前の対応点」を求める自前の関数(実装済)。

  5. skew関数の最後の行のファイル名のところを「3双一次補間.jpg」から「2スキューX.jpg」に変更する。
  6. 実行して選択範囲を適当に歪ませてからクリックする。


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

Y方向のスキュー


課題 3

  1. 課題2が完了した状態のプログラムを上書き保存する(Ctrl+S)。
  2. 「ファイル」→「名前を付けて保存」で「img04_2」という名前でプログラムを保存する。
  3. img04_2のdataフォルダを開き、「1元.jpg」以外の画像を削除する。
  4. ※ 今日提出するプログラムは「img04_1」「img04_2」の2つ。このあと「img04_2」の方を書き換えていくので、完成済みの「img04_1」はそのまま残しておく。
  5. プログラムの先頭(3行目)を以下のように書き換える。
  6. float skewX; // 横スキューの比率
    
    float skewY; // 縦スキューの比率
    

  7. setup関数のエラーになっている部分を「skewY = 0;」に書き換える。
  8. draw関数のエラーになっている部分を以下のように書き換える。
  9.     c[i].x += skewX * c[i].y; // skewXに応じて頂点のx座標を変える
    
        c[i].y += skewY * c[i].x; // skewYに応じて頂点のy座標を変える
    

  10. getScreenShot関数のファイル名の部分を「data/3選択範囲Y.jpg」に書き換える。
  11. skew関数のファイル名の部分を「data/3スキューY.jpg」に書き換える。
  12. getSkewedPosition関数のエラーになっている部分を以下のように書き換える。
  13.   float r = skewX*height/width*img.width/img.height;
      f.x += (f.y - c.y)*r;
    
      float r = skewY/height*width/img.width*img.height;
      f.y += (f.x - c.x)*r;
    
    ※ 上の行はskewXがskewYに、「*」と「/」が逆になる。
    ※ 下の行はxとyが逆になる。

  14. keyPressed関数のエラーになっている部分を書き換え、下キーでskewYが増え、上キーでskewYが減るようにする。

  15. 実行して枠の形を適当に歪ませてからクリックする。
  16. ※ 画像を開いてみると、「2スキューX.jpg」「3スキューY.jpg」のいずれも2つの隅に黒い部分(選択範囲が元画像からはみ出した分)があるはず。

    この時点で存在するファイル→提出ファイル
    ※ うまくいかないときは最終状態のプログラム(先頭~getScreenShot関数)最終状態のskew関数最終状態のプログラム(getSkewedPosition関数~最後)とコードを見比べる。

提出

小テスト予告



戻る inserted by FC2 system