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



※ 영상의 특징값 추출 

얼굴 인식 기술과 같이 영상에서 특정 대상에 대한 정보를 얻어내는 작업을 한다고 할 때 영상 내 수많은 픽셀 값을 모두 사용하는 것은 효율성이나 정확도 측면에서 바람직하지 못하다. 따라서 영상에서 중요한 정보들을 걸러내고 이를 활용하는 것이 중요하다. 영상의 주요 정보들로는 영상의 평균 색, 히스토그램, 경계선, 꼭지점이 있고 이들을 영상의 특징값이라 한다. 이러한 특징값은 영상에서 가장 기본적인 정보의 단위로 영상 내 사물이나 사람을 인식하는 것과 같은 고차원적인 작업을 수행하는 데 중요한 역할을 한다.



※ 영상의 경계 추출

영상에서 경계선이란 색이나 밝기가 급격하게 변하는 부분을 가리킨다. 전체 픽셀 수보다 경계선을 이루는 픽셀 수가 훨씬 적기 때문에 경계선을 추출하여 이를 처리하는 방법이 정보량 측면에서 많은 장점이 있다. 여기서 소벨 경계선 검출기와 캐니 경계선 검출기에 대해서 알아볼 예정이다.


※ 소벨 경계선 검출기

소벨 마스크를 이용하여 회선 연산을 해 결과로 영상의 경계가 추출한다. 영상의 경계선은 색이나 밝기가 급격하게 변하기 때문에 픽셀의 미분을 이용해 소벨 마스크를 도출할 수 있다. 소벨 마스크는 아래와 같이 가로 방향 미분값을 계산하는 가로 방향 마스크와 세로 방향 미분값을 계산하는 세로 방향 마스크 두 개로 구성되어 있다. 가로 방향 마스크는 왼쪽 표이고 세로 방향 마스크는 오른쪽 표이다.

소벨 마스크는  가로, 세로 방향 두 개의 마스크로 구성되어 있기 때문에 하나의 영상에 종합적인 경계선 정보를 표시해야한다. 따라서 두 마스크를 이용한 결과값을 하나로 합쳐주기 위해서 각 방향 미분값으로 구성되는 벡터의 크기를 구한다. 다음은 소벨 경계선 검출기를 수행하는 함수이다.

void SobelEdge(const CByteImage& imageIn, CByteImage& imageOut)
{
	int nWidth	= imageIn.GetWidth();
	int nHeight = imageIn.GetHeight();
	imageOut = CByteImage(nWidth, nHeight);
	imageOut.SetConstValue(0);

	int nWStep = imageIn.GetWStep();

	// Sobel 마스크
	int Gx[9], Gy[9];
	Gx[0] = -1; Gx[1] = 0; Gx[2] = 1;
	Gx[3] = -2; Gx[4] = 0; Gx[5] = 2;
	Gx[6] = -1; Gx[7] = 0; Gx[8] = 1;

	Gy[0] =  1; Gy[1] =  2; Gy[2] =  1;
	Gy[3] =  0; Gy[4] =  0; Gy[5] =  0;
	Gy[6] = -1; Gy[7] = -2; Gy[8] = -1;

	BYTE* pIn  = imageIn.GetPtr();
	BYTE* pOut = imageOut.GetPtr();

	for (int r=1 ; r<nHeight-1 ; r++) // 영상 경계는 제외
	{
	for (int c=1 ; c<nWidth-1 ; c++) // 영상 경계는 제외
	{
		int sumX = 0;
		int sumY = 0;
		for (int y=0 ; y<3 ; y++)
		{
		for (int x=0 ; x<3 ; x++)
		{ 
			int py = r-1+y;
			int px = c-1+x;
                             //영상과 Sobel 마스크와의 회선 연산 수행
			if (px>=0 && px<nWidth && py>=0 && py<nHeight)
			{
				sumX += Gx[y*3+x]*pIn[py*nWStep+px];
				sumY += Gy[y*3+x]*pIn[py*nWStep+px];
			}
		}
		}
		pOut[c] = sqrt((double)(sumX*sumX + sumY*sumY)/32); 
                 //미분 벡터의 크기를 이용하여 경계선의 세기 계산
	}
	pOut += nWStep;
	}
}

