跳至主要内容

第 10 章:Camera2 管線

10.1 從使用到理解

在本系列的之前章節中,你使用了 Camera2:顯示預覽、擷取照片以及處理 RAW 文件。現在是時候反轉鏡頭,向內觀察了——Camera2 到底是如何交付這些幀的?

理解管線不僅僅是為了學術研究。當你了解請求如何在系統中流動時,你就可以:

  • 診斷高幀率拍攝時的掉幀問題
  • 解釋為什麼更改設定需要 1-2 幀才能生效
  • 優化連拍擷取以實現零黑屏
  • 為回呼時機建立正確的思維模型

Android Camera Parameters 應用(GitHubPlay 商店)即時可視化了管線行為——查看 Frame Timing (幀時序)Raw JSON 索引標籤,即可在你自己的設備上即時觀察本章中的概念。

10.2 核心數據結構

在觀察管線本身之前,讓我們深入研究流經管線的兩個物件:CaptureRequest(進入的)和 CaptureResult(出來的)。

CaptureRequest:不可變的幀藍圖

CaptureRequest 是針對單幀的完整、不可變配置。它描述了感光元件、鏡頭和 ISP 在一次曝光中應該執行的所有操作:感光元件曝光時間、ISO、鏡頭對焦距離、3A 模式、輸出目標、JPEG 質量、裁剪區域等等。

CaptureRequest 的關鍵屬性:

  • build() 後不可變 — 一旦你呼叫了 .build(),請求就被凍結了。要更改設定,你必須建立一个新的 Builder。
  • 建構器模式 — 透過 CaptureRequest.Builder 構造,從 CameraDevice.createCaptureRequest(template) 獲取。
  • 逐幀 — 每一个獨立的幀都有其自己的請求物件。即使是重複擷取也會(隱式地)為每一幀建立一个新請求。
  • 定向到 Surface — 每個請求都會明確列出哪些輸出 Surface 接收處理後的影像緩衝區。
// 使用建構器模式建構 CaptureRequest
val builder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)

// 感光元件級參數
builder.set(CaptureRequest.SENSOR_EXPOSURE_TIME, 10_000_000L) // 10ms
builder.set(CaptureRequest.SENSOR_SENSITIVITY, 400) // ISO 400
builder.set(CaptureRequest.SENSOR_FRAME_DURATION, 33_333_333L) // 最大約 30fps

// 鏡頭參數
builder.set(CaptureRequest.LENS_FOCUS_DISTANCE, 0.1f) // 10cm 對焦
builder.set(CaptureRequest.LENS_APERTURE, 1.8f) // f/1.8

// 3A 控制模式
builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF)
builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF)
builder.set(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_OFF)

// 輸出目標
builder.addTarget(previewSurface)
builder.addTarget(jpegReader.surface)

// 建構 — 現在不可變了!
val request: CaptureRequest = builder.build()

// request.set(...) 將會失敗 — 建構後的物件沒有 set() 方法!
備註

不可變性對於管線的正確性至關重要。由於 HAL 會非同步讀取請求,如果你能在提交後修改它,就會在應用執行緒和硬體處理執行緒之間產生競態條件。

CaptureResult:元數據報告(而非影像!)

CaptureResult 是處理後幀的元數據輸出。至關重要的是:CaptureResult 不包含影像像素數據。像素流向了你添加到請求中的 Surface 目標;CaptureResult 流向了你的 CaptureCallback,承載著拍攝期間發生的故事

以下是 CaptureResult 中最重要的欄位:

結果鍵類型說明
SENSOR_EXPOSURE_TIMELong實際使用的曝光時間(納秒),可能與請求值不同
SENSOR_SENSITIVITYInt實際應用的 ISO 增益
SENSOR_TIMESTAMPLong曝光開始時的納秒時間戳(來自 SystemClock.elapsedRealtimeNanos()
CONTROL_AE_STATEInt自動曝光狀態:INACTIVE、SEARCHING、CONVERGED、LOCKED、FLASH_REQUIRED
CONTROL_AF_STATEInt自動對焦狀態:INACTIVE、PASSIVE_SCAN、ACTIVE_SCAN、FOCUSED_LOCKED、NOT_FOCUSED_LOCKED
CONTROL_AWB_STATEInt自動白平衡狀態
LENS_FOCUS_DISTANCEFloat鏡頭設定的實際對焦距離
SCALER_CROP_REGIONRect用於數位變焦的實際裁剪區域
JPEG_GPS_LOCATIONLocation寫入 JPEG 的 GPS 標籤(如果請求了)
STATISTICS_FACE_DETECT_MODEInt實際使用的人臉偵測模式

結果欄位是你的地面真值 (ground truth)CaptureRequest 是你要求硬體做的;CaptureResult 是硬體實際執行的。在 LEGACY 或 LIMITED 設備上,HAL 可能會靜默地夾斷、捨入或覆蓋你的請求值——而結果能讓你檢測到這一點。

val captureCallback = object : CameraCaptureSession.CaptureCallback() {
override fun onCaptureCompleted(
session: CameraCaptureSession,
request: CaptureRequest,
result: TotalCaptureResult
) {
val timestampNs = result.get(CaptureResult.SENSOR_TIMESTAMP)
val exposureNs = result.get(CaptureResult.SENSOR_EXPOSURE_TIME)
val iso = result.get(CaptureResult.SENSOR_SENSITIVITY)
val aeState = result.get(CaptureResult.CONTROL_AE_STATE)
val afState = result.get(CaptureResult.CONTROL_AF_STATE)
val focusDistance = result.get(CaptureResult.LENS_FOCUS_DISTANCE)
val cropRegion = result.get(CaptureResult.SCALER_CROP_REGION)

val aeStateStr = when (aeState) {
CaptureResult.CONTROL_AE_STATE_INACTIVE -> "未活動"
CaptureResult.CONTROL_AE_STATE_SEARCHING -> "搜索中"
CaptureResult.CONTROL_AE_STATE_CONVERGED -> "已收斂"
CaptureResult.CONTROL_AE_STATE_LOCKED -> "已鎖定"
CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED -> "需要閃光"
else -> "未知($aeState)"
}

val afStateStr = when (afState) {
CaptureResult.CONTROL_AF_STATE_INACTIVE -> "未活動"
CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN -> "被動掃描"
CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN -> "主動掃描"
CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED -> "對焦鎖定"
CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED -> "未對焦鎖定"
else -> "未知($afState)"
}

Log.d("Pipeline", buildString {
append("幀 @${timestampNs?.let { it / 1_000_000 } ?: "?"}ms | ")
append("曝光: ${exposureNs?.let { "%.2fms".format(it / 1_000_000.0) } ?: "?"} | ")
append("ISO: $iso | ")
append("對焦: ${focusDistance?.let { "%.3f".format(it) } ?: "?"} 屈光度 | ")
append("AE: $aeStateStr | ")
append("AF: $afStateStr | ")
append("裁剪: ${cropRegion?.width()}x${cropRegion?.height()}")
})
}
}
提示

Android Camera Parameters 應用中,在設定中啟用 Live Result Logging (即時結果記錄),即可即時觀察元數據的流動。隨著曝光趨於平穩,你會看到 AE_SEARCHING 轉換為 AE_CONVERGED;點擊對焦時,AF_SCAN 會轉換為 FOCUSED_LOCKED。

10.3 請求佇列

Camera2 在框架層使用雙佇列管線模型。理解這些佇列可以解釋你觀察到的幾乎所有時序行為。

掛起請求佇列 (FIFO)

當你呼叫 session.capture()session.captureBurst()session.setRepeatingRequest() 時,請求不會立即發送到 HAL。相反,它進入掛起請求佇列 (Pending Request Queue)——一个由 Camera2 框架管理的 FIFO(先進先出)佇列。

把它想像成「候診室」。請求在這裡等待,直到 HAL 有能力接受新請求進行處理。

關鍵屬性:

  • FIFO 順序 — 請求按提交的確切順序進行處理。
  • 連拍原子性captureBurst() 中的所有幀都會被連續排隊並處理,不會被重複請求穿插。
  • 優先級覆蓋 — 單次/連拍請求在佇列中會跳到重複請求之前執行(單次完成後,重複請求會自動重新排隊)。
  • 有界的 — 佇列深度有限(通常為 4-8 個請求);溢出會觸發錯誤。

在途佇列 (In-Flight Queue)

當 HAL 從掛起佇列中取出一個請求並開始感光元件讀取/ISP 處理時,該請求就會移動到在途佇列 (In-Flight Queue)。此佇列包含硬體目前正在處理的所有請求。

在途佇列的深度 (CameraCharacteristics.REQUEST_PIPELINE_MAX_DEPTH) 告訴你硬體可以同時處理多少個幀。在典型的 FULL 設備上,這通常是 3–4 幀深度,這意味著:當幀 N 正在曝光時,幀 N-1 正在由 ISP 處理,幀 N-2 正在被寫入記憶體,而幀 N-3 正在返回給應用。這就是 Camera2 儘管每幀端到端耗時約 100ms 卻能實現 30+ fps 的原因。

10.4 結果回呼:CaptureCallback 生命週期

結果透過 CameraCaptureSession.CaptureCallback 返回。HAL 可以分階段返回結果,讓你在完整幀就緒之前提前獲取部分元數據。

四種回呼方法

方法呼叫時機包含內容用例
onCaptureStarted感光元件開始對此幀進行曝光最少資訊:幀號、時間戳精確的時序同步
onCaptureProgressedISP 已部分處理該幀PartialCaptureResult — 部分元數據欄位就緒早期 AE/AF 狀態更新
onCaptureCompleted完整幀已完成,所有緩衝區已交付TotalCaptureResult — 所有欄位最終元數據記錄
onCaptureFailed掉幀 / 發生錯誤CaptureFailure — 錯誤代碼、原因錯誤恢復

部分結果 vs. 全體結果

當 ISP 計算出一些元數據欄位但尚未完成整個管線時,會返回 PartialCaptureResult。當一切完成後,會返回 TotalCaptureResult

val fullPipelineCallback = object : CameraCaptureSession.CaptureCallback() {
override fun onCaptureStarted(
session: CameraCaptureSession,
request: CaptureRequest,
timestamp: Long,
frameNumber: Long
) {
super.onCaptureStarted(session, request, timestamp, frameNumber)
Log.d("Pipeline", "幀 #$frameNumber${timestamp / 1_000_000}ms 開始曝光")
}

override fun onCaptureProgressed(
session: CameraCaptureSession,
request: CaptureRequest,
partialResult: CaptureResult
) {
super.onCaptureProgressed(session, request, partialResult)
val aeState = partialResult.get(CaptureResult.CONTROL_AE_STATE)
val afState = partialResult.get(CaptureResult.CONTROL_AF_STATE)
Log.v("Pipeline", "部分結果: AE=$aeState AF=$afState")
}

override fun onCaptureCompleted(
session: CameraCaptureSession,
request: CaptureRequest,
result: TotalCaptureResult
) {
super.onCaptureCompleted(session, request, result)
val totalFrames = result.frameNumber
Log.d("Pipeline", "幀 #$totalFrames 已完全完成")
}

override fun onCaptureFailed(
session: CameraCaptureSession,
request: CaptureRequest,
failure: CaptureFailure
) {
super.onCaptureFailed(session, request, failure)
val reason = when (failure.reason) {
CaptureFailure.REASON_ERROR -> "內部錯誤"
CaptureFailure.REASON_FLUSHED -> "由於 abortCaptures() 被刷新丟棄"
else -> "未知 (${failure.reason})"
}
Log.e("Pipeline", "幀 #${failure.frameNumber} 失敗: $reason。已擷取影像: ${failure.wasImageCaptured()}")
}
}

10.5 管線內部機制:無狀態、順序、非同步、多輸出

Camera2 暴露的 HAL3 管線模型具有四個定義性屬性。內化這些屬性,大多數 Camera2 的「詭異」行為就會突然變得合理。

1. 無狀態性 (Statelessness)

硬體在請求之間沒有記憶。每一个 CaptureRequest 必須是自包含的——它包含每一个設定,而不僅僅是你相對於前一幀所做的更改。

這意味著:

  • 如果你在第 N 幀設定了 SENSOR_EXPOSURE_TIME 但在第 N+1 幀中省略了它,它將恢復為模板預設值。
  • 重複請求不是一組「覆蓋設定」——它是框架在每一幀都完整產生並重新提交的。
  • 在 HAL 層級没有「設定並忘掉」。
// 🔴 錯誤:期待設定能持久存在
session.setRepeatingRequest(requestWithExposure10ms, callback, handler)
// 稍後:僅更改 AF 觸發,忘記重新設定曝光
val triggerBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
triggerBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START)
triggerBuilder.addTarget(previewSurface)
session.capture(triggerBuilder.build(), callback, handler)
// 🐛 在此單次幀中,曝光恢復為 TEMPLATE_PREVIEW 的預設值!

