OpenCV中颜色分布直方图及其应用

1、图像直方图

直方图是对数据的集合 统计,并将统计结果分布于一系列预定义的 bins 中。这里的 数据 不仅仅指的是颜色灰度值 , 统计数据可能是任何能有效描述图像的特征(如梯度、方向等)。特别地,当图像直方图描述的是图像的各个灰度级的统计特性时,称之为灰度直方图。

结合灰度直方图了解直方图的一些具体细节(参考自OpenCV官网):
dims: 需要统计的特征的数目,仅统计灰度值(灰度图像)时dims=1,对于三通道图像dims=3。
bins: 每个特征空间 子区段 的数目,如:灰度图中每十六个连续的灰度值作为一个统计区间,则 bins = 16。
range: 每个特征空间的取值范围,灰度图像中, range = [0,255]。

2、颜色分布直方图的计算

OpenCV提供了计算图像直方图的API函数calcHist,其函数原型为:

void calcHist(const Mat* arrays, 
              int narrays, 
              const int* channels, 
              InputArray mask,
              OutputArray hist, 
              int dims, 
              const int* histSize, 
              const float** ranges, 
              bool uniform=true, 
              bool accumulate=false )

parameters:
第一个参数:源输入(图像)数组,必须是深度和大小均相同的CV_8U或者CV_32F(即uchar或者float)图片,每一个可以是任意通道的;
第二个参数:源输入数组中的图片个数;
第三个参数:用来计算直方图的通道维数的数组,第一张图片的通道由0到arrays[0].channels()-1列出,第二张图片的通道从arrays[0].channels()到arrays[0].channels()+arrays[1].channels()-1,以此类推,而该参数就是从通道序列中选取一子序列,参与直方图计算的是子序列而不是所有通道序列;
第四个参数:可选的掩模,如果该矩阵不是空的,则必须是8位的并且与arrays[i]的大小相等,掩模的非零值标记需要在直方图中统计的数组元素;
第五个参数:输出直方图,是一个稠密或者稀疏的dims维的数组;
第六个参数:直方图的维数,必须为正,并且不大于CV_MAX_DIMS(当前的OpenCV版本中为32,即最大可以统计32维的直方图);
第七个参数:用于指出直方图数组每一维的大小的数组,即指出每一维的bin的个数的数组;
第八个参数:用于指出直方图每一维的每个bin的上下界范围(数组)的数组,当直方图是均匀的(uniform =true)时,对每一维i指定直方图的第0个bin的下界和最后一个即第histSize[i]-1个bin的上界,也就是说对均匀直方图来说,每一个ranges[i]都是一个两个元素的数组【指出该维的上下界】。当直方图不是均匀的时,每一个ranges[i]数组都包含histSize[i]+1个元素;
第九个参数:直方图是否均匀的标志;【指定直方图每个bin统计的是否是相同数量的灰度级】;
第十个参数:累加标志;
详细的参数说明见OpenCV documentation

单纯看文档中的参数说明和示例,对参数中的channels和dims还是不甚理解,于是借阅了下这篇博客,算是有了点眉目。其主要内容如下:

calcHist函数的channels参数和narrays以及dims共同来确定用于计算直方图的图像;首先dims是最终的直方图维数,narrays指出了arrays数组中图像的个数,其中每一幅图像都可以是任意通道的【只要最终dims不超过32即可】。

如果channels参数为0,则narrays和dims必须相等,否则弹出assert,此时计算直方图的时候取数组中每幅图像的第0通道。当channels不是0的时候,用于计算直方图的图像是arrays中由channels指定的通道的图像,channels与arrays中的图像的对应关系,如channels的参数说明的,将arrays中的图像从第0幅开始按照通道摊开排列起来,然后channels中的指定的用于计算直方图的就是这些摊开的通道。

假设有arrays中只有一幅三通道的图像image,那么narrays应该为1,如果是想计算3维直方图【最大也只能是3维的】,想将image的通道2作为第一维,通道0作为第二维,通道1作为第三维,则可以将channels设置为channesl={2,0,1};这样calcHist函数计算时就按照这个顺序来统计直方图。

图像的直方图计算如下:

/*计算hsv图像的直方图*/
#include <iostream>
#include "opencv2/core/core.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"

using namespace std;
using namespace cv;

class CalcHistogram
{
private:
    int histSize[3];         //直方图项的数量
    float hranges[2];        //h通道像素的最小和最大值
    float sranges[2];
    float vranges[2];
    const float *ranges[3];  //各通道的范围
    int channels[3];         //三个通道
    int dims;

public:
    CalcHistogram(int hbins=30, int sbins=32, int vbins=32)
    {
        histSize[0]=hbins; 
        histSize[1]=sbins;
        histSize[2]=vbins;
        hranges[0]=0; hranges[1]=180;
        sranges[0]=0; sranges[1]=256;
        vranges[0]=0; vranges[1]=256;
        ranges[0]=hranges;
        ranges[1]=sranges;
        ranges[2]=vranges;
        channels[0]=0;
        channels[1]=1;
        channels[2]=2;
        dims=3;
    }

    Mat getHistogram(const Mat &image);
    void getHistogramImage(const Mat &image);
};

Mat CalcHistogram::getHistogram(const Mat &image)
{
    Mat hist;
    calcHist(&image,
        1,
        channels,
        Mat(),
        hist,
        dims,
        histSize,
        ranges,
        true,      //直方图每一维的histSize是均匀的
        false
        );

    return hist;
}

void CalcHistogram::getHistogramImage(const Mat &image)
{
    Mat hist=getHistogram(image);
    int scale = 4;
    int hbins=histSize[0];
    int sbins=histSize[1];
    int vbins=histSize[2]; 
    float *hist_sta = new float[sbins];  
    float *hist_val = new float[vbins];  
    float *hist_hue = new float[hbins];  
    memset(hist_val, 0, vbins*sizeof(float));  
    memset(hist_sta, 0, sbins*sizeof(float));  
    memset(hist_hue, 0, hbins*sizeof(float));

    for( int s = 0; s < sbins; s++ )  
    {  
        for( int v = 0; v < vbins; v++ )  
        {  
            for(int h=0; h<hbins; h++)  
            {
                float binVal = hist.at<float>(h, s, v);  
                hist_hue[h] += binVal;
                hist_val[v] += binVal;  
                hist_sta[s] += binVal;
            }  
        }  
    }  

    double max_sta=0, max_val=0,max_hue=0;  
    for(int i=0; i<sbins; ++i)  
    {  
        if(hist_sta[i]>max_sta)  
            max_sta = hist_sta[i];  
    }  
    for(int i=0; i<vbins; ++i)  
    {  
        if(hist_val[i]>max_val)  
            max_val = hist_val[i];  
    }  
    for(int i=0; i<hbins; ++i)  
    {  
        if(hist_hue[i]>max_hue)  
            max_hue = hist_hue[i];  
    }  

    Mat sta_img = Mat::zeros(240, sbins*scale+20, CV_8UC3);  
    Mat val_img = Mat::zeros(240, vbins*scale+20, CV_8UC3);  
    Mat hue_img = Mat::zeros(240, hbins*scale+20, CV_8UC3);  

    for(int i=0; i<sbins; ++i)  
    {  
        int intensity = cvRound(hist_sta[i]*(sta_img.rows-10)/max_sta);  
        rectangle(sta_img, Point(i*scale+10, sta_img.rows-intensity),Point((i+1)*scale-1+10, sta_img.rows-1), Scalar(0,255,0), 1);  
    }  
    for(int i=0; i<vbins; ++i)  
    {  
        int intensity = cvRound(hist_val[i]*(val_img.rows-10)/max_val);  
        rectangle(val_img, Point(i*scale+10, val_img.rows-intensity),Point((i+1)*scale-1+10, val_img.rows-1), Scalar(0,0,255), 1);  
    }  
    for(int i=0; i<hbins; ++i)  
    {  
        int intensity = cvRound(hist_hue[i]*(hue_img.rows-10)/max_hue);  
        rectangle(hue_img, Point(i*scale+10, hue_img.rows-intensity),Point((i+1)*scale-1+10, hue_img.rows-1), Scalar(255,0,0), 1);  
    } 

    imshow("Shist", sta_img);    
    imshow("Vhist", val_img);        
    imshow("Hhist", hue_img); 

    delete[] hist_sta;
    delete[] hist_val;
    delete[] hist_hue;
}

