VS+openCV 用直方图统计像素(上)计算图像直方图、利用查找表修改图像外观

一、计算图像直方图

        图像由各种数值的像素构成。例如在单通道灰度图像中,每个像素都有一个 0(黑色)~255(白色)的整数。对于每个灰度,都有不同数量的像素分布在图像内,具体取决于图片内容。

        直方图是一个简单的表格,表示一幅图像(有时是一组图像)中具有某个值的像素的数量。因此,灰度图像的直方图有 256 个项目,也叫箱子(bin)。0 号箱子提供值为 0 的像素的数量,1 号箱子提供值为 1 的像素的数量,以此类推。很明显,如果把直方图的所有箱子进行累加,得到的结果就是像素的总数。你也可以把直方图归一化,即所有箱子的累加和等于 1。这时,每个箱子的数值表示对应的像素数量占总数的百分比。

【实现】

#include<opencv2/core.hpp>
#include<opencv2/highgui.hpp>
#include<opencv2/imgproc.hpp>
#include<iostream>
using namespace std;
using namespace cv;

//创建灰度图像的直方图
class Histogram1D {
private:
	int histSize[1];//直方图中箱子的数量
	float hranges[2];//值范围
	const float* ranges[1];//值范围的指针
	int channels[1];//要检查的通道数量
public:
	Histogram1D() {
		//准备一维直方图的默认参数
		histSize[0] = 256;//256个箱子
		hranges[0] = 0.0;//从0开始(含)
		hranges[1] = 256.0;//到256(不含)
		ranges[0] = hranges;
		channels[0] = 0;//先关注通道0
	}
	cv::Mat getHistogram(const cv::Mat& image);
};

//计算一维直方图
cv::Mat Histogram1D::getHistogram(const cv::Mat& image) {
	cv::Mat hist;
	//用calcHist函数计算一维直方图
	cv::calcHist(&image, 1,	//仅为一幅图像的直方图
				 channels,	//使用的通道
				 cv::Mat(),	//不使用掩码
				 hist,		//作为结果的直方图
				 1,			//这是一维的直方图
				 histSize,	//箱子数量
				 ranges		//像素值的范围
	);
	return hist;
}

int main()
{
	//读取输入的图像
	cv::Mat image = cv::imread("girl.jpg", 0);//以黑白方式打开
	//直方图对象
	Histogram1D h;
	//计算直方图
	cv::Mat histo = h.getHistogram(image);
	//循环遍历每个箱子
	for (int i = 0; i < 256; i++)
		cout << "Value" << i << "="
			 << histo.at<float>(i) << endl;
}

        显然,只看这一系列数值很难得到任何有意义的信息。因此比较实用的做法是以函数的方式 显示直方图,例如用柱状图。

#include<opencv2/core.hpp>
#include<opencv2/highgui.hpp>
#include<opencv2/imgproc.hpp>
#include<iostream>
using namespace std;
using namespace cv;

//创建灰度图像的直方图
class Histogram1D {
private:
	int histSize[1];//直方图中箱子的数量
	float hranges[2];//值范围
	const float* ranges[1];//值范围的指针
	int channels[1];//要检查的通道数量
public:
	Histogram1D() {
		//准备一维直方图的默认参数
		histSize[0] = 256;//256个箱子
		hranges[0] = 0.0;//从0开始(含)
		hranges[1] = 256.0;//到256(不含)
		ranges[0] = hranges;
		channels[0] = 0;//先关注通道0
	}
	cv::Mat getHistogram(const cv::Mat& image);
	cv::Mat getHistogramImage(const cv::Mat& image, int zoom = 1);
	static cv::Mat getImageOfHistogram(const cv::Mat& hist, int zoom);
};

//计算一维直方图
cv::Mat Histogram1D::getHistogram(const cv::Mat& image) {
	cv::Mat hist;
	//用calcHist函数计算一维直方图
	cv::calcHist(&image, 1,	//仅为一幅图像的直方图
				 channels,	//使用的通道
				 cv::Mat(),	//不使用掩码
				 hist,		//作为结果的直方图
				 1,			//这是一维的直方图
				 histSize,	//箱子数量
				 ranges		//像素值的范围
	);
	return hist;
}

