본문 바로가기

프로그래밍 강좌/C++ - OpenCV

[OpenCV][C++] 템플릿 매칭 (template matching) 총정리(2) - 영상 회전 rotated matchTemplate matchShape 모양 형태 정합 찾기

지난번에 템플릿 매칭의 기본에 대해 알아보았습니다.

템플릿 매칭 함수 만들기

템플릿 매칭 방법에 따라 SQDIFF 최소값을 갖는 위치가 매칭 위치이고, 나머지 CCORR 또는 CCOEFF 는 최대값을 갖는 위치가 매칭 위치입니다.

가끔 구현을 하다보면 헷갈리는 경우가 발생할 수 있어서 함수로 만들어 놓으면 좋을 것 같습니다.

지난번에 템플릿 매칭 관련 기본 이론은 간략히 살펴보았으니 오늘은 바로 실전에 들어갑니다.

일단 함수는 입력 영상 템플릿 영상 empty()이면 안됩니다. 그리고 템플릿 매칭 8bit 또는 32bit floating-point 영상에서 동작을 합니다. 하지만 일반적으로 CV_8UC1 영상을 많이 사용하므로 함수 앞쪽 부분에 이와 관련된 예외처리를 해 놓고 시작하겠습니다.

또한 마스크 영상을 입력 받을 수 있습니다. 마스크 영상 TM_CCORR_NORMED 에서만 동작을 하는 것 같아요.

마스크 영상 유무에 따라 matchTemplate()을 부릅니다. 그 이후 minMaxLoc() 함수로 minVal, maxVal, minLoc, maxLoc 을 계산합니다.

마지막으로 템플릿 방법에 따라 minLoc 또는 maxLoc  matchPos로 받아 반환을 해주면 됩니다.

int templateMatching(double& match_score, cv::Point2i& match_pos, const cv::Mat& tmpl, 
       const cv::Mat& src, int method = cv::TM_CCORR_NORMED, const cv::Mat& mask = cv::Mat())
{
    if (src.empty() || tmpl.empty())                        return 1;
    if (src.cols < tmpl.cols || src.rows < tmpl.rows)       return 1;
    if (src.type() != CV_8UC1 || tmpl.type() != CV_8UC1)    return 1;

    cv::Mat result;
    if (mask.empty())    cv::matchTemplate(src, tmpl, result, method);
    else {
        if (cv::TM_CCORR_NORMED == method)  cv::matchTemplate(src, tmpl, result, method, mask);
        else                                cv::matchTemplate(src, tmpl, result, method);
    }

    double minVal, maxVal;
    cv::Point minLoc, maxLoc;
    cv::minMaxLoc(result, &minVal, &maxVal, &minLoc, &maxLoc, cv::Mat());

    if (cv::TM_SQDIFF_NORMED == method) {
        match_pos = cv::Point2i(minLoc.x, minLoc.y);
        match_score = minVal;
    }
    else {
        match_pos = cv::Point2i(maxLoc.x, maxLoc.y);
        match_score = maxVal;
    }

    return 0;
}
 

이 함수가 잘 동작하는지 확인해보겠습니다.

Test를 위해 3가지 영상을 로딩하고, 이에 맞는 템플릿 영상도 읽어 옵니다.

그리고 매칭 결과를 표시하기 위해 각 영상의 color 버전도 만들어 놓습니다.

cv::Mat src1 = cv::imread("../../images/Src1.bmp", cv::IMREAD_GRAYSCALE);
cv::Mat src2 = cv::imread("../../images/Src2.bmp", cv::IMREAD_GRAYSCALE);
cv::Mat src3 = cv::imread("../../images/Src3.bmp", cv::IMREAD_GRAYSCALE);

cv::Mat tmpl1 = cv::imread("../../images/tmpl1.bmp", cv::IMREAD_GRAYSCALE);
cv::Mat tmpl2 = cv::imread("../../images/tmpl2.bmp", cv::IMREAD_GRAYSCALE);
cv::Mat tmpl3 = cv::imread("../../images/tmpl3.bmp", cv::IMREAD_GRAYSCALE);

cv::Mat src1_color, src2_color, src3_color;
cv::cvtColor(src1, src1_color, cv::COLOR_GRAY2BGR);
cv::cvtColor(src2, src2_color, cv::COLOR_GRAY2BGR);
cv::cvtColor(src3, src3_color, cv::COLOR_GRAY2BGR);
 

