图像直方图是反映一个图像像素分布的统计表,其实横坐标代表了图像像素的种类,可以是灰度的,也可以是彩色,图像是由像素构成,因为反映像素分布的直方图往往可以作为图像一个很重要的特征。
在opencv中,直方图可以通过函数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 );
其中,参数1:images(arrays): 源输入(图像)数组,必须是相同深度的CV_8U或者CV_32F(即uchar或者float),相同大小,每一个可以是任意通道的;
参数2:nimages(narrays): 源输入数组中的元素个数(本例只有一幅图像,故为1)
参数3:channels: 图像的通道,它是一个数组,如果是灰度图像(单通道)则channels[1]={0},如果是彩色图像则channels[3]={0,1,2};如果是只是求彩色图像第2个通道的直方图,则channels[1]={1};
参数4:mask: 可选掩码,是一个遮罩图像用于确定哪些点参与计算,实际应用中是个很好的参数,默认情况我们都设置为一个空图像,即:Mat()。
参数5:hist: 输出直方图,是一个稠密或者稀疏的dims维的数组;
参数6:dims: 直方图的维数,(得到的直方图的维数,灰度图像为1维,彩色图像为3维。)必须为正,并且不大于CV_MAX_DIMS(32)(本例中为1,因为统计的是每幅单通道图像的灰度直方图)
参数7:histSize:用于指出直方图数组每一维的大小的数组,即指出每一维的bin的个数的数组(本例中,只有1维,所以例子1中直接对int取地址作为参数,即该维的bin的个数为256)
参数8: ranges:用于指出直方图每一维的每个bin的上下界范围数组的数组。
参数9:uniform:判断直方图是均匀与否(均匀:true,非均匀:false)
参数10:累加标志(单幅图像不进行累计所以例子1中为false)
需要注意的是:
(1)如果channels参数为0,则nimages(narrays):和dims必须相等。
(2)当channels不是0的时候,用于计算直方图的图像是images(arrays)中由channels指定的通道的图像,channels与images(arrays)中的图像的对应关系,如channels的参数说明的,将images(arrays)中的图像从第0幅开始按照通道摊开排列起来,然后channels中的指定的用于计算直方图的就是这些摊开的通道;
例如images(arrays)中只有一幅三通道的图像image,那么nimages(narrays)应该为1,如果是想计算3维直方图【最大也只能是3维的】,想将images的通道2作为第一维,通道0作为第二维,通道1作为第三维,则可以将channels设置为channesl={2,0,1};这样calcHist函数计算时就按照这个顺序来统计直方图。
可以看出channels不为0时narrays可以和dims不相等,只要保证images(arrays)中至少有channels指定的通道就可以。
下面给出一个详细的例子:
#include "stdafx.h"
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <iostream>
#pragma comment(lib, "opencv_core246d.lib")
#pragma comment(lib, "opencv_highgui246d.lib")
#pragma comment(lib, "opencv_imgproc246d.lib")
using namespace cv;
using namespace std;
#define HIST_DIM1
int main( int argc, char** argv )
{
#ifdef HIST_DIM1
//----------------------example 1-------------------------------//
Mat src, dst;
/// Load image
//加载图像
src = imread("D:\\EgtProject\\image\\test.jpg");
if( !src.data )
{
cout<<"load image failed"<<endl;
return -1;
}
/// Separate the image in 3 places ( R, G and B )
vector<Mat> rgb_planes;
//#define SHOW_HSV
#ifdef SHOW_HSV
Mat hsv;
cvtColor(src, hsv, COLOR_BGR2HSV);
split(hsv, rgb_planes );
#else
split(src, rgb_planes );
#endif
/// Establish the number of bins
int histSize = 256; //用于形成直方图的“收集箱”的个数(级灰度级)
/// Set the ranges ( for R,G,B) )
float range[] = { 0, 255 } ;
const float* histRange = { range };
bool uniform = true; bool accumulate = false;
Mat r_hist, g_hist, b_hist;
/// Compute the histograms:
calcHist( &rgb_planes[2], 1, 0, Mat(), r_hist, 1, &histSize, &histRange, uniform, accumulate );
calcHist( &rgb_planes[1], 1, 0, Mat(), g_hist, 1, &histSize, &histRange, uniform, accumulate );
calcHist( &rgb_planes[0], 1, 0, Mat(), b_hist, 1, &histSize, &histRange, uniform, accumulate );
//r_hist中的data指针指向的数据,是每一个收集箱中,对应像素值得个数,即每个像素都有属于
//它的一个收集箱
for( int i = 1; i < histSize; i++ )
{
int rt=r_hist.at<float>(i-1);
//r_hist.data[256]={14094,1373,1474,1188,...,722,825,1012}
cout << "ii=" <<i<<"->"<<rt<<endl;
}
// Draw the histograms for R, G and B
int hist_w = 600; int hist_h = 400;
int bin_w = cvRound( (double) hist_w/histSize ); //每个收集箱的宽度
Mat rgb_hist[3];
for(int i=0; i<3; ++i)
{
rgb_hist[i] = Mat(hist_h, hist_w, CV_8UC3, Scalar::all(128)); //创建3个矩阵由于存储图像数据,背景填充灰色
}
Mat rgb_hist1[3];
for(int i=0; i<3; ++i)
{
rgb_hist1[i] = Mat(hist_h, hist_w, CV_8UC3, Scalar::all(128)); //创建3个矩阵由于存储图像数据,背景填充灰色
}
Mat histImage(hist_h, hist_w, CV_8UC3, Scalar(128,128,128)); //创建矩阵用于存储图像数据,背景填充灰色
/// Normalize the result to [ 0, histImage.rows-10]
//将直方图归一化到了0~histImage.row-10之间,而histImage.row也即是上面的hist_h,即400,
//后面画出的直方图线的原点是在左下方,而图像的原点是在左上方,所以应该是做一个减法,至于cvRound就是浮点转换为整数。
//由由于收集箱中的数值比较到,为了绘图的方便,我们将收集箱中的数值归一化到[0,histImage-10]之间
normalize(r_hist, r_hist, 0, histImage.rows-10, NORM_MINMAX);
normalize(g_hist, g_hist, 0, histImage.rows-10, NORM_MINMAX);
normalize(b_hist, b_hist, 0, histImage.rows-10, NORM_MINMAX);
/// Draw for each channel
//绘制1条竖线
for( int i = 1; i < histSize; i++ )
{
//遍历r_hist的data指向的区域中的每一个数据
int rt=r_hist.at<float>(i-1); //每个收集箱上面的值
//由于图像的原点在左上角,故需要hist_h-cvRound(rt)
line( histImage, Point( bin_w*(i-1), hist_h-cvRound(rt) ) , //r_hist[0-255]:存储的数据为,如:0对应3个点,1对应30个点,...,255对应20个点
Point( bin_w*(i), hist_h-cvRound(r_hist.at<float>(i)) ), //r_hist.data={3,30,...,20}
Scalar( 0, 0, 255), 4); //BGR
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), 4);
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), 4);
}
//将计数绘制到图像上(BGR)
putText(histImage, "R", Point(300+10, 30), FONT_HERSHEY_SIMPLEX, 1, Scalar( 0, 0, 255), 3);
putText(histImage, "G", Point(300-20, 250), FONT_HERSHEY_SIMPLEX, 1, Scalar(0, 255, 0), 3);
putText(histImage, "B", Point(300+10, 300), FONT_HERSHEY_SIMPLEX, 1, Scalar( 255, 0, 0), 3);
//绘制一个矩形(条形)
for (int j=0; j<histSize; ++j)
{
int val = saturate_cast<int>(r_hist.at<float>(j)); //每个收集箱(bin)中的数值
//rgb_hist[0].row是rgb_hist[0]这个图像的高
//由于图像的原点在左上角,因此rgb_hist[0].rows-val
//点 Point(j*2+10, rgb_hist[0].rows):矩形的左下角,点Point((j+1)*2+10, rgb_hist[0].rows-val):矩形的右上角
rectangle(rgb_hist[0], Point(j*2+10, rgb_hist[0].rows), Point((j+1)*2+10, rgb_hist[0].rows-val), Scalar(0,0,255),1,8);
val = saturate_cast<int>(g_hist.at<float>(j));
rectangle(rgb_hist[1], Point(j*2+10, rgb_hist[1].rows), Point((j+1)*2+10, rgb_hist[1].rows-val), Scalar(0,255,0),1,8);
val = saturate_cast<int>(b_hist.at<float>(j));
rectangle(rgb_hist[2], Point(j*2+10, rgb_hist[2].rows), Point((j+1)*2+10, rgb_hist[2].rows-val), Scalar(255,0,0),1,8);
/**********直方图绘制倒立了-因为图像的原点从左上角开始的***********/
val = saturate_cast<int>(r_hist.at<float>(j)); //每个收集箱(bin)中的数值
//rgb_hist[0].row是rgb_hist[0]这个图像的高
//由于图像的原点在左上角,因此rgb_hist[0].rows-val
rectangle(rgb_hist1[0], Point(j*2+10, 0), Point((j+1)*2+10, val), Scalar(0,0,255),1,8);
val = saturate_cast<int>(g_hist.at<float>(j));
rectangle(rgb_hist1[1], Point(j*2+10, 0), Point((j+1)*2+10,val), Scalar(0,255,0),1,8);
val = saturate_cast<int>(b_hist.at<float>(j));
rectangle(rgb_hist1[2], Point(j*2+10, 0), Point((j+1)*2+10, val), Scalar(255,0,0),1,8);
}
/// Display
namedWindow("histImage", CV_WINDOW_AUTOSIZE );
namedWindow("wnd");
imshow("histImage", histImage );
imshow("wnd", src);
imshow("R", rgb_hist[0]);
imshow("G", rgb_hist[1]);
imshow("B", rgb_hist[2]);
imshow("R1", rgb_hist1[0]);
imshow("G1", rgb_hist1[1]);
imshow("B1", rgb_hist1[2]);
#else
//----------------------example 2-------------------------------//
Mat src, hsv;
if(!(src=imread("D:\\EgtProject\\image\\test.jpg")).data)
return -1;
cvtColor(src, hsv, CV_BGR2HSV);
// Quantize the hue to 30 levels
// and the saturation to 32 levels
int hbins = 60, sbins = 64;
int histSize[] = {hbins, sbins};
// hue varies from 0 to 179, see cvtColor
float hranges[] = { 0, 180 };
// saturation varies from 0 (black-gray-white) to
// 255 (pure spectrum color)
float sranges[] = { 0, 256};
const float*ranges[] = { hranges, sranges };
MatND hist;
// we compute the histogram from the 0-th and 1-st channels
int channels[] = {0, 1};
calcHist( &hsv, 1, channels, Mat(),hist, 2, histSize, ranges,true, false );
double maxVal=0;
minMaxLoc(hist, 0, &maxVal, 0, 0);
int scale = 8;
Mat histImg = Mat::zeros(sbins*scale, hbins*scale, CV_8UC3);
for( int h = 0; h < hbins; h++ )
{
for( int s = 0; s < sbins; s++ )
{
float binVal = hist.at<float>(h, s);
int intensity = cvRound(binVal*255/maxVal);
rectangle( histImg, Point(h*scale, s*scale),Point((h+1)*scale-1, (s+1)*scale-1), Scalar::all(intensity), CV_FILLED);
}
}
namedWindow( "Source", 1 );
imshow( "Source", src );
namedWindow( "H-S Histogram", 1 );
imshow( "H-S Histogram", histImg );
#endif
//-------------------------------------------------------------------------//
waitKey(0);
destroyAllWindows();
return 0;
}
运行结果:
图1 原始图
图2 RGB三个通道的直方图曲线
图3 R通道的直方图(正立)
图4 R通道的直方图(倒立)
图5 G通道的直方图(正立)
图 6 G通道的直方图(倒立)
图 7 B通道的直方图(正立)
图8 B 通道的直方图(倒立)
分析:
(1)出现倒立的原因是因为,opencv中图像默认是把图像的左上角作为原点。
(2)在opencv中RGB图像中的排列顺序是:BGR,因此,在设置颜色的时候也需要注意,如设置颜色值为红色, 是使用Scalar( 0, 0, 255)。
(3)在直方图中,横坐标是收集箱(bins)的个数,纵坐标是相应的频数,即与收集箱区域的值对应的像素个数的统计。
(4)对于函数calcHist,它的输出变量hist,是一个Mat类型,这个变量对应的data指针是指向,收集箱(bins)的频数,如在上面例子中data指向序列{14094,1373,1474,1188,...,722,825,1012},其中14094是第1个bins中的频数,1373是第2个bin中的频数,...,1012是第256个bins中的频数(本例中收集箱(bins)的个数是256个)。
下面我们通过matlab,来测试一下,我们只打算输出R分量,同样我们打算设置收集箱(bins)的个数为256,一般情况下, bins的个数或者说条状的个数,可以认为是灰度级的个数,比如收集箱(bins)的个数为128,数据的变化范围是0-255,那么我们可以在0-255这256个数据区间中,分割128个区间,然后统计像素掉落在每个区间个数,即得到每个区间的频数。这128个区间,在数学上,可以这样分,[-0.5,1.5],[1.5,3.5],...,[253.5,255.5],也就是所分区间的最大值要比255稍微大,区间的最小值比0要稍微小,在这里,我们去了-0.5和255.5,下面是测试结果:
图9 matlab中测试的R分量的柱状图
图10 matlab中R分量的曲线图直方图
代码:
close all
clc,clear
img=imread('test.jpg');
r=img(:,:,1); %R分量
g=img(:,:,2); %G分量
b=img(:,:,3); %B分量
%nbins=128; %区间(收集箱,即灰度级的个数)的个数
%[n,xout]=imhist(r,nbins); %n:是一个nbins*1大小的向量,记录了每个bin内的像素的个数
%xout是区间分界值
h=imhist(r,256);
h1=h(1:1:256);
horz = 1:1:256;
bar(horz,h1);
axis([0,255,0 2600])
ylabel('频数');
xlabel('灰度级')
title('R分量')
figure
plot(1:256,h)
ylabel('频数');
xlabel('灰度级')
title('R分量')
axis([0,255,0 2600])
参考文献:
1.OpenCV深入学习(5)--直方图之calcHist使用
2.OpenCV深入学习(6)--直方图之calcHist使用(补)