概要
OpenCV の cv2.watershed()
を使用して、画像のセグメンテーションを行う方法を紹介します。
このアルゴリズムは、単に二値化しただけでは各領域が連結してしまい、個々のインスタンスとして区別できない場合に有効なセグメンテーション方法です。
手順
次の画像の各コインの輪郭を取得することを目指します。
2 値化する
import cv2
from IPython.display import Image, display
def imshow(img):
"""ndarray 配列をインラインで Notebook 上に表示する。"""
ret, encoded = cv2.imencode(".jpg", img)
display(Image(encoded))
import cv2
import numpy as np
from matplotlib import pyplot as plt
# 画像を読み込む。
img = cv2.imread("sample.jpg")
# グレースケール形式に変換する。
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 大津の手法で2値化する。
ret, img_bin = cv2.threshold(img_gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
# ノイズを削除する。
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
img_bin = cv2.morphologyEx(img_bin, cv2.MORPH_OPEN, kernel, iterations=2)
imshow(img_bin)
![](/wp_pystyle/wp-content/uploads/2020/09/opencv-watershed-algorithm_02.jpg)
コインの領域が繋がっており、このまま輪郭抽出を行った場合、以下のように 1 つのインスタンスとして抽出されてしまいます。
contours, hierarchy = cv2.findContours(
img_bin, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
dst = cv2.drawContours(img.copy(), contours, -1, (0, 255, 0), 2)
imshow(dst)
![](/wp_pystyle/wp-content/uploads/2020/09/opencv-watershed-algorithm_03.jpg)
sure background を抽出する
確実に背景といえる領域を sure background といいます。 膨張演算により、前景領域 (コインの部分) を膨張させ、膨張後もなお背景である部分は sure background といえます。
# sure background 領域を抽出する。
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
img_sure_bg = cv2.dilate(img_bin, kernel, iterations=3)
imshow(img_sure_bg)
![](/wp_pystyle/wp-content/uploads/2020/09/opencv-watershed-algorithm_04.jpg)
sure foreground を抽出する
確実に前景といえる領域を sure foreground といいます。 2 値画像に対して距離変換を行ったあと、背景からの距離が一定以上の領域のみ前景とすることで、sure foreground が抽出できます。
# 距離マップを作成する。
img_dist = cv2.distanceTransform(img_bin, cv2.DIST_L2, 5)
imshow(img_dist)
# sure foreground 領域を抽出する。
ret, img_sure_fg = cv2.threshold(img_dist, 0.5 * img_dist.max(), 255, cv2.THRESH_BINARY)
img_sure_fg = img_sure_fg.astype(np.uint8) # float32 -> uint8
imshow(img_sure_fg)
![](/wp_pystyle/wp-content/uploads/2024/07/opencv-watershed-algorithm_05.jpg)
前景か背景か判断できない領域を抽出する
確実に背景といえる領域 (sure background) から確実に前景といえる領域 (sure foreground) の差分をとることで、前景か背景か判断できない領域を抽出します。
# 前景か背景か判断できない領域を抽出する。
img_unknown = cv2.subtract(img_sure_bg, img_sure_fg)
imshow(img_unknown)
![](/wp_pystyle/wp-content/uploads/2024/07/opencv-watershed-algorithm_06.jpg)
各領域にラベル付けを行う
connectedComponents() で確実に前景といえる部分をラベルを付けます。connectedComponents() では、背景が 0 で、各物体が 1, 2, … とラベル付けされます。そこで、ラベルを 1 ずつ増やして、前景か背景か判断できない領域がラベル 0 となるように調整します。
- ラベル 0: 前景か背景か判断できない領域
- ラベル 1: 背景
- ラベル 2 以上: 物体
# sure foreground にたいして、ラベル付を行う。
ret, markers = cv2.connectedComponents(img_sure_fg)
print(np.unique(markers))
# 前景か背景か判断できない領域はラベル0を割り当てる。
markers += 1
markers[img_unknown == 255] = 0
print(np.unique(markers))
fig, ax = plt.subplots(figsize=(6, 6))
ax.imshow(markers, cmap="gist_ncar")
plt.show()
[ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24] [ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25]
watershed アルゴリズムを適用する
cv2.watershed()
を使用して、Watershed アルゴリズムによるセグメンテーションを行います。アルゴリズムが完了すると、第 2 引数で渡した markers
のうち、0 のラベルが境界ラベル (-1) または 1 以上のラベルのいずれかに確定します。
その結果、各コインがそれぞれ別々にラベル付けされます。
- ラベル -1: 境界
- ラベル 1: 背景
- ラベル 2 以上: 物体
# watershed アルゴリズムを適用する。
markers = cv2.watershed(img, markers)
print(np.unique(markers))
fig, ax = plt.subplots(figsize=(6, 6))
ax.imshow(markers, cmap="tab20b")
plt.show()
[-1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25]
watershed アルゴリズムの結果を元に輪郭抽出を行う
watershed アルゴリズムにより、各物体が区別できるようにラベリングが行われたので、これを元に輪郭抽出を行います。
labels = np.unique(markers)
coins = []
for label in labels[2:]: # labels[0]: 境界ラベル、labels[1]: 背景ラベルは無視する。
# ラベル label の領域のみ前景、それ以外は背景となる2値画像を作成する。
target = np.where(markers == label, 255, 0).astype(np.uint8)
# 作成した2値画像に対して、輪郭抽出を行う。
contours, hierarchy = cv2.findContours(
target, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
coins.append(contours[0])
# 輪郭を描画する。
cv2.drawContours(img, coins, -1, color=(0, 0, 255), thickness=2)
imshow(img)
![](/wp_pystyle/wp-content/uploads/2024/07/opencv-watershed-algorithm_07.jpg)
コメント