第07回 エッジ抽出

画像に含まれる建物や人物とその背景との境目の線だけを線画のように取り出したいことがある。そのような処理のことをエッジ抽出という。
境目の部分では隣り合うピクセルの明度が大きく異なるので、その差を取り出した情報を画像として保存すれば、輪郭だけの画像ができる。

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

プログラムを実行して少し待つとコンソールに「完了」が表示され、元画像をグレースケール化した画像が実行画面に表示される。
dataフォルダには8つの画像が作られる。
そのあとでキーボードの1~8のキーを押すとこれらの画像が画面に表示される。
(すべての画像は自動的に作成されるので、キーボードを使うのは結果確認のためだけ)
キー 画像 意味 特徴
0 元画像をグレースケール化した画像
1 1プレヴィットX.png X方向のプレヴィットフィルタでエッジ抽出した画像 主に鉛直方向のエッジが取り出される
2 1プレヴィットY.png Y方向のプレヴィットフィルタでエッジ抽出した画像 主に水平方向のエッジが取り出される
3 1ソーベルX.png X方向のソーベルフィルタでエッジ抽出した画像 主に鉛直方向のエッジが取り出される
1キーの画像より線が明るい
4 1ソーベルY.png Y方向のソーベルフィルタでエッジ抽出した画像 主に水平方向のエッジが取り出される
2キーの画像より線が明るい
5 2ラプラシアン縦横.png 縦横方向のラプラシアンフィルタでエッジ抽出した画像 縦横のエッジが取り出される
6 2ラプラシアン全.png 全方向のラプラシアンフィルタでエッジ抽出した画像 全方向のエッジが取り出される
7 3プレヴィット合成.png X, Y方向のプレヴィットフィルタの結果を合成した画像 全方向のエッジが取り出される
6キーの画像より線が明るい
8 3ソーベル合成.png X, Y方向のソーベルフィルタの結果を合成した画像 全方向のエッジが取り出される
7キーの画像より線が明るい
キーと表示される画像

1. 1次微分フィルタ

概要

図の赤い部分の中心のピクセルは「左の暗いエリアと右の明るいエリアの境目」にあたる。

仮にこのピクセルを含む周辺の9個の明度が
40 70 100
40 70 100
40 70 100
だとして、
-1 0 1
-1 0 1
-1 0 1
のようなマスクを考え、前回のディザリングと同様に対象のピクセルを含む周辺9個のピクセルの明度とマスクの値の積を足し上げる処理を行うと
40×(-1)+70×0+100×1
+40×(-1)+70×0+100×1
+40×(-1)+70×0+100×1
=180
のような正の値が得られる。

一方、青い部分の中心のピクセルは「左の明るいエリアと右の暗いエリアの境目」で、右側の方の明度が低くなる。例えばこのあたりの明度が
100 70 40
100 70 40
100 70 40
なら、同様の計算の結果は
100×(-1)+70×0+40×1
+100×(-1)+70×0+40×1
+100×(-1)+70×0+40×1
=-180
のように負の値になる。

緑のエリアのようなエッジ以外の部分では範囲内のピクセルの明度はどれも同じ。例えばこのあたりの明度が
70 70 70
70 70 70
70 70 70
なら、同様の計算の結果は
70×(-1)+70×0+70×1
+70×(-1)+70×0+70×1
+70×(-1)+70×0+70×1
=0
で0になる。

要するに、このような計算を行って得られる値は、元画像のその部分で左右の明度の差があれば0でない値で、なければ0になる。
この値の絶対値をとれば、左右のどちらが明るい場合ても正の値になる。そこで、計算結果の絶対値を明度とした画像を作ればこのような縦方向のエッジの部分だけが白く取り出される。

ここで使った
-1 0 1
-1 0 1
-1 0 1
を「X方向のプレヴィット (Prewitt) フィルタ」のマスクという。
同様に「Y方向のプレヴィットフィルタ」のマスク
-1 -1 -1
0 0 0
1 1 1
を使えば縦方向に明度の差がある場所、つまり横方向のエッジの部分が白く取り出される。

