空间域图像增强之限制对比度自适应直方图均衡(CLAHE)
一、自适应直方图均衡化(Adaptive histgram equalization/AHE)
1.简述
自适应直方图均衡化(AHE)用来提升图像的对比度的一种计算机图像处理技术。和普通的直方图均衡算法不同,AHE算法通过计算图像的局部直方图,然后重新分布亮度来来改变图像对比度。因此,该算法更适合于改进图像的局部对比度以及获得更多的图像细节。
不过,AHE有过度放大图像中相同区域的噪音的问题,另外一种自适应的直方图均衡算法即限制对比度直方图均衡(CLAHE)算法能有限的限制这种不利的放大。
2. 算法的解释
普通的直方图均衡算法对于整幅图像的像素使用相同的直方图变换,对于那些像素值分布比较均衡的图像来说,算法的效果很好。然后,如果图像中包括明显比图像其它区域暗或者亮的部分,在这些部分的对比度将得不到有效的增强。
AHE算法通过对局部区域执行响应的直方图均衡来改变上述问题。该算法首先被开发出来适用于改进航天器驾驶舱的显示效果。其最简单的形式,就是每个像素通过其周边一个矩形范围内的像素的直方图进行均衡化。均衡的方式则完全同普通的均衡化算法:变换函数同像素周边的累积直方图函数(CDF)成比例。
图像边缘的像素需要特殊处理,因为边缘像素的领域不完全在图像内部。这个通过镜像图像边缘的行像素或列像素来解决。直接复制边缘的像素进行扩充是不合适的。因为这会导致带有剑锋的领域直方图。
3. AHE的属性
- 领域的大小是该方法的一个参数。领域小,对比度得到增强,领域大,则对比度降低。
- 当某个区域包含的像素值非常相似,其直方图就会尖状化,此时直方图的变换函数会将一个很窄范围内的像素映射到整个像素范围。这将使得某些平坦区域中的少量噪音经AHE处理后过度放大。
二、限制对比度自适应直方图均衡(Contrast Limited Adaptive histgram equalization/CLAHE)
1.简述
CLAHE同普通的自适应直方图均衡不同的地方主要是其对比度限幅。这个特性也可以应用到全局直方图均衡化中,即构成所谓的限制对比度直方图均衡(CLHE),但这在实际中很少使用。在CLAHE中,对于每个小区域都必须使用对比度限幅。CLAHE主要是用来克服AHE的过度放大噪音的问题。
这主要是通过限制AHE算法的对比提高程度来达到的。在指定的像素值周边的对比度放大主要是由变换函数的斜度决定的。这个斜度和领域的累积直方图的斜度成比例。CLAHE通过在计算CDF前用预先定义的阈值来裁剪直方图以达到限制放大幅度的目的。这限制了CDF的斜度因此,也限制了变换函数的斜度。直方图被裁剪的值,也就是所谓的裁剪限幅,取决于直方图的分布因此也取决于领域大小的取值。
通常,直接忽略掉那些超出直方图裁剪限幅的部分是不好的,而应该将这些裁剪掉的部分均匀的分布到直方图的其他部分。如下图所示。
这个重分布的过程可能会导致那些倍裁剪掉的部分由重新超过了裁剪值(如上图的绿色部分所示)。如果这不是所希望的,可以不带使用重复不的过程指导这个超出的部分已经变得微不足道了。
代码参考:限制对比度自适应直方图均衡(CLAHE算法) 中的修建直方图(CL)代码
CLAHE的实现和研究 中的AHE代码与线性插值代码(这里的线性插值代码好像较为复杂,后续看有没有更简便的实现手段)
#include<iostream>
#include<opencv.hpp>
using namespace std;
using namespace cv;
void ClipHistogram(int* pHistogram, float clipThreshold);
int main()
{
Mat originImage = imread("E://匹配.jpg", 0);
imshow("原图", originImage);
Mat src = originImage.clone();
Mat src1 = originImage.clone();
const int blockNumber = 8;//把图像分成block数量
int width = src.cols;
int height = src.rows;
int singleBlockWidth = src.cols / 8;//每个block大小
int singleBlockHeight = src.rows / 8;
int pixelNumber[blockNumber *blockNumber][256] = { 0 };//存储不同block中各个不同灰度像素数量
float total[blockNumber *blockNumber][256] = { 0.0 };//累计直方图
for (int i = 0; i < blockNumber; i++)
{
for (int j = 0; j < blockNumber; j++)
{
int startPixelW = (i)*singleBlockWidth;
int endPixelW = (i + 1)*singleBlockWidth;
int startPixelH = (j)*singleBlockHeight;
int endPixelH = (j + 1)*singleBlockHeight;
int number = i + 8 * j;//统计运算到哪一个block了
int singleBlockPixelNumber = singleBlockWidth*singleBlockHeight;
for (int x = startPixelW; x < endPixelW; x++)//统计不同block中各个不同灰度像素数量
for (int y = startPixelH; y < endPixelH; y++)
{
int pixelValue = src.at<uchar>(y, x);
pixelNumber[number][pixelValue]++;
}
ClipHistogram(pixelNumber[number], 20);
for (int k = 0; k < 256; k++)//计算累计直方图
{
if (k == 0)
total[number][k] = 1.0*pixelNumber[number][k] / singleBlockPixelNumber;
else
total[number][k] = total[number][k - 1] + 1.0*pixelNumber[number][k] / singleBlockPixelNumber;
}
}
}
for (int i = 0; i < blockNumber; i++)//利用累计直方图对于原像素灰度在各自block中进行映射
{
for (int j = 0; j < blockNumber; j++)
{
int startPixelW = (i)*singleBlockWidth;
int endPixelW = (i + 1)*singleBlockWidth;
int startPixelH = (j)*singleBlockHeight;
int endPixelH = (j + 1)*singleBlockHeight;
int number = i + 8 * j;
int singleBlockPixelNumber = singleBlockWidth*singleBlockHeight;
for (int x = startPixelW; x < endPixelW; x++)
for (int y = startPixelH; y < endPixelH; y++)
{
int pixelValue = src1.at<uchar>(y, x);
src1.at<uchar>(y, x) = total[number][pixelValue] * 255;
}
}
}
imshow("均衡图无线性差值+限制对比度", src1);
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
//four coners
if (i <= singleBlockWidth / 2 && j <= singleBlockHeight / 2)
{
int num = 0;
src.at<uchar>(j, i) = (int)(total[num][src.at<uchar>(j, i)] * 255);
}
else if (i <= singleBlockWidth / 2 && (j >= ((blockNumber - 1)*singleBlockHeight +singleBlockHeight / 2))) {
int num = blockNumber*(blockNumber - 1);
src.at<uchar>(j, i) = (int)(total[num][src.at<uchar>(j, i)] * 255);
}
else if (i >= ((blockNumber - 1)*singleBlockWidth + singleBlockHeight / 2) && j <=singleBlockHeight / 2) {
int num = blockNumber - 1;
src.at<uchar>(j, i) = (int)(total[num][src.at<uchar>(j, i)] * 255);
}
else if (i >= ((blockNumber - 1)*singleBlockWidth + singleBlockWidth / 2) && j >= ((blockNumber - 1)*singleBlockHeight + singleBlockHeight / 2)) {
int num = blockNumber*blockNumber - 1;
src.at<uchar>(j, i) = (int)(total[num][src.at<uchar>(j, i)] * 255);
}
//four edges except coners
else if (i <= singleBlockWidth/ 2)
{
//线性插值
int num_i = 0;
int num_j = (j -singleBlockHeight / 2) /singleBlockHeight;
int num1 = num_j*blockNumber + num_i;
int num2 = num1 + blockNumber;
float p = (j - (num_j*singleBlockHeight +singleBlockHeight / 2)) / (1.0f*singleBlockHeight);
float q = 1 - p;
src.at<uchar>(j, i) = (int)((q*total[num1][src.at<uchar>(j, i)] + p*total[num2][src.at<uchar>(j, i)]) * 255);
}
else if (i >= ((blockNumber - 1)*singleBlockWidth+ singleBlockWidth/ 2)) {
//线性插值
int num_i = blockNumber - 1;
int num_j = (j -singleBlockHeight / 2) /singleBlockHeight;
int num1 = num_j*blockNumber + num_i;
int num2 = num1 + blockNumber;
float p = (j - (num_j*singleBlockHeight +singleBlockHeight / 2)) / (1.0f*singleBlockHeight);
float q = 1 - p;
src.at<uchar>(j, i) = (int)((q*total[num1][src.at<uchar>(j, i)] + p*total[num2][src.at<uchar>(j, i)]) * 255);
}
else if (j <=singleBlockHeight / 2) {
//线性插值
int num_i = (i - singleBlockWidth/ 2) / singleBlockWidth;
int num_j = 0;
int num1 = num_j*blockNumber + num_i;
int num2 = num1 + 1;
float p = (i - (num_i*singleBlockWidth+ singleBlockWidth/ 2)) / (1.0f*singleBlockWidth);
float q = 1 - p;
src.at<uchar>(j, i) = (int)((q*total[num1][src.at<uchar>(j, i)] + p*total[num2][src.at<uchar>(j, i)]) * 255);
}
else if (j >= ((blockNumber - 1)*singleBlockHeight +singleBlockHeight / 2)) {
//线性插值
int num_i = (i - singleBlockWidth/ 2) / singleBlockWidth;
int num_j = blockNumber - 1;
int num1 = num_j*blockNumber + num_i;
int num2 = num1 + 1;
float p = (i - (num_i*singleBlockWidth+ singleBlockWidth/ 2)) / (1.0f*singleBlockWidth);
float q = 1 - p;
src.at<uchar>(j, i) = (int)((q*total[num1][src.at<uchar>(j, i)] + p*total[num2][src.at<uchar>(j, i)]) * 255);
}
//双线性插值
else {
int num_i = (i - singleBlockWidth/ 2) / singleBlockWidth;
int num_j = (j -singleBlockHeight / 2) /singleBlockHeight;
int num1 = num_j*blockNumber + num_i;
int num2 = num1 + 1;
int num3 = num1 + blockNumber;
int num4 = num2 + blockNumber;
float u = (i - (num_i*singleBlockWidth+ singleBlockWidth/ 2)) / (1.0f*singleBlockWidth);
float v = (j - (num_j*singleBlockHeight +singleBlockHeight / 2)) / (1.0f*singleBlockHeight);
src.at<uchar>(j, i) = (int)((u*v*total[num4][src.at<uchar>(j, i)] +
(1 - v)*(1 - u)*total[num1][src.at<uchar>(j, i)] +
u*(1 - v)*total[num2][src.at<uchar>(j, i)] +
v*(1 - u)*total[num3][src.at<uchar>(j, i)]) * 255);
}
//最后这步,类似高斯平滑
src.at<uchar>(j, i) = src.at<uchar>(j, i) + (src.at<uchar>(j, i) << 8) + (src.at<uchar>(j, i) << 16);
}
}
imshow("均衡图线性差值+限制对比度", src);
waitKey(0);
return 0;
}
void ClipHistogram(int* pHistogram, float clipThreshold)
{
float binExcess = 0, totalExcess = 0, avgBinIncr = 0, upperLimit = 0;
// 累积超出阈值部分
for (int i = 0; i < 256; i++)
{
binExcess = pHistogram[i] - clipThreshold;
if (binExcess > 0)
{
totalExcess += binExcess;
}
}
avgBinIncr = totalExcess / 256;
upperLimit = clipThreshold - avgBinIncr;
// 修剪直方图并重新分配数值
for (int i = 0; i < 256; i++)
{
if (pHistogram[i] > clipThreshold)
{
pHistogram[i] = clipThreshold;
}
else
{
if (pHistogram[i] > upperLimit)
{
totalExcess -= (clipThreshold - pHistogram[i]);
pHistogram[i] = clipThreshold;
}
else
{
totalExcess -= avgBinIncr;
pHistogram[i] += avgBinIncr;
}
}
}
// 剩余部分再次分配
int *pCurBin = pHistogram;
int *pStartBin = pHistogram;
int *pEndBin = pHistogram + 255;
while (totalExcess > 0 && pStartBin < pEndBin)
{
int stepSize = 256 / totalExcess;
if (stepSize < 1)
{
stepSize = 1;
}
for (pCurBin = pStartBin; pCurBin < pEndBin && totalExcess > 0; pCurBin += stepSize)
{
if (*pCurBin < clipThreshold)
{
(*pCurBin)++;
totalExcess--;
}
}
pStartBin++;
}
}
效果如下: