OpenCV 4로 배우는 컴퓨터 비전과 머신 러닝/ 지역 특징점 검출과 매칭

코너 검출

해리스 코너 검출 방법

  • 영상에서 특징(feature)란 영상으로부터 추출할 수 있는 유용한 정보를 의미하며, 평균 밝기, 히스토그램, 에지, 직선 성분, 코너 등이 특징이 될 수 있다.
    • 영상의 특징 중에서 에지, 직선, 성분, 코너처럼 영상 전체가 아닌 일부 영역에서 추출할 수 있는 특징을 지역 특징(local feature)이라고 한다.
    • 영상의 지역 특징 중 코너(corner)는 에지의 방향이 급격하게 변하는 부분으로서 삼각형의 꼭지점이나 연필 심처럼 뾰족하게 튀어나와 있는 부분이 코너가 될 수 있다.
    • 코너는 에지나 직선 성분에 비해 분별력이 높고 대체로 영상 전 영역에 골고루 분포하기 때문에 영상을 분석하는데 유용한 지역 특지응로 사용된다.
    • 코너처럼 한 점의 형태로 표현할 수 있는 특징을 특징점(feature point)라고 하며, 특징점은 키포인트(keypoint) 또는 관심점(interest point)라고 부르기도 한다.
  • 아래 그림에서 A 부분은 내부 픽셀값 변화가 크지 않은 평탄한 영역이며, 원본 영상에서 하늘 여역 전체는 A와 비슷한 픽셀값 분포를 갖는다.
    • B 부분은 하늘과 바다가 만나는 수평선 부근으로 정확한 x 좌표는 가늠하기 어렵다. 
    • C 부분은 건물이 뾰족하게 튀어나와 있는 부분으로 원본 영상 오른쪽 산등성이에서 유일한 위치를 찾을 수 있다.
    • 이렇듯 코너는 에지나 평탄한 영역에 비해 변별력이 높아서 그 위치를 파악하기 쉽다.

  • 영상에서 코너를 찾는 연구는 1970년대 후반부터 활발하게 진행되었는데, 1988년 해리스(C. Harris)가 개발한 코너 검출 방법은 코너 점 구분을 위한 기본적인 아이디어를 수학적으로 잘 정의하였다는 점에서 큰 의미가 있다.
    • 해리스는 영상의 특정 위치 (x, y) 에서 \Delta x \Delta y 만큼 떨어진 픽셀과의 밝기 차이를 다음 수식으로 표현하였다.

E(\Delta x, \Delta y) = \sum_{x, y} w(x, y) [I(x + \Delta x, y + \Delta y) - I(x, y)]^{2}

  • 위 수식에서 w(x, y) 는 균일한 값을 갖는 사각형 윈도우 또는 가우시안 형태의 가중치를 갖는 윈도우이다.
    • 만약 E(\Delta x, \Delta y) 함수가 모든 방향으로 값이 크게 나타난다면 점 (x, y) 는 코너라고 간주할 수 있다.
    • 해리스는 E(\Delta x, \Delta y) 가 모든 방향으로 그 값이 크게 나타나는지를 검사하기 위해 테일러 급수(Taylor series), 고윳값 분석(eigenvalue analysis) 등의 수학적 기법을 적용하여 코너 응답 함수 R을 유도하였다.

R = Det(M) - k \cdot Tr(M)^{2}

  • 위 수식에서 Det()는 행렬식(determinant)을, Tr()은 대각합(trace)을 의미하고, 행렬 M은 다음과 같이 정의된다.

M = \sum_{x, y} w(x, y) \left[ \begin{array}{rr} I_{x} I_{x} & I_{x} I_{y} \\ I_{x} I_{y} & I_{y} I_{y} \end{array} \right]

  • 위 수식에서 I_{x} I_{y} s는 입력 영상 I 를 각각 x축 방향과 y축 방향으로 편미분한 결과이다.
    • 코너 응답 함수 정의에서 상수 k는 보통 0.04~0.06 사이의 값을 사용한다.
  • 해리스에 의해 정의된 코너 응답 함수 R은 입력 영상 각각의 픽셀에서 정의되는 실수 값이며, 이 값을 분석하여 코너, 에지, 평탄한 영역을 판별할 수 있다. 
    • 만약 R이 0보다 충분히 큰 양수이면 코너 점이라고 간주한다.
    • 반면 R이 0에 가까운 실수이면, 평탄한 영역이고, 0보다 작은 음수이면 에지라고 판별한다.
  • OpenCV는 해리스 코너 응답 함수 값을 계산하는 cornerHarris() 함수를 제공한다.
    • cornerHarris() 함수는 입력 영상 src의 모든 픽셀 위치에서 해리스 코너 응답 함수 값을 계산하고 그 결과를 dst 행렬로 반환한다.
    • dst 행렬의 모든 원소는 float 자료형을 사용하며, 이 값이 사용자가 지정한 임계값보다 크면 코너 점으로 판단할 수 있다.
    • 이때 하나의 코너 위치에 사용자가 지정 임계값보다 큰 픽셀이 여러 개 발생할 수 있으므로 간단한 비최대 억제를 수행하여 지역 최댓값 위치만 코너로 판별하는 것이 좋다.
