OpenCVとscikit-imageでの画像セグメンテーション

はじめに

近年はディープラーニングで画像のセグメンテーションを行うのがメジャーかもしれませんが、 訓練データの準備や訓練かかるコストなどを考えると、適用が難しい場面もあるかと思います。 今回はディープラーニングなしのセグメンテーション手段を紹介します。

以前にいくつかの手法で行った前景抽出は前景と背景の2通りの分離でしたが、今回は多種類の分離をしなければなりません。 よって今回は二値化を使用して分離する代わりに、減色処理を行います。 これは色の特徴を用いたセグメンテーションとなるので、多数の色を用いたカラフルなセグメントを分離するのには向きません。

手法としては以下の二つを紹介いたします。

  • Watershed: このアルゴリズムに関する解説はよく見かけますが、大抵は1種類の対象を抽出するもので多種類を抽出するものは少ないと思います。
  • Region Adjacency Graph (RAG): 隣接領域の関係、類似領域を表現したグラフ(RAG)を用いてセグメンテーションを行います。

今回セグメンテーションを行う対象は以下になります。 以下の画像は https://pixabay.com/ja/photos/%e7%9f%b3%e3%82%92%e6%9e%9c%e3%81%9f%e3%81%99-%e8%89%b2%e3%81%a8%e3%82%8a%e3%81%a9%e3%82%8a-%e6%96%87%e5%ad%97-1743645/ からダウンロードをしました。重なりが多く、セグメント対象も多いので若干難易度は高め画像です。 使用する言語はPythonでJupyter Notebookから実行します。 WatershedはOpenCV、RAGに関してはscikit-imageを使用します。

Watershed(OpenCV)

Watershedは領域ごとにその領域に属すると確信できる部分と、不確かな部分(領域間の境界部分)にそれぞれラベルを付けて、 確信領域を徐々に広げて元画像の画素値の差により境界を見つけるという手法です。

今回はkmeansにより減色を行い、それぞれの色ごとに領域の分割を行って、色領域を数回収縮処理(erode)することで その領域に属すると確信できる部分を取得します。それ以外の部分を不確かな部分と設定します。不確かな部分に境界が存在すると想定しています。 減色時の色数や収縮回数などハイパーパラメータとなる部分はそれなりにうまくいくように調整しています。

以下のコードは、openCVにより画像を読み込みk-meansにより減色処理を行う部分になります。 色の数は6色に設定しています。 ここでは初期化にcv2.KMEANS_RANDOM_CENTERSを使用していますが、 k-means++にしたい場合はcv2.KMEANS_PP_CENTERSを使用してください。

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

img = cv2.imread('sample.jpg')

z = img.reshape((-1,3)).astype(np.float32)

criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
ret, label, center = cv2.kmeans(
    z, 6, None, criteria, attempts=10, flags=cv2.KMEANS_RANDOM_CENTERS
)

center = np.uint8(center)
res = center[label.flatten()]
img_q = res.reshape((img.shape))
plt.imshow(img_q)
    

次にk-meansで得られたそれぞれの色領域を抽出し、それに対して収縮処理を行うことで確信領域を取得します。 4回の収縮処理を行っています。また状況により収縮後にクロージングやオープニング処理を行ってもいいかと思います。

    fg = np.zeros_like(img)
kernel = np.ones((3,3),np.uint8)

for i in range(len(center)):
    tmpimg = np.zeros_like(img)
    tmpimg[img_q == center[i]] = 255
    tmpimg = cv2.erode(tmpimg, kernel, iterations = 4)
    fg += tmpimg

fg = cv2.cvtColor(fg, cv2.COLOR_BGR2GRAY)

plt.imshow(fg, cmap="gray")
    

差分を取ることで不確かな部分を取得します。

    unknown = cv2.subtract(np.ones_like(fg) * 255, fg)
plt.imshow(unknown, cmap="gray")
    

連結領域ごとにマーカーを作成し、Watershedアルゴリズムを適用します。 その後、マーカー領域ごとにランダムで着色を行い画像を保存します。

    ret, markers = cv2.connectedComponents(fg)
markers = markers+1
markers[unknown==255] = 0

