OpenCV-9-直方图和模板匹配

1 摘要

在分析图像、物体和视频信息的时候,我们通常使用直方图来表示我们关注的信息。直方图可以描述很多不同的信息,如物体的颜色分布,物体的边缘梯度模板,假设的物体位置的概率分布。下图简单演示了如何使用直方图进行快速的手势识别。

在该示例中模型手的边缘梯度分布被分为5个不同的组,分别表示上、下、左、右和OK手势。通过网络摄像头分析用户的手势可以实现用户通过手势控制视频的播放。在获取到每一帧图像后,首先分析感兴趣的颜色区域,这里是用户的手,然计算其边缘的梯度直方图,再将其和已知的5类手势直方图模型进行匹配,找到匹配程度最高的模型,从而达到手势识别的目的。

图中的竖直细长矩形条表示当前帧的兴趣区域边缘直方图和已有手势模型直方图的匹配程度,水平灰色线条表示匹配程度的阈值,即只有当匹配程度高于此值时这种匹配关系才是有效的。

在很多计算机视觉程序中都使用到了直方图,如视频数据中每帧画面的边缘和颜色统计直方图发生显著变化表示场景的切换。你也可以通过兴趣点附近特征的直方图为其添加标签,从而识别兴趣点。边缘、颜色和角点等直方图形成了目标的通用特征,这些特征可以被分类器使用从而识别目标对象。颜色和边缘直方图的序列也可以被用于鉴定视频是否复制于网络。类似的应用实例还有很多,总之直方图是计算机视觉的经典工具。

直方图只是简单的将原始数据分到预先定义好的多个组中,并计算每个组内的样本数量。也可以先计算出原始数据中的梯度幅度、方向和颜色等特征,然后再对这些特征计数。不管是哪一种情况,直方图获得的都是原始数据分布的统计图像。通常直方图的维度都比原始数据更低,如下图中左上图展示了一个二维分布的样本集合,通过右上图中定义的组统计每个组内样本的数量,则得到右下图的一维统计直方图。

由于直方图可以处理任意含义的原始数据,因此它是一个用于表示图像信息的便利工具。

在生成直方图时可能会遇到下图演示的问题。左侧的两幅图像的分组过宽,则得到的结果会过于粗糙,并且丢失了数据的分布细节信息。右侧的两幅图像的分组过细,每个组中就没有足够的数据点来准确的表示分布信息,并且会出现一些细小的突刺单元。

2 直方图

新版本的OpenCV使用矩阵cv::Mat和稀疏矩阵cv::SparseMat对象来表示一维或者多维的直方图,同时支持在每个维度上组的分布都可以是均匀的或者非均匀的,另外还提供了一些有用的函数来执行常见的直方图操作。

尽管直方图对象使用了和图像数据同类型的对象,尽管他们底层的数据结构是相同的,但是它们内部的数据含义却不相同。对于n行矩阵表示的直方图(N×1的矩阵除外),每个数据表示的都是在某个维度(以行索引标识)的某组数据(以列索引标识)的统计结果。数据组的索引和其实际含义是分离的,在使用直方图时需要在它们之间转换。例如,在表示人体重的直方图中,数据组可能被分为20到40、40到60、60到80和80到100四组,它们的索引是0、1、2、3,幸运的是OpenCV中的很多函数都会在内部执行这种转换逻辑。

如果需要使用高维的直方图时,通常情况下大部分元素的值都是0,此时可以选择更合适的数据类型cv::SparseMat。实际上该类型的设计原因就是为了更好的处理直方图数据。稠密矩阵的大部分函数都适用于稀疏矩阵,当然我们也会介绍一些值得关注的特例。

2.1 创建直方图

创建直方图的函数原型如下,需要注意直方图的维度和输入矩阵数组的维度无关,仅和其数量及矩阵的数量相关,直方图的每个维度反应的是某个输入矩阵的某个通道上的数据统计结果。当然你也可以指定应当统计哪些输入矩阵的哪些通道。

