【OpenCV C++20 学习笔记】直方图计算-split, calcHist, normalize

ops/2024/10/19 6:25:17/

直方图计算-split, calcHist, normalize

  • 广义直方图
  • 示例
    • 目标
    • 分离通道
    • 计算直方图
    • 绘制计算结果
      • 归一化
      • 绘制
    • 最终结果

广义直方图

直方图的横坐标除了可以是图片中的强度值,也可以是任何其他我们想要观察的特征。例如,下面的图片矩阵中包含了0-255的强度值:
图片矩阵
如果想观察每个宽度为16的强度值区间上的频数分布,我们就可以将横坐标分成下面的区间:
[ 0 , 255 ] = [ 0 , 15 ] ∪ [ 16 , 31 ] ∪ . . . . . ∪ [ 240 , 255 ] r a n g e = b i n 1 ∪ b i n 2 ∪ . . . . . ∪ b i n n = 15 [0, 255] = [0, 15] \cup [16, 31] \cup ..... \cup [240, 255] \\ range = bin_1 \cup bin_2 \cup ..... \cup bin_{n=15} [0,255]=[0,15][16,31].....[240,255]range=bin1bin2.....binn=15
这样就可以得到类似于下图的直方图:
区间直方图
直方图中的元素的定义如下:

  1. 维数(dims):即想要观察的参数的数量,比如上例中只观察灰度图中每个像素的强度值,因此dims = 1
  2. 组数(bins):每个维度中的数据被分组的数量,比如上例中分了16组区间,所以bins = 16
  3. 全距(range):被观察的数据的总区间,比如上例中range = [0, 255]

如果你相观察的参数不止一个,比如说2个,即dims = 2,那就需要画一个3维的图了。

示例

目标

  1. 导入图片
  2. 分离通道:用split函数将图片分离为R, G, B3个矩阵数据
  3. 计算直方图:用calcHist函数对分离出来的3个矩阵分别计算直方图
  4. 绘制计算结果

分离通道

split函数,其原型如下:

void cv::split(	const Mat& src,Mat*		mvbegin)

该函数将多通道的矩阵数组分成多个单通道的矩阵数组,其中:

  • src为要进行通道分离的原矩阵
  • mvbegin为接收分离结果的数组的指针,该数组的长度要和原矩阵的通道数相同

该函数还有以下更便利的重载版本(第2个参数不再是指针,而是多维数组):

void cv::split(	InputArray			m,OutputArrayOfArrays	mv)

在本例中的应用如下:

Mat src{ imread("lena.jpg") };	//导入图片vector<Mat> bgr_planes;	//接收通道分离结果的向量
split(src, bgr_planes);	//通道分离之后,bgr_planes中的3个元素分别是b,g, r,3个通道的数据矩阵

原图是 512 × 512 512 \times 512 512×512的3通道矩阵,在VS调试中,可以看到分离出来的结果bgr_planes中分别有3个元素,而每个元素也是 512 × 512 512 \times 512 512×512的矩阵。
通道分离结果
至此,通道分离完成;接下来对每个通道进行直方图计算。

计算直方图

计算直方图用的函数是calcHist,该函数有3个版本,这里选择比较常用的一个版本,其原型如下:

void cv::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 )	
  • imagesconst Mat*类型,可以是一个图片矩阵的指针,也可以是图片矩阵的数组;代表需要计算直方图的图片
  • nimages:第1个参数中包含的图片数量
  • channelsconst int*类型,可以是一个整数常量,也可以是整数数组,代表对应图片中需要进行计算的通道索引,从0开始。如果数组的话,也就是说有多张图片,且每张图片中需要有多个通道被计算,那么这个参数可能遵循以下写法:
    [ i m a g e s [ 0 ] . c h a n n e l s ( ) − 1 , i m a g e s [ 0 ] . c h a n n e l s ( ) , i m a g e s [ 0 ] . c h a n n e l s ( ) + i m a g e s [ 1 ] . c h a n n e l s ( ) − 1 , . . . , ∑ i m a g e s [ n ] . c h a n n e l s ( ) ] [images[0].channels()-1, images[0].channels(), \\ images[0].channels()+images[1].channels()-1, ... , \sum images[n].channels() ] [images[0].channels()1,images[0].channels(),images[0].channels()+images[1].channels()1,...,images[n].channels()]
  • mask:可以不指定,如果指定则其中的矩阵必须是与对应图片具有相同尺寸且是8位的数据类型,从而给图片提供了一个掩码
  • hist:输出结果
  • dims:直方图的维数,必须是正数,且不能超过32
  • histSize:直方图每个维度的组数
  • rangesconst float **类型,所以必须是一个数组的住宿。代表直方图每个维度的全距;如果是均匀分布的直方图,那每个维度只需要提供最大值和最小值就行了,即 m i n , m a x {min, max} minmax,注意是左闭右开的区间;如果不是均匀分布的直方图,则需要提供每组的最小值及最后一组的最大值,即KaTeX parse error: Expected '}', got 'EOF' at end of input: …{histSize[i]-1}
  • uniform:是否为均匀分布
  • accumulate:是否允许覆盖,即不清除之前的直方图

