해당 포스트는 "열혈강의 영상처리 프로그래밍" 책의 내용을 요약한 것이다.



※NCC(Normalized Cross Correlation)

앞 포스트 영상 정합 포스트를 사정상 중간에 마무리했다. 해당 포스트는 영상 정합 포스트 마지막 내용을 이어서 쓰는 포스트이다. 따라서 해당 포스트를 읽기 전에 앞 포스트 "영상 정합 포스트"를 읽기 추천한다. 시작하겠다.


평균 밝기를 0으로 맞추는 것은 두 패치의 전체적인 밝기/평균 밝기에 큰 차이가 있을 때 SSD와 SAD에서 제대로 영상 접합을 할 수 없기 때문에 한다. 밝기의 표준 편차를 1로 맞추는 것은 평균 밝기가 같더라도 명암이 크게 차이가 나서 SSD나 SAD값이 달라질 수 있기 때문이다. 명암이 크다는 것은 어두운 픽셀과 밝은 픽셀이 영상 안에 많이 존재한다는 뜻이다. 따라서 평균 밝기가 같더라도 명암의 차이가 많이 난다면 입력 영상들의 픽셀값이 서로 많이 차이나 SSD나 SAD가 제대로 이루어 질 수 없다. 평균 밝기를 0으로 맞추고 표준 편차를 1로 맞추는 개념을 바탕으로 두 패치 사이의 NCC 값을 다음과 같이 나타낼 수 있다.


위에서 m1과 m2는 각 패치의 평균 밝기를 나타내고 시그마1과 시그마2는 각 패치 밝기의 표준편차를 나타낸다. 이렇게 구한 NCC값은 클수록 두 패치가 비슷하다고 나타내고, 작을수록 다르다는 것을 나타낸다. 다음은 NCC를 계산하는 코드이다. 평균과 표준편차를 구하는 부분과 표준편차를 이용하여 앞의 수식에 해당하는 NCC값을 구하는 부분으로 이루어진다. 표준편차를 구하는 데는 다음의 공식을 사용한다.



float _CalcNCC(const CByteImage& src1, const CByteImage& src2, int x1, int y1, int x2, int y2, int nhSize)
{
	if ((x1-nhSize)<0 || (x2-nhSize)<0 || (x1+nhSize)>=src1.GetWidth()  || (x2+nhSize)>=src2.GetWidth() ||
		(y1-nhSize)<0 || (y2-nhSize)<0 || (y1+nhSize)>=src1.GetHeight() || (y2+nhSize)>=src2.GetHeight())
		return FLT_MIN;

	// 평균과 표준편차 계산
	int nSum1=0, nSum2=0;
	int nSqr1=0, nSqr2=0;

	for (int r=-nhSize ; r<=nhSize ; r++)
	{
		BYTE* pSrc1 = src1.GetPtr(y1+r);
		BYTE* pSrc2 = src2.GetPtr(y2+r);

		for (int c=-nhSize ; c<=nhSize ; c++)
		{
			nSum1 += pSrc1[x1+c];
			nSqr1 += pSrc1[x1+c]*pSrc1[x1+c];
			nSum2 += pSrc2[x2+c];
			nSqr2 += pSrc2[x2+c]*pSrc2[x2+c];
		}
	}

	int nPixels = (nhSize*2+1)*(nhSize*2+1);
	float fMean1 = nSum1/nPixels;
	float fMean2 = nSum2/nPixels;
	float fVar1 = nSqr1/nPixels - fMean1*fMean1;
	float fVar2 = nSqr2/nPixels - fMean2*fMean2;

	// NCC값 계산
	float fSumProd = 0.0f;
	for (int r=-nhSize ; r<=nhSize ; r++)
	{
		BYTE* pSrc1 = src1.GetPtr(y1+r);
		BYTE* pSrc2 = src2.GetPtr(y2+r);

		for (int c=-nhSize ; c<=nhSize ; c++)
		{
			fSumProd += (pSrc1[x1+c]-fMean1)*(pSrc2[x2+c]-fMean2);
		}
	}

	return fSumProd/(sqrt(fVar1*fVar2))/nPixels;
}

※ 교차 검토를 이용한 패치 기반 정합

