Pynote

Python、機械学習、画像処理について

OpenCV - フィルタリング / 畳み込みについて

フィルタリング

入力画像の各画素に対して、対象の画素及びその近傍の画素の画素値の重み付き総和を計算し、出力画像の画素値を計算する処理をフィルタリング (filtering) または畳み込み (convolution) という。
フィルタリングを行う際の重みはカーネル (kernel) またはフィルタ (filter) と呼ばれる。

数式による表現

入力画像の形状が H_x \times W_x \times C_x で各画素値は

\displaystyle
x_{ijk} \ , (i = 0, 1, \cdots, H_x - 1, j = 0, 1, \cdots, W_x - 1, k = 0, 1, \cdots, C_x - 1)

と参照する。(添字が0から始まっているのは、画像を配列で表現する際にインデックスは0から始まるため)

カーネルの形状が H_f \times W_f \times C_f で各要素は

\displaystyle
w_{ijk} \ , ( i = 0, 1, \cdots, H_f - 1, j = 0, 1, \cdots, W_f - 1, k = 0, 1, \cdots, C_f - 1)

と参照する。
カーネルC_f は、入力画像のチャンネル数 C_x と一致させる。

このとき、フィルタリングの結果、生成される出力画像の各画素値 u_{ijk} は次のように計算される。

\displaystyle
u_{ijk} = \sum_{s = 0}^{H_f - 1} \sum_{t = 0}^{W_f - 1} \sum_{u = 0}^{C_f - 1} x_{i + s, j + t, k + u} \ \ \ \ w_{stu}

フィルタリングの例

入力画像は形状が 5 \times 5 \times 1 のグレースケール形式とする。

カーネル3 \times 3 \times 1 で各要素は上記の値とする。

このとき、出力画像の各画素値はフィルタをスライドさせながら、カーネルと重なっている画素とカーネルの要素同士を乗算し、総和を取ることで求められる。


OpenCV でフィルタリングを行う。

OpenCV では、copyMakeBorder() でフィルタリングが行える。

関数

dst = cv2.filter2D(src, ddepth, kernel[, dst[, anchor[, delta[, borderType]]]])
  • 引数
    • src: 入力画像
    • ddepth: 出力画像の型。デフォルトは -1。-1 の場合は入力画像と同じ型。
    • kernel: カーネル
    • anchor: フィルタを表す行列のアンカー成分。デフォルトは (-1, -1)。(-1, -1) の場合は行列の中心。
    • delta: 重み付き総和を計算したあと、この delta を足す。デフォルトは0。
    • borderType: パディングする部分の外挿方法。
  • 返り値
    • dst: 出力画像

サンプルコード

サンプルとして Lena の画像を使用する。

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

# 画像をグレースケールで読み込む。
src = cv2.imread('lena.png', 0)

plt.imshow(src, cmap=plt.cm.gray)
plt.axis('off')
plt.show()

画像のエッジを抽出するのに利用される微分フィルタによるフィルタリングを行う例を示す。

pynote.hatenablog.com

# 微分フィルタ
kernel = np.array([[0, 0, 0],
                   [0, -1, 1],
                   [0, 0, 0]])

# フィルタリングを行う。
dst = cv2.filter2D(src, -1, kernel)

# 描画する。
plt.imshow(dst, cmap=plt.cm.gray)
plt.title('Difference Filter')
plt.axis('off')


パディング

フィルタリングを行った結果、出力画像の形状は入力画像より小さくなってしまう。
形状が H_x \times W_x の入力画像に対して、形状が H_f \times W_fカーネルを適用した場合、出力画像の形状は以下のようになる。

\displaystyle
(H_x – 2 \lfloor H_f / 2 \rfloor) \times (W_x – 2 \lfloor W_f / 2 \rfloor)

ただし、\lfloor \cdot \rfloor は小数点以下を切り捨てる床関数である。(例: \lfloor 1.5 \rfloor = 1)

そのため、出力画像が入力画像と同じ形状になるように、フィルタリングをする前に、入力画像の周囲に余白を足すパディング (padding) を行うことがある。
入力画像の高さ方向に 2 \lfloor H_f / 2 \rfloor、幅方向に 2 \lfloor W_f / 2 \rfloor だけパディングを加えれば、出力画像の形状が入力画像と同じになる。
通常、パディングは上下、左右それぞれ均等に行う。

