作为图像增强算法系列的第二篇文章,下面我们将要介绍功能强大、用途广泛、影响深远的对比度有限的自适应直方图均衡(CLAHE,Contrast Limited Adaptive Histogram Equalization)算法。尽管最初它仅仅是被当作一种图像增强算法被提出,但是现今在图像去雾、低照度图像增强,水下图像效果调节、以及数码照片改善等方面都有应用。这个算法的算法原理看似简单,但是实现起来却并不那么容易。我们将结合相应的Matlab代码来对其进行解释。希望你在阅读本文之前对朴素的直方图均衡算法有所了解,相关内容可以参见本系列的前一篇文章:基于直方图的图像增强算法(HE、CLAHE、Retinex)之(一)。
先来看一下待处理的图像效果:
下面是利用CLAHE算法处理之后得到的两个效果(后面我们还会具体介绍我们所使用的策略):
效果图A 效果图B
对于一幅图像而言,它不同区域的对比度可能差别很大。可能有些地方很明亮,而有些地方又很暗淡。如果采用单一的直方图来对其进行调整显然并不是最好的选择。于是人们基于分块处理的思想提出了自适应的直方图均衡算法AHE。维基百科上说的也比较明白:AHE improves on this by transforming each pixel with a transformation function derived from a neighbourhood region. 但是这种方法有时候又会将一些噪声放大,这是我们所不希望看到的。于是荷兰乌得勒支大学的Zuiderveld教授又引入了CLAHE,利用一个对比度阈值来去除噪声的影响。特别地,为了提升计算速度以及去除分块处理所导致的块边缘过渡不平衡效应,他又建议采用双线性插值的方法。关于算法的介绍和描述,下面这两个资源已经讲得比较清楚。
[1] https://en.wikipedia.org/wiki/Adaptive_histogram_equalization#Contrast_Limited_AHE
[2] K. Zuiderveld: Contrast Limited Adaptive Histogram Equalization. In: P. Heckbert: Graphics Gems IV, Academic Press 1994 (http://www.docin.com/p-119164091.html)
事实上,尽管这个算法原理,然而它实现起来却仍然有很多障碍。但在此之前,笔者还需说明的是,Matlab中已经集成了实现CLAHE的函数adapthisteq(),如果你仅仅需要一个结果,其实直接使用这个函数就是最好的选择。我给出一些示例代码用以生成前面给出之效果。函数adapthisteq()只能用来处理灰度图,如果要处理彩色图像,则需要结合自己编写的代码来完成。上一篇文章介绍了对彩色图像进行直方图均衡的两种主要策略:一种是对R、G、B三个通道分别进行处理;另外一种是转换到另外一个色彩空间中再进行处理,例如HSV(转换后只需对V通道进行处理即可)。
首先,我们给出对R、G、B三个通道分别使用adapthisteq()函数进行处理的示例代码:
上述程序之结果效果图A所示。
下面程序将原图像的色彩空间转换到LAB空间之后再对L通道进行处理。
上述程序所得之结果如图B所示。
如果你希望把这个算法进一步提升和推广,利用用于图像去雾、低照度图像改善和水下图像处理,那么仅仅知其然是显然不够的,你还必须知其所以然。希望我下面一步一步实现的代码能够帮你解开这方面的困惑。鉴于前面所列之文献已经给出了比较详细的算法描述,下面将不再重复这部分内容,转而采用Matlab代码来对其中的一些细节进行演示。
首先来从灰度图的CLAHE处理开始我们的讨论。为此清理一下Matlab的环境。然后,读入一张图片(并将其转化灰度图),获取图片的长、宽、像素灰度的最大值、最小值等信息。
图像的初始状态显示如下。此外该图的 Height = 395,Width = 590,灰度最大值为255,最小值为8。
我们希望把原图像水平方向分成8份,把垂直方向分成4份,即原图将被划分成4 × 8 = 32个SubImage。然后可以算得每个块(tile)的height = 99,width = 74。注意,由于原图的长、宽不太可能刚好可被整除,所以我在这里的处理方式是建立一个稍微大一点的图像,它的宽和长都被补上了deltax和deltay,以保证长、宽都能被整除。
对长和宽进行填补之后,对新图像的一些必要信息进行更新。
然后指定图像中直方图横坐标上取值的计数(也就指定了统计直方图上横轴数值的间隔或计数的精度),对于色彩比较丰富的图像,我们一般都要求这个值应该大于128。
然后用原图的灰度取值范围重新映射了一张Look-Up Table(当然你也可以直接使用0~255这个范围,这取决你后续建立直方图的具体方法),并以此为基础为每个图像块(tile)建立直方图。
注意:按通常的理解,上面这一步我们应该建立的直方图(集合)应该是一个4×8=32个长度为256的向量(你当然也可以这么做)。但由于涉及到后续的一些处理方式,我这里是生成了一个长度为256的4×8矩阵。Index = 1的矩阵其实相当于是整张图像各个tile上灰度值=0的像素个数计数。例如,我们所得的Hist(:, :, 18)如下。这就表明图像中最左上角的那个tile里面灰度值=17的像素有零个。同理,它右边的一个tile则有46个灰度值=17的像素。
然后来对直方图进行裁剪。Matlab中内置的函数adapthisteq()中cliplimit参数的取值范围是0~1。这里我们所写的方法则要求该值>1。当然这完全取决你算法实现的策略,它们本质上并没有差异。然后我们将得到新的(裁剪后的)映射直方图。
因为这里没有具体给出clipHistogram函数的实现,所以此处我希望插入一部分内容来解释一下我的实现策略(也就是说,在实际程序中并不需要包含这部分)。我们以图像最左上角的一个tile为例,它的原直方图分布可以用下面代码来绘出:
输出结果下图中的左图所示。
如果我们给ClipLimit赋初值为2.5,则经过语句ClipLimit = max(1,ClipLimit * HSize * WSize/NrBins);计算之后,ClipLimit将变成71.54。然后我们再用上述代码绘制新的直方图,其结果将如上图中的右图所示。显然,图中大于71.54的部分被裁剪掉了,然后又平均分配给整张直方图,所以你会发现整张图都被提升了。这就是我们这里进行直方图裁剪所使用的策略。但是再次强调,matlab中的内置函数adapthisteq()仅仅是将这个参数进行了归一化,这与我们所使用的方法并没有本质上的区别。
继续回到程序实现上的讨论。最后,也是最关键的步骤,我们需要对结果进程插值处理。这也是Zuiderveld设计的算法中最复杂的部分。
这个地方,作者原文中已经讲得比较清楚了,我感觉我也没有必要狗尾续貂,班门弄斧了。下面截作者原文中的一段描述,足以说明问题。
最后来看看我们处理的效果如何(当然,这里还需要把之前我们填补的部分裁掉)。
来看看结果吧~可以对比一下之前的灰度图,不难发现,图像质量已有大幅改善。