该函数也有其他重载版本,参数即原理基本与上述版本相同,这里就不赘述了。
乍一看这个函数非常复杂,事实也确实如此。但是在本例中,因为我们将一个3通道的矩阵分离成3个单通道的矩阵,然后分别对它们进行计算,所以事情就变得相对简单了。我们直接看代码和注释吧:

int histSize{ 256 };	//定义直方图中的组数为256,即每个强度值一组//定义直方图中的全距
float range[]{ 0, 256 };			//表示全距的区间,左闭右开
const float* histRange[]{ range };	//由于直方图只有一个维度,所以数组只有一个元素bool uniform{ true };		//均匀分布
bool accumulate{ false };	//不允许覆盖Mat b_hist, g_hist, r_hist;	//接收计算结果的矩阵
//b通道的直方图计算
calcHist(&bgr_planes[0],	//b通道矩阵,因为形参是指针类型,所以要加取址符&1,						//只有b通道一个矩阵,相当于只有一张图片,所以nimages = 10,						//矩阵中只有一个通道,所以只有一个通道索引,且从0开始,channels = 0Mat(),					//空矩阵代表不使用掩码b_hist,					//接收计算结果的矩阵1,						//直方图只有一个维度,即b的强度值的频数分布,dims = 1&histSize,				//直方图的组数,因为是指针类型,所以要加取址符&histRange,				//直方图的全距,因为这里只有1个维度、1个矩阵,所以该数组包含一个区间uniform,				//均匀分布accumulate);			//不允许覆盖
//g通道的直方图计算
calcHist(&bgr_planes[1], 1, 0, Mat(), g_hist, 1, &histSize, histRange, uniform, accumulate);
//r通道的直方图计算
calcHist(&bgr_planes[2], 1, 0, Mat(), r_hist, 1, &histSize, histRange, uniform, accumulate);

对于单通道的矩阵来说,很多需要传入数组的形参,只要传入字面量就行了,所以简化了很多。
在Image watch中查看计算结果:
直方图计算结果
可以看到每个通道的计算结果都是 1 × 256 1 \times 256 1×256的矩阵,代表原图中每个通道上从0到255这256个强度值的频数。

绘制计算结果

归一化

在绘制直方图的之前需要对数据进行归一化,从而使数据的值域能够适应直方图尺寸。这就要用到normalize函数,其原型如下:

void cv::normalize(	InputArray			src,InputOutputArray	dst,double				alpha = 1,double				beta = 0,int					norm_type = NORM_L2,int					dtype = -1,InputArray			maxk = noArray())
  • alpha:值域归一化中的值域最小值
  • beta:值域归一化中的值域最大值
  • norm_type:归一化类型
  • dtype:输出矩阵的数据类型,默认为-1,即与原矩阵保持一致
  • mask:掩码矩阵(可选)

这里我们定义的直方图的尺寸是 512 × 400 512 \times 400 512×400,而直方图的计算结果肯定会有超出512的数值,所以必须进行归一化处理:

int hist_w{ 512 }, hist_h{ 400 };	//直方图的长和宽Mat histImage(hist_h, hist_w, CV_8UC3, Scalar(0, 0, 0));	//用来绘制直方图的图片//对直方图计算结果进行归一化处理
normalize(b_hist,	b_hist,0,				//归一化之后值域的最小值 alpha = 0histImage.rows,	//归一化之后值域的最大值 alpha = 400NORM_MINMAX,	//归一化类型-1,				//输出结果类型与原矩阵一致Mat());			//空矩阵代表不是用掩码
normalize(g_hist, g_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat());
normalize(r_hist, r_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat());

归一化之后,3个通道的计算结果的值域都在0到400之间。

绘制

绘制直方图的基本思路是将每个通道中的计算结果(频数结果)转换成点的坐标(横坐标为强度值,纵坐标为结果值,即频数),然后将每个点和前一个点进行连线,最后组成一条完整的折线。具体实现方法如下:

int bin_w{ cvRound(static_cast<double>(hist_w / histSize)) };	//每组的宽度,即组距
for (int i{ 1 }; i < histSize; i++) {line(histImage,//前一个点Point(bin_w * (i - 1), hist_h - cvRound(b_hist.at<float>(i - 1))),//当前点(注意,原点在图的左上角)Point(bin_w * (i),	//组距X当前索引=当前点的横坐标hist_h - cvRound(b_hist.at<float>(i))),	//图的高度-当前的频数值=当前点的纵坐标Scalar(255, 0, 0), 2, 8, 0);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), 2, 8, 0);line(histImage, Point(bin_w * (i - 1), hist_h - cvRound(r_hist.at<float>(i - 1))),Point(bin_w * (i), hist_h - cvRound(r_hist.at<float>(i))),Scalar(0, 0, 255), 2, 8, 0);
}

最终结果

计算结果绘制
右边就是左图的直方图计算结果。横坐标是0-255的每个强度值,纵坐标分别为R, G, B3个通道的强度值频数。


http://www.ppmy.cn/ops/94296.html

相关文章

Jmeter接口测试+压力测试

接口测试 Jmeter-http接口脚本 一般分五个步骤:&#xff08;1&#xff09;添加线程组 &#xff08;2&#xff09;添加http请求 &#xff08;3&#xff09;在http请求中写入接入url、路径、请求方式和参数 &#xff08;4&#xff09;添加查看结果树 &#xff08;5&#xff09;…

Highcharts 条形图:数据可视化的利器

Highcharts 条形图:数据可视化的利器 引言 在数据分析和可视化领域,Highcharts 是一个广受欢迎的 JavaScript 图表库。它以其易用性、灵活性和丰富的图表类型而著称。其中,条形图作为一种基础但功能强大的图表类型,被广泛应用于各种场景,以直观地展示数据分布和比较。本…

dockerfile

dockerfile 自定义镜像--------通过dockerfile创建镜像 创建镜像的方式 1、dockerfile最基本的方式&#xff0c;也是最常用的方式 2、docker pull 拉取的是最基础的镜像&#xff0c;只有基础功能&#xff0c;没有定制化的功能。 3、基于基础镜像&#xff0c;创建好了容器之…

vue 3d echarts scatter3D元素塌陷,图标塌陷进地图完美解决方案

当我们手机用 scatter3D 类型时&#xff0c;最小值因为渲染问题会塌陷进模型里面&#xff0c;所以只要让value固定&#xff0c;再将label formatter 配合 boxHeight属性即可解决&#xff0c;&#xff08;代码附带自定义label图标解决办法&#xff09; 解决&#xff1a; <…

Docker日志管理

一、知识点介绍 1.ELK(Elasticserach、Logstash、Kibana) 前面笔记有 2.什么是 Filebeat Filebeat 是 ELK 组件的新成员&#xff0c; 也是 Beat 成员之一。基于 Go 语言开发&#xff0c;无任何依赖并且比 Logstash 更加轻量&#xff0c;不会带来过高的资源占用&#xff0c; …

Mysql-窗口函数二

文章目录 1. 前百分之N的问题 排名 row_number1.1 需求1.2 准备工作1.3 分析1.4 实现 2. 前百分之N的问题 ntile2.1 介绍2.2 语法2.2.1 示例2.2.2 结果示例2.2.3 注意事项 2.3 需求2.4 分析2.5 实现 3. 前百分之N的问题 百分比 PERCENT_RANK3.1 语法3.1.1 示例3.1.2 注意事项 3…

QT: QVerticalLayout 如何根据 index 获得对应的 widget?

在Qt中&#xff0c;QVBoxLayout&#xff08;或者更一般地&#xff0c;QLayout类及其子类&#xff09;并没有直接提供通过索引来访问布局中widgets的API。这是因为QLayout主要是负责管理widgets的排列和大小调整&#xff0c;而不直接存储widgets的列表。widgets的添加和管理是通…

ClickHouse 24.7 版本发布说明

本文字数&#xff1a;8563&#xff1b;估计阅读时间&#xff1a;22 分钟 作者&#xff1a;ClickHouse Team 本文在公众号【ClickHouseInc】首发 时间&#xff01;又一个月过去了&#xff0c;这意味着又到了发布新版本的时间&#xff01; 发布概要 本次ClickHouse 24.7 版本包含…