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



※ 영상 잡음

영상 잡음(노이즈)는 디지털 영상을 획득하는 과정에서 픽셀 값이 원본 대상과는 다른 엉뚱한 값이 들어가는 것을 말한다. 이는 나중에 다룰 영상 정합과 같은 알고리즘에서 성능을 저하하는 주요 원인이므로 영상 잡음에 대한 처리가 필요하다.


- 영상 잡음의 종류

1. 양자화 잡음 : 원래 자연 상태의 픽셀값은 실수 범위를 가지지만 디지털 영상으로 변환하면서 소수분이 반올림되어 0에서 255사이의 정수 값으로 양자화된다. 이 과정에서 +(-)0.5이내의 오차가 발생하게 되고 이를 양자화 잡음이라고 한다. 양자화 잡음은 원래 밝기값과 차이가 거의 안 나므로 특별한 경우가 아니라면 잡음 처리를 하지 않는다.


2. 가우스 잡음 : 가우스 잡음은 잡음의 확률 분포가 가우스 분포를 따르는 잡음이다. 주로 카메라 센서에 있는 소자를 통하여 신호를 증폭시키는 과정에서 발생하고 양자화 잡음과 달리 잡음의 크기가 클 수 있어 화질에 많은 영향을 미칠 수 있다. 또한 영상의 지글거림도 가우스 잡음의 영향이 크다. 가우스 잡음을 감소시키는 방법으로는 평균값 필터와 가우스 필터가 있는 데 차차 배울 정이다.




3. 소금과 후추 잡음 : 영상에 마치 소금과 후추를 뿌려놓은 것처럼 영상의 중간 중간에 검거나 흰 픽셀이 나타나는 잡음이다. 주로 전기 신호를 아날로그에서 디지털로 변환하는 과정이나 신호를 전송하는 과정에서 발생한다. 이를 해결하는 방법으로는 중간값 필터가 있는데 차차 배울 예정이다.







※ 평균값 필터로 잡음 감소하기

평균값 필터란 대상 픽셀에 대하여 이웃한 픽셀 값들과 평균을 계산한 값을 다시 넣어주는 작업이다. 필터는 주파수 공간 처리에서 특정 주파수 성분을 걸러낸다는 의미인데 이에 대해서는 나중에 배우기로 하고 필터와 같은 효과를 낼 수 있는 마스크를 통한 회선 연산 기법으로 필터 효과를 낼 것이다. 마스크란 영상 일부분에 연속해 있는 숫자 배열을 가리킨다. 밑에 그림을 통해서 보자. 



위 그림은 마스크의 크기를 3으로 한 예로 연속한 숫자 3개의 평균값을 결과값으로 저장한다. 그래서 8이 마스크 회선 연산 기법을 통해 6.3으로 저장된 것을 볼 수 있다. 평균값 필터는 이러한 마스크 회선 연산을 영상의 모든 픽셀에 적용하는 것이다. 평균값 필터를 영상에 적용하려면 영상은 2차원 데이터이기 때문에 아래와 같은 2차원 마스크를 사용해야 한다. 이 때 마스크의 가로, 세로 크기는 홀수로 주어지며 모든 원소의 합은 항상 1이 되어야 한다. 

마스크의 크기가 클수록 더 심한 잡음을 감소시킬 수 있지만, 역효과로 영상이 갈수록 뿌예지는 부작용도 생긴다.



void MeanFiltering()
{
	int nWidth  = m_imageIn.GetWidth();
	int nHeight = m_imageIn.GetHeight();
	int nChnnl  = m_imageIn.GetChannel();
	int nWStep  = m_imageIn.GetWStep();
	int nHalf   = maskSize / 2; 

	BYTE* pIn  = m_imageIn.GetPtr();
	BYTE* pOut = m_imageOut.GetPtr();

	int r, c, l;
	for (r=0 ; r<nHeight ; r++) // 행 이동
	{
	for (c=0 ; c<nWidth ; c++) // 열 이동
	{
	for (l=0 ; l<nChnnl ; l++) // 채널 이동
	{
		int nSum = 0;  // 픽셀 값의 합
		int nCnt  = 0; // 픽셀 수
		for (int y=-nHalf ; y<=nHalf ; y++)
		{
		for (int x=-nHalf ; x<=nHalf ; x++)
		{
			int px = c+x;
			int py = r+y;

			if (px>=0 && px<nWidth && py>=0 && py<nHeight) //영상 범위 유무
			{
				nSum += pIn[nWStep*py + nChnnl*px + l];
				nCnt++;
			}
		}
		}
		pOut[nWStep*r + nChnnl*c + l] = (BYTE)(nSum / (double)nCnt);
	} // 채널 이동 끝
	} // 열 이동 끝
	} // 행 이동 끝

	ShowImage(m_imageOut, "Image");
}

