Featured image of post 全局阈值二值化

全局阈值二值化

主要包含全局阈值二值化方法:128阈值、平均阈值、OSTU大津法

128阈值

在灰度图像中,像素值范围是0到255(共 256 个离散级)。把灰度图二值化时常见的简单规则是:

  • 若像素值 $I(x,y)\geq T$,则置为 255(白)
  • 否则置为0(黑)

当T=128时,即将灰度中点作为阈值:所有亮度 $\ge 128$ 的像素判为“亮”,小于的则判为”暗。

关于图片适用性的判别,则可以用似然比检验法进行确认(只不过需要知道前景和后景的分布):

假设图像像素由前景和背景组成,每个部分的像素值服从高斯分布(正态分布)。设图像有 N 个像素,像素值为 $x_{i} (i=1,2,…,N)$ 。我们需要比较两个假设:

  • 零假设($H_{0}$):阈值 T=128是合理的,即图像二值化使用固定阈值128。
  • 备择假设($H_{1}$):阈值 T 是任意的,即存在一个最优阈值 $T^*$ 能更好地拟合图像数据。

似然比检验通过比较在 $H_{0}$ 和 $H_{1}$ 下的最大似然值来评估 $H_{0}$ 是否被拒绝。

假设前景和背景的像素值分别服从高斯分布:

  • 背景像素 ($x_{i} \leq T $) 服从 $ N(\mu_{b},\sigma_{b}^{2})$

  • 前景像素 ($ x_{i}>T $) 服从 $ N(\mu_{f},\sigma_{f}^{2}) $

则似然函数(给定阈值T)为:

$$ L(\mu_{b},\sigma_{b}^{2},\mu_{f},\sigma_{f}^{2},T)=\prod_{x_{i}\leq T}\frac{1}{\sqrt{2\pi\sigma_{b}^{2}}}\exp\left(-\frac{(x_{i}-\mu_{b})^{2}}{2\sigma_{b}^{2}}\right)\times\prod_{x_{i}>T}\frac{1}{\sqrt{2\pi\sigma_{f}^{2}}}\exp\left(-\frac{(x_{i}-\mu_{f})^{2}}{2\sigma_{f}^{2}}\right) $$

分别计算 $L_{0} 和 L_{1} $,似然比定义为:

$$ \Lambda=\frac{L_{0}}{L_{1}} $$

检验统计量为 $ \lambda=-2\ln\Lambda $,在$ H_{0} $下,$ \lambda $ 近似服从自由度为1的卡方分布(因为$ H_{1} $比$ H_{0} $多一个自由参数T)。

  • 选择显著性水平$ \alpha $(通常为0.05),对应的卡方临界值为 $ \chi^{2}_{1,1-\alpha} $(例如,$ \alpha=0.05 $时,临界值为3.841)。
  • 如果$ \lambda>\chi^{2}{1,1-\alpha} $,则拒绝$ H{0} $,表示阈值128不合理;否则不拒绝$ H_{0} $,表示128阈值可接受。

均值阈值

核心思想:使用图像所有像素灰度值的平均值作为阈值

设图像有N个像素,第 $i$ 个像素的灰度值为 $p_i$ ,则平均阈值 $T$ 计算为:

$$ T = \frac{1}{N} \sum_{i=1}^{N} p_i $$

代码示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import cv2
import numpy as np
import matplotlib.pyplot as plt

def mean_thresholding(image):
    """
    平均阈值二值化
    """
    # 计算平均阈值
    mean_value = np.mean(image)
    
    # 应用阈值
    _, binary_image = cv2.threshold(image, mean_value, 255, cv2.THRESH_BINARY)
    
    return binary_image, mean_value

# 读取图像
image = cv2.imread('document.jpg', cv2.IMREAD_GRAYSCALE)

# 应用平均阈值
binary_result, threshold_value = mean_thresholding(image)

print(f"计算得到的平均阈值: {threshold_value}")

