[OpenCV] 04-15. Image Segmentation with Watershed Algorithm
🐍Python/OpenCV

[OpenCV] 04-15. Image Segmentation with Watershed Algorithm

728x90
반응형

< Image Segmentation with Watershed Algorithm >

이번 장에서는,

  • Watershed 알고리즘을 이용하여 marker-based 이미지 구분(segmentation)을 하는 법
  • cv2.watershed 함수

에 대해서 알아볼 것이다.

Theory

흑백스케일 이미지의 높은 채도는 봉우리와 언덕을 나타내는 지형 표면이며, 낮은 채도는 계곡을 나타낸다. 다른 색의 물들로 고립된 모든 계곡을 채우기 시작해보자. 물이 차오르면서 근처의 봉우리에 따라 분명히 다른 색깔의 물들이 합쳐지기 시작할 것이다. 이를 피하기 위해 물이 합쳐지는 곳에 장벽을 쌓는다. 봉우리가 물에 잠길 때까지, 물을 채우고 장벽을 세우는 것을 반복한다. 그러면 장벽이 분할(segmentation)의 결과를 반환한다. 이것이 watershed의 “철학”이다.http://www.cmm.mines-paristech.fr/~beucher/wtshed.html 사이트에 자세한 설명이 있다.



(사진출처:https://m.blog.naver.com/PostView.nhn?blogId=laonple&logNo=220902777415&categoryNo=8&proxyReferer=&proxyReferer=https%3A%2F%2Fwww.google.com%2F)

하지만 이 접근방식은 노이즈나 이미지의 다른 불규칙성으로 인해 oversegmented(과하게 분리)된 결과를 반환한다. 그래서 OpenCV는 어떤 것이 모두 병합될 것인지 그리고 어떤 것이 통합되지 않을 것인지를 지정하는 marker-based watershed 알고리즘을 구현한다. 이는 쌍방향의 이미지 분할이다. 알고 있는 대상에 다른 라벨을 붙이는 것이 해야할 일이다. 우리가 전경 또는 물체라고 확신하는 지역에 한 색상(또는 채도)으로 라벨을 붙이고, 우리가 배경 또는 물체가 아니라고 확신하는 지역에 다른 색상으로 라벨을 붙이고 마지막으로 아무것도 확신하지 못하는 지역에 0으로 라벨을 붙인다. 이것이 우리의 marker이다. 그런 다음 watershed 알고리즘을 적용한다. 그러면 우리의 marker는 우리가 준 라벨로 업데이트 될 것이고, 물체의 경계는 -1 값을 갖게 될 것이다.

Code

아래의 예시를 통해서, Distance Tranform을 사용하는 방법과 함께 서로 접촉하는 객체를 분할하는 watershed를 볼 것이다.

아래의 동전 이미지를 보자, 동전들은 서로 맞닿아있다. 임계를 만들더라도, 그대로 붙어있을 것이다.


먼저 동전들의 대략적인 추정치를 찾는 것으로 시작하고, 이를 위해 우리는 Otsu’s binarization을 사용할 수 있다.

04-2. Image Thresholding 참고!

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

img = cv2.imread('./images/coins.jpg')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gray,0,255,cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)

plt.imshow(thresh,cmap='gray')
plt.axis('off')
plt.show()

결과:


이제 우리는 이미지내의 작은 흰색 노이즈들을 제거할 필요가 있다. 이를 위해 Morphological Opening을 사용할 수 있다.

04-5. Morphological Transformations 참고!

객체 내의 작은 구멍들을 제거하기 위해서는, Morphological Closing을 사용할 수 있다. 그래서 이제 우리는 객체의 중심에 가까운 지역이 전경에 속하고, 객체와 떨어져 있는 지역이 배경이라는 것을 확실히 알게 되었다. 우리가 확신하지 못하는 유일한 지역은 동전의 경계 지역이다.

그래서 우리는 동전이라고 확신하는 지역을 추출할 필요가 있다. Erosion은 경계 픽셀들을 제거할 수 있다. 그래서 남는 것들이 동전이라고 확신할 수 있다. 만약 물체가 서로 닿지 않는다면 그것은 효과가 있을 것이다. 그러나 그들이 서로 접촉하고 있기 때문에, 다른 좋은 선택으로는 거리 변환(Distance Transform)을 찾아 적절한 임계값을 적용하는 것이다. 다음으로 우리는 동전이 아니라고 생각되는 지역을 찾아야 한다. 이를 위해, 결과를 확대(Dilate)한다. Dilation은 배경에 대한 객체 경계를 증가시킨다. 이렇게 하면 경계 영역이 제거되므로 결과적인 배경 영역이 실제로 배경인지 확인할 수 있다. 아래 이미지를 보자.


