2013년 1월 20일 일요일

OpenCV #5-4 Example (워터쉐드를 이용한 영상 분할)

 - 워터쉐드 변환 : 영상을 빨리 분할해 동일한 영역으로 만들기 위해 사용하는 영상처리 알고리즘.
 - 영상이 위상적 입체감을 보여준다는 아이디어에 따름.
 - 동일한 영역은 상대적으로 급격한 에지로 기술된 평탄한 분지에 대응.
 - 즉, 워터쉐드 알고리즘의 원래 버전이 영상을 과도하게 분할해 여러 개의 작은 영역을 만든다.
 - 영상 분할에 대한 정의를 유도하는 미리 정의된 마커 집합을 사용.

 - 워터쉐드 분할은 cv::watershed 함수를 사용해 얻음.
 - 32비트 부호에 있는 정수 마커 영상으로, 레이블을 대표하는 넌제로 화소로 구성한다.
 - 영상 내의 확실히 특정 영역에 속하는 것으로 알려진 일부 화소를 마크한다.
 - 초기 레이블링에서 워터쉐드 알고리즘이 다른 화소가 속할 영역을 결정.
 - 예제에서는 그레이레벨 영상인 마커 영상을 생성한 후, 정수형 영상으로 변환.
 - 편의를 위해 이 단계를 WatershedSegmenter 클래스에 캡슐화.

  • Example

#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>

// 그레이레벨 영상인 마커 영상을 생성
// 정수형 영상으로 변환
class WatershedSegmenter {
private:
cv::Mat markers;

public:
void setMarkers(const cv::Mat& markerImage) {
markerImage.convertTo(markers,CV_32S);
// 정수형 영상 변환
}
cv::Mat process(const cv::Mat &image) {
cv::watershed(image,markers);
// 워터쉐드 적용
return markers;
 }

 cv::Mat getSegmentation() { // 영상 형태인 결과를 반환
cv::Mat tmp;
markers.convertTo(tmp,CV_8U);
// 255 이상인 레이블을 갖는 모든 분할은 255인 값으로 할당
return tmp;
 }

 cv::Mat getWatersheds() { // 영상 형태인 워터쉐드를 반환
cv::Mat tmp;
markers.convertTo(tmp,CV_8U,255,255);
return tmp;
 }
};

