템플릿 매칭

  • 입력 영상에서 (작은 크기의) 부분 영상 위치를 찾는 기법
  • 템플릿(template) : 찾을 대상이 되는 작은 영상

템플릿 매칭 함수

void matchTemplate(InputArray image, InputArray templ, OutputArray result, int method, InputArray mask = noArray());
  • image : (입력) 입력 영상. 8비트 or 32비트
  • templ : (입력) 템플릿 영상. image보다 같거나 작은 크기, 같은 타입
  • result : (출력) 비교 결과를 저장할 행렬. 1채널 32비트 실수형
    • image의 크기는 W x H 이고, templ의 크기는 w x h 이면 result 크기는 (W - w + 1) x (H - h + 1)
  • mask : 마스크 영상
  • method : 비교 방법
    • TM_SQDIFF / TM_SQDIFF_NORMED : Sum of squared difference
    • TM_CCORR / TM_CCORR_NORMED : (Cross) Correlation
    • TM_CCOEFF / TM_CCOEFF_NORMED : Correlation Coefficent

image

image

image

Sum of Squared Difference

  • SQDIFF는 두개의 값이 작을수록 서로 비슷한 것이므로 x와 y가 더 비슷하다고 할 수 있음

image

Correlation

  • Correlation은 두개의 값이 비슷할수록 크게 나타나므로 x와 y가 더 비슷하다고 할 수 있음
  • 만약 z대신 z’ 의 값을 x와 비교할 경우 튀는 값이 있다는 이유로 x, y와 비교했을 때보다 높은 값이 나옴 (부정확함)
    • 그래서 정규화된 Correlation을 계산

image

image

Normalized Correlation

  • x와 z’ 값을 비교했을때 z’의 튀는 값의 제곱이 분모형태로 들어가므로, 정규화된 값은 작아짐
  • Normalized Correlation은 두개의 값이 비슷할수록 크게 나타나므로 x와 y가 x와 z’ 보다 더 비슷하다고 할 수 있음

image

image

Correlation Coefficient

  • 평균값을 빼고 Correlation을 계산
    • x 값에서 평균값 3을 빼면 x’ 이 나옴
    • y 값에서 평균값 4를 빼면 y’ 이 나옴
    • z 값에서 평균값 2를 빼면 z’ 이 나옴
  • x와 y의 CCOEFF를 구하는 것은 x’와 y’의 CCORR을 구하는 것 과 같음
  • CCOEFF는 두개의 값이 비슷할수록 크게 나타나므로 x와 y가 더 비슷하다고 할 수 있음
  • CCOEFF(x, y)를 정규화해서 계산하게 되면 1 값이 나오고, CCOEFF(x, z)를 정규화해서 계산하게 되면 -1 값이 됨
    • Normalized CCOEFF의 결과값은 완전히 일치하면 1, 상호 연관성이 없으면 0, 역일치하면 -1이 나옴

image

유사도 기준에 따른 템플릿 매칭 결과

  • TM_CCORR에서는 로고의 하얀색 값이 크기 때문에 값이 튀어서 로고를 검출함

image

  • TM_SQDIFF와 TM_SQDIFF_NORMED 는 값이 작을수록(검정색) 유사도가 높아지고 나머지는 값이 클수록(흰색) 유사도가 높아짐
  • 다양한 비교 방법중 가장 정확도가 높은 방법은 TM_CCOEFF_NORMED
    • 복잡한 수식을 사용하는 만큼 연산의 시간이 더 길어짐

image

템플릿 매칭 예제 코드

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

using namespace std;
using namespace cv;

