이번 포스팅은 Watershed 알고리즘을 이용하여 Image segmentation을 하는 방법을 알아보고
OpenCV에서 어떻게 사용하는지 알아보겠습니다.
포스팅 관련 내용은 아래에서 출처를 얻어 사용하였습니다.
https://docs.opencv.org/4.0.1/d3/db4/tutorial_py_watershed.html
Goal
- Watershed 알고리즘을 이용한 이미지 구분
- cv2.watershed() 사용
Theory
- 밑의 예시처럼 grayscale 이미지를 픽셀 값 ( 0 ~ 255 )에 따른 분포도를 그려보면 값이 높은 부분과 낮은 부분으로 되어있습니다.
- 여기서 값이 높은 부분을 봉우리로 값이 낮은 부분을 계곡이라 부르기로 하고 이렇게 만들어진 봉우리와 계곡에 물을 붓는다고 생각하면 물이 섞이는 부분이 생깁니다. 그 부분에 경계선을 만들어 서로 섞이지 않게 하며 우리는 그 경계선을 이미지의 구분을 위해 사용함으로 분할을 하게 됩니다.
- 아래 예시는 시각적으로 잘 나타나 있습니다.
Code
- 하지만 계속해서 봉우리와 계곡의 물들이 합쳐지고 장벽을 세우는 것을 반복하는 방식은 노이즈나 불규칙한 이미지로 과하게 분리된(oversegmented)의 결과를 나타낼 수 있습니다.
- 이런 문제점을 해결하기 위해 OpenCV에서는 marker-based(마커 기반) watershed 알고리즘을 사용합니다.
marker-based watershed는 배경과 전경(객체) 부분을 우리가 마킹하고 알고리즘을 실행하는 것입니다.
- 우리가 알고 있는 객체 각각에 대해 0 이상의 번호를 매기는 것인데 명확하지 않은 것에는 0을 붙이고, watershed 알고리즘을 통해 생긴 경계선에 대해서는 -1의 라벨링을 합니다.
코드)
import numpy as np
import cv2
import matplotlib.pyplot as plt
img = cv2.imread('./images/water_coins.jpg')
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, thresh = cv2.threshold(img, 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)
# 확실한 전경(객체) 영역 찾음
dist_transform = cv2.distanceTransform(opening, cv2.DIST_L2, 5)
_, 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)
결과)
- threshold 이미지에서 동전이라고 구분되는 영역을 구분하고 분리했다.
- 객체 안에 있는 작은 구멍(검은색) 픽셀들을 제거하기 위해 opening을 하여 처리했다.
- 서로 접촉되어 있는 동전들을 구분하기 위해 거리 변환(distance transform)을 하여 구분하였다.
- 동전이 아니라고 생각하는 지역을 찾기 위해 결과를 확대(dilate)하여 배경에 대한 객체 경계를 증가시켜 배경 영역이 실제로 배경인지 확실하게 할 수 있다.
- distance_transform으로 얻어진 이미지로 threshold를 적용하여 확실한 전경을 찾아냈다.
- (배경 - 전경)을 하여 확실하지 않은 영역을 구분하였다.
라벨링(labelling) 작업
# markers 라벨링
ret, markers = cv2.connectedComponents(sure_fg)
# 모든 라벨에 1을 더하여 배경이 0이 아니라 1이 되도록 함
# -> unknown 지역과 같은 값을 가지는 것을 피하기 위해
markers = markers + 1
# unknown 지역을 0으로 마크한다.
markers[unknown == 255] = 0
markers = cv2.watershed(img, markers)
img[markers == -1] = [255, 0, 0]
- 우리는 이미지의 배경과 전경의 구역을 알고 있습니다. 그래서 마커를 만들고 라벨을 붙일 것입니다.
확실히 알고 있는 지역은 양의 정수로 라벨링을 하지만 확실히 알지 못하는 unknown은 0으로 라벨링을 하였습니다.
이를 위해 cv2.connectedComponents()를 사용하여 이미지의 배경을 0으로 표시하고 다른 물체는 1부터 시작하는 정수로 라벨링을 했습니다.
결과)
- 결과를 보시면 marker를 JET 컬러 맵으로 나타내었습니다. 짙은 파란색은 unknown의 영역을 보여주며 동전 부분은 각기 다른 색의 값들로 되어있습니다. 배경이 확실한 영역은 unknown 영역에 비해 밝은 파란색으로 보입니다.
- 준비된 마커를 가지고 동전 이미지에 watershed 알고리즘을 적용했습니다.
하지만 결과는 잘 분리된 동전이 있는 반면에 분리되어 있지 않은 동전도 보입니다.
watershed를 통한 segmentation을 알아보았고
opencv를 통해 구현해 보았습니다.
혹시 틀린 점이나 질문이 있으시면 댓글로 남겨주세요
감사합니다 :)
'영상처리' 카테고리의 다른 글
Python - OpenCV (14) : Image Pyramid (0) | 2021.07.26 |
---|---|
Python - OpenCV (13) : SVM(Support Vector Machines) (0) | 2021.07.20 |
Python - OpenCV (11) : Grayscale 함수 비교 (2) | 2021.07.16 |
Python - OpenCV (10) : Otsu's Binarization (0) | 2021.07.15 |
Python - OpenCV (9) : Image Thresholding (0) | 2021.07.14 |