꿈꾸는 개발자

OpenCV 라이브러리를 이용한 특징점 매칭 예제(C++) 본문

컴퓨터 비전

OpenCV 라이브러리를 이용한 특징점 매칭 예제(C++)

Anssony 2023. 2. 21. 21:05

작년 말부터 갑자기 연구실 과제에 대한 내용이 급격하게 많아지면서 너무 바쁜 일상을 보냈던 것 같다.

 

이번에 새해도 되었고(너무 많이 지났지만..) 새 마음으로 시작하고자 CMake와 OpenCV 및 SLAM에서 많이 사용되는 라이브러리들을 하나하나씩 익혀보면서 공부해나가는 1년을 보내려고 한다.

정말 기초부터 차근차근히 공부해보고 있다.

또한, C++ 언어에 대한 기량이 부족하기 때문에 같이 공부하면서 진행하려고 한다.

 

https://github.com/ans2568/SLAM_Tutorial

 

GitHub - ans2568/SLAM_Tutorial: 혼자서 Cmake 프로젝트를 관리하기 위해 처음부터 배워서 기록하는 문서

혼자서 Cmake 프로젝트를 관리하기 위해 처음부터 배워서 기록하는 문서. Contribute to ans2568/SLAM_Tutorial development by creating an account on GitHub.

github.com

 

 

예제들을 하나하나 올리고 있던 와중 이번에 SLAM 프레임워크 중 frontend라고 부르는 부분에서 OpenCV 라이브러리를 이용한 특징점 매칭 예제를 구성하고 진행한 것을 토대로 글을 작성하려고 한다.

 

Test 환경

Ubuntu 22.04

CMake 3.22.1

gcc 11.3.0

OpenCV 4.x.x

 

#include <iostream>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/core.hpp>
#include <opencv2/features2d.hpp>
#include <opencv2/xfeatures2d.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/calib3d.hpp>
using namespace cv;

int main()
{
    Mat img = imread("../picture/puppy.png");
    Mat rotate_img = imread("../picture/rotated_puppy_only.png");
    // Mat rotate_img = imread("../puppy_only.png");

    if(img.empty() || rotate_img.empty())
    {
        std::cout<<"Can't find image" << std::endl;
        std::cout<<"please press any key and enter" << std::endl;
        std::cin.get();
        return -1;
    }
    
    Ptr<FeatureDetector> detector = ORB::create();              // ORB detector 생성
    std::vector<KeyPoint> keypoint, keypoint2;                  // keypoint 선언
    Mat desc, desc2;                                            // descriptor 선언
    detector->detectAndCompute(img, noArray(), keypoint, desc); // 이미지로부터 keypoint와 descriptor를 추출
    detector->detectAndCompute(rotate_img, noArray(), keypoint2, desc2);    // 이미지로부터 keypoint와 descriptor를 추출

    Ptr<DescriptorMatcher> matcher = BFMatcher::create(NORM_HAMMING);   // Hamming distance를 사용하는 BruteForce matcher 선언
    std::vector<std::vector<DMatch>> matches;                   // 매칭된 특징점 선언
    
    // knn(K-Nearest Neighbors)를 이용한 매칭점 2개를 matches에 저장(2차원 배열)
    // 2개의 매칭점을 추출하는데 첫 번째 원소가 Best Match, 두 번째 원소가 Second Best Match이다.
    matcher->knnMatch(desc, desc2, matches, 2);
    
    std::vector<DMatch> good;                         // 더 좋은 매칭점을 찾기 위한 DMatch타입 good 변수 선언
    for (const auto& m : matches)                     // for문으로 2차원 배열로 된 매칭점에서 거리에 따라 good에 넣어준다.
    {
        if(m[0].distance<0.75 * m[1].distance)        // Best Match가 Second Best Match보다 0.75배 작을 경우 더 좋은 매칭점으로 간주
            good.push_back(m[0]);
    }

    std::vector<Point2f> pts1, pts2;
    for(const auto& match : good)
    {
        pts1.push_back(keypoint[match.queryIdx].pt);    // 매칭점에 해당하는 index를 keypoint에 적용 후 해당 Point를 벡터에 넣는다.
        pts2.push_back(keypoint2[match.queryIdx].pt);
    }

    auto H = findHomography(pts1, pts2, RANSAC, 5.0);   // Homography Matrix를 keypoint들의 Point 좌표와 RANSAC을 이용해 계산한다.
    Mat transformed_img;
    warpPerspective(img, transformed_img, H, transformed_img.size());   // Homography Matrix를 이용한 warpping을 원본 이미지에 진행한다.
    
    Mat img_matches;
    drawMatches(img, keypoint, rotate_img, keypoint2, good, img_matches);   // 원본 이미지 1과 2에 해당하는 키 포인트들과 매칭점들을 보여준다.
    imshow("match", img_matches);
    waitKey();

    imwrite("../picture/output.png", img_matches);
}

 

 

 

