호모그래피

  • 두 평면 사이의 투시 변환(Perspective transform)
  • 8DOF : 최소 4개의 대응점 좌표가 필요함

image

Homography와 Perspective Transform의 차이?

  • 호모그래피와 Perspective Transform이 결과적으로 구하려고 하는 행렬은 3 x 3 행렬의 투시 변환 행렬이라 둘이 비슷한 일을 하는데 왜 따로 있는지 모호하다고 생각할 수 있음.
  • findHomography() 는 입력 점 좌표가 4개 이상일 경우 사용하면 됨
  • getPerspectiveTransform()는 입력 점과 출력 점에 대응하는 쌍이 4개일 때 사용하는 함수
  • 특징점 매칭을 했을 때 특징점 매칭이 제대로 되지 않은 것들이 존재할 때(잘못 매칭된 것들) findHomography()를 사용하면, 잘못 매칭된 것들의 영향을 제거하고 잘된 매칭들을 위주로 선별해서 최적의 3 x 3 투시 변환 행렬을 반환함

호모그래피 행렬 구하기

Mat findHomography(InputArray srcPoints, InputArray dstPoints, int method = 0, double ransacReproThreshold = 3, OutputArray mask = noArray(), const int maxIters = 2000, const double confidence = 0.995);
  • findHomography ()에서 srcPoints 가 80개가 있다고 하면.. 그 중에서 4개를 뽑아 dstPoints 영상에서 어떻게 이동했는지 perpective transform을 계산하고 나머지 76개의 점들이 dstPoints 영상에서 어디로 이동했는지 확인을 하고 실제로 몇 개의 점들이 구한 perspective transform의 근방(ransacReprojThreshold)안에 있는지를 확인해야 함

  • srcPoints : 입력 점 좌표. CV_32FC2 행렬 or vector
  • dstPoints : 결과 점 좌표. CV_32FC2 행렬 or vector

  • method : 호모그래피 행렬 계산 방법. 기본값은 0(최소자승법, least square method 사용)이며 이상치가 있을 경우 LMEDS, RANSAC, RHO 중 하나를 지정
  • ransacReprojThreshold : 대응점들을 Inlier로 인식하기 위한 최대 허용 에러(픽셀 단위)(RANSAC, RHO)
  • mask : 출력 Nx1 마스크 행렬
    • RANSAC, RHO 방법 사용시 Inlier로 사용된 점들을 1로 표시한 행렬
  • maxIters : RANSAC 알고리즘을 최대 몇 번 반복할지 횟수
  • confidence : 신뢰도 레벨. 0에서 1 사이의 실수를 지정
  • 반환값 : 3 x 3 호모그래피 행렬. CV_64FC1
    • 만약 호모그래피를 계산할 수 없는 상황이면 비어 있는 Mat 객체가 반환됨

RANSAC 알고리즘

  • RANdom SAmple Consensus
  • 이상치(Outlier)가 많은 원본 데이터로부터 모델 파라미터를 예측하는 방법
  • 이상치의 영향을 받지 않고 대다수의 점들에서 그럴싸한 직선을 찾아 반환함

image

  • RANSAC 과정

image

image

image

  • 마진을 설정하고, 설정한 마진 안쪽으로 들어오는 점들의 개수를 구함

image

image

  • 작업을 반복하여 가장 많은 N의 개수가 검출되는 점 2개의 직선의 방정식을 찾음

image

호모그래피 행렬 예제코드