void corner_harris()
{
Mat src = imread("building.jpg", IMREAD_GRAYSCALE);

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

Mat harris;
cornerHarris(src, harris, 3, 3, 0.04);

Mat harris_norm;
normalize(harris, harris_norm, 0, 255, NORM_MINMAX, CV_8U);

Mat dst;
cvtColor(src, dst, COLOR_GRAY2BGR);

for (int j = 1; j < harris.rows - 1; j++)
{
for (int i = 1; i < harris.cols - 1; i++)
{
if (harris_norm.at<uchar>(j, i) > 120)
{
if (harris.at<float>(j, i) > harris.at<float>(j-1, i) &&
harris.at<float>(j, i) > harris.at<float>(j+1, i) &&
harris.at<float>(j, i) > harris.at<float>(j, i-1) &&
harris.at<float>(j, i) > harris.at<float>(j, i+1))
{
circle(dst, Point(i, j), 5, Scalar(0, 0, 255), 2);
}
}
}
}

imshow("src", src);
imshow("harris_norm", harris_norm);
imshow("dst", dst);

waitKey(0);
destroyAllWindows();
}

Fast 코너 검출 방법

  • 해리스 코너 검출 방법은 영상의 코너 특성을 수학적으로 잘 정의하고, 복잡한 수식을 잘 전개하여 수치적으로 코너를 검출하였다는데 의미가 있다. 이후로도 비슷한 컨셉을 발전시켜 추적에 적합한 특징(Good Features to Track)이라는 이름의 코너 검출 방법도 제안되었고, OpenCV에도 그 기능이 구현되어 있다.
    • 그러니 이러한 코너 검출 방법들은 복잡한 연산을 필요로 하기 때문에 연산 속도가 느리다는 단점이 있다.
    • 이러한 코너 검출 방법과 달리 2006년에 발표된 FAST 코너 검출 방법은 단순한 픽셀값 비교 방법을 통해 코너를 검출한다. FAST는 Features from Accelerated Segment Test의 약자이다.
  • FAST 방법은 영상의 모든 픽셀에서 픽셀을 둘러싸고 있는 16개의 주변 픽셀과 밝기를 비교하여 코너 여부를 판별한다.
    • 아개 그림에서 점 p가 코너인지 판별하기 위해 점 p 주변 1번부터 16번 픽셀과의 밝기를 비교한다.
    • 만약 주변 16개의 픽셀 중에서 점 p 보다 충분히 밝거나 충분히 어두운 픽셀이 9개 이상 연속으로 존재하면 코너로 정의한다.

  • 점 P에서의 밝기를 I_{p} 라고 할 때, 주변 16개의 픽셀 중에서 그 값이 I_{p} + t 보다 큰 픽셀이 9개 이상 연속으로 나타나면 점 p는 어두운 영역이 뾰족하게 돌출되어 있는 코너이다.
    • 반면 주변 16개의 픽셀 중에서 그 값이 I_{p} - t 보다 작은 픽셀이 9개 이상 연속으로 나타나면 점 p는 밝은 영역이 돌출되어 있는 코너라고 간주한다.
    • 여기서 t는 충분히 밝거나 어두운 정도를 조절하기 위한 임계값이다.
  • FAST 방법은 특정 코너 점 주변 픽셀들도 함께 코너로 검출하는 경우가 많기 때문에 주변 코너 픽셀 중에서 가장 코너에 적합한 픽셀을 선택하는 비최대 억제 작업을 추가적으로 수행하는 것이 좋다.
    • FAST 방법에서는 코너 점 주변 16개 점과 픽셀 값 차이 합을 코너 점수로 정의하고, 인접한 코너 중에서 코너 점수가 가장 큰 코너만 최종 코너로 선택한다.
  • OpenCV는 FAST 코너 검출 방법을 구현한 FAST() 함수를 제공한다.
    • FAST() 함수의 입력 영상으로는 CV_8UC1 타입의 그레이스케일 영상만 사용할 수 있다.
    • FAST() 함수의 두 번째 인자 keypoints는 KeyPoint 클래스 객체의 벡터로 저장한다.