전체 코드는 다음과 같고, 아래는 간단히 설명을 진행한다.

 

    Mat img = imread("../picture/puppy.png");
    Mat rotate_img = imread("../picture/rotated_puppy_only.png");
    // Mat rotate_img = imread("../puppy_only.png");

    if(img.empty() || rotate_img.empty())
    {
        std::cout<<"Can't find image" << std::endl;
        std::cout<<"please press any key and enter" << std::endl;
        std::cin.get();
        return -1;
    }

이미지를 읽어오는 과정으로 imread() 함수를 사용

이미지가 없을 경우 오류 출력하면서 종료

 

imread()에 대한 설명은 OpenCV 라이브러리 document를 참고하거나

위 깃헙에 opencv_example/src/imread.cpp를 보면 확인할 수 있다.

 

    Ptr<FeatureDetector> detector = ORB::create();              // ORB detector 생성
    std::vector<KeyPoint> keypoint, keypoint2;                  // keypoint 선언
    Mat desc, desc2;                                            // descriptor 선언
    detector->detectAndCompute(img, noArray(), keypoint, desc); // 이미지로부터 keypoint와 descriptor를 추출
    detector->detectAndCompute(rotate_img, noArray(), keypoint2, desc2);    // 이미지로부터 keypoint와 descriptor를 추출

ORB detector를 통해 keypoint와 descriptor를 추출하는 과정

detectAndCompute() 함수를 통해 이미지에서 추출한 keypoint와 descriptor를 해당하는 변수에 넣어준다.

KeyPoint -> keypoint, keypoint2

Descriptor -> desc, desc2

 

    Ptr<DescriptorMatcher> matcher = BFMatcher::create(NORM_HAMMING);   // Hamming distance를 사용하는 Brute Force matcher 선언
    std::vector<std::vector<DMatch>> matches;                   // 매칭된 특징점 선언

BFMatcher::create(NORM_HAMMING) 에서 NORM_HAMMING을 넣어주었기 때문에 Hamming distance를 이용한 Descriptor matcher를 선언한다.

여기서 BFMatcher는 Brute Force Matcher이고, Brute Force라는 의미는 대략 모든 경우의 수를 다 계산하면서 최적의 결과를 반환한다고 생각하면 될 것 같다.

이후 매칭점을 반환받기 위해 matches 변수를 미리 선언한다.

 

    // knn(K-Nearest Neighbors)를 이용한 매칭점 2개를 matches에 저장(2차원 배열)
    // 2개의 매칭점을 추출하는데 첫 번째 원소가 Best Match, 두 번째 원소가 Second Best Match이다.
    matcher->knnMatch(desc, desc2, matches, 2);

BFMatcher::create(NORM_HAMMING)으로 선언한 matcher를 이용해 knnMatch를 수행한다.

이때, knn이란 K-Nearest Neighbors라는 알고리즘으로 K 값에 해당하는 인접한 K개의 원소를 가져오는 알고리즘이다.

knnMatch()의 파라미터로 2를 넣어준 모습을 볼 수 있는데, 이때 2 값이 K를 의미한다.

따라서, 최적의 매칭점을 2개를 찾는다는 의미인데, 이때 첫 번째 원소가 Best Match이고, 두 번째 원소가 Second Best Match이다.

