IP 06・色彩影像處理
先備知識: IP 05・影像復原與重建
你將學到
- 為什麼對人類觀察者來說,三個數字就足以描述一種顏色;CIE 色度圖與裝置色域是什麼
- RGB、CMY(K)、HSI 與 CIE L*a*b* 之間的關係與精確轉換公式,以及 sRGB gamma 與 YCbCr 的定位
- 偽彩色如何把灰階影像變成好讀的彩色圖,以及為什麼 viridis 這類感知均勻色彩對應表勝過彩虹色
- 如何對全彩影像做轉換、白平衡、等化、平滑、銳化與分割,以及何時該逐通道處理、何時該當成向量處理
- 彩色影像中的雜訊有什麼特性,以及向量濾波器如何去除雜訊
- 近年研究(基於學習的色彩恆常性、色彩對應表設計、深度學習上色)為經典工具帶來了什麼
先看全貌
到目前為止,每個像素都只有一個數字:亮度。彩色影像的每個像素則有三個數字。色彩幫助我們找到物體(「那台紅色的車」)、區分材質,也是大家看照片時第一個察覺「不對勁」的地方。
依照教科書的編排 [1],本章回答三個問題:怎麼精確描述色彩(色彩基礎與色彩模型)、怎麼用色彩呈現本身沒有顏色的資料(偽彩色),以及怎麼處理真正的彩色照片(轉換、濾波、分割、雜訊、壓縮)。第 3 到 5 章的灰階工具大多能沿用,但有一個反覆出現的關鍵:彩色像素是一個向量,把它當成三個互不相干的數字,有時沒問題,有時錯得離譜。
色彩基礎
白話版。 光是許多波長的混合。眼睛不會量測整個混合,而是把它壓縮成三個數字,每種色彩感測器一個。兩種差很多的光,只要產生相同的三個數字,在我們看來就一模一樣。
光、視錐細胞與三色視覺
可見光的波長大約在 400 到 700 奈米之間。光源或物體表面可以用光譜功率分布(spectral power distribution) 描述:在每個波長 發出或反射多少能量。
視網膜有三種視錐細胞(cones,對色彩敏感的感光細胞),分別對短、中、長波長最敏感。每一種視錐細胞 都有一條敏感度曲線 ,其反應為
其中 是入射光譜, 是第 種視錐細胞的敏感度。整條光譜被化約成三個數字,所以人類色覺是三色視覺(trichromatic)。直接的結果是同色異譜(metamerism):兩條不同的光譜 只要產生相同的三個反應,看起來就是同一種顏色。這正是螢幕能運作的原因:螢幕從來沒有重現檸檬的光譜,只重現了它的三個視錐反應。
亮度、色相與飽和度
人用三個知覺屬性描述顏色:
- 亮度(brightness):看起來有多少光(對無色的灰來說,這就是全部)。
- 色相(hue):主波長,也就是我們用紅、橘、藍這些字叫出來的東西。
- 飽和度(saturation):顏色有多純,也就是摻了多少白。粉紅色是低飽和度的紅。
色相與飽和度合稱色度(chromaticity),所以顏色=「色度+亮度」,這正是 HSI 模型背後的想法。
CIE 三刺激值與色度圖
為了讓色彩可以量測,國際照明委員會(CIE)在 1931 年根據人類配色實驗,制定了三條配色函數(color-matching functions)。一條光譜的三刺激值(tristimulus values)為
依設計都是非負的,而 被選成符合人眼的亮度敏感度,所以 就是輝度(luminance)。把色度與亮度分開的方法是正規化:
是色度座標, 是多餘的。把每一個純光譜色(單一波長)畫在 平面上,會得到一條馬蹄形曲線,稱為光譜軌跡(spectral locus)。連接兩端的直線是紫線(line of purples;紫色不是單一波長)。人類看得到的所有顏色都落在這個區域內。
色度圖好用,是因為兩個幾何性質:
- 混色是線性的。 兩種光的任意混合落在兩者色度之間的線段上;三種光的混合落在它們圍成的三角形內。
- 白色在中間。 參考白點,例如日光標準光源 D65,位於 ,靠近中央。從白點往軌跡移動,飽和度增加;移動的方向決定色相。
色域
使用三個原色的顯示器,只能產生三原色圍成的三角形內的顏色。這個三角形就是裝置的色域(gamut)。由於光譜軌跡是彎的,沒有任何角落可實際製造的三角形能完全涵蓋它,所以每一台三原色顯示器都會漏掉某些可見顏色,尤其是高飽和的青色與綠色。

CIE 函數原本是很長的數值表;Wyman、Sloan 與 Shirley [2] 用幾個分段高斯函數做擬合,精度足以畫出這樣的圖。每一個波瓣是 ,,而寬度 在峰值 左右兩側不同。
色彩模型
白話版。 色彩模型就是顏色的座標系統。不同工作需要不同座標:螢幕用紅綠藍光思考,印表機用墨水思考,人則用「什麼顏色、多鮮豔、多亮」思考。
RGB
在 RGB 模型中,每種顏色是單位立方體裡的一個點 。黑色是 ,白色是 ,主對角線上是各種灰。每通道 8 位元的影像有 萬種可能顏色。RGB 是加法混色:描述光的相加,也就是顯示器與相機感測器的運作方式。

