本日は平均値フィルタを用いた画像のノイズ除去について紹介したいと思います。
平均値フィルタ
平均値フィルタは、ある特定画素の周囲n×n画素の平均値を計算し、当該画素を平均値で置き換えることで、ノイズの除去等を行うことができる画像処理のフィルタです。
一番小さいサイズのものでは、3×3のフィルタを使うことが多いです。例えば以下の図のように、ある入力画像の各画素位置を中心とした周囲3×3の画素について平均値を計算し、その値で出力画像の画素値を置き換えます。

画像の端の方の画素を処理する場合には、フィルタが画像の外にはみ出すことがあるため、そこは計算しないなどの例外処理が必要となります。
平均値フィルタの効果
一般的にはノイズの除去に有益なフィルタと考えられています。
例えば0~255の画素で示される画像の中で、一画素だけ255のような飛び抜けた値が入っている場合には、このような画素はノイズであったりすることがあります。
このような画素を周囲画素の値の平均値で置き換えることで、画像全体の画素値の変化を滑らかにし、ノイズの除去等をすることが可能になります。
フィルタのサイズは3×3に限定されず、5×5や7×7などアレンジ可能です。 フィルタが大きければ大きいほど、画素値の変化は滑らかになりますが、副作用として画像全体の鮮鋭な部分もぼかされてしまい、ピンボケしたような画像になってしまうこともあるので注意が必要です。
画像への平均値フィルタによるノイズ除去プログラム(Python+OpenCV)
平均値フィルタはOpenCVの関数で簡単に実現することができます。以下にPythonとOpenCVで書いたプログラムを紹介します。
動作環境:OpenCV 4.5.5
This file contains 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__))
# 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 average filter
# Average Filter (3*3)
kernel1 = np.array([[1/9, 1/9, 1/9],
[1/9, 1/9, 1/9],
[1/9, 1/9, 1/9]])
denoised_image1 = cv2.filter2D(noisy_image, -1, kernel1)
# Average Filter (5*5)
kernel2 = np.array([[1/25, 1/25, 1/25, 1/25, 1/25],
[1/25, 1/25, 1/25, 1/25, 1/25],
[1/25, 1/25, 1/25, 1/25, 1/25],
[1/25, 1/25, 1/25, 1/25, 1/25],
[1/25, 1/25, 1/25, 1/25, 1/25]])
denoised_image2 = cv2.filter2D(noisy_image, -1, kernel2)
# Average Filter (7*7)
kernel3 = np.array([[1/49, 1/49, 1/49, 1/49, 1/49, 1/49, 1/49],
[1/49, 1/49, 1/49, 1/49, 1/49, 1/49, 1/49],
[1/49, 1/49, 1/49, 1/49, 1/49, 1/49, 1/49],
[1/49, 1/49, 1/49, 1/49, 1/49, 1/49, 1/49],
[1/49, 1/49, 1/49, 1/49, 1/49, 1/49, 1/49],
[1/49, 1/49, 1/49, 1/49, 1/49, 1/49, 1/49],
[1/49, 1/49, 1/49, 1/49, 1/49, 1/49, 1/49]])
denoised_image3 = cv2.filter2D(noisy_image, -1, kernel3)
# 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 OpenCV (Blue): " + str(round(PSNR_opencv[0],3)))
print(" PSNR OpenCV (Green): " + str(round(PSNR_opencv[1],3)))
print(" PSNR OpenCV (Red): " + str(round(PSNR_opencv[2],3)))
print(" PSNR OpenCV (RGB Average): " + str(round(((PSNR_opencv[0] + PSNR_opencv[1] + PSNR_opencv[2]) / 3),3)))
def SSIMEval(image1, image2):
SSIM_opencv, _ = cv2.quality.QualitySSIM_compute(image1, image2)
print("SSIM Evaluation Results")
print(" SSIM OpenCV (Blue): " + str(round(SSIM_opencv[0],3)))
print(" SSIM OpenCV (Green): " + str(round(SSIM_opencv[1],3)))
print(" SSIM OpenCV (Red): " + str(round(SSIM_opencv[2],3)))
print(" SSIM OpenCV (RGB Average): " + str(round(((SSIM_opencv[0] + SSIM_opencv[1] + SSIM_opencv[2]) / 3),3)))
if __name__ == "__main__":
main()
入力データとしては、以下の画像を用いました。

