5.16. カスタム畳み込みカーネル¶
ここまでに取り上げた近傍フィルタは、それぞれが各位置のウィンドウに適用する組み込みの統計量(平均、ガウス重み付き平均、メディアン)を持っていました。morph() は、その統計量自体をアプリケーション側がカーネルという形で与えられる唯一のフィルタです。カーネルとは、近傍のピクセルを単一の出力値にどのように合成するかを記述する、小さな重みの行列です。
その仕組みは古典的な畳み込み演算です。各出力位置で、近傍の各ピクセルがカーネル内の対応する重みと掛け合わされ、その積が合計され、結果は必要に応じてスケーリングおよびオフセットされて、出力ピクセルに書き込まれます。同じ入力でも、カーネルが異なれば異なる結果が得られます。すべて等しい正の重みを持つカーネルは mean() フィルタを再現し、ベル型のカーネルは gaussian() を再現します。それを超えるパターンは、エッジ応答、エンボス、勾配、シャープ化、モーションブラー、そして他にも数多くの効果を生み出します。これは古典的な画像処理が単一の線形パスで行おうとしてきたすべてです。
5.16.1. morph メソッド¶
シグネチャは他の近傍フィルタとよく似ていますが、引数が1つ追加されています。
img.morph(size, kernel, mul=1.0, add=0.0)
size は他の箇所と同じく半径であり、したがってカーネルは正確に (2 * size + 1) 行 × (2 * size + 1) 列でなければなりません。カーネルそのものは、これらの個数の数値を行優先順に並べたフラットなPythonのリストです。最初の (2 * size + 1) 個のエントリが先頭行、次の (2 * size + 1) 個が2番目の行、というように最下行まで続きます。mul は積の総和を出力ピクセルに書き込む前にスケーリングし、add は定数を加算します。デフォルトの mul=1.0 と add=0.0 では、畳み込みの出力は変更されません。
明示しておく価値のある詳細が1つあります。このメソッドは、出力を書き込む前に、積の総和をカーネルの各エントリの合計で自動的に除算します。この自動除算により、エントリの合計が9になる平均化カーネル(例えば3×3のボックスブラー)は、追加の手間なしに9分の1のスケールで出力され、合計が16になるガウス近似カーネルは16分の1のスケールで出力されます。いずれもアプリケーション側で除算を計算する必要はありません。アプリケーションが mul を設定するのは、自動正規化の上にさらなるスケールをかけたい場合だけです。あるいは、より一般的には、カーネルの合計がゼロになる(エッジ応答カーネル)ため、自動除算がゼロでない何かでの除算にならない場合です。その場合フレームワークは合計を1として扱い、mul がスケーリングされていない積の総和を範囲内に収めるための唯一のつまみになります。
適応的しきい値処理のセクションで説明した threshold=True / offset=N のペアは morph() でも機能します。したがって同じカスタムカーネルのフレームワークで、カットオフがカスタム統計量によって計算される2値しきい値処理を生成できます。
5.16.2. カーネルのレイアウト¶
3×3のカーネル(size=1)は、左から右、上から下へと並べた9個の数値のフラットなリストです。この規約は、リストをPythonの3行に分けて書くと自然に読めます。
sobel_x = [-1, 0, 1,
-2, 0, 2,
-1, 0, 1]
これはSobel-x勾配演算子です。どんなアプリケーションでも最初に欲しくなる標準的なカーネルであり、最初から最後まで通して見ておくのに役立つものです。パターンは単純で、左の列に負の重み、右の列に正の重みを置き、中央の列はゼロです。行の重み -1, -2, -1(右側では 1, 2, 1)は、隅よりも中央のほうが大きく、これにより中央の行が隅の行よりも結果に大きな影響を与えます。
カーネルが垂直なエッジ(左が暗く右が明るくなっていくピクセルの列)を走査するとき、負の重みは暗い側を、正の重みは明るい側を拾います。積の総和は大きな正の数となり、フィルタはそれを明るい出力ピクセルとして書き込みます。一様な明るさの水平なパッチではゼロになります。なぜなら、すべての正の重みは、同じ値のピクセル上にある同じ大きさの負の重みと釣り合うからです。
カーネルの実行:
img.morph(1, sobel_x, mul=0.25)
Sobelカーネルの合計はゼロです。左側のすべての負の重みは右側の等しい正の重みと釣り合うため、自動除算は何かで除算することはなく、mul が積の総和に対する唯一のスケールになります。mul=0.25 は応答を範囲内に保ちます。Sobel-xが3×3のパッチから生成できる絶対値の最大の総和はおよそ 4 * 255 = 1020(8個の明るいピクセルが最大 2 まで重み付けされる)であり、それを4で割り下げると、極端なケースは 255 に着地し、そこでフォーマットがきれいにクリップします。
対応するSobel-yカーネルは、同じ重みパターンを90度回転させることで水平なエッジを検出します。
sobel_y = [-1, -2, -1,
0, 0, 0,
1, 2, 1]
方向に関係なくあらゆるエッジを検出したいアプリケーションは、通常、両方のSobelを実行してその応答を組み合わせます。
5.16.3. 出力のオフセット¶
add はスケーリングのもう半分の話です。合計がゼロのカーネルの応答は符号付きです。エッジの一方の側では正、もう一方の側では負になり、その負の半分は符号なしピクセルに書き込まれるとゼロにクリップされます。add=128 は応答を中間グレーを中心とするようにシフトし、負の応答は 128 未満の値として、正の応答はそれより上として残ります。つまりエッジ応答やエンボスが両方向で見えるようになりますが、その代償として各方向で範囲が半分になります。
カーネルがどの mul と add の組み合わせを想定しているかは、そのカーネルの設計の一部です。標準カーネルカタログ には、各一般的なカーネルに適した設定が記載されています。
5.16.4. より大きなカーネル¶
このページのすべては3×3のカーネル(size=1)で説明してきました。これは標準カタログが使うサイズであり、また行優先のレイアウトをそのサイズなら手で書き出すのが容易だからです。とはいえ、仕組み自体はカーネルを3×3に制限するものではありません。size=2 は5×5のカーネルを実行し、フラットなリストには25個のエントリが入ります。size=3 は49個の7×7を実行します。以下同様に、アプリケーションが負担を許容できる半径まで可能です。フレームワークは、任意の奇数サイズでフラットリストとネストされた行のどちらのレイアウトにも対応します。
より大きなカーネルに手を伸ばす理由は、組み込みフィルタのいずれかでより大きな近傍に手を伸ばす理由と同じです。より多くの平均化、より広い特徴検出、単一ピクセルのノイズに対する感度の低下です。コストは半径の2乗に比例して増加します。5×5は3×3のおよそ2.8倍、7×7はおよそ5.4倍のピクセルあたりの処理を行い、その倍率はそのままフレームレートから引かれます。
実践的なパターンは、標準カタログでは size=1 にとどまり、アルゴリズムがより大きな近傍を必要とするときにだけ大きなサイズに手を伸ばすことです。エッジ検出器が3×3を超えて恩恵を受けることはまれです。平滑化フィルタは恩恵を受けることがあります。適切なサイズは、アプリケーションが強調または抑制しようとしている特徴のスケールによって決まります。
5.16.5. morph に手を伸ばすべきとき¶
日常的な平滑化には、mean()、gaussian()、bilateral() のほうが高速できれいです。エッジ検出には、laplacian() と find_edges() が専用に作られています。morph() に直接手を伸ばす根拠があるのは、組み込みフィルタが公開していない特定の畳み込みをアプリケーションが必要とする場合です。方向性のあるSobel、カスタムのエッジテンプレート、パイプラインの残りの部分が探そうとしている特定のテクスチャに合わせたカーネル、あるいは古典的な画像処理が数十年にわたって積み上げてきた有用なカーネルの標準カタログのいずれかです。任意のカーネルが持つ完全な柔軟性が利用できます。その代償は、望む結果を生み出すカーネル値を選ぶ責任がアプリケーション側にあることです。