5.9. 算術演算

前のセクションの描画ファミリーは画像描き込むものでした。算術ファミリーは2つの画像を組み合わせて3つ目を作ります。ピクセル値を足し合わせたり、一方から他方を引いたり、各位置で最小値や最大値を取ったりします。この小さなピクセル単位の算術演算のセットこそが、フレーム差分、背景差分、露出スタッキング、そしてその他いくつかの古典的なパターンが構築される土台です。

Image クラスの算術ファミリーは、一度に列挙できる程度に小さいものです。

  • add() -- ピクセルごとの self + other。フォーマットの最大値でクリップされます。

  • sub() -- ピクセルごとの self - other。下限は 0 でクリップされます。

  • rsub() -- ピクセルごとの other - self0 でクリップされます(オペランドを逆にした sub と同じ算術です)。

  • min() -- 2つの値のピクセルごとの最小値。

  • max() -- ピクセルごとの最大値。

  • difference() -- ピクセルごとの |self - other|、すなわち絶対差です。

加えて、関連する2つの単一画像演算があります。

  • invert() -- 各ピクセルを 255 - pixel(またはフォーマットに応じた等価な最大値)で置き換えます。

  • negate() -- invert() のエイリアスです。

上部にソース画像 A と B を表す2本の水平 グラデーションバー。A は左から右へ暗から 明へ、B は左から右へ明から暗へと変化します。 その下に、A と B に各ペア演算を適用した 結果を表す5本のグラデーションバー。 A.add(B) はあらゆる位置で合計が 255 を超えて クリップされるため一様に白くなります。 A.sub(B) は左半分が0で右へ向かって明るく なります。A.difference(B) は両端が明るく 中央が暗い V 字を示します。A.min(B) は両端が 暗く中央が明るくなります。A.max(B) は両端が 明るく中央が灰色になります。

2つのソースグラデーション A と B、そしてそれらに各ペア演算を適用した結果。すべての演算は位置ごとに実行されます。任意の位置の結果に現れるものは、その位置にある2つのソースピクセルのみに依存します。

5.9.1. 2つのオペランド形式

2画像メソッドはそれぞれ、第2オペランドとして次のいずれかの形式を受け入れます。

  • 同じ寸法の別の Image。算術は位置ごとに実行されます。(x, y) における結果は、両画像の (x, y) のソースピクセルに演算を適用したものです。

  • スカラー値 -- グレースケールでは整数、RGB565 では (r, g, b) のタプルです。同じスカラーがすべての位置に適用されます。

スカラー形式は、アプリケーションがすべてのピクセルを一定量だけシフトさせたい場合に役立ちます。img.add(40) は画像全体を40だけ明るくし、img.sub((20, 20, 20)) は各チャンネルごとにすべてのピクセルを20だけ暗くし、img.max(50) は50未満のピクセルを50まで持ち上げて残りはそのままにします。これは、ほぼ真っ黒なセンサーの下限を、後続の段階が処理するための平坦な暗い灰色に変えるような演算です。

5.9.2. クリッピング

ピクセル値はあらゆる演算を通じてフォーマットの範囲内に留まります。8ビットチャンネルの場合、それは 0 -- 255 を意味します。255 を超えてオーバーフローするものは 255 にクリップされ、0 を下回るものは 0 までクリップアップされます。ラップアラウンドはありません。

この選択は実際に重要です。ピクセルを明るくする add は、そうでなければ算術がオーバーフローするであろう明るい端で、突然暗くなるアーティファクトを決して生じさせません。ピクセルを暗くする sub は、そうでなければアンダーフローするであろう暗い端で、突然明るくなるアーティファクトを決して生じさせません。飽和した極値での情報の一部損失と引き換えに、結果は視覚的に意味を持ち続けます。

クリッピングは、subrsub が互いに異なる結果を返す理由でもあります。img_a.sub(img_b)a のうち b より明るい部分を返し、それ以外はすべて0になります。img_a.rsub(img_b)b のうち a より明るい部分を返します。どちらも片側の変化検出に役立ちます。アプリケーションが明るくなったピクセルだけ、あるいは暗くなったピクセルだけを気にする場合です。しかしどちらも、2つのフレーム間のすべての変化を捉えるわけではありません。

5.9.3. 差分演算

両側の変化検出には、頼るべき演算は difference() です。これは各位置で |self - other| を計算します。すなわち符号なしの絶対差です。どちらの方向に変化したピクセルも結果では非ゼロ値として現れ、その大きさはその位置でどれだけ変化したかに比例します。

この性質 -- 2つの画像が一致しない箇所でちょうど非ゼロになること -- が、difference をフレーム単位の変化検出の主力たらしめています。起動時に保存した参照フレームと新たなキャプチャを difference に通すと、シーン内で何かが動いたり明るさが変化したりしたすべての位置を非ゼロのピクセルが示す画像が得られます。

5.9.4. マスクによるスコープ指定

すべての算術メソッドは、領域とマスクのページで紹介された mask キーワード引数を受け入れます。マスクが渡されると、演算はマスクが非ゼロの位置でのみ実行され、それ以外の場所では出力先の画像はそのまま残されます。

この合成は2つのパターンで現れます。1つ目は、演算を既知の領域に制約することです。たとえば、検出されたマーカーのバウンディングボックス内でのみ2つのフレームを足し合わせる、といったものです。2つ目は、合成フレームを少しずつ組み上げることです。前景マスク内でフレームのシーケンスに対して min を取り、補完マスク内で同じシーケンスに対して max を取る、といった類のパターンです。

5.9.5. その場での処理と、入力の保持

算術メソッドはすべて、先に確立された動作の慣例に従います。各メソッドはソース画像をその場で変更し、チェーン用に同じ画像を返します。呼び出し後、ソースのピクセルは失われ、第2オペランドとして渡されたものに対する演算の結果で置き換えられます。

アプリケーションが両方の入力を保持する必要がある場合、安全なパターンはまず一方をコピーすることです。

diff = current.copy()       # leaves current intact
diff.difference(reference)  # diff now holds the absolute difference

このパターン -- コピーしてから演算する -- は、あらゆるフレーム差分パイプラインの基幹です。そこでは参照フレームは比較を生き延びて、次にキャプチャしたフレームで再利用できなければなりません。

6つの組み合わせ演算、2つの単一画像演算、絶対差の主力、そしてスコープ指定用のマスクキーワードを備えたこのピクセル算術ツールキットは、古典的なマシンビジョンが必要とする明るさとチャンネルの組み合わせをカバーします。表面上は算術に似た残りのツールは、値単位ではなくビット単位で動作します。