赞
踩
直方图的横坐标除了可以是图片中的强度值,也可以是任何其他我们想要观察的特征。例如,下面的图片矩阵中包含了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=bin1∪bin2∪.....∪binn=15
这样就可以得到类似于下图的直方图:
直方图中的元素的定义如下:
dims = 1
;bins = 16
;range = [0, 255]
;如果你相观察的参数不止一个,比如说2个,即dims = 2
,那就需要画一个3维的图了。
split
函数将图片分离为R, G, B3个矩阵数据calcHist
函数对分离出来的3个矩阵分别计算直方图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 )
images
:const Mat*
类型,可以是一个图片矩阵的指针,也可以是图片矩阵的数组;代表需要计算直方图的图片nimages
:第1个参数中包含的图片数量channels
:const 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
:直方图的维数,必须是正数,且不能超过32histSize
:直方图每个维度的组数ranges
:const float **
类型,所以必须是一个数组的住宿。代表直方图每个维度的全距;如果是均匀分布的直方图,那每个维度只需要提供最大值和最小值就行了,即 m i n , m a x {min, max} min,max,注意是左闭右开的区间;如果不是均匀分布的直方图,则需要提供每组的最小值及最后一组的最大值,即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个通道的强度值频数。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。