2. 对比度拉伸:从线性到自适应
图像预处理里,对比度拉伸是我用得最多的操作之一。说白了,就是把图像里太暗和太亮的像素拉开,让细节能看清楚。你想想看,一张雾蒙蒙的图,经过拉伸之后,纹理就全出来了。
这一节我们聊四种方法:线性拉伸、分段线性拉伸、直方图均衡化、自适应直方图均衡化。每种都有它的脾气,我一个个说。
核心思路:对比度拉伸的本质是重新映射像素的灰度值,让原本集中在狭窄区间的像素分布到更宽的区间。
2.1 线性拉伸:最直接的映射
线性拉伸的原理很简单。假设原始图像的最小灰度值是 \(I_{min}\),最大是 \(I_{max}\),你想把它拉伸到 \([0, 255]\) 区间。公式就是:
O = (I - I_min) * 255 / (I_max - I_min)
嗯,这里要注意。如果图像里有一个孤立的噪声点,比如一个纯白像素,那 \(I_{max}\) 就被拉到了 255,其他像素反而被压缩了。我在项目中遇到过这种情况——一张 CT 图像,因为传感器有个坏点,线性拉伸后整张图都发灰。
我的习惯:做线性拉伸前,先去掉上下 1% 的极值。用百分位截断,效果会稳很多。
代码实现也很直接:
import numpy as np
import cv2
def linear_stretch(img, low_percent=1, high_percent=99):
# 计算百分位
p_low = np.percentile(img, low_percent)
p_high = np.percentile(img, high_percent)
# 截断并拉伸
img_clipped = np.clip(img, p_low, p_high)
img_stretched = (img_clipped - p_low) / (p_high - p_low) * 255
return img_stretched.astype(np.uint8)
2.2 分段线性拉伸:想拉哪里拉哪里
线性拉伸是全局的,但有时候我们只想增强某个灰度区间。比如医学图像里,骨骼和软组织分布在不同的灰度范围,你想让骨骼更清晰,同时保留软组织细节。这时候分段线性拉伸就派上用场了。
分段线性拉伸把灰度轴分成几段,每段用不同的斜率拉伸。斜率大于 1 的区间会被拉伸,小于 1 的会被压缩。
| 区间 | 斜率 | 效果 |
|---|---|---|
| [0, r1] | k1 | 压缩暗部 |
| [r1, r2] | k2 | 拉伸目标区域 |
| [r2, 255] | k3 | 压缩亮部 |
我曾经处理过一批工业检测的 X 光图像。缺陷区域集中在灰度 80-150 之间,背景在 200 以上。我设了三个分段:暗部压缩、中间拉伸、亮部压缩。效果立竿见影,缺陷的对比度提升了 3 倍。
def piecewise_linear(img, r1, s1, r2, s2):
# 三段映射
lut = np.zeros(256, dtype=np.uint8)
for i in range(256):
if i < r1:
lut[i] = int(s1 / r1 * i)
elif i < r2:
lut[i] = int((s2 - s1) / (r2 - r1) * (i - r1) + s1)
else:
lut[i] = int((255 - s2) / (255 - r2) * (i - r2) + s2)
return cv2.LUT(img, lut)
避坑指南:我曾经把斜率设得太大,结果目标区域出现了明显的量化伪影——灰度级不够用了,图像出现条带。分段拉伸时,输出灰度级最好保留 256 级,不要过度拉伸。
2.3 直方图均衡化:自动增强的利器
直方图均衡化(HE)是另一种思路。它不依赖人工设定参数,而是根据图像的直方图分布,自动生成一个映射函数。目标是让输出图像的直方图尽可能均匀分布。
原理其实不复杂。先计算累积分布函数(CDF),然后用 CDF 作为映射函数。直方图里像素多的灰度级会被拉伸,像素少的会被压缩。
我举个例子。一张背光拍摄的照片,人脸区域集中在暗部,背景在亮部。均衡化之后,人脸的细节会变得非常清楚。但有个问题——背景可能会过曝。
def histogram_equalization(img):
# 如果是彩色图,先转 YUV 只处理亮度通道
if len(img.shape) == 3:
img_yuv = cv2.cvtColor(img, cv2.COLOR_BGR2YUV)
img_yuv[:,:,0] = cv2.equalizeHist(img_yuv[:,:,0])
return cv2.cvtColor(img_yuv, cv2.COLOR_YUV2BGR)
else:
return cv2.equalizeHist(img)
关键点:直方图均衡化是全局操作。如果图像光照不均匀,比如半边亮半边暗,均衡化后暗部噪声会被放大,亮部细节可能丢失。
2.4 自适应直方图均衡化:解决光照不均
全局均衡化的短板很明显。我处理过一张显微图像,左上角有强光源,右下角是阴影。全局均衡化之后,阴影区域的噪声被放大了好几倍,根本没法看。
自适应直方图均衡化(AHE)就是来解决这个问题的。它把图像分成一个个小窗口(tile),在每个窗口内独立做均衡化。这样每个局部都能得到合适的增强。
但 AHE 有个毛病——它容易放大噪声。尤其是在均匀区域,比如天空或皮肤,噪声会被明显增强。
后来有了限制对比度自适应直方图均衡化(CLAHE)。它在每个窗口做均衡化时,会限制对比度的放大倍数。超过阈值的部分会被裁剪并重新分布。
| 方法 | 窗口大小 | 对比度限制 | 适用场景 |
|---|---|---|---|
| 全局 HE | 整图 | 无 | 光照均匀的图像 |
| AHE | 8×8 或 16×16 | 无 | 光照不均但噪声低 |
| CLAHE | 8×8 | 2-4 | 大多数场景 |
def clahe_enhance(img, clip_limit=2.0, tile_size=8):
clahe = cv2.createCLAHE(
clipLimit=clip_limit,
tileGridSize=(tile_size, tile_size)
)
if len(img.shape) == 3:
img_lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
img_lab[:,:,0] = clahe.apply(img_lab[:,:,0])
return cv2.cvtColor(img_lab, cv2.COLOR_LAB2BGR)
else:
return clahe.apply(img)
参数调优建议:clip_limit 我一般从 2.0 开始试。tile_size 默认 8×8 够用,如果图像纹理很细,可以降到 4×4,但计算量会翻倍。
2.5 四种方法对比总结
| 方法 | 优点 | 缺点 | 我常用的场景 |
|---|---|---|---|
| 线性拉伸 | 简单、速度快 | 受极值影响大 | 传感器标定后的原始数据 |
| 分段线性拉伸 | 灵活控制局部 | 需要人工调参 | 工业检测、医学图像 |
| 直方图均衡化 | 自动增强、效果好 | 全局操作、可能过增强 | 背光照片、监控图像 |
| CLAHE | 自适应、抑制噪声 | 计算量大、参数敏感 | 显微图像、遥感图像 |
我个人习惯是:先试线性拉伸,不行就上 CLAHE。分段线性拉伸虽然灵活,但调参太费时间,除非场景非常固定,否则我不太推荐在生产环境里用。
最后提醒一句:对比度拉伸不是万能的。如果原始图像的信噪比很低,拉伸只会让噪声更明显。这时候先做去噪,再做拉伸,顺序别搞反了。