直方图均衡化—C++代码实现

在医疗图像检测领域,为了能够使得采集到效果较好的X光超声图像,会经常需要进行图像增强,如下图所示。图像增强的方法有很多,本文主要针对直方图均衡化相关的方法进行C++实现。共分为以下几个部分:

  • 1 直方图均衡化(HE)
  • 2 自适应直方图均衡化(AHE)
  • 3 限制对比度的自适应直方图均衡化
  • 4 局部直方图统计量

直方图均衡化(HE)

直方图均衡化(histogram equlization)的主要思想是将一副图像的直方图分布变成近似均匀分布,从而增强图像的对比度。直方图均衡化虽然只是数字图像处理(Digital Image Processing)里面的基本方法,但是其作用很强大,是一种很经典的算法。
关于直方图均衡化原理介绍可以看这篇文章
OpenCV代码:

Mat srcMat = imread("./1.bmp", 0);
Mat histMat;
equalizeHist(srcMat, histMat);

自己手写代码如下:

//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;
}

自适应直方图均衡化(AHE)

自适应直方图均衡化(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)

CLAHE是1993年提出的方法,通过对局部直方图均衡化的对比度进行限制,从而避免所有局部区域的像素灰度值都映射到(0-255)的范围内,能够较好地保持原图的自然度,同时也能起到抑制噪声的作用。

在这里插入图片描述
上图a代表原图的直方图,b是其累积概率密度函数;c和d则为进行对比度限制后的直方图和累积概率分布函数。对比度限制原理如上图右部分所示,通过设置一个阈值(通常情况下该值表示为图像像素个数的百分比),将像素数较多的区域切除掉,平均分配到所有其他的灰度值区域。未使用限制对比度时,原图中灰度值为60的像素映射后变为217,灰度值为160的像素映射后变为242,限制对比度后,则分别变为了115和209。可见能够起到限制对比度拉伸和抑制噪声的作用。

具体细节部分可以参考原论文:Contrast Limited Adaptive Histogram Equalization
作者为什么可以有这种灵感我们不得而知,但对于这种局部对比度限制的方法博主也在许多其他论文中看到过,我认为应该没有比较科学的解释,只是因为这种方法确实能够限制对比度的拉伸,且得到的效果也挺不错的。

几种不同的图像增强效果如下:

在这里插入图片描述
OpenCV 代码如下

Mat claheMat;
Ptr<CLAHE> clahe = createCLAHE(40.0, Size(8, 8));
clahe->apply(blurMat, claheMat);

自己手写 代码如下:

//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;
			}
		}
	}
}
  • 4
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值