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



※ 배경 분리를 이용한 영상 분할

앞 포스트에서 배운 이진화를 통한 영상 분할은 밝기값을 기준으로 이진화를 수행한다. 영상이 복잡해 영상의 배경과 물체의 밝기값이 비슷할 시 밝기값을 기준으로 한 이진화는 문턱값을 적절히 조절하더라도 제대로 된 영상 분할 결과를 얻지 못한다. 이렇게 복잡한 영상에 대한 영상 분할 알고리즘 중 가장 쉽게 구현하고 실제 시스템에 적용할 수 있는 배경 분리를 이용한 영상 분할 기법에 대해서 알아보자.


배경 분리를 이용한 영상 분할은 감시 시스템이나 불량 검출 시스템과 같이 배경 영상을 미리 촬영해둘 수 있는 스템에서 매우 유용하다. 예를 들어, 감시 영역을 사람이 없는 상태에서 촬영하고 이 영상을 사람이 있을 때 촬영한 영상과 비교하면 사람에 해당하는 영역을 분리할 수 있다. 단, 배경 영상과 검사 영상은 카메라의 위치가 동일해야 하며 밝기 등의 변화가 있으면 보정해야 한다.


이러한 배경 분리를 이용한 영상 분할은 입력 영상과 배경 영상의 빼서 얻은 결과에 문턱값을 적용하여 영상 이진화를 수행해 구현한다. 회색조 영상의 경우 두 영상 밝기값 차의 절대값에 문턱값을 적용하여 이진화를 수행한다. 컬러 영상은 각 채널 밝기 값의 차에 제곱한 값을 문턱값의 제곱값을 적용하여 이진화를 수행한다. 다음은 이에 관한 코드이다.


void BinarizationBG(const CByteImage& imageIn, const CByteImage& imageBG, 
						  CByteImage& imageOut, int nThreshold)
{
	ASSERT(imageIn.GetChannel()==1); // 회색조 영상 확인

	int nWidth  = imageIn.GetWidth();
	int nHeight = imageIn.GetHeight();

	for (int r=0 ; r<nHeight ; r++)
	{
		BYTE* pIn	= imageIn.GetPtr(r);
		BYTE* pBG	= imageBG.GetPtr(r);
		BYTE* pOut	= imageOut.GetPtr(r);

		for (int c=0 ; c<nWidth ; c++)
		{
			if (abs(pIn[c]-pBG[c]) > nThreshold)
				pOut[c] = 255;
			else
				pOut[c] = 0;
		}
	}
}

void BinarizationBGCol(const CByteImage& imageIn, const CByteImage& imageBG, 
							 CByteImage& imageOut, int nThreshold)
{
	ASSERT(imageIn.GetChannel()==3); // 컬러 영상 확인

	nThreshold *= nThreshold; // 문턱값을 제곱

	int nWidth  = imageIn.GetWidth();
	int nHeight = imageIn.GetHeight();

	int dB, dG, dR, dd;
	for (int r=0 ; r<nHeight ; r++)
	{
		BYTE* pIn	= imageIn.GetPtr(r);
		BYTE* pBG	= imageBG.GetPtr(r);
		BYTE* pOut	= imageOut.GetPtr(r);

		int pos = 0;
		for (int c=0 ; c<nWidth ; c++)
		{
			dB = pIn[pos]-pBG[pos]; pos++;
			dG = pIn[pos]-pBG[pos]; pos++;
			dR = pIn[pos]-pBG[pos]; pos++;
			dd = dB*dB + dG*dG + dR*dR;
			if (dd > nThreshold) // 제곱끼리 비교
				pOut[c] = 255;
			else
				pOut[c] = 0;
		}
	}
}

위 메서드들은 문턱값을 매개변수로 해서 해당 문턱값을 바로 적용했는 데 바로 전 포스트에서 배운 자동 문턱값 조절 코드를 넣어서 구현을 해도 좋다. 다음은 그림은 위 메서드 실행 결과이다.




배경 분리를 이용한 위 그림을 보면 분할된 손에 조그만 검정색 표시가 아직 있다. 위 그림에서 배경 영상과 손 사이에 비슷한 색이 없는 것처럼 보여도 실제 영상의 잡음으로 두 영상에서 같은 위치의 픽셀이 문턱값 이하의 차이를 가질 수 있다. 그래서 앞에서 배운 가우스 필터를 이용해 잡음을 제거 해주고 배경 분리를 이용해 영상 분할을 한다면 다음과 같이 더 깔끔한 분할 결과가 나온다.



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



※ 영상 분할

영상을 주어진 기준에 따라 몇 개의 영역으로 나누는 것을 말한다. 영상 분할은 물체 인식, 제스처 인식, 검사 장비에서 이상 영역 검출, 사물/사람 인식 등에 널리 쓰인다. 여기서는 기초적인 영상 분할 기법인 픽셀 값 기반의 영상 분할 기법을 알아볼 것이다.



