본문 바로가기

영상처리/OpenCV 및 영상처리 이론

OpenCV 기초 - 7. 전경 객체 추출 (워터쉐드 + 그랩컷)

 

 

워터쉐드와 그랩컷을 사용한 객체 추출에 대해서 알아보겠습니다.

 

 

 

 

 

 

 

 

- void watershed ( InputArray image, InputOutputArray makers )

 

입력 영상을 여러 구역으로 나누는 과정을 수행할 때, 사용하는 방법 중 하나로 워터쉐드 알고리즘이 있습니다. 워터쉐드 알고리즘에 대해서 간단히 설명하자면 엣지에 해당하는 부분을 "산등성이"로 보고, 특징이 없는 균일한 영역을 "계곡"으로 생각하여 객체분할을 수행합니다.먼저 영상의 그래디언트를 구합니다. 이 과정을 통해서 질감이 없는 평탄한 지역에는 "계곡"이나 "웅덩이"가 생기게 되고 (낮은 지점), 영상 내의 엣지가 두드러지는 부분에서는 "산" 또는 "산맥"이 (높은 지점) 생기게 됩니다. 이렇게 만든 그래디언트 영상에서 사용자가 지정한 또는 알고리즘에 의해 설정되는 점들에서 물을 채우기 시작하고 다른 영역이 만날 때 멈추게 됩니다. 마커에 의해 연결된 영역은 물이 채워지면서 하나의 영역으로 합쳐지게 됩니다. 

Vachier, Corinne, and Fernand Meyer. "The viscous watershed transform."Journal of Mathematical Imaging and Vision 22.2-3 (2005): 251-267.

 

 

자세한 내용은 논문을 참고하시길 바랍니다.
워터쉐드 알고리즘의 원래 버전은 영상을 과도하게 분할하여 작은 영역을 만들어버리는 문제점이 있다고 합니다. OpenCV에서는 이러한 문제점을 해결하기 위해 변형된 워터쉐드 알고리즘을 제안하고 있으며 사용법은 위의 코드와 같은 형대로 코딩하여 사용할 수 있습니다.
침식을 먼저 수행해주는 이유는 영상 내에 흰색 픽셀이 너무 많이 포함되어 있기 때문에 침식을 통해 중요한 객체에 속하는 픽셀을 남기고 나머지는 제거하는데에 목적이 있습니다.
침식을 통해 Foreground Image를 얻게 되는데 해당 이미지는 아직 배경이 숲에 속하는 몇 픽셀들이 존재하고 있습니다. 하지만 영향이 크지 않기 때문에 이 정도면 충분하다고 판단, 다음 스텝을 진행하게 됩니다.
다음으로 크게 팽창을 수행한 영상을 따로 얻은 후 팽창 영상과 침식 영상을 더해 Markers를 얻게 되고 이 마커를 워터쉐드에 사용하여 최종적으로 결과를 얻게 됩니다.
- void grabCut ( InputArray img, InputOutputArray mask, Rect rect, InputOutputArray bgdModel

InputOutputArray fgdModel, int iterCount, int mode = GC_EVAL )

그랩컷 알고리즘은 워터쉐드보다 계산하는데 더 시간을 소모하지만, 일반적으로 더 좋은 결과를 얻을 수 있습니다. 정지 영상에서 전경 객체를 추출할 때 사용하면 좋습니다.
grabCut 함수는 단순히 영상과 배경 또는 전경에 속하는 픽셀의 일부분에 대한 레이블만 있으면 사용할 수 있습니다. 부분 레이블링에 기반하여 동작하고 영상 전체에서 전경/배경을 구분하게 됩니다.

 

입력/결과 분할 영상은 아래 

4가지 케이스를 가지게 됩니다.

1) GC_BGD : 확실히 배경에 포함되는 픽셀2) GC_FGD : 확실히 전경에 포함되는 픽셀3) GC_PR_BGD : 배경일 가능성이 있는 픽셀4) GC_PR_FGD : 전경일 가능성이 있는 픽셀

 

동작은 처음 입력받은 파라미터에 기반하여, 픽셀의 컬러를 보고 유사한 값끼리 클러스터링을 수행하여 묶게 됩니다. 이후 전경과 배경 픽셀 간의 경계를 이용하여 분할을 어떻게 할지를 결정하게 되고, 다음으로 유사한 레이블을 갖는 픽셀끼리 연결 시키는 과정을 최적화를 통해 수행하게 됩니다.

 

 

※ 참고 : 워터쉐드와 그랩컷 알고리즘에 대한 자세한 내용을 원하시는 분들은 논문을 참고하시길 바랍니다.

 