# 显示结果
plt.figure(figsize=(12, 4))

plt.subplot(1, 3, 1)
plt.imshow(image, cmap='gray')
plt.title('原图')
plt.axis('off')

plt.subplot(1, 3, 2)
plt.hist(image.ravel(), 256, [0, 256])
plt.axvline(threshold_value, color='r', linestyle='--', label=f'阈值 = {threshold_value:.2f}')
plt.title('灰度直方图')
plt.legend()

plt.subplot(1, 3, 3)
plt.imshow(binary_result, cmap='gray')
plt.title('平均阈值二值化结果')
plt.axis('off')

plt.tight_layout()
plt.show()

OSTU大津法

核心准则:最优阈值 ($ T^*$) 是使两类像素的 “类间方差” 最大的阈值

背后逻辑:类间方差越大,说明前景与背景的灰度差异越显著,分割效果越好;反之,类间方差越小,两类灰度重叠越严重,分割效果越差。

关键前提:总方差 = 类内方差 + 类间方差(总方差不随阈值 t 变化),因此 “最大化类间方差” 等价于 “最小化类内方差”,二者是完全等价的优化目标。

假设输入图像为单通道灰度图,灰度级范围为 $[0, L-1]$ (通常 $L=256$ ,即8位图像),定义:

  • $n_i$ :灰度值为 $i$ 的像素个数;
  • 总像素数 $N = \sum_{i=0}^{L-1} n_i$ ;
  • 灰度级 $i$ 的概率 $p_i = \frac{n_i}{N}$ ,满足 $\sum_{i=0}^{L-1} p_i = 1$ (所有灰度的概率和为1)。

对于任意候选阈值 $t$ ( $0 \leq t \leq L-2$ ),将像素分为 $C_0$ (0t)和 $C_1$ (t+1L-1),定义以下关键统计量:

(1)两类的概率(权重)

  • 背景类 $C_0$ 的概率: $\omega_0(t) = \sum_{i=0}^t p_i$ (所有灰度≤t的像素占比);

  • 前景类 $C_1$ 的概率: $\omega_1(t) = \sum_{i=t+1}^{L-1} p_i = 1 - \omega_0(t)$ (所有灰度>t的像素占比)。

(2)两类的均值(灰度中心)

  • 背景类 $C_0$ 的均值: $\mu_0(t) = \frac{1}{\omega_0(t)} \sum_{i=0}^t i \cdot p_i$ (仅当 $\omega_0(t) \neq 0$ ,否则均值无意义);

  • 前景类 $C_1$ 的均值: $\mu_1(t) = \frac{1}{\omega_1(t)} \sum_{i=t+1}^{L-1} i \cdot p_i$ (仅当 $\omega_1(t) \neq 0$ );

  • 全局均值(整幅图像的灰度均值): $\mu = \sum_{i=0}^{L-1} i \cdot p_i = \omega_0(t) \cdot \mu_0(t) + \omega_1(t) \cdot \mu_1(t)$ (加权平均,权重为两类概率)。

类间方差的定义是:两类均值与全局均值的加权平方和,权重为两类概率:

$$ \sigma_b^2(t) = \omega_0(t) \cdot (\mu_0(t) - \mu)^2 + \omega_1(t) \cdot (\mu_1(t) - \mu)^2 $$

简化有:

$$ \boxed{\sigma_b^2(t) = \omega_0(t) \cdot \omega_1(t) \cdot (\mu_0(t) - \mu_1(t))^2} $$

大津算法的具体实现步骤如下(以8位图像 $L=256$ 为例):

步骤1:计算灰度概率分布

遍历图像所有像素,统计每个灰度级 $i$ (0~255)的像素数 $n_i$ ,并计算概率 $p_i = \frac{n_i}{N}$ ( $N$ 为总像素数)。

步骤2:遍历所有候选阈值

遍历 $t \in [0, 254]$ (阈值需将灰度级分为非空两类,故 $t$ 不取255),对每个 $t$ 执行以下计算:

步骤3:计算两类的概率与均值
  • 背景概率 $\omega_0(t) = \sum_{i=0}^t p_i$ ;

  • 前景概率 $\omega_1(t) = 1 - \omega_0(t)$ ;

  • 背景均值 $\mu_0(t) = \frac{1}{\omega_0(t)} \sum_{i=0}^t i \cdot p_i$ (若 $\omega_0(t) = 0$ ,跳过该 $t$ );

  • 前景均值 $\mu_1(t) = \frac{1}{\omega_1(t)} \sum_{i=t+1}^{255} i \cdot p_i$ (若 $\omega_1(t) = 0$ ,跳过该 $t$ )。

步骤4:计算类间方差

用简化公式计算 $\sigma_b^2(t) = \omega_0(t) \cdot \omega_1(t) \cdot (\mu_0(t) - \mu_1(t))^2$ 。

步骤5:寻找最优阈值

记录所有 $t$ 对应的 $\sigma_b^2(t)$ ,找到使 $\sigma_b^2(t)$ 最大的阈值 $t^*$ (若有多个 $t$ 对应最大方差,取任意一个即可)。

步骤6:二值化分割

将图像中灰度值 $\leq t^$ 的像素设为背景(通常为0),灰度值 $> t^$ 的像素设为前景(通常为255),或根据需求反转(前景为0,背景为255)。

示例验证(简化场景)

为直观理解,假设一幅图像的灰度级仅为 $[0,1,2,3]$ ,统计信息如下:

灰度级 $i$ 0 1 2 3
像素数 $n_i$ 10 20 30 40
概率 $p_i$ 0.1 0.2 0.3 0.4

总像素数 $N=100$ ,全局均值 $\mu = 0 \times 0.1 + 1 \times 0.2 + 2 \times 0.3 + 3 \times 0.4 = 2.0$ 。

遍历候选阈值 $t=0,1,2$ :

  1. 当 $t=0$ 时:
  • $\omega_0 = 0.1$ , $\mu_0 = 0$ ;

  • $\omega_1 = 0.9$ , $\mu_1 = \frac{1 \times 0.2 + 2 \times 0.3 + 3 \times 0.4}{0.9} = \frac{2.0}{0.9} \approx 2.222$ ;

  • $\sigma_b^2 = 0.1 \times 0.9 \times (0 - 2.222)^2 \approx 0.444$ 。

  1. 当 $t=1$ 时:
  • $\omega_0 = 0.1 + 0.2 = 0.3$ , $\mu_0 = \frac{0 \times 0.1 + 1 \times 0.2}{0.3} \approx 0.667$ ;

  • $\omega_1 = 0.3 + 0.4 = 0.7$ , $\mu_1 = \frac{2 \times 0.3 + 3 \times 0.4}{0.7} = \frac{1.8}{0.7} \approx 2.571$ ;

  • $\sigma_b^2 = 0.3 \times 0.7 \times (0.667 - 2.571)^2 \approx 0.760$ 。

  1. 当 $t=2$ 时:
  • $\omega_0 = 0.1 + 0.2 + 0.3 = 0.6$ , $\mu_0 = \frac{0 \times 0.1 + 1 \times 0.2 + 2 \times 0.3}{0.6} \approx 1.333$ ;

  • $\omega_1 = 0.4$ , $\mu_1 = 3$ ;

  • $\sigma_b^2 = 0.6 \times 0.4 \times (1.333 - 3)^2 \approx 0.667$ 。

最优阈值:

类间方差最大值为 $\approx 0.760$ ,对应 $t^*=1$ 。因此,最优分割为:灰度01(背景)、灰度23(前景),分割效果最优。

示例代码:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
import cv2
import numpy as np
import matplotlib.pyplot as plt