追加した余白にどのような値を入れるかは次に紹介するようにいくつかの方法がある。

パディングの方法

関数

OpenCV のパディングを行う関数 copyMakeBorder() で、BorderType により指定できるパディング方法を確認する。
filter2D() では、内部でパディングが行われるので、明示的に copyMakeBorder() でパディングを予め行っておく必要はない。

dst = cv2.copyMakeBorder(src, top, bottom, left, right, borderType[, dst[, value]])
  • 引数
    • src: 入力画像
    • top: 上側のパディング幅
    • bottom: 下側のパディング幅
    • left: 左側のパディング幅
    • right: 右側のパディング幅
    • borderType: パディングの方法
    • dst: 出力画像 (引数経由で受け取る場合)
    • value: borderType=cv2.BORDER_CONSTANT の場合、外挿する値
  • 返り値
    • dst: 出力画像

指定した値で埋める。

例: iiiiii|abcdefgh|iiiiiii

dst = cv2.copyMakeBorder(src, 2, 2, 2, 2, cv2.BORDER_CONSTANT, value=255)
print(dst)
[[255 255 255 255 255 255 255]
 [255 255 255 255 255 255 255]
 [255 255   1   2   3 255 255]
 [255 255   4   5   6 255 255]
 [255 255   7   8   9 255 255]
 [255 255 255 255 255 255 255]
 [255 255 255 255 255 255 255]]

端の画素値を繰り返し並べて埋める。

例: aaaaaa|abcdefgh|hhhhhhh

dst = cv2.copyMakeBorder(src, 2, 2, 2, 2, cv2.BORDER_REPLICATE)
print(dst)
[[1 1 1 2 3 3 3]
 [1 1 1 2 3 3 3]
 [1 1 1 2 3 3 3]
 [4 4 4 5 6 6 6]
 [7 7 7 8 9 9 9]
 [7 7 7 8 9 9 9]
 [7 7 7 8 9 9 9]]

同じ画像を反転させ、並べて埋める。

例: fedcba|abcdefgh|hgfedcb

dst = cv2.copyMakeBorder(src, 2, 2, 2, 2, cv2.BORDER_REFLECT )
print(dst)
[[5 4 4 5 6 6 5]
 [2 1 1 2 3 3 2]
 [2 1 1 2 3 3 2]
 [5 4 4 5 6 6 5]
 [8 7 7 8 9 9 8]
 [8 7 7 8 9 9 8]
 [5 4 4 5 6 6 5]]

同じ画像を並べて埋める。

例: cdefgh|abcdefgh|abcdefg

dst = cv2.copyMakeBorder(src, 2, 2, 2, 2, cv2.BORDER_WRAP)
print(dst)
[[5 6 4 5 6 4 5]
 [8 9 7 8 9 7 8]
 [2 3 1 2 3 1 2]
 [5 6 4 5 6 4 5]
 [8 9 7 8 9 7 8]
 [2 3 1 2 3 1 2]
 [5 6 4 5 6 4 5]]

同じ画像を反転させ、並べて埋める。

cv2.BORDER_REFLECT の場合、端の画素は gh|hg のように連続して2回繰り返されるが、cv2.BORDER_REFLECT_101 の場合は gh|gf のように連続して2回繰り返さない。

cv2.BORDER_REFLECT の場合: fedcba|abcdefgh|hgfedcb
cv2.BORDER_REFLECT_101 の場合: gfedcb|abcdefgh|gfedcba

dst = cv2.copyMakeBorder(src, 2, 2, 2, 2, cv2.BORDER_REFLECT_101)
print(dst)
[[5 4 4 5 6 6 5]
 [2 1 1 2 3 3 2]
 [2 1 1 2 3 3 2]
 [5 4 4 5 6 6 5]
 [8 7 7 8 9 9 8]
 [8 7 7 8 9 9 8]
 [5 4 4 5 6 6 5]]

cv2.BORDER_DEFAULT、cv2.BORDER_REFLECT101 も cv2.BORDER_REFLECT_101 と同じである。
OpenCV の BorderType が引数にある関数はデフォルト引数が cv2.BORDER_DEFAULT になっているため、とくに指定しない場合はこの方法でパディングが行われる。