はじめに
画像処理を簡単に解説していきます。
下部にはJavaでのソースコードも載せているのでぜひご参照ください。
不定期にはなりますが、随時更新していく予定です。
メディアンとは?
日本語では中間値といい、名前の通り数集合の中間に位置する数のことです。
簡単な例ですが、あるテストを受けた人が7人いて、点数分布は以下の通りでした。
$\begin{matrix}
34 & 36 & 39 & 42 & 44 & 48 & 100
\end{matrix}$
この例において平均点を計算すると、49点となります。
直感的には49点が真ん中というのは高いように感じませんか?
2位ですら平均点以下となってしまっています。
これは一人だけ100点という高得点を取っている人がいるため、平均点が高めに出ているのです。
次に、メディアンを計算してみましょう。
7人の中間なので、4位の点数である42点が採用されます。
この点は4位の点数そのままなので、1位の100点には引っ張られていません。
メディアンのほうが「真ん中」という感じがしますよね?
以上、説明した通りメディアンには外れ値(極端に他と異なる値)の影響を受けにくいという長所があります。
これを利用したのが以下のメディアンフィルタです。
メディアンフィルタとは?
ノイズ除去などの平滑化処理で用いられるフィルタです。
名前の通り、自身を含む周囲 $n\times m$ 画素の中から、メディアンを取得し、新しい画素としてセットします。
例えば、以下のカーネルの場合、メディアンは5となり外れ値の255が除外されます。
\begin{bmatrix}
10 & 4 & 8 \\
2 & 255 & 12 \\
1 & 3 & 5
\end{bmatrix}
アルゴリズム
- 画像をラスタスキャンし、各画素に2から5の処理を行う。
- カーネル範囲の値を保存するための配列変数を用意する。
- カーネル範囲を全探索し、各画素の値を2で用意した配列変数に追加する。
- 3で更新した配列変数を昇順にソートする
- 4でソートした配列の中間値を取得し、1で選択している画素にセットする
メディアンフィルタの例
ソースコード
public static SImage median(SImage oriImg, int ksize) {
// グレースケール画像の場合
if (oriImg.channel == 1) {
// メディアン画像用を生成
var medImg = new SImage(oriImg.width(), oriImg.height(), 1);
// 画像をラスタスキャン
for (int y = ksize / 2; y < oriImg.height() - ksize / 2; y++)
for (int x = ksize / 2; x < oriImg.width() - ksize / 2; x++) {
// メディアン取得用の変数初期化
int[] vArray = new int[ksize * ksize];
// カーネル範囲をスキャン
int index = 0;
for (int dy = -ksize / 2; dy <= ksize / 2; dy++)
for (int dx = -ksize / 2; dx <= ksize / 2; dx++)
// 値配列に値を追加
vArray[index++] = oriImg.getGray(x + dx, y + dy);
// 値配列をソート
Arrays.sort(vArray);
// 中間値を出力画像にセット
medImg.setGray(x, y, vArray[4]);
}
return medImg;
// RGBカラー画像の場合
} else if (oriImg.channel == 3) {
// メディアン画像用を生成
var medImg = new SImage(oriImg.width(), oriImg.height(), 3);
// 画像をラスタスキャン
for (int y = ksize / 2; y < oriImg.height() - ksize / 2; y++)
for (int x = ksize / 2; x < oriImg.width() - ksize / 2; x++) {
// メディアン取得用の変数初期化
int[] rArray = new int[ksize * ksize];
int[] gArray = new int[ksize * ksize];
int[] bArray = new int[ksize * ksize];
// カーネル範囲をスキャン
int index = 0;
for (int dy = -ksize / 2; dy <= ksize / 2; dy++)
for (int dx = -ksize / 2; dx <= ksize / 2; dx++) {
// 値配列に値を追加
int[] rgb = oriImg.getRGB(x + dx, y + dy);
rArray[index] = rgb[0];
gArray[index] = rgb[1];
bArray[index++] = rgb[2];
}
// 値配列をソート
Arrays.sort(rArray);
Arrays.sort(gArray);
Arrays.sort(gArray);
// 中間値を出力画像にセット
int[] newRgb = new int[]{rArray[ksize * ksize / 2], gArray[ksize * ksize / 2], bArray[ksize * ksize / 2]};
medImg.setRGB(x, y, newRgb);
}
return medImg;
// アルファチャネルを持つ場合
} else {
// メディアン画像用を生成
var medImg = new SImage(oriImg.width(), oriImg.height(), 4);
// 画像をラスタスキャン
for (int y = ksize / 2; y < oriImg.height() - ksize / 2; y++)
for (int x = ksize / 2; x < oriImg.width() - ksize / 2; x++) {
// メディアン取得用の変数初期化
int[] aArray = new int[ksize * ksize];
int[] rArray = new int[ksize * ksize];
int[] gArray = new int[ksize * ksize];
int[] bArray = new int[ksize * ksize];
// カーネル範囲をスキャン
int index = 0;
for (int dy = -ksize / 2; dy <= ksize / 2; dy++)
for (int dx = -ksize / 2; dx <= ksize / 2; dx++) {
// 値配列に値を追加
int[] argb = oriImg.getARGB(x + dx, y + dy);
aArray[index] = argb[0];
rArray[index] = argb[1];
gArray[index] = argb[2];
bArray[index++] = argb[3];
}
// 値配列をソート
Arrays.sort(aArray);
Arrays.sort(rArray);
Arrays.sort(gArray);
Arrays.sort(gArray);
// 中間値を出力画像にセット
int[] newArgb = new int[]{aArray[ksize * ksize / 2], rArray[ksize * ksize / 2], gArray[ksize * ksize / 2], bArray[ksize * ksize / 2]};
medImg.setARGB(x, y, newArgb);
}
return medImg;
}
}