위 메서드에서 가로, 세로 방향 결과값을 종합하기 위해서 미분 벡터의 크기를 구했다. 구하는 방식은 다음 식과 같다.


 

이렇게 구한 벡터의 크기가 경계선의 세기가 된다. sumX와 sumY는 각각 가로 세로 방향의 미분값을 나타낸다. 위에서 벡터 크기를 구할 때 32를 나누는데 그 이유는 마스크 회선 연산한 sumX^2+sumY^2 값의 최댓값이 255*루트 32이기 때문이다. 만약 32를 나누지 않는다면 값이 255를 초과해 결과 영상을 저장하는 픽셀의 데이터형이 바이트형이여서 오류가 일어난다. 하지만 32로 나누게 되면 경계선 추출 결과 영상이 전체적으로 알아보기 어렵게 어둡게 나타날 수도 있다. 이러한 문제를 해결하려면 다음 코드와 같이 해도 괜찮다.


pOut[c] = MIN(sqrt((double)(sumX*sumX + sumY*sumY)/4), 255);


다음 그림은 회색조 입력 영상에 대해서 소벨 경계선 추출한 결과 영상이다.


소벨 경계선 검출기는 영상의 경계선을 쉽고 빠르게 검출할 수 있다는 장점이 있다. 추출된 경계선의 두께와 색이 경계선의 세기에 따라 달라진다. 즉, 경계가 뚜렷한 부분에서는 추출된 경계선이 굵고 밝게 나타나고 경계가 또렷하지 못한 부분에서는 가늘고 어둡게 나타난다.



※ 캐니 경계선 검출기

소벨 경계선 검출기처럼 굵기가 변하는 경계썬 보다는 가늘고 정확한 윤곽선 정보만 추출한 결과를 얻고 싶다면 캐니 경계선 검출기 알고리즘을 사용하는 것이 좋다. 캐니 경계선 검출기도 미분값을 이용하지만 구한 미분값을 분석하여 더욱 깔끔한 경계선을 추출한다는 점에서 다른 마스크 추출 기법과 차이가 있다. 다음은 캐니 경계선 검출기의 알고리즘 순서이다.


1. 가우시안 마스크를 사용하여 영상의 잡음 감소

2. 마스크를 이용하여 가로와 세로 방향 미분값 계산

3. 경계선의 세기 및 방향 분석

4. 지역적으로 최대 세기가 아닌 경계선 제거

5. 경계선의 방향을 따라 비교 수행


위 알고리즘 각각에 대해서 자세히 알아보자.


1. 가우시안 마스크를 사용하여 영상의 잡음 감소

먼저 가우시안 마스크를 이용해서 영상의 잡음을 감소시킨다. 왜냐하면 잡음이 있는 픽셀은 픽셀 변화량이 커 일반적으로 큰 미분값을 가지기 때문에 나중에 수행하는 단계에 큰 영향을 미칠 수 있다. 여기서 사용하는 가우시안 마스크의 크기와 표준편차값은 상황에 따라 달라지는 데 우리는 다음과 같은 마스크를 사용한다.



2. 마스크를 이용하여 가로와 세로 방향 미분값 계산

영상의 잠음 감소 후 다음 단계로 영상의 가로와 세로 방향의 미분을 구한다. 미분을 구할 때 우리는 소벨 마스크를 기본으로 사용한다. 


3. 경계선의 세기 및 방향 분석

