4. 非线性灰度变换:对数、指数与伽马变换

线性拉伸搞定了大部分基础场景,但现实世界没这么听话。你想想看,一张背光拍摄的照片,暗部细节全埋在低灰度区,亮部又过曝——线性拉伸要么把暗部提亮的同时让亮部更亮,要么顾此失彼。这时候就需要非线性变换出手了。

说白了,非线性变换的核心思路就一句话:对不同灰度区间区别对待。有的区间要压缩,有的区间要拉伸,全看你的需求。我个人习惯把这类变换分成三大家族:对数、指数和幂律(伽马)。它们数学上都是单调递增函数,但弯曲方向完全不同。

核心记忆点:对数族压缩高亮区、拉伸暗区;指数族压缩暗区、拉伸高亮区;伽马族通过调节 γ 值在两者之间平滑切换。

4.1 对数变换:把暗部细节「挖」出来

对数变换的数学形式很简单:

s = c * log(1 + r)

其中 r 是输入灰度,s 是输出灰度,c 是缩放常数。注意这里加了个 1,防止 r=0 时 log(0) 爆炸。

为什么对数能提亮暗部?你想想对数曲线的形状——在低输入区域斜率大,高输入区域斜率小。斜率大意味着输出变化明显,暗部细节就被放大了。高亮区斜率平缓,输出变化小,相当于被压缩了。

我在项目中遇到过一个典型的医学影像案例:CT 图像的原始数据动态范围极大(12-16 bit),但人眼只能分辨 8 bit 左右的灰度差异。直接线性映射到 8 bit,暗部病灶区域根本看不清。用对数变换把高灰度区压缩、低灰度区拉伸,病灶边缘一下就清晰了。

实用技巧:对数变换的 c 值一般取 255 / log(256) ≈ 46,这样能把 0-255 的输入映射到 0-255 的输出。但实际项目中我经常手动调 c,因为不同图像的暗部密度不一样。

4.2 指数变换:让亮部更「通透」

指数变换正好反过来:

s = c * (r^γ)

当 γ > 1 时,这就是指数变换(或者说幂律变换的特例)。它的曲线在低输入区斜率小,高输入区斜率大——暗部被压缩,亮部被拉伸。

什么时候用指数变换?我曾经处理过一批水下摄影图像,水体对蓝绿光的衰减导致整体偏暗,但暗部其实没什么细节,真正有价值的信息在中等灰度以上的珊瑚和鱼群。用指数变换把中等灰度以上区域拉开,视觉对比度明显提升,暗部噪声反而被抑制了——一举两得。

变换类型 曲线形状 拉伸区域 压缩区域 典型应用
对数变换 上凸(凹函数) 暗部(低灰度) 亮部(高灰度) 医学影像、背光照片
指数变换 (γ>1) 下凸(凸函数) 亮部(高灰度) 暗部(低灰度) 水下图像、过暗场景
幂律变换 (γ<1) 上凸(凹函数) 暗部(低灰度) 亮部(高灰度) 显示器伽马校正、HDR

4.3 幂律(伽马)变换:最灵活的「变形金刚」

幂律变换的公式和指数变换一模一样:

s = c * r^γ

但区别在于 γ 可以取任意正实数。当 γ < 1 时,它表现得像对数变换(提暗部);当 γ > 1 时,它表现得像指数变换(提亮部);当 γ = 1 时,它就是线性变换。

这就是为什么我说伽马变换是「变形金刚」——一个公式覆盖了所有非线性需求。你只需要调一个参数 γ,就能在暗部拉伸和亮部拉伸之间平滑过渡。

⚠ 避坑指南:我曾经在一个工业质检项目里直接用 γ=0.4 处理所有图像,结果部分样本的亮部细节完全丢失。后来发现不同光照条件下最优 γ 值差异很大。我的教训是:永远不要对一批图像用固定 γ 值,至少要做个简单的自适应估计。

4.4 三大家族对比:什么时候用哪个?