def otsu_threshold_manual(gray_img):
    """
    手动实现大津算法,返回最优阈值和二值化图像
    :param gray_img: 输入灰度图(shape: (H, W),值范围0-255)
    :return: (optimal_thresh, binary_img)
    """
    # 步骤1:计算灰度概率分布(灰度直方图)
    H, W = gray_img.shape
    total_pixels = H * W  # 总像素数 N
    hist, _ = np.histogram(gray_img.flatten(), bins=256, range=[0, 256])  # 统计每个灰度级的像素数 n_i
    p = hist / total_pixels  # 灰度级概率 p_i = n_i / N

    # 步骤2:遍历所有候选阈值 t(0~254,避免某一类为空)
    max_var = 0.0  # 最大类间方差
    optimal_thresh = 0  # 最优阈值

    for t in range(0, 255):
        # 步骤3:计算两类的概率 ω0、ω1
        omega0 = np.sum(p[:t+1])  # C0:0~t 的概率和
        omega1 = 1 - omega0       # C1:t+1~255 的概率和(ω0+ω1=1)

        # 跳过空类(避免除以零)
        if omega0 == 0 or omega1 == 0:
            continue

        # 步骤4:计算两类的均值 μ0、μ1
        # μ0 = sum(i*p_i for i=0~t) / omega0
        mu0 = np.sum(np.arange(t+1) * p[:t+1]) / omega0
        # μ1 = sum(i*p_i for i=t+1~255) / omega1
        mu1 = np.sum(np.arange(t+1, 256) * p[t+1:]) / omega1

        # 步骤5:计算类间方差(使用简化公式)
        between_var = omega0 * omega1 * (mu0 - mu1) ** 2

        # 更新最大方差和最优阈值
        if between_var > max_var:
            max_var = between_var
            optimal_thresh = t

    # 步骤6:根据最优阈值二值化
    binary_img = np.where(gray_img <= optimal_thresh, 0, 255).astype(np.uint8)  # 0=背景,255=前景
    # 若需反转前景背景,改为:np.where(gray_img <= optimal_thresh, 255, 0)

    return optimal_thresh, binary_img

# ---------------------- 主流程 ----------------------
# 1. 读取图像并转为灰度图
img_path = "test_image.jpg"  # 替换为你的图像路径
img = cv2.imread(img_path)
if img is None:
    print("Error: 无法读取图像,请检查路径是否正确!")
    exit()
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 2. 手动调用大津算法
manual_thresh, manual_binary = otsu_threshold_manual(gray_img)

# 3. 对比 OpenCV 结果(验证正确性)
cv2_thresh, cv2_binary = cv2.threshold(gray_img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

# 4. 显示结果
plt.figure(figsize=(15, 5))

# 原图
plt.subplot(1, 4, 1)
plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
plt.title("原图")
plt.axis("off")

# 灰度图
plt.subplot(1, 4, 2)
plt.imshow(gray_img, cmap="gray")
plt.title("灰度图")
plt.axis("off")

# 手动实现二值化
plt.subplot(1, 4, 3)
plt.imshow(manual_binary, cmap="gray")
plt.title(f"手动实现大津二值化\n阈值={manual_thresh}")
plt.axis("off")

# OpenCV 实现二值化(对比)
plt.subplot(1, 4, 4)
plt.imshow(cv2_binary, cmap="gray")
plt.title(f"OpenCV 大津二值化\n阈值={cv2_thresh:.0f}")
plt.axis("off")

plt.tight_layout()
plt.show()

# 输出阈值对比(验证手动实现正确性)
print(f"手动实现最优阈值:{manual_thresh}")
print(f"OpenCV 最优阈值:{cv2_thresh:.0f}")
print(f"两者阈值是否一致:{manual_thresh == int(cv2_thresh)}")

# 保存结果(可选)
cv2.imwrite("manual_otsu_binary.jpg", manual_binary)
print("手动实现二值化结果已保存!")
恍如昨日,嗤笑今朝
使用 Hugo 构建
主题 StackJimmy 设计