4、经典光照补偿方法(一):直方图均衡化(HE)、自适应直方图均衡化(AHE)、原理与实现

光照不均,说白了就是图像里有的地方太亮、有的地方太暗。做图像预处理时,这问题特别头疼。我早期做工业检测项目时,就吃过这个亏——产品表面反光不均,导致后续特征提取全乱套。后来发现,直方图均衡化这类方法,虽然老,但真管用。

这一章,咱们就聊聊两种最经典的光照补偿方法:直方图均衡化(HE)和自适应直方图均衡化(AHE)。我会把原理讲透,再配上代码实现,让你看完就能上手用。

核心观点:直方图均衡化不是万能药,但它是光照补偿的基石。理解它,你才能用好更高级的方法。

4.1 直方图均衡化(HE)原理

直方图均衡化的思路很直观:把图像的灰度分布“拉平”。你想想看,如果一张图偏暗,它的直方图肯定集中在低灰度区域。均衡化就是把这些像素重新分配,让它们在整个灰度范围内均匀分布。

数学上,它做的是非线性拉伸。具体来说,用累积分布函数(CDF)作为映射函数:

s_k = T(r_k) = (L-1) * Σ p_r(r_j)   (j从0到k)

其中,r_k是输入灰度,s_k是输出灰度,L是灰度级数(通常256),p_r(r_j)是第j级灰度的概率。

我习惯把这个过程理解为“资源再分配”。原来拥挤在暗区的像素,被强制分散到更亮的区域。效果就是:对比度提升,细节更清晰。

小提示:HE对整体偏暗或偏亮的图像效果很好。但如果图像本身光照分布很复杂,HE可能会过度增强噪声。

4.2 HE的Python实现

OpenCV里直接提供了HE函数,一行代码就能搞定。但我建议你手动实现一次,这样才能真正理解它的原理。

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

# 读取图像(灰度图)
img = cv2.imread('low_light.jpg', 0)

# 方法一:OpenCV内置函数
he_opencv = cv2.equalizeHist(img)

# 方法二:手动实现
def my_equalize_hist(img):
    # 计算直方图
    hist = cv2.calcHist([img], [0], None, [256], [0, 256])
    # 计算累积分布
    cdf = hist.cumsum()
    # 归一化到[0, 255]
    cdf_normalized = (cdf - cdf.min()) * 255 / (cdf.max() - cdf.min())
    # 映射
    result = np.interp(img.flatten(), range(256), cdf_normalized)
    return result.reshape(img.shape).astype(np.uint8)

he_manual = my_equalize_hist(img)

# 显示结果
plt.subplot(131), plt.imshow(img, cmap='gray'), plt.title('原始')
plt.subplot(132), plt.imshow(he_opencv, cmap='gray'), plt.title('OpenCV HE')
plt.subplot(133), plt.imshow(he_manual, cmap='gray'), plt.title('手动 HE')
plt.show()

你看,手动实现的核心就三步:算直方图、算CDF、做映射。OpenCV的equalizeHist底层也是这么干的。

注意:手动实现时,np.interp的输入必须是一维数组。我刚开始写的时候忘了flatten(),结果报错半天没找到原因。

4.3 HE的局限性

HE虽然简单,但问题也很明显。我总结了几点:

  • 全局处理:它把整张图当成一个整体,无法处理局部光照差异
  • 噪声放大:在均匀区域,HE会把微小的灰度差异放大成明显的噪声
  • 过度增强:某些区域对比度提升过头,看起来不自然

举个例子,一张图左边暗右边亮,HE会让左边变亮,但右边可能就过曝了。这就是全局方法的硬伤。

4.4 自适应直方图均衡化(AHE)原理

为了解决HE的全局问题,AHE出现了。它的思路是:分块处理。

具体做法:

  1. 把图像分成若干个小块(比如8×8或16×16)
  2. 对每个小块单独做HE
  3. 用插值法消除块之间的边界效应

这样做的好处很明显:每个小块只处理自己的局部区域,光照不均的问题自然就解决了。我做过一个医疗影像项目,X光片里骨骼和软组织亮度差异巨大,用HE效果很差,换成AHE后细节全出来了。

关键点:AHE的块大小直接影响效果。块越小,局部适应性越强,但计算量也越大,而且可能引入块状伪影。

4.5 AHE的Python实现

OpenCV里没有直接的AHE函数,但提供了createCLAHE——这是AHE的改进版。不过,咱们先看看纯AHE怎么实现:

def ahe(img, tile_size=8):
    h, w = img.shape
    result = np.zeros_like(img)
    
    # 分块处理
    for i in range(0, h, tile_size):
        for j in range(0, w, tile_size):
            # 提取小块
            tile = img[i:i+tile_size, j:j+tile_size]
            # 对小块做HE
            tile_he = cv2.equalizeHist(tile)
            # 放回结果
            result[i:i+tile_size, j:j+tile_size] = tile_he
    
    return result

# 使用
ahe_result = ahe(img, tile_size=16)

这段代码很直观,但有个问题:块与块之间会有明显的边界。为了解决这个问题,实际应用中会用双线性插值来平滑过渡。

避坑指南:我曾经在实时视频处理中直接用这个简单AHE,结果画面像打满了马赛克。后来加了插值,效果才正常。记住,插值是AHE的灵魂。

4.6 HE与AHE对比

我把两者的核心差异整理成了一张表,方便你对比:

特性 HE AHE
处理范围 全局 局部(分块)
光照不均适应性 差 好
计算复杂度 低(O(n)) 较高(O(n×块数))
噪声放大 中等 较高(均匀区域更明显)
块状伪影 无 有(需插值消除)
适用场景 整体光照均匀的图像 光照变化复杂的图像

从表里能看出来,AHE在适应性上完胜,但代价是计算量和噪声问题。所以实际项目中,我一般先用HE试试,效果不行再上AHE。

4.7 知识体系图

下面这张SVG图,帮你理清HE和AHE的关系:

直方图均衡化知识体系 光照补偿方法 直方图均衡化 (HE) 自适应直方图均衡化 (AHE) 全局映射 CDF拉伸 对比度提升 分块处理 局部HE 插值平滑 关键区别: HE:全局CDF映射,简单快速,但无法处理局部光照变化。 AHE:分块+局部HE+插值,适应性强,但计算量大,需注意噪声。

4.8 实际项目中的选择建议

说了这么多,到底什么时候用HE,什么时候用AHE?我根据经验给你几个建议:

  • 图像整体光照均匀:用HE,简单高效
  • 光照变化平缓:用AHE,块大小设大一点(32×32以上)
  • 光照变化剧烈:用AHE,块大小设小一点(8×8到16×16)
  • 实时性要求高:优先HE,或者用下一章要讲的CLAHE(带对比度限制的AHE)

重要提醒:AHE不是万能的。如果图像本身噪声很大,AHE会把噪声放大到不可接受的程度。这时候,先做去噪,再做AHE,顺序不能乱。