跳至主要内容

第 16 章:白平衡與色彩

你已經掌握了亮度(曝光)和清晰度(對焦)。現在是時候控制影像的觀感了——即色彩基調。

當你在一盞暖色白熾燈下拍攝一張白紙時,燈光的黃/橙色光線照射在紙上,感光元件看到的就是橙色。你的大腦會立即糾正這一點,依然看到「白紙」——但原始感光元件數據記錄的是事實:它是橙色的。

白平衡 (White Balance, WB) 是相機補償光源顏色的過程,從而使中性白色看起來真的是白色的。如果搞錯了,整張照片都會帶有不想要的色偏(太橙、太藍或太綠)。

Android Camera Parameters 應用在即時網格視圖中展示了每一个 AWB 預設,並提供了一个手動增益滑塊——打開應用,切換到白平衡面板,你就可以觀察到我們將在本章中實現的精確效果。


色溫:從暖到冷的頻譜

光源是用開爾文 (K) 為單位的色溫來描述的。這個標度描述了一个理論上的「黑體輻射器」發出的光的顏色與其溫度的關係。

反直覺的規則: 暖色光 = 開爾文數值(1800K 燭光 = 非常橙)。冷色光 = 開爾文數值(10000K 天空 = 非常藍)。你的眼睛在童年就學會了這一點;你的程式碼必須顯式記住它。

場景典型色溫若使用「日光」白平衡產生的偏色
燭光晚餐1800–2200K非常橙 / 琥珀色
家用鎢絲燈泡2700–3000K橙色 / 黃色
日出 / 日落3000–4000K暖金色調(通常很討喜!)
「冷白色」螢光燈4000–5000K帶綠色的色調
正午陽光5200–5800K正確的中性色
電子閃光燈5500–6000K中性色(與日光比對)
陰天 / 濃雲6000–7500K略微偏藍
開闊陰影(無直射陽光)7000–9000K藍色偏色
朦朧藍天9000–12000K非常藍

自動白平衡的工作:從場景統計數據中偵測可能的光源,然後減去偏色,使中性物體呈現中性色。


Camera2 中的自動白平衡 (AWB) 模式

透過 CaptureRequest.CONTROL_AWB_MODE 進行設定:

模式 (CONTROL_AWB_MODE_*)效果用例
OFF僅手動白平衡。顯式使用 COLOR_CORRECTION_GAINS_TRANSFORM專業模式、自定義調色、RAW + 後期
AUTO預設。ISP 持續執行光源偵測。普通攝影
INCANDESCENT (鎢絲燈)~2800K。強藍色增益以抵消溫暖的鎢絲燈光。室內家用燈具、舞台燈光
FLUORESCENT~4500K。針對典型辦公螢光燈的增益(傾向於偏綠)。辦公室 / 教室
WARM_FLUORESCENT~3200K。補償暖白色螢光燈管。家用節能燈 「暖白色」 模式
DAYLIGHT~5500K。標準的正午陽光光源簡報。戶外晴天,比對閃光燈
CLOUDY_DAYLIGHT~6500K。輕微變暖以抵消冷色調的陰天。多雲 / 朦朧天
TWILIGHT暖色調的黃昏金色時刻簡報 (~4500K)。日落、黃昏、溫暖的風景
SHADE~7500K。強紅色增益以抵消深藍色的陰影光線。陰影中的人像、城市背陰處

首先查詢支持的模式: 并不是每台設備都提供全部 9 種預設。旗艦手機通常提供;而入門級設備可能僅提供 AUTO + OFF

val availableAwbModes = characteristics.get(
CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES
) ?: intArrayOf()
Log.d("AWB", "可用模式: ${availableAwbModes.toList()}")

AWB 狀態(類似於 AF,但沒那麼「囉唆」)

AWB 狀態機在概念上與 AF 類似,但更簡單——它的狀態較少:

