使用OpenCV在C++中匹配一组图像中的图像以进行识别

23

编辑:我通过这篇文章获得了足够的声望,可以编辑它并添加更多链接,这将有助于更好地阐述我的观点。

玩《以撒的结合》的人经常在小基座上找到重要物品。

目标是让用户能够按下一个按钮,指示他“框定”该物品(类似于 Windows 桌面的选框)。此框会提供我们所关注的区域(实际物品及其一些背景环境)以与整个网格中的物品进行比较。

理论上的用户框出的物品 enter image description here

理论上的物品网格(没有更多了,我只是从维基百科中摘取了这个) enter image description here

在网格中被确定为用户框选的物品的位置将表示与正确链接相关的图像上的某个区域,该链接可提供关于该物品的信息。

在网格中,该物品位于第一列、底部第三行。我在下面尝试的所有内容中都使用了这两个图像


我的目标是创建一个程序,可以从游戏《以撒的结合》中手动裁剪出一个物品,通过将图像与游戏中物品表的图像进行比较来识别裁剪出的物品,然后显示正确的维基页面。

从库的角度来看,这将是我的第一个“真正的项目”,因为它需要学习大量的库才能完成我想要的工作。让我有一些不知所措。

我已经尝试了一些选项,仅仅是从谷歌上搜索到的(你可以通过搜索方法名称和 opencv 快速找到我使用的教程,我的帐户由于某种原因严重受限于链接发布)。

使用 bruteforcematcher:

http://docs.opencv.org/doc/tutorials/features2d/feature_description/feature_description.html

#include <stdio.h>
#include <iostream>
#include "opencv2/core/core.hpp"
#include <opencv2/legacy/legacy.hpp>
#include <opencv2/nonfree/features2d.hpp>
#include "opencv2/highgui/highgui.hpp"

using namespace cv;

void readme();

/** @function main */
int main( int argc, char** argv )
{
  if( argc != 3 )
   { return -1; }

  Mat img_1 = imread( argv[1], CV_LOAD_IMAGE_GRAYSCALE );
  Mat img_2 = imread( argv[2], CV_LOAD_IMAGE_GRAYSCALE );

  if( !img_1.data || !img_2.data )
   { return -1; }

  //-- Step 1: Detect the keypoints using SURF Detector
  int minHessian = 400;

  SurfFeatureDetector detector( minHessian );

  std::vector<KeyPoint> keypoints_1, keypoints_2;

  detector.detect( img_1, keypoints_1 );
  detector.detect( img_2, keypoints_2 );

  //-- Step 2: Calculate descriptors (feature vectors)
  SurfDescriptorExtractor extractor;

  Mat descriptors_1, descriptors_2;

  extractor.compute( img_1, keypoints_1, descriptors_1 );
  extractor.compute( img_2, keypoints_2, descriptors_2 );

  //-- Step 3: Matching descriptor vectors with a brute force matcher
  BruteForceMatcher< L2<float> > matcher;
  std::vector< DMatch > matches;
  matcher.match( descriptors_1, descriptors_2, matches );

  //-- Draw matches
  Mat img_matches;
  drawMatches( img_1, keypoints_1, img_2, keypoints_2, matches, img_matches );

  //-- Show detected matches
  imshow("Matches", img_matches );

  waitKey(0);

  return 0;
  }

 /** @function readme */
 void readme()
 { std::cout << " Usage: ./SURF_descriptor <img1> <img2>" << std::endl; }

输入图像

使用flann可以得到更干净但同样不可靠的结果。

http://docs.opencv.org/doc/tutorials/features2d/feature_flann_matcher/feature_flann_matcher.html

#include <stdio.h>
#include <iostream>
#include "opencv2/core/core.hpp"
#include <opencv2/legacy/legacy.hpp>
#include <opencv2/nonfree/features2d.hpp>
#include "opencv2/highgui/highgui.hpp"

using namespace cv;

void readme();

