【opencv实践】带你再学一遍直方图

今天给大家总结下直方图的知识,争取一文帮你搞定直方图。
本文篇幅有点长,给大家列个目录,大家可以跳着看:

  1. 直方图介绍
  2. 使用opencv自带绘制直方图的函数绘制直方图
  3. 自己定义函数进行直方图绘制
  4. 直方图均衡化简介
  5. 直方图均衡化自定义函数的实现

1:直方图介绍

直方图到底可以干什么呢?我觉得最明显的作用就是有利于你对这个图像进行分析了,直方图就像我们常用的统计图,只不过直方图统计的是图片的一些特征,例如像素值(这是最常用的了)。
因此我们在开始前,先列个统计图的例子,来帮助大家理解,也有利于我解释一些概念:
在这里插入图片描述
我们统计了一个有11个学生的班级的身高和体重情况,身高为160cm的有5人,170cm的有4人,180cm的有2人。然后看体重,体重160斤的有3人,170斤的有5人,180斤的有3人。

嘿嘿,有点怪异是不是,奈何我用ppt导入统计图实在不是很会,就这样吧

举完例子,就开始学习吧,我觉得搞懂直方图真的很有必要,所以你要静下心来好好看下面的内容啦。
我们常规的统计图,往往需要x轴,y轴,组距,统计对象等等,直方图也一样,有三个术语:

  • dims:需要统计的特征的数目。如上面例子里有身高和体重两个特征。
  • bins:每个特征空间子区段的数目,可以翻译为“直条”和“组距”。

统计一个班级的身高和体重,身高就是一个特征区间,身高有160,170,180三个段位,那么子区段数目就是三。

  • range:每个特征空间的取值范围。例如:range = [0,255]。

上例中身高的取值范围就是[160,180]

2:使用opencv自带绘制直方图的函数绘制直方图

opencv提供了计算直方图的函数calcHist(),函数原型:

    calcHist(
        const Mat*   images,    //输入数组
        int          nimages,   //输入数组个数
        const int*   channels,  //通道索引
        InputArray   mask;      //Mat(),  //不使用腌膜
        OutputArray  hist,      //输出的目标直方图,一个二维数组
        int       dims,      //需要计算的直方图的维度  例如:灰度,R,G,B,H,S,V等数据
        congst int*  histSize,   //存放每个维度的直方图尺寸的数组
        const float**    ranges, //每一维数组的取值范围数组
        bool          uniform=true,   
        bool          accumulate = false
      );

为什么直方图要计算呢?其实这个函数执行的就是统计的功能,比如我们统计灰度图(灰度值为[0,255])的各个灰度值的像素点个数,我们不能自己数吧?这个函数就可以返回一个二维数组告诉我们。

下面我们用这个函数画一幅直方图(我将代码拆开讲,但大家直接顺次复制就可以了):

#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main()
{
  //【1】读取原图并显示
  Mat srcImage = imread("5.jpg", 0);
  imshow("原图:", srcImage);
  if (!srcImage.data) {
    cout << "fail to load image" << endl;
    return 0;
  }

首先,上面的是开头,将需要计算的图片载入并显示,原图如下图:
在这里插入图片描述
但我们载入时

Mat srcImage = imread(5.jpg”, 0);

也就是按灰度图载入的,所以显示出来为:
在这里插入图片描述

//【2】定义变量
  MatND dstHist;   
  int dims = 1;  //特征数目(直方图维度)
  float hranges[] = { 0,255 }; //特征空间的取值范围
  const float *ranges[] = { hranges };
  int size = 256;  //存放每个维度的直方图的尺寸的数组
  int channels = 0;  //通道数

然后就需要定义变量了,MatND为多维,多通道的密集数组类型
dims为特征数目,此程序只计算该图片的一个特征,且图片是一张灰度图,由后面的int channals = 0我们可以看出,计算的是该图片的通道0,也就是灰度的直方图。
hranges[]为特征空间的取值范围数组,为0-255;有几个特征就需要定义几个这样的数组,然后将这些数组存到
const float *ranges[] = { hranges }中。

当我们需要统计的直方图包含多个特征空间时,这么做的意义就很明显了,不如我计算一幅彩色图RGB三个通道的直方图,就需要有三个hranges[],然后将这三个放到const
float *ranges[]中,并传给直方图计算函数calcHist()

size为存放每个维度的直方图的尺寸的数组。因为我们只统计灰度,所以用一个int也可以。

    //【3】计算直方图
  calcHist(&srcImage, 1, &channels, Mat(), dstHist, dims, &size, ranges);
  int scale = 1;
  cout << dstHist << endl;

然后我们计算直方图,并将结果传递给了dstHist,我们可以输出看一下我们计算出来的直方图到底是啥?(下图只是截取了一小段):
在这里插入图片描述
我们可以看到输出的是一个n行(其实应该是256行,因为我们的灰度值是0-255)1列的数组,每一行代表图像中在该灰度的像素点个数。
但很明显这样的输出是不直观的,所以我们要将直方图进行绘制(也就是可视化):

  Mat dstImage(size * scale, size, CV_8U, Scalar(0));
  //【4】获取最大值和最小值
  double minValue = 0;
  double maxValue = 0;
  minMaxLoc(dstHist, &minValue, &maxValue, 0, 0);
//【5】绘制直方图
  int hpt = saturate_cast<int>(0.9*size);
  for (int i = 0; i < 256; i++)
  {
    float binValue = dstHist.at<float>(i);
    int realValue = saturate_cast<int>(binValue*hpt / maxValue);
    rectangle(dstImage, Point(i*scale, size - 1), Point((i + 1)*scale - 1, size - realValue), Scalar(255));
  }
  imshow("一维直方图", dstImage);
  waitKey(0);
  return 0;
}

