【图像处理】二值化与阈值——从灰度到黑白的决策

二值化是图像处理中最"武断"的操作: 每个像素只有两种命运------要么全黑,要么全白。 但正是这种武断,让机器能够"读懂"文字、识别形状。


一、什么是二值化

二值化(Binarization / Thresholding):将灰度图像转换为只有黑(0)和白(255)两种值的图像。

复制代码
输入:灰度图(像素值 0~255)
输出:二值图(像素值只有 0 或 255)

规则:
  像素亮度 ≥ 阈值(threshold) → 白色(255)
  像素亮度 < 阈值              → 黑色(0)

为什么需要二值化?

人类用眼睛看图时,自然能区分前景和背景。但计算机处理灰度图时,"哪里是字,哪里是纸"这个问题并不简单------所有像素都是 0~255 之间的连续值。

二值化把这个连续问题变成了离散问题:任意像素要么属于前景,要么属于背景,没有中间地带。

典型应用场景

场景 说明
OCR(文字识别) 把扫描文档变成黑白,文字和背景分离
条码/二维码识别 黑条白条的判断
医学图像分割 从 CT 图像中分离骨骼和软组织
手写识别 提取笔画轮廓
运动检测 前景物体 vs 静态背景

二、固定阈值法

公式

css 复制代码
I' = 255  if I ≥ T
I' = 0    if I < T

其中 T 是手动指定的阈值,默认通常取 128(中间灰)。

实现

swift 复制代码
public struct ThresholdFilter: ImageFilter {

    /// 阈值,像素亮度 ≥ threshold 变白,< threshold 变黑
    public let threshold: UInt8

    public init(threshold: UInt8 = 128) {
        self.threshold = threshold
    }

    public func apply(to bitmap: MLBitmap) -> MLBitmap {
        // Step 1:先灰度化(Sobel 只对亮度操作,不是颜色)
        let gray = GrayscaleFilter().apply(to: bitmap)
        var result = gray

        for i in stride(from: 0, to: result.pixels.count, by: 4) {
            let luminance = result.pixels[i]   // 灰度图中 R = G = B
            let v: UInt8 = luminance >= threshold ? 255 : 0
            result.pixels[i]     = v
            result.pixels[i + 1] = v
            result.pixels[i + 2] = v
            // Alpha 不变
        }

        return result
    }
}

为什么先灰度化?

彩色图像的 RGB 三通道需要变成单一的"亮度"才能做阈值比较。直接对 R/G/B 分别阈值化,会得到三张独立的黑白图,颜色信息混杂,不是我们想要的。


三、阈值的选择:最关键的参数

固定阈值的最大问题:阈值选错,结果完全不对

场景 1:高亮文档

白纸黑字,背景 200250,文字 050。

  • 阈值 128:文字全黑 ✅,背景全白 ✅,效果完美

场景 2:光照不均匀的文档

文档左侧曝光过度(背景 220),右侧曝光不足(背景 150)。

  • 阈值 128:左侧背景 220 → 白 ✅,右侧背景 150 → 白 ✅(还行)
  • 阈值 180:左侧背景 220 → 白 ✅,右侧背景 150 → 黑 ❌(背景被判为前景)

固定阈值在光照不均匀时会失效。

场景 3:低对比度图像

灰色背景 160,深灰文字 120。

  • 阈值 128:文字 120 → 黑 ✅,背景 160 → 白 ✅(勉强可以)
  • 但信噪比很差,稍有噪点就会误判

四、Otsu 算法:自动寻找最优阈值

Otsu 算法 (1979,大津展之)是经典的自适应阈值方法,核心思想:找到一个阈值 T,使得分割后前景和背景的类间方差最大

直觉理解

方差描述数据的"散布程度"。如果 T 把图像分成两组:

  • 组 1(暗区,亮度 < T):方差小 → 组内像素相似
  • 组 2(亮区,亮度 ≥ T):方差小 → 组内像素相似
  • 两组之间方差大 → 两组差异明显

最好的 T = 让类间方差最大的那个值

