感測器端白平衡
不同色溫光源讓白色偏色。AWB 對 R/G/B 套不同 gains 把白校正回白。OV5640 內建 AWB(register 0x5180 起)。
v4l2-ctl -d /dev/v4l-subdev0 -c auto_n_white_balance=0 # 關 AWB v4l2-ctl -d /dev/v4l-subdev0 -c red_balance=128 # R gain v4l2-ctl -d /dev/v4l-subdev0 -c blue_balance=128 # B gain
v4l2-ctl -d /dev/v4l-subdev0 -c auto_n_white_balance=0
AWB 只解決「白平衡」——讓灰卡變灰。真正讓顏色「準」還需要 CCM(色彩校正矩陣):把感測器響應換算到目標色彩空間。
| 概念 | 意義 | OV5640 |
|---|---|---|
| 色溫(K) | 光源偏藍(高)/偏黃(低) | AWB 依此估 gains |
| AWB gains | R/G、B/G 比值 | 0x5180 帶 |
| CCM | 3×3 矩陣校正色偏 | 0x4750 帶(部分公開) |
| 灰卡白點 | 中性參考 | 校正基準 |
灰卡校正為什麼可靠:灰卡(如 18% gray)在可見光譜中性反射。把「灰卡在影像中 R=G=B」當條件解出 gains,就得到該光源的 AWB。CCM 則在固定光源下用色卡(如 ColorChecker)解矩陣。
# 關自動曝光與 AWB,鎖定曝光,避免灰卡亮度跑掉
v4l2-ctl -d /dev/v4l-subdev0 -c auto_exposure=0
v4l2-ctl -d /dev/v4l-subdev0 -c exposure=2000
v4l2-ctl -d /dev/v4l-subdev0 -c auto_n_white_balance=0v4l2-ctl -d /dev/video0 --set-fmt-video=width=1280,height=720,pixelformat=YUYV \ --stream-mmap=3 --stream-count=1 --stream-to=gray.yuv
python3 - <<'EOF'
import numpy as np
yuv = np.fromfile('gray.yuv', dtype=np.uint8).reshape(720,1280,2)
y = yuv[...,0].astype(float)
u = (yuv[...,1]-128).astype(float)
v = (yuv[...,1]-128).astype(float) # 簡化:用中央區
roi = y[300:420, 500:780]
print('Y mean =', roi.mean()) # 曝光基準參考
EOFv4l2-ctl -d /dev/v4l-subdev0 -c red_balance=140 v4l2-ctl -d /dev/v4l-subdev0 -c blue_balance=110 v4l2-ctl -d /dev/v4l-subdev0 -C red_balance -C blue_balance
| 現象 | 方向 |
|---|---|
| 整體偏黃/偏藍 | AWB 沒收斂或混合色溫 → 灰卡校正 |
| 灰卡上 R≠B | gains 沒調平 → 重做步驟 4 |
| 白色正常但彩色彩偏 | CCM 問題 → 後處理矩陣校正 |
| 特定光源下突然色偏 | AWB 演算法誤判 → 鎖定手動 gains |
| 色偏只在角落 | LSC 色差 → 回單元 12 |
python3 - <<'EOF'
import cv2, numpy as np
img = cv2.imread('shot.png').astype(float)/255.0
b,g,r = cv2.split(img) # BGR 順序
ccm = np.array([[1.6,-0.4,-0.2],
[-0.2,1.4,-0.2],
[-0.1,-0.2,1.3]])
rgb = np.stack([r,g,b],axis=-1) @ ccm.T
r,g,b = cv2.split(rgb.clip(0,1))
out = cv2.merge([b,g,r])*255
cv2.imwrite('shot_ccm.png', out.astype(np.uint8))
EOF場景:Orange Pi 5 接 OV5640,在室內日光燈 + 窗邊日光的混合色溫場景下,AWB 嚴重偏色。需要用分層策略校正。
v4l2-ctl -d /dev/v4l-subdev0 -c auto_exposure=0 -c exposure=2000 v4l2-ctl -d /dev/video0 --set-fmt-video=width=1280,height=720,pixelformat=YUYV \ --stream-mmap=3 --stream-count=1 --stream-to=mixed.yuv
python3 - <<'EOF'
import numpy as np
yuv = np.fromfile('mixed.yuv', dtype=np.uint8).reshape(720,1280,2)
# 假設 left=日光燈區, right=窗邊日光區
left_roi = yuv[:, 0:640, :] # 日光燈偏綠
right_roi = yuv[:, 640:1280, :] # 日光偏暖
print('左區 Y mean:', left_roi[...,0].mean())
print('右區 Y mean:', right_roi[...,0].mean())
EOFpython3 - <<'EOF'
import cv2, numpy as np
img = cv2.imread('mixed.png').astype(float)/255.0
# 左半用冷色 CCM,右半用暖色 CCM
ccm_cool = np.array([[1.2,-0.1,-0.1],[-0.1,1.1,-0.0],[-0.1,-0.1,1.2]])
ccm_warm = np.array([[1.0,-0.2,0.2],[0.0,1.0,0.0],[0.0,-0.1,1.1]])
h, w = img.shape[:2]
left = img[:, :w//2] @ ccm_cool.T
right = img[:, w//2:] @ ccm_warm.T
out = np.clip(np.concatenate([left, right], axis=1), 0, 1) * 255
cv2.imwrite('mixed_ccm.png', out.astype(np.uint8))
EOF| 項目 | OV5640 內建 AWB | RK3588 ISP3 AWB |
|---|---|---|
| zone 數 | 1(全局) | 15×15 或 17×13 |
| 色溫範圍 | 固定(2800K–6500K) | 可調(依 tuning) |
| 混合色溫處理 | ❌ 無法 | ⚠️ zone-based 可部分處理 |
| gains 精度 | 8-bit(0–255) | 12-bit(更高精度) |
| CCM 支援 | 固定矩陣(不公開) | 可調 3×3 + 分色溫 |
容易忽略的邊界案例:OV5640 的 AWB gains 是 R/G 和 B/G 的比值,而不是 R/B 的絕對值——所以「red_balance=140」在不同 OV 感測器上可能代表不同的絕對色彩。跨感測器比較時必須用色溫 K 值而非 gains 值。
| 症狀 | 可能原因 | 解決方案 |
|---|---|---|
| 灰卡校正後在另一光源仍偏色 | 每光源需一組 gains;不能複用 | 為每種光源各做一次灰卡校正,建立查表 |
| AWB gains 設定後被覆寫 | auto AWB 仍在跑 | 先關 auto_n_white_balance=0 |
| CCM 後白色仍偏色 | AWB 未先做好,CCM 無法補白平衡 | 順序:AWB → CCM |
| 混合色溫場景 AWB 抖動 | zone-based AWB 在邊緣震盪 | 增 zone 數量或調整 zone 權重 |
| OV5640 red_balance 設 128 但偏紅 | 128 不是中性值(因 OV 的 gains 定義) | 用灰卡實測中性值,非假設 128 |
把單元 11 的色彩知識做成完整的色彩校正資料庫專案:為多種光源建立「色溫 → AWB gains → CCM」查表,並驗證不同光源下的色準。這是產品「色彩一致性」的根基。
# 日光燈(4000K)、白熾燈(2700K)、陰天(6500K)、LED(5000K)、黃昏(3000K)
declare -A K=( [fluo]=4000 [tungsten]=2700 [cloudy]=6500 [led]=5000 [dusk]=3000 )#!/bin/bash
for src in fluo tungsten cloudy led dusk; do
v4l2-ctl -d /dev/v4l-subdev0 -c auto_exposure=0 -c exposure=2000
v4l2-ctl -d /dev/v4l-subdev0 -c auto_n_white_balance=0
v4l2-ctl -d /dev/video0 --set-fmt-video=pixelformat=YUYV \
--stream-mmap=3 --stream-count=1 --stream-to=gray_$src.yuv
# 手動調 red/blue_balance 直到灰卡 R≈G≈B
echo "$src: 記錄 red_balance / blue_balance"
donepython3 - <<'EOF'
import csv
rows = [('fluo',4000,135,105), ('tungsten',2700,160,85),
('cloudy',6500,120,135), ('led',5000,128,112), ('dusk',3000,150,95)]
with open('awb_lut.csv','w',newline='') as f:
w = csv.writer(f)
w.writerow(['light','kelvin','red_balance','blue_balance'])
w.writerows(rows)
print('已寫入 awb_lut.csv')
EOFpython3 - <<'EOF' # 用 ColorChecker 的 patch 平均色比對目標值,算 Delta E import numpy as np # measured = 讀色卡 patch 的 RGB;target = 標準值 # Delta E 計算(簡化):對每個 patch 比對 LAB 距離 print('目標:所有 patch 平均 Delta E < 8(Consumer)、< 5(工控)') EOF
awb_lut.csv 查表 + 各光源的 Delta E 報告。產品需要換光源或換場景時,直接查表套用,不必重新摸索——這是色彩一致性的核心資產。| 步驟 | 作法 | 通過判據 |
|---|---|---|
| 1. 灰卡校正 | 固定光源/曝光,調 gains 到 R=G=B | 灰卡中性 |
| 2. 色卡量測 | 拍 ColorChecker,比對 24 patches | 計算平均 Delta E |
| 3. AWB 一致性 | 同一光源重複 5 次校正 | gains 變異 < 5% |
| 4. 跨光源驗證 | 每光源套用各自查表 | 白平衡保持中性 |
| 5. CCM 收尾 | 固定光源解 CCM 矩陣 | 彩色彩差收斂 |
| 面向 | Orange Pi | RPi5 | Orin Nano | Thor |
|---|---|---|---|---|
| AWB | 感測器端 / 手動 gains | libcamera AWB | NVIDIA AWB | NVIDIA AWB |
| CCM | 後處理 OpenCV | libcamera tuning | NVIDIA tuning | NVIDIA tuning |
| gains 表示 | red_balance/blue_balance(整數) | ColourGains(浮點比值) | Argus 內部 | Argus 內部 |
| 混合色溫 | 後處理分區 CCM | libcamera 全局 | NVIDIA 分區 AWB | NVIDIA 分區 AWB |
| 色準工具 | 自寫 Python Delta E | libcamera 工具 | NVIDIA 工具 | NVIDIA 工具 |
- [ ] 我能完成多光源校正專案並產出 awb_lut.csv。 - [ ] 我能用灰卡校正並量化 Delta E。 - [ ] 我理解「AWB 讓白變白,CCM 讓彩色變準」的差異。 - [ ] 我已建立至少兩組色溫的 gains 查表。 - [ ] 我理解 Orange Pi 整數 gains 與 RPi5 浮點 gains 的換算。 - [ ] 我能處理混合色溫場景(分區 CCM 或接受限制)。
白平衡(AWB)與色彩校正(CCM)的「讀→改→寫→驗證」是色彩準確性的基礎:
| 步驟 | 暫存器/位址 | 位元欄位 | 操作 | 預期值 |
|---|---|---|---|---|
| 1. 開啟 AWB | 0x5183 | [0] AWB enable | i2cset -y 3 0x3c 0x5183 0x01 | AWB 開啟 |
| 2. 設定 red gain | 0x5185 | [7:0] red balance | i2cset -y 3 0x3c 0x5185 0x80 | 1.0×(128=1.0×) |
| 3. 設定 blue gain | 0x5187 | [7:0] blue balance | i2cset -y 3 0x3c 0x5187 0x80 | 1.0× |
| 4. 回讀驗證 | 0x5185 / 0x5187 | [7:0] | i2cget ... 0x5185; i2cget ... 0x5187 | 0x80 + 0x80 |
| 5. 開關 LSC | 0x5800 | [0] | i2cset ... 0x5800 0x00 | LSC off(測色彩) |
| 6. 取幀驗證 | v4l2-ctl | — | v4l2-ctl --stream-mmap=1 --stream-count=1 | 色彩平衡正確 |
決策樹 A:白平衡偏色
影像偏色? ├─ 偏暖(偏紅/橙)→ red gain 太高 │ ├─ 減少 0x5185 值 │ └─ 或增加 0x5187(blue gain) ├─ 偏冷(偏藍)→ blue gain 太高 │ ├─ 減少 0x5187 值 │ └─ 或增加 0x5185(red gain) ├─ 混合色溫 → AWB 無法單一值修正 │ └─ 接受限制或用分區 CCM └─ AWB 完全失效 → 0x5183=0x00 └─ 檢查 AWB enable 狀態
決策樹 B:色彩不準(Delta E 太大)
Delta E > 5? ├─ AWB 正常但色彩不準 → CCM 問題 │ ├─ 校正 CCM(用灰卡 + 色卡) │ └─ 檢查 0x5800~0x5830 的 CCM 係數 ├─ 特定色偏 → 該色域 CCM 係數錯誤 │ └─ 調整對應 CCM 係數 └─ 全域色偏 → AWB + CCM 都需校正 └─ 先校正 AWB(白變白),再校正 CCM(色變準)
| 步驟 | 指令 | 預期輸出 | 判讀標準 |
|---|---|---|---|
| 1. 用灰卡校正 | 拍攝 18% 灰卡 | 灰卡影像 | RGB 均值接近 |
| 2. 設定 AWB | i2cset ... 0x5185 0x80; i2cset ... 0x5187 0x80 | 1.0× gains | 灰卡偏中性 |
| 3. 量測 Delta E | Python script | Delta E 值 | < 3(良好) |
| 4. 調整 gains | 遞增/遞減 0x5185/0x5187 | 色彩變化 | 最小化 Delta E |
| 5. 建 LUT | 多光源測試 | awb_lut.csv | 至少兩組色溫 |
| 6. 驗證混合色溫 | 拍攝混合光源場景 | 影像 | 記錄局限 |
| 面向 | Orange Pi | RPi5 | Orin Nano | Thor | 推薦 |
|---|---|---|---|---|---|
| AWB 控制 | 0x5180~0x5190 | libcamera AWB | NVIDIA AWB | NVIDIA AWB | 底層→OV5640 |
| gains 表示 | 整數(128=1.0×) | 浮點比值 | NVIDIA 內部 | NVIDIA 內部 | 換算時注意定義 |
| CCM 控制 | 0x5800~0x5830 | ColourCorrection | NVIDIA CCM | NVIDIA CCM | 學習→OV5640 |
| 混合色溫 | 後處理分區 | libcamera 全局 | NVIDIA 分區 | NVIDIA 分區 | 接受局限 |
| 色準工具 | 自寫 Python | libcamera 工具 | NVIDIA 工具 | NVIDIA 工具 | 學習→Python |
- [ ] 完成「多光源校正專案」並產出 awb_lut.csv - [ ] 用灰卡校正並量化 Delta E - [ ] 理解「AWB 讓白變白,CCM 讓彩色變準」 - [ ] 建立至少兩組色溫的 gains 查表 - [ ] 換算 Orange Pi 整數 gains(128=1.0×)與 RPi5 浮點 gains - [ ] 處理混合色溫場景(分區 CCM 或接受限制) - [ ] 產出「色彩校正報告」(含 Delta E 數據) - [ ] 驗證 AWB 收斂後色彩穩定 - [ ] 用色卡(X-Rite)驗證校正效果 - [ ] 產出「AWB tuning log」(含 gains/Delta E/光源)