void corner_fast()
{
Mat src = imread("building.jpg", IMREAD_GRAYSCALE);

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

vector<KeyPoint> keyPoints;
FAST(src, keyPoints, 60, true);

Mat dst;
cvtColor(src, dst, COLOR_GRAY2BGR);

for (KeyPoint kp : keyPoints)
{
Point pt(cvRount(kp.pt.x), cvRound(kp.pt.y));
circle(dst, pt, 5, Scalar(0, 0, 255), 2);
}

imshow("src", src);
imshow("dst", dst);

waitKey(0);
destroyAllWindows();
}
  • Note)
    • cornerHarris() 함수와 FAST(0 함수의 동작 시간을 비교해보면 FAST 코너 검출 방법이 대략 20배 이상 빠르게 동작한다.

크기 불변 특징점 검출과 기술

크기 불변 특징점 알고리즘

  • 코너는 영상이 회전되어도 여전히 코너로 검출된다. 그러므로 코너는 회전 불변 특징점이라 할 수 있다. 그러나 영상의 크기가 변경될 경우 코너는 더 이상 코너로 검출되지 않을 수 있다.
    • 아래 그림은 객체의 크기 변화에 따른 코너의 형태 변화를 보여준다. 왼쪽 그림에서 파란색 사각형 내부는 에지가 급격하게 휘어지는 코너처럼 보이지만 영상이 확대되어 오른쪽 그림처럼 변경되면 같은 크기의 사각형 안에서 코너보다는 에지에 가까운 형태로 관측되는 것을 볼 수 있다.

  • 따라서 크기가 다른 두 객체 영상에서 단순한 코너 점을 이용하여 서로 같은 위치를 찾는 것에는 한계가 있다. 그래서 많은 사람들이 크기가 다른 영상에서도 지속적으로 검출될 수 있는 크기 불변 특징에 대해 연구하였고, 그중 가장 대표적인 알고리즘이 SIFT이다.
    • SIFT는 크기 불변 특징 변환(Scale Invariant Feature Transform)의 약자이며, 2004년 캐나다의 브리티시 컬럼비아 대학교의 로우(D. Lowe) 교수가 발표한 논문에 소개된 방법이다.
  • SIFT 알고리즘은 영상의 크기 변화에 무관하게 특징점을 추출하기 위해 입력 영상으로부터 스케일 스페이스(scale space)를 구성한다.
    • 스케일 스페이스는 영상에 다양한 표준 편차를 이용한 가우시안 블러링을 적용하여 구성한 영상 집합을 의미한다.
    • 레나 영상에 대해 스케일 스페이스를 구성한 예가 아래 그림과 같다.
    • 그림의 맨 윗줄에 나타난 여섯 개의 블러링된 영상이 스케일 스페이스를 구성한 결과이며, 이렇게 구성한 영상 집합을 옥타브(octave)라고 부른다.
    • 이후 입력 영상의 크기를 가로, 세로 반으로 줄여 가면서 여러 옥타브를 구성한다.

  • SIFT 알고리즘에서 크기에 불변한 특징점을 검출할 때는 인접한 가우시안 블러링 영상끼리의 차영상을 사용하며, 이를 DoG(Difference of Gaussian) 영상이라고 한다.
    • 위 그림 아래쪽에 나열한 영상이 레나 영사으로부터 구한 DoG 영상이다.
    • SIFT 알고리즘은 DoG 영상 집합에서 인접한 DoG 영상을 고려한 지역 극값 위치를 특징점으로 사용하며, 이후 에지 성분이 강하거나 명암비가 낮은 지점은 특징점에서 제외한다.
  • SIFT 알고리즘은 특징점을 검출하는 기능뿐만 아니라 특징점 주변의 픽셀 값을 이용한 기술자(descriptor) 계산 방법도 포함한다.
    • 특징점 기술자는 특징점 주변 영상의 특성을 여러 개의 실수 값으로 표현한 것을 의미하며, 특징 벡터(feature vector)라고도 한다.
    • 서로 같은 특징점에서 추출된 기술자는 실수 값 구성이 서로 일치해야 한다.
    • SIFT는 기본적으로 특징점 부근의 부분 영상으로부터 그래디언트 방향 히스토그램을 추출하여 기술자로 사용한다. 특징점 근방으로부터 특징점의 주된 방향 성분을 계산하고, 이 방향만큼 회전한 부분 영상으로부터 128개의 빈으로 구성된 그래디언트 방향 히스토그램을 계산한다.
    • 각각의 빈 값은 float 자료형을 사용하며, 하나의 SIFT 특징점은 512바이트 크기의 기술자료 표현된다.
  • SIFT 알고리즘은 영상의 크기, 회전 등의 변환 뿐만 아니라 촬영 시점 변환에도 충분히 강인하게 동작하며, 잡음의 영향과 조명 변화가 있어도 특징점을 반복적으로 잘 찾아낸다.
    • SIFT 알고리즘은 다양한 컴퓨터 비전 분야에서 적용되었고, 특히 객체 인식, 파노라마 영상 이어 붙이기 3차원 장면 인식 등의 분야에서 효과적으로 사용되었다.
  • SIFT 알고리즘이 발표된 이후, 많은 사람들이 SIFT 알고리즘의 속도와 성능을 개선한 알고리즘을 발표했다.
    • 2008년에 발표된 SURF(Speed-Up Robust Feature) 알고리즘은 SIFT에서 사용한 DoG 영상을 단순한 이진 패턴으로 근사화하여 속도를 향상시켰다.
    • 2012년에 발표된 KAZE 알고리즘은 가우시안 함수 대신 비등방성 확산 필터(nonlinear diffusion filter)를 이용하여 비선형 스케일 스페이스를 구축하여 특징점을 검출한다.
    • KAZE 알고리즘은 객체의 윤곽을 잘 보전함으로써 블러링, 크기 및 회전 변환, 잡음 등의 영향으로 변형된 영상에서 같은 특징점을 반복적으로 찾아내는 성능이 뛰어나다.
  • 그러나 SIFT, SURF, KAZE 방법은 스케일 스페이스를 구성하는 등의 복잡한 연산을 수행해야 하기 때문에 실시간 응용에서 사용하기 어렵다는 단점이 있다.
    • 또한 이들 특징점 알고리즘에 의해 만들어지는 기술자는 128개 또는 64개의 실수 값으로 구성되어 있어서 메모리 사용량이 많고, 특징점 사이의 거리 게산도 오래 걸릴 수 있다는 단점이 있다.
    • 그래서 2010년 전후 특징점 검출이 매우 빠르고 이진수로 구성된 기술자를 사용하는 알고리즘이 발표되기 시작했고, 그중 2011년에 발표된 ORB(Oriented FAST and Rotated BRIEF) 알고리즘은 당시 OpenCV를 관리하던 연구소에서 개발한 방법으로서 SIFT와 SURF를 대체하기에 좋은 알고리즘이다.
  • ORB 알고리즘은 기본적으로 FAST 코너 검출 방법을 이용하여 특징점을 추출한다. 다만 기본적인 FAST 알고리즘은 영상의 크기 변화에 취약하기 때문에 ORB 알고리즘은 입력 영상의 크기를 점진적으로 축소한 피라미드 영상을 추구하여 특징점을 추출한다.
    • 그리고 각 특징점에 주된 방향 성분을 계산하고, 방향을 고려한 BRIEF 알고리즘으로 이진 기술자를 계산한다.
  • ORB에서 사용한 BRIEF(Binary Robust Independent Elementary Feature)는 순수하게 특징점 기술자만을 생성하는 알고리즘으로 특징점 주변의 픽셀 쌍을 미리 정하고 해당 픽셀 값 크기를 비교하여 0 또는 1로 특징을 기술한다.
    • 두 점 x, y 에서의 픽셀 값 크기 비교 테스트 \tau  는 다음과 같이 정의한다.

\tau(x, y) = \begin{cases} 1 & I(x) < I(y) \\ 0 & else \end{cases}

  • 예컨대 아래 그림과 같이 특징점 p 주변에 a, b, c 점을 미리 정의하고, \tau(a, b), \tau(b, c), \tau(c, a)  를 구하면 이진수 110_{2} 를 얻을 수 있다.
    • 이진수 110_{2} 는 b 점이 a보다 밝고, c 점이 b 보다 밝고, a가 c 보다 어둡다는 정보를 표현한다.
    • 이처럼 특징점 주변 정보를 이진수 형태로 표현하는 기술자를 이진 기술자(binary descriptor)라고 한다.

  • ORB 알고리즘은 FAST 기반의 방법으로 특징점을 구한 후, 각 특징점에서 픽셀 밝기 값 분포를 고려한 코너 방향 성분을 계산한다.
    • 그리고 이 방향 성분을 이용하여 BRIEF 계산에 필요한 점들의 위치를 보정함으로써 회전에 불변한 BRIEF 기술자를 계산한다.
    • ORB 알고리즘에서는 기본적으로 256개의 크기 비교 픽셀 쌍을 사용하여 이진 기술자를 구성하며, 결과적으로 하나의 특징점은 256비트로 표현할 수 있다.
    • SIFT와 SURF 기술자가 512, 256 바이트를 사용하는 것에 비해 ORB는 32바이트의 크기로 특징점을 기술할 수 있어서 효율적이다.
  • 이진 기술자로 표현된 특징점 사이의 거리 계산은 주로 해밍 거리(Hamming distance) 방법을 사용한다.
    • 해밍 거리는 이진수로 표현된 두 기술자에서 서로 값이 다른 비트의 개수를 세는 방식으로 계산한다.
    • 해밍 거리 계산은 두 기술자의 비트 단위 배타적 논리합(XOR) 연산 후, 비트 값이 1인 개수를 세는 방식으로 빠르게 계산할 수 있다.
    • ORB 외에도 BRISK, AKAZE, FREAK 등의 이진 기술자를 사용하는 특징점 알고리즘이 있다.

OpenCV 특징점 검출과 기술

  • OpenCV에서 특징점 정보를 저장할 때 사용하는 클래스는 KeyPoint이며, 이 클래스는 특징점 좌표뿐만 아니라 특징점 검추 시 고려한 주변 영역의 크기, 주된 방향, 옥타브 정보 등을 변수로 갖고 있다.
  • OpenCV에서 특징점 관련 클래스는 모두 Feature2D 클래스를 상속 받아 만들어진다.
    • Feature2D 클래스는 detect(), compute(), detectAndCompute()라는 이름의 가상 멤버 함수를 갖고 있고, 이 클래스를 상속 받은 각각의 특징점 알고리즘 구현 클래스는 이들 멤버 함수의 기능을 실제로 구현하도록 설계되어 있다.
    • detect() 함수는 영상에서 키포인트를 검출하고, compute() 함수는 검출된 키포인트를 표현하는 기술자를 생성한다. detectAndCompute() 함수는 그 둘을 한번에 수행한다.
  • (이하 클래스와 함수 설명 생략)
void detect_keypoints()
{
Mat src = imread("box_in_scene.png", IMREAD_GRAYSCALE);

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

Ptr<Feature2D> feature = ORB::create();

vector<KeyPoint> keyPoints;
feature->detect(src, keypoints);

Mat desc;
feature->compute(src, keypoints, desc);

cout << "keypoints.size(): " << keyponts.size() << endl;
cout << "desc.size(): " << desc.size() << endl;

Mat dst;
drawKeypoints(src, keypoints, dst, Scalar::all(-1), DrawMatchesFlags::DRAW_RICH_KEYPOINTS);

imshow("src", src);
imshow("dst", dst);

waitKey();
destroyAllWindows();
}

특징점 매칭

OpenCV 특징점 매칭

  • 특징점 매칭이란 두 영상에서 추출한 특징점 기술자를 비교하여 서로 비슷한 특징점을 찾는 작업을 의미한다. 특히 크기 불변 특징점으로부터 구한 기술자를 매칭하면 크기와 회전에 강인한 영상 매칭을 수행할 수 있다.
  • OpenCV에서 특징점 매칭 정보를 저장할 때 DMatch 라는 클래스를 사용한다.
    • DMatch 클래스는 한 장의 영상에서 추출한 특징점과 다른 한 장의 영상 또는 여러 영상에서 추출한 특징점 사이의 매칭 정보를 표현할 수 있다.
    • DMatch 클래스에서 distance 멤버 변수는 두 키포인트 기술자가 얼마나 차이가 나는지를 나타내는 매칭 척도의 역할을 한다. 두 특징점이 서로 유사하면 distance 값이 0에 가깝고, 서로 다른 특징점이면 distance가 크게 나타난다.
    • distance 계산 방식은 다차원 벡터의 유클리드 거리로 주로 계산하며, 다만 이진 기술자끼리 비교하는 경우에는 해밍 거리를 사용한다.
    • DMatch 클래스 객체는 보통 사용자가 직접 생성하지 않고, 특징점 매칭 알고리즘 내부에서 생성해서 사용자에게 반환한다.
  • OpenCV의 특징점 매칭 클래스는 DescriptorMatcher 클래스를 상속받아 만들어지는데, 이 클래스는 match(), knnMatch(), radiusMatch() 등의 가상 멤버 함수를 갖고 있는 추상 클래스이며 BFMatcher, FlannBasedMatcher 클래스는 이들 멤버 함수 기능을 구현하도록 설계되어 있다.
    • match() 함수는 가장 비슷한 기술자 쌍을 하나 찾고, knnMatch() 함수는 비슷한 기술자 쌍 k개를 찾는다. radiusMatch() 함수는 지정한 거리 반경 안에 있는 기술자 쌍을 모두 찾아 반환한다.
  • BFMatcher 클래스는 전수 조사(Brute-Force) 매칭을 수행한다. BFMatcher 클래스는 질의 기술자 집합에 있는 모든 기술자와 훈련 기술자 집합에 있는 모든 기술자 사이의 거리를 계산하고 이중 가장 거리가 작은 기술자를 찾아 매칭하는 방식이다.
    • BFMatcher 클래스의 매칭 결정 방법은 매우 직관적이지만, 특징점 개수가 늘어날수록 거리 계산 횟수가 급격하게 늘어날 수 있다는 단점이 있다.
    • 이러한 경우에는 FlannBasedMatcher 클래스를 사용하는 것이 효율적이다.
  • Flann(Fast Library approximate nearest neighbors)는 근사화된 최근방 이웃(ANN, Approximate Nearest Neighbors) 알고리즘을 빠르게 구현한 라이브러리이다.
    • FlannBasedMatcher 클래스는 Flann 라이브러리를 이용하여 빠르게 매칭을 수행한다.
    • FlannBasedMatcher 클래스는 근사화된 거리 계산 방법을 사용하므로 가장 거리가 작은 특징점을 찾지 못할 수 있지만, 매우 빠르게 동ㅈ가한다.
    • 다만 FlannBasedMatcher  클래스는 기본적으로 L2 노름 거리 측정 방식을 사용하므로 해밍 거리를 사용하는 이진 기술자에 대해서는 사용할 수 없다.
  • (클래스와 함수 설명 생략)
void keypoint_matching()
{
Mat src1 = imread("box.png", IMREAD_GRAYSCALE);
Mat src2 = imread("box_in_scene.png", IMREAD_GRAYSCALE);

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

Ptr<Feature2D> feature = ORB::create();

vector<KeyPoint> keypoints1, keypoints2;
Mat desc1, desc2;
feature->detectAndCompute(src1, Mat(), keypoints1, desc1);
feature->detectAndCompute(src2, Mat(), keypoints2, desc2);

Ptr<DescriptorMatcher> matcher = BFMatcher::create(NORM_HAMMING);

vector<DMatch> matches;
matcher->match(desc1, desc2, matches);

Mat dst;
drawMatches(src1, keypoints1, src2, keypoints2, matches, dst);

imshow("dst", dst);

waitKey();
destroyAllWindows();
}

  • DMatch 클래스는 기술자 사이의 거리를 표현하는 distance를 멤버 변수로 갖고 있다. 그러므로 distance 값이 너무 큰 매칭 결과는 무시하고 distance 값이 작은 결과만 사용하는 것이 좋다.
    • DMatch 클래스는 부등호 연산자에 대해 재정의가 되어 있고, 이 연산자 재정의에서는 distance 멤버 변수 크기를 비교하기 때문에 DMatch 객체를 std::sort() 함수로 정렬하면 자동으로 distance 값을 기준으로 정렬된다.
void good_matching()
{
Mat src1 = imread("box.png", IMREAD_GRAYSCALE);
Mat src2 = imread("box_in_scene.png", IMREAD_GRAYSCALE);

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

Ptr<Feature2D> feature = ORB::create();

vector<KeyPoint> keypoints1, keypoints2;
Mat desc1, desc2;
feature->detectAndCompute(src1, Mat(), keypoints1, desc1);
feature->detectAndCompute(src2, Mat(), keypoints2, desc2);

Ptr<DescriptorMatcher> matcher = BFMatcher::create(NORM_HAMMING);

vector<DMatch> matches;
matcher->match(desc1, desc2, matches);

std::sort(matches.begin(), matches.end());
// match 된 것 중에 상위 50개만 선별
vector<DMatch> good_matches(matches.begin(), matches.begin() + 50);

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

imshow("dst", dst);

waitKey();
destroyAllWindows();
}

호모그래피와 영상 매칭

  • 호모그래피란 3차원 공간사으이 평면을 서로 다른 시점에서 바라봤을 때 획득되는 영상 사이의 관계를 나타내는 용어이다.
    • 호모그래피는 수학적으로 하나의 평면을 다른 평면으로 투시 변환(perspective transform) 하는 것과 같은 관계에 있다.
    • 아래 그림은 3차원 공간에서 평면과 획득된 영상과의 호모그래피 관계를 보여준다.
    • 아래 그림은 바닥에 놓인 평면 P를 v_{1} 시점에서 바라본 영상 I_{1} v_{2} 시점에서 바라본 영상 I_{2} 사이의 관계를 호모그래피 H_{12} 로 표현하였다.
    • 또한 영상 I_{1} 과 평면 P 사이의 관계 또는 영상 I_{2} 와 평면 P 사이의 관계도 각각 호모그래피 H_{1} H_{2} 형태로 표현할 수 있다.

  • 실제적인 연산 관점에서 호모그래피는 투시 변환과 같기 때문에 호모그래피는 3 x 3 실수 행렬로 표현할 수 있다.
    • 또한 투시 변환을 구할 때와 마찬가지로 4개의 대응되는 점의 좌표 이동 정보가 있으면 호모그래피 행렬을 구할 수 있다.
    • 그러나 특징점 매칭 정보로부터 호모그래피를 구하는 경우에는 서로 대응되는 점 개수가 4개보다 훨씬 많기 때문에, 이러한 경우에는 투시 변환시 에러가 최소가 되는 형태의 호모그래피 행렬을 구해야 한다.
  • OpenCV에서는 두 영상 평면에서 추출된 특징점 매칭 정보로부터 호모그래피를 계산할 때 사용할 수 있는 findHomography() 함수를 제공한다.
    • findHomography() 함수는 두 평면 위에 있는 점들을 투영 변환하는 3 x 3 호모그래피 행렬을 반환한다. 원본 평면 상의 점 좌표를 (x_{i}, y_{i}) 로 표현하고 목표 평면상의 점 좌표를 (x_{i}', y_{i}') 로 표현할 경우 호모그래피 H는 다음 수식을 최소화하는 형태의 행렬이 된다.

\sum_{i} (x_{i}' - {h_{11}x_{i} + h_{12}y_{i} + h_{13} \over h_{31}x_{i} + h_{32}y_{i} + h_{33}})^{2} - (y_{i}' - {h_{21}x_{i} + h_{22}y_{i} + h_{23} \over h_{31}x_{i} + h_{32}y_{i} + h_{33}})^{2}

  • 위 수식에서 h_{ij}(1 \leq i, j \leq 3) 는 호모그래피 행렬 H의 원소를 나타내고 다음과 같은 관계를 만족시킨다.

s_{i} \left[ \begin{array}{rrr} x_{i}' \\ y_{i}' \\ 1 \end{array} \right] \sim H \left[ \begin{array}{rrr} x_{i} \\ y_{i} \\ 1 \end{array} \right] = \left[ \begin{array}{rrr} h_{11} & h_{12} & h_{13} \\ h_{21} & h_{22} & h_{23} \\ h_{31} & h_{32} & h_{33} \end{array} \right] \left[ \begin{array}{rrr} x_{i} \\ y_{i} \\ 1 \end{array} \right]

  • findHomography() 함수의 method 인자에 기본값인 0을 지정하면 입력 점과 출력 점을 모두 사용하는 최소자승법(least squares)으로 호모그래피 행렬을 계산한다.
    • 그러나 일반적으로는 특징점 매칭 결과로부터 호모그래피를 계산할 때 최소자승법을 사용하면 호모그래피가 제대로 계산되지 않는다.
    • 잘못 매칭된 점들처럼 오차가 큰 입력 정보를 이상치(outlier)라고 부르며, 이상치가 많이 존재하는 경우에는 호모그래피 계산 방법 method를 LMEDS, RANSAC, RHO 방법으로 설정하는 것이 좋다.
    • LMEDS 메서드는 보통 이상치가 50% 이하인 경우 올바르게 작동하며, RANSAC, RHO 방법은 이상치가 50% 이상이라도 호모그래피 행렬을 잘 찾아주는 편이다.
    • RANSAC과 RHO 방법을 사용할 경우에는 srcPoints와 dstPoints에 저장된 점이 이상치가 아니라고 판단하기 위한 임계값을 설정해야 하며, 이 값은 ransacReprojThreshold 인자로 지정한다.
    • 만약 h * srcPoints_{i} dstPoints_{i} 사이의 거리가 ransacReprojThreshold 보다 작으면 정상치(inlier)로 간주한다.
  • Note)
    • RANSAC(RANdom SAmple Consensus) 알고리즘은 이상치가 포함된 입력 데이터로부터 수학적 모델 파라미터를 효과적으로 결정하는 알고리즘이다.
    • RANSAC 알고리즘으로 호모그래피를 계산하는 경우, 다수의 특징점 매칭 정보로부터 네 개의 대응점을 임의로 추출한다. 이 대응점 정보를 이용하여 3 x 3 호모그래피 행렬을 계산하고, 나머지 특징점 매칭 쌍 중에서 현재 구한 호모그래피 행렬에 부합되는 매칭 쌍 개수를 센다.
    • 그리고 다시 임의로 네 개의 대응점을 추출하고, 호모그래피 행렬 계산과 해당 호모그래피에 부합되는 매칭 쌍 개수 세는 작업을 반복한다.
    • 이 작업을 여러 번 반복한 후, 가장 많은 매칭 쌍의 지지를 받은 호모그래피 행렬을 최종 호모그래피 행렬로 결정하는 방식이 RANSAC이다.
