第05回 平滑化

書類をスキャンした画像や、条件の悪いところで撮影された写真などには小さいノイズが乗ることがある。
平滑化という処理を行うことで、このノイズを取り除くことができる。

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

プログラムを実行すると、下の図のように元画像にノイズを乗せた「ノイズ.jpg」がdataフォルダにが作られ、画面上にもその画像が表示される。
さらに実行画面上でクリックすると、dataフォルダに の6つの画像が作られる。
そのあとでキーボードの0~6のキーを押すとこれらの画像が画面に表示される。
(サムネイルでは結果を確認しにくいので、実行画面では拡大して表示する。クリックだけですべての画像が作成されるので、キーボードを使うのは結果確認のためだけ)
キー画像
0ノイズjpg
11移動平均1.jpg
21移動平均2.jpg
32ガウシアン1.jpg
42ガウシアン2.jpg
53メディアン1.jpg
63メディアン2.jpg

移動平均フィルタ

概要

移動平均フィルタでは、あるピクセルのまわりのピクセルの色を平均して変換後の画像のピクセルの色を決める (実際は赤・緑・青の成分それぞれについて平均をとるが、下の図では話を簡単にするため単色であらわしている)。

画素半径 (色を決めたいピクセルからどれだけ離れたところまで考慮するかの値) が1の場合は、対象のピクセルを囲む9個のピクセルの色を平均する。


画素半径が2の場合は、対象のピクセルの上下左右に2つ離れたところまでの25個のピクセルの色を平均する。


ただし、例えば図の○のような場所のピクセルの色を求めるときは、画像の内側に納まる範囲 (灰色) のピクセルのみについて平均をとる。


具体的に画素半径 ra の移動平均フィルタで (x, y) のピクセルの色を決める処理は以下の通り。
(画素半径1のときは、ループが完了すると普通の (内側の) ピクセルのについて考えているときなら countは9になる。一方、上の図の○のピクセルの場合はcountは6になる。足しあげた値をcountで割れば平均の色成分ができる)
出力画像のピクセルの色 r, g, b を初期化
加算した回数 count を初期化
縦位置 j を y-ra から y+ra まで変える {
  横位置 i を x-ra から x+ra まで変える {
    もし (i, j) が画像の範囲内なら {
      r, g, b に (i, j) のピクセルの色を加算する
      count を 1増やす
    }
  }
}
r, g, b を count で割る
r, g, b から色を作る

課題 1

ベースのプログラム (元画像にノイズを加えた画像がdataフォルダに作られ、クリックすると黒い画像が6つできるだけ)
// ソートのためのライブラリ
import java.util.ArrayList;
import java.util.Collections;

// 出力用画像の変数
// 0:ノイズ, 1, 2: 移動平均, 3, 4: ガウシアン, 5, 6:メディアン
PImage[] img = new PImage[7];
// 出力ファイル名
String[] fName = {"1移動平均1", "1移動平均2", "2ガウシアン1", "2ガウシアン2", "3メディアン1", "3メディアン2"};
int w, h;

void setup() {
  size(600, 450);
  img[0] = loadImage("元.jpg");
  w = img[0].width;
  h = img[0].height;
  if (w!=200 || h!=150) {
    println("画像サイズが違います");
    exit();
  }
  // 元画像にランダムノイズを追加する(全画素数の1%)
  for (int i=0; i<w*h/100; i++) {
    img[0].pixels[int(random(w*h))] = color(random(256), random(256), random(256));
  }
  // ノイズを加えた画像を保存
  img[0].save("data/ノイズ.jpg");
  background(0);
  image(img[0], 0, 0, width, height);
  // 1~6番を同じサイズの黒画像にする
  for (int i=1; i<=6; i++) {
    img[i] = createImage(w, h, ARGB);
  }
}

void draw() {
}

// type(1~6)に応じた処理を行い、その画像を保存
void colorFilter(int type) {
  int ra = (type+1)%2+1; // 画素半径(1 or 2)
  // 画像の範囲内での繰り返し
  for (int y=0; y<h; y++) {
    for (int x=0; x<w; x++) {
      if (type<=2) {
        // 移動平均フィルタの色取得
        img[type].pixels[x+y*w] = getColorA(x, y, ra);
      } else if (type<=4) {
        // ガウシアンフィルタの色取得
        img[type].pixels[x+y*w] = getColorG(x, y, ra);
      } else {
        // メディアンフィルタの色取得
        img[type].pixels[x+y*w] = getColorM(x, y, ra);
      }
    }
  }
  img[type].save("data/" + fName[type-1] + ".jpg");
}

// 移動平均フィルタでの出力画像の(x, y)のピクセルの色を返す
// (課題1で変更を加える)
color getColorA(int x, int y, int ra) {
  float r=0;
  float g=0;
  float b=0;
  return color(r, g, b);
}

