第03回 スキュー

概要

前回のプログラムに変更を加え、元画像の一部を拡大しつつスキューをかけるプログラムを作る。
前回の完成状態のコードが必須なので、まだの場合は先にそちらを完成させる。

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

プログラムを実行すると実行画面上に元画像が表示され、マウスカーソルにくっついて長方形をX方向にスキューした赤枠とY方向にスキューした青枠 (どちらも画面サイズの1/5) が表示される。
さらに実行画面上でクリックすると、dataフォルダに7つの画像が作られる。
コンソールに「完了」が表示されてからキーボードの0~7のキーを押すとこれらの画像が画面に表示される。
(元画像は実行画面の大きさ800x600にリサイズされる)
(クリックだけですべての画像が作成されるので、キーボードを使うのは結果確認のためだけ)
キー 画像 意味 特徴
0 元.jpg 元画像
1 1選択範囲.jpg Processingの実行画面のスクリーンショット 元画像にクリック時の赤枠・青枠がついたもの
2 2描画機能X.jpg Processingの描画機能で赤枠の範囲を画面のサイズに変形させた画像 なめらかに拡大される
上側が左、下側が右に動いたように歪む
3 2描画機能Y.jpg Processingの描画機能で青枠の範囲を画面のサイズに変形させた画像 なめらかに拡大される
左側が上、右側が下に動いたように歪む
4 3最近傍補間X.jpg 最近傍補間を使って赤枠の範囲を画面のサイズに変形させた画像 元の1ピクセルがそのまま拡大されたような粗いドットが見える
上側が左、下側が右に動いたように歪む
5 3最近傍補間Y.jpg 最近傍補間を使って青枠の範囲を画面のサイズに変形させた画像 元の1ピクセルがそのまま拡大されたような粗いドットが見える
左側が上、右側が下に動いたように歪む
6 3双一次補間X.jpg 双一次補間を使って赤枠の範囲を画面のサイズにに変形させた画像 なめらかに拡大される
上側が左、下側が右に動いたように歪む
7 3双一次補間Y.jpg 双一次補間を使って青枠の範囲を画面のサイズにに変形させた画像 なめらかに拡大される
左側が上、右側が下に動いたように歪む
完了時点でdataフォルダにあるファイル

実行例

1. 枠を変形させる

概要

前回の課題では赤枠を回転させて表示していた。
回転させるためにはrotate関数を使ったが、上の実行例のようにゆがませるには shearX関数、shearY関数を使う。
さらに、今回は赤枠と青枠の2つの枠を表示させたい。画面全体にあたる長方形を赤枠の形にするには、初期状態の座標系に対して
  1. 原点をずらす (translate関数)
  2. ゆがませる (shearX関数)
  3. 倍率をかける (scale関数)
  4. 原点をもどす (translate関数)

  5. のような座標変換をかければよい。同様に、青枠の形にするには初期状態の座標系を
  6. 原点をずらす (translate関数)
  7. ゆがませる (shearY関数)
  8. 倍率をかける (scale関数)
  9. 原点をもどす (translate関数)
のように変換する必要がある。ただし、4の時点で座標系は初期状態のものとは大きく異なっているので、そのまま続けて5~8の変換赤枠を行っても青枠を描画するための座標系にはならない。

そこで、その時点の座標系の状態を記憶するpushMatrix関数と、記憶した座標系の状態を復元するpopMatrix関数を使い、 のようにしてやれば、実行例のような赤枠と青枠が表示される。

