読者です 読者をやめる 読者になる 読者になる

OpenCV-Python Tutorials (4) ~Image Processing in OpenCV~

OpenCV OpenCV-Python Tutorials Tutorial プログラミング

OpenCV-Python Tutorialsの記事,Image Processing in OpenCV の章の Smoothing Images から見ていきます.

公式:Image Processing in OpenCV — OpenCV-Python Tutorials 1 documentation

この中で試したコードはGitHubに置いておくことにした.

github.com

画像の平滑化

目標

次のことを学ぶ

  • 何種類かのローパスフィルタで画像をぼかす
  • カスタムメイドのフィルタを画像に適用する(二次元畳み込み)

二次元畳み込み(画像のフィルタ処理)

1次元の信号と同様に,ローパスフィルタ(low-pass filters:LPF)やハイパスフィルタ(high-pass filters:HPF)などで画像にフィルタ処理することができる.LPFはノイズを取り除いたり,画像をぼかしたりするのを助けるフィルタ処理をする.HPFは画像のエッジを見つけるのを助けるフィルタ処理をする.

OpenCVは,画像のカーネルを畳み込むためのcv2.filter2D()という関数を提供している.例として,平均化フィルタを試してみよう.5x5の平均化フィルタAは次のように定義される:

{K = \frac{1}{25}
\begin{bmatrix}
1 & 1 & 1 & 1 & 1\\\
1 & 1 & 1 & 1 & 1\\\
1 & 1 & 1 & 1 & 1\\\
1 & 1 & 1 & 1 & 1\\\
1 & 1 & 1 & 1 & 1
\end{bmatrix}
}

上記のカーネルで次のようにフィルタ処理される:5x5の窓の中央のピクセルとなるそれぞれのピクセルに対し,窓の中のすべてのピクセルの合計し,その結果を25で割る.これは窓の中の平均値を計算することと等しい.この操作は出力のフィルタ処理された画像を生成するために画像中のすべてのピクセルについて行う.このコードを試して結果を見てみよう:

import cv2
import numpy as np
from matplotlib import pyplot as plt

img = cv2.imread('logo.png')

kernerl = np.ones((5,5),np.float32)/25
dst = cv2.filter2D(img, -1, kernerl)

plt.subplot(121), plt.imshow(img),plt.title('Original')
plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(dst), plt.title('Averaging')
plt.xticks([]), plt.yticks([])
plt.show()

結果: f:id:asdm:20160203160232p:plain

画像のぼかし(画像の平滑化)

画像のぼかしはローパスフィルタカーネルでの畳み込みによって成される.これはノイズを取り除くのに有効だ.実際はこのフィルタが適応されるとエッジをぼかす中で高い周波数のもの(例:ノイズ,エッジ)を画像から取り除く.(エッジをぼかさないようなぼかしの手法もある).OpenCVは主に4種類のぼかし手法を提供している.

1. Averaging

正規化されたボックスフィルタでの畳み込みによって成される.これは単純にカーネル範囲のピクセルの平均をとり,中心のピクセルの値と置き換える.関数 cv2.blur() または cv2.boxFilter() によって行われる.このカーネルの詳細はドキュメントを参照しよう.カーネルの幅と高さを指定する必要がある.3x3の正規化されたボックスフィルタは次のようになる.

$$ K = \frac{1}{9} \begin{bmatrix} 1 & 1 & 1\\ 1 & 1 & 1\\ 1 & 1 & 1 \end{bmatrix} $$

メモ

もし正規化されたボックスフィルタを使いたくないなら,cv2.boxFilter()を使い関数の引数にnormalize=Falseを渡せばいい.


下記の5x5のカーネルでのサンプルを見てみよう:

import cv2
import numpy as np
from matplotlib import pyplot as plt

img = cv2.imread('logo.png')

blur = cv2.blur(img, (5,5))

plt.subplot(121), plt.imshow(img), plt.title('Original')
plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(blur), plt.title('Blurred')
plt.xticks([]), plt.yticks([])
plt.show()