// images:C语言风格的待处理的矩阵数组,基本数据类型为8U或者32F
// nimages:输入矩阵的个数
// channels:C语言风格的待处理通道列表,下文介绍
// mask:蒙版矩阵,只统计其中非0值对应的原始数据中的点
// hist:统计结果直方图
// dims:直方图的维度,必须小于cv::MAX_DIMS(32)
// histSize:C语言风格数组,直方图在每个维度应当分配的数据组数
// ranges:C语言风格的数组,每个元素都是一个数组,表示在对应维度上的分组策略,
//        具体细节和uniform相关,下文介绍
// uniform:直方图分组策略是均匀分组还是非均匀分组,下文介绍
// accumulate:在生成直方图时是累加(false)还是清空(true)hist内部的数据
void cv::calcHist(const cv::Mat* images, int nimages, const int* channels,
                  cv::InputArray mask, cv::OutputArray hist, int dims,
                  const int* histSize, const float** ranges, bool uniform = true,
                  bool accumulate = false);

// 和前一个函数类似,但是输出数据类型为稀疏矩阵
void cv::calcHist(const cv::Mat* images, int nimages, const int* channels,
                  cv::InputArray mask, cv::SparseMat& hist, int dims,
                  const int* histSize, const float** ranges, bool uniform = true,
                  bool accumulate =false);

函数cv::calcHist()有三种形式,除了上文列出的形式使用C语言风格的数组作为输入矩阵外,第三种使用STL向量模板容器作为参数类型。

函数的第一个参数images包含了构建直方图需要的nimages个数据矩阵。所有的矩阵尺寸必须相同,但是通道数可以不同,基本事件类型可以是8位或者32位,但是它们应该是相同的。channels指定了输入数组中的哪些通道应当被处理的,通道的索引是按顺序编号的,即images[0]中的第一个通道的索引是0,然后递增,images[1]的通道索引在此基础上继续递增。可以明显看出channels的元素个数和最后得到的直方图的维度相同。

参数ranges表示直方图各个维度上的分组策略,数组的具体含义和参数uniform的取值相关。如果参数uniform设置为true,则每个组的区间是相同的,此时需要在参数ranges中指定每个组的下边界和上边界(不包含),例如ranges[i] = [0, 100.0)。如果参数uniform设置为false,如果在第i维度存在N个分组,则ranges[i]应该包含N+1个元素,其中的第j个元素表示第j-1分组的上界,以及低j分组的下界。如果使用的参数类型为STL的vector <float>,则和c语言数组的区别在于向量类型的参数中数据都是一维的。Ranges内数据的含义如下图。

2.2 基础直方图操作

尽管直方图使用的数据类型是图像数据使用的cv::Mat,但是在OpenCV中对于直方图含义的矩阵支持一些新的操作,在本小节将会介绍这些操作,另外也会介绍如何使用矩阵本身已经支持的函数来完成一些重要的直方图操作。

2.2.1 直方图标准化

通常我们得到直方图数据后需要将其标准化,使直方图的每个维度上的数值都表示的是占整个直方图的比值,其实现方式如下。

// 使用运算符
cv::Mat normalized = my_hist / sum(my_hist)[0];
// 使用函数
cv::normalize(my_hist, my_hist, 1, 0, NORM_L1);
2.2.2 直方图阈值处理

另外一个很常见的操作是你希望阈值化处理一个直方图并丢弃其中所以值低于阈值的元素,此时你可以使用处理标准矩阵的阈值函数,示例代码如下。

cv::threshold(my_hist, my_thresholded_hist, threshold, 0, cv::THRESH_TOZERO);
2.2.3 寻找最大最小值

通常在处理概率分布直方图时,你可能并不想做阈值处理,而是像找到其中的最大或者最小值,OpenCV提供一种函数来实现这个功能,其中处理二维矩阵函数void cv::minMaxLoc()的原型如下。

