ここから本文です
ベランダ菜園とWindows用アプリ作成とExcel用アプリ(アドイン)作成

書庫WPF、C# .NET

記事検索
検索

全21ページ

[1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11]

[ 次のページ ]

3×3メディアンフィルタの高速アルゴリズム
https://www.ipsj.or.jp/award/9faeag0000004f1r-att/LI_9.pdf
ここを見てC#で書いて実行した結果

イメージ 1
前回4.39秒だったのが0.20秒になった
439/20≒22で、約22倍も速くなったことになる、すごい

使った画像は
イメージ 2
2048x1536ピクセル、これをグレースケール画像(ピクセルフォーマットGray8)に変換したもの
パソコンのCPUはPhenomⅡX3 720@3.0GHz(2009年発売は10年前)
7/7発売予定のRyzen 9 3900Xだとどれくらい速いんだろうねえ
グレードを合わせるとPhenomⅡX3 720はRyzen 5 3400Gかもう少し下かな、ちょうど10年でどれだけ速くなったのかとか、わたし、気になります!


前回はLINQでソートしていた
メディアンフィルタの処理は自身の値を、自身と周囲8近傍の値でソートして、その中央値に置き換える
この処理の中で時間がかかるのが中央値を得るための「ソート」
前回はここをLINQのOrderByメソッドを使って

中央値 = 値の配列.OrderBy(z => z).ToList()[4];

こう書いていた
1行で済むからラクだけど、これが遅い



イメージ 5
7, 2, 6
6,10, 9
9, 3, 2
この中の中央値を求める
9つの数値を順番に並べ替えて中央(5番目)


3つのユニットに分ける
イメージ 4
3列に分ける、グループって書いてるけどunitで


step1、1回目のソート
イメージ 6
unitの中をソートする


step2、ユニットのソート
イメージ 7
ユニットの中央値を使ってユニットをソート


ここまでで確定したところを不等号で表す
イメージ 3
不等号がないところは不確定

さっきの数値を入れてみると
イメージ 8
ソートはこれで終了
次はここから中央値を見つけ出す


配列のインデックス
イメージ 9
配列のインデックスを左上から下に割り振ったところ
中央の[4]に注目して
[4]より大きな数値が入っていることが確定している場所は
左上の3つ[0][1][3]で
逆に小さいことが確定しているのは
右下の3つ[5][7][8]
不確定なのは[2][6]


case 1
もし[4]が[2][6]との中間なら
[4]の値が中央値として確定
イメージ 10
[2] >= [4] >= [6]
または
[6] >= [4] >= [2]
なら
[4]が中央値

[4]からみて上位3つと下位3つは確定していて

上位 不明 下位
[][][] [][][] [][][]

もし[2] >= [4] >= [6]なら
[2]は上位に入って、[6]は下位に入ることになる
もし[6] >= [4] >= [2]なら
[6]は上位に入って、[2]は下位に入ることになる
どちらにしても結果は

上位 下位
[][][][] [4] [][][][]

[2][6]の順位はわからないけど[4]が中央なのが確定する



case 2
[4]が小さい
イメージ 11
if (v[2] >= v[4] && v[6] >= v[4])
[4]が[2][6]以下だった場合は
[4]が大きい順だと6番目が確定
これは

イメージ 12
[4]より大きいのが確定していた[0][1][3]に[2][6]が加わったので
[4]は6番目
これで中央値は[0][1][3][2][6]の中の5番目(最小値)ってことになる
ここで[0][1]は[2]より大きいので対象から外れて

のこりの[3][2][6]の最小値が中央値になる
イメージ 13
あとはMathクラスのMinメソッドで求める
中央値 = Math.Min(v[2], Math.Min(v[3], v[6]));


case 3
case1, 2どちらにも当てはまらなかったときは
[4]が[2][6]以上なので
イメージ 14
case 2の逆になっただけ

[4]は小さい順の6番目が確定なので
イメージ 16
残りの5個の中の最大値が中央値(5番目)
[7][8]は[6]より小さいので最大値になりえないので
残りの[2][5][6]のどれかに絞られる

Maxメソッドで
イメージ 15
[2][5][6]の最大値を求めればいい
中央値 = Math.Max(v[2], Math.Max(v[5], v[6]));
これで全部


さっきの例だと
イメージ 17
step2のソートが終わった状態(右)
case 1の
[2] >= [4] >= [6]
または
[6] >= [4] >= [2]
に当てはまる
6>=6>=10
10>=6>=6←これ
なので中央値は6

ここまでの方法を使ったのがメディアンフィルタ高速化1ボタン
イメージ 18
これで4.39/0.25≒17.5倍速くなった


もっと速く
イメージ 19
(2, 2)を処理、次は右隣の

イメージ 20
(3,2)

前後の処理範囲で重なる部分
イメージ 21
赤の範囲を処理したときの右2列は次回の青の範囲にもなるので
赤のときのソート結果を記録しておけば
青のときにソートする手間が省ける
これを使ったのが
イメージ 22
メディアンフィルタ高速化2
これでLINQより22倍速くなって
高速化1より25/20=1.25倍速くなった
素晴らしい


メディアンフィルタ高速化1
イメージ 23
156〜164、自身と8近傍の値はユニットごとに分けずに一つの配列に入れた
ユニットの中をソートする