미분값 계산 후 경계선의 세기 및 방향을 분석한다. 경계선의 세기는 소벨 마스크에서 경계선을 추출한 것처럼 미분벡터의 크기를 구하면 된다. 경계선의 방향은 다음 식과 같이 벡터의 방위각을 구한다. 이 방위각은 경계선에 대해 수직이다. 미분은 픽셀들 사이의 차를 통해 구하기 때문에 인접한 경계선들의 미분 결과는 거의 제로에 가깝다. 픽셀들 사이의 차가 큰 쪽이 미분값이 크고 경계선에 수직인 픽셀들이 미분값이 클 가능성이 높으므로 우리가 구한 벡터의 방위각은 경계선에 거의 수직이라고 할 수 있다. 



이렇게 구한 경계선의 방향은 나중에 경계선의 방향을 따라서 경계선의 세기를 비교하는 작업에 사용된다. 위 알고리즘 순서에는 가로/세로 미분값을 계산한 후 경계선의 세기 및 방향을 계산하지만 실제로는 입력 영상의 매 픽셀을 방문할 때 이 두 작업을 동시에 수행하여 메모리 접근에 소요되는 시간을 절약한다.


4. 지역적으로 최대 세기가 아닌 경계선 제거

지역적으로 최댓값이 아닌 경계선을 제거하는 데 이 과정을 비최댓값 억제(Non-maximum suppression)라고 한다. 여기서 세 번째 단계에서 구한 미분값의 크기와 방향을 사용한다. 다음 그림은 경계선의 세기와 방향이다.




경계선 세기의 지역적 최대란 위 그림에서 흰색 배경으로 표시된 픽셀들과 같이 경계선의 세기가 경계선 방향에 수직인 위치에 있는 양쪽 픽셀에 비하여 높은 것을 의미한다. 예를 들어 위 그림의 가운데 픽셀 7을 보자. 경계선 방향에 수직인 위치에 있는 픽셀은 왼쪽 위와 오른쪽 아래인 픽셀 2 두 개이다. 이 두 픽셀에 비해 가운데 픽셀은 7로 더 높기 때문에 지역적 최대가 된다. 하지만 가운데 바로 아래 있는 픽셀 6을 보면 경계선 방향에 수직인 왼쪽 위 6과 오른쪽 아래 1을 비교 했을 때 픽셀 6이 왼쪽 위와 똑같기 때문에 지역적 최대가 되지 않는다. 이런 과정을 거쳐서 조건을 만족하는 픽셀을 경계선의 후보 픽셀로 지정하는 데 그 결과로 가느다란 선들만이 후보 픽셀들로 남게 된다.


5. 경계선을 따라 비교 수행

마지막 단계는 후보로 지정된 픽셀들을 대상으로 경계선 세기의 문턱값 검사를 수행한다. 여기에서 두 개의 문턱값을 사용하는 데 높은 문턱값 T(H)와 낮은 문턱값 T(L)로 구성된다. 각 후보 픽셀이 다음 두 조건 중 하나를 만족하면 해당 픽셀은 최종 경계선으로 결정된다.


- 경계선의 세기가 T(H)보다 높다.

- 경계선의 세기가 T(L)보다 높으면서, 경계선 방향과 같은 방향에 있는 양쪽 두 픽셀 가운데 하나 이상이 경계선 후보 픽셀임과 동시에 경계선의 세기가 T(H)보다 높다. 


T(H) 값을 높일수록 전체적인 경계선의 개수가 줄어들고 낮은 문턱값 T(L)을 높일수록 각 경계선의 길이가 짧아진다. 이렇게 두 개의 문턱값을 활용하여 조건을 검사하는 것을 히스테리시스라고 한다. 이러한 다섯 단계를 거치면 얇은 선으로 정리된 경계선이 얻어진다.


위 알고리즘을 수행한 케니 경계선 검출기 메서드는 아래와 같다. 


