飽和度為 0,圖像變成灰色。
飽和度為 1,顏色恢復原樣。
看似簡單的一個滑塊,背后是顏色空間的混合運算——
而"直接灰度化"并不總是最好的選擇。
一、飽和度的直覺
在 HSB(或 HSL)色彩模型里,顏色由三個維度描述:
H(Hue,色相) → 顏色的種類(紅/綠/藍/黃…)
S(Saturation,飽和度)→ 顏色的濃淡(鮮艷 ? 灰白)
B(Brightness,亮度) → 顏色的明暗(亮 ? 暗)
調(diào)整飽和度,就是在"原始顏色"和"同亮度的灰色"之間做插值:
飽和度 = 1.0:鮮艷的紅色 (255, 0, 0)
飽和度 = 0.5:粉灰色 (191, 128, 128) ← 向灰色靠近 50%
飽和度 = 0.0:純灰色 (128, 128, 128) ← 完全變成灰色
二、數(shù)學:灰度化 + 插值
CIColorControls 的 inputSaturation 本質(zhì)上是兩步:
Step 1:計算該像素對應的灰度值
gray = 0.299 × R + 0.587 × G + 0.114 × B
這是 Rec.601 亮度公式(Day 3 詳細講過),權(quán)重來自人眼對紅綠藍的感知靈敏度差異。
Step 2:在原色和灰度之間線性插值
R' = gray × (1 - s) + R × s
G' = gray × (1 - s) + G × s
B' = gray × (1 - s) + B × s
其中 s = inputSaturation(0.0 ~ 1.0)
驗證:
-
s = 1.0:輸出 = 原始 RGB(完全保留顏色) -
s = 0.0:輸出 = (gray, gray, gray)(完全灰度化) -
s = 0.5:輸出 = 原始和灰度各占一半
具體計算示例
鮮紅色 (255, 0, 0),計算 s = 0.3:
gray = 0.299 × 255 + 0.587 × 0 + 0.114 × 0 = 76.2 ≈ 76
R' = 76 × 0.7 + 255 × 0.3 = 53.2 + 76.5 = 129.7 ≈ 130
G' = 76 × 0.7 + 0 × 0.3 = 53.2 + 0.0 = 53.2 ≈ 53
B' = 76 × 0.7 + 0 × 0.3 = 53.2 + 0.0 = 53.2 ≈ 53
s=0.3 后的顏色:(130, 53, 53) ← 帶點紅色調(diào)的暗灰
三、完全灰度化(s=0.0)并不總是最好的
初看之下,OCR 場景應該直接灰度化:數(shù)字本身是無色的,顏色只是干擾。
但實測有兩個副作用:
3.1 Vision 丟失顏色線索
Vision OCR 是多模態(tài)模型,訓練時見過大量彩色圖像。顏色是它區(qū)分相似形狀的輔助信號之一:
例:深藍背景上的白色 LOGO 文字
彩色圖:深藍(30, 80, 180) vs 白色(255, 255, 255) → 顏色對比強,Vision 輕松分割
灰度圖:亮度 ≈ 0.25 vs 亮度 ≈ 1.0 → 亮度對比也夠,沒問題
例:金色數(shù)字在米白色背景上
彩色圖:金色(255, 215, 0) vs 米白(255, 250, 220) → 顏色略有差異,Vision 可感知
灰度圖:金色亮度 ≈ 0.81 vs 米白亮度 ≈ 0.97 → 亮度差僅 0.16,對比度很弱!
結(jié)論:當顏色對比強于亮度對比時,降飽和會損失有效信息。
3.2 特定顏色組合在灰度下消失
問題場景:橙色數(shù)字在青色背景上
橙色 RGB(255, 140, 0):
gray = 0.299×255 + 0.587×140 + 0.114×0 = 76.2 + 82.2 = 158
青色 RGB(0, 200, 200):
gray = 0.299×0 + 0.587×200 + 0.114×200 = 0 + 117.4 + 22.8 = 140
灰度對比度 = |158 - 140| / 255 ≈ 7% ← 幾乎看不見!
但彩色對比度(色相完全不同):非常鮮明
這就是為什么銀行卡預處理不能無腦灰度化:不同顏色組合需要不同策略。
四、各場景的 saturation 策略
s = 1.0 完全保留顏色
適用:彩色背景對 OCR 有益,或顏色是區(qū)分字符的關(guān)鍵線索
例:深色底變體(不降飽和,顏色通道提供額外邊緣信息)
s = 0.7 輕度降飽和
適用:顏色有一定干擾,但不希望完全失去顏色線索
例:全卡自適應預處理的基線值(正常/低對比度場景)
s = 0.3 保留少量顏色
適用:背景顏色復雜,但擔心某些卡面的顏色對比消失
例:ROI 淺底提亮灰變體(在大 radius USM 之后,顏色已大幅弱化,保留 30% 兜底)
s = 0.0 完全灰度化
適用:已確認亮度對比足夠強,顏色只是噪聲
例:ROI 背景減除變體(大 radius USM 已經(jīng)消除低頻背景,顏色無意義)
ROI 超對比灰變體(高 contrast 專門壓暗處理,顏色干擾大于收益)
五、saturation 與 contrast 的聯(lián)動
降低飽和度會改變有效亮度對比度,兩者需要協(xié)調(diào)。
5.1 降飽和降低了可用對比度
原始顏色對比:
橙色 (255,140,0) 亮度 0.62
青色 (0,200,200) 亮度 0.55
顏色對比很明顯,亮度對比只有 0.07
s=0.0 灰度化后:
橙色 灰度 0.62
青色 灰度 0.55
僅靠亮度,對比度只有 7%,需要 contrast 補償
contrast 需要多高?理論上需要 1/0.07 ≈ 14 才能將對比拉至全范圍
實際上 contrast 最多用到 2.0,因此這種組合用灰度化會失敗
5.2 contrast 依賴飽和度處理后的亮度分布
推薦處理順序:
CIColorControls(saturation → contrast → brightness)
這三個參數(shù)在同一個 CIFilter 里,同時生效,內(nèi)部順序由 Core Image 決定。
實際等效于:先降飽和(得到亮度均等的圖),再拉伸亮度分布。
5.3 選擇 saturation 值的決策樹
開始
│
├─ 背景是否為復雜圖案(風景/紋理/漸變)?
│ ├─ 是 → 用 CIUnsharpMask 大 radius(25px)消背景 → s=0.0 灰度化
│ └─ 否 ↓
│
├─ 數(shù)字顏色是否與背景亮度接近(亮度差 < 0.2)?
│ ├─ 是 → 顏色對比是主要線索 → s=0.7~1.0,同時提高 contrast
│ └─ 否 ↓
│
├─ 背景顏色是否鮮艷(高飽和)?
│ ├─ 是 → 顏色干擾 Vision → s=0.0~0.3 降飽和
│ └─ 否 ↓
│
└─ 默認:s=0.7(保留顏色線索,輕度降低顏色干擾)
六、Swift 實現(xiàn)
手動實現(xiàn)(適合 MLBitmap)
public struct SaturationFilter: ImageFilter {
/// 飽和度系數(shù),0.0 = 完全灰度化,1.0 = 原色
public let saturation: Float
public func apply(to bitmap: MLBitmap) -> MLBitmap {
var result = bitmap
for i in stride(from: 0, to: result.pixels.count, by: 4) {
let r = Float(result.pixels[i])
let g = Float(result.pixels[i + 1])
let b = Float(result.pixels[i + 2])
// Rec.601 亮度
let gray = 0.299 * r + 0.587 * g + 0.114 * b
// 在原色和灰度之間插值
result.pixels[i] = UInt8(clamping: Int(gray * (1 - saturation) + r * saturation))
result.pixels[i + 1] = UInt8(clamping: Int(gray * (1 - saturation) + g * saturation))
result.pixels[i + 2] = UInt8(clamping: Int(gray * (1 - saturation) + b * saturation))
// i + 3 = Alpha,不變
}
return result
}
}
通過 CIColorControls(適合 CIImage 管線)
func adjustSaturation(_ cgImage: CGImage, saturation: Float) -> CGImage? {
let input = CIImage(cgImage: cgImage)
guard let filter = CIFilter(name: "CIColorControls") else { return nil }
filter.setValue(input, forKey: kCIInputImageKey)
filter.setValue(saturation, forKey: kCIInputSaturationKey)
// contrast / brightness 不傳時保持默認(1.0 / 0.0)
return filter.outputImage.flatMap { context.createCGImage($0, from: $0.extent) }
}
兩種方式等效,CIColorControls 在 GPU 上運行,大圖時更快。
七、"降飽和"不等于"灰度化"的證明
之前講的灰度化:
gray = 0.299R + 0.587G + 0.114B
輸出像素 = (gray, gray, gray)
CIColorControls(saturation=0.0) 的結(jié)果:
R' = gray × 1 + R × 0 = gray
G' = gray × 1 + G × 0 = gray
B' = gray × 1 + B × 0 = gray
輸出像素 = (gray, gray, gray)
兩者數(shù)學上完全等價,都是 Rec.601 加權(quán)灰度化。區(qū)別在于:
-
saturation=0.0是完整 CIColorControls 管線的一個參數(shù),可以同時調(diào)整 contrast/brightness - 手動灰度化(Day 3 的方法)是獨立的 CPU 操作,更靈活但更慢
八、小結(jié)
saturation(s)的本質(zhì):
s=1.0 → 原色
s=0.0 → Rec.601 加權(quán)灰度
中間值 → 兩者線性插值
何時降飽和:
? 背景顏色是噪聲(復雜圖案、鮮艷彩色背景)
? 已用 CIUnsharpMask 消除背景,顏色不再有意義
? 處于"超對比"模式,需要純亮度信息
何時保留顏色(s=0.5~1.0):
? 數(shù)字顏色與背景亮度接近(顏色對比 > 亮度對比)
? Vision 需要顏色線索區(qū)分相似字形
? 保底措施:不確定時先用 s=0.7,再根據(jù)識別結(jié)果調(diào)整
如果這篇對你有一點啟發(fā):點個贊,讓更多人少踩一個坑 轉(zhuǎn)發(fā)給那個正在糾結(jié)的人也歡迎關(guān)注我—— 我們一起,把認知變成長期復利。
往期推薦:
一圖了解飽和度:控制色彩鮮艷程度的關(guān)鍵
一圖了解OCR的處理流程及相關(guān)圖像處理技術(shù)
一圖了解二值化與閾值,從灰度到黑白的決策
一張圖了解圖像處理中的亮度、對比度與實現(xiàn)
顏色科學與灰度化
從"圖片"到"內(nèi)存"——你真正理解圖像處理的第一天
iPhone相冊背后的圖像處理知識(下)
iPhone相冊背后的圖像處理知識(中)
iPhone相冊背后的圖像處理知識(上)
一張圖了解圖像處理的本質(zhì)
圖像到底是什么
圖像處理技術(shù)概要圖
AI時代,軟件工程師必備概念全景圖