多角度的模板匹配
背景介绍
熟悉OpenCV的朋友肯定都知道OpenCV自带的模板匹配matchTemplate方法是不支持旋转的,也就是说当目标和模板有角度差异时匹配常常会失败,可能目标只是轻微的旋转,匹配分数就会下降很多,导致匹配精度下降甚至匹配出错。另一个方法是matchShape(形状匹配),匹配时需要轮廓分明才容易匹配成功,但无法的到匹配角度,也不方便使用。本文介绍基于matchTemplate + 旋转 + 金字塔下采样实现多角度的模板匹配,返回匹配结果(斜矩形的端点、角度、匹配得分)。
实现思路
【1】如何适应目标的角度变化?
- 我们可以将模板旋转,从0~360°依次匹配找到最佳的匹配位置
【2】如何提高匹配速度?
- 使用金字塔下采样,将模板和待匹配图均缩小后匹配;加大匹配搜寻角度的步长,比如从每1°匹配一次改为每5°匹配一次等。
实现步骤:
【1】旋转模板图像。
旋转图像本身比较简单,下面是代码:
c++
//旋转图像
Mat ImageRotate(Mat image, double angle)
{
Mat newImg;
Point2f pt = Point2f((float)image.cols / 2, (float)image.rows / 2);
Mat M = getRotationMatrix2D(pt, angle, 1.0);
warpAffine(image, newImg, M, image.size());
return newImg;
}
但需要注意,很多时候按照上面方法旋转时,会丢失模板信息产生黑边,这里提供两种方法供大家参考尝试:
① 旋转时放大目标图像尺寸,保证模板图像上信息不丢失,然后模板匹配时使用mask;
② 旋转时不放大目标图像尺寸,剔除黑边剩余部分做mask来匹配。
【2】图像金字塔下采样。
什么是图像金字塔?什么是上下采样?
下采样的目的前面已介绍,减小图像分辨率提高图像匹配速度,代码如下:
c++
//对模板图像和待检测图像分别进行图像金字塔下采样
for (int i = 0; i < numLevels; i++)
{
pyrDown(src, src, Size(src.cols / 2, src.rows / 2));
pyrDown(model, model, Size(model.cols / 2, model.rows / 2));
}
【3】0~360°各 角度匹配。
旋转模板图像,依次调用matchTemplate在目标图中匹配,记录最佳匹配分数,以及对应的角度。
【4】计算匹配结果。
根据模板图大小、匹配结果角度计算出匹配后的矩形四个角点,根据角点关系即可绘制方向:
【5】举例演示。
模板图从下图中截取并保存template.png:
测试图像12张,来自Halcon例程图片,路径如下:
...\MVTec\HALCON-20.11-Steady\examples\images\modules
匹配结果:
相关代码
c++
/*
旋转模板匹配函数(通过图像金字塔、增大旋转步长来提升匹配速度)
Mat src:原图像
Mat model:模板图
double startAngle:旋转的最小角
double endAngle:旋转的最大角
double firstStep:角度旋转时的最大步长
double secondStep:角度旋转时的最小步长
int numLevels = 0:图像金字塔缩放次数
*/
MatchResult rotateMatch(Mat src, Mat model, double startAngle, double endAngle, double firstStep, double secondStep, int numLevels = 0) {
//对模板图像和待检测图像分别进行图像金字塔下采样
for (int i = 0; i < numLevels; i++) {
pyrDown(src, src, Size(src.cols / 2, src.rows / 2));
pyrDown(model, model, Size(model.cols / 2, model.rows / 2));
}
Mat rotatedImg, result;
double score = -1;
Point location;
double angle;
bool isSecond = false;
while (true) {
for (double curAngle = startAngle; curAngle <= endAngle; curAngle += firstStep) {
rotatedImg = ImageRotate(model, curAngle);
//imshow("rotated", rotatedImg);
//imshow("src-pyrDown", src);
//waitKey();
matchTemplate(src, rotatedImg, result, TM_CCOEFF_NORMED);
double minval, maxval;
Point minloc, maxloc;
minMaxLoc(result, &minval, &maxval, &minloc, &maxloc);
if (maxval > score)
{
location = maxloc;
score = maxval;
angle = curAngle;
}
}
if (isSecond) break;
startAngle = angle - firstStep;
endAngle = angle + firstStep;
if ((endAngle - startAngle) / 5 > secondStep) {
firstStep = (endAngle - startAngle) / 5;
} else {
firstStep = secondStep;
isSecond = true;
}
}
Point finalPoint = Point(location.x * pow(2, numLevels), location.y * pow(2, numLevels));
vector<Point> points = GetRotatePoints(Size(model.cols * pow(2, numLevels), model.rows * pow(2, numLevels)), angle);
for (int j = 0; j < points.size(); j++)
{
points[j].x += finalPoint.x;
points[j].y += finalPoint.y;
}
return MatchResult(points, angle, score);
}
int main() {
//读取所有图像
vector<Mat> imgs;
string imageName;
string path = "D:\\zMaterials\\algorithm\\Algo\\Data\\modules\\";
ifstream fin(path + "modules.seq");
while (getline(fin, imageName))
{
Mat img = imread(path + imageName + ".png");
imgs.push_back(img);
}
Mat templateImg = imread(path + "template.png");
int i = 0;
for (Mat img: imgs)
{
i += 1;
MatchResult matchResult = rotateMatch(img, templateImg, 0, 360, 30, 1, 0);
vector<Point> points = matchResult.points;
cout << i << "- 角度:" << matchResult.angle << endl;
cout << i << "- 得分:" << matchResult.score << endl;
line(img, points[0], points[1], Scalar(255, 0, 0), 2);
line(img, points[1], points[2], Scalar(255, 0, 0), 2);
line(img, points[2], points[3], Scalar(255, 0, 0), 2);
line(img, points[3], points[0], Scalar(255, 0, 0), 2);
Point pt1 = Point((points[0].x + points[1].x) / 2, (points[0].y + points[1].y) / 2);
Point pt2 = Point((points[2].x + points[3].x) / 2, (points[2].y + points[3].y) / 2);
arrowedLine(img, pt2, pt1, Scalar(0, 0, 255), 2);
imshow("img_" + to_string(i), img);
waitKey(0);
}
return 0;
}