5.17. 標準的なカーネルのカタログ¶
古典的な画像処理では、繰り返し登場するカーネルの重みパターンのかなりの規模のカタログが蓄積されてきました。エッジ検出器、シャープ化、エンボス、平滑化、モーションブラーなどです。そしてそのすべてが morph() を通じて実行されます。それぞれは短く、それぞれが一つのことを行い、ほとんどは重みの基本的なロジックさえ理解できれば読み解くのが容易です。
以下のカーネルは、特に記載がない限りすべて3×3なので、呼び出しではすべて size=1 を使用します。各カーネルの重み構造はそれぞれに併記されています。なぜなら、なぜあるカーネルがエンボスを行い、別のカーネルがシャープ化を行うのかという直感を養うのは、重みを読み解くことだからです。
5.17.1. 恒等カーネル¶
考えられる最も単純なカーネルは恒等カーネルです。中心に1、それ以外はすべて0です。
identity = [0, 0, 0,
0, 1, 0,
0, 0, 0]
img.morph(1, identity)
各出力ピクセルは、近傍の中心、つまり同じ位置の入力ピクセルから値を取ります。画像は変化せずにそのまま通過します。恒等カーネルにはフィルタとしての実用的な用途はありませんが、他のすべてのカーネルを理解するための有用な基準となります。恒等以外のカーネルはすべて、恒等カーネルに何らかの変更を加えたものだからです。
中心の重みが大きく、その周囲に小さな負の重みを持つカーネルは、中心から周囲を減算します。中心の重みが0のカーネルはピクセル自体を無視し、その近傍同士の差にのみ反応します。このようにカーネルを読み解くこと、つまり中心の重みがピクセルに何をするか、周囲の重みが何を加えたり取り去ったりするかを読み取ることが、その効果を予測する最も速い方法です。
5.17.2. エッジ検出¶
エッジ検出カーネルは、明るさが特定の方向に急速に変化している位置に強く反応し、明るさが一様な場所ではほぼ0の出力を生成します。これらは重みの合計が0になるファミリーです。平坦な領域(すべてのピクセルが同じ値)では出力が0になります。なぜなら、すべての正の重みが、それと同じ大きさの負の重みによってちょうど打ち消されるからです。
Sobel-xは典型的な例です。これは垂直エッジ(左右の明るさの遷移)を検出します。
sobel_x = [-1, 0, 1,
-2, 0, 2,
-1, 0, 1]
img.morph(1, sobel_x, mul=0.25, add=128)
対応するSobel-yは同じパターンを90度回転させたもので、水平エッジ(上下の明るさの遷移)を検出します。
sobel_y = [-1, -2, -1,
0, 0, 0,
1, 2, 1]
Sobel-xの中央行は、-1 と 1 ではなく -2 と 2 の重みを持ちます。中央行の追加の重みにより、カーネルはエッジに沿った方向にわずかな平滑化を組み込みます。これにより、それらの追加の大きさを省いた、より単純なPrewitt演算子よりもノイズに対して堅牢になります。
prewitt_x = [-1, 0, 1,
-1, 0, 1,
-1, 0, 1]
prewitt_y = [-1, -1, -1,
0, 0, 0,
1, 1, 1]
Prewittはすべての行を等しく重み付けするため、その応答はSobelよりもわずかに鋭くなりますが、その代わり単一ピクセルのノイズに対してより敏感になります(カーネルの実行コストは同一です。畳み込みは重みが何であれ同じ作業を行います)。強いエッジを持つきれいな画像では、Sobelの完全に実用的な代替となります。
Scharrは逆の方向に進みます。その重みはより大きく、より細かい角度でのエッジ方向の正確な検出のために調整されています。
scharr_x = [-3, 0, 3,
-10, 0, 10,
-3, 0, 3]
img.morph(1, scharr_x, mul=0.0625, add=128)
mul=0.0625 の除数(1/16)は、より大きな積和の後に出力を 0 から 255 の範囲内に戻します。Scharrは、アプリケーションが幾何学的に最も忠実な勾配応答を必要とし、そのためにわずかに多くの演算を払うことを厭わない場合の正しい選択です。
5.17.3. ラプラシアン¶
ラプラシアンカーネルは、任意の方向のエッジに一度に反応します。Sobelがそれぞれ一つの軸に沿った明るさの変化を検出するのに対し、ラプラシアンの対称的な重みパターンは、エッジがどの方向に向かっているかに関係なく同じように反応します。
laplacian_4 = [ 0, -1, 0,
-1, 4, -1,
0, -1, 0]
img.morph(1, laplacian_4, add=128)
構造は次の通りです。中心の重み 4、上下左右の4つの近傍は -1 で重み付けされ、4つの対角は0で重み付けされます。カーネルの合計は0になるため、平坦な領域では出力が0になります。明るさが変化している場所では、中心の値がその4つの主要な近傍の平均と異なり、出力はその差の大きさになります。
8連結のバリアントには対角の近傍が含まれます。
laplacian_8 = [-1, -1, -1,
-1, 8, -1,
-1, -1, -1]
各カーネルはわずかに異なるものを検出します。4連結バージョンは水平および垂直エッジでよりきれいな出力を生成します。8連結のものはより等方的で、どの方向にも等しく反応しますが、わずかにノイズの多い出力を生成します。8連結カーネルは、エッジの可視化に使われることからアウトラインという名前でも流通しています。
5.17.5. エンボス¶
エンボスカーネルは、古典的な画像編集ソフトに見られる横から光を当てたような効果を生成します。出力は、画像がレリーフに押し出され、その後一つの隅から光を当てられたように見えます。
emboss = [-2, -1, 0,
-1, 1, 1,
0, 1, 2]
img.morph(1, emboss, add=128)
その仕掛けは対角線をまたぐ非対称性にあります。左上の隅が最も負の重みを持ち、右下が最も正の重みを持ち、隅から隅への対角線は負から1を経て正へと変化します。各ピクセルで、カーネルは本質的に「自分の右下の明るさから自分の左上の明るさを引いたもの」を計算します。これは画像がその方向に明るくなる場所では正に、暗くなる場所では負になります。128 を加えることで符号付きの出力を中間のグレーに再センタリングし、効果が見えるようにしています。
非対称性をもう一方の対角線をまたぐように回転させると、反対の方向からエンボスを行います。
emboss_alt = [ 0, 1, 2,
-1, 1, 1,
-2, -1, 0]
img.morph(1, emboss_alt, add=128)
二つのエンボス方向は組み合わせて使うと有用です。一方を他方から減算したり、同じ画像でそれぞれを実行して応答を比較したりすることで、アプリケーションが方向を検出する必要がある場合に役立ちます。
5.17.6. 平滑化¶
平滑化カーネルは、重みの合計が1になる(かつすべて非負である)ファミリーです。このようなカーネルを通した平坦な領域は同じ平坦な明るさを生成します。なぜなら、カーネルはピクセル値の差を増幅するのではなく、それらを平均化するからです。
最も単純なものはボックスブラーで、これはまさに mean() が計算するものです。
box_blur = [1, 1, 1,
1, 1, 1,
1, 1, 1]
img.morph(1, box_blur)
カーネルの合計は 9 なので、カーネル合計による自動除算によって積和が9個の近傍ピクセルにわたる真の平均に変わります。実際には mean() がこのカーネルを実行するより良い方法です。同じ出力をより速く、平均の計算だけに最適化された経路を通じて生成します。一方 morph は汎用の畳み込み機構を実行します。ボックスブラーがカタログに含まれているのは、他のすべての平滑化カーネルを理解するための正しい基準だからです。
ガウシアンの3×3近似は、隅よりも中心と主要な近傍を多く重み付けします。
gaussian = [1, 2, 1,
2, 4, 2,
1, 2, 1]
img.morph(1, gaussian)
重みはパスカルの三角形の行 1, 2, 1 をそれ自身と外積したものです。中心の重み 4 が最も大きいのは、中心ピクセルが自分自身の出力に最も寄与するからです。隅は中心から最も遠いため 1 です。カーネルの合計は 16 で、カーネル合計による自動除算が正規化を処理するため、mul 引数は不要です。3×3の形は真のガウシアンの粗い近似であり、size=1 での gaussian() と区別がつきません。morphの形が主に有用なのは、アプリケーションが平滑化を同じパス内の別の演算と組み合わせたい場合です。
5.17.7. モーションブラー¶
モーションブラーカーネルは一つの方向に沿ってピクセルを平均化し、垂直方向はぼかさずに残します。最も単純なケースは水平方向です。
motion_h = [0, 0, 0,
1, 1, 1,
0, 0, 0]
img.morph(1, motion_h)
中央行は水平軸に沿って3つのピクセルを平均化します。上下の行は0です。カーネルの合計は 3 なので、カーネル合計による自動除算は mul を一切必要とせずに真の3ピクセル平均を生成します。出力は入力を水平方向ににじませたコピーであり、これは露出中に被写体が横方向に動いているときにカメラが捉える効果です。垂直方向のモーションブラーは同じパターンを回転させたものです。
motion_v = [0, 1, 0,
0, 1, 0,
0, 1, 0]
対角方向のモーションブラーは主対角線を使用します。
motion_diag = [1, 0, 0,
0, 1, 0,
0, 0, 1]
img.morph(1, motion_diag)
モーションブラーカーネルは、効果として(視覚的な目的でフレームを意図的にぼかす)も、モーションアーティファクトに対して堅牢である必要があるアルゴリズムのテストパターンとして(モーションブラーのかかった入力でアルゴリズムを実行し、それでも正しい答えを生成するか確認する)も有用です。
5.17.8. カーネルを一目で読み解く¶
いくつかの経験則によって、新しいカーネルを一目で読み解きやすくなります。
非負の重みで合計が1 ⇒ 平滑化(平均的な明るさを保持する)。
正と負の両方の重みで合計が0 ⇒ エッジ応答(平坦な領域では0)。
大きな正の中心と小さな負の周囲で合計が1 ⇒ シャープ化(恒等にエッジ応答を加えたもの)。
合計が1で対角線をまたいで非対称 ⇒ エンボス(あらゆる明るさの遷移の片側を強調する)。
合計が1で一つの軸に沿って集中 ⇒ 方向性のあるブラー。
カーネルが最初に一致するこれらのうちの一つが、たいていそれが何をするかの正しい推測になります。最も有用なカーネルのほとんどは、その重みパターンのレイアウトだけから識別できます。
標準的なカーネルのいずれもアプリケーションが望むことを行わない場合、次のステップは手作業で一つを調整することです。上記の規則と mul / add の制御の組み合わせは、古典的なマシンビジョンのパイプラインがこれまでに望んだほぼすべての線形パスをカバーします。そこから先は、重みを試し、出力を見て、反復していくことの問題です。