3. 直方图均衡化实现:从理论到代码
上一章我们聊了直方图均衡化的数学原理,说白了就是让像素分布更均匀。但理论归理论,真正写代码时你会发现——嗯,坑还挺多的。这一章我就带大家亲手实现一遍,顺便聊聊我踩过的那些雷。
3.1 Python 实现灰度图均衡化
先别急着调 OpenCV 的现成函数。我个人习惯,学任何算法都先手撸一遍,这样你才知道黑盒子里到底发生了什么。
手写均衡化的核心就三步:
- 统计直方图——数一数每个灰度级有多少像素
- 计算累积分布函数(CDF)——归一化后得到映射表
- 像素映射——用映射表替换原图每个像素值
下面是我常用的实现,注释都写清楚了:
import numpy as np
import cv2
from matplotlib import pyplot as plt
def my_equalize_hist(img):
"""
手写直方图均衡化
img: 灰度图,dtype=uint8
"""
# 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. 映射:CDF * 255 并取整
lut = np.round(cdf * 255).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
# 测试
img = cv2.imread('dark_image.jpg', cv2.IMREAD_GRAYSCALE)
eq_img, lut = my_equalize_hist(img)
np.vectorize 或直接查表索引 result = lut[img] 会快两个数量级。
我曾经在嵌入式设备上跑这个循环版本,一张 640×480 的图用了 0.8 秒。换成查表法后,降到 5 毫秒。你想想看,差了 160 倍。
3.2 OpenCV 的 equalizeHist 函数
当然,实际工程中没人会手写。OpenCV 提供了现成的 equalizeHist,一行代码搞定:
import cv2
img = cv2.imread('dark_image.jpg', cv2.IMREAD_GRAYSCALE)
eq_img = cv2.equalizeHist(img)
就这么简单。但注意——这个函数只接受单通道灰度图,输入必须是 uint8 类型。我见过有人直接传彩色图进去,结果 OpenCV 报错,然后跑来问我为什么。嗯,文档还是要看的。
OpenCV 内部实现比我手写的要优化得多:
- 它用了 并行计算,多核 CPU 下速度更快
- 对边界情况做了处理(比如全黑图、全白图)
- 内存访问模式做了缓存友好优化
但核心算法和我上面写的完全一致。所以如果你手写的结果和 OpenCV 对不上,那一定是你写错了。
equalizeHist 处理 16 位 DICOM 图像,结果一片漆黑。因为函数默认输入是 8 位,16 位数据会被截断。处理高位深图像时,需要先归一化到 0-255 再均衡化。
3.3 彩色图像均衡化策略
彩色图比灰度图麻烦得多。你不能直接把三个通道分别均衡化然后拼回去——那样颜色会完全乱掉。我刚开始学的时候就这么干过,结果出来的图像鬼片一样。
为什么会这样?因为 RGB 三个通道的像素分布是耦合的,单独调整会破坏色彩平衡。正确的做法有三种:
| 策略 | 方法 | 效果 | 适用场景 |
|---|---|---|---|
| RGB 通道独立均衡 | 分别对 R、G、B 做均衡化 | 颜色失真严重,不推荐 | 几乎不用 |
| HSV/HSL 亮度均衡 | 转 HSV,只对 V 通道均衡化 | 保持色调,对比度提升 | 通用场景,最常用 |
| Lab 亮度均衡 | 转 Lab,只对 L 通道均衡化 | 色彩更自然,但计算稍慢 | 对色彩保真度要求高的场景 |
我个人最常用的是 HSV 方案,代码也很简洁:
import cv2
import numpy as np
def equalize_color_hsv(img):
"""
彩色图像均衡化:HSV 空间,只处理 V 通道
img: BGR 彩色图
"""
# 转 HSV
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
h, s, v = cv2.split(hsv)
# 只对亮度通道均衡化
v_eq = cv2.equalizeHist(v)
# 合并回 HSV
hsv_eq = cv2.merge([h, s, v_eq])
# 转回 BGR
return cv2.cvtColor(hsv_eq, cv2.COLOR_HSV2BGR)
# 使用
img_color = cv2.imread('color_image.jpg')
result = equalize_color_hsv(img_color)
Lab 方案效果更好,但速度慢一些。如果你做的是医学图像或遥感图像,我建议用 Lab。普通照片的话,HSV 完全够用。
3.4 三种实现方式对比
我把三种方式放在一起对比一下,方便你选型:
| 实现方式 | 代码量 | 速度 | 可控性 | 推荐场景 |
|---|---|---|---|---|
| 手写实现 | ~20 行 | 慢(循环版) | 最高 | 学习原理、定制算法 |
| OpenCV 灰度 | 1 行 | 极快 | 低 | 灰度图快速处理 |
| OpenCV 彩色 | ~8 行 | 快 | 中 | 彩色图通用场景 |
你想想看,如果只是做快速验证,OpenCV 一行代码就够了。但如果你要理解算法本质,或者需要定制映射曲线,手写实现是必须的。
3.5 核心流程总结
下面这张图把本章的知识点串起来了,我建议你保存下来对照着看:
这张图把整个流程理得很清楚。灰度图走左边,直接调 equalizeHist。彩色图走右边,先转色彩空间,再只均衡亮度通道。殊途同归,最后都输出增强后的图像。
- 手写均衡化理解原理,OpenCV 函数用于工程
- 彩色图像均衡化必须转换色彩空间,只处理亮度通道
- HSV 方案通用,Lab 方案色彩更准
- 永远不要对 RGB 三个通道分别均衡化