鏈路)
做圖像調優(yōu)的人多半被問過這個問題ISP 前處理和后處理到底怎么分有人說是去馬賽克之前和之后有人說是 RAW 域和 YUV 域還有人干脆按“修數(shù)據(jù)”和“調味道”來切。這個問題看著像概念辨析實際上它直接決定了你調參的順序、降噪放在哪一級、噪聲模型怎么建甚至決定了硬件模塊該切給 Sensor 端的片上 ISP 還是主控側的后處理單元。我做過幾年畫質調優(yōu)見過太多項目因為前后處理邊界沒理清前處理的黑電平?jīng)]扣干凈后處理把銳化和飽和度拉到爆最后畫面又臟又假回頭一看是鏈路位置放錯了。ISP 前處理指的是圖像信號從 Sensor 輸出之后、進入顏色重建和顯示映射之前這一段的校正類處理核心任務是把模擬世界帶進來的“臟數(shù)據(jù)”洗干凈、拉回線性可計算的物理量ISP 后處理則是圖像完成顏色重建之后面向人眼觀感和編碼需求的增強類處理核心任務是把“正確的圖”變成“好看的圖”。這篇文章會把這條鏈路從頭到尾拆一遍講清楚每個模塊為什么放在那個位置、放錯了會出什么問題附上可以直接抄的參數(shù)和代碼片段。不管你是剛接觸畫質調優(yōu)的新人還是已經(jīng)能獨立跑通一條 pipeline 的老手都能從里面找到能直接用的東西。1. ISP 流水線的整體認知與前后處理邊界先把整條鏈路鋪開看。光線穿過鏡頭、濾光片打到 Sensor 的像素陣列上光子轉電子、電子轉電壓、電壓經(jīng)模擬增益和 ADC 變成數(shù)字碼值這時候出來的是 Bayer 格式的 RAW 數(shù)據(jù)每個像素只有 R、G、B 中的一個分量。這份 RAW 數(shù)據(jù)就是 ISP 的輸入原料后面所有事情都是在這份原料上做文章。從 RAW 到最終能給人看的 YUV 或 RGB中間要經(jīng)過十幾級處理這十幾級怎么切分成前段和后段就是本文要討論的核心。1.1 從光子到像素一條完整鏈路的真實模樣我習慣把整條鏈路畫成四段光學段、感光段、校正段、呈現(xiàn)段。光學段是鏡頭和紅外截止濾光片負責把場景成像到 Sensor 面上感光段是像素陣列、模擬增益、ADC負責把光強轉成數(shù)字碼值校正段就是狹義的前處理包含黑電平校正、壞點修復、鏡頭陰影校正、Bayer 域降噪、白平衡增益、顏色校正矩陣呈現(xiàn)段就是狹義的后處理包含去馬賽克之后的伽馬、色調映射、色彩增強、銳化、局部對比度、縮放裁剪。這里有個容易被忽略的點去馬賽克本身不算前處理也不算后處理它是分界線。去馬賽克之前數(shù)據(jù)是每個像素單色的 CFA 排列處理手段必須考慮通道交錯的空間關系去馬賽克之后每個像素有了完整的 RGB 三元組處理手段就變成了常規(guī)的圖像處理。這條線一劃兩邊的算法工具箱完全不同所以很多團隊干脆把去馬賽克叫“中處理”前后各管一攤。實際項目里鏈路順序大致是這樣的RAW 輸入 → 黑電平校正 → 靜態(tài)壞點校正 → 鏡頭陰影校正 → 動態(tài)壞點校正 → Bayer 域降噪 → 白平衡增益 → 去馬賽克 → 顏色校正矩陣 → 伽馬 → 色彩增強 → 亮度銳化 → 色度降噪 → 縮放輸出。這個順序不是隨便排的每一步的位置都有物理依據(jù)后面幾節(jié)會逐個解釋。1.2 三種劃分口徑位置、數(shù)據(jù)域、職責我問過不少同行“你怎么分前后處理”答案基本落在三種口徑上每種都有道理但混著用就會吵架。第一種是按位置分Sensor 輸出到去馬賽克之前叫前處理去馬賽克之后到輸出叫后處理。這種分法最直觀工程上也好落地硬件模塊的邊界基本就是這么切的。第二種是按數(shù)據(jù)域分線性 RAW 域叫前處理非線性 RGB/YUV 域叫后處理。這種分法的依據(jù)是數(shù)學性質線性域里疊加、縮放、矩陣乘法都成立非線性域里這些運算的物理意義就變了。第三種是按職責分把數(shù)據(jù)修正確叫前處理把圖像調好看叫后處理。這種分法最貼近調優(yōu)的實際感受但邊界最模糊因為“正確”和“好看”之間有一大片灰色地帶比如飽和度增強到底算不算修正。劃分口徑分界線優(yōu)點容易踩的坑按位置去馬賽克硬件邊界清晰模塊歸屬明確忽略了數(shù)據(jù)域差異容易在線性域做非線性操作按數(shù)據(jù)域伽馬前后數(shù)學性質一致算法選擇有依據(jù)伽馬位置各家有差異跨方案對比會錯位按職責校正與增強貼近調優(yōu)直覺便于分工邊界模糊降噪、銳化這類模塊歸屬爭議大我個人的用法是對外溝通按位置說對內(nèi)調參按數(shù)據(jù)域想。跟硬件同事對齊模塊歸屬時用第一種自己坐下來調參數(shù)時用第二種。這樣既不吵架也不出錯。1.3 為什么這條邊界值得較真邊界不清的代價是很具體的。最典型的是降噪把降噪放在去馬賽克之后你會看到畫面里那些彩色的斑點因為去馬賽克把單通道噪聲插值成了三通道噪聲原本亮度上的隨機起伏變成了色度上的紅斑綠斑這時候再做降噪就得同時處理亮度噪聲和色度噪聲濾波器的設計難度翻倍還容易把細節(jié)一起抹掉。反過來在 Bayer 域做降噪噪聲還是單通道的隨機起伏用方向性濾波就能壓得很干凈代價是計算量集中在前段。第二個代價是噪聲模型失效。線性域里噪聲標準差和信號強度基本是線性關系你可以用一個簡單的斜率參數(shù)描述整個動態(tài)范圍的噪聲特性一旦過了伽馬信號被非線性拉伸暗部噪聲被顯著放大同一個噪聲模型在整個碼值范圍內(nèi)就不再適用。這時候降噪強度如果還按線性域的思路給暗部會糊成一片亮部又壓不住。第三個代價是調參順序混亂。我見過有人先調銳化再調黑電平結果黑電平一動整張圖的對比度基準變了銳化的過沖閾值全部失效前面調的幾百個參數(shù)白干。正確的順序必須是前處理全部鎖定之后再動后處理因為后處理的輸入分布依賴于前處理的輸出。提示接手一個陌生項目第一件事是把鏈路順序打印出來貼在顯示器邊上每調一個模塊就在上面標一下。鏈路順序錯了后面所有努力都是負收益。2. 前處理把模擬世界的臟數(shù)據(jù)洗成可用素材前處理這一段的核心目標只有一個把 Sensor 吐出來的數(shù)字碼值還原成和場景輻亮度成線性比例關系的物理量。聽起來簡單做起來要跟一堆非理想因素搏斗——暗電流、像素響應不一致、鏡頭漸暈、顏色串擾、隨機噪聲每一個都得單獨對付。這一段做得好不好決定了后面所有處理的天花板。2.1 黑電平校正與壞點修復先擦干凈桌子黑電平是 Sensor 在完全無光條件下輸出的碼值來源是像素的暗電流和讀出電路的偏置。它的存在意味著“0”這個碼值并不對應“0 光強”。如果不扣掉整張圖會蒙上一層灰暗部細節(jié)全被壓在這層灰里。更麻煩的是黑電平會隨模擬增益和溫度漂移高增益下可能從 64 LSB 漲到 200 LSB 以上??酆陔娖奖仨毞旁跀?shù)字增益之前。原因很簡單數(shù)字增益是一個乘法操作黑電平是加性偏置先乘后減和先減后乘的結果差了一個增益倍數(shù)。假設黑電平 64數(shù)字增益 4 倍先乘再減會得到 (x·4 - 64)而正確結果應該是 (x - 64)·4。前者在暗部會殘留 3 倍的黑電平殘余畫面發(fā)灰暗部細節(jié)全丟。實際工程里黑電平不是簡單減一個常數(shù)而是按增益檔位和溫度分檔查表。我通常用 OB光學黑區(qū)的平均值作為當前幀的黑電平估計再和標定表做融合防止 OB 區(qū)被污染時整幀偏色。import numpy as np def black_level_correction(raw, ob_pixels, bl_table, gain_idx, temp_idx): raw: 輸入 RAW 數(shù)據(jù)H x Wuint16 ob_pixels: OB 區(qū)像素用于實時估計 bl_table: 標定黑電平表 [gain_idx][temp_idx] - dict(r, gr, gb, b) bl_ref bl_table[gain_idx][temp_idx] # 用 OB 區(qū)實時估計與標定值做加權融合OB 異常時依賴標定 bl_est np.mean(ob_pixels) alpha 0.3 if abs(bl_est - bl_ref[gr]) 32 else 0.0 bl alpha * bl_est (1 - alpha) * bl_ref[gr] out raw.astype(np.int32) - int(round(bl)) return np.clip(out, 0, 65535).astype(np.uint16)壞點分靜態(tài)和動態(tài)兩類。靜態(tài)壞點出廠標定存一張坐標表上電時直接查表替換動態(tài)壞點靠鄰域判斷當前像素和上下左右四個同色像素的差值超過閾值就判定為壞點。動態(tài)壞點的閾值不能給太死高 ISO 下噪聲本身就大閾值太嚴會把正常噪聲當成壞點畫面會變得像塑料。注意動態(tài)壞點校正一定要放在降噪之前否則壞點會被降噪濾波器擴散成一片越濾越臟。這個順序我見過不止一個項目搞反。2.2 鏡頭陰影校正不均勻的光照得先攤平鏡頭陰影的來源是鏡頭的余弦四次方衰減和像素的斜入射響應表現(xiàn)是畫面四角發(fā)暗、邊緣色偏。校正方法是標定一張增益網(wǎng)格每個網(wǎng)格點上存 R、Gr、Gb、B 四個通道的增益系數(shù)運行時雙線性插值把邊緣乘回去。標定的網(wǎng)格密度我一般用 16×13 或者 32×32。密度太低插值誤差會在網(wǎng)格交界處形成可見的塊狀邊界密度太高存儲和插值開銷上去了收益不明顯。16×13 對大多數(shù)定焦鏡頭夠用廣角鏡頭建議 32×32。這里有個必須說清楚的點LSC 是乘法增益會同時放大信號和噪聲。畫面中心增益 1.0四角可能到 2.0 甚至 2.5也就是說邊緣的噪聲被放大了 2 倍以上。這意味著邊緣的信噪比天然比中心差降噪強度必須做空間自適應不能全圖一個參數(shù)。我調過的項目里邊緣彩色噪點壓不住八成是 LSC 增益給太猛加上降噪沒做空間加權。def lsc_apply(raw, mesh_gain, bayer_patternRGGB): mesh_gain: (h, w, 4) 的增益網(wǎng)格通道順序按 bayer_pattern 實際實現(xiàn)用雙線性插值這里給出最近鄰簡化版說明邏輯 H, W raw.shape gh, gw mesh_gain.shape[:2] out raw.astype(np.float32) for ch, (dy, dx) in enumerate([(0,0),(0,1),(1,0),(1,1)]): # 按通道切片僅處理對應相位 sub out[dy::2, dx::2] gain mesh_gain[:, :, ch] # 網(wǎng)格坐標映射到子圖坐標 ys np.linspace(0, gh - 1, sub.shape[0]) xs np.linspace(0, gw - 1, sub.shape[1]) gy, gx np.meshgrid(ys, xs, indexingij) y0, x0 np.floor(gy).astype(int), np.floor(gx).astype(int) y1, x1 np.minimum(y01, gh-1), np.minimum(x01, gw-1) fy, fx gy - y0, gx - x0 g (gain[y0,x0]*(1-fy)*(1-fx) gain[y0,x1]*(1-fy)*fx gain[y1,x0]*fy*(1-fx) gain[y1,x1]*fy*fx) out[dy::2, dx::2] sub * g return np.clip(out, 0, 65535).astype(np.uint16)LSC 還有一個隱蔽的坑增益網(wǎng)格和色溫綁定。不同色溫下鏡頭的透過率曲線不一樣同一張網(wǎng)格在暖光下可能把邊緣壓得剛好到了冷光下就過校正邊緣反而發(fā)亮發(fā)藍。正經(jīng)做法是按幾個色溫檔位分別標定運行時按 AWB 的估計結果插值選表。2.3 Bayer 域降噪為什么一定要在去馬賽克之前做這個問題我被問過很多次。答案在噪聲的統(tǒng)計特性里RAW 域的噪聲是單通道的每個像素只有一個觀測值噪聲表現(xiàn)為該像素相對真實值的隨機起伏空間上是各向同性的去馬賽克的過程本質是插值它把相鄰同色像素的觀測值加權組合噪聲也被一起插值結果是原本獨立的單通道噪聲變成了三個通道間相關的噪聲視覺上就是彩色斑點。彩色斑點的處理難度遠高于亮度噪聲因為人眼對色度噪聲的容忍度低但色度降噪又特別容易把顏色邊界糊掉導致高飽和區(qū)域出現(xiàn)顏色滲漏。所以工程上的標準做法是在 Bayer 域盡量把噪聲壓下去去馬賽克之后只做輕量的色度殘留清理。Bayer 域降噪的常用手段有三類。第一類是基于方向性的濾波先算水平和垂直方向的梯度沿梯度小的方向濾波避免跨越邊緣第二類是頻域方法小波變換后對高頻子帶做閾值收縮第三類是基于噪聲模型的自適應濾波把噪聲標準差作為濾波強度的輸入暗部強、亮部弱。我實測下來方向性濾波加噪聲模型自適應的組合性價比最高算力可控效果也穩(wěn)。def bayer_denoise(raw, noise_sigma, strength1.0): 簡化版基于同色鄰域的方向性加權平均 noise_sigma: 當前 ISO 下的噪聲標準差估計 kernel np.array([[0,1,0],[1,0,1],[0,1,0]], dtypenp.float32) # 同色鄰域均值Bayer 上同色像素間隔 2 neighbor_sum ( np.roll(raw, 2, axis0) np.roll(raw, -2, axis0) np.roll(raw, 2, axis1) np.roll(raw, -2, axis1) ).astype(np.float32) / 4.0 # 權重隨噪聲水平自適應噪聲大時更相信鄰域 w np.clip(noise_sigma / 32.0, 0.0, 0.9) * strength out raw.astype(np.float32) * (1 - w) neighbor_sum * w return np.clip(out, 0, 65535).astype(np.uint16)注意Bayer 域降噪的強度必須跟 ISO 聯(lián)動。固定強度在高 ISO 下壓不住在低 ISO 下又會把皮膚紋理磨成塑料。這個聯(lián)動曲線是畫質調優(yōu)里最需要手工打磨的部分之一。2.4 白平衡增益與顏色校正矩陣把顏色拉回真實白平衡增益是一個對角矩陣分別乘在 R、G、B 三個通道上把灰色物體在任意色溫光源下都拉成灰色。它在去馬賽克之前做因為 RAW 域的通道是分離的直接乘增益等價于在線性域縮放物理意義清晰。如果放到去馬賽克之后再做插值過程會引入通道間串擾增益就不再是純粹的對角操作了。顏色校正矩陣 CCM 是一個 3×3 矩陣作用是把 Sensor 的光譜響應映射到目標色彩空間。它必須放在去馬賽克之后、伽馬之前。放去馬賽克之前每個像素只有一個通道矩陣乘法無從談起放伽馬之后數(shù)據(jù)已經(jīng)非線性矩陣乘法會把中間調的色調拉歪出現(xiàn)明顯的偏色。CCM 有兩個硬性約束必須遵守。第一行列和歸一也就是每一行三個系數(shù)加起來等于 1保證灰色物體經(jīng)過矩陣變換后仍然是灰色否則整張圖會出現(xiàn)跟隨亮度的色偏。第二負系數(shù)要限幅矩陣里出現(xiàn)負值是正常的但過大的負系數(shù)會讓高飽和顏色出現(xiàn)異常工程上一般把單元素限制在 [-1, 2] 區(qū)間內(nèi)。def ccm_apply(rgb, ccm): rgb: H x W x 3, float32, 線性域 ccm: 3 x 3, 行和歸一 assert np.allclose(ccm.sum(axis1), 1.0, atol1e-3), CCM 行和必須歸一 out rgb ccm.T return np.clip(out, 0, 65535)CCM 會放大噪聲這是它的固有缺陷。矩陣元素越偏離單位陣噪聲放大倍數(shù)越高。工程上會計算 CCM 的噪聲放大系數(shù)作為后續(xù)降噪強度的輸入之一。我一般會在調 CCM 的時候同時看兩個指標色卡上的 ΔE 誤差和噪聲放大系數(shù)在兩者之間找平衡。追求極致色準把 CCM 拉得很猛噪點會明顯劣化用戶第一眼看到的往往是噪點而不是色準。2.5 前處理參數(shù)配置與實操心得前處理的參數(shù)多而雜我習慣把它們整理成一張表調參時按順序過一遍避免遺漏。模塊關鍵參數(shù)典型取值踩坑點黑電平校正分檔黑電平、OB 融合權重10bit 下 64~256 LSB放在數(shù)字增益之后做暗部發(fā)灰靜態(tài)壞點坐標表、替換策略出廠標定約 0.01% 像素表過期新批次 Sensor 漏檢鏡頭陰影網(wǎng)格密度、分色溫表16×13 或 32×32增益過大導致邊緣噪點爆炸Bayer 降噪噪聲模型斜率、強度曲線隨 ISO 分段強度固定不聯(lián)動高低 ISO 都不對白平衡R/B 增益、色溫插值增益范圍 0.5~4.0低色溫下增益飽和畫面偏黃CCM3×3 矩陣、行和歸一單元素 [-1, 2]行和不為 1灰階出現(xiàn)色偏我個人的調參順序是黑電平 → 靜態(tài)壞點 → LSC → 白平衡 → CCM → Bayer 降噪。降噪放最后是因為它的噪聲模型依賴前面所有模塊的輸出統(tǒng)計特性前面一變降噪?yún)?shù)就得重調。這個順序反過來做你會反復推翻自己的結論。3. 后處理從“正確”到“好看”的最后一公里前處理做完圖像在物理上是“正確”的灰是灰紅是紅暗部沒有偏置四角亮度均勻。但這張圖拿給人看多半會被評價“平淡”“發(fā)灰”“不夠銳”。原因在于人眼的感知特性和物理量的線性關系并不一致后處理要做的就是把線性數(shù)據(jù)映射到符合人眼預期的非線性空間再補上觀感層面的增強。3.1 伽馬與色調映射把線性數(shù)據(jù)裝進 8bit 的盒子里人眼對亮度的感知接近冪律關系大致是亮度的 2.2 次方根。而顯示設備和存儲格式普遍只有 8bit線性映射到 8bit 會讓暗部層次全部擠在少數(shù)幾個碼值里看上去就是一片死黑。伽馬校正把線性數(shù)據(jù)做一次冪運算把暗部的碼值拉開亮部壓縮讓 256 個碼值在人眼感知上均勻分布。伽馬的位置很關鍵。它標志著數(shù)據(jù)從線性域進入非線性域也就標志著一大堆線性運算不能再做了。矩陣乘法、疊加、縮放這些操作放在伽馬之后物理意義全部失效結果的色調會隨亮度漂移。所以我把伽馬叫做后處理的“門檻”跨過去之后工具箱就換了一套。實際實現(xiàn)上很少用純冪函數(shù)常用的是分段函數(shù)暗部用線性段保證黑位不糊中間調用冪函數(shù)亮部做軟截斷避免高光死白。這種分段做法在數(shù)學上稍微復雜但在暗部細節(jié)和噪聲控制上的收益很明顯。def gamma_encode(linear, gamma2.2, linear_seg0.0031308): 線性 RGB [0,1] 轉非線性帶線性段 out np.where( linear linear_seg, linear * 12.92, 1.055 * np.power(np.clip(linear, 1e-6, 1.0), 1/2.2) - 0.055 ) return np.clip(out, 0.0, 1.0)HDR 場景下還會疊加局部色調映射把高動態(tài)范圍壓縮到顯示設備能呈現(xiàn)的范圍內(nèi)。這一步的調參極其主觀壓得太狠畫面發(fā)灰失去層次壓得太松高光直接過曝。我一般用直方圖分布來判斷壓縮后中間調的直方圖應該是平緩的單峰如果出現(xiàn)明顯的雙峰或者長尾說明映射曲線有問題。3.2 銳化與邊緣增強在正確的域里做正確的事銳化的本質是提升高頻分量讓邊緣過渡變陡觀感上更清晰。經(jīng)典做法是 USM非銳化掩模先用低通濾波得到模糊圖原圖減模糊圖得到高頻細節(jié)再按系數(shù)加回原圖。銳化必須放在伽馬之后而且是只在亮度通道上做。放伽馬之前暗部的高頻被放大后經(jīng)過伽馬拉伸暗部會出現(xiàn)夸張的白邊在三通道上同時銳化色度邊緣會被強化出現(xiàn)彩色描邊。只在 Y 通道銳化色度保持平滑觀感最自然。def unsharp_mask(y, amount0.8, radius1.0, threshold2, clip_limit24): y: 亮度通道 [0,255] uint8 對過沖做限幅防止白邊黑邊 from scipy.ndimage import gaussian_filter yf y.astype(np.float32) blur gaussian_filter(yf, sigmaradius) detail yf - blur # 低于閾值的細節(jié)視為噪聲不銳化 detail np.where(np.abs(detail) threshold, 0, detail) out yf amount * detail # 過沖限幅相對原圖的偏差不超過 clip_limit out np.clip(out, yf - clip_limit, yf clip_limit) return np.clip(out, 0, 255).astype(np.uint8)過沖限幅這一步絕對不能省。沒有限幅的 USM 會在強邊緣兩側制造亮暗光暈畫面看起來像刻上去的放大看尤其明顯。限幅范圍我一般給 ±16 到 ±24 LSB具體看輸出分辨率小圖可以放寬一點大圖必須收緊。3.3 色彩增強與局部對比度調的是記憶色不是真實色色彩增強分兩部分飽和度提升和膚色保護。全局飽和度提升會等比放大所有顏色的飽和度高飽和區(qū)域容易溢出人眼最敏感的是膚色一旦膚色過飽和就會顯得油膩、發(fā)紅。做法是在飽和度曲線上做分區(qū)低飽和區(qū)域輕度提升高飽和區(qū)域抑制同時對膚色色相范圍做單獨的保護帶。膚色的保護范圍用 HSV 空間里的橢圓區(qū)域描述比較方便中心在色相約 25 度、飽和度 0.4 附近長短軸根據(jù)實際膚色標定。落在這個區(qū)域內(nèi)的像素飽和度增益強制壓到 1.0 以下。局部對比度提升是另一個大類本質是提升中頻分量讓畫面更有立體感。常用的方法是大尺度低通濾波得到基礎層原圖減基礎層得到細節(jié)層細節(jié)層乘系數(shù)后加回去。系數(shù)給大了畫面會出現(xiàn)明顯的halo尤其在天空和建筑的邊界處看起來像貼紙。這里有個心得局部對比度的強度要跟場景內(nèi)容聯(lián)動。人像場景要壓風景場景可以放文字場景要幾乎關掉。全局一個參數(shù)在多種場景下必然翻車。3.4 3A 與后處理的耦合關系3AAE、AWB、AF雖然是獨立模塊但和前后處理深度耦合。AE 決定了曝光時間和增益直接影響噪聲水平而噪聲水平是降噪強度和銳化強度的輸入AWB 決定了白平衡增益間接影響 CCM 的選擇和色彩增強的基準AF 的結果影響局部區(qū)域的清晰度評估銳化強度有時會依賴對焦區(qū)域的統(tǒng)計。我遇到過最典型的耦合問題是AE 在暗光下拉高增益噪聲暴增但降噪強度曲線是固定的結果畫面噪點明顯同時銳化強度也是固定的銳化把噪聲當細節(jié)放大畫面又臟又硬。解決辦法是讓降噪和銳化都讀取當前的 ISO 和曝光時間做動態(tài)曲線。這個聯(lián)動邏輯必須寫進調參規(guī)范里不能靠調參工程師憑感覺給。3.5 后處理順序的坑與量化誤差后處理的順序錯了問題比前處理更隱蔽因為圖像看起來“還行”只是不夠好。常見的錯誤順序是先銳化再降噪降噪會把銳化出來的細節(jié)一起磨掉銳化白做了先色彩增強再伽馬顏色會隨亮度漂移先縮放再銳化縮放把細節(jié)插值掉了銳化放大的是插值偽影。量化誤差是另一個隱蔽問題。8bit 輸出的碼值間隔是 1/255弱細節(jié)在量化時會被截斷尤其是在平坦區(qū)域會出現(xiàn)明顯的色階斷層banding。工程上的解法是在后處理內(nèi)部保持 10bit 或 12bit只在線路末端做 dithering 后降到 8bit。提示如果你看到畫面大面積平坦區(qū)域出現(xiàn)一圈一圈的色帶先別懷疑算法先檢查位寬在哪一級被降到了 8bit。4. 前后處理的分工原則與工程取舍知道了每個模塊該干什么接下來是更實際的問題這些模塊該切成什么樣交給誰做。同一個功能放在 Sensor 端的片上 ISP 里做和放在主控側的軟件后處理里做成本、延遲、畫質上限完全不同。這一節(jié)講取舍邏輯。4.1 線性域做事非線性域做感覺這是我總結的一條最簡原則。凡是涉及物理量運算的操作都放在線性域黑電平、增益、陰影校正、白平衡、顏色矩陣、降噪。凡是涉及人眼感知的操作都放在非線性域伽馬、銳化、飽和度、局部對比度。為什么要這么分因為線性域的運算滿足疊加原理你可以把多個模塊的傳遞函數(shù)合并成一個矩陣調試時可以單獨驗證每個模塊的增益出了問題容易定位。非線性域不滿足疊加原理模塊順序變了結果就變但人眼的感知恰恰是非線性的增強類操作只有放在非線性域才符合觀感預期。違反這條原則的典型例子是在線性域做銳化。線性域里暗部的絕對值很小同樣的高頻增量在暗部占比極大銳化后暗部會出現(xiàn)夸張的噪點和光暈亮部反而看不出效果。反過來在非線性域做增益暗部被伽馬拉伸后增益的放大倍數(shù)不一致整張圖的亮度關系會亂。4.2 位寬與精度預算每一步都在丟信息整條鏈路的位寬預算是必須提前規(guī)劃的。Sensor 輸出 10bit 或 12bit前處理內(nèi)部通常要擴到 14bit 或 16bit因為 LSC 增益和 CCM 都會放大數(shù)值范圍不給余量會溢出截斷。后處理在伽馬之前保持 12bit 以上伽馬之后可以降到 10bit最終輸出再降到 8bit。環(huán)節(jié)建議位寬理由Sensor 輸出10 / 12 bit硬件原生前處理內(nèi)部14 / 16 bit留出增益與矩陣放大的余量伽馬輸入12 bit 以上暗部拉伸后需要精度支撐伽馬輸出10 bit人眼分辨極限再高收益有限輸出 YUV8 bit dithering編碼與顯示標準位寬不夠的后果是截斷和量化噪聲。LSC 四角增益 2.5 倍時12bit 輸入乘以 2.5 需要至少 14bit 才不溢出CCM 的負系數(shù)會讓某些通道的中間結果超出輸入范圍同樣需要余量。我一般按“最大增益 × 輸入位寬 2bit 余量”來估算內(nèi)部位寬。4.3 硬件資源與延遲約束下的方案選擇片上 ISP 的優(yōu)勢是延遲低、功耗低、帶寬省缺點是算法固化、調參空間小、迭代周期長。軟件后處理的優(yōu)勢是靈活、可迭代、算法上限高缺點是延遲高、功耗大、占用主控算力。選擇依據(jù)主要是產(chǎn)品形態(tài)。對延遲敏感的場景比如車載倒車影像、工業(yè)檢測必須把關鍵處理放在片上 ISP保證端到端延遲在幾十毫秒以內(nèi)對畫質上限要求高、迭代頻繁的場景比如內(nèi)容創(chuàng)作設備可以把增強類后處理放到軟件側隨時更新算法。4.4 不同產(chǎn)品形態(tài)的分工方案對比產(chǎn)品形態(tài)前處理歸屬后處理歸屬關鍵約束移動終端片上 ISP片上 ISP 部分軟件增強功耗與發(fā)熱車載視覺片上 ISP 為主極簡保證實時延遲與可靠性安防監(jiān)控片上 ISP編碼前輕量增強低照度與帶寬機器視覺片上 ISP 做基礎校正主機側做測量相關處理精度與可復現(xiàn)性內(nèi)容創(chuàng)作設備片上 ISP 輸出 RAW主機側完整后處理畫質上限與靈活性機器視覺這一類特別值得說。它的目標和消費影像完全相反不需要“好看”需要“可測量”。所以它的后處理往往跳過伽馬、銳化、色彩增強這些觀感類模塊直接輸出線性數(shù)據(jù)做測量算法前處理的精度和可復現(xiàn)性反而要求極高。5. 實操搭一條最小可用的處理鏈路光講原理不夠我把一條能跑起來的最小鏈路寫出來你可以直接拿一份 RAW 數(shù)據(jù)跑通觀察每個模塊的效果。代碼用 Python 和 NumPy重點在流程和參數(shù)不在性能優(yōu)化。5.1 數(shù)據(jù)約定與輸入準備輸入假定是 12bit 打包的 Bayer RAW排列為 RGGB尺寸 1920×1080。讀取后先解包成 16bit 的二維數(shù)組再按 2×2 相位切出四個通道。import numpy as np def unpack_mipi_raw12(buf, width, height): 12bit 緊湊打包解包每 3 字節(jié)存 2 個像素 b np.frombuffer(buf, dtypenp.uint8) b b.reshape(-1, 3).astype(np.uint16) p0 (b[:, 0] 4) | (b[:, 1] 4) p1 ((b[:, 1] 0x0F) 8) | b[:, 2] inter np.empty(p0.size p1.size, dtypenp.uint16) inter[0::2] p0 inter[1::2] p1 return inter.reshape(height, width)數(shù)據(jù)準備好之后先做一次統(tǒng)計看全圖的最小值、最大值、均值以及 OB 區(qū)的均值。這一步能快速判斷黑電平是否異常、是否有過曝區(qū)域比直接跑算法有效得多。5.2 前處理鏈路的串接前處理按前面講的順序串起來黑電平 → 靜態(tài)壞點 → LSC → 白平衡 → Bayer 降噪 → 去馬賽克 → CCM。每一步的輸出都存一份中間結果方便對比。def preprocess_pipeline(raw, cfg): x black_level_correction(raw, cfg[ob], cfg[bl_table], cfg[gain_idx], cfg[temp_idx]) x static_dpc(x, cfg[dpc_map]) x lsc_apply(x, cfg[lsc_mesh]) x white_balance(x, cfg[wb_gain]) x bayer_denoise(x, cfg[noise_sigma], cfg[dn_strength]) rgb demosaic(x, patternRGGB) rgb ccm_apply(rgb, cfg[ccm]) return rgb去馬賽克這里我用的是邊緣自適應插值核心思路是對 G 通道先做方向判斷沿梯度小的方向插值R/B 通道再用 G 的結果做色差插值。這個實現(xiàn)比雙線性復雜一些但在高頻紋理區(qū)域的偽彩明顯更少值得多寫幾十行。中間結果一定要存圖看。我習慣把每個模塊的輸出都存成 PNG用同一塊區(qū)域做對比尤其是暗部區(qū)域和高頻紋理區(qū)域。數(shù)字指標只能告訴你“變了多少”看不出“變好還是變壞”。5.3 后處理鏈路的串接后處理從線性 RGB 開始伽馬 → 色彩增強 → 銳化 → 輸出。注意伽馬是分界線伽馬之前的數(shù)據(jù)還在線性域之后的數(shù)據(jù)是非線性域。def postprocess_pipeline(rgb_linear, cfg): rgb rgb_linear / cfg[white_level] rgb gamma_encode(rgb, gammacfg[gamma]) yuv rgb_to_yuv(rgb) yuv[..., 1:] chroma_denoise(yuv[..., 1:], cfg[chroma_dn]) yuv[..., 0] unsharp_mask(yuv[..., 0] * 255, amountcfg[sharp_amount], thresholdcfg[sharp_thr], clip_limitcfg[sharp_clip]) / 255 yuv[..., 1:] color_enhance(yuv[..., 1:], cfg[sat_curve], cfg[skin_protect]) return yuv_to_rgb(yuv)后處理的參數(shù)我建議分場景存幾套人像、風景、低照度、文檔切換時整組加載而不是單參數(shù)微調。單一場景下調得再好換場景必然出問題。5.4 調參順序與驗證方法調參順序必須嚴格從前到后黑電平 → 壞點 → LSC → 白平衡 → CCM → 降噪 → 伽馬 → 色彩 → 銳化。每次只動一個模塊改完立刻看中間結果確認無誤再進下一個。跳步調參是最常見的時間黑洞。驗證用標準圖卡最省事。灰卡驗證黑電平、白平衡和 CCM 的行和歸一色卡驗證色彩還原看 ΔE 的分布重點看膚色的幾個色塊噪聲圖卡驗證降噪強度用平坦區(qū)域的噪聲標準差量化分辨率卡驗證銳化和去馬賽克的細節(jié)保留看高頻區(qū)域的 MTF 響應。def evaluate(yuv_out, yuv_ref): 基礎指標噪聲標準差、亮度均值、色彩誤差 noise np.std(yuv_out[100:200, 100:200, 0]) # 平坦區(qū) luma np.mean(yuv_out[..., 0]) de np.mean(np.abs(yuv_out[..., 1:] - yuv_ref[..., 1:])) return {noise_std: float(noise), luma_mean: float(luma), delta_e: float(de)}我一般會盯住三個數(shù)字平坦區(qū)噪聲標準差、中間調亮度均值、色卡平均 ΔE。這三個數(shù)穩(wěn)住畫面基本不會有大問題剩下的就是主觀微調。6. 常見問題與排查實錄這一節(jié)是我這些年踩過的坑整理出來的速查表遇到問題先對著表掃一遍能省掉大量瞎猜的時間。6.1 典型現(xiàn)象速查現(xiàn)象可能原因排查路徑處理方向整圖發(fā)灰發(fā)白黑電平未扣或扣在增益之后查 OB 區(qū)均值與標定表差異把 BLC 移到數(shù)字增益前四角明顯發(fā)暗LSC 未生效或網(wǎng)格與色溫不匹配查網(wǎng)格增益是否為 1.0按色溫重新標定邊緣彩色噪點多LSC 增益過大 降噪無空間加權對比中心與邊緣噪聲標準差降 LSC 增益降噪做空間自適應灰階出現(xiàn)色偏CCM 行和不為 1計算矩陣行和歸一化后重新驗證強邊緣白邊黑邊銳化過沖無限幅查 USM 算子有無 clip加 ±16~24 LSB 限幅平坦區(qū)出現(xiàn)色帶位寬中途降到 8bit逐級打印位寬伽馬前保 12bit末端加 dithering暗部噪點隨亮度跳變降噪強度未與 ISO 聯(lián)動查 ISO 與強度曲線重做聯(lián)動曲線膚色發(fā)黃發(fā)紅缺膚色保護或 CCM 高飽和失準看膚色色相是否偏移加膚色保護帶高頻紋理出現(xiàn)偽彩去馬賽克插值策略過于簡單對比插值前后色度差換邊緣自適應插值高光區(qū)域死白伽馬高光段無軟截斷查高光碼值分布加高光 roll-off6.2 幾個不容易查出來的隱蔽問題第一個是OB 區(qū)污染。有些 Sensor 的 OB 區(qū)位置比較靠邊容易被漏光或者暗電流不均勻影響導致黑電平估計偏高或偏低整幀出現(xiàn)亮度漂移。排查方法是對比 OB 區(qū)均值和標定表偏差超過 32 LSB 就要警惕這時候應該降權實時估計、提高標定表權重。第二個是LSC 網(wǎng)格的坐標對齊。網(wǎng)格是按標定圖的分辨率生成的如果運行時改了裁剪窗口或者縮放比例網(wǎng)格坐標就會錯位表現(xiàn)是畫面出現(xiàn)斜向的明暗條紋。這個問題在切換分辨率時特別容易出現(xiàn)我一般會在分辨率切換的流程里強制重新加載網(wǎng)格。第三個是CCM 與 AWB 的時序錯配。AWB 收斂需要幾幀時間如果 CCM 立刻跟著 AWB 結果切換中間幾幀會出現(xiàn)明顯的色偏閃爍。工程上的做法是給 CCM 加切換遲滯色溫變化超過閾值才切換并且切換時做幾幀的線性插值過渡。第四個是銳化與縮放的順序。先縮放后銳化銳化放大的是插值偽影畫面會出現(xiàn)細密的振鈴先銳化后縮放銳化出來的高頻被縮放濾波抹掉銳化白做。正確順序是先縮放再銳化且銳化強度要按縮放比例調整縮小越多強度越小。注意這四類問題的共同點是“看指標正常、看畫面別扭”。所以永遠不要只看 PSNR 和 SSIM一定要肉眼過一遍尤其是暗部、膚色、高頻紋理、平坦?jié)u變這四個區(qū)域。6.3 幾條不太出現(xiàn)在文檔里的經(jīng)驗關于降噪我從來不相信“一個參數(shù)打天下”。降噪強度必須是 ISO、場景亮度、局部方差三個量的函數(shù)做成一維或二維查找表最省事。查表的時候注意邊界要做插值否則檔位切換會看到畫面噪點“跳變”。關于銳化銳化前的降噪比銳化本身重要。噪聲沒壓住就去銳化等于把噪點當細節(jié)放大效果適得其反。我調試的順序永遠是先把降噪調到“微微偏軟”再上銳化補回來這樣出來的畫面干凈且銳利。關于色卡驗證色卡上的 ΔE 平均值好看不代表沒問題一定要看分布。如果只有膚色的幾個色塊 ΔE 偏高說明 CCM 在膚色區(qū)域擬合不足需要單獨加權優(yōu)化。全局平均會掩蓋這種局部誤差。關于中間結果養(yǎng)成存中間圖的習慣每個模塊輸出都存一份命名帶上參數(shù)。出了問題回溯時能直接定位到是哪一版參數(shù)引起的。這個習慣救過我很多次尤其是在參數(shù)改了幾十輪之后。關于鏈路順序接手新項目先花半天把鏈路順序完整走一遍用一份標準 RAW 從頭跑到尾中間每一步都存圖。這半天看起來是浪費實際上能幫你省掉后面幾周的定位時間。我見過太多人上來就調參數(shù)調了兩周才發(fā)現(xiàn)黑電平位置不對。7. 寫在最后的個人體會前后處理的邊界說到底不是一個學術問題是一個工程協(xié)作問題。它的價值不在于定義多精確而在于能不能讓硬件、算法、調優(yōu)三撥人對同一件事有共同語言。我現(xiàn)在的做法很簡單鏈路圖上把去馬賽克畫成一條粗線左邊標“校正”右邊標“呈現(xiàn)”任何模塊要跨線移動都必須拿出噪聲模型或者主觀評測的數(shù)據(jù)來說服大家。這條規(guī)矩立起來之后項目里關于“這個模塊該誰做”的扯皮少了八成。另外一個體會是別把前處理當成“必須做完才能開始調后處理”的前置任務。實際項目里前后處理是反復迭代的后處理調出來發(fā)現(xiàn)暗部噪點壓不住回頭要改前處理的降噪強度前處理改了降噪后處理的銳化參數(shù)又得重調。真正高效的做法是把鏈路搭通先用一套保守參數(shù)跑通全流程再從前到后一輪一輪收緊每輪只動一到兩個模塊。這種滾雪球式的調法比“一次調好一個模塊再進下一個”更貼合真實項目的節(jié)奏因為畫質的各個維度本來就是互相牽制的。最后一個實用建議給你的鏈路寫一份參數(shù)說明文檔每個參數(shù)寫清楚物理含義、取值范圍、和哪些其他參數(shù)耦合。這份文檔在你自己隔了三個月再回頭看的時候價值會遠超你的預期。我吃過這個虧一套參數(shù)調完沒記錄兩個月后回來完全看不懂當時為什么這么給只能重調一遍。