특징점 기반 영상 정합을 수행할 때는 일반적으로 한 영상을 기준 영상으로, 다른 영상을 목표 영상으로 설정한다. 그리고 기준 영상의 모든 특징점에 대해, SSD와 SAD, NCC 등의 방법을 통해 목표 영상의 대응점을 찾는 방식을 사용한다. 그런데 기준 영상의 한 특징점 P1에 대해 목표 영상에서 대응점 Q1을 찾았다고 해서, 거꾸로 목표 영상의 점 Q1에 대해 기준 영상에서 대응점을 찾은 결과가 처음의 P1과 일치한다는 보장은 없다. 즉, 기준 영상의 P1에 대해서는 목표 영상의 Q1이 가장 유사도가 높은 점일지라도 Q1의 입장에서는 기준 영상의 또 다른 점 P2가 가장 유사도가 높은 점일 수도 있는 것이다.

이렇듯 양 방향으로 검색 결과가 다르다는 것은 찾아낸 대응이 믿을 만한 결과가 아니라는 것을 뜻한다. 이러한 점들은 최종 대응 결과에서 제외하는 것이 바람직하다. 이러한 기법을 서로 교차하여 대응점 검색을 수행한다고 하여 교차검토라고 한다. 교차 검토를 사용하면 양방향으로 검사해야 하므로 계산시간이 최대 두 배까지 걸릴 수 있다. 하지만 정합을 더욱 정확하게 수행하고자 한다면 이를 사용하는 것이 바람직하다. 다음은 교차 검토를 이용한 패치 기반 SSD, SAD, NCC 정합을 구현한 메서드이다.

void CImageMatchDlg::_MAtchUsingSSD(int nPatchSize, int nThreshold)
{
	// 코너 추출
	double X1[1000], Y1[1000];
	double X2[1000], Y2[1000];

	int numCorner1 = HarrisCorner(m_imageIn1, 1e8, 0.04, 1000, X1, Y1);
	int numCorner2 = HarrisCorner(m_imageIn2, 1e8, 0.04, 1000, X2, Y2);

	m_imageOut = CByteImage(m_imageIn1.GetWidth() + m_imageIn2.GetWidth(), 
							MAX(m_imageIn1.GetHeight(), m_imageIn2.GetHeight()), 3);
	int nWidth1 = m_imageIn1.GetWidth();
	m_imageOut.Paste(Gray2RGB(m_imageIn1), 0, 0);
	m_imageOut.Paste(Gray2RGB(m_imageIn2), nWidth1, 0);

	// 패치 단위 대응점 검색
	for (int i=0 ; i<numCorner1 ; i++)
	{
		int nIdx2 = -1;
		int minSSD = INT_MAX;
		for (int j=0 ; j<numCorner2 ; j++)
		{
			int nSSD = _CalcSSD(m_imageIn1, m_imageIn2, X1[i], Y1[i], X2[j], Y2[j], nPatchSize/2);
			if (nSSD < minSSD)
			{
				nIdx2 = j;
				minSSD = nSSD;
			}
		}

		if (minSSD > nPatchSize*nPatchSize*nThreshold*nThreshold)
			continue;

		bool bCrsChk = true;
		for (int j=0 ; j<numCorner1 ; j++)
		{
			int nSSD = _CalcSSD(m_imageIn1, m_imageIn2, X1[j], Y1[j], X2[nIdx2], Y2[nIdx2], nPatchSize/2);
			if (nSSD < minSSD)
			{
				bCrsChk = false;
				break;
			}
		}

		if (bCrsChk)
		{
			DrawLine(m_imageOut, X1[i], Y1[i], X2[nIdx2]+nWidth1, Y2[nIdx2], 255, 0, 0);
		}
	}
}

