5.5. 領域とマスク¶
image モジュールのすべての操作は、デフォルトでソース画像のすべてのピクセルに作用します。これは説明するのが最も単純な動作であり、アルゴリズムの役割が本当にフレーム全体に及ぶ場合 -- 一様なカラー補正、グローバルなヒストグラム、伝送用のエンコード処理など -- には正しい選択です。しかし、実際には大半のアルゴリズムはそれより狭い範囲だけを見たいと考えます。色付きマーカーを追跡するブロブトラッカーは、マーカーが現れ得るシーンの一部に関心があるのであって、その背後の壁には関心がありません。モルフォロジー的なクリーンアップ処理は、前段で候補としてマークされたピクセルに対してのみ安全に行えます。顔検出器は、より粗い検出器が既に絞り込んだバウンディングボックスの内部でのみ実行されるかもしれません。image モジュールは、操作をピクセルの部分集合に限定する2つの仕組み -- 矩形の関心領域とバイナリのマスク -- を通じてこの作業をサポートします。これらは自由に組み合わせられ、ピクセルに作用するほぼすべてのメソッドが、いずれか一方 -- または両方 -- をキーワード引数として受け取ります。
5.5.1. 関心領域¶
関心領域とは、座標のページで紹介した (x, y, w, h) の4要素タプルで指定されるピクセルの矩形です。インターフェース上の約30個のメソッドが roi キーワード引数を受け取ります。これが指定されると、操作はその矩形内のピクセルに対してのみ実行され、画像の残りの部分には手を付けません。roi が None であるか省略された場合、操作は画像全体に対して実行されます -- これは roi=(0, 0, width, height) が渡された場合と同じです。
コード上では、このキーワードは操作が受け取る他の引数と並んで記述されます。
# Compute a histogram over a centred crop of the image.
h = img.get_histogram(roi=(64, 64, 128, 128))
ROI がもたらす1つ目の利点は誤検出の抑制です。テーブルだけを見るカラートラッカーは、その前を通り過ぎるシャツに反応することは決してありません。定義された作業領域内でのみ実行されるエッジ検出器は、カメラマウント自体のエッジを報告することは決してありません。検索範囲をアルゴリズムが実際に関心を持つシーンの一部にまで絞り込むことは、パイプラインが自身の信頼性に対して行える最も安価な改善です。
2つ目の利点は粗から細へのパイプラインです。検出結果オブジェクト -- blob、rect、apriltag など -- は、そのバウンディングボックスを roi が受け取るのと同じ (x, y, w, h) の4要素タプルとして公開します。そのため、粗い第1段がバウンディングボックスを返し、そのボックスを次段の roi にそのまま渡すと、第2段はより狭い領域に対して実行されます。段階的に絞り込むたびに、次段の処理が高速化されると同時にその結果の信頼性も高まります。検索空間が既にフィルタリングされているためです。
5.5.2. バイナリマスク¶
関心領域が軸に沿った形状の場合は、矩形が適切な形です。そうでない場合 -- 曲線状の領域、非凸な領域、前段が「一致」と分類したピクセルなど -- には、操作を任意のピクセルパターンに限定するよう指示しなければなりません。そのための仕組みがバイナリマスクです。これはソースと同じ寸法を持つ別個の Image で、ピクセルごとのオン/オフスイッチとして使われます。マスク内の非ゼロのピクセルは「対応するソースピクセルを含める」ことを意味し、ゼロのピクセルは「ソースピクセルをそのままにする」ことを意味します。
マスクは通常 BINARY 画像 -- まさにこの目的のために存在する1ピクセルあたり1ビットのフォーマット -- ですが、消費側は非ゼロの値をすべてオンとして扱うため、任意の単一チャンネル画像が使えます。
フィルタリング、しきい値処理、算術演算のメソッドは mask キーワード引数を受け取ります。形式はどれも同じで、ソースと同じ寸法の、別途確保したバイナリ画像を渡します。
ROI とマスクは組み合わせられます。両方を渡すと、操作は ROI の内側にあり、かつマスクでオンになっているピクセルに対してのみ実行されます。この2つの仕組みは、アプリケーションコードに独立したレバー -- 矩形の関心領域用と、その中の任意のパターン用 -- を与え、どちらの形式も他方から制約を受け継ぐことはありません。
ROI は操作を軸に沿った矩形に限定します。マスクはそれをさらに任意のピクセルパターンへと絞り込みます。この2つは組み合わせられ、ROI の内側にあり、かつマスクでオンになっているピクセルだけが変更されます。¶
5.5.3. マスクの構築¶
3つの Image メソッドが、選択した領域の外側のピクセルをゼロにすることで、一般的なマスク形状をその場で構築します。
mask_rectangle()は矩形を残します。mask_circle()は円を残します。mask_ellipse()は楕円を残します。
それぞれ (x, y, w, h)(矩形と楕円の場合)または (x, y, radius)(円の場合)を受け取ります。いずれも引数なしで呼び出すと、形状が中央に配置され、画像を埋めるサイズに調整されます。これは、隅以外は何も隠さない単純な全画面の楕円や円が欲しいときにアプリケーションが用いる形式です。
mask = image.Image(img.width(), img.height(), image.BINARY)
mask.clear() # start from all zeros
mask.mask_ellipse() # centred, full-size oval
興味深いマスクが mask_* メソッドだけから得られることはまれです。それらはパイプラインの前段から生まれます。しきい値処理は、非ゼロのピクセルが一致箇所を示すバイナリ画像を生成し、これは次段の mask= 引数に渡すのにまさに適した形式です。モルフォロジー的なクリーンアップ処理は、その形式を変えることなくマスクを洗練します。単一チャンネル画像として得られるものは、それ自体が有効なマスクです。
5.5.4. 操作が画像を変更する仕組み¶
ここ数ページのすべてのコードスニペットに見られるパターン -- 操作がチェーン用に同じ img を返すこと -- は、新しいメソッドが導入されるたびに繰り返し述べなくて済むように、明示的に取り出しておく価値があります。Image インターフェースには3つのメソッド群が現れ、それぞれソース画像を異なる形で扱います。
操作系メソッドは、ソースのピクセルをその場で変更し、チェーン用に同じ画像を返します。描画、算術演算、しきい値、フィルタの各群はすべてこのように動作します。
img.gaussian(1)はimgをぼかして同じimgを返します。img = img.gaussian(1)のように再代入しても無害ですが不要です。変換系メソッドは、デフォルトでは操作系メソッドと同じようにその場で動作しますが、ソースを保持する必要がある場合には
copy=Trueとcopy_to_fb=Trueを受け取り、別個の結果画像を確保します。フォーマット変換と幾何学的なコピーがこの群の主なメンバーです。検査系メソッドは、ピクセルを読み取り、ソース画像をまったく変更することなく結果オブジェクト -- 検出された特徴のリスト、ヒストグラム、統計値の集合など -- を返します。
この3分類はインターフェース全体にわたって一貫しています。あるメソッドがどの群に属するかが分かれば、呼び出しから何を期待すべきか -- ソースのピクセルが無傷で残るのか、別個の結果画像が確保されるのか、戻り値がソース自体なのかそれ以外なのか -- がアプリケーションに分かります。