AWB 狀態含義
CONTROL_AWB_STATE_INACTIVEAWB 已禁用 (AWB_MODE = OFF)
CONTROL_AWB_STATE_SEARCHING正在尋找正確的光源(色調可能會漂移)
CONTROL_AWB_STATE_CONVERGED找到了穩定的光源 — 色彩穩定
CONTROL_AWB_STATE_LOCKED透過 CONTROL_AWB_LOCK = true 被顯式鎖定

對於對色彩要求極高的攝影(產品拍攝、目錄工作),請使用與 AF 相同的「等待收斂 / 鎖定後再拍攝」模式。


白平衡校正原理:底層機制

AWB 透過應用兩次色彩變換來實現從感光元件 RGB 到可顯示的 sRGB 的轉換。理解這些原理可以讓你透過手動數值繞過 AWB。

第 1 步:通道增益(白點校正)

首先,將每個顏色通道乘以一個增益值,使中性表面呈現出相等的 R、G、B:

如果在 3200K 鎢絲燈下的場景中,灰度目標產生的感光元件輸出為 [R=200, G=150, B=100],則 AWB 應用大約為 R: 1.0, G: 1.33, B: 2.0 的通道增益來將其歸一化為 [200, 200, 200]

在 Camera2 中,這透過 CaptureRequest.COLOR_CORRECTION_GAINS 暴露:一个 4 元素的浮點陣列,順序為 [R, Geven, B, Godd]

之所以有兩個綠色通道 (Geven, Godd),是因為許多智慧型手機感光元件使用 2×2 拜耳陣列:GR / BG 交替行。以 Green-R 開始的行與以 Green-B 開始的行具有略微不同的光譜靈敏度,需要獨立的數位增益。對於日常工作,將兩個綠色設定為相同的值即可。

// COLOR_CORRECTION_GAINS = [ R 增益, G-even 增益, B 增益, G-odd 增益 ]
val warmGains = floatArrayOf(1.0f, 1.2f, 0.8f, 1.2f) // 暖色調: 提升 R, 降低 B
val coolGains = floatArrayOf(0.85f, 1.0f, 1.25f, 1.0f) // 冷色調: 提升 B, 降低 R
val neutralGains = floatArrayOf(1.0f, 1.0f, 1.0f, 1.0f) // 單位增益 (原始感光元件色彩)

有效範圍: 增益通常由 HAL 限制在 [0.0, 4.0] 範圍內。使用 0.5× 到 3× 之間的乘數即可獲得合理的結果。

第 2 步:3×3 色彩變換矩陣(色域映射)

通道增益僅糾正了白點。但不同的感光元件具有不同的原生色彩濾波器光譜響應,且不同的輸出設備具有不同的顯示色域(sRGB/Rec.709 對比 DCI-P3 對比 Display P3)。3×3 色彩校正矩陣 (CCM) 將感光元件的原生 RGB 色彩空間映射到標準輸出空間。

數學表示為:

[ R' ] [ m11 m12 m13 ] [ R ]
[ G' ] = [ m21 m22 m23 ] [ G ]
[ B' ] [ m31 m32 m33 ] [ B ]

程式碼表示為:output = M × input,其中 M 是一個 3×3 矩陣。

Camera2 透過 COLOR_CORRECTION_TRANSFORM 暴露了這一點,它使用一个 Rational[9] 陣列進行設定(行優先:m11, m12, m13, m21, m22, m23, m31, m32, m33)。單位矩陣表示直接複製輸入:

// 有理數表示的 3x3 單位矩陣:對角線為 1/1, 非對角線為 0/1
val identityMatrix = arrayOf(
Rational(1,1), Rational(0,1), Rational(0,1),
Rational(0,1), Rational(1,1), Rational(0,1),
Rational(0,1), Rational(0,1), Rational(1,1)
)

Rec.709 與 DCI-P3 色域:

色彩空間覆蓋率用例
Rec.709 (sRGB)~35% 的可見光HDTV、網頁、JPEG 預設值,2020 年前約 100% 的手機顯示器
DCI-P3~45% 的可見光數位影院、4K 超高清、現代 iPhone/Android 廣色域顯示器

P3 顯示器比 Rec.709 能顯示更豐富的紅色和綠色。你的輸出 CCM 必須挑選一个與觀看者螢幕預期比對的目標色域。在 Android 上,請檢查 Display.isWideColorGamut() 並使用適當的矩陣。

實踐建議: 除非你正在編寫專業的 RAW 顯影工具或經過色彩管理的電影應用,否則請設定 COLOR_CORRECTION_MODE = TRANSFORM_MATRIX 並讓 OEM 的預設矩陣處理色域映射。大多數專業模式應用僅調整 COLOR_CORRECTION_GAINS(那 4 個增益),而保持矩陣不變。


完整範例 1:將 AWB 鎖定為日光預設(暖色調鎖定)

讓我們從簡單的開始。有時你不需要全手動——你只是想防止 AWB 在幀與幀之間漂移(例如:延時攝影、有場景變化的影片)。設定一个像 DAYLIGHT 這樣的固定預設可以保證跨拍攝的色彩一致性。

這是最簡單的手動色彩控制。

class AwbPresetController(
private val characteristics: CameraCharacteristics,
private val captureSession: CameraCaptureSession,
private val previewSurface: Surface
) {
// 如果 HAL 實際支持該模式,則返回 true
fun isModeSupported(mode: Int): Boolean {
val available = characteristics.get(
CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES
) ?: intArrayOf()
return mode in available
}

fun setPresetDaylightForWarmTintLock(): Boolean {
if (!isModeSupported(CameraMetadata.CONTROL_AWB_MODE_DAYLIGHT)) {
Log.w("AWB", "此設備不支援 DAYLIGHT 預設")
return false
}

val request = captureSession.device.createCaptureRequest(
CameraDevice.TEMPLATE_PREVIEW
).apply {
addTarget(previewSurface)

// 將白平衡鎖定為 DAYLIGHT (~5500K) 模式。
// 這將使室內的鎢絲燈場景呈現出刻意的暖色/橙色,
// 這是電影攝影中偏愛的「電影感」效果。
set(CaptureRequest.CONTROL_AWB_MODE,
CameraMetadata.CONTROL_AWB_MODE_DAYLIGHT)

// 在本例中保持 AE 和 AF 為預設設定(自動)
set(CaptureRequest.CONTROL_MODE,
CameraMetadata.CONTROL_MODE_AUTO)
}

captureSession.setRepeatingRequest(request.build(),
object : CameraCaptureSession.CaptureCallback() {
override fun onCaptureCompleted(
session: CameraCaptureSession,
request: CaptureRequest,
result: TotalCaptureResult
) {
val awbState = result.get(CaptureResult.CONTROL_AWB_STATE)
Log.d("AWB", "已應用 DAYLIGHT 預設, AWB 狀態=$awbState")
}
}, null
)
return true
}
}

藝術應用: 如果你使用 AWB_MODE = DAYLIGHT 拍攝日落,3000K 的日落光線在固定的 5500K 平衡下會顯得溫暖——從而產生濃郁、飽和的金橙色調。在這裡使用 AWB_MODE = AUTO 將會中和日落(這正是要點!),因為它會注入更多藍色來抵消金色的光線。預設可以保留氛圍。


完整範例 2:全手動 AWB — 自定義暖色日落增益

為了獲得終極的創意控制,請完全禁用 AWB 並寫入你自己的增益。讓我們建立一个「暖色日落觀感」——略微提升紅色,抑制藍色,並帶有微妙的綠色提升以避免出現紫色偏移。

class ManualColorGradingController(
private val characteristics: CameraCharacteristics,
private val captureSession: CameraCaptureSession,
private val previewSurface: Surface,
private val jpegReaderSurface: Surface
) {
// 典型的調色預設 (R, Geven, B, Godd)
object Presets {
val NEUTRAL = floatArrayOf(1.00f, 1.00f, 1.00f, 1.00f)
val WARM_SUNSET = floatArrayOf(1.00f, 1.20f, 0.80f, 1.20f) // 暖琥珀色
val COOL_MORNING = floatArrayOf(0.85f, 1.00f, 1.25f, 1.00f) // 冷藍色
val VINTAGE_KODAK = floatArrayOf(1.15f, 1.00f, 0.85f, 1.00f) // 經典電影感
val GREEN_SHIFT_FLUO = floatArrayOf(1.00f, 1.25f, 1.00f, 1.25f) // 螢光燈修正
}

fun applyManualGains(gains: FloatArray, includeMatrix: Boolean = true) {
// 驗證:必須支持 AWB_MODE = OFF (具備 MANUAL 性能時通常都支持)
val hwLevel = characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)
val supportsManualColor = (hwLevel == CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_3 ||
hwLevel == CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_FULL ||
isModeSupported(CameraMetadata.CONTROL_AWB_MODE_OFF))
if (!supportsManualColor) {
Log.e("AWB", "此 LEGACY 層級設備無法進行手動 AWB 增益調整")
return
}

val request = captureSession.device.createCaptureRequest(
CameraDevice.TEMPLATE_PREVIEW
).apply {
addTarget(previewSurface)

// 1) 完全禁用 AWB
set(CaptureRequest.CONTROL_AWB_MODE, CameraMetadata.CONTROL_AWB_MODE_OFF)

// 2) 應用 4 個通道的增益 (R, Geven, B, Godd)
set(CaptureRequest.COLOR_CORRECTION_GAINS, gains)

// 3) 選擇色彩校正策略
if (includeMatrix) {
// FAST: 讓 HAL 為此光源計算一个好的矩陣
// (矩陣是自動推導的;僅增益受用戶控制)
set(CaptureRequest.COLOR_CORRECTION_MODE,
CameraMetadata.COLOR_CORRECTION_MODE_FAST)
} else {
// EXPERT: 同時手動設定我們自己的 3x3 變換矩陣 + 增益
set(CaptureRequest.COLOR_CORRECTION_MODE,
CameraMetadata.COLOR_CORRECTION_MODE_TRANSFORM_MATRIX)
set(CaptureRequest.COLOR_CORRECTION_TRANSFORM, identityMatrix())
}
}

captureSession.setRepeatingRequest(request.build(), null, null)
Log.d("AWB", "已應用手動增益: [${gains.joinToString()}]")
}

// ------- 使用鎖定的手動色彩進行靜態拍攝 -------
fun captureStillWithColorGrading(gains: FloatArray) {
applyManualGains(gains)

val stillRequest = captureSession.device.createCaptureRequest(
CameraDevice.TEMPLATE_STILL_CAPTURE
).apply {
addTarget(previewSurface)
addTarget(jpegReaderSurface)

set(CaptureRequest.CONTROL_AWB_MODE, CameraMetadata.CONTROL_AWB_MODE_OFF)
set(CaptureRequest.COLOR_CORRECTION_GAINS, gains)
set(CaptureRequest.COLOR_CORRECTION_MODE,
CameraMetadata.COLOR_CORRECTION_MODE_FAST)

set(CaptureRequest.JPEG_QUALITY, 95.toByte())
}

captureSession.capture(stillRequest.build(), null, null)
}

// ------- 輔助函式 -------
private fun identityMatrix(): Array<Rational> = arrayOf(
Rational(1,1), Rational(0,1), Rational(0,1),
Rational(0,1), Rational(1,1), Rational(0,1),
Rational(0,1), Rational(0,1), Rational(1,1)
)

private fun isModeSupported(mode: Int): Boolean {
val available = characteristics.get(
CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES
) ?: intArrayOf()
return mode in available
}
}

使用預設

// 用戶點擊「日落暖色」按鈕
controller.applyManualGains(ManualColorGradingController.Presets.WARM_SUNSET)

// 用戶點擊「拍攝」 — 相同的增益流向 JPEG
controller.captureStillWithColorGrading(ManualColorGradingController.Presets.WARM_SUNSET)

COLOR_CORRECTION_MODE:FAST 對比 TRANSFORM_MATRIX

使用此決策表:

場景選擇 COLOR_CORRECTION_MODE =
我只需要手動增益;讓 OEM 選擇矩陣(多數應用)FAST
我正在外部應用完整的調色 LUT / 矩陣,需要未經改動的原始色彩空間TRANSFORM_MATRIX + 單位矩陣
我有為此感光元件專門推導的自定義色彩設定檔 (ICC / DCP)TRANSFORM_MATRIX + 自定義 3x3

警告: 使用單位矩陣的 TRANSFORM_MATRIX 會給你未經 OEM 色域映射的原始感光元件色彩。在許多感光元件上,如果不進行額外處理,這看起來會明顯偏灰(低飽和度)且略微帶綠。這是正確行為——它是為你的自定義處理管線準備好的原始感光元件輸出。


手動開爾文轉增益轉換器(色溫滑塊)

專業相機應用(包括 Android Camera Parameters)會提供一个開爾文溫度滑塊。由於 Camera2 不直接接受開爾文數值,我們透過數學近似 R/B 增益曲線。

一个適用於大多數智慧型手機感光元件的簡單近似值(請根據你的目標硬體憑經驗校準你的增益曲線):

class KelvinGainsConverter {
// 將開爾文 [2000..10000] 轉換為近似增益 [R, Geven, B, Godd]
// 簡單的普朗克軌跡近似 (對於 UI 滑塊足夠用了)
fun kelvinToRgbGains(kelvin: Int): FloatArray {
val k = kelvin.coerceIn(2000, 10000)
val temp = k / 100.0

// 紅色 (在低開爾文下偏暖)
val r = when {
temp <= 66 -> 255.0
else -> {
var x = temp - 60.0
x = 329.698727446 * Math.pow(x, -0.1332047592)
x.coerceIn(0.0, 255.0)
}
}

// 綠色
val g = when {
temp <= 66 -> {
var x = temp
x = 99.4708025861 * Math.log(x) - 161.1195681661
x.coerceIn(0.0, 255.0)
}
else -> {
var x = temp - 60.0
x = 288.1221695283 * Math.pow(x, -0.0755148492)
x.coerceIn(0.0, 255.0)
}
}

// 藍色 (在高開爾文下偏冷)
val b = when {
temp >= 66 -> 255.0
temp <= 19 -> 0.0
else -> {
var x = temp - 10.0
x = 138.5177312231 * Math.log(x) - 305.0447927307
x.coerceIn(0.0, 255.0)
}
}

// 進行歸一化,使 GREEN = 1.0, 然後取倒數: 我們需要的是補償溫度的增益。
// 如果用戶選擇了 2800K (暖色), 我們需要更多的藍色增益來抵消暖色調。
// 此函式返回源 RGB; 增益為 1/R : 1/G : 1/B, 以 G=1 進行歸一化
val rGain = (g / r).toFloat().coerceIn(0.3f, 3.0f)
val bGain = (g / b).toFloat().coerceIn(0.3f, 3.0f)
return floatArrayOf(rGain, 1.0f, bGain, 1.0f) // [R, Geven, B, Godd]
}
}

配合 SeekBar 使用(2000–10000 K 範圍):

val converter = KelvinGainsConverter()
seekKelvin.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(sb: SeekBar, progress: Int, fromUser: Boolean) {
val kelvin = 2000 + progress * 8 // 0→2000K, 1000→10000K
val gains = converter.kelvinToRgbGains(kelvin)
textKelvinLabel.text = "$kelvin K"
controller.applyManualGains(gains)
}
override fun onStartTrackingTouch(sb: SeekBar) = Unit
override fun onStopTrackingTouch(sb: SeekBar) = Unit
})