このようにプレヴィットフィルタでは縦・横のエッジを取り出せるが、X方向のマスクでは上と下のピクセルも計算に含めるため、半径1の移動平均の平滑化がかかってしまう。
そこで、対象のフィルタと同じY座標のピクセルの方が上下のものよりも大きく影響する「X方向のソーベル (Sobel) フィルタ」のマスク
-1 0 1
-2 0 2
-1 0 1
を使えばプレヴィットフィルタに比べてぼやけの効果は小さくなる。また、左右のピクセルの影響は単純に大きくなるので、一般に出力画像の明度も大きくなる。
「Y方向のソーベルフィルタ」のマスクも同様の考え方ででき、
-1 -2 -1
0 0 0
1 2 1
のようになる。

課題 1

  1. Processingを起動する。
  2. 以下の未完成状態のコードをコピー&ペーストする。
  3. // 画像用の変数
    PImage[] img = new PImage[9];
    // 出力ファイル名
    String[] fName = {"元", "1プレヴィットX", "1プレヴィットY",
      "1ソーベルX", "1ソーベルY", "2ラプラシアン縦横", "2ラプラシアン全",
      "3プレヴィット合成", "3ソーベル合成"};
    int w, h;
    // マスク
    int[] mask_prewittX = {-1, 0, 1, -1, 0, 1, -1, 0, 1};
    int[] mask_prewittY = {0, 0, 0, 0, 0, 0, 0, 0, 0};
    int[] mask_sobelX = {0, 0, 0, 0, 0, 0, 0, 0, 0};
    int[] mask_sobelY = {0, 0, 0, 0, 0, 0, 0, 0, 0};
    int[] mask_laplacian4 = {0, 0, 0, 0, 0, 0, 0, 0, 0};
    int[] mask_laplacian8 = {0, 0, 0, 0, 0, 0, 0, 0, 0};
    
    void setup() {
      size(800, 600);
      w = width;
      h = height;
      img[0] = loadImage("元.jpg");
      img[0].resize(w, h);
      img[0].filter(GRAY);
      // 1~8番を元画像と同じ画像にする
      for (int i=1; i<=8; i++) {
        img[i] = img[0].get();
      }
      textFont(createFont("MS Pゴシック", 48));
      single(mask_prewittX, 1); // プレヴィットフィルタ(X)でエッジ抽出した画像をimg[1]に保存
      single(mask_prewittY, 2); // プレヴィットフィルタ(Y)でエッジ抽出した画像をimg[2]に保存
      single(mask_sobelX, 3);   // ソーベルフィルタ(X)でエッジ抽出した画像をimg[3]に保存
      single(mask_sobelY, 4);   // ソーベルフィルタ(Y)でエッジ抽出した画像をimg[4]に保存
      single(mask_laplacian4, 5);       // ラプラシアンフィルタ(横・縦)でエッジ抽出した画像をimg[5]に保存
      single(mask_laplacian8, 6);       // ラプラシアンフィルタ(全方向)でエッジ抽出した画像をimg[6]に保存
      compose(mask_prewittX, mask_prewittY, 7); // 縦横のプレヴィットフィルタの結果を合成した画像をimg[7]に保存
      compose(mask_sobelX, mask_sobelY, 8);     // 縦横のソーベルフィルタの結果を合成した画像をimg[8]に保存
      println("完了");
      image(img[0], 0, 0, width, height);
    }
    
    void draw() {
    }
    
    // 1種類のフィルタでエッジ抽出する(課題1, 2)
    void single(int[] mask, int n) {
      for (int j=0; j<h; j++) {
        for (int i=0; i<w; i++) {
          // 周辺ピクセルの場合は黒にする
          if (i==0 || i==w-1 || j==0 || j==h-1) {
            // 出力画像の(i, j)のピクセルに明度が0の色を設定
          }
          // それ以外の場合は自分を含む9個のピクセルで明度を決める
          else {
            // 出力画像の明度の変数br
            // マスク上の位置pos
            for (int y=-1; y<=1; y++) {
              for (int x=-1; x<=1; x++) {
                // 元画像の(x+i, y+j)のピクセルの明度とマスクの値の積をbrに加える
                // マスク上の位置更新
              }
            }
            // 出力画像の(i, j)のピクセルに明度がbrの絶対値の色を設定
          }
        }
      }
      img[n].save("data/"+ fName[n] +".png");
    }
    
    // 複数方向のフィルタの結果を合成する(課題3)
    void compose(int[] mask1, int[] mask2, int n) {
    }
    
    void keyPressed() {
      int k = key-'0';
      if (k>=0 && k<=8) {
        background(0);
        image(img[k], 0, 0, width, height);
        fill(255, 0, 0);
        text(fName[k], 30, height-30);
        fill(255);
      }
    }
  4. 「img07」という名前で保存する。
  5. 適当に画像検索して元画像 (写っているものの輪郭線が鉛直・水平・斜めの部分を含むもの。輪郭がくっきりしているもの) を用意する。
  6. 画像の形式に応じて以下の変更を加える。
    • JPG形式の場合→名前を元.jpgに変更
    • それ以外の場合→ペイントで開き、形式をJPGに指定して「元」という名前で保存
  7. 「元.jpg」をProcessingのウインドウにドラッグ&ドロップする。
  8. single関数の「// 出力画像の(i, j)のピクセルに明度が0の色を設定」の下に以下のコードを追加する。
            img[n].pixels[i+w*j] = color(0);

  9. single関数の「// 出力画像の明度の変数br」の左に以下のコードを追加する。
    float br = 0;

  10. single関数の「// マスク上の位置pos」の左に以下のコードを追加する。
    int pos = 0;

  11. single関数の「// 元画像の(x+i, y+j)のピクセルの明度とマスクの値の積をbrに加える」の下に以下のコードを追加する。
                br += brightness(img[0].pixels[x+i+(y+j)*w]) * mask[pos];

  12. single関数の「// マスク上の位置更新」の左に以下のコードを追加する。
    pos++;

  13. single関数の「// 出力画像の(i, j)のピクセルに明度がbrの絶対値の色を設定」の下に以下のコードを追加する。
            img[n].pixels[i+w*j] = color(abs(br));
    (abs関数は引数の絶対値を返す関数。例えば 「int a = abs(-3);」を実行するとaには3が入る)

  14. プログラムを実行する。
    • コンソールに「完了」が表示されてから1キーを押すと黒背景に縦方向のエッジのある画像が表示される。

  15. 概要を参考にして、プログラム先頭の配列「mask_prewittY」「mask_sobelX」「mask_sobelY」に正しい値を入れる。
  16. (配列名が示す通り、それぞれ「Y方向のプレヴィットフィルタ」「X方向のソーベルフィルタ」「Y方向のソーベルフィルタ」のマスクの値を入れればよい)
  17. プログラムを実行する。
    • コンソールに「完了」が表示されてから1~4キーを押すとX方向、Y方向のプレヴィットフィルタ、X方向、Y方向のソーベルフィルタをかけた画像が表示される。