int main()
{
cv::Mat image= cv::imread("group.jpg");
cv::namedWindow("Original Image");
cv::imshow("Original Image",image);

// 워터쉐드는 초기 감지에서 전체 객체를 결정하기 위해 사용
// 원 영상의 동물을 식별하기 위해 사용하는 이진 영상을 사용

// 이진 영상에서 확실히 전경(동물)에 속하는 화소와
// 배경(잔디)에 속하는 화소를 분명하게 식별해야 한다.

cv::Mat binary;
binary= cv::imread("binary.bmp",0); // 이진 맵 가져오기

// 이진 영상은 영상의 여러 부분에 속하는 흰색 화소를 많이 포함
// 중요한 객체에 속하는 화소만 남기기 위해 영상을 여러 번 침식
cv::Mat fg;
cv::erode(binary, fg, cv::Mat(), cv::Point(-1, -1), 6);
// 잡음과 작은 객체 제거
cv::namedWindow("Foreground Image");
cv::imshow("Foreground Image", fg);
// 배경인 숲에 속하는 몇 화소는 여전히 존재
// 관심 객체에 대응하는 부분을 고려

// 원 이진 영상의 큰 팽창인 배경의 화소를 선택
cv::Mat bg;
cv::dilate(binary,bg,cv::Mat(),cv::Point(-1,-1),6);
cv::threshold(bg,bg,1,128,cv::THRESH_BINARY_INV);
// 객체 없는 영상 화소 식별
cv::namedWindow("Background Image");
cv::imshow("Background Image",bg);
// 결과인 검은 화소는 배경 화소와 일치
// 255 레이블인 전경화소와 128 레이블인 배경화소를 마크
// 팽창 시 128 값을 화소에 할당한 후 즉시 경계화 작업

// 영상을 마커 영상과 조합
cv::Mat markers(binary.size(),CV_8U,cv::Scalar(0));
markers= fg+bg; // 마커 영상 생성
// 영상 조합시 오버로드 된 operator+ 사용
cv::namedWindow("Markers");
cv::imshow("Markers",markers);
// 워터쉐드 알고리즘에 입력하기 위해 사용하는 입력 영상

// 영상 분할
WatershedSegmenter segmenter; // 웨터쉐드 분할 객체 생성

segmenter.setMarkers(markers);
segmenter.process(image);
// 마커를 설정한 후 처리

// 발견된 경계선에 속하는 화소값이 -1이면
// 각 제로 화소를 입력 레이블 중의 하나에 할당하는 방법으로 마커 영상을 갱신

cv::namedWindow("Segmentation"); // 분할 결과 띄워 보기
cv::imshow("Segmentation",segmenter.getSegmentation());

cv::namedWindow("Watersheds"); // 워터쉐드 띄워 보기
cv::imshow("Watersheds",segmenter.getWatersheds());

cv::waitKey(0);

return 0;
}

  • Result

  • 예제 분석
 - 워터쉐드 분할을 만들려면 레벨 0에서 시작하는 영상을 점진적으로 침수케 하는 아이디어에 따른다.
 - '워터'인 레벨이 점진적으로 증가(1, 2, 3 등의 레벨)하고, 유역을 형성.
 - 분지의 크기가 점차 증가하고 결국 다른 두 분지의 워터가 합쳐진다.
 - 이런 상황에서 두 분리된 분지를 유지하기 위해 워터쉐드를 생성.
 - 워터 레벨이 최대 레벨에 도달하면 워터쉐드 분할은 생성된 분지와 워터쉐드 형태의 집합이 된다.

 - 침수과정은 처음에 작은 개별 분지를 많이 만든다.
 - 모두 병합했을 때 과도하게 분할된 영상 안에는 많은 워터쉐드 선이 만들어진다.
 - 이런 문제점을 위해 미리 정의된 마커 화소 집합에서 시작하는 침수 과정이 있는 변형 알고리즘이 제안됨.
 - 마커에서 생성된 분지는 초기 마커에 할당된 값에 일치하게 레이블링.
 - 두 분지를 동일한 레이블로 병합하면 더 이상 워터쉐드를 생성하지 않으므로 과도한 분할을 막음.

 - 위 방법은 cv::watershed 함수를 호출해서 처리.
 - 입력 마커 영상은 최종 워터쉐드 분할을 생성하기 위해 갱신.
 - 사용자는 알 수 없는 레이블링의 화소를 0으로 남겨 놓은 레이블의 임의의 개수를 갖는 마커 영상을 입력할 수 있음.
 - 마커 영상은 255 레벨 이상으로 정의하기 위한 32비트 부호 있는 정수형 영상으로 선택될 수 있다.
 - 또한 특정 값인 -1을 워터쉐드와 연계된 화소에 할당 할 수 있다.
 - cv::watershed 함수는 이 결과를 반환.

 - 결과를 용이하게 보기위해 두개의 메소드를 참고한다.
 - 첫 번째 메소드 : 레이블(0인 값의 워터쉐드와 함께)의 영상을 반환. 경계화로 쉽게 수행함.

 - 소스
// 영상 형태인 결과를 반환
cv::Mat getSegmentation() {
cv::Mat tmp;
//255 이상인 레이블을 갖는 모든 분할은 255인 값으로 할당
markers.convertTo(tmp,CV_8U);
return tmp;
}

 - 두 번째 메소드 : 워터쉐드 선에 0인 값을 할당한 영상을 반환하고, 나머지 부분은 255.
 - cv::converTo 메소드로 이런 결과를 얻는데 사용.

 - 소스
// 영상 형태인 워터쉐드를 반환
 cv::Mat getWatersheds() {
cv::Mat tmp;
// 각 화소인 p는 변환 전에 255p+255로 변환된다.
markers.convertTo(tmp,CV_8U,255,255);
return tmp;
}

 - -1인 화소를 0(-1*255+255=0인 이후)으로 변환하게 미리 바꾼 후 선형 변환을 적용.
 - 255보다 큰 값을 갖는 화소값에 255를 할당.
 - 부호 있는 정수를 부호 없는 정수로 변활할 때 적용하는 새터레이션 연산을 따름.

  • 참고문헌 : OpenCV 2 Computer Vision Application Programming Cookbook