校準註記: 這是一个通用的普朗克近似。為了獲得完美結果,請在你的目標設備上執行 Macbeth ColorChecker 或白點校準,然後將曲線擬合到實測的 R/B 增益比與真實開爾文的關係上。Android Camera Parameters 應用使用了透過 SENSOR_CALIBRATION_TRANSFORM1(若可用)從 HAL 加載的每設備校準數據。


色彩問題排查

症狀原因解決辦法
設定了手動增益但色彩没有變化忘記設定 CONTROL_AWB_MODE = OFF → AWB 仍在覆蓋增益必須在設定 GAINS/TRANSFORM 之前設定 AWB_MODE = OFF
COLOR_CORRECTION_TRANSFORM 被忽略模式仍為 FAST; 該鍵僅在 TRANSFORM_MATRIX 模式下被尊重先設定 COLOR_CORRECTION_MODE = TRANSFORM_MATRIX
延時攝影幀之間 AWB 漂移(出現綠色/紫色偏色閃爍)AWB 仍處於 AUTO 狀態且每幀都在重新評估為延時攝影設定固定的 AWB_MODE 預設或全手動增益
JPEG 色彩與預覽不同JPEG 應用的模式/增益與最後的重複請求不同將相同的增益應用到 TEMPLATE_PREVIEW 和 TEMPLATE_STILL_CAPTURE 建構器中
LEGACY 層級設備:手動增益崩潰INFO_SUPPORTED_HARDWARE_LEVEL = LEGACY (不支援手動色彩)優雅回退;僅展示 AUTO + 預設 UI