//创建一个表示直方图的图像(静态方法)
cv::Mat Histogram1D::getImageOfHistogram(const cv::Mat& hist, int zoom) {
	//取得箱子值的最大值和最小值
	double maxVal = 0;
	double minVal = 0;
	cv::minMaxLoc(hist, &minVal, &maxVal, 0, 0);
	//取得直方图的大小
	int histSize = hist.rows;
	//用于显示直方图的方形图像
	cv::Mat histImg(histSize * zoom, histSize * zoom,
					CV_8U, cv::Scalar(255));
	//设置最高点为90%(即图像高度)的箱子个数
	int hpt = static_cast<int>(0.9 * histSize);
	//为每个箱子画垂直线
	for (int h = 0; h < histSize; h++) {
		float binVal = hist.at<float>(h);
		if (binVal > 0) {
			int intensity = static_cast<int>(binVal * hpt / maxVal);
			cv::line(histImg, cv::Point(h * zoom, histSize * zoom),
					 cv::Point(h * zoom, (histSize - intensity) * zoom),
					 cv::Scalar(0), zoom);
		}
	}
	return histImg;
}

//计算一维直方图,并返回它的图像
cv::Mat Histogram1D::getHistogramImage(const cv::Mat& image, int zoom) {
	zoom = 1;
	//先计算直方图
	cv::Mat hist = getHistogram(image);
	//创建图像
	return getImageOfHistogram(hist, zoom);
}

int main()
{
	//读取输入的图像
	cv::Mat image = cv::imread("girl.jpg", 0);//以黑白方式打开
	//直方图对象
	Histogram1D h;
	cv::namedWindow("Histogram");
	cv::imshow("Histogram", h.getHistogramImage(image));
	cv::waitKey(0);
}

        从图形化的直方图可以看出,在中等灰度值处有一个大的尖峰,并且比中等值更黑的像素有很多。巧的是,这两部分像素分别对应了图像的背景和前景。要验证这点,可以在这两部分的汇合处进行阈值化处理。OpenCV 中的 cv::threshold 函数可以实现这个功能。上一章介绍过,它是一个很实用的函数。我们取直方图中在升高为尖峰之前的最小值的位置(灰度值为 70), 对其进行阈值化处理,得到二值图像。

int main()
{
	//读取输入的图像
	cv::Mat image = cv::imread("girl.jpg", 0);//以黑白方式打开
	cv::Mat thresholded;					//输出二值图像
	cv::threshold(image, thresholded, 70,	//阈值
				  255,						//对超过阈值的像素赋值
				  cv::THRESH_BINARY);		//阈值化类型
	cv::namedWindow("Binary Image");
	cv::imshow("Binary Image", thresholded);
	cv::waitKey(0);
}

【实现原理】

        为了适应各种场景,cv::calcHist 函数带有很多参数。

void calcHist(const Mat*images, // 源图像 
     int nimages, // 源图像的个数(通常为 1)
     const int*channels, // 列出通道
     InputArray mask, // 输入掩码(需处理的像素)
     OutputArray hist, // 输出直方图
     int dims, // 直方图的维度(通道数量)
     const int*histSize, // 每个维度位数
     const float**ranges, // 每个维度的范围
     bool uniform=true, // true 表示箱子间距相同
     bool accumulate=false) // 是否在多次调用时进行累加

        大多数情况下,直方图是单个的单通道或三通道图像,但也可以在这个函数中指定一个分布在多幅图像(即多个 cv::Mat)上的多通道图像。这也是把输入图像数组作为函数第一个参数的原因。第六个参数 dims 指明了直方图的维数,例如 1 表示一维直方图。在分析多通道图像时,可以只把它的部分通道用于计算直方图,将需要处理的通道放在维数确定的数组 channel 中。在这个类的实现中只有一个通道,默认为 0。直方图用每个维度上的箱子数量(即整数数组histSize)以及每个维度(由 ranges 数组提供,数组中每个元素又是一个二元素数组)上的最小值(含)和最大值(不含)来描述。你也可以定义一个不均匀的直方图(倒数第二个参数应 设为false),这时需要指定每个箱子的限值。

        和很多 OpenCV 函数一样,可以使用掩码表示计算时用到的像素(所有掩码值为 0 的像素都不使用)。此外还可以指定两个布尔值类型的附加参数,第一个表示是否采用均匀的直方图(默认为 true),第二个表示是否允许累加多个直方图计算的结果。如果第二个参数为 true,那么图像中的像素数量会累加到输入直方图的当前值中。在计算一组图像的直方图时,就可以使用这个参数。

        得到的直方图存储在 cv::Mat 的实例中。事实上,cv::Mat 类可用于操作通用的 N 维矩阵。第 2 章讲过,cv::Mat 类定义了适用于一维、二维和三维矩阵的 at 方法。正因如此,我们才可以在 getHistogramImage 方法中用下面的代码访问一维直方图的每个箱子:

         float binVal = hist.at(h);

        注意,直方图中的值存储为 float 值。

