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

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