// 处理二维矩阵的函数
// src:待查询的矩阵
// minVal:最小值,设置为NULL时不会计算
// maxVal:最大值,设置为NULL时不会计算
// minLoc:最小值的位置,设置为NULL时不会计算
// maxLoc:最大值的位置,设置为NULL时不会计算
// mask:蒙板矩阵,数值为0的点对应的原始矩阵在统计时将被忽略
void cv::minMaxLoc(cv::InputArray src, double* minVal, double* maxVal = 0,
                   cv::Point* minLoc = 0, cv::Point* maxLoc = 0,
                   cv::InputArray mask = cv::noArray());

// 处理任意维度稀疏矩阵的函数
void cv::minMaxLoc(const cv::SparseMat& src, double* minVal, double* maxVal = 0,
                   int* minLoc = 0, int* maxLoc = 0,
                   cv::InputArray mask = cv::noArray());

该函数的使用实例如下。提示:对于vector<>数组,可以通过cv::Mat(vec).reshape(1)将其转换为通道数为1的矩阵。

// 寻找二维矩阵表示的直方图中的最大值及其位置
double max_val;
cv::Point max_pt;
cv::minMaxLoc(my_hist, NULL, &max_val, NULL,  &max_pt);

// 寻找任意维度稀疏矩阵表示的直方图中的最大值及其位置
double maxval;
int max_pt[CV_MAX_DIM];
cv::minMaxLoc(my_hist, NULL, &max_val, NULL, max_pt);

函数cv::minMaxIdx()用于寻找任意维度矩阵中的最大最小值以及它们的位置,其函数原型如下。

void cv::minMaxIdx(cv::InputArray src, double* minVal, double* maxVal = 0,
                   int* minLoc = 0, int* maxLoc = 0,
                   cv::InputArray mask = cv::noArray());

需要注意的是如果使用了一维矩阵作为输入函数,参数minLocmaxLoc分配的矩阵仍需要包含两个元素,因为该函数内部会将一维矩阵转换为二维矩阵。如果寻找到的元素索引为k,当输入矩阵尺寸为N✖️1,返回值为(k, 0),当输入矩阵尺寸为1✖️N,返回值为(0, k)。

2.2.4 直方图比较

在某种指定的标准下比较直方图的相似性是一个必不可缺的图像处理工具,该方法由Swain和Ballard发明,并由Schiele和Crowley加以推广。该方法可以有很多应用场景,可以通过该函数比较两幅图像的直方图的相似性完成图像匹配任务,也可以通过直方图比较来搜索模型。后者的做法是比较图像不同子区域和目标模型的直方图,通过匹配程度判断该子区域是否包含模型。其函数原型如下。

// H1:待比较的直方图1
// H2:待比较的直方图2,尺寸需要和H1相同
// method:比较方法,下文介绍
double cv::compareHist(cv::InputArray H1, cv::InputArray H2,
                       int method);

double cv::compareHist(const cv::SparseMat& H1, const cv::SparseMat& H2,
                       int method);

参数method指定了直方图相似的判断标准,可以选择的方法有四种。

相关性方法

定义相关性方法的值为cv::COMP_CORREL,该方法基于皮尔逊(Pearson)相关性系数实现,当H1和H2表示概率分布时,使用该方法非常合适。距离计算的公式如下。

其中

N表示直方图的分组数量,直方图的匹配程度越高,使用该方法的匹配函数返回值越大。1表示完全匹配,-1表示完全不匹配,而0表示两个分布无关系,也就是随机组合。

Chi-square方法

定义Chi-square方法的值为cv::COMP_CHISQR_ALT,该方法基于chi-squared测试统计方法实现,它同样可以检测两个分布的相关性,其函数原型如下。

直方图的匹配程度越高,该方法的到的值越低,完全匹配的结果为0,而完全不匹配的值取决于直方图的大小。

交集法