【扩展阅读】

        我们可以用同一个 cv::calcHist 函数计算多通道图像的直方图。例如,若想计算彩色 BGR 图像的直方图,可以这样定义这样一个类:

class ColorHistogram { 
     private: 
     int histSize[3]; // 每个维度的大小
     float hranges[2]; // 值的范围(三个维度用同一个值)
     const float* ranges[3]; // 每个维度的范围
     int channels[3]; // 需要处理的通道
     public: 
     ColorHistogram() { 
     // 准备用于彩色图像的默认参数
     // 每个维度的大小和范围是相等的
     histSize[0]= histSize[1]= histSize[2]= 256; 
     hranges[0]= 0.0; // BGR 范围为 0~256 
     hranges[1]= 256.0; 
     ranges[0]= hranges; // 这个类中
     ranges[1]= hranges; // 所有通道的范围都相等
     ranges[2]= hranges; 
     channels[0]= 0; // 三个通道:B 
     channels[1]= 1; // G 
     channels[2]= 2; // R 
 } 

        这里的直方图将会是三维的,因此需要为每个维度指定一个范围。本例中的 BGR 图像的三个通道范围都是[0,255]。准备好参数后,就可以用下面的方法计算颜色直方图了:

// 计算直方图
cv::Mat getHistogram(const cv::Mat &image) { 
     cv::Mat hist; 
     // 计算直方图
     cv::calcHist(&image, 1, // 单幅图像的直方图
                  channels, // 用到的通道
                  cv::Mat(), // 不使用掩码
                  hist, // 得到的直方图
                  3, // 这是一个三维直方图
                  histSize, // 箱子数量
                  ranges // 像素值的范围
     ); 
     return hist; 
} 

        上述方法返回一个三维的 cv::Mat 实例。如果选用含有 256 个箱子的直方图,这个矩阵就有(256)^3 个元素,表示超过 1600 万个项目。在很多应用程序中,最好在计算直方图时减少箱子的数量。也可以使用数据结构 cv::SparseMat 表示大型稀疏矩阵(即非零元素非常稀少的矩阵),这样不会消耗过多的内存。cv::calcHist 函数具有返回这种矩阵的版本,因此只需要简单地修改一下前面的方法,即可使用 cv::SparseMatrix:

// 计算直方图
cv::SparseMat getSparseHistogram(const cv::Mat &image) { 
     cv::SparseMat hist(3, // 维数
                        histSize, // 每个维度的大小
                        CV_32F); 
     // 计算直方图
     cv::calcHist(&image, 1, // 单幅图像的直方图
                  channels, // 用到的通道
                  cv::Mat(), // 不使用掩码
                  hist, // 得到的直方图
                  3, // 这是三维直方图
                  histSize, // 箱子数量
                  ranges // 像素值的范围
     ); 
     return hist; 
}

        这是一个三维直方图,画起来比较困难。我们也可以通过显示独立的 R、G 和 B 通道的直方图来说明图像中颜色的分布情况。

【遇到的问题】

        扩展阅读里的代码不完整,其他的地方我不会修改,所以运行不起来。

二、利用查找表修改图像外观

        图像直方图提供了利用现有像素强度值进行场景渲染的方法。通过分析图像中像素值的分布 情况,你可以利用这个信息来修改图像,甚至提高图像质量。本节将解释如何用一个简单的映射函数(称为查找表)来修改图像的像素值。我们即将看到,查找表通常根据直方分布图生成。

【实现】

        查找表是个一对一(或多对一)的函数,定义了如何把像素值转换成新的值。它是一个一维数组,对于规则的灰度图像,它包含 256 个项目。利用查找表的项目 i,可得到对应灰度级的新强度值。

	newIntensity = lookup[oldIntensity];