イメージ 24
この部分の処理
168行目の,MedianSort3x3byteメソッドは
イメージ 25
地道な並べ替え


イメージ 26
step 2のソート
174行目のMedianSortUnitは
イメージ 27
地道すぎる


176行目、ソートが終わった配列から中央値を見つけ出す
MedianFind
イメージ 28
これでやっと中央値がわかる
LINQだと1行なのに100行くらいになったw



メディアンフィルタ高速化2
イメージ 29
高速化1の改変で、一時記録用の変数が増えただけなんだけど、長くなってしまった
違っていところはユニットの中をソートするところ、3ユニットから1ユニットに減っているので、9要素の配列のどこから3つをソートするかを指定できる
MedianSortUnitを365、370、395行目で使っている

イメージ 30
3ユニットまとめてソートするMedianSort3x3byteとほとんど同じ


最初はユニットごとに配列にして、さらにそれを配列にしたもの、つまり配列の配列byte[][]を使おうとしたんだけど、うまく書けなかった
前後のピクセルで処理が重なる6ピクセル分のソート結果を一時記録しておく変数も6個の変数じゃなくて配列にまとめたかったけど、これもうまく書けなかった
まだ参照型の値渡し、参照型の参照渡し、そもそも参照型ってのが理解できていない
高速化1に比べてソートするユニットは3から1ユニットに減っているのに、速度は1.25倍に留まったのは、やっぱり書き方が良くないのかも

高速化1はあっさり書けて、それで10倍くらい速くなったから気を良くしてたんだけど、高速化2のほうは2日もかかった、それで1.25倍だったからねちょっとがっかりしたのがあったw
でももとに比べると22倍速くなったからね


ReleaseとDebugの速度差
イメージ 31
LINQはReleaseでもDebugでもほとんど変化なしだけど、手動でソートする高速化はReleaseだとかなり速くなる


参照したところ
3×3メディアンフィルタの高速アルゴリズム
https://www.ipsj.or.jp/award/9faeag0000004f1r-att/LI_9.pdf
さらに速くする方法も書いてあるみたいだけど理解できなかった…
でも、こういうの思いつくのすごい!
20倍以上も速くなるなんて思ってなかったから驚いた


ギットハブ
アプリダウンロード先
イメージ 32
画像表示は画像ファイルドロップorクリップボードからの貼り付け
フィルタは重ねがけできるようにしてあるので、それぞれのフィルタの速度差を測るときは、フィルタを掛けたら一度元の画像に戻すを押してから次のフィルタをすると、同じ条件でできる
画像クリックで元の画像と切り替え


関連記事
前回、2019/5/29は3日前
メディアンフィルタで画像のノイズ除去試してみた、WPF、C# ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15965377.html




メディアンフィルタでごま塩ノイズ除去
イメージ 1
全体的にぼやけてしまうけどごま塩ノイズを除去できた

メディアンフィルタ
イメージ 2
注目セルとその周囲8近傍の値の中央値を
注目セルの新しい値にする


エクセルには中央値を求めるMEDIAN関数がある
イメージ 4
便利だなあ

色を付けてみると
イメージ 3
黒ごま(ノイズ)が消えているのがわかる


イメージ 5
ランダムノイズに対してはイマイチかなあ、ノイズがにじんだようになる


盛大なノイズが乗った画像にメディアンフィルタ
イメージ 6
ごま塩ノイズに対しては大きな効き目がある
重ねがけした結果は画像エフェクトとしてはありかも


イメージ 7
ランダムなノイズに対してはイマイチな感じ


低画質なjpg画像にメディアンフィルタ
イメージ 8
ぼやけてしまうけど見やすくなっていると思う、png形式で保存したときもファイルサイズが減った

左上を拡大
イメージ 9
ノイズが減っている


図形画像
イメージ 10
これも低画質jpg


メディアンフィルタの前後の画像を拡大して
ラプラシアンフィルタでエッジ抽出して比較
イメージ 11
かなりきれいにノイズが取れている
こういう図形についたノイズ除去に適しているのかも
これはいいなあ


参照するピクセルを変更してみた
イメージ 12
赤マスが注目ピクセル、青マスが参照ピクセル
結果はノイズ除去には適していない、普通の8近傍がいいことがわかった

イメージ 19
赤いところのボタンがそれ


ごま塩ノイズにメディアンフィルタ
イメージ 13
4近傍は少しごま塩が残っている、4近傍(斜め)もごま塩が残っている+エッジ部分がメッシュ状になっている
左下の自身を除く8近傍ってのは、注目ピクセル以外の8ピクセルの中央値、この結果もいまいちでエッジ部分でざわざわしている、24近傍は盛大にぼやけているし、処理時間もかなり掛かる


4近傍を2回と普通の8近傍を比較
イメージ 14
似たような結果になった
よく見ると8近傍は直角のエッジ部分が丸くなってしまっているけど、4x2のほうはエッジが残っている、意外に4x2もいいかも


ランダムノイズにメディアンフィルタ
イメージ 15
どれもいまいち


低画質jpgにメディアンフィルタ
イメージ 16
全体的には普通の8近傍がいいけど、4x2はエッジが残っているのがいい