- 결과

 

 

 

 

 

※ 참고 : 단순히 예제를 실행하면 Foreground objects 결과가 책과 다르게 나오게 됩니다. 책과 같은, 객체만 얻어 내는 코드를 함께 첨부합니다.

 

 

- 코드 첨부

watershedSegmentation.h

더보기
/*------------------------------------------------------------------------------------------*\This file contains material supporting chapter 5 of the cookbook:Computer Vision Programming using the OpenCV Library.by Robert Laganiere, Packt Publishing, 2011.
This program is free software; permission is hereby granted to use, copy, modify,and distribute this source code, or portions thereof, for any purpose, without fee,subject to the restriction that the copyright notice may not be removedor altered from any source or altered source distribution.The software is released on an as-is basis and without any warranties of any kind.In particular, the software is not guaranteed to be fault-tolerant or free from failure.The author disclaims all warranties with regard to this software, any use,and any consequent failure, is purely the responsibility of the user.
Copyright (C) 2010-2011 Robert Laganiere, www.laganiere.name\*------------------------------------------------------------------------------------------*/
#if !defined WATERSHS#define WATERSHS
#include <opencv2/core/core.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; //255 이상인 레이블을 갖는 모든 분할은 255인 값으로 할당 markers.convertTo(tmp, CV_8U);
return tmp; }
// 영상 형태인 워터쉐드를 반환 cv::Mat getWatersheds() {
cv::Mat tmp; markers.convertTo(tmp, CV_8U, 255, 255);
return tmp; }};

 

#endif
 
main.cpp
더보기
/*------------------------------------------------------------------------------------------*\
This file contains material supporting chapter 5 of the cookbook:
Computer Vision Programming using the OpenCV Library.
by Robert Laganiere, Packt Publishing, 2011.
 
This program is free software; permission is hereby granted to use, copy, modify,
and distribute this source code, or portions thereof, for any purpose, without fee,
subject to the restriction that the copyright notice may not be removed
or altered from any source or altered source distribution.
The software is released on an as-is basis and without any warranties of any kind.
In particular, the software is not guaranteed to be fault-tolerant or free from failure.
The author disclaims all warranties with regard to this software, any use,
and any consequent failure, is purely the responsibility of the user.
 
Copyright (C) 2010-2011 Robert Laganiere, www.laganiere.name
\*------------------------------------------------------------------------------------------*/
 
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include "watershedSegmentation.h"
 
 
int main()
{
// 입력 영상 읽기
cv::Mat image = cv::imread("robert/group.jpg");
if (!image.data)
return 0;
 
// 영상 띄워 보기
cv::namedWindow("Original Image");
cv::imshow("Original Image", image);
 
// 이진 맵 가져오기
cv::Mat binary;
binary = cv::imread("robert/binary.bmp", 0);
 
// 이진 맵 영상 띄워 보기
cv::namedWindow("Binary Image");
cv::imshow("Binary Image", binary);
 
// 잡음과 작은 객체 제거
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);
 
// 마커 영상 보기
cv::Mat markers(binary.size(), CV_8U, cv::Scalar(0));
markers = fg + bg;
cv::namedWindow("Markers");
cv::imshow("Markers", markers);
 
// 웨터쉐드 분할 객체 생성
WatershedSegmenter segmenter;
 
// 마커를 설정한 후 처리
segmenter.setMarkers(markers);
segmenter.process(image);
 
// 분할 결과 띄워 보기
cv::namedWindow("Segmentation");
cv::imshow("Segmentation", segmenter.getSegmentation());
 
// 워터쉐드 띄워 보기
cv::namedWindow("Watersheds");
cv::imshow("Watersheds", segmenter.getWatersheds());
 
// 다른 영상 열기
image = cv::imread("robert/tower.jpg");
 
// 배경 화소 식별
cv::Mat imageMask(image.size(), CV_8U, cv::Scalar(0));
cv::rectangle(imageMask, cv::Point(5, 5), cv::Point(image.cols - 5, image.rows - 5), cv::Scalar(255), 3);
// 전경 화소 식별(영상의 중앙 내)
cv::rectangle(imageMask, cv::Point(image.cols / 2 - 10, image.rows / 2 - 10),
cv::Point(image.cols / 2 + 10, image.rows / 2 + 10), cv::Scalar(1), 10);
 
// 마커를 설정한 후 처리
segmenter.setMarkers(imageMask);
segmenter.process(image);
 