3가지 방법으로 각 영상을 매칭하여 matchPos를 계산합니다. 그리고 사각형 표시를 해 줍니다.

double match_score1, match_score2, match_score3;
cv::Point match_pos1, match_pos2, match_pos3;

templateMatching(match_score1, match_pos1, tmpl1, src1, cv::TM_CCOEFF_NORMED);
templateMatching(match_score2, match_pos2, tmpl2, src2, cv::TM_CCORR_NORMED);
templateMatching(match_score3, match_pos3, tmpl3, src3, cv::TM_SQDIFF_NORMED);

cv::Rect rect1_(cv::Point(match_pos1.x, match_pos1.y), tmpl1.size());
cv::Rect rect2_(cv::Point(match_pos2.x, match_pos2.y), tmpl2.size());
cv::Rect rect3_(cv::Point(match_pos3.x, match_pos3.y), tmpl3.size());

cv::rectangle(src1_color, rect1_, cv::Scalar(0, 0, 255));
cv::rectangle(src2_color, rect2_, cv::Scalar(0, 0, 255));
cv::rectangle(src3_color, rect3_, cv::Scalar(0, 0, 255));
 

각 영상에서 템플릿 영상과의 각도가 약간 다른 것들도 있지만 가장 유사도가 좋은 것으로 매칭을 한 것이기 때문에 검출이 된 것 입니다.

 

회전이 포함된 템플릿 매칭

이번에는 회전이 포함된 템플릿 매칭을 구현해보도록 하겠습니다.

원리는 간단합니다. 찾을 각도의 범위를 인자로 받아서 그 범위 구간 내에서 템플릿 매칭을 수행하고, 그 중 유사도가 가장 좋은 것을 선택하는 원리 입니다.

이걸 위해서는 영상 회전이 필요하겠죠?

https://blog.naver.com/dorergiverny/223103254105

 

[OpenCV][C++] 영상 회전(image rotation)하는 방법 - getRotationMatrix2D(), warpAffine()

이번에는 영상을 특정 각도로 회전시키는 방법에 대해 알아보겠습니다. 2D 회전 행렬은 아래와 같습니다. ...

blog.naver.com

 

rotationMatching()이라는 함수를 만들어보겠습니다.

위의 템플릿 매칭 함수와 유사합니다. for 문을 이용해서 입력 받은 minAngle 부터 maxAngle 까지 deltaAngle  step으로 회전 시키고 매칭하고 유사도를 비교해서 더 좋은 유사도를 가지면 출력 변수를 업데이트 해주는 것 입니다.

영상을 회전시킨 후 매칭을 해야 해서 마스크를 만들었어요. 영상을 회전시킨 결과 비는 공간이 생길텐데 그 부분을 검은 색으로 처리를 했는데, 만약 그 영상을 그대로 매칭을 하게 되면 검은 색 영역도 매칭율에 영향을 미치기 때문입니다.

int rotationMatching(double& match_score, double& match_angle, cv::Rect& rect, const cv::Mat& tmpl, const cv::Mat& src,
    double minAngle, double maxAngle, double deltaAngle, int method = cv::TM_CCORR_NORMED, const bool& isForegroundBlack = false)
{
    if (src.empty() || tmpl.empty())                        return 1;
    if (src.cols < tmpl.cols || src.rows < tmpl.rows)       return 1;
    if (src.type() != CV_8UC1 || tmpl.type() != CV_8UC1)    return 1;

    auto maxCost = DBL_MIN;
    auto minCost = DBL_MAX;
    double tmp_score = 0.;

    cv::Point match_pos(0, 0);
    cv::Point match_pos_final(0, 0);

    cv::Mat tmpl_rot = tmpl.clone();

    for (double angle = minAngle; angle <= maxAngle; angle += deltaAngle) {
        cv::Mat tmpl_mask;        
        if (isForegroundBlack)      tmpl_rot = 255 - tmpl_rot;
        cv::Mat outer = ImageRotateOuter(tmpl_rot, angle);
        cv::threshold(outer, tmpl_mask, 1, 255, cv::THRESH_BINARY);
        if (isForegroundBlack)       outer = 255 - outer;
        
        templateMatching(tmp_score, match_pos, outer, src, method, tmpl_mask);

        if (method == cv::TM_SQDIFF_NORMED) {
            if (tmp_score < minCost) {
                minCost = tmp_score;
                match_angle = angle;
                rect = cv::Rect({ match_pos.x, match_pos.y, outer.cols, outer.rows });
                match_pos_final = match_pos;
            }            
        }
        else {
            if (tmp_score > maxCost) {
                maxCost = tmp_score;
                match_angle = angle;
                rect = cv::Rect({ match_pos.x, match_pos.y, outer.cols, outer.rows });
                match_pos_final = match_pos;
            }
        }

        match_score = (method == cv::TM_SQDIFF_NORMED) ? minCost : maxCost;
    }

    return 0;
}
 

