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



※ 히스토그램

영상의 광학적 변환(1) 밝기와 명암 조절 포스트에서 눈에 잘 보이는 영상을 보여주기 위해 특정 상수를 곱하는 방법을 배웠다. 이 방법은 영상이 좋아지는 데 제한이 있다. 영상 안 모든 픽셀 값을 고르게 분포해 주지 못하기 때문이다. 히스토그램은 이를 보완해 모든 픽셀값을 대체로 고르게 사용하도록 개선하여 눈에 더 잘 보이는 영상을 만들어준다. 

왼쪽 그림은 픽셀 값이 고르게 분포하지 못한 영상이고 오른쪽은 고르게 분포한 영상이다. 왼쪽 그림은 픽셀이 고르게 분포하지 못하여 어두운 부분이 세밀하게 보이지 못하지만, 오른쪽 그림은 고르게 분포하여 어두운 부분이 세밀하게 보인다.

두 그래프는 위에 있는 영상에 대한 각각의 히스토그램이다. 히스토그램은 데이터의 분포를 막대 그래프 형태로 나타낸 것으로, x축은 0에서 255까지의 픽셀 값을 y축은 픽셀 수를 나타낸다. 왼쪽 히스토그램은 픽셀 수가 0에서 255까지 고르게 분포하지 않는다. 오른쪽은 픽셀 수가 왼쪽에 비해 고르게 분포한다. 누적 합은 오른쪽이 왼쪽에 비해 선형으로 일직선으로 되는 것을 알 수 있다.



※ 히스토그램 평활화를 이용하여 영상 명암 보정

위에서 살펴보았듯이 우리는 픽셀 값이 고르게 분포하지 않은 영상을 고르게 분포하도록 바꿔야한다. 즉, 히스토그램에서 중간 밝기 영역에 많은 픽셀을 할당하고 어둡고 밝은 밝기 영역은 조금 할당해야 한다. 이를 영상 처리에서 히스토그램 평활화 또는 균등화 작업이라고 한다.(컬러 영상의 히스토그램은 지식이 더 필요해 회색조 영상의 히스토그램에 대해서만 설명한다.)


- 히스토그램 평활화 작업

먼저, 히스토그램 평활화 작업을 하려면 히스토그램을 구해야 한다. 히스토그램은 각 밝기에 대한 픽셀 수를 나타낸다. 따라서 아래 코드와 같이 모든 픽셀을 돌면서 밝기를 배열의 인덱스로 하는 256 크기의 배열 안에 1을 더해 주면 각 밝기에 대한 픽셀수가 나온다.

memset(m_histogram, 0 , 256*sizeof(int));
int r, c;
for(r=0; r < nHeight; r++)
   for(c=0; c < nWidth ; c++)
       m_histogram[pIn[r*nWStep+c]]++;

그 다음 계산한 히스토그램을 가지고 히스토그램 누적 합을 계산한다.

m_histogramCdf[i] = m_histogram[0] + m_histogram[1] + ....... + m_histogram[i];

마지막으로 누적 합 값으로 픽셀 값을 변환한다. 히스토그램 평탄화는 픽셀 값이 0에서 255까지 고르게 분포하도록 하는 것이다. 위에서 본 그래프와 같이 픽셀값(밝기)가 증가할수록 누적합이 일정 비율로 증가해야 한다. 즉, 비례해야 한다.  그래서 다음과 같은 방법을 사용한다. 각 픽셀에 대한 누적합이 0에서 Height*Width까지의 범위를 가진다. 누적합에 255/(Height*Width)를 곱해 0에서 255까지의 범위를 가지게 만든다. 특정 위치(x,y)에 대한 픽셀 값(p)을 픽셀값의 누적합(z)으로 바꾼다. 그러면 픽셀 값(p)를 가지고 있는 영상 내 모든 위치들의 픽셀 값은 z로 바뀌고 해당 픽셀의 누적값은 그대로 z가 된다. 따라서 히스토그램이 y=x 그래프가 되 서로 비례하게 되고 히스토그램 평활화가 완료된다. 아래는 이에 관한 소스 코드이다. 

void CImageEnhancerDlg::_HistogramEqualization() { if (m_imageIn.GetChannel() != 1) { AfxMessageBox("회색조 영상을 입력하세요."); return; } int nWidth = m_imageIn.GetWidth(); int nHeight = m_imageIn.GetHeight(); memset(m_histogram, 0, 256*sizeof(int)); int r, c; for (r=0 ; r<nHeight ; r++) //히스토그램 계산 { BYTE* pIn = m_imageIn.GetPtr(r); for (c=0 ; c<nWidth ; c++) { m_histogram[pIn[c]]++; } } double dNormFactor = 255.0 / (nWidth * nHeight); for (int i=0 ; i<256 ; i++) //누적합이 0에서 255까지 범위를 갖도록 하기 위해 { m_histogramCdf[i] = m_histogram[i]*dNormFactor; } for (int i=1 ; i<256 ; i++)//누적합 도출 { m_histogramCdf[i] = m_histogramCdf[i-1] + m_histogramCdf[i]; } for (r=0 ; r<nHeight ; r++) { BYTE* pIn = m_imageIn.GetPtr(r); BYTE* pOut = m_imageOut.GetPtr(r); for (c=0 ; c<nWidth ; c++) { //픽셀 값을 누적합 값으로 변환 pOut[c] = (BYTE)(m_histogramCdf[pIn[c]]+0.5); } } ShowImage(m_imageOut, "히스토 그램 평활화 결과 영상"); }



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



※ 영상 잡음

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


- 영상 잡음의 종류

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