void CImageMatchDlg::_MAtchUsingSAD(int nPatchSize, int nThreshold)
{
	// 코너 추출
	double X1[1000], Y1[1000];
	double X2[1000], Y2[1000];

	int numCorner1 = HarrisCorner(m_imageIn1, 1e8, 0.04, 1000, X1, Y1);
	int numCorner2 = HarrisCorner(m_imageIn2, 1e8, 0.04, 1000, X2, Y2);

	m_imageOut = CByteImage(m_imageIn1.GetWidth() + m_imageIn2.GetWidth(), 
							MAX(m_imageIn1.GetHeight(), m_imageIn2.GetHeight()), 3);
	int nWidth1 = m_imageIn1.GetWidth();
	m_imageOut.Paste(Gray2RGB(m_imageIn1), 0, 0);
	m_imageOut.Paste(Gray2RGB(m_imageIn2), nWidth1, 0);

	for (int i=0 ; i<numCorner1 ; i++)
	{
		int nIdx2 = -1;
		int minSAD = INT_MAX;
		for (int j=0 ; j<numCorner2 ; j++)
		{
			int nSAD = _CalcSAD(m_imageIn1, m_imageIn2, X1[i], Y1[i], X2[j], Y2[j], nPatchSize/2);
			if (nSAD < minSAD)
			{
				nIdx2 = j;
				minSAD = nSAD;
			}
		}

		if (minSAD > nPatchSize*nPatchSize*nThreshold)
			continue;

		bool bCrsChk = true;
		for (int j=0 ; j<numCorner1 ; j++)
		{
			int nSAD = _CalcSAD(m_imageIn1, m_imageIn2, X1[j], Y1[j], X2[nIdx2], Y2[nIdx2], nPatchSize/2);
			if (nSAD < minSAD)
			{
				bCrsChk = false;
				break;
			}
		}

		if (bCrsChk)
		{
			DrawLine(m_imageOut, X1[i], Y1[i], X2[nIdx2]+nWidth1, Y2[nIdx2], 255, 0, 0);
		}
	}
}

void CImageMatchDlg::_MAtchUsingNCC(int nPatchSize, float fThreshold)
{
	double X1[1000], Y1[1000];
	double X2[1000], Y2[1000];

	int numCorner1 = HarrisCorner(m_imageIn1, 1e8, 0.04, 1000, X1, Y1);
	int numCorner2 = HarrisCorner(m_imageIn2, 1e8, 0.04, 1000, X2, Y2);

	m_imageOut = CByteImage(m_imageIn1.GetWidth() + m_imageIn2.GetWidth(), 
							MAX(m_imageIn1.GetHeight(), m_imageIn2.GetHeight()), 3);
	int nWidth1 = m_imageIn1.GetWidth();
	m_imageOut.Paste(Gray2RGB(m_imageIn1), 0, 0);
	m_imageOut.Paste(Gray2RGB(m_imageIn2), nWidth1, 0);

	for (int i=0 ; i<numCorner1 ; i++)
	{
		int nIdx2 = -1;
		float maxNCC = FLT_MIN;
		for (int j=0 ; j<numCorner2 ; j++)
		{
			float fNCC = _CalcNCC(m_imageIn1, m_imageIn2, X1[i], Y1[i], X2[j], Y2[j], nPatchSize/2);
			if (fNCC > maxNCC)
			{
				nIdx2 = j;
				maxNCC = fNCC;
			}
		}

		if (maxNCC < fThreshold)
			continue;

		bool bCrsChk = true;
		for (int j=0 ; j<numCorner1 ; j++)
		{
			float fNCC = _CalcNCC(m_imageIn1, m_imageIn2, X1[j], Y1[j], X2[nIdx2], Y2[nIdx2], nPatchSize/2);
			if (fNCC > maxNCC)
			{
				bCrsChk = false;
				break;
			}
		}

		if (bCrsChk)
		{
			DrawLine(m_imageOut, X1[i], Y1[i], X2[nIdx2]+nWidth1, Y2[nIdx2], 255, 0, 0);
		}
	}
}

위 메서드들은 정합에 사용할 특징점을 이전 포스트에서 배운 해리스 코너 검출기로 얻는다. SSD에서는 밝기의 차이를 제곱하기 때문에 패치 전체의 문턱값을 nPatchSize*nPatchSize*nThreshold*nThreshold와 같이 계산했다. SAD에서는 절댓값을 이용하므로 패치 전체의 문턱값을 nPatchSize*nPatchSize*nThreshold와 같이 계산한다.


다음 그림은 전체적인 밝기가 차이가 있는 영상에 대해서 영상 접합을 수행한 결과이다. 

