【OpenCV C++20 学习笔记】直方图计算-split, calcHist, normalize

广义直方图

直方图的横坐标除了可以是图片中的强度值,也可以是任何其他我们想要观察的特征。例如,下面的图片矩阵中包含了0-255的强度值:
图片矩阵
如果想观察每个宽度为16的强度值区间上的频数分布,我们就可以将横坐标分成下面的区间:
[ 0 , 255 ] = [ 0 , 15 ] ∪ [ 16 , 31 ] ∪ . . . . . ∪ [ 240 , 255 ] r a n g e = b i n 1 ∪ b i n 2 ∪ . . . . . ∪ b i n n = 15 [0, 255] = [0, 15] \cup [16, 31] \cup ..... \cup [240, 255] \\ range = bin_1 \cup bin_2 \cup ..... \cup bin_{n=15} [0,255]=[0,15][16,31].....[240,255]range=bin1bin2.....binn=15
这样就可以得到类似于下图的直方图:
区间直方图
直方图中的元素的定义如下:

  1. 维数(dims):即想要观察的参数的数量,比如上例中只观察灰度图中每个像素的强度值,因此dims = 1
  2. 组数(bins):每个维度中的数据被分组的数量,比如上例中分了16组区间,所以bins = 16
  3. 全距(range):被观察的数据的总区间,比如上例中range = [0, 255]

如果你相观察的参数不止一个,比如说2个,即dims = 2,那就需要画一个3维的图了。

示例

目标

  1. 导入图片
  2. 分离通道:用split函数将图片分离为R, G, B3个矩阵数据
  3. 计算直方图:用calcHist函数对分离出来的3个矩阵分别计算直方图
  4. 绘制计算结果

分离通道

split函数,其原型如下:

void cv::split(	const Mat& src,
				Mat*		mvbegin)

该函数将多通道的矩阵数组分成多个单通道的矩阵数组,其中:

  • src为要进行通道分离的原矩阵
  • mvbegin为接收分离结果的数组的指针,该数组的长度要和原矩阵的通道数相同

该函数还有以下更便利的重载版本(第2个参数不再是指针,而是多维数组):

void cv::split(	InputArray			m,
				OutputArrayOfArrays	mv)

在本例中的应用如下:

Mat src{ imread("lena.jpg") };	//导入图片

vector<Mat> bgr_planes;	//接收通道分离结果的向量
split(src, bgr_planes);	//通道分离之后,bgr_planes中的3个元素分别是b,g, r,3个通道的数据矩阵

原图是 512 × 512 512 \times 512 512×512的3通道矩阵,在VS调试中,可以看到分离出来的结果bgr_planes中分别有3个元素,而每个元素也是 512 × 512 512 \times 512 512×512的矩阵。
通道分离结果
至此,通道分离完成;接下来对每个通道进行直方图计算。

计算直方图

计算直方图用的函数是calcHist,该函数有3个版本,这里选择比较常用的一个版本,其原型如下:

void cv::calcHist(	const Mat * 	images,
					int 			nimages,
					const int * 	channels,
					InputArray 		mask,
					OutputArray 	hist,
					int 			dims,
					const int * 	histSize,
					const float ** 	ranges,
					bool 			uniform = true,
					bool 			accumulate = false )	
  • imagesconst Mat*类型,可以是一个图片矩阵的指针,也可以是图片矩阵的数组;代表需要计算直方图的图片
  • nimages:第1个参数中包含的图片数量
  • channelsconst int*类型,可以是一个整数常量,也可以是整数数组,代表对应图片中需要进行计算的通道索引,从0开始。如果数组的话,也就是说有多张图片,且每张图片中需要有多个通道被计算,那么这个参数可能遵循以下写法:
    [ i m a g e s [ 0 ] . c h a n n e l s ( ) − 1 , i m a g e s [ 0 ] . c h a n n e l s ( ) , i m a g e s [ 0 ] . c h a n n e l s ( ) + i m a g e s [ 1 ] . c h a n n e l s ( ) − 1 , . . . , ∑ i m a g e s [ n ] . c h a n n e l s ( ) ] [images[0].channels()-1, images[0].channels(), \\ images[0].channels()+images[1].channels()-1, ... , \sum images[n].channels() ] [images[0].channels()1,images[0].channels(),images[0].channels()+images[1].channels()1,...,images[n].channels()]
  • mask:可以不指定,如果指定则其中的矩阵必须是与对应图片具有相同尺寸且是8位的数据类型,从而给图片提供了一个掩码
  • hist:输出结果
  • dims:直方图的维数,必须是正数,且不能超过32
  • histSize:直方图每个维度的组数
  • rangesconst float **类型,所以必须是一个数组的住宿。代表直方图每个维度的全距;如果是均匀分布的直方图,那每个维度只需要提供最大值和最小值就行了,即 m i n , m a x {min, max} minmax,注意是左闭右开的区间;如果不是均匀分布的直方图,则需要提供每组的最小值及最后一组的最大值,即KaTeX parse error: Expected '}', got 'EOF' at end of input: …{histSize[i]-1}
  • uniform:是否为均匀分布
  • accumulate:是否允许覆盖,即不清除之前的直方图

