作者:王先荣
前言
模板匹配是在图像中寻找目标的方法之一。Come On, Boy.我们一起来看看模板匹配到底是怎么回事。
模板匹配的工作方式
模板匹配的工作方式跟直方图的反向投影基本一样,大致过程是这样的:通过在输入图像上滑动图像块对实际的图像块和输入图像进行匹配。
假设我们有一张100×100的输入图像,有一张10×10的模板图像,查找的过程是这样的:
(1)从输入图像的左上角(0,0)开始,切割一块(0,0)至(10,10)的临时图像;
(2)用临时图像和模板图像进行对比,对比结果记为c;
(3)对比结果c,就是结果图像(0,0)处的像素值;
(4)切割输入图像从(0,1)至(10,11)的临时图像,对比,并记录到结果图像;
(5)重复(1)~(4)步直到输入图像的右下角。
大家可以看到,直方图反向投影对比的是直方图,而模板匹配对比的是图像的像素值;模板匹配比直方图反向投影速度要快一些,但是我个人认为直方图反向投影的鲁棒性会更好。
模板匹配的匹配方式
在OpenCv和EmguCv中支持以下6种对比方式:
CV_TM_SQDIFF 平方差匹配法:该方法采用平方差来进行匹配;最好的匹配值为0;匹配越差,匹配值越大。
CV_TM_CCORR 相关匹配法:该方法采用乘法操作;数值越大表明匹配程度越好。
CV_TM_CCOEFF 相关系数匹配法:1表示完美的匹配;-1表示最差的匹配。
CV_TM_SQDIFF_NORMED 归一化平方差匹配法
CV_TM_CCORR_NORMED 归一化相关匹配法
CV_TM_CCOEFF_NORMED 归一化相关系数匹配法
根据我的测试结果来看,上述几种匹配方式需要的计算时间比较接近(跟《学习OpenCv》书上说的不同),我们可以选择一个能适应场景的匹配方式。
模板匹配的示例代码
下面是模板匹配的C#版本代码:
模板匹配
//
模板匹配
private
void
btnCalc_Click(
object
sender, EventArgs e)
{
//
输入图像
Image
<
Bgr, Byte
>
imageInput
=
new
Image
<
Bgr,
byte
>
((Bitmap)pbInput.Image);
//
模板图像
Image
<
Bgr, Byte
>
imageTemplate
=
new
Image
<
Bgr,
byte
>
((Bitmap)pbTemplate.Image);
//
缩放因子,更小的图像可以提高处理速度
double
scale
=
1d;
double
.TryParse(txtScale.Text,
out
scale);
if
(scale
!=
1d)
{
imageInput
=
imageInput.Resize(scale, INTER.CV_INTER_LINEAR);
imageTemplate
=
imageTemplate.Resize(scale, INTER.CV_INTER_LINEAR);
}
//
色彩空间
string
colorSpace
=
(
string
)cmbColorSpace.SelectedItem;
IImage imageInput2, imageTemplate2;
if
(colorSpace
==
”
Gray
”
)
{
imageInput2
=
imageInput.Convert
<
Gray, Byte
>
();
imageTemplate2
=
imageTemplate.Convert
<
Gray, Byte
>
();
}
else
if
(colorSpace
==
”
HSV
”
)
{
imageInput2
=
imageInput.Convert
<
Hsv, Byte
>
();
imageTemplate2
=
imageTemplate.Convert
<
Hsv, Byte
>
();
}
else
{
imageInput2
=
imageInput.Copy();
imageTemplate2
=
imageTemplate.Copy();
}
//
匹配方式数组
TM_TYPE[] tmTypes
=
new
TM_TYPE[] { TM_TYPE.CV_TM_SQDIFF, TM_TYPE.CV_TM_SQDIFF_NORMED, TM_TYPE.CV_TM_CCORR, TM_TYPE.CV_TM_CCORR_NORMED, TM_TYPE.CV_TM_CCOEFF, TM_TYPE.CV_TM_CCOEFF_NORMED };
//
输出图像(匹配结果)
Image
<
Gray, Single
>
[] imageResults
=
new
Image
<
Gray,
float
>
[tmTypes.Length];
//
依次执行每种匹配,并归一化结果
int
i
=
0
;
double
totalTime
=
0d;
//
总共用时
double
time;
//
每种匹配的用时
Stopwatch sw
=
new
Stopwatch();
txtResult.Text
+=
string
.Format(
”
开始执行匹配(色彩空间:{0},缩放因子:{1})\r\n
”
, colorSpace, scale);
foreach
(TM_TYPE tmType
in
tmTypes)
{
sw.Start();
//
模板匹配(注意:因为接口IImage中没有名为MatchTemplate的定义,所以需要进行强制转换)
//
Image<Gray, Single> imageResult = imageInput2.MatchTemplate(imageTemplate2, tmType);
Image
<
Gray, Single
>
imageResult;
if
(colorSpace
==
”
Gray
”
)
imageResult
=
((Image
<
Gray, Byte
>
)imageInput2).MatchTemplate((Image
<
Gray, Byte
>
)imageTemplate2, tmType);
else
if
(colorSpace
==
”
HSV
”
)
imageResult
=
((Image
<
Hsv, Byte
>
)imageInput2).MatchTemplate((Image
<
Hsv, Byte
>
)imageTemplate2, tmType);
else
imageResult
=
((Image
<
Bgr, Byte
>
)imageInput2).MatchTemplate((Image
<
Bgr, Byte
>
)imageTemplate2, tmType);
sw.Stop();
time
=
sw.Elapsed.TotalMilliseconds;
totalTime
+=
time;
sw.Reset();
//
归一化结果
CvInvoke.cvNormalize(imageResult.Ptr, imageResult.Ptr, 1d, 0d, NORM_TYPE.CV_MINMAX, IntPtr.Zero);
//
找到最匹配的点,以及该点的值
double
bestValue;
Point bestPoint;
FindBestMatchPointAndValue(imageResult, tmType,
out
bestValue,
out
bestPoint);
//
在最匹配的点附近画一个跟模板一样大的矩形
Rectangle rect
=
new
Rectangle(
new
Point(bestPoint.X
–
imageTemplate.Size.Width
/
2
, bestPoint.Y
–
imageTemplate.Size.Height
/
2
), imageTemplate.Size);
imageResult.Draw(rect,
new
Gray(bestValue),
2
);
//
保存结果图像到数组
imageResults[i]
=
imageResult;
i
++
;
//
显示结果
txtResult.Text
+=
string
.Format(
”
匹配方式:{0:G},用时:{1:F05}毫秒,最匹配的点:({2},{3}),最匹配的值:{4}\r\n
”
, tmType, time, bestPoint.X, bestPoint.Y, bestValue);
}
txtResult.Text
+=
string
.Format(
”
匹配结束,共用时:{0:F05}毫秒\r\n
”
, totalTime);
//
显示结果图像
pbResultSqdiff.Image
=
ImageConverter.ImageSingleToBitmap
<
Gray
>
(imageResults[
0
]);
pbResultSqdiffNormalized.Image
=
ImageConverter.ImageSingleToBitmap
<
Gray
>
(imageResults[
1
]);
pbResultCcorr.Image
=
ImageConverter.ImageSingleToBitmap
<
Gray
>
(imageResults[
2
]);
pbResultCcorrNormalized.Image
=
ImageConverter.ImageSingleToBitmap
<
Gray
>
(imageResults[
3
]);
pbResultCcoeff.Image
=
ImageConverter.ImageSingleToBitmap
<
Gray
>
(imageResults[
4
]);
pbResultCcoeffNormalized.Image
=
ImageConverter.ImageSingleToBitmap
<
Gray
>
(imageResults[
5
]);
//
释放资源
imageInput.Dispose();
imageTemplate.Dispose();
imageInput2.Dispose();
imageTemplate2.Dispose();
foreach
(Image
<
Gray, Single
>
imageResult
in
imageResults)
imageResult.Dispose();
}
//
找到最匹配的点,以及该点的值
private
void
FindBestMatchPointAndValue(Image
<
Gray, Single
>
image, TM_TYPE tmType,
out
double
bestValue,
out
Point bestPoint)
{
bestValue
=
0d;
bestPoint
=
new
Point(
0
,
0
);
double
[] minValues, maxValues;
Point[] minLocations, maxLocations;
image.MinMax(
out
minValues,
out
maxValues,
out
minLocations,
out
maxLocations);
//
对于平方差匹配和归一化平方差匹配,最小值表示最好的匹配;其他情况下,最大值表示最好的匹配
if
(tmType
==
TM_TYPE.CV_TM_SQDIFF
||
tmType
==
TM_TYPE.CV_TM_SQDIFF_NORMED)
{
bestValue
=
minValues[
0
];
bestPoint
=
minLocations[
0
];
}
else
{
bestValue
=
maxValues[
0
];
bestPoint
=
maxLocations[
0
];
}
}
显示结果图像
模板匹配和直方图反向投影生成的结果图像都是32位浮点型单通道图像。如果用C/C++,可以很方便的用OpenCv中的cvShowImage函数来显示;如果用.net,因为EmguCv中将32位浮点图像转换成8位位图的方法有些小问题,我们要自己编写一段转换的代码,然后再显示。
将浮点型图像转换成8位byte图像
///
<summary>
///
将任意浮点型图像转换成Byte图像;
///
本转换函数对浮点型图像的具体像素值没有要求,自动将值缩放到0~255之间。
///
</summary>
///
<typeparam name=”TColor”>
图像的色彩空间
</typeparam>
///
<param name=”source”>
浮点型图像
</param>
///
<returns>
返回Byte型图像
</returns>
public
static
Image
<
TColor, Byte
>
ImageSingleToByte
<
TColor
>
(Image
<
TColor, Single
>
source)
where
TColor :
struct
, IColor
{
Image
<
TColor, Byte
>
dest
=
new
Image
<
TColor, Byte
>
(source.Size);
//
得到源图像的最小和最大值
double
[] minVal, maxVal;
Point[] minLoc, maxLoc;
source.MinMax(
out
minVal,
out
maxVal,
out
minLoc,
out
maxLoc);
double
min
=
minVal[
0
];
double
max
=
maxVal[
0
];
for
(
int
i
=
1
; i
<
minVal.Length; i
++
)
{
min
=
Math.Min(min, minVal[i]);
max
=
Math.Max(max, maxVal[i]);
}
//
得到缩放比率和偏移量
double
scale
=
1.0
, shift
=
0.0
;
scale
=
(max
==
min)
?
0.0
:
255.0
/
(max
–
min);
shift
=
(scale
==
0
)
?
min :
–
min
*
scale;
//
缩放图像,并浮点图像缩放到256级的灰度
CvInvoke.cvConvertScaleAbs(source.Ptr, dest.Ptr, scale, shift);
return
dest;
}
///
<summary>
///
将任意浮点型图像转换成每通道8位的Bitmap;
///
本转换函数对浮点型图像的具体像素值没有要求,自动将值缩放到0~255之间。
///
</summary>
///
<typeparam name=”TColor”>
图像的色彩空间
</typeparam>
///
<param name=”source”>
浮点型图像
</param>
///
<returns>
返回每通道8位的Bitmap
</returns>
public
static
Bitmap ImageSingleToBitmap
<
TColor
>
(Image
<
TColor, Single
>
source)
where
TColor :
struct
, IColor
{
Image
<
TColor, Byte
>
dest
=
ImageSingleToByte
<
TColor
>
(source);
Bitmap bitmap
=
dest.Bitmap;
dest.Dispose();
return
bitmap;
}
左上是输入图像,左中是模板图像,右边是各种匹配方式的结果(相关匹配的结果明显不正确)
模板匹配和直方图反向投影的效率
总的来说,模板匹配和直方图反向投影的效率都不高。在我的机器上,在1136*852大小的输入图像上匹配104*132的大小的模板图像(都是单通道灰度图像),大约需要700毫秒;而直方图反向投影大约需要75000毫秒(1.25分钟)。看来还需要继续学习,寻找更好的处理方法。
另一方面,通过搜索OpenCv的源代码,发现OpenCv基本上没有使用并行计算。如果学习完之后,还有时间和热情,我准备尝试优化下OpenCv的并行计算;如果.net 4.0正式版推出了,也可以选择在这一方面做点优化。
感谢您耐心看完本文,希望对您有所帮助。