위 함수는 아래와 같이 특정 각도의 물체에 대해 매칭을 하도록 합니다.

rotationMatching(match_score1, match_angle1, rect1, tmpl1, src1, 10, 30, 2, cv::TM_CCORR_NORMED);
rotationMatching(match_score2, match_angle2, rect2, tmpl2, src2, -70, -50, 2, cv::TM_CCORR_NORMED);
rotationMatching(match_score3, match_angle3, rect3, tmpl3, src3, 10, 30, 2, cv::TM_CCORR_NORMED, true);
 

동작 원리를 src1에 대해서만 알아보면,

입력 템플릿 영상 10도~30도까지 2도 간격으로 회전시키면서 매칭을 수행하고 제일 유사도가 높은 위치와 각도를 저장하여 그 것을 결과로 표시하는 것 입니다.

이 방법은 같은 물체가 여러개 있을 때 특정 각도의 물체의 위치를 알아낼 때 유용하겠죠?

이런 방식으로 해서 각 영상에서 특정 각도의 물체를 검출한 결과는 아래와 같습니다.

cv::RotatedRect rt1(cv::Point(rect1.x + rect1.width / 2, rect1.y + rect1.height / 2), sz1, -match_angle1);
cv::RotatedRect rt2(cv::Point(rect2.x + rect2.width / 2, rect2.y + rect2.height / 2), sz2, -match_angle2);
cv::RotatedRect rt3(cv::Point(rect3.x + rect3.width / 2, rect3.y + rect3.height / 2), sz3, -match_angle3);

cv::Point2f points1[4], points2[4], points3[4];
rt1.points(points1);
rt2.points(points2);
rt3.points(points3);
for (int i = 0; i < 4; i++) {
    cv::line(src1_color, points1[i], points1[(i + 1) % 4], cv::Scalar(0, 0, 255), 4);
    cv::line(src2_color, points2[i], points2[(i + 1) % 4], cv::Scalar(0, 0, 255), 4);
    cv::line(src3_color, points3[i], points3[(i + 1) % 4], cv::Scalar(0, 0, 255), 4);
}
 

특정 각도의 물체를 잘 찾은 모습을 보실 수 있습니다.

전체 소스는 아래와 같습니다.

#include <iostream>
#include "opencv2/opencv.hpp"

cv::Mat ImageRotateOuter(const cv::Mat src, double angle) {
    cv::Point2d center(src.cols / 2.0, src.rows / 2.0);
    cv::Mat M = cv::getRotationMatrix2D(center, angle, 1.0);
    cv::Rect bbox = cv::RotatedRect(center, src.size(), angle).boundingRect();
    M.at<double>(0, 2) += bbox.width / 2.0 - center.x;
    M.at<double>(1, 2) += bbox.height / 2.0 - center.y;    
    cv::Mat dst;
    cv::warpAffine(src, dst, M, bbox.size());
    return std::move(dst);
}

