こんにちは、タナカです。
この記事では、ソーベルフィルタをnumpyのみで実装する方法を説明します。
基本的には画像処理と言えばopencvだと思います。opencvにもソーベルフィルタを実行するメソッドが存在するため、それだけも十分ことが足ります。
しかし、画像処理の基礎でもある畳み込み演算が実際にどう処理されているかを理解したいと考え、記事を書いています。
ちなみにプログラミングにおいて答えはないので、今回紹介する方法はあくまで一例です。
1. 動作環境
私の場合の動作環境はこのようになります。
matplotlib 3.4.3
numpy 1.21.2
opencv-python 4.5.3.56
2. ソーベルフィルタとは
ソーベルフィルタとは画像中の明るさが急に変化するエッジ部分を取り出すためのフィルタです。画像中のある物体の特徴や形、位置などを検出するための前処理として利用されます。
【opencv】画像処理とはの記事で画像は画素値と呼ばれる値を保持していると説明しました。
ソーベルフィルタはその画素値と四則演算することである値に変換するということをやっています。
四則演算?変換?と思うかもしれませんが、どういうことかを説明していきます。
まず、画像はこのような小さな四角の集まりで構成されています。この四角はピクセルと呼ばれ、0-255までの値を保持しています。
これらの値に対して、ソーベルフィルタで画像を畳み込むことでエッジ部を強調することができます。
ソーベルフィルタはこのような3×3の行列で、x方向とy方向で係数が異なります。この係数はすでに決まっている値になります。x方向は横のエッジを強調し、y方向は縦のエッジを強調できるように係数が決められています。
では、畳み込むとはどういうことか説明します。
畳み込みとは、左上の3×3の画素値(ここでいう赤四角で囲んだ範囲)とソーベルフィルタの対応するそれぞれの係数をかけて、最終的に9つの数字を足し合わせる処理のことをいいます。この処理が右下まで繰り返し行われます。
この畳み込み演算を行うことで、エッジを強調することができます。
3. ソーベルフィルタを実装する
pythonでopencvを使わずにnumpyのみでソーベルフィルタを実装したいと思います。
pythonで記述したコードがこちらになります。
import cv2
import matplotlib.pyplot as plt
import numpy as np
def zero_pad(img, pad):
img_pad = np.pad(img, ((pad, pad), (pad, pad)), mode = 'constant', constant_values = (0, 0))
return img_pad
def conv_filter(img, stride, padding, kernel):
h, w = img.shape
f, f = kernel.shape
n_h = int(int(h + 2 * padding - f) / stride + 1)
n_w = int(int(w + 2 * padding - f) / stride + 1)
z = np.zeros([n_h, n_w])
img_pad = zero_pad(img, padding)
for h in range(n_h):
vertical_start = stride * h
verttical_end = vertical_start + f
for w in range(n_w):
horizontal_start = stride * w
horizontal_end = horizontal_start + f
target = img_pad[vertical_start:verttical_end, horizontal_start:horizontal_end]
conv = np.multiply(target, kernel)
conv_sum = np.sum(conv)
z[h, w] = conv_sum
return z
img = cv2.imread('apple-1834639_640.jpg', 0)
# sobel filter
kernel_y = np.array([[-1, -2, -1],
[0, 0, 0],
[1, 2, 1]])
kernel_x = np.array([[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]])
sobel_img = conv_filter(img, stride = 1, padding = 0, kernel = kernel_x)
plt.imshow(sobel_img)
plt.show()
strideとはソーベルフィルタで畳み込む際のずらし量のことです。paddingは元々の画像の周りをある値で埋めるためのものです。今は何も埋めていない状態ですが、paddingについては後ほど説明します。kernelはソーベルフィルタになります。
ソーベルフィルタで畳み込みを実施した画像がこちらになります。
実を言うと、この画像は元々のリンゴの画像よりも2pix小さくなっています。元々の画像サイズは640×640ですが、ソーベルフィルタで畳み込みを行った後の画像サイズは638×638になっています。
なぜそうなるかについて説明します。
例えば、5×5の画像に3×3のソーベルフィルタを適用した場合を考えます。その場合の出力結果はこのようになります。一番上の段を見れば、畳み込めるのは3回ですよね。
つまり3つの値しか出力されません。
元の画像サイズを維持するために、paddingの出番です。先ほどはpaddingを0、つまり何も埋めない状態で畳み込みを行いました。元々の画像サイズを維持した状態で畳み込みを実施するためには、あらかじめある値で上下左右を1マス分埋めてあげる必要があります。
今回は、0で埋めることにします。
# paddingを1にする(上下左右に1マス分0で埋める)
sobel_img = conv_filter(img, stride = 1, padding = 1, kernel_x)
4. opencvのソーベルフィルタと比較する
opencvでもソーベルフィルタを実装し違いを比較したいと思います。
opencvでのソーベルフィルタの実装はこのようになります。
sobel_img = cv2.Sobel(img, cv2.CV_32F, 1, 0, ksize = 3)
先ほどの結果とほとんど変わらないですね。ただ、opencvの場合は、paddingの方法が0埋めではない気がします。ここは正直どう処理しているかよくわかりませんでした。
最後に処理速度を比較して終わりにしたいと思います。
start = time.time()
sobel_img = conv_filter(img, 1, 0, kernel_x)
end = time.time() - start
print(str.format('自作: {}s', end))
start = time.time()
sobel_img = cv2.Sobel(img, cv2.CV_32F, 1, 0, ksize = 3)
end = time.time() - start
print(str.format('opencv: {}s', end))
自作: 4.049669504165649s
opencv: 0.0020008087158203125s
今回自作したコードとopencvでのソーベルフィルタでは、比較にならないほどopencvの方が高速です。レベチですね(笑)。
やはりfor文で回すとかなり時間がかかります。普段ソーベルフィルタを使うなら、opencvを使うでいいでしょう。
ただ、今回の趣旨が畳み込み演算の概念や処理内容を理解することだったので、その点に関しては、理解いただけたのではないでしょうか。
ちなみにこの畳み込み処理は、機械学習でも使われている処理になります。今後、畳み込みニューラルネットワークについても何かしらの記事が書ければと思います。
まとめ
今回は、ソーベルフィルタを使って、画像処理の基礎である畳み込む演算ついて説明しました。
ソーベルフィルタは画像の画素値の変化が大きい箇所を強調することができるフィルタです。
このフィルタを画像の左上から、右下まで繰り返し畳み込み処理を実施し、エッジ部が強調された画像が出力されます。
ソーベルフィルタ以外にもノイズを除去するフィルタなどがありますが、基本的にはこの畳み込み演算が行われています。
機械学習でも使われている処理なので、ぜひ興味がある方は覚えておきましょう。