void find_homography()
{
Mat src1 = imread("box.png", IMREAD_GRAYSCALE);
Mat src2 = imread("box_in_scene.png", IMREAD_GRAYSCALE);

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

Ptr<Feature2D> feature = ORB::create();

vector<KeyPoint> keypoints1, keypoints2;
Mat desc1, desc2;
feature->detectAndCompute(src1, Mat(), keypoints1, desc1);
feature->detectAndCompute(src2, Mat(), keypoints2, desc2);

Ptr<DescriptorMatcher> matcher = BFMatcher::create(NORM_HAMMING);

vector<DMatch> matches;
matcher->match(desc1, desc2, matches);

std::sort(matches.begin(), matches.end());
vector<DMatch> good_matches(matches.begin(), matches.begin() + 50);

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

vector<Point2f> pts1, pts2;
for (size_t i = 0; i < god_matches.size(); i++)
{
pts1.push_back(keypoints1[good_matches[i].queryIdx].pt);
pts2.push_back(keypoints2[good_matches[i].trainIdx].pt);
}

Mat H = findHomography(pts1, pts2, RANSAC);

vector<Point2f> corners1, corners2;
corners1.push_back(Point2f(0, 0));
corners1.push_back(Point2f(src1.cols-1.f, 0));
corners1.push_back(Point2f(src1.cols-1.f, src1.rows-1.f));
corners1.push_back(Point2f(0, src1.rows-1.f));
perspectiveTransform(corners1, corners2, H);

vector<Point> corners_dst;
for (Point2f pt : corners2)
{
corners_dst.push_back(Point(cvRound(pt.x + src1.cols), cvRound(pt.y)));
}

polylines(dst, corners_dst, true, Scalar(0, 255, 0), 2, LINE_AA);

imshow("dst", dst);

waitKey();
destroyAllWindows();
}

