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),像素分布如下:
| 灰度级 | 像素数 | 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 实际应用中的注意事项
理论讲完了,说点实际的。我在嵌入式视觉项目中用直方图均衡化时,遇到过几个坑:
- 灰度级丢失:取整操作会导致多个原始灰度映射到同一个输出灰度。如果原始图像灰度级很少(比如 16 级),均衡化后可能只剩 8 级有效灰度。
- 噪声放大:如果原始图像有大量暗部噪声,均衡化会把这些噪声放大。我建议先做去噪再做均衡化。
- 实时性:在嵌入式平台上,计算直方图和 CDF 的开销不大,但映射过程需要查表。用查找表(LUT)可以做到 O(1) 的映射速度。
我的建议:对于嵌入式视觉,可以预先计算好映射表,然后直接查表映射。这样每帧图像只需要一次直方图计算和一次查表操作,效率很高。我在一个 ARM Cortex-M4 的项目中,用这种方法做到了 30fps 的实时处理。
最后总结一下:PDF 告诉我们原始图像的灰度分布,CDF 告诉我们累积分布,映射函数把 CDF 拉直。这三步环环相扣,缺一不可。理解了这些,你就能灵活运用直方图均衡化,而不是只会调库函数了。