该函数也有其他重载版本,参数即原理基本与上述版本相同,这里就不赘述了。
乍一看这个函数非常复杂,事实也确实如此。但是在本例中,因为我们将一个3通道的矩阵分离成3个单通道的矩阵,然后分别对它们进行计算,所以事情就变得相对简单了。我们直接看代码和注释吧:

int histSize{ 256 };	//定义直方图中的组数为256,即每个强度值一组

//定义直方图中的全距
float range[]{ 0, 256 };			//表示全距的区间,左闭右开
const float* histRange[]{ range };	//由于直方图只有一个维度,所以数组只有一个元素

bool uniform{ true };		//均匀分布
bool accumulate{ false };	//不允许覆盖

Mat b_hist, g_hist, r_hist;	//接收计算结果的矩阵
//b通道的直方图计算
calcHist(&bgr_planes[0],	//b通道矩阵,因为形参是指针类型,所以要加取址符&
	1,						//只有b通道一个矩阵,相当于只有一张图片,所以nimages = 1
	0,						//矩阵中只有一个通道,所以只有一个通道索引,且从0开始,channels = 0
	Mat(),					//空矩阵代表不使用掩码
	b_hist,					//接收计算结果的矩阵
	1,						//直方图只有一个维度,即b的强度值的频数分布,dims = 1
	&histSize,				//直方图的组数,因为是指针类型,所以要加取址符&
	histRange,				//直方图的全距,因为这里只有1个维度、1个矩阵,所以该数组包含一个区间
	uniform,				//均匀分布
	accumulate);			//不允许覆盖
//g通道的直方图计算
calcHist(&bgr_planes[1], 1, 0, Mat(), g_hist, 1, &histSize, histRange, uniform, accumulate);
//r通道的直方图计算
calcHist(&bgr_planes[2], 1, 0, Mat(), r_hist, 1, &histSize, histRange, uniform, accumulate);

对于单通道的矩阵来说,很多需要传入数组的形参,只要传入字面量就行了,所以简化了很多。
在Image watch中查看计算结果:
直方图计算结果
可以看到每个通道的计算结果都是 1 × 256 1 \times 256 1×256的矩阵,代表原图中每个通道上从0到255这256个强度值的频数。

绘制计算结果

归一化

在绘制直方图的之前需要对数据进行归一化,从而使数据的值域能够适应直方图尺寸。这就要用到normalize函数,其原型如下:

void cv::normalize(	InputArray			src,
					InputOutputArray	dst,
					double				alpha = 1,
					double				beta = 0,
					int					norm_type = NORM_L2,
					int					dtype = -1,
					InputArray			maxk = noArray())
  • alpha:值域归一化中的值域最小值
  • beta:值域归一化中的值域最大值
  • norm_type:归一化类型
  • dtype:输出矩阵的数据类型,默认为-1,即与原矩阵保持一致
  • mask:掩码矩阵(可选)

这里我们定义的直方图的尺寸是 512 × 400 512 \times 400 512×400,而直方图的计算结果肯定会有超出512的数值,所以必须进行归一化处理:

int hist_w{ 512 }, hist_h{ 400 };	//直方图的长和宽

Mat histImage(hist_h, hist_w, CV_8UC3, Scalar(0, 0, 0));	//用来绘制直方图的图片

//对直方图计算结果进行归一化处理
normalize(b_hist,	
	b_hist,
	0,				//归一化之后值域的最小值 alpha = 0
	histImage.rows,	//归一化之后值域的最大值 alpha = 400
	NORM_MINMAX,	//归一化类型
	-1,				//输出结果类型与原矩阵一致
	Mat());			//空矩阵代表不是用掩码
normalize(g_hist, g_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat());
normalize(r_hist, r_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat());

归一化之后,3个通道的计算结果的值域都在0到400之间。

绘制

绘制直方图的基本思路是将每个通道中的计算结果(频数结果)转换成点的坐标(横坐标为强度值,纵坐标为结果值,即频数),然后将每个点和前一个点进行连线,最后组成一条完整的折线。具体实现方法如下:

int bin_w{ cvRound(static_cast<double>(hist_w / histSize)) };	//每组的宽度,即组距
for (int i{ 1 }; i < histSize; i++) {
	line(histImage,
		//前一个点
		Point(bin_w * (i - 1), hist_h - cvRound(b_hist.at<float>(i - 1))),
		//当前点(注意,原点在图的左上角)
		Point(bin_w * (i),	//组距X当前索引=当前点的横坐标
			hist_h - cvRound(b_hist.at<float>(i))),	//图的高度-当前的频数值=当前点的纵坐标
		Scalar(255, 0, 0), 2, 8, 0);
	line(histImage, Point(bin_w * (i - 1), hist_h - cvRound(g_hist.at<float>(i - 1))),
		Point(bin_w * (i), hist_h - cvRound(g_hist.at<float>(i))),
		Scalar(0, 255, 0), 2, 8, 0);
	line(histImage, Point(bin_w * (i - 1), hist_h - cvRound(r_hist.at<float>(i - 1))),
		Point(bin_w * (i), hist_h - cvRound(r_hist.at<float>(i))),
		Scalar(0, 0, 255), 2, 8, 0);
}

最终结果

计算结果绘制
右边就是左图的直方图计算结果。横坐标是0-255的每个强度值,纵坐标分别为R, G, B3个通道的强度值频数。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值