昨天算到 30 維光譜投影到 3 個通道、27 維核空間永久遺失就停住了,但那句話只講完一半:遺失的部分回不來是真的,活下來的那 3 個數字後來發生什麼事,昨天完全沒交代。今天補這個洞。從感光元件讀出的 12 或 14 bit 整數,到你螢幕上那顆 sRGB 像素之間,至少經過一次去馬賽克插值、一次對角增益、兩次 3×3 矩陣、一次色適應、一次 2.5D 查表和一次轉換曲線。每一步都由不同的人定義,而且其中有幾步是故意不準的。這篇要做的事是把 Adobe DNG 規格第五、六章的每一個矩陣拆開,用一台真實相機(Canon EOS 5D Mark II)的實際係數逐步算一遍,然後證明一件反直覺的事:同一個 DNG 檔、同一個 profile、兩個都完全合規的解檔程式,輸出色差可以到 ΔE*ab 3.5。
📖 學
權威在哪裡:DNG 規格的章節地圖
先把座標定好。Adobe《Digital Negative (DNG) Specification》1.4.0.0 版的目錄結構是這樣的:第五章 “Mapping Raw Values to Linear Reference Values” 從第 77 頁開始,底下四節分別是 Linearization(p.77)、Black Subtraction(p.77)、Rescaling(p.77)、Clipping(p.78)。第六章 “Mapping Camera Color Space to CIE XYZ Space” 從第 79 頁開始,節次是 Camera Calibration Matrices(p.79)、One or Two Color Calibrations(p.79)、Definitions used in the following sections(p.80)、Translating White Balance xy Coordinates to Camera Neutral Coordinates(p.80)、Translating Camera Neutral Coordinates to White Balance xy Coordinates(p.80)、Camera to XYZ (D50) Transform(p.81)、“If the ForwardMatrix tags are not included in the camera profile”(p.81)、“If the ForwardMatrix tags are included in the camera profile”(p.82)、Applying the Hue/Saturation/Value Mapping Table(p.82)。第七章是 Opcode List Processing(p.83),裡面才是 WarpRectilinear、FixVignetteRadial、GainMap 那些鏡頭與感光元件修正。
到了 1.6.0.0(2021 年 12 月)和 1.7.1.0(2023 年 9 月),第六章的頁碼移到第 85 頁,內容也長出新東西:1.6.0.0 加了第三顆校正光源的整組標籤(CalibrationIlluminant3、ColorMatrix3、ForwardMatrix3、ProfileHueSatMapData3、ReductionMatrix3),並且允許用自訂光源資料而不只是 EXIF 光源列舉值;1.7.0.0 加了 ProfileDynamicRange 和 ProfileGroupName,前者是為了讓 profile 宣告自己是 HDR profile。
更正 1:DNG 規格裡沒有「去馬賽克」這一章
把 1.4 版的目錄從頭掃到尾——第一章 Introduction(p.11)、第二章 DNG Format Overview(p.13)、第三章 Restrictions and Extensions to Existing TIFF Tags(p.18)、第四章 DNG Tags(p.22)、第五章、第六章、第七章——七章加附錄,沒有任何一節在講怎麼把 Bayer 馬賽克插成三通道影像。
這不是疏漏,是刻意的。DNG 規範的是檔案裡有什麼、每個數字是什麼意思、怎麼把它們變成 XYZ,不規範怎麼從稀疏採樣重建密集影像。換句話說,規格保證了「色彩」的可重現性,完全放生了「解析度與細節」的可重現性。這是為什麼同一個 raw 檔在 Lightroom、Capture One、RawTherapee、DxO 裡的顏色可以吵得起來、但吵得有邊界,而細節與偽色卻可以差到像不同台相機拍的——後者根本沒有共同規格可依。
這件事對後面所有討論都有影響,因為去馬賽克是色彩管線的第一步,而且它已經注入了跨通道的統計假設。矩陣乘法之前的那個插值,不是中立的。
Bayer CFA:一個結構性欠定的重建問題
Bayer 圖案(Bayer 的美國專利 3971065,1976)的幾何是:綠色以 quincunx 格佔滿一半的像素,紅藍各佔四分之一,圖案在「紅列」(R, G, R, G, …)和「藍列」(G, B, G, B, …)之間交替。一顆 2400 萬像素的感光元件,實際上有 1200 萬個綠樣本、600 萬個紅樣本、600 萬個藍樣本,而輸出要求 2400 萬 × 3 = 7200 萬個數字。你要從 2400 萬個數字生出 7200 萬個,缺的 4800 萬全是猜的。
最笨的猜法是雙線性:綠色在紅/藍位置取四個軸向鄰居平均,紅藍在綠位置取四個對角鄰居平均。三個通道各自獨立插值,結果是邊緣附近三個通道的錯位互相打架,產生強烈的色彩失真和拉鍊狀偽影(zipper artifact)。
Malvar、He、Cutler 在 2004 年的 ICASSP 論文 “High-quality linear interpolation for demosaicing of Bayer-patterned color images” 提出的方法,沿用了 Pei 與 Tam(ICIP 2000)的跨通道拉普拉斯修正,做法極簡:在雙線性的結果上加一個別的通道的拉普拉斯項。
紅色位置的綠色估計是
Ĝ(i,j) = Ĝ_bl(i,j) + α·ΔR(i,j)
其中 ΔR 是紅色通道的 5 點離散拉普拉斯:
ΔR(i,j) = R(i,j) − ¼[R(i−2,j) + R(i+2,j) + R(i,j−2) + R(i,j+2)]
綠色位置的紅色估計用綠色通道的 9 點拉普拉斯乘 β,藍色位置的紅色估計用藍色通道的 5 點拉普拉斯乘 γ。三個權重是在 Kodak 標準測試集上最小化均方誤差求出來、再捨入到 dyadic rational 的:
α = 1/2, β = 5/8, γ = 3/4
捨入到二進位分數不是偷懶,是為了讓整個濾波器能用整數運算加位移實作。以「紅列中綠色位置估紅色」為例,IPOL 版本給出的整數實作是:
R(i,j) = ( F(i,j−2) + F(i,j+2)
− 2[F(i−1,j−1) + F(i+1,j−1) + F(i−2,j) + F(i+2,j) + F(i−1,j+1) + F(i+1,j+1)]
+ 8[F(i−1,j) + F(i+1,j)]
+ 10·F(i,j) ) / 16
除以 16 就是右移 4 位。整個去馬賽克是八組 5×5 線性濾波器的卷積,沒有分支、沒有梯度判斷、可以完全向量化。
效果呢?原論文報告相對雙線性 PSNR 提升超過 5.5 dB,對 R 和 B 通道相對前一代線性插值器再好約 0.7 dB,而且這組 5×5 濾波器在均方誤差上逼近同樣 5×5 支撐下最佳 Wiener 濾波器的 5% 以內。
更正 2:PSNR 不是去馬賽克演算法的排序依據
Getreuer 在 IPOL(Image Processing On Line, vol. 1, 2011, pp. 83–89)的複現文章裡,同時列了同一張 Kodak 測試圖上多個演算法的 PSNR:
| 演算法 | 圖 A 的 PSNR (dB) | 圖 B 的 PSNR (dB) |
|---|---|---|
| Bilinear | 29.47 | 25.61 |
| Gunturk et al. (2002) | 26.98 | — |
| Li (2005) | 23.58 | — |
| Hamilton-Adams (1997) | 29.17 | 31.62 |
| Zhang-Wu (2005) | 28.94 | — |
| Buades et al. (2011) | 29.11 | — |
| Malvar-He-Cutler (2004) | 29.66 | 31.15 |
兩件事同時成立。第一,在圖 A,MHC(29.66)贏過所有非線性方法,包括 Hamilton-Adams(29.17)、Zhang-Wu(28.94)、Buades(29.11);在圖 B,MHC(31.15)反而輸給 Hamilton-Adams(31.62)。排序會隨圖片翻轉。第二,更誇張的是 Li(2005)在圖 A 只拿到 23.58 dB,比雙線性的 29.47 還低了將近 6 dB——一個 2005 年發表在 IEEE TIP 的方法,在這張圖上遠遜於最原始的雙線性。
這代表 PSNR 在去馬賽克這個題目上幾乎沒有排序能力。它對「整張圖平均起來偏了多少」敏感,對「邊緣附近的假色有多刺眼」不敏感,而後者才是人眼會抱怨的。Getreuer 自己在文中的判斷是:MHC 視覺上比雙線性銳,但沒有 Hamilton-Adams 銳,因為 Hamilton-Adams 用的是非線性(方向自適應)插值策略。線性方法的天花板就在那裡。
更重要的是,MHC 的核心假設不是「綠色可以被鄰居的綠色預測」,而是「R−G 與 B−G 的差值在局部是低頻的」。跨通道拉普拉斯項本質上是在說:別的通道的高頻結構,可以借來當這個通道的高頻先驗。這個假設在灰階與自然材質上非常好用,在強色邊(綠底上的紅字、藍天與白雲的交界)會崩掉,崩掉的形式就是假色與拉鍊。也就是說,你送進色彩矩陣的那個「相機 RGB」,已經是一個帶著跨通道統計假設的估計值,不是量測值。這一點在後面談 ΔE 時很重要:再精確的矩陣,乘的都是一個已經有偏的輸入。
白平衡增益:它存的不是增益
DNG 的 AsShotNeutral 標籤,規格的定義是「拍攝當下選定的白平衡,編碼為一個完美中性色在線性參考空間中的座標」。注意這句話:它存的是中性色長什麼樣,不是你要乘多少。增益是它的倒數。
用真實數字走一遍。colour-hdri 的 DNG 模組文件裡用的是 Canon EOS 5D Mark II 的矩陣。給定白點 xy = (0.32816244, 0.34698169),文件的 doctest 輸出的 camera neutral 是
(0.4130699, 1.0000000, 0.6464650)
我用同一組矩陣自己算過,得到 (0.413076, 1.000000, 0.646461),差在小數第五位,來自 CCT 估算法不同(我用 McCamy 近似,colour-hdri 用 Robertson 1968)。所以白平衡增益是
| 通道 | camera neutral | 增益 = 1/neutral |
|---|---|---|
| R | 0.4130699 | 2.4209 |
| G | 1.0000000 | 1.0000 |
| B | 0.6464650 | 1.5469 |
也就是說,這台相機在這個光源下,紅色濾片只收到綠色的 41.3% 訊號,藍色只收到 64.6%。
更正 3:白平衡增益「放大雜訊」是半句真話
常見說法是「白平衡把紅色乘 2.42,所以紅色的雜訊也被放大 2.42 倍,所以藍紅通道比較髒」。前半句對,結論的因果錯了。
散粒雜訊的標準差是 √N(N 為光電子數),訊噪比 SNR = N/√N = √N。紅通道收到 0.4131 倍的光子,所以
SNR_R / SNR_G = √0.4131 = 0.6427
SNR_B / SNR_G = √0.6465 = 0.8040
紅通道的訊噪比只有綠通道的 64.3%,藍通道 80.4%。這個差距在光子打到 CFA 濾片的瞬間就決定了。
接下來乘上 2.4209 這個純量:訊號變 2.4209 倍,雜訊標準差也變 2.4209 倍,SNR 完全不變。乘一個常數不會改變 SNR。所以白平衡增益既沒有製造這個差距,也沒有惡化它,它只是把一個本來就存在的差距,原封不動搬到白平衡後的域裡,讓你在同一個亮度尺度上並排看見它。真正的元兇是濾片的透射率與光源的光譜——這正好接回昨天那條 E_c(φ) = ∫∫ S(λ,t)·ρ(λ)·R_c(λ) dλ dt:R 通道的積分值天生就比較小。
順帶一提,DNG 還有一個容易搞混的 AnalogBalance 標籤。規格的說法是:通常存下來的 raw 值不做白平衡,因為任何數位白平衡都會在使用者事後調整時損失動態範圍;但如果相機硬體有能力在數位化之前做通道平衡,那會改善最終影像的動態範圍。AnalogBalance 存的就是「已經被套用在存下來的 raw 值上的增益」,規格明確標註建議是類比增益,不建議是數位增益。它在後面的矩陣鏈裡是一個對角矩陣 AB。
三個矩陣的分工:方向是反的
這是整篇最容易搞錯的地方。DNG 第六章定義的幾個矩陣,方向並不一致:
| 標籤 | 方向 | 輸入是否已白平衡 | 輸出白點 |
|---|---|---|---|
ColorMatrix1/2/3 | XYZ → 參考相機原生空間 | 否 | 校正光源的白點 |
CameraCalibration1/2/3 | 參考相機原生 → 個體相機原生 | — | — |
AnalogBalance | 對角,個體通道增益 | — | — |
ForwardMatrix1/2/3 | 白平衡後的相機空間 → XYZ D50 | 是 | 固定 D50 |
ReductionMatrix1/2/3 | 降維第一階段(僅 ColorPlanes > 3) | — | — |
ColorMatrix 是從 XYZ 走向相機,ForwardMatrix 是從相機走向 XYZ。這兩個不是彼此的反矩陣,連定義域都不同——ColorMatrix 吃的是未白平衡的資料(白平衡資訊被烘在矩陣裡),ForwardMatrix 吃的是已經白平衡好的資料,而且輸出的白點永遠釘死在 D50。
更正 4:ColorMatrix 不是「你這台相機的色彩矩陣」
規格對 CameraCalibration 的定義寫得很清楚,它「定義一個把參考相機原生空間值轉換到個體相機原生空間值的校正矩陣,在第一顆校正光源下」。而它為什麼要跟 ColorMatrix 分開存?規格自己給了理由:為了讓 raw converter 可以根據 UniqueCameraModel 標籤抽換掉色彩矩陣,同時仍然享用相機廠做的個體校正。
所以正確的心智模型是:ColorMatrix 描述的是「這個型號的參考機」,CameraCalibration 描述的是「你手上這一台相對於參考機的偏差」。前者可以被第三方 profile 整個換掉,後者是相機出廠時燒進去的。
而且這個抽換有防呆。DNG 1.2.0.0 以後允許不同公司用不同的參考相機來製作校正標籤,所以規格規定:CameraCalibration1/2 只有在檔案的 CameraCalibrationSignature 字串完全等於所選 profile 的 ProfileCalibrationSignature 時才可以用;不相等就必須改用單位矩陣,以最小化最壞情況下的校正錯配誤差。dng_sdk 的 dng_color_spec 建構子裡就是這樣寫的:先 SetIdentity,只有簽章比對通過才把真值填進去。
這也是為什麼你用第三方 DCP profile 時,相機的個體校正通常直接被丟掉——不是軟體偷懶,是規格要求的。
內插發生在 mired 上,而且是一個雞生蛋問題
一顆 profile 通常有兩組矩陣,對應兩個校正光源(最常見是 CIE 標準光源 A 的 2856 K 附近,和 D65 的 6500 K)。實際場景色溫落在中間時要內插,而內插的自變數不是色溫,是色溫的倒數(mired,微倒數度)。
dng_sdk 的 FindXYZtoCamera 寫得很直白:
if T <= T1: g = 1.0
else if T >= T2: g = 0.0
else: g = (1/T − 1/T2) / (1/T1 − 1/T2)
colorMatrix = g·ColorMatrix1 + (1−g)·ColorMatrix2
用 5D2 的實際數字算一次。T1 = 2850 K、T2 = 6500 K、目標 T = 5000 K:
1/T = 200.0000 mired
1/T1 = 350.8772 mired
1/T2 = 153.8462 mired
g = (200.0000 − 153.8462) / (350.8772 − 153.8462)
= 46.1538 / 197.0310
= 0.2342466
注意這個數字:5000 K 在 2850–6500 K 之間線性看應該是 0.4110,在 mired 上卻只有 0.2342。差很多。用 mired 是對的,因為人眼與相機對色溫的反應在倒數尺度上才接近線性——這也是為什麼相機的白平衡微調刻度是 mired。
代進去:
CM[0][0] = 0.2342466 × 0.5309 + 0.7657534 × 0.4716
= 0.1243636 + 0.3611272
= 0.4854908
完整的內插結果是
[[ 0.4854908, 0.0408107, -0.0714282],
[-0.7433278, 1.4956549, 0.2680749],
[-0.1336947, 0.1767874, 0.6654045]]
和 colour-hdri 文件裡 matrix_interpolated(5000, 2850, 6500, M_T, M_R) 的 doctest 輸出到小數第七位完全一致。
然後是雞生蛋:要選矩陣需要色溫,要算色溫需要白點的 xy,而 xy 是從 camera neutral 反推的,反推又需要矩陣。
DNG 第六章有一整節在處理這件事(“Translating Camera Neutral Coordinates to White Balance xy Coordinates”,1.4 版 p.80)。dng_sdk 的 NeutralToXY 用的是不動點迭代:
last = D50
for pass in 0 .. 29: // kMaxPasses = 30
xyzToCamera = FindXYZtoCamera(last)
next = XYZtoXY( Invert(xyzToCamera) · neutral )
if |next.x − last.x| + |next.y − last.y| < 1e-7: return next
if pass == 29: // 沒收斂
next = (last + next) / 2 // 取平均,放棄
last = next
程式碼裡的註解很誠實:「如果到達上限還沒收斂,我們多半是在兩個值之間震盪。所以取最後兩次估計的平均然後放棄。」這是一個可能不收斂的演算法,而且規格認可這個放棄行為。
更正 5:同一個 AsShotNeutral,不同 profile 會得到不同的色溫
因為色溫是從 profile 自己的矩陣反推出來的,而不是從檔案裡讀出來的。當你在 Lightroom 裡切換 profile,滑桿上的色溫數字會跳——不是軟體 bug,是 NeutralToXY 用了不同的矩陣重新迭代。
反過來也一樣:兩台相機拍同一個場景,AsShotNeutral 的三個數字完全不同(因為 CFA 不同),但反推出來的 xy 應該接近。DNG 的設計就是讓「白點」這個場景屬性成為可攜的,「camera neutral」這個裝置屬性不可攜。
ForwardMatrix 路徑:色適應藏在一個對角矩陣裡
第六章 p.82 那一節(“If the ForwardMatrix tags are included in the camera profile”)的三行公式:
ReferenceNeutral = Inverse(AB · CC) · CameraNeutral
D = Invert( AsDiagonalMatrix(ReferenceNeutral) )
CameraToXYZ_D50 = FM · D · Inverse(AB · CC)
用 5D2 走一次。假設 AnalogBalance 是單位向量、CameraCalibration 是單位矩陣(第三方 profile 的常態),那麼 ReferenceNeutral 就等於 CameraNeutral = (0.4130699, 1, 0.646465),於是
D = diag( 1/0.4130699, 1/1, 1/0.646465 )
= diag( 2.4208978, 1.0000000, 1.5468742 )
ForwardMatrix(5D2,兩個光源用同一顆)是
FM = [[0.8924, -0.1041, 0.1760],
[0.4351, 0.6621, -0.0972],
[0.0505, -0.1562, 0.9308]]
FM · D 只是把 FM 的第一行整列乘 2.4208978、第三行整列乘 1.5468742:
第一列 = [0.8924 × 2.4208978, −0.1041 × 1, 0.1760 × 1.5468742]
= [2.1604092, −0.1041, 0.2722499]
colour-hdri 的 doctest 輸出第一列是 [2.1604087, -0.1041, 0.2722498]。對上了。完整矩陣:
CameraToXYZ_D50 = [[ 2.1604087, -0.1041, 0.2722498],
[ 1.0533324, 0.6621, -0.1503561],
[ 0.1222553, -0.1562, 1.4398304]]
現在做一件規格沒明說但非常有啟發性的驗算——把這個矩陣套回 camera neutral:
X = 2.1604087 × 0.4130699 + (−0.1041) × 1 + 0.2722498 × 0.646465
= 0.892400 − 0.104100 + 0.176005
= 0.964305
Y = 1.0533324 × 0.4130699 + 0.6621 × 1 + (−0.1503561) × 0.646465
= 0.435100 + 0.662100 − 0.097200
= 1.000000
Z = 0.1222553 × 0.4130699 + (−0.1562) × 1 + 1.4398304 × 0.646465
= 0.050500 − 0.156200 + 0.930800
= 0.825100
得到 (0.9643, 1.0000, 0.8251),而 D50 的 XYZ 是 (0.9642, 1.0000, 0.8249)。中性色被精確地送到了 D50。
而且注意 FM 本身的列和:
0.8924 − 0.1041 + 0.1760 = 0.9643
0.4351 + 0.6621 − 0.0972 = 1.0000
0.0505 − 0.1562 + 0.9308 = 0.8251
一模一樣。這就是 dng_sdk 裡 NormalizeForwardMatrix() 在做的事:它強制
FM · (1, 1, 1)ᵀ = XYZ_D50
也就是「當白平衡後的相機 RGB 三個值相等時,結果必須是 D50 白」。這是 ForwardMatrix 的定義性約束,不是巧合。任何人自己擬合 ForwardMatrix 都必須先滿足這個歸一化,否則 DNG reader 會替你歸一化,擬合出來的精度就跑掉了。
更正 6:ForwardMatrix 路徑沒有「不做色適應」,它做在別的座標軸上
常見的說法是「ColorMatrix 路徑要做 Bradford 色適應,ForwardMatrix 路徑不用」。前半對後半錯。ForwardMatrix 路徑也做了色適應,只是做在那個對角矩陣 D 裡面。
D = diag(1/0.4131, 1, 1/0.6465) 是一個純對角縮放。這在形式上就是 von Kries 色適應——只是縮放的軸不是 LMS 錐體軸、也不是 Bradford 的銳化錐體軸,而是相機自己的原生 RGB 軸。
所以兩條路徑的真正差別是:
- ForwardMatrix 路徑:在相機原生軸上做 von Kries 對角縮放,然後用一個為 D50 訓練的固定矩陣轉到 XYZ。
- ColorMatrix 路徑:先把未白平衡的資料用反矩陣送到場景白點下的 XYZ,再用 Bradford 銳化錐體軸做 von Kries 縮放到 D50。
同一個概念,不同的錐體基。而色適應的結果強烈依賴錐體基的選擇——這是色彩科學的常識,卻很少有人意識到它就藏在 DNG 的兩條分支裡。
Bradford 矩陣:它不是生理學
dng_sdk 的 MapWhiteMatrix 是這樣寫的(註解原文說用的是 linearized Bradford adaptation matrix):
Mb = [[ 0.8951, 0.2664, -0.1614],
[-0.7502, 1.7135, 0.0367],
[ 0.0389, -0.0685, 1.0296]]
w1 = Mb · XYtoXYZ(white1)
w2 = Mb · XYtoXYZ(white2)
B = Inverse(Mb) · diag(w2/w1) · Mb
兩個工程性的防護值得注意,因為它們洩漏了實務上會遇到什麼:
w1和w2的三個分量都被Max_real64(·, 0.0)夾到非負。程式碼註解說「負的白點座標基本上沒有意義」——會出現負值,代表輸入的白點跑到了光譜軌跡外面。- 縮放比被
Pin_real64(0.1, ratio, 10.0)限制在 0.1 到 10 之間,而且當w1[i] == 0時直接用 10.0。這是硬性截斷,不是平滑處理。
更正 7:Bradford 是刻意「光譜銳化」過的,它不打算模擬視錐
Bradford 變換的三個基元是被刻意收窄到比真實人類視錐靈敏度更窄的(spectrally sharpened)。它不是從生理量測來的,是從「人在不同光源下對顏色的感知恆常性判斷」的心理物理資料上擬合出來的工程解。你不能拿 Bradford 的三個基元去談視網膜。
這件事的實務後果:當你把 Bradford 用在光譜非常窄帶的光源(LED、鈉燈、雷射)時,銳化過的基元會放大誤差。而昨天談過的 TM-30 那類指標之所以有意義,正是因為它們保留了完整的 SPD;Bradford 只吃 xy 兩個數字,對窄帶光源的容錯本來就很差。
兩條路徑不等價:用數字證明
DNG 規格把兩條路徑並列在第六章 p.81 和 p.82,一個 reader 只要選其中一條就是合規的。那它們給的是同一個答案嗎?
我拿 5D2 的實際係數算了。同一顆 profile、同一個白點 xy = (0.32816244, 0.34698169):
路徑 A(ForwardMatrix):
[[ 2.1604, -0.1041, 0.2723],
[ 1.0533, 0.6621, -0.1504],
[ 0.1223, -0.1562, 1.4398]]
路徑 B(反 ColorMatrix + Bradford 到 D50,並歸一化使中性色 Y = 1):
[[ 2.1699, -0.0925, 0.2481],
[ 1.0530, 0.6602, -0.1472],
[ 0.1404, -0.1782, 1.4620]]
看起來很接近。但把兩個矩陣套到一組相機 RGB 上,轉成 CIELAB(以 D50 為白點)算 ΔE*ab:
| 測試色 | ΔE*ab(路徑 A vs 路徑 B) |
|---|---|
| 中性灰 18% | 0.01 |
| 膚色 | 0.97 |
| 綠色葉子 | 1.31 |
| 洋紅 | 1.48 |
| 青色 | 2.01 |
| 飽和紅 | 2.02 |
| 藍天 | 3.53 |
(說明:測試用的相機 RGB 三元組是我為了說明而構造的合成值,不是實測色卡讀值。ΔE 的數量級與趨勢可信,絕對數字不應引用。)
中性色的 ΔE 是 0.01——這是設計上的必然,兩條路徑都被強制把中性色送到 D50。但一離開中性軸,差異立刻長出來,飽和藍到了 3.53。ΔE*ab 大於 2 在並排比較時多數人看得出來,3.5 在天空這種大面積平滑漸層上是明顯的。
更正 8:「raw 是客觀的」在矩陣這一段就已經不成立
不需要談廠商的「風格」,光是規格內部兩條都合法的路徑,就已經產生了肉眼可辨的色差。而實際的 raw converter 差異還要更大,因為它們還在色溫估算(Robertson 1968 vs McCamy 近似 vs 查表)、去馬賽克演算法、tone curve 上各自為政。
「同一個 raw 檔在不同軟體裡顏色不同」不是誰做錯了,是規格允許的範圍就這麼寬。
為什麼精確的 3×3 根本不存在:Luther–Ives
現在回頭接昨天的 27 維核空間。
Luther–Ives 條件:當且僅當相機的三條光譜靈敏度函數是 CIE 配色函數(CMF)的精確線性組合時,存在一個 3×3 矩陣能把相機 RGB 精確映到 CIE XYZ。
真實相機不滿足這個條件。所以誤差不是「擬合得不夠好」,是結構性的:一定存在某些光譜對,相機看起來一樣但眼睛看起來不同(相機無法分辨的顏色),也一定存在某些光譜對,眼睛看起來一樣但相機看起來不同(相機分辨出人眼看不見的差異)。這叫捕捉端同色異譜誤差(capture metameric error)。
昨天說 30 維光譜投影到 3 通道、27 維核空間永久遺失。Luther–Ives 補上的是更狠的一句:**相機丟掉的那 27 維,和人眼丟掉的那 27 維,不是同一個 27 維。**兩個 3 維子空間在 30 維空間裡是斜交的,不重合。這才是色彩再現問題的根。
量化這個「斜交程度」的標準工具是 Vora–Value:
ν(Q, X) = (1/3) · trace( P_Q · P_X )
其中 P_Q 和 P_X 分別是相機光譜響應與人眼靈敏度所張成子空間的正交投影矩陣。ν = 1 代表兩個子空間完全重合(Luther 條件成立,存在精確 3×3);ν = 0 代表兩個子空間正交。原始定義出自 Vora & Trussell, “Measure of goodness of a set of color-scanning filters”, JOSA A 10 (1993), pp. 1499–1508。
Vora–Value 的關鍵性質是:它只取決於濾片本身,和你怎麼擬合矩陣、用什麼色卡、用什麼損失函數完全無關。它是這台相機色彩準確度的理論上界。你買相機的那一刻,這個數字就固定了。
工業界另一個常見指標是 ISO 17321-1:2012 附錄 B 定義的 DSC/SMI(digital still camera sensitivity metamerism index),用 CIE Publication 13 定義的 8 個代表反射物體的色票來評分。跟昨天談的 CRI 是同一套 8 色票的血統——只是把偵測器從「人眼」換成「相機」。
廠商 profile 的「有意的不準」:三條硬證據
既然精確不可能,矩陣就退化成一個最佳化問題;而最佳化問題的目標函數是可以選的。Adobe 選的目標,不是最小化 ΔE。
證據一:hue twist。 ProfileHueSatMapData 是一張 HueDivisions × SatDivisions × ValueDivisions 的三維查表,對每個格點存 (色相偏移, 飽和度乘數, 明度乘數)。Adobe 在新一代 profile 裡用第三維 ValueDivisions 來實作亮度相依的色相偏移——也就是同一個色相、同一個飽和度,亮的和暗的會被推到不同的顏色。這就是 hue twist。它的效果是讓天空更藍、樹葉更綠,而這在任何色度學意義上都不是「更準」,是更討喜。Alexander McGuffog 的 dcpTool 提供的 “untwist” 功能,做法就是選定某一個亮度層的 twist,把它套到所有亮度層,藉此把亮度相依性整個拿掉。一個工具要專門提供「拆掉這個」的功能,本身就說明了它是刻意加上去的。
證據二:profile 不是 scene-referred 的。 Anders Torger(DCamProf 作者)做過一個很尖銳的實驗:把 Adobe Camera Raw 的 profile 配上線性 tone curve 來看。結果是明顯偏淡(desaturated)。他的結論是:這證明 ACR 的 profile 根本不是 scene-referred,它是為了配合 DNG 的 S 形 tone curve 而預先減飽和的。換句話說,矩陣的偏差是為了抵銷後面曲線帶來的偏差;兩個「錯」相乘才等於設計者想要的那個「對」。
Torger 對比同一張圖時的具體觀察是:ACR profile 的紅過飽和、紫和亮黃綠飽和度不足、膚色略微過飽和且偏黃、白襯衫原本那點輕微藍調被抹掉了。他判斷這些「大部分是 Adobe 的 profile 設計師刻意的主觀調整,目的是做出一個設計過的『look』,就像類比時代的底片一樣」。
證據三:規格根本沒定義 tone curve 該怎麼做。 這是最技術性也最要命的一條。DNG 的 HueSat LUT 在格式上支援對灰階做明度縮放,但公開的 DNG 參考程式碼以及 Adobe 自家產品都忽略灰階的 value 乘數,強制設為 1.0。所以 LUT 改不了灰階的明暗,曲線只能靠 ProfileToneCurve 這個標籤。
而 DNG 規格沒有規定 tone curve 應該怎麼套用。依 Torger 的描述,Adobe 用的是一種「色相穩定化的 RGB 曲線」:曲線只套用在三通道的最大值和最小值上,中間值被調整以維持 RGB-HSL/HSV 定義下的色相不變。這比純 RGB 曲線少一些色偏,又比在 Lab 的 L 或 HSV 的 V 上套曲線更接近 RGB 曲線的飽和度提升行為。這個行為完全不在規格裡,是實作慣例——所以任何不照這個做的 raw converter,拿到同一顆 profile 會得到不同結果。(此段對 Adobe 曲線的具體機制我沒能從 Adobe 官方文件直接讀到,依 Torger 的描述轉述,已標存疑。)
更正 9:ColorMatrix / ForwardMatrix 不能當作相機光譜靈敏度的代理
網路上常見的做法是:抓一顆 DCP profile,反推 ColorMatrix,拿來當這台相機的「光譜響應」用。這是錯的,而且錯得很深。
原因有三層。第一,Luther–Ives 不成立時,3×3 本來就只是 3 維子空間之間的最佳投影,它丟掉的資訊(相機子空間與人眼子空間不重合的那部分)無法從矩陣裡還原。第二,矩陣的擬合目標函數不是最小 ΔE,是 Adobe 的美學目標。第三,如上所述,矩陣還被預先扭曲以配合後段的 tone curve。
這個矩陣不是量測結果,它是渲染意圖(rendering intent)的第一段。 要拿真的光譜靈敏度,只能用單色儀實測,或用已知光譜的多光源解反問題。
最後一哩:D50 是 ICC 的決定,不是攝影的決定
CameraToXYZ_D50 給你的是 D50 白點下的 XYZ。但 sRGB 的白點是 D65,Display P3 是 D65,Adobe RGB 是 D65。所以還要再做一次色適應:
相機 RGB(已白平衡)
→ [ForwardMatrix · D · Inverse(AB·CC)] → XYZ_D50
→ [Bradford: D50 → D65] → XYZ_D65
→ [XYZ_D65 → sRGB 線性 3×3] → linear sRGB
→ [IEC 61966-2-1 的分段轉換函數] → sRGB 編碼值
為什麼中間要繞 D50?因為 ICC 的 Profile Connection Space 規定白點是 D50(XYZ ≈ 0.9642, 1.0000, 0.8249,對應 xy ≈ 0.3457, 0.3585),而 D50 之所以被選中,是 1990 年代印刷業標準觀察條件的產物。DNG 沿用 ICC 的 PCS,於是每一張數位照片都要從場景白點適應到 D50,再從 D50 適應回 D65。兩次色適應,而不是一次。
理論上 Bradford 是可逆的,兩次應該互相抵銷。實務上不會完全抵銷,因為中間夾了 ForwardMatrix 的線性投影、hue twist 的非線性查表、以及 tone curve。誤差在這幾段之間會被放大。
還有一個容易忘記的細節:sRGB 的轉換函數(IEC 61966-2-1)不是 gamma 2.2,是分段的:
線性段: V = 12.92 · L (L ≤ 0.0031308)
冪次段: V = 1.055 · L^(1/2.4) − 0.055 (L > 0.0031308)
整體曲線近似 gamma 2.2,但在暗部因為那段線性,實際斜率完全不同。這是為什麼在陰影區用 2.2 近似做運算,會在極暗處產生可見誤差。
全鏈路總表
| 階段 | 輸入 | 輸出 | 定義來源 | 是否可逆 |
|---|---|---|---|---|
| 光譜積分 | 30+ 維 SPD | 每像素 1 個 DN | 物理 | 否(27 維核空間) |
| 線性化 / 減黑階 / 重縮放 / 截切 | raw DN | 線性參考值 | DNG Ch.5(1.4 p.77–78) | 截切不可逆 |
| Opcode 修正(暗角、壞點、幾何) | 線性參考值 | 修正後 | DNG Ch.7(1.4 p.83–) | 部分 |
| 去馬賽克 | N 個樣本 | 3N 個值 | 無規格 | 否 |
| 白平衡增益 | 相機 RGB | 白平衡相機 RGB | AsShotNeutral 倒數 | 是(對角) |
| ForwardMatrix · D · inv(AB·CC) | 白平衡相機 RGB | XYZ D50 | DNG Ch.6(1.4 p.82) | 是(3×3) |
| HueSatMap 2.5D LUT | XYZ→HSV | HSV | DNG Ch.6(1.4 p.82) | 否 |
| ProfileToneCurve | RGB | RGB | 規格未定義行為 | 是(單調) |
| LookTable | HSV | HSV | DNG Ch.4 | 否 |
| Bradford D50→D65 + 輸出矩陣 | XYZ D50 | linear sRGB | ICC / IEC | 是 |
| sRGB 轉換函數 | linear | 編碼值 | IEC 61966-2-1 | 是 |
不可逆的三個地方:光譜積分(27 維)、截切(高光)、去馬賽克與查表(空間與色彩的高頻)。前一個昨天講過了,後兩個是今天的重點。
🧠 記
方向記反是最常見的錯。ColorMatrix 是 XYZ → 相機,ForwardMatrix 是相機 → XYZ D50。它們不是彼此的反矩陣,連輸入是否已白平衡都不同。
ForwardMatrix 的列和必須等於 D50 的 XYZ。這是 NormalizeForwardMatrix 強制的約束,等價於「白平衡後的相機 RGB 三值相等時必須輸出 D50 白」。5D2 的實例:0.8924 − 0.1041 + 0.1760 = 0.9643 ≈ 0.9642。
內插在 mired 上,不在色溫上。5000 K 在 2850–6500 K 之間的權重是 0.2342,不是 0.4110。
白點求解是不動點迭代,可能不收斂。dng_sdk 的 NeutralToXY 從 D50 起步,最多 30 次,門檻 1e-7,不收斂就取最後兩次平均然後放棄。所以「白平衡色溫」這個數字本身是一個數值解,不是查表值。
白平衡增益不改變 SNR。5D2 紅通道的 SNR 只有綠通道的 √0.4131 = 64.3%,這個差距在光子打到 CFA 的瞬間就決定了,乘 2.42 既沒惡化也沒改善它。
兩條合規路徑不等價。同一顆 profile,ForwardMatrix 路徑與反 ColorMatrix + Bradford 路徑在中性色上一致(ΔE 0.01),在飽和藍上差到 ΔE*ab 3.5。
Vora–Value 是硬上界。ν(Q,X) = (1/3)·trace(P_Q·P_X),只取決於濾片,不取決於你怎麼擬合矩陣。買相機時就決定了。
廠商矩陣是渲染意圖,不是量測。它為了配合 S 形 tone curve 而預先減飽和,拿去當光譜靈敏度用一定會錯。
DNG 規格不管去馬賽克,也不定義 tone curve 的具體行為。這兩個空白就是不同 raw converter 差異最大的地方。
✍️ 實踐
用 exiftool 把矩陣挖出來,親手算一遍。 對任何一張自己的 raw 檔跑
exiftool -ColorMatrix1 -ColorMatrix2 -ForwardMatrix1 -ForwardMatrix2 \
-CameraCalibration1 -CameraCalibration2 -AnalogBalance \
-AsShotNeutral -CalibrationIlluminant1 -CalibrationIlluminant2 \
-UniqueCameraModel your_file.dng
然後用二十行 numpy 復現本文的 FM · D · Inverse(AB·CC),驗算它把 AsShotNeutral 送到 (0.9642, 1.0000, 0.8249)。如果對不上,通常是矩陣的行列順序讀錯了,或忘了 ForwardMatrix 需要 normalize。這個驗算做過一次,DNG 色彩管線就不再是黑盒。
做「兩條路徑」的對照。 用同一顆 profile,分別算 ForwardMatrix 路徑與反 ColorMatrix + Bradford 路徑,把你自己拍的色卡 raw 值餵進去,算 ΔE。這會告訴你在你的相機、你常拍的光源下,這個歧異有多大。5D2 在 D50 附近是 1–3.5,你的相機和你的光源可能更大或更小。
用 RawTherapee 驗證 hue twist 的存在。 RawTherapee 的 DCP profile 設定裡可以獨立勾選 “Base table”(HueSatMap)、“Look table”、“Tone curve”、“Baseline exposure”。拿一張有大片藍天和飽和綠葉的照片,單獨開關 base table,看藍色跑多遠。再用 dcpTool 的 untwist 產生一顆無 twist 的 profile,三方對比。
用 dcraw / LibRaw 出一張真正線性的檔案來當基準。 dcraw -v -D -4 -T 給你未去馬賽克、未縮放的 16-bit 線性 TIFF;dcraw -v -d -4 -T 給你未去馬賽克但有白平衡的。用這個當「什麼都還沒發生」的參考點,再逐步套上每一段變換,你會很清楚看到每一步各自貢獻了多少變化。
測一次自己相機的 R/G/B 訊號比。 拍一張中性灰卡,讀 raw 的三個通道平均值,算比值。這個比值就是 AsShotNeutral,也直接告訴你哪個通道的散粒雜訊最吃虧。5D2 是 (0.413, 1, 0.646),很多現代相機的紅通道更低——這解釋了為什麼推高 ISO 時紅色雜訊往往先崩。
不要用網路上抓的 DCP 當光譜靈敏度。 如果你真的需要相機的光譜響應(做多光源模擬、做客製 profile、做科學攝影),去找 ground truth 資料集(有若干公開的 Nikon / Canon 光譜靈敏度量測資料集),或者用已知光譜的窄帶光源自己解反問題。矩陣不是量測。
🔗 延伸學習
- Digital Negative (DNG) Specification 1.7.1.0(Adobe, 2023-09) — 第六章 “Mapping Camera Color Space to CIE XYZ Space” 是本文的骨架。1.4.0.0 版對應章節在 p.79–82,1.6/1.7 版在 p.85 起。
- Pascal Getreuer, “Malvar-He-Cutler Linear Image Demosaicking”, Image Processing On Line, 1 (2011), pp. 83–89 — 附完整 ANSI C 原始碼與線上 demo,可以直接跑出本文那張 PSNR 對照表。
- colour-hdri 的 DNG 模組原始碼 — 把 DNG 第六章逐條翻譯成 Python,doctest 用的就是 Canon EOS 5D Mark II 的真實矩陣,本文所有數值都可以在這裡對照。它還誠實標註了參考實作(dng_sdk)與白皮書之間的分歧點。
- Anders Torger, DCamProf 文件 — 關於「廠商 profile 為什麼刻意不準」寫得最硬的一份公開文件,含 tone curve 與色彩外觀交互作用的實測對照圖。
- Alexander McGuffog, “Hue Twists”(dcpTool) — hue twist 的定義與 untwist 的實作原理。
💬 問 AI
我在研究 Adobe DNG 規格第六章的色彩管線,想確認幾個具體的技術點,請盡量引用規格章節或參考實作(dng_sdk)的原始碼:
1. ForwardMatrix 的歸一化約束到底是什麼?我算出 Canon 5D2 的 ForwardMatrix 列和是 (0.9643, 1.0000, 0.8251),幾乎等於 D50 的 XYZ (0.9642, 1, 0.8249)。dng_sdk 的 NormalizeForwardMatrix() 是不是就在強制 FM·(1,1,1)ᵀ = XYZ_D50?如果我自己用最小平方擬合一顆 ForwardMatrix,應該在擬合前還是擬合後做這個歸一化,對精度的影響有多大?
2. DNG 第六章提供了兩條 camera → XYZ_D50 的路徑(有 ForwardMatrix 用 FM·D·inv(AB·CC),沒有的話用 Bradford CAT · inv(AB·CC·CM))。我實測這兩條路徑在中性色上完全一致(ΔE*ab 0.01),但在飽和藍上差到 3.5。這個歧異是規格已知並接受的嗎?Adobe Camera Raw 實際上走哪一條?有沒有辦法從輸出反推一個 raw converter 走了哪條路?
3. 我的理解是 ForwardMatrix 路徑裡那個對角矩陣 D = diag(1/neutral) 其實就是在「相機原生 RGB 軸」上做 von Kries 色適應,而 ColorMatrix 路徑是在「Bradford 銳化錐體軸」上做。這個對比正確嗎?如果正確,有沒有文獻比較過這兩種錐體基在窄帶 LED 光源下的表現差異?
4. dng_sdk 的 NeutralToXY 用不動點迭代求白點,kMaxPasses = 30,收斂門檻 |Δx|+|Δy| < 1e-7,不收斂就取最後兩次平均。實務上什麼樣的 profile 或什麼樣的 AsShotNeutral 會導致不收斂?這個震盪的數學原因是什麼(是不是 CCT→矩陣→白點→CCT 這個映射的 Lipschitz 常數在某些區域大於 1)?
5. 關於「廠商 profile 刻意不準」:除了 hue twist 和「為了配合 S 形 tone curve 而預先減飽和」這兩點,還有哪些可以量化驗證的證據?有沒有論文或公開實測,量過 Adobe Standard profile 相對於最小 ΔE 擬合矩陣的偏差量?
6. Vora-Value 和 ISO 17321-1 的 DSC/SMI 這兩個指標,對同一批相機的排序會一致嗎?如果不一致,差異來自哪裡(Vora-Value 是子空間幾何、SMI 是特定 8 色票下的 ΔE,前者與擬合無關、後者與擬合有關)?有沒有公開的、涵蓋多台相機的 Vora-Value 對照表?
7. DNG 規格沒有定義 ProfileToneCurve 應該怎麼套用。Adobe 據說用的是「色相穩定化的 RGB 曲線」(曲線套在 max 和 min 通道,中間值調整以維持 RGB-HSL 色相)。這個描述有官方出處嗎?RawTherapee、darktable、Capture One 各自是怎麼實作的?差異在視覺上有多大?
請針對每一點標明你的把握程度,查不到出處的請直接說查不到,不要用推測填空。