위 메서드는 입력 영상에 대해서 마스크 회선 연산 기법을 통한 평균값 필터를 적용하는 기능을 구현했다. 마스크 크기에 따라 for문을 통해 자기 주변의 픽셀 데이터 값을 더해서 평균값을 내는 것을 볼 수 있다. 이 평균값 필터를 실제 영상에 적용하면 잡음이 제거되는 효과를 얻을 수 있으나 마스크 크기를 크게 할수록 잡음 제거보다는 영상이 흐려지는 효과가 두드려진다. 이러한 부작용 때문에 더 좋은 성능을 보이는 가우스 필터를 통해서 잡음 감소를 수행한다.



※ 가우스 필터로 잡음 감소하기

가우스 필터는 가우스 분포 형태를 가진 마스크를 이용하여 회선 연산을 하는 필터이다. 평균값 필터의 경우 마스크의 중심으로 가중치가 없어서 영상이 흐려지는 부작용이 있지만 가우스 필터는 마스크의 중심에서부터 멀어질수록 가우스 분포 형태의 가중치를 가져 가중치가 줄어드는 형태여서 원래의 신호를 잘 유지할 수 있다.






위에서와 같이 2차원 가우스 마스크는 오른쪽의 가우스 함수에 의해서 위와 같이 표현된다. 실제 가우스 함수는 무한대의 영역을 가지고 있으나 0에 가까운 값들은 잘라내고 의미 있는 영역의 확률값만 가우스 마스크로 나타낸다. 또한 전체 확률 분포의 합은 1이여야 하기 때문에 2차원 가우스 마스크 값들의 합이 1이 되어야 한다. 그래서 실제 프로그램을 구현할 때는 우리가 평균값 필터에서 픽셀 값의 합을 마스크 원소의 개수로 나누었듯이, 가우스 필터에서도 픽셀 값과 마스크 원소를 곱하여 더한 값을 더한 마스크 총 합으로 나눈 방식으로 전체 원소 합이 1이 되도록 만들 것이다. 가우스 필터를 적용할 때는 각 픽셀 데이터에 2차원 가우스 마스크 값들을 곱해야 한다. 그러기 위해선 2차원 행렬을 만들어야 하는데 이는 프로그래밍 상으로 구현하기 복잡하다. 따라서 2차원 가우스 마스크가 대칭행렬인 것을 이용한다. 아래의 그림과 같이 대칭 행렬은 1행의 행렬에 같은 원소를 가지는 1열의 행렬을 곱하면 된다. 이를 이용해 입력 영상에 1행의 행렬과 연산 후 1열을 행렬과 연산할 것이다.



