アルゴリズム
画像処理
設計
デザインパターン
組み込み

この記事について

現在、とある画像処理システムを趣味で開発中です。その中で使用した、フレームバッファ管理の制御と実装方法についてまとめました。

想定するシステムは、カメラなどの画像処理を行うシステムですが、一般的なバッファ管理にも応用できると思います。

登場人物

  • Buffer
    • メモリ上に確保されたバッファ。画像データなどを格納する
  • Writer
    • Bufferに書き込むを行うモジュール。例えば、カメラからの入力画像を書き込む
  • Reader
    • Bufferから読出しを行うモジュール。例えば、バッファ上の画像データを外部ディスプレイに出力する
  • Modifier
    • ReadしてWriteするモジュール。例えば、画像処理エンジン (本記事では扱わない)

各種バッファ制御

シングルバッファ

image.png

WriteとReadをシリアルに別々に実行する場合にはOK。Write/Readが同時実行されると、バッファ内の画像データが壊れたり、ちらつきの原因になる。

ダブルバッファ

image.png

ReaderがバッファAをReadしている裏で、Writerは次のバッファBにWriteする。次のフレームでは、WriterはバッファAにWriteして、ReaderはバッファBからReadする。これを繰り返す。

  • 読出し中バッファの画像データ破壊やちらつきは発生しない。
  • フレーム同期のタイミングで、バッファを切り替えるという制御が必要
  • Write時間が長く、1フレーム(例えば、16.6msec)以上の場合、同じフレームを再Readするという制御が必要
  • Write時間が短く、仮に1フレーム以内に完了しても、次のフレーム開始まで処理を待つ必要がある

    • そもそも、Writerにフレーム同期で制御させたくない場合もある。次フレームを待たず、どんどん処理させたい場合に、ボトルネックになる。 image.png
  • バッファAとバッファBの状態

    • W = Write中
    • WT = Write完了、Read待ち
    • R = Read中

上の図は、ダブルバッファ時の、両バッファへの制御のタイミングチャートです。
Readは必ずFrameSyncに同期します。また、最初の2回を除き、Writeの開始もFrameSyncに同期する必要があります。さもないと、表示中(Read中)のバッファを壊してしまうためです。

Write時間 > Read時間の場合には、次のバッファ書き込みが完了するまで、同じバッファの内容をReadし続けます。注目していただきたいのは、Write処理がフレームの途中で完了したとしても、次のフレーム開始まで待つ必要があります。つまり、無駄な時間が発生します。また、当然、「フレームに同期させる」という制御も必要になります。

トリプルバッファ

image.png

ひとつバッファを追加して、トリプルバッファにします。ダブルバッファの時には、Write処理を待つ必要がありましたが、追加されたバッファに対して休むことなくWriteすることが出来ます。

image.png

Write時間 > Read時間の場合には、Write処理はフレームに同期する必要がなく、常に処理することが可能です。
Write時間 < Read時間の場合は、あまりメリットがないように感じるかもしれませんが、Write時間が非常に短い場合には大きなメリットがあります。例えば、1フレーム内に、2回以上(例えば、10回)のWriteが可能な場合、次のフレームでは、最新のWriteされたデータをReadすることが出来ます。それ以外のデータはスキップされます。(↑の図では、W3のデータはスキップされて、より新しいW4のデータをReadしている)

データフローを考える

複雑なシステム

シンプルなGUIやOSD面の制御なら、上述のような「バッファを切り替える」という制御で良いと思います。しかし、複雑な画像処理システム考えると、少しややこしくなります。

image.png

上図のように、Readerが2つあるシステムを考えます。カメラから入力した画像を、ディスプレイに表示して、さらにJPEGに保存するというケースです。

「JPEGエンコード」という時点で、フレーム同期とは完全に独立しています。また、通常は1フレーム以上時間がかかります。そうなると、必要なバッファ枚数が増えてきます。さらに、Reader3, 4, ... と、他のモジュールが並列/直列につながる可能性もあります。

このような場合、「バッファを切り替える」という制御は非常に困難になります。また、このような課題はたびたび現れるので、一般化しておくと後々便利です。

データフローとして扱う

image.png

上述のシステムをデータフローとしてあらわすと、上図のようになります。非常に分かりやすいですね。

バッファ管理する人に任せる

バッファマネージャ(BufferMgr)を作り、この人にバッファの管理を任せます。WriterやReaderは、データフロー上のただのモジュールであり、バッファ管理は意識しません。

BufferMgrは、以下の関数を提供します。

  • uint32_t getBuffer()
    • 空いているバッファアドレスを返す
  • void incRefCnt(uint32_t address)
    • 指定バッファ(アドレス)の、参照カウンタを+1する
  • void decRefCnt(uint32_t address)
    • 指定バッファ(アドレス)の、参照カウンタを-1する。カウンタが0になった時、そのバッファは「空き」状態

image.png

BufferMgrを用いて、先ほどのデータフローに、制御も併せて記載したものが上の図になります。

  1. Writerは、getBuffer()によってBufferMgrから空きバッファを取得する
  2. Writerは、取得したバッファに対して、データを書き込む
  3. データを後段に送信する前に、参照カウンタを+1する
  4. 3と同様
  5. データを後段に送信する (バッファアドレスとして)
    • 後段であるReaderは受信したデータを使用して処理を行う
    • 処理完了後、参照カウンタを-1する
  6. 5と同様
  7. BufferMgrは、参照カウンタが0になった時点で、そのバッファを「空き」状態にする

バッファのアドレスや個数は、BufferMgrが管理します。各モジュールは、バッファは意識せずに、エラーやリトライ処理を実装しておけばOKです。

例えば、以下のような処理が考えられます。

  • Writerは、getBuffer()でエラーが発生したらリトライする。
    • ただし、常時エラーが発生する場合は、システム的にバッファ枚数が足りていないので、見直しを検討する
  • Readerは、新しいバッファ情報を受信するまでは、現在のバッファを再利用する
  • Readerは、現バッファの処理中に、新しいバッファ情報を複数回受信したら、最後のバッファ情報を残して、他は捨てる

ノート

これまでFrameSyncに同期するようなReaderを考えてきました。しかし、フレームの解放処理をバッファマネージャに任せていること、非同期に処理できることから、特にFrameSyncに同期する必要もありません。カメラ入力やディスプレイ出力の場合には必然的に処理の開始/終了タイミングはFrameSyncになると思いますが、特に合わせる必要もありません。商品仕様、対応可能な工数、やるき、スキルレベルに応じて以下のどちらかを選べばいいだけです。

  • ガチガチのタイミング制御をして、遅延やフレームバッファ使用量を抑えるか
  • ゆるゆる制御にして、遅延やフレームバッファ使用量が増加するか

ノート

タイトルは「フレームバッファ」としているが、それ以外のバッファにも適用できるはず。例えば、JPEGエンコード出力 ⇒ SDカード保存の間のバッファなど。

ノート

BufferMgrは通常タスクだけでなく、FrameSyncなどの割り込みハンドラから呼ばれる可能性もあります。そのため、必要に応じてちゃんと排他処理を実装しておく必要があります。

おわりに

特に調べながら書いたわけではないので、一般論とは異なる内容を書いてしまっているかもしれません。
そんなに外してはいないと思うのですが、訂正や改善点などがありましたら教えてください。