// ガウシアンフィルタでの出力画像の(x, y)のピクセルの色を返す
// (課題2で変更を加える)
color getColorG(int x, int y, int ra) {
  float r=0;
  float g=0;
  float b=0;
  return color(r, g, b);
}

// メディアンフィルタでの出力画像の(x, y)のピクセルの色を返す
// (課題3で変更を加える)
color getColorM(int x, int y, int ra) {
  float r = 0;
  float g = 0;
  float b = 0;
  return color(r, g, b);
}

// フィルタ処理を実行
void mousePressed() {
  for (int i=1; i<=6; i++) {
    colorFilter(i);
  }
}

// 押したキーに応じて対応する画像を表示
void keyPressed() {
  int k = key-'0';
  if (k>=0 && k<7) {
    background(0);
    image(img[k], 0, 0, width, height);
  }
}
  1. Processingのエディタに上のサンプルプログラムのコードをコピー&ペーストする。
  2. 「img05」という名前で保存する。
  3. 適当に画像検索してサンプル用の画像を用意する。
  4. 縮小もしくはトリムして横200, 縦150ピクセルの大きさにし、「元.jpg」という名前で保存する。
  5. (元画像のサイズが200x150でない場合はプログラムが自動的に終了するようになっている)
    (このようになんらかの構造がある部分を含める。ほぼ単色などのものはNG)


  6. 「元.jpg」をProcessingのエディタにドラッグ&ドロップする。
  7. getColorA関数の「float b=0;」の下に以下のコードを追加して実行して画面をクリックする。
  8. (そのあとでキーボードの1, 2を押すと、赤成分のみ画素半径1, 2で平滑化したものが表示される)
      int count = 0; // 加算した回数
      for (int j=y-ra; j<=y+ra; j++) {
        for (int i=x-ra; i<=x+ra; i++) {
          // 参照ピクセルが画像範囲内の場合
          if (i>=0 && i<w && j>0 && j<h) {
            // ノイズ画像の(i, j)のピクセルの赤,緑,青成分をr,g,bに加算
            r += red(img[0].pixels[i+j*w]);
            count++;
          }
        }
      }
      r /= count;


  9. 上で追加したコードに緑・青成分にかかわる処理を追加して実行し、平滑化の処理を完成させる。
  • この段階でdataフォルダの「1移動平均1.jpg」「1移動平均2.jpg」「ノイズ.jpg」は然るべきもの、それ以外は真っ黒な状態になっている。
  • うまくいかない場合は、getColorA関数の最終状態とコードを見比べる。

ガウシアンフィルタ

概要

移動平均フィルタでは周辺のピクセルの色を混ぜて使うので、ノイズを消せるかわりにエッジがぼやけてしまう。これは、元ピクセルからの距離と関係なく色を混ぜてしまっているためである。
そこで、「元のピクセルに近いものは強く、遠いものは弱く効くようにすれば、元画像の特徴を多く残すことができる。

画素半径1のガウシアンフィルタでは、
  • 自分自身の位置のピクセルの色が一番強く効く
  • その上下左右のピクセルの効き方はその半分
  • 斜め隣のピクセルの効き方はさらにその半分
のような決め方をする。こうなるように色成分を求めるには、
  • 中央ピクセルの色成分 × 4
  •  上ピクセルの色成分 × 2
  •  下ピクセルの色成分 × 2
  •  左ピクセルの色成分 × 2
  •  右ピクセルの色成分 × 2
  • 左上ピクセルの色成分 × 1
  • 右上ピクセルの色成分 × 1
  • 左下ピクセルの色成分 × 1
  • 右下ピクセルの色成分 × 1
を加えたものを (4+2+2+2+2+1+1+1+1) で割ればよい。
プログラムでこの計算を効率的に行うには、下の行列のようなマスクというものを用意して
121
242
121
出力画像のピクセルの色 r, g, b を初期化
使ったマスクの値の和 msum を初期化
縦位置 j を y-ra から y+ra まで変える {
  横位置 i を x-ra から x+ra まで変える {
    もし (i, j) が画像の範囲内なら {
      r, g, b に (i, j) のピクセルの色×その場所のマスクの値を加算する
      msumにその場所のマスクの値を加算する
    }
  }
}
r, g, b を msum で割る
r, g, b から色を作る
のような処理を行う。
ただし、「その場所のマスクの値」でいう「その場所」はこれまでに出てきた変数だけでは表しにくいので、マスクを配列 m で表わして
m[0]m[1]m[2]
m[3]m[4]m[5]
m[6]m[7]m[8]
のように対応させれば、以下のように処理することで「その場所のマスク」を対応させられる。
出力画像のピクセルの色 r, g, b を初期化
マスク内の参照位置 n を初期化
使ったマスクの値の和 msum を初期化
縦位置 j を y-ra から y+ra まで変える {
  横位置 i を x-ra から x+ra まで変える {
    もし (i, j) が画像の範囲内なら {
      r, g, b に (i, j) のピクセルの色×m[n] を加算する
      msum に m[n] を加算する
    }
    n を1増やす
  }
}
r, g, b を msum で割る
r, g, b から色を作る
画素半径が2のときも基本的には同じで、マスクは以下のようになる。
14641
41624164
62436246
41624164
14641