数学推导

设灰度值 i ∈ [0, 255],频率(概率)为 p[i](直方图归一化后的值)。

对于阈值 T,将像素分为两类:

  • 类 0(背景):像素值 0 ~ T-1
  • 类 1(前景):像素值 T ~ 255

计算各类的概率和均值:

css 复制代码
ω₀(T) = Σ p[i]  (i=0 to T-1)    ← 背景比例
ω₁(T) = Σ p[i]  (i=T to 255)    ← 前景比例
μ₀(T) = Σ i·p[i]/ω₀             ← 背景均值
μ₁(T) = Σ i·p[i]/ω₁             ← 前景均值
μ   = ω₀·μ₀ + ω₁·μ₁            ← 全局均值

类间方差(Between-class variance):

r 复制代码
σ²_B(T) = ω₀(T)·ω₁(T)·[μ₀(T) - μ₁(T)]²

最优阈值:

r 复制代码
T* = argmax σ²_B(T)   (T ∈ [0, 255])

Swift 实现

swift 复制代码
/// 计算 Otsu 最优阈值
static func otsuThreshold(for bitmap: MLBitmap) -> UInt8 {
    // 先灰度化
    let gray = GrayscaleFilter().apply(to: bitmap)

    // 统计直方图(256 个桶)
    var histogram = [Int](repeating: 0, count: 256)
    for i in stride(from: 0, to: gray.pixels.count, by: 4) {
        histogram[Int(gray.pixels[i])] += 1
    }

    let total = gray.width * gray.height

    var sumAll: Double = 0
    for i in 0..<256 {
        sumAll += Double(i * histogram[i])
    }

    var sumBackground: Double = 0
    var weightBackground = 0
    var maxVariance: Double = 0
    var bestThreshold: UInt8 = 128

    for t in 0..<256 {
        weightBackground += histogram[t]
        if weightBackground == 0 { continue }

        let weightForeground = total - weightBackground
        if weightForeground == 0 { break }

        sumBackground += Double(t * histogram[t])

        let meanBackground = sumBackground / Double(weightBackground)
        let meanForeground = (sumAll - sumBackground) / Double(weightForeground)

        let variance = Double(weightBackground) * Double(weightForeground)
                     * (meanBackground - meanForeground)
                     * (meanBackground - meanForeground)

        if variance > maxVariance {
            maxVariance = variance
            bestThreshold = UInt8(t)
        }
    }

    return bestThreshold
}

Otsu 算法的时间复杂度是 O(256) = O(1)(与图像大小无关,只取决于直方图),非常高效。


五、局部自适应阈值(CLAHE 思路)

Otsu 是全局阈值------整张图用同一个 T。当光照极不均匀时,还需要局部阈值

复制代码
将图像分成若干小块(如 32×32)
对每个小块单独计算 Otsu 阈值
用该块的局部阈值做二值化

更进阶的是 CLAHE(Contrast Limited Adaptive Histogram Equalization,限制对比度自适应直方图均衡),这是 Phase 2 的内容。


六、二值化的后处理

原始二值化结果通常有噪点(孤立的黑点或白点),需要后处理:

形态学操作

腐蚀(Erosion):用一个小结构元素(如 3×3 全白)扫描,只有结构元素完全在白区内时才保留白色。效果:缩小白色区域,消除细小噪点。

膨胀(Dilation):相反操作,扩大白色区域,填补空洞。

开运算(Opening) = 腐蚀 → 膨胀:去除小噪点,保留大物体。

闭运算(Closing) = 膨胀 → 腐蚀:填补小孔洞,连接断裂边缘。

这些操作是 Phase 2 vImage 的标配功能。


七、二值化与边缘检测的关系

二值化和 Sobel 边缘检测经常配合使用:

swift 复制代码
// Sobel 输出的是梯度强度图(每个像素是边缘强度 0~255)
let edges = SobelEdgeFilter(preBlur: true).apply(to: bitmap)

// 再做二值化,只保留强边缘
let strongEdges = ThresholdFilter(threshold: 50).apply(to: edges)