/** @function main */
int main( int argc, char** argv )
{
  if( argc != 3 )
  { readme(); return -1; }

  Mat img_1 = imread( argv[1], CV_LOAD_IMAGE_GRAYSCALE );
  Mat img_2 = imread( argv[2], CV_LOAD_IMAGE_GRAYSCALE );

  if( !img_1.data || !img_2.data )
  { std::cout<< " --(!) Error reading images " << std::endl; return -1; }

  //-- Step 1: Detect the keypoints using SURF Detector
  int minHessian = 400;

  SurfFeatureDetector detector( minHessian );

  std::vector<KeyPoint> keypoints_1, keypoints_2;

  detector.detect( img_1, keypoints_1 );
  detector.detect( img_2, keypoints_2 );

  //-- Step 2: Calculate descriptors (feature vectors)
  SurfDescriptorExtractor extractor;

  Mat descriptors_1, descriptors_2;

  extractor.compute( img_1, keypoints_1, descriptors_1 );
  extractor.compute( img_2, keypoints_2, descriptors_2 );

  //-- Step 3: Matching descriptor vectors using FLANN matcher
  FlannBasedMatcher matcher;
  std::vector< DMatch > matches;
  matcher.match( descriptors_1, descriptors_2, matches );

  double max_dist = 0; double min_dist = 100;

  //-- Quick calculation of max and min distances between keypoints
  for( int i = 0; i < descriptors_1.rows; i++ )
  { double dist = matches[i].distance;
    if( dist < min_dist ) min_dist = dist;
    if( dist > max_dist ) max_dist = dist;
  }

  printf("-- Max dist : %f \n", max_dist );
  printf("-- Min dist : %f \n", min_dist );

  //-- Draw only "good" matches (i.e. whose distance is less than 2*min_dist )
  //-- PS.- radiusMatch can also be used here.
  std::vector< DMatch > good_matches;

  for( int i = 0; i < descriptors_1.rows; i++ )
  { if( matches[i].distance < 2*min_dist )
    { good_matches.push_back( matches[i]); }
  }

  //-- Draw only "good" matches
  Mat img_matches;
  drawMatches( img_1, keypoints_1, img_2, keypoints_2,
               good_matches, img_matches, Scalar::all(-1), Scalar::all(-1),
               vector<char>(), DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS );

  //-- Show detected matches
  imshow( "Good Matches", img_matches );

  for( int i = 0; i < good_matches.size(); i++ )
  { printf( "-- Good Match [%d] Keypoint 1: %d  -- Keypoint 2: %d  \n", i, good_matches[i].queryIdx, good_matches[i].trainIdx ); }

  waitKey(0);

  return 0;
 }

 /** @function readme */
 void readme()
 { std::cout << " Usage: ./SURF_FlannMatcher <img1> <img2>" << std::endl; }

在此输入图片描述

目前为止,模板匹配是我最好的方法。虽然在6种方法中,只能正确识别0-4项。

http://docs.opencv.org/doc/tutorials/imgproc/histograms/template_matching/template_matching.html

#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include <iostream>
#include <stdio.h>

using namespace std;
using namespace cv;

/// Global Variables
Mat img; Mat templ; Mat result;
char* image_window = "Source Image";
char* result_window = "Result window";

int match_method;
int max_Trackbar = 5;

/// Function Headers
void MatchingMethod( int, void* );

/** @function main */
int main( int argc, char** argv )
{
  /// Load image and template
  img = imread( argv[1], 1 );
  templ = imread( argv[2], 1 );

  /// Create windows
  namedWindow( image_window, CV_WINDOW_AUTOSIZE );
  namedWindow( result_window, CV_WINDOW_AUTOSIZE );

  /// Create Trackbar
  char* trackbar_label = "Method: \n 0: SQDIFF \n 1: SQDIFF NORMED \n 2: TM CCORR \n 3: TM CCORR NORMED \n 4: TM COEFF \n 5: TM COEFF NORMED";
  createTrackbar( trackbar_label, image_window, &match_method, max_Trackbar, MatchingMethod );

  MatchingMethod( 0, 0 );

  waitKey(0);
  return 0;
}

/**
 * @function MatchingMethod
 * @brief Trackbar callback
 */