int main()
{
    Mat src=imread("test.jpg"),hsv;
    if(!src.data)
    {
        cout<<"error, the image is not built!"<<endl;
        return -1;
    }
    cvtColor(src, hsv, CV_BGR2HSV);
    imshow("src", src);
    imshow("hsv", hsv);
    CalcHistogram h;
    h.getHistogram(hsv);
    h.getHistogramImage(hsv);
    waitKey();
    return 0;
}

代码运行结果:
这里写图片描述
hsv图像各通道的颜色分布直方图:
这里写图片描述

代码总结:
1)由于参考了官方opencvdoc的例程,用到了函数minMaxLoc(hist, 0, &maxVal, 0, 0),但该函数只能求二维数组的最大最小值;
2)调用calcHist函数时,参数channels为0时,参数narrays和dims要相等。
3)若channels不为0时,dims最大不超过channels的个数,当dims小于channels的个数时,参与直方图计算的是channels所有通道还是其中的前dims个,略有疑问。

3、直方图均衡化

从直方图的计算过程中,我们可以体会到直方图的本质其实就是单通道图像的特征统计表示,所以将图像的颜色分布直方图称为灰度直方图也不是没有道理的。同样,直方图均衡化也称为灰度均衡化,其主要想法是:通过某种灰度映射使输入图像转换为每一灰度级上都有近似相同的像素点数的图像,即输出的直方图是均匀的。经过均衡化处理后的图像中,像素将占有尽可能多的灰度级并且分布均匀,使得图像具有较高的对比度,感官上理解就是使图像更加清晰。

直方图均衡化主要用于增强动态范围偏小的图像对比度,丰富图像的灰度级,如:我们在基于Nao机器人的名片邮箱识别中,由于名片的灰度范围过小,所以我们可以在前期的图像预处理阶段使用直方图均衡化增强对比度。直方图均衡化的优点是操作简便。

计算原理:理想情况下,一个完全均衡的直方图中所有的灰度级都有相同数量的像素,即50%的像素的强度低于128,25%的像素强度低于64,归纳地,我们有p%的像素的强度值必须低于或等于255*p%,换个角度就是:强度为 i 的映射值应对应强度值小于 或等于i 的像素所占的比例。则我们通过如下方式构建查找表修改像素就能得到均衡化直方图:

lookup.at<uchar>(i) = static_cast<uchar>(255.0*p[i]); //p[i]是强度值小于 i 的像素所占的比例

为了更加严谨,还是数学证明一下吧[好吧,不证明不舒服斯基]。

简便起见,先考虑灰度范围为0~1且连续情况,此时图像的归一化直方图即为概率密度函数:

p(x),0x1

则:
1x=0p(x)dx

设转换前后的概率密度函数分别为 pr(r),ps(s) 转换函数为 s=f(r)
则:
ps(s)=pr(r)drds

要使转换后图像直方图是均匀的,即 PS(S)=1,0S1 ,则必须有:
pr(r)=dsdr

等式两边积分,得累计分布函数:
s=f(r)=r0pr(u)du

对于[0, 255]的情形,只要乘以 Dmax 即可。即
DB=f(DA)=DmaxDA0PDA(u)du

其中 DB,DA 分别为转换前后的灰度值。
离散形式为:
DB=f(DA)=DmaxA0i=0DAHi

其中, Hi 为第i级离散灰度的像素个数, A0 为图像的面积,即像素总数。

由于离散情形下无法像连续变换得到严格的均匀概率密度函数,所以均衡化的直方图是近似均匀的。OpenCV中直方图均衡化的API函数为:

void equalizeHist(InputArray src, OutputArray dst)

待续

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值