点击上方“小白学视觉”,选择加"星标"或“置顶

 
 
重磅干货,第一时间送达

导读

本文将介绍使用OpenCV实现多角度模板匹配的详细步骤 + 代码。(来源公众号:OpenCV与AI深度学习)

背景介绍

    熟悉OpenCV的朋友肯定都知道OpenCV自带的模板匹配matchTemplate方法是不支持旋转的,也就是说当目标和模板有角度差异时匹配常常会失败,可能目标只是轻微的旋转,匹配分数就会下降很多,导致匹配精度下降甚至匹配出错。另一个方法是matchShape(形状匹配),匹配时需要轮廓分明才容易匹配成功,但无法的到匹配角度,也不方便使用。本文介绍基于matchTemplate + 旋转 + 金字塔下采样实现多角度的模板匹配,返回匹配结果(斜矩形、角度、方向)。

实现效果

如上面视频所示,本方法可以对不同角度的元件做匹配并标注元件方向。

实现思路

【1】如何适应目标的角度变化?我们可以将模板旋转,从0~360°依次匹配找到最佳的匹配位置;

00d5d6a21ebffdc0908811931d5b493b.gif

【2】如何提高匹配速度?使用金字塔下采样,将模板和待匹配图均缩小后匹配;加大匹配搜寻角度的步长,比如从每1°匹配一次改为每5°匹配一次等。

322eadabacf008f83b8d64ce83de33aa.png

实现步骤:

【1】旋转模板图像。旋转图像本身比较简单,下面是代码:

//旋转图像
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掩码有什么用?看下面链接文章介绍:

实战 | OpenCV带掩码(mask)的模板匹配使用技巧与演示(附源码)

② 旋转时不放大目标图像尺寸,剔除黑边剩余部分做mask来匹配。

【2】图像金字塔下采样。什么是图像金字塔?什么是上下采样?直接百度。

e6289b5359cfd6cfe89dd7328649e3c5.png

下采样的目的前面已介绍,减小图像分辨率提高图像匹配速度,代码如下:

//对模板图像和待检测图像分别进行图像金字塔下采样
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在目标图中匹配,记录最佳匹配分数,以及对应的角度。

542068d8e28911cc10000f7c9cd32150.png

4d3deb85a4dc3ade2d0f5fde97b6c06d.png

cb279c880ffc316c0fc03d7b293f8c60.png

模板匹配详细使用说明:

https://docs.opencv.org/4.x/df/dfb/group__imgproc__object.html#ga586ebfb0a7fb604b35a23d85391329be

旋转匹配代码:

TemplateMatchModes matchMode = TM_CCOEFF_NORMED;
  switch (nccMethod)
  {
  case 0:
    matchMode = TM_SQDIFF;
    break;
  case 1:
    matchMode = TM_SQDIFF_NORMED;
    break;
  case 2:
    matchMode = TM_CCORR;
    break;
  case 3:
    matchMode = TM_CCORR_NORMED;
    break;
  case 4:
    matchMode = TM_CCOEFF;
    break;
  case 5:
    matchMode = TM_CCOEFF_NORMED;
    break;
  }


  //在没有旋转的情况下进行第一次匹配
  double minVal, maxVal;
  Point minLoc, maxLoc;
  matchTemplate(src, model, result, matchMode);
  minMaxLoc(result,  &minVal, &maxVal, &minLoc, &maxLoc);


  Point location = maxLoc;
  double temp = maxVal;
  double angle = 0;


  Mat newImg;


  //以最佳匹配点左右十倍角度步长进行循环匹配,直到角度步长小于参数角度步长
  if (nccMethod == 0 || nccMethod == 1)
  {
    do
    {
      for (int i = 0; i <= (int)range / step; i++)
      {
        newImg = ImageRotate(model, start + step * i);
        
        matchTemplate(src, newImg, result, matchMode);
        double minval, maxval;
        Point minloc, maxloc;
        minMaxLoc(result, &minval, &maxval, &minloc, &maxloc);
        if (maxval < temp)
        {
          location = maxloc;
          temp = maxval;
          angle = start + step * i;
        }
      }
      range = step * 2;
      start = angle - step;
      step = step / 10;
    } while (step > angleStep);
    return ResultPoint(location.x * pow(2, numLevels) + modelImage.cols / 2, location.y * pow(2, numLevels) + modelImage.rows / 2, -angle, temp);
  }
  else
  {
    do
    {
      for (int i = 0; i <= (int)range / step; i++)
      {
        newImg = ImageRotate(model, start + step * i);
        imshow("rotate", newImg);
        imshow("src-pyrDown", src);
        waitKey();
        matchTemplate(src, newImg, result, matchMode);
        double minval, maxval;
        Point minloc, maxloc;
        minMaxLoc(result, &minval, &maxval, &minloc, &maxloc);
        if (maxval > temp)
        {
          location = maxloc;
          temp = maxval;
          angle = start + step * i;
        }
      }
      range = step * 2;
      start = angle - step;
      step = step / 10;
    } while (step > angleStep);
    if (temp > thresScore)
    {
      return ResultPoint(location.x * pow(2, numLevels), location.y * pow(2, numLevels), -angle, temp);
    }
  }
  return ResultPoint(-1, -1, 0, 0);