單說「RGB」並不是精確的顏色: 在每台螢幕上都是不同的紅。sRGB 標準 [3] 規定了原色(與 ITU-R BT.709 [4] 相同的色度)、D65 白點與非線性編碼來解決這個問題。這個編碼常被籠統地稱為「gamma」,把儲存值 轉成線性光 :
是某個通道的儲存(編碼)值, 是線性光的值。這條曲線把較多的碼值分配給暗部,因為人眼對暗部比較敏感。實用原則:模擬物理的運算(模糊、縮放、混光)在線性值上做才比較正確,所以在乎精度時要先解碼。
CMY 與 CMYK
印刷剛好相反。白紙上的墨水會吸收光,所以印刷是減法混色。青(cyan)、洋紅(magenta)、黃(yellow)分別吸收紅、綠、藍。數值在 時:
這裡 是墨量(別和 CIE 的 搞混)。真實的 C、M、Y 墨水等量混合會變成混濁的褐色而不是黑色,所以印表機加上第四種黑色墨水 。一個簡單的轉換是
當 (純黑)時令 。實際的印表機改用量測得到的色彩描述檔,但道理一樣:把顏色中共同的「灰」移到黑色墨水。
HSI
RGB 適合硬體,卻不符合人的直覺:沒有人會說一個顏色是「70% 紅、40% 綠、20% 藍」。HSI 模型(色相 hue、飽和度 saturation、強度 intensity)把亮度與色度分開,既貼近我們談論顏色的方式,也讓許多演算法變簡單。
從幾何上看,把 RGB 立方體用黑色那個角立起來,讓灰色對角線朝上。強度是沿這條軸的高度。用垂直於軸的平面切開立方體:色相是繞軸的角度(紅在 、綠在 、藍在 ),飽和度是離軸的距離。
RGB 轉 HSI():
是像素的色彩分量與紅色軸的夾角。 是以度為單位的色相, 是飽和度, 是強度。程式裡有兩個邊界情況要小心:灰色像素()時 的分母為零,色相沒有定義,依慣例設為 ;黑色()時飽和度沒有定義,也設為 。
HSI 轉 RGB 要看 落在哪一個 扇區。在 RG 扇區():
在 GB 扇區(),先把 減去 ,再套用同樣三條公式,但把 換成 。在 BR 扇區(),減去 ,並把 換成 。每個扇區中,與色相「相對」的那個通道得到最小值 。
import numpy as np
def rgb_to_hsi(rgb):
"""rgb: float array in [0, 1] with shape (..., 3). Returns H (degrees), S, I."""
R, G, B = rgb[..., 0], rgb[..., 1], rgb[..., 2]
num = 0.5 * ((R - G) + (R - B))
den = np.sqrt((R - G) ** 2 + (R - B) * (G - B))
theta = np.degrees(np.arccos(np.clip(num / (den + 1e-12), -1, 1)))
H = np.where(B > G, 360.0 - theta, theta)
I = rgb.mean(axis=-1)
S = np.where(I > 0, 1 - rgb.min(axis=-1) / (I + 1e-12), 0.0)
H = np.where(den > 1e-12, H, 0.0) # gray pixels: hue undefined -> 0
return H, S, I
def hsi_to_rgb(H, S, I):
"""H in degrees, S and I in [0, 1]. Inverse of rgb_to_hsi."""
H = np.mod(H, 360.0)
out = np.zeros(H.shape + (3,))
# (sector start, index of the 'big' channel, of the smallest, of the rest)
for lo, (a, b, c) in ((0, (0, 2, 1)), (120, (1, 0, 2)), (240, (2, 1, 0))):
m = (H >= lo) & (H < lo + 120)
h = np.radians(H[m] - lo)
x = I[m] * (1 - S[m])
y = I[m] * (1 + S[m] * np.cos(h) / np.cos(np.radians(60) - h))
out[m, a], out[m, b], out[m, c] = y, x, 3 * I[m] - (x + y)
return out
prim = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1], [0.5, 0.5, 0.5]], float)
print(np.round(np.stack(rgb_to_hsi(prim), -1), 3))
# [[ 0. 1. 0.333] [120. 1. 0.333] [240. 1. 0.333] [ 0. 0. 0.5 ]]
來回轉換 hsi_to_rgb(*rgb_to_hsi(x)) 對隨機 RGB 輸入的誤差約在 以內。

