datchの日記

気がついたら社会人。気になる技術的なことについて少しずつ書いていけたらと思っております。

【画像処理】エッジ抽出

どうも、こんばんは。

色々と作業してたらこんな時間になってしまいました。

本当はCreateJSの方を触りたかったのですが、残り2時間という時間の都合上、今回は自身が触っている専門分野である画像処理について話を進めていきたいと思います。

エッジ抽出とは?

エッジとは境界を指すものであり、ある物体の境目がエッジ。

画像処理では画像からの物体の境目を抽出することを「エッジ抽出」と呼ぶ。

画像は周波数で出来ている

フーリエの授業の時に聞いた時ははっきり言って何を言っているかさっぱりだったが、

画像処理に触れることで画像が周波数で出来ているという意味を理解できた。

周波数とはその名の通り波のことだが、画像でいう波とはピクセル値の変動を指している。

今回は画像処理界隈で有名なレナさんを元に考えてみよう。

波を見る

まずは以下のC++ソースコードを見てみる。

// 実際に動作確認をしている訳ではないので、コピペ使用非推奨
cv::Mat img = cv::imread("lena.jpg");
const int y = img.rows / 2;
for(int x = 1; x < img.cols; ++x)
{
    cv::Vec3b vp = img.at<cv::Vec3b>(y, x - 1);
    cv::Vec3b vn = img.at<cv::Vec3b>(y, x);
    // 前の画素値との差分を出力
    std::cout << "R : " << vn.val(0) - vp..val(0) << std::endl;
    std::cout << "G : " << vn.val(1) - vp.val(1) << std::endl;
    std::cout << "B : " << vn.val(2) - vp.val(2) << std::endl;
}

これは画像の高さの中心からx方向にラスタスキャンを行い、値を単純に出力しているだけだ。

レナさんの中央の帽子の繊維の青紫色の部分に差し掛かった瞬間、

この差分の値を非常に大きくなるのは少し考えれば分かるはずだ。

なぜなら帽子の繊維背景の色には大きな違いがあるからだ。

エッジをみるには微分する

実は先程のソースコードでピクセル間の色の差分を取っていたが、あれは離散数学的に言えば微分を行っている。

微分については解説していると大変なので端折るが、要はどれだけ値が前の状態と変化したかを表す。

さて、勘の良い人はソースコードを見た時点で気付き始めていると思うが、

エッジ抽出を行うにはこの微分を用いればいいのだ。

よ~し、分かった人もいることだし叔父さんが実装しちゃうぞ~!

cv::Mat img = cv::imread("lena.jpg");
cv::Mat edge(cv::Size(img.cols, img.rows), CV_8U);
for(int y = 0; y < img.rows; ++y)
{
    for(int x = 1; x < img.cols; ++x)
    {
        cv::Vec3b vp = img.at<cv::Vec3b>(y, x - 1);
        cv::Vec3b np = img.at<cv::Vec3b>(y, x);
        int sum = 0;
        for(int c = 0; c < 3; ++c) sum += np..val(c) - vp..val(c);
        edge.at<uchar>(y, x) = sum / 3; // 今回は単純に平均を使用
    }
}
cv::imshow("edge", edge);

手元でOpenCVが動かせないのが非常に残念だが、これで縦方向のエッジを抽出することが出来た。

よし、これでy方向にスキャンすれば縦方向、横方向のエッジが取れるぞ~!

なんていう、思いつきでのやり方だとこんな感じか。

これでも一応取れるのだが、世の中にはもっと便利でありがたいやり方がある。

畳み込み積とフィルタ

ここで初等数学で習う畳み込み積を使う。

畳み込む値の対象はもちろん微分をした値だ。

分かりやすい画像があったのでお借りします。

これが畳み込み積で行った結果。

中心のピクセルに対して、周囲3x3の値をフィルタの値を掛けて足し合わせる処理をしている。

コードで表すとこんな感じか。

cv::Mat img = cv::imread("lena.jpg");
cv::Mat edge(cv::Size(img.cols, img.rows), CV_8U);
static const int filter[3][3] = {{0,1,0},{0,0,0},{0,0,0}};
for(int y = 0; y < img.rows; ++y)
{
    for(int x = 0; x < img.cols; ++x)
    {
        int sum = 0;
        cv::Vec3b v = img.at<cv::Vec3b>(y, x);
        for(int fy = 0; fy < 3; ++fy)
        {
            for(int fx = 0; fx < 3; ++fx)
            {
                const int tx = fx - 3/2 + x, int ty = fy - 3/2 + y;
                int avg = 0;
                for(int c = 0; c < 3; ++c) avg += v.val(c);
                sum += filter[fy][fx] * (avg / 3);
            }
        }
        edge.at<uchar>(y, x) = (sum > 255) ? 255 : sum; // オーバーフロウ対策
    }
}

畳み込み積をコードで表すとこんな感じ。

4重ループでネストが深くなるので、自前のUtilで作るなら関数化必須ですが。

フィルタの違い

世の中には沢山のエッジ抽出方法があります。

Sobel, Canny, Laplacian, Gaussian(ガウシアンは平滑化フィルタなのになぜこの時の自分、ここに書いてるのだ?), LoG(Laplacia of Gaussian), etc.

うわ~、沢山手法あって大変そう。

なんて思ってる人も結構多かったりしそうですが、名前が違うだけで所詮フィルタの値が違うだけなのです。

つまり、フィルタを対応するものに変えて上記のソースコードを回せばエッジ抽出が完了します。

以下はLoGを使ったレナお姉さんのエッジ抽出結果。

ちなみにOpenCVとかだと一行でこの処理をすることが出来たりもします。

OpenCVさん、マジパネっす。


最後に

ここまで呼んで頂きありがとうございました。

エッジ抽出とか言葉尻は難しいけど、コードに直すと所詮ただの4重ループということがわかったと思います。

たまにはこうやって画像処理の話し挟むのも面白いかもね。

けど、次はCreateJSをやる!