第01回 拡大・縮小

概要

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

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

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

実行例

2キー, 4キーで表示される画像はどちらも滑らかになっている。
3キーで表示される画像は元画像の1ピクセルを5×5ピクセルにそのまま拡大しているためブロック状の塊が見える。

1. Processingの描画機能を使う

概要

Processingでは、translate関数で原点を平行移動させ、scale関数で倍率を変更できる。
原点を右に cx, 下に cy だけ移動させる命令は
translate(cx, cy);

スケールを s 倍にする命令は
scale(s);

である。 (cx, cy) の点を中心にして画像を s倍に拡大したければ、 という手順を実行すればよい。

課題 1

ベースのプログラム
(drawToImage, nearestNeighbor, bilinearはどれも元画像をs倍に拡大したものをn番目の画像に保存する関数)
(課題1でdrawToImage関数を完成させる)
(課題2でnearestNeighbor関数, 課題3でbilinear関数を作るが、この時点では未実装)
// 画像用の変数
PImage[] img = new PImage[5];
// 出力ファイル名
String[] fName = {"元", "選択範囲", "1描画機能", "2最近傍補間", "3双一次補間"};
int w, h;
boolean created = false; // 画像を作ったかどうかのフラグ
float cx, cy; // 拡大の基準位置
float s = 5;  // 拡大の倍率

void setup() {
  size(800, 600);
  img[0] = loadImage("元.jpg");
  img[0].resize(width, height);
  w = width;
  h = height;
  // 1~4番を元画像と同じサイズの黒画像にする
  for (int i=1; i<=4; i++){
    img[i] = img[0].get();
  }
  textFont(createFont("MS Pゴシック", 48));
  noFill(); // 図形の中を塗りつぶさない設定にする
  strokeWeight(2*s); // 図形の枠の太さを実質2ptにする
  stroke(255, 0, 0); // 図形の枠の色を赤にする
}

void draw() {
  cx = mouseX; // 拡大基準の横位置の更新
  cy = mouseY; // 拡大基準の縦位置の更新
  // 画像作成前 (選択範囲に枠を描画する)
  if (!created) {
    background(0);
    image(img[0], 0, 0, width, height);// 画像を表示
    translate(cx, cy); // 原点をカーソル位置に移動
    scale(1/s);        // 拡大率の逆数のスケールをかける
    translate(-cx, -cy); // 枠内での中心位置移動
    rect(0, 0, width, height); // 元画像サイズの長方形を描く
  }
}

// 実行画面のスクリーンショットを保存する
void saveScreenshot(int n) {
  loadPixels(); // 実行画面の情報をpixelsにロード
  img[n].pixels = pixels; // pixelsの情報をimg[n]にコピー
  img[n].save("data/" + fName[n] + ".jpg");
}

// Processingの描画機能を使って拡大(課題1)
void drawToImage(float s, int n) {
  PGraphics pg = createGraphics(w, h);
  pg.beginDraw();
  // (pg)原点を(cx, cy)だけ平行移動
  // (pg)sだけスケールをかける
  // (pg)原点を(-cx, -cy)だけ平行移動
  pg.image(img[0], 0, 0);
  pg.endDraw();
  img[n].pixels = pg.pixels;
  img[n].save("data/"+ fName[n] +".jpg");
}

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

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

// 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 mousePressed(){
  saveScreenshot(1);     // 実行画面をimg[1]に保存
  drawToImage(s, 2);     // 描画機能でs倍に拡大した画像をimg[2]に保存
  nearestNeighbor(s, 3); // 最近傍補間でs倍に拡大した画像をimg[3]に保存
  bilinear(s, 4);        // 双一次補間でs倍に拡大した画像をimg[4]に保存
  println("完了");
  created = true;
}

void keyPressed() {
  int k = key-'0';
  if (k>=0 && k<=4) {
    background(0);
    image(img[k], 0, 0, width, height);
    fill(255, 0, 0);
    text(fName[k], 30, height-30);
    fill(255);
  }
}
  1. Processingのエディタに上のコードをコピー&ペーストする。
  2. 「ファイル」→「名前を付けて保存」で、適当な場所に「img01」という名前で保存する。
  3. 適当に画像検索してサンプル用の画像を用意する (どこを拡大したかがわかるように、例えば文字のような細かい構造がどこかに含まれるものにする)。
  4. 画像の形式に応じて以下の変更を加える。
    • JPG形式の場合→名前を元.jpgに変更
    • それ以外の場合→ペイントで開き、形式をJPGに指定して「元」という名前で保存
  5. 「元.jpg」をProcessingのウインドウにドラッグ&ドロップする。
  6. drawToImage関数の「// (pg)原点を(cx, cy)だけ平行移動」のところに、対応するコードを入れる。
    • 概要の説明にある translate関数を使う。
    • ただし、仮想的描画対象「pg」に適用させるため、先頭に「pg.」をつける。
  7. drawToImage関数の「// (pg)sだけスケールをかける」のところに、対応するコードを入れる。
    • 概要の説明にある scale関数を使う。
    • ただし、仮想的描画対象「pg」に適用させるため、先頭に「pg.」をつける。
  8. drawToImage関数の「// (pg)原点を(-cx, -cy)だけ平行移動」のところに、対応するコードを入れる。
    • 2行前のコードを再利用すると楽。
  9. プログラムを実行し、拡大したい場所をクリックする。
    • そのあとで0キーを押すと元画像が表示される。
    • 1キーを押すとクリック時の状態 (赤枠つき) が表示される。
    • 2キーを押すと赤枠部分が拡大された画像が表示される。