void CannyEdge(const CByteImage& imageIn, CByteImage& imageOut, int nThresholdHi, int nThresholdLo)
{
	int nWidth	= imageIn.GetWidth();
	int nHeight = imageIn.GetHeight();
	int nWStep = imageIn.GetWStep();

	// 가우시안 마스크
	CDoubleImage  imageGss(nWidth, nHeight);
	imageGss = _Gaussian5x5(imageIn);  //밑에 함수 정의되어 있음

	// 소벨 마스크
	int Gx[9] = {-1, 0, 1, -2, 0, 2, -1, 0, 1};
	int Gy[9] = {1, 2, 1, 0, 0, 0, -1, -2, -1};

	CDoubleImage  imageMag(nWidth, nHeight);
	CByteImage imageAng(nWidth, nHeight);
	int nWStepG = imageGss.GetWStep();
	int nWStepA = imageAng.GetWStep();

	double* pGss = imageGss.GetPtr();

	// 미분 구하기
	for (int r=0 ; r<nHeight ; r++)
	{
	double* pMag = imageMag.GetPtr(r);
	BYTE*   pAng = imageAng.GetPtr(r);
	for (int c=0 ; c<nWidth ; c++)
	{
		double sumX = 0.0;
		double sumY = 0.0;
		for (int y=0 ; y<3 ; y++)
		{
			for (int x=0 ; x<3 ; x++)
			{
				int py = r-1+y;
				int px = c-1+x;
				if (px>=0 && px<nWidth && py>=0 && py<nHeight)
				{
					sumX += Gx[y*3+x]*pGss[py*nWStepG+px];
					sumY += Gy[y*3+x]*pGss[py*nWStepG+px];
				}
			}
		}

		pMag[c] = sqrt(sumX*sumX + sumY*sumY); // 경계선의 세기
		double theta;					 // 경계선의 수직 방향
		if (pMag[c] == 0)
		{
			if(sumY == 0)
			{
				theta = 0;
			}
			else
			{
				theta = 90;
			}
		}
		else
		{
			theta = atan2((float)sumY, (float)sumX)*180.0/M_PI;
		}

		if ((theta > -22.5 && theta < 22.5) || theta > 157.5 || theta < -157.5)
		{
			pAng[c] = 0;
		}
		else if ((theta >= 22.5 && theta < 67.5) || (theta >= -157.5 && theta < -112.5))
		{
			pAng[c] = 45;
		}
		else if ((theta >= 67.5 && theta <= 112.5) || (theta >= -112.5 && theta <= -67.5))
		{
			pAng[c] = 90;
		}
		else
		{
			pAng[c] = 135;
		}
	} // 열 이동 끝
	} // 행 이동 끝

	// 비 최대값 억제
	CByteImage imageCand(nWidth, nHeight);
	imageCand.SetConstValue(0);

	for (int r=1 ; r<nHeight-1 ; r++)
	{
	BYTE*	pCand = imageCand.GetPtr(r);
	double* pMag  = imageMag.GetPtr(r);
	BYTE*   pAng  = imageAng.GetPtr(r);
	for (int c=1 ; c<nWidth-1 ; c++)
	{
		switch (pAng[c])
		{
		case 0:		// 0도 방향 비교
			if (pMag[c] > pMag[c-1] && pMag[c] > pMag[c+1])
			{
				pCand[c] = 255;
			}
			break;
		case 45:	// 45도 방향 비교
			if (pMag[c] > pMag[c-nWStepG+1] && pMag[c] > pMag[c+nWStepG-1])
			{
				pCand[c] = 255;
			}
			break;
		case 90:		// 90도 방향 비교
			if (pMag[c] > pMag[c-nWStepG] && pMag[c] > pMag[c+nWStepG])
			{
				pCand[c] = 255;
			}
			break;
		case 135:	// 135도 방향 비교
			if (pMag[c] > pMag[c-nWStepG-1] && pMag[c] > pMag[c+nWStepG+1])
			{
				pCand[c] = 255;
			}
			break;
		}
	} // 열 이동 끝
	} // 행 이동 끝

	imageCand.SaveImage("Cand.bmp");
	
	// 문턱값 검사
	imageOut = CByteImage(nWidth, nHeight);
	imageOut.SetConstValue(0);
	for (int r=1 ; r<nHeight-1 ; r++)
	{
	BYTE*	pOut  = imageOut.GetPtr(r);
	BYTE*	pCand = imageCand.GetPtr(r);
	double* pMag  = imageMag.GetPtr(r);
	BYTE*   pAng  = imageAng.GetPtr(r);
	for (int c=1 ; c<nWidth-1 ; c++)
	{
		if (pCand[c])
		{
			if (pMag[c] > nThresholdHi)
			{
				pOut[c] = 255;
			}
			else if (pMag[c] > nThresholdLo) // 연결된 픽셀 검사
			{
				bool bIsEdge = true;
				switch (pAng[c])
				{
				case 0:		// 90도 방향 검사
					if ((pMag[c-nWStepG] > nThresholdHi) || 
						(pMag[c+nWStepG] > nThresholdHi))
					{
						pOut[c] = 255;
					}
					break;
				case 45:	// 135도 방향 검사
					if ((pMag[c-nWStepG-1] > nThresholdHi) || 
						(pMag[c+nWStepG+1] > nThresholdHi))
					{
						pOut[c] = 255;
					}
					break;
				case 90:		// 0도 방향 검사
					if ((pMag[c-1] > nThresholdHi) || 
						(pMag[c+1] > nThresholdHi))
					{
						pOut[c] = 255;
					}
					break;
				case 135:	// 45도 방향 검사
					if ((pMag[c-nWStepG+1] > nThresholdHi) || 
						(pMag[c+nWStepG-1] > nThresholdHi))
					{
						pOut[c] = 255;
					}
					break;
				}
			}
		}
	} // 열 이동 끝
	} // 행 이동 끝
}