void find_homography()
{
	Mat src1 = imread(file1, IMREAD_GRAYSCALE);
	Mat src2 = imread(file2, IMREAD_GRAYSCALE); // 2개의 영상을 불러옴

	if (src1.empty() || src2.empty()) {
		cerr << "Image load failed!" << endl;
		return;
	}

	TickMeter tm;
	tm.start();

	vector<KeyPoint> keypoints1, keypoints2;
	Mat desc1, desc2;
	feature->detectAndCompute(src1, Mat(), keypoints1, desc1);
	feature->detectAndCompute(src2, Mat(), keypoints2, desc2); // 특징점과 기술자를 계산함

	Ptr<DescriptorMatcher> matcher = BFMatcher::create(); // BFMatcher(Brute-force)을 사용함

#if 1 // 좋은 매칭 선별방법#1 (상위 80개)
	vector<DMatch> matches;
	matcher->match(desc1, desc2, matches);

	std::sort(matches.begin(), matches.end());
	vector<DMatch> good_matches(matches.begin(), matches.begin() + 80); 
#else // 좋은 매칭 선별방법#2 (가장 좋은 매칭 결과의 distance 값과 두 번째로 좋은 매칭 결과의 distance 값의 비율을 계산)
 	vector<vector<DMatch>> matches;
	matcher->knnMatch(desc1, desc2, matches, 2);

	vector<DMatch> good_matches;
	for (const auto& m : matches) {
		if (m[0].distance / m[1].distance < 0.7)
			good_matches.push_back(m[0]);
	}
	
#endif

	vector<Point2f> pts1, pts2;
	for (size_t i = 0; i < good_matches.size(); i++) { 
	// if 1을 기준으로 for문이 80번 작동하여 pts1, pts2에 각각 80개의 점들이 들어감
		pts1.push_back(keypoints1[good_matches[i].queryIdx].pt);
		// keypoints1에 들어있는 전체 특징점의 좌표들중에서
        // 그 중에서 잘된 매칭들의 i번째의 1번 영상의 특징점 번호(queryIdx) 점의 좌표를 pts1에 저장함
		pts2.push_back(keypoints2[good_matches[i].trainIdx].pt);
		// keypoints2에 들어있는 전체 특징점의 좌표들중에서
        // 그 중에서 잘된 매칭들의 i번째의 2번 영상의 특징점 번호(trainIdx) 점의 좌표를 pts2에 저장함
	}

	Mat H = findHomography(pts1, pts2, RANSAC);
	// RANSAC 알고리즘을 이용하여 Outlier의 영향을 최소화시켜 가장 좋은 매칭 결과(가장 좋은 투시변환 행렬)을 반환함

	tm.stop();
	cout << "time: " << tm.getTimeMilli() << endl;

	Mat dst;
	drawMatches(src1, keypoints1, src2, keypoints2, good_matches, dst,
		Scalar::all(-1), Scalar::all(-1), vector<char>(),
		DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);

	vector<Point2f> corners1, corners2;
	corners1.push_back(Point2f(0, 0)); // corner1에서 첫번째 점은 (0,0)
	corners1.push_back(Point2f(src1.cols - 1.f, 0)); // corner1에서 첫번째 점은 (가로크기,0)
	corners1.push_back(Point2f(src1.cols - 1.f, src1.rows - 1.f)); // corner1에서 첫번째 점은 (가로크기,세로크기)
	corners1.push_back(Point2f(0, src1.rows - 1.f)); // corner1에서 첫번째 점은 (0,세로크기)
	perspectiveTransform(corners1, corners2, H); 
	// 4개의 점이 위에서 구한 투시변환행렬에 의해서 어떻게 변하는지 corners2에 저장
	

	vector<Point> corners_dst; // 점들의 좌표를 vector<Point>로 지정해야하므로 corners_dst를 생성
	for (Point2f pt : corners2) {
		corners_dst.push_back(Point(cvRound(pt.x + src1.cols), cvRound(pt.y))); 
		// 변환과 동시에 왼쪽 영상의 크기만큼을 더해서 corner_dst에 저장함
	}
	// 두개의 영상이 가로방향으로 붙어진 상태로 나오므로 오른쪽 영상에 사각형을 그리기 위해서 왼쪽 영상의 크기만큼을 위치값에 더해야 함

	polylines(dst, corners_dst, true, Scalar(0, 255, 0), 2, LINE_AA);
	// 4개의 점으로 구성된 임의의 사각형을 녹색으로 표현
	// corners_dst 정수형으로 되어있는 점들의 좌표 벡터를 만들어 Polylines에 전달
	// true 는 폐곡선으로 그림

	imshow("dst", dst);
	waitKey();
	destroyAllWindows();
}
  • 결과 영상에서 찾고자하는 물건이 녹색 테두리로 표현되는 것을 확인할 수 있음

image