markers = cv2.watershed(img_q, markers)

svimg = img.copy()
for i in range(ret):
    svimg[markers == i+1] = list(np.random.choice(range(256), size=3)) 

cv2.imwrite("result.png", svimg)
plt.imshow(markers)
    

若干精度が甘いですが、多種類の重なった対象に対してセグメンテーションができていることがわかります。

Region Adjacency Graph (RAG) (scikit-image)

Region Adjacency Graph (RAG)は隣接領域間を重み付き無向エッジでつないだグラフですが、 ここでは同色ごとに領域分割した画像において、scikit-imageのrag_boundaryやrag_mean_color関数にてconnectivityで定義された値が画素間距離の2乗以下だった場合、エッジで接続されているとみなします。 connectivityのデフォルトは2なので注目画素を囲む8近傍画素は隣接しているとみなされエッジが構築されます。 ただしconnectivityは1から入力の次元数までしか設定できません。

RAGを使用する利点はエッジに重みがつけられることです。 merge_hierarchicalを使用して、重みの小さい類似領域同士をマージすることができます。 エッジ重みを変えることでセグメンテーションを制御することができます。

scikit-imageを使用して画像を読み込み、segmentation.slicでk-meansにより減色処理を行ってから、 ソーベルフィルターでエッジ(グラフの辺ではなく近傍画素値の差としてのエッジ)を検出します。

    from skimage import io, data, segmentation, color, filters, feature, morphology
from skimage.future import graph
import numpy as np

file_name = "sample.jpg"
img = io.imread(file_name)[:,:,:3]
gray = color.rgb2gray(img[:,:,:3])

sobel = (filters.sobel(gray) > 0.07).astype(np.float32)
labels = segmentation.slic(img, compactness=30, n_segments=150, start_label=1)
    

graph.rag_boundaryを使用して、エッジマップを使用したRAGグラフを構築することができます。 エッジマップにおいて境界に沿った画素値の平均がエッジ(辺)の重みになります。 作成したragに対して、graph.merge_hierarchicalで重みの閾値より小さい類似領域をマージします。 最後にそれぞれの領域に対し画素値を平均したもので塗りつぶしてセグメンテーション処理画像を出力します。

    def weight_boundary(graph, src, dst, n):
    default = {'weight': 0.0, 'count': 0}

    count_src = graph[src].get(n, default)['count']
    count_dst = graph[dst].get(n, default)['count']

    weight_src = graph[src].get(n, default)['weight']
    weight_dst = graph[dst].get(n, default)['weight']

    count = count_src + count_dst
    return {
        'count': count,
        'weight': (count_src * weight_src + count_dst * weight_dst)/count
    }

def merge_boundary(graph, src, dst):
    pass

rag = graph.rag_boundary(labels, sobel)
labels2 = graph.merge_hierarchical(labels, rag, thresh=0.02, rag_copy=False,
                                    in_place_merge=True,
                                    merge_func=merge_boundary,
                                    weight_func=weight_boundary)

out = color.label2rgb(labels2, img, kind='avg', bg_label=0)
io.imshow(out)
    

RAGもセグメンテーションができてはいますが、精度としてはwatershedと比較して一長一短というところでしょうか。 segmentation.slicのパラメータやマージの閾値の値を調節するとセグメンテーションが変わりやすいので画像の特徴に合わせて調節すればいいでしょう。 またcannyでも試しましたが、この場合はsobelの方が精度は良かったです。

まとめ

OpenCVやscikit-imageを使用して画像のセグメンテーションを行いました。 Watershed、RAGともに設定するパラメータでセグメンテーションのされ方が変わりやすいため、 これらを使用して精度を上げるにはmerge_func・weight_funcで頑張るか、適当なパラメータを探す必要があります。適応的な方法があればいいですが。

DeepLabV3といったディープラーニング手法のものよりも精度は低そうですが、 対象の位置や数、種別などを大まかに知りたい場合などはDNNの学習などの手間がないので気軽に使えそうに思います。

Reference

  1. https://docs.opencv.org/4.x/
  2. https://scikit-image.org/docs/stable/api/api.html
Next Post Previous Post
No Comment
Add Comment
comment url