後處理與調校流程
日期: ____ 場景: ____ 改動: 參數 A → B 原因: ____ 結果: ____(量化) 回歸: ____ ✓/✗ 附圖: ____
日期/場景/改動/原因/結果(量化)/回歸 ✓✗/附圖
「動態範圍」是場景最亮與最暗可同時保留的範圍。RAW 的 DR 上限 ≈ 20·log₁₀(FullWell / ReadNoise);輸出受限於位元深(10-bit → 60dB 的量化上限)。
| 技術 | 原理 | trade-off |
|---|---|---|
| 單幀 + tone mapping | 壓縮亮部曲線 | 犧牲對比,運動無假影 |
| DOL HDR(多幀) | 短/長曝光合併 | 動態範圍↑,但運動 ghost |
| Sensor HDR | 雙增益/雙曝光硬體 | 依感測器支援 |
判斷要不要 HDR:量場景「最亮/最暗 ROI 的 RAW 值」。若亮部已 1023、暗部仍貼近黑位,表示超過單幀 DR → 才考慮 HDR。若兩端都還在範圍內,HDR 只是徒增 ghost 風險。
python3 - <<'EOF'
import numpy as np
raw = np.fromfile('dark.raw', dtype=np.uint16).reshape(720,1280)
ob = np.median(raw[600:700, 400:880]) # 蓋鏡頭拍的暗幀
print('OB =', ob)
np.save('ob.npy', ob)
EOFpython3 - <<'EOF'
import numpy as np
raw = np.fromfile('shot.raw', dtype=np.uint16).reshape(720,1280)
ob = np.load('ob.npy')
flat = (raw - ob).clip(0) # 先減黑位
for name, sl in {'R':(slice(0,None,2),slice(0,None,2)),
'Gr':(slice(0,None,2),slice(1,None,2)),
'Gb':(slice(1,None,2),slice(0,None,2)),
'B':(slice(1,None,2),slice(1,None,2))}.items():
flat[sl] = flat[sl] * np.load(f'gain_{name}.npy')
flat.tofile('shot_lsc.raw')
EOF# 用灰卡定曝光,讓灰卡 Y ≈ 目標值;再調 red/blue gains
v4l2-ctl -d /dev/v4l-subdev0 -c auto_exposure=0 -c auto_n_white_balance=0
v4l2-ctl -d /dev/v4l-subdev0 -c exposure=2000
v4l2-ctl -d /dev/v4l-subdev0 -c red_balance=140 -c blue_balance=110# 後處理 NR(單元 13)+ 輕度 sharpen,同場景前後各一張
python3 - <<'EOF'
import cv2
img = cv2.imread('shot.png')
nr = cv2.fastNlMeansDenoisingColored(img, None, 6, 6, 7, 21)
sharp = cv2.addWeighted(nr, 1.0, cv2.GaussianBlur(nr,(0,0),1.5), -0.3, 0)
cv2.imwrite('shot_tuned.png', sharp)
EOF| 現象 | 方向 |
|---|---|
| 黑位減錯 → 畫面偏暗 | OB 在中間區算,別用邊角(shading) |
| LSC 後仍偏色 | 先確認 AWB 已做,LSC 與 AWB 順序別亂 |
| HDR 有 ghost | 場景有運動 → 改 tone mapping |
| 調一個參數全畫面變 | 參數間耦合 → 回歸對照組 |
| 無法重現上次結果 | 沒記錄 → 補齊 tuning log |
場景:Orange Pi 5 Plus(RK3588)接 OV5640,在高動態範圍場景(窗戶逆光)下比較 DOL HDR 與單幀 + tone mapping 的效果。
python3 - <<'EOF'
import numpy as np
raw = np.fromfile('scene.raw', dtype=np.uint16).reshape(1080, 1920)
bright = raw[100:200, 800:1120].mean() # 窗戶區
dark = raw[800:900, 800:1120].mean() # 室內暗區
dr_db = 20 * np.log10(bright / max(dark, 1))
print(f'DR = {dr_db:.1f} dB (10-bit 上限 ≈ 60 dB)')
EOF# 需要 OV5640 DOL 模式 + RK3588 ISP3 HDR 管線
media-ctl -d /dev/media0 -V "'rkisp-isp':0[hdr_mode=dol]"
v4l2-ctl -d /dev/video0 --stream-mmap=3 --stream-count=1 --stream-to=hdr.yuvpython3 - <<'EOF'
import cv2, numpy as np
img = cv2.imread('single_frame.png', 0).astype(float)
# 簡易 tone mapping: 雙曲正切壓縮
mapped = np.tanh(img / img.max() * 3) / np.tanh(3) * 255
cv2.imwrite('tonemapped.png', mapped.astype(np.uint8))
EOFRK3588 ISP3 內建硬體 tone mapping,但它不是「萬能的」:
| 技術 | 硬體支援? | 限制 |
|---|---|---|
| 單幀 tone mapping | ✅ ISP3 硬體 | 曲線固定;無法場景自適應 |
| DOL HDR | ✅ ISP3 + OV5640 DOL | 需要感測器驅動支援;有 ghost |
| Stagger HDR | ❌ OV5640 不支援 | 需更高端感測器 |
| 後處理 tone mapping | ⚠️ CPU/GPU | 可自適應;但非即時 |
容易忽略的邊界案例:ISP3 的 tone mapping 預設曲線是針對「平均場景」設計的——在極端逆光(如直視太陽)下,它可能把天空完全壓成灰色而失去所有高光細節。後處理 tone mapping 可以用場景自適應曲線(如 Reinhard/ACES)來改善,但需要先量測場景的 histogram。
| 症狀 | 可能原因 | 解決方案 |
|---|---|---|
| HDR 有 ghost(運動物體殘影) | DOL 多幀合併時物體移動 | 改用單幀 + tone mapping;或設 ghost reduction 參數 |
| ISP3 tone mapping 天空全灰 | 預設曲線不適合極端逆光 | 後處理用場景自適應曲線(Reinhard/ACES) |
| 調校順序錯(先 AWB 再 LSC) | LSC 色差污染白平衡基準 | 校正順序:黑位 → LSC → AWB/CCM |
| 每次改動無法重現 | 沒記錄 tuning log | 建立版本管理資料夾,每改動一參數就記錄 |
| 後處理 sharpen 後雜訊暴增 | sharp 強度過高放大雜訊 | 先 NR 再 sharpen;降低 sharp 強度 |
把單元 14 的工作流知識做成自動化調校專案:把「黑位 → LSC → AWB/CCM → 曝光 → NR/Sharpen」串成一支可重複執行的腳本,自動產出校正前後對比與 tuning log。這是把「會調」升級成「可重現調」的關鍵。
mkdir -p tuning_v1/{raw,params,out,log}
cd tuning_v1#!/bin/bash # 蓋鏡頭拍黑幀定黑位 v4l2-ctl -d /dev/video0 --set-fmt-video=pixelformat=SRGGB10 \ --stream-mmap=3 --stream-count=1 --stream-to=raw/dark.raw # 均勻亮場定 LSC v4l2-ctl -d /dev/video0 --stream-count=1 --stream-to=raw/flat.raw # 灰卡定 AWB + 曝光 v4l2-ctl -d /dev/video0 --set-fmt-video=pixelformat=YUYV \ --stream-count=1 --stream-to=raw/gray.yuv # 實拍場景(前後對比用) v4l2-ctl -d /dev/video0 --stream-count=1 --stream-to=raw/scene.yuv
python3 - <<'EOF' import numpy as np, cv2, json # 黑位 dark = np.fromfile('raw/dark.raw', dtype=np.uint16).reshape(720,1280) ob = np.median(dark[600:700,400:880]) # LSC(分通道增益圖) flat = np.fromfile('raw/flat.raw', dtype=np.uint16).reshape(720,1280) - ob gains = {} for name, sl in {'R':(slice(0,None,2),slice(0,None,2)), 'Gr':(slice(0,None,2),slice(1,None,2)), 'Gb':(slice(1,None,2),slice(0,None,2)), 'B':(slice(1,None,2),slice(1,None,2))}.items(): c = flat[sl].astype(float) gains[name] = cv2.GaussianBlur(c[100:260,200:440].mean()/(c+1e-6), (21,21), 0) # 儲存參數 np.save('params/ob.npy', ob) for k,v in gains.items(): np.save(f'params/gain_{k}.npy', v) json.dump({'ob': float(ob)}, open('params/meta.json','w')) print('校正參數已儲存:', json.load(open('params/meta.json'))) EOF
python3 - <<'EOF'
import numpy as np
raw = np.fromfile('raw/scene.raw', dtype=np.uint16).reshape(720,1280)
out = (raw - np.load('params/ob.npy')).clip(0).astype(float)
for name, sl in {'R':(slice(0,None,2),slice(0,None,2)),
'Gr':(slice(0,None,2),slice(1,None,2)),
'Gb':(slice(1,None,2),slice(0,None,2)),
'B':(slice(1,None,2),slice(1,None,2))}.items():
out[sl] *= np.load(f'params/gain_{name}.npy')
np.clip(out,0,1023).astype(np.uint16).tofile('out/scene_tuned.raw')
print('校正完成,log 寫入 log/tuning_v1.txt')
EOFtune.sh + 專案目錄結構(raw/params/out/log),任何時刻都能重跑並產出可比較的結果。調校從「手動藝術」變成「可重現工程」——這是團隊協作的基礎。| 步驟 | 作法 | 通過判據 |
|---|---|---|
| 1. 校正前後對比 | 同一場景前後各存一張 | 畫質指標改善(亮度/色準/SNR) |
| 2. 中性灰檢查 | 灰卡 ROI 的 R=G=B | 色偏 < 5% |
| 3. 曝光基準 | 灰卡 Y 達目標 | Y 落在目標區間 |
| 4. 動態範圍檢查 | 亮/暗 ROI 未爆 | 亮部 < 1023、暗部 > 黑位 |
| 5. 場景回歸 | 室內/日光/逆光各測 | 無新缺陷(過曝/偏色/雜訊) |
| 6. 可重現性 | 重跑 tune.sh | 輸出一致(無隨機差異) |
| 面向 | Orange Pi | RPi5 | Orin Nano | Thor |
|---|---|---|---|---|
| 校正層級 | RAW/後處理為主 | libcamera tuning | NVIDIA tuning | NVIDIA tuning |
| HDR | OV5640 DOL(依驅動) | libcamera HDR | NVIDIA HDR | NVIDIA HDR |
| 參數管理 | 自建 JSON/CSV | libcamera tuning file | NVIDIA 工具 | NVIDIA 工具 |
| 回歸工具 | 自寫腳本 | libcamera test | NVIDIA test | NVIDIA test |
| 自動化程度 | 完全自建 | 中(半自動) | 高(封閉) | 高(封閉) |
- [ ] 我能完成 tune.sh 自動化調校專案並產出 log。 - [ ] 我已建立 raw/params/out/log 的專案結構。 - [ ] 我遵守「黑位→LSC→AWB/CCM→曝光→美化」的順序。 - [ ] 我能量測場景動態範圍並判斷要不要 HDR。 - [ ] 我已跑過至少三個場景的回歸並記錄結果。 - [ ] 我理解「可重現調校 = 完整 log + 版本管理」。
清晰度/HDR/調校工作流的「讀→改→寫→驗證」是影像品質的關鍵:
| 步驟 | 暫存器/位址 | 位元欄位 | 操作 | 預期值 |
|---|---|---|---|---|
| 1. 開啟 sharpen | 0x5308 | [0] sharpen enable | i2cset -y 3 0x3c 0x5308 0x01 | sharpen 開啟 |
| 2. 設定 sharpen 強度 | 0x5309 | [7:0] strength | i2cset ... 0x5309 0x20 | 依需求 |
| 3. 開啟 gamma | 0x5300 | [0] gamma enable | i2cset ... 0x5300 0x01 | gamma 開啟 |
| 4. 設定 contrast | 0x5301 | [7:0] contrast | i2cset ... 0x5301 0x20 | 依需求 |
| 5. 回讀驗證 | 0x5308 / 0x5309 | [7:0] | i2cget ... 0x5308; i2cget ... 0x5309 | 兩者皆正確 |
| 6. 取幀對比 | v4l2-ctl | — | 拍兩幀(sharpen on/off) | 清晰度改善 |
決策樹 A:影像模糊
影像模糊? ├─ sharpen 未開啟 → 0x5308=0x01 ├─ sharpen 強度不足 → 增加 0x5309 值 ├─ NR 抹掉細節 → 減少 NR 強度 └─ 對焦問題 → 物理對焦(不是 ISP 問題)
決策樹 B:HDR 效果不明顯
HDR 效果不明顯? ├─ 場景動態範圍不夠大 │ └─ 測試高對比場景(逆光/窗戶) ├─ 曝光包圍不夠 → 增加 EV 檔數 │ └─ 嘗試 -2EV/0EV/+2EV ├─ 合成方法問題 → 換 HDR 合成算法 └─ OV5640 DOL HDR 未啟用 └─ 檢查 DT 與驅動支援
| 步驟 | 指令 | 預期輸出 | 判讀標準 |
|---|---|---|---|
| 1. 關閉 sharpen | i2cset ... 0x5308 0x00 | 無 sharpen | 基準清晰度 |
| 2. 開啟 sharpen | i2cset ... 0x5308 0x01; i2cset ... 0x5309 0x20 | sharpen on | 清晰度改善 |
| 3. 量測 MTF | Python script(邊緣擴散) | MTF 值 | sharpen 後提升 |
| 4. 測動態範圍 | 灰階圖 + Python | DR 值(EV) | 依場景 |
| 5. 測 HDR | 高對比場景 | HDR 影像 | 亮暗細節保留 |
| 6. 測調校工作流 | 照順序執行五步 | 最終影像 | 品質最佳 |
| 面向 | Orange Pi | RPi5 | Orin Nano | Thor | 推薦 |
|---|---|---|---|---|---|
| Sharpen | OV5640 內建 | ISP 內建 | NVIDIA sharpen | NVIDIA sharpen | 學習→OV5640 |
| HDR | OV5640 DOL(有限) | ISP HDR | NVIDIA HDR | NVIDIA HDR | 量產→NVIDIA |
| 調校工具 | 自建 JSON/CSV | libcamera tuning | NVIDIA 工具 | NVIDIA 工具 | 底層→自建 |
| 回歸工具 | 自寫腳本 | libcamera test | NVIDIA test | NVIDIA test | 底層→自寫 |
| 自動化 | 完全自建 | 半自動 | 高(封閉) | 高(封閉) | 學習→自建 |
- [ ] 完成「tune.sh 自動化調校專案」並產出 log - [ ] 建立 raw/params/out/log 專案結構 - [ ] 遵守「黑位→LSC→AWB/CCM→曝光→美化」的順序 - [ ] 量測場景動態範圍並判斷要不要 HDR - [ ] 跑過至少三個場景的回歸並記錄結果 - [ ] 驗證「可重現調校 = 完整 log + 版本管理」 - [ ] 測試 sharpen 對清晰度的影響(含雜訊副作用) - [ ] 測試 contrast 對動態範圍的影響 - [ ] 產出「tuning workflow report」(含五步序列/回歸/品質) - [ ] 用「先 RAW 對、再後處理」原則驗證