課題 1

  1. Processingを起動する。
  2. 前回提出した、完成状態のコードをコピー&ペーストする。
  3. 「img03」という名前で保存する。
  4. 適当に画像検索して元画像を用意する。
  5. 画像の形式に応じて以下の変更を加える。
    • JPG形式の場合→名前を元.jpgに変更
    • それ以外の場合→ペイントで開き、形式をJPGに指定して「元」という名前で保存
  6. 「元.jpg」をProcessingのウインドウにドラッグ&ドロップする。
  7. mousePressed関数の先頭に以下のコードを追加する。
  8. if (created) return; // 画像作成後なら何もしない
    (画面を複数回クリックした場合の誤動作を避けるため)

  9. プログラム先頭部分の
    PImage[] img = new PImage[5];
    // 出力ファイル名
    String[] fName = {"元", "1選択範囲", "2描画機能", "3最近傍補間", "3双一次補間"};
    PImage[] img = new PImage[8];
    // 出力ファイル名
    String[] fName = {"元", "1選択範囲", "2描画機能X", "2描画機能Y", "3最近傍補間X", "3最近傍補間Y", "3双一次補間X", "3双一次補間Y"};
    に変更する。
    (出力画像の数が増えるので、配列imgの要素数もそれに合わせて変える)

  10. プログラム先頭部分の
    float a = PI/6; // 選択範囲の傾き(30°)
    float a = radians(20); // 選択範囲の傾き(20°)
    に変更する。
    (このように、radians関数を使えば単位を度からラジアンに換算した値が得られる。角度を30°から20°にしたことに深い意味はない)

  11. setup関数の
      // 1~4番を元画像と同じサイズの黒画像にする
      for (int i=1; i<=4; i++) {
      // 1~7番を元画像と同じ画像にする
      for (int i=1; i<=7; i++) {
    に変更する (実は前回までのここのコメント文は誤り。実際には黒画像ではなく画像がコピーされる)。

  12. draw関数の
        rotate(-a);        // 座標系を-aだけ回転
        shearX(-a);        // 座標系を横に-aだけスキュー
    に変更する。

  13. プログラムを実行する。
    • マウスカーソルを動かすと、図のような平行四辺形の赤枠がカーソルにくっついて表示される。
    • まだクリックはしないこと。
  14. setup関数にある「stroke(255, 0, 0); // 図形の枠の色を赤にする」をdraw関数の「rect(0, 0, width, height); // 元画像サイズの長方形を描く」の上に移動させる。
    (これは、このあと続けて青枠描画のための処理を追加するための準備)
  15. draw関数に図のような命令を追加する。
    (青枠描画のための座標変換を、初期状態をベースにして行うための準備)
  16. draw関数の赤枠の部分のコードをコピーし、「popMatrix(); // 座標系を元の状態に復元する」のすぐ下にペーストする。
    (追加した分をこのあとで書き換えて青枠描画用の命令に変える。もとの赤枠描画用の命令はそのまま残す)
  17. コピーされた下側の命令文のセットのうち
        shearX(-a);        // 座標系を横に-aだけスキュー
        shearY(-a);        // 座標系を縦に-aだけスキュー
    に変更する。

  18. 同様に、コピーされた下側の命令文のセットのうち
        stroke(255, 0, 0); // 図形の枠の色を赤にする
        stroke(0, 0, 255); // 図形の枠の色を青にする
    に変更する。

  19. プログラムを実行する。
    • マウスカーソルを動かすと、さっきの赤枠に加えて図のような形の平行四辺形の青枠がカーソルにくっついて表示される。
    • まだクリックはしないこと。

2. Processingの描画機能を使った拡大機能にスキューを加える

概要

前回のプログラムのdrawToImage関数では、translate関数, scale関数, rotate関数でカーソル位置を基準とした拡大・回転を実装していた。
今回は回転は行わないのでrotate関数をなくし、かわりにshearX関数、shearY関数を使う。
出力する画像がX方向とY方向のスキューの2種類あるので、drawToImage関数で「どちらにスキューさせるか」を引数として受け取れるように変更し、それに応じて変形のさせかたを使い分ける。

課題 2

  1. プログラムの下の方にあるmousePressed関数の
      drawToImage(s, a, 2);     // 描画機能でs倍に拡大、aだけ回転した画像をimg[2]に保存
      drawToImage(s, a, 2, "X");     // 描画機能でs倍に拡大、aだけX方向にスキューした画像をimg[2]に保存
      drawToImage(s, a, 3, "Y");     // 描画機能でs倍に拡大、aだけY方向にスキューした画像をimg[3]に保存
    に変更する。
    (まだdrawToImage関数に変更を加えていないので、この時点ではエラーになる)

  2. mousePressed関数の
      nearestNeighbor(s, a, 3); // 最近傍補間でs倍に拡大、aだけ回転した画像をimg[3]に保存
      bilinear(s, a, 4);        // 双一次補間でs倍に拡大、aだけ回転した画像をimg[4]に保存
    
    をコメントアウトする (それぞれの先頭に「//」をつける)。
    (課題3でこれらの処理にも変更を加える。そのままだとdrawToImage関数で作った3番目の画像がnearestNeighbor関数で上書きされてしまうので、この時点では無効にしておく)

  3. drawToImage関数の
    void drawToImage(float s, float a, int n) {
    void drawToImage(float s, float a, int n, String direction) {
    に変更する。
    (directionは方向を受け取るための引数。さっき書き換えたmousePressed関数からの呼び出しでは"X"か"Y"が渡される)
    (これでmousePressed関数の方のエラーが解決する)

  4. drawToImage関数の
      pg.rotate(a);            // (pg)aだけ回転
      if (direction == "X") {
        pg.shearX(a);          // (pg)X方向にaだけスキュー
      } else {
        pg.shearY(a);          // (pg)Y方向にaだけスキュー
      }
    
    に変更する。
    (こうして場合分けすることでdirectionの値によってスキューの方向を変える)

  5. プログラムを実行し、適当な場所が選択されている状態でクリックする。
    • コンソールに「完了」が表示されてから1キーを押すと赤枠と青枠が元画像に重なった画像、2, 3キーを押すとそれぞれ赤枠部分、青枠部分を元画像のサイズの長方形に変形した画像が表示される。

3. 自前のスキュー処理を実装する

概要

第1回、第2回と同様に最近傍補間、双一次補間による変換では、座標変換を行うgetScaledPosition関数をnearestNeighbor関数とbilinear関数から呼び出して使う。
getScaledPosition関数では、PVector型のベクトル変数に対してスキューの操作を行わなければならないが、座標変換とは異なりshearX関数、shearY関数は使えない。

X方向のスキューでは、ベクトルのY座標は変化せず、X座標はもとのY座標の値に比例した値が加わる。角度(-a)のスキューでは、その係数は\(-\tan(a)\) である。つまりベクトルfのx座標とy座標は
\(f.x → f.x - f.y * \tan(a)\)
\(f.y → f.y\)
のように変わる。同様に、Y方向の角度(-a)のスキューではX座標は変化せず、X座標はもとのY座標の値に比例した値が加わり、変換後の座標は
\(f.x → f.x\)
\(f.y → f.y- f.x * \tan(a)\)
となる。

あとはgetScaledPosition、nearestNeighbor、bilinearの3つの関数で「String direction」を受け取れるようにし、その値に応じて処理を使い分けるだけ。

課題 3

  1. nearestNeighbor関数の
    void nearestNeighbor(float s, float a, int n) {
    void nearestNeighbor(float s, float a, int n, String direction) {
    に変更する。

  2. bilinear関数の
    void bilinear(float s, float a, int n) {
    void bilinear(float s, float a, int n, String direction) {
    に変更する。

  3. getScaledPosition関数の
    // cを中心として画像を1/s倍して-a回転させたときのベクトルfの移動先のベクトルを返す(課題3で使用)
    PVector getScaledPosition(PVector c, PVector f, float s, float a) {
    // cを中心として画像を1/s倍して-aスキューさせたときのベクトルfの移動先のベクトルを返す(課題3で使用)
    PVector getScaledPosition(PVector c, PVector f, float s, float a, String direction) {
    に変更する。
    (コメント文の修正も忘れずに)

  4. getScaledPosition関数の
      f.rotate(-a); // fを-a回転させる
      if (direction == "X"){
        f.x -= f.y * tan(a);
      } else {
        f.y -= f.x * tan(a);
      }
    
    に変更する。

  5. mousePressed関数の
      //  nearestNeighbor(s, a, 3); // 最近傍補間でs倍に拡大、aだけ回転した画像をimg[3]に保存
      //  bilinear(s, a, 4);        // 双一次補間でs倍に拡大、aだけ回転した画像をimg[4]に保存
    
      nearestNeighbor(s, a, 4, "X"); // 最近傍補間でs倍に拡大、aだけX方向にスキューした画像をimg[4]に保存
      nearestNeighbor(s, a, 5, "Y"); // 最近傍補間でs倍に拡大、aだけY方向にスキューした画像をimg[5]に保存
      bilinear(s, a, 6, "X");        // 双一次補間でs倍に拡大、aだけX方向にスキューした画像をimg[6]に保存
      bilinear(s, a, 7, "Y");        // 双一次補間でs倍に拡大、aだけY方向にスキューした画像をimg[7]に保存
    に変更する。
    (コメント文の修正も忘れずに)

  6. nearestNeighbor関数、bilinear関数の
          PVector pos = getScaledPosition(new PVector(cx-0.5, cy-0.5), new PVector(i, j), s, a);
          PVector pos = getScaledPosition(new PVector(cx-0.5, cy-0.5), new PVector(i, j), s, a, direction);
    に変更する (どちらの関数にもこのコードがあるので、両方変更する)。

  7. keyPressed関数の
    if (k>=0 && k<=4) {
    if (k>=0 && k<=7) {
    に変更する (7番目の画像の表示にまで対応させるため)。

  8. プログラムを実行し、適当な場所が選択されていて赤枠と青枠が画面に収まっている状態でクリックする。
    • コンソールに「完了」が表示されてから4キー, 5キーを押すと最近傍補間で赤枠と青枠の部分が変形された画像、6キーと7キーを押すと双一次補間で赤枠と青枠の部分が変形された画像が表示される。
    • 2, 4, 6キーを続けて押すと赤枠部分のProcessingの機能、最近傍補間、双一次補間による変換のされかたの違いを確認できる。これまでと同様に、最近傍補間では荒いドットが見える。
    • 3, 5, 7キーを続けて押すと青枠部分のProcessingの機能、最近傍補間、双一次補間による変換のされかたの違いを確認できる。これまでと同様に、最近傍補間では荒いドットが見える。
    • 赤枠か青枠が画面からはみ出した状態でクリックするとエラーになってしまう。エラーが出ないようにするにはnearestNeighbor関数、bilinear関数に例外処理を追加すればよいが、具体的な方法はここでは省略する。

提出

締切後提出用フォーム
※ 点数はつきますが、欠席だった場合は出席にはなりません。
※ コードはTeamsのClass Notebookに保存してください。
戻る