第01回 拡大・縮小

Processingにはサイズを指定して画像を表示する機能があるので、それを利用すれば簡単に拡大・縮小した画像を作れる。
それとは別に、変換後の画像の各ピクセルの色を計算し、自前で画像を作る方法を考える。

Processingの描画機能を使う

概要

Processingのimage関数で横幅と高さを指定すれば、画像を拡大・縮小して表示することができる。
image(画像の変数, 画像左端の表示横位置, 画像上端の表示縦位置, 画像の表示横幅, 画像の表示縦幅);

元の画像の(cx, cy)の点を中心にして画像を拡大し、元画像と同じ範囲(黒い範囲)に収まる部分だけを残した画像を作りたければ、下の図のように赤い範囲に画像を拡大して表示すればいいことになる。


拡大画像の左端の横位置は「0から右にcx進み、左にcx*s戻った位置」→ -cx*(s-1)
拡大画像の上端の縦位置は「0から下にcy進み、上にcy*s戻った位置」→ -cy*(s-1)

また、
表示するときの横幅は「元画像の横幅wのs倍」→ w*s
表示するときの縦幅は「元画像の縦幅hのs倍」→ h*s

課題 1

ベースのプログラム (マウスカーソルに白丸がくっついて表示される。これが拡大の基準点になる)
PImage img; // 元画像の変数

void setup() {
  size(600, 400);
  textAlign(CENTER, CENTER);
  img = loadImage("元.jpg");
}

void draw() {
  background(255);
  image(img, 0, 0, width, height);// 画像を表示
  fill(255);
  ellipse(mouseX, mouseY, 10, 10);
}

// Processingの描画機能を使って拡大(課題1)
void drawToImage(float s) {
  int w = img.width;
  int h = img.height;
  float cx = mouseX*w/width;  // 画像サイズに換算した拡大基準の横位置
  float cy = mouseY*h/height; // 画像サイズに換算した拡大基準の縦位置
  PImage outputImg = img.get(); // 出力画像
  PGraphics pg = createGraphics(w, h);
  pg.beginDraw();
  // ここに「拡大画像の描画」の命令を書く
  pg.endDraw();
  outputImg.pixels = pg.pixels;
  outputImg.save("data/1描画機能.jpg");
}

// 最近傍補間(課題2)
void nearestNeighbor(float s) {
}

// 双一次補間(課題3)
void bilinear(float s) {
}

// cを中心として画像を1/s倍したときのベクトルfの移動先のベクトルを返す(課題2, 3で使用)
PVector getScaledPosition(PVector c, PVector f, float s) {
  f.sub(c); // fからcを引く
  f.div(s); // fを1/s倍する
  f.add(c); // fにcを加える
  return f;
}

void keyPressed() {
  drawToImage(10);     // 描画機能で10倍に拡大
  nearestNeighbor(10); // 最近傍補間で10倍に拡大
  bilinear(10);        // 双一次補間で10倍に拡大
}
  1. Processingのエディタに上のサンプルプログラムのコードをコピー&ペーストする。
  2. 「ファイル」→「名前を付けて保存」で、適当な場所に「img01」という名前で保存する。
  3. 適当に画像検索してサンプル用の画像(デジカメで撮った写真のようなもの)を用意する。
  4. 画像の形式に応じて以下の変更を加える。
    • JPG形式の場合→名前を元.jpgに変更
    • それ以外の場合→ペイントで開き、形式をJPGに指定して「元」という名前で保存
  5. 「元.jpg」をProcessingのウインドウにドラッグ&ドロップする。
  6. drawToImage関数の「// ここに「拡大画像の描画」の命令を書く」のところに、対応するコードを入れる。
    • 書式は「pg.image(???, ???, ???, ???, ???);」という形。???に入るものは上の説明を参考にして考える。

  7. 実行し、ウインドウのどこかで一度クリックしてから、拡大したい場所にマウスポインタを置いてキーボードのキー(どれでもよい)を押す。
    (dataフォルダに「1描画機能.jpg」が作られる)

    • 選んだ位置を基準として10倍に拡大した状態のものになる。画像の解像度は元画像と同じ。
ここで作る提出物
  • 元.jpg
  • 1描画機能.jpg

最近傍補間

概要

今度は出力画像のピクセルの色を自前で計算する方法で拡大してみる。しかし、たとえば出力画像の左からi番目、上からj番目のピクセルに対応する元画像の点は、(特別な場合を除けば)どのピクセルにも対応しない半端な点になる。「元画像の左から1.7番目、上から1.8番目」のようなピクセルは存在しないので、なんらかの工夫が必要になる。