メディアンフィルタはごま塩ノイズ除去が得意なフィルタなのかと思っていたけど、低画質なjpg画像のノイズにも有効なのがわかった
エッジを残してノイズだけ除去するのが理想だけど、トレードオフな感じ
エッジなのかノイズなのかを判定できればなあ



デザイン画面
イメージ 17


MainWindow.xaml.cs

using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace _20190528_メディアンフィルタ
{
/// <summary>
/// MainWindow.xaml の相互作用ロジック
/// </summary>
public partial class MainWindow : Window
{
string ImageFileFullPath;//ファイルパス、画像保存時に使う
BitmapSource MyBitmapOrigin;//元画像、リセット用
byte[] MyPixelsOrigin;//元画像、リセット用
byte[] MyPixels;//処理後画像

public MainWindow()
{
InitializeComponent();

this.Drop += MainWindow_Drop;
this.AllowDrop = true;

}

/// <summary>
/// ピクセルフォーマットGray8専用
/// </summary>
/// <param name="pixels">ピクセルの値の配列</param>
/// <param name="width"></param>
/// <param name="height"></param>
/// <returns></returns>
private (byte[] pixels, BitmapSource bitmap) Filterメディアン(byte[] pixels, int width, int height)
{
byte[] filtered = new byte[pixels.Length];//処理後の輝度値用
int stride = width;//一行のbyte数、Gray8は1ピクセルあたり1byteなのでwidthとおなじになる
byte[] v = new byte[9];//自身と8近傍の輝度値用

for (int y = 1; y < height - 1; y++)
{
for (int x = 1; x < width - 1; x++)
{
int p = x + y * stride;//注目ピクセルの位置
v[0] = pixels[p - stride - 1];//注目ピクセルの左上
v[1] = pixels[p - stride];//上
v[2] = pixels[p - stride + 1];//右上
v[3] = pixels[p - 1];//左
v[4] = pixels[p];
v[5] = pixels[p + 1];//右
v[6] = pixels[p + stride - 1];//左下
v[7] = pixels[p + stride];//下
v[8] = pixels[p + stride + 1];//右下
//ソートして中央の値(5番目)を新しい値にする
filtered[p] = v.OrderBy(z => z).ToList()[4];
}
}
return (filtered, BitmapSource.Create(
width, height, 96, 96, PixelFormats.Gray8, null, filtered, width));
}

private (byte[] pixels, BitmapSource bitmap) Filterメディアン4近傍(byte[] pixels, int width, int height)
{
byte[] filtered = new byte[pixels.Length];//処理後の輝度値用
int stride = width;
byte[] v = new byte[5];

for (int y = 1; y < height - 1; y++)
{
for (int x = 1; x < width - 1; x++)
{
int p = x + y * stride;//注目ピクセルの位置
v[0] = pixels[p - stride];//注目ピクセルの上
v[1] = pixels[p - 1];//左
v[2] = pixels[p];
v[3] = pixels[p + 1];//右
v[4] = pixels[p + stride];//下

//ソートして中央の値(3番目)を新しい値にする
filtered[p] = v.OrderBy(z => z).ToList()[2];
}
}
return (filtered, BitmapSource.Create(
width, height, 96, 96, PixelFormats.Gray8, null, filtered, width));
}

private (byte[] pixels, BitmapSource bitmap) Filterメディアン4近傍斜め(byte[] pixels, int width, int height)
{
byte[] filtered = new byte[pixels.Length];//処理後の輝度値用
int stride = width;//一行のbyte数、Gray8は1ピクセルあたりのbyte数は1byteなのでwidthとおなじになる
byte[] v = new byte[5];

for (int y = 1; y < height - 1; y++)
{
for (int x = 1; x < width - 1; x++)
{
int p = x + y * stride;//注目ピクセルの位置
v[0] = pixels[p - stride - 1];//注目ピクセルの左上
v[1] = pixels[p - stride + 1];//右上
v[2] = pixels[p];
v[3] = pixels[p + stride - 1];//左下
v[4] = pixels[p + stride + 1];//右下

//ソートして中央の値(3番目)を新しい値にする
filtered[p] = v.OrderBy(z => z).ToList()[2];
}
}
return (filtered, BitmapSource.Create(
width, height, 96, 96, PixelFormats.Gray8, null, filtered, width));
}


//自身を除く8近傍のメディアン、要素数が偶数のメディアンなので中央の2つの値の平均値
private (byte[] pixels, BitmapSource bitmap) Filterメディアン自身を除く(byte[] pixels, int width, int height)
{
byte[] filtered = new byte[pixels.Length];//処理後の輝度値用
int stride = width;
byte[] v = new byte[8];

for (int y = 1; y < height - 1; y++)
{
for (int x = 1; x < width - 1; x++)
{
int p = x + y * stride;//注目ピクセルの位置
v[0] = pixels[p - stride - 1];//注目ピクセルの左上
v[1] = pixels[p - stride];//上
v[2] = pixels[p - stride + 1];//右上
v[3] = pixels[p - 1];//左
//v[4] = pixels[p];
v[4] = pixels[p + 1];//右
v[5] = pixels[p + stride - 1];//左下
v[6] = pixels[p + stride];//下
v[7] = pixels[p + stride + 1];//右下
//ソートして4,5番目の平均値を四捨五入を新しい値にする
var temp = v.OrderBy(z => z).ToList();
//byte neko = (byte)Math.Round((temp[3] + temp[4]) / 2.0, MidpointRounding.AwayFromZero);
filtered[p] = (byte)Math.Round((temp[3] + temp[4]) / 2.0, MidpointRounding.AwayFromZero);
}
}
return (filtered, BitmapSource.Create(
width, height, 96, 96, PixelFormats.Gray8, null, filtered, width));
}

private (byte[] pixels, BitmapSource bitmap) Filterメディアン24近傍(byte[] pixels, int width, int height)
{
byte[] filtered = new byte[pixels.Length];//処理後の輝度値用
int stride = width;
byte[] v = new byte[25];//注目ピクセルとその近傍24ピクセルの輝度値用

for (int y = 2; y < height - 2; y++)
{
for (int x = 2; x < width - 2; x++)
{
//24近傍と注目ピクセルの輝度値収集
for (int i = 0; i < 5; i++)
{
int pp = (y + i - 2) * stride + x - 2;
for (int j = 0; j < 5; j++)
{
v[i * 5 + j] = pixels[pp + j];
}
}
var neko = v.OrderBy(z => z).ToList()[12];
//輝度値をソートして中央値(13番目)を新しい値にする
filtered[y * stride + x] = v.OrderBy(z => z).ToList()[12];
}
}
return (filtered, BitmapSource.Create(
width, height, 96, 96, PixelFormats.Gray8, null, filtered, width));
}




#region その他

//画像ファイルドロップ時の処理
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];
}
}