CDoubleImage _Gaussian5x5(const CIntImage& imageIn)
{
	int nWidth	= imageIn.GetWidth();
	int nHeight = imageIn.GetHeight();
	int nWStep = imageIn.GetWStep();

	CDoubleImage imageGss(nWidth, nHeight);
	imageGss.SetConstValue(0);
	int nWStepG = imageGss.GetWStep();

	int nWStep2 = 2*nWStep;

	for (int r=2 ; r<nHeight-2 ; r++)
	{
		double* pGss = imageGss.GetPtr(r);
		int*    pIn  = imageIn.GetPtr(r);
		for (int c=2 ; c<nWidth-2 ; c++)
		{
			pGss[c] = (2*(pIn[c-nWStep2-2] + pIn[c-nWStep2+2] + pIn[c+nWStep2-2] + pIn[c+nWStep2+2]) + 
					  4*(pIn[c-nWStep2-1] + pIn[c-nWStep2+1] + pIn[c-nWStep-2] + pIn[c-nWStep+2] +
						 pIn[c+nWStep-2] + pIn[c+nWStep+2] + pIn[c+nWStep2-1] + pIn[c+nWStep2+1]) + 
					  5*(pIn[c-nWStep2] + pIn[c-2] + pIn[c+2] + pIn[c+nWStep2]) + 
					  9*(pIn[c-nWStep-1] + pIn[c-nWStep+1] + pIn[c+nWStep-1] + pIn[c+nWStep+1]) + 
					  12*(pIn[c-nWStep] + pIn[c+nWStep] + pIn[c-1] + pIn[c+1]) + 
					  15*(pIn[c]))/159.0;
		}
	}
	
	return imageGss;
}


첨부 파일에 있는 코드를 보면 가우시안 필터링의 결과값과 미분 벡터의크기 등을 저장하도록 CDoubleImage형 영상을 사용했는 데 프로그램 수행 시간 단축이 중요하다면 정확도 저하를 감수하고 CIntImage 형을 사용해도 좋다. 다음 그림은 캐니 경계선 검출기를 한 결과 영상이다.





※ 허프 변환을 이용한 직선 검출