最近傍補間 (Nearest neighbor) 法と呼ばれる方法では、元画像でこの対応点にもっとも近いピクセルの色を出力画像のピクセルの色として採用する。つまり、出力画像の
	(i, j)
		
に対応する元画像の座標
	(i', j')
		
が、例えば下の図のように(16.8, 24.3)だった場合に、この周りの4つのピクセルのうち、最も近い(17, 24)のピクセルの色を使う方法である。使う座標は単純に i', j' をそれぞれ四捨五入するだけで求められる。

課題 2

  1. (いまは空っぽになっている) nearestNeighbor関数の中に、drawToImage関数の最初の5行と最後の1行をコピー&ペーストする (図のようになる)。


  2. nearestNeighbor関数の最後の行のファイル名のところを「data/1描画機能.jpg」から「data/2最近傍補間.jpg」に変更する。
  3. nearestNeighbor関数の最後の行の前に、ピクセルの色を決めるための2重ループを追加する。

  4. (iが横方向、jが縦方向の繰り返し)

  5. ループの中に以下のコメント文を入れる (コピペ可)。
  6.       // 出力画像の(i, j)の位置に対応する元画像の位置(i_, j_)を求める
          // 出力画像の(i, j)の点の色を元画像の(i_, j_)の色にする

  7. 「// 出力画像の(i, j)の位置に対応する元画像の位置(i_, j_)を求める」の下に、以下のコードを入れる (コピペ可)。
  8.       PVector ij = getScaledPosition(new PVector(cx, cy), new PVector(i, j), s);
          int i_ = round(ij.x);
          int j_ = round(ij.y);
    • getScaledPositionは、このプログラムの下の方にある座標変換の関数。
    • その関数で計算した結果を入れた「ij」はベクトル型の変数で、横位置と縦位置の両方の情報を含む。
    • 「ij.x」「ij.y」が元画像の対応点の横と縦の位置にあたる。ただし、値はfloat型。
    • 「round」は四捨五入の関数。これを使うことで(i_, j_)が「対応点に一番近い元画像のピクセルの位置」になる。

  9. 「// 出力画像の(i, j)の点の色を元画像の(i_, j_)の色にする」の下に、以下のコードを入れる (コピペ可)。
  10.       outputImg.pixels[i+j*w] = img.pixels[i_+j_*w];
    • ピクセルの色情報は「画像変数.pixels」という配列に格納されている。
    • 配列の番号は図のような通し番号になる。つまり、番号は「1つ右にずれると1増える」「1つ下にずれると「横幅分」増える」ことになる。そのため、(i, j)のピクセルの情報はこの配列の「i+j*w」番目に入る。


  11. 実行し、ウインドウのどこかで一度クリックしてから、拡大したい場所にマウスポインタを置いてキーボードのキー(どれでもよい)を押す。
    (dataフォルダに「2最近傍補間.jpg」が作られる (「1描画機能.jpg」も更新される))

    • 実際に画像を開いて「1描画機能.jpg」と「2最近傍補間.jpg」を比べてみると、「2最近傍補間.jpg」の方は粗いブロック状になっているはず。これは、出力側の10×10個のピクセルが元画像の同じ点に対応してしまうため。
    1描画機能.jpg2最近傍補間.jpg
ここで作る提出物
  • 2最近傍補間.jpg

双一次補間

概要

最近傍補間で画像を拡大すると、拡大率が大きいときはジャギーが目立ってしまう。
対応点を囲むピクセルの色の情報を組み合わせて使うと、出力側の色の変化をもっと滑らかにできる。その際に、対応点を囲む4つのピクセルからの縦横の距離に応じて重みを付ける方法を双一次補間 (Bilinear) 法という。
例えば、対応点の位置が下の図のようになっている場合は、(16, 24)の点の影響の方がその他の3点よりも大きくなる。


もう少し一般的に考えると、下の図のように対応点の位置(x, yはいずれも0以上1未満)を表したとき、正方形の中の点の色の変化が縦横の移動量に比例するものとすれば、


図の点Aは左上と右上のピクセルの色、点Bは左下と右下のピクセルの色を「1-x : x」で混ぜた色になる。
「点Aの色」 = (1-x)「左上の色」 + x「右上の色」
「点Bの色」 = (1-x)「左下の色」 + x「右下の色」
(xが0ならA, Bの色は左側の点と同じになり、xが1なら右側の点と同じになる)


