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

古い変色したアナログ写真のような色に変える「セピア化」、明度をもとに疑似的な凹凸を表現する「エンボス処理」、カメラの動きによるブレを表現する「モーションブラー」について紹介する。
課題3では第05回のコードが必須なので、まだの場合は先にそちらを完成させる。

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

プログラムを実行して少し待つとコンソールに「完了」が表示され、元画像をグレースケール化した画像が実行画面上に表示される。
dataフォルダには5つの画像が作られる。
そのあとでキーボードの0~5のキーを押すとこれらの画像が画面に表示される。
(すべての画像は自動的に作成されるので、キーボードを使うのは結果確認のためだけ)
キー 画像 意味 特徴
0 元.jpg 元画像
1 1セピア.jpg 元画像をセピア化した画像 古い変色したアナログ写真のような色になる
2 2エンボス1.png 右と下に1ピクセルずつずらしてエンボス処理した画像 白い部分が盛り上がり、黒い部分がくぼんでいて、左上から光が当たっているように見える
サイズが元画像より縦横1ピクセルずつ小さくなる
3 2エンボス2.png 右と下に2ピクセルずつずらしてエンボス処理した画像 2キーの画像より凹凸が深くなったように見える
サイズが元画像より縦横2ピクセルずつ小さくなる
4 3ブラー1.jpg σ=1 で左上-右下方向のモーションブラーをかけた画像 カメラを左上から右下に動かして撮ったように見える
5 3ブラー5.jpg σ=5 で左上-右下方向のモーションブラーをかけた画像 全体的にぼやけて見える
キーと表示される画像

1. セピア化

概要

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

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

で明るさを決める。計算式からわかるように、これはいままで扱ってきた明度とはことなる値になる。これは、人間の目に対する感度が色によって違うためである。
例えば (255, 0, 0) よりも (0, 255, 0) が、つまり単純な赤よりも単純な緑の方が明るく感じられる。
\(F\) は感覚的な明るさを表わす量といえる。これをもとに

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