結果: f:id:asdm:20160203160250p:plain

2. Gaussian Filtering

このアプローチでは,同じ係数からなるボックスフィルタの代わりに,ガウシアンカーネルが用いられる.cv2.GaussianBlur() 関数を使う.カーネルの幅と高さを正の奇数で指定しなければならない.また,X方向とY方向の標準偏差\sigma X\sigma Yも指定する必要がある.もし\sigma Xだけが指定された場合は,\sigma Y\sigma Xと同じ値が取られる.もしどちらも0が与えられた場合は,カーネルサイズから計算される.ガウシアンフィルタは画像からガウシアンノイズを取り除くのにとても効果的だ.

もしやってみたいなら,ガウシアンカーネルcv2.getGaussianKernel() 関数から作ることができる.

上記のコードをガウシアンを使ったぼかしの処理に書き換えられる.

blur = cv2.GaussianBlur(img,(5,5),0)

結果: f:id:asdm:20160203161807p:plain

3. Median Filtering

cv2.medianBlur() 関数はウィンドウ内のすべてのピクセルの中央値を計算し,それを中央に位置するピクセルと置き換える.これはごま塩雑音を取り除くのにとても効果的だ.興味深いことの一つは,ガウシアンやボックスフィルタでは中央の要素としてフィルタ処理された値は元画像の値として存在しないこともありうる.しかし,メディアンフィルタでは中央の要素は常に画像中のピクセルによって置き換えられるのでそのようなことはない.カーネルサイズは正の奇数である必要がある.

結果を見てみよう.

median = cv2.medianBlur(img, 5)

結果: f:id:asdm:20160203165613p:plain

4. Bilateral Filtering

お気付きだと思うが,これまで見てきたフィルターだと簡単にぼやけたエッジになるがちだ.バイラテラルフィルタ,cv2.bilateralFilter,はそうならないように定義されていて,エッジを保ちながら効果的にノイズを取り除く.しかし,他のフィルタと比べてこの処理は遅い.既に見てきたようにガウシアンフィルタは近くのピクセルに対してガウシアン重み平均を求める.ガウシアンフィルタは近くのピクセルの空間にだけが考慮された関数である.これは,そのピクセルがおおよそ同じ強度の値を持っているかやそのピクセルがライン上にあるかどうかは考慮されていない.結果として,ガウシアンフィルタはエッジをぼかしてしまう傾向にあり,これは望ましくない.

(バイラテラルフィルタの説明あったが正しく訳せる自信がないので略)

次のサンプルでバイラテラルフィルタを使ったデモを示す.(詳しくは,OpenCVのドキュメントを見よう)

blur = cv2.bilateralFilter(img,9,75,75)

結果: f:id:asdm:20160205175106p:plain

表面の凸凹はなくなっているが,エッジはそのまま残っているのに気づくだろう.

色が気持ち悪いことになった….同じようなこと前もあったな….

追加資料

  1. A Gentle Introduction to Bilateral Filtering and its Applications

↓ここからは日本語の記事で参考にしたもの 2. バイラテラルフィルタ | イメージングソリューション

  1. バイラテラルフィルタ | IBIP

練習

画像を取得してガウシアン雑音とごま塩雑音を追加し,ノイズの程度を変化させながら,ボックス,ガウシアン,メディアン,バイラテラルのフィルタをそれら両方の画像に適応し,その効果を比べてみよう.

モルフォロジー処理

目標

この章では, - 縮小演算,膨張演算,オープニング演算,クロージング演算など異なるモルフォロジー処理を学ぶ - 次のような関数を見ていく:cv2.erode(),cv2.dilate(),cv2.morphologyEx() など

理論

モルフォロジー処理は,画像の形に基づいたいくつかのシンプルな処理だ.これは通常ニ値化画像に対し行われる.2つの入力を必要としていて,1つ目は元の画像,2つ目はstructuring elementまたはkernelと呼ばれる処理の性質を決めるものだ.基本的な2つの演算は縮小と膨張だ.そして,その変形がOpening, Closing, Gradientなどに関係する.次の画像を使って一つずつ見ていこう:

