导读
在进行物体图像和视频信息分析的过程中,我们常常会习惯于将眼中看到的物体用直方图(histogram)表示出来,得到比较直观的数据官感展示。直方图可以用来描述各种不同的参数和事物,如物体的色彩分布、物体边缘梯度模板,以及表示目标位置的当前假设的概率分布。
本章你将学到:
● 什么是直方图
● 直方图的计算与绘制
● 如何进行直方图的对比
● 反向投影技术
● 模板匹配技术
9.1 图像直方图概述
直方图广泛运用于很多计算机视觉运用当中,通过标记帧与帧之间显著的边缘和颜色的统计变化,来检测视频中场景的变化。在每个兴趣点设置一个有相近特征的直方图所构成“标签”,用以确定图像中的兴趣点。边缘、色彩、角度等直方图构成了可以被传递给目标识别分类器的一个通用特征类型。色彩和边缘的直方图序列还可以用来识别网络视频是否被复制。如图9.1所示。
其实,简单点说,直方图就是对数据进行统计的一种方法,并且将统计值组织到一系列事先定义好的bin当中。其中,bin为直方图中经常用到的一个概念,可翻译为“直条”或“组距”,其数值是从数据中计算出的特征统计量,这些数据可以是诸如梯度、方向、色彩或任何其他特征。且无论如何,直方图获得的是数据分布的统计图。通常直方图的维数要低于原始数据。总而言之,直方图是计算机视觉中最经典的工具之一。
在统计学中,直方图(Histogram)是一种对数据分布情况的图形表示,是一种二维统计图表,它的两个坐标分别是统计样本和该样本对应的某个属性的度量。
我们在图像变换的那一章中讲过直方图的均衡化,它是通过拉伸像素强度分布范围来增强图像对比度的一种方法。大家在自己的心目中应该已经对直方图有一定的理解和认知。下面就来看一看对图像直方图比较书面化的解释。
图像直方图(Image Histogram)是用以表示数字图像中亮度分布的直方图,标绘了图像中每个亮度值的像素数。可以借助观察该直方图了解需要如何调整亮度分布。这种直方图中,横坐标的左侧为纯黑、较暗的区域,而右侧为较亮、纯白的区域。因此,一张较暗图片的图像直方图中的数据多集中于左侧和中间部分,而整体明亮、只有少量阴影的图像则相反。计算机视觉领域常借助图像直方图来实现图像的二值化。
直方图的意义如下。
● 直方图是图像中像素强度分布的图形表达方式。
● 它统计了每一个强度值所具有的像素个数。
上面已经讲到,直方图是对数据的统计集合,并将统计结果分布于一系列预定义的bins中。这里的数据不仅仅指的是灰度值,且统计数据可能是任何能有效描述图像的特征。下面看一个例子,假设有一个矩阵包含一张图像的信息(灰度值0-255),让我们按照某种方式来统计这些数字。既然已知数字的范围包含256个值,于是可以将这个范围分割成子区域(也就是上面讲到bins),如:
然后再统计每一个bini的像素数目。采用这一方法来统计上面的数字矩阵,可以得到图9.2(其中x轴表示bin,y轴表示各个bin中的像素个数)。
以上就是一个说明直方图的用途的简单示例。其实,直方图并不局限于统计颜色灰度,而是可以统计任何图像特征,如梯度、方向等。
让我们具体讲讲直方图的一些术语和细节。
● dims:需要统计的特征的数目。在上例中,dims=1因为我们仅仅统计了灰度值(灰度图像)。
● bins:每个特征空间子区段的数目,可翻译为“直条”或“组距”。在上例中,bins=16。
● range:每个特征空间的取值范围。在上例中,range=[0,255]。
9.2 直方图的计算与绘制
直方图的计算在OpenCV中可以使用calcHist()函数,而计算完成之后,可以采用OpenCV中的绘图函数,如绘制矩形的rectangle()函数,绘制线段的line()来完成。
9.2.1 计算直方图:calcHist()函数
在OpenCV中,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 );
● 第一个参数,const Mat*类型的images,输入的数组(或数组集),它们需为相同的深度(CV_8U或CV_32F)和相同的尺寸。
● 第二个参数,int类型的nimages,输入数组的个数,也就是第一个参数中存放了多少张“图像”,有几个原数组。
● 第三个参数,const int*类型的channels,需要统计的通道(dim)索引。第一个数组通道从0到images[0].channels()-1,而第二个数组通道从images[0].channels()计算到images[0].channels()+images[1].channels()-1。
● 第四个参数,InputArray类型的mask,可选的操作掩码。如果此掩码不为空,那么它必须为8位,并且与images[i]有同样大小的尺寸。这里的非零掩码元素用于标记出统计直方图的数组元素数据。
● 第五个参数,OutputArray类型的hist,输出的目标直方图,一个二维数组。
● 第六个参数,int类型的dims,需要计算的直方图的维度,必须是正数,且不大于CV_MAX_DIMS(在当前版本的OpenCV中等于32)。
● 第七个参数,const int*类型的histSize,存放每个维度的直方图尺寸的数组。
● 第八个参数,const float**类型的ranges,表示每一个维度数组(第六个参数dims)的每一维的边界阵列,可以理解为每一维数值的取值范围。
● 第九个参数,bool类型的uniform,指示直方图是否均匀的标识符,有默认值true。
● 第十个参数,bool类型的accumulate,累计标识符,有默认值false。若其为true,直方图在配置阶段不会被清零。此功能主要是允许从多个阵列中计算单个直方图,或者用于在特定的时间更新直方图。
9.2.2 找寻最值:minMaxLoc()
函数minMaxLoc()函数的作用是在数组中找到全局最小值和最大值。它有两个版本的原型,在此介绍常用的那一个版本。
void minMaxLoc(InputArray src, CV_OUT double* minVal,
CV_OUT double* maxVal = 0, CV_OUT Point* minLoc = 0,
CV_OUT Point* maxLoc = 0, InputArray mask = noArray());
● 第一个参数,InputArray类型的src,输入的单通道阵列。
● 第二个参数,double*类型的minVal,返回最小值的指针。若无须返回,此值置为NULL。
● 第三个参数,double*类型的maxVal,返回的最大值的指针。若无须返回,此值置为NULL。
● 第四个参数,Point*类型的minLoc,返回最小位置的指针(二维情况下)。若无须返回,此值置为NULL。
● 第五个参数,Point*类型的maxLoc,返回最大位置的指针(二维情况下)。若无须返回,此值置为NULL。
● 第六个参数,InputArray类型的mask,用于选择子阵列的可选掩膜。
9.2.3 示例程序:绘制H—S直方图
下面的示例说明如何计算彩色图像的色调,饱和度二维直方图。
注意:色调(Hue),饱和度(Saturation)。所以“H-S直方图”就是“色调—饱和度直方图”。
/* @File : 79_H-S-Histogram.cpp
* @Brief : 示例程序79
* @Details : H-S二维直方图的绘制
* @Date : 2015-11-01
* @OpenCV Version : 4.8.0
* @Development Tools : Windows 11 64bit && Visual Studio 2017
* @Modify : 2024-05-01
*/
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
using namespace cv;
//-----------------------------------【ShowHelpText( )函数】-----------------------------
// 描述:输出一些帮助信息
//----------------------------------------------------------------------------------------------
void ShowHelpText()
{
//输出欢迎信息和OpenCV版本
printf("\n\n\t\t\t非常感谢购买《OpenCV3编程入门》一书!\n");
printf("\n\n\t\t\t此为本书OpenCV3版的第79个配套示例程序\n");
printf("\n\n\t\t\t 当前使用的OpenCV版本为:" CV_VERSION );
printf("\n\n ----------------------------------------------------------------------------\n");
}
int main( )
{
//【1】载入源图,转化为HSV颜色模型
Mat srcImage, hsvImage;
srcImage=imread("1.jpg");
cvtColor(srcImage,hsvImage, COLOR_BGR2HSV);
system("color 2F");
ShowHelpText();
//【2】参数准备
//将色调量化为30个等级,将饱和度量化为32个等级
int hueBinNum = 30;//色调的直方图直条数量
int saturationBinNum = 32;//饱和度的直方图直条数量
int histSize[ ] = {hueBinNum, saturationBinNum};
// 定义色调的变化范围为0到179
float hueRanges[] = { 0, 180 };
//定义饱和度的变化范围为0(黑、白、灰)到255(纯光谱颜色)
float saturationRanges[] = { 0, 256 };
const float* ranges[] = { hueRanges, saturationRanges };
MatND dstHist;
//参数准备,calcHist函数中将计算第0通道和第1通道的直方图
int channels[] = {0, 1};
//【3】正式调用calcHist,进行直方图计算
calcHist( &hsvImage,//输入的数组
1, //数组个数为1
channels,//通道索引
Mat(), //不使用掩膜
dstHist, //输出的目标直方图
2, //需要计算的直方图的维度为2
histSize, //存放每个维度的直方图尺寸的数组
ranges,//每一维数值的取值范围数组
true, // 指示直方图是否均匀的标识符,true表示均匀的直方图
false );//累计标识符,false表示直方图在配置阶段会被清零
//【4】为绘制直方图准备参数
double maxValue=0;//最大值
minMaxLoc(dstHist, 0, &maxValue, 0, 0);//查找数组和子数组的全局最小值和最大值存入maxValue中
int scale = 10;
Mat histImg = Mat::zeros(saturationBinNum*scale, hueBinNum*10, CV_8UC3);
//【5】双层循环,进行直方图绘制
for( int hue = 0; hue < hueBinNum; hue++ )
for( int saturation = 0; saturation < saturationBinNum; saturation++ )
{
float binValue = dstHist.at<float>(hue, saturation);//直方图组距的值
int intensity = cvRound(binValue*255/maxValue);//强度
//正式进行绘制
rectangle( histImg, Point(hue*scale, saturation*scale),
Point( (hue+1)*scale - 1, (saturation+1)*scale - 1),
Scalar::all(intensity),FILLED );
}
//【6】显示效果图
imshow( "素材图", srcImage );
imshow( "H-S 直方图", histImg );
waitKey();
}
程序中新出现的MatND类是用于存储直方图的一种数据结构,其用法简单,常常在直方图相关OpenCV程序中出现。而+-**程序运行截图所示。
9.2.4 示例程序:计算并绘制图像一维直方图
上文中已经讲解了calcHist()函数的用法,并绘制出了图像的H-S二维直方图。而本节我们会通过一个示例,来学习图像一维直方图的计算和绘制过程。
/* @File : 80_Histogram2.cpp
* @Brief : 示例程序80
* @Details : 一维直方图的绘制
* @Date : 2015-11-01
* @OpenCV Version : 4.8.0
* @Development Tools : Windows 11 64bit && Visual Studio 2017
* @Modify : 2024-05-01
*/
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include <iostream>
using namespace cv;
using namespace std;
//-----------------------------------【ShowHelpText( )函数】-----------------------------
// 描述:输出一些帮助信息
//----------------------------------------------------------------------------------------------
void ShowHelpText()
{
//输出欢迎信息和OpenCV版本
printf("\n\n\t\t\t非常感谢购买《OpenCV3编程入门》一书!\n");
printf("\n\n\t\t\t此为本书OpenCV3版的第80个配套示例程序\n");
printf("\n\n\t\t\t 当前使用的OpenCV版本为:" CV_VERSION );
printf("\n\n ----------------------------------------------------------------------------\n");
}
int main()
{
//【1】载入原图并显示
Mat srcImage = imread("1.jpg", 0);
imshow("原图",srcImage);
if(!srcImage.data) {cout << "fail to load image" << endl; return 0;}
system("color 1F");
ShowHelpText();
//【2】定义变量
MatND dstHist; // 在cv中用CvHistogram *hist = cvCreateHist
int dims = 1;
float hranges[] = {0, 255};
const float *ranges[] = {hranges}; // 这里需要为const类型
int size = 256;
int channels = 0;
//【3】计算图像的直方图
calcHist(&srcImage, 1, &channels, Mat(), dstHist, dims, &size, ranges); // cv 中是cvCalcHist
int scale = 1;
Mat dstImage(size * scale, size, CV_8U, Scalar(0));
//【4】获取最大值和最小值
double minValue = 0;
double maxValue = 0;
minMaxLoc(dstHist,&minValue, &maxValue, 0, 0); // 在cv中用的是cvGetMinMaxHistValue
//【5】绘制出直方图
int hpt = saturate_cast<int>(0.9 * size);
for(int i = 0; i < 256; i++)
{
float binValue = dstHist.at<float>(i); // 注意hist中是float类型 而在OpenCV1.0版中用cvQueryHistValue_1D
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;
}
运行此详细注释的代码,可以得到如下图所示的运行结果。
9.2.5 示例程序:绘制RGB三色直方图
上文我们讲解的是单个分量的一维直方图的绘制,接下来看看如何分别绘制图像的RGB三色直方图。详细注释的代码如下。
/* @File : 81_histogram3.cpp
* @Brief : 示例程序81
* @Details : 绘制RGB三色分量的直方图
* @Date : 2015-11-01
* @OpenCV Version : 4.8.0
* @Development Tools : Windows 11 64bit && Visual Studio 2017
* @Modify : 2024-05-01
*/
#include <opencv2/opencv.hpp>
#include <opencv2/imgproc/imgproc.hpp>
using namespace cv;
//-----------------------------------【ShowHelpText( )函数】-----------------------------
// 描述:输出一些帮助信息
//----------------------------------------------------------------------------------------------
void ShowHelpText()
{
//输出欢迎信息和OpenCV版本
printf("\n\n\t\t\t非常感谢购买《OpenCV3编程入门》一书!\n");
printf("\n\n\t\t\t此为本书OpenCV3版的第81个配套示例程序\n");
printf("\n\n\t\t\t 当前使用的OpenCV版本为:" CV_VERSION );
printf("\n\n ----------------------------------------------------------------------------\n");
}
int main( )
{
//【1】载入素材图并显示
Mat srcImage;
srcImage=imread("1.jpg");
imshow( "素材图", srcImage );
system("color 3F");
ShowHelpText();
//【2】参数准备
int bins = 256;
int hist_size[] = {bins};
float range[] = { 0, 256 };
const float* ranges[] = { range};
MatND redHist,grayHist,blueHist;
int channels_r[] = {0};
//【3】进行直方图的计算(红色分量部分)
calcHist( &srcImage, 1, channels_r, Mat(), //不使用掩膜
redHist, 1, hist_size, ranges,
true, false );
//【4】进行直方图的计算(绿色分量部分)
int channels_g[] = {1};
calcHist( &srcImage, 1, channels_g, Mat(), // do not use mask
grayHist, 1, hist_size, ranges,
true, // the histogram is uniform
false );
//【5】进行直方图的计算(蓝色分量部分)
int channels_b[] = {2};
calcHist( &srcImage, 1, channels_b, Mat(), // do not use mask
blueHist, 1, hist_size, ranges,
true, // the histogram is uniform
false );
//-----------------------绘制出三色直方图------------------------
//参数准备
double maxValue_red,maxValue_green,maxValue_blue;
minMaxLoc(redHist, 0, &maxValue_red, 0, 0);
minMaxLoc(grayHist, 0, &maxValue_green, 0, 0);
minMaxLoc(blueHist, 0, &maxValue_blue, 0, 0);
int scale = 1;
int histHeight=256;
Mat histImage = Mat::zeros(histHeight,bins*3, CV_8UC3);
//正式开始绘制
for(int i=0;i<bins;i++)
{
//参数准备
float binValue_red = redHist.at<float>(i);
float binValue_green = grayHist.at<float>(i);
float binValue_blue = blueHist.at<float>(i);
int intensity_red = cvRound(binValue_red*histHeight/maxValue_red); //要绘制的高度
int intensity_green = cvRound(binValue_green*histHeight/maxValue_green); //要绘制的高度
int intensity_blue = cvRound(binValue_blue*histHeight/maxValue_blue); //要绘制的高度
//绘制红色分量的直方图
rectangle(histImage,Point(i*scale,histHeight-1),
Point((i+1)*scale - 1, histHeight - intensity_red),
Scalar(255,0,0));
//绘制绿色分量的直方图
rectangle(histImage,Point((i+bins)*scale,histHeight-1),
Point((i+bins+1)*scale - 1, histHeight - intensity_green),
Scalar(0,255,0));
//绘制蓝色分量的直方图
rectangle(histImage,Point((i+bins*2)*scale,histHeight-1),
Point((i+bins*2+1)*scale - 1, histHeight - intensity_blue),
Scalar(0,0,255));
}
//在窗口中显示出绘制好的直方图
imshow( "图像的RGB直方图", histImage );
waitKey(0);
return 0;
}
运行此程序,可以得到如下图所示的结果
至此,图像的一维和二维直方图,以及图像的三色直方图我们都通过实例进行了演示和讲解。
9.3 直方图对比
对于直方图来说,一个不可或缺的工具便是用某些具体的标准来比较两个直方图的相似度。要对两个直方图(比如说H1和H2)进行比较,首先必须选择一个衡量直方图相似度的对比标准(d(H1, H2))。在OpenCV 2.X中,我们用compareHist()函数来对比两个直方图的相似度,而此函数的返回值就是d(H1, H2)。
9.3.1 对比直方图:compareHist()函数
compareHist()函数用于对两幅直方图进行比较。有两个版本的C++原型,如下。
它们的前两个参数是要比较的大小相同的直方图,第三个变量是所选择的距离标准。可采用如下4种方法,比较两个直方图(H1表示第一个,H2表示第二个):
1.相关,Correlation(method=CV_COMP_CORREL)
且N等于直方图中bin(译为“直条”或“组距”)的个数。
2.卡方,Chi-Square(method=CV_COMP_CHISQR)
3.直方图相交,Intersection(method=CV_COMP_INTERSECT)
4.Bhattacharyya距离(method=CV_COMP_BHATTACHARYYA)
这里的Bhattacharyya距离和Hellinger距离相关,也可以写作method=CV_COMP_HELLINGER
此处的宏定义在当前版本的OpenCV3中依然沿用“CV_”前缀,在未来版本中应该会有更改。如需使用,可以分别用int类型的1、2、3、4替代CV_COMP_CORREL、CV_COMP_CHISQR、CV_COMP_INTERSECT、CV_COMP_BHATTACHARYYA这四个宏。或者“#include<cv.h>”加入cv.h头文件。
9.3.2 示例程序:直方图对比
此次的示例程序为大家演示了如何用compareHist()函数进行直方图对比。代码中的MatND类是用于存储直方图的一种数据结构,用法简单,在这里就不多做讲解,大家看到详细注释的示例程序就会明白。
/* @File : 82_compareHist.cpp
* @Brief : 示例程序82
* @Details : 直方图对比
* @Date : 2015-11-01
* @OpenCV Version : 4.8.0
* @Development Tools : Windows 11 64bit && Visual Studio 2017
* @Modify : 2024-05-01
*/
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
using namespace cv;
//-----------------------------------【ShowHelpText( )函数】-----------------------------
// 描述:输出一些帮助信息
//----------------------------------------------------------------------------------------------
static void ShowHelpText()
{
//输出欢迎信息和OpenCV版本
printf("\n\n\t\t\t非常感谢购买《OpenCV3编程入门》一书!\n");
printf("\n\n\t\t\t此为本书OpenCV3版的第82个配套示例程序\n");
printf("\n\n\t\t\t 当前使用的OpenCV版本为:" CV_VERSION );
printf("\n\n ----------------------------------------------------------------------------\n");
//输出一些帮助信息
printf("\n\n欢迎来到【直方图对比】示例程序~\n\n");
}
int main( )
{
//【0】改变console字体颜色
system("color 2F");
//【1】显示帮助文字
ShowHelpText();
//【1】声明储存基准图像和另外两张对比图像的矩阵( RGB 和 HSV )
Mat srcImage_base, hsvImage_base;
Mat srcImage_test1, hsvImage_test1;
Mat srcImage_test2, hsvImage_test2;
Mat hsvImage_halfDown;
//【2】载入基准图像(srcImage_base) 和两张测试图像srcImage_test1、srcImage_test2,并显示
srcImage_base = imread( "1.jpg",1 );
srcImage_test1 = imread( "2.jpg", 1 );
srcImage_test2 = imread( "3.jpg", 1 );
//显示载入的3张图像
imshow("基准图像",srcImage_base);
imshow("测试图像1",srcImage_test1);
imshow("测试图像2",srcImage_test2);
// 【3】将图像由BGR色彩空间转换到 HSV色彩空间
cvtColor( srcImage_base, hsvImage_base, COLOR_BGR2HSV );
cvtColor( srcImage_test1, hsvImage_test1, COLOR_BGR2HSV );
cvtColor( srcImage_test2, hsvImage_test2, COLOR_BGR2HSV );
//【4】创建包含基准图像下半部的半身图像(HSV格式)
hsvImage_halfDown = hsvImage_base( Range( hsvImage_base.rows/2, hsvImage_base.rows - 1 ), Range( 0, hsvImage_base.cols - 1 ) );
//【5】初始化计算直方图需要的实参
// 对hue通道使用30个bin,对saturatoin通道使用32个bin
int h_bins = 50; int s_bins = 60;
int histSize[] = { h_bins, s_bins };
// hue的取值范围从0到256, saturation取值范围从0到180
float h_ranges[] = { 0, 256 };
float s_ranges[] = { 0, 180 };
const float* ranges[] = { h_ranges, s_ranges };
// 使用第0和第1通道
int channels[] = { 0, 1 };
// 【6】创建储存直方图的 MatND 类的实例:
MatND baseHist;
MatND halfDownHist;
MatND testHist1;
MatND testHist2;
// 【7】计算基准图像,两张测试图像,半身基准图像的HSV直方图:
calcHist( &hsvImage_base, 1, channels, Mat(), baseHist, 2, histSize, ranges, true, false );
normalize( baseHist, baseHist, 0, 1, NORM_MINMAX, -1, Mat() );
calcHist( &hsvImage_halfDown, 1, channels, Mat(), halfDownHist, 2, histSize, ranges, true, false );
normalize( halfDownHist, halfDownHist, 0, 1, NORM_MINMAX, -1, Mat() );
calcHist( &hsvImage_test1, 1, channels, Mat(), testHist1, 2, histSize, ranges, true, false );
normalize( testHist1, testHist1, 0, 1, NORM_MINMAX, -1, Mat() );
calcHist( &hsvImage_test2, 1, channels, Mat(), testHist2, 2, histSize, ranges, true, false );
normalize( testHist2, testHist2, 0, 1, NORM_MINMAX, -1, Mat() );
//【8】按顺序使用4种对比标准将基准图像的直方图与其余各直方图进行对比:
for( int i = 0; i < 4; i++ )
{
//进行图像直方图的对比
int compare_method = i;
double base_base = compareHist( baseHist, baseHist, compare_method );
double base_half = compareHist( baseHist, halfDownHist, compare_method );
double base_test1 = compareHist( baseHist, testHist1, compare_method );
double base_test2 = compareHist( baseHist, testHist2, compare_method );
//输出结果
printf( " 方法 [%d] 的匹配结果如下:\n\n 【基准图 - 基准图】:%f, 【基准图 - 半身图】:%f,【基准图 - 测试图1】: %f, 【基准图 - 测试图2】:%f \n-----------------------------------------------------------------\n", i, base_base, base_half , base_test1, base_test2 );
}
printf( "检测结束。" );
waitKey(0);
return 0;
}
学习完详细注释的代码,让我们一起看看程序的运行截图。
需要注意的是,在上述代码中还会将基准图像与它自身及其半身图像进行对比。而我们知道,当将基准图像直方图及其自身进行对比时,会产生完美的匹配;当与来源于同一样的背景环境的半身图对比时,应该会有比较高的相似度;当与来自不同亮度光照条件的其余两张测试图像对比时,匹配度应该不是很好。输出的匹配结果如下图所示。
其中的方法0至3,分别表示之前讲过的Correlation、Chi-square、Intersection、Bhattacharyya对比标准。其中,对于Correlation(方法0)和Intersection(方法2)标准,值越大表示相似度越高。可以发现,【基准图—基准图】的匹配数值结果相对于其他几种匹配方式是最大的,符合实际情况。【基准图—半身图】的匹配结果次大,正如我们预料。而【基准—测试图1】和【基准图—测试图2】的匹配结果却不尽人意,同样和之前的预料吻合。
9.4 反向投影
9.4.1 引言
如果一幅图像的区域中显示的是一种结构纹理或者一个独特的物体,那么这个区域的直方图可以看作一个概率函数,其表现形式是某个像素属于该纹理或物体的概率。
而反向投影(back projection)就是一种记录给定图像中的像素点如何适应直方图模型像素分布方式的一种方法。
简单的讲,所谓反向投影就是首先计算某一特征的直方图模型,然后使用模型去寻找图像中存在的该特征的方法。
9.4.2 反向投影的工作原理
下面,我们将使用H-S肤色直方图为例来解释反向投影的工作原理。
首先通过之前讲过的求H-S直方图的示例程序,得到如图9.13~9.16所示的H-S肤色直方图。
而我们要做的,就是使用模型直方图(代表手掌的皮肤色调)来检测测试图像中的皮肤区域。以下是检测步骤。
(1)对测试图像中的每个像素(p(i, j)),获取色调数据并找到该色调(hi, j, si, j)在直方图中的bin的位置。
(2)查询模型直方图中对应bin的数值。
(3)将此数值储存在新的反射投影图像中。也可以先归一化直方图数值到0-255范围,这样可以直接显示反射投影图像(单通道图像)。
(4)通过对测试图像中的每个像素采用以上步骤,可以得到最终的反射投影图像。如图9.17所示。
图9.17 最终的反射投影图像
(5)使用统计学的语言进行分析。反向投影中储存的数值代表了测试图像中该像素属于皮肤区域的概率。比如以图9.17为例,亮起的区域是皮肤区域的概率更大,而更暗的区域则表示是皮肤的概率更低。另外,可以注意到,手掌内部和边缘的阴影影响了检测的精度。
反向投影用于在输入图像(通常较大)中查找与特定图像(通常较小或者仅1个像素,以下将其称为模板图像)最匹配的点或者区域,也就是定位模板图像出现在输入图像的位置。9.4.4 反向投影的结果
9.4.3 反向投影的作用
反向投影用于在输入图像(通常较大)中查找与特定图像(通常较小或者仅1个像素,以下将其称为模板图像)最匹配的点或者区域,也就是定位模板图像出现在输入图像的位置。
9.4.4 反向投影的结果
反向投影的结果包含了以每个输入图像像素点为起点的直方图对比结果。可以把它看成是一个二维的浮点型数组、二维矩阵,或者单通道的浮点型图像。
9.4.5 计算反向投影:calcBackProject()函数
calcBackProject()函数用于计算直方图的反向投影。
void calcBackProject( const Mat* images, int nimages,
const int* channels, InputArray hist,
OutputArray backProject, const float** ranges,
double scale = 1, bool uniform = true );
● 第一个参数,const Mat*类型的images,输入的数组(或数组集),它们须为相同的深度(CV_8U或CV_32F)和相同的尺寸,而通道数则可以任意。
● 第二个参数,int类型的nimages,输入数组的个数,也就是第一个参数中存放了多少张“图像”,有几个原数组。
● 第三个参数,const int*类型的channels,需要统计的通道(dim)索引。第一个数组通道从0到images[0].channels()-1,而第二个数组通道从images[0].channels()计算到images[0].channels()+images[1].channels()-1。
● 第四个参数,InputArray类型的hist,输入的直方图。
● 第五个参数,OutputArray类型的backProject,目标反向投影阵列,其须为单通道,并且和image[0]有相同的大小和深度。
● 第六个参数,const float**类型的ranges,表示每一个维度数组(第六个参数dims)的每一维的边界阵列,可以理解为每一维数值的取值范围。
● 第七个参数,double scale,有默认值1,输出的方向投影可选的缩放因子,默认值为1。
● 第八个参数,bool类型的uniform,指示直方图是否均匀的标识符,有默认值true。
9.4.6 通道复制:mixChannels()函数
此函数由输入参数复制某通道到输出参数特定的通道中。有两个版本的C++原型,采用函数注释方式分别介绍如下。
此函数为重排图像通道提供了比较先进的机制。其实,之前我们接触到的split()和merge(),以及cvtColor()的某些形式,都只是mixChannels()的一部分。
下面给出一个示例,将一个4通道的RGBA图像转化为3通道BGR(R通道和B通道交换)和一个单独的Alpha通道的图像。
9.4.7 综合程序:反向投影
下面将给大家展示一个浓缩了本节内容的讲解内容经过详细注释的反向投影示例程序。
/* @File : 83_calcBackProject.cpp
* @Brief : 示例程序83
* @Details : 反向投影示例程序
* @Date : 2015-11-01
* @OpenCV Version : 4.8.0
* @Development Tools : Windows 11 64bit && Visual Studio 2017
* @Modify : 2024-05-01
*/
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
using namespace cv;
#define WINDOW_NAME1 "【原始图】" //为窗口标题定义的宏
Mat g_srcImage; Mat g_hsvImage; Mat g_hueImage;
int g_bins = 30;//直方图组距
//-----------------------------------【全局函数声明部分】--------------------------------------
// 描述:全局函数声明
//-----------------------------------------------------------------------------------------------
static void ShowHelpText();
void on_BinChange(int, void* );
int main( )
{
//【0】改变console字体颜色
system("color 6F");
//【0】显示帮助文字
ShowHelpText();
//【1】读取源图像,并转换到 HSV 空间
g_srcImage = imread( "1.jpg", 1 );
if(!g_srcImage.data ) { printf("读取图片错误,请确定目录下是否有imread函数指定图片存在~! \n"); return false; }
cvtColor( g_srcImage, g_hsvImage, COLOR_BGR2HSV );
//【2】分离 Hue 色调通道
g_hueImage.create( g_hsvImage.size(), g_hsvImage.depth() );
int ch[ ] = { 0, 0 };
mixChannels( &g_hsvImage, 1, &g_hueImage, 1, ch, 1 );
//【3】创建 Trackbar 来输入bin的数目
namedWindow( WINDOW_NAME1 , WINDOW_AUTOSIZE );
createTrackbar("色调组距 ", WINDOW_NAME1 , &g_bins, 180, on_BinChange );
on_BinChange(0, 0);//进行一次初始化
//【4】显示效果图
imshow( WINDOW_NAME1 , g_srcImage );
// 等待用户按键
waitKey(0);
return 0;
}
//-----------------------------------【on_HoughLines( )函数】--------------------------------
// 描述:响应滑动条移动消息的回调函数
//---------------------------------------------------------------------------------------------
void on_BinChange(int, void* )
{
//【1】参数准备
MatND hist;
int histSize = MAX( g_bins, 2 );
float hue_range[] = { 0, 180 };
const float* ranges = { hue_range };
//【2】计算直方图并归一化
calcHist( &g_hueImage, 1, 0, Mat(), hist, 1, &histSize, &ranges, true, false );
normalize( hist, hist, 0, 255, NORM_MINMAX, -1, Mat() );
//【3】计算反向投影
MatND backproj;
calcBackProject( &g_hueImage, 1, 0, hist, backproj, &ranges, 1, true );
//【4】显示反向投影
imshow( "反向投影图", backproj );
//【5】绘制直方图的参数准备
int w = 400; int h = 400;
int bin_w = cvRound( (double) w / histSize );
Mat histImg = Mat::zeros( w, h, CV_8UC3 );
//【6】绘制直方图
for( int i = 0; i < g_bins; i ++ )
{ rectangle( histImg, Point( i*bin_w, h ), Point( (i+1)*bin_w, h - cvRound( hist.at<float>(i)*h/255.0 ) ), Scalar( 100, 123, 255 ), -1 ); }
//【7】显示直方图窗口
imshow( "直方图", histImg );
}
//-----------------------------------【ShowHelpText( )函数】----------------------------------
// 描述:输出一些帮助信息
//----------------------------------------------------------------------------------------------
static void ShowHelpText()
{
//输出欢迎信息和OpenCV版本
printf("\n\n\t\t\t非常感谢购买《OpenCV3编程入门》一书!\n");
printf("\n\n\t\t\t此为本书OpenCV3版的第83个配套示例程序\n");
printf("\n\n\t\t\t 当前使用的OpenCV版本为:" CV_VERSION );
printf("\n\n ----------------------------------------------------------------------------\n");
//输出一些帮助信息
printf("\n\n\t欢迎来到【反向投影】示例程序\n\n");
printf("\n\t请调整滑动条观察图像效果\n\n");
}
编译并运行此程序,可以通过滑动条的调节改变直方图组距(bin)的值,得到不同的反向投影效果图。如下图所示。
9.5 模板匹配
9.5.1 模板匹配的概念与原理
模板匹配是一项在一幅图像中寻找与另一幅模板图像最匹配(相似)部分的技术。在OpenCV2和OpenCV3中,模板匹配由MatchTemplate()函数完成。需要注意,模板匹配不是基于直方图的,而是通过在输入图像上滑动图像块,对实际的图像块和输入图像进行匹配的一种匹配方法。
如图9.23所示,通过一个人脸“图像模板”,在整幅输入图像上移动这张“脸”,来寻找和这张脸相似的最优匹配。
9.5.2 实现模板匹配:matchTemplate()函数
matchTemplate()用于匹配出和模板重叠的图像区域。
void matchTemplate( InputArray image, InputArray templ,
OutputArray result, int method, InputArray mask = noArray() );
● 第一个参数,InputArray类型的image,待搜索的图像,且需为8位或32位浮点型图像。
● 第二个参数,InputArray类型的templ,搜索模板,需和源图片有一样的数据类型,且尺寸不能大于源图像。
● 第三个参数,OutputArray类型的result,比较结果的映射图像。其必须为单通道、32位浮点型图像.如果图像尺寸是W×H而templ尺寸是w×h,则 此参数result一定是(W-w+1)×(H-h+1).
● 第四个参数,int类型的method,指定的匹配方法,OpenCV为我们提供了如下6种图像匹配方法可供使用。
1.平方差匹配法method=TM_SQDIFF
这类方法利用平方差来进行匹配,最好匹配为0。而若匹配越差,匹配值则越大。
2.归一化平方差匹配法method=TM_SQDIFF_NORMED
3.相关匹配法method=TM_CCORR
这类方法采用模板和图像间的乘法操作,所以较大的数表示匹配程度较高,0标识最坏的匹配效果。
4.归一化相关匹配法method=TM_CCORR_NORMED
5.系数匹配法method=TM_CCOEFF
这类方法将模版对其均值的相对值与图像对其均值的相关值进行匹配,1表示完美匹配,-1表示糟糕的匹配,而0表示没有任何相关性(随机序列)。
其中:
6.化相关系数匹配法method=TM_CCOEFF_NORMED
上述的6个宏,在OpenCV2依然是可以加上“CV_”前缀,分别写作:CV_TM_SQDIFF、CV_TM_SQDIFF_NORMED、CV_TM_CCORR、CV_TM_CCORR_NORMED、CV_TM_CCOEFF、CV_TM_CCOEFF_NORMED。
通常,随着从简单的测量(平方差)到更复杂的测量(相关系数),我们可获得越来越准确的匹配。然而,这同时也会以越来越大的计算量为代价。比较科学的办法是对所有这些方法多次测试实验,以便为自己的应用选择同时兼顾速度和精度的最佳方案。
9.5.3 综合示例:模板匹配
讲解完成基本概念和函数用法,下面依然是放出一个经过详细注释的示例程序源代码,演示了如何种不同的模板匹配方法对人脸进行检测。
/* @File : 84_matchTemplate.cpp
* @Brief : 示例程序84
* @Details : 模板匹配示例
* @Date : 2015-11-01
* @OpenCV Version : 4.8.0
* @Development Tools : Windows 11 64bit && Visual Studio 2017
* @Modify : 2024-05-01
*/
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
using namespace cv;
#define WINDOW_NAME1 "【原始图片】" //为窗口标题定义的宏
#define WINDOW_NAME2 "【匹配窗口】" //为窗口标题定义的宏
Mat g_srcImage; Mat g_templateImage; Mat g_resultImage;
int g_nMatchMethod;
int g_nMaxTrackbarNum = 5;
void on_Matching( int, void* );
static void ShowHelpText( );
int main( )
{
//【0】改变console字体颜色
system("color 1F");
//【0】显示帮助文字
ShowHelpText();
//【1】载入原图像和模板块
g_srcImage = imread( "1.jpg", 1 );
g_templateImage = imread( "2.jpg", 1 );
//【2】创建窗口
namedWindow( WINDOW_NAME1, WINDOW_AUTOSIZE );
namedWindow( WINDOW_NAME2, WINDOW_AUTOSIZE );
//【3】创建滑动条并进行一次初始化
createTrackbar( "方法", WINDOW_NAME1, &g_nMatchMethod, g_nMaxTrackbarNum, on_Matching );
on_Matching( 0, 0 );
waitKey(0);
return 0;
}
//-----------------------------------【on_Matching( )函数】--------------------------------
// 描述:回调函数
//-------------------------------------------------------------------------------------------
void on_Matching( int, void* )
{
//【1】给局部变量初始化
Mat srcImage;
g_srcImage.copyTo( srcImage );
//【2】初始化用于结果输出的矩阵
int resultImage_rows = g_srcImage.rows - g_templateImage.rows + 1;
int resultImage_cols = g_srcImage.cols - g_templateImage.cols + 1;
g_resultImage.create(resultImage_rows,resultImage_cols, CV_32FC1);
//【3】进行匹配和标准化
matchTemplate( g_srcImage, g_templateImage, g_resultImage, g_nMatchMethod );
normalize( g_resultImage, g_resultImage, 0, 1, NORM_MINMAX, -1, Mat() );
//【4】通过函数 minMaxLoc 定位最匹配的位置
double minValue; double maxValue; Point minLocation; Point maxLocation;
Point matchLocation;
minMaxLoc( g_resultImage, &minValue, &maxValue, &minLocation, &maxLocation, Mat() );
if( g_nMatchMethod == TM_SQDIFF || g_nMatchMethod == TM_SQDIFF_NORMED )
{ matchLocation = minLocation; }
else
{ matchLocation = maxLocation; }
//【6】绘制出矩形,并显示最终结果
rectangle( srcImage, matchLocation, Point( matchLocation.x + g_templateImage.cols , matchLocation.y + g_templateImage.rows ), Scalar(0,0,255), 2, 8, 0 );
rectangle( g_resultImage, matchLocation, Point( matchLocation.x + g_templateImage.cols , matchLocation.y + g_templateImage.rows ), Scalar(0,0,255), 2, 8, 0 );
imshow( WINDOW_NAME1, srcImage );
imshow( WINDOW_NAME2, g_resultImage );
}
//-----------------------------------【ShowHelpText( )函数】----------------------------------
// 描述:输出一些帮助信息
//----------------------------------------------------------------------------------------------
static void ShowHelpText()
{
//输出欢迎信息和OpenCV版本
printf("\n\n\t\t\t非常感谢购买《OpenCV3编程入门》一书!\n");
printf("\n\n\t\t\t此为本书OpenCV3版的第84个配套示例程序\n");
printf("\n\n\t\t\t 当前使用的OpenCV版本为:" CV_VERSION );
printf("\n\n ----------------------------------------------------------------------------\n");
//输出一些帮助信息
printf("\t欢迎来到【模板匹配】示例程序~\n");
printf("\n\n\t请调整滑动条观察图像效果\n\n");
printf( "\n\t滑动条对应的方法数值说明: \n\n"
"\t\t方法【0】- 平方差匹配法(SQDIFF)\n"
"\t\t方法【1】- 归一化平方差匹配法(SQDIFF NORMED)\n"
"\t\t方法【2】- 相关匹配法(TM CCORR)\n"
"\t\t方法【3】- 归一化相关匹配法(TM CCORR NORMED)\n"
"\t\t方法【4】- 相关系数匹配法(TM COEFF)\n"
"\t\t方法【5】- 归一化相关系数匹配法(TM COEFF NORMED)\n" );
}
运行此程序,可以得到三个窗口。说明窗口、附有滑动条的原始图窗口以及根据滑动条数值变化的匹配效果图窗口。首先,让我们通过说明窗口了解此程序的使用方法。如图9.25所示。
图9.25 序说明窗口
接着,让我们一起看看匹配的效果图。
可以发现,除了相关匹配法(TM CCORR)得到了错误的匹配结果之外,其他的5种匹配方式结果都匹配较为准确。
9.6 本章小结
本章我们学习了广泛运用于很多计算机视觉运用当中的直方图,而简单点说,直方图就是对数据进行统计的一种方法。然后还讲到了反向投影和模板匹配。所谓反向投影就是首先计算某一特征的直方图模型,最后使用模型去寻找图像中存在的该特征的方法。而模板匹配是一项在一幅图像中寻找与另一幅模板图像最匹配(相似)部分的技术。
本章核心函数清单
本章示例程序清单