CIE L*a*b*
HSI 把亮度和色度分開了,但 HSI 中相同的步長看起來並不一樣大。CIE 1976 L*a*b* 色彩空間 [5] 的設計目標,是讓歐氏距離大致對應感知上的差異。它由相對於參考白點 的三刺激值計算:
是明度(lightness), 從綠(負)到紅(正), 從藍(負)到黃(正)。立方根模仿人眼的壓縮式反應,線性段則避免在接近黑色時斜率無限大。色差 就是兩個 點之間的歐氏距離。
從 sRGB 檔案出發:先解 gamma(上面的公式),把線性 RGB 乘上 sRGB 到 XYZ 的矩陣,再以 D65 白點套用上述公式。
import numpy as np
from skimage import color
M = np.array([[0.4124, 0.3576, 0.1805], # linear sRGB -> XYZ (D65)
[0.2126, 0.7152, 0.0722],
[0.0193, 0.1192, 0.9505]])
def srgb_to_lab(rgb):
lin = np.where(rgb <= 0.04045, rgb / 12.92, ((rgb + 0.055) / 1.055) ** 2.4)
xyz = lin @ M.T / np.array([0.95047, 1.0, 1.08883]) # divide by D65 white
d = 6 / 29
f = np.where(xyz > d**3, np.cbrt(xyz), xyz / (3 * d**2) + 4 / 29)
return np.stack([116 * f[..., 1] - 16,
500 * (f[..., 0] - f[..., 1]),
200 * (f[..., 1] - f[..., 2])], -1)
x = np.random.default_rng(0).random((1000, 3))
print(np.abs(srgb_to_lab(x) - color.rgb2lab(x[None])[0]).max()) # about 0.02
微小的殘差來自矩陣的四捨五入;skimage.color.rgb2lab 用了更多位數。
一段話看懂 YCbCr
影像與視訊編碼器會把亮度訊號(luma) 和兩個色差訊號(chroma)分開,因為人眼對亮度細節的解析力遠高於色彩細節。ITU-R BT.601 [6] 對 gamma 編碼後的 定義
、,再縮放並平移到 8 位元的「studio」範圍( 在 , 在 )。HDTV 使用 BT.709 [4],其亮度權重不同()。兩者搞混會造成輕微但看得出來的偏色。skimage.color.rgb2ycbcr 實作的是 BT.601 studio 範圍版本。
偽彩色影像處理
白話版。 偽彩色(pseudocolor,又稱假色)就是幫灰階影像上色,讓細微的亮度差異變得容易看見。這些顏色不是「真的」,而是一種代碼,就像氣象圖上的顏色。
動機來自知覺:人能分辨的色彩組合遠多於灰階層次。熱像儀、X 光、深度圖或高程模型每個像素只有一個值,但用顏色呈現這個值,可以顯露在灰階中會被忽略的結構。
強度切片
最簡單的做法是把灰階範圍切成數段,每段給一個顏色。設門檻 與顏色 :
是輸入灰階值, 是灰階數, 是分段數, 是輸出顏色。若把 畫成 3-D 地形,每個門檻就是一個水平面切過「強度地形」,名稱由此而來。 時就是用兩種顏色畫出的二值化。當分段本身有意義時(「溫度高於 40 °C」、「骨骼與軟組織」),強度切片(intensity slicing)最合適。
強度到色彩的轉換
更一般的做法是對灰階值套用三個獨立函數:
把灰階值對應到紅、綠、藍輸出。強度切片是三者都是階梯函數的特例;平滑的函數則構成連續色彩對應表(colormap)。同樣的想法可推廣到多張輸入影像:對多光譜資料,把三個波段(例如近紅外、紅、綠)分別指定給顯示的三個通道,就得到假色合成影像。
感知均勻的色彩對應表
怎麼選,比看起來重要得多。常見的彩虹色對應表(jet)中段經過明亮的黃與青,兩端則變暗。它的明度不是單調的:資料上相等的步長在視覺上不相等,平坦區段看起來像邊緣,真正的邊緣反而可能消失。Kovesi [7] 指出,一些廣泛使用的色彩對應表有感知上的「平坦區」,可以藏住大到資料範圍十分之一的特徵,並主張色彩對應表應該設計成明度均勻變化。
viridis 由 van der Walt 與 Smith 設計,現在是 Matplotlib 的預設 [8]。它在 CAM02-UCS 色貌空間中被最佳化為感知均勻,轉成灰階列印時仍然均勻,對常見的色覺異常也易於辨讀。Crameri、Shephard 與 Heron [9] 在各科學領域提出同樣的主張:彩虹色與紅綠色對應表會扭曲資料,並把約 8% 有色覺異常的男性排除在外。