2. ラプラシアンフィルタ

概要

課題1で見たソーベルフィルタは、いずれもある方向への明るさの変化を取り出すものであった。この変化の値のことを数学的には「一次微分」または「一階微分」という。
それに対し、「『明るさの変化』の値がどのように変化するか」を考えることもできる。これは「微分したものをさらに微分する」ということから、「二次微分」あるいは「二階微分」とよばれる。

次のようにピクセルが並んでいるとき
A B C
D E F
G H I
とすると、
「Eから一つ右に動くとどれだけ明るくなるか」は「Fの明度 - Eの明度」で求められる。
一方、一つ左で同じことを調べると「Eの明度 - Dの明度」となる。その結果、前者から後者を引いたもの、すなわち
「Eのところでの明度の横方向の変化の変化」は「Dの明度 - 2 x Eの明度 + Fの明度」となる。同様に
「Eのところでの明度の縦方向の変化の変化」は「Bの明度 - 2 x Eの明度 + Hの明度」で求められる。

これらを加えた値「Bの明度 + Dの明度 + Fの明度 + Hの明度 - 4 x Eの明度」は、4方向のラプラシアンフィルタのマスク
0 1 0
1 -4 1
0 1 0
を使ってプレヴィットフィルタやソーベルフィルタと同様の処理を行うことで求めることができる。