で計算される \(R', G', B'\) をピクセルの赤・青・緑の値として使えば、セピア色の画像が得られる。

課題 1

  1. Processingを起動する。
  2. 以下のコードをコピー&ペーストする。
  3. // 画像用の変数
    PImage[] img = new PImage[6];
    // 出力ファイル名
    String[] fName = {"元", "1セピア", "2エンボス1", "2エンボス2", "3ブラー1", "3ブラー5"};
    int w, h;
    
    void setup() {
      size(800, 600);
      w = width;
      h = height;
      img[0] = loadImage("元.jpg");
      img[0].resize(w, h);
      textFont(createFont("MS Pゴシック", 48));
      background(0);
      image(img[0], 1, 1);
      sepia(1); // セピア化した画像をimg[1]に保存
      emboss(1, 2);// 右下方向に1ピクセルずつずらしてエンボス処理した画像をimg[2]に保存
      emboss(2, 3);// 右下方向に2ピクセルずつずらしてエンボス処理した画像をimg[3]に保存
      motionBlur(10, 1, 1, 1, 4); // σ=1で左上-右下方向のモーションブラーをかけた画像をimg[4]に保存
      motionBlur(10, 5, 1, 1, 5); // σ=5で左上-右下方向のモーションブラーをかけた画像をimg[5]に保存
      println("完了");
    }
    
    void draw() {
    }
    
    // セピア化(課題1)
    void sepia(int n) {
      // n番を元画像と同じサイズの黒画像にする
      img[n] = createImage(w, h, ARGB);
      for (int j=0; j<h; j++) {
        for (int i=0; i<w; i++) {
          // 元画像の(i, j)のピクセルの赤・緑・青の成分をR, G, Bに入れる
          // R, G, Bから計算した値をFに入れる
          // 出力画像の(i, j)のピクセルにFを元にして計算したR', G', B'の色を設定する
        }
      }
      img[n].save("data/"+ fName[n] +".jpg");
    }
    
    // エンボス処理(課題2)
    void emboss(int gap, int n) {
      // n番を元画像より縦横ともgapだけ小さいサイズの黒画像にする
      img[n] = createImage(w-gap, h-gap, ARGB);
      for (int j=0; j<h-gap; j++) {
        for (int i=0; i<w-gap; i++) {
          // br1に元画像の(i+gap, j+gap)のピクセルの明度を入れる
          // br2に元画像の(i, j)のピクセルの明度を反転させた値を入れる
          // 出力画像の(i, j)のピクセルに明度(br1+br2-128)の色を設定する
        }
      }
      img[n].save("data/"+ fName[n] +".png");
    }
    
    // モーションブラーをかける(課題3)
    void motionBlur(int radius, double sigma, float dx, float dy, int n) {
      // n番を元画像と同じサイズの黒画像にする
      img[n] = createImage(w, h, ARGB);
      img[n].save("data/"+ fName[n] +".jpg");
    }
    
    void keyPressed() {
      int k = key-'0';
      if (k>=0 && k<=5) {
        background(0);
        image(img[k], 0, 0);
        fill(255, 0, 0);
        text(fName[k], 30, height-30);
        fill(255);
      }
    }
  4. 「img09」という名前で保存する。
  5. 適当に画像検索して元画像 (明るい部分と暗い部分を含み、小さめの被写体が写っているもの) を用意する。
  6. 画像の形式に応じて以下の変更を加える。
    • JPG形式の場合→名前を元.jpgに変更
    • それ以外の場合→ペイントで開き、形式をJPGに指定して「元」という名前で保存
  7. 「元.jpg」をProcessingのウインドウにドラッグ&ドロップする。
  8. sepia関数の「// 元画像の(i, j)のピクセルの赤・緑・青の成分をR, G, Bに入れる」の下に、対応するコードを追加する。
    • R, G, Bはいずれもfloat型の変数にする。
    • 元画像の(i, j)のピクセルの色は「img[0].pixels[i+j*w]」。
    • 赤・緑・青成分はそれぞれred関数、green関数、blue関数で取り出せる。
  9. sepia関数の「// R, G, Bから計算した値をFに入れる」の下に、対応するコードを追加する。
    • Fはfloat型の変数にする。
    • 概要の式を参照。
  10. sepia関数の「// 出力画像の(i, j)のピクセルにFを元にして計算したR', G', B'の色を設定する」の下に、対応するコードを追加する。
    • 「出力画像の(i, j)のピクセル」は「img[n].pixels[i+j*w]」。
    • color関数にR', G', B' にあたる3つの引数を入れて色を作る。
    • 割り算の書き方に注意。Processingでは「整数/整数」の小数部分は切り捨てられるので「74/107*F」「43/107*F」では正しいG', B'の値にならない。掛け算・割り算のどちらも、計算に使われる値のどちらかが実数なら結果も実数になり、正しい値が得られる。
  11. プログラムを実行する。
    • コンソールに「完了」が表示されてから1キーを押すと、元画像をセピア化した画像が表示される。

2. エンボス処理

概要

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

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

課題 2

  1. emboss関数の「// br1に元画像の(i+gap, j+gap)のピクセルの明度を入れる」の下に、対応するコードを追加する。
    • br1はfloat型の変数にする。
    • 元画像の(i+gap, j+gap)のピクセルの色は「img[0].pixels[i+gap+(j+gap)*w]」。
    • 明度はbrightness関数で取り出せる。
  2. emboss関数の「// br2に元画像の(i, j)のピクセルの明度を反転させた値を入れる」の下に、対応するコードを追加する。
    • br2はfloat型の変数にする。
    • 元画像の(i, j)のピクセルの色は「img[0].pixels[i+j*w]」。
    • 明度はbrightness関数で取り出せる。
    • 反転させると明度は0→255, 255→0のように変わる。つまり、255から元の明度を引けば反転させた明度になる。
  3. emboss関数の「// 出力画像の(i, j)のピクセルに明度(br1+br2-128)の色を設定する」の下に、対応するコードを追加する。
    • 出力画像の幅はwではなくw-gapなので、その(i, j)のピクセルは「img[n].pixels[i+j*(w-gap)]」。
    • 色はcolor関数で作る。
  4. プログラムを実行する。
    • コンソールに「完了」が表示されてから2, 3キーを押すと右、下それぞれ1ピクセル、2ピクセルずつずらして合成したエンボス画像が表示される。
    • もともと黒かった部分はくぼんだように、白かった部分は盛り上がったように見える。
    • 3キーで表示される方が「彫りの深い」画像になる。

3. モーションブラー

概要

モーションブラーとは、撮影時にカメラがブレたために起こるような効果を画像処理で加えるものである。
この処理は第05回のガウシアンフィルタのように、参照ピクセルの周囲のピクセルの色を重みをつけて混ぜて行うことができる。
ブレた方向にあたる単位ベクトル (長さが1のベクトル) を \((d_x, d_y)\) とすると、参照ピクセルからの位置ずれ \((x, y)\) の位置でのマスクの値は
\( \begin{eqnarray} &&e^{-\frac{(xd_y-yd_x)^2}{2\sigma^2}} \end{eqnarray} \)
に比例した値になる。 \((d_x, d_y)\) が (0, 0)から(1, 1) の向きの場合のこの値をグラフで表わすと

のようになる。赤は \(\sigma=1\)、青は \(\sigma=5\) の場合のもので、\(\sigma\) が小さいと「カメラが動いた向き」のピクセルだけの色を混ぜ合わせるのに対し、\(\sigma\) が大きくなるとそれ以外のピクセルの影響も大きくなることがわかる。\(\sigma\) が充分に大きくなれば、結果は単純な移動平均フィルタによるぼかしに近づく。

課題 3

  1. 第05回の完成状態のプログラムのgaussian関数の中のコードのうち、最後の「img[n].save("data/"+ fName[n] +".jpg");」以外のコードを、今回のプログラムのmotionBlur関数の「img[n] = createImage(w, h, ARGB);」と「img[n].save("data/"+ fName[n] +".jpg");」の間にコピー&ペーストする。
  2. motionBlur関数の「img[n] = createImage(w, h, ARGB);」の後に以下のコードを追加する。
      PVector dir = new PVector(dx, dy);// dx, dyを成分とするベクトル
      dir.normalize(); // dirを規格化
    (normalize関数はベクトルを規格化、つまり向きをそのままで長さを1にする関数)

  3. motionBlur関数の「gauss」を「mask」に変更する (5箇所)。
    (プログラムの動作には影響しないが、実情に合わせる)
  4. motionBlur関数の
                double mask = Math.exp(-(x*x+y*y)/2/sigma/sigma);
                double mask = Math.exp(-Math.pow(x*dir.y-y*dir.x, 2)/2/sigma/sigma);
    に変更する。
    (概要の \(d_x, d_y\) がコードの 「dir.x, dir.y」に、概要の \(x, y\) がコードの「x, y」に対応する)

  5. プログラムを実行する。
    • コンソールに「完了」が表示されてから (処理の量が多いためかなり時間がかかる) 4, 5キーを押すとそれぞれ \(\sigma=1, \sigma=5\) でモーションブラー処理をした画像が表示される。
    • 4キーで表示される画像 (\(\sigma=1\)) は左上から右下にカメラがブレたような画像になる。
    • 5キーで表示される画像 (\(\sigma=5\)) はそれにくらべて単純にぼかしたような画像になる。

提出

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