Python과 OpenCV – 28 : Watershed 알고리즘을 이용한 이미지 분할

이 글의 원문은 https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_watershed/py_watershed.html 입니다.

회색조 이미지는 지형처럼 해석할 수 있는데, 값이 높은 픽셀 위치는 산꼭대기이고 값이 낮은 픽셀 위치는 계곡이라고 해석할 수 있습니다. 지형이므로 고립되어 분리된 계곡이 있을 것이고 이 계곡들을 서로 다른 색의 물로 채우기 시작하면 물이 점점 차오르다가 이웃한 계곡의 언저리에서 물이 합쳐지게 됩니다. 물이 합쳐지는 것을 피하기 위해서 합쳐지는 순간에서의 위치에 경계를 생성하는거죠. 그럼 이 경계선이 이미지 분할의 결과가 됩니다. 이것이 바로 Watershed 알고리즘의 기본철학입니다. 아래의 동영상 이미지를 보면 좀더 직관적으로 이해할 수 있습니다.

이 방식을 통해 이미지를 분할하게 되면 분할에 오류가 발생할 수 있는데, 이는 이미지의 잡음이나 어떤 불규칙한 것들로 인한 요소 등이 이유입니다. 그래서 OpenCV는 마커 기반의 Watershed 알고리즘을 구현해 제공하는데.. 각 계곡을 구성하는 화소들을 병합시켜 번호를 매기고, 병합 수 없는 애매한 화소는 0값을 매깁니다. 이를 인터렉티브한 이미지 분할 기법이라고 합니다. 우리가 알고 있는 객체에 각각에 대해 0 이상의 번호를 매기는 것인데요. 전경이 되거나 객체인 것에, 또 배경에도 0 이상의 값을 매깁니다. 그러나 그외 불명확하다라고 판단되는 것은 0을 매깁니다. 이 불명확한 것이 어떤 요소, 즉 배경인지 전경인지 또는 어떤 객체의 소유인지는 Watershed 알고리즘을 통해 결정됩니다. Watershed 알고리즘을 통해 분할 경계선이 생길 것이고 이 경계선에 대해서는 -1 값을 매깁니다.

자, 이제 이론에 대한 설명은 끝났으므로 예제 코드를 살펴보겠습니다.

import cv2
import numpy as np
from matplotlib import pyplot as plt

img = cv2.imread('./data/water_coins.jpg')

# 이진 이미지로 변환
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)

# 잡음 제거
kernel = np.ones((3,3), np.uint8)
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)

# 이미지 확장을 통해 확실한 배경 요소 확보
sure_bg = cv2.dilate(opening, kernel, iterations=3)

# distance transform을 적용하면 중심으로 부터 Skeleton Image를 얻을 수 있음.
# 이 결과에 Threshold를 적용하여 확실한 객체 또는 전경 요소를 확보
dist_transform = cv2.distanceTransform(opening, cv2.DIST_L2, 5)
ret, sure_fg = cv2.threshold(dist_transform, 0.5*dist_transform.max(), 255, 0)
sure_fg = np.uint8(sure_fg)

# 배경과 전경을 제외한 영역 곳을 확보
unknown = cv2.subtract(sure_bg, sure_fg)

# 마커 생성 작성
ret, markers = cv2.connectedComponents(sure_fg)
markers = markers + 1
markers[unknown == 255] = 0

# 앞서 생성한 마커를 이용해 Watershed 알고리즘을 적용
markers = cv2.watershed(img, markers)
img[markers == -1] = [255,0,0]

images = [gray,thresh,opening, sure_bg, dist_transform, sure_fg, unknown, markers, img]
titles = ['Gray', 'Binary', 'Opening', 'Sure BG', 'Distance', 'Sure FG', 'Unknow', 'Markers', 'Result']

for i in range(len(images)):
    plt.subplot(3,3,i+1)
    plt.imshow(images[i])
    plt.title(titles[i])
    plt.xticks([])
    plt.yticks([])

plt.show()

실행 결과는 다음과 같습니다.

코드와 실행 결과를 비교해 가며, 설명을 해 보면.. 먼저 5번 코드에서 입력 이미지를 파일로부터 읽고 이 이미지를 2진 이미지로 생성하는 것이 5-9번 코드이고 결과 이미지의 Binary입니다. 잡음을 제거 하기 위해 12-13번 코드가 실행되고 결과 이미지의 Opening입니다. 잡음을 제거한 이미지에 dilate 함수를 통해 이미지의 객체를 확장시킨 것이 16번 코드이고 결과 이미지의 Sure_BG입니다. 이제 확실한 전경 또는 객체에 대한 화소를 얻기 위해 18-22번 코드가 실행되고 그 결과 이미지는 Sure_FG입니다. Sure_FG는 결과 이미지의 Distance 이미지로부터 threshold 처리를 통해 얻어진 것입니다. 이제 배경인 Sure_BG에서 전경인 Sure_FG를 빼면 애매모호한 영역을 얻을 수 있게 되는데, 25번 코드가 이에 해당되고 그 결과 이미지는 Unknonw입니다. 즉 어떤 문제를 해결하기 위해 문제의 범위를 좁혀 나가고 있다는 것을 직감할 수 있습니다. 이제 마커 이미지를 sure_fg를 이용해 생성하는데 28-30번 코드입니다. 머커는 0값부터 지정되므로 결과 마커에 1씩 증감시키고, 애매모호한 부분에 대해서는 0 값을 지정합니다. 앞서 이론에 언급했던 것처럼요. 마커가 준비되었으므로, 이제 Watershed 알고리즘을 적용하고 분할 경계선에 해당되는 화소에 지정된 값인 -1을 가지는 부분을 [255,0,0] 색상으로 지정하는 것이 33-34번 코드이며, 최종 결과 이미지인 Result입니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다