这次要整理记录的内容是图像直方图及直方图的绘制(折线图和柱状图)。
- 首先,一幅图像是由很多的、有限的像素点组成的,那么对于这些像素点进行数学统计,将其灰度值和像素点数量分别作为X轴和Y轴形成一幅统计图,就成为一幅图像的直方图。也就是说,图像直方图的X轴表示0~255这256个灰度值,而Y轴表示该图像中具有某个灰度值的像素点的个数。所以直方图中点(x, y)表示:与该直方图对应的图像中,具有灰度值x的像素点个数为y个。
那么既然知道了图像直方图表示什么,我们就可以通过对图像像素点的遍历来获取每个像素点的值,并进行归类、统计数量。然而,如果对每张需要求直方图的图像进行遍历是很麻烦的,尤其是分辨率很高的图像,逐一遍历像素点更是十分消耗时间,所以我们需要掌握OpenCV中提供了的APIcalcHist()
来计算图像的直方图,代码如下:
vector<Mat> bgr;
split(image, bgr);
//存放直方图数据的数组
Mat b_hist;
Mat g_hist;
Mat r_hist;
//定义参数;用数组形式定义,便于取首地址
int histSize[1] = { 255 }; //定义多少个区间
float range[2] = { 0, 256 }; //直方图上下限
const float* histRanges[1] = { range }; //定义指针数组,存放range[]的首地址;histRange即为range[]的地址的地址
int dims = bgr[0].channels();
calcHist(&bgr[0], 1, 0, Mat(), b_hist, dims, histSize, histRanges, true, false);
calcHist(&bgr[1], 1, 0, Mat(), g_hist, dims, histSize, histRanges, true, false);
calcHist(&bgr[2], 1, 0, Mat(), r_hist, dims, histSize, histRanges, true, false);
首先我们将一幅图像的三通道进行分离,分别对三张单通道图像进行直方图计算。随后,对calcHist()
这个函数的参数进行定义,注意这里都是使用数组来定义,便于取变量的地址,至于为什么要取地址 呢,主要是因为calcHist()
这个函数自身对传入参数的要求,下面整理一下该函数的参数详情:
const Mat* images:输入图像地址
int nimages:输入图像的个数
const int* channels:需要统计直方图的第几通道
InputArray mask:若有mask计算掩膜内的直方图;若没有掩膜则使用Mat()替代
OutputArray hist:输出的直方图数组,可用Mat类型定义
int dims:需要统计直方图通道的个数
const int* histSize:将直方图分成多少个区间,就是 bin的个数;需传入地址
const float** ranges: 统计像素值的区间;传入float类型的数组range[]的地址的地址
bool uniform=true: 是否对得到的直方图数组进行归一化处理
bool accumulate=false:在多个图像时,是否累计计算像素值的个数
再对该函数需要的各个参数分别进行定义:
int histSize[1] = { 255 }
这一行代码定义了一个容量为1的整型数组,其中的元素是值255,表示了直方图有255个区间;
float range[2] = { 0, 256 }
这行代码定义了一个容量为2的浮点型数组,包含元素是0和256,分别表示了直方图的灰度级的下限和上限,即灰度级区间为[ 0, 256 )的半开半闭区间。注意这里使用了float类型定义,这也是calcHist()
的传入参数要求,否则就会报错,某得什么办法。
const float* histRanges[1] = { range };
这行代码定义一个容量为1的指针数组,存放元素为数组range[]的首地址;histRange即为数组range[]的地址的地址。
int dims = bgr[0].channels()
这行代码就定义了要进行直方图计算的通道数。
到这里我们就定义完了所需的参数,可以调用函数来计算直方图了,代码如下:
calcHist(&bgr[0], 1, 0, Mat(), b_hist, dims, histSize, histRanges, true, false);
calcHist(&bgr[1], 1, 0, Mat(), g_hist, dims, histSize, histRanges, true, false);
calcHist(&bgr[2], 1, 0, Mat(), r_hist, dims, histSize, histRanges, true, false)
到这里,就完成了三通道直方图的统计,分别输出为三个Mat()对象:b_hist、g_hist、r_hist,接下来,我们就将统计出来的直方图进行可视化,毕竟眼见为实,不然的话谁知道统计出来是个什么玩意呢对吧。
- 直方图的绘制
在已经得到直方图的前提下,我们就可以对直方图进行可视化,也就是将我们的直方图绘制出来,这里需要用到先前整理过的几何形状绘制的知识。当我们绘制折线直方图时,就需要用到线段的绘制;当我们绘制柱状直方图时,就需要用到矩形的绘制。实现代码如下:
//定义直方图宽高
int hist_height = 400;
int hist_width = 512;
//定义每个像素区间的宽度
int bin_width = hist_width / histSize[0];
//定义直方图画布
Mat Hist_image = Mat::zeros(hist_height, hist_width, CV_8UC3);
Mat Hist_image_B = Mat::zeros(hist_height, hist_width, CV_8UC3);
Mat Hist_image_G = Mat::zeros(hist_height, hist_width, CV_8UC3);
Mat Hist_image_R = Mat::zeros(hist_height, hist_width, CV_8UC3);
//对三个通道直方图做归一化处理;即归一化到画布的Y轴长度hist_height上
normalize(b_hist, b_hist, 0, hist_height, NORM_MINMAX, -1, Mat());
normalize(g_hist, g_hist, 0, hist_height, NORM_MINMAX, -1, Mat());
normalize(r_hist, r_hist, 0, hist_height, NORM_MINMAX, -1, Mat());
for (int i = 1; i < histSize[0]; i++) //i必须从1开始计数,否则下标溢出报错
{
//绘制折线直方图
line(Hist_image, Point((i - 1) * bin_width, hist_height - cvRound(b_hist.at<float>(i - 1, 0))),
Point((i)*bin_width, hist_height - cvRound(b_hist.at<float>(i))), Scalar(255, 0, 0), 2, LINE_AA);
line(Hist_image, Point((i - 1) * bin_width, hist_height - cvRound(g_hist.at<float>(i - 1, 0))),
Point((i)*bin_width, hist_height - cvRound(g_hist.at<float>(i))), Scalar(0, 255, 0), 2, LINE_AA);
line(Hist_image, Point((i - 1) * bin_width, hist_height - cvRound(r_hist.at<float>(i - 1, 0))),
Point((i)*bin_width, hist_height - cvRound(r_hist.at<float>(i))), Scalar(0, 0, 255), 2, LINE_AA);
//绘制柱状直方图
int x_b = (i - 1) * bin_width;
int y_b = hist_height - cvRound(b_hist.at<float>(i - 1, 0));
int width_b = i * bin_width - x_b;
int height_b = cvRound(b_hist.at<float>(i - 1, 0));
Rect rect_b(x_b, y_b, width_b, height_b);
rectangle(Hist_image_B, rect_b, Scalar(255, 0, 0), -1, LINE_AA, 0);
int x_g = (i - 1) * bin_width;
int y_g = hist_height - cvRound(g_hist.at<float>(i - 1, 0));
int width_g = i * bin_width - x_g;
int height_g = cvRound(g_hist.at<float>(i - 1, 0));
Rect rect_g(x_g, y_g, width_g, height_g);
rectangle(Hist_image_G, rect_g, Scalar(0, 255, 0), -1, LINE_AA, 0);
int x_r = (i - 1) * bin_width;
int y_r = hist_height - cvRound(r_hist.at<float>(i - 1, 0));
int width_r = i * bin_width - x_r;
int height_r = cvRound(r_hist.at<float>(i - 1, 0));
Rect rect_r(x_r, y_r, width_r, height_r);
rectangle(Hist_image_R, rect_r, Scalar(0, 0, 255), -1, LINE_AA, 0);
}
imshow("Hist_image", Hist_image);
imshow("Hist_image_B", Hist_image_B);
imshow("Hist_image_G", Hist_image_G);
imshow("Hist_image_R", Hist_image_R);
首先我们需要定义一块画布,也就是绘制直方图的Mat对象,先规定它的尺寸,也就是高和宽,再通过int bin_width = hist_width / histSize[0]
定义每个灰度级区间在画布中的宽度,然后再创建我们的画布:
Mat Hist_image = Mat::zeros(hist_height, hist_width, CV_8UC3);
Mat Hist_image_B = Mat::zeros(hist_height, hist_width, CV_8UC3);
Mat Hist_image_G = Mat::zeros(hist_height, hist_width, CV_8UC3);
Mat Hist_image_R = Mat::zeros(hist_height, hist_width, CV_8UC3);
从上到下分别是:绘制RGB三通道折线图、B通道柱状图、G通道柱状图、R通道柱状图。
接着非常重要的一点是,对直方图进行归一化:
normalize(b_hist, b_hist, 0, hist_height, NORM_MINMAX, -1, Mat());
normalize(g_hist, g_hist, 0, hist_height, NORM_MINMAX, -1, Mat());
normalize(r_hist, r_hist, 0, hist_height, NORM_MINMAX, -1, Mat())
对三个通道直方图做归一化处理,即归一化到画布的Y轴长度hist_height上。因为像素数量可能非常多,数值可能会超过画布的高度,所以进行归一化操作来防止溢出。
到现在我们的准备工作就完成了,可以开始通过for循环来遍历每个灰度级,并绘制直方图。其中折线图的绘制,就是以该灰度级为X1坐标,以对应像素点数量为Y1坐标,从而找到坐标点(X1,Y1);再以上一级灰度级为X2坐标,以上一级灰度级对应的像素点数量为Y2坐标,找到坐标点(X2, Y2)。最后从(X1,Y1)到(X2, Y2)绘制线段,循环绘制255次,每一次对三通道直方图分别进行取值就可以得到一幅三通道的折线直方图。效果图如下:
要注意:坐标原点(0,0)是在图像的左上角!!!
然后分别绘制三幅单通道的柱状直方图,实际上和折线图大同小异,只不过是需要先初始化我们想要绘制的矩形区域,再调用rectangle()
函数进行绘制,这里同样需要注意坐标的问题。具体效果图如下:
总结:直方图在图像处理中具有非常多的应用,例如图像增强等等方面都需要使用到直方图,所以能将直方图计算、可视化出来是比较重要的环节,对于后续学习有很好的帮助。
本次整理到此结束啦~~~
PS:本人的注释比较杂,既有自己的心得体会也有网上查阅资料时摘抄下的知识内容,所以如有雷同,纯属我向前辈学习的致敬,如果有前辈觉得我的笔记内容侵犯了您的知识产权,请和我联系,我会将涉及到的博文内容删除,谢谢!