// 마커가 있는 영상 띄워 보기
cv::rectangle(image, cv::Point(5, 5), cv::Point(image.cols - 5, image.rows - 5), cv::Scalar(255, 255, 255), 3);
cv::rectangle(image, cv::Point(image.cols / 2 - 10, image.rows / 2 - 10),
cv::Point(image.cols / 2 + 10, image.rows / 2 + 10), cv::Scalar(1, 1, 1), 10);
cv::namedWindow("Image with marker");
cv::imshow("Image with marker", image);
 
// 워터쉐드 띄워 보기
cv::namedWindow("Watersheds of foreground object");
cv::imshow("Watersheds of foreground object", segmenter.getWatersheds());
 
// 다른 영상 열기
image = cv::imread("robert/tower.jpg");
 
// 경계 직사각형 정의
cv::Rect rectangle(50, 70, image.cols - 150, image.rows - 180);
 
cv::Mat result; // 분할 결과(4가지 가능한 값)
cv::Mat bgModel, fgModel; // 모델(초기 사용)
 // GrabCut segmentation
cv::grabCut(image,    // 입력 영상
result,   // 분할 결과
rectangle,// 전경을 포함하는 직사각형
bgModel, fgModel, // 모델
1,        // 반복 회수
cv::GC_INIT_WITH_RECT); // 직사각형 사용
 
// 전경일 가능성이 있는 화소를 마크한 것을 가져오기
cv::compare(result, cv::GC_PR_FGD, result, cv::CMP_EQ);
// 결과 영상 생성
cv::Mat foreground(image.size(), CV_8UC3, cv::Scalar(255, 255, 255));
image.copyTo(foreground, result); // 배경 화소는 복사되지 않음 
 
 // 원 영상 내 직사각형 그리기
cv::rectangle(image, rectangle, cv::Scalar(255, 255, 255), 1);
cv::namedWindow("Image");
cv::imshow("Image", image);
 
// 결과 띄워 보기
cv::namedWindow("Segmented Image");
cv::imshow("Segmented Image", foreground);
 
// 다른 영상 열기
image = cv::imread("robert/group.jpg");
 
// 경계 직사각형 정의
cv::Rect rectangle2(10, 100, 380, 180);
 
cv::Mat bkgModel, fgrModel; // 분할 결과(4가지 가능한 값)
// 그랩컷 분할
cv::grabCut(image,  // 입력 영상
result, // 분할 결과
rectangle2, bkgModel, fgrModel, 5, cv::GC_INIT_WITH_RECT);
// 전경을 포함하는 직사각형
// cv::compare(result,cv::GC_PR_FGD,result,cv::CMP_EQ);
result = result & 1;
foreground.create(image.size(), CV_8UC3);
foreground.setTo(cv::Scalar(255, 255, 255));
image.copyTo(foreground, result); // bg pixels not copied
 
 // 원 영상 내 직사각형 그리기
cv::rectangle(image, rectangle2, cv::Scalar(255, 255, 255), 1);
cv::namedWindow("Image 2");
cv::imshow("Image 2", image);
 
// 결과 띄워 보기
cv::namedWindow("Foreground objects");
cv::imshow("Foreground objects", foreground);
 
cv::waitKey();
return 0;
}
 

추가 코드 (그랩컷 객체만)

더보기

#include <iostream>

#include <opencv2/core/core.hpp>

#include <opencv2/highgui/highgui.hpp>

#include <opencv2/imgproc/imgproc.hpp>

 

int main()

{

cv::Mat image = cv::imread("robert/group.jpg");

cv::namedWindow("Original Image");

cv::imshow("Original Image", image);

// 영상 열기

 

cv::Rect rectangle(10, 100, 380, 180);

// 경계 직사각형 정의

// 직사각형 밖의 화소는 배경으로 레이블링

 

cv::Mat result; // 분할 (4자기 가능한 값)

cv::Mat bgModel, fgModel; // 모델 (초기 사용)

cv::grabCut(image,    // 입력 영상

result,    // 분할 결과

rectangle,   // 전경을 포함하는 직사각형

bgModel, fgModel, // 모델

5,     // 반복 횟수

cv::GC_INIT_WITH_RECT); // 직사각형 사용

 

// 전경일 가능성이 있는 화소를 마크한 것을 가져오기

cv::compare(result, cv::GC_PR_FGD, result, cv::CMP_EQ);

 

// 결과 영상 생성

cv::Mat foreground(image.size(), CV_8UC3,

cv::Scalar(255, 255, 255));

 

image.copyTo(foreground, // 배경 화소는 복사되지 않음

result);

 

cv::namedWindow("Result");

cv::imshow("Result", result);

 

cv::namedWindow("Foreground");

cv::imshow("Foreground", foreground);

 

cv::waitKey(0);

 

return 0;

}