3、直方图均衡化数学原理:PDF、CDF与映射函数推导

好,咱们进入正题。直方图均衡化,说白了就是让图像的灰度分布更均匀。你想想看,一张图如果整体偏暗,那它的直方图肯定都挤在左边。均衡化要做的,就是把这些挤在一起的像素「拉开」,让它们均匀地铺满整个灰度范围。

嗯,这里要注意,均衡化不是万能的。我在项目中遇到过一张过曝的图,直方图全堆在右边,均衡化之后反而出现了很多噪点。所以理解它的数学原理,才能知道什么时候该用,什么时候不该用。

3.1 概率密度函数(PDF)

我们先从最基础的说起。对于一张灰度图像,假设灰度级范围是 [0, L-1],比如 8 位图就是 0 到 255。那么每个灰度级出现的概率是多少?

定义很简单:

P(r_k) = n_k / N

其中:

  • r_k 是第 k 个灰度级
  • n_k 是该灰度级的像素个数
  • N 是总像素数

这个 P(r_k) 就是概率密度函数(PDF)。它描述的是每个灰度级出现的相对频率。

关键点:PDF 的所有值加起来必须等于 1。这是概率的基本性质。如果你算出来不等于 1,那肯定是代码写错了。

我个人习惯在写代码时先做一步检查:

# 伪代码示意
hist = calc_histogram(image)  # 计算直方图
pdf = hist / total_pixels     # 归一化得到 PDF
assert abs(sum(pdf) - 1.0) < 1e-6  # 检查总和是否为 1

为什么 PDF 这么重要?因为它直接决定了后续的映射函数。说白了,PDF 就是原始图像的「灰度分布画像」。

3.2 累积分布函数(CDF)

有了 PDF,下一步就是累积分布函数(CDF)。CDF 的定义是:

C(r_k) = Σ_{i=0}^{k} P(r_i)

也就是从灰度 0 到当前灰度 k 的所有概率累加。CDF 有几个重要性质:

  • 单调递增:随着灰度级增加,CDF 只增不减
  • 取值范围 [0, 1]:最小值是 0,最大值是 1
  • 非线性:CDF 的形状完全由原始图像的 PDF 决定

我的经验:CDF 的单调递增性质非常关键。它保证了映射后的灰度顺序不会乱——原来暗的像素映射后还是暗的,亮的还是亮的。我曾经试过用非单调的映射函数做实验,结果图像出现了「灰度反转」,看起来像底片一样,完全不能用。

举个例子,假设一张 4 级灰度图(0, 1, 2, 3),像素分布如下:

灰度级 像素数 PDF CDF
0 50 0.5 0.5
1 20 0.2 0.7
2 20 0.2 0.9
3 10 0.1 1.0

你看,CDF 从 0.5 一路涨到 1.0,每一步都在累加。这就是均衡化的「原材料」。

3.3 映射函数的推导

好,重头戏来了。映射函数怎么来的?

直方图均衡化的核心思想是:让输出图像的 CDF 变成一条直线。换句话说,我们希望输出图像的灰度分布是均匀的。

对于连续情况,映射函数是:

s = T(r) = (L-1) * ∫₀ʳ P(w) dw

其中 ∫₀ʳ P(w) dw 就是 CDF。所以映射函数其实就是:

s_k = round( (L-1) * C(r_k) )

嗯,这里要注意,对于离散图像,我们得取整。因为灰度级必须是整数。

避坑指南:我曾经在项目中直接用了 s_k = (L-1) * C(r_k) 没取整,结果图像出现了很多「伪轮廓」。取整操作虽然简单,但直接影响最终效果。建议用 np.round() 或者 int() 做转换。

完整的映射过程可以写成:

# 计算 CDF
cdf = np.cumsum(pdf)

# 归一化到 [0, L-1]
cdf_normalized = cdf * (L-1)

# 取整得到映射表
mapping = np.round(cdf_normalized).astype(np.uint8)

# 应用映射
output_image = mapping[input_image]

为什么这样就能均衡化?我们来推导一下。

假设原始图像的 CDF 是 C(r),映射后的灰度是 s = (L-1) * C(r)。那么输出图像的 CDF 是什么?

对于输出图像,灰度 s 的 CDF 是:

C_out(s) = P( s_out ≤ s ) = P( (L-1) * C(r) ≤ s ) = P( r ≤ C⁻¹(s/(L-1)) ) = C( C⁻¹(s/(L-1)) ) = s/(L-1)

你看,输出图像的 CDF 变成了 s/(L-1),这是一条直线!这就证明了均衡化后的图像灰度分布是均匀的。

核心结论:映射函数 s_k = round( (L-1) * C(r_k) ) 的本质,就是把原始图像的 CDF「拉直」成一条直线。拉得越直,均衡化效果越好。

3.4 实际应用中的注意事项

理论讲完了,说点实际的。我在嵌入式视觉项目中用直方图均衡化时,遇到过几个坑:

  1. 灰度级丢失:取整操作会导致多个原始灰度映射到同一个输出灰度。如果原始图像灰度级很少(比如 16 级),均衡化后可能只剩 8 级有效灰度。
  2. 噪声放大:如果原始图像有大量暗部噪声,均衡化会把这些噪声放大。我建议先做去噪再做均衡化。
  3. 实时性:在嵌入式平台上,计算直方图和 CDF 的开销不大,但映射过程需要查表。用查找表(LUT)可以做到 O(1) 的映射速度。

我的建议:对于嵌入式视觉,可以预先计算好映射表,然后直接查表映射。这样每帧图像只需要一次直方图计算和一次查表操作,效率很高。我在一个 ARM Cortex-M4 的项目中,用这种方法做到了 30fps 的实时处理。

最后总结一下:PDF 告诉我们原始图像的灰度分布,CDF 告诉我们累积分布,映射函数把 CDF 拉直。这三步环环相扣,缺一不可。理解了这些,你就能灵活运用直方图均衡化,而不是只会调库函数了。