作者:王先榮
前言 模板匹配是在圖像中尋找目標(biāo)的方法之一。Come On, Boy.我們一起來看看模板匹配到底是怎么回事。
模板匹配的工作方式 模板匹配的工作方式跟直方圖的反向投影基本一樣,大致過程是這樣的:通過在輸入圖像上滑動(dòng)圖像塊對(duì)實(shí)際的圖像塊和輸入圖像進(jìn)行匹配。 假設(shè)我們有一張100x100的輸入圖像,有一張10x10的模板圖像,查找的過程是這樣的: (1)從輸入圖像的左上角(0,0)開始,切割一塊(0,0)至(10,10)的臨時(shí)圖像; (2)用臨時(shí)圖像和模板圖像進(jìn)行對(duì)比,對(duì)比結(jié)果記為c; (3)對(duì)比結(jié)果c,就是結(jié)果圖像(0,0)處的像素值; (4)切割輸入圖像從(0,1)至(10,11)的臨時(shí)圖像,對(duì)比,并記錄到結(jié)果圖像; (5)重復(fù)(1)~(4)步直到輸入圖像的右下角。 大家可以看到,直方圖反向投影對(duì)比的是直方圖,而模板匹配對(duì)比的是圖像的像素值;模板匹配比直方圖反向投影速度要快一些,但是我個(gè)人認(rèn)為直方圖反向投影的魯棒性會(huì)更好。
模板匹配的匹配方式 在OpenCv和EmguCv中支持以下6種對(duì)比方式: CV_TM_SQDIFF 平方差匹配法:該方法采用平方差來進(jìn)行匹配;最好的匹配值為0;匹配越差,匹配值越大。 CV_TM_CCORR 相關(guān)匹配法:該方法采用乘法操作;數(shù)值越大表明匹配程度越好。 CV_TM_CCOEFF 相關(guān)系數(shù)匹配法:1表示完美的匹配;-1表示最差的匹配。 CV_TM_SQDIFF_NORMED 歸一化平方差匹配法 CV_TM_CCORR_NORMED 歸一化相關(guān)匹配法 CV_TM_CCOEFF_NORMED 歸一化相關(guān)系數(shù)匹配法 根據(jù)我的測(cè)試結(jié)果來看,上述幾種匹配方式需要的計(jì)算時(shí)間比較接近(跟《學(xué)習(xí)OpenCv》書上說的不同),我們可以選擇一個(gè)能適應(yīng)場(chǎng)景的匹配方式。
模板匹配的示例代碼 下面是模板匹配的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(); } //匹配方式數(shù)組 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 }; //輸出圖像(匹配結(jié)果) Image<Gray, Single>[] imageResults = new Image<Gray, float>[tmTypes.Length]; //依次執(zhí)行每種匹配,并歸一化結(jié)果 int i = 0; double totalTime = 0d; //總共用時(shí) double time; //每種匹配的用時(shí) Stopwatch sw = new Stopwatch(); txtResult.Text += string.Format("開始執(zhí)行匹配(色彩空間:{0},縮放因子:{1})\r\n", colorSpace, scale); foreach (TM_TYPE tmType in tmTypes) { sw.Start(); //模板匹配(注意:因?yàn)榻涌贗Image中沒有名為MatchTemplate的定義,所以需要進(jìn)行強(qiáng)制轉(zhuǎn)換) //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(); //歸一化結(jié)果 CvInvoke.cvNormalize(imageResult.Ptr, imageResult.Ptr, 1d, 0d, NORM_TYPE.CV_MINMAX, IntPtr.Zero); //找到最匹配的點(diǎn),以及該點(diǎn)的值 double bestValue; Point bestPoint; FindBestMatchPointAndValue(imageResult, tmType, out bestValue, out bestPoint); //在最匹配的點(diǎn)附近畫一個(gè)跟模板一樣大的矩形 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); //保存結(jié)果圖像到數(shù)組 imageResults[i] = imageResult; i++; //顯示結(jié)果 txtResult.Text += string.Format("匹配方式:{0:G},用時(shí):{1:F05}毫秒,最匹配的點(diǎn):({2},{3}),最匹配的值:{4}\r\n", tmType, time, bestPoint.X, bestPoint.Y, bestValue); } txtResult.Text += string.Format("匹配結(jié)束,共用時(shí):{0:F05}毫秒\r\n", totalTime); //顯示結(jié)果圖像 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(); }
//找到最匹配的點(diǎn),以及該點(diǎn)的值 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); //對(duì)于平方差匹配和歸一化平方差匹配,最小值表示最好的匹配;其他情況下,最大值表示最好的匹配 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]; } }

顯示結(jié)果圖像 模板匹配和直方圖反向投影生成的結(jié)果圖像都是32位浮點(diǎn)型單通道圖像。如果用C/C++,可以很方便的用OpenCv中的cvShowImage函數(shù)來顯示;如果用.net,因?yàn)镋mguCv中將32位浮點(diǎn)圖像轉(zhuǎn)換成8位位圖的方法有些小問題,我們要自己編寫一段轉(zhuǎn)換的代碼,然后再顯示。

/// <summary> /// 將任意浮點(diǎn)型圖像轉(zhuǎn)換成Byte圖像; /// 本轉(zhuǎn)換函數(shù)對(duì)浮點(diǎn)型圖像的具體像素值沒有要求,自動(dòng)將值縮放到0~255之間。 /// </summary> /// <typeparam name="TColor">圖像的色彩空間</typeparam> /// <param name="source">浮點(diǎn)型圖像</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; //縮放圖像,并浮點(diǎn)圖像縮放到256級(jí)的灰度 CvInvoke.cvConvertScaleAbs(source.Ptr, dest.Ptr, scale, shift); return dest; }
/// <summary> /// 將任意浮點(diǎn)型圖像轉(zhuǎn)換成每通道8位的Bitmap; /// 本轉(zhuǎn)換函數(shù)對(duì)浮點(diǎn)型圖像的具體像素值沒有要求,自動(dòng)將值縮放到0~255之間。 /// </summary> /// <typeparam name="TColor">圖像的色彩空間</typeparam> /// <param name="source">浮點(diǎn)型圖像</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; }


左上是輸入圖像,左中是模板圖像,右邊是各種匹配方式的結(jié)果(相關(guān)匹配的結(jié)果明顯不正確)
模板匹配和直方圖反向投影的效率 總的來說,模板匹配和直方圖反向投影的效率都不高。在我的機(jī)器上,在1136*852大小的輸入圖像上匹配104*132的大小的模板圖像(都是單通道灰度圖像),大約需要700毫秒;而直方圖反向投影大約需要75000毫秒(1.25分鐘)??磥磉€需要繼續(xù)學(xué)習(xí),尋找更好的處理方法。 另一方面,通過搜索OpenCv的源代碼,發(fā)現(xiàn)OpenCv基本上沒有使用并行計(jì)算。如果學(xué)習(xí)完之后,還有時(shí)間和熱情,我準(zhǔn)備嘗試優(yōu)化下OpenCv的并行計(jì)算;如果.net 4.0正式版推出了,也可以選擇在這一方面做點(diǎn)優(yōu)化。
感謝您耐心看完本文,希望對(duì)您有所幫助。
|