1 概念
直方图均衡化,同伽马变换一样,也是增强图像对比度的一种工具。区别在于,直方图均衡化是一种自适应的工具,即自动工具。也就是说,我们只需要将一张待处理的图片丢给这个工具,就可以实现对比度的增强,而无需像伽马变换一样需要手动调整一个输入参数。
什么是直方图呢?这里以很简单的方式来解释。我们可以将直方图视为图形或绘图,它可以用来从而可以总体了解图像的强度分布。其x轴是像素的等级(根据图像的类型判断,大部分情况是0到255),而y轴是相应像素值的数量。换言之,其实就是一个统计图,用于统计图片中像素值为x的像素点有多少个。
如下是一个直方图的例子,这个图片是彩色的,但实际上是先将该图转化为对应的灰度图像再绘制的直方图。其实想要对彩色图像做直方图均衡化也很简单,只要分别对三个通道单独做一次,再将三个结果组合在一起即可。
2 原理
2.1 数学原理
直方图均衡化其实会用到一些简单的概率论知识。我尽量以比较通俗的方法来叙述,所以其中的一些表述可能不是很严谨,不过仅用于理解本例是足够了。(如果你对概率论比较了解可以直接看公式掠过废话)
ps:以下仅为理论部分的叙述,实际代码实现时还会有一些不同的地方,但其实本质是一样的,只不过先熟悉原理会更好理解。
首先第一步,肯定就是要统计出所有像素值分别有多少个,这个其实就是一个图像遍历的过程。
做完上一步后,我们需要求取每一个像素值的概率密度函数(PDF),什么是概率密度函数呢?其实很好理解,就是图像中像素值为x的点的个数对于图像像素点总数的比重。因为这里是离散的情况,所以非常好求。假设像素值为k的点在图像(假设图像总共有MN个像素)中有r个,那么该点的概率密度函数,或者说这些点所占据的个数比重就是
P
D
F
=
r
M
N
PDF={r\over{MN}}
PDF=MNr
之后还需要介绍一下累积分布函数(CDF)的概念,这个概念刚开始理解可能会不知所云,其实,跟PDF对照起来就很好理解。
PDF是针对一个特定的点计算的,也就是说是一对一的关系。
而CDF是指(在本例中),假如针对一个像素值x,求它的CDF,就是所有像素值≤x的点的PDF之和。
举一个例子,假设我现在要求像素值为5的PDF,即 P D F 5 PDF_5 PDF5 ,那么结果就是像素值为5的点在所有点中占据的比重。
而如果现在我要求 C D F 5 CDF_5 CDF5 ,根据上面的定义,结果就是所有像素值≤5的PDF之和。由于这里是离散的情况,且像素值的最低边界是0,所以这里计算起来并没有很复杂,无非就是
C D F 5 = P D F 0 + P D F 1 + P D F 2 + P D F 3 + P D F 4 + P D F 5 CDF_5=PDF_0+PDF_1+PDF_2+PDF_3+PDF_4+PDF_5 CDF5=PDF0+PDF1+PDF2+PDF3+PDF4+PDF5
不难知道,CDF是非递减的,也就是说, C D F x + 1 ≥ C D F X CDF_{x+1}≥CDF_X CDFx+1≥CDFX
得到CDF以后,就可以引出最核心的步骤了,也就是直方图均衡化的核心公式:
S
x
=
(
L
−
1
)
C
D
F
x
,
x
∈
[
0
,
L
−
1
]
S_x=(L-1)CDF_x\ ,\ \ \ \ x\in[0, L-1]
Sx=(L−1)CDFx , x∈[0,L−1]
其中,L表示图像的像素等级数,对于256级的灰度图L就是256。
S
x
S_x
Sx是映射的值,表示的是原图中像素值为x的点需要映射到这个值,其实就是一种变化关系。这个公式其实也跟概率论有关,如果不理解的话直接记忆即可,或者可以学习一下概率论相关的知识。
利用这个公式,将原图中的每个像素点进行映射输出以后,就得到了直方图均衡化的结果。可以发现,直方图均衡化其实是一种线性变化,所以这种方法也被叫做直方图线性变化。
3 python代码实现
上面说到,代码实现跟理论是由一些差别的。至于差别在哪,先上代码再说结论
def equalize_self(src_img):
"""
:param src_img: 待处理图像
:return: 直方图均衡化后的新图像
""" height, width = src_img.shape[:2]
# 统计r_k
r_dict = [0 for i in range(256)]
for r in range(height):
for c in range(width):
r_dict[src_img[r, c]] += 1
# 计算累积分布函数(使用前缀和算法优化)
cdf = [0 for i in range(256)]
cdf[0] = r_dict[0]
for i in range(1, 256):
cdf[i] = cdf[i - 1] + r_dict[i]
# 计算映射关系,建立查找表
s = []
for i in range(256):
# 累积分布函数的最小值不一定是0,直接采取书上的公式会导致图片偏灰
# 对于每一个cdf,计算时将其减去cdf_min使最小的像素映射到0,得到的图片效果更接近库方法
s.append((cdf[i] - cdf[0]) * 255.0 / (height * width - cdf[0]))
# 查表修改原像素值
dst_img = np.zeros((height, width), dtype=np.uint8)
for r in range(height):
for c in range(width):
dst_img[r, c] = round(s[src_img[r, c]])
return dst_img
首先关注到s.append这一行,append内的参数其实就是上面所给的直方图均衡化的映射公式。
有趣的是,会发现这里的cdf[i]在参与运算时,还减去了cdf[0],这是为什么呢?其实,这是为了正确调整归一化的比例范围,确保映射结果的动态范围合理。
实际图像中的像素值并不总是从 0 开始,图像的最小像素值可能不是 0。因此,累积分布函数的最小值 C D F m i n CDF_{min} CDFmin也不一定是 0。
如果不减去 C D F m i n CDF_min CDFmin,则累积分布函数 CDF 中的最小值不会映射为 0,而是映射为一个正值,导致图像中没有真正的黑色区域,会让图像看起来灰蒙蒙的,缺少足够的对比度。因此,通过减去 C D F m i n CDF_{min} CDFmin,我们可以将图像中的最小灰度值映射为 0,从而增强对比度,最大化使用图像的动态范围。
注意到为什么这一项还除上了一串东西,其实这是一个运算简化的过程。在前面计算CDF时,我并没有先计算PDF,这是因为PDF都会除以一个公共项,那就是图像的像素总数。因此我们可以先算分子的部分,最后只进行一次除法就可以了。不过由于上述我们减去 C D F m i n CDF_{min} CDFmin,累积分布函数的起点变成 0,但其终点也相应地缩小了。因此,整个映射范围也需要缩小,以确保新的 CDF 被正确地归一化到 0 和 255 之间,即像素总数也要减去 C D F m i n CDF_{min} CDFmin
还有一个值得提的点,计算CDF时,可以使用前缀和算法优化,这样可以帮我们省去很多的重复计算,关于前缀和算法可以自行查阅相关资料了解。
4 测试效果
下面给出几组测试案例。每一组图像第一行是原图,第二行是上述实现的直方图均衡化处理,第三行是使用OpenCV提供的直方图均衡化方法处理,右侧绘制出了对应的直方图。
5 结论
可以看到,直方图均衡化适用于处理一些灰度分布较集中的图像。通过直方图均衡化可以动态拉伸图像的灰度范围,并通过这种方式增强图像的对比度。如果待处理图像的灰度比较集中,或者是看上去对比度很低,不妨可以试试用直方图均衡化处理。