定义交集法的值为cv::COMP_INTERSECT,该方法基于两个直方图的简单交集实现,其计算公式如下。

直方图的匹配程度越高,该方法计算得到的值越高,如果H1和H2是两个经过标准化处理的直方图,则完全匹配的结果为1,完全不匹配的结果为0。

巴氏距离

定义巴氏距离(Bhattacharyya)的值为cv::COMP_BHATTACHARYYA,也是基于两个分布的重叠部分实现的一种距离计算方法,其公式如下。

直方图的匹配程度越高,该方法计算得到的值越低,完全匹配的结果为0,完全不匹配的结果为1。尽管该方法内部会有一个因子来对直方图进行标准化处理,但是通常情况像你应当自行对输入参数进行标准化处理,因为类似计算直方图交集的概念对于未标准化的直方图是完全没有意义的。

考虑简单的只包含两个分组的一维标准化直方图,模型左侧分组的值为1.0,右侧分组的值为0,则使用上述4种方法计算出的匹配结果如下图所示。仔细观察下图会发现当直方图匹配模型只是简单反转时,即下图的第2和第4行图像,前4种方法计算得到的都是完全不匹配的结果,即使在某种程度上这两种分布仍然有一定相似性。

EMD也是一种距离算法,在处理半匹配时该方法能够得到更准确的结果,下文还会详细的介绍到EMD(Earth Mover‘s Distance)算法,这里暂时先不再讨论。根据该书原作者的经验,使用交集法处理快速粗糙的直方图匹配效果更好,而chi-square方法和巴氏方法精度更高,但是计算成本更高,EMD算法给出的结果是最符合视觉感受的,但是它的计算速度更慢。

2.2.5 直方图创建示例

示例Computation读取了一副图像,将其转换为HSV颜色空间,然后统计器色度分量H和饱和度分量S的分布直方图,其核心代码如下。

int main(int argc, const char * argv[]) {
    // 读取原图
    cv::Mat src = cv::imread(argv[1], cv::IMREAD_COLOR);
    // 构建HSV颜色空间数据
    cv::Mat hsv;
    cv::cvtColor(src, hsv, cv::COLOR_BGR2HSV);

    // 设定二维直方图中的分组策略
    // HSV标准定义色调H取值区域为[0, 360),S和V取值区域为[0, 1],而在OpenCV中为了能够用8位
    // 统一表示,H减半处理及,OpenCV中HSV的H值仅为实际值的一半,因此其取值范围为[0, 180),
    // 而S和V的取值为[0, 255]
    float h_ranges[] = {0, 180};
    float s_ranges[] = {0, 256};
    const float * ranges[] = {h_ranges, s_ranges};
    int histSize[] = {30, 32};
    int ch[] = {0, 1};

    // 计算二维直方图
    cv::Mat hist;
    cv::calcHist(&hsv, 1, ch, cv::noArray(), hist, 2, histSize, ranges, true);
    // 标准化处理直方图
    cv::normalize(hist, hist, 0, 255, cv::NORM_MINMAX);
    
    // 定义单个数据点表示的方块直径
    int scale = 10;
    // 创建显示二维直方图的图像
    cv::Mat hist_img(histSize[0] * scale, histSize[1] * scale, CV_8UC3);
    // 在图像hist_img中绘制scale✖️scale像素的方块表示二维直方图hist中的每个数据点
    for (int h = 0; h < histSize[0]; h++) {
        for (int s = 0; s < histSize[1]; s++) {
            float hval = hist.at<float>(h, s);
            cv::rectangle(hist_img,
                          cv::Rect(h * scale, s * scale, scale, scale),
                          cv::Scalar::all(hval), -1);
        }
    }
    return 0;
}

程序的运行结果如下图。其中左图为原始图像,右图为其色度和饱和度分布直方图,不用为图中的大部分黑色区域感到好奇,这是因为色度和饱和度的分布过于集中引起的。