課題 2

  1. getColorG関数の中のコードを削除し、その部分にgetColorA関数の中のコードをコピー&ペーストする。
  2. getColorG関数の中の先頭に以下のコードをコピー&ペーストする。
  3. (画素半径1と2のマスクをまとめて2重配列で定義し、画素半径 ra の値に応じて配列 m に入れる)
      float[][] mask = {
        // mask[0](画素半径1のマスク)
        {
          1, 2, 1, 
          2, 4, 2, 
          1, 2, 1
        }, 
        // mask[1](画素半径2のマスク)
        {
          1, 4, 6, 4, 1, 
          4, 16, 24, 16, 4, 
          6, 24, 36, 24, 6, 
          4, 16, 24, 16, 4, 
          1, 4, 6, 4, 1
        }
      };
    
      float[] m = mask[ra-1];

  4. 回数のカウントにかかわる変数の部分を以下のように書き換える。
  5. (変数 count がなくなるため、4か所でエラーが出る)
      int count = 0; // 加算した回数
      int n=0;    // マスクの内の参照位置
      int msum=0; // 重みの係数の総和

  6. 「概要」の最後の囲み (青文字・赤文字・黒文字で書かれた疑似コード) の動作になるようにコードを書き換える。
  7. 実行してクリックし、1キーと3キー、2キーと4キーをそれぞれ交互に押し、同じ画素半径だと移動平均フィルタよりもガウシアンフィルタの方がぼやけ方が少ないことを確認する。
  8. キー画像種類
    1移動平均
    画素半径1
    2移動平均
    画素半径2
    3ガウシアン
    画素半径1
    4ガウシアン
    画素半径2
  • この段階でdataフォルダの「2ガウシアン1.jpg」「2ガウシアン2.jpg」も真っ黒でなくなる。
  • うまくいかない場合は、getColorG関数の最終状態とコードを見比べる。

メディアンフィルタ

概要

移動平均フィルタ、ガウシアンフィルタには、どちらもノイズを消そうとすると本来の像がぼやけ、元の情報を残そうとするとノイズも消えないという弱点がある。
メディアンフィルタでは、平均を取るのではなく、範囲内のピクセルの色成分の中央値を取ることでノイズを消すという方法をとる。多くの場合、ノイズは周囲のピクセルとは大きく色が異なっているので、これが中央値になることはほとんどなく、1ピクセルだけの孤立したノイズならほとんどのケースできれいに消せる。

画素半径2の場合なら、考慮する25個のピクセルの色成分を昇順に並べ替え、中央番目、つまり13番目 (プログラミングの数え方的には12番目) のピクセルの色を採用する。
(図は赤成分。緑・青成分についても同様にして中央番目の値を使う)

Processingでは、ArrayListのsort関数を使えば並べ替えは簡単に行うことができる。

課題 3

  1. getColorM関数の中の先頭に以下のコードをコピー&ペーストする。
  2. (これは赤成分のみの処理。初めに空っぽのリスト ar を用意し、for文の繰り返しで要素を追加し、昇順に並べ替える処理までを行うコード)
      // 色成分格納用のリスト
      ArrayList<Float> ar = new ArrayList<Float>();
      for (int j=y-ra; j<=y+ra; j++) {
        for (int i=x-ra; i<=x+ra; i++) {
          // 参照ピクセルが画像範囲内の場合
          if (i>=0 && i<w && j>0 && j<h) {
            // ピクセルの色成分をリストに追加
            ar.add(red(img[0].pixels[i+j*w]));
          }
        }
      }
      // 色リストの値を昇順にソートする
      Collections.sort(ar);
      // 中央の値を取り出す
    

  3. 出力用の赤成分にリストの「中央番目」の色を入れるように処理を書き換える。
  4. (「ar.size()」が ar の要素数。9なら「ar.size()」は4.5の小数部分を切り捨てた4になる)
    (ArrayListt型でget関数を使うと、指定した「番目」の要素が取り出される)
      float r = 0;
      float r = ar.get(ar.size()/2);

  5. 緑・青成分についても同様の処理を追加する。
  6. 実行して画面をクリックする。
  7. (そのあとで5, 6キーを押すと、画素半径1, 2のメディアンフィルタをかけた結果が表示される)
    (画素半径1でもほぼ完全にノイズが消えているはず)
    (画素半径2だと、元画像の細部がつぶされ、パステル画のようになる)
この時点で存在するファイル → 提出ファイル
  • 元.jpg
  • ノイズ.jpg
  • 1移動平均1.jpg
  • 1移動平均2.jpg
  • 2ガウシアン1.jpg
  • 2ガウシアン2.jpg
  • 3メディアン1.jpg
  • 3メディアン2.jpg

提出

小テスト予告

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

戻る

inserted by FC2 system