4. 高级技巧:伽马校正、对数变换、CLAHE与多尺度Retinex

前面我们聊了线性拉伸和直方图均衡,这些方法对付大部分场景已经够用了。但总有些「硬骨头」——比如背光拍摄的人脸、雾霾天的监控画面、或者医学X光片那种整体偏暗但局部细节又很重要的图像。这时候,就该上点高级技巧了。

我个人习惯把这四种方法称为「四大金刚」:伽马校正、对数变换、CLAHE、多尺度Retinex。它们各自擅长处理不同类型的非线性光照问题。今天咱们一个一个拆开看。

核心观点: 没有万能算法。伽马适合整体亮度调整,对数擅长压暗提亮,CLAHE专治局部对比度不足,Retinex则是光照分离的终极方案。

4.1 伽马校正:调整感知亮度

伽马校正最早来自CRT显示器的物理特性——输入电压和显示亮度不是线性关系。后来发现,人眼对暗部变化的敏感度远高于亮部,所以伽马校正反而成了一种「感知均匀」的编码方式。

公式很简单:O = I ^ (1/γ)。当 γ > 1 时,暗部被拉伸,亮部被压缩;γ < 1 时则相反。

import cv2
import numpy as np

def gamma_correction(img, gamma=1.0):
    # 归一化到 [0, 1]
    normalized = img / 255.0
    # 伽马变换
    corrected = np.power(normalized, 1/gamma)
    # 还原到 [0, 255]
    return np.uint8(corrected * 255)

# 使用示例
img = cv2.imread('dark_face.jpg', cv2.IMREAD_GRAYSCALE)
result = gamma_correction(img, gamma=2.2)
我的经验: 伽马值 2.2 是显示器的标准值,但做图像增强时我通常从 1.5 开始试。有一次处理夜间监控画面,γ=2.8 才把车牌从阴影里「挖」出来。别死记参数,多试。

4.2 对数变换:压缩动态范围

对数变换的核心思想是:把指数增长映射为线性增长。说白了,就是让暗部细节「浮」出来,同时不让亮部过曝。

公式:O = c * log(1 + I)。c 是缩放常数,通常取 255 / log(256)。

def log_transform(img, c=None):
    if c is None:
        c = 255 / np.log(1 + 255)
    # 防止 log(0)
    img_float = np.float32(img) + 1
    transformed = c * np.log(img_float)
    return np.uint8(np.clip(transformed, 0, 255))

对数变换和伽马校正有什么区别?我画了张图帮你理解:

伽马 vs 对数:变换曲线对比 输入像素值 输出像素值 线性 (γ=1) 伽马 γ=2.2 对数变换 暗部:对数拉伸更强 伽马提升较温和 亮部:对数压缩明显 伽马仍保持一定区分
注意: 对数变换对暗部提升非常剧烈。我曾经在处理一张星空照片时,log 变换把背景噪声也放大了,结果星星没看清,噪点倒是一清二楚。建议先做去噪,再做对数变换。

4.3 CLAHE:限制对比度的自适应直方图均衡

普通直方图均衡有个毛病:它把整张图当成一个整体处理。如果图像一部分很暗、一部分很亮,均衡后暗部可能过曝,亮部可能丢失细节。

CLAHE(Contrast Limited Adaptive Histogram Equalization)的解决思路是:把图像切成小块,每个小块单独做直方图均衡,然后用插值消除块之间的边界。最关键的是——它限制了对比度的放大倍数,防止噪声被过度增强。

参数 含义 典型值 我的建议
clipLimit 对比度限制阈值 2.0 ~ 5.0 先试 3.0,效果不够再往上加
tileGridSize 分块大小 (8,8) ~ (16,16) 小图用 8x8,大图用 16x16
def clahe_enhance(img, clip_limit=3.0, grid_size=(8,8)):
    # 创建 CLAHE 对象
    clahe = cv2.createCLAHE(
        clipLimit=clip_limit,
        tileGridSize=grid_size
    )
    # 应用 CLAHE
    enhanced = clahe.apply(img)
    return enhanced

# 彩色图像处理:先转 LAB,只处理 L 通道
img_color = cv2.imread('low_contrast.jpg')
lab = cv2.cvtColor(img_color, cv2.COLOR_BGR2LAB)
l, a, b = cv2.split(lab)
l_enhanced = clahe_enhance(l, clip_limit=2.5)
lab_enhanced = cv2.merge([l_enhanced, a, b])
result = cv2.cvtColor(lab_enhanced, cv2.COLOR_LAB2BGR)
避坑指南: 我曾经在医学内窥镜图像上直接用 CLAHE 处理 RGB 三个通道,结果颜色完全偏掉。后来才意识到——CLAHE 应该只作用于亮度通道。彩色图像先转 LAB 或 HSV,只处理 L 或 V 通道,再合并回去。这是血的教训。

4.4 多尺度 Retinex:分离光照与反射

Retinex 理论认为:人眼看到的图像 = 光照分量 × 反射分量。光照是低频的、缓慢变化的;反射是高频的、包含细节的。我们想要的增强,本质上是「去掉不均匀光照,保留或增强反射细节」。

多尺度 Retinex(MSR)的做法是:用多个不同尺度的高斯滤波估计光照,然后从原图中「除掉」光照,剩下的就是反射。多个尺度加权平均,兼顾局部和全局。

def multi_scale_retinex(img, scales=[15, 80, 250], weights=None):
    if weights is None:
        weights = [1/3] * len(scales)
    
    img_float = np.float32(img) + 1.0
    img_log = np.log(img_float)
    
    msr = np.zeros_like(img_float)
    for scale, weight in zip(scales, weights):
        # 高斯模糊估计光照
        blur = cv2.GaussianBlur(img_float, (0,0), scale)
        blur_log = np.log(blur)
        # 单尺度 Retinex
        ssr = img_log - blur_log
        msr += weight * ssr
    
    # 归一化到 [0, 255]
    msr = cv2.normalize(msr, None, 0, 255, cv2.NORM_MINMAX)
    return np.uint8(msr)
关键参数: 三个尺度分别控制局部、中频、全局。15 负责保留边缘,80 处理中等纹理,250 消除整体光照不均。权重可以平均分配,也可以根据场景调整——雾霾天我倾向于加大中尺度权重。

这四种方法各有侧重,我整理了一张对比表:

方法 适用场景 优点 缺点
伽马校正 整体偏暗/偏亮 简单、可逆、感知均匀 无法处理局部光照不均
对数变换 动态范围极大(如HDR) 强暗部提升、压缩亮部 噪声放大明显
CLAHE 局部对比度不足 自适应、限制噪声 参数敏感、彩色需特殊处理
多尺度Retinex 光照不均、雾霾、阴影 光照分离、细节保留好 计算量大、可能有光晕

最后说一句:这些方法不是互斥的。我经常把 CLAHE 和伽马校正串起来用——先用 CLAHE 提升局部对比度,再用伽马微调整体亮度。效果往往 1+1 > 2。但要注意顺序:先局部后整体,先细节后亮度。