前面我们完成了对一张图像像素灰度值的统计,并成功绘制了图像的直方图。但是由于绘制直方图的图像高度小于某些灰度值统计的数目,因此我们在绘制直方图时将所有的数据都缩小为原来的二十分之一之后再进行绘制,目的就是为了能够将直方图完整的绘制在图像中。如果换一张图像的直方图统计结果或者将直方图绘制到一个尺寸更小的图像中时,可能需要将统计数据缩小为原来的三十分之一、五十分之一甚至更低。数据缩小比例与统计结果、将要绘制直方图图像的尺寸相关,因此每次绘制时都需要计算数据缩小的比例。另外,由于像素灰度值统计的数目与图像的尺寸具有直接关系,如果以灰度值数目作为最终统计结果,那么一张图像经过尺寸放缩后的两张图像的直方图将会有巨大的差异,然而直方图可以用来表示图像的明亮程度,从理论上讲通过缩放的两张图像将具有大致相似的直方图分布特性,因此用灰度值的数目作为统计结果具有一定的局限性。
图像的像素灰度值统计结果主要目的之一就是查看某个灰度值在所有像素中所占的比例,因此可以用每个灰度值像素的数目占一幅图像中所有像素数目的比例来表示某个灰度值数目的多少,即将统计结果再除以图像中像素个数。这种方式可以保证每个灰度值的统计结果都是0到100%之间的数据,实现统计结果的归一化,但是这种方式也存在一个弊端,就是再CV_8U类型的图像中,灰度值有256个等级,平均每个像素的灰度值所占比例为0.39%,这个比例非常低,因此为了更直观的绘制图像直方图,常需要将比例扩大一定的倍数后再绘制图像。另一种常用的归一化方式是寻找统计结果中最大数值,把所有结果除以这个最大的数值,以实现将所有数据都缩放到0到1之间。
针对上面这两种归一化方式,OpenCV 4提供了normalize()函数实现多种形式的归一化功能,该函数的函数原型在代码清单4-3中给出。
void cv::normalize(InputArray src,
InputOutputArray dst,
double alpha = 1,
double beta = 0,
int norm_type = NORM_L2,
int dtype = -1,
InputArray mask = noArray()
)
- src:输入数组矩阵。
- dst:输入与src相同大小的数组矩阵。
- alpha:在范围归一化的情况下,归一化到下限边界的标准值
- beta:范围归一化时的上限范围,它不用于标准规范化。
- norm_type:归一化过程中数据范数种类标志,常用可选择参数在表4-1中给出
- dtype:输出数据类型选择标志,如果为负数,则输出数据与src拥有相同的类型,否则与src具有相同的通道数和数据类型。
- mask:掩码矩阵。
该函数输入一个存放数据的矩阵,通过参数alpha设置将数据缩放到的最大范围,然后通过norm_type参数选择计算范数的种类,之后将输入矩阵中的每个数据分别除以求取的范数数值,最后得到缩放的结果。输出结果是一个CV_32F类型的矩阵,可以将输入矩阵作为输出矩阵,或者重新定义一个新的矩阵用于存放输出结果。该函数的第五个参数用于选择计算数据范数的种类,常用的可选择参数以及计算范数的公式都在表4-1中给出。计算不同的范数,最后的结果也不相同,例如选择NORM_L1标志,输出结果为每个灰度值所占的比例;选择NORM_INF参数,输出结果为除以数据中最大值,将所有的数据归一化到0到1之间。
为了了解归一化函数normalize()的作用,在代码清单4-4中给出了通过不同方式归一化数组的计算结果,并且分别用灰度值所占比例和除以数据最大值的方式对图像直方图进行归一化操作。为了更加直观的展现归一化后的结果,我们将每个灰度值所占比例放大了30倍,并将绘制直方图的图像高度作为1进行绘制直方图,最终结果在图4-3给出,根据结果显示,无论是否进行归一化,或者采用那种归一化方法,直方图的分布特性都不会改变。
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;
using namespace std;
int main(){
vector<double>positiveData{2.0,8.0,10.0};
vector<double>normalized_L1,normalized_L2,normalized_Inf,normalized_L2SQR;
//测试不同的归一化方法
normalize(positiveData,normalized_L1,1.0,0.0,NORM_L1);//绝对值求和归一化
cout<<"normalized_L1 = ["<<normalized_L1[0]<<", "
<<normalized_L1[1]<<", "<<normalized_L1[2]<<"]"<<endl;
normalize(positiveData,normalized_L2,1.0,0.0,NORM_L2);//模长归一化
cout<<"normalized_L2 = ["<<normalized_L2[0]<<", "
<<normalized_L2[1]<<", "<<normalized_L2[2]<<"]"<<endl;
normalize(positiveData,normalized_Inf,1.0,0.0,NORM_INF);//最大值归一化
cout<<"normalized_Inf = ["<<normalized_Inf[0]<<", "
<<normalized_Inf[1]<<", "<<normalized_Inf[2]<<"]"<<endl;
normalize(positiveData,normalized_L2SQR,1.0,0.0,NORM_MINMAX);//偏移归一化
cout<<"normalized_L2SQR = ["<<normalized_L2SQR[0]<<", "
<<normalized_L2SQR[1]<<", "<<normalized_L2SQR[2]<<"]"<<endl;
//将直方图归一化
Mat img=imread("699342568.jpg");
if(img.empty()){
cout<<"请确认输入的图片路径是否正确"<<endl;
return -1;
}
Mat gray;
cvtColor(img,gray,COLOR_BGR2GRAY);
Mat hist;
const int channels[1]={0};
float inRanges[2]={0,255};
const float* ranges[1]={inRanges};
const int bins[1]={256};
calcHist(&gray,1,channels,Mat(),hist,1,bins,ranges);
int hist_w=512;
int hist_h=400;
int width=2;
Mat histImage_L1=Mat::zeros(hist_h,hist_w,CV_8UC3);
Mat histImage_Inf=Mat::zeros(hist_h,hist_w,CV_8UC3);
Mat hist_L1,hist_Inf;
normalize(hist,hist_L1,1,0,NORM_L1,-1,Mat());
for(int i=1;i<=hist_L1.rows;++i){
rectangle(histImage_L1,Point(width*(i-1),hist_h-1),
Point(width*i-1,hist_h-cvRound(hist_h*hist_L1.at<float>(i-1))-1),
Scalar(255,255,255),-1);
}
normalize(hist,hist_Inf,1,0,NORM_INF,-1,Mat());
for(int i=1;i<=hist_Inf.rows;++i){
rectangle(histImage_Inf,Point(width*(i-1),hist_h-1),
Point(width*i-1,hist_h-cvRound(hist_h*hist_Inf.at<float>(i-1))-1),
Scalar(255,255,255),-1);
}
imshow("histImage_L1",histImage_L1);
imshow("histImage_Inf",histImage_Inf);
waitKey(0);
return 0;
}