カメラキャプチャパイプライン
プレビューは滑らかなのに撮影画像だけ暗い・ブレる、その原因はパイプライン設計にある。AVFoundationとCameraXのセッション構造からバッファ処理、HDR・露出・フォーカス制御までを原理から押さえられる。
- カメラは入力(デバイス)→セッション→複数出力(プレビュー・静止画・動画・解析)という有向グラフで、AVFoundationはこのグラフを明示的に組み、CameraXはユースケース抽象でCamera2の上に隠蔽する。
- センサーの各フレームはISPを通りYUVなどのバッファになる。プレビューと撮影は同じ流れから分岐する別出力なので、露出や焦点の状態が食い違うと見た目と結果がずれる。
- 露出・焦点・白色点は3AとしてISPが自動制御し、アプリは注目点、ロック、露出補正、HDRなどで介入する。非同期に収束するため、撮影前に安定を待つ設計が要る。
カメラは「グラフ」である
スマートフォンのカメラを「シャッターを押すと1枚撮れる装置」と捉えると、実装で必ず躓きます。実体は、1つの入力(カメラデバイス)から複数の出力(画面プレビュー・静止画・動画録画・フレーム解析)へフレームを分配する、常時稼働の有向グラフです。センサーはアプリが撮影を要求していなくてもストリーミングを続けており、各出力はその共通ストリームから分岐した枝にすぎません。iOSのAVFoundationはこのグラフをオブジェクトで明示的に組み立てさせ、AndroidのCameraXは「ユースケース」という抽象で低レベルのCamera2の上に覆いをかけます。両者の構造を原理から押さえると、「プレビューは綺麗なのに保存画像だけ暗い」「解析フレームだけ向きが違う」といった典型的な不具合が、どの枝で起きているかを切り分けられるようになります。
AVFoundation:入力・セッション・出力の明示的な配線
AVFoundationのキャプチャは、いくつかのオブジェクトを配線して1つのグラフを組む設計です。中心は AVCaptureSession で、これが全体のデータフローと開始・停止を統括します。
| オブジェクト | 役割 |
|---|---|
| AVCaptureDevice | 物理カメラ(広角・超広角・望遠、前面・背面)。露出・フォーカス・ISO等のハードウェア設定を保持する実体 |
| AVCaptureDeviceInput | デバイスをセッションに接続する入力ノード |
| AVCaptureSession | 入力と出力を束ね、データフローを制御する中枢。sessionPresetで解像度・品質を規定 |
| AVCaptureOutput(各種) | PhotoOutput(静止画)・MovieFileOutput(動画)・VideoDataOutput(生フレーム)・PreviewLayer への出力ノード |
| AVCaptureConnection | 特定の入力と出力を結ぶ枝。向き(回転)やミラーリング、安定化はここで設定する |
重要なのは、プレビューと静止画と解析フレームがそれぞれ別の出力ノードとして同じセッションにぶら下がる点です。プレビューを表示する AVCaptureVideoPreviewLayer はセッション直結でGPUへ流れ、AVCapturePhotoOutput は撮影要求時に高解像度フレームを取り出します。したがって「プレビューに映っている絵」と「保存される絵」は同一フレームではなく、同じセンサーから分岐した別出力です。向きの補正やミラーリングを AVCaptureConnection 単位で設定し忘れると、この枝ごとに結果が食い違います。
入力・出力の追加やプリセット変更は beginConfiguration() と commitConfiguration() で囲みます。これは複数の設定変更を1つのアトミックな再構成としてまとめ、中途半端な状態でグラフが再始動するのを防ぐためです。囲まずに個別変更するとフレームの供給が一瞬途切れたり、整合しない中間状態が露出したりします。
CameraX:ユースケース抽象がCamera2を覆う
Android素のCamera2 APIは、CaptureRequest を組み立ててサーフェスへストリームを流す低レベルな制御を提供しますが、端末ごとのハードウェア差やライフサイクル管理が非常に煩雑です。CameraXはその上に載るJetpackライブラリで、開発者が組むのは「配線」ではなく「何をしたいか」というユースケース単位になります。
| CameraXのユースケース | 対応する意図 | AVFoundation側の相当 |
|---|---|---|
| Preview | 画面にプレビューを出す | AVCaptureVideoPreviewLayer |
| ImageCapture | 静止画を撮る | AVCapturePhotoOutput |
| ImageAnalysis | フレームを取り出して解析する | AVCaptureVideoDataOutput |
| VideoCapture | 動画を録画する | AVCaptureMovieFileOutput |
これらのユースケースを cameraProvider.bindToLifecycle(...) でライフサイクル所有者に束ねると、CameraXが内部でCamera2のセッションとサーフェスを構築し、Activity/Fragmentの状態に合わせて自動で開始・停止します。ライフサイクルとの結合を自前で書かずに済むのが最大の利点で、Androidのアクティビティライフサイクル(/mobile-development/android-activity-lifecycle/)に沿ったカメラの解放漏れを構造的に防ぎます。抽象の代償として、端末固有の細かなチューニングが必要な場合はCamera2へ降りる(Camera2Interopを使う)判断が要ります。
フレームバッファ:YUVとISPの通り道
センサーが吐く生データは、そのままRGBのビットマップではありません。各フレームはハードウェアのISP(Image Signal Processor)を通り、デモザイク・ノイズ低減・レンズ補正などを経てから、多くはYUV系のフォーマットでバッファとして供給されます。プレビューやリアルタイム解析でRGBA前提のコードを書くと、この段で変換コストと取り違えが発生します。
- 色情報はYUV(輝度Yと色差U/V)で持つのが標準。人間の視覚が輝度に敏感で色差に鈍いため、色差を間引くクロマサブサンプリング(例 4:2:0)で帯域を節約できる。
- iOSでは
CVPixelBufferに格納され、kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange(いわゆるNV12系のBiPlanar)などが典型。Y平面とUV平面が別プレーンに分かれる。 - Androidの
ImageAnalysisでは既定がYUV_420_888で、Imageオブジェクトが平面ごとのByteBufferを返す。行パディング(rowStride)を無視して詰め直すと画像が斜めにずれる。
YUVバッファの各行は、幅ぴったりではなくメモリアライメントのためのパディングを含む場合があります。1行のバイト数を表す rowStride が画像幅と等しいと決め打ちして走査すると、行が進むごとに数バイトずつずれ、斜めに歪んだ画像になります。必ずプレーンの rowStride(と色差プレーンの pixelStride)を参照して読み出してください。
解析出力のフレームは基本的にゼロコピーに近い形でアプリへ渡され、参照を握り続けると次のフレーム用バッファが枯渇します。iOSの AVCaptureVideoDataOutput は専用のシリアルキューでコールバックを回し、alwaysDiscardsLateVideoFrames で処理落ちフレームを捨てて詰まりを防ぎます。CameraXの ImageAnalysis も STRATEGY_KEEP_ONLY_LATEST(最新だけ保持し古いものは捨てる)と STRATEGY_BLOCK_PRODUCER(詰まったら供給を止める)を選べます。解析処理をこのコールバックスレッドで重く回すと、共通センサーストリーム由来のプレビューまで巻き添えでカクつく点に注意が必要です。
プレビュー:低遅延優先の別経路
プレビューは撮影とは要求特性が違います。撮影は品質最優先で数百ミリ秒の遅延を許容できますが、プレビューは低遅延で毎フレーム描画し続ける必要があります。そのためプレビュー用フレームはアプリのメモリを経由せず、可能な限りGPUのテクスチャへ直接流し込む経路をとります。iOSの AVCaptureVideoPreviewLayer は CALayer のサブクラスとして描画パイプラインに直結し、CameraXの PreviewView は内部でSurfaceViewやTextureViewにストリームを結線します。ここから先の合成・ラスタライズはモバイルUIの描画パイプライン(/mobile-development/mobile-view-rendering-pipeline/)と同じGPU経路に合流します。プレビューだけを止めたい場合でもセッション自体は動き続けることがあり、電力面ではセンサーとISPが稼働し続ける点を意識する必要があります(バッテリー消費の詳細は /mobile-development/mobile-battery-power-profiling/)。
横にスクロール
3A制御:AE・AF・AWBは非同期の収束プロセス
露出・フォーカス・ホワイトバランスは、アプリが数値を直接書き込むより、ISPの自動制御(3A)に委ねるのが基本です。3AはAE(Auto Exposure:自動露出)、AF(Auto Focus:自動フォーカス)、AWB(Auto White Balance:自動ホワイトバランス)の総称で、いずれも「現在のフレームを測光・測距してパラメータを少しずつ目標へ寄せる」フィードバック制御です。ここで見落としがちなのは、これらが瞬時に決まらない収束プロセスだという点です。
| 制御 | 自動時の挙動 | アプリからの主な介入 |
|---|---|---|
| AE(露出) | 測光してシャッター速度(露光時間)とISO(ゲイン)を調整(スマホのレンズは絞り固定が一般的) | 露出補正(EV)、関心領域(POI)指定、AEロック |
| AF(フォーカス) | コントラスト/位相差検出で合焦位置を探索 | タップ位置へのフォーカスPOI、連続AFと単発AFの切替、AFロック |
| AWB(色) | 光源色温度を推定し色かぶりを補正 | プリセット色温度指定、AWBロック |
実務上の要点は、タップして被写体を指定した瞬間に撮ってはいけないことです。フォーカスと露出が目標へ収束するまで待ち、収束完了の通知(iOSの isAdjustingFocus / isAdjustingExposure の監視、Camera2/CameraXの CONTROL_AF_STATE 等の状態遷移)を見てから撮影トリガを引く必要があります。これを怠ると「タップした直後に撮ると毎回ボケる・暗い」という症状になります。露出補正でユーザーが明るさを持ち上げる操作も、内部的にはこの制御ループへの目標オフセットの注入として実現されます。
AVFoundationでフォーカスモードやISOをアプリから直接いじるには lockForConfiguration() でデバイスを排他ロックし、変更後に unlockForConfiguration() で解放します。ロックしたまま握り続けると、他コンポーネントや3A自動制御が設定を更新できなくなり、露出やフォーカスが固まったように見える不具合を生みます。変更は最小の区間でロックし、直ちに解放するのが原則です。
HDRと計算写真:1枚は「合成」で作られる
現代のスマートフォンの静止画は、多くの場面で単一露光ではなく複数フレームの計算合成です。センサーが常時ストリーミングしていることを利用し、露出の異なる複数フレーム(あるいは同一露出の連写)を撮影トリガの前後で溜め込み、後段で1枚に統合します。
- HDR/ブラケット合成: 露出の異なる複数フレームを合成し、白飛び(ハイライト)と黒潰れ(シャドウ)の両立するダイナミックレンジを稼ぐ。
- ノイズ低減の合成(iOSのDeep Fusion系や各社のマルチフレーム処理): 同一シーンの複数フレームを画素単位で統合し、暗所ノイズを抑えつつディテールを残す。
- 出力の色域とビット深度: 表示・保存でHDR(例 HDRディスプレイ向けの広い輝度レンジ)を扱うか、従来のSDRに丸めるかで、プレビューの見えと保存結果が変わり得る。
ここで再び効いてくるのが「プレビューと保存は別出力」という原理です。多くの端末では計算写真の重い合成は撮影要求後に走るため、プレビューはリアルタイムの簡易処理、保存画像は数百ミリ秒かけた高品質合成という非対称が生まれます。「プレビューより保存画像のほうが綺麗(あるいは色味が違う)」のはバグではなく、この二経路の設計の帰結です。撮影後の合成中にセッションを即座に破棄すると結果を取り逃すため、完了コールバックまで生かす配慮が要ります。
「AVFoundationは入力・セッション・出力を明示的に配線し、CameraXはユースケース抽象でCamera2を覆う」という対比、「プレビュー・静止画・解析は同一センサーストリームから分岐した別出力である」という原理、「3A(AE/AF/AWB)は瞬時値でなく収束プロセスであり、収束を待ってから撮る」という運用は面接・設計レビューで問われやすい急所です。YUV/NV12バッファのrowStride取り扱いも実装力の判定点になります。
まとめ
モバイルのカメラは、1つのデバイス入力から複数出力へフレームを分配する常時稼働の有向グラフです。AVFoundationは AVCaptureSession を中枢に入力・出力・接続を明示的に配線させ、CameraXはPreview/ImageCapture/ImageAnalysis/VideoCaptureというユースケース抽象でCamera2の煩雑さとライフサイクル管理を覆います。センサーの生データはISPを通ってYUV系(iOSのBiPlanar CVPixelBuffer、AndroidのYUV_420_888)のバッファとして供給され、rowStrideやプレーン構造を正しく扱わないと画像が歪みます。プレビューは低遅延優先でGPUへ直結する別経路、静止画は品質優先で計算合成を伴う別経路であり、両者が同一フレームでないことが「見えと結果のずれ」の根本原因です。露出・フォーカス・ホワイトバランスは3Aとしてフィードバック制御され、収束を待ってから撮影トリガを引くこと、デバイス設定のロックを最小区間に留めることが安定した撮影の鍵になります。カメラの権限モデルはアプリサンドボックスと権限(/mobile-development/mobile-app-sandbox-permissions/)、バックグラウンドでのセッション解放はバックグラウンド実行制限(/mobile-development/mobile-background-execution-limits/)と合わせて理解すると、権限・電力・ライフサイクルまで含めた実装像が立体的になります。
モバイル開発の記事ガイド
カメラキャプチャパイプラインを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
iOS
比較で見る軸
難易度: advanced / カテゴリ: モバイル開発 / タグ数: 6
導入後に効く点
センサーの各フレームはISPを通りYUVなどのバッファになる。プレビューと撮影は同じ流れから分岐する別出力なので、露出や焦点の状態が食い違うと見た目と結果がずれる。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- モバイル開発
- タグ数
- 6
判断チェックリスト
- 自社の用途が「iOS / Android」に近いか確認する。
- 強みである「カメラは入力(デバイス)→セッション→複数出力(プレビュー・静止画・動画・解析)という有向グラフで、AVFoundationはこのグラフを明示的に組み、CameraXはユースケース抽象でCamera2の上に隠蔽する。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。