※ 이진화를 이용한 영상 분할

이진화는 회색조 영상에서 특정 밝기값을 기준으로 더 밝은 영역과 어두운 영역을 나누는 것이다. 단순히 if문을 사용하여 각 픽셀 값을 기준 값과 비교한다. 밑에 있는 코드가 이진화를 이용한 영상 분할 코드다.

void Binarization(const CByteImage& imageIn, CByteImage& imageOut, int nThreshold) { ASSERT(imageIn.GetChannel()==1); int nWidth = imageIn.GetWidth(); int nHeight = imageIn.GetHeight(); for (int r=0 ; r<nHeight ; r++) { BYTE* pIn = imageIn.GetPtr(r); BYTE* pOut = imageOut.GetPtr(r); for (int c=0 ; c<nWidth ; c++) { if (pIn[c] > nThreshold) pOut[c] = 255; else pOut[c] = 0; } } }

영상의 각 픽셀 영역을 돌면서 if문을 사용해 nThreshold 즉, 문턱값을 기준으로 픽셀값을 비교한다. 단 위 메서드는 회색조 영상에서만 이진화를 수행할 수 있기 때문에 컬러 영상의 경우 회색조 영상으로 변환해야 한다. 아래 그림이 결과 영상이다. 결과 영상을 보면 문턱값에 따라서 영상의 분할 정도가 다르다. 따라서 적절한 문턱값으로 설정해야 한다. 다음은 적절한 문턱값을 찾기 위한 방법을 알아보자.


 



※ 이진화를 수행하는 문턱값 자동 조절하기

회색조 영상에서 최적의 문턱값을 찾는 방법은 영상의 밝기값 분포를 이용하는 것이다.


위 그림은 손과 배경화면에 해당하는 히스토그램이다. 위 히스토그램에서 초록색 화살표가 가리키는 밝기값 즉, 히스토그램의 분포가 드문 밝기값을 기준으로 이진화를 수행한다면 손과 배경화면이 잘 분할될 것이라 예상할 수 있다. 초록색 화살표가 가리키는 밝기값을 구하려면 두 영역의 평균 밝기의 중간값을 구하면 된다. 각 영역의 평균 밝기는 (영역 내 픽셀의 밝기값 합) / (영역 내 픽셀 수) 이다. 두 영역의 평균 밝기의 중간값을 구하면 그 중간값을 가지고 이진화를 수행한다. 그러면 다시 두 영역이 나오게 되고 두 영역의 평균 밝기 중간값이 새롭게 나온다. 구한 중간값을 가지고 또 이진화를 수행한다. 이진화는 중간값을 기준으로 픽셀의 변화가 없을 때까지 수행한다. 다음은 이에 관한 순서도다.


위 순서도를 구현한 코드는 다음과 같다.


int BinarizationAuto(const CByteImage& imageIn, CByteImage& imageOut, int nThreshold)
{
	ASSERT(imageIn.GetChannel()==1);

	int nWidth  = imageIn.GetWidth();
	int nHeight = imageIn.GetHeight();

	unsigned int nSumHi, nSumLo; // 두 영역 밝기의 합
	unsigned int nNumHi, nNumLo; // 두 영역의 픽셀 수
	bool bChanged = true;

	while (bChanged)
	{
		bChanged = false;
		nSumHi = nSumLo = 0; // 합 초기화
		nNumHi = nNumLo = 0; // 개수 초기화

		for (int r=0 ; r<nHeight ; r++)
		{
			BYTE *pIn  = imageIn.GetPtr(r);
			BYTE *pOut = imageOut.GetPtr(r);

			for (int c=0 ; c<nWidth ; c++)
			{
				if (pIn[c] > nThreshold)
				{
					nSumHi += pIn[c]; // 밝기 합 더하기
					nNumHi++;		  // 픽셀 수 증가	
					if (!pOut[c]) // 이전에 for문에서 문턱값 이하
						bChanged = true; // 분할에 변화 있음
					pOut[c] = 255;
				}
				else
				{
					nSumLo += pIn[c]; // 밝기 합 더하기
					nNumLo++;		  // 픽셀 수 증가
					if (pOut[c])  // 이전에 for문에서 문턱값 초과
						bChanged = true; // 분할에 변화 있음
					pOut[c] = 0;
				}
			}
		}

		if (!nNumHi) //nNumHi가 0일 경우를 대비
			nThreshold = nSumLo/(double)nNumLo;
		else if (!nNumLo)
			nThreshold = nSumHi/(double)nNumHi;
		else
			nThreshold = (nSumHi/(double)nNumHi + nSumLo/(double)nNumLo) / 2;
	} // while (bChanged)

	return nThreshold; // 최종 문턱값
}

위 메서드는 매개 변수로 주는 초기 문턱값과 상관없이 항상 같은 최종 문턱값을 얻는다. 단, 초기 문턱값은 자동 조절 기능이 수렴하는 시간에 영향을 줄 수 있다.

+ Recent posts