우리는 경계선에서 특정 모양이나 패턴을 검출한다. 예를 들어 자동차의 번호판을 인식하는 시스템을 만든다고 하면 영상 내의 경계선에서 번호판 모양에 해당하는 직사각형을 검출해야 한다. 직사각형은 네 개의 직선이 조합된 것으로 직선은 경계선으로부터 추출할 수 있는 특수 패턴 가운데 가장 기본적인 단위이다.  여기서는 경계선 영상으로부터 직선 성분을 추출하고 분석하는 기법에 대해 살펴볼 것이다. 

선은 점들의 집합이다. 따라서 우리는 영상에서 모든 직선/경게선들 가운데 되도록 많은 점들을 지나는 직선을 골라내야한다. 우리는 2차원 공간에서 직선을 표현하는 방법 중 우리에게 가장 익숙한 것은 직선의 방정식 y=ax+b이다. 직선의 방정식을 이용하면 모든 직선을 실수 a,b의 조합으로 나타낼 수 있다. 하지만 기울기 a가 무수히 증가하게 되면 y절편에 해당하는 b의 절대값 또한 무한히 증가할 가능성이 높다. 즉, 실수 a, b의 조합이 무수히 많다는 뜻이다. 따라서 이러한 직선 표현 기법으로는 영상 안에서 표현할 수 있는 모든 직선을 검사하기란 거의 불가능하다. 

검사해야 할 변수의 조합이 무수히 많은 문제를 해결하고자 허프 변환을 사용한다. 직선에 대한 허프 변환은 직선을 다음과 같이 극좌표계 형식으로 표현한다.


  


p는 원점에서부터 직선까지의 거리를 나타내고, theta는 직선에 그어진 수선과 y축 사이의 각도를 나타낸다. 극좌표계를 사용하면 점 (x,y)을 지나는 모든 직선을 p와 theta의 조합으로 표현할 수 있다. 각도가 가질 수 있는 범위는 0에서 360도 이지만 각도의 구간을 얼마나 정밀하게 나누어 조사할 것인지는 사용자의 입력에 달려 있다. 직선의 방향을 더욱 정밀하게 검출하고자 한다면 구간을 조밀하게 나누면 된다. 이렇게 하면 더욱 많은 직선을 검출할 수 있지만 계산 시간이 더욱 오래 걸린다. 프로그램으로 구현할 때에는 각도의 범위를 0에서 180도 사이로 하고 p값이 음수를 가질 수 있도록 할 것이다. 위 허프 변환 식에서 각도에 "180도+각도"를 대입하면 -p가 나오는 걸 확인할 수 있다. 그리고 무수히 많은 p와 각도의 조합이 나오므로 먼저 p와 각도 조합이 출현한 횟수를 셀 수 있는 2차원 배열을 할당한다. 매 경계선 픽셀에 대해서 가능한 직선을 만들 때마다 해당 p와 각도 조합의 출현 횟수를 증가시킨다. 그리고 기준값 이상으로 출현한 p와 각도 조합을 골라내면 직선 검출 과정이 완료된다. 다음은 직선에 대한 허프 변환을 수행하는 함수이다. 입력 영상 imageIn은 소벨이나 캐니 경계선 검출기로 얻은 경계선 영상을 사용한다. nTNum은 경계선 픽셀 수에 대한 문턱값으로 몇 개 이상의 점을 지나는 직선을 검출할 것인지를 결정하는 값이다. nTVal은 입력 영상의 경계선 픽셀들 가운데 얼마 이상의 값을 가지는 픽셀을 경계썬 픽셀로 인정할 것인가를 결정한다.(캐니 경계선 검출기는 0 아니면 255값을 가지지만 소벨 경계선 검출기는 0에서 255사이의 값을 가진다) double형 변수 resTheta는 픽셀을 지나는 직선을 구할 때 0도에서 180도 사이의 각도 값을 몇 도 간격으로 사용할지를 결정한다. numLine은 검출되는 최대 직선 수를 나타낸다. pRho와 pTheta는 검출된 직선에 대한 p와 각도 값이 들어간다. 