さらに
「Eのところでの斜め (左下から右上) の変化の変化」は「Gの明度 - 2 x Eの明度 + Cの明度」
「Eのところでの斜め (左上から右下) の変化の変化」は「Aの明度 - 2 x Eの明度 + Iの明度」
となるので、これを上記に加えれば「E以外の明度を加えたもの - 8 x Eの明度」になる。これは8方向のラプラシアンフィルタのマスク
1 1 1
1 -8 1
1 1 1
で求めることができる。これを使えば、全方向のエッジを取り出すことができる。

課題 2

  1. 概要を参考にして、プログラム先頭の配列「mask_laplacian4」「mask_laplacian8」に正しい値を入れる。
    (配列名が示す通り、それぞれ「4方向のラプラシアンフィルタ」「8方向のラプラシアンフィルタ」のマスクの値を入れればよい)
  2. プログラムを実行する。
    • コンソールに「完了」が表示されてから5キーを押すと縦横の4方向のラプラシアンフィルタをかけた画像、6キーを押すと斜めも加えた全方向のラプラシアンフィルタをかけた画像が表示される。
    • 縦横だけのフィルタに比べて、全方向のフィルタのほうが全体の明度が高くなる。

3. 縦横の抽出結果の合成

概要

プレヴィットフィルタ、ソーベルフィルタのどちらの場合も、X方向の明度の変化を取り出すマスクでは鉛直方向のエッジを取り出せるが、水平方向のエッジは取り出せない。
逆に、Y方向の明度の変化を取り出すマスクでは水平方向のエッジを取り出せるが、鉛直方向のエッジは取り出せない。
斜め方向のエッジはどちらのフィルタでも取り出せるが、いずれも「得意な方向のエッジ」に比べると薄暗くなってしまう。

そこで、

\( \begin{eqnarray} \sqrt{\left(X方向のフィルタによる計算結果\right)^2+\left(Y方向のフィルタによる計算結果\right)^2} \end{eqnarray} \)

を求めれば、水平・鉛直・斜めすべてのエッジを均等な明るさで取り出すことができる。

課題 3

  1. single関数の中のコードをcompose関数の中にコピー&ペーストする。
  2. compose関数の
            float br = 0;// 出力画像の明度の変数br
            float br1 = 0;// mask1を使った計算用の変数
            float br2 = 0;// mask2を使った計算用の変数
    に変更する。

  3. compose関数の
                // 元画像の(x+i, y+j)のピクセルの明度とマスクの値の積をbrに加える
                br += brightness(img[0].pixels[x+i+(y+j)*w]) * mask[pos];
                // 元画像の(x+i, y+j)のピクセルの明度とそれぞれのマスクの値の積をbr1, br2に加える
                br1 += brightness(img[0].pixels[x+i+(y+j)*w]) * mask1[pos];
                br2 += brightness(img[0].pixels[x+i+(y+j)*w]) * mask2[pos];
    に変更する。

  4. compose関数の
            // 出力画像の(i, j)のピクセルに明度がbrの絶対値の色を設定
            img[n].pixels[i+w*j] = color(abs(br));
            // 出力画像の(i, j)のピクセルに明度が√(br1^2+br2^2)の色を設定
            img[n].pixels[i+w*j] = color(dist(0, 0, br1, br2));
    
    に変更する。
    dist関数は2つの点の距離を求める関数。例えば「float d = dist(x1, y1, x2, y2);」とすると、変数dには 点(x1, y1) と点(x2, y2) の距離が入る。
    原点と点(x, y)の距離なら「dist(0, 0, x, y)」で得られる。
    これは「『xの2乗 + yの2乗』の平方根」に等しい。
    「sqrt(x*x+y*y)」でも同じ結果が得られる。

  5. プログラムを実行する。
    • コンソールに「完了」が表示されてから7キーを押すとX, Y方向のプレヴィットフィルタの結果を合成したもの、8キーを押すとX, Y方向のソーベルフィルタの結果を合成したものが表示される。
    • どちらも全方向のエッジが均等に取り出される。
    • プレヴィットフィルタよりもソーベルフィルタの合成結果の方が明るくなる。

提出

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