import numpy as np
import matplotlib.pyplot as plt
from skimage import data, util, color
g = util.img_as_float(data.moon())
# intensity slicing into 8 equal bands
bands = np.digitize(g, np.linspace(0, 1, 9)[1:-1]) # 0..7
palette = plt.get_cmap("tab10")(np.arange(8))[:, :3]
sliced = palette[bands] # (H, W, 3)
# continuous intensity-to-color transformation
rgb = plt.get_cmap("viridis")(g)[..., :3]
# check a colormap's lightness profile
L = color.rgb2lab(plt.get_cmap("jet")(np.linspace(0, 1, 256))[None, :, :3])[0, :, 0]
print(np.all(np.diff(L) >= 0)) # False: jet is not monotonic
經驗法則:有順序的資料用循序型(sequential)均勻對應表(例如 viridis);有意義的中點(零、門檻)時用發散型(diverging);角度與色相用循環型(cyclic);只有無序類別才用定性型(qualitative)調色盤,例如強度切片。
全彩影像處理基礎
白話版。 處理彩色影像有兩種方式:當成三張獨立的灰階影像,或把每個像素看成色彩空間中的小箭頭,直接處理箭頭。有時兩者結果相同,但常常不同。
一個全彩像素就是向量
處理它有兩種方式:
- 逐通道(per-channel,componentwise)。對每個通道各自套用灰階方法,再疊回去。
- 向量處理(vector processing)。直接對向量 運算,使用同時考慮三個分量的操作,例如向量距離、向量中值或彩色梯度。
逐通道處理要和向量處理結果相同,需要兩個條件:運算同時適用於純量與向量,而且對每個分量的作用相同、不會混在一起。線性鄰域運算兩者都滿足。對每個通道在視窗內取平均,等於對向量在視窗內取平均:
其中 是以 為中心的鄰域, 是其像素數。非線性運算會打破這個等價關係。逐通道中值可能從第一個像素取紅、從第二個取綠、從第三個取藍,組出鄰域裡根本不存在的顏色。門檻、邊緣偵測與直方圖等化也都是需要向量觀點的例子,後面各節會說明。
色彩轉換
白話版。 色彩轉換就是一個只看單一像素、改變它顏色的配方:讓它變暗、翻成相反色、調暖一點,或者只有「夠橘」時才保留它。
數學表述
和第 3 章一樣,點轉換寫成 ,只是現在 與 都是色彩向量。逐分量寫出來是
其中 、 是第 個輸入與輸出分量, 是分量數(RGB 與 HSI 為 ,CMYK 為 )。每個輸出都可以依賴所有輸入。
任何效果都能在任何模型中寫出來,但成本不同。把強度乘上 ,在 HSI 中只需一次乘法(),在 RGB 中要三次(),在 CMY 中則變成仿射映射()。選擇讓運算最自然的模型,但別忘了轉換本身要花時間,也會在暗部或灰色像素放大雜訊,因為那裡的色相與飽和度不穩定。
補色
色環上位置相對的色相互為補色(complements):紅與青、綠與洋紅、藍與黃。在 RGB 中,補色就是照相負片:
也就是第 3 章灰階負片的彩色版。在 HSI 中,色相補色是 ,但 RGB 負片同時反轉了強度,因此不能只靠色相算出來。補色有助於顯露暗部細節。
色彩切片
色彩切片(color slicing)是彩色版的強度切片:保留顏色接近某個原型(prototype) 的像素,其餘推向中性色。「接近」常見的兩種定義:
是以 為中心的立方體寬度, 是球的半徑, 是輸入色彩向量, 是中灰。球對各方向一視同仁;立方體計算較便宜,也方便逐通道推理。比較友善的變形是讓沒選中的像素保留自己的灰階值,而不是一片平灰,這樣仍看得到上下文。
import numpy as np
from skimage import data, util, color
img = util.img_as_float(data.astronaut())
a = np.array([0.85, 0.40, 0.20]) # prototype: the orange of the suit
d = np.linalg.norm(img - a, axis=-1) # distance of every pixel to a
gray = color.gray2rgb(color.rgb2gray(img))
sliced = np.where((d <= 0.25)[..., None], img, gray)
print((d <= 0.25).mean()) # fraction kept, about 0.165

色調與色彩校正
色調校正(tone correction)調整亮度與對比,最好只作用在類似強度的通道(、 或 )上,以保留色相。偏亮(high-key)的影像需要拉開亮部的曲線,偏暗(low-key)的影像需要提亮暗部的曲線,平淡的影像則需要 S 形曲線。
色彩平衡(color balance)校正用來去除色偏:要減少某個顏色,就增加它的補色,或減少它在色環上的鄰居。青色太多?增加紅色,或同時減少藍與綠。場景中放一張灰卡就有精確的目標:校正後灰卡的 R、G、B 應該相等。
實務流程會透過校正過的色彩描述檔,把各裝置的顏色轉到與裝置無關的空間(例如 L*a*b*),讓顏色在螢幕與紙上看起來一致;超出某裝置色域(圖 6.1)的顏色必須往內映射。
白平衡
白平衡(white balance)是最常見的自動色彩校正。相機記錄的是表面顏色乘上光源顏色,所以同一面白牆在鎢絲燈下偏橘、在陰影中偏藍。我們的視覺系統大多能把光源的影響扣掉(色彩恆常性,color constancy),相機則必須用計算來做到。
標準模型是對角模型:光源顏色為 時,觀察到的像素為 ,其中 是表面在白光下的反應。校正就是除以估計的光源,(通常再縮放以維持整體亮度)。困難在於只靠一張影像估計 。經典的估計法都對場景做了某種假設:
- 灰色世界(gray world)[10]:場景的平均反射率是無彩色的,所以 影像的平均 RGB。
- 白色區塊(white patch,max-RGB)[11], [14]:最亮的表面是白色,所以 各通道最大值(實務上用高百分位數,以忽略少數飽和像素)。
- 灰色邊緣(gray edge)[11]:平均邊緣(影像導數)是無彩色的。van de Weijer、Gevers 與 Gijsenij 證明三者屬於同一個家族:
逐通道計算,其中 是以尺度 的高斯平滑過的影像, 是導數階數, 是 Minkowski 範數, 是常數。 是灰色世界; 是 max-RGB; 是灰色邊緣。
import numpy as np
from skimage import data, util
def white_balance(img, method="gray-world", p=99.5):
"""Diagonal white balance. img: float RGB in [0, 1]."""
px = img.reshape(-1, 3)
e = px.mean(0) if method == "gray-world" else np.percentile(px, p, axis=0)
gain = e.mean() / e # one gain per channel
return np.clip(img * gain, 0, 1), e
img = util.img_as_float(data.astronaut()) * 0.8
biased = np.clip(img * [0.75, 0.95, 1.25], 0, 1) # add a bluish cast
fixed, e = white_balance(biased, "white-patch")
print(e.round(3)) # [0.598 0.757 0.996]
拖動滑桿,比較偏藍的輸入與白色區塊法的校正結果。