위 그림을 보면 NCC가 SSD와 SAD에 비해서 입력 영상의 대응점을 잘 찾는 걸 볼 수 있다.



※ 특징 기술자를 이용한 영상 정합

패치를 이용한 영상 정합은 영상의 가하학적 변화에 상당히 민감하다는 문제가 있다. 영상의 크기가 조금만 변하거나 카메라의 시점 변화가 있을 시 치명적이다. 따라서 이러한 기하학적 변화가 있어도 안정적으로 두 점 사이의 대응 관계를 찾는 기법이 연구 중이다. 

특징 기술자는 영상 안의 점에 대한 고유 특성을 표현할 수 있는 숫자들의 조합으로 보통 벡터나 행렬로 주어진다. 영상 패치도 일종의 특정 기술자이다. 패치 자체가 특징 기술자가 되어, 픽셀 값들로 이루어진 행렬이 특징 기술자가 된다. 그러나 패치 자체를 사용하는 것은 많은 문제가 있기 때문에 더 효율적인 특정 기술자들이 제안되고 이는 데 그중 가장 대표적인 SIFT 특징 기술자에 대해서 알아보자


※SIFT 특징 검출기

SIFT(Scale Invariant Feature Transform)은 영상의 스케일(비율) 변화에도 변하지 않는 특징 기술자를 생성하는 것을 목표로 개발되었다. 따라서 이를 사용하면 스케일이 다른 두 영상도 정합할 수 있다. 또한 시점 변화에 따른 영상의 기하학적 변화에도 어느 정도 강인하게 정합을 수행할 수 있는 장점이 있다.

SIFT는 특징점 검출기와 특징 기술자 생성기가 함께 구성된다. 특징점 검출기는 여러 스케일을 가지는 특징점을 모두 검출하기 위해 입력 영상을 여러 단계의 크기로 축소하여 이를 쌓아 올려 만든 영상 피라미드 상에서 특징점 검출을 수행한다. 다음은 영상 피라미드의 예이다.



영상 피라미드를 사용하면 위 그림처럼 모서리의 모양이 같지만 스케일이 서로 다른 코너들이 한 영상에 나타나도 이들을 모두 코너로 검출할 수 있다. 이때, 영상 피라미드에서 레벨 0에 가까운 낮은 단계에서는 위 그림에서 작은 사각형과 같이 세밀한 구조의 특징점이 검출되고 높은 단계에서는 큰 사각형에서와 같은 큼직한 특징점이 검출된다. 예를 들어 해리스 코너 검출기를 큰 스케일 영상에서 한다면 다음 그림의 오른쪽과 같이 커다란 하나의 코너만 나올 것이다. 하지만 작은 스케일 영상에서 한다면 왼쪽 그림과 같이 여러 개의 코너가 나올 수 있다. 따라서 해리스 코너 검출기를 사용할 때도 별도의 영상 피라미드를 만들어 각 단계에 대해 특징점 검출을 수행해야한다. 이러한 원리로 SIFT에서도 특징점을 모두 검출하기 위해서 영상 피라미드를 사용한다.





이 포스트도 이쯤에서 마치고 SIFT에 대한 추가적인 설명은 사정상 다음 포스트에서 설명할 예정이다. 양해 부탁드립니다.


해당 포스트는 "열혈강의 영상처리 프로그래밍" 책의 내용을 요약한 것이다.



※ 영상 정합