위에서 std::vector<std::vector<DMatch>> matches라고 선언한 이유가 아래 코드에서 knnMatch()에서 K=2를 사용하기에 2차원 배열이 되기 때문에 저렇게 선언하였다.

 

    std::vector<DMatch> good;                         // 더 좋은 매칭점을 찾기 위한 DMatch타입 good 변수 선언
    for (const auto& m : matches)                     // for문으로 2차원 배열로 된 매칭점에서 거리에 따라 good에 넣어준다.
    {
        if(m[0].distance<0.75 * m[1].distance)        // Best Match가 Second Best Match보다 0.75배 작을 경우 더 좋은 매칭점으로 간주
            good.push_back(m[0]);
    }

더 좋은 매칭점을 찾기 위해 knnMatch()를 이용해 얻은 매칭점들에서 첫 번째 원소가 두 번째 원소의 distance * 0.75보다 작으면 더 좋은 매칭점으로 판단하고 good이라는 벡터에 넣는다.

 

    std::vector<Point2f> pts1, pts2;
    for(const auto& match : good)
    {
        pts1.push_back(keypoint[match.queryIdx].pt);    // 매칭점에 해당하는 index를 keypoint에 적용 후 해당 Point를 벡터에 넣는다.
        pts2.push_back(keypoint2[match.queryIdx].pt);
    }

    auto H = findHomography(pts1, pts2, RANSAC, 5.0);   // Homography Matrix를 keypoint들의 Point 좌표와 RANSAC을 이용해 계산한다.

findHomography() 함수를 사용하기 위해 매칭점에 해당하는 index를 keypoint에 적용 후 해당 Point들을 선언한 pts1과 pts2에 넣고 호모그래피 행렬을 계산한다.

이때 아웃라이어들을 제거하기 위해 RANSAC을 사용하는데, RANSAC에 대해 궁금하거나 Homography 행렬이 무엇인지 궁금하다면 아래 링크를 참조하길 바란다.

해당 코드를 넣은 것은 여기 실행 결과에는 담기지 않았지만, 중간에 RANSAC을 이용해서 아웃라이어를 제거할 수 있을까 하고 넣었던 코드다!

 

RANSAC

https://gnaseel.tistory.com/33

 

중학생도 이해할 수 있는 RANSAC 알고리즘 원리

이 글은 RANSAC에 대해 아무것도 알지 못해도, 중학교 이상의 수학적 지식만 가지고 있다면 충분히 이해할 수 있도록 포스팅할 예정이다. 실제로 RANSAC은 매우 중요한 알고리즘이지만 실상 들여다

gnaseel.tistory.com

 

Homography

https://darkpgmr.tistory.com/80

 

[영상 Geometry #4] Homography 보완

앞서 설명한 [컴퓨터 비전에서의 Geometry #3] 2D 변환 (Transformations)에 대한 보완 글입니다. 내용상으로는 앞 글에 추가하는게 맞겠으나 이렇게 따로 포스팅하는게 좀더 효과적일 것 같습니다. 앞서

darkpgmr.tistory.com

 

    Mat img_matches;
    drawMatches(img, keypoint, rotate_img, keypoint2, good, img_matches);   // 원본 이미지 1과 2에 해당하는 키 포인트들과 매칭점들을 보여준다.
    imshow("match", img_matches);
    waitKey();

    imwrite("../picture/output.png", img_matches);

마지막으로 첫 번째 원소의 distance가 두 번째 원소의 distance * 0.75에 해당하는 매칭점들만을 가지고 drawMatches()를 통해 이미지 그린 후 imshow()를 통해 보여준다.

또한, 해당 결과를 imwrite()로 이미지 파일로 저장한다.

 

특징점 매칭 결과

실행 결과를 보면 어느정도 잘 잡고 있는 것을 확인할 수 있다!

 

Visual SLAM 논문도 읽고 ppt로  정리는 해놓았으나 블로그 글을 쓰지 않고 있어서 계속 쌓이는 중이다...부지런하게 글을 써보도록 하겠다.

 

추가적으로 코드를 구현하면서 공부를 진행하는 것 또한 글로 작성할 예정이다!

'컴퓨터 비전' 카테고리의 다른 글

OpenCV 주요 모듈 설명(C++)  (0) 2023.02.23