第03回 スキュー

スキューとは、長方形を平行四辺形に歪めるような変形のことをいう。
逆に、平行四辺形を長方形にする変形もこれにあたる。
Processingで直接この変形を行う機能はないので、今回は自前の方法で実装する。

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

プログラムを実行すると下の図のように元画像と赤・青の枠が表示される。
赤がX軸方向のスキュー、青がY軸方向のスキューにあたる。
赤枠の下、青枠の右の辺の中点にある○をドラッグすると、それに合わせて枠の歪み方が変わる。


画面上で右クリックすると、dataフォルダに の3つの画像が作られる。

枠の表示

概要

赤枠は実行画面の80%のサイズの長方形を歪ませたもの。画面の中心を基準にすると、歪んでいない状態では頂点の座標は図のようになる。これをPVector型の変数 c[0]~c[3]に入れる。
実行画面の横・縦の長さの8割をw, hとすると、それぞれの座標は
\(c[0] = (w/2, h/2)\)
\(c[1] = (-w/2, h/2)\)
\(c[2] = (-w/2, -h/2)\)
\(c[3] = (w/2, -h/2)\)
のようになる。


これを傾き \(a\) で歪ませると、下側の2つの頂点 (c[0], c[1]) のx座標は \(ah/2\) 増え、上側の2つの頂点 (c[2], c[3]) のx座標は同じ分だけ減る。
y座標は歪んでも変わらない。


青枠の頂点の座標には、同様にして変数 c[4] ~ c[7] を使う。歪んでいない状態の座標は c[0] ~ c[3] と同じで、右側の2つの頂点 (c[4], c[7]) のy座標は \(bw/2\) 増え、左側の2つの頂点 (c[5], c[6]) のy座標は同じ分だけ減る。
x座標は歪んでも変わらない。


Processingでは、Shapeでこれらをつないでやれば平行四辺形を描ける。
また、赤枠の下の辺、青枠の右の辺の中点の○はellipseで描ける。座標はc[0]とc[3], c[4]とc[7]から求められる。

課題 1

ベースのプログラム (元画像が表示されるだけ)
PImage img;  // 元画像の変数
float scale = 0.8; // 元画像に対するスキュー抽出部分のサイズ比
// 枠の座標
// 0~3 : 赤枠(横スキュー範囲)の右下、左下、左上、右上
// 4~7 : 青枠(縦スキュー範囲)の右下、左下、左上、右上
PVector[] c = new PVector[8]; 
int dragging = -1; // ドラッグ中の点
float a = 0.2; // 赤枠の傾き
float b = 0.2; // 青枠の傾き

void setup() { // (課題1で変更を加える)
  size(600, 450);
  img = loadImage("元.jpg");
  imageMode(CENTER); // 画像描画のときに中心で位置指定する設定に変える
  float w = width * scale;
  float h = height * scale;
  c[0] = new PVector(w/2, h/2);   // 赤の右下
  c[1] = new PVector(-w/2, h/2);  // 赤の左下
  c[2] = new PVector(-w/2, -h/2); // 赤の左上
  c[3] = new PVector(w/2, -h/2);  // 赤の右上
  c[4] = new PVector(w/2, h/2);   // 青の右下
  c[5] = new PVector(-w/2, h/2);  // 青の左下
  c[6] = new PVector(-w/2, -h/2); // 青の左上
  c[7] = new PVector(w/2, -h/2);  // 青の右上
}

void draw() { // (課題1で変更を加える)
  background(0);
  translate(width/2, height/2); // 基準位置を画面中央にする
  image(img, 0, 0, width, height);// 画像を表示
  noFill(); // 図形の中を塗りつぶさない設定にする
  strokeWeight(2);   // 図形の枠の太さを2ptにする

  // 赤枠の描画
  stroke(255, 0, 0); // 線の色を赤にする
  // 下辺中点の○
  // 平行四辺形
  beginShape();
  endShape(CLOSE);

  // 青枠の描画
}

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

// 双一次補間でスキューした画像を保存 (課題2, 3で変更を加える)
void skew(char type) {
}

// cを中心として画像を1/scale倍にして、横にスキューしたときのベクトルfの移動先のベクトルを返す
PVector getXSkewedPosition(PVector c, PVector f) {
  f.mult(scale);
  f.x += img.width * (1-scale)/2;
  f.y += img.height * (1-scale)/2;
  float r = a*height/width*img.width/img.height;
  f.x += (f.y - c.y)*r;
  return f;
}