小結

Camera2 中的白平衡與色彩校正為你提供了手動控制三部曲的最後一塊拼圖:

  • 色溫 (K): 低 K (1800K 燭光) = 暖/橙色;高 K (10000K 陰影) = 冷/藍色。AWB 進行補償以中和光源顏色。
  • AWB 模式: 9 種預設 (INCANDESCENTSHADE) + AUTO + OFF。使用前請查詢 CONTROL_AWB_AVAILABLE_MODES
  • AWB 狀態: SEARCHING → CONVERGED → LOCKED。在色彩關鍵的序列中請等待 CONVERGED/LOCKED。
  • 手動控制有兩個層級:
    1. COLOR_CORRECTION_GAINS = 4 元素浮點陣列 [R, Geven, B, Godd] — 白點校正。使用 COLOR_CORRECTION_MODE = FAST(OEM 矩陣, 自定義增益)。
    2. COLOR_CORRECTION_TRANSFORM = 3×3 Rational[9] 矩陣 — 完整的色域映射。在 TRANSFORM_MATRIX 模式下用於單位矩陣或自定義 CCM。
  • Rec.709 對比 DCI-P3: 3×3 矩陣將感光元件色彩空間映射到顯示目標色域。
  • 開爾文滑塊: 透過普朗克軌跡數學近似開爾文→增益,在 AWB 為 OFF 時應用。

下一章

你現在已經分別理解了曝光、對焦和白平衡。在第 17 章:3A 管線中,我們將最終把這三者編排在一起,作为一个內聚的靜態照片擷取序列:

  • 完整的 AF 觸發 → AF 鎖定 → AE 預擷取 → AE 閃光燈收斂 → 拍攝照片 流程
  • AE 閃光燈模式 (ON_AUTO_FLASH, ON_ALWAYS_FLASH, ON_AUTO_FLASH_REDEYE)
  • AE 狀態和預擷取觸發序列
  • 與 AE+AF 協調的 AWB 狀態
  • 一个實現整個 3A 編排的生產級 Kotlin 類別,並配有 Mermaid 序列圖
  • 參考 3A 控制管線研究

這一章將把所有內容串聯成一个可以工作的專業相機應用。千萬不要錯過。