在很多实际的应用中肤色的颜色直方图很有用,下图包含了不同光照环境下的手掌照片及整张图片的颜色直方图,最左侧一列是手掌的照片,中间列是BGR颜色空间的直方图,而右侧一列是HSV颜色空间的直方图。从图中可以看出随着光照环境的改变,手掌的肤色也会发生一些变化。

为了测试直方图比较的结果,从某个图片中选择部分区域(如室内图片的上部分),计算其颜色直方图并将其和室内环境的下半部分图片及另外两个光照环境下的整幅手掌图片的颜色直方图比较。这里选择使用HSV颜色空间的直方图,它可以使用色度H和饱和度S的直方图进行肤色匹配,尽管还有变量亮度V的值未使用,但是这已经足够我们完成即使是跨种族的肤色匹配任务。

一个实现了上述肤色匹配任务的比较结果如下表。其中,室内图像的上半幅图片和下半幅图片对颜色直方图匹配程度很高,和另外两种光照环境下的颜色直方图匹配程度较低。

比较目标 相关性法 Chi-Square方法 交集法 巴氏距离
完全匹配参考目标 (1.0) (0.0) (1.0) (0.0)
下半幅室内图片 0.96 0.14 0.82 0.2
室外阴影图片 0.09 1.57 0.13 0.8
室外明亮图片 0.0 1.98 0.01 0.99

2.3 复杂直方图方法

在介绍了一些基础的直方图操作后,接下来将会介绍一些高级的直方图方法,这些方法包括比较直方图,计算或者可视化图像的哪部分对指定部分的直方图有贡献。

2.3.1 EMD距离

在前面的文章中我们已经看见光照条件的改变会导致明显的颜色位移,如在前文的手掌照片及直方图展示中就能观察到这个现象。尽管这些位移不会改变颜色直方图的形状,但是会改变它的位置,从而使得直方图匹配度较低。这也是直方图匹配度一个难点,我们经常想要找到一个距离度量标准使得形状相同但是发生位移的直方图分布能够得到一个匹配的比较结果,EMD距离(Earth Mover‘s Distance)就是这样的距离度量标准。

该方法的基本思想是通过移动部分或者整个直方图到新的位置将其“搬到”另外一个直方图中,并度量该操作耗费的成本。如在上文演示函数cv::compareHist()使用的简单匹配模型结果中,该方法的完美匹配直方图计算得到的距离为0,而对于半匹配的模型距离为0.5,而对于其中完全不匹配的情况需要将整个直方图向由移动一步,因此得到的距离为1。

实际上EMD是一个相同通用的算法,它允许用户自定义距离度量衡以及移动成本矩阵。你可以通过记录数据从一个直方图的何处移动到了另一个直方图中的何处,并且基于数据的先验信息(Prior Information)的到一个非线性的距离。OpenCV提供的EMD函数原型如下。

// 参数详细含义下文介绍
// signature1:待比较的直方图转换的签名格式,尺寸为sz1✖️dms+1
// signature2:待比较的直方图转换的签名格式,尺寸为sz2✖️dms+1
// distType:距离类型,如cv::DIST_L1
// cost:移动成本矩阵,尺寸为sz1✖️sz2,当参数选择cv::DIST_USER需要设置此参数
// lowerBound:两个直方图重心距离下界,可以同时作为输入和输出
// flow:尺寸为sz1✖️sz2,表示直方图1中第I个元素流向直方图2中第j个元素的质量
float cv::EMD(cv::InputArray signature1, cv::InputArray signature2,
              int distType, cv::InputArray cost = noArray(),
              float* lowerBound = 0, cv::OutputArray flow = noArray());

在使用该函数比较直方图距离的时候,必须将直方图转换为一种称为签名的格式,该格式需要传入一个sz1✖️dms+1的矩阵,每一行数据都包含直方图在某个坐标上的统计结果以及对应的坐标。如在三维直方图中在点(7, 43, 11)的分组的值为537,则签名矩阵对应的某行数据为[537, 7, 43, 11]。

