本博客在https://www.cnblogs.com/zhaoweiwei/p/OpenVC_matchTemplate.html基础上进行更加详细的注解。当初有几个地方看的比较费劲,但是里面没有注释,现给加上,主要是那些带黄色及红色部分的注释。
在此向weiwei22844致敬。
模板匹配是在一幅图像中寻找一个特定目标的方法之一,这种方法的原理非常简单,遍历图像中的每一个可能的位置,比较各处与模板是否“相似”,当相似度足够高时,就认为找到了我们的目标。OpenCV提供了6种模板匹配算法:
用T表示模板图像,I表示待匹配图像,切模板图像的宽为w高为h,用R表示匹配结果,匹配过程如下图所示:
上述6中匹配方法可用以下公式进行描述:
较新版本的OpenCV库中的模板匹配已经进行了较多的算法改进,直接看新版本中的算法需要了解很多相关理论知识,所以我们结合OpenCV0.9.5的源码进行讲解,该版本的源码基本上是C风格代码,对于初学者来说更容易进行理解(如果要对OpenCV源码进行研究,建议用该版本进行入门),下面以第6项归一化相关系数匹配法为例进行分析。
源码部分及注解
/******************参数说明****************************
* pImage: 待匹配图像数据,相对要大于或等于模板图像的宽和高
* imageStep: 待匹配图像宽(width*depth并以4字节对齐)
* roiSize: 待匹配图像尺寸
* pTemplate: 模板图像数据(在大图pImage里去找这个模板pTemplate图像)
* templStep: 模板图像宽
* templSize: 模板图像尺寸
* pResult: 匹配结果
* resultStep: 匹配结果宽
* pBuffer: 中间结果数据缓存
*/
IPCVAPI_IMPL( CvStatus, icvMatchTemplate_CoeffNormed_32f_C1R,
(const float *pImage, int imageStep, CvSize roiSize,
const float *pTemplate, int templStep, CvSize templSize,
float *pResult, int resultStep, void *pBuffer) )
{
float *imgBuf = 0; // 待匹配图像相关数据
float *templBuf = 0; // 模板图像数据
double *sumBuf = 0; // 待匹配图像遍历块单行和
double *sqsumBuf = 0; // 待匹配图像遍历块单行平方和
double *resNum = 0; // 模板图像和待匹配图像遍历块内积
double *resDenom = 0; // 待匹配图像遍历块累加和及待匹配图像遍历块平方累加和
double templCoeff = 0; // 模板图像均分差倒数
double templSum = 0; // 模板图像累加和
int winLen = templSize.width * templSize.height;
double winCoeff = 1. / (winLen + DBL_EPSILON); // + DBL_EPSILON 加一个小整数防止分母为零
CvSize resultSize = cvSize( roiSize.width - templSize.width + 1,
roiSize.height - templSize.height + 1 );
int x, y;
// 计算imgBuf、templBuf、sumBuf、sqsumBuf、resNum、resDenom大小并分配存储空间
CvStatus result = icvMatchTemplateEntry( pImage, imageStep, roiSize,
pTemplate, templStep, templSize,
pResult, resultStep, pBuffer,
cv32f, 1, 1,
(void **) &imgBuf, (void **) &templBuf,
(void **) &sumBuf, (void **) &sqsumBuf,
(void **) &resNum, (void **) &resDenom );
if( result != CV_OK )
return result;
imageStep /= sizeof_float;
templStep /= sizeof_float;
resultStep /= sizeof_float;
/* calc common statistics for template and image */
{
const float *rowPtr = (const float *) imgBuf;
double templSqsum = icvCrossCorr_32f_C1( templBuf, templBuf, winLen ); // 模板图像平方累加和 Sqsum +=I(x,y)*I(x,y)
templSum = icvSumPixels_32f_C1( templBuf, winLen ); // 模板图像累加和 Sum +=I(x,y)
templCoeff = (double) templSqsum - ((double) templSum) * templSum * winCoeff; // 模板图像均方差的平方//templCoeff = sum(I(x,y)*I(x,y))-(sum(I(x,y))*sum(I(x,y)/width*height
templCoeff = icvInvSqrt64d( fabs( templCoeff ) + FLT_EPSILON ); // 模板图像均方差倒数 //正好是公式6分母的左半部分
//下面按每rows 进行滑动,Mat里的rows,相对于图像的height,有效区域是模板的大小
for( y = 0; y < roiSize.height; y++, rowPtr += templSize.width )
{
sumBuf[y] = icvSumPixels_32f_C1( rowPtr, templSize.width ); // 待匹配图像按模板图像宽度求每行之和(遍历位置第一列)
sqsumBuf[y] = icvCrossCorr_32f_C1( rowPtr, rowPtr, templSize.width ); // 待匹配图像按模板图像宽度求每行平方之和(遍历位置第一列)
}
}
/* main loop - through x coordinate of the result从结果矩阵result的x坐标进行滑动*/
for( x = 0; x < resultSize.width; x++ )
{
double sum = 0;
double sqsum = 0;
float *imgPtr = imgBuf + x; // 待匹配图像内存部分的起始位置
/* update sums and image band buffer */ // 如果不是第0列需重新更新sumBuf,sqsumBuf[y],更新后sumBuf为遍历位置第x列每行之和(行宽为模板图像宽)相当于最左边一列数据要随着x滑动需要实时更新。更新方法就是新的最右边一列数据减去老的最左边的一列数据就可以了。
if( x > 0 )
{
const float *src = pImage + x + templSize.width - 1; //图像数据源起始地址
float *dst = imgPtr - 1; // float *imgPtr = imgBuf + x;
float out_val = dst[0]; //这次模板宽度起始的位置的前一个像素
dst += templSize.width;
for( y = 0; y < roiSize.height; y++, src += imageStep, dst += templSize.width )
{
float in_val = src[0];//原图(x,y)的数据
sumBuf[y] += in_val - out_val;//求原图像一列的像素差值总和
sqsumBuf[y] += (in_val - out_val) * (in_val + out_val);//x^2-y^2
out_val = dst[0];//作为上一行的数据
dst[0] = (float) in_val; //设置dst[0]
}
}
for( y = 0; y < templSize.height; y++ ) // 单独求遍历位置第x列,遍历块累加和sum及平方差累加和sqsum
{
sum += sumBuf[y];
sqsum += sqsumBuf[y];
}
//对x列下y行部分进行预先处理
for( y = 0; y < resultSize.height; y++, imgPtr += templSize.width )
{
double res = icvCrossCorr_32f_C1( imgPtr, templBuf, winLen ); // 求模板图像和待匹配图像y行x列处遍历块的内积 //res +=imgPtr(x,y)*templBuf(x,y)
//分子左上部分卷积
if( y > 0 ) // 如果不是第0行需更新遍历块累加和sum及平方累加和sqsum
更新方法是新的最下面一行减去老的最上面一行就可以了
{
sum -= sumBuf[y - 1];
sum += sumBuf[y + templSize.height - 1];
sqsum -= sqsumBuf[y - 1];
sqsum += sqsumBuf[y + templSize.height - 1];
}
resNum[y] = res;
resDenom[y] = sum;
resDenom[y + resultSize.height] = sqsum;
}
//进行最后汇总计算,基于x列滑动的结果再进行结果矩阵的y行列滑动
for( y = 0; y < resultSize.height; y++ )
{
double sum = ((double) resDenom[y]);
double wsum = winCoeff * sum;//分子右半部分第一项
double res = ((double) resNum[y]) - wsum * templSum; //公式6的分子部分
// wsum * templSum 表示分子右半部分;
double nrm_s = ((double) resDenom[y + resultSize.height]) - wsum * sum;
//上面表示分母右边开根号里的内容 (I(x,y)*I(x,y))-1/width/height*sum(temp(x,y))*sum(temp(x,y))
res *= templCoeff * icvInvSqrt64d( fabs( nrm_s ) + FLT_EPSILON );
//公式6的分母右半部分
pResult[x + y * resultStep] = (float) res;
//把结果相似度值存入结果矩阵x列y行中
}
}
return CV_OK;
}