해당 포스트는 "열혈강의 영상처리 프로그래밍" 책의 내용을 요약한 것이다.
※ 컬러 영상 분할
회색조 영상 분할은 쉽게 구현할 수 있다. 컬러 영상은 앞 포스트에서 배운 배경 분리를 이용한 분할을 사용해도 되지만 배경이 없다면 영상 분할이 어렵다. 컬러 영상을 회색조 영상으로 변환 후 회색조 영상 분할을 할 수 도 있다 .하지만 다음 그림과 같이 제대로 분할이 안 된다.
따라서, 컬러 영상을 쉽게 분할하기 위해서 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 형으로 선언해도 된다.
위 메서드는 초기 평균값 설정이 수행 속도에 영향을 많이 미친다. 초기 평균값이 실제 분할된 각 영역의 평균값에 최대한 가까울수록 좋다. 그러나 각 영역의 평균값은 알 수 없으므로 각 영역에서 임의의 픽셀을 선택하여 선택된 픽셀의 색상 값을 초기 평균값으로 사용하면 좋은 결과를 얻을 수 있다.
'영상처리 프로그래밍' 카테고리의 다른 글
영상의 모폴로지(침식, 팽창, 닫힘, 열림) (0) | 2017.06.17 |
---|---|
주파수 영역 영상 처리와 푸리에 변환(1) (0) | 2017.06.16 |
배경 분리를 이용한 영상 분할 (0) | 2017.06.14 |
이진화를 이용한 영상 분할 (0) | 2017.06.14 |
HSV 색 공간을 이용한 히스토그램 평활화 (0) | 2017.06.13 |