首先我们定义了一个画布dstImage,我们就在它上面画直方图。
我们用第五行的
minMaxLoc(dstHist, &minValue, &maxValue, 0, 0);
返回了数组dstHist中的最大值和最小值。

为什么需要最大值和最小值呢?回想下我们画统计图时,是不是需要先知道人数最多的那个和最少的那个,然后才知道如何分派纸的空间。

然后变开始绘制,先进行读取数值,然后对数值进行归一化,然后用画矩形的函数将柱形图画出来。

rectangle(
    img,  //输入图像
    pt1,  //矩阵的一个定点
    pt2,  //矩阵对角线上另一个顶点
    color, //线条颜色(RGB)或亮度(灰度图像)(grayscale image)
    thickness,  //组成矩形的线条的粗细程度。取负值时函数绘制填充了色彩的矩形
    line_type,  //线条的类型  
    shift  //坐标点的小数点位数
    );

上面程序第8行为

int hpt = saturate_cast<int>(0.9*size);

感觉0.9出现的很突然,这一句其实是可以调整直方图绘制的大小的,看了下面截图应该就明白了:
在这里插入图片描述
当:

int hpt = saturate_cast(0.5*size); 时:

在这里插入图片描述
这下应该很清楚明白了吧?
但到目前为止我们仅会用了一个函数而已,如果你没有耐心了,可以先退出并收藏,或者关注公众号【行走的机械人】。

3:自己定义函数进行直方图绘制

然后我们自己来实现一个函数来进行一维直方图的绘制。
我们来统计这幅图的灰度图的灰度直方图。
在这里插入图片描述
首先看主函数:

int main(void)
{
  Mat img = imread("4.jpg",0);  //读取图片
  if (img.empty())   //判断图片是否为空
  {
    cout << "图片为空";
    return -1;
  }
  imshow("灰度图", img); //展示灰度图
  int img_num[256] = { 0 };  //定义一个存放统计数据的数组
  Mat histogram; //定义直方图
  histogram = histogram_draw(img, img_num);
  imshow("直方图", histogram);
  waitKey(0);
}

主函数就很简单啦,其中我们用了我们自定义的画直方图的函数
histogram_draw( )。
然后我们看自定义函数:

//@img:需要计算的图像
//@img_num[]:计算直方图的特征空间子区段的数目
Mat histogram_draw(Mat img, int *img_num)
{
  int r = 200; //定义高
  int w = 1000; //定义宽
  Mat histogram = Mat(r, w, CV_8UC3); //直方图画布
  int row = img.rows;  //图片的高度
  int col = img.cols;  //图片的宽度
  for (int i = 0; i < row; i++)
  {
    for (int j = 0; j < col; j++)
    {
      int num = img.at<uchar>(i, j); //读取图片像素位置(i,j)处的灰度值
      img_num[num]++;  //将对应灰度值的个数加一
    }
  }
  int all = row * col;
  for (int i = 0; i < 256; i++)  //对灰度值0-255循环处理
  {
    int hight = int(double(img_num[i])  / double(all)*r); //对灰度值i进行归一化
    //opencv图像的像素坐标系原点在左上角
    Point ps(i * 4, r);   
    Point pe(i * 4, r - hight);
    line(histogram, pe, ps, Scalar(0, 0, 255));
  }
  return histogram;
}

上面函数实现思想:
遍历整幅图像的像素点,统计灰度值0-256的像素点个数并存到数组img_num[]中
遍历这个img_num[]数组,对灰度值进行归一化,计算出的高度为各灰度值所占的比值
用画直线函数进行绘制
最后运行程序,所画直方图为:
在这里插入图片描述
可以看到右下角红色的为直方图的柱形。
因为不明显,所以我们将上面程序第23行归一化后的高再乘100来扩大,就可以明了的看出各灰度值所占的比例了。
在这里插入图片描述
好了!到此我们已经会画直方图了,如果你没有耐心了,可以先退出收藏,或者关注【行走的机械人】不迷路哦。

4:直方图均衡化简介

下面我们来说说直方图均衡化,这是图像处理的一大利器哦。
在这里插入图片描述
我们可以看到上面图片灰蒙蒙的能见度很低,有没有方法给它处理一下,来使细节更明显呢?当然有了,就是直方图均衡化。
opencv给了一个内置函数equalizeHist来帮助我们完成直方图均衡化,这是个无脑函数,有两个输入,一个是原图像,另一个就是与原图像同大小的输出图像。我们先看看用该函数均衡化后的结果:
在这里插入图片描述
可以看到,细节要多很多了。我们用上面的画直方图函数来看看均衡化后直方图:
在这里插入图片描述
可以看到灰度值的分布要更为均匀了,这就使均衡化的图像对比度更为明显。细节也就更为凸显了。

