专栏首页行走的机械人【opencv】带你再学一遍直方图

【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;
}

上面函数实现思想:

  1. 遍历整幅图像的像素点,统计灰度值0-256的像素点个数并存到数组img_num[]中
  2. 遍历这个img_num[]数组,对灰度值进行归一化,计算出的高度为各灰度值所占的比值
  3. 用画直线函数进行绘制

最后运行程序,所画直方图为:

可以看到右下角红色的为直方图的柱形。

因为不明显,所以我们将上面程序第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);
}

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

本文分享自微信公众号 - Opencv视觉实践(gh_31e12b1be0e0),作者:周旋

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-03-07

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 给彦女王生成一副蒙太奇画像

    大家好呀,前两天烈阳天道1上映了,不知道大家看没看呢,里面还有一小段彦穿越虫洞与猴哥相遇的画面,彦女王啊啊啊~~

    周旋
  • 唉,再再再学一下直方图:直方图反投影

    之前写过一篇【opencv】带你再学一遍直方图,里面的内容可以看下图。所以今天还要再再再学一个直方图的API:直方图反投影。

    周旋
  • 二寸照片识别/切边/矫正

    大体思路可以描述为Canny边缘检测-形态学闭操作-轮廓检测-Hough直线检测-确定四个角点-透视变换。

    周旋
  • P2782 友好城市

    题目背景 无 题目描述 有一条横贯东西的大河,河有笔直的南北两岸,岸上各有位置各不相同的N个城市。北岸的每个城市有且仅有一个友好城市在南岸,而且不同城市的友好城...

    attack
  • 5709 01背包

    5709 01背包  时间限制: 1 s  空间限制: 128000 KB  题目等级 : 黄金 Gold 题解  查看运行结果 题目描述 Descriptio...

    attack
  • PAT 甲级 1001 A+B Format

    1001. A+B Format (20) 时间限制 400 ms 内存限制 65536 kB 代码长度限制 16000 B 判题程...

    ShenduCC
  • 2015年javaB组1-4题解析与理解

    X老板脾气古怪,他们公司的电话分机号都是3位数,老板规定,所有号码必须是降序排列,且不能有重复的数位。比如:

    萌萌哒的瓤瓤
  • 第八届蓝桥杯省赛javaB组题目解析

    作者自己做完之后发现省赛的一幕其实是不难的,说实话,自己觉得题目难度还没有PAT甲级的难度高。 而且作者做了这么些天之后发现了,PAT甲级主要喜欢考数据结构方...

    萌萌哒的瓤瓤
  • SwipeLayout一个展示条目底层菜单的侧滑控件

    由于项目上的需要侧滑条目展示收藏按钮,记得之前代码家有写过一个厉害的开源控件 AndroidSwipeLayout 本来准备直接拿来使用,但是看过 issu...

    夏洛克的猫
  • 6.listview显示不用条目

    六月的雨

扫码关注云+社区

领取腾讯云代金券