//画像の保存
private void SaveImage(BitmapSource source)
{
var saveFileDialog = new Microsoft.Win32.SaveFileDialog();
saveFileDialog.Filter = "*.png|*.png|*.bmp|*.bmp|*.tiff|*.tiff";
saveFileDialog.AddExtension = true;
saveFileDialog.FileName = System.IO.Path.GetFileNameWithoutExtension(ImageFileFullPath) + "_";
saveFileDialog.InitialDirectory = System.IO.Path.GetDirectoryName(ImageFileFullPath);
if (saveFileDialog.ShowDialog() == true)
{
BitmapEncoder encoder = new BmpBitmapEncoder();
if (saveFileDialog.FilterIndex == 1)
{
encoder = new PngBitmapEncoder();
}
else if (saveFileDialog.FilterIndex == 2)
{
encoder = new BmpBitmapEncoder();
}
else if (saveFileDialog.FilterIndex == 3)
{
encoder = new TiffBitmapEncoder();
}
encoder.Frames.Add(BitmapFrame.Create(source));

using (var fs = new System.IO.FileStream(saveFileDialog.FileName, System.IO.FileMode.Create, System.IO.FileAccess.Write))
{
encoder.Save(fs);
}
}
}


/// <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 Grid_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
int aa = Panel.GetZIndex(MyImage);
Panel.SetZIndex(MyImageOrigin, aa + 1);
}

private void Grid_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
int aa = Panel.GetZIndex(MyImage);
Panel.SetZIndex(MyImageOrigin, aa - 1);
}

//表示画像リセット
private void Button_Click_2(object sender, RoutedEventArgs e)
{
MyImage.Source = MyBitmapOrigin;
MyPixels = MyPixelsOrigin;
}

//画像保存
private void Button_Click_3(object sender, RoutedEventArgs e)
{
if (MyImage.Source == null) { return; }
//BitmapSource source = (BitmapSource)MyImage.Source;
//SaveImage(new FormatConvertedBitmap(source, PixelFormats.Indexed4, new BitmapPalette(source, 16), 0));
//SaveImage(new FormatConvertedBitmap(source, PixelFormats.Indexed4, null, 0));
SaveImage((BitmapSource)MyImage.Source);
}



#endregion

private void Button_Click_1(object sender, RoutedEventArgs e)
{
//クリップボードから画像を貼り付け
var source = Clipboard.GetImage();
if (source == null)
{
MessageBox.Show("(クリップボードに画像は)ないです");
}
else
{
//クリップボードに画像があったらピクセルフォーマットをGray8に変換して取り込む
int w = source.PixelWidth;
int h = source.PixelHeight;
int stride = w;
var gray = new FormatConvertedBitmap(source, PixelFormats.Gray8, null, 0);
byte[] pixels = new byte[h * stride];
gray.CopyPixels(pixels, stride, 0);
MyPixels = pixels;
MyPixelsOrigin = pixels;
MyBitmapOrigin = gray;
MyImage.Source = gray;
MyImageOrigin.Source = gray;
}
}

private void Button_Click_4(object sender, RoutedEventArgs e)
{
//クリップボードに画像をコピー
Clipboard.SetImage((BitmapSource)MyImage.Source);
}

private void Button_Click(object sender, RoutedEventArgs e)
{
if (MyPixels == null) { return; }
(byte[] pixels, BitmapSource bitmap) = Filterメディアン(
MyPixels,
MyBitmapOrigin.PixelWidth,
MyBitmapOrigin.PixelHeight);
MyImage.Source = bitmap;
MyPixels = pixels;
}

