第07回 エッジ抽出

隣接するピクセルの明度を比較してその差を取り出すことで、画像に含まれるオブジェクトの輪郭を取り出すことができる。

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

プログラムを実行すると実行画面上に元画像をグレースケール化したものが表示される。
さらに実行画面上でクリックすると、dataフォルダに の6つの画像が作られる。
コンソールに「完了」が表示されてからキーボードの0~6のキーを押すとこれらの画像が画面に表示される。
(画像サイズが大きい場合は出力画像が作られるまでに時間がかかる)
(クリックだけですべての画像が作成されるので、キーボードを使うのは結果確認のためだけ)
キー画像
0元.jpg (グレースケール化した状態)
11プレウィットX.png
21プレウィットY.png
32ソーベルX.png
42ソーベルY.png
53プレウィット.png
63ソーベル.png

「1」「3」キーで表示される画像はX方向の明るさの差をとったもので、主に縦向きのエッジが取り出される。
「2」「4」キーで表示される画像はY方向の明るさの差をとったもので、主に横向きのエッジが取り出される。
「5」は「1」と「2」、「6」は「3」と「4」の結果を合成したもの。縦横だけでなく、全方向のエッジがきれいに取り出される。

プレウィットフィルタ

概要

輪郭とは「あるモノと背景の境目」のこと。一般に、モノの内側や背景の中では明るさがほぼ一定であり、境目のところでは隣り合うピクセルの明るさの差が大きい。画像処理ではこの性質を利用してエッジを取り出す。
例えば


の囲みの部分を拡大してみると


のようになる。そこで、
-101
-101
-101
のようなマスク (X方向プレウィットフィルタのマスクという) を用意し、元画像の参照ピクセルを含む9個についてマスクと明るさの積を足し上げてみる。

左の暗い部分の中 (例えば明度が20) のところでは、
202020
202020
202020
×
-101
-101
-101
= 0
で、結局0になる。

一方、エッジの部分を中心にして同様のことをすると、左側は暗くて右側が明るいので
205080
205080
205080
×
-101
-101
-101
= 180
のように正の値になる。

逆に左が明るくて右が暗い場合は
805020
805020
805020
×
-101
-101
-101
= -180
のように負の値になる。そこで、こうして足しあげた値の絶対値をとれば、
  • 横方向の色変化が小さいときは小さい値
  • 横方向の色変化が大きいときは大きい値
が得られる。これを明るさとした画像を作ると


からは、このように縦方向のエッジが白く取り出される。


一方、Y方向のプレウィットフィルタ
-1-1-1
000
111
を使えば、「縦方向の明るさの変化」が得られるので、


のように横方向のエッジが取り出される。

課題 1

ベースのプログラム (実行すると元画像をグレースケール化したものが表示されるが、クリックすると真っ黒な出力画像が作られるだけ)
// 出力用画像の変数
PImage[] img = new PImage[7];
// 出力ファイル名
String[] fName = {"1プレウィットX", "1プレウィットY", "2ソーベルX", "2ソーベルY", "3プレウィット", "3ソーベル"};
int w, h;

void setup() {
  size(600, 450);
  img[0] = loadImage("元.jpg");
  img[0].filter(GRAY);
  w = img[0].width;
  h = img[0].height;
  // 1~6番を同じサイズの黒画像にする
  for (int i=1; i<=6; i++) {
    img[i] = createImage(w, h, ARGB);
  }
  background(0);
  image(img[0], 0, 0, width, height);
}

void draw() {
}

// type(1~6)に応じた処理を行い、その画像を保存
void extractEdge(int type) {
  // 画像の上下左右の端1ピクセルのライン以外について繰り返し
  for (int y=1; y<h-1; y++) {
    for (int x=1; x<w-1; x++) {
      if (type<=4) {
        // XかYの方向のみのエッジ抽出
      } else {
        // 両方向の抽出値を合成
      }
    }
  }
  img[type].save("data/" + fName[type-1] + ".png");
}

// (課題1, 2で変更を加える)
// プレウィットフィルタ、ソーベルフィルタでの出力画像の(x, y)のピクセルの色を返す
// typeの値とフィルタの対応 (1: プレウィットX, 2: プレウィットY, 3:ソーベルX, 4:ソーベルY)
float getFilteredBrightness(int x, int y, int type) {
  float[][] mask = {
    {
      0, 0, 0, 
      0, 0, 0, 
      0, 0, 0
    }, 
    {
      0, 0, 0, 
      0, 0, 0, 
      0, 0, 0
    }, 
    {
      0, 0, 0, 
      0, 0, 0, 
      0, 0, 0
    }, 
    {
      0, 0, 0, 
      0, 0, 0, 
      0, 0, 0
    }
  };
  int pos = 0; // マスク上のインデックス
  int br = 0;  // 明るさを足しあげた値
  for (int j=y-1; j<=y+1; j++) {
    for (int i=x-1; i<=x+1; i++) {
      br += brightness(img[0].pixels[i+j*w])*mask[type-1][pos];
      pos++;
    }
  }
  return abs(br);
}

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

