OpenCVで画像の平滑化をしてみた


はじめに

画像処理では、必ずしもキレイな画像を用意できるとは限りません。
不鮮明だったり、ノイズが入っていたりといったことも多くあります。
ノイズが入っている場合には、平滑化という手法を用いてそれを取り除くことがあります。
あるいはわざと不鮮明な画像を作り、ダミーデータとして使用する、ということもあります。

今回は、Pythonを使ってOpenCVによる画像の平滑化を行います。

平滑化とは

平滑化(へいかつか)、あるいはスムージング処理とは、簡単に言うと画像をぼかすことです。
画像をぼかすということは、画素値の変化を滑らかにすることとも言えます。
ノイズやエッジは、画素値の急激な変化です。
平滑化により、ノイズやエッジを消したり、目立たなくすることができます。

準備

環境はGoogle Colaboratoryを使用します。
Pythonのバージョンは以下です。

import platform
print("python " + platform.python_version())
# python 3.6.9

画像を表示してみよう

では、早速コードを書いていきましょう。
まずは、OpenCVをインポートします。

import cv2

さらに、Colaboratoryで画像を表示するため、以下もインポートします。

from google.colab.patches import cv2_imshow

サンプルの画像も用意しておきましょう。
今回は、Pixabayのフリー画像を使用します。

それでは、用意したサンプル画像を表示してみましょう。

img = cv2.imread(path) # pathは画像を置いている場所を指定
cv2_imshow(img)

また、後で必要になるため、ノイズ入りの画像も用意しておきます。
ここでは、「salt-and-pepper noise(ごま塩ノイズとも呼ばれる)」というノイズを画像に付加してみましょう。

# salt-and-pepper noise
# コードは以下を参照
# https://lp-tech.net/articles/nCvfb?page=2
import numpy as np

row, col, ch = img.shape
img_sp = cv2.imread(path)
# salt
pts_x = np.random.randint(0, col-1 , 1000)
pts_y = np.random.randint(0, row-1 , 1000)
img_sp[(pts_y,pts_x)] = (255, 255, 255)
# pepper
pts_x = np.random.randint(0, col-1 , 1000)
pts_y = np.random.randint(0, row-1 , 1000)
img_sp[(pts_y, pts_x)] = (0, 0, 0)
cv2_imshow(img_sp)

平滑化

一般的な平滑化

平滑化とは、簡単に言うと画像をぼかすことです。
OpenCVで画像を平滑化する最も簡単な方法は、cv2.blurを使用する方法です。
ここで、blurとは「ぼかし」という意味です。

それでは、平滑化した画像を表示してみましょう。
元画像と並べて表示してみます。

img_blur = cv2.blur(img, (3, 3))
imgs = cv2.hconcat([img, img_blur])
cv2_imshow(imgs)

左が元画像、右が平滑化した画像です。
右のほうが少しボケているのがわかると思います。

cv2.blurの引数は2つあります。
1つ目は、インプットとなる画像です。
2つ目は、カーネルと呼ばれるものです。画像の1点を決めたとき、周囲のどれだけの領域を含むかを表します。「箱の大きさ」だと考えてみて下さい。

上の例では(3, 3)となっており、これは画像の1点を中心とする3×3の領域を対象とすることを表します。
一般的な平滑化であるcv2.blurでは、このカーネル内の画素の平均値でカーネル内を塗りつぶします。
カーネルのサイズが大きくなればなるほど、画像のぼけ具合が強くなっていきます。

カーネルサイズを色々と変更してみた画像を表示してみましょう。

img1 = cv2.blur(img, (1, 1))
img2 = cv2.blur(img, (2, 2))
img3 = cv2.blur(img, (3, 3))
img4 = cv2.blur(img, (4, 4))
img5 = cv2.blur(img, (5, 5))
img6 = cv2.blur(img, (6, 6))

imgs_1 = cv2.hconcat([img1, img2, img3])
imgs_2 = cv2.hconcat([img4, img5, img6])
imgs = cv2.vconcat([imgs_1, imgs_2])
cv2_imshow(imgs)

左上から順に、カーネルサイズを大きくしていった画像になります。
画像のぼけ具合がどんどん強くなっているのがわかりますね。

Gaussianフィルタ

一般的な平滑化の他にも、OpenCVではいくつかの平滑化処理が可能です。

次は、Gaussianフィルタというものを紹介します。
一般的な平滑化では、カーネル内の画素を平均値という一定値で塗りつぶしました。
Gaussianフィルタでは、カーネルの中心からの距離に応じて値が変わります。
中心での値が一番大きく、離れるにつれて値は小さくなります。
その様子がガウス関数(Gaussian)と呼ばれる関数に従うので、Gaussianフィルタと呼ばれます。
ガウス関数の式は以下です。

\frac{1}{\sqrt{2\pi\sigma^2}}\exp\Bigl(-\frac{x^2}{2\sigma^2}\Bigr) \\

また、ガウス関数のグラフは以下のようになります。

それでは、ガウシアンフィルタを用いて画像を平滑化してみましょう。
こちらも、元画像と並べて表示してみます。

img_gauss = cv2.GaussianBlur(img, (3, 3), 3)
imgs = cv2.hconcat([img, img_gauss])
cv2_imshow(imgs)

cv2.GaussianBlurの引数は3つです。
最初の2つは、cv2.blur同様、インプット画像とカーネルサイズです。
3つ目の引数は、ガウス関数の$\sigma$(シグマ)に相当します。
$\sigma$が小さいと、ピークは高くなる代わりに広がりは狭くなります。
逆に$\sigma$が大きくなると、広がりは大きくなりますが、ピークは低くなります。

medianフィルタ

次は、medianフィルタを紹介します。
medianとは「中央値」のことで、指定したカーネル内に含まれる画素から中央値を取り出し、その値でカーネル全体を塗りつぶします。
一般的な平滑化と異なるのは、平均化された画素値ではなく、必ず存在する画素値を使用するという点になります。

medianフィルタを使用した画像を元画像と比較してみましょう。

img_med = cv2.medianBlur(img, 3)
imgs = cv2.hconcat([img, img_med])
cv2_imshow(imgs)

cv2.medianBlurの引数は2つで、インプット画像とカーネルサイズです。
上記では、2つ目の引数を3としていますが、これは3×3のカーネルを表しています。

このmedianフィルタは、salt-and-pepper noiseの除去に威力を発揮します。
実際に、ノイズ入りの画像に対してmedianフィルタをかけた結果は以下になります。

きれいに除去できているのがわかりますね。

bilateralフィルタ

最後に、bilateral(バイラテラル)フィルタを紹介します。

bilateralとは、「両方ともある」といった意味ですが、このフィルタはエッジをうまく残すことができます。
今までの平滑化フィルタは、エッジなどのピークも含めてぼかしていました。
bilateralフィルタは、画像をぼかしつつ、エッジも残すことができる便利なフィルタです。
元画像とあわせて出力してみましょう。

img_bi = cv2.bilateralFilter(img, 9, 75, 75)
imgs = cv2.hconcat([img, img_bi])
cv2_imshow(imgs)

全体的に滑らかになっているにも関わらず、エッジ部分はしっかりと残しているのがわかりますね。

cv2.bilateralFilterの引数の詳細は省きます。OpenCVの公式ドキュメントなどを参照下さい。

まとめ

今回は、Pythonを使ってOpenCVにより画像の平滑化を行いました。

画像処理でノイズの除去が必要になった際には、平滑化を試してみて下さい。

平滑化に関するさらに詳しい内容は、以下が参考になります。