지난번에 템플릿 매칭의 기본에 대해 알아보았습니다.
템플릿 매칭 함수 만들기
템플릿 매칭 방법에 따라 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));
각 영상에서 템플릿 영상과의 각도가 약간 다른 것들도 있지만 가장 유사도가 좋은 것으로 매칭을 한 것이기 때문에 검출이 된 것 입니다.
회전이 포함된 템플릿 매칭
이번에는 회전이 포함된 템플릿 매칭을 구현해보도록 하겠습니다.
원리는 간단합니다. 찾을 각도의 범위를 인자로 받아서 그 범위 구간 내에서 템플릿 매칭을 수행하고, 그 중 유사도가 가장 좋은 것을 선택하는 원리 입니다.
이걸 위해서는 영상 회전이 필요하겠죠?
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;
}