【画像処理】PythonでOpenCVを使ったラベリング処理


PythonでOpenCVを使ったラベリング処理
前回の講座では[PythonでOpenCVを使わずにラベリング処理](https://qiita.com/spc_ehara/items/8d2348f38de54b6a24dc)を学び、ラベリングの仕組みを理解できたかと思います。

今回は「PythonでOpenCVを使ったラベリング処理」を学んでいきます。
それぞれのバージョンはPython 3.8.2、OpenCV 4.2.0になります。また、今回の記事の内容はOpenCVの[公式ドキュメント](https://docs.opencv.org/4.2.0/d3/dc0/group__imgproc__shape.html#gaedef8c7340499ca391d459122e51bef5)を参考にしています。

# OpenCVでラベリング処理
OpenCVに実装されているラベリング処理は大きく分けて、
①簡易版ラベリング(connectedComponents)
②詳細版ラベリング(connectedComponentsWithStats)

以上の2種類があります。
それぞれの違いと具体的な使い方について説明します。

# 簡易版ラベリング処理
簡易版は、戻り値が`ラベル数`と`入力画像と同じ大きさで、ラベル番号が振られた配列`の2つのみです。

```Python:connectedComponents.py

# 簡易版ラベリング処理
retval, labels = cv2.connectedComponents(image)

# 簡易版ラベリング処理(アルゴリズムを指定できる)
retval, labels = cv2.connectedComponentsWithAlgorithm(image, connectivity, ltype, ccltype)

```

|Parameters|説明|
|:--:|:--:|
|image|8ビットの2値化画像(CV_8U)|
|retval|ラベル数|
|labels|ラベル番号が振られた配列(入力画像と同じ大きさ)|
|connectivity|近傍指定(8 or 4)|
|ltype|戻り値labelsの型を指定(CV_32S or CV_16U)|
|ccltype|[ラベリングアルゴリズムを指定(SAUF or BBDT)](#ラベリングアルゴリズムの違いsauf-or-bbdt)|


### 実装
実際の実装はこのようになります。

```Python:connectedComponents_easy.py

import sys
import numpy as np
import cv2
import random

# 2値化
def binarize(src_img, thresh, mode):
    gray_img = cv2.cvtColor(src_img, cv2.COLOR_RGB2GRAY)
    bin_img = cv2.threshold(gray_img, thresh, 255, mode)[1]
    return bin_img

# ラベルテーブルの情報を元に入力画像に色をつける
def put_color_to_objects(src_img, label_table):
    label_img = np.zeros_like(src_img)
    for label in range(label_table.max()+1):
        label_group_index = np.where(label_table == label)
        label_img[label_group_index] = random.sample(range(255), k=3)
    return label_img

if __name__ == "__main__":
    
    src_img = cv2.imread(sys.argv[1])
    bin_img = binarize(src_img, 180, cv2.THRESH_BINARY_INV)
    
    retval, labels = cv2.connectedComponents(bin_img)
    cv2.imwrite("labels.png", put_color_to_objects(src_img, labels))

```

|1.1 入力画像|1.2 2値化画像|1.3 処理画像|
|:--:|:--:|:--:|
|![spc_logo.png](https://user-images.githubusercontent.com/74283288/98771500-f0f75e80-2427-11eb-819e-f392656a1fbc.png)src_img.png|![binarize.png](https://user-images.githubusercontent.com/74283288/108004827-4e5c0680-703a-11eb-9c4c-0eb7bd03c1bc.png)bin_img.png|![label.png](https://user-images.githubusercontent.com/74283288/107470976-a664ba00-6baf-11eb-8e8d-6fe30bc44cd4.png)labels.png|

「1.3 処理画像」のようにラベリング処理ができたことが確認できると思います。
単純にラベリング処理された結果のみが必要な場合はこれで十分ですが、物体の位置や面積が必要な場合は次の詳細版ラベリング処理を使うことで、簡単に取得できます。

# 詳細版ラベリング処理
簡易版の戻り値2つに加えて、`物体ごとの座標`や`面積`、`物体の中心座標`が戻り値に含まれています。

```Python:

# 詳細版ラベリング処理
retval, labels, stats, centroids = cv.connectedComponentsWithStats(image)

# 詳細版ラベリング処理(アルゴリズムを指定できる)
retval, labels, stats, centroids = cv.connectedComponentsWithStatsWithAlgorithm(image, connectivity, ltype, ccltype)

```


|Parameters|説明|
|:--:|:--:|
|image|8ビットの2値化画像(CV_8U)|
|retval|ラベル数|
|labels|ラベル番号が振られた配列(入力画像と同じ大きさ)|
|stats|物体ごとの座標と面積(ピクセル数)|
|centroids|物体ごとの中心座標|
|connectivity|近傍指定(8 or 4)|
|ltype|戻り値labelsの型を指定(CV_32S or CV_16U)|
|ccltype|[ラベリングアルゴリズムを指定(SAUF or BBDT)](#ラベリングアルゴリズムの違いsauf-or-bbdt)|

### 実装
実際の実装はこのようになります。

```Python:connectedComponentsWithStats.py

import sys
import numpy as np
import cv2
import random

# 2値化
def binarize(src_img, thresh, mode):
    gray_img = cv2.cvtColor(src_img, cv2.COLOR_RGB2GRAY)
    bin_img = cv2.threshold(gray_img, thresh, 255, mode)[1]
    return bin_img

# ラベルテーブルの情報を元に入力画像に色をつける
def put_color_to_objects(src_img, label_table):
    label_img = np.zeros_like(src_img)
    for label in range(label_table.max()+1):
        label_group_index = np.where(label_table == label)
        label_img[label_group_index] = random.sample(range(255), k=3)
    return label_img

# 各ラベルの座標と面積を描画する
def draw_stats(src_img, stats):
    stats_img = src_img.copy()
    for coordinate in stats[1:]:
        left_top = (coordinate[0], coordinate[1])
        right_bottom = (coordinate[0] + coordinate[2], coordinate[1] + coordinate[3])
        stats_img = cv2.rectangle(stats_img, left_top, right_bottom, (0, 0, 255), 1)
        stats_img = cv2.putText(stats_img, str(coordinate[4]), left_top, cv2.FONT_HERSHEY_PLAIN, 1, (0, 0, 255), 1)
    return stats_img

# 物体の中心座標を描画する
def draw_centroids(src_img, centroids):
    centroids_img = src_img.copy()
    for coordinate in centroids[1:]:
        center = (int(coordinate[0]), int(coordinate[1]))
        centroids_img = cv2.circle(centroids_img, center, 1, (0, 0, 255))
    return centroids_img

if __name__ == "__main__":
    
    src_img = cv2.imread(sys.argv[1])
    bin_img = binarize(src_img, 180, cv2.THRESH_BINARY_INV)
    
    retval, labels, stats, centroids = cv2.connectedComponentsWithStats(bin_img)
    stats_img = draw_stats(src_img, stats)
    centroids_img = draw_centroids(stats_img, centroids)

    cv2.imwrite("labels.png", put_color_to_objects(src_img, labels))
    cv2.imwrite("centroids_img.png", centroids_img)

```

|2.1 入力画像|2.2 2値化画像|2.3 処理画像|
|:--:|:--:|:--:|
|![spc_logo.png](https://user-images.githubusercontent.com/74283288/98771500-f0f75e80-2427-11eb-819e-f392656a1fbc.png)src_img.png|![binarize.png](https://user-images.githubusercontent.com/74283288/108004827-4e5c0680-703a-11eb-9c4c-0eb7bd03c1bc.png)bin_img.png|![label.png](https://user-images.githubusercontent.com/74283288/108007796-283a6480-7042-11eb-9c45-2e17a293ddb7.png)centroids_img.png|

「2.3 処理画像」のようにラベリング処理に加えて、各物体の位置と面積、中心座標が取得できたことが確認できると思います。


# ラベリングアルゴリズムの違い(SAUF or BBDT)

OpenCVで指定できるアルゴリズムは「`SAUF`」または「`BBDT`」の2つで、指定の方法が以下の3つあります。公式ドキュメントは[こちら](https://docs.opencv.org/4.2.0/d3/dc0/group__imgproc__shape.html#ga5ed7784614678adccb699c70fb841075)。

||8近傍|4近傍|
|:--|:--:|:--:|
|cv2.CCL_WU|SAUF|SAUF|
|cv2.CCL_DEFAULT|BBDT|SAUF|
|cv2.CCL_GRANA|BBDT|SAUF|

この2つのアルゴリズムの違いは、ラベル割り当ての順番です。
`SAUF`は、必ず上の行からラベルが割り当てられるのに対して、`BBDT`は、必ずしも上の行からラベルが割り当てられるとは限りません。

今回は、`SAUF`と`BBDT`の違いを確認するために、実験を行いました。
実験方法としては、「それぞれのアルゴリズムで同じ画像からラベリングされたオブジェクトを小さいラベル番号から1つずつ出力し、その順番が異なれば、そこに差分がある」としました。
そして実際に実験した結果、同じ8近傍でも複雑な画像は特に`SAUF`と`BBDF`の差分が出ることがわかりました。(今回の実験では500個以上のオブジェクトを比較したため、途中結果は載せずに結論のみ記載しています。)


以下、各アルゴリズムでの速度比較になります。比較に使用した画像は、「1.1 入力画像(3264×2448)」です。

簡易版(connectedComponentsWithAlgorithm)

||8近傍|4近傍|
|:--|:--:|:--:|
|SAUF|10.1053ms|10.2545ms|
|BBDT|8.1517ms|-|


詳細版(connectedComponentsWithStatsWithAlgorithm)

||8近傍|4近傍|
|:--|:--:|:--:|
|SAUF|12.1041ms|12.4613ms|
|BBDT|9.6318ms|-|


上記の結果から、`BBDT`のほうが実行速度の面では有利なので、通常はアルゴリズムを意識する必要はないと言えます。(デフォルト設定が`cv2.CCL_DEFAULT`のため)しかし、ラベル割り当ての順番を保証する必要があるといった場合には、8近傍でもSAUFアルゴリズムを使うために、`cv2.CCL_WU`の設定にするべきでしょう。


# さいごに
今回は、「PythonでOpenCVを使ったラベリング処理」について解説しました。

それでは引き続きよろしくお願いいたします。

目次は以下の記事からご覧になれます。

https://qiita.com/spc_ehara/items/e425b6dcc0398299c40d#%E7%9B%AE%E6%AC%A1