서로 다른 영상에 같은 대상이 나타난 부분이 있을 때 그 대응 관계를 찾는 것으로 주로 컴퓨터 비전 분야에서 많이 다룬다. 영상 정합은 주어진 두 영상이 거의 같다면 대응 관계를 찾기 쉽지만 두 영상 사이에 다양한 변형이 주어진다면 대응 관계를 찾기 힘들다. 그래서 영상 정합에서는 영상 사이에 나타나는 변형 상태에 따라 그에 적합한 기법을 사용해야 한다. 영상 정합은 서로 다른 두 영상에 나타는 같은 부분을 찾는 것이다. 예로 로봇이 영상을 이용하여 특정 물체를 찾는 시스템을 만든다고 하자. 이 때 로봇은 찾고자하는 목표 물체의 영상을 가지고 있어야 한다. 목표 영상을 매 순간 들어오는 입력 영상과 영상 정합을 수행해 목표 물체를 찾거나 물체의 위치를 파악할 수 있다. 영상 정합 문제는 입력 받은 두 영상이 비슷할수록 쉽고, 차이가 커질수록 어렵다. 영상 사이에 밝기/명암/색상 등 광학적 특성, 가로와 세로 위치/크기 등 기하학적 특성, 번짐/잡음 등으로 두 영상이 차이가 날 수 있다. 이러한 영상 접합 문제에 대한 접근 기법들로는 세기 기반 방식과 특정 기반 방식으로 나눌 수 있다. 세기 기반 방식은 주어진 영상의 모든 픽셀을 통째로 사용하여 정합을 수행하는 방식이고, 특징 기반 방식은 특징점들과 같이 선택된 픽셀들만을 이용해서 정합을 수행하는 방식이다. 세기 기반 방식은 더욱 많은 데이터를 사용하기 때문에 두 영상 사이의 변화가 적을수록 정확한 정합 결과를 얻을 수 있는 장점이 있지만 해결할 수 있는 변형이 제한적이라는 단점이 있다. 특징 기반 방식은 정확도가 떨어질 수 있는 문제가 있지만, 영상의 부분적인 가려짐이나 크기 등의 심한 변화에도 비교적 강인하게 정합을 수행할 수 있다는 장점이 있다.


특징 기반 영상 정합은 선택된 점들에 대해 점들 사이의 대응관계를 통하여 영상 전체의 대응과 정합을 얻는 방식이다. 점들을 선택할 때는 균등한 간격으로 선택할 수 있고 앞 포스트에서 배웠던 해리 코너 검출기 같은 특징점 검출기로 선택할 수 있다. 점들을 균등한 간격으로 선택하는 방법에서는 두 영상에서 주어진 픽셀 간격만큼 점들을 격자 형태로 선택한 다음, 선택된 각 점에서 얻은 영상의 패치나 특징 기술자를 비교한다. 특징점 검출기를 사용하는 방법은 점들을 격자 형태로 선택하는 대신, 특징점 검출기를 통하여 얻은 점들에 대하여 패치나 특징 기술자를 얻어 비교한다. 특징점 검출기를 사용하면 검출기 수행에 시간이 소요되기는 하지만, 특징점 검출기를 통하여 얻은 점들이 훨씬 대응 관계를 차즌 데 유리하기 때문에 일반적으로는 검출기로 점을 선택하는 방식을 사용하게 된다.



※ 패치 기반 영상 정합

특징점 단위로 수행하는 정합 기법에서도 세부적으로 특징점들을 어떻게 비교할 것인가에 따라 수행 방법이 나뉜다. 두 점의 유사도를 비교하는 방법 가운데 가장 간단한 것은 점 주변의픽셀 값 자체를 비교하는 것으로, 이 때 점 주변의 픽셀들을 모아서 얻은 작은 영상을 패치라고 한다. 서로 다른 두 영상에서 얻은 특징점들의패치를 모두 비교해서 유사도가 높은 패치들이 많다면 두 영상의 정합을 달성할 수 있다. 이 때, 패치의 크기를 크게 잡을수록 두 점의 유사도를 더욱 엄격하게 검사한다. 패치의 크기가 크면 실제로 같지 않은 두 점을 같다고 판단하는 오류가 줄어드는 반면, 실제로 같은 두 점을 같지 않다고 판단하는 오류가 생길 위험이 늘어난다. 두 패치의 유사도를 측정하는 방법에는 SSD(Sum of Squared Difference), SAD(Sum of Absolute Difference), NCC(Normalized Cross Correlation)이 있다. SSD는 두 패치에서 같은 위치에 있는 픽셀들끼리의 차이를 구하고 제곱합을 구하는 것이고 SAD는 이의 절댓값을 합하는 것이다. 다음은 이들을 수식으로 나타낸 것이다.


  