영상 이어붙이기

  • 영상 이어 붙이기(image stitching)는 여러 장의 영상을 서로 이어 붙여서 하나의 큰 영상을 만드는 기법이다.
    • 영상 이어 붙이기로 만들어진 영상을 파노라마 영상(panorama image)라고 부르며, 많은 디지털 카메라 또는 스마트폰 카메라 앱에서도 파노라마 영상을 만드는 기능을 제공하고 있다.
    • 영앗 이어 붙이기에서 입력으로 사용할 영상은 서로 일정 비율 이상으로 겹치는 영역이 존재해야 하며, 서로 같은 위치를 분간할 수 있도록 유효한 특징점이 많이 있어야 한다.
  • 영상 이어 붙이기를 수행하려면 입력 영상에서 특징점을 검출하고, 서로 매칭을 수행하여 호모그래피를 구해야 한다.
    • 그리고 구해진 호모그래피 행렬을 기반으로 입력 영상을 변형하여 서로 이어 붙이는 작업을 수행한다.
    • 이때 영상을 이어 붙인 결과가 자연스럽게 보이도록 이어 붙인 영사으이 밝기를 적절하게 보정하는 블렌딩(blending) 처리도 해야 한다.
  • OpenCV는 이러한 일련의 영상 이어 붙이기 작업ㅇ르 수행하는 Stitcher 클래스를 제공한다.
    • Stitcher 객체는 Stitcher ::create() 함수를 이용해서 생성할 수 있다. Stitcher ::create() 함수는 하나의 인자 mode를 가지지만 기본값으로 Stitcher::PANORAMA가 정의되어 있으므로 생략할 수 있다.
    • 만약 스캐너 등으로 여러 장의 영상을 이어 붙이려면 Stitcher::SCANS를 mode 값으로 지정한다.
    • Stitcher::PANORAMA는 입력 영상들이 서로 투시 변환(또는 호모그래피) 관계에 있다고 가정하고 Stitcher::SCANS 모드는 입력 영상들이 서로 어파인 관계라고 간주한다.
int main(int argc, char* argv[])
{
if (argc < 3)
{
cerr << "Usage: stitching.exe <image_file1> <image_file2> [<image_file3>...]" << endl;
return -1;
}

vector<Mat> imgs;
for (int i = 1; i < argc; i++)
{
Mat img = imread(argv[i]);

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

Ptr<Stitcher> stitcher = Stitcher::create();

Mat dst;
Stitcher::Status status = stitcher->stitch(imgs, dst);

if (status != Stitcher::Status::OK)
{
cerr << "Error on stitching!" << endl;
return -1;
}

imwrite("result.jpg", dst);
imshow("dst", dst);

waitKey();
return 0;
}
[ssba]

The author

지성을 추구하는 사람/ suyeongpark@abyne.com

댓글 남기기

This site uses Akismet to reduce spam. Learn how your comment data is processed.