プログラム中のgt_imageがGroundTruthの画像(正解画像)であり、noisy_imageがノイズが付与された画像となります。ノイズ画像には全チャネルにガウシアンノイズを付与しています。ガウシアンノイズに関してはこちらの記事を参照してください。
今回は、このnoisy_imageに対して平均値フィルタを適用することで、どれだけ正解に近づけることができるのか(ノイズ除去の効果)を確認します。
効果測定の評価指標としてはPSNRとSSIMと採用しました。PSNRとSSIMについてはこちらの記事をご参照ください。
画像のPSNR(ピーク信号対雑音比)の測定(Python+OpenCV)
画像のSSIM(structural similarity)の測定(Python+OpenCV)
プログラム実行結果
ノイズレベルσを段々大きくして、PSNRとSSIMの値を確認してみましょう。
ノイズレベルは19行目のnoisy_image = AddGaussianNoise(gt_image, 0, 30)の「30」の箇所を変えることで変更可能です。
実行すると以下のような出力が得られます。
PSNR Evaluation Results PSNR OpenCV (Blue): 18.588 PSNR OpenCV (Green): 18.912 PSNR OpenCV (Red): 19.064 PSNR OpenCV (RGB Average): 18.855 SSIM Evaluation Results SSIM OpenCV (Blue): 0.256 SSIM OpenCV (Green): 0.349 SSIM OpenCV (Red): 0.31 SSIM OpenCV (RGB Average): 0.305 PSNR Evaluation Results PSNR OpenCV (Blue): 26.2 PSNR OpenCV (Green): 25.162 PSNR OpenCV (Red): 26.158 PSNR OpenCV (RGB Average): 25.84 SSIM Evaluation Results SSIM OpenCV (Blue): 0.601 SSIM OpenCV (Green): 0.649 SSIM OpenCV (Red): 0.651 SSIM OpenCV (RGB Average): 0.634 PSNR Evaluation Results PSNR OpenCV (Blue): 26.293 PSNR OpenCV (Green): 23.923 PSNR OpenCV (Red): 25.259 PSNR OpenCV (RGB Average): 25.159 SSIM Evaluation Results SSIM OpenCV (Blue): 0.684 SSIM OpenCV (Green): 0.678 SSIM OpenCV (Red): 0.702 SSIM OpenCV (RGB Average): 0.688 PSNR Evaluation Results PSNR OpenCV (Blue): 25.439 PSNR OpenCV (Green): 22.616 PSNR OpenCV (Red): 23.917 PSNR OpenCV (RGB Average): 23.991 SSIM Evaluation Results SSIM OpenCV (Blue): 0.677 SSIM OpenCV (Green): 0.644 SSIM OpenCV (Red): 0.674 SSIM OpenCV (RGB Average): 0.665
上から順に
- ノイズ画像のPSNR
- ノイズ画像のSSIM
- 3×3の平均値フィルタ適用時のPSNR
- 3×3の平均値フィルタ適用値のSSIM
- 5×5の平均値フィルタ適用値のPSNR
- 5×5の平均値フィルタ適用値のSSIM
- 7×7の平均値フィルタ適用値のPSNR
- 7×7の平均値フィルタ適用値のSSIM
となります。
各ノイズレベルでの結果を表にまとめたものが以下となります。PSNRとSSIM共に値が大きいほど品質が良い(正解画像に類似している)ことを示します。

興味深いことに、ノイズレベルが小さい(σ=3)の場合は、平均値フィルタを掛けると画質は劣化するという結果が出ました。これは、ノイズが少ないのにも関わらず平均値フィルタを掛けることで、ノイズ除去による改善よりも、副作用である画像全体がボケてしまうという部分が強く出てしまったためだと思います。
一方、ノイズレベルが大きめのものでは改善効果が見られ、3×3のフィルタで特に良い効果が得られているケースが多いようです。
そのため、単にフィルタのサイズを大きくすることが品質の改善には繋がらないこともわかります。
以下にσ=30の場合の各画像の比較も示します。7×7などでは少々ボケ過ぎているような傾向がみられるようです。

まとめ
本日は平均値フィルタを用いたノイズ除去を紹介しました。
このようなノイズ除去ができるフィルタは多数提案されていますので、他も今後紹介してみたいと思います。