偏藍白色區塊法校正這些假設都會在某些場景失敗。下方的貓照片真實平均色是橘褐色,灰色世界法把溫暖的毛色「校正」成灰,留下藍色調;白色區塊法則靠白色鬍鬚與高光表現好得多。每張結果上方標示的是估計光源與真實光源之間的角度誤差(angular error,這個領域的標準指標)。

彩色影像的直方圖處理
對 R、G、B 三個直方圖分別做等化,通常會產生錯誤的顏色,因為它改變了通道之間的比例,也就改變了色相。比較安全的做法是只等化強度通道、保留色度:
- 轉到能分離亮度的模型(HSI、HSV 或 L*a*b*)。
- 用第 3 章的方法等化 (或 、)。
- 轉回來。
import numpy as np
from skimage import data, util, exposure, color
img = util.img_as_float(data.astronaut())
per_channel = np.stack([exposure.equalize_hist(img[..., k]) for k in range(3)], -1) # shifts hues
lab = color.rgb2lab(img)
lab[..., 0] = 100 * exposure.equalize_hist(lab[..., 0] / 100) # lightness only
better = np.clip(color.lab2rgb(lab), 0, 1)
色相保住了,但有些顏色可能看起來變淡,或在轉回時超出色域,因此有時需要裁切或稍微提高飽和度。區域直方圖方法(第 3 章)也能用同樣方式套用在 上。
彩色影像的平滑與銳化
白話版。 要模糊或銳化彩色照片,可以對三個通道分別處理,因為這些是線性運算。不過只處理亮度,結果往往更乾淨。
平滑
彩色影像的鄰域平均為
其中 是鄰域, 是鄰域大小。如前所述,這等於對每個通道分別平均,所以對 3 通道陣列呼叫 cv2.GaussianBlur 是正確的。
另一種做法是只平滑 HSI(或 )影像的強度,保留色相與飽和度。兩者在邊緣附近不同:逐通道平滑會在紅綠交界產生中間色(一條橄欖色的線),只平滑強度則保留原本的色相、只柔化亮度。要選哪種取決於任務:去雜訊通常會對色度做比亮度更強的平滑,因為人眼對色度模糊較不敏感(這也正是 YCbCr 色度降取樣所利用的事實)。
銳化
向量的 Laplacian 就是各分量 Laplacian 組成的向量:
所以 Laplacian 銳化 (強度 )可以逐通道進行。反銳化遮罩(unsharp masking,第 3 章)也一樣。實務上只銳化強度通道,可以避免高對比邊緣周圍出現彩色光暈(色邊),這種光暈來自三個通道過衝的程度不同。
在影像分割中使用色彩
白話版。 色彩是把影像切成幾塊最容易的線索之一:「褐色的都是染色組織,藍色的都是背景」。問題在於怎麼量化「夠褐」。
在 HSI 空間中分割
如果目標是用色相描述的,HSI 是自然的選擇。典型流程:先對飽和度設門檻,做出「真的有顏色」的像素遮罩(低飽和度時色相沒有意義),把色相影像乘上遮罩,再對遮罩後的色相設門檻或看直方圖,挑出目標範圍。弱點在於色相的奇異點:接近灰色的像素中,雜訊會產生隨機色相,所以飽和度遮罩要先做。
在 RGB 向量空間中分割
RGB 向量常常效果更好,因為它避開了不穩定的色相計算。從目標中選一組樣本像素,算出平均色 ,再依每個像素 到 的距離分類。歐氏距離為
的像素標為目標,這個集合是半徑 的實心球。但樣本顏色通常在某些方向上分散得比較開(以染色組織來說,主要是由亮到暗的方向)。Mahalanobis 距離把這點納入考量:
其中 是樣本的 共變異數矩陣。此時 是順著資料方向的橢球。更便宜的近似是以 為中心的外接盒(bounding box),每個軸的半寬取該通道標準差的倍數;判斷是否落在盒內只需比較,不需平方或開根號。
import numpy as np
from skimage import data, util
img = util.img_as_float(data.immunohistochemistry())
samples = img[60:100, 60:100].reshape(-1, 3) # a brown (stained) region
a, C = samples.mean(0), np.cov(samples.T)
z = img.reshape(-1, 3) - a
d_euc = np.sqrt((z ** 2).sum(1)).reshape(img.shape[:2])
d_mah = np.sqrt(np.einsum("ij,jk,ik->i", z, np.linalg.inv(C), z)).reshape(img.shape[:2])
mask_euc, mask_mah = d_euc <= 0.25, d_mah <= 3.0

彩色邊緣偵測
對每個通道各算梯度再相加是常見的捷徑,但那並不是向量影像的梯度。更糟的是,在灰階版本上做邊緣偵測,會漏掉等亮度(isoluminant)邊緣,也就是兩種亮度相同的顏色之間的邊界。Di Zenzo [13] 為多通道影像定義了正確的梯度。令 、,定義
它們組成 結構張量 。變化最大的方向,以及該方向上的變化率為
是梯度方向(用 arctan2,並同時檢查 ,因為公式會給出兩個極值), 是邊緣強度。等價地, 是結構張量的最大特徵值 ,數值上這樣算比較穩定。

