OpenCV灰度直方图绘制的两种方式

一、前言

直方图(Histogram)是对数据进行统计的一种方法,也是直观表现数据分布特征的一种表现方式。在数字图像分析过程中,通过图像的灰度、梯度、方向和颜色等特征属性的分布直方图我们能更客观分析图像的某些特征,对直方图的分布进行处理(如重排、区间映射等),往往能达到我们想要的视觉效果,比如:对灰度直方图进行均衡化处理,扩散灰度区间,可以有效调整图像对比度,以达到图像增强的目的,所以在传统低光照图像增强的算法中,基于直方图的增强方法是最直观和高效的。

二、直方图相关概念

直方图的概念:

在统计学中,直方图是对数据分布情况的图形表示,是一种二维统计图表,它的横纵坐标分别是统计样本和样本对应的某个属性的度量。

而在图像中,直方图可以用来统计像素的特征和属性,比如灰度、梯度、方向等,注意,直方图不局限于统计图像灰度特征,它还可以统计其他属性!本文只讨论灰度直方图的计算与绘制,简单来说:

图像灰度直方图是用以表示数字图像中灰度分布的直方图,在一幅图像中,灰度直方图的横坐标表示灰度范围(一般是0-255,也可以选择其中某一区间),从左到由灰度逐渐增加,即代表图像中暗区域过渡到亮区域;纵坐标表示每一个灰度值在图像中的个数。

从定义中可知直方图有两层含义:

  • 描述了图像中像素灰度值的分布
  • 统计了每一个灰度值具有的像素个数

以灰度直方图为例,描述一下直方图创建过程,假设有一个灰度图像矩阵包含灰度范围是0-255,
在这里插入图片描述
按照某种方式来统计这些灰度信息,我们将256个范围分割成16个子区域(用bin表示),如下:

[0,255]=[0,15][16,31]...[240,255][0,255]=[0,15]\cup [16,31]\cup ...\cup [240,255]

[0,255]=[0,15]∪[16,31]∪...∪[240,255]

range=bin1bin2bin3...binn=15range=bin_1\cup bin_2\cup bin_3\cup...\cup bin_{n=15}

range=bin1?∪bin2?∪bin3?∪...∪binn=15?

然后遍历所有像素,统计每一个子区域

bini(i=0?15)bin_i(i=0-15)

bini?(i=0?15)的像素数目。可得到下如下所示灰度直方图:
在这里插入图片描述
图中,横坐标表示子区域

binibin_i

bini?,纵坐标表示各个子区域的像素个数。同样如此,在统计其他属性的直方图时,需明确:

  1. 特征,上例为灰度
  2. bins:特征划分的子区域,上例中每16个灰度组成一个bins
  3. range:特征空间的取值范围,上例中range=[0,255]

三、灰度直方图绘制的两种方法

opencv中提供了calcHist() 函数计算图像的直方图,计算完成后可以采用opencv中的绘图函数rectangle()line() 等绘制显示出来。各函数原型如下:

calcHist()函数原型:

1
2
3
4
5
6
7
8
9
10
11
void cv::calcHist(const Mat* images,  //输入的图像指针
                  int nimages,  //需计算直方图图像的个数,单图像时取1
                  const int* channels,  //图像通道(数组表示),灰度图则channels[1]={0},彩色图像则channels[3]={0,1,2}
                  InputArray mask,  //掩膜图像,表示图像哪些区域参与统计,默认为空图像即Mat()
                  OutputArray hist,  //目标直方图,一个二维数组
                  int dims,  //目标直方图维数,灰度为1,彩色为3
                  const int* histSize,  //目标直方图区间数,即灰度范围
                  const float** ranges,  //每个灰度范围,二维数组表示
                  bool uniform = true,  //直方图等距标识符,默认等距即true
                  bool accumulate = false  //累计标识符,默认为false
                  )

通过calcHist可以得到一个二维Mat数组,表示每一个bins区间像素个数,在绘制直方图时为避免最大区间个数超出表示范围,我们通常借助minMaxLoc()函数来求区间最值,并使用normalize()函数将直方图纵坐标归一化到一指定区间,便于显示。他们的函数原型分别为:
minMaxLoc()函数原型:

1
2
3
4
5
6
7
void cv::minMaxLoc(InputArray src,  //输入一维向量或数组
                   double*  minVal,  //double类型指针,返回最小值(不需要时置NULL)
                   double*  maxVal=0,  //double类型指针,返回最大值(不需要时置NULL)
                   Point* minLoc=0,  //最小值位置指针(不需要时置NULL)
                   Point* maxLoc=0,  //最大值位置指针(不需要时置NULL)
                   InputArray mask=noArray()  //掩膜图像,默认noArray()
                   )

