何为直方图
直方图是一种常用的数据统计图。对某一物理或特征量不同取值,找出它的最大值和最小值,然后确定一个区间,使其包含全部测量数据,将区间分成若干小区间,统计测量结果出现在各小区间的频数或占比,以测量数据为横坐标,以频数或占比为纵坐标,划出各小区间及其对应的频数或占比高度,则可得到一个矩形图,即统计直方图。
直方图统计
直方图中分成的若干小区间数量称为组数,用bins表示,其中每组两个端点的横坐标差称为组距。直方图所表征数据的最小和最大值的区间称为直方图的值的范围(range)。
如果所有组的组距相同的直方图称为均匀直方图(uniform histogram),否则称为非均匀直方图。
基于一个物理量或物体特征值建立的直方图为一维直方图,事实上也可以基于多个物理量或特征值来建立多维直方图,如人口数据中基于年龄+受教育年数来建立人口数量分布直方图就是二维直方图。物理量或特征量的个数称为直方图的维数,用dims表述。
图像直方图
在图像处理中,以图像具体通道的像素值的数据构成的集合值作为直方图的横坐标,每个值出现的数量或比例作为纵坐标,所得的直方图就是图像直方图(Image Histogram)。
其实不只基于图像像素值建立直方图,也可以对图像梯度、每个像素的角度等一切图像的属性值都可以建立直方图。但在图像处理中基于图像像素值的直方图是最常见的。一般图像直方图都是一维直方图。
图像直方图具有平移、旋转和缩放不变性的特点。对于平移图像,旋转图像角度的情况下,图像操作前后的直方图分布不变,对于缩放图像,前后直方图的分布也基本不变可以通过图像对应的直方图来判断一张图像是否偏暗、偏亮或者光线正常,图像直方图的横坐标的左侧为纯黑、较暗的区域,而右侧为较亮、纯白的区域。因此一张较暗图片的直方图中的数据多集中于左侧和中间部分,而整体明亮、只有少量阴影的图像则相反。如下图:
OpenCV直方图绘制
对于一般的8bit图像,可以直接使用opencv的函数进行处理,具体可以参考这篇博客。下面我直接把代码贴上:
三通道图像的直方图绘制:
Mat histogram_demo(Mat &image)
{
/*图像直方图是图像像素值的统计学特征,计算代价较小,具有图像的平移、旋转、缩放不变性的优点。
Bins是指直方图的大小范围
*/
//三通道分离
std::vector<Mat>bgr_plane;
split(image, bgr_plane);
//定义参数变量
const int channels[1] = { 0 };
const int bins[1] = { 256 };//一共有256个灰度级别
float hranges[2] = { 0,255 };//每个通道的灰度级别是0-255
const float* ranges[1] = { hranges };
Mat b_hist;
Mat g_hist;
Mat r_hist;
//计算Blue、Green、Red通道的直方图,1表示只有一张图,因为可以支持多张图多个通道;0表示只有1个通道;raanges就是直方图的取值范围0-25
calcHist(&bgr_plane[0], 1, 0, Mat(), b_hist, 1, bins, ranges);
calcHist(&bgr_plane[1], 1, 0, Mat(), g_hist, 1, bins, ranges);
calcHist(&bgr_plane[2], 1, 0, Mat(), r_hist, 1, bins, ranges);
//显示直方图
int hist_w = 512;
int hist_h = 400;
int bin_w = cvRound((double)hist_w / bins[0]);
Mat histImage = Mat::zeros(hist_h, hist_w, CV_8UC3);
//归一化直方图数据
normalize(b_hist, b_hist, 0, histImage.rows, 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());
//绘制直方图曲线
for (int i = 1; i < bins[0]; i++) {
line(histImage, 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(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);
}
//显示直方图
//namedWindow("Histogram Demo", WINDOW_AUTOSIZE);
//imshow("Histogram Demo", histImage);
return histImage.clone();
}
处理效果如下:
灰度图像的直方图绘制:
Mat histogram_grayImage(const Mat& image)
{
//定义求直方图的通道数目,从0开始索引
int channels[] = { 0 };
//定义直方图的在每一维上的大小,例如灰度图直方图的横坐标是图像的灰度值,就一维,bin的个数
//如果直方图图像横坐标bin个数为x,纵坐标bin个数为y,则channels[]={1,2}其直方图应该为三维的,Z轴是每个bin上统计的数目
const int histSize[] = { 256 };
//每一维bin的变化范围
float range[] = { 0,256 };
//所有bin的变化范围,个数跟channels应该跟channels一致
const float* ranges[] = { range };
//定义直方图,这里求的是直方图数据
Mat hist;
//opencv中计算直方图的函数,hist大小为256*1,每行存储的统计的该行对应的灰度值的个数
calcHist(&image, 1, channels, Mat(), hist, 1, histSize, ranges, true, false);
//找出直方图统计的个数的最大值,用来作为直方图纵坐标的高
double maxValue = 0;
//找矩阵中最大最小值及对应索引的函数
minMaxLoc(hist, 0, &maxValue, 0, 0);
//最大值取整
int rows = cvRound(maxValue);
//定义直方图图像,直方图纵坐标的高作为行数,列数为256(灰度值的个数)
//因为是直方图的图像,所以以黑白两色为区分,白色为直方图的图像
Mat histImage = Mat::zeros(rows, 256, CV_8UC1);
//直方图图像表示
for (int i = 0; i < 256; i++)
{
//取每个bin的数目
int temp = (int)(hist.at<float>(i, 0));
//如果bin数目为0,则说明图像上没有该灰度值,则整列为黑色
//如果图像上有该灰度值,则将该列对应个数的像素设为白色
if (temp)
{
//由于图像坐标是以左上角为原点,所以要进行变换,使直方图图像以左下角为坐标原点
histImage.col(i).rowRange(Range(rows - temp, rows)) = 255;
}
}
//由于直方图图像列高可能很高,因此进行图像对列要进行对应的缩减,使直方图图像更直观
Mat resizeImage;
resize(histImage, resizeImage, Size(256, 256));
return resizeImage;
}
同样的图将其灰度化后的直方图统计:
自定义绘制直方图
void DrawHist(void* srcData, int *m_dim)
{
int iWidth = m_dim[0];
int iHeight = m_dim[1];
//histgram
int min = 0;
int max = 8000;
int bin = 200;
int internal = (max - min) / bin;
MatND dstHist;
Mat bins = GetBins(internal, 0, bin);
dstHist = CalcGrayHist(bin, internal, (signed short*)srcData, iWidth, iHeight, 1);
//get soft value
Mat histMaxValue = GetMaxBin(dstHist, bins, bin, 500);
int softValue = histMaxValue.at<int>(0, 0);
int softIndex = histMaxValue.at<int>(0, 1);
//设置直方图画布的参数,直方图要花在一个“幕布”上,这个幕布也要设置参数
int hist_w = 1200;//画布的宽
int hist_h = 800; //画布的高
int bin_w = cvRound((double)hist_w / bin); //设置直方 图中每一点的步长,直方图有256个横坐标,每个坐标在画布中占多长,通过hist_w/bin计算得出。cvRound()函数是“四舍五入”的作用。
Mat hist_canvas(hist_h, hist_w, CV_8UC3, cv::Scalar(255, 255, 255));//通过我们设置的参数创建出一个黑色的画布
//对三个通道的直方图数据进行归一化处理,这是一个必要环节
normalize(dstHist, dstHist, 0, hist_h - 100, NORM_MINMAX, -1, Mat());
int isGrid = 1;
cv::Scalar scalarGrid = Scalar(255, 206, 135);
if (isGrid)
{
//添加网格
for (int i = 0; i < hist_w; )
{
cv::line(hist_canvas, Point(bin_w * i, hist_h - cvRound(0)),
Point(bin_w * i, hist_h - cvRound(hist_h)), scalarGrid, 1, 8, 0);
i += bin_w;
}
for (int i = 0; i < hist_h; )
{
cv::line(hist_canvas, Point(0, hist_w - bin_w * i),
Point(hist_w, hist_w - bin_w * i), scalarGrid, 1, 8, 0);
i += bin_w;
}
}
//以折线统计图的方式绘制三个通道的直方图在画布上
for (int i = 1; i < bin; i++)
{
cv::line(hist_canvas, Point(bin_w * (i - 1), hist_h - cvRound(dstHist.at<int>(i - 1))),
Point(bin_w * (i), hist_h - cvRound(dstHist.at<int>(i))), Scalar(0, 0, 255), 2, CV_AA, 0);
}
cv::imshow("hist", hist_canvas);
cv::waitKey(0);
cv::destroyAllWindows();
}
//获取直方图bins
Mat GetBins(int internal, int min, int bin)
{
Mat bins = Mat::zeros(Size(bin, 1), CV_32SC1);
for (int i = 0; i < bin; i++)
{
bins.at<int>(i) = min + i*internal;
}
return bins;
}
//直方图统计
Mat CalcGrayHist(int bin, int internal, signed short* srcImage, int width, int height, int counts)
{
Mat histogram = Mat::zeros(Size(bin, 1), CV_32SC1); //注意,Size对应的是x和y,也就是第一个元数是矩阵的列数
for (int k = 0; k < counts; k++)
{
for (int i = 0; i < height; i++)
{
for (int j = 0; j < width; j++)
{
int index = k*width*height + i*width + j; //获取每个点的像素值
if (srcImage[index] > 3)
{
int value = srcImage[index] / internal;
if (value < (bin - 1))
{
histogram.at<int>(0, value) += 1; //获取了一个像素值,在相应的位置上加1
}
else
{
histogram.at<int>(0, bin - 1) += 1;
}
}
}
}
}
return histogram;
}
参考文献:
[1] C++OpenCV图像处理的基本操作(五)——直方图
[2] RGB颜色表