import numpy as np
from skimage import filters
def di_zenzo(img):
"""Edge strength of a float RGB image via the largest structure-tensor eigenvalue."""
gx = np.stack([filters.sobel_v(img[..., k]) for k in range(3)], -1) # d/dx
gy = np.stack([filters.sobel_h(img[..., k]) for k in range(3)], -1) # d/dy
gxx, gyy, gxy = (gx * gx).sum(-1), (gy * gy).sum(-1), (gx * gy).sum(-1)
return np.sqrt(0.5 * (gxx + gyy + np.sqrt((gxx - gyy) ** 2 + 4 * gxy ** 2)))
彩色影像中的雜訊
白話版。 每個色彩通道都有自己的雜訊。把有雜訊的通道轉回色相與飽和度時,雜訊會混在一起,看起來可能糟得多,尤其是在暗部或灰色區域。
第 5 章的雜訊模型可套用到每個通道。相機中的雜訊主要產生於感測器,也就是在色彩重建之前。大多數感測器前面有一片彩色濾光陣列,每個感光點只量一種顏色,另外兩種靠內插補出來(去馬賽克,demosaicking);內插會讓雜訊擴散並在通道之間產生相關。Lukac 等人 [12] 回顧了這整條處理鏈。
兩個觀察影響了實務做法:
- 色彩轉換會重新分配雜訊。 強度 是三個通道的平均,所以每通道變異數為 的獨立零均值雜訊,在 中變成 :強度比任何單一通道都乾淨。相反地,色相與飽和度是在灰色軸附近導數很大的非線性函數,因此會變得更雜。這就是為什麼去雜訊器常在亮度—色度空間中運作,並對色度做更強的平滑。
- 單一通道的脈衝雜訊是一個彩色斑點。 只出現在綠色通道的「鹽」雜訊會變成一個亮綠點。逐通道中值濾波器可以去除它,但在混合情況下可能產生鄰域裡沒有的顏色。
向量中值濾波器(vector median filter)避免了捏造顏色。在每個視窗中,它輸出與視窗內所有其他向量距離總和最小的那個輸入向量 [12]:
其中 是視窗內色彩向量的集合。輸出一定是輸入之一;對一維資料,它就退化成一般的中值。
import numpy as np
from skimage import data, util
def vector_median(img, k=3):
r = k // 2
H, W, _ = img.shape
p = np.pad(img, ((r, r), (r, r), (0, 0)), mode="edge")
nb = np.stack([p[i:i + H, j:j + W] for i in range(k) for j in range(k)], 2) # (H, W, k*k, 3)
D = np.linalg.norm(nb[:, :, :, None] - nb[:, :, None, :], axis=-1).sum(-1) # (H, W, k*k)
return np.take_along_axis(nb, D.argmin(-1)[..., None, None], 2)[:, :, 0]
img = util.img_as_float(data.astronaut())
rng = np.random.default_rng(0)
noisy = img.copy()
hit = rng.random(img.shape[:2]) < 0.05
noisy[hit] = rng.random((hit.sum(), 3)) # 5% random color impulses
out = vector_median(noisy)
psnr = lambda x: 10 * np.log10(1 / ((x - img) ** 2).mean())
print(round(psnr(noisy), 1), round(psnr(out), 1)) # about 20.4 -> 31.3 dB
這種向量化寫法每個像素使用的記憶體與 成正比;視窗大時請改用迴圈。
彩色影像壓縮
白話版。 彩色影像的資料量是灰階的三倍,但其中很多是冗餘的:三個通道長得很像,而且人眼看不太到色彩細節。編碼器同時利用這兩點:
- 去除通道間的相關性。 R、G、B 高度相關(圖 6.2 中三張影像非常相似)。轉成一個亮度加兩個色差,例如 YCbCr [6],能把大部分能量集中在一個通道。
- 色度降取樣(chroma subsampling)。人眼對色彩細節的解析力不如亮度細節,所以色度通道可以只在水平方向用一半解析度(4:2:2),或在兩個方向都用一半(4:2:0)儲存,看起來幾乎沒有損失。光是 4:2:0 就讓原始資料減半:每個像素 個樣本,而不是 3 個。
之後每個通道再用第 8 章的轉換、量化與熵編碼方法壓縮。常見的色彩瑕疵有:銳利邊緣處的滲色(來自色度降取樣),以及高壓縮率下的塊狀色斑。
現代觀點
上面的經典工具至今仍在每一台相機與每一個影像函式庫裡運作。大約 2010 年後改變的,主要是參數怎麼決定:從固定的假設,變成從資料中學習。以下五篇回顧與綜述性質的論文描繪了這個領域的版圖。
色彩恆常性:從假設到學習。 Gijsenij、Gevers 與 van de Weijer [14] 綜述了計算色彩恆常性,把方法分成三類:採用固定假設的靜態方法(灰色世界、白色區塊、灰色邊緣及上面的 Minkowski 家族)、把觀察到的顏色與標準光源下的顏色集合比較的色域式方法,以及用已知光源影像訓練的學習式方法。他們也提出比較方法的準則,並在公開資料集上評測公開演算法;反覆出現的結論是,沒有任何單一固定假設適用所有場景。之後學習式方法成為主流。Fast Fourier Color Constancy(FFCC)[15] 把光源估計轉成在 log-chroma 直方圖中找峰值,並在頻域中高效求解;它輸出光源的完整機率分布,能在手機上即時執行,讓視訊中的自動白平衡在時間上平順。FC4 [16] 是全卷積網路,每個影像區塊都以學到的信心度替光源投票,所以一面素牆的權重比人臉或白色物體低。Afifi 與 Brown [17] 則在相機已經把影像渲染成非線性 sRGB 之後才校正白平衡,這是對角模型做不到的:他們的網路把影像重新渲染成其他白平衡設定下的樣子。對角模型仍是骨幹,但光源估計與在渲染後空間中的校正,現在都是學出來的。
彩色向量濾波。 Lukac 等人 [12] 回顧了把彩色像素當成向量的濾波器:向量中值與向量方向濾波器及其變形、向量邊緣偵測器,以及去馬賽克。他們的結論與本章主題一致:逐分量處理忽略了通道間的相關性,可能產生色彩瑕疵,而向量順序統計量能保留輸入的顏色。在有訓練資料與運算資源時,學習式去雜訊器如今更強,但這些濾波器在講求速度與可預測性的場合仍是標準基準。
色彩對應表設計。 Kovesi [7] 為線性、發散、彩虹與循環色彩對應表提出設計方法與測試影像,核心原則只有一個:感知明度必須沿著對應表均勻變化。viridis 系列 [8] 在 Matplotlib 2.0 成為預設。Crameri 等人 [9] 檢視了科學出版中色彩的使用方式,主張非均勻與紅綠色對應表應從軟體預設中消失。深度學習在這裡沒有改變任何事:問題在於人類知覺,答案是在感知色彩空間中量測的細心設計。
以深度學習上色。 偽彩色用固定規則把灰階對應到顏色;上色(colorization)則試圖預測真實的顏色。Zhang、Isola 與 Efros [18] 把上色視為對量化色彩的分類問題,並對稀有類別做再平衡,避免鮮豔的顏色被平均成褐色;他們以「上色圖靈測試」評估,請受試者判斷影像是否為真。Anwar 等人 [19] 綜述深度上色方法,將其分為七類,討論損失函數與評估指標,並提出新的基準資料集。DDColor [20](ICCV 2023)使用兩個解碼器,一個恢復空間細節,一個透過與多尺度語意特徵的交叉注意力精修色彩,再加上鼓勵色彩豐富度的損失;這減少了顏色跨越物體邊界的滲色。兩個經典想法始終存在:在 L*a*b* 中運作(預測色度、保留給定的明度),以及把顏色當成一個分布,因為一件灰色襯衫原本可能是任何顏色。
沒有改變的部分。 色度學(CIE XYZ、色度、色域)、編碼(sRGB、BT.601/709 YCbCr)與色差空間是標準,不是要學的東西。網路用 sRGB 資料訓練,也在這些空間中被評估。清楚知道你的張量處於哪個空間(編碼或線性、BT.601 或 BT.709、studio 或 full range),仍是色彩工作中最實用的技能之一。
重點整理
- 人類視覺是三色視覺,所以三個數字就能為人類觀察者描述顏色;反應相同的不同光譜(同色異譜)看起來一樣。
- CIE xy 色度圖呈現所有可見色度;三原色裝置只能涵蓋其中一個三角形(色域)。
- RGB 適合硬體,CMY(K) 適合墨水,HSI 把強度與色相、飽和度分開,L*a*b* 讓歐氏距離大致符合感知。注意灰色像素的色相沒有定義、sRGB gamma,以及 BT.601 與 BT.709 的差異。
- 偽彩色讓灰階資料更好讀;請用 viridis 這類感知均勻的色彩對應表,而非彩虹色。
- 線性運算(平滑、Laplacian 銳化)可以逐通道做;非線性運算(中值、直方圖等化、邊緣偵測、分割)應把像素當向量處理,或只處理強度通道。
- 白平衡就是除以估計的光源;灰色世界、白色區塊與灰色邊緣是同一個框架的特例,現代系統則用學習來估計。
- 用向量距離(歐氏、Mahalanobis、外接盒)分割色彩,用能看見等亮度邊界的 Di Zenzo 梯度偵測彩色邊緣。
- 強度比任何單一通道雜訊更少,色相與飽和度則更雜;向量中值濾波器能去除彩色脈衝雜訊而不捏造新顏色。
練習
- 某像素 。請手算它的 H、S、I。接著不計算,預測 B 增加到 時 H 會怎麼變。
提示
。,所以 。計算 :分子 ,分母 ,所以 ;因為 ,(黃色)。B 增加到 時仍有 ,所以色相維持 ,只有 S 下降。
- 證明 RGB 負片 會把 HSI 座標為 的像素,映射到強度為 、色相為 的像素(非灰色像素)。飽和度會保持不變嗎?
提示
強度由線性性質直接得到。對色相而言,色彩分量 只是變號,相當於繞灰色軸旋轉 。飽和度是 ;取負片後最小值變成 ,強度變成 ,所以飽和度一般會改變。試試 。
- 你對一張藍天照片的 R、G、B 直方圖分別做等化。解釋為什麼天空可能變成灰青色,並提出兩種修正方式。
提示
等化會把每個通道拉滿整個範圍。原本幾乎一致偏高的通道(藍)相對其他通道會被壓低,而弱的通道被拉高,所以 R:G:B 比例改變。修正方式:只等化 、 或 ;或用平均直方圖算出一個共同的映射,套用到三個通道。
- 在灰色世界/白色區塊的框架中,、 會得到什麼估計法?把
white_balance的想法用在貓的影像上,取 ( 用高百分位數),畫出角度誤差對 的關係。
提示
用每個通道的均方根值估計光源,介於平均值與最大值之間。以本章的合成偏色而言,誤差從約 12°()降到約 11°()、7°()與 4°()。在這張影像上誤差隨 增大而下降,因為明亮的白色細節比橘色的平均更能透露光源。在沒有白色物體的場景中,趨勢可能相反。
- 設計一個在戶外照片中分割綠色植物、而且不受陰影影響的色彩分割器。你會用哪個空間與哪種距離?為什麼單純的 RGB 歐氏距離可能失敗?
提示
陰影會把三個通道等比例縮小,使像素沿著一條通過原點的直線移動,RGB 歐氏距離因此大幅改變。改用不受縮放影響的色度特徵(正規化的 、,或加上飽和度遮罩的色相,或 ),或用以陽光下與陰影中樣本訓練出共變異數的 Mahalanobis 距離。
- 解釋為什麼向量中值濾波器永遠不會產生新顏色,並舉一個 3 個像素的一維例子,說明逐通道中值會產生新顏色。
提示
依定義,向量中值是視窗內的輸入向量之一。對三個像素 、、(黃、洋紅、青),逐通道中值是 ,純白色,而視窗中根本沒有白色。
本章所有圖片都由 scripts/figures/dip_ch06.py 從 skimage.data 影像與合成陣列產生。
參考文獻
- R. C. Gonzalez and R. E. Woods, Digital Image Processing, 4th ed., Pearson, 2018, Ch. 6. publisher page
- C. Wyman, P.-P. Sloan, and P. Shirley, “Simple Analytic Approximations to the CIE XYZ Color Matching Functions,” Journal of Computer Graphics Techniques, vol. 2, no. 2, 2013. jcgt.org
- IEC 61966-2-1:1999, “Multimedia systems and equipment – Colour measurement and management – Part 2-1: Colour management – Default RGB colour space – sRGB,” IEC, 1999. IEC webstore
- Recommendation ITU-R BT.709-6, “Parameter values for the HDTV standards for production and international programme exchange,” ITU, 2015. ITU
- ISO/CIE 11664-4:2019, “Colorimetry – Part 4: CIE 1976 L*a*b* colour space,” ISO/CIE, 2019. standard page
- Recommendation ITU-R BT.601-7, “Studio encoding parameters of digital television for standard 4:3 and wide screen 16:9 aspect ratios,” ITU, 2011. ITU
- P. Kovesi, “Good Colour Maps: How to Design Them,” arXiv:1509.03700, 2015. arXiv
- S. van der Walt and N. Smith, viridis colormap design notes (BIDS project page; presented at SciPy 2015), 2015. project page
- F. Crameri, G. E. Shephard, and P. J. Heron, “The misuse of colour in science communication,” Nature Communications, vol. 11, art. 5444, 2020. PMC
- G. Buchsbaum, “A spatial processor model for object colour perception,” Journal of the Franklin Institute, vol. 310, no. 1, pp. 1–26, 1980. DOI
- J. van de Weijer, T. Gevers, and A. Gijsenij, “Edge-Based Color Constancy,” IEEE Transactions on Image Processing, vol. 16, no. 9, pp. 2207–2214, 2007. DOI
- R. Lukac, B. Smolka, K. Martin, K. N. Plataniotis, and A. N. Venetsanopoulos, “Vector Filtering for Color Imaging,” IEEE Signal Processing Magazine, vol. 22, no. 1, pp. 74–86, Jan. 2005. PDF
- S. Di Zenzo, “A note on the gradient of a multi-image,” Computer Vision, Graphics, and Image Processing, vol. 33, pp. 116–125, 1986. DOI
- A. Gijsenij, T. Gevers, and J. van de Weijer, “Computational Color Constancy: Survey and Experiments,” IEEE Transactions on Image Processing, vol. 20, no. 9, pp. 2475–2489, 2011. DOI
- J. T. Barron and Y.-T. Tsai, “Fast Fourier Color Constancy,” CVPR, 2017. arXiv
- Y. Hu, B. Wang, and S. Lin, “FC4: Fully Convolutional Color Constancy with Confidence-Weighted Pooling,” CVPR, 2017. paper
- M. Afifi and M. S. Brown, “Deep White-Balance Editing,” CVPR, 2020. arXiv
- R. Zhang, P. Isola, and A. A. Efros, “Colorful Image Colorization,” arXiv:1603.08511, 2016. arXiv
- S. Anwar, M. Tahir, C. Li, A. Mian, F. S. Khan, and A. W. Muzaffar, “Image Colorization: A Survey and Dataset,” arXiv:2008.10774, 2020. arXiv
- X. Kang, T. Yang, W. Ouyang, P. Ren, L. Li, and X. Xie, “DDColor: Towards Photo-Realistic Image Colorization via Dual Decoders,” ICCV, 2023. arXiv