두 패치에 대해 구한 SSD나 SAD 값이 작을수록 두 패치는 비슷한 패치라고 볼 수 있다. 보통 문턱값을 설정하여 두 패치가 유사한지 아닌지 결정한다. 문턱값을 높게 잡을수록 실제로 다른 두 점을 같다고 판단할 위험이 커지지만, 낮게 잡을 수록 실제로 같은 점도 같지 않다고 판단할 가능성이 커진다. 따라서 패치의 크기와 유사도의 문턱값 두 가지가 패치 기반 영상 정합의 성능을 결정짓는 중요한 요소가 된다. 

SAD는 픽셀 값의 차이에 따른 결과값이 선형으로 증가하지만, SSD는 2차 함수로 증가하기 때문에 SSD가 픽셀 값의 큰 차이에 대해 더욱 민감하게 반응하는 특성이 있다. 

다음은 SSD와 SSA를 계산하는 메서드이다. 매개 변수 x1, y1, x2, y2는 두 입력 영상에서 유사도 값을 측정할 대응점들의 위치를 가리킨다. src1과 src2는 각각 정합하고자 하는 두 입력 영상에 해당한다. 이 때, 대응점을 중심으로 한 패치가 아니라 입력 영상 전체를 매개변수로 하는 것은 메모리 할당 등 패치를 따로 만드는 데 필요한 계산을 줄이기 위해서다. nhSize는 패치의 범위를 나타내는 매개변수이다.

int _CalcSSD(const CByteImage& src1, const CByteImage& src2, int x1, int y1, int x2, int y2, int nhSize)
{
	if ((x1-nhSize)<0 || (x2-nhSize)<0 || (x1+nhSize)>=src1.GetWidth()  || (x2+nhSize)>=src2.GetWidth() ||
		(y1-nhSize)<0 || (y2-nhSize)<0 || (y1+nhSize)>=src1.GetHeight() || (y2+nhSize)>=src2.GetHeight())
		return INT_MAX;

	int nSum = 0;
	for (int r=-nhSize ; r<=nhSize ; r++)
	{
		BYTE* pSrc1 = src1.GetPtr(y1+r);
		BYTE* pSrc2 = src2.GetPtr(y2+r);

		for (int c=-nhSize ; c<=nhSize ; c++)
		{
			nSum += (pSrc1[x1+c]-pSrc2[x2+c])*(pSrc1[x1+c]-pSrc2[x2+c]);
		}
	}
	return nSum;
}

int _CalcSAD(const CByteImage& src1, const CByteImage& src2, int x1, int y1, int x2, int y2, int nhSize)
{
	if ((x1-nhSize)<0 || (x2-nhSize)<0 || (x1+nhSize)>=src1.GetWidth()  || (x2+nhSize)>=src2.GetWidth() ||
		(y1-nhSize)<0 || (y2-nhSize)<0 || (y1+nhSize)>=src1.GetHeight() || (y2+nhSize)>=src2.GetHeight())
		return INT_MAX;

	int nSum = 0;
	for (int r=-nhSize ; r<=nhSize ; r++)
	{
		BYTE* pSrc1 = src1.GetPtr(y1+r);
		BYTE* pSrc2 = src2.GetPtr(y2+r);

		for (int c=-nhSize ; c<=nhSize ; c++)
		{
			nSum += abs(pSrc1[x1+c]-pSrc2[x2+c]);
		}
	}
	return nSum;
}


NCC(Normalized Cross Correlation)은 SSD와 SAD가 가지는 밝기 차이 문제를 해결하기 위해 사용하는 유사도 측정 방법이다. SSD와 SAD는 단순히 입력 영상들의 픽셀들만 비교하기 때문에 영상 안 물체가 똑같더라도 서로의 밝기가 다르면 같은 물체로 취급하지 않게 된다. 즉, SSD와 SAD는 영상의 전체적인 밝기값이 차이가 난다면 정확한 영상 정합을 기대하기가 힘들다.  NCC는 이러한 문제를 정규화를 통해 해결한다. 두 입력영상 패치 각각의 밝기 평균과 표준편차를 계산하여 평균 밝기가 0, 표준편차는 1이 되도록 정규화(normalization)한다. 따라서 전체적인 밝기의차이보다는 픽셀 하나하나의 밝기 차이에 더 의미를 두는 유사도 측정 기법이라 할 수 있다. 이에 관해서 더 포스트할 예정인데 사정상 다음 포스트에서 계속 해서 연재하겠다.

+ Recent posts