1.2 色彩空間:RGB、HSV、CMYK 與亮度色度分離(1970-2000)#
一、物理基礎:人眼如何看見顏色#
1.1 Young–Helmholtz 理論(1802)#
色彩視覺的第一個現代理論由 Thomas Young(1802) 提出:眼睛的視網膜上存在三種類型的受器,每一類對光譜的不同區段敏感;三種受器的相對反應量共同決定我們感知到的顏色。Hermann von Helmholtz(1850s) 確認並量化了這個理論,因此合稱 Young–Helmholtz 三色視覺說。
其重要性不在於「正確」(它並未考慮視網膜上神經元的處理與視覺適應),而在於它可量化:任何顏色都可以用三個數字描述,這使得「顏色的度量」成為可能,也是所有色彩空間的共同起點。
現代生理學補充了細節:
| 受器 | 峰值波長 | 數量 | 功能 |
|---|---|---|---|
| S 錐(短波) | 約 420 nm | 少量 | 藍紫 |
| M 錐(中波) | 約 534 nm | 中等 | 綠 |
| L 錐(長波) | 約 564 nm | 最多 | 紅(也貢獻明度) |
重要事實:三色受器的光譜敏感曲線高度重疊,這是「一切色彩都是不唯一的」之來源——許多不同的光譜組合會產生相同的三刺激值,這種現象稱為異譜同色(metamerism)。它解釋了為什麼兩張看起來完全一樣的色卡,在不同光源下可能出現差異(這是攝影中「白平衡調整」的必要性),也解釋了為什麼 RGB 三通道本身無法唯一決定「物體的真實顏色」。
1.2 視覺的適應性與對比#
第二個關鍵生理現象是適應:人眼對亮度的反應是非線性的,且會隨環境平均亮度調整(Weber–Fechner 與 Stevens 的冪次律)。這導致:
- 人眼在暗處對相對亮度差異更敏感(對數型響應);
- 人眼對同一區域內的對比比對絕對亮度更敏感。
這些性質直接導致了色彩空間設計中的一個核心問題:我們要設計的座標系,其座標軸應當對應「物理量」(RGB 的輻射量)還是对應「感知量」(明度、彩度、色相)? 兩者各有用途,且不可互相取代——RGB 用於顯示與感測,HSV 用於分割與選擇,Lab 用於科學度量。
1.3 CIE XYZ(1931):第一個國際色彩標準#
CIE(國際照明委員會)在 1931 年定義了標準色度系統 CIE 1931 XYZ,其重要性在於它是第一個數學上定義清楚、可重現、與國際共識綁定的色彩度量系統。此後所有色彩空間(sRGB、HSV、CIELAB、YCbCr)都可以追溯到 XYZ。
XYZ 的三個座標定義:
- \(X\) :與紅色感知(cone response)成比例的「虛擬原色」量;
- \(Y\) :明度(lightness),與人眼對亮度的感知直接相關,且被刻意設計為與均勻白色點的亮度成比例;
- \(Z\) :藍色感知量。
三刺激值由光譜輻亮度分佈 \(S(\lambda)\) 與 CIE 標準觀察者的三條配色函數 \(\bar{x}(\lambda), \bar{y}(\lambda), \bar{z}(\lambda)\) 積分得到:
其中 \(\bar{y}(\lambda)\) 被定義為** luminous efficiency function(光視效率函數)** ,也就是人眼整體明度感的權重曲線。
關鍵設計決策: \(Y\) 被安排為明度,而 \(X, Z\) 被安排為「色度」。這是一個故意的、專為壓縮色度資料而做的工程選擇——因為人眼對色度的解析度遠低於對明度的解析度。這是 1.6 節「亮度色度分離」的源頭,比具體的 YUV 早了四十多年。
XYZ 的弱點:不感知均勻。在 XYZ 空間中等距離的兩點,在感知上的差異並不相同;近白色區域尤其擁擠,遠飽和區域尤其稀疏。這個問題直到 CIELAB(CIELUV, 1976) 才被解決。
二、RGB 模型與伽瑪校正#
2.1 加法混色#
RGB 是加法混色(additive mixing)模型:紅、綠、藍三束光的強度相加得到最終顏色。
其中 \(\mathbf{r}, \mathbf{g}, \mathbf{b}\) 為 CIE 定義的原色向量。所有加法混色的核心性質:
- 黑色是三者的零(沒有光);
- 白色是三者的最大值(等能白);
- 亮度隨 RGB 的總和增加(在線性域中)。
三原色區形成的三角形(RGB 三角形)是所有螢幕顯示器與投影機的色域(gamut)。
2.2 光線性化與伽瑪:整個數位影像領域最重要的一個約定#
問題:CRT 顯示器的磷光體發光強度與施加電壓並非線性關係。歷史上 CRT 的傳輸特性被設計為近似 \(V = V_0 \, L^{1/2.2}\) (Power law),其中 \(L\) 是光線性亮度, \(V\) 是訊號電壓。
後果:當我們把數位影像的數值直接送到顯示器時,顯示出來的亮度不是數值的線性函數。若影像處理在「顯示值域」上運算,同樣的數值差在暗部與亮部代表極不相同的物理亮度差。
解決:標準化為伽瑪編碼(gamma encoding)。標準 sRGB 傳遞函數(IEC 61966-2-1)定義如下:
而反向的編碼(顯示時使用)為
其中 \(C\) 是編碼值( \(\in [0,1]\) ,對應 8 bit 的 0-255), \(C_{\text{lin}}\) 是光線性(linear-light)值。
在低階區間( \(C_{\text{lin}} \le 0.0031308\) )使用線性段而非純冪次,是為了避免冪次函數在 0 附近的導數無窮大所造成的數值不穩定(同時保證斜率連續)。這條線性段的斜率 \(12.92\) 大於 \(1/2.2\) 對應的斜率,因此曲線是「凸」的——即暗部被分配了更多數位碼位。
sRGB 的三個設計意圖: 1. 讓光線性值與顯示器的物理發光近似成正比(物理正確); 2. 在 8 bit 下讓暗部有足夠的碼位,避免 banding(工程實用); 3. 讓 RGB 成為一個有明確、可轉換定義的標準,而不是各自為政的「RGB」(標準化)。
2.3 為什麼「直接在 sRGB 空間算梯度」是錯的#
這是初學者最常犯的錯誤,也是本章最需要反覆強調的實務重點。
在 sRGB 編碼值上計算相鄰像素差:
這個量不是物理亮度差。以數值為例: \(C = 0.02\) 與 \(C' = 0.03\) 的編碼差為 0.01; \(C = 0.50\) 與 \(C' = 0.51\) 的編碼差同樣是 0.01。但它們對應的光線性差異天差地別:
- 暗部: \(C_{\text{lin}} = 0.00152\) 對 \(0.00267\) ,物理差約 \(1.15\times10^{-3}\)
- 亮部: \(C_{\text{lin}} = 0.2140\) 對 \(0.2195\) ,物理差約 \(5.5\times10^{-3}\)
同樣的 0.01 編碼差,在亮部代表的物理亮度差是暗部的近 5 倍。 因此:
- 梯度運算(Sobel、Canny、Scharr)在編碼域計算會過度強化暗部的邊緣,同時弱化亮部的邊緣。這會導致演算法在暗部產生大量假邊緣(noise triggering),這是機器視覺中大量 false positive 的來源之一。
- 相似度量與損失函數(MSE、PSNR、SSIM)在編碼域計算同樣失真。
- 混合與合成(alpha blending)在編碼域做會產生與物理不符的亮度路徑。
正確做法:任何涉及「加權、差分、線性組合、物理量估計」的運算,都應先轉換到光線性域。
import numpy as np
def srgb_to_linear(x):
x = np.asarray(x, dtype=np.float64)
return np.where(x <= 0.04045, x / 12.92, ((x + 0.055) / 1.055) ** 2.4)
def linear_to_srgb(x):
x = np.asarray(x, dtype=np.float64)
return np.where(x <= 0.0031308, 12.92 * x, 1.055 * np.power(np.clip(x, 0, None), 1 / 2.4) - 0.055)
# 驗證往返一致性
x = np.linspace(0, 1, 1001)
assert np.allclose(srgb_to_linear(linear_to_srgb(x)), x, atol=1e-9)
# 展示非線性:編碼域等距 > 物理域等距
enc = np.array([0.02, 0.03, 0.50, 0.51])
print("編碼域差:", enc[2] - enc[1], enc[3] - enc[2])
print("物理域差:", srgb_to_linear(enc[3]) - srgb_to_linear(enc[2]),
srgb_to_linear(enc[1]) - srgb_to_linear(enc[0]))
2.4 為什麼 PSNR 在 sRGB 空間算會高估品質#
PSNR 定義為
其中 MSE 是編碼域逐像素差平方的平均。若改在光線性域計算:
則暗部的權重會被大幅放大。因為編碼域的 1 個碼位在暗部對應的光線性差非常小,在亮部則相對較大。
結論:在編碼域算 PSNR,會把「亮部的嚴重錯誤」與「暗部的輕微錯誤」視為同等重要——這與人眼的實際感知相反(人眼對暗部相對差異更敏感)。所以:
在 sRGB 編碼域計算的 PSNR 會系統性地高估影像品質,尤其在暗部細節豐富的場景(如夜景、人像)。這個偏差看似微小,卻足以讓 A/B 測試與競賽排名失真。
這是一個測量工具本身的系統誤差——它教會我們一件事:當一個指標被廣泛使用時,它的定義慣例(用哪個空間、哪個線性化)會成為隱形的「規則」,而這些規則未必有物理依據。深度學習時代的 LPIPS(第 1.1 節提到的感知損失)正是為了修正這類問題而生:它直接比較深度網路的特徵,用「特徵空間的距離」取代「像素空間的距離」,在經驗上與人眼判斷的一致性遠高於 PSNR。
2.5 線性域中的「加權平均」#
另一個相關的實務重點:在光線性域做加權平均(例如灰度化、alpha 合成、下採樣預濾波)時,權重應反映人眼的光視效率,這正是 CIE 的 \(\bar{y}(\lambda)\) 曲線的用途。
常見的 BT.601 亮度權重(定義在光線性 RGB 上):
而 BT.709(HDTV)改為
Green 係數最大,因為 L 與 M 錐細胞的峰值波長都落在綠黃區,且人眼對綠光最敏感(這也解釋了為什麼早期 CRT 的綠色磷光體效率最高,因為要省電)。
三、HSV 與 HSL:色相的圓柱座標本质#
3.1 為什麼需要 HSV#
RGB 的三個軸都是「量」,不是「屬性」。我們無法用 RGB 直接表達「我要找紅色的東西,不管它多暗多亮」——因為紅色物體在 RGB 中可能是 \((200, 30, 30)\) 或 \((80, 10, 10)\) 或 \((255, 120, 110)\) ,取樣條件寫不出來。
HSV(Hue-Saturation-Value)與 HSL(Hue-Saturation-Lightness)把顏色分解為三個語意屬性:
- Hue(色相):顏色的「基本色」——紅、橙、黃、綠、藍、紫。用圓周上的角度表示, \(H \in [0, 360^\circ)\) 。
- Saturation(飽和度):純度/彩度, \(S \in [0,1]\) 。
- Value / Lightness:明亮程度。
3.2 色相的本質:圓柱座標與「不可排序」性#
HSV 在幾何上就是一個圓柱座標系統:色相是角度,飽和度是半徑,明度是高度。這是一個三維幾何事實,但它帶來一個極重要的後果:
色相是一個循環量,沒有「大小」順序。紅(0°)與紫(300°)在數值上相差大,但在感知上相鄰;紅(0°)與綠(120°)數值相差小,感知上卻很遠。
任何「對色相取平均」的運算都因此在數學上是錯的。計算平均色相必須走圓周路徑:
這是統計學中「方向統計(circular statistics)」的標準做法。遺憾的是,OpenCV 與許多工具直接對 H 做算術平均,會得到錯誤結果——例如紅色與紫色的平均會變成「藍綠」。
實務建議:需要對多張圖或多個區域取平均顏色時,在線性 RGB 上平均(物理正確),或使用圓周平均的 H。
3.3 RGB → HSV 的轉換#
以 \(\max\) 與 \(\min\) 記 \(C_{\max} = \max(R,G,B)\) 、 \(C_{\min} = \min(R,G,B)\) 、 \(\Delta = C_{\max} - C_{\min}\) ,在編碼域(標準做法):
注意:標準的 HSV 轉換是在 sRGB 編碼域上做的(因為它是「顯示域」的屬性分解)。這意味著 HSV 的 H 與 S 對編碼的非線性是不感知均勻的——從物理角度而言這是缺陷,從工程角度而言卻是實用的(因為使用者看到的顏色就是編碼域的樣子)。
HSL 的定義略有不同: \(L = (C_{\max} + C_{\min}) / 2\) ,且
HSL 的優點是亮度線性對稱(把白色降低會等比例地拉暗所有通道),因此 HSL 更適合「調整色調/去飽和」的設計工具;HSV 的優點是 Value 直接等於最大通道值,適合「找亮區域」。
3.4 為什麼 HSV 適合顏色分割#
HSV 的殺手級應用是基於顏色的物件分割,因為它把「陰影」與「顏色」分離開了:
- 陰影會降低 V(明度)但幾乎不影響 H;
- 陰影對 S 的影響也較小(因為編碼域的非線性讓暗部的 S 略微上升,但相對量小)。
因此,一個判斷式
就能在幾乎不變的 H 區間內、允許 V 大幅變動的情況下,抓出所有該色相的物體——這就是「陰影不變性(shadow invariance)」。在 RGB 上做同樣的事需要對每個 R、G、B 通道設定獨立的範圍,因為陰影是對三通道的非等比縮放。
另一個理解角度:把「陰影」視為乘法因子 \(I = R \times L\) (見 5.3 節 Retinex),在 HSV 中,乘法縮放主要影響 V;所以 HSV 的色相軸天然是「近似反射率不變」的軸。
3.5 OpenCV 的實作細節:H 是 0-179,且 uint8 是危險的#
使用 OpenCV 時有三個極易出錯的細節:
(1)H 的量綱是 0-179,不是 0-255。 OpenCV 把 H 存成 uint8,但 H 的角度是 0-359(浮點),因此 OpenCV 把 H 除以 2 後存入(360/2 = 180,剛好塞進 uint8)。所以:
# 錯誤:門檻會永遠不成立
mask = cv2.inRange(hsv, (170, 50, 50), (179, 255, 255)) # 紅色區間錯了
# 正確:紅色分兩段(H=0 附近與 H=175-179)
lower1 = np.array([0, 50, 50]); upper1 = np.array([10, 255, 255])
lower2 = np.array([170, 50, 50]); upper2 = np.array([179, 255, 255])
mask = cv2.inRange(hsv, lower1, upper1) | cv2.inRange(hsv, lower2, upper2)
紅色跨越 0° 邊界是所有色相分割程式最常見的 bug 來源——因為色相是循環的。
(2)uint8 的 H 精確度只有 2°。 在需要精細色相分割(例如同色系但不同表面反射率的研究)時,應該把影像轉成 float32 並自行計算 H,或使用 Lab 的 a/b 軸。
(3)inRange 之外的選擇性。 現代 OpenCV 的 cv2.selectROI 互動選取、cv2.createTrackbar 調參都是 HSV 分割的標準流程。
3.6 HSV 的侷限#
- H 不是感知均勻的:CIELAB 的 a/b 平面上的等距,才大致對應感知等距;HSV 的角度軸不滿足此性質。
- H 在低飽和區不穩定:灰色像素的色相是任意的(因為 \(\Delta \approx 0\) ),微小雜訊會讓 H 劇烈跳動。
- H 對 RGB 的非線性編碼敏感:同一物理顏色的不同亮度版本,其 HSV 的 H 會略有偏移。
四、CMYK 與印刷:減法混色#
4.1 減法混色與 CIE 的原色#
印刷與攝影遵循相反的物理原理。印刷油墨是吸收(減法)光線:白紙反射所有光,油墨選擇性地吸收某些波長,剩餘的反射光進入眼睛。
CIE 1931 標準觀察者的配色函數即以此原理定義原色:
- \(\bar{x}(\lambda)\) :與「紅色」(波長約 600 nm)的原色匹配;
- \(\bar{y}(\lambda)\) :與「綠色」(約 550 nm)的原色匹配;
- \(\bar{z}(\lambda)\) :與「藍色」(約 450 nm)的原色匹配。
CIE RGB 原色( \(R, G, B\) )在物理上是不可實現的(單色光必須極純才能匹配),且色域很小(覆蓋不了多數自然色)。這是 CIE XYZ 改用虛擬原色的原因——XYZ 的 \(X, Y, Z\) 是不存在的假想原色,但它們的線性組合可以匹配所有真實顏色。
4.2 CMY 是 RGB 的補色#
在反射的物理模型下,青(Cyan)油墨吸收紅光,因此它給出的是「紅的補色」;品紅(Magenta)吸收綠光;黃(Yellow)吸收藍光。所以:
(在編碼域近似)。這解釋了 CMY 與 RGB 的「補色」對應關係,也解釋了一個初學者常有的疑問:為什麼「青 + 黃 = 綠」?因為青吸收紅、黃吸收藍,剩下綠光被反射。
4.3 為什麼需要 K(黑色)#
理論上 CMY 三色相乘可得黑色( \(C = M = Y = 1\) ),但實務上不可行:
- 不純:實際油墨不是理想的窄帶吸收體,每種油墨都會吸收一部分本該保留的波段。三種油墨疊加後,殘留的光譜通量會讓結果呈現髒褐色(mud),而不是中性黑。
- 多色疊加的套印誤差:三次疊印必須三次對位,任何微小偏移都會在暗部顯著顯現(因為深色區域是「靠疊加」得來的)。
- TAC 限制(Total Area Coverage):印刷廠的濕墨總覆蓋率限制(例如 300%),全版滿版三色會超標。
- 效率:用單一黑墨代替深青+深品紅+深黃,成本與速度都更優。
因此實務公式為:
(GCR / UCR:灰階成分取代與底色移除,用以控制總墨量。)
4.4 RGB ↔ CMYK 的轉換是非線性的#
RGB → CMY 是逐通道的相減(線性、可逆),但 CMY → RGB 需要除以 \((1 - K)\) ,因此不可逆,而且整體是非線性的。
這帶來一個重要的實務後果:往返轉換是有損的。一張在螢幕上看到的圖片轉成 CMYK 再轉回 RGB,色調會偏移(尤其在飽和色區與暗部)。所以「螢幕預覽」與「印刷輸出」之間的色差管理(color management / ICC profile)是專門的產業。
另一個用現代視覺模型思考的層次:CMYK 是「物質性」的顏色,RGB 是「光性」的顏色。當我們用相機拍攝一張印刷品並做顏色辨識時,如果不做色彩管理,取到的是 CMYK 印墨的反射光,其光譜落在 RGB 三原色的三角形之外——相機的 RGB 會把它「拉回」可顯示範圍內,得到一個不等於原印墨顏色的值。這個現象在食品、材質、藝術品的機器視覺檢測中是真實的工程問題。
五、亮度-色度分離#
5.1 共同的思想#
亮度-色度分離(luminance–chrominance separation)的核心命題:
人眼對「亮度」的解析度遠高於對「色度」的解析度。
RGB 三通道中,兩個獨立的通道都用來攜帶亮度資訊(紅色物件的 R 與 G 通道都很大),因此在有限位元預算下,RGB 是冗餘且低效的編碼。把顏色拆成「一個亮度 + 兩個色度」可以用很少的位元保存色度。
這個思想源於 CIE XYZ 對 \(Y\) 的刻意安排(1.3 節),並在下列標準中定型:
- NTSC(1953)、PAL / SECAM(1960s):第一代電視系統即採用「亮度-色度分離 + 色度副取樣」。NTSC 甚至採用了 I/Q(正交調幅) 與 YUV 兩種方案。
- YCbCr(ITU-R BT.601 / BT.709):數位視訊標準。
- CIELAB / CIELUV(1976):科學度量。
5.2 YUV 與 YCbCr#
YUV: \(Y\) 為亮度(權重同 BT.601 或 BT.709), \(U = B' - Y\) , \(V = R' - Y\) ,其中 \(R', G', B'\) 是光線性 RGB。
從 RGB(光線性)到 YUV(BT.601)的轉換:
反轉換:
注意反轉換是線性的——YUV 與 RGB 之間只有線性變換。
YCbCr: \(Y\) 相同,色度改為:
(BT.601 full range 數值;BT.709 用 \(2.112\) 與 \(1.574\) 。)
YCbCr 的好處是 \(C_b, C_r\) 的數值範圍固定且對稱(約 \(\pm 0.5\) ),因此可用整數定點儲存,工程上更簡潔。影視工業界幾乎一律使用 YCbCr 而非 YUV。
OpenCV 的注意事項:OpenCV 預設讀入影像時做的是 BGR → RGB 的通道交換,並不做伽瑪線性化。而 cv2.cvtColor(img, cv2.COLOR_BGR2YCrCb) 是在編碼域上做近似轉換(不是嚴格的 BT.601 光線性轉換)。這是效能與正確性的取捨——多數應用(編碼、顯示)不需要嚴格線性化。
5.3 色度副取樣(Chroma Subsampling)#
有了亮度色度分離,就可以對色度做降取樣而保持亮度全解析度。
| 格式 | 色度取樣 | 資料量(相對 luma) | 典型用途 |
|---|---|---|---|
| 4:4:4 | 無降取樣 | 1.00 | 主控、母帶、專業後製 |
| 4:2:2 | 水平方向 1/2 | 0.75 | 廣播、專業攝影(DV、ProRes422) |
| 4:2:0 | 水平與垂直皆 1/2 | 0.50 | DVD、Blu-ray、視訊串流 |
| 4:1:1 | 水平 1/4 | 0.50 | 舊式 MPEG-1 |
4:2:0 的資料量只有全彩的 50%,而人眼幾乎看不出差別——因為色度的空間解析度需求遠低於亮度。
注意一個反直覺的細節:4:2:0 不是把「每 2×2 像素區塊取 1 個色度」那麼粗糙(在 MPEG-2 之前的老式實作確實如此,導致 chroma 邊緣滲色 chroma bleeding)。現代的 4:2:0 是把色度在水平與垂直各以 2 倍取樣,再以濾波器重建,取決於編碼器的 chroma filter 設計。
4:2:0 對 AI 的影響(值得記住的一個事實):Google 的 JXL 與 AVIF、以及網路上流通的大多數影像素材,都是 YCbCr 4:2:0。也就是說:當我們訓練影像模型時,資料中的色度資訊天生只有亮度的一半解析度。這是「彩色邊緣比灰階邊緣更模糊」的直接原因,也是模型在處理細緻色邊界(如髮絲與背景的交界)時先天吃虧的原因。
5.4 為什麼色度降取樣在機器視覺中特別有效#
在機器視覺中,亮度色度分離的價值不只在於壓縮,而在於把問題解耦:
- 陰影去除:陰影主要是亮度(乘法)變化,色度相對穩定。因此在色度平面做運算,可以得到對陰影不敏感的特徵。
- 色彩恆常性:把色度當作「物體固有顏色」的估計,亮度當作「照明」的估計(這正是 5.3 節 Retinex 的形式化)。
- 降維:從 3 通道降到「1 亮度 + 2 色度」的表示,讓後續的機器學習模型更容易學到解耦的特徵。
六、Retinex 與 Intrinsic Image#
6.1 Land & McCann(1971)的論點#
Edward Land 與 John McCann 在 1971 年發表的論文提出一個大膽的直覺:
人類的視覺系統並不直接量測「到達眼睛的光」,而是分解出「物體的反射率」與「照明的強度」,然後感知的是前者。
這是一個物理先驗 + 心理學觀察的結合,也是影像處理史上第一個明確把「物理模型」寫進演算法的嘗試之一。
6.2 乘法模型與 log 域的加法分解#
在同質照明( \(L\) 為常數)與 Lambertian 反射(反射率與入射角無關)的假設下:
其中 \(R\) 是反射率(reflectance),即物體的「真正顏色」; \(L\) 是照明(illumination)。
關鍵的數學技巧是取對數,把乘法變成加法:
在 log 域中,照明是加性的平滑結構,反射率是乘性的高頻細節。這一分解的深遠之處在於:
- log 域的加法結構讓「估計照明」變成一個低通濾波問題(估計平滑的 \(\log L\) );
- 這就直接連到 1.3 節的傅立葉域:低通濾波器保留緩變成分,高通濾波器保留急變成分。
6.3 對數邊緣與「邊界梯度一致性」#
Land & McCann 進一步注意到,在 log 域中,跨越物體邊界的 log 梯度與照明無關(因為兩側的 \(\log L\) 相同,而物體邊界在 \(\log R\) 上有跳變)。這導致一個強大的不變量:
在物體邊界處,兩側的 \(\frac{d}{dx}\log L \approx 0\) (照明平滑),因此邊界的 log 梯度主要由反射率的跳變決定,與照明無關。這是「邊界處的 \(\log I\) 梯度大小與方向一致」的現象。
這個觀察在後續衍生出重要的演算法:
- MSR(Multi-Scale Retinex, Jobson et al. 1997):以多個高斯尺度(對應不同 \(\sigma\) )對 \(\log I\) 做高斯模糊,得到多個尺度的「照明估計」,相減後加權求和:
這是多尺度的:大 \(\sigma\) 捕捉大範圍照明(陰影),小 \(\sigma\) 捕捉局部照明(高光與細節)。這是 Retinex 家族中實務效果最好的版本。
- MPEG-4 / 相機的 HDR 合成:利用不同曝光下的 log 域關係重建 HDR 輻射圖。
6.4 為什麼 Retinex 是深度學習的前身#
這是本章最需要強調的「前後因果」鏈條:
- Retinex 的核心主張:不要在「到達眼睛的光」上運算,要分解出「反射率」與「照明」,前者才是與物體本質相關的量。
- 傳統機器視覺的實踐:Canny 邊緣、SIFT 描述子(第 2 章)都隱含地假設了某種光照不變性;LBP(局部二值模式)等紋理描述子明確設計為對單調亮度變化不變。
- 深度學習的隱式版本:卷積網路用多層非線性,實際上就在學出一種「什麼是特徵、什麼是照明」的分解。舉個具體例子:將一張人臉影像做光照變化後送進已訓練的 CNN,特徵向量的變化量遠小於送入兩個不同人臉的變化量——這就是「學出來的光照不變性」。
- 自監督與去混疊:2020 年之後的「特徵空間解耦」研究(自監督學習中的 instance discrimination、乃至 VICReg 的方差–協方差正則化)用統計正則項強迫表徵分離「變化因(nuisance factors)」與「不變因」——這幾乎就是 Retinex 目標的現代深度學習版本。
因此可以說:Retinex 第一次把「物理世界如何成像」寫進了影像處理的目標函數,而深度學習則把這個目標函數換成了可微分、可從資料中端到端學到的形式。
6.5 Intrinsic Image Decomposition#
Basile 等人(2007)與 Grosse 等人(2009)把 Retinex 的目標推廣為一個更一般的分解問題:把影像分解為
分別對應物體的反射特徵、陰影與高光(specular)。這就是 intrinsic image decomposition(本質影像分解),在計算攝影的「去陰影」與「去光澤」中成為標準前處理。
注意這裡的侷限與代價:
- 分解不唯一(同一張圖可以用無窮多種 \(R, L\) 配對解釋),必須靠正則化( \(R\) 邊緣稀疏、 \(L\) 平滑)與先驗來決定;
- 深度學習方法(intrinsic image network、RGB↔RGBD 網路等)在合成資料上表現良好,但在真實照片上泛化有限——因為真實世界的反射率與照明交互極其複雜;
- 但這個方向在 2020 年後重新熱鬧起來:NeRF 與 3D 生成模型本質上就是在做「幾何 + 反射率 + 照明」的分層建模——只不過從 2D 統計先驗升級成了 3D 幾何先驗。
七、色彩空間對照總表#
| 色彩空間 | 軸 | 物理基礎 | 優點 | 缺點 | 典型適用 |
|---|---|---|---|---|---|
| RGB(線性) | R, G, B 加法 | 光輻射量 | 加法混色物理正確、可線性運算 | 不感知均勻、紅綠軸不可分 | 影像處理、渲染、物理運算 |
| sRGB(編碼) | R', G', B' | 帶伽瑪的顯示值 | 顯示標準、8 bit 下暗部碼位充足 | 非線性,梯度/PSNR 失真 | 顯示、儲存、網頁 |
| CIE XYZ | X, Y, Z | CIE 標準觀察者 | 國際標準、 \(Y\) 即明度 | 不感知均勻、色域大 | 色彩科學、色度計算 |
| CIELAB | L*, a*, b* | XYZ + 感知均勻化 | 感知均勻、與度量吻合、ΔE 可用 | 與 RGB 轉換非線性、轉換耗時 | 色彩品質管控、膚色、ΔE 評估 |
| HSV | H, S, V | 圓柱座標 | 直覺、對陰影較不變、分割方便 | 非感知均勻、低 S 時 H 不穩 | 顏色分割、色彩選擇 |
| HSL | H, S, L | 圓柱座標 | 亮度線性對稱、適合調色 | 同 HSV,且非感知均勻 | 設計工具、调色 |
| YUV / YCbCr | Y, Cb, Cr | 亮度 + 兩色度 | 線性可逆、色度範圍固定、支援副取樣 | 色度是 RGB 的線性函數,非感知均勻 | 視訊編碼、影像處理前置 |
| CMYK | C, M, Y, K | 減法混色 | 對應印刷、黑色可控 | 不可逆、暗部偏髒、需色彩管理 | 印刷、出版 |
八、實作:OpenCV 的 HSV 分割與 Retinex 分解#
8.1 HSV 顏色分割(含陰影抑制)#
import cv2
import numpy as np
def hsv_segment(img_bgr, h_center, h_tol, s_min=80, v_min=40):
"""以色相為主、飽和度與明度為輔的分割。回傳二值遮罩。"""
hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV)
# OpenCV 的 H 是 0-179(角度除以 2),且色相是循環的:
# 需要處理跨 0 邊界的情況(例如紅色)。
lo = (h_center - h_tol) % 180
hi = (h_center + h_tol) % 180
if lo <= hi:
mask = cv2.inRange(hsv, (lo, s_min, v_min), (hi, 255, 255))
else:
m1 = cv2.inRange(hsv, (lo, s_min, v_min), (179, 255, 255))
m2 = cv2.inRange(hsv, (0, s_min, v_min), (hi, 255, 255))
mask = cv2.bitwise_or(m1, m2)
# 形態學閉運算:填補遮罩上的小孔洞,讓遮罩更連貫
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
return mask, hsv
# 示範:模擬一張有陰影的紅色物件影像
img = np.zeros((240, 320, 3), np.uint8)
img[:] = (40, 40, 40)
cv2.circle(img, (160, 120), 70, (0, 0, 200), -1) # BGR 的紅色圓
cv2.rectangle(img, (0, 170), (320, 240), (120, 80, 30), -1)
img = cv2.addWeighted(img, 0.6, np.full_like(img, 90), 0.4, 0) # 加上不均勻陰影
mask, hsv = hsv_segment(img, h_center=0, h_tol=12, s_min=60, v_min=40)
print("遮罩像素比例:", round(mask.mean() / 255, 3))
cv2.imshow("original", img)
cv2.imshow("mask", mask)
cv2.waitKey(0)
cv2.destroyAllWindows()
重點:這個遮罩在 RGB 上很難用簡單的門檻達成,因為陰影同時改變三通道。改成 HSV(以 H 為主軸)後,陰影幾乎不影響判斷。
8.2 對比:HSV 分割 vs. 直接 RGB 門檻#
b, g, r = img[:, :, 0].astype(int), img[:, :, 1].astype(int), img[:, :, 2].astype(int)
rgb_mask = ((r > 90) & (r > g * 1.6) & (r > b * 1.6)).astype(np.uint8) * 255
inter = np.logical_and(mask > 0, rgb_mask > 0).sum()
print("HSV 遮罩面積:", (mask > 0).sum(), " RGB 遮罩面積:", (rgb_mask > 0).sum())
print("IoU:", round(inter / max((mask > 0).sum() + (rgb_mask > 0).sum() - inter, 1), 3))
典型的結果是:RGB 門檻在陰影區大量漏檢(因為 \(r\) 被拉低而門檻固定),HSV 則能覆蓋整個物體。這就是「把屬性與量分離」的實用價值。
8.3 Retinex 分解#
def multiscale_retinex(img_bgr, sigmas=(1, 2, 4, 8, 16, 32), weights=None):
"""多尺度 Retinex:輸出反射率估計(log 域加權求和後轉回域)。"""
img = img_bgr.astype(np.float64) / 255.0
log_img = np.log(img + 1e-6) # log 域:乘法 -> 加法
n = len(sigmas)
if weights is None:
weights = np.ones(n) / n
out = np.zeros_like(log_img)
for w, s in zip(weights, sigmas):
illum = cv2.GaussianBlur(log_img, (0, 0), s) # 估計 log L(平滑的照明)
out += w * (log_img - illum) # 殘差 = log R
out = out - out.min()
out = out / (out.max() + 1e-9)
return (out * 255).astype(np.uint8)
# 對合成影像做分解
refl = multiscale_retinex(img)
# 單一尺度版本作為對照:把 log 域的差直接當作邊緣增強
log_img = np.log(img.astype(np.float64) / 255.0 + 1e-6)
illum = cv2.GaussianBlur(log_img, (0, 0), 15)
detail = (log_img - illum) # 細節層:一種 log-domain unsharp mask
detail_vis = ((detail - detail.min()) / (detail.max() - detail.min()) * 255).astype(np.uint8)
cv2.imshow("input", img)
cv2.imshow("retinex reflectance", refl)
cv2.imshow("log domain detail", detail_vis)
cv2.waitKey(0); cv2.destroyAllWindows()
8.4 色彩空間轉換的正確性檢查#
# OpenCV 與 skimage 的色彩轉換基準
img_u8 = np.random.default_rng(1).integers(0, 256, (64, 64, 3), dtype=np.uint8)
bgr2rgb = cv2.cvtColor(img_u8, cv2.COLOR_BGR2RGB)
assert np.array_equal(bgr2rgb[..., ::-1], img_u8)
# 檢查 YCbCr 往返(編碼域近似)
ycrcb = cv2.cvtColor(img_u8, cv2.COLOR_BGR2YCrCb)
back = cv2.cvtColor(ycrcb, cv2.COLOR_YCrCb2BGR)
err = np.abs(back.astype(int) - img_u8.astype(int))
print("YCbCr 往返最大誤差:", err.max(), " 平均:", round(err.mean(), 3))
YCbCr 往返的誤差來自編碼域近似(OpenCV 沒有先做伽瑪線性化就套用線性矩陣)。若要求嚴格的物理正確往返,必須手動做「線性化 → 矩陣變換 → 再編碼」三步:
def rgb_to_ycbcr_strict(rgb_u8):
"""嚴格流程:sRGB 解碼 -> 線性 RGB -> BT.709 YCbCr。"""
lin = srgb_to_linear(rgb_u8.astype(np.float64) / 255.0)
r, g, b = lin[..., 0], lin[..., 1], lin[..., 2]
y = 0.2126 * r + 0.7152 * g + 0.0722 * b
cb = (b - y) / 1.8556
cr = (r - y) / 1.5748
return np.stack([y, cb + 0.5, cr + 0.5], axis=-1)
這段程式碼本身就是本章的論點:同一件事(RGB 轉 YCbCr)在「編碼域」與「光線性域」是兩個不同的運算,結果不同,而正確與否取決於你的目的是「顯示/編碼」(編碼域即可)還是「物理量測/混合運算」(必須線性域)。色彩空間的選擇從來不是美學問題,而是物理問題。
九、小結與前後銜接#
- 色彩的本質是三刺激值:Young–Helmholtz(1802)給出三色視覺,CIE XYZ(1931)給出第一個國際標準,且刻意把明度獨立為 \(Y\) ——這個設計選擇預告了五十年後的 YUV。
- RGB 必須分「編碼域」與「光線性域」:sRGB 的分段傳遞函數把物理正確與 8 bit 實用性結合;在編碼域算梯度會誤導邊緣偵測,在編碼域算 PSNR 會高估品質。
- HSV 的價值是屬性分解:色相是循環量(平均要走圓周路徑),H 對陰影近似不變,這使它成為顏色分割的首選;OpenCV 的 H 為 0-179 是最常見的 bug 來源。
- CMYK 是減法混色,需要 K 是因為油墨不純、套印誤差與 TAC 限制;RGB↔CMYK 不可逆,因此需要 ICC 色彩管理。
- 亮度色度分離源於「人眼對色度解析度低」這個生理事實,導致視訊編碼的色度副取樣(4:2:0 等)與機器視覺的特徵解耦。
- Retinex(1971)第一次把物理成像模型寫進影像處理目標: \(I = R \times L\) 在 log 域變成加法,光照估計退化成低通濾波。這既是 1.3 節頻域濾波的天然橋樑,也是深度學習「學出光照不變特徵」的思想前身。
銜接 1.3 節:本章反覆出現的一個共同技術是「用低通濾波估計平滑的成分(照明),用殘差保留急變的成分(反射率/邊緣)」。這個操作在空間域需要小心設計卷積核,而在頻域只需要一次乘法。1.3 節將給出傅立葉變換的完整工具箱,讓我們第一次能夠「看見」影像的頻譜,設計理想低通、巴特沃斯與高斯濾波器,並導出卷積定理——卷積在頻域變成乘法,這是全書最重要的數學工具之一,也是後續所有 CNN 卷積層能夠高效實作的根基。
銜接第 2 章:本章的「梯度在編碼域會失真」問題,將在 2.2 節的邊緣偵測(Canny)中再次出現——Canny 的高斯預平滑步驟,除了抑制雜訊,也部分扮演了「把光線性化」的近似角色。