// ✅ 正確:每個請求都是自包含的
val triggerBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
triggerBuilder.set(CaptureRequest.SENSOR_EXPOSURE_TIME, 10_000_000L)
triggerBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START)
triggerBuilder.addTarget(previewSurface)
session.capture(triggerBuilder.build(), callback, handler)

2. 順序處理

在單個邏輯相機串流中,請求按 FIFO 順序一次處理一個。没有重排序,没有並行請求評估。如果第 50 幀在佇列中位於第 49 幀之後,那麼即使第 50 幀的處理速度會「更快」,它也必須等待第 49 幀完成曝光。

這就是為什麼連拍擷取能產生連續、無間隙的幀:連拍的 N 個請求保證背靠背執行。

3. 非同步結果

提交請求的執行緒永遠不是接收結果的執行緒。結果是在你提供的 Handler 執行緒上交付的(如果你傳遞了 null,則在 Binder 執行緒上交付)。

實際後果:絕對不要在没有同步的情況下從回呼中存取共享的可變狀態。一个常見的 bug 是從點擊擷取按鈕和回呼中同時讀取/寫入 latestExposure

4. 單個請求多輸出

一個請求 → 多個輸出。單個 CaptureRequest 可以同時針對 2、3 甚至 4 個以上的 Surface 目標:

  • 預覽 SurfaceTexture(用於顯示)
  • JPEG ImageReader(用於靜態拍攝)
  • RAW ImageReader(用於 DNG)
  • MediaRecorder Surface(用於影片錄製)
  • Allocation Surface(用於 RenderScript/機器學習處理)