void GaussianFiltering()
{
	int nWidth  = m_imageIn.GetWidth();
	int nHeight = m_imageIn.GetHeight();
	int nChnnl  = m_imageIn.GetChannel();
	int nWStep  = m_imageIn.GetWStep();
	BYTE* pIn  = m_imageIn.GetPtr();
	BYTE* pOut = m_imageOut.GetPtr();

	// 1차원 가우스 마스크 생성
	int nHalf = max((m_dGaussian*6-1) / 2, 1);
	int nMeanSize = nHalf*2+1;
	for (int n=0 ; n<=nHalf ; n++)
	{
		m_bufGss[nHalf-n] = m_bufGss[nHalf+n]
				= exp(-n*n/(2*m_dGaussian*m_dGaussian));
	}

	int r, c, l;
	CDoubleImage tmpConv(nWidth, nHeight, nChnnl);
	double* pTmp  = tmpConv.GetPtr();

	// 가로 방향 회선
	for (r=0 ; r<nHeight ; r++) // 행 이동
	{
	for (c=0 ; c<nWidth ; c++) // 열 이동
	{
	for (l=0 ; l<nChnnl ; l++) // 채널 이동
	{
		double dSum = 0; // (마스크*픽셀) 값의 합
		double dGss = 0; // 마스크 값의 합
		for (int n=-nHalf ; n<=nHalf ; n++)
		{
			int px = c+n;

			if (px>=0 && px<nWidth)
			{
				dSum += (pIn[nWStep*r + nChnnl*px + l]*m_bufGss[nHalf+n]);
				dGss += m_bufGss[nHalf+n];
			}
		}
		pTmp[nWStep*r + nChnnl*c + l] = dSum / dGss;
	} // 채널 이동 끝
	} // 열 이동 끝
	} // 행 이동 끝

	// 세로 방향 회선
	for (r=0 ; r<nHeight ; r++) // 행 이동
	{
	for (c=0 ; c<nWidth ; c++) // 열 이동
	{
	for (l=0 ; l<nChnnl ; l++) // 채널 이동
	{
		double dSum = 0; // 픽셀 값의 합
		double dGss = 0; // 마스크 값의 합
		for (int n=-nHalf ; n<=nHalf ; n++)
		{
			int py = r+n;

			if (py>=0 && py<nHeight)
			{
				int absN = abs(n);
				dSum += pTmp[nWStep*py + nChnnl*c + l]*m_bufGss[nHalf+n];
				dGss += m_bufGss[nHalf+n];
			}
		}
 		pOut[nWStep*r + nChnnl*c + l] = (BYTE)(dSum / dGss);
	} // 채널 이동 끝
	} // 열 이동 끝
	} // 행 이동 끝

	ShowImage(m_imageOut, "Image");
}




※ 중간값 필터로 잡음 감소

소금과 후추 잡음과 같이 픽셀의 값이 크게 변하는 잡음에 대해선 평균값/가우스 필터로는 잡음을 감소하기 어렵다. 소금과 후추 잡음의 경우 하나의 픽셀이 주변 픽셀보다 많이 차이 나기 때문에 이를 이용해 한 픽셀을 포함하는 일정한 범위에 있는 픽셀들의 값을 크기 순으로 정렬하여 그 중간값을 필터의 결과값으로 취하는 필터인 중간값 필터를 사용하여 잡음을 감소시킬 수 있다.  

int cmpInt(const void *arg1, const void *arg2)
{
	return (*(int*)arg1 - *(int*)arg2);
}

void CImageEnhancerDlg::_MedianFiltering()
{
	int nWidth  = m_imageIn.GetWidth();
	int nHeight = m_imageIn.GetHeight();
	int nChnnl  = m_imageIn.GetChannel();
	int nWStep  = m_imageIn.GetWStep();
	BYTE* pIn  = m_imageIn.GetPtr();
	BYTE* pOut = m_imageOut.GetPtr();

	int nHalf = m_nMedSize / 2;

	int r, c, l;
	for (r=0 ; r<nHeight ; r++) // 행 이동
	{
	for (c=0 ; c<nWidth ; c++) // 열 이동
	{
	for (l=0 ; l<nChnnl ; l++) // 채널 이동
	{
		int nCnt = 0; // 픽셀 수
		for (int y=-nHalf ; y<=nHalf ; y++)
		{
			for (int x=-nHalf ; x<=nHalf ; x++)
			{
				int px = c+x;
				int py = r+y;

				if (px>=0 && px<nWidth && py>=0 && py<nHeight)
				{
					m_bufMed[nCnt++] = pIn[nWStep*py + nChnnl*px + l];
				}
			}
		}
		qsort((void*)m_bufMed, nCnt, sizeof(int), cmpInt);
		pOut[nWStep*r + nChnnl*c + l] = m_bufMed[nCnt/2];
	} // 채널 이동 끝
	} // 열 이동 끝
	} // 행 이동 끝

	ShowImage(m_imageOut, "Image");
}

중간값 필터는 픽셀 데이터를 정렬한 후 중간 값을 결과값으로 입력하는 것이기에 먼저 정렬이 필요하다 그래서 위 메서드에서는 c에서 제공하는 퀵정렬을 사용하였다. 위 중간값 필터 메서드에서 퀵정렬을 한 후에 중간에 있는 값을 픽셀 데이터 결과값으로 하는 것을 볼 수 있다.



위에서 설명한 기본적인 잡음 제거 알고리즘들에 대해서는 OpenCV 라이브러리에 구현이 되어 있다. 나중에 이에 대해서 알아보기로 하자.

+ Recent posts