void MatchingMethod( int, void* )
{
  /// Source image to display
  Mat img_display;
  img.copyTo( img_display );

  /// Create the result matrix
  int result_cols =  img.cols - templ.cols + 1;
  int result_rows = img.rows - templ.rows + 1;

  result.create( result_cols, result_rows, CV_32FC1 );

  /// Do the Matching and Normalize
  matchTemplate( img, templ, result, match_method );
  normalize( result, result, 0, 1, NORM_MINMAX, -1, Mat() );

  /// Localizing the best match with minMaxLoc
  double minVal; double maxVal; Point minLoc; Point maxLoc;
  Point matchLoc;

  minMaxLoc( result, &minVal, &maxVal, &minLoc, &maxLoc, Mat() );

  /// For SQDIFF and SQDIFF_NORMED, the best matches are lower values. For all the other methods, the higher the better
  if( match_method  == CV_TM_SQDIFF || match_method == CV_TM_SQDIFF_NORMED )
    { matchLoc = minLoc; }
  else
    { matchLoc = maxLoc; }

  /// Show me what you got
  rectangle( img_display, matchLoc, Point( matchLoc.x + templ.cols , matchLoc.y + templ.rows ), Scalar::all(0), 2, 8, 0 );
  rectangle( result, matchLoc, Point( matchLoc.x + templ.cols , matchLoc.y + templ.rows ), Scalar::all(0), 2, 8, 0 );

  imshow( image_window, img_display );
  imshow( result_window, result );

  return;
}

http://imgur.com/pIRBPQM,h0wkqer,1JG0QY0,haLJzRF,CmrlTeL,DZuW73V#3

6个项目里面,有3个 fail 和 3个 pass。这个结果其实算是不错的,因为我尝试的第二个项目是enter image description here,结果都是 fail。

每个图片处理方法都有优点和缺点。我想问一下,模板匹配是我最好的选择吗?还是有其他处理方法我没有考虑到的,能够让我事半功倍?

怎样才能让用户手动裁剪图片呢?opencv 的文档真的很差,而且我在网上找到的例子都是用 C++ 或者 C 写的,而且非常老旧。

感谢你们的帮助。这次尝试真的很有趣。其实我本来可以放更多链接来更好地描述我的问题,但是网站说我发布了超过10个链接,即使我没发那么多。


以下是游戏中的一些项目示例:

石头是一种稀有物品,可以出现在屏幕的任何位置。像石头这样的物品是最适合用户手动裁剪来隔离物品的,否则它们的位置只会出现在几个特定的地方。

enter image description here

enter image description here

这是一次 boss 战后的房间,到处都是东西,中间还有透明部分。我想象这可能是处理得最困难的之一。

enter image description here

enter image description here

这是一间稀有房间,背景简单,没有物品的透明度。

enter image description here

enter image description here

这是游戏中所有物品的两张表格,我最终会把它们合成一张图片,但目前它们是直接从 isaac 维基上复制下来的。

enter image description here

enter image description here


1
所以我了解工作流大致如下:您和用户拥有某些大型图像,用户裁剪该图像的某个区域并将其发送给您;之后,您想将此区域与大图像中的所有项目进行比较,并找到最佳匹配项。这正确吗?到目前为止,它听起来更像分类而不是使用SURF检测到的匹配特征。快速查看Eigenfaces和Haar分类器等方法。此外,更多输入示例(剪裁区域和要搜索的大图像)和预期结果的示例将非常有帮助。 - ffriend
我编辑了原帖以更清楚地表达我的意图(最初我没有足够的声望来使用图像/链接)。我将研究你建议的那些东西,尽管乍一看它们似乎比我正在处理的东西更加复杂。不同项目之间输入不会有显着变化,网格本身将保持现状,来自裁剪图像的输入只是具有不同数量背景的不同项目,这取决于用户的准确性和不同的背景取决于用户所在的级别。我很快就会添加一些。 - 2c2c
2个回答