int templateMatching(double& match_score, cv::Point2i& match_pos, const cv::Mat& tmpl, const cv::Mat& src, int method = cv::TM_CCORR_NORMED, const cv::Mat& mask = cv::Mat())
{
    if (src.empty() || tmpl.empty())                        return 1;
    if (src.cols < tmpl.cols || src.rows < tmpl.rows)       return 1;
    if (src.type() != CV_8UC1 || tmpl.type() != CV_8UC1)    return 1;

    cv::Mat result;
    if (mask.empty())    cv::matchTemplate(src, tmpl, result, method);
    else {
        if (cv::TM_CCORR_NORMED == method)  cv::matchTemplate(src, tmpl, result, method, mask);
        else                                cv::matchTemplate(src, tmpl, result, method);
    }

    double minVal, maxVal;
    cv::Point minLoc, maxLoc;
    cv::minMaxLoc(result, &minVal, &maxVal, &minLoc, &maxLoc, cv::Mat());

    if (cv::TM_SQDIFF_NORMED == method) {
        match_pos = cv::Point2i(minLoc.x, minLoc.y);
        match_score = minVal;
    }
    else {
        match_pos = cv::Point2i(maxLoc.x, maxLoc.y);
        match_score = maxVal;
    }

    return 0;
}

int rotationMatching(double& match_score, double& match_angle, cv::Rect& rect, const cv::Mat& tmpl, const cv::Mat& src,
    double minAngle, double maxAngle, double deltaAngle, int method = cv::TM_CCORR_NORMED, const bool& isForegroundBlack = false)
{
    if (src.empty() || tmpl.empty())                        return 1;
    if (src.cols < tmpl.cols || src.rows < tmpl.rows)       return 1;
    if (src.type() != CV_8UC1 || tmpl.type() != CV_8UC1)    return 1;

    auto maxCost = DBL_MIN;
    auto minCost = DBL_MAX;
    double tmp_score = 0.;

    cv::Point match_pos(0, 0);
    cv::Point match_pos_final(0, 0);

    cv::Mat tmpl_rot = tmpl.clone();

    for (double angle = minAngle; angle <= maxAngle; angle += deltaAngle) {
        cv::Mat tmpl_mask;        
        if (isForegroundBlack)      tmpl_rot = 255 - tmpl_rot;
        cv::Mat outer = ImageRotateOuter(tmpl_rot, angle);
        cv::threshold(outer, tmpl_mask, 1, 255, cv::THRESH_BINARY);
        if (isForegroundBlack)       outer = 255 - outer;
        
        templateMatching(tmp_score, match_pos, outer, src, method, tmpl_mask);

        if (method == cv::TM_SQDIFF_NORMED) {
            if (tmp_score < minCost) {
                minCost = tmp_score;
                match_angle = angle;
                rect = cv::Rect({ match_pos.x, match_pos.y, outer.cols, outer.rows });
                match_pos_final = match_pos;
            }            
        }
        else {
            if (tmp_score > maxCost) {
                maxCost = tmp_score;
                match_angle = angle;
                rect = cv::Rect({ match_pos.x, match_pos.y, outer.cols, outer.rows });
                match_pos_final = match_pos;
            }
        }

        match_score = (method == cv::TM_SQDIFF_NORMED) ? minCost : maxCost;
    }

    return 0;
}

int main()
{   
    cv::Mat src1 = cv::imread("../../images/Src1.bmp", cv::IMREAD_GRAYSCALE);
    cv::Mat tmpl1 = cv::imread("../../images/tmpl1.bmp", cv::IMREAD_GRAYSCALE);

    cv::Mat src1_color;
    cv::cvtColor(src1, src1_color, cv::COLOR_GRAY2BGR);

    double match_score1;
    cv::Point match_pos1;

    templateMatching(match_score1, match_pos1, tmpl1, src1, cv::TM_CCOEFF_NORMED);
    cv::Rect rect1_(cv::Point(match_pos1.x, match_pos1.y), tmpl1.size());
    cv::rectangle(src1_color, rect1_, cv::Scalar(0, 0, 255), 3);

    cv::Size sz1 = tmpl1.size();
    double match_angle1;
    cv::Rect rect1;

    rotationMatching(match_score1, match_angle1, rect1, tmpl1, src1, 10, 30, 2, cv::TM_CCORR_NORMED);
    cv::RotatedRect rt1(cv::Point(rect1.x + rect1.width / 2, rect1.y + rect1.height / 2), sz1, -match_angle1);

    cv::Point2f points1[4];
    rt1.points(points1);

    for (int i = 0; i < 4; i++) {
        cv::line(src1_color, points1[i], points1[(i + 1) % 4], cv::Scalar(0, 255, 0), 4);
    }
    return 0;
}