コンテンツにスキップ

Raspberry Piのカメラ映像をブラウザーへ届ける――方式選定と止まった映像を誤認させない設計

Raspberry Piへ接続したOV5647の映像を、同じLAN上のブラウザーで見られるようにした。 画面は単純だが、「カメラの画像をWebブラウザで表示する」だけでは見守りカメラとして不十分だった。

この記事では、下記について解説しています。 - V4L2から継続してフレームを受け取る方法 - ブラウザーへの配信方式をMJPEGに決めた理由 - Webサーバの応答と映像の更新を分けて監視する設計方針

NOTE: GBRGのRAWデータとコンテナ運用は、それぞれ情報量が多いため別の記事で扱う。

今回の条件

方式を選ぶ前に、必要なことを絞った。

  • Raspberry PiとOV5647を使う
  • 解像度は640×480、目標は約30 fps
  • 同じLAN内で、少人数がPCまたはスマートフォンから見る
  • ブラウザー以外の専用クライアントを作らない
  • 音声、録画、過去映像の再生は不要
  • カメラ停止と通信エラーを利用者が見分けられるようにする

大人数への配信やインターネット越しの視聴は、今回の範囲に含めていない。この条件が変われば、適切な配信方式も変わる。

カメラからJPEGを作るまで

映像と状態は、次の経路でブラウザーへ送る。

OV5647からブラウザーまでの映像処理

図1: OV5647からブラウザーまでの映像処理

OV5647から取得するGB10は、各画素を10 bit値として持つGBRG配列だ。この実装では16 bit little-endianの値として読み、8 bitへ変換してからOpenCVでデモザイクする。

ただし、画素の並び、メモリ上のbyte配置、OpenCVの変換名を一度に説明すると、配信方式の話が埋もれてしまう。記事を分け、次の3点を図解する予定だ。

  1. 左上を原点とした4×4のGBRGカラーフィルター配列
  2. 10 bit値を16 bit little-endianで保持する1画素2 byteの配置
  3. GBRG10 → GBRG8 → COLOR_BayerGR2BGR → BGRの変換経路

V4L2のバッファを順番に処理する

フレーム取得にはV4L2のmmap方式を使う。最初にドライバーへ複数のバッファを要求し、それぞれをプロセスのアドレス空間へmapする。空のバッファをキューへ登録してから、ストリームを開始する。

V4L2 mmapバッファの循環

図2: V4L2 mmapバッファの循環

フレームが入ると、DQBUFで処理対象のバッファをキューから取り出す。内容をPython側へコピーしたら、同じバッファをQBUFで再びキューへ登録する。これは、カメラドライバーが繰り返し使うバッファをキューで循環させる考え方が近いかな。

終了時にはSTREAMOFF、mmap解除、ファイルディスクリプターのcloseまでをまとめて行う。詳しいAPIの流れはLinux KernelのV4L2 mmap資料で確認できる。

配信方式を比べる

今回検討した方式は、静止画の定期取得、MJPEG、HLS、WebRTCの4つだ。

方式 良い点 今回困る点 判断
JPEGを一定間隔で取得 実装が最も単純。1枚ずつ確認しやすい 滑らかな動きにするほどHTTPリクエストが増える。取得間隔の揺れも見えやすい 状態確認用には使えるが、ライブ観察には使わない
MJPEG 各フレームが独立したJPEG。ブラウザーのimg要素で表示できる。既存のJPEG変換をそのまま使える フレーム間圧縮がないため通信量が多い。大人数への配信には向かない 今回採用
HLS 通常のWebサーバやCDNで配りやすく、回線に合わせた配信や多数視聴へ広げやすい エンコード、segment、playlistが必要。通常構成では遅延が増え、低遅延化すると構成が複雑になる LAN内の少人数・低遅延用途には過剰
WebRTC 低遅延の音声・映像通信に向き、外部ネットワーク越しの接続も設計できる signalingに加え、外部接続ではSTUN/TURNなどの構成が必要。今回不要な双方向通信まで含む 旅行先から見る段階で再検討

HLSは信頼性や回線への適応、大規模配信に強い。一方、AppleのLow-Latency HLS資料でも、低遅延化にはpartial segment、playlist差分、blocking reloadなどの追加要素が必要になる。今回の1台のRaspberry Piと少人数のLAN内視聴には過剰設計なので不採用。

WebRTCは低遅延配信の有力候補だが、peer同士の接続方法を決めるsignalingは仕様に含まれず、STUN/TURNを含む接続設計が必要になる。外から安全に見る機能を作るときには価値があるが、今回は宅内のLANからの閲覧なので不採用。

MJPEGは帯域効率では劣る。それでも今回は、カメラ処理の出力がすでにJPEGであること、各フレームを独立して扱えること、ブラウザー側をimg要素だけで構成できることが決め手。

配信にはIANAに登録されているmultipart/x-mixed-replaceを使い、JPEGごとにboundaryとContent-Lengthを付けて送る。

Webサーバが応答しても、映像が更新されているとは限らない

WebサーバがHTTPリクエストへ応答できても、カメラから新しいフレームを取得できているとは限らない。そこで、プロセスの応答確認と映像の配信処理を分割した。

エンドポイント 確認するもの
/healthz 取得処理を含むサービスが稼働しているか
/readyz 鮮度上限以内の新しいフレームがあるか
/api/status 状態、フレーム番号、実測fps、直近のエラー

また、実測fpsは、直近最大30フレームのフレーム取得時刻を保存しておき、下記の計算式で求める。

fps = (記録数 - 1) / (最後の取得時刻 - 最初の取得時刻)

ブラウザーの状態表示判定

図3: Webサーバへ接続できない状態と、接続できても映像が更新されない状態も分割した。

合成画像は、実機試験をなくすためのものではない

カメラ固有の処理と、それ以外の変更を分けるため、CameraSourceをprotocolとして定義した。実機用のV4L2RawSourceと同じインターフェースでSyntheticSourceを用意し、移動する帯、フレーム番号、時刻を描いたJPEGを生成する。

CAMERA_BACKEND=synthetic python -m underwater_monitor

これは「実カメラを使うテストが不要」という意味ではない。変更内容によって確認場所を分けるための仕組みだ。

変更内容 開発PCの合成画像で確認 OV5647実機で確認
Web画面のレイアウトや状態表示 できる 最終確認のみ
API、フレーム更新、異常時の処理 できる 実機条件も確認
GBRGの色、露光、ゲイン、回転 できない 必須
V4L2 mmapとデバイス制御 できない 必須
実測fpsと表示遅延 合成入力の基礎確認のみ 必須

UIやAPIの変更は、合成画像を使った自動テストや開発PCでの動作確認を先に行える。カメラフォーマット、デバイス制御、実際の性能に関わる変更は、OV5647を接続したRaspberry Piで確かめる。

次の記事では、まずGBRGの4×4配列と1画素2 byteのメモリ配置をSVGにし、実際のRAWデータを小さく切り出して図と対応させる。

関連記事

参考サイト