参数distType表示EMD距离的度量衡,其取值在前文很多地方都已经有过介绍,可以是曼哈顿街区距离(Manhattan Distance),取值为cv::DIST_L1,欧式距离(Euclidean Distance),取值为cv::DIST_L2,也可以是棋盘距离(Checkerboard Distance),取值为cv::DIST_C,或者是用户自定义的距离衡,取值为cv::DIST_USER。当选择用户自定义距离时,需要通过参数cost指定移动成本,即自定义的移动距离。其尺寸为sz1✖️sz2,该矩阵的每个元素(坐标为ij)表示从直方图1的第i个元素表示的位置到直方图2第j个元素表示的位置之间的距离。

lowerBound可以同时作为输入和输出参数。作为输出参数,它表示两个直方图的重心(Center of Mass)距离的下界。为了计算这个下届,必须使用标准的距离度量衡,即参数distType不能设置为cv::DIST_USER,并且两个直方图的总权重必须相同(经过标准化处理的两个直方图总权重就是相同的,都为1)。做为输入参数,它必须被赋予有意义的值,即如果两个直方图的重心距离低于等于该值才会计算EMD距离,这种机制很有用,因为计算重心距离会比计算EMD距离快很多,如果重心距离都已经大于指定的值了,则我们认为两个直方图已经不匹配了,就没有必要再计算其准确的EMD距离。如果想跳过此逻辑,只需将参数lowerBound的值设置为0即可。

参数flow是一个可选的sz1✖️sz2矩阵,其中的元素E(i, j)表示直方图1中第i个元素流向直方图2中第j个元素的质量,其实该参数就是表示数据流动的过程。

示例程序EMD使用已经介绍过的5种距离比较了前文室内手掌图片上半幅图与下半幅图,以及与其他光照条件下的整幅手掌图,以及一副完全无关图像的颜色直方图的相似程度。程序运行后得到的原始图像及其颜色直方图表示如下。

距离比较结果如下表。这里在进行前四种方法比较直方图时标准化的区域为0到255,使用EMD比较直方图距离时的标准化方式是直方图的所有元素数据权重和为1。这里和前文原书中给出的比较结果不同,推测应当是标准化策略以及图像选取的范围有差异。

比较目标 相关性法 Chi-Square方法 交集法 巴氏距离 EMD距离
下半幅室内图片 0.501425 1648.11 301.039 0.55207 1.52593
室外阴影图片 0.459135 244318 633.146 0.449522 2.73921
室外明亮图片 0.676008 35634.4 756.639 0.408152 1.80114
完全无关的水果图片 0.0938923 3380830 389.745 0.689889 2.8382
2.3.2 反向投影

反向投影(Back Projection)可以判断像素集合与指定的直方图表示的颜色分布的匹配程度。例如假定我们有肤色的颜色直方图,可以通过该技术寻找图像中和肤色匹配的区域。从统计学的角度看,如果将已知的直方图分布看作是在特定目标上的颜色分布的先验概率分布(Prior Probability Distribution),则反向投影就是计算图像中的任意区域符合该先验概率分布的概率,也就是说属于目标物体的概率。实际上反向投影就是计算每个像素在直方图分布对应分组中的计数值。

OpenCV提供了两个函数分别用于处理密集矩阵和稀疏矩阵,其函数原型如下。直方图中只包含了维度、维度分组及每个分组的统计结果,但是不包含分组的区间,所以需要额外的参数ranges确定。