HAL 負責將單次感光元件讀取路由到多個 ISP 分支,以產生每種輸出格式。你不需要重複進行擷取;你只需宣告目標,硬體就會扇出。

10.6 端到端:追蹤單幀

讓我們先追蹤一个 JPEG 擷取請求流經整個管線的過程,將所有內容串聯起來:

10.7 觀察管線執行

Android Camera Parameters 應用(GitHubPlay 商店)包含一个 Pipeline Visualizer (管線可視化器) 偵錯視圖,它顯示了目前掛起佇列深度、在途佇列深度以及每幀的時間戳。打開應用,在設定中啟用 Developer Mode (開發者模式),選擇一個相機,並切換到 Pipeline 索引標籤以查看:

  • 有多少請求正在排隊 vs. 在途
  • 從開始到完成的每幀延遲
  • 每幀的部分結果計數(觸發了多少次 onCaptureProgressed 呼叫)
  • 任何帶有失敗原因的掉幀

該索引標籤是建立本章概念直覺的最佳方式。

10.8 小結

概念關鍵要點
CaptureRequest不可變的逐幀藍圖。透過 Builder 建構。包含所有設定(無持久性)。
CaptureResult僅包含元數據(無像素)。硬體實際執行情況的真實記錄。檢查 AE/AF 狀態、曝光、裁剪。
掛起佇列FIFO 候診室。連拍保持連續。單次拍攝會插到重複請求之前。
在途佇列目前正在處理的請求。深度 = 管線最大深度。在 FULL 設備上通常為 3-4 幀。
CaptureCallback四個階段:started → progressed → completed (或 failed)。部分結果 vs 全部結果。
無狀態性硬體沒有記憶。每個請求必須包含你關心的每一個設定。
順序 + 非同步保證 FIFO 順序。回呼執行緒與提交執行緒不同。
多輸出一个請求 → 多個 Surface(預覽 + JPEG + RAW + 影片,一次完成)。

下一章

第 11 章:擷取類型中,我們將研究向此管線提交請求的三種方式——單次、連拍和重複——以及何時使用每一種。我們還將探索內建模板(TEMPLATE_PREVIEWTEMPLATE_STILL_CAPTURE 等),它們為常見用例預配置了合理的預設值。