Pynote

Python、機械学習、画像処理について

OpenCV - Watershed アルゴリズムで物体の輪郭抽出を行う

試した環境

手順

画像を読み込む。

今回使用した画像

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

# 画像を読み込む。
src = cv2.imread('coins.jpg')

plt.imshow(cv2.cvtColor(src, cv2.COLOR_BGR2RGB))
plt.show()

大津の手法で2値化する。

pynote.hatenablog.com

# グレースケール形式に変換する。
gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY)

# 大津の手法で2値化する。
_, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)

plt.imshow(binary, cmap=plt.cm.gray)
plt.show()


opening 処理でノイズを除去する。

pynote.hatenablog.com

# opening 処理でノイズを除去する。
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3));
opening = cv2.morphologyEx(
    binary, cv2.MORPH_OPEN, kernel, iterations=2)

plt.imshow(opening, cmap=plt.cm.gray)
plt.show()


そのまま輪郭抽出を行った場合 (失敗例)

2値化画像に対して、そのまま輪郭抽出を行ってみる。
すると、コイン同士が接しているため、すべて1つの物体として検出されてしまう。
各コインをそれぞれ区別したいので、watershed アルゴリズムを適用する。

# 2値化画像に対して、輪郭抽出を実行する。
ret, contours, hierarchy = cv2.findContours(
    opening, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
drawn = cv2.drawContours(src.copy(), contours, -1, (0, 255, 0), 2)

plt.imshow(cv2.cvtColor(drawn, cv2.COLOR_BGR2RGB))
plt.show()


sure background を抽出する。

pynote.hatenablog.com

確実に背景といえる領域 (sure background) を抽出する。
dilate 処理により、前景領域 (コインの部分) を膨張させる。 膨張後もなお背景である部分は sure background といえる。

# sure background 領域を抽出する。
sure_bg = cv2.dilate(opening, kernel, iterations=2)

plt.imshow(sure_bg, cmap=plt.cm.gray)
plt.show()


sure foreground を抽出する。

pynote.hatenablog.com

確実に前景といえる領域 (sure foreground) を抽出する。
距離変換は背景から遠い部分 (コインの中心) ほど大きい値になるような変換である。
その後、背景からの距離が一定以上の領域のみ前景とすることで、sure foreground を抽出する。

# 距離マップを作成する。
dist = cv2.distanceTransform(opening, cv2.DIST_L2, 5)
plt.imshow(dist, cmap=plt.cm.gray)
plt.show()


# sure foreground 領域を抽出する。
ret, sure_fg = cv2.threshold(dist, 0.5 * dist.max(), 255, cv2.THRESH_BINARY)
plt.imshow(sure_fg, cmap=plt.cm.gray)
plt.show()


前景か背景か判断できない領域を抽出する。

確実に背景といえる領域から確実に前景といえる領域の差分をとる。

pynote.hatenablog.com

# 前景か背景か判断できない領域を抽出する。
sure_fg = np.uint8(sure_fg)
unknown = cv2.subtract(sure_bg, sure_fg)

plt.imshow(unknown, cmap=plt.cm.gray)
plt.show()


各領域にラベル付けを行う。

connectedComponents() で確実に前景といえる部分をラベル付けする。
ラベルは背景が0で、各物体が1, 2, ... とラベル付けされる。

pynote.hatenablog.com

ラベルを1ずつ増やして、前景か背景か判断できない領域をラベル0とする。

  • ラベル0: 前景か背景か判断できない領域
  • ラベル1: 背景
  • ラベル2以上: 物体
# sure foreground にたいして、ラベル付を行う。
ret, markers = cv2.connectedComponents(sure_fg)

# 前景か背景か判断できない領域はラベル0
markers += 1
markers[unknown == 255] = 0

plt.imshow(markers)
plt.show()


watershed アルゴリズムを適用する。

# watershed アルゴリズムを適用する。
markers = cv2.watershed(src, markers)
plt.imshow(markers)
plt.show()


watershed アルゴリズムの結果を元に輪郭抽出を行う。

watershed アルゴリズムにより、各物体が区別できるようにラベリングが行われた。
これを元に輪郭抽出を行う。

drawn = src.copy()

labels = np.unique(markers)
for label in labels[2:]:  # 0:背景ラベル 1:境界ラベル は無視する。
    
    # ラベル label の領域のみ前景、それ以外は背景となる2値画像を作成する。
    target = np.where(markers == label, 255, 0).astype(np.uint8)
    
    # 作成した2値画像に対して、輪郭抽出を行う。
    ret, contours, hierarchy = cv2.findContours(
        target, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    # 輪郭抽出の結果を描画する。
    color = np.random.randint(0, 255, 3).tolist()
    cv2.drawContours(drawn, contours, -1, color, 3)

plt.imshow(drawn)
plt.show()