直方图与匹配
- 1. 图像直方图概述
- 2.直方图的计算与绘制
- 2.1 计算直方图:calcHist()函数
- 2.2 找寻最值:minMAxLoc()函数
- 2.3 示例程序:绘制H-S直方图
- 3.直方图对比
- 3.1 对比直方图:compareHist()函数
- 3.2 示例程序:直方图对比
- 4.反射投影
- 4.1 什么是反向投影
- 4. 2 反向投影是如何工作的
- 4.3 反向投影的作用
- 4.4 计算反向投影:calcBackProject()函数
- 4.5 复制通道:mixChannels()函数
- 4.6 综合程序:反向投影
- 5. 模板匹配
- 5.1 模板匹配工作原理
- 5.2 实现模板匹配:matchTemplate()函数
1. 图像直方图概述
直方图广泛运用于很多计算机视觉运用当中,通过标记帧与帧之间显著的边缘和颜色的统计变化,来检测视频中场景中场景的变化。在每个兴趣点设置一个有相近特征的直方图所构成"标签",用以确定图像中的兴趣点。边缘、色彩、角度等方图构成了可以被传递给目标识别分类器的一个通用特征类型。色彩和边缘的直方图序列还可以用来识别网络视频是否被复制。
其实,简单点说,直方图就是对数据进行统计的一种方法,并且将统计值组织到一系列事先定义好的bin当中。其中,bin为直方图中经常用到的一个概念,可翻译为“直条”或“组距”,其数值是从数据中计算出的特征统计量,这些数据可以是诸如梯度、方向、色彩或任何其他特征。且无论如何,直方图获得的是数据分布的统计图。通常直方图的维数要低于原始数据。总而言之,直方图是计算机视觉中最经典的工具之一。
其实,简单点说,直方图就是对数据进行统计的一种方法,并且将统计值组织到一系列事先定义好的bin当中。其中,bin为直方图中经常用到的一个概念,可翻译为“直条”或“组距”,其数值是从数据中计算出的特征统计量,这些数据可以是诸如梯度、方向、色彩或任何其他特征。且无论如何,直方图获得的是数据分布的统计图。通常直方图的维数要低于原始数据。总而言之,直方图是计算机视觉中最经典的工具之一。
在统计学中,直方图(Histogram)是一种对数据分布情况的图形表示,是一种二维统计图表,它的两个坐标分别是统计样本和该样本对应的某个属性的度量。
我们在图像变换的那一章中讲过直方图的均衡化,它是通过拉伸像素强度分布范围来增强图像对比度的一种方法。大家在自己的心目中应该已经对直方图有一定的理解和认知。下面就来看一看对图像直方图比较书面化的解释。
图像直方图(Image Histogram)是用以表示数字图像中亮度分布的直方图,标绘了图像中每个亮度值的像素数。可以借助观察该直方图了解需要如何调整亮度分布。这种直方图中,横坐标的左侧为纯黑、较暗的区域,而右侧为较亮、纯白的区域。因此,一张较暗图片的图像直方图中的数据多集中于左侧和中间部分,而整体明亮、只有少量阴影的图像则相反。计算机视觉领域常借助图像直方图来实现图像的二值化。
直方图的意义如下。
- 直方图是图像中像素强度分布的图形表达方式。
- 它统计了每一个强度值所具有的像素个数。
上面已经讲到,直方图是对数据的统计集合,并将统计结果分布于一系列预定的bins中。这里的数据不仅仅指的是灰度值,且统计数据可能是任何能有效
描述图像的特征。下面看一个例子,假设有一个矩阵包含一张图像的信息(灰度值0-255),让我们按照某种方式来统计这些数字。既然已知数字的范围包含256个值,于是可以将这个范围分割成子区域(也就是上面讲到bins),如:
然后再统计每一个bini的像素数目。采用这一方法来统计上面的数字矩阵。
以上就是一个说明直方图的用途的简单示例。其实,直方图并不局限于统计颜色灰度,而是可以统计任何图像特征,如梯度、方向等。
让我们具体讲讲直方图的一些术语和细节。
- dims:需要统计的特征的数目。在上例中,dims = 1因为我们仅仅统计了灰度值(灰度图像)。
- bins:每个特征空间子区段的数目,可翻译为“直条”或“组距”。在上例中,bins = 16。
- range:每个特征空间的取值范围。在上例中,range = [0,255]。
2.直方图的计算与绘制
2.1 计算直方图:calcHist()函数
void calcHish(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,输入的数组(或数组集),他们需为相同的深度和相同的尺寸。
- 第二个参数:int类型的nimages,输入数组的个数,也就是第一个参数中存放了多少张图像。
- 第三个参数:const int*类型的channels,用于计算直方图的dims通道的列表。第一个数组通道从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
- 第七个参数:const int*类型的histSize,存放每个维度的直方图尺寸的数组。
- 第八个参数:const float**类型range,每一维度数组(第六个参数dims)的每一维的边界阵列,可以理解为每一维数值的取值范围。
- 第九个参数:表示直方图是否均匀的标志。
- 第十个参数:积累标志。如果它被设置,直方图在开始分配时就不会被清除。这个特性使你能够从几组数组中计算出一个单一的直方图,或者及时更新直方图。
2.2 找寻最值:minMAxLoc()函数
void minMaxLoc(InputArray src, double* minVal, double* maxVal=0,Point* minLoc=0,Point* maxLoc=0,InputArray mask=noArray())
- 第一个参数:输入图像
- 第二个参数:返回最小值的指针;如果不需要,则使用NULL。
- 第三个参数:返回最大值的指针;如果不需要,则使用NULL。
- 第四个参数:返回最小位置的指针;如果不需要,则使用NULL。
- 第五个参数:返回最大位置的指针;如果不需要,则使用NULL。
- 第六个参数:可选的掩码,用于选择一个子阵列。
2.3 示例程序:绘制H-S直方图
注意:色调(Hue)、饱和度(Saturation)。所以H-S直方图就是色调——饱和度直方图。
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main() {
Mat srcImage, hsvImage;
srcImage = imread("../../image/1.tif");
cvtColor(srcImage, hsvImage, COLOR_BGR2HSV);
//参数准备
//将色调量化为30个等级,将饱和度量化为32个等级
int hueBinNum = 30; //色调的直方图直条数量
int saturationBinNum = 32; //饱和度的直方图直条数量
int histSize[] = { hueBinNum, saturationBinNum };
//定义色调的范围为0-179
float hueRnages[] = { 0,180 };
//定义饱和度的变化范围为0-255
float saturationRanges[] = { 0,256 };
const float* ranges[] = { hueRnages,saturationRanges };
MatND dstHist;
//参数准备,calHist函数中将计算第0通道和第1通道的直方图
int channels[] = { 0,1 };
calcHist(&hsvImage,//输入的数组
1,//数组个数为1
channels,
Mat(),//不适掩膜
dstHist,
2,//需要计算的直方图的维度为2
histSize,//存放每个维度的直方图尺寸的数组
ranges,//每一维数值的取值范围数组
true,//指示直方图是否均匀的标识符
false//累计标识符
);
double maxValue = 0;
minMaxLoc(dstHist, 0, &maxValue, 0, 0);
int scale = 10;
Mat histImage = Mat::zeros(saturationBinNum * scale, hueBinNum * scale, CV_8UC3);
//进行直方图绘制
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(histImage, Point(hue * scale, saturation * scale),
Point((hue+1)*scale-1,(saturation+1)*scale-1),Scalar::all(intensity),FILLED);
}
}
imshow("素材图", srcImage);
imshow("H-s直方图", histImage);
waitKey();
}
3.直方图对比
3.1 对比直方图:compareHist()函数
compareHist()函数用于对两幅直方图进行比较。与两个版本的C++原型。
double compareHist(InputArray H1, InputArray H2, int method)
double compareHist(const SparseMat &H1, const SparseMat &H2)
- 第一个参数:第一个比较直方图。
- 第二个参数:第二个比较直方图,大小与H1相同
- 第三个参数:比较方法
3.2 示例程序:直方图对比
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main() {
Mat srcImage_base, hsvImage_base;
Mat srcImage_test1, hsvImage_test1;
Mat srcImage_test2, hsvImage_test2;
Mat hsvImage_halfDown;
//载入基准图像和两张测试图像
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);
//将图像有BGR色彩空间转换到HSV色彩空间
cvtColor(srcImage_base, hsvImage_base, COLOR_BGR2HSV);
cvtColor(srcImage_test1, hsvImage_test1, COLOR_BGR2HSV);
cvtColor(srcImage_test2, hsvImage_test2, COLOR_BGR2HSV);
//创建包含基准图像下半部的半身图像
hsvImage_halfDown = hsvImage_base(Range(hsvImage_base.rows / 2,
hsvImage_base.rows - 1), Range(0, hsvImage_base.cols - 1));
//初始化计算直方图需要的实参
int h_bins = 50;
int s_bins = 60;
int histSize[] = { h_bins,s_bins };
float h_range[] = { 0,256 };
float s_range[] = { 0,180 };
const float* range[] = { h_range,s_range };
int channels[] = { 0,1 };
//创建存储直方图的MatND类的实例
MatND baseHist;
MatND halfDownHist;
MatND testHist1;
MatND testHist2;
//计算基准图像,两张测试图像,半身基准图像的HSV直方图
calcHist(&hsvImage_base, 1, channels, Mat(), baseHist, 2, histSize,
range, true, false);
normalize(baseHist, baseHist, 0, 1, NORM_MINMAX, -1);
calcHist(&hsvImage_halfDown, 1, channels, Mat(), halfDownHist, 2, histSize,
range, true, false);
normalize(halfDownHist, halfDownHist, 0, 1, NORM_MINMAX, -1);
calcHist(&hsvImage_test1, 1, channels, Mat(), testHist1, 2, histSize,
range, true, false);
normalize(testHist1, testHist1, 0, 1, NORM_MINMAX, -1);
calcHist(&hsvImage_test2, 1, channels, Mat(), testHist2, 2, histSize,
range, true, false);
normalize(testHist2, testHist2, 0, 1, NORM_MINMAX, -1);
//按顺序使用四种对比标准图像的直方图与其余各直方图进行对比
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);
//输出结果
cout << "\n方法" << i+1 << "匹配结果如下";
cout << "\n【基准图-基准图】" << base_base << " ";
cout << "\n【基准图-半身图】" << base_half << " ";
cout << "\n【基准图-测试图1】" << base_test1 << " ";
cout << "\n【基准图-测试图2】" << base_test2 << " ";
}
waitKey();
return 0;
}
4.反射投影
4.1 什么是反向投影
反向投影是一种记录给定图像的像素与直方图模型中的像素分布的吻合程度的方法。
说得简单些:对于Back Projection,你计算一个特征的直方图模型,然后用它来寻找图像中的这个特征。
应用实例:如果你有一个肉色的直方图(例如,色相-饱和度直方图),那么你可以用它来寻找图像中的肉色区域。
4. 2 反向投影是如何工作的
我们用皮肤的例子来解释这个问题:
假设你已经根据下面的图片得到了一个皮肤直方图(色相-饱和度)。这个直方图除了将是我们的模型直方图(我们知道它代表了皮肤色调的一个样本)。你应用了一些遮罩,只捕捉皮肤区域的直方图:
现在,让我们想象一下,你得到另一个像下面这样的手部图像(测试图像):(连同其各自的直方图)
我们要做的是使用我们的模型直方图(我们知道它代表皮肤色调)来检测测试图像中的皮肤区域。以下是具体步骤
- 在测试图像的每个像素(即p(i,j))中,收集数据并找到该像素对应的bin位置(即(hi,j,si,j))。
- 在对应的bin - (hi,j,si,j)中查询模型直方图,并读取bin值。
- 将这个bin值存储在一个新的图像中(BackProjection)。另外,你可以考虑先将模型直方图归一化,这样测试图像的输出就可以为你所见。
- 应用上述步骤,我们得到以下测试图像的BackProjection图像:
- 在统计学方面,根据我们使用的模型直方图,BackProjection中存储的值代表了测试图像中一个像素属于皮肤区域的概率。例如,在我们的测试图像中,较亮的区域更有可能是皮肤区域(因为它们实际上是),而较暗的区域的概率较小(注意这些 "暗 "的区域属于有一些阴影的表面,这反过来影响了检测)。
4.3 反向投影的作用
反向投影用于在输入图像中找与特定图像最匹配的点或者区域,也就是定位模板图像出现在输入图像的位置。
4.4 计算反向投影:calcBackProject()函数
void calcBackProject(const Mat* images, int nimages, const int* channels, InputArray hist, OutputArray backProject, const float** ranges, double scale=1, bool uniform=true)
- 第一个参数:源数组。它们都应该有相同的深度,CV_8U、CV_16U或CV_32F,而且大小相同。它们中的每一个都可以有任意数量的通道。
- 第二个参数:源图像的数量。
- 第三个参数:用来计算反向投影的通道列表。通道的数量必须与直方图的维度一致。第一个数组通道从0到images[0].channels()-1,第二个数组通道从images[0].channels()到images[0].channels() + images[1].channels()-1,以此类推。
- 第四个参数:输入直方图,可以是密集的或稀疏的。
- 第五个参数:目标反向投影阵列,其须为单通道,并且和image[0]有相同的大小和深度。
- 第六个参数:const float**类型的ranges,表示每一维度数组的每一维的边界阵列,可以理解为每一维数值的取值范围。
- 第七个参数:输出的方向投影可选地缩放因子
- 第八个参数:指示直方图是否均匀的标识符
4.5 复制通道:mixChannels()函数
将输入阵列中的指定通道复制到输出阵列的指定通道。
4.6 综合程序:反向投影
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
#define WINDOW_NAME1 "原始图"
Mat g_srcImage, g_hsvImage, g_hueImage;
int g_bins = 30;
//回调函数
void on_BinChange(int, void*) {
MatND hist;
int histSize = MAX(g_bins, 2);
float hue_range[] = { 0,180 };
const float* ranges = { hue_range };
calcHist(&g_hueImage,1,0,Mat(),hist,1,&histSize,&ranges,true,false);
normalize(hist, hist, 0, 255, NORM_MINMAX, -1);
//计算反向投影
MatND backproj;
calcBackProject(&g_hueImage, 1, 0, hist, backproj, &ranges, 1, true);
imshow("反向投影图", backproj);
//绘制直方图的参数准备
int w = 400, h = 400;
int bin_w = cvRound((double)w / histSize);
Mat histImg = Mat::zeros(w, h, CV_8UC3);
//绘制直方图
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);
}
imshow("直方图", histImg);
}
int main() {
g_srcImage = imread("../../image/1.tif");
cvtColor(g_srcImage, g_hsvImage, COLOR_BGR2HSV);
g_hueImage.create(g_hsvImage.size(), g_hsvImage.depth());
int ch[] = { 0,0 };
mixChannels(&g_hsvImage, 1, &g_hueImage, 1, ch, 1);
namedWindow(WINDOW_NAME1);
createTrackbar("色调组距", WINDOW_NAME1, &g_bins, 180, on_BinChange);
on_BinChange(0, 0);
imshow(WINDOW_NAME1, g_srcImage);
waitKey();
}
5. 模板匹配
模板匹配是一种寻找图像中与模板图像(补丁)相匹配(相似)的区域的技术。
虽然补丁必须是一个矩形,但可能并非所有的矩形都是相关的。在这种情况下,可以用一个遮罩来隔离应该用来寻找匹配的那部分补丁。
5.1 模板匹配工作原理
我们需要两个主要组成部分:
- 源图像(I):我们希望在其中找到与模板图像相匹配的图像
- 模板图像(T):将与源图像进行比较的补丁图像
我们的目标是检测最高的匹配区域:
为了识别匹配区域,我们必须通过滑动模板图像与源图像进行比较:
通过滑动,我们的意思是每次移动一个像素的补丁(从左到右,从上到下)。在每一个位置,都会计算出一个度量,因此它代表了该位置的匹配度有多 "好 "或 “坏”(或者该补丁与源图像的那个特定区域有多相似)。
对于T在I上的每一个位置,你把度量值存储在结果矩阵R中:
上面的图片是用TM_CCORR_NORMED指标滑动补丁的结果R。最亮的位置表示最高的匹配。正如你所看到的,红圈标记的位置可能是数值最高的位置,所以这个位置(以该点为角,宽度和高度等于补丁图像所形成的矩形)被认为是匹配的。
在实践中,我们在R矩阵中定位最高值(或更低,取决于匹配方法的类型),使用函数minMaxLoc()。
5.2 实现模板匹配:matchTemplate()函数
- 第一个参数:正在运行搜索的图像。它必须是8位或32位浮点。
- 第二个参数:搜索模板,需和源图像有一样的数据类型,且大小不能超过源模板
- 第三个参数:比较结果的映射图像。它必须是单通道的32位浮点。如果图像是W×H,模板是w×h,那么结果是(W-w+1)×(H-h+1) 。
- 第四个参数:指定比较方法的参数
- 第五个参数:可选的掩码。它必须具有与templ相同的大小。它必须具有与模板相同的通道数,或者只有一个通道,然后用于所有模板和图像通道。如果数据类型是CV_8U,掩码被解释为二进制掩码,意味着只有掩码为非零的元素被使用,并且保持不变,与实际掩码值无关(权重等于1)。对于数据 tpye CV_32F,掩码值被用作权重。