那直方图均衡化的实现原理呢?我推荐大家看冈萨雷斯的《数字图像处理》第三章,讲的很细致。本人能力有限,在这里我只能给大家照本宣科的简单介绍一下了,大家可以关注我公众号【行走的机械人】回复【电子书资源】,里面有这本书的电子版(还要其他近10G的我搜集的各种电子书)。

在原图直方图中,灰度值大部分之中在一小段区域,而其他部分都是空白的,我们要做的就是将这一小段区域展开到整个灰度范围内(如上图)。
如何展开到整个区域呢?我们可以制作一个映射表,将原本集中在一起的像素值映射到整幅图中。
那映射的依据呢?比如我们原来有个像素点的灰度为240,我们凭什么把它映射为灰度120呢?靠一个数学公式:
在这里插入图片描述
r0是我们图像某个像素点的灰度值,T(r0)就是映射函数,S0就是映射后的灰度值。上式中我们r0本来为0,映射后为1.33。
再看一个:
在这里插入图片描述
上式就是灰度为r1的像素点,r1=1,经过映射后S1为3.08。
这样看来,我们的目的是不是就达到了?
在深入看一下T()这个映射函数,它映射的算法是计算对应灰度的概率乘灰度的累加,还乘了个7,乘7是因为我们只有(7+1)个灰度值。
我们从整体上来看一下:
在这里插入图片描述
我们以一幅图的七个像素点来看,像素点的灰度值分布本来为:
在这里插入图片描述
经过映射函数T()之后灰度值:
在这里插入图片描述
再看一下分布:
在这里插入图片描述
是不是更均匀了呢?
如果你明白一些原理了,那就继续看下面的代码吧,如果没有,那肯定是我讲的水平有限,你只能再去看我上面推荐的《数字图像处理》这本书了。

5:直方图均衡化自定义函数的实现

我们要做的是希望实现上面的函数T(),然后将函数T映射出来的新的灰度值存到数组中,并将原图像中的灰度值进行替换。
把代码放下面了,我都详细注释了,我就不讲了,挺简单的,越说越乱不如大家自己看看。


#include <iostream>
#include <opencv2/opencv.hpp>
#include <string.h>
using namespace std;
using namespace cv;
//@img:输入灰度图
//@int *img_num:定义一个存放统计数据的数组
//@double* ratio:存放各个灰度所占比例的数组
//@int* map_num:映射数组
Mat junheng(Mat img, int *img_num, double* ratio, int* map_num)
{
  Mat map = img;  
  double gailv = 0.0;
  int row = img.rows; //获取原图的高和宽
  int col = img.cols;
  int all = row * col; //计算总像素点数
  for (int i = 0; i < row; i++)   //统计灰度值个数
  {
    for (int j = 0; j < col; j++)
    {
      int num = img.at<uchar>(i, j); //读取图片像素位置(i,j)处的灰度值
      img_num[num]++;  //将对应灰度值的个数加一
    }
  }
  for (int i = 0; i < 256; i++)  //计算灰度值概率
  {
    ratio[i] = double(img_num[i]) / double(all); //将概率存到数组中
  }
  for (int i = 0; i < 256; i++)  //设置映射数组
  {
    gailv += ratio[i];  //累计概率
    map_num[i] = int(gailv * 255 + 0.5);  //加0.5起到四舍五入的作用
  }
  for (int i = 0; i < row; i++)   //进行灰度值的映射(替换)
  {
    for (int j = 0; j < col; j++)
    {
      int num = img.at<uchar>(i, j);
      map.at<uchar>(i, j) = map_num[num];
    }
  }
  return map;  //返回均衡完毕的图像
}
int main(void)
{
  Mat img_copy,img = imread("4.jpg",32);  //读取图片
  img.copyTo(img_copy);
  if (img.empty())   //判断图片是否为空
  {
    cout << "图片为空";
    return -1;
  }
  imshow("灰度图", img); //展示灰度图
  double ratio[256] = { 0 };  //存放各个灰度所占比例的数组
  int map_num[256] = { 0 };   //映射数组
  int img_num[256] = { 0 };  //定义一个存放统计数据的数组
  img_copy=junheng(img, img_num,ratio, map_num);  //均衡化
  imshow("均衡化", img_copy);
  waitKey(0);
}

直方图我们就到这里啦,除了上面说的,直方图还有很多其他的东西,比如直方图匹配,直方图规定化等等,因为篇幅就不介绍了,还是推荐大家去看看《数字图像处理》这本书。

有疑问或者有错的地方,欢迎大家评论哦~

作者简介

最后,欢迎大家关注我的微信公众号【行走的机械人】,我会在上面更新视觉以及深度学习的知识哦,一起来学习吧!
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_43667130/article/details/104713024