第09回 様々なフィルタ処理

今回はこれまで触れなかった様々なフィルタ処理について紹介する。

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

プログラムを実行すると実行画面上に元画像が表示される。
さらに実行画面上でクリックすると、dataフォルダに の5つの画像が作られる。
コンソールに「完了」が表示されてからキーボードの0~5のキーを押すとこれらの画像が画面に表示される。
(画像サイズが大きい場合は出力画像が作られるまでに時間がかかる)
(クリックだけですべての画像が作成されるので、キーボードを使うのは結果確認のためだけ)
キー画像
0元.jpg
11エンボス1.png
21エンボス2.png
32ブラー横.jpg
42ブラー斜め.jpg
53セピア.jpg

「1エンボス1.png」「1エンボス2.png」は縦横サイズが元画像よりそれぞれ1ピクセル, 2ピクセルずつ小さくなる。
「2ブラー横.jpg」「2ブラー斜め.jpg」は縦横サイズが元画像より10ピクセルずつ小さくなる。

エンボス加工

概要

エンボス処理は、「元画像をグレースケール化したもの」と「それをネガポジ反転したもの」をちょっとずらして合成することで行われる。
(実際はこんなにずらさずに、数ピクセルだけずらす)


このように右下方向にずらす場合は、「白い部分が盛り上がっていて、左上から光が当たっているように見える画像」になる。もちろん、この見た目上の凹凸は実際の被写体の凹凸とは一致しない。
また、この2つの画像が重なる分だけを出力すると、画像の縦横の幅はずらし幅分だけ小さくなる。
なお、この処理はこれまでのフィルタのようにマスクを使って行うこともできるが、ここでは処理を簡単にするために直接明るさを計算する。

課題 1

ベースのプログラム (クリックしても黒い画像が作られるだけ)
// 出力用画像の変数
PImage[] img = new PImage[6];
// 出力ファイル名
String[] fName = {"1エンボス1", "1エンボス2","2ブラー横","2ブラー斜め", "3セピア"};
int w, h;

void setup() {
  size(600, 450);
  img[0] = loadImage("元.jpg");
  w = img[0].width;
  h = img[0].height;
  background(0);
  image(img[0], 0, 0, width, height);
}

void draw() {
}

// エンボス加工した画像を出力する
void emboss(int n){
  // 出力画像を黒画像にする(縦横のサイズは元画像よりn小さくする)
  img[n] = createImage(w-n, h-n, ARGB);
  for (int j=0; j<h-n; j++){
    for (int i=0; i<w-n; i++){
      // 元画像の(i+diff, j+diff)のピクセルの明るさ → float f1
      // 元画像の(i, j)のピクセルの明るさを反転させた値 → float f2
      // 出力画像の(i, j)のピクセルの明るさを(f1+f2-128)にする
    }
  }
  img[n].save("data/" + fName[n-1] + ".png");
}

// モーションブラーをかけた画像を出力する
void motionBlur(int type, float angle, int ra){
  // 出力画像を黒画像にする(縦横のサイズは元画像よりn小さくする)
  img[type] = createImage(w-ra*2, h-ra*2, ARGB);
  for (int j=ra; j<h-ra; j++){
    for (int i=ra; i<w-ra; i++){
      // 変換後の(i, j)のピクセルの色を取得し、出力画像の(i-ra, j-ra)のピクセルに設定する
    }
  }
  img[type].save("data/" + fName[type-1] + ".jpg");
}

// モーションブラーの出力画像の(x, y)のピクセルの色を返す
color bluredColor(int x, int y, float angle, int ra){
  // 画素半径raのマスクを生成する
  float[] mask = new float[(ra*2+1)*(ra*2+1)];
  float sum = 0;
  int n = 0;
  // ブラーをかける方向をあらわす値
  float dx = cos(radians(angle));
  float dy = sin(radians(angle));
  for (int j=-ra; j<=ra; j++){
    for (int i=-ra; i<=ra; i++){
      // 概要の式に応じた値をマスクに入れる
      sum += mask[n];
      n++;
    }
  }
  // マスクを規格化(要素の総和が1になるように)する
  for (int i=0; i<mask.length; i++){
    mask[i] /= sum;
  }
  float r=0;
  float g=0;
  float b=0;
  n=0;
  // マスクと元画像の画素の色の値の積を求める
  for (int j=y-ra; j<=y+ra; j++){
    for (int i=x-ra; i<=x+ra; i++){
      r += red(img[0].pixels[i+j*w])*mask[n];
      g += green(img[0].pixels[i+j*w])*mask[n];
      b += blue(img[0].pixels[i+j*w])*mask[n];
      n++;
    }
  }
  return color(r, g, b);
}

// セピア化した画像を出力する
void sepia(){
  // 出力画像を元画像と同じにする
  img[5] = img[0].copy();
  for (int j=0; j<h; j++){
    for (int i=0; i<w; i++){
      float r = 0;
      float g = 0;
      float b = 0;
      img[5].pixels[i+j*w] = color(r, g, b);
    }
  }
  img[5].save("data/" + fName[4] + ".jpg");
}

// フィルタ処理を実行
void mousePressed() {
  emboss(1);
  emboss(2);
  motionBlur(3, 0, 5);
  motionBlur(4, 30, 5);
  sepia();
  println("完了");
}

