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



※ 컬러 영상 분할

회색조 영상 분할은 쉽게 구현할 수 있다. 컬러 영상은 앞 포스트에서 배운 배경 분리를 이용한 분할을 사용해도 되지만 배경이 없다면 영상 분할이 어렵다. 컬러 영상을 회색조 영상으로 변환 후 회색조 영상 분할을 할 수 도 있다 .하지만 다음 그림과 같이 제대로 분할이 안 된다.

따라서, 컬러 영상을 쉽게 분할하기 위해서 K-평균 군집화 알고리즘을 사용해야 한다. 회색조 영상의 경우 밝기를 1차원 공간에 점으로 표현이 가능해 값을 기준으로 대소 비교를 해 분할이 가능하다. 컬러 영상의 경우 3차원 공간상에 표현 되기 때문에  점이 아닌 면을 기준으로 분할해야 한다. 이 때 기준에 해당하는 면의 정의가 중요한데 K-평균 군집화 알고리즘을 사용하면 쉽게 면의 정의가 가능하다. 

K-평균 군집화 알고리즘은 고차원 공간에서 데이터를 분할하는 데 유용한 방법이다. 이미 분류된 데이터들에 대한 평균 위치로부터의 거리를 기준으로 새로운 분할을 수행하는 기법이다. "K"는 분류된 영역의 개수를 나타내는 데 K-평균 균집화는 K개의 평균값을 기준으로 데이터를 군집화한다는 의미가 된다. 

먼저 3차원 색 공간에 K-평균 군집화을 이용하기 위해 기준이 되는 영역 "K" 여러 개를 임의로 정의한다. 영역 "K"는 RGB 색공간 내 (R, G, B) 값이다. 영상 내  영상의 모든 픽셀에 대해서 여러 개의 K 영역 중에 어느 곳에 속하는 지 분류한다. 분류 방법은 기준이 되는 (R,G,B)까지의 거리를 계산해 가장 가까운 곳에 분류한다. 분류 후 K개로 군집화된 픽셀 rgb 값들의 평균을 구하고 그 평균을 기준의 되는 영역 "K"로 설정한다. 그리고 다시 모든 픽셀을 K개의 영역으로 분류한다. 이 과정을 계속 반복하다가 새로은 K 영역에서 분류된 픽셀들이 분류되기 전과 변화가 없다면 제대로 K개의 영역으로 분류가 되 영상 분할이 완료된다. 이 과정은 이전 포스트에서 배웠던 이진화를 이용한 영상 분할에서 자동으로 문턱값을 조절하던 순서도와 비슷하다. 다음은 컬러 영상 분할을 위한 순서도다.  


다음은 이를 구현한 소스코드이다.

void KMeansSegmentation(const CByteImage& imageIn, CByteImage& imageOut, int nCluster, BYTE meanR[], BYTE meanG[], BYTE meanB[])
{
	int nWidth  = imageIn.GetWidth();
	int nHeight = imageIn.GetHeight();

	CByteImage imageK = CByteImage(nWidth, nHeight);

	bool bChanged = true;

	int cR, cG, cB, dR, dG, dB, dd, minD;
	int newK;

	unsigned int nSumR[MAX_CLUSTER]; // 각 영역 R 채널 합
	unsigned int nSumG[MAX_CLUSTER]; // 각 영역 G 채널 합
	unsigned int nSumB[MAX_CLUSTER]; // 각 영역 B 채널 합
	unsigned int nNumP[MAX_CLUSTER]; // 각 영역 픽셀 수

	while (bChanged)
	{
		bChanged = false;

		memset(nSumB, 0, sizeof(int)*nCluster); // B채널 합 초기화
		memset(nSumG, 0, sizeof(int)*nCluster); // G채널 합 초기화
		memset(nSumR, 0, sizeof(int)*nCluster); // R채널 합 초기화
		memset(nNumP,  0, sizeof(int)*nCluster); // 픽셀 수 초기화
		
		for (int r=0 ; r<nHeight ; r++)
		{
			BYTE *pIn	= imageIn.GetPtr(r);
			BYTE *pK	= imageK.GetPtr(r);

			int pos = 0;
			for (int c=0 ; c<nWidth ; c++)
			{
				// 현재 위치의 픽셀 값
				cB = pIn[pos++];
				cG = pIn[pos++];
				cR = pIn[pos++];

				// 가장 가까운 분류 탐색 (m_imageK 갱신)
				minD = INT_MAX;
				newK = 0;
				for (int k=0 ; k<nCluster ; k++)
				{
					dB = cB - meanB[k];
					dG = cG - meanG[k];
					dR = cR - meanR[k];
					dd = dB*dB + dG*dG + dR*dR;

					if (dd < minD)
					{
						minD = dd;
						newK = k;
					}
				}

				if (newK != pK[c])
					bChanged = true;

				pK[c] = newK; // 가장 가까운 값으로 변경

				// 평균을 구하기 위한 합 계산
				nSumB[newK] += cB;
				nSumG[newK] += cG;
				nSumR[newK] += cR;
				nNumP[newK]++;
			}
		}

		// 새로운 평균 계산 (m_meanC 갱신)
		for (int k=0 ; k<nCluster ; k++)
		{
			if (nNumP[k])
			{
				meanB[k] = nSumB[k] / (double)nNumP[k];
				meanG[k] = nSumG[k] / (double)nNumP[k];
				meanR[k] = nSumR[k] / (double)nNumP[k];
			}
			else
			{
				meanB[k] = 0;
				meanG[k] = 0;
				meanR[k] = 0;
			}
		}
	} // while (bChanged)

	// 결과 영상 생성
	int curK;
	for (int r=0 ; r<nHeight ; r++)
	{
		BYTE* pOut	= imageOut.GetPtr(r);
		BYTE* pK	= imageK.GetPtr(r);

		int pos = 0;
		for (int c=0 ; c<nWidth ; c++)
		{
			curK = pK[c];
			pOut[pos++] = meanB[curK];
			pOut[pos++] = meanG[curK];
			pOut[pos++] = meanR[curK];
		}
	}
}

매개 변수 meanR[], meanG[], meanB[] 가 기준이 되는 "K" 이다. 예를 들어 meanR[2] = {0, 255}, meanG[2] = {0, 255}, meanB[2] = {0, 255} 라면 RGB(0.0.0), RGB(255,255,255) 를 초기 기준값으로 정하고 nCluster 값이 2가 된다. 각 영역 R,G,B의 채널 합 변수 nSumR/G/B/ 변수는 unsigned int 형으로 선언되었다. unsigned int 는 0xffffffff(4,294,967,295)까지 나타 낼 수 있어 최대 16,843,009(0xffffffff / 255) 개 픽셀의 채널 값 합을 구할 수 있다. 이는 가로 4000 * 3000 픽셀 크기 영상보다 큰 크기이다. 만약 영상의 크기가 더 크다면 unsigned long long int 형으로 선언해도 된다.

위 메서드는 초기 평균값 설정이 수행 속도에 영향을 많이 미친다. 초기 평균값이 실제 분할된 각 영역의 평균값에 최대한 가까울수록 좋다. 그러나 각 영역의 평균값은 알 수 없으므로 각 영역에서 임의의 픽셀을 선택하여 선택된 픽셀의 색상 값을 초기 평균값으로 사용하면 좋은 결과를 얻을 수 있다.

+ Recent posts