private void Button_Click_5(object sender, RoutedEventArgs e)
{
if (MyPixels == null) { return; }
(byte[] pixels, BitmapSource bitmap) = Filterメディアン4近傍(
MyPixels,
MyBitmapOrigin.PixelWidth,
MyBitmapOrigin.PixelHeight);
MyImage.Source = bitmap;
MyPixels = pixels;

}

private void Button_Click_6(object sender, RoutedEventArgs e)
{
if (MyPixels == null) { return; }
(byte[] pixels, BitmapSource bitmap) = Filterメディアン4近傍斜め(
MyPixels,
MyBitmapOrigin.PixelWidth,
MyBitmapOrigin.PixelHeight);
MyImage.Source = bitmap;
MyPixels = pixels;
}

private void Button_Click_7(object sender, RoutedEventArgs e)
{
if (MyPixels == null) { return; }
(byte[] pixels, BitmapSource bitmap) = Filterメディアン自身を除く(
MyPixels,
MyBitmapOrigin.PixelWidth,
MyBitmapOrigin.PixelHeight);
MyImage.Source = bitmap;
MyPixels = pixels;
}

private void Button_Click_8(object sender, RoutedEventArgs e)
{
if (MyPixels == null) { return; }
(byte[] pixels, BitmapSource bitmap) = Filterメディアン24近傍(
MyPixels,
MyBitmapOrigin.PixelWidth,
MyBitmapOrigin.PixelHeight);
MyImage.Source = bitmap;
MyPixels = pixels;
}
}
}



ギットハブ

アプリダウンロード先
イメージ 18
画像ファイルドロップで画像表示、またはクリップボードからの貼り付け
画像クリックで元の画像と切り替え


関連記事
次回、2019/06/01は3日後
3x3のメディアンフィルタの高速化をC#で書いてみた ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15967500.html
20倍以上速くなった!


2019/5/24は5日前
画像にノイズ付加するアプリ、カラー版 ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15962326.html


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


2019/5/10は19日前
画像のエッジ抽出、ラプラシアンフィルタ ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15953028.html





画像にノイズ付加するアプリ、一様分布乱数から正規分布乱数生成、エクセルのNORMINV関数で正規乱数 ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15961286.html
この前はグレースケール画像だったのをカラー画像でも試してみた
カラー画像にはカラーノイズとそうじゃないノイズの2つができた

イメージ 1

イメージ 2
元の画像


ノイズの強さ50
左がカラーノイズ
イメージ 38
できた
電波状況が良くないアナログテレビみたい
昔住んでたところの共同アンテナがイマイチでこれに
疑似輪郭のゴーストもプラスされてひどかったのを思い出すなど



4倍に拡大してみる
イメージ 3
元のRGBに別々の乱数を足している左は、元の色相とはかけ離れた色になるけど
同じ乱数を足した右の方は、ある程度同じ色相になる感じ



ごま塩ノイズ
イメージ 4
カラーは2種類できた、最初にできたのは左なんだけどなんか違うのに気づいて真ん中のカラー2を加えた
カラー2はピクセルごとにノイズを付加するかどうかを判定して、付加するならRGB別々の乱数で0か255にしているので、ノイズ判定になったピクセルの色は、RGBそれぞれで0か255の組み合わせ8色のうちのどれかになる
左はRGBごとにノイズを付加するかどうかを判定して、そのまま0か255にしている
なので同じピクセルでもRGBごとに元の値だったり、0か255になるから色んな色になる


カラーごま塩
イメージ 5
これもいいけど、これじゃない


カラーごま塩2
イメージ 7
真・カラーごま塩


普通のごま塩
イメージ 6


前回のグレースケール画像専用と比較
イメージ 9
これがグレースケール画像用で、これをピクセルフォーマットRgb24のカラー版にすると

ピクセルフォーマットRgb24専用
イメージ 8
書き換えたところは
ピクセルフォーマットがGray8からRgb24になったので
画像の1ピクセル行のbyte数を表すstrideの値が3倍、86行目
それに合わせてbyte配列の要素数も変わったので、ループ回数も変化、90行目
これだけ、思っていたよりだいぶ少ない
これの結果が
カラーノイズ付加
イメージ 10
元の色とはかけ離れた色相の色が付加される



ノイズ付加
イメージ 11
元の色に準拠した感じのノイズ
同じ乱数を使っているから楽そうに見えるけど

イメージ 12
こっちのほうが長くなった





処理時間(2048x1536ピクセルの画像)
2.0秒 カラーノイズ付加(12回足して6引く)
1.0秒 ノイズ付加(12回足して6引く)
1.5秒 カラーノイズ付加(ボックス=ミュラー法)
0.4秒 ノイズ付加(ボックス=ミュラー法)
0.3秒 カラーノイズ付加(一様分布乱数)
0.1秒 ノイズ付加(一様分布乱数)
0.2秒 カラーごま塩
0.1秒 カラーごま塩
0.1秒 ごま塩
手元の体内ストップウォッチで計測
グレースケール画像より2倍位になった






イメージ 13
真ん中がカラーノイズ付加、下がノイズ付加
ノイズが弱いとどちらも同じ感じ

元画像の色の分布
イメージ 19
1色



ノイズの強さ5、カラーノイズ
イメージ 20
ほとんど青なのは変わらず

イメージ 21
色数は3万以上になって、一番多い色でも全体の0.05%
意外にバラけるんだなあ