int main()
{
	Mat src = imread("circuit.bmp", IMREAD_GRAYSCALE); // 입력 영상
	Mat tmpl = imread("crystal.bmp", IMREAD_GRAYSCALE); // 템플릿 영상
	// 템플릿 매칭은 반드시 그레이스케일 영상을 사용해야하는 것은 아님
	// 그레이스케일 영상을 사용했을때 디테일이 많고 메모리도 효율적으로 사용하고 연산도 빨리 하는 장점이 있음

#if 0
	src = imread("wheres_wally.jpg", IMREAD_GRAYSCALE); // IMREAD_COLOR
	tmpl = imread("wally.bmp", IMREAD_GRAYSCALE);
#endif

	if (src.empty() || tmpl.empty()) {
		cerr << "Image load failed!" << endl;
		return -1;
	}

#if 1 // 그대로 수행하면 너무 쉽게 찾으므로 영상을 살짝 변형을 함
	src = src + 50; // src의 영상을 바꾸기 위해 밝기를 조절함

	Mat noise(src.size(), CV_32S); // 부호가 있는 정수형 형태로 잡음을 생성
	randn(noise, 0, 10); // 10, 50, 100 // 표준편차가 10인 가우시안 잡음을 발생시킴
	add(src, noise, src, noArray(), CV_8U); // 지저분한 형태의 입력 영상으로 만들고 결과 행렬은 그레이스케일 형식으로 저장하게 함. 
	// 이렇게 설정해야 음수로 설정된 Noise값도 추가가 되므로 조금 더 자연스러운 형태의 잡음이 추가가 됨
#endif

#if 1
	GaussianBlur(src, src, Size(), 1);
	GaussianBlur(tmpl, tmpl, Size(), 1);
#endif

#if 0
	resize(src, src, Size(), 0.9, 0.9); // 0.8, 0.7
#endif

#if 0
	Point2f cp(src.cols / 2.f, src.rows / 2.f);
	Mat rot = getRotationMatrix2D(cp, 10, 1); // 20, 30
	warpAffine(src, src, rot, src.size());
#endif

	Mat res, res_norm;
	matchTemplate(src, tmpl, res, TM_CCOEFF_NORMED);
	// TM_CCOEFF_NORMED로 지정했으므로 res 값은 -1 ~ 1의 값으로 나타는데 1에 가까울수록 좀 더 비슷함
	normalize(res, res_norm, 0, 255, NORM_MINMAX, CV_8U);
	// res의 값이 어떻게 구성되어있는지 그레이스케일로 변환해서 눈으로 확인해보자

	double maxv;
	Point maxloc;
	minMaxLoc(res, 0, &maxv, 0, &maxloc); // 최댓값과 최대위치를 찾기 위해
    // res_norm으로 안하는 이유는 실수형을 정수형으로 바꾸면서 운이 없으면 최댓값 위치가 여러군데서 발생할 수 있음

	cout << "maxv: " << maxv << endl;
	cout << "maxloc: " << maxloc << endl; // 최댓값, 최댓값 위치 출력

	Mat dst;
	cvtColor(src, dst, COLOR_GRAY2BGR); // src 그레이스케일 영상을 dst 컬러영상으로 바꿈

	rectangle(dst, Rect(maxloc.x, maxloc.y, tmpl.cols, tmpl.rows), Scalar(0, 0, 255), 2); 
	// Rect 함수를 이용해서 검출된 위치를 표현
	// 그리는 위치를 사각형의 좌측상단을 지정하고 템플릿 영상의 크기와 동일하게 사각형의 크기를 지정함
	// 빨간색 2pixel의 두께로 사각형을 그림

//	imshow("src", src);
	imshow("tmpl", tmpl);
	imshow("res_norm", res_norm);
	imshow("dst", dst);
	waitKey();
}
  • 변형시킨 src의 영상

image

  • matchTemplate()함수를 통해 나온 결과 res의 값이 어떻게 구성되어있는지 그레이스케일로 변환한 res_norm의 영상
    • 눈으로봐도 영상의 우측 중앙 아래에 흰색 부분이 비슷한게 있는 곳이라는 것을 알 수 있음

image

  • 템플릿 매칭을 완료한 dst 결과 영상
    • matchTemplate의 결과의 최대값이 0.976662로 나옴
    • 입력영상을 변형없이 그대로 사용하면 최대값이 1로 나옴

image

image

  • noise 값을 50으로 올려도 잘 찾는 것을 확인할 수 있음
    • matchTemplate의 결과의 최대값이 0.711416로 나옴

image

image

  • noise 값을 100으로 올려도 잘 찾는 것을 확인할 수 있음
    • matchTemplate의 결과의 최대값이 0.442244로 나옴

image

image

  • 사이즈를 조절하면(resize) 0.9, 0.8일때는 찾지만 0.7 일 때는 못 찾고 다른 것을 찾음
    • Correlation Coefficient 값이 상당히 낮아짐

image

  • 입력 영상이 회전해도 찾기는 하지만 Correlation Coefficient 값이 상당히 낮아짐

image

  • 템플릿 매칭시, 입력 영상의 변형이 있을때
    • 잡음은 Gaussian Filter로 해결 가능
    • 밝기/명암비 변화가 있을 경우 Normalization으로 어느정도 괜찮은 결과가 나옴
    • 크기 변환이나 회전 변환은 템플릿 매칭에서는 취약함
      • 크기변환만 있을 경우 미리 여러가지 크기로 만들어 놓거나, 회전 변환만 있을 경우 미리 다양한 각도로 만들어 놓을 수는 있겠지만 다른 방법을 사용하는 것이 더 좋은 대안임

템플릿 매칭 응용

여러 개의 템플릿 매칭

  • 입력 영상에 여러개의 템플릿 영상이 존재하는 경우 어떤 방법으로 찾아낼 것 인가?

image

matchTemplate(src, tmpl, res, TM_CCOEFF_NORMED);
normalize(res, res_norm, 0, 255, NORM_MINMAX, CV_8UC1)	

image

  • 코드