        OpenCV 中的 cv::LUT 函数在图像上应用查找表生成一个新的图像。查找表通常根据直方图生成,以下是完整代码。

#include<opencv2/core.hpp>
#include<opencv2/highgui.hpp>
#include<opencv2/imgproc.hpp>
#include<iostream>
using namespace std;
using namespace cv;

class Histogram1D {
public:
	static cv::Mat applyLookUp(const cv::Mat& image, const cv::Mat& lookup);
};

cv::Mat Histogram1D::applyLookUp(const cv::Mat& image,	 //输入图像
								 const cv::Mat& lookup) {//uchar类型的1x256数组
	//输出图像
	cv::Mat result;
	//应用查找表
	cv::LUT(image, lookup, result);
	return result;
}

int main()
{
	//读取输入的图像
	cv::Mat image = cv::imread("girl.jpg");
	//创建一个图像翻转的查找表
	cv::Mat lut(1, 256, CV_8U);//256x1矩阵
	for (int i = 0; i < 256; i++) {
		//0变成255,1变成254,以此类推
		lut.at<uchar>(i) = 255 - i;
	}							  
	//直方图对象
	Histogram1D h;
	cv::namedWindow("Negative image");
	cv::imshow("Negative image", h.applyLookUp(image, lut));
	cv::waitKey(0);
}

        注意:这里我把上一部分的直方图代码删掉了,只存留了这次的查找表代码。其实也不一定非要定义一个类,直接用函数也可以实现。

【实现原理】

        在图像上应用查找表后得到一个新图像,新图像的像素强度值被修改为查找表中规定的值。例如上述代码对像素强度进行了简单的反转,即强度 0 变成 255、1 变成 254、最后 255 变成0。对图像应用这种查找表后,会生成原始图像的反向图像。

【扩展阅读】

        对于需要更换全部像素强度值的程序,都可以使用查找表。但是这个转换过程必须是针对整幅图像的。也就是说,一个强度值对应的全部像素都必须使用同一种转换方法。

1. 伸展直方图以提高图像对比度

        定义一个修改原始图像直方图的查找表可以提高图像的对比度。例如,如果图中根本没有大于 200 的像素值。我们可以通过伸展直方图来生成一个对比度更高的图像。为此要使用一个百分比阈值,表示伸展后图像的最小强度值(0)和最大强度值(255) 像素的百分比。