// 押したキーに応じて対応する画像を表示
void keyPressed() {
  int k = key-'0';
  if (k>=0 && k<7) {
    background(0);
    image(img[k], 0, 0, width, height);
  }
}
  1. Processingのエディタに上のサンプルプログラムのコードをコピー&ペーストする。
  2. 「img07」という名前で保存する。
  3. 適当に画像検索してサンプル用の画像を用意し、「元.jpg」という名前で保存する (建物や看板を正面から撮影したような、ほぼ水平・鉛直なエッジがあるもの)。
  4. 「元.jpg」をProcessingのエディタにドラッグ&ドロップする。
  5. extractEdge関数の「// XかYの方向のみのエッジ抽出」の下に以下のコードを追加する。
  6. (左辺は「出力画像の座標 (x, y) のピクセルの色」。これに値を代入することで色を設定する)
    (右辺で新しい色を作成している。colorの引数が1つなので、R, G, Bに同じ値が入る (灰色になる))
            img[type].pixels[x+y*w] = color(getFilteredBrightness(x, y, type));
    

  7. getFilteredBrightness関数の以下 (赤い囲み) の部分を、X方向のプレウィットフィルタのマスクに応じた値に変更する。
  8. (「概要」を参照)


  9. 実行して画面をクリックする。
  10. (「完了」が表示されたあとでキーボードの1キーを押すと、X方向のプレウィットフィルタをかけたものが表示される)
    (主に縦向きのエッジが取り出される)


  11. 6番目で変更を加えた部分の一つ下の9個のデータを、Y方向のプレウィットフィルタのマスクに応じた値に変更し、実行して画面をクリックする。
  12. (「完了」が表示されたあとでキーボードの2キーを押すと、Y方向のプレウィットフィルタをかけたものが表示される)
    (主に横向きのエッジが取り出される)

ソーベルフィルタ

概要

X方向のプレウィットフィルタでは参照ピクセルの上と下の行のピクセルも計算に入れるため、移動平均フィルタのようなぼかしの効果が含まれてしまう。
そこで、「参照ピクセルと同じ行の影響は大きく、上下の行の影響は小さく」なるようにすれば、よりくっきりとエッジを取り出せる。これがソーベルフィルタで、マスクはそれぞれ次のようになる。

X方向
-101
-202
-101

Y方向
-1-2-1
000
121

課題 2

  1. getFilteredBrightness関数のmask配列のうち値が0だけになっているブロックに、それぞれX方向のソーベルフィルタのマスク、Y方向のソーベルフィルタのマスクに対応する値を入れて実行し、画面をクリックする。
  2. (「完了」が表示されたあとでキーボードの3キーを押すと、X方向のソーベルフィルタをかけたものが表示される)


    (「完了」が表示されたあとでキーボードの4キーを押すと、Y方向のソーベルフィルタをかけたものが表示される)


  3. 「1」「3」キーを交互に押すと、3キーで表示されるソーベルフィルタの結果の方が明るい線になっているはず。同様に、「2」「4」キーを交互に押すと、4キーの方が明るい線になっているはず。
  4. この時点で「2ソーベルX.png」「2ソーベルY.png」も然るべき状態になる。
  5. うまくいかない場合は、最終的なgetFilteredBrightness関数とコードを見比べる。

全方向のエッジの抽出

概要

プレウィットフィルタ、ソーベルフィルタのどちらでも、縦向きのエッジを取り出そうとすると横向きのエッジはうまく取り出せず、その逆も同様になる。
そこで、X方向、Y方向のフィルタで取り出した明るさを組み合わせ、

\( \begin{eqnarray} \sqrt{(X方向のフィルタの結果)^2+(Y方向のフィルタの結果)^2} \end{eqnarray} \)


を計算すれば、縦・横・斜めのエッジがどれもきれいに取り出せる。
Processingではsqrt関数 (参考) で平方根を取得できるが、第02回で使ったdist関数 (参考) を使った方がシンプルにこの値を求められる。

課題 3

  1. extractEdge関数の「// 両方向の抽出値を合成」の下に以下のコードを追加する。
  2. (typeが5のときはX方向のプレウィットフィルタ (typeに1を入れると得られる) とY方向のプレウィットフィルタ (typeに2を入れると得られる) を合成する)
    (typeが6のときはX方向のソーベルフィルタ (typeに3を入れると得られる) とY方向のソーベルフィルタ (typeに4を入れると得られる) を合成する)
    (getFilteredBrightness関数の第3引数を以下のようにすることで、ちょうどこれらの値がfx, fyに入る)
            float fx = getFilteredBrightness(x, y, (type-5)*2+1); // X方向のフィルタで得られる明るさ
            float fy = getFilteredBrightness(x, y, (type-5)*2+2); // Y方向のフィルタで得られる明るさ
            img[type].pixels[x+y*w] = color(dist(0, 0, fx, fy));

    (「完了」が表示されたあとでキーボードの5キーを押すと、X, Y方向のプレウィットフィルタの結果を合成したものが表示される)


    (「完了」が表示されたあとでキーボードの6キーを押すと、X, Y方向のソーベルフィルタの結果を合成したものが表示される)


  3. どちらの結果も、全方向のエッジが均等に取り出されているはず。
  4. 「5」「6」キーを交互に押すと、6キーで表示されるソーベルフィルタの結果の方が明るい線になっているはず。
  5. この時点で「3プレウィット.png」「3ソーベル.png」も然るべき状態になる。
  6. うまくいかない場合は、最終的なextractEdge関数とコードを見比べる。

提出

小テスト予告

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

戻る

inserted by FC2 system