3、直方图均衡化:原理推导、手动实现与OpenCV实现、均衡化效果对比

3.1 直方图均衡化到底在干什么?

直方图均衡化,说白了就是「把图像的灰度分布拉开」。

我刚开始接触图像增强时,总觉得这名字很唬人。后来自己动手推了一遍公式,才发现核心思想其实特别朴素:让图像中每个灰度级的像素数量尽可能均匀

你想想看,一张曝光不足的照片,像素大多集中在暗部区域。直方图均衡化就是把这些「扎堆」的像素,重新映射到整个灰度范围。结果就是暗部细节变亮了,亮部细节也不会过曝。

嗯,这里要注意:均衡化不是万能的。我在项目中遇到过一张雾霾严重的航拍图,均衡化后噪声被放大了,反而更难看清细节。所以它更适合对比度偏低、灰度分布集中的图像。

3.2 原理推导:从概率到映射函数

我们先从数学上理解一下。假设一张灰度图像,灰度级范围是 [0, L-1]。对于灰度级 r,它的概率密度函数(PDF)为:

p(r) = n_r / N

其中 n_r 是灰度级 r 的像素个数,N 是总像素数。

直方图均衡化的核心,是找到一个变换函数 s = T(r),使得输出图像 s 的直方图近似均匀分布。这个变换函数其实就是累积分布函数(CDF)

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

对于离散图像,积分变成求和:

s_k = (L-1) * Σⱼ₌₀ᵏ p(rⱼ) = (L-1) * Σⱼ₌₀ᵏ (nⱼ / N)

为什么用 CDF?因为 CDF 的斜率就是 PDF。当 PDF 不均匀时,CDF 在像素密集的区域斜率大,映射后灰度级被拉开;在像素稀疏的区域斜率小,灰度级被压缩。最终结果就是直方图被「拉平」了。

核心结论:直方图均衡化 = 用 CDF 作为灰度映射函数。

3.3 手动实现:一步步写出来

光看公式不过瘾,我们来手动实现一遍。我用 Python 写了一个纯手工版本,不依赖 OpenCV 的均衡化函数。

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

def my_equalize_hist(img):
    # 1. 计算直方图
    hist = np.zeros(256, dtype=np.int32)
    h, w = img.shape
    for i in range(h):
        for j in range(w):
            hist[img[i, j]] += 1
    
    # 2. 计算累积分布函数(CDF)
    cdf = np.zeros(256, dtype=np.float64)
    cdf[0] = hist[0] / (h * w)
    for k in range(1, 256):
        cdf[k] = cdf[k-1] + hist[k] / (h * w)
    
    # 3. 映射:s_k = 255 * cdf[k]
    lut = np.round(255 * cdf).astype(np.uint8)
    
    # 4. 应用查找表
    result = np.zeros_like(img)
    for i in range(h):
        for j in range(w):
            result[i, j] = lut[img[i, j]]
    
    return result, lut

这段代码我故意用了循环,方便你理解每一步在做什么。实际项目中当然要用向量化操作,但教学嘛,清晰第一。

我曾经在嵌入式设备上移植过这个算法,发现循环太慢,后来改成了查表法——就是上面代码里的 lut。预先算好映射表,然后逐像素查表,速度能快几十倍。

3.4 OpenCV 实现:一行代码搞定

OpenCV 提供了现成的函数,简单到令人发指:

import cv2

img = cv2.imread('dark_image.jpg', cv2.IMREAD_GRAYSCALE)
equalized = cv2.equalizeHist(img)

就这一行。但别以为用了 OpenCV 就万事大吉。我记得有一次处理医学 X 光片,直接调 equalizeHist 后,骨骼和软组织的对比度反而变差了。为什么?因为 X 光片的直方图分布比较特殊,全局均衡化会把背景噪声也放大。

避坑指南:OpenCV 的 equalizeHist 只接受单通道灰度图。如果你处理的是彩色图,需要先转到 YUV 或 Lab 色彩空间,只对亮度通道做均衡化,再合并回去。我曾经直接对 RGB 三个通道分别均衡化,结果颜色完全失真,像一幅抽象画。

3.5 均衡化效果对比

我们来看一组实际对比。假设有一张低对比度的图像,直方图集中在中间灰度区域。

对比项 原始图像 手动实现 OpenCV 实现
直方图分布 集中在 [80, 160] 近似均匀分布 近似均匀分布
对比度 显著提升 显著提升
细节可见性 暗部细节丢失 暗部细节恢复 暗部细节恢复
噪声放大 轻微 轻微
计算速度 较慢(纯 Python 循环) 极快(C++ 底层实现)

从结果上看,手动实现和 OpenCV 实现的效果几乎一致。区别在于:OpenCV 版本经过了高度优化,处理一张 1080p 图像只需要几毫秒。而我的纯 Python 版本,嗯,大概要几百毫秒。

注意:直方图均衡化会改变图像的整体亮度。如果你需要保持平均亮度不变,可以考虑「自适应直方图均衡化」(CLAHE),这个我们后面会讲到。

3.6 核心逻辑流程图

下面我用一张 SVG 图来总结直方图均衡化的完整流程:

直方图均衡化核心流程 输入图像 灰度图 [0, 255] 计算直方图 统计每个灰度级像素数 计算 CDF 累积分布函数 生成映射表 s_k = 255 * CDF(k) 逐像素映射 查表替换灰度值 输出均衡化图像 直方图近似均匀 核心思想:用 CDF 将密集灰度级拉开,稀疏灰度级压缩

3.7 什么时候该用,什么时候不该用

根据我的经验,直方图均衡化最适合以下场景:

  • 曝光不足或过度的照片:比如逆光拍摄的人像,面部太暗,均衡化后能看清五官。
  • 医学图像:X 光片、CT 扫描图,灰度范围窄,均衡化有助于区分组织边界。
  • 卫星遥感图:云层遮挡导致对比度低,均衡化能突出地表特征。

但以下情况要慎用:

  • 图像本身对比度已经很好:均衡化反而会破坏原有的层次感。
  • 噪声较多的图像:均衡化会放大噪声,让画面变「脏」。
  • 需要保持亮度一致性的场景:比如视频序列,帧间亮度突变会让人眼不适。

我的建议:拿到一张图,先看直方图。如果直方图像一座「孤峰」立在中间,均衡化大概率有效。如果直方图已经铺满了整个灰度范围,就别折腾了。

好了,直方图均衡化就讲到这里。原理、手动实现、OpenCV 实现、效果对比,该有的都有了。下一节我们会聊到更高级的 CLAHE——自适应直方图均衡化,它能解决全局均衡化的一些痛点。


公众号:蓝海资料掘金营,微信deep3321