カラーノイズじゃないほう
イメージ 22
色相は変化なし

イメージ 23
色数も51色と少ない
一番多い色で8%


強さ30
イメージ 14
カラーとそうじゃないのでは少し差が出てきた

カラーノイズのほう
イメージ 24
色相の分布が広がってきた
色数は100万近い

からーじゃないほう
イメージ 25
こちらの色相分布も分かれてきたけどほぼ青




ノイズの強さ120
イメージ 15
ここまで強くするとカラーの方は違う色が目立つ

イメージ 26
色相は青を中心に全体に広がっている
色数が100万近いのはさっきの、ノイズの強さ30と変わらずだけど
使用色が偏って青と水色の二強になった
色の少ない順に並べ替えてみると
イメージ 27
かなりいろいろな色が使われているのがわかった

カラーじゃないほう
イメージ 28
色相は広がったけど青が中心なのは変わらず
使用色は黒と白の二強になっていた




強さ255
イメージ 16
上のカラーノイズは元の色がわからないくらいになった

カラーノイズ
イメージ 29
分散するのかと思ったら逆に本の青が多くなってる
色数も半分くらいに減った


カラーじゃないほう
イメージ 30
色数は全く変わらず
黒と白が増えた




強さ255で2回処理
イメージ 17
まだ上があった


イメージ 31
色数は少し減って色相はRGBとその中間と白黒の8つに偏ってきた


カラーじゃない方
イメージ 32
色数は450万から80万に減少、白と黒がさらに増える
色相分布はあまり変化なし


強さ255で3回処理
イメージ 18
ザーッて音が聞こえてきそう
これ以上は処理を重ねても同じような画像になった
ごま塩ノイズだと強さ255の1回処理でこうなる

イメージ 34
何故か色数が増えたけど
このまま繰り返すと8色だけになりそう

イメージ 33
こっちも何故か色数は増えたけど最後は白と黒だけになりそう

正規乱数と一様乱数
イメージ 35
だいたい同じくらいのノイズになるようにしたのを比較
やっぱりそんなに違いがないかなあ、これなら簡単な一様乱数System.Randomでいいような





ギットハブ
アプリダウンロード
イメージ 36
画像ファイルドロップで画像表示
クリップボードの画像も表示できる
表示画像をクリップボードにわたすことができる
ノイズの強さを決めてノイズ付加ボタンでノイズ付加
表示画像クリックで元の画像と切り替え
確認ボタンは平均輝度と標準偏差を表示
イメージ 37
平均輝度はR*G*B/3で計算している



関連記事
次回、2019/05/29は5日後
メディアンフィルタで画像のノイズ除去試してみた、WPF、C# ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15965377.html


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


2019/4/8は2ヶ月くらい前
画像の色相をバブルチャート風に表示するアプリ ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15926942.html

2019/3/30は2ヶ月くらい前
画像の使用色数とその色のピクセル数を表示するアプリその5 ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15918266.html


画像にノイズ付加してみた
ピクセルの輝度値に乱数を加えればノイズになる
一様分布になる乱数と正規分布になる乱数、正規乱数

一様分布
イメージ 1
エクセルのRAND関数の結果1000個のヒストグラム
どの範囲もだいたい同じ個数になっている、こういうのを一様分布っていうみたい
エクセルのRAND関数や.NET FrameworkのSystem.Randomクラスから得られる乱数はこれ
それに対して正規乱数は

イメージ 2
正規分布になる乱数を正規乱数とかいうみたい
この正規乱数(に近いもの)を生成する方法がいくつかあって今回は
  • 乱数を12回足して6引く
  • ボックス=ミュラー法
この2つを使ってみた
イメージ 3



元画像はいつもの
イメージ 4
これにノイズ付加

イメージ 5

上の2つは正規乱数生成法が違うだけど標準偏差20と同じなので結果もほぼ同じ、結構ジャリジャリになるんだなあ
左下は普通の乱数-20〜20で生成したのを元の輝度値に足して作成、これも思っていたよりジャリジャリになった、それでも正規乱数よりもノイズが少ない、これは標準偏差20の正規乱数だと乱数の幅が-20〜20よりも大きくなるからだろうねえ
7.8%のごま塩、これもごま塩多いなあって印象







ごま塩ノイズ
元の輝度値に関係なく0(黒)か255(白)にするだけなので、普通の乱数を使っている
10%のごま塩なら白と黒を5%づつ作るだけ
イメージ 6
209行目、乱数がしきい値以下ならごま塩にする
211行目、乱数が0.5未満なら0(黒)、以上なら255(白)


普通の乱数
イメージ 7
178行目、元の輝度値(pixels[p])に指定幅(noise)の乱数を足す






正規乱数生成
普通の乱数を12回足して6引く方法

この方法でできるのは正規乱数ではなくて、正規乱数に近い乱数らしい
どれくらい近いのかエクセルのRAND関数で試してみた
イメージ 9
グラフの形見ると完璧じゃん!ってくらいのができている

イメージ 10
1000個のセルに
=RAND()+RAND()+RAND()+RAND()+RAND()+RAND()+RAND()+RAND()+RAND()+RAND()+RAND()+RAND()-6
って入れて

イメージ 11
0.1範囲ごとにカウントして

イメージ 12
グラフに、きれいな正規分布になっている

