什么是图像直方图
图像直方图是反映一个图像像素分布的统计表,其实横坐标代表了图像像素的种类,可以是灰度的,也可以是彩色的。纵坐标代表了每一种颜色值在图像中的像素总数或者占所有像素个数的百分比
calcHist函数参数
void 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
const Mat* images:输入图像
int nimages:输入图像的个数
const int* channels:需要统计直方图的第几通道(0表示第一通道)
InputArray mask:掩膜,控制运算的范围。不懂的可以看这篇博文:[图像位操作(带mask解说)](https://blog.csdn.net/m0_60447786/article/details/125650689?spm=1001.2014.3001.5501)
OutputArray hist:输出的直方图数组
int dims:需要统计直方图通道的个数(一般都是1)
const int* histSize:指的是直方图分成多少个区间,就是 bin的个数
const float** ranges: 统计像素值得区间(相当于每一个横坐标区间的取值范围)
bool uniform=true::是否对得到的直方图数组进行归一化处理
bool accumulate=false:在多个图像时,是否累计计算像素值得个数
注意:后两个参数也可以了忽略,忽略的时候默认是 true false
calcHist函数只是计算直方图的数据,(直方图数据需要一个mat类型的变量来存储)
绘制直方图流程
1.读入图像
2.分开图像通道(不一定只有三个通道)
3.定义直方图的参数
4.使用calcHist()函数得到直方图数据
5.定义幕布参数
6.归一化直方图参数
7.将直方图数据画在幕布上
8.显示幕布
绘制直方图实战
源码如下:
#include<opencv2/opencv.hpp>
#include<iostream>
#include<vector>
using namespace std;
using namespace cv;
int main()
{
//绘制图像直方图
Mat src = imread("C:\\Users\\86151\\Desktop\\opencv\\picture\\4.jpg");
imshow("src",src);
if (src.empty()) //这里建议大家加上,形成习惯,有时候会帮助我们及时发现bug,博主亲身经历
{
cout << "no picture" << endl;
return -1;
}
//提取三通道
vector<Mat> all_channel;
split(src,all_channel); //split函数将图像的三通道分别提取出来,放到all_channel数组里面
//定义参数变量
const int bin = 256;//因为图像一共有256个灰度级别,意思就是像素点0-255有256个,那么需要256个横坐标,所以bin赋值256。
float bin_range[2] = {
0,255 };//每个通道的灰度级别0-255,也就是像素点的取值范围
const float* ranges[1] = {
bin_range };//这样做只是方便下面clacHist函数的传参
//定义变量来存储直方图数据,一共提取出了三个通道:R G B,所以定义三个变量
Mat b_hist;
Mat g_hist;
Mat r_hist;
//计算得到直方图数据
calcHist(&all_channel[0], 1, 0, Mat(), b_hist, 1, &bin, ranges, true, false);
calcHist(&all_channel[1], 1, 0, Mat(), g_hist, 1, &bin, ranges, true, false);
calcHist(&all_channel[2], 1, 0, Mat(), r_hist, 1, &bin, ranges, true, false);
/*
参数解析:
all_channel[i]:传入要计算直方图的通道,根据函数原函数可以得出,要以引用的方式传入
1:传入图像的个数,就一个
0:表示传入一个通道
Mat():没有定义掩膜,所以默认计算区域是全图像
b/g/r_hist:用来存储计算得到的直方图数据
1:对于当前通道需要统计的直方图个数,我们统计一个
bin:直方图的横坐标有多少个,我们将其赋值为256,即统计每一个像素值的数量。要求用引用方式传入。
ranges:每个像素点的灰度等级,要求以引用方式传入。
true:进行归一化,
false:计算多个图像的直方图时,不累加上一张图像的像素点数据。
*/
//设置直方图画布的参数,直方图要花在一个“幕布”上,这个幕布也要设置参数
int hist_w = 512;//画布的宽
int hist_h = 400;//画布的高
int bin_w = cvRound((double)hist_w/bin);//设置直方 图中每一点的步长,直方图有256个横坐标,每个坐标在画布中占多长,通过hist_w/bin计算得出。cvRound()函数是“四舍五入”的作用。
Mat hist_canvas = Mat::zeros(hist_h, hist_w, CV_8UC3);//通过我们设置的参数创建出一个黑色的画布
//对三个通道的直方图数据进行归一化处理,这是一个必要环节,后续仔细说明。
normalize(b_hist, b_hist, 0, 255, NORM_MINMAX, -1, Mat());
normalize(g_hist, g_hist, 0, 255, NORM_MINMAX, -1, Mat());
normalize(r_hist, r_hist, 0, 255, NORM_MINMAX, -1, Mat());
//以折线统计图的方式绘制三个通道的直方图在画布上
for (int i = 1; i < 256; i++)
{
line(hist_canvas, Point(bin_w * (i-1), hist_h - cvRound(b_hist.at<float>(i-1))),
Point(bin_w * (i), hist_h - cvRound(b_hist.at<float>(i))), Scalar(0, 0, 255), 2, 8, 0);
line(hist_canvas, 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(hist_canvas, 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(255, 0, 0), 2, 8, 0);
}
//展示到屏幕上
imshow("result",hist_canvas);
waitKey(0);
destroyAllWindows();
return 0;
}
最终效果
带有归一化的效果:
不带有归一化的效果:
由此可见,我们归一化的目的就是让数据同一在一个范围内,因为我们观察的就是各个像素值所占的比例,所以同时放缩到一个范围是不错的选择,易于观察。对normalize函数有疑问的也可以参考下这篇文章:normalize函数详解
line函数解析(对应代码中的line)
line函数解析:
hist_canvas : 要绘制的画布
第一个Point:bin_w* (i - 1)表示第一个点的横坐标,因为步长就是bin_w, 而且一共有256个横坐标区间,那么随着横坐标的移动,绘制直线的横坐标也要随之移动,所以使用(i - 1) * bin_w。b_hist.at(i - 1):表示取出这个像素点的数量n,然后通过hist_h(画布的高)减去n进行绘制,为什么要减去呢?因为我们一半的管理是从左下角作为图像的起始点(0,0),但是计算机是以图像的左上角点作为其实坐标(0, 0)的,所以用画布的高减去n,绘制出来就可以达到我们一般眼光看到的效果。
第二个Point : 画一条直线需要两点,所以第二个Point也需要,那么横坐标的话就是比上一点多一个bin_w,纵坐标的话也是一样的原理,只不过数值变化了。
通过这样的方式画出三个通道的全部直方图便可,后面的Scalar(255, 0, 0)表示颜色,第一个Scalar表示蓝色线,第二个是绿色线,第三个是红色线。(blue green red)