归一化函数normalize()函数原型:

1
2
3
4
5
6
7
void cv::normalize(InputArray src,  //原始图像数组
                   OutputArray dst,  //输出图像数组,与Inout尺寸相同
                   double alpha = 1,  //归一化范围最大值,默认为1
                   double beta = 0,  //归一化范围最小值,默认为0
                   int norm_type=NORM_L2,  //归一化方式,默认为NORM_L2
                   int dtype=-1,  //类型标识,输出图像和源图是否相同类型
                   InputArray mask = noArray()  //掩膜图像数组,默认为noArray(),不用管                )

当然说了那么多,还要落脚到最初的问题上:绘制直方图,这里我们可以使用line()或rectangle()函数绘制直线式或矩形式灰度直方图。这里留着下个Blog系统介绍绘图函数。

本文针对灰度直方图的绘制总结了两种绘制方式:

  1. 计数方式:不使用calcHist()函数,直接统计灰度个数,前提是我们知道了灰度范围[0-255]。
  2. calcHist统计方式:使用calcHist()函数,输出灰度特征二维向量。这种方式适用性更广,且可以统计图像其他属性的直方图。

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#include<opencv2/opencv.hpp>
#include<iostream>

using namespace cv;

int main()
{
    Mat src=imread("C:/Users/Administrator/Desktop/beauty.jpg",0);
    //Mat img2=imread("C:/Users/Administrator/Desktop/dog.jpg");
    Size dsize = Size(600, 400);//Size型 改变尺寸
    resize(src, src, dsize, 0, 0, INTER_LINEAR); //使用双线性插值缩放一下尺寸

    int image_count=1;  //输出单个直方图
    int channels[1]={0}; //单通道
    Mat out; //输出二维数组
    int dims=1; //维度为1
    int histSize[1]={256};  //灰度值Size:256个
    float hrange[2]={0,255}; //灰度范围[0-255]
    const float* ranges[1]={hrange}; //单个灰度范围[0-255]

    //1、计数直接统计灰度个数
    Mat histImage=getHistImage(src);
   
    //2、使用calcHist输出二维灰度统计数组
    calcHist(&src,image_count,channels,Mat(),out,dims,histSize,ranges);
    Mat histImage1=getHistImage(out);   //第二种方法

    imshow("srcImage",src);
    imshow("count_histogram",histImage);
    imshow("calcHist_histogram",histImage1);

    cv::waitKey();
    return 0;
}

Mat getHistImage(const Mat& src){
    int histogram[256]={0};
    int tmp;

    //统计灰度个数
    for(int i=0;i<src.rows;i++){
        for(int j=0;j<src.cols;j++){
            tmp=src.at<uchar>(i,j);
            histogram[tmp]++;
        }
    }

    Mat hist(1,256,CV_32F,histogram);   //用一个一维数组实例化Mat数组:hist并初始化

    double minVal=0;
    double maxVal=0;
    minMaxLoc(hist,&minVal,&maxVal,0,0);  //寻找全局最小、最大像素数目

    //绘制直方图
    Mat histImage(255,255,CV_8UC3,Scalar(255,255,255));
    int hpt=static_cast<uchar>(0.9*hist.cols); //直方图最大高度
    for(int i=0;i<255;i++){
        float binVal=hist.at<float>(i);
        int intensity=static_cast<int>(binVal*hpt/maxVal);
        line(histImage,Point(i,hist.cols),Point(i,hist.cols-intensity),Scalar(255,0,0));
    }
    return histImage;
}

cv::Mat getHistImage1(const cv::Mat& hist){
    double maxValue=0;
    double minValue=0;
    //int a=0;
    //for(int i=0;i<hist.rows;i++){
    //    for(int j=0;j<hist.cols;j++){
    //        float b=hist.at<float>(i,j);
    //        a+=1;
    //        std::cout<< b <<std::endl;  //输出该二维数组
    //    }
    //}
    minMaxLoc(hist,&minValue,&maxValue,0,0); //寻找全局最小、最大像素数目

    int histSize=hist.rows;  
    Mat histImage(histSize,histSize,CV_8UC3,Scalar(255,255,255));

    int hpt=static_cast<int>(0.9*histSize); //设置最大高度
    for(int i=0;i<hist.rows;i++){
        float binVal=hist.at<float>(i);
        int intensity=static_cast<int>(binVal*hpt/maxValue);
        cv::line(histImage,cv::Point(i,histSize),cv::Point(i,histSize-intensity),Scalar(255,0,0));
    }
   
    return histImage;
}

为方便后续调用,可以把计算绘制Histogram函数写在Header文件中,程序运行效果如下:
在这里插入图片描述
可以看出两种方法得到的灰度直方图是一致的。