f:id:asdm:20160205193550p:plain

1. 縮小 (Erosion)

Erosion の基本的なアイディアは土壌浸食のようなもので,前景の境界を侵食する(前景は常に白となるようにされている).これは何をしているのだろうか?カーネルは画像上を動く(二次元畳み込み).カーネル下のすべてのピクセルが1である時,元画像上のあるピクセル(1か0のどちらか)は1だとされる.

つまり何が起きているかというと,カーネルのサイズに応じて境界近くのピクセルはすべて0とされる.従って,全景の厚さや大きさが減少する,または単純に画像上の白色の領域が減少する.これは(色空間の章で見たような)小さな白いノイズを取り除く,つながった対象物を切り離すなどに有効である.

下に例として,5x5のすべて1のカーネルを使った.どのようになるか見てみよう:

import cv2
import numpy as np

img = cv2.imread('j.png', 0)
kernel = np.ones((5,5), np.uint8)
erosion = cv2.erode(img, kernel, iterations = 1)

cv2.imwrite('erosion.png', erosion)

結果:

f:id:asdm:20160208154232p:plain

2. 膨張 (Dilation)

これは erosion の反対だ.ここでは,カーネル下のピクセル値が1つでも'1'であるならそのピクセル値は1となる.よってこれは白い領域を増やす,または前景のサイズを増加させる.通常,ノイズ除去のような場合だと,erosion の後に dilation をする.なぜなら,erosion は白いノイズを除くが対象物を小さくしてしまうため,それを膨張させる.ノイズはなくなってしまったら戻ってこないが,対象物は膨張する.これは,対象物が離れてしまっているのをくっつける場合にも有効である.

dilation = cv2.dilate(img,kernel,iterations = 1

結果:

f:id:asdm:20160208233252p:plain

3. Opening

opening は erosion の次に dilation をしたものだ.これは先に説明したように,ノイズを取り除くのに有効だ.ここでは,関数 cv2.morphologyEx() を用いる.

opening = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)

結果:

f:id:asdm:20160216160640p:plain f:id:asdm:20160216160825p:plain

4. Closing

closing は opening の反対で,dilation の次に erosion をしたものだ.これは,前景に小さな穴が空いている場合や黒い点が載っている場合に有効だ.

closing = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)

結果:

f:id:asdm:20160216161623p:plain f:id:asdm:20160216161630p:plain

5. Morphological Gradient

これは,dilation と erosion との差分だ.

結果は,オブジェクトの輪郭のように見える.

gradient = cv2.morphologyEx(img, cv2.MORPH_GRADIENT, kernel)

結果:

f:id:asdm:20160205193550p:plain f:id:asdm:20160216162248p:plain

6. Top Hat

これは入力画像とその画像のOpeningの差分だ.下に示すのが,9x9のカーネルでの実行の例だ.

tophat = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel)

結果:

f:id:asdm:20160205193550p:plain f:id:asdm:20160216163120p:plain

7. Black Hat

これは入力画像のclosingと入力画像の差分だ.

blackhat = cv2.morphologyEx(img, cv2.MORPH_BLACKHAT, kernel)

結果:

f:id:asdm:20160205193550p:plain f:id:asdm:20160216163355p:plain

構成要素

これまで見てきた例ではNumpyの助けを借りて手動で構成要素を作ってきた.これは長方形だ,しかし,いくつかの場合では,楕円形や円形のカーネルが必要となる.このために,OpenCVは関数 cv2.getStructuringElement() を持っている.これにカーネルの形と大きささえ渡せば,求めるカーネルを得られる.

追加資料

  1. Morphology at HIPR2

  2. http://compsci.world.coocan.jp/OUJ/2012PR/pr_10_a.pdf
    画像処理についての基礎知識が足りてなかったので参考にしました.