赞
踩
在医疗图像检测领域,为了能够使得采集到效果较好的X光或超声图像,会经常需要进行图像增强,如下图所示。图像增强的方法有很多,本文主要针对直方图均衡化相关的方法进行C++实现。共分为以下几个部分:
直方图均衡化(histogram equlization)的主要思想是将一副图像的直方图分布变成近似均匀分布,从而增强图像的对比度。直方图均衡化虽然只是数字图像处理(Digital Image Processing)里面的基本方法,但是其作用很强大,是一种很经典的算法。
关于直方图均衡化原理介绍可以看这篇文章
代码如下:
//HE(直方图均衡化) bool HE_MY(Mat gray, Mat result) { //统计0~255像素值的个数 map<int, int>mp; for (int i = 0; i < gray.rows; i++) { uchar* ptr = gray.data + i * gray.cols; for (int j = 0; j < gray.cols; j++) { int value = ptr[j]; mp[value]++; } } //统计0~255像素值的频率,并计算累计频率 map<int, double> valuePro; int sum = gray.cols * gray.rows; double sumPro = 0; for (int i = 0; i < 256; i++) { sumPro += 1.0 * mp[i] / sum; valuePro[i] = sumPro; } //根据累计频率进行转换 for (int i = 0; i < gray.rows; i++) { uchar* ptr1 = gray.data + i * gray.cols; for (int j = 0; j < gray.cols; j++) { int value = ptr1[j]; double p = valuePro[value]; result.at<uchar>(i, j) = p * 255; } } return true; }
自适应直方图均衡化(adaptive histogram equalization)其实是将图像切分为许多不同的区域, 对每个区域分别进行直方图均衡化,即局部直方图均衡化。通常会产生块状的效果,需要对其进行插值运算,使得块状区域之间过度较为自然。常用的插值算法有:最邻近插值、线性插值、双线性插值、双三次插值,opencv中的 resize() 函数就是使用的双线性插值的方法。具体介绍可以参考这篇文章:图像处理之-----插值算法
几种对比度拉伸算法的效果对比如下图所示。原图的阴影区域中存在一些对比度不明显的字符,使用全局拉伸的直方图均衡化后,阴影区域中的字符对比度拉伸强度较小,而且白色区域的一些噪声被放大了。使用AHE拉伸后,阴影区域的字符对比度明显,但对比度拉伸范围过大,使得阴影区域的亮度提高了很多,且放大了很多噪声点。使用CLAHE算法则能够得到较好的效果,将在下面进行介绍。
AHE(插值)算法代码如下:
Mat AHE(Mat originImage, int blockNumber = 16) { Mat src = originImage.clone(); Mat src1 = originImage.clone(); Mat dst; //const int blockNumber = 8;//把图像分成block数量 int width = src.cols; int height = src.rows; int singleBlockWidth = src.cols / blockNumber;//每个block大小 int singleBlockHeight = src.rows / blockNumber; int (*pixelNumber)[256] = new int[blockNumber * blockNumber][256]{ 0 };//存储不同block中各个不同灰度像素数量 float (*total)[256] = new float[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 + blockNumber * 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]++; } 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; } } } //利用累积分布函数对于原像素灰度在各自block中进行映射 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 + blockNumber * 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; } } } //插值 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 + singleBlockWidth / 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); } } } return src; }
CLAHE是1993年提出的方法,通过对局部直方图均衡化的对比度进行限制,从而避免所有局部区域的像素灰度值都映射到(0-255)的范围内,能够较好地保持原图的自然度,同时也能起到抑制噪声的作用。
上图a代表原图的直方图,b是其累积概率密度函数;c和d则为进行对比度限制后的直方图和累积概率分布函数。对比度限制原理如上图右部分所示,通过设置一个阈值(通常情况下该值表示为图像像素个数的百分比),将像素数较多的区域切除掉,平均分配到所有其他的灰度值区域。未使用限制对比度时,原图中灰度值为60的像素映射后变为217,灰度值为160的像素映射后变为242,限制对比度后,则分别变为了115和209。可见能够起到限制对比度拉伸和抑制噪声的作用。
具体细节部分可以参考原论文:Contrast Limited Adaptive Histogram Equalization
作者为什么可以有这种灵感我们不得而知,但对于这种局部对比度限制的方法博主也在许多其他论文中看到过,我认为应该没有比较科学的解释,只是因为这种方法确实能够限制对比度的拉伸,且得到的效果也挺不错的。
几种不同的图像增强效果如下:
CLAHE代码如下:
//CLAHE Mat CLAHE_MY(Mat src, int block = 8, int limit = 4) { Mat CLAHE_GO = src.clone(); int width = src.cols; int height = src.rows; int width_block = width / block; //每个小格子的长和宽 int height_block = height / block; //存储各个直方图 int(*tmp2)[256] = new int[block * block][256]{ 0 }; float(*C2)[256] = new float[block * block][256]{ 0.0 }; //分块 int total = width_block * height_block; for (int i = 0; i < block; i++) { for (int j = 0; j < block; j++) { int start_x = i * width_block; int end_x = start_x + width_block; int start_y = j * height_block; int end_y = start_y + height_block; int num = i + block * j; //遍历小块,计算直方图 for (int ii = start_x; ii < end_x; ii++) { for (int jj = start_y; jj < end_y; jj++) { int index = src.at<uchar>(jj, ii); tmp2[num][index]++; } } //裁剪和增加操作 int LIMIT = limit * width_block * height_block / 255; int steal = 0; for (int k = 0; k < 256; k++) { if (tmp2[num][k] > LIMIT) { steal += tmp2[num][k] - LIMIT; tmp2[num][k] = LIMIT; } } int bonus = steal / 256; //hand out the steals averagely for (int k = 0; k < 256; k++) { tmp2[num][k] += bonus; } //计算累积分布直方图 for (int k = 0; k < 256; k++) { if (k == 0) C2[num][k] = 1.0f * tmp2[num][k] / total; else C2[num][k] = C2[num][k - 1] + 1.0f * tmp2[num][k] / total; } } } //计算变换后的像素值 //根据像素点的位置,选择不同的计算方法 for (int i = 0; i < width; i++) { for (int j = 0; j < height; j++) { //four coners if (i <= width_block / 2 && j <= height_block / 2) { int num = 0; CLAHE_GO.at<uchar>(j, i) = (int)(C2[num][CLAHE_GO.at<uchar>(j, i)] * 255); } else if (i <= width_block / 2 && j >= ((block - 1) * height_block + height_block / 2)) { int num = block * (block - 1); CLAHE_GO.at<uchar>(j, i) = (int)(C2[num][CLAHE_GO.at<uchar>(j, i)] * 255); } else if (i >= ((block - 1) * width_block + width_block / 2) && j <= height_block / 2) { int num = block - 1; CLAHE_GO.at<uchar>(j, i) = (int)(C2[num][CLAHE_GO.at<uchar>(j, i)] * 255); } else if (i >= ((block - 1) * width_block + width_block / 2) && j >= ((block - 1) * height_block + height_block / 2)) { int num = block * block - 1; CLAHE_GO.at<uchar>(j, i) = (int)(C2[num][CLAHE_GO.at<uchar>(j, i)] * 255); } //four edges except coners else if (i <= width_block / 2) { //线性插值 int num_i = 0; int num_j = (j - height_block / 2) / height_block; int num1 = num_j * block + num_i; int num2 = num1 + block; float p = (j - (num_j * height_block + height_block / 2)) / (1.0f * height_block); float q = 1 - p; CLAHE_GO.at<uchar>(j, i) = (int)((q * C2[num1][CLAHE_GO.at<uchar>(j, i)] + p * C2[num2][CLAHE_GO.at<uchar>(j, i)]) * 255); } else if (i >= ((block - 1) * width_block + width_block / 2)) { //线性插值 int num_i = block - 1; int num_j = (j - height_block / 2) / height_block; int num1 = num_j * block + num_i; int num2 = num1 + block; float p = (j - (num_j * height_block + height_block / 2)) / (1.0f * height_block); float q = 1 - p; CLAHE_GO.at<uchar>(j, i) = (int)((q * C2[num1][CLAHE_GO.at<uchar>(j, i)] + p * C2[num2][CLAHE_GO.at<uchar>(j, i)]) * 255); } else if (j <= height_block / 2) { //线性插值 int num_i = (i - width_block / 2) / width_block; int num_j = 0; int num1 = num_j * block + num_i; int num2 = num1 + 1; float p = (i - (num_i * width_block + width_block / 2)) / (1.0f * width_block); float q = 1 - p; CLAHE_GO.at<uchar>(j, i) = (int)((q * C2[num1][CLAHE_GO.at<uchar>(j, i)] + p * C2[num2][CLAHE_GO.at<uchar>(j, i)]) * 255); } else if (j >= ((block - 1) * height_block + height_block / 2)) { //线性插值 int num_i = (i - width_block / 2) / width_block; int num_j = block - 1; int num1 = num_j * block + num_i; int num2 = num1 + 1; float p = (i - (num_i * width_block + width_block / 2)) / (1.0f * width_block); float q = 1 - p; CLAHE_GO.at<uchar>(j, i) = (int)((q * C2[num1][CLAHE_GO.at<uchar>(j, i)] + p * C2[num2][CLAHE_GO.at<uchar>(j, i)]) * 255); } //双线性插值 else { int num_i = (i - width_block / 2) / width_block; int num_j = (j - height_block / 2) / height_block; int num1 = num_j * block + num_i; int num2 = num1 + 1; int num3 = num1 + block; int num4 = num2 + block; float u = (i - (num_i * width_block + width_block / 2)) / (1.0f * width_block); float v = (j - (num_j * height_block + height_block / 2)) / (1.0f * height_block); CLAHE_GO.at<uchar>(j, i) = (int)((u * v * C2[num4][src.at<uchar>(j, i)] + (1 - v) * (1 - u) * C2[num1][src.at<uchar>(j, i)] + u * (1 - v) * C2[num2][src.at<uchar>(j, i)] + v * (1 - u) * C2[num3][src.at<uchar>(j, i)]) * 255); } } } return CLAHE_GO; }
局部直方图统计量的方法在冈萨雷斯的《数字图像处理》第四版中有介绍,比较简单好用,对于特定的场景往往能起到更好的效果。
代码如下:
//局部直方图统计量 /* 功能:对于图像中的每个像素点,截取其7*7的邻域框,计算邻域框的灰度均值mxy和方差deltaxy1,与整幅图像的邻域mean和方差delta1进行比较, if (mxy <= mean * k0 && deltaxy1 <= k2 * delta1 && deltaxy1 >= k1 * delta1){ 将该像素灰度值*10 }; */ double getMean(double* r_pdf) { double m = 0; for (int i = 0; i < 256; i++) m += i * r_pdf[i]; return m; } double getVariance(double* r_pdf, double m) { double delta = 0; for (int i = 0; i < 256; i++) if (r_pdf[i] != 0) delta += (pow((i - m), 2) * r_pdf[i]); return delta; } void init(double* r_pdf) { for (int i = 0; i < 256; i++) { r_pdf[i] = 0; } } int histStatistic(cv::Mat& src, int dim, float k0, float k1, float k2, float E) { if (dim % 2 == 0) { return -1; } int width = src.rows; int height = src.cols; int mn = width * height; double r_pdf[256] = {}; for (int row = 0; row < src.rows; row++) { for (int col = 0; col < src.cols; col++) { int g = src.at<uchar>(row, col); r_pdf[g] += 1.0 / mn; } } double mean = getMean(r_pdf); double delta = getVariance(r_pdf, mean); double delta1 = std::sqrt(delta); double mxy = 0; double deltaxy = 0; double local = dim * dim; for (int i = dim / 2; i < height - dim / 2 - 1; i++) { for (int j = dim / 2; j < width - dim / 2; j++) { init(r_pdf); //统计局部直方图 for (int p = j - dim / 2; p < j + dim / 2 + 1; p++) { for (int q = i - dim / 2; q < i + dim / 2 + 1; q++) { int g = src.at<uchar>(p, q); r_pdf[g] += 1.0 / local; } } mxy = getMean(r_pdf); deltaxy = getVariance(r_pdf, mxy); double deltaxy1 = sqrt(deltaxy); if (mxy <= mean * k0 && deltaxy1 <= k2 * delta1 && deltaxy1 >= k1 * delta1) { src.at<uchar>(j, i) = src.at<uchar>(j, i) * E; } } } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。