これまで画像のノイズ除去ができるフィルタとして、
を過去の記事で紹介しました。
さらに、高機能なフィルタとして、ガウシアンフィルタを改善したバイラテラルフィルタというフィルタが存在します。
こちらは、OpenCVにも関数が整備されていて、容易に実験ができますので、本日はこのバイラテラルフィルタについて紹介し、プログラムを実装して効果を見ていきたいと思います。
バイラテラルフィルタとは
バイラテラルフィルタとは、エッジを保存しつつ、平均化を行うように設計されたフィルタです。
過去の記事で紹介したガウシアンフィルタでは、計算する際に画像のエッジがぼやけてしまうという欠点がありました。
この問題点を軽減しようと開発されたのがバイラテラルフィルタです。
バイラテラルフィルタの式は以下で表され、一見すると物凄く複雑な式に見えるため、理解を諦めてしまうことに繋がりがちですが、バイラテラルフィルタの考え方は、ガウシアンフィルタに一工夫を加えたものでしかなく、コンセプトは難解なものではありません。

バイラテラルフィルタでは、対象画素と輝度値の差が大きい画素は小さい重み付けになるようにガウシアンフィルタの重みを変更します。
以下の図で3×3のフィルタを用いて説明します。
まず、下の図は通常のガウシアンフィルタを表しています。ガウシアンフィルタでは、中央に近い画素ほど大きな重みづけが成されるように処理がされますが、左のような入力画像が入力された場合、周囲の255、254、254、253などの高い値に引っ張られて、フィルタ出力後の値もそれなりに大きい値になります。結果、エッジが不鮮明になってしまいます。

次に、以下の図はバイラテラルフィルタを表しています。バイラテラルフィルタでは、中央の画素値「6」に対する差に応じて、ガウシアンフィルタにさらに重みづけが成されます。255、254、254、253あたりの画素は「6」との差が非常に大きいので、このあたりのフィルタ値はほぼ0に近くなると考えてください。結果、出力画像の画素値は1,2,3,6,7などの「6」に近い画素値を重視して生成されます。結果、エッジが保存されることになります。

バイラテラルフィルタ適用によるノイズ除去プログラム(Python+OpenCV)
バイラテラルフィルタはOpenCVの関数で簡単に実現することができます。以下にPythonとOpenCVで書いたプログラムを紹介します。
動作環境:OpenCV 4.5.5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import numpy as np
import cv2
import sys
def main():
print("OpenCV Version: " + str(cv2.__version__) + "\n")
# Loading image data (COLOR)
filename = "data/lenna.png"
gt_image = cv2.imread(filename, cv2.IMREAD_COLOR)
if gt_image is None:
print("Cannot find image data : " + filename)
sys.exit()
# Generation of noisy image
noisy_image = AddGaussianNoise(gt_image, 0, 30)
# Denoising with Gaussian filter
# Bilateral Filter (d = 3)
denoised_image1 = cv2.bilateralFilter(noisy_image, d=3, sigmaColor=100, sigmaSpace=10)
# Bilateral Filter (d = 5)
denoised_image2 = cv2.bilateralFilter(noisy_image, d=5, sigmaColor=100, sigmaSpace=10)
# Bilateral Filter (d = 9)
denoised_image3 = cv2.bilateralFilter(noisy_image, d=9, sigmaColor=100, sigmaSpace=10)
# Evaluation with PSNR and SSIM
PSNREval(gt_image, noisy_image, 255)
SSIMEval(gt_image, noisy_image)
PSNREval(gt_image, denoised_image1, 255)
SSIMEval(gt_image, denoised_image1)
PSNREval(gt_image, denoised_image2, 255)
SSIMEval(gt_image, denoised_image2)
PSNREval(gt_image, denoised_image3, 255)
SSIMEval(gt_image, denoised_image3)
cv2.imwrite('noisy_image.png',noisy_image)
cv2.imwrite('denoised_image1.png',denoised_image1)
cv2.imwrite('denoised_image2.png',denoised_image2)
cv2.imwrite('denoised_image3.png',denoised_image3)
def AddGaussianNoise(image, mean, sigma):
noise = np.random.normal(mean, sigma, np.shape(image))
noisy_image = image + noise
noisy_image[noisy_image > 255] = 255
noisy_image[noisy_image < 0] = 0
noisy_image = noisy_image.astype(np.uint8) # Float -> Uint
return noisy_image
def PSNREval(image1, image2, R):
PSNR_opencv, _ = cv2.quality.QualityPSNR_compute(image1, image2)
print("PSNR Evaluation Results")
print(" PSNR (Blue): " + str(round(PSNR_opencv[0],3)))
print(" PSNR (Green): " + str(round(PSNR_opencv[1],3)))
print(" PSNR (Red): " + str(round(PSNR_opencv[2],3)))
print(" PSNR (RGB Average): " + str(round(((PSNR_opencv[0] + PSNR_opencv[1] + PSNR_opencv[2]) / 3),3)) + "\n")
def SSIMEval(image1, image2):
SSIM_opencv, _ = cv2.quality.QualitySSIM_compute(image1, image2)
print("SSIM Evaluation Results")
print(" SSIM (Blue): " + str(round(SSIM_opencv[0],3)))
print(" SSIM (Green): " + str(round(SSIM_opencv[1],3)))
print(" SSIM (Red): " + str(round(SSIM_opencv[2],3)))
print(" SSIM (RGB Average): " + str(round(((SSIM_opencv[0] + SSIM_opencv[1] + SSIM_opencv[2]) / 3),3)) + "\n")
if __name__ == "__main__":
main()
プログラムの解説
バイラテラルフィルタはOpenCVの以下の関数で実現することができます。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Bilateral Filter (d = 3)
denoised_image1 = cv2.bilateralFilter(noisy_image, d=3, sigmaColor=100, sigmaSpace=10)
与える引数は入力画像に加えて三つで、dが下記の式のwに該当します。ただしdはDiameterらしいので、d=3のときが3×3のバイラテラルフィルタ(式的にはw=1)に該当しそうです。sigmaColorとsigmaSpaceはガウス分布の標準偏差を表し、以下の式のσcとσsに該当します。