さらに、点CはAとBのピクセルの色を「1-y : y」で混ぜた色になる。
「点Cの色」 = (1-y)「点Aの色」 + y「点Bの色」
(yが0ならCの色は点Aと同じになり、yが1なら点Bと同じになる)


つまり、このピクセルの色は
(1-y){(1-x)(左上の色) + x(右上の色)} + y{(1-x)(左下の色) + x(右下の色)}
となる。

課題 3

  1. nearestNeighbor関数の中のコードをbilinear関数の中にコピー&ペーストする。
  2. ファイル出力の部分を以下のように書き換える。
    • 「data/2最近傍補間.jpg」→「data/3双一次補間.jpg」

  3. bilinear関数から以下の2行を削除する。
  4.       // 出力画像の(i, j)の点の色を元画像の(i_, j_)の色にする
          outputImg.pixels[i+j*w] = img.pixels[i_+j_*w];
    • 双一次補間では特定のどれかのピクセルの色ではなく、4つのピクセルの色を混ぜる。前の関数の名残はここで消しておく。


  5. 3で削除した部分に以下のコメント文を追加する (コピペ可)。
  6.       // 対応点の左上の点の色
          // 対応点の右上の点の色
          // 対応点の左下の点の色
          // 対応点の右下の点の色
          // 対応点の左上のピクセルからの横、縦のずれ
          // 対応点の色の赤成分
          // 対応点の色の緑成分
          // 対応点の色の青成分
          // (r, g, b)から色を作り、それを出力画像の(i, j)の点の色とする

    この時点でbilinear関数は以下のようになっているはず。


  7. ijからi_, j_を作る部分を以下のように書き換える。
  8.       int i_ = round(ij.x);
          int j_ = round(ij.y);
          int i_ = int(ij.x);
          int j_ = int(ij.y);
    • int関数では小数部分を切り捨てて整数化される。
    • つまり、(i_, j_)は元画像の対応点の左上のピクセルの座標になる。

  9. 「// 対応点の左上の点の色」の左に以下のコードを追加する。
  10.         color clt = img.pixels[i_+j_*w];

  11. 同様にして、その下の3行でcolor型の変数crt, clb, crbを作り、然るべき値を入れる。
    • これらの変数名は「color」→「left」か「right」→「top」か「bottom」の略。
    • 基本はcltと同じ。
    • 位置に応じて「i_」のところが「i_+1」、「j_」のところが「(j_+1)」になる。
    • (「j_+1」にカッコがつくのは、「wをかけること」よりも「j_に1を足すこと」を優先させるため)

  12. 「// 対応点の左上のピクセルからの横、縦のずれ」の下に以下のコードを追加する。
  13.       float x = ij.x-i_;
          float y = ij.y-j_;
    • これがまさに上の説明でいうx, yにあたる。

  14. 「// 対応点の色の赤成分」の左に以下のコードを追加する。
  15.       float r = (1-y)*((1-x)*red(clt)+x*red(crt))+y*((1-x)*red(clb)+x*red(crb));
    • red関数は色の変数から赤成分を取り出す関数。同様にgreen関数、blue関数で緑と青の成分を取り出せる。
    • これが上の説明でいうx, yの値に応じて左上、右上、左下、右下のピクセルの色(の赤成分)を混ぜる工程。

  16. 同様にして「// 対応点の色の緑成分」「// 対応点の色の青成分」の左に、然るべき値をfloat型の変数gとbに入れるコードを追加する。

  17. 「// (r, g, b)から色を作り、それを出力画像の(i, j)の点の色とする」の左に以下のコードを追加する。
  18.       outputImg.pixels[i+j*w] = color(r, g, b);

  19. 実行し、ウインドウのどこかで一度クリックしてから、拡大したい場所にマウスポインタを置いてキーボードのキー(どれでもよい)を押す。
    (dataフォルダに「3双一次補間.jpg」が作られる (「1描画機能.jpg」「2最近傍補間.jpg」も更新される))

    • 「3双一次補間.jpg」を開いてみると、「2最近傍補間.jpg」のような粗いブロックはできず、ほぼ「1描画機能.jpg」と同じ (位置だけがちょっとずれる) になっているはず。
    2最近傍補間.jpg3双一次補間.jpg
ここで作る提出物
  • 3双一次補間.jpg

提出

小テスト予告

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

戻る

inserted by FC2 system