【圖像處理】飽和度——顏色的濃淡與灰度化

飽和度為 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ù)學:灰度化 + 插值

CIColorControlsinputSaturation 本質(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時代,軟件工程師必備概念全景圖

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容