再計算
イメージ 13
多少ずれたりもするけどこの程度



イメージ 15
標準正規分布は平均が0、標準偏差が1
得られた乱数での結果は平均が0.025、標準偏差は1.022
ほとんど差がなく、これもいい結果

イメージ 14
標準偏差はSTDEVP関数で



標準正規分布では-1から1までの範囲に要素の68.27%が入る
-2〜2の範囲には95.45%
-3〜3の範囲には99.73%ってことなので、これとも比較
イメージ 16
1.57%の誤差は大きい気もするけど要素数が少ないからかもねえ
でも十分だと思う





ボックス=ミュラー法
イメージ 18
イメージ 19
これでできるらしい

XとYにそれぞれ別の普通の乱数を入れて計算、つまり1つ作るときでも2つの乱数が必要
logは対数、自然対数とからしい、よくわからんけどエクセルにはそれに相当するLOG関数がある、.NET Frameworkにもあるので安心
2つ式があるけど、両方使う必要はない?よくわからんけどたくさん生成するなら2つの乱数から1つを作るより、ついでに2つ作ったほうがいいのかも

エクセルだと
Z1=SQRT(-2*LOG(RAND(),EXP(1)))*COS(2*PI()*RAND())
Z2=SQRT(-2*LOG(RAND(),EXP(1)))*SIN(2*PI()*RAND())

エクセルで試した結果
イメージ 20
グラフの形を見るとイマイチな気もするけど正規分布になっていると思う

イメージ 21

LOG関数
イメージ 22
=LOG(256,2)、この結果は8になる
2の8乗=256
今回の式ではLOG関数の第2引数には自然対数の底を指定する
エクセルだとEXP関数に1を渡したのが自然対数の底になるので
LOG(数値、EXP(1))













c#でSystem.Randomの乱数から
普通の乱数を12回足して6引く方法で、指定した個数乱数を作成
イメージ 8

ボックス=ミュラー法で正規乱数作成
イメージ 23
Math.Log関数
イメージ 24
C#にもLog関数あった


1000個作成
イメージ 17
一時停止して値をエクセルにコピペ

結果
イメージ 25
どれもいい結果になった

エクセルではイマイチな気もしたボックス=ミュラー法、C#では良くなっているように見える
それでも12回足して6引くほうがきれいかなあ
ボックス=ミュラー法はコサイン使うのとサイン使うのがあるけど、どちらも同じ感じだった



エクセルには正規分布関数NORMDIST関数の逆関数NORMINV関数がある
イメージ 26
これもよくわからんけど逆関数っていう名前的に、結果の値からもとの値を求める感じかしら
第1引数の確率に乱数を入れて、平均と標準偏差には標準正規分布の値を入れると

イメージ 27
多分これが一番正規乱数だと思います
それでもグラフを見ると他の生成法と同じくらいずれている
ってことは1000個くらいだとこれくらいずれるってことかな
逆に言うと12回足して6引く方法も、ボックス=ミュラー法も実質正規乱数
エクセルは何でも揃っているなあ




得られた正規乱数で画像の輝度を変化させる

元の輝度が200
得られた乱数が0.9
標準偏差を40に指定
この場合は236になる

40*0.9+200=236
これは

標準偏差*乱数+元の輝度


切り捨てと切り上げ
標準偏差を100に指定して、それ以外が同じだと
100*0.9+200=290
輝度の最大値は255なので290は255に切り捨てる
同じように結果が0未満だったときは0に切り上げる


12回足して6引く方法の乱数で輝度変化(ノイズ付加)
イメージ 28
107〜115行目、乱数生成
125行目、標準偏差*乱数+元の輝度



ボックス=ミュラー法でノイズ付加
イメージ 29
乱数生成の140〜146行目以外はさっきと同じ



表示画像の平均輝度、分散、標準偏差の表示
イメージ 30
輝度値の配列pixelsから計算して表示

イメージ 31
この画像の輝度平均は119
標準偏差は70.5だった

標準偏差100、12回足して6引く
イメージ 32
平均輝度121
標準偏差=90.6


標準偏差100でボックス=ミュラー法
イメージ 33
平均輝度120
標準偏差=90.5
やっぱりさっきのとほとんど同じ


元の輝度値に-100〜100の普通の乱数を加算
イメージ 34
平均輝度118
標準偏差=80.4
少しおとなしくなる
正規乱数なら-3〜3の範囲に99.73%

つまりほとんど全部
標準正規分布なら範囲ごとに含まれる要素の割合が
-1〜1 -2〜2 -3〜3
68.27% 95.45% 99.73%
ということなので
普通乱数で-100〜100ってのは
標準偏差100の正規乱数の-1〜1の範囲、これは68.27%しか当てはまらないことになる
じゃあ普通乱数で-200〜200にすれば、正規乱数だと-2〜2に相当するから95.45%と範囲だけならほぼ同等になる

普通乱数で-200〜200
イメージ 35
普通乱数は一様分布だから両極端な値も同確率で出るから
よりノイズが大きくなる


イメージ 36


イメージ 37

一様分布乱数の範囲に使う数値の半分を、正規乱数の標準偏差の半分にすると、似たようなノイズになるかなあ
これなら正規乱数を使わなくても、普通の一様分布乱数でいいような気がする



