Python3 & OpenCV で画像処理を学ぶ[7] 〜 エッジを保存する平滑化フィルタ
1. はじめに
前回
は線形フィルタによるたたみこみで平滑化を行いました。
しかし、平均を用いた平滑化は全体が一様にぼけるため, エッジが失われてしまいます。
そこで今回は, エッジを保存したまま平滑化を行う方法を見ていきます。
2. エッジ保存平滑化
2.1. 下準備
いつものように, 必要なライブラリなどをインポートしていきます。
import cv2 import numpy as np from numpy.lib.stride_tricks import as_strided from matplotlib import pyplot as plt import seaborn as sns %config InlineBackend.figure_format = 'retina'
def imshow2(img1,img2, title1=None, title2=None): fig = plt.figure(figsize=(20,15)) ax1 = fig.add_subplot(121) ax2 = fig.add_subplot(122) ax1.imshow(img1) ax2.imshow(img2) if title1: ax1.set_title(title1) if title2: ax2.set_title(title2) ax1.set_xticks([]), ax1.set_yticks([]) ax2.set_xticks([]), ax2.set_yticks([]) plt.show()
photo = cv2.cvtColor(cv2.imread('img_src/IMGP2769.jpg',1),cv2.COLOR_BGR2RGB) wiz = cv2.cvtColor(cv2.imread('img_src/Wizdomiot.png',1),cv2.COLOR_BGR2RGB)
2.2. メディアンフィルタ
メディアンフィルタは, 目的のピクセルの周囲の画素値の中央値を出力とします。
中央値というのは, 例えば,
nums = np.random.randint(1,100,19) print(nums)
[59 2 94 97 59 8 17 59 61 86 84 20 70 2 90 58 3 5 51]
このような数字の列を考える場合,
print(np.sort(nums))
[ 2 2 3 5 8 17 20 51 58 59 59 59 61 70 84 86 90 94 97]
このように昇順にソートしたときに,
np.median(nums)
59.0
ちょうど中央に来る値のことです。中央が存在しない場合、中央付近の2つの平均を出力とします。
メディアンフィルタは非線形フィルタです。中央値の計算はソートが入ってくるため, 平均値の計算のようにカーネルとの積和の形に書き直すことができず, 前回扱ったような線形フィルタでは実現できないことがわかると思います。
opencvでは cv2.medianBlur(img,size)
で利用できます。使ってみましょう。
med = cv2.medianBlur(wiz,19) imshow2(wiz,med,"ref", "filtered")
やや強くかけてみました。魔法陣が溶けた、あるいはにじんだような効果が得られています。
ところで、メディアンフィルタの本来の用途はノイズの除去であるようです。 そこで、やや恣意的ですが, 以下にノイズをかけた画像を用意しました。
noise = np.random.binomial(1,0.95,photo.shape[:2])[:,:,np.newaxis].repeat(3,axis=2) noised_photo = (photo * noise).astype('uint8') plt.figure(figsize=(10,7.5)) plt.imshow(noised_photo) plt.show()
これにメディアンフィルタをかけてみます。
med = cv2.medianBlur(noised_photo,3) imshow2(noised_photo,med, "ref", "filtered")
かなり綺麗に除去できています。中央値が外れ値に強いことを利用した方法ですね。
2.3. バイラテラルフィルタ
バイラテラルフィルタは, 「画素値の差」と「画素間の距離」に応じた重み付けを行って平滑化をするフィルタで、以下の式で表されます。
$P_{in}(x,y)$を入力, $P_{Out}(x,y)$を出力として,
ただし,
$w$ :カーネルサイズ, : 画素間距離の分散, : 画素値の分散
一見ちょっと地獄みたいな式ですが, 前回扱った二次元正規分布の発展形です。
平均:0, 分散:の二次元正規分布:
これを思い出しながらバイラテラルフィルタの式を分解して考えると,
まず,
は, 画素値間の距離に応じた正規分布の重みと考えることができます。
すなわち, 出力の画素 $P_{out}(x,y)$ と同じ位置 ($i=j=0$) なら $e^0=1$ で重みが最大になり, $(x,y)$から離れるほど小さい値になります。
次に,
は, 画素値の差の大きさに応じた重みです。
$\big(P_{in}(x,y) - P_{in}(x+i, y+j)\big)^2 = 0$, すなわち注目している $P_{in}(x,y)$ の画素値と $P_{in}(x+i, y+j)$ が同じであれば, $e^0=1$ で重みが最大になり, 画素値の差が開くほど小さい重みになります。
ここから, は遠方の画素値の影響度を制御していて, は差の大きい値の影響度を制御していると見ることができます。
opencvでは cv2.bilateralFilter(img, size, sigma1, sigma2)
で利用できます。
これを画像に繰り返し適用してみます。
photo2 = cv2.cvtColor(cv2.imread('img_src/IMGP2794.jpg',1),cv2.COLOR_BGR2RGB)
b1 = cv2.bilateralFilter(photo2,5,40,40) imshow2(photo2,b1, "ref", "bilateral:1") for i in range(11): b1 = cv2.bilateralFilter(b1,5,40,40) b2 = cv2.bilateralFilter(b1,5,40,40) imshow2(b1,b2,"bilateral:"+str((i+1)*2),"bilateral:"+str((i+1)*2+1)) b1 = np.copy(b2)
(中略)
あまり良い例ではなかった気もしますが, エッジを保ちつつぼかしがかかっていることがわかると思います。
2.4. 最大値・最小値フィルタ
先ほど中央値を扱ったので, 他の統計量について見てみます。試しに、最大値と最小値でフィルタリングをしてみたいと思います。面白い効果が得られると嬉しいです。
知る限り、最大値と最小値を返すフィルタはopencvにはない(あるかも)ので, 前回書いた畳み込み演算の関数を改造して, 非線形フィルタ用の関数を作ってみます。
def nonlinear_convolve2d(img, f, size, padding='0'): out_shape = img.shape if len(img.shape) != 2: print('img should be 2d-array') return if size%2 == 0: print('Kernel size shold be odd') return edge = int(size/2) if padding=='edge': img = np.pad(img, [edge,edge], mode='edge') elif padding=='reflect': img = np.pad(img, [edge,edge], mode='reflect') else: img = np.pad(img, [edge,edge], mode='constant', constant_values=0) sub_shape = tuple(np.subtract(img.shape, (size, size)) + 1) conv_shape = sub_shape + (size, size) strides = img.strides + img.strides submatrices = as_strided(img, conv_shape, strides) convolved_mat = np.apply_over_axes(f, submatrices, [2,3]).reshape(out_shape) return convolved_mat
まず, 最大値を使ってみます。
max_filtered = np.zeros(photo.shape) for (i, ch) in enumerate(['R','G','B']): max_filtered[:,:,i] = nonlinear_convolve2d(photo[:,:,i], np.max, 7, padding='edge') max_filtered = max_filtered.astype('uint8') imshow2(photo, max_filtered)
陰影が落ちたかのような独特な効果が得られました。
次に, 最小値でフィルタリングを行ってみます。
min_filtered = np.zeros(photo.shape) for (i, ch) in enumerate(['R','G','B']): min_filtered[:,:,i] = nonlinear_convolve2d(photo[:,:,i], np.min, 7, padding='edge') min_filtered = min_filtered.astype('uint8') imshow2(photo, min_filtered, "ref", "filtered")
どこかドット絵っぽい?雰囲気になりました。
最大値・最小値フィルタは領域ごとに同じような値が返るためか, 角っぽい感じになっています。
そこで, これらの上に更にメディアンフィルタをかけてみます。
med = cv2.medianBlur(max_filtered,11) imshow2(max_filtered,med,"ref", "filtered")
med = cv2.medianBlur(min_filtered,11) imshow2(min_filtered,med,"ref", "filtered")
最小値・最大値フィルタの角が丸められた関係で, 絵画っぽくなった気がします。
3. まとめ
今回は、メディアン・バイラテラルなどの非線形フィルタを用いてエッジを保存する平滑化の概念とやり方を学びました。
次回はエッジ抽出をやります。
4. 参考文献
- 「ディジタル画像処理」の6章