// cを中心として画像を1/scale倍にして、縦にスキューしたときのベクトルfの移動先のベクトルを返す
PVector getYSkewedPosition(PVector c, PVector f) {
  f.mult(scale);
  f.x += img.width * (1-scale)/2;
  f.y += img.height * (1-scale)/2;
  float r = b*width/height*img.height/img.width;
  f.y += (f.x - c.x)*r;
  return f;
}

void mousePressed() {
  if (mouseButton == LEFT) {
    if (dist(mouseX, mouseY, (c[0].x+c[1].x)/2+width/2, c[0].y+height/2)<5) {
      dragging = 0;
    } else if (dist(mouseX, mouseY, c[4].x+width/2, (c[4].y+c[7].y)/2+height/2)<5){
      dragging = 1;
    }
  }
  if (mouseButton == RIGHT) {
    getScreenShot(); // 実行画面のスクリーンショットを保存
    skew('x');       // 双一次補間で横スキューした画像を保存
    skew('y');       // 双一次補間で縦スキューした画像を保存
  }
}

void mouseDragged() {
  if (dragging != -1) {
    float w = width * scale;
    float h = height * scale;
    if (dragging ==0) {
      float dx = mouseX - width/2;
      c[0] = new PVector(w/2+dx, h/2);   // 赤の右下
      c[1] = new PVector(-w/2+dx, h/2);  // 赤の左下
      c[2] = new PVector(-w/2-dx, -h/2); // 赤の左上
      c[3] = new PVector(w/2-dx, -h/2);  // 赤の右上
      a = dx * 2 / h;
    } else {
      float dy = mouseY - height/2;
      c[4] = new PVector(w/2, h/2+dy);   // 青の右下
      c[5] = new PVector(-w/2, h/2-dy);  // 青の左下
      c[6] = new PVector(-w/2, -h/2-dy); // 青の左上
      c[7] = new PVector(w/2, -h/2+dy);  // 青の右上
      b = dy * 2 / w;
    }
  }
}

void mouseReleased() {
  dragging = -1;
}
枠の初期座標を計算する部分 (setup関数) と、枠を描画する部分 (draw関数) を完成させるのがこの課題のゴール。
  1. Processingのエディタに上のサンプルプログラムのコードをコピー&ペーストする。
  2. 「img03」という名前で保存する。
  3. 適当に画像検索してサンプル用の画像 (縦横の線が含まれていたり、文字が入っているなど、歪んだかどうかがわかりやすいもの) を用意する。
  4. draw関数の「beginShape();」の下に以下のコードを追加して実行する。
  5. (setup関数でc[0]~c[3]には仮の座標を入れてあるので、これでとりあえず長方形が表示されるはず)
      for (int i=0; i<4; i++){
        vertex(c[i].x, c[i].y);
      }

  6. draw関数の「// 下辺中点の○」の下に以下のコードを追加して実行する。
  7.   ellipse((c[0].x+c[1].x)/2, c[0].y, 10, 10);
    (ellipse関数の第1, 2引数が中心の位置、第3, 4引数が横と縦の径をきめる)
    (下の辺の両端はc[0]とc[1]。横位置はこれらの平均、縦位置はどちらも同じ)
    (ここまで書いたときの実行結果はこのようになるはず)


  8. setup関数の c[0] ~ c[3] の値を入れている部分に変更を加え、変数 a に応じて枠が傾くようにする。
  9. (「概要」の2つ目の図を参考にする)
    (それぞれのx座標、つまり第1引数に「a*h/2」を足す、または引く)
    (実行結果はこのようになるはず)


  10. draw関数の「// 赤枠の描画」と「// 青枠の描画」の間のコード (stroke ~ endShape();) を「// 青枠の描画」の下にコピーし、必要な変更を加える。
  11. (色は青にしたい → stroke関数を変更)
    (○は右辺の中点 → c[4]とc[7]の中点、x座標はどちらかをそのまま、y座標はそれぞれのy成分の平均)
    (頂点はc[4]~c[7] → for文では i が4~7に変化するように)
    (実行結果はこのようになるはず)


  12. setup関数の c[4] ~ c[7] の値を入れている部分に変更を加え、変数 b に応じて青枠が傾くようにする。
  13. (「概要」の3つ目の図を参考にする)
    (それぞれのy座標、つまり第2引数に「b*w/2」を足す、または引く)
    (実行結果はこのようになるはず)


  • ○をつかんでドラッグする機能は実装済み。興味のある人はmousePressed関数、mouseDragged関数、mouseReleased関数を参照するとよい。
  • この段階で右クリックすると実行画面が「1選択範囲.jpg」として保存されるが、これは確認用なのでまだ何もしなくてよい。
  • うまくいかない場合は、setup関数の最終状態draw関数の最終状態とコードを見比べる。

