< Interactive Foreground Extraction using GrabCut Algorithm >
이번 장에서는
- 이미지에서 전경을 추출하기 위한 GrabCut 알고리즘
- 이를 위한 상호작용 기능
에 대해서 알아볼 것이다.
Theory
GrabCut 알고리즘은 원저자의 논문인 “GrabCut”: interactive foreground extraction using iterated graph cuts에서 설계되었다. 알고리즘은 사용자 상호작용이 최소화된 전경을 추출하기해 필요했고, 결과가 GrabCut이다.
사용자 관점에서 어떻게 작동하는가? 처음에 사용자는 전경을 중심으로 직사각형을 그린다. (전경 영역은 직사각형 내부에 위치해야만 한다.) 그리고 알고리즘은 최상의 결과를 얻기 위해 반복적으로 분할한다. 끝이다. 하지만 어떤 경우에는 분할이 좋지 않을 수도 있다. 예를 들어서, 어떤 전경을 배경으로 표시했을 수도 있고, 그 반대일 수도 있다. 이 경우 사용자는 미세하게 수정해야할 필요가 있다. 일부 잘못된 결과가 있는 곳에 이미지를 몇 번 강조하면된다. 강조는 기본적으로 “이 영역은 전경이어야해, 근데 배경으로 했네? 다음에 다시 제대로해!” 또는 반대의 경우를 말해준다. 그리고 그 다음번에는, 더 좋은 결과를 얻을 것이다.
아래의 이미지를 보자. 먼저, 선수와 공은 파란 직사각형으로 둘러싸여 있다. 그런 다음 흰색 스트로크와 검은색 스트로크로 최종 마무리한다. 그리고 좋은 결과를 얻었다!
그러면 배경에서 어떤 일이 일어날까?
- 사용자가 직사각형을 입력한다. 이 직사각형 밖의 모든 것은 배경으로 처리된다.(이것이 아까전에 모든 객체가 직사각형 안에 있어야한다는 이유이다.) 직사각형 내의 모든 것은 불분명하다. 사용자가 전경인지 배경인지 지정하는 입력은 과정속에서 변경되지 않음을 의미하는 “하드 라벨”로 간주된다.
- 컴퓨터는 우리가 주었던 데이터에 기반하여 초기 라벨링을 한다. 전경과 배경 픽셀에 라벨링을 한다.
- 그리고 가우시안 혼합 모델 (GMM)이 전경과 배경을 모델링하는데 사용된다.
- 우리가 주었던 정보에 기반하여, GMM은 학습하고 새로운 픽셀 분포를 생성한다. 즉, 알 수 없는 픽셀은 색상 통계 측면에서 다른 하드 라벨 픽셀과의 관계에 따라 전경일 것 같은 것 또는 배경일 것 같은 것으로 표시된다. (클러스터링과 같다.)
- 그래프는 이 픽셀 분포로부터 만들어진다. 그래프의 노드들은 픽셀들이다. 추가적으로 Source node와 Sink node라는 두 개의 노드들이 추가된다. 모든 전경 픽셀은 Source node와 연결되어 있고, 모든 배경 픽셀은 Sink node와 연결되어 있다.
- 픽셀 소스 노드/엔드 노드에 연결하는 가장자리의 가중치는 픽셀이 전경/배경일 확률을 통해 정의된다. 픽셀 사이의 가중치는 가장자리 정보나 픽셀 유사성으로 정의된다. 만약 픽셀 색상의 차이가 크다면, 그들 사이의 가장자리는 낮은 가중치를 갖는다.
- 그런 다음 그래프를 분할하는데 mincut 알고리즘을 사용한다. 이는 최소 비용 함수를 가진 두 개의 소스 노드와 싱크 노드로 그래프를 분리한다. 비용 함수는 분리된 가장자리들의 가중치들의 합이다. 자르고 난 후, Source node와 연결된 모든 픽셀들은 전경이 되고, Sink node와 연결된 픽셀은 배경이 된다.
- 이 과정은 분류가 수렴할 때 까지 진행된다.
Demo
OpenCV와 함께 grabcut 알고리즘을 볼 것이다. OpenCV는 이를 위한 cv2.grabCut() 함수가 있다. 인자들을 먼저 보자!
img - 입력 이미지이다mask - 이는 우리가 배경,전경 또는 배경일 것 같은/전경일 것 같은 등이라고 지정하는 마스크 이미지이다. 이는 다음의 flags로 표현되는데, cv2.GC_BGD,cv2.GC_FGD,cv2.GC_PR_BGD,cv2.GC_PR_FGD로 나타내거나, 또는 간단하게 0,1,2,3으로 넘긴다.rect - 이는 전경 물체를 포함하고 있는 직사각형의 좌표이다. (x,y,w,h)bdgModel,fgdModel - 이들은 알고리즘이 내부적으로 사용하는 배열들이다. 두 개의 np.float64 타입의 0 배열을 (1,65) 크기로 만들면 된다.iterCount - 알고리즘이 실행해야 하는 반복 횟수mode - 이는 cv2.GC_INIT_WITH_RECT 또는 cv2.GC_INIT_WITH_MASK 또는 우리가 직사각형을 그리고 있는지 마지막 마무리를 하는지 결정하는 조합이다.
http://blog.naver.com/PostView.nhn?blogId=samsjang&logNo=220606250662&categoryNo=66&parentCategoryNo=0&viewDate=¤tPage=3&postListTopCurrentPage=&from=postList&userTopListOpen=true&userTopListCount=10&userTopListManageOpen=false&userTopListCurrentPage=3의 코드를 가져와서 진행해보겠습니다!
포인트는 처음에 직사각형으로만 cut했을 때, 그리고 부분부분 선택하면서 지우고 살리는 것을 보면 될 것 같다.
import numpy as np
import cv2
from matplotlib import pyplot as plt
Blue,Green,Red,Black,White = (255,0,0),(0,255,0),(0,0,255),(0,0,0),(255,255,255)
DRAW_BG = {'color':Black,'val':0}
DRAW_FG = {'color':White,'val':1}
rect = (0,0,1,1)
drawing = False
rectangle = False
rect_over = False
rect_or_mask = 100
value = DRAW_FG
thickness = 3
def onMouse(event,x,y,flags,params):
global ix, iy, img, img2, drawing, value, mask,rectangle
global rect, rect_or_mask,rect_over
if event == cv2.EVENT_RBUTTONDOWN:
rectangle = True
ix,iy = x,y
elif event == cv2.EVENT_MOUSEMOVE:
if rectangle:
img = img2.copy()
cv2.rectangle(img,(ix,iy),(x,y),Red,2)
rect = (min(ix,x),min(iy,y),abs(ix-x),abs(iy-y))
rect_or_mask = 0
elif event == cv2.EVENT_RBUTTONUP:
rectangle = False
rect_over = True
cv2.rectangle(img,(ix,iy),(x,y),Red,2)
rect = (min(ix,x),min(iy,y),abs(ix-x),abs(iy-y))
rect_or_mask = 0
print('n:적용하기')
if event == cv2.EVENT_LBUTTONDOWN:
if not rect_over:
print('마우스 왼쪽을 누른채로 전경이 되는 부분을 선택하시오')
else:
drawing = True
cv2.circle(img,(x,y),thickness,value['color'],-1)
cv2.circle(mask,(x,y),thickness,value['val'],-1)
elif event == cv2.EVENT_MOUSEMOVE:
if drawing:
cv2.circle(img,(x,y),thickness,value['color'],-1)
cv2.circle(mask,(x,y),thickness,value['val'],-1)
elif event == cv2.EVENT_LBUTTONUP:
if drawing:
drawing = False
cv2.circle(img,(x,y),thickness,value['color'],-1)
cv2.circle(mask,(x,y),thickness,value['val'],-1)
return
def grapcut():
global ix, iy, img, img2, drawing, value, mask,rectangle
global rect, rect_or_mask,rect_over
img = cv2.imread('./images/son.jpg')
img2 = img.copy()
mask = np.zeros(img.shape[:2],dtype=np.uint8)
output = np.zeros(img.shape,np.uint8)
cv2.namedWindow('input')
cv2.namedWindow('output')
cv2.setMouseCallback('input',onMouse,param=(img,img2))
cv2.moveWindow('input',img.shape[1]+10,90)
print('오른쪽 마우스 버튼을 누르고 영역을 지정한 후 n을 누르시오')
while True:
cv2.imshow('output',output)
cv2.imshow('input',img)
k = cv2.waitKey(1) & 0xFF
if k == 27:
break
if k == ord('0'):
print('왼쪽 마우스로 제거할 부분을 표시한 후 n을 누르세요')
value = DRAW_BG
elif k == ord('1'):
print('왼쪽 마우스로 복원할 부분을 표시한 후 n을 누르세요')
value = DRAW_FG
elif k == ord('r'):
print('리셋합니다')
rect = (0,0,1,1)
drawing = False
rectangle = False
rect_or_mask = 100
rect_over = False
value = DRAW_FG
img = img2.copy()
mask = np.zeros(img.shape[:2],dtype=uint8)
output = np.zeros(img.shape,np.uint8)
print('0:제거할 배경선택, 1:복원할 전경선택, n:적용하기, r:리셋')
elif k == ord('n'):
bgdModel = np.zeros((1,65),np.float64)
fgdModel = np.zeros((1,65),np.float64)
if rect_or_mask == 0:
cv2.grabCut(img2,mask,rect,bgdModel,fgdModel,1,cv2.GC_INIT_WITH_RECT)
rect_or_mask = 1
elif rect_or_mask == 1:
cv2.grabCut(img2,mask,rect,bgdModel,fgdModel,1,cv2.GC_INIT_WITH_MASK)
print('0:제거할 배경선택, 1:복원할 전경선택, n:적용하기, r:리셋')
mask2 = np.where((mask==1) + (mask == 3),255,0).astype('uint8')
output = cv2.bitwise_and(img2,img2,mask=mask2)
cv2.destroyAllWindows()
grapcut()
- 우클릭으로 긁어서 박스를 그린다(객체가 다 담기게)
- 1을 눌러 살릴 부분을 긁고
- 0을 눌러 버릴 부분을 긁는다
- 그리고 n을 눌러 적용한다!
(매우 잘 만드신듯…)
(n을 누르고 조금 기다리면 결과를 볼 수 있다!)
먼저 기본인 직사각형만 그렸을 때는 제대로 전경과 배경을 구분하지 못하는 것을 볼 수 있다. 여기서 다음처럼 긁으며 부분 부분 수정해 주면 된다.
그러면 다음과 같은 결과를 얻게 된다!
조금 더 신경써서 긁어내면 더 좋은 결과를 얻을 수 있을 것이다!