OpenCV – watershed で画像のセグメンテーションを行う方法

目次

概要

OpenCV の cv2.watershed() を使用して、画像のセグメンテーションを行う方法を紹介します。 このアルゴリズムは、単に二値化しただけでは各領域が連結してしまい、個々のインスタンスとして区別できない場合に有効なセグメンテーション方法です。

手順

次の画像の各コインの輪郭を取得することを目指します。

sample.jpg

2 値化する

In [1]:
import cv2
from IPython.display import Image, display


def imshow(img):
    """ndarray 配列をインラインで Notebook 上に表示する。"""
    ret, encoded = cv2.imencode(".jpg", img)
    display(Image(encoded))
In [2]:
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)

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

In [3]:
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)

sure background を抽出する

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

In [4]:
# sure background 領域を抽出する。
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
img_sure_bg = cv2.dilate(img_bin, kernel, iterations=3)
imshow(img_sure_bg)

sure foreground を抽出する

確実に前景といえる領域を sure foreground といいます。 2 値画像に対して距離変換を行ったあと、背景からの距離が一定以上の領域のみ前景とすることで、sure foreground が抽出できます。

In [5]:
# 距離マップを作成する。
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)

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

確実に背景といえる領域 (sure background) から確実に前景といえる領域 (sure foreground) の差分をとることで、前景か背景か判断できない領域を抽出します。

In [6]:
# 前景か背景か判断できない領域を抽出する。
img_unknown = cv2.subtract(img_sure_bg, img_sure_fg)
imshow(img_unknown)

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

connectedComponents() で確実に前景といえる部分をラベルを付けます。connectedComponents() では、背景が 0 で、各物体が 1, 2, … とラベル付けされます。そこで、ラベルを 1 ずつ増やして、前景か背景か判断できない領域がラベル 0 となるように調整します。

  • ラベル 0: 前景か背景か判断できない領域
  • ラベル 1: 背景
  • ラベル 2 以上: 物体
In [7]:
# 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]
2024-07-28T14:38:48.722467 image/svg+xml Matplotlib v3.7.1, https://matplotlib.org/

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

cv2.watershed() を使用して、Watershed アルゴリズムによるセグメンテーションを行います。アルゴリズムが完了すると、第 2 引数で渡した markers のうち、0 のラベルが境界ラベル (-1) または 1 以上のラベルのいずれかに確定します。 その結果、各コインがそれぞれ別々にラベル付けされます。

  • ラベル -1: 境界
  • ラベル 1: 背景
  • ラベル 2 以上: 物体
In [8]:
# 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]
2024-07-28T14:38:48.846892 image/svg+xml Matplotlib v3.7.1, https://matplotlib.org/

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

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

In [9]:
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)

参考文献

コメント

コメントする

目次