X軸方向のスキュー

概要

X軸方向のスキューとは、この時点のプログラムの赤枠のような変形のこと。
Y座標はそのままで、X座標が「Y座標に応じた値」だけ変化する。

\(x' = x + ay\)
\(y' = y\)

(ただし、ここではスキューと「拡大・縮小」を合成した変換を行っている)

課題 2

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

  4. skew関数の「// 出力画像の(i, j)の位置に対応する元画像の位置(i_, j_)を求める」の下の行を以下のように書き換える。
  5. (ここで呼び出している「getXSkewedPosition関数」が、X軸方向のスキューの座標変換後の位置を求める関数。単純なスキューのほかに、基準位置を画像中央に移す処理、実行画面と画像の縦横比が違う場合の補正処理が含まれる)
          PVector ij = getRotatedPosition(new PVector(cx, cy), new PVector(i, j), 1/scale, angle);
          PVector ij = getXSkewedPosition(new PVector(cx, cy), new PVector(i, j));

  6. 実行して適当に赤枠を変形させてから右クリックする。
    • dataフォルダに「2スキューX.jpg」が作られる。
    • (「1選択範囲.jpg」も更新される)
    • 「1選択範囲.jpg」の赤枠の部分が「2スキューX.jpg」の中身になるはず。
    • 枠が右に傾いていれば、「2スキューX.jpg」は左に傾く。
この時点で存在するファイル
  • 元.jpg
  • 1選択範囲.jpg
  • 2スキューX.jpg

Y軸方向のスキュー

概要

Y軸方向のスキューとは、このプログラムの青枠のような変形のこと。
X座標はそのままで、Y座標が「X座標に応じた値」だけ変化する。

\(x' = x\)
\(y' = y + bx\)

(ただし、ここではスキューと「拡大・縮小」を合成した変換を行っている)

課題 3

  1. skew関数のファイル出力の部分を、「type」の値に応じてファイル名が「2スキューX.jpg」「3スキューY.jpg」のどちらかになるように書き換える。
  2. (「type」はこの関数の引数。課題2の時点では未使用だった)
    (skew関数はmousePressed関数から呼び出されている。そのときに「type」の値として渡されるのは'x'か'y'のどちらか)
    ('x'のときは「2スキューX.jpg」、'y'のときは (つまり'x'でないときは) 「3スキューY.jpg」という名前で保存されてほしい)

  3. skew関数の「// 出力画像の(i, j)の位置に対応する元画像の位置(i_, j_)を求める」の下で呼び出される関数が、「type」の値に応じて「getXSkewedPosition関数」「getYSkewedPosition関数」のどちらかになるように書き換える。
  4. (呼び出される関数はどちらも実装済み)
    (変数「PVector ij」の宣言は if ~ else の構造の前に書き、分岐の中では ij への代入を行う)

  5. 実行して赤枠・青枠のどちらも一部が画面外にはみ出るように変形させてから右クリックする。
    • dataフォルダに「3スキューY.jpg」が作られる。
    • (「1選択範囲.jpg」「2スキューX.jpg」も更新される)
    • 「1選択範囲.jpg」の青枠の部分が「3スキューY.jpg」の中身になるはず。
    • 青枠が右上がりなら、「3スキューY.jpg」は右下がりに変形される。
    • 「2スキューX.jpg」「3スキューY.jpg」のどちらにも黒いエリアができるはず。
この時点で存在するファイル → 提出ファイル
  • 元.jpg
  • 1選択範囲.jpg
  • 2スキューX.jpg
  • 3スキューY.jpg

提出

小テスト予告

次回の授業の初めに今回の内容についての小テストを行う。

戻る

inserted by FC2 system