2
这里有一个重要的细节,就是您在表格中拥有每个项目的纯图像。您知道背景的颜色,并可以将项目从图片的其余部分分离出来。例如,除了表示图像本身的矩阵外,您还可以存储相同大小的1和0的矩阵,其中1对应于图像区域,0对应于背景。让我们称这个矩阵为“掩码”,项目的纯图像为“模式”。
有两种比较图像的方法:用模式匹配图像和用图像匹配模式。您所描述的是将图像与模式匹配-您有一些裁剪的图像,并希望找到类似的模式。相反,考虑在图像上搜索模式。
让我们首先定义一个函数match(),它接受相同大小的模式、掩码和图像,并检查掩码下的模式区域是否与图像中的区域完全相同(伪代码):
def match(pattern, mask, image):
    for x = 0 to pattern.width:
        for y = 0 to pattern.height: 
           if mask[x, y] == 1 and              # if in pattern this pixel is not part of background
              pattern[x, y] != image[x, y]:    # and pixels on pattern and image differ
               return False  
    return True

但是,图案和裁剪图像的大小可能不同。在级联分类器中使用的标准解决方案是使用滑动窗口 - 只需将模式“窗口”移动到图像上并检查模式是否与所选区域匹配。这基本上就是OpenCV中图像检测的工作方式。
当然,这种解决方案并不是非常稳健 - 裁剪、缩放或任何其他图像转换可能会改变一些像素,在这种情况下,方法match()将始终返回false。为了克服这个问题,您可以使用图像和模式之间的距离代替布尔值答案。在这种情况下,函数match()应该返回某个相似度值,例如0到1之间的值,其中1表示“完全相同”,而0表示“完全不同”。然后,您可以设置相似度的阈值(例如,图像应至少与模式相似85%),或者只选择具有最高相似度值的模式。
由于游戏中的物品是人造图像,并且它们的变化非常小,因此这种方法应该足够了。但是,对于更复杂的情况,您需要的不仅仅是简单的掩膜下的像素。正如我在评论中提到的那样,像Eigenfaces、使用Haar-like特征的级联分类器或甚至是Active Appearance Models这样的方法可能更有效。至于SURF,据我所知,它更适用于具有不同背景和所有这些事物的对象大小和角度变化的任务,而不是简单的像素。

2

我在解决自己的模板匹配问题时看到了你的问题,现在回来分享一下我根据自己的经验认为可能是你最好选择的方案。你可能已经放弃了这个问题,但嘿,也许有一天其他人会遇到类似的问题。

你分享的所有项目都不是一个实心矩形,由于opencv中的模板匹配无法使用掩码,因此你将始终比较参考图像与我必须假设至少是几种不同背景(更不用说在不同背景上找到的物品,使模板匹配变得更糟)的东西相对应。
它总是会比较背景像素混淆您的匹配,除非您能收集到参考图像可能出现的每种情况的截图。如果血液等贴花还会在物品周围引入更多的背景变量,那么模板匹配可能不会产生很好的结果。

因此,如果我是你,我会尝试以下两件事:

  1. 如果可能,请剪切出物品出现的每种情况的参考模板(这不是一件好事),然后将用户指定区域与每个物品的每个模板进行比较。从这些比较中选出最佳结果,你就会有一个正确的匹配(如果幸运的话)。
  2. 你分享的示例屏幕截图背景上没有任何黑暗/黑线,所以所有项目的轮廓都很明显。如果游戏在整个过程中保持一致,你可以在用户指定区域内找到边缘并检测外轮廓。事先,你需要处理每个参考物品的外轮廓并存储这些轮廓。然后,你可以将用户截取的轮廓与数据库中的每个轮廓进行比较,并将最佳匹配作为答案。

我相信这两种方法都适用于你,具体取决于游戏是否在你的截图中有很好的展现。

注意:轮廓匹配速度要比模板匹配快得多。足够快,可以实时运行,否则用户可能不需要剪裁任何东西。


本月发布了游戏的新版本,这可能会促使我重新尝试。然而,游戏中的一个重大变化是从Flash引擎转移到C++引擎。最初,我只尝试通过视觉来解决这个问题,因为分析Flash游戏的内存对我来说行不通。总的来说,我喜欢看到这一点,根据事情的进展,我可能会应用它。 - 2c2c

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接