流程

复制代码
原图 → 灰度化 → 高斯模糊 → Sobel 梯度 → 阈值化 → 二值边缘图

这是 Canny 边缘检测的简化版(Canny 还增加了非极大值抑制和双阈值法,Phase 2 可实现)。


八、验证测试

swift 复制代码
func testThresholdOutputBinary() {
    // 任意颜色的图,经过二值化后只有黑白两种值
    let bmp = MLBitmap(width: 10, height: 10, filling: .init(r: 130, g: 80, b: 200))
    let result = ThresholdFilter().apply(to: bmp)

    for y in 0..<10 {
        for x in 0..<10 {
            let px = result[x, y]
            XCTAssertTrue(px == .white || px == .black,
                         "二值化输出只应包含黑或白,实际:\(px)")
        }
    }
}

func testThresholdAboveBecomesWhite() {
    // 亮度 200 ≥ 128 → 白色
    var bmp = MLBitmap(width: 1, height: 1, filling: .white)
    bmp[0, 0] = MLBitmap.Pixel(r: 200, g: 200, b: 200)
    let result = ThresholdFilter(threshold: 128).apply(to: bmp)
    XCTAssertEqual(result[0, 0], .white)
}

九、小结

概念 核心内容
二值化目的 区分前景和背景,简化图像信息
固定阈值 简单高效,光照均匀时效果好
Otsu 算法 自动寻找最优阈值,适合光照均匀但不知道阈值时
局部阈值 光照不均匀时的解决方案
先灰度化 二值化需要单通道亮度,彩色图先灰度化
后处理 形态学操作去噪(腐蚀/膨胀/开/闭运算)

思考题

  1. 对一张全黑图像(所有像素 = 0)做 Otsu 二值化,结果是什么?Otsu 算法此时有没有意义?
  2. 医学图像中,CT 扫描的骨骼很亮,软组织较暗,如何用二值化分离骨骼?
  3. 手机相机拍摄的照片,文字区域可能因为光照角度不同而出现阴影,导致同一张纸的背景亮度不均。设计一个算法流程,解决这个问题。

提示:3. 分块局部均值阈值化:对每个局部区域,阈值 = 局部均值 × 系数(如 0.85)。这比 Otsu 更鲁棒,是手机文档扫描 App 的常用算法。

往期推荐:

一张图了解图像处理中的亮度、对比度与实现

颜色科学与灰度化

从"图片"到"内存"------你真正理解图像处理的第一天

iPhone相册背后的图像处理知识(下)

iPhone相册背后的图像处理知识(中)

iPhone相册背后的图像处理知识(上)

一张图了解图像处理的本质

图像到底是什么

图像处理技术概要图

AI时代,软件工程师必备概念全景图

相关推荐
guslegend3 小时前
第9节:前端工程与一键启动
前端·大模型·状态模式·ai编程
Dvesiz3 小时前
【ClaudeCode平替(免费)】OpenCode 完整安装与 VSCode 使用指南
ide·vscode·编辑器·github·ai编程·claude·visual studio code
xiaoxue..3 小时前
Harness Engineering 讲解
架构·ai编程·harness
美狐美颜SDK开放平台4 小时前
美颜SDK接入流程详解:Android、iOS、鸿蒙兼容方案解析
android·人工智能·ios·华为·harmonyos·美颜sdk·视频美颜sdk
360智汇云4 小时前
AI开发平台TAI:PD分离加持,让大模型推理“快且稳”
ai·ai编程
小江的记录本4 小时前
【AI大模型选型指南】《2026年5月(最新版)国内外主流AI大模型选型指南》(个人版)
前端·人工智能·后端·ai·aigc·ai编程·ai写作
90后的晨仔5 小时前
Combine 操作符 —— 打造强大的数据处理管道
ios
90后的晨仔5 小时前
Combine 高级操作符:掌控数据流的节奏与方向
ios
90后的晨仔5 小时前
Combine 与 SwiftUI 集成:构建响应式 UI 的黄金搭档
ios