남아있는 역역은 동전인지 배경인지 아무 아이디어가 없는 곳이다. Watershed 알고리즘이 찾아야만 한다. 이러한 영역은 보통 전경과 배경이 만나는 동전의 경계(또는 서로 다른 두 개의 동전이 만나는 곳)에 있다. 우리는 이를 “경계선”이라고 부른다. sure_fg영역을 sure_bg 영역에서 빼면 얻을 수 있다.

# 노이즈 제거
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)

# 전경이 확실한 영역 찾기
dist_transform = cv2.distanceTransform(opening,cv2.DIST_L2,5)
ret, sure_fg = cv2.threshold(dist_transform,0.7*dist_transform.max(),255,0)

# 모르겠는 영역 찾기
sure_fg = np.uint8(sure_fg)
unknown = cv2.subtract(sure_bg,sure_fg)

plt.figure(figsize=(12,8))
plt.subplot(221), plt.imshow(opening,cmap='gray')
plt.title("Noise Removed"), plt.axis('off')
plt.subplot(222), plt.imshow(sure_bg,cmap='gray')
plt.title("Sure Background"), plt.axis('off')
plt.subplot(223), plt.imshow(dist_transform,cmap='gray')
plt.title("Distance Transform"), plt.axis('off')
plt.subplot(224), plt.imshow(sure_fg,cmap='gray')
plt.title("Threshold"), plt.axis('off')
plt.show()

결과를 보자. Threshold 이미지에서, 동전이 확실시되는 일부 영역을 확보하여 현재 동전이 분리되어 있다. (어떤 경우에는 서로 접촉하는 객체를 분리하는 것이 아니라 전경 분할에만 관심이 있을 수 있다. 그런 경우에는 거리 변환을 사용할 필요가 없고, Erosion만으로도 충분하다. Erosion은 확실한 정경 부위를 추출하는 또 다른 방법이다)

이제 우리는 동전의 어느 지역이 배경이고 전부인지 확실히 알 고 있다. 그래서 우리는 마커를 만들고 그 안에 있는 부분에 라벨을 붙인다. 우리가 확실히 알고 있는 영역은 어떤 양의 정수로 표시되지만, 다른 정수로 표시되며, 우리가 확실히 알지 못하는 영역은 단지 0으로 남게 된다. 이를 위해 cv2.connectedComponents()를 사용한다. 이는 이미지의 배경을 0으로 표시한 다음, 다른 물체는 1부터 시작하는 정수로 라벨을 표시한다.

하지만 만약 배경이 0으로 표시되어 있다면, watershed가 이를 알 수 없는 영역으로 간주할 것이라는 것을 알고있다. 그래서 우리는 이를 다른 정수로 표시하기를 원한다. 대신에, 무명으로 정의한 모르겠는 지역을 0으로 표시한다.

# Marker 라벨링
ret, markers = cv2.connectedComponents(sure_fg)

# 모든 라벨에 1을 더하여 배경이 0이 하니라 1이 되도록 한다.
# unknown 지역과 같은 값을 가지는 것을 피하기 위해
markers = markers + 1

# 이제 unknown 지역을 0으로 마크한다.
markers[unknown==255] = 0

plt.figure(figsize=(12,8))
plt.imshow(markers, cmap='jet')
plt.axis('off')
plt.show()

JET 컬러맵으로 나타낸 결과를 보자. 짙은 파랑색은 알려지지 않은 지역을 보여준다. 확실히 동전은 다른 값의 색들로 칠해져있다. 배경임이 확실한 남은 영역은 알려지지 않은 영역에 비해 밝은 파란색으로 표시된다.


이제 우리의 마커는 준비되었다. 이제 마지막을 위한 단계이니, watershed를 적용하자. 그러면 마커 이미지가 수정될 것이다. 경계 영역은 -1로 마크될 것이다.

markers = cv2.watershed(img,markers)
img[markers == -1] = [255,0,0]
plt.subplot(121),plt.imshow(markers,cmap="Accent")
plt.title("Marker Image after Segmentation"), plt.axis('off')
plt.subplot(122),plt.imshow(img)
plt.title('Result'), plt.axis('off')
plt.show()

아래 결과를 보자. 어떤 동전들은, 잘 분리되어 있거나 그러지 않다.


728x90
반응형