        我们必须在强度值中找到最小值(imin)和最大值(imax),使得所要求的最小的像素数量高于阈值指定的百分比。以下是完整代码:

#include<opencv2/core.hpp>
#include<opencv2/highgui.hpp>
#include<opencv2/imgproc.hpp>
#include<iostream>
using namespace std;
using namespace cv;

//创建灰度图像的直方图
class Histogram1D {
private:
	int histSize[1];//直方图中箱子的数量
	float hranges[2];//值范围
	const float* ranges[1];//值范围的指针
	int channels[1];//要检查的通道数量
public:
	Histogram1D() {
		//准备一维直方图的默认参数
		histSize[0] = 256;//256个箱子
		hranges[0] = 0.0;//从0开始(含)
		hranges[1] = 256.0;//到256(不含)
		ranges[0] = hranges;
		channels[0] = 0;//先关注通道0
	}
	cv::Mat getHistogram(const cv::Mat& image);
	cv::Mat getHistogramImage(const cv::Mat& image, int zoom = 1);
	static cv::Mat getImageOfHistogram(const cv::Mat& hist, int zoom);
	cv::Mat stretch(const Mat& image, int minValue = 0);
	static cv::Mat applyLookUp(const cv::Mat& image, const cv::Mat& lookup);
};

//计算一维直方图
cv::Mat Histogram1D::getHistogram(const cv::Mat& image) {
	cv::Mat hist;
	//用calcHist函数计算一维直方图
	cv::calcHist(&image, 1,	//仅为一幅图像的直方图
		channels,	//使用的通道
		cv::Mat(),	//不使用掩码
		hist,		//作为结果的直方图
		1,			//这是一维的直方图
		histSize,	//箱子数量
		ranges		//像素值的范围
	);
	return hist;
}

//创建一个表示直方图的图像(静态方法)
cv::Mat Histogram1D::getImageOfHistogram(const cv::Mat& hist, int zoom) {
	//取得箱子值的最大值和最小值
	double maxVal = 0;
	double minVal = 0;
	cv::minMaxLoc(hist, &minVal, &maxVal, 0, 0);
	//取得直方图的大小
	int histSize = hist.rows;
	//用于显示直方图的方形图像
	cv::Mat histImg(histSize * zoom, histSize * zoom,
		CV_8U, cv::Scalar(255));
	//设置最高点为90%(即图像高度)的箱子个数
	int hpt = static_cast<int>(0.9 * histSize);
	//为每个箱子画垂直线
	for (int h = 0; h < histSize; h++) {
		float binVal = hist.at<float>(h);
		if (binVal > 0) {
			int intensity = static_cast<int>(binVal * hpt / maxVal);
			cv::line(histImg, cv::Point(h * zoom, histSize * zoom),
				cv::Point(h * zoom, (histSize - intensity) * zoom),
				cv::Scalar(0), zoom);
		}
	}
	return histImg;
}

//计算一维直方图,并返回它的图像
cv::Mat Histogram1D::getHistogramImage(const cv::Mat& image, int zoom) {
	zoom = 1;
	//先计算直方图
	cv::Mat hist = getHistogram(image);
	//创建图像
	return getImageOfHistogram(hist, zoom);
}

cv::Mat Histogram1D::applyLookUp(const cv::Mat& image,	 //输入图像
	const cv::Mat& lookup) {//uchar类型的1x256数组
//输出图像
	cv::Mat result;
	//应用查找表
	cv::LUT(image, lookup, result);
	return result;
}

//伸展直方图
cv::Mat Histogram1D::stretch(const Mat& image, int minValue) {
	cv::Mat hist = getHistogram(image);
	//找到直方图的左极限
	int imin = 0;
	for (; imin < 256; imin++) {
		//小于或等于imin的像素数量必须>minValue
		if ((hist.at<float>(imin)) > minValue)
			break;
	}
	//找到直方图的右极限
	int imax = 255;
	for (; imax >= 0; imax--) {
		//大于或等于imax的像素必须>minValue
		if ((hist.at<float>(imax)) > minValue)
			break;
	}
	//minValue代表的是次数、个数,像素值最小(0左右的)以及像素值最大(255左右的)
	//这些极端的值都比较少,找到比较少的个数对应的像素值坐标(横坐标)
	Mat lookup(1, 256, CV_8U);  //LUT查找表的像素重映射的规则
	for (int i = 0; i < 256; i++)      //根据像素值大小划分
	{
		if (i < imin)                 //像素值(横坐标)imin左边的都置为0;极小的置0
			lookup.at<uchar>(i) = 0;
		else if (i > imax)          //像素值(横坐标)右边的都置255;极大的置255
			lookup.at<uchar>(i) = 255;
		else
			lookup.at<uchar>(i) = cvRound(255.0 * (i - imin) / (imax - imin)); //[min,max]重新分配 cvRound为取整,中间的重新映射
	}
	Mat result;

	result = applyLookUp(image, lookup);
	return result;     //返回处理好的增强的对比度 图片
	//这里需要分析下形参传进来的minValue。如果minValue过大,两边的0,255就会多;如果minValue过小,两边的0,255就会少
}   

int main()
{
	//读取输入的图像
	cv::Mat image = cv::imread("bluesky.jpg");
	//直方图对象
	Histogram1D h;
	cv::Mat streteched = h.stretch(image, 200);
	cv::namedWindow("Streched Image");
	cv::imshow("Streched Image", streteched);
	cv::namedWindow("Original Image");
	cv::imshow("Original Image", image);
	cv::waitKey(0);
}

2. 在彩色图像上应用查找表

        之前我们定义了一个减色函数,通过修改图像中的 BGR 值减少可能的颜色数量。当时的实现方法是循环遍历图像中的像素,并对每个像素应用减色函数。实际上,更高效的做法是预先计算好所有的减色值,然后用查找表修改每个像素。利用本节的方法,这很容易实现。下面是新的减色函数。

void colorReduce(cv::Mat &image, int div=64) { 
     // 创建一维查找表
     cv::Mat lookup(1,256,CV_8U); 
     // 定义减色查找表的值
     for (int i=0; i<256; i++)
        lookup.at<uchar>(i)= i/div*div + div/2; 
     // 对每个通道应用查找表
     cv::LUT(image,lookup,image); 
}

        这种减色方案之所以能起作用,是因为在多通道图像上应用一维查找表时,同一个查找表会独立地应用在所有通道上。如果查找表超过一个维度,那么它和所用图像的通道数必须相同。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值