最適な値がいくつかは画像の特徴にもよりそうなので、今回は実験的にsigmaColor=100, sigmaSpace=10を設定し、dについては3,5,9の3パターンを確認しています。
プログラムの動作結果
入力データとしては、以下の画像を用いました。

プログラム内のgt_imageがGroundTruthの画像(正解画像)であり、noisy_imageがノイズが付与された画像となります。
画像には全チャネルにガウシアンノイズを付与しています。ガウシアンノイズに関してはこちらの記事を参照してください。
今回は、このnoisy_imageに対してガウシアンフィルタを適用することで、どれだけ正解に近づけることができるのか(ノイズ除去の効果)を確認します。
ノイズレベルを表すσの値をプログラムの18行目「noisy_image = AddGaussianNoise(gt_image, 0, 30)」で変更することができます。
現在はσ=30に設定されていますが、この値を大きくすると、より大きいノイズが付与されます。
プログラムを実行すると、以下のような結果がコンソールに得られます。
PSNR Evaluation Results PSNR (Blue): 18.648 PSNR (Green): 18.888 PSNR (Red): 19.033 PSNR (RGB Average): 18.856 SSIM Evaluation Results SSIM (Blue): 0.257 SSIM (Green): 0.35 SSIM (Red): 0.309 SSIM (RGB Average): 0.305 PSNR Evaluation Results PSNR (Blue): 23.732 PSNR (Green): 23.728 PSNR (Red): 23.953 PSNR (RGB Average): 23.804 SSIM Evaluation Results SSIM (Blue): 0.458 SSIM (Green): 0.541 SSIM (Red): 0.515 SSIM (RGB Average): 0.505 PSNR Evaluation Results PSNR (Blue): 26.361 PSNR (Green): 25.857 PSNR (Red): 26.282 PSNR (RGB Average): 26.167 SSIM Evaluation Results SSIM (Blue): 0.597 SSIM (Green): 0.65 SSIM (Red): 0.644 SSIM (RGB Average): 0.63 PSNR Evaluation Results PSNR (Blue): 27.818 PSNR (Green): 26.572 PSNR (Red): 27.345 PSNR (RGB Average): 27.245 SSIM Evaluation Results SSIM (Blue): 0.694 SSIM (Green): 0.72 SSIM (Red): 0.732 SSIM (RGB Average): 0.715
上から順に
- ノイズ画像のPSNR
- ノイズ画像のSSIM
- d=3のバイラテラルフィルタ適用時のPSNR
- d=3のバイラテラルフィルタ適用時のSSIM
- d=5のバイラテラルフィルタ適用時のPSNR
- d=5のバイラテラルフィルタ適用時のSSIM
- d=9のバイラテラルフィルタ適用時のPSNR
- d=9のバイラテラルフィルタ適用時のSSIM
となります。
実験結果
各ノイズレベルでの結果を表にまとめたものが以下となります。PSNRとSSIM共に値が大きいほど品質が良い(正解画像に類似している)ことを示します。

フィルタにより評価値がかなり改善していることがわかります。パラメータが複数あるので、最適なパラメータについては検討の余地がありそうです。
以下にσ=30の場合の画像も示します。d=9のパターンが主観的にもかなりノイズが除去できているように見えます。

まとめ
本日はバイラテラルフィルタを用いたノイズ除去を紹介しました。このようなフィルタを用いることで、ノイズの多い画像を綺麗にすることが可能になります。