很多初学者会问:既然伽马变换能覆盖所有情况,为什么还要学对数和指数?

嗯,这里要注意。伽马变换虽然灵活,但它的数学形式决定了它在极端暗部(r 接近 0)时斜率会趋于无穷大(γ<1)或 0(γ>1),这会导致暗部噪声被过度放大或完全丢失。对数变换在 r→0 时斜率虽然也大,但增长更平缓,对噪声的放大更可控。

我个人习惯的选型原则:

  • 暗部细节极其重要(如医学影像、天文图像)→ 优先对数变换,噪声控制更好
  • 亮部细节更重要(如高光纹理分析)→ 优先指数变换(γ=1.5~2.0)
  • 需要精细调节(如显示器校正、HDR 合成)→ 伽马变换,γ 从 0.3 到 2.5 慢慢试
  • 图像整体偏暗但暗部无细节(如夜间监控)→ 别用对数,用伽马 γ=0.7~0.9 轻微提亮即可

4.5 知识体系:一张图看懂非线性变换

下面这张 SVG 图把三种变换的核心逻辑、曲线特征和适用场景串在了一起。我建议你把它当作本章的「思维导图」来记。

非线性灰度变换知识体系 非线性灰度变换 对数变换 幂律(伽马)变换 指数变换 公式: s = c·log(1+r) 曲线: 上凸(凹函数) 拉伸暗部 · 压缩亮部 应用: 医学影像、背光 公式: s = c·r^γ γ<1: 上凸 · γ>1: 下凸 γ=1: 线性变换(特例) 应用: 显示器校正、HDR 公式: s = c·r^γ (γ>1) 曲线: 下凸(凸函数) 拉伸亮部 · 压缩暗部 应用: 水下图像、过暗 核心选择逻辑 暗部细节重要 → 对数 / γ<1 伽马 | 亮部细节重要 → 指数 / γ>1 伽马 | 需要精细控制 → 伽马

4.6 代码实战:三行代码调出三种效果

理论说完了,直接上代码。我用 OpenCV 实现三种变换,你复制就能跑:

import cv2
import numpy as np

def log_transform(img, c=46):
    """对数变换:提亮暗部"""
    img_float = img.astype(np.float32) / 255.0
    result = c * np.log(1 + img_float)
    return np.clip(result, 0, 255).astype(np.uint8)

def gamma_transform(img, gamma=0.5, c=1.0):
    """伽马变换:γ<1 提暗部,γ>1 提亮部"""
    img_float = img.astype(np.float32) / 255.0
    result = c * np.power(img_float, gamma)
    return np.clip(result * 255, 0, 255).astype(np.uint8)

def exp_transform(img, gamma=1.5, c=1.0):
    """指数变换:本质是 γ>1 的伽马变换"""
    return gamma_transform(img, gamma=gamma, c=c)

# 使用示例
img = cv2.imread('dark_image.jpg', cv2.IMREAD_GRAYSCALE)
log_img = log_transform(img, c=50)
gamma_img = gamma_transform(img, gamma=0.4)
exp_img = exp_transform(img, gamma=2.0)

cv2.imwrite('log_result.jpg', log_img)
cv2.imwrite('gamma_result.jpg', gamma_img)
cv2.imwrite('exp_result.jpg', exp_img)

调试建议:第一次跑的时候,先用一张简单的 8 bit 灰度图试。把 gamma 从 0.2 到 2.0 每隔 0.2 跑一遍,观察输出图像的变化趋势。我当年就是这么「肉眼标定」出最适合自己项目的 γ 值的。

4.7 本章小结

非线性灰度变换的核心就三件事:对数提暗部、指数提亮部、伽马全覆盖。选型时别死记硬背,想想你的图像里哪部分信息更重要——暗部细节多就用对数或 γ<1 的伽马,亮部细节多就用指数或 γ>1 的伽马。如果拿不准,从 γ=0.5 和 γ=1.5 两个方向试,对比结果再决定。