int HoughLines(const CByteImage& imageIn, int nTNum, int nTVal, double resTheta, int numLine, double* pRho, double* pTheta) { int nWidth = imageIn.GetWidth(); int nHeight = imageIn.GetHeight(); int nWStep = imageIn.GetWStep(); int numRhoH = (int)(sqrt((double)(nWidth*nWidth + nHeight*nHeight))); // 영상 대각선의 길이 int numRho = numRhoH*2; // rho의 음수 영역을 위해 2를 곱함 int numThe = 180 / resTheta; int numTrans = numRho*numThe; // rho와 theta 조합의 출현 횟수를 저장하는 공간 double* sinLUT = new double[numThe]; // sin 함수 룩업 테이블 double* cosLUT = new double[numThe]; // cos 함수 룩업 테이블 double toRad = M_PI/numThe; for (int theta=0 ; theta<numThe ; theta++) { sinLUT[theta] = (double)sin(theta*toRad); cosLUT[theta] = (double)cos(theta*toRad); } int* pCntTrans = new int[numTrans]; memset(pCntTrans, 0, numTrans*sizeof(int)); for (int r=0 ; r<nHeight ; r++) { BYTE* pIn = imageIn.GetPtr(r); for (int c=0 ; c<nWidth ; c++) { if (pIn[c] > nTVal) // 경계선 픽셀 { for (int theta=0 ; theta<numThe ; theta++) { int rho = (int)(c*sinLUT[theta] + r*cosLUT[theta] + numRhoH + 0.5);

//numRhoH를 더하는 이유는 rho가 음수가 되는 걸 막기 위해서

//0.5 를 더한 이유는 반올림 하기 위해 pCntTrans[rho*numThe+theta]++; } } } } // nThreshold을넘는 결과 저장 int nLine = 0; for (int i=0 ; i<numTrans && nLine<numLine ; i++) { if (pCntTrans[i] > nTNum) { pRho[nLine] = (int)(i/numThe); // rho의 인덱스 pTheta[nLine] = (i - pRho[nLine]*numThe)*resTheta; //theta 의 인덱스 pRho[nLine] -= numRhoH; // 음수 값이 차지하는 위치만큼 뺄셈 nLine++; } } delete [] pCntTrans; delete [] sinLUT; delete [] cosLUT; return nLine; }


※ 해리스 코너 검출기

해리스 코너 검출기는 미분값을 기반으로 모서리로 된 코너 점을 검출하는 방법이다. 해리스 코너 검출기는 미분값을 사용하여 코너 응답 함수를 정의하는데 코너 응답 함수란 해당 픽셀 위치가 얼마나 코너(모서리)같이 생겼는가 하는 것이다. 즉, 코너 응답 함수에서 코너의 특성을 이용해 코너 점을 검출한다. 코너의 특성은 미분을 이용한 고유 벡터로 정의할 수 있다. 해리 코너 검출기에서는 고유 벡터를 간단히 해 코너 특성을 다음 식처럼 표현한다.



이렇게 모든 픽셀에 대해서 코너 응답 함수를 적용한 다음에 그 값을 적절한 문턱값으로 걸러 코너 점을 검출한다. 또한 영상의 잡음에 대한 영향을 줄이기 위해서 가우시안 필터를 사용한다. 위에서 본 캐니 경계선 검출기는 미분값을 구하기 전에 가우시안 필터를 적용했으나 해리스 코너 검출기는 미분을 한 후 결과값에 가우시안 필터를 적용한다. 또한 문턱값을 넘는 코너 점들이 한 지역에서 반복적으로 검출되는 것을 막기 위해서 비최댓값 억제 기법을 사용한다. 다음은 해리스 코너 검출기 알고리즘의 순서이다.


1. 소벨 마스크를 사용하여 미분값 계산

2. 미분값의 곱 계산

3. 미분값의 곱에 가우시안 마스크 적용

4. 코너 응답 함수 계산

5. 비최대값 억제 수행으로 최종 코너 검출


해리스 코너 검출기 메서드의 선언부는 다음과 같다.


int HarrisCorner(const CByteImage& imageIn,  double dThreshold, double paramK, int nMaxCorner, double* posX, double* posY)


dThreshold 는 문턱값을 의미한다. 소벨 검출기를 이용해서 미분값의 자리수가 커진다. 따라서 보통 문턱값으로 10^8을 넣는다. paramK는 코너 응답 함수를 계산하는데 필요한 K값이다. posX와 posY는 해리스 코너 검출기 결과 로 얻어진 코너점의 x좌표와 y좌표를 넣을 변수이다. 

해리스 코너 검출기 메서드에 대한 정의 부분은 다음과 같다.

int HarrisCorner(const CByteImage& imageIn,  double dThreshold, double paramK, int nMaxCorner, double* posX, double* posY)
{
	int nWidth	= imageIn.GetWidth();
	int nHeight = imageIn.GetHeight();
	int nWStep = imageIn.GetWStep();

	CIntImage imageDxx(nWidth, nHeight); imageDxx.SetConstValue(0);
	CIntImage imageDxy(nWidth, nHeight); imageDxy.SetConstValue(0);
	CIntImage imageDyy(nWidth, nHeight); imageDyy.SetConstValue(0);
	int nWStepD = imageDxx.GetWStep();

	// 미분 계산
	int dx, dy;

	for (int r=1 ; r<nHeight-1 ; r++)
	{
		BYTE* pIn = imageIn.GetPtr(r);
		int* pDxx = imageDxx.GetPtr(r);
		int* pDxy = imageDxy.GetPtr(r);
		int* pDyy = imageDyy.GetPtr(r);
		for (int c=1 ; c<nWidth-1 ; c++)
		{
			dx = pIn[c-nWStep-1] + 2*pIn[c-1] + pIn[c+nWStep-1] 
			   - pIn[c-nWStep+1] - 2*pIn[c+1] - pIn[c+nWStep+1];
			dy = pIn[c-nWStep-1] + 2*pIn[c-nWStep] + pIn[c-nWStep+1] 
			   - pIn[c+nWStep-1] - 2*pIn[c+nWStep] - pIn[c+nWStep+1];
			pDxx[c] = dx*dx;
			pDxy[c] = dx*dy;
			pDyy[c] = dy*dy;
		}
	}

	// 가우시안 필터링
	CDoubleImage imageGDxx = _Gaussian5x5(imageDxx);
	CDoubleImage imageGDxy = _Gaussian5x5(imageDxy);
	CDoubleImage imageGDyy = _Gaussian5x5(imageDyy);

	// 코너 응답 함수
	CDoubleImage imageCornerScore(nWidth, nHeight);

	for (int r=2 ; r<nHeight-2 ; r++)
	{
		double *pScore = imageCornerScore.GetPtr(r);
		double* pGDxx = imageGDxx.GetPtr(r);
		double* pGDxy = imageGDxy.GetPtr(r);
		double* pGDyy = imageGDyy.GetPtr(r);
		for (int c=2 ; c<nWidth-2 ; c++)
		{
			pScore[c] = (pGDxx[c]*pGDyy[c] - pGDxy[c]*pGDxy[c]) 
				- paramK*(pGDxx[c]+pGDyy[c])*(pGDxx[c]+pGDyy[c]);
		}
	}

	// 지역 최대값 추출
	int numCorner = 0;
	for (int r=2 ; r<nHeight-2 ; r++)
	{
		double *pScore = imageCornerScore.GetPtr(r);
		for (int c=2 ; c<nWidth-2 ; c++)
		{
			if (pScore[c] > dThreshold)
			{
				if (pScore[c] > pScore[c-nWStepD] &&
					pScore[c] > pScore[c+nWStepD] &&
					pScore[c] > pScore[c-1] &&
					pScore[c] > pScore[c+1])
				{
					posX[numCorner] = c;
					posY[numCorner] = r;
					numCorner++;

					if (numCorner >= nMaxCorner)
						return nMaxCorner;
				}
			}
		}
	}

	return numCorner;
}


+ Recent posts