処理時間
体感だと
12回足して6引く >> ボックス=ミュラー法 >> 一様分布乱数 > ごま塩ノイズ

2048x1536ピクセルの画像だと
1秒、0.5秒、0.2秒、0.1秒
こんな体感
ボックス=ミュラー法が一番重い処理かなと予想したけど、12回足して6引くのほうが2倍くらい重かった、一様分布でも乱数は乱数だから結構重たいのかしらねえ



イメージ 39
輝度値192の画像
これに少しのノイズを付加

イメージ 38



今回のフォントは初心に返って
イメージ 40
HGP創英角ポップ体






Compression Technology of Image Data
http://www.comp.tmu.ac.jp/morbier/imagproc/imageconvert.html
ここを見て画像にノイズ付加を試してみようと思ったんだけど
12回足して6引く方法を使っていて、これが何なのかさっぱりわかんなかった
ここに書いてある正規分布乱数でググればよかったんだけど、それが鍵になっているってのに気づかなくて、"画像にノイズ付加"とかでググっているうちに
ガウシアンノイズ(正規分布ノイズ)、ホワイトノイズ、ホワイトガウスノイズ、正規乱数ってのが出てきて
正規分布になる乱数が正規乱数っていうみたい、これが重要
普通の乱数(System.Random)は一様分布
普通の乱数を12回足して6引くと正規乱数に近いものが得られるってのを見つけて、理屈は理解できなかったけどこれだったのかと
ノイズ付加するだけでも難しいねえ、ごま塩ノイズは簡単だけどね

RANDOM……お決まりの乱数……一様分布乱数12個から生成する疑似正規乱数……中心極限定理……理解しようなんていうおとぎ話は始まってすらいない……ごま塩ノイズ程度に覚えておいてくれ……


参照したところ
レーシングポエムったー
https://shindanmaker.com/137465
こんな診断メーカーがあるのかよ……冗談じゃねえ……



Box-Muller法による正規分布列生成 - Qiita
https://qiita.com/mude/items/8c8b24b1404facfd3c03


自然対数・常用対数・二進対数の使い分け。log,ln,lg,expはどういう意味? | アタリマエ!
https://atarimae.biz/archives/12731



一様乱数から正規乱数を作る方法? | 配電盤
http://blog.unfindable.net/archives/7097


平均値から正規分布乱数を生成する方法(PHP) | colori
https://colo-ri.jp/develop/2017/10/gausian-rand-from-average.html



C#でボックスミューラー法による正規分布に従う乱数生成 - 真実の楽譜(フルスコア)
http://truthfullscore.hatenablog.com/entry/2014/06/03/204446


正規乱数・正規分布する乱数を発生させる−NORMINV関数・RAND関数:Excel(エクセル)の関数・数式の使い方-数学
https://www.relief.jp/docs/003097.html


【C#】正規分布に従う乱数の取得
https://imagingsolution.net/program/csharp/normal-random/





ギットハブ

アプリダウンロード先
イメージ 41
画像ファイルドロップで画像表示
ノイズの強さをスライダーで決めて、ノイズ付加のボタンでノイズ付加
ノイズの重ねがけできる
画像のクリックで元の画像と切り替えて比較できる


関連記事
次回、2019/05/24は2日後
画像にノイズ付加するアプリ、カラー版 ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15962326.html


2019/4/27は3週間前
エクセルで1次のガウス関数(確率密度関数)、正規分布関数のNORMDIST ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15942730.html

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




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

ラプラシアンフィルタでエッジ抽出した結果
イメージ 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差なんだから絶対値で計算すればいいのかと思ったら、そうでもないようで場合によりけりみたい



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


イメージ 8
輝度にマイナスはないから-127は0(黒)に置き換えられるので

イメージ 9
エッジとして出てくる線は1本

絶対値でみると
イメージ 10
線が2本になる

正確さでいったら絶対値で取ったほうかなあ
白と灰色とのエッジと、灰色と黒のエッジの2つが出てきたほうが自然だと思う
でも実際の画像で見ると絶対値の方はブレたように見えるから不自然なんだよねえ



イメージ 11
白背景に黒の1ピクセル幅のライン

イメージ 12
これも絶対値じゃない方は1ピクセル幅で太さが変わっていないから自然に見える
絶対値だと2ピクセル幅になって不自然な印象



イメージ 13
黒背景に白の1ピクセル幅のライン

イメージ 14
今度は逆に絶対値じゃないほうが2重線になって不自然



5x5のカーネル
イメージ 15
ググって見つかったカーネルを試してみた
どれもエッジって感じがしないかなあ、これなら3x3のカーネルのほうがいいと思った、使いみちによるのかも

画像のぼかし処理にはザラザラしたノイズを抑える効果もあるけど、輪郭(エッジ)もぼやけてしまう
今回のエッジ抽出を使ってエッジ以外をぼかし処理すれば、エッジを残したままノイズだけを抑えた画像ができるかも


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



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





参照したところ

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];
}
}



ギットハブ
アプリダウンロード先
イメージ 16
画像ファイルドロップで表示、ボタンで変換、表示画像クリックで元の画像と切り替え



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


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


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


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



全21ページ

[1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11]

[ 次のページ ]

本文はここまでですこのページの先頭へ
みんなの更新記事