// images:反向投影的目标查询数组矩阵,单通道8U或者三通道32F,所以矩阵大小类型必须相同,
//         通道数可以不同
// nimages:images中包含矩阵的个数
// channels:需要比较的通道索引,索引的编号规则和函数cv::calcHist()相同,该数组的格式和
//           参数hist表示的直方图的维度相同
// hist:比较的直方图矩阵
// backProject:反向投影的结果,和参数images中的矩阵大小和数据类型相同,单通道
// ranges:直方图每个维度的分组策略,和函数cv::calcHist()相同
// scale:输出结果的缩放系数,通常反向投影得到的数据值都较低,有时适当放大结果可视化效果更好
// uniform:分组策略是否为均匀分布,和函数cv::calcHist()相同
void cv::calcBackProject(const cv::Mat* images, int nimages, const int* channels,
                         cv::InputArray hist, cv::OutputArray backProject,
                         const float** ranges,
                         double scale = 1, 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);

如果调用该函数使用的直方图是经过标准化处理的,则反向投影的结果就是某个像素是直方图表示的分布中的一部分的概率。即对于前文讲到的肤色示例而言,如果C是某个像素的颜色值,而F表示该像素属于皮肤的概率。则可以通过p(C|F)表示在皮肤中像素C出现的概率,这和p(F|C)表示的含义不同,后者表示颜色C属于皮肤的概率,也是反向投影在某种程度上表示的含义。但是这两个概率可以通过如下贝叶斯公式计算,其中p(F)表示场景中指定目标的累计概率,p(C)表示场景中颜色C的累计概率,当然对于直方图而言这是颜色区间的累计概率。

下图使用肤色直方图计算了一副图像中像素颜色属于皮肤的概率,直方图计算的是HSV颜色空间中的色度H和饱和度S。其中左上角图片为皮肤模型的颜色直方图,可以用于计算上述公式中的p(C|F),右上侧图像为测试图像,左下图为测试图像的颜色直方图,右下角图像是根据该颜色直方图进行反向投影得到的结果,也就是上述公式中的p(F|C)。需要注意这里进行反向投影时传入的直方图为皮肤模型的颜色直方图。

通常情况下你可以通过如下三步来寻找感兴趣的区域。首先创建需要寻找的目标或者区域的直方图。然后使用该直方图计算待查询图像的后向投影,其中较高的值表示和感兴趣区域匹配程度更高。最后根据后向投影中的高值取原图的部分区域,再计算其直方图和目标区域的直方图进行比较。

需要注意的是如果后向投影图的基本数据类型为cv::U8,不要对直方图进行标准化,对于已经标准化的直方图要进行放大处理,因为标准后后的直方图中的最大值为1,在cv::U8数据类型中除了1都会被转换为0。

3 模版匹配

模版匹配不依赖于直方图,相反其实现方式是通过一个图像块在输入图像上滑动,匹配的方法下文介绍,一个模版匹配的示例如下图。其中左上角图片表示了待匹配目标HSV颜色模型下的HS直方图。而右侧图像是需要查询的图片,其中白色方块表示滑动图像块的大小,这里需要查找的是咖啡杯。左下图为待查询图像的HS直方图,而右下图为测试图像应用模版匹配的结果,可以明显看出咖啡杯已经被挑选出来。

另外一个模版匹配的场景如下图,这里用的是一副包含人脸的图像滑块,通过在输入图像滑动该滑块可以在整个图像中寻找到最到最好的匹配效果来确定人脸的存在。

OpenCV提供的模版匹配函数如下。

// image:待查询的图片,8U或者32F的灰度或者彩色图像,大小为W✖️H
// templ:图像滑块,大小为w✖️h
// result:模版匹配的结果,单通道,数据类型为32F,大小为W-w+1✖️H-h+1
// method:模版匹配使用的方法,下文介绍
void cv::matchTemplate(cv::InputArray image, cv::InputArray templ,
                       cv::OutputArray result, int method);

参数method指定了模版匹配的方法,可选值如下,在下面的公式中将使用I表示输入图像,T表示图像滑块,R表示模版匹配的结果。每一种方法都有一个标准化版本,因为不同光照环境下的数据标准化后系数相同,可以排除光照条件带来的干扰。

3.1 方差匹配方法

