多角度的模板匹配

多角度的模板匹配

背景介绍

熟悉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;
}