文章目录
- 前言
- 一、直方图为什么可以进行图像处理?
- 二、直方图处理怎么实现?
- 直方图均衡化
- 直方图匹配-规定化
- 局部直方图处理
- 直方图统计量增强图像
- 三、OpenCv提供的直方图基础操作
- 直方图均衡化
- OpenCv中直方图的表示
- 从数据创建直方图:cv::calcHist()
- 显示直方图
- 直方图归一化
- 直方图二值化
- 找出最显著的区间
- 比较两个直方图
- 四、OpenCv提供的一些复杂的直方图操作
- EMD距离
- 反向投影
- 模板匹配
- 五、 MATLAB中的直方图操作
- 计算并显示图像直方图
- 直方图归一化
- 直方图均衡化
- 正比例变换
前言
直方图操作是图像处理中的一个基本工具,直方图计算简单,适合于快速硬件的实现,基于直方图的技术成为实时图像处理的一个流行工具。
在了解直方图进行图像处理前先明确几个概念:
- 直方图容器:细分的灰度级称为直方图容器,个人理解:容器顾名思义是容纳一类事物的东西,直方图容器就是容纳灰度级的;
- 直方图或图像直方图:通常把处理的归一化直方图简单称为直方图或图像直方图
一、直方图为什么可以进行图像处理?
直方图的形状与图像的外观有关。
如下图显示了具有4个基本灰度特性的几幅图像:暗图像(左上)、亮图像(右上)、低对比度图像(左下)和高对比度图像(右下)。
下图分别是这几幅图像的归一化直方图:
从归一化直方图中我们可以得出这几个结论:
- 暗图像的直方图中,大多数直方图容器集中在灰度级的低端/暗部;
- 亮图像的直方图中,大多数直方图容器集中在灰度级的高端/亮部;
- 低对比度图像的直方图中,直方图容器基本位于灰度级的中间;对于单色图像来说,这表示图像具有灰色的外观;
- 高对比度图像的直方图中,直方图容器覆盖了较宽范围的灰度级,并且像素的分布也基本上是均匀的,直方图容器的高度基本相同。
可以简单的得出结论:像素占据整个灰度级范围并且均匀分布的图像,将具有高对比度的外观和多种灰色调。最终呈现的图像效果将是显示了大量灰度细节并具有高动态范围。
另外,直方图可以表达更多的信息,比如,物体的颜色分布、物体的边缘梯度模板或是以概率分布的形式表达当前对物体位置的估计。
直方图可以说是计算机视觉领域中的经典工具之一。
例如,通过判断帧与帧之间边缘和颜色的统计量是否出现巨大变化,来检测视频中场景的变换。
通过使用兴趣点邻域内的特征组成的直方图,来辨识兴趣点。若将边缘、颜色、角点等等的直方图作为特征,可以使用分类器来进行目标识别。
提取视频中的颜色或边缘直方图序列,可以用来判断视频是否拷贝自网络。
直方图是一种用来揭示数据分布的统计特性的工具。
二、直方图处理怎么实现?
基础的直方图处理有:直方图均衡化、直方图规定化、局部直方图处理、直方图统计量增强图像等。
直方图均衡化
具体理论基础参考《数字图像处理(第四版)》冈萨雷兹版3.3.1节P87。
直方图均衡化的计算公式:
s k = T ( r k ) = ( L − 1 ) ∑ j = 0 k p r ( r j ) = L − 1 M N ∑ j = 0 k n j s_k = T(r_k) = (L-1) \sum_{j=0}^{k} p_r(r_j) = \frac {L-1} {MN} \sum_{j=0}^{k} n_j sk=T(rk)=(L−1)j=0∑kpr(rj)=MNL−1j=0∑knj
其中 r k r_k rk是输入图像的灰度值,通常 r k = k r_k=k rk=k, k = 0 , 1 , 2 , . . . L − 1 k=0,1,2,...L-1 k=0,1,2,...L−1; s k s_k sk是 r k r_k rk对应的输出图像灰度值; L L L是图像的灰度级,比如 8 b i t 8bit 8bit图像灰度级为 255 255 255; M N MN MN是图像像素总数; n j n_j nj表示灰度值为 r k r_k rk的像素数量。
直方图均衡化视会常事产生一幅具有均匀直方图的输出图像,希望自动增强图像时,直方图均衡化是很好的选择。
直方图匹配-规定化
具体理论基础参考《数字图像处理(第四版)》冈萨雷兹版3.3.2节P92。
用于生成具有规定直方图的图像的方法,称为直方图匹配或直方图规定化。
直方图规定化需要有一幅图像,这幅图像的直方图即为规定的直方图,将输入图像经过某种转换,得到输出图像,使得输出图像的直方图与规定的直方图一致或者大致相同。
离散直方图规定化的过程总结如下:
- 计算输入图像的直方图,并将利用式将(1)输入图像中的灰度映射到直方图均衡化后的图像中的灰度,将得到的值 s k s_k sk四舍五入到整数区间 [ 0 , L − 1 ] [0, L-1] [0,L−1];
- 由规定的直方图值 p z p_z pz,用式 G ( z q ) = ( L − 1 ) ∑ i = 0 q p z ( z i ) G(z_q) = (L-1) \sum_{i=0}^{q} p_z(z_i) G(zq)=(L−1)∑i=0qpz(zi) 计算 G ( z q ) G(z_q) G(zq)的所有值,将得到的值四舍五入到区间 [ 0 , L − 1 ] [0, L-1] [0,L−1]内的整数,将四舍五入后的 G ( z q ) G(z_q) G(zq)值存储到一个查找表中;
- 找到 s k s_k sk与 G ( z q ) G(z_q) G(zq)最接近时对应的 z q z_q zq值,存储从 s s s到 z z z的映射;当多个 z q z_q zq值有相同的匹配时,按照约定选择最小值;
- 根据步骤3中找到的映射,将每个均衡化后的像素 s k s_k sk值映射到直方图规定化图像中值为 z q z_q zq的对应像素,形成直方图规定化后的图像。
如下以一个简单的示例展示了直方图均衡化和直方图规定化的原理。
上图的表格中展示了直方图均衡化以及直方图规定化过程中的计算数据;表格上下文字是对过程的描述;右上图表中展示了规定直方图与规定化实际计算的直方图,从这里可以看出实际计算的结果与规定直方图并不完全匹配,但是实现了将灰度向灰度级的高端移动的一般趋势。
具体文件链接:
链接1;
链接2 提取码:ci06;
局部直方图处理
当需要增强图像中几个小区域的细节时,通常采用基于像素邻域的灰度分布的变换函数进行局部增强。
局部直方图处理的过程大致是:定义一个邻域,并将其中心在水平方向或垂直方向上从一个像素移动到另一个像素;在每个位置,计算邻域中的各点的直方图,得到直方图均衡化或直方图规定化变换函数;这个函数用于映射邻域中心像素的灰度;然后将邻域的中心移到一个相邻像素位置,并重复这个过程。
直方图统计量增强图像
直方图统计量增强图像的主要思路是计算图像的均值和方差,根据邻域像素的均值和方差判断中心像素是否需要增强,如果需要,则根据给定的灰度变换函数增强该像素。
均值是平均灰度的测度,方差是图像对比度的测度。
使用局部均值和方差处理图像的一个重要优点是,根据这些与图像外观紧密相关的统计测度可以开发处简单且强大的增强规则;
如下是根据直方图统计量增强图像的一种方法:
-
确定某个区域在点 ( x , y ) (x,y) (x,y)处是相对较亮还是较暗的方法:将局部灰度均值 m s x y m_{s_{xy}} msxy与全局灰度均值 m G m_G mG进行比较,如果 k 0 m G ≤ m s x y ≤ k 1 m G k_0 m_G \le m_{s_{xy}} \le k_1 m_G k0mG≤msxy≤k1mG,则将 ( x , y ) (x,y) (x,y)处的像素作为待处理的候选像素;其中 k 0 k_0 k0和 k 1 k_1 k1是非负常数,且 k 0 ≤ k 1 k_0 \le k_1 k0≤k1。比如,如果关注区域是比平均灰度的 1 / 4 1/4 1/4更暗的区域,那么选择 k 0 = 0 k_0 = 0 k0=0和 k 1 = 0.25 k_1 = 0.25 k1=0.25。
-
增强低对比度的区域,确定某个区域的对比度是否使得它是一个增强的候选区域的方法:将局部灰度方差 σ s x y \sigma_{s_{xy}} σsxy与全局灰度方差 σ G \sigma _G σG比较,若 k 2 σ G ≤ σ s x y ≤ k 3 σ G k_2 \sigma _G \le \sigma_{s_{xy}} \le k_3 \sigma _ G k2σG≤σsxy≤k3σG,则考虑将 ( x , y ) (x,y) (x,y)处的像素作为候选像素;其中 k 2 k_2 k2和 k 2 k_2 k2是非负常数,且 k 2 < k 3 k_2 < k_3 k2<k3。比如,要增强对比度较低的一个暗色区域,可以选择 k 2 = 0 k_2 = 0 k2=0和 k 3 = 0.1 k_3=0.1 k3=0.1。
-
满足上面所有局部增强的候选像素按照如下方式处理:乘以一个规定常数 C C C,增大或减小其相对于图像剩下部分的灰度值,不满足增强条件的像素则保持不变;
上面方法总结为公式如下:
g ( x , y ) = { C f ( x , y ) , k 0 m G ≤ m s x y ≤ k 1 m G A N D k 2 σ G ≤ σ s x y ≤ k 3 σ G f ( x , y ) 其他 g(x,y) = \left\{\begin{matrix} Cf(x,y), & k_0 m_G \le m_{s_{xy}} \le k_1 m_G AND k_2 \sigma _G \le \sigma _{s_{xy}} \le k_3 \sigma _G \\ f(x,y) & 其他 \end{matrix}\right. g(x,y)={Cf(x,y),f(x,y)k0mG≤msxy≤k1mGANDk2σG≤σsxy≤k3σG其他
其中, x = 0 , 1 , 2 , . . . , M − 1 x=0,1,2,...,M-1 x=0,1,2,...,M−1, y = 0 , 1 , 2 , . . . N − 1 y=0,1,2,...N-1 y=0,1,2,...N−1, C 、 k 0 、 k 1 、 k 2 和 k 3 C、k_0、k_1、k_2和k_3 C、k0、k1、k2和k3是规定的常数 , , , m G m_G mG是输入图像的全局均值, σ G \sigma _G σG是输入图像的标准差,参数 m s x y m_{s_{xy}} msxy和 σ s x y \sigma _{s_{xy}} σsxy分别是局部均值和局部标准差,在每个 ( x , y ) (x,y) (x,y)位置都会变化, M M M和 N N N是输入图像中的行数和列数。
从上面的叙述中可以知道, k 0 、 k 1 、 k 2 、 k 3 、 C k_0、k_1、k_2、k_3、C k0、k1、k2、k3、C等参数的选择很重要,
三、OpenCv提供的直方图基础操作
直方图均衡化
扩大图像的动态范围以增强对比度最常用的技术是直方图均衡化。
解决扩展分布值的问题的一个较好的方法是重映射函数是累积分布函数,这也是直方图均衡化的原理。
OpenCv提供函数cv::equalizeHist()
用于直方图均衡处理,要注意,该函数只能处理 8 b i t 8bit 8bit一维图像,并且输出也是 8 b i t 8bit 8bit一维图像;如果要处理的是彩色图像,需要分通道逐一处理。
函数声明:
void cv::equalizaHist(const cv::InputArray src,cv::OutputArray dst
);
OpenCv中的直方图均衡化操作实际并没有用到直方图,仅仅将原始图像与结果图像在直方图上直观展示时用到了直方图操作。
OpenCv提供一种数据类型来表达直方图,这个数据类型可以表达一维至多维的直方图,并包括必要的数据以支持均匀或非均匀的组宽,并配备各种有用的函数方便对直方图进行各种操作。
OpenCv中直方图的表示
在C++ API中,OpenCv使用数组表示直方图,可以使用cv::Mat
或vector<>
或稀疏矩阵cv::SparseMat
表示。
当把一个数组视为直方图时,这个数据和普通数据的含义完全不同。
如果一个n维数组被视为直方图,那么数组中的每个元素表示该n维直方图对应组的计数结果,直方图中组的编号即数组某维与该组对应的元素的下标,下标通常是整数。
直方图中组的实际含义不同于该组的编号,当使用直方图时,可能需要在组的实际含义和组的编号之间转换。比如,有一个表达人群体重的直方图,其中的组可能被设计为 20 − 40 20-40 20−40、 40 − 60 40 - 60 40−60、 60 − 80 60 - 80 60−80、 80 − 100 80 - 100 80−100,单位是 k g kg kg,表示为数组时这些组的编号是 0 0 0、 1 1 1、 2 2 2和 3 3 3。
OpenCv也提供了一些函数完成转换工作。
处理一个高维直方图时,经常会出现直方图中大部分元素都是0的情况,这时可以选择使用cv::SparseMat
类表示直方图。
从数据创建直方图:cv::calcHist()
OpenCv提供函数cv::calcHist()
可以从一个或多个数组中创建直方图。
直方图的维度和输入数组的维度或大小无关,而是取决于输入数组的数量。直方图中的每个维度对应于某个输入数组上的某个通道。
如果不想使用图片的所有通道,选择想使用的通道传入cv::calcHist()
函数即可。
cv::calcHist()
函数声明:
void 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);void calcHist(const Mat* images, int nimages, const int* channels, InputArray mask, SparseMat& hist, int dims, const int* histSize, const float** ranges, bool uniform=true, bool accumulate=false);void calcHist(InputArrayOfArrays images, const vector<int>& channels,InputArray mask, OutputArray hist, const vector<int>& histSize, const vector<float>& ranges,bool accumulate=false);
前两种方式是C风格数组,第三种使用 STL vector模板类型的参数;前两种的主要区别是计算结果被保存在稠密数组还是稀疏数组中。
以一个示例说明这些参数。假设有一幅8bit三通道图像images,要求计算该图像的后两个通道的直方图,每个通道的直方图分为32个区间,每个区间的范围是均匀分布的。 示例代码如下:
int nimages = 1; // 数组个数int channels[] = { 1,2}; // 计算数组images中的1、2通道的直方图,通道从0开始int dims = 2; // 输出的直方图是2维const int histSize[] = {32, 32}; // 每一通道的区间个数float range[] = {0, 256}; // uniform=true时每一维的区间的上限与下限,// 比如对于1维区间范围是(256-0)/32=8,// 其中第一个区间范围[0, 8),第二个区间范围[8, 16)...直到第20个区间范围[248, 256)const float *ranges[] = {range, range}; // 计算2个维度的直方图,所以有4个范围,每个范围都是[0,256)bool uniform = true; // true表示使用规范化的情况bool accumulate = false; // false表示计算结果被累加前,将设置hist为0
函数参数详细说明:
第一个参数是数组数据,images
是一个指向C风格数组列表的指针,或者是一个更现代化的InputArrayOfArrays
。直方图从images或InputArrayOfArrays中一个或更多数组中计算。images数组的要求:通道数可以不同,大小和数组的数据类型必须相同,数组元素可以是8位整数或32位浮点数。
如果以C风格作为输入,必须同时指定nimages
作为images中包含的数组个数。
参数channels
控制直方图计算时,哪些通道需要被考虑。channel中的整数表示在计算直方图时,使用输入数据images中的哪些通道。通道按顺序进行序列编号,比如images包含2个大小一致、数据类型相同的三通道数组,那么images[0]的3个通道被编号为 0 、 1 、 2 0、1、2 0、1、2,images[1]的3个通道被编号为 3 、 4 、 5 3、4、5 3、4、5,如果要计算images[0]的前2个通道和images[1]的后2个通道,那么channels={0,1,4,5};
。channels中的元素个数是计算得到的直方图的维度。
数组mask
是一个可选掩膜,可以用来选择images的每个数组中哪些像素参与直方图的计算。mask必须是一个8-bit数组并且和images中的输入数组大小相同。计算直方图时会考虑每个在mask中有非零值的像素,如果不想使用掩膜,传入cv::noArray()
。
参数hist
是直方图计算的输出值。
参数dims
是输出直方图的维度,该值是channels中元素的个数,每个维度的来源由channels指定。
参数histSize
表示直方图中每个维度需要分为多少个区间,histSize
中元素的个数也应该等于dims,与channels中元素的个数保持一致。
参数ranges
指定每个区间对应的值。如果以C风格传参,ranges
中的每个元素是一个数组,元素个数等于dims,与参数channel和histSize中元素个数保持一致,这时ranges[i]
表示第i
维中区间的结构。
参数uniform
决定了ranges
参数的具体内容。
对于C风格数组
- 如果
uniform=true
,表示规范化的情况,即第i维中的所有区间都等长,此时只需要指定[下限,上限)
,此时得到的是均匀直方图;- 如果
uniform=false
,表示非规范化的情况,即第i维中的区间值需从ranges[i]
中获取, 此时得到的是非均匀直方图。
对于vector风格的数组,含义与使用C风格时相同,只是将C风格中的内容摊平成为一维数组
- 规范化情况下,按照维度的顺序每个维度对应两个元素;
- 非规范化情况下按照维度的顺序每个维度对应 N + 1 N+1 N+1个元素。
简单说,在均匀直方图中,只需提供每个区间的最小边界和最大边界,在非均匀直方图中,需要提供每一维度中的每个区间的最小边界和最大边界。
参数accumulate
表示在从images得到的数据被累加进入hist之前,不要将其中的元素删除,重新分配或是设置为0。
显示直方图
calcHist()
计算得到的直方图hist不能直接显示出来,需要经过处理绘制直方图。
// 绘制二维直方图
void Draw2DimsHistogram(const cv::InputArray &calchist, const int *histSize, int scale, const char* histname = "histogram")
{cv::Mat hist_img(histSize[0]*scale, histSize[1]*scale, CV_8UC3);cv::Mat hist = calchist.getMat();for(int h=0; h<histSize[0]; ++h){for(int w=0; w<histSize[1]; ++w){float hVal = hist.at<float>(h,w);cv::rectangle(hist_img, cv::Rect(h*scale,w*scale, scale, scale), cv::Scalar::all(hVal), -1);}}cv::imshow(histname, hist_img);
}
// 绘制一维直方图
void Draw1DimHistogram(const cv::InputArray &calchist, const int histSize, int scale, const char *histname = "historgam")
{// 获取最大值double maxVal;cv::minMaxIdx(calchist, NULL, &maxVal);cv::Mat hist_img(maxVal + 20, histSize*scale, CV_8UC3);cv::Mat hist = calchist.getMat().t();for(int w=0; w<histSize; ++w){float hVal = hist.at<float>(0,w);cv::rectangle(hist_img, cv::Rect(w*scale, hist_img.rows-hVal, scale, int(hVal)), cv::Scalar(0,0,hVal), -1);}cv::imshow(histname, hist_img);
}
// 绘制一维直方图的折线图
void Draw1DimLineChartm(const cv::InputArray &calchist, const int histSize, int scale, const char *histname = "Line chart")
{// 获取最大值double maxVal;cv::minMaxIdx(calchist, NULL, &maxVal);cv::Mat hist_img(maxVal + 20, histSize*scale, CV_8UC3);cv::Mat hist = calchist.getMat().t();for(int w=1; w<histSize; ++w){float preVal = hist.at<float>(0, w-1);float hVal = hist.at<float>(0,w);cv::line(hist_img, cv::Point((w-1)*scale, hist_img.rows - preVal), cv::Point(w*scale, hist_img.rows - hVal), cv::Scalar(0,0,255), 2, 8);}cv::imshow(histname, hist_img);
}
直方图归一化
构造直方图时,首先需要将信息放入在各个区间。有时希望将直方图变为归一化的形式,这时的每个区间表示的是该区间内的信息占总体的百分比。比如说,对一幅8bit灰度图来说,设定区间为灰度值 [ 0 , 255 ] [0, 255] [0,255], 那么归一化前每个区间表示图像中当前灰度级的像素数量,归一化后表示当前灰度级的像素数量占总像素数量的百分比。
在C++ API
中,可以简单使用数组的代数算子和操作完成直方图的归一化,如下:
cv::Mat normalized = my_hist / sum(my_hist)[0];
// 或者
cv::normalize(my_hist, my_hist, 1, 0, NORM_L1);
直方图二值化
如果希望将直方图二值化并丢弃所有其中元素个数少于给定值的区间,可以使用标准数组二值化函数:
cv::threshold(my_hist, my_threshold_hist, threshold, 0, cv::THRESHOLD_TOZERO);
找出最显著的区间
如果希望找出所有元素个数高于某个给定阈值的区间,或者希望找出有最多元素的区间,比如在使用直方图表示概率分布的情况时,可以选择使用cv::minMaxLoc()
。
函数声明如下:
void minMaxLoc( cv::InputArray src, double *minVal, double *maxVal, cv::Point* minLoc=0, cv::Point* maxLoc=0,cv::InputArray mask = cv::noArray);void minMaxLoc(const cv::SparseMat& src, double* minVal, double* maxVal, int* minLoc=0, int* maxLoc=0);
该函数需要传入minVal
和maxVal
用来存放计算出来的最小值和最大值,需要传入minLoc
和maxLoc
存放计算出的最小值和最大值的位置。如果不想要某个计算结果,将该参数置为NULL即可。
当处理二维数组时,可以使用cv::InputArray
的形式。
当处理二维直方图时,可以使用cv::SparseMat
的形式。如果处理的是一个一维vector<>
数组,可以使用cv::Mat(vec).reshape(1)
将其转换为一个 N × 1 N \times 1 N×1的二维数组。
需要注意使用稀疏矩阵作为输入时,要求传入的minLoc
和maxLoc
为int*型,这是因为稀疏矩阵形式的minMaxLoc()
支持任意维的矩阵,因此需要手动分配存放位置的变量并确保其可存放n维稀疏直方图对应的原图中的一个点。
比较两个直方图
可以通过对两幅图像取直方图,并通过直方图比较方法来比较原来的两幅图像,也可以借助直方图比较方法在图像中识别物体,方法是将物体的直方图和图片中不同的子区域的直方图进行比较,通过直方图相配的程度来判断该子区域是否有物体。
OpenCv提供函数cv::compareHist()
通过特殊的判据对两个直方图的相似度进行度量。
cv::compareHist()
函数声明:
double cv::compareHist(cv::InputArray H1, cv::InputArray H2, int method);
double cv::compareHist(const cv::SparseMat& H1, const cv::SparseMat& H2, int method);
前两个参数是待比较的直方图,该直方图应该具有相同的大小。
第三个参数是需要选择的距离度量方法。OpenCv提供四个可用的方法:
- 相关性方法
cv::COMP_CORRL
,使用基于统计相关性的方法,实现了皮尔逊相关性系数方法,计算公式:$d_{correl}(H_1, H_2) = \frac{ {\textstyle \sum_{i}^{} H_1’(i) \cdot H_2’(i) } } {\sqrt{ {\textstyle \sum_{i}^{} H_1’^2(i) \cdot H_2’^2(i) } } } ,这里的 , 这里的 ,这里的H_1 和 和 和H_2$解释为概率分布。大的相关性系数比小的相关性系数匹配的好。完全匹配的相关性系数为 1 1 1, 完全不匹配的相关性系数为 − 1 -1 −1, 0 0 0表示两个分布没有关系,随机组合; - 卡方方法
cv::COMP_CHISQR_ALT
,该距离度量是基于卡方检验统计量,是一种检验两个分布是否相关的替换检验方法,计算公式:$d_{chi-square} (H_1, H_2) = \sum_{i}^{} \frac{ (H_1(i) - H_2(i))^2 } { H_1(i) + H_2(i) } $,对于该检验方法,低分值表示直方图匹配的很好,完全匹配的分值为 0 0 0,完全不匹配的分值无下限,依赖于直方图的大小; - 交集法
cv::COMP_INTERSECT
,简单基于两个直方图的交集,该方法对两个直方图中的共同部分求和,计算公式: d i n t e r s e c t i o n ( H 1 , H 2 ) = ∑ i m i n ( H 1 ( i ) , H 2 ( i ) ) d_{intersection}(H_1, H_2) = \sum_{i}^{} min(H_1 (i), H_2(i)) dintersection(H1,H2)=∑imin(H1(i),H2(i)),对于该检验方法,高分值表示匹配的好,如果两个直方图都进行了归一化, 1 1 1表示完全匹配, 0 0 0表示完全不匹配。 - 巴氏距离
cv::COMP_BHATTACHARYYA
,对两个分布重叠程度的一种度量方法,计算公式:$d(H_1, H_2) = \sqrt{1- \frac{ {\textstyle \sum_{i}^{} H_1(i) \cdot H_2(i)} }{\sqrt{ {\textstyle \sum_{i}^{} H_1(i) \cdot {\textstyle \sum_{i}^{} H_2(i)} } } } } $,对于该检验方法,值越小表示匹配的效果越好, 0 0 0表示完全匹配, 1 1 1表示完全不匹配。
交集法在快速而粗略的匹配中效果很好,卡方法和巴氏距离法效果更好,但是速度更慢。另外还有EMD距离方法可以比较直方图,该方法最符合直觉的匹配效果,但是计算速度更慢。
四、OpenCv提供的一些复杂的直方图操作
OpenCv提供的一些可用的复杂的方法,在某些特定应用中比较有用。
EMD距离
EMD距离的基本思路是:通过将一部分(或全部)直方图搬到一个新位置,度量花多大的功夫才能把一个直方图搬到另一个直方图里。
光照的变化会使得颜色值产生很大的偏移,这种偏移不改变颜色直方图的形状,上面介绍的相关性法、卡方方法、交集法、巴氏距离法都是对直方图颜色位置的度量,而对直方图颜色位置的移动度量不准确。EMD距离是一种对平移不敏感的距离度量方法。EMD距离可以在任意维度下工作。
EMD算法允许用户设置自己的距离度量或是自己的移动花费矩阵,即记下材料如何从一个直方图流向另一个直方图的,并用从数据的先验信息导出的非线性距离对其度量。OpenCv提供的EMD函数为cv::EMD()
,函数声明如下:
float cv::EMD(cv::InputArray signature1, cv::InputArray signature2, int distType, cv::InputArray cost = cv::noArray(), float* lowerBound = 0, cv::OutputArray flow = cv::noArray());
这个函数要求使用签名的方式传入前两个数组参数,签名要求是float
类型的数组,每行包括直方图区间的计数值,接下来是该区间的坐标。比如对于下列的一维直方图,其签名分别为:上- [ [ 1 , 0 ] , [ 0 , 1 ] ] [[1, 0], [0, 1]] [[1,0],[0,1]];中- [ [ 0.5 , 0 ] , [ 0.5 , 1 ] ] [[0.5, 0], [0.5, 1]] [[0.5,0],[0.5,1]];下- [ [ 0 , 0 ] , [ 1 , 1 ] ] [[0, 0], [1, 1]] [[0,0],[1,1]]。如果在一个三维直方图 ( x , y , z ) (x, y, z) (x,y,z)位置上有区间计数 537 537 537,那么该区间这个位置的签名为 [ 537 , x , y , z ] [537, x, y, z] [537,x,y,z]。在调用cv::EMD()
函数之前需要将直方图转换为签名的形式。
参数distType
应该选择以下之一:
- 曼哈顿距离
cv::DIST_L1
- 欧几里得距离
cv::DIST_L2
- 棋盘距离
cv::DIST_C
- 用户自定义距离
cv::DIST_USER
,使用该选择时需要通过参数cost
传进一个预先计算好的矩阵,这个矩阵大小是一个 n 1 × n 2 n_1 \times n_2 n1×n2,其中 n 1 n_1 n1和 n 2 n_2 n2是signature1
和signature2
的大小。
参数lowerBound
有两个功能,一个作为输入,一个作为输出。作为输出时,表示两个直方图重心距离的下限,要计算这个下限,必须提供一个标准的距离度量方法,同时两个签名的总权重必须相同,比如直方图归一化后的情况。如果选择提供一个下限,必须将该值初始化为一个有意义的值,低于这个下限的,EMD距离才会被计算。如果希望任何情况下都计算EMD距离,将lowerBound
置为0即可。
参数flow
是一个可选的 n 1 × n 2 n_1 \times n_2 n1×n2的矩阵,用来记录从signature1
的第 i i i个点流向signature2
的第 j j j个点的质量,本质上给出的是计算整个EMD中质量的具体安排情况。
示例:从直方图为EMD算法构造签名,在不同的光照条件下对手部的直方图进行比较。代码运行工具:Qt。
void on_action_EMD_triggered()
{QStringList files;// 0.jpg:model图片(该图片会被拆分为上下两部分) 1.jpg:和第一张图片一样 2.jpg/3.jpg/4.jpg:与第一张图片进行比较的三张图片files << "0.jpg" << "1.jpg" << "2.jpg" << "3.jpg" << "4.jpg";std::vector<cv::Mat> src(5);cv::Mat tmp;// 读取model图片tmp = cv::imread(files.at(0).toStdString().data());if(tmp.empty()){qCritical() << "Error on reading image 1," << files.at(0) << "\n\n";return;}// 将第一张图片拆分一半cv::Size size = tmp.size();int width = size.width;int height = size.height;int halfheight = height >> 1;qDebug() << "Getting size [[" << tmp.cols << "] [" << tmp.rows << "]]\n\n";qDebug() << "Got size (w, h): (" << width << ", " << height << ")\n";src[0] = cv::Mat(cv::Size(width, halfheight), CV_8UC3);src[1] = cv::Mat(cv::Size(width, halfheight), CV_8UC3);// 将第一张图像分为上下两部分,分别赋给src[0]和src[1]cv::Mat_<cv::Vec3b>::iterator tmpit = tmp.begin<cv::Vec3b>();// 上半部分cv::Mat_<cv::Vec3b>::iterator s0it = src[0].begin<cv::Vec3b>();for(int i=0; i<width*halfheight; ++i, ++tmpit, ++s0it)*s0it = *tmpit;// 下半部分cv::Mat_<cv::Vec3b>::iterator s1it = src[1].begin<cv::Vec3b>();for(int i=0; i<width*halfheight; ++i, ++tmpit, ++s1it)*s1it = *tmpit;// 加载另外三张图像for(int i=2; i<5;++i){src[i] = cv::imread(files.at(i).toStdString().data());if(src[i].empty()){qCritical() << "Error on reading image " << i << ": " << files.at(i) << "\n";return;}}// 计算HSV图像,分解为单独的平面std::vector<cv::Mat> hsv(5), hist(5), hist_img(5);int h_bins = 8;int s_bins = 8;int hist_size[] = {h_bins, s_bins}, ch[] = {0,1};float h_range[] = {0, 180};float s_range[] = {0, 360};const float *range[] = {h_range, s_range};int scale = 30;// 计算并绘制直方图for(int i=0; i<5; ++i){// 转换为HSV图像cv::cvtColor(src[i], hsv[i], cv::COLOR_BGR2HSV);// 计算直方图cv::calcHist(&src[i], 1, ch, cv::noArray(), hist[i], 2, hist_size, range);// 归一化cv::normalize(hist[i], hist[i], 0, 255, cv::NORM_MINMAX);// 绘制直方图hist_img[i] = cv::Mat::zeros(hist_size[0]*scale, hist_size[1]*scale, CV_8UC3);for(int h=0; h<hist_size[0]; ++h){for(int s=0; s<hist_size[1]; ++s){float hval = hist[i].at<float>(h,s);cv::rectangle(hist_img[i],cv::Rect(h*scale, s*scale, scale, scale),cv::Scalar::all(hval), -1);}}}// 显示int index=0;cv::namedWindow("0source", cv::WINDOW_AUTOSIZE); cv::imshow("0source", hsv[index]);cv::namedWindow("0H-S Histogram", cv::WINDOW_AUTOSIZE); cv::imshow("0H-S Histogram", hist_img[index++]);cv::namedWindow("1source", cv::WINDOW_AUTOSIZE); cv::imshow("1source", hsv[index]);cv::namedWindow("1H-S Histogram", cv::WINDOW_AUTOSIZE); cv::imshow("1H-S Histogram", hist_img[index++]);cv::namedWindow("2source", cv::WINDOW_AUTOSIZE); cv::imshow("2source", hsv[index]);cv::namedWindow("2H-S Histogram", cv::WINDOW_AUTOSIZE); cv::imshow("2H-S Histogram", hist_img[index++]);cv::namedWindow("3source", cv::WINDOW_AUTOSIZE); cv::imshow("3source", hsv[index]);cv::namedWindow("3H-S Histogram", cv::WINDOW_AUTOSIZE); cv::imshow("3H-S Histogram", hist_img[index++]);cv::namedWindow("4source", cv::WINDOW_AUTOSIZE); cv::imshow("4source", hsv[index]);cv::namedWindow("4H-S Histogram", cv::WINDOW_AUTOSIZE); cv::imshow("4H-S Histogram", hist_img[index++]);// 比较直方图 src0 vc src1 , vs src2, vs src3, vs src4std::cout << "Comparsion:\n"<< "\t\t Corr Chi Intersect Bhat\n";for(int i=1; i<5; ++i){std::cout << "Hist[0] vs Hist[" << i << "]: ";for(int j=0;j<4;j++){ // 4种算法std::cout << /*"method[" << j << "]: " <<*/ cv::compareHist(hist[0], hist[i], j) << " ";}std::cout << std::endl;}// 使用EMD方法比较并输出数据std::vector<cv::Mat> sig(5);std::cout << "\nEMD:" << std::endl;// 转换为签名形式for(int i=0; i<5;++i){std::vector<cv::Vec3f> sigv;// 归一化直方图使得总和为1cv::normalize(hist[i], hist[i], 1, 0, cv::NORM_L1);// 转换for(int h=0; h<h_bins; ++h){for(int s=0; s<s_bins; ++s){float bin_val = hist[i].at<float>(h,s);if(bin_val != 0){sigv.push_back(cv::Vec3f(bin_val, float(h), float(s)));}}}// 转换为 Nx3 32FC1的矩阵,n是直方图区间的个数sig[i] = cv::Mat(sigv).clone().reshape(1);if(i > 0){std::cout << "Hist[0] vs Hist[" << i << "]:"<< cv::EMD(sig[0], sig[i], CV_DIST_L2) << std::endl;}}}
反向投影
反向投影是计算像素和直方图模型中像素吻合度的一种方法。比如,现有肤色的直方图,可以使用反向投影在图像中寻找肤色区域。
OpenCv提供实现这种基础操作的函数:cv::calcBackProject()
,该函数从输入图像的指定通道中计算出一个向量。不同于cv::calcHist()
在直方图中记录累计值,反向投影从输入图像的直方图中读取当前像素对应的计数值作为结果。比如,计算输入图像的H-S直方图,在某个区间的计数概率为 p p p,那么就将当前图像中在该区间的所有像素置为 p p p。
从统计学的角度来看,如果将输入的直方图视为某个物体上特定的向量的一个概率分布,那么反向投影就是计算图片上某个特定部分来自该先验分布的概率。
函数声明如下:
void cv::calcBackProject(const cv::Mat* images, int nimages, const int* channels, cv::InputArray hist, cv::OutputArray backProject, const float** ranges, double scale, bool uniform = true);void cv::calcBackProject(const cv::Mat* images, int nimages,const int* channels,const cv::SparseMat& hist,cv::OutputArray backProject,const float** ranges, double scale = 1,bool uniform = true);void cv::calcBackProject(cv::InputArrayOfArrays images,const vector<int>& channels,cv::InputArray hist,cv::OutputArray backProject,const vector<float>& ranges,double scale = 1,bool uniform = true);
cv::calcBackProject()
函数以单通道或是多通道数据集合的形式提供图像,即传入的images
参数。同时以cv::calcHist()
输出的直方图的格式传入直方图,即hist
参数。如果使用C风格数组,需要参数nimages
指定输入数组中元素的个数。
参数channels
指定在反向投影中发挥作用的通道的列表,该参数与cv::calcHist()
时的参数形式一致。
反向投影的计算结果保存在参数backProject
中,该参数和images[0]
的大小类型相同,只有一个通道。
参数ranges
作用与cv::calcHist
中的参数作用一致。
参数scale
是一个可选的应用于backProject的缩放因子,如果希望对结果可视化,这个参数很有用。
参数uniform
表示输入的直方图是不是一个均匀的直方图,和cv::calcHist
中意义相同。
在新的图片中寻找一个物体或是需要的区域的一个方法如下:
- 创建要寻找的物体或区域的直方图;
- 为了在新图片中找到它,使用步骤1中的直方图对新图片进行
cv::calcBackProject()
以创建一个反向投影图,在反向投影图中,具有最大值的区域很可能包含感兴趣的物体; - 反向投影中的高值区域取局部直方图并使用
cv::compareHist()
将它与要寻找的物体或区域的直方图进行比较;
模板匹配
OpenCv提供函数cv::matchTemplate()
进行模板匹配,但是该函数实现原理不基于直方图,而是使用一个图像块在输入图像上进行滑动,并用指定的匹配方法进行比较。
cv::matchTemplate()
函数声明:
void cv::matchTemplate(cv::InputArray image,cv::InputArray temp1,cv::OutputArray result,int method);
参数image
是一幅8bit或浮点型灰度或彩色图像。
参数temp1
是一个包含给定物体的另一张图片上选取的模板图片块,另一张图片和当前图片相似。
参数result
存放计算结果,是一个单通道、大小为image.width - temp1.width +1, image.height - temp1.height +1
、以整数字节或浮点型存储的图片。
参数method
表示匹配方法,每一种匹配方法,都有一个归一化的版本。归一化的方法可以降低模板和图片之间由于光照不同引起的效果。具体方法如下所示, I I I表示输入图像, T T T表示模板, R R R表示输出的结果图像。
-
方差匹配方法
cv::TM_SQDIFF
,以方差为依据进行匹配,完全匹配结果为0, 不匹配会得到一个很大的值。计算公式: R s q _ d i f f = ∑ x ′ , y ′ [ T ( x ′ , y ′ ) − I ( x + x ′ , y + y ′ ) ] 2 R_{sq\_diff} = \sum_{x', y'}^{} [T(x', y') - I(x+x', y+y')]^2 Rsq_diff=∑x′,y′[T(x′,y′)−I(x+x′,y+y′)]2。 -
归一化方差匹配方法
cv::TM_SQDIFF_NORMED
,是方差匹配方法的归一化版本,完全匹配结果为0。计算公式:$R_{sq_diff_normed} = \frac{ \sum_{x’, y’}^{} [T(x’, y’) - I(x+x’, y+y’)]^2 }{\sqrt{ \sum_{x’, y’}^{} T(x’, y’)^2 \cdot \sum_{x’, y’}^{} I(x+x’, y+y’)^2 } } $。 -
相关性匹配方法
cv::TM_CCORR
,以相乘的方式进行模板匹配,完全匹配结果值很大,不匹配会得到一个很小的值或0。计算公式: R c c o r r = ∑ x ′ , y ′ T ( x ′ , y ′ ) ⋅ I ( x + x ′ , y + y ′ ) R_{ccorr} = \sum_{x', y'}^{} T(x', y') \cdot I(x+x', y+y') Rccorr=∑x′,y′T(x′,y′)⋅I(x+x′,y+y′)。 -
归一化的互相关匹配方法
cv::TM_CCORR_NORMED
,是相关性匹配方法的归一化版本,图片和模板极度不匹配,结果为0。计算公式:$R_{ccorr_normed} = \frac{R_{ccorr} = \sum_{x’, y’}^{} T(x’, y’) \cdot I(x+x’, y+y’)}{\sqrt{\sum_{x’, y’} T(x’, y’)^2} \cdot \sum _{x’, y’} I(x+x’, y+y’)^2 } $。 -
相关系数匹配方法
cv::TM_CCOEFF
,该方法以模板相对于其平均值的变化和图片相对于其平均值的变化进行匹配,完全匹配会得到1,完全不匹配得到-1, 如果是随机图片匹配得到0。计算公式:
R c c o e f f = ∑ x ′ , y ′ T ′ ( x ′ , y ′ ) ⋅ I ′ ( x + x ′ , y + y ′ ) T ′ ( x ′ , y ′ ) = T ( x ′ , y ′ ) − ∑ x ′ ′ , y ′ ′ T ( x ′ ′ , y ′ ′ ) w − h I ′ ( x + x ′ , y + y ′ ) = I ( x + x ′ , y + y ′ ) − ∑ x ′ ′ , y ′ ′ I ( x ′ ′ , y ′ ′ ) w − h R_{ccoeff} = \sum_{x', y'}^{} T'(x', y') \cdot I'(x+x', y+y') \\ T'(x', y') = T(x', y') - \frac{\sum_{x'', y''} T(x'', y'')}{w-h} \\ I'(x+x', y+y') = I(x+x', y+y') - \frac{\sum_{x'', y''} I(x'', y'')}{w-h} Rccoeff=x′,y′∑T′(x′,y′)⋅I′(x+x′,y+y′)T′(x′,y′)=T(x′,y′)−w−h∑x′′,y′′T(x′′,y′′)I′(x+x′,y+y′)=I(x+x′,y+y′)−w−h∑x′′,y′′I(x′′,y′′)
- 归一化的相关系数匹配方法
cv::TM_CCOEFF_NORMED
,相关系数匹配方法的归一化版本,相对好的匹配得到正值,误匹配得到负值。计算公式: R c c o e d d _ n o r m e d = ∑ x ′ , y ′ T ′ ( x ′ , y ′ ) ⋅ I ′ ( x + x ′ , y + y ′ ) ∑ x ′ , y ′ T ′ ( x ′ , y ′ ) 2 ⋅ ∑ x ′ , y ′ I ′ ( x + x ′ , y + y ′ ) 2 R_{ccoedd\_normed} = \frac {\sum_{x', y'} T'(x', y') \cdot I'(x + x', y+ y')}{\sqrt { \sum_{x', y'} T'(x', y')^2 \cdot \sum_{x', y'} I'(x+x', y+y')^2 }} Rccoedd_normed=∑x′,y′T′(x′,y′)2⋅∑x′,y′I′(x+x′,y+y′)2∑x′,y′T′(x′,y′)⋅I′(x+x′,y+y′),其中 T ′ T' T′和 I ′ I' I′与相关系数匹配中含义一致。
实际使用中可以从一个相对简单的度量方法换成一个更复杂的度量方法,比如从方差换成相关系数,会得到更精确的结果,但同时计算时间也会更长。最好的办法是尝试所有的度量方法,选择一个可以兼顾精确度和计算时间的最好的方法。
五、 MATLAB中的直方图操作
MATLAB提供了一些直方图操作函数。
计算并显示图像直方图
首先imread
函数读取图像,然后imhist
函数计算图像直方图,最后使用图窗绘制函数subplot
及图像显示函数imshow
将原图及结果显示出来。具体参考代码如下:
MATLAB版本:R2014B
%% 显示图像直方图filename = './images/lena.jpg';
img = imread(filename);% 如果是彩色图,转换为灰度图
if ndims(img) < 3grayimg = img;
elsegrayimg = rgb2gray(img);
end% 显示原图
subplot(2,2,1); imshow(img); title('原图');
subplot(2,2,2); imshow(grayimg); title('灰度图');
% 计算并显示直方图,传入的图像为灰度图
subplot(2,2,[3,4]); bar(imhist(grayimg, 256), 'b');title('图像直方图');
直方图归一化
利用imhist()
得到图像直方图,将该直方图除以像素总数即得到归一化的直方图。
MATLAB版本:R2014B
%% 图像直方图归一化filename = './images/lena.jpg';
img = imread(filename);% 如果是彩色图,转换为灰度图
if ndims(img) < 3grayimg = img;
elsegrayimg = rgb2gray(img);
end% 显示原图
subplot(2,2,1); imshow(img); title('原图');
subplot(2,2,2); imshow(grayimg); title('灰度图');
% 计算并显示直方图,传入的图像为灰度图,默认划分256个灰度级,可以设置该参数
h = imhist(grayimg, 256); % 计算图像直方图
subplot(2,2,3); bar(h, 'b'); title('图像直方图');
subplot(2,2,4); bar(h/numel(grayimg), 'r'); title('图像直方图归一化');
直方图均衡化
histeq
函数实现图像直方图均衡化。
MATLAB版本:R2014B
%% 直方图均衡化filename = './images/lena.jpg';
img = imread(filename);% 如果是彩色图,转换为灰度图
if ndims(img) < 3grayimg = img;
elsegrayimg = rgb2gray(img);
end% 显示原图
subplot(2,2,1); imshow(grayimg); title('原始灰度图');
% 计算并显示直方图,传入的图像为灰度图,默认划分256个灰度级,可以设置该参数
h1 = imhist(grayimg, 256); % 计算图像直方图
subplot(2,2,3); bar(h1, 'b'); title('原始灰度直方图');
e = histeq(grayimg); % 直方图均衡化
h2 = imhist(e); % 计算均衡化后图像的直方图
subplot(2,2,2); imshow(e); title('均衡化后的图像');
subplot(2,2,4); bar(h2, 'b'); title('均衡化后图像的灰度直方图');
正比例变换
加入一幅图像的灰度范围在 [ 40 , 200 ] [40,200] [40,200]之间,将其缩放到 [ 0 , 255 ] [0, 255] [0,255]范围。
MATLAB版本:R2014B
%% 正比例变换
clc;
filename = './images/lena.jpg';
img = imread(filename);
if ndims(img) < 3grayimg = img;
elsegrayimg = rgb2gray(img);
endsubplot(2,2,1); imshow(grayimg); title('(a)原始灰度图');
subplot(2,2,2); imhist(grayimg); title('(b)原始灰度图的统计直方图')
l = min(min(grayimg))
h = max(max(grayimg))
% 原始图像灰度值最大为20, 最小为245,将其缩放到[0, 255]
a = imadjust(grayimg, [20/255, 245/255], [0, 1]);
subplot(2,2,3); imshow(a); title('(c)变换图像');
subplot(2,2,4); imhist(a); title('(d)变换图像的灰度统计直方图')