2. 最近傍補間

概要

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

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

課題 2

  1. (いまは空っぽになっている) nearestNeighbor関数の中に、drawToImage関数の最後の1行をコピー&ペーストする。
  2. nearestNeighbor関数の最後の行の前に、ピクセルの色を決めるための2重ループを追加する (この時点で図のようになる)。
  3. ループの中 (iに関わるfor文の下) に以下のコメント文を入れる (コピペ可)。
  4.       // 出力画像の(i, j)の位置に対応する元画像の位置(i0, j0)を求める
          // 出力画像の(i, j)の点の色を元画像の(i0, j0)の色にする

  5. 「// 出力画像の(i, j)の位置に対応する元画像の位置(i0, j0)を求める」の下に、以下のコードを入れる (コピペ可)。
  6.       PVector pos = getScaledPosition(new PVector(cx-0.5, cy-0.5), new PVector(i, j), s);
          int i0 = round(pos.x);
          int j0 = round(pos.y);
    • getScaledPositionは、このプログラムの下の方にある座標変換の関数。
    • 第1引数が拡大の基準位置だが、ピクセルの中心に合わせるため横・縦それぞれ0.5を引いてある。
    • 第2引数が出力画像の座標、第3引数が拡大の倍率。
    • 座標変換の結果を入れた「pos」はベクトル型の変数で、横位置と縦位置の両方の情報を含む。
    • 「pos.x」「pos.y」が元画像の対応点の横と縦の位置にあたる。ただし、値はfloat型。
    • 「round」は四捨五入の関数。これを使うことで(i0, j0)が「対応点に一番近い元画像のピクセルの位置」になる。

  7. 「// 出力画像の(i, j)の点の色を元画像の(i0, j0)の色にする」の下に、以下のコードを入れる (コピペ可)。
  8.       img[n].pixels[i+j*w] = img[0].pixels[i0+j0*w];
    • ピクセルの色情報は「画像変数.pixels」という配列に格納されている。
    • 元画像の変数は img[0]、ここで作る出力画像の変数は img[n]。
    • 配列の番号は図のような通し番号になる。つまり、番号は1つ右にずれると1増え、1つ下にずれると画像横方向のピクセル数分増える。
      そのため、例えば図のような横15ピクセル、縦10ピクセルの画像の左から8番目、上から6番目のピクセルの番号は (0, 0)のピクセルからの横ずれにより8増え、縦ずれにより15×6=90増えるため98となる。
      一般的に幅 w の画像なら、(i, j)のピクセルの番号は「i+j*w」になる。

  9. プログラムを実行し、拡大したい場所をクリックする。
    • 3キーを押すとこの課題で作られた画像が表示される。
    • 2キーと3キーを交互に押してみると課題1での結果との違いがわかる。粗いブロック状になるのは、出力側の5×5個のピクセルが元画像の同じ点に対応してしまうため。

3. 双一次補間

概要

最近傍補間で画像を拡大すると、拡大率が大きいときはジャギーが目立ってしまう。
対応点を囲むピクセルの色の情報を組み合わせて使うと、出力側の色の変化をもっと滑らかにできる。その際に、対応点を囲む4つのピクセルからの縦横の距離に応じて重みを付ける方法を双一次補間 (Bilinear) 法という。
例えば対応点の座標が (16.7, 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なら右側の点と同じになる)

さらに、点Aと点Bのピクセルの色を混ぜて点Cの色を決める。
この場合は上下の色を「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. bilinear関数から以下の2行を削除する。
  3.       // 出力画像の(i, j)の点の色を元画像の(i0, j0)の色にする
          img[n].pixels[i+j*w] = img[0].pixels[i0+j0*w];
    • 双一次補間では特定のどれかのピクセルの色ではなく、4つのピクセルの色を混ぜる。前の関数の名残はここで消しておく。

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

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

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

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

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

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

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

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

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

  18. プログラムを実行し、拡大したい場所をクリックする。
    • 4キーを押すとこの課題で作られた画像が表示される。
    • こんどは課題2とは異なり、滑らかに拡大できているはず。

提出

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

戻る