【4】标注匹配结果根据模板图大小、匹配结果角度计算出匹配后的矩形四个角点,根据角点关系即可绘制方向:

//获取旋转后矩形对应的端点坐标
vector<Point> GetRotatePoints(Mat img, Rect inRect, double angle)
{
  Rect rect = inRect;
  vector<Point>pts;
  Point2f center = Point2f(img.cols / 2, img.rows / 2);
  Mat M = getRotationMatrix2D(center, angle, 1.0);
  //cout << M << endl;


  Mat ptMat = Mat::ones(3, 4, CV_32FC1);
  ptMat.at<float>(0, 0) = 0;
  ptMat.at<float>(0, 1) = (float)rect.width - 1;
  ptMat.at<float>(0, 2) = (float)rect.width - 1;
  ptMat.at<float>(0, 3) = 0;
  ptMat.at<float>(1, 0) = 0;
  ptMat.at<float>(1, 1) = 0;
  ptMat.at<float>(1, 2) = (float)rect.height - 1;
  ptMat.at<float>(1, 3) = (float)rect.height - 1;


  M.convertTo(M, CV_32F);


  Mat result = M * ptMat;
  //cout << result << endl;
  pts.push_back(Point((int)result.at<float>(0, 0), (int)result.at<float>(1, 0)));
  pts.push_back(Point((int)result.at<float>(0, 1), (int)result.at<float>(1, 1)));
  pts.push_back(Point((int)result.at<float>(0, 2), (int)result.at<float>(1, 2)));
  pts.push_back(Point((int)result.at<float>(0, 3), (int)result.at<float>(1, 3)));
  return pts;
}

【5】举例演示模板图从下图中截取并保存template.png:

c6e1cf9155fc889fb929f0c2c0350b1c.png

测试图像12张,来自Halcon例程图片,路径如下:

C:\Users\Public\Documents\MVTec\HALCON-20.11-Steady\examples\images\modules

043947d47b42070a6a61affd25d78b11.png

匹配结果:

f33bfc54a82e153b8bdae3190210f02c.png

68ca8690ac0f085b81bfb50caabc7b1b.png

1c437141af45e22775af0b7d9dbfa6b8.png

8d11c9bda71b771e41b055cf84f71436.png

105e01c512dfa7e7efa9e71242bc2d7c.png

后记

    可以添加匹配分数阈值和NMS实现多目标匹配,后续还会介绍其他匹配方法的实现,敬请期待。

下载1:OpenCV-Contrib扩展模块中文版教程

在「小白学视觉」公众号后台回复:扩展模块中文教程,即可下载全网第一份OpenCV扩展模块教程中文版,涵盖扩展模块安装、SFM算法、立体视觉、目标跟踪、生物视觉、超分辨率处理等二十多章内容。


下载2:Python视觉实战项目52讲
在「小白学视觉」公众号后台回复:Python视觉实战项目,即可下载包括图像分割、口罩检测、车道线检测、车辆计数、添加眼线、车牌识别、字符识别、情绪检测、文本内容提取、面部识别等31个视觉实战项目,助力快速学校计算机视觉。


下载3:OpenCV实战项目20讲
在「小白学视觉」公众号后台回复:OpenCV实战项目20讲,即可下载含有20个基于OpenCV实现20个实战项目,实现OpenCV学习进阶。


交流群

欢迎加入公众号读者群一起和同行交流,目前有SLAM、三维视觉、传感器、自动驾驶、计算摄影、检测、分割、识别、医学影像、GAN、算法竞赛等微信群(以后会逐渐细分),请扫描下面微信号加群,备注:”昵称+学校/公司+研究方向“,例如:”张三 + 上海交大 + 视觉SLAM“。请按照格式备注,否则不予通过。添加成功后会根据研究方向邀请进入相关微信群。请勿在群内发送广告,否则会请出群,谢谢理解~
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