该方法取值为cv::TM_SQDIFF,比较的是两个矩阵的方差,得到的值越小表示越相似,其公式如下。

3.2 归一化方差匹配

归一化方差匹配的取值为cv::TM_SQDIFF_NORMED,完全匹配情况下计算得到的值为0,其公式如下。

3.3 相关性匹配

相关性匹配方法的取值为cv::TM_CCORR,该方法以乘法的方式匹配模版,越匹配的矩阵计算结果越大,完全不匹配的矩阵计算结果为0,其公式如下。

3.4 归一化相关性匹配

归一化相关性匹配的取值为cv::TM_CCORR_NORMED,极度不匹配的矩阵计算结果趋于0,其公式如下。

3.5 相关性系数匹配

相关性系数匹配的取值为cv::TM_CCOEFF,它比较的是两个矩阵中元素和矩阵本身均值差值的相关性,完美匹配矩阵的计算结果为1,完全不匹配矩阵的计算结果为-1,0表示无相关性,其公式如下。

3.6 归一化相关系数匹配

归一化相关系数匹配的取值为cv::TM_CCOEFF_NORMED,较好的匹配矩阵计算结果是较大正值,而匹配程度很差的矩阵计算结果是较大的负值,其公式如下,其中T’和I’计算方式通相关性系数匹配方法。

使用相对复杂的匹配方法(相关性系数匹配)替换相对简单的匹配方法(如方差匹配)将会得到更准确的匹配结果,但是会花费更多的计算成本。最后尝试所有的方法,并在最终的应用程序中权衡计算成本和结果精度选择最合适的方法。需要注意的是除了方差匹配和归一化方差匹配对于越好的匹配得到的计算结果越小,其他方法都是相反的。

当模版匹配完成后,可以通过函数cv::minMaxLoc()或者cv::minMaxIdx()寻找最佳匹配结果的位置。一个好的匹配模式应当是一个局部区域的像素点都取得较好的匹配结果,因为模版在像素点临域内滑动时,模版覆盖区域内像素点轻微改变通常不会使得匹配结果相差太多。同时为了避免图像噪声引起的异常高匹配,可以轻微的平滑模版匹配得到的结果再寻找极值。在这种场景下,前文介绍的图像形态学的方法能够发挥很好的作用。

示例程序Template实现了不同方法的模版匹配,其核心代码如下。

int main(int argc, const char * argv[]) {
    // 读取匹配模版图像
    cv::Mat templ = cv::imread(argv[1], 1);
    // 读取待查找图像
    cv::Mat src = cv::imread(argv[2], 1);

    // 使用6种不同的方法执行模版匹配操作
    cv::Mat ftmp[6];
    for (int i = 0; i < 6; ++i) {
        cv::matchTemplate( src, templ, ftmp[i], i);
        cv::normalize(ftmp[i],ftmp[i],1,0,cv::NORM_MINMAX);
    }
    
    return 0;
}

示例程序使用的模版图像和待查找的图像如下图,其中左侧图像是使用的模版滑块,右侧图像是待查找的图像。

6种不同的模版匹配方法得到的效果图如下,需要注意方差法和归一化方差法的最佳匹配计算结果为0,其他方法则刚好相反。因此在下图中第一列的两幅图片中的黑色区域为匹配好的区域,而右侧两列图片中亮色区域为匹配程度较高的区域。

4 小结

本章介绍了在OpenCV中如何使用矩阵和稀疏矩阵对象表示直方图,在实际应用中直方图通常表示为概率密度函数,即其内部的每个元素表示其对应分组在整个随机变量分布中的概率。此外还介绍了如何使用直方图识别物体和兴趣区域。另外本章也介绍了直方图的基本操作,当直方图被看作是概率密度函数时这些操作通常能发挥较大的作用。例如直方图的标准化,以及比较直方图。在本章的最后,我们讨论了模版匹配,该技术能很好的处理高度结构化的图片。