#include <iostream>
#include "opencv2/opencv.hpp"

using namespace std;
using namespace cv;

int main()
{
	Mat src = imread("cookierun.png"); // 입력 영상
	Mat tmpl = imread("item.png"); // 템플릿 영상

	if (src.empty() || tmpl.empty()) {
		cerr << "Image load failed!" << endl;
		return -1;
	}

	Mat res, res_norm;
	matchTemplate(src, tmpl, res, TM_CCOEFF_NORMED);
	normalize(res, res_norm, 0, 255, NORM_MINMAX, CV_8UC1); // 위의 템플릿 매칭의 결과 행렬을 그레이스케일로 보여줌

	Mat local_max = res_norm > 220; // res_norm의 행렬이 220보다 큰 부분을 찾음

	Mat labels;
	int num = connectedComponents(local_max, labels); 
	// connectedComponent 함수를 이용해서 local_max에 6개의 하얀색 객체가 있음을 알기 때문에 num에는 7이라는 값이 전달됨

	Mat dst = src.clone();

	for (int i = 1; i < num; i++) { // for문을 통해 배경인 0번은 건너뛰고 1번 객체부터..
		Point max_loc;
		Mat mask = (labels == i); // labels 에서 i번 객체가 있는 부분만 하얀색으로 되는 마스크 행렬이 생성됨
		minMaxLoc(res, 0, 0, 0, &max_loc, mask); 
		// res에서 min값, max값을 찾는 것이 아닌 mask 부분이 흰색으로 된 부분에서만 최댓값 위치를 찾음
		

		cout << max_loc.x << ", " << max_loc.y << endl;

		Rect b_rect = Rect(max_loc.x, max_loc.y, tmpl.cols, tmpl.rows);
		rectangle(dst, b_rect, Scalar(0, 255, 255), 2);
	}

//	imshow("src", src);
//	imshow("templ", templ);
//	imshow("res_norm", res_norm);
	imshow("local_max", local_max);
	imshow("dst", dst);
	waitKey();
}
  • dst 결과 영상

image

image

  • res_norm의 결과 영상

image

  • local_max = res_norm > 220로 지정하고 local_max 영상
    • 220이라는 임의의 임계값보다 큰 부분만 표현
    • 여기 위치 근방에 아이템이 있음

image

  • 위 local_max 영상의 하얀색 부분을 통해 저기에 대략적으로 아이템의 위치가 있음을 알 수 있음
    • 하지만 수십개의 픽셀이 뭉쳐서 만들어진 6개의 영역이므로, 각각 6개의 영역 안에서 어떤 좌표가 아이템이 있는 좌표인지 판단해야함

image

템플릿 매칭을 이용한 숫자 인식

인식이란?
  • Classifying a detected object into different categories
  • 여러 개의 클래스 중에서 가장 유사한 클래스를 선택

image

숫자 템플릿 영상 생성
  • Consolas 폰트(가로의 크기가 동일한 크기가 되도록 글씨가 써지는 폰트)로 쓰여진 숫자 영상을 digit0.bmp ~ digit9.bmp로 저장

  • 각 숫자 영상의 크기는 100 x 150 크기로 정규화

image

digit_consolas 를

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

using namespace std;
using namespace cv;

int main()
{
	Mat src = imread("digit_consolas.bmp", IMREAD_GRAYSCALE);

	if (src.empty()) {
		cerr << "Image load failed!" << endl;
		return -1;
	}

	Mat src_bin;
	threshold(src, src_bin, 128, 255, THRESH_BINARY_INV); // THRESH_BINARY_INV를 진행

	Mat labels, stats, centroids;
	int label_cnt = connectedComponentsWithStats(src_bin, labels, stats, centroids);
	// 각각의 바운딩 박스를 stats 행렬로 받아옴
	
	for (int i = 1; i < label_cnt; i++) {
		int sx = stats.at<int>(i, 0);
		int sy = stats.at<int>(i, 1);
		int w = stats.at<int>(i, 2);
		int h = stats.at<int>(i, 3);
		// 바운딩 박스의 정보를 받아옴

		Mat digit;
		resize(src(Rect(sx, sy, w, h)), digit, Size(100, 150)); 
		// 부분영상을 추출하고 가로 100, 세로 150으로 resize 한 후 digit에 저장함
		String filename = cv::format("temp%d.bmp", i - 1); // filename을 temp0.bmp부터 temp9.bmp까지로 작성 
		imwrite(filename, digit); // 위에 지정한 filename으로 저장
		cout << filename << " file is generated!" << endl;
		//imshow("digit", );
		//waitKey();
	}
}
  • 코드를 실행하면 다음과 같이 0부터 9까지 숫자 bmp 파일이 생성됨

