4. 直方图均衡化原理

直方图均衡化,说白了就是让图像的灰度分布更均匀。我刚开始接触这个概念时,觉得它就是个简单的“拉直”操作。后来在项目中处理过一张严重曝光不足的工业检测照片,才真正体会到这背后的数学力量。

咱们今天就把这块硬骨头啃下来。我会从数学推导讲起,再到CDF,最后落到具体的像素映射上。嗯,保证让你听完就能手撕代码。

4.1 为什么需要均衡化?

先看一个实际问题。一张图像如果整体偏暗,它的直方图会集中在低灰度区域。如果偏亮,就集中在高灰度区域。对比度低的时候,直方图就像一根细长的柱子,挤在中间一小块地方。

均衡化的目标是什么?就是把这种“扎堆”的分布,拉伸成近似均匀的分布。你想想看,如果每个灰度级上的像素数量差不多,图像的细节是不是就全出来了?

核心思想: 通过一个非线性变换,让输出图像的直方图近似平坦。这样原本暗部被压缩的细节,就能在更宽的灰度范围内展开。

4.2 数学推导:从概率密度到变换函数

咱们把图像看成随机变量。假设输入图像的灰度级为 r,取值范围 [0, L-1]。它的概率密度函数 PDF 记作 p_r(r)。

我们要找一个变换函数 T,把 r 映射到输出灰度 s:

s = T(r)   其中 0 ≤ r ≤ L-1

T 必须满足两个条件:

  • 单调递增 —— 保证灰度顺序不变,黑的不会变成白的
  • 值域一致 —— 输出灰度也在 [0, L-1] 范围内

我当年推导的时候,卡在最关键的一步上:输出灰度 s 的概率密度 p_s(s) 和输入 p_r(r) 之间是什么关系?

答案是:

p_s(s) = p_r(r) · |dr/ds|

如果我们要让 p_s(s) 是均匀分布,即 p_s(s) = 1/(L-1),那么:

dr/ds = (L-1) · p_r(r)

两边积分,就得到了传说中的变换函数:

s = T(r) = (L-1) · ∫₀ʳ p_r(w) dw

这个积分,就是累积分布函数 CDF。

我的经验: 很多教材直接甩出这个公式,但没告诉你为什么积分就是CDF。其实很简单——CDF 就是 PDF 从 0 到 r 的累积和。在离散图像里,就是像素数量的累加。

4.3 累积分布函数 CDF:离散世界的版本

上面是连续域的推导。实际图像是离散的,灰度级只有 256 个(8位图)。

设图像总像素数为 N,灰度级 r_k 对应的像素数为 n_k。那么:

PDF:  p_r(r_k) = n_k / N
CDF:  cdf(r_k) = Σⱼ₌₀ᵏ p_r(r_j) = Σⱼ₌₀ᵏ n_j / N

均衡化后的灰度 s_k 就是:

s_k = T(r_k) = (L-1) · cdf(r_k)

注意,s_k 算出来是浮点数,要四舍五入取整到 [0, L-1] 的整数。

概念 连续域 离散域
灰度范围 [0, L-1] 连续 {0, 1, ..., L-1} 整数
概率密度 p_r(r) 连续函数 p_r(r_k) = n_k / N
累积分布 ∫₀ʳ p_r(w) dw Σⱼ₌₀ᵏ n_j / N
变换结果 精确均匀 近似均匀

我曾经踩过的坑: 离散均衡化后,直方图不会完全平坦。因为取整操作会把多个输入灰度映射到同一个输出灰度上。这是理论限制,不是代码写错了。

4.4 映射过程详解:手算一个例子

光说不练假把式。咱们拿一个 8×8 的小图,4个灰度级(L=4),手算一遍。

假设灰度统计如下:

