午後わてんのブログ

ベランダ菜園とWindows用アプリ作成(WPFとC#)

画像のエッジ抽出、ラプラシアンフィルタ

画像のエッジ抽出、ラプラシアンフィルタ、ラプラシアンオペレータ
 
ラプラシアンフィルタでエッジ抽出した結果
イメージ 1
色の境界、輪郭を白で表現する感じ
 
元の画像
イメージ 2
この前のガウスぼかしと同じようにカーネル(マスク、オペレータ)があって
イメージ 3
この2つがよく使われるみたい、左が上下左右の4近傍、右が斜めも入れた8近傍
 
 
中心の注目ピクセルと、その周りとの輝度差が大きいほど、結果も大きく(白く)なってエッジ(輪郭)になって、差が全くなければプラスマイナス0(黒)
 
 
イメージ 4
8近傍のほうがより強く表現される
最大差
中心輝度が0で周りが255のとき
4近傍だと、255*4-0*4=1020
8近傍だと、255*8-0*8=2040
輝度の最大は255だからそれ以上でも切り捨てになるけど、8近傍のほうがちょっとした差でも大きな差になって現れてくるってことかな
 
 
8近傍のとき上下左右の重みを増して2にしてみると
イメージ 5
さらに強くなった
 
 
 
差を絶対値で取ると
イメージ 6
二重線みたいになる
 
輝度差でエッジを表現するなら、-100も100も同じ100差なんだから絶対値で計算すればいいのかと思ったら、そうでもないようで場合によりけりみたい
 
 
 
エクセル方眼紙で確認

f:id:gogowaten:20191214123322p:plain

左が元の画像で、右2つがエッジ抽出
元画像は黒(0)と白(255)の間に中間の灰色(128)が1ピクセルあるけど
普通に離れて見た場合は灰色は見えなくて黒と白が隣接しているように見えるはず
 
 

f:id:gogowaten:20191214123338p:plain

輝度にマイナスはないから-127は0(黒)に置き換えられるので
 
イメージ 9
エッジとして出てくる線は1本
 
絶対値でみると
イメージ 10
線が2本になる
正確さでいったら絶対値で取ったほうかなあ
白と灰色とのエッジと、灰色と黒のエッジの2つが出てきたほうが自然だと思う
でも実際の画像で見ると絶対値の方はブレたように見えるから不自然なんだよねえ
 
 
 
イメージ 11
白背景に黒の1ピクセル幅のライン
 
イメージ 12
これも絶対値じゃない方は1ピクセル幅で太さが変わっていないから自然に見える
絶対値だと2ピクセル幅になって不自然な印象
 
 
 
イメージ 13
黒背景に白の1ピクセル幅のライン
 
イメージ 14
今度は逆に絶対値じゃないほうが2重線になって不自然
 
 
 
5x5のカーネル

f:id:gogowaten:20191214123359p:plain

ググって見つかったカーネルを試してみた
どれもエッジって感じがしないかなあ、これなら3x3カーネルのほうがいいと思った、使いみちによるのかも
 
画像のぼかし処理にはザラザラしたノイズを抑える効果もあるけど、輪郭(エッジ)もぼやけてしまう
今回のエッジ抽出を使ってエッジ以外をぼかし処理すれば、エッジを残したままノイズだけを抑えた画像ができるかも


イメージ 18
見た目的にきれいなエッジが欲しい時は絶対値じゃないほうだなあ
でもエッジを残してのぼかしは絶対値の方だと思うんだよねえ

 
 
エクセル方眼紙でいろいろ試してみた
左が元の画像、右に並んでいるのが3x3カーネルでいろいろ
クリック注意
大きな画像2511x7521ピクセル

f:id:gogowaten:20191214123430p:plain

 
 
 

参照したところ
 
convolution - When should the sum of all elements of a gaussian kernel be zero? -Signal Processing Stack Exchange
https://dsp.stackexchange.com/questions/8501/when-should-the-sum-of-all-elements-of-a-gaussian-kernel-be-zero
・空間フィルタ2 ラプラシアンフィルタ: 虹色の旋律
http://nijikarasu.cocolog-nifty.com/blog/2014/07/2-4d2e.html
 
画像処理・実習 第五回: 空間フィルタ (特徴抽出,ラプラシアン,鮮鋭化) 東海大学 情報理工学部情報メディア学科 濱本和彦. - ppt download
https://slidesplayer.net/slide/11007913/
微分フィルタで画像のエッジ抽出 - Qiita
https://qiita.com/shim0mura/items/5d3cbef873f2dd81d82c
 
エッジ検出・エッジ強調・ぼかし: koujinz blog
http://koujinz.cocolog-nifty.com/blog/2009/05/post-200b.html
 
【画像処理】ラプラシアンフィルタの原理・特徴・計算式 | アルゴリズム雑記
https://algorithm.joho.info/image-processing/laplacian-filter/

微分ってなんやねん、セブンイレブンかよ
 
 
 
 
ラプラシアンフィルタ、上下左右の4近傍
/// <summary>
/// エッジ抽出、注目ピクセル*4-上下左右、PixelFormats.Gray8専用
/// </summary>
/// <param name="pixels">画像の輝度値配列</param>
/// <param name="width">横ピクセル数</param>
/// <param name="height">縦ピクセル数</param>
/// <param name="absolute">trueなら絶対値で計算</param>
/// <returns></returns>
private (byte[] pixels, BitmapSource bitmap) Filterラプラシアン(byte[] pixels, int width, int height, bool absolute = false)
{
    byte[] filtered = new byte[pixels.Length];//処理後の輝度値用
    int stride = width;//一行のbyte数、Gray8は1ピクセルあたりのbyte数は1byteなのでwidthとおなじになる

    for (int y = 1; y < height - 1; y++)
    {
        for (int x = 1; x < width - 1; x++)
        {
            int p = x + y * stride;//注目ピクセルの位置
            int total = 0;
            total += pixels[p - stride];//上のピクセル
            total += pixels[p - 1];//左
            total += pixels[p + 1];//右
            total += pixels[p + stride];//下
            total -= pixels[p] * 4;//上下左右 - 注目ピクセル*4
            if (absolute)
            {
                total = Math.Abs(total);//絶対値で計算
            }
            //0~255の間に収める
            total = total < 0 ? 0 : total > 255 ? 255 : total;
            filtered[p] = (byte)total;
        }
    }
    return (filtered, BitmapSource.Create(
        width, height, 96, 96, PixelFormats.Gray8, null, filtered, width));
}
基本はこのまえのガウスぼかしと全く同じ、外周の1ピクセルは0で埋めるのも同じ
違うのはカーネルの数値だけだから、輝度にかける数値が違うだけ
 
 

8近傍
private (byte[] pixels, BitmapSource bitmap) Filterラプラシアン8近傍(byte[] pixels, int width, int height, bool absolute = false)
{
    byte[] filtered = new byte[pixels.Length];//処理後の輝度値用
    int stride = width;//一行のbyte数、Gray8は1ピクセルあたりのbyte数は1byteなのでwidthとおなじになる
    int total;
    int begin = stride + 1;
    int end = pixels.Length - stride - 1;
    for (int i = begin; i < end; i++)
    {
        total = 0;
        total += pixels[i - stride - 1];//注目ピクセルの左上
        total += pixels[i - stride];    //上
        total += pixels[i - stride + 1];//右上
        total += pixels[i - 1];         //左
        total += pixels[i + 1];         //右
        total += pixels[i + stride - 1];//左下
        total += pixels[i + stride];    //した
        total += pixels[i + stride + 1];//右下
        total -= pixels[i] * 8;
        if (absolute) total = Math.Abs(total);

        total = total < 0 ? 0 : total > 255 ? 255 : total;
        filtered[i] = (byte)total;
    }

    return (filtered, BitmapSource.Create(
        width, height, 96, 96, PixelFormats.Gray8, null, filtered, width));
} 
forを1個減らしてみた、こっちのほうが速いはずだけど誤差程度
 
 
 
5x5のカーネル、中心が-24でそれ以外は1の固定

private (byte[] pixels, BitmapSource bitmap) Filterラプラシアン5x5近傍(byte[] pixels, int width, int height, bool absolute = false)
{
    //1, 1,  1, 1, 1
    //1, 1,  1, 1, 1
    //1, 1,-24, 1, 1
    //1, 1,  1, 1, 1
    //1, 1,  1, 1, 1
    byte[] filtered = new byte[pixels.Length];//処理後の輝度値用
    int stride = width;//一行のbyte数、Gray8は1ピクセルあたりのbyte数は1byteなのでwidthとおなじになる
    int total;
    int diff1 = -stride * 2 - 2;
    int diff2 = -stride - 2;
    int diff3 = -2;
    int diff4 = stride - 2;
    int diff5 = stride * 2 - 2;

    for (int y = 2; y < height - 2; y++)
    {
        for (int x = 2; x < width - 2; x++)
        {
            int p = y * stride + x;
            total = 0;
            for (int z = 0; z < 5; z++)
            {
                total += pixels[p + diff1 + z];
                total += pixels[p + diff2 + z];
                total += pixels[p + diff3 + z];
                total += pixels[p + diff4 + z];
                total += pixels[p + diff5 + z];
            }
            total -= pixels[p];
            total -= pixels[p] * 24;
            if (absolute) total = Math.Abs(total);

            total = total < 0 ? 0 : total > 255 ? 255 : total;
            filtered[p] = (byte)total;
        }
    }
    return (filtered, BitmapSource.Create(
        width, height, 96, 96, PixelFormats.Gray8, null, filtered, width));
}
forをできるだけ使いたくないけど5x5だと参照ピクセルが25個もあってさすがに長くなるから一番内側の5個以外はforで
 
 
 
 
 
5x5のカーネル、汎用版
カーネルの数値はint[5,5]の配列で指定

private (byte[] pixels, BitmapSource bitmap) Filterラプラシアン5x5近傍2(int[,] weight, byte[] pixels, int width, int height, bool absolute = false)
{
    byte[] filtered = new byte[pixels.Length];//処理後の輝度値用
    int stride = width;//一行のbyte数、Gray8は1ピクセルあたりのbyte数は1byteなのでwidthとおなじになる
    int total;

    for (int y = 2; y < height - 2; y++)
    {
        for (int x = 2; x < width - 2; x++)
        {
            int p = y * stride + x;
            total = 0;
            for (int i = 0; i < 5; i++)
            {
                int pp = p + stride * (i - 2);
                for (int j = 0; j < 5; j++)
                {
                    total += pixels[pp + (j - 2)] * weight[i, j];
                }
            }
            if (absolute) total = Math.Abs(total);

            total = total < 0 ? 0 : total > 255 ? 255 : total;
            filtered[p] = (byte)total;
        }
    }
    return (filtered, BitmapSource.Create(
        width, height, 96, 96, PixelFormats.Gray8, null, filtered, width));
}
さっきの固定だったカーネルを指定できるようにしたもの
ここは素直にforを使って書いた
これを使っているところは

private void Button_Click_11(object sender, RoutedEventArgs e)
{
    if (MyPixels == null) { return; }
    int[,] weight = {
        { -1, -4, -7, -4, -1 },
        { -4,  0,  8,  0, -4 },
        { -7,  8, 32,  8, -7 },
        { -4,  0,  8,  0, -4 },
        { -1, -4, -7, -4, -1 } };

    (byte[] pixels, BitmapSource bitmap) = Filterラプラシアン5x5近傍2(
        weight,
        MyPixels,
        MyBitmapOrigin.PixelWidth,
        MyBitmapOrigin.PixelHeight,
        (bool)CheckBoxAbsolute.IsChecked);
    MyImage.Source = bitmap;
    MyPixels = pixels;
}
 
 
 
画像の読み込み

/// <summary>
/// 画像ファイルからbitmapと、そのbyte配列を取得、ピクセルフォーマットを指定したものに変換
/// </summary>
/// <param name="filePath">画像ファイルのフルパス</param>
/// <param name="pixelFormat">PixelFormatsを指定</param>
/// <param name="dpiX">96が基本、指定なしなら元画像と同じにする</param>
/// <param name="dpiY">96が基本、指定なしなら元画像と同じにする</param>
/// <returns></returns>
private (byte[] array, BitmapSource source) MakeBitmapSourceAndByteArray(string filePath, PixelFormat pixelFormat, double dpiX = 0, double dpiY = 0)
{
    byte[] pixels = null;
    BitmapSource source = null;
    try
    {
        using (System.IO.FileStream fs = new System.IO.FileStream(filePath, System.IO.FileMode.Open, System.IO.FileAccess.Read))
        {
            var bf = BitmapFrame.Create(fs);

            var convertedBitmap = new FormatConvertedBitmap(bf, pixelFormat, null, 0);
            int w = convertedBitmap.PixelWidth;
            int h = convertedBitmap.PixelHeight;
            int stride = (w * pixelFormat.BitsPerPixel + 7) / 8;
            pixels = new byte[h * stride];
            convertedBitmap.CopyPixels(pixels, stride, 0);
            //dpi指定がなければ元の画像と同じdpiにする
            if (dpiX == 0) { dpiX = bf.DpiX; }
            if (dpiY == 0) { dpiY = bf.DpiY; }
            //dpiを指定してBitmapSource作成
            source = BitmapSource.Create(
                w, h, dpiX, dpiY,
                convertedBitmap.Format,
                convertedBitmap.Palette, pixels, stride);
        };
    }
    catch (Exception)
    {
    }
    return (pixels, source);
}
この部分はいつもと同じ
 
 
//画像ファイルドロップ時の処理
private void MainWindow_Drop(object sender, DragEventArgs e)
{
    if (e.Data.GetDataPresent(DataFormats.FileDrop) == false) { return; }
    string[] filePath = (string[])e.Data.GetData(DataFormats.FileDrop);
    var (pixels, bitmap) = MakeBitmapSourceAndByteArray(filePath[0], PixelFormats.Gray8, 96, 96);

    if (bitmap == null)
    {
        MessageBox.Show("画像ファイルじゃないみたい");
    }
    else
    {
        MyPixels = pixels;
        MyPixelsOrigin = pixels;
        MyBitmapOrigin = bitmap;
        MyImage.Source = bitmap;
        MyImageOrigin.Source = bitmap;
        ImageFileFullPath = filePath[0];
    }
}
 
 
ギットハブ

github.com

 
 (ギットハブ)
イメージ 16
画像ファイルドロップで表示、ボタンで変換、表示画像クリックで元の画像と切り替え
 
 
 
関連記事
2019/4/22は18日前

gogowaten.hatenablog.com

画像のぼかし処理、注目ピクセルとその上下左右の平均値に変換 ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15938990.html
 
前回、2019/4/30は10日前

gogowaten.hatenablog.com

ガウス関数からカーネル作成、標準偏差カーネルサイズ、グレースケール画像のぼかし処理 ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15945699.html
 
次、2019/05/22は2週間後

gogowaten.hatenablog.com

画像にノイズ付加するアプリ、一様分布乱数から正規分布乱数生成、エクセルのNORMINV関数で正規乱数 ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15961286.html
 
2019/05/29は19日後

gogowaten.hatenablog.com

メディアンフィルタで画像のノイズ除去試してみた、WPFC# ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15965377.html
 
1年後