image

  • 5번 사진에 숫자 8이, 6번 사진에 숫자 9가 저장됨
    • 8, 9 숫자가 다른 숫자들보다 픽셀이 한 조금 더 높기 때문에 발생한 현상
    • 몇개 안되니 직접 수정해서 사용

image

인쇄체 숫자 인식 과정
  • 레이블링을 수행하면 각각의 객체에 바운딩 박스를 얻어냄
  • 각각의 숫자들을 부분영상으로 추출
  • 가로 100 세로 150으로 resize 해서 앞의 템플릿과 크기를 일치하게 함
  • matchTemplate()를 시행할 때 입력영상과 template 영상의 사이즈가 같으면, 1 x 1 하나의 element만 있는 결과 행렬이 나타남
    • 값이 가장 큰 레퍼런스 이미지를 찾아내면 그 숫자의 값을 예측할 수 있음

image

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

using namespace cv;
using namespace std;

Mat img_digits[10];

bool load_digits();
int  find_digit(const Mat& img);
void set_label(Mat& img, int idx, vector<Point>& contour);

int main()
{
	Mat src = imread("digits.png");

	if (src.empty()) {
		cerr << "Image load failed!" << endl;
		return -1;
	}

	if (!load_digits()) {
		cerr << "Digit image load failed!" << endl;
		return -1;
	}

	Mat src_gray, src_bin;
	cvtColor(src, src_gray, COLOR_BGR2GRAY);
//	GaussianBlur(src_gray, src_gray, Size(11, 11), 2.);
	threshold(src_gray, src_bin, 0, 255, THRESH_BINARY_INV | THRESH_OTSU);
	// Inverse를 해서 이진화를 진행함

	vector<vector<Point>> contours;
	findContours(src_bin, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE);
	// findContours를 이용해서 각각의 객체에 외곽선 좌표를 얻어낼 수 있음

	Mat dst = src.clone();

	for (unsigned i = 0; i < contours.size(); i++) { // 각각의 외곽선 객체들에 대해서
		if (contourArea(contours[i]) < 1000) // 면적이 1000보다 작으면.. (이미지의 dot를 걸러내기 위해 입력)
			continue;

		Rect rect = boundingRect(contours[i]); // 바운딩 박스 정보를 추출할 수 잇음
		int digit = find_digit(src_gray(rect)); // 부분영상을 추출
		// 추출한 부분영상을 find_digit 함수에 넣어 해당하는 숫자를 찾아 digit에 저장함

		drawContours(dst, contours, i, Scalar(0, 255, 255), 1, LINE_AA);
		set_label(dst, digit, contours[i]);
	}

//	imshow("src", src);
	imshow("dst", dst);
	waitKey();
}

bool load_digits()
{
	for (int i = 0; i < 10; i++) {
		String filename = format("./digits/digit%d.bmp", i); 
		img_digits[i] = imread(filename, IMREAD_GRAYSCALE); // 0~9 bmp 파일을 img_digits 배열에 저장 
		if (img_digits[i].empty())
			return false;
	}

	return true;
}

int find_digit(const Mat& img) // 0 부터 9까지 어느 숫자랑 가장 비슷한지 찾는 함수
{
	int max_idx = -1;
	float max_ccoeff = -9999;

	for (int i = 0; i < 10; i++) {
		Mat src, res;
		resize(img, src, Size(100, 150)); // 부분 영상을 가로 100 세로 150으로 resize 진행
		matchTemplate(src, img_digits[i], res, TM_CCOEFF_NORMED);
		// 부분 영상을 img_digits 배열에 들어있는 10개의 template 영상과 matchTemplate를 10번 진행하고 결과를 res 행렬에 넣음

		float ccoeff = res.at<float>(0, 0); // res 행렬에 들어있는 하나의 element를 불러옴

		if (ccoeff > max_ccoeff) { 
			max_idx = i; // 이 값이 최대인 위치에 대해서 max_idx에 저장함
			max_ccoeff = ccoeff;
		}
	}

	return max_idx; // 최대인 인덱스 값을 반환함
}

void set_label(Mat& img, int digit, vector<Point>& contour)
{
	int fontface = FONT_HERSHEY_SIMPLEX;
	double scale = 0.8;
	int thickness = 1;
	int baseline = 0;

	String label = format("%d", digit);

	Size text = getTextSize(label, fontface, scale, thickness, &baseline);
	Rect r = boundingRect(contour);

	Point pt(r.x + ((r.width - text.width) / 2), r.y + ((r.height + text.height) / 2));
	rectangle(img, pt + Point(0, 2), pt + Point(text.width, -text.height), Scalar(255, 255, 255), -1);
	putText(img, label, pt, fontface, scale, Scalar(0, 0, 0), thickness, LINE_AA);
}
  • Consolas 폰트를 쓴 위 숫자는 잘 인식했지만 다른 폰트를 사용한 숫자는 부분적으로 잘못 인식함

image