灰度 r_k 像素数 n_k PDF p_r(r_k) CDF cdf(r_k) s_k = 3 × cdf 取整
0 16 16/64 = 0.25 0.25 0.75 1
1 32 32/64 = 0.50 0.75 2.25 2
2 8 8/64 = 0.125 0.875 2.625 3
3 8 8/64 = 0.125 1.000 3.000 3

映射关系就是:

  • 原灰度 0 → 新灰度 1
  • 原灰度 1 → 新灰度 2
  • 原灰度 2 → 新灰度 3
  • 原灰度 3 → 新灰度 3

你看,原来灰度 0 和 1 的像素被拉开了,但灰度 2 和 3 合并了。这就是离散均衡化的代价——有些细节会丢失,但整体对比度提升了。

4.5 核心流程图:均衡化全链路

下面这张图,把整个流程串起来了。我建议你把它存下来,写代码时对照着看。

直方图均衡化完整流程 输入图像 统计直方图 计算 PDF 计算 CDF 映射新灰度 关键公式 s_k = (L-1) · cdf(r_k) cdf(r_k) = Σⱼ₌₀ᵏ n_j / N 注意事项 结果需四舍五入取整 直方图不会完全平坦 输出图像

4.6 代码实现:Python 手写均衡化

理论讲完了,咱们直接上代码。我习惯用 NumPy 实现,清晰且高效。

import numpy as np
import cv2
import matplotlib.pyplot as plt

def histogram_equalization(image):
    """
    直方图均衡化实现
    image: 输入灰度图,uint8 类型
    """
    # 1. 统计直方图
    hist = np.zeros(256, dtype=np.int32)
    h, w = image.shape
    total_pixels = h * w

    for i in range(h):
        for j in range(w):
            pixel = image[i, j]
            hist[pixel] += 1

    # 2. 计算 PDF
    pdf = hist / total_pixels

    # 3. 计算 CDF
    cdf = np.zeros(256, dtype=np.float64)
    cdf[0] = pdf[0]
    for k in range(1, 256):
        cdf[k] = cdf[k-1] + pdf[k]

    # 4. 映射新灰度
    mapping = np.round(255 * cdf).astype(np.uint8)

    # 5. 生成输出图像
    output = np.zeros_like(image)
    for i in range(h):
        for j in range(w):
            output[i, j] = mapping[image[i, j]]

    return output, mapping

# 使用示例
img = cv2.imread('dark_image.jpg', cv2.IMREAD_GRAYSCALE)
eq_img, mapping_table = histogram_equalization(img)

# 可视化对比
plt.figure(figsize=(12, 4))
plt.subplot(121), plt.imshow(img, cmap='gray'), plt.title('原图')
plt.subplot(122), plt.imshow(eq_img, cmap='gray'), plt.title('均衡化后')
plt.show()

我的建议: 实际工程中别用纯 Python 循环,太慢了。用 NumPy 的向量化操作,或者直接用 OpenCV 的 cv2.equalizeHist()。上面这段代码是为了让你看清每一步在做什么。

4.7 避坑指南:我踩过的三个坑

  1. CDF 归一化问题 —— 我曾经忘记把 CDF 乘上 (L-1),结果输出图像一片漆黑。检查了半小时才发现。
  2. 数据类型溢出 —— 映射表用 uint8 没问题,但中间计算 CDF 时一定要用 float64,否则精度丢失严重。
  3. 多通道图像 —— 彩色图不能直接对 RGB 三个通道分别均衡化,会破坏色彩平衡。正确做法是转到 HSV 或 YUV 空间,只对亮度通道做均衡化。

4.8 均衡化效果评估

怎么知道均衡化做得好不好?我一般看三个指标:

指标 说明 理想值
直方图平坦度 各灰度级像素数方差 越小越好
对比度提升 均衡化前后灰度标准差变化 明显增大
信息熵 图像信息量度量 增大

说白了,均衡化不是万能的。如果原图本身对比度就很好,强行均衡化反而会让图像失真。我一般在做预处理时,会先看一眼直方图分布,再决定要不要用。