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



※ 모폴로지 처리

영상의 밝은 영역이나 어두운 영역을 축소, 확대하는 기법


※ 이진 영상의 모폴로지 처리

여러 가지 영상 이진화 기법을 사용했을 때 때로는 결과 영상에서 흰색 영역이나 검은색 영역이 원하는 의도보다 넓거나 좁게 얻어질 수 있다. 최적의 문턱값을 사용하면서 잘못된 분리 영역은 후처리 과정을 통하여 결과를 수정하는 기법을 많이 사용하는 데 이를 모폴로지 처리라고 한다. 모폴로지 처리는 대상 영역이 좁아지는 침식 연산, 대상 영역이 넓어지는 팽창 연산, 대상 영역에서 세부 영역이 제거되는 열림 연산, 빈틈이 채워지는 닫힘 연산이 있다. 일반적으로 대상 영역은 이진 영상에서 흰색으로 표시된 영역을, 회색조 영상에서는 밝은 영역을 가리킨다. 침식과 팽창 연산은 기본적인 연산에 해당하며 열림과 닫힘 연산은 침식과 팽창 연산의 조합으로 구성된다.


※ 이진 영상의 침식과 팽창 연산

1. 이진 영상의 침식 연산

침식은 입력한 이진 영상의 각 픽셀에 마스크를 놓았을 때 마스크가 255값을 가지는 모든 픽셀 위치에 대하여 입력 영상도 255 값을 가져야만 결과값이 255가 되는 연산이다. 만약 대상 위치에서 한 픽셀이라도 0 값을 가지면 결과값은 0이 되기 때문에 전체적으로 255값을 가지는 영역이 줄어드는 결과가 나타난다.



다음은 침식 연산을 수행하는 메서드이다.

void Erode(const CByteImage& imageIn, const CByteImage& mask, CByteImage& imageOut)
{
	// 입력 영상 및 마스크 영상의 크기 정보
	int nWidth  = imageIn.GetWidth();
	int nHeight = imageIn.GetHeight();
	int nMaskW = mask.GetWidth();
	int nMaskH = mask.GetHeight();
	int hW = nMaskW/2;
	int hH = nMaskH/2;

	for (int r=hH ; r<nHeight-hH ; r++) // 입력 영상의 세로 방향
	{
		BYTE* pIn  = imageIn .GetPtr(r);
		BYTE* pOut = imageOut.GetPtr(r);

		for (int c=hW ; c<nWidth-hW ; c++) // 입력 영상의 가로 방향
		{
			if (!pIn[c]) // 값이 0인 입력 픽셀은 처리하지 않음
				continue;

			pOut[c] = 255;
			bool bEroded = false;
			for (int y=0 ; y<nMaskH && !bEroded ; y++) // 마스크의 세로 방향
			{
				BYTE* pMask = mask.GetPtr(y);
				BYTE* pInM = imageIn.GetPtr(r-hH+y, c-hW); // 입력 영상에서 마스크 내의 각 행

				for (int x=0 ; x<nMaskW && !bEroded ; x++) // 마스크의 가로 방향
				{
					if (pMask[x]) // 마스크 값이 0이 아니면 입력 픽셀을 검사
					{
						if (!pInM[x]) // 한 픽셀이라도 0 값을 가지면
						{
							pOut[c] = 0;	// 결과 값은 0으로
							bEroded = true;	// 더 이상의 검사를 중단
						}
					}
				}
			}
		}
	}
}


2. 이진 영상의 팽창 연산

팽창 연산은 침식 연산과 반대로 마스크의 유효 영역에 이쓴 픽셀들을 모두 밝게 만드는 역할을 한다.



다음은 팽창 연산을 수행하는 메서드이다.

void Dilate(const CByteImage& imageIn, const CByteImage& mask, CByteImage& imageOut)
{
	// 입력 영상 및 마스크 영상의 크기 정보
	int nWidth  = imageIn.GetWidth();
	int nHeight = imageIn.GetHeight();
	int nMaskW = mask.GetWidth();
	int nMaskH = mask.GetHeight();
	int hW = nMaskW/2;
	int hH = nMaskH/2;

	for (int r=hH ; r<nHeight-hH ; r++) // 입력 영상의 세로 방향
	{
		BYTE* pIn  = imageIn .GetPtr(r);

		for (int c=hW ; c<nWidth-hW ; c++) // 입력 영상의 가로 방향
		{
			if (!pIn[c]) // 값이 0인 입력 픽셀은 처리하지 않음
				continue;

			for (int y=0 ; y<nMaskH ; y++) // 마스크의 세로 방향
			{
				BYTE* pMask = mask.GetPtr(y);
				BYTE* pOut = imageOut.GetPtr(r-hH+y, c-hW); // 입력 영상에서 마스크 내의 각 행

				for (int x=0 ; x<nMaskW ; x++) // 마스크의 가로 방향
				{
					if (pMask[x]) // 마스크 값이 유효하면
					{
						pOut[x] = 255; // 결과 값을 255로
					}
				}
			}
		}
	}
}



※ 이진 영상의 열림과 닫힘 연산

열림과 닫힘 연산은 침식과 팽창 연산을 결합한 형태로 구현할 수 있다. 열림 연산은 밝은 영역에 나타난 미세한 조각을 제거할 수 있도록 하는 연산이다. 먼저 침식 연산을 수행하여 밝은 영역을 전체적으로 축소한 다음 팽창 연산을 뒤이어 수행하여 전체적인 넓이를 원래대로 복구한다. 이러한 원리로 미세한 조각을 제거한다.

닫힘 연산은 밝은 영역에 생긴 미세한 틈을 메우는 역할을 한다. 먼저 팽창 연산을 수행하여 밝은 영역을 넓히고 다시 침식 연산을 수행한다. 틈새에 해당하는 영역은 팽창 연산을 통해 메워진다. 열림과 닫힘 연산을 구현하는 방법은 단순히 위에서 구현한 침식과 팽창연산을 순차적으로 수행하면 된다.



※ 회색조 영상의 모폴로지 처리

회색조 영상의 모폴로지 처리는 로직상 이진 영상의 모폴로지 처리와 똑같다. 다만 대상 영역이 이진 영상 모폴로지에서는 하얀색 영역이였다면 회색조 영상에서는 대상 영역이 밝은 영역이 된다. 


1. 회색조 영상의 침식 연산

이진 영상의 침식 연산에서 마스크는 입력 영상의 흰 부분, 즉 픽셀 값이 255인 영역이 마스크를 완전히 포함해야만 마스크가 놓인 위치의 결과 픽셀 값이 255가 되고 그렇지 않으면 결과 픽셀 값이 0이 되었다. 이를 다르게 말하면 침식 연산의 결과값은 마스크가 걸친 영역에서 입력 영상 픽셀의 최솟값으로 주어지는 것이라 할 수 있다. 즉 ,입력 영상이 마스크 내 영역에서 모두 255인 값을 가지면 결과 픽셀 값도 255가 되고 하나의 픽셀이라도 마스크 내에서 값이 0이면 결과 픽셀 값이 0이 되는 것이다. 이 개념을 그대로 회색조 영상에 일반화하면 회색조 영상에서 모폴로지 침식 연산이란 마스크 영역 내 입력 영상 픽셀의 최솟값을 구하는 것이라 할 수 있다. 회색조 영상의 침식 연산에서 마스크의 크기가 최솟값을 구하는 영역을 결정한다면 마스크에 담기는 값들은 최솟값을 구하기 전에 입력 영상에서 뺄 값을 나타낸다. 즉, 마스크의 모든 원소가 1이라면 침식 연산의 결과값은 입력 영상의 마스크가 걸친 영역에서 각각 1을 뺀 값들 가운데 최솟값이 된다. 따라서 회색조 영상의 침식 연산은 마스크의 원소 값을 조절함으로써 밝은 영역을 축소할 뿐 아니라, 전체적인 밝기도 조절 가능하다. 다음은 회색조 영상의 침식 연산을 구현한 메서드이다.

void ErodeG(const CByteImage& imageIn, const CByteImage& mask, CByteImage& imageOut)
{
	// 입력 영상 및 마스크 영상의 크기 정보
	int nWidth  = imageIn.GetWidth();
	int nHeight = imageIn.GetHeight();
	int nMaskW = mask.GetWidth();
	int nMaskH = mask.GetHeight();
	int hW = nMaskW/2;
	int hH = nMaskH/2;

	for (int r=hH ; r<nHeight-hH ; r++) // 입력 영상의 세로 방향
	{
		BYTE* pIn  = imageIn .GetPtr(r);
		BYTE* pOut = imageOut.GetPtr(r);

		for (int c=hW ; c<nWidth-hW ; c++) // 입력 영상의 가로 방향
		{
			int minVal = 255;
			for (int y=0 ; y<nMaskH ; y++) // 마스크의 세로 방향
			{
				BYTE* pMask = mask.GetPtr(y);
				BYTE* pInM = imageIn.GetPtr(r-hH+y, c-hW); // 입력 영상에서 마스크 내의 각 행

				for (int x=0 ; x<nMaskW ; x++) // 마스크의 가로 방향
				{
					int diff = pInM[x] - pMask[x];
					if (diff < minVal) // 최소값 탐색
					{
						minVal = diff;
					}
				}
			}

			pOut[c] = CLIP(minVal); // 결과 값이 [0, 255] 구간에 오도록 조절
		}
	}
}


2. 회색조 영상의 팽창 연산

회색조 영상의 팽창 연산은 침식 연산과 반대로 마스크 영역 내 입력 영상 픽셀의 최댓값을 구하는 것이다. 입력 영상에서 마스크가 걸친 영역 가운데 가장 밝은 픽셀 값이 결과 픽셀로 저장되기 때문에 결과 영상은 밝은 영역이 확대되고 어두운 영역이 축소된다. 팽창 연산도 침식 연산과 마찬가지로 마스크에 값을 지정할 수 있는 데 팽창 연산에서는 마스크 원소 값을 입력 영상의 각 픽셀에 더하고 나서 더한 값의 최댓값이 결과 픽셀 값이 된다. 다음은 회색조 영상의 팽창 연산을 구현한 메서드이다.

void DilateG(const CByteImage& imageIn, const CByteImage& mask, CByteImage& imageOut)
{
	// 입력 영상 및 마스크 영상의 크기 정보
	int nWidth  = imageIn.GetWidth();
	int nHeight = imageIn.GetHeight();
	int nMaskW = mask.GetWidth();
	int nMaskH = mask.GetHeight();
	int hW = nMaskW/2;
	int hH = nMaskH/2;

	for (int r=hH ; r<nHeight-hH ; r++) // 입력 영상의 세로 방향
	{
		BYTE* pIn  = imageIn .GetPtr(r);
		BYTE* pOut = imageOut.GetPtr(r);

		for (int c=hW ; c<nWidth-hW ; c++) // 입력 영상의 가로 방향
		{
			int maxVal = 0;
			for (int y=0 ; y<nMaskH ; y++) // 마스크의 세로 방향
			{
				BYTE* pMask = mask.GetPtr(y);
				BYTE* pInM = imageIn.GetPtr(r-hH+y, c-hW); // 입력 영상에서 마스크 내의 각 행

				for (int x=0 ; x<nMaskW ; x++) // 마스크의 가로 방향
				{
					int sum = pInM[x] + pMask[x];
					if (sum > maxVal) // 최대값 탐색 
					{
						maxVal = sum;
					}
				}
			}

			pOut[c] = CLIP(maxVal); // 결과 값이 [0, 255] 구간에 오도록 조절
		}
	}
}



3. 회색조 영상의 열림과 닫힘 연산

회색조 영상의 열림과 닫힘 연산 또한 이진 영상의 열림과 닫힘 연산과 똑같다. 회색조 영상의 침식과 팽창 연산을 순차적으로 수행하면 된다. 회색조 영상에서 열림과 닫힘 연산을 연속해서 모두 적용하면 밝고 어두운 잡음 모두가 제거된다. 단, 열림과 닫힘 연산을 적용하는 순서에 따라 결과 영상이 달라진다. 잡음을 효과적으로 제거하려면 모폴로지 연산의 적용 순서와 마스크 설정에 주의해야 한다.

+ Recent posts