OpenCV – cv2.watershed で繋がっている輪郭を分離する方法

目次
概要
OpenCV の cv2.watershed で繋がっている輪郭を分離する方法を紹介します。
Advertisement
手順
次の画像の各コインの輪郭を取得することを目指します。
2値化する
In [1]:
import cv2
import numpy as np
from IPython.display import Image, display
from matplotlib import pyplot as plt
def imshow(img):
"""ndarray 配列をインラインで Notebook 上に表示する。
"""
ret, encoded = cv2.imencode(".jpg", img)
display(Image(encoded))
# 画像を読み込む。
img = cv2.imread("sample.jpg")
# グレースケール形式に変換する。
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 大津の手法で2値化する。
ret, bin_img = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
# ノイズを削除する。
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
bin_img = cv2.morphologyEx(bin_img, cv2.MORPH_OPEN, kernel, iterations=2)
imshow(bin_img)

コインの領域が繋がっており、このまま領域抽出を行った場合、以下のように1つの固まりとして抽出されてしまいます。

sure background を抽出する
確実に背景といえる領域を sure background といいます。 膨張演算により、前景領域 (コインの部分) を膨張させ、膨張後もなお背景である部分は sure background といえます。
In [2]:
# sure background 領域を抽出する。
sure_bg = cv2.dilate(bin_img, kernel, iterations=3)
imshow(sure_bg)

Advertisement
sure foreground を抽出する
確実に前景といえる領域を sure foreground といいます。 2値画像に対して距離変換を行ったあと、背景からの距離が一定以上の領域のみ前景とすることで、sure foreground が抽出できます。
In [3]:
# 距離マップを作成する。
dist = cv2.distanceTransform(bin_img, cv2.DIST_L2, 5)
imshow(dist)
# sure foreground 領域を抽出する。
ret, sure_fg = cv2.threshold(dist, 0.5 * dist.max(), 255, cv2.THRESH_BINARY)
sure_fg = sure_fg.astype(np.uint8) # float32 -> uint8
imshow(sure_fg)

前景か背景か判断できない領域を抽出する
確実に背景といえる領域 (sure background) から確実に前景といえる領域 (sure foreground) の差分をとることで、前景か背景か判断できない領域を抽出します。
In [4]:
# 前景か背景か判断できない領域を抽出する。
unknown = cv2.subtract(sure_bg, sure_fg)
imshow(unknown)

各領域にラベル付けを行う
connectedComponents() で確実に前景といえる部分をラベルを付けます。connectedComponents() では、背景が0で、各物体が 1, 2, … とラベル付けされます。そこで、ラベルを1ずつ増やして、前景か背景か判断できない領域がラベル0となるように調整します。
- ラベル0: 前景か背景か判断できない領域
- ラベル1: 背景
- ラベル2以上: 物体
In [5]:
# sure foreground にたいして、ラベル付を行う。
ret, markers = cv2.connectedComponents(sure_fg)
# 前景か背景か判断できない領域はラベル0
markers += 1
markers[unknown == 255] = 0
fig, ax = plt.subplots(figsize=(6, 6))
ax.imshow(markers, cmap="tab20b")
plt.show()

Advertisement
watershed アルゴリズムを適用する
watershed アルゴリズムを適用すると、各コインがそれぞれ別れた形でラベル付けされます。
In [6]:
# watershed アルゴリズムを適用する。
markers = cv2.watershed(img, markers)
fig, ax = plt.subplots(figsize=(6, 6))
ax.imshow(markers, cmap="tab20b")
plt.show()

watershed アルゴリズムの結果を元に輪郭抽出を行う
watershed アルゴリズムにより、各物体が区別できるようにラベリングが行われたので、これを元に輪郭抽出を行います。
In [7]:
labels = np.unique(markers)
coins = []
for label in labels[2:]: # 0:背景ラベル 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)

参考文献
-
前の記事
OpenCV – floodFill() で指定した点と同じ色を塗りつぶす方法 2020.09.01
-
次の記事
OpenCV – cv2.compareHist で画像のヒストグラムを比較する方法 2020.09.02