// 押したキーに応じて対応する画像を表示
void keyPressed() {
  int k = key-'0';
  if (k>=0 && k<6) {
    background(0);
    image(img[k], 0, 0, width, height);
  }
}
  1. Processingのエディタに上のサンプルプログラムのコードをコピー&ペーストする。
  2. 「img09」という名前で保存する。
  3. 適当に画像検索してサンプル用の画像を用意し、「元.jpg」という名前で保存する (サイズが小さめ(600ピクセル以下程度)で、くっきりしたエッジがあるもの)。
  4. 「元.jpg」をProcessingのエディタにドラッグ&ドロップする。
  5. emboss関数の「// 元画像の(i+diff, j+diff)のピクセルの明るさ → float f1」の下に以下のコードを追加する。
  6.       float f1 = brightness(img[0].pixels[i+n+w*(j+n)]);

  7. emboss関数の「// 元画像の(i, j)のピクセルの明るさを反転させた値 → float f2」の下に、その説明に応じたコードを追加する。
  8. (反転させると0は255, 255は0になる。つまり、255から明るさの値を引けば反転させた値になる)

  9. emboss関数の「// 出力画像の(i, j)のピクセルの明るさを(f1+f2-128)にする」の下に、その説明に応じたコードを追加する。
  10. (出力画像の(i, j)のピクセルは 「img[n].pixels[i+(w-n)*j]」)
    (colorに引数を1つだけ入れれば、その明るさを持つ灰色ができる)

  11. 実行して画面をクリックする。
  12. (「完了」が表示されたあとでキーボードの1, 2キーを押すと、浮彫のような画像が表示される)
    (2キーで表示されるものの方が1キーのものより「彫りが深い」画像になる)
  • この段階でdataフォルダの出力画像のうち「1エンボス1.png」「1エンボス2.png」は然るべきもの、残りの3枚は真っ黒になっている。
  • うまくいかない場合は、最終的なemboss関数とコードを見比べる。

モーションブラー

概要

モーションブラーのフィルターでは、「動いているように見える方向」の単位ベクトルを \((d_x, d_y)\) とすると、マスクの中心から \((x, y)\) だけ離れたマスクの値は

\( \begin{eqnarray} &&e^{-\frac{(xd_y-yd_x)^2}{2\sigma}} \end{eqnarray} \)

に比例する値になる (\(e\) は自然対数の底。\(\sigma\) が大きくなると、移動に対する横方向にもブレが出るようになる)。
この式でマスクの値を計算すると、中心から移動方向に沿ったマスでは大きい値になり、そのラインから離れると値が小さくなる。そのため、自分自身からみて進行方向の線上にあるピクセル(この例では左上と右下)の色の影響を強く受けるような色の混ざり方になる。



今回は画素半径5の場合についてこのフィルターの処理を行う。そのためのマスクの要素数は11×11とかなり大きく、ブラーをかける方向によっても値が変わるので、プログラムにあらかじめ書かずに関数の中で値を計算する。

課題 2

  1. bluredColor関数の「// 概要の式に応じた値をマスクに入れる」の下に説明に応じたものを追加する。
  2. (代入されるものは「mask[n]」)
    (「\(e\)の\(x\)乗」は、Processingでは「exp(x)」と書く)
    (概要の式の \(x, y, d_x, d_y\) は、プログラムでは i, j, dx, dy に読み替える)

  3. motionBlur関数の「// 変換後の(i, j)のピクセルの色を取得し、出力画像の(i-ra, j-ra)のピクセルに設定する」の下に説明に応じたものを追加する。
  4. (取得して代入されるのは、出力画像の(x, y)のピクセル)
    (出力画像は img[type])
    (出力画像の横幅は元画像より「ra*2」だけ小さい → (x, y)の場所の通し番号は「i-ra+(j-ra)*(w-ra*2)」)
    (変換後の色はbluredColor関数を読んで取得する。引数は(x座標, y座標, ブラーの角度, 画素半径))

  5. 実行して画面をクリックする。
  6. (「完了」が表示されたあとでキーボードの3キーを押すと、横方向にモーションブラーがかかったものが表示される)
    (キーボードの4キーを押すと、斜めにモーションブラーがかかったものが表示される)


  7. mousePressed関数からmotionBlur関数を呼ぶときの第2引数はブラーをかける方向(角度)第3引数はブラーの強さにあたる。結果がわかりにくいときは、第3引数を大きくして出力する (ただし、処理に時間がかかるようになる)。
  8. この段階でdataフォルダの出力画像のうち「2ブラー横.jpg」「2ブラー斜め.jpg」も然るべきものになる。
  9. うまくいかない場合は、最終的なmotionBlur関数最終的なbluredColor関数とコードを見比べる。

セピア化

概要

セピア色とはR, G, Bの値の比が 107:74:43の色のことで、元画像の各ピクセルの明るさに応じてこの比率をもつ画像を作れば、変色した古い写真のようなものができる。
まず元画像のR, G, Bの値から

  \( \begin{eqnarray} F &=& 0.299R + 0.587G + 0.114B \end{eqnarray} \)

で明るさを決め、

  \( \begin{eqnarray} R' &=& F\\ G' &=& \frac{74}{107}F\\ B' &=& \frac{43}{107}F\\ \end{eqnarray} \)

とすれば出力画像のRGB値が得られる。

課題 3

  1. sepia関数の r, g, bに仮の値0を入れている部分を、概要の説明に対応する処理に書き換えて実行する。
  2. (「完了」が表示されたあとでキーボードの5キーを押すと、セピア化されたものが表示される)

  3. うまくいかない場合は、最終的なsepia関数とコードを見比べる。

提出

理解度確認テスト予告

次回の授業の初めに第05~09回の内容についての理解度確認テストを行う。

戻る

inserted by FC2 system