【OpenCV-Python】ラベリング(connectedComponents)

ラベリングとは、二値化された画像において、画素がつながっている領域に対して同じ番号(ラベル)を与える処理のことを言います。

このつながっている領域のことをブロブ(blob)といいますが、このブロブに対して、面積や幅、高さなどを求め、キズや打痕、汚れなどの欠陥検査を行うための前処理として、ラベリング処理が用いられることが多いです。

↓ラベリング

詳細はこちら↓を参照ください。

ラベリング

 

OpenCVでラベリング処理を行うには、connectedComponents()関数を用います。

関連して、connectedComponentsWithStats()connectedComponentsWithStatsWithAlgorithm()という関数もあります。

 

connectedComponents()関数は、二値化画像に対して、ラベリング画像のみを出力します。

 

connectedComponentsWithStats()関数は、ラベリング画像に追加して、さらにブロブの領域(位置、幅、高さ)と面積、重心の情報を出力します。

 

connectedComponentsWithStatsWithAlgorithm()関数は、さらに、ラベリング処理時の4連結、8連結の指定や出力するラベリング画像のデータ型の指定、ラベリング処理アルゴリズムの指定が可能になっています。

処理アルゴリズムについては、詳細は分からなかったのですが、こちら↓を参照くだだい。

https://docs.opencv.org/4.6.0/d3/dc0/group__imgproc__shape.html#ga5ed7784614678adccb699c70fb841075

 

connectedComponents()関数

connectedComponents( image[, labels[, connectivity[, ltype]]] ) -> retval, labels
引数 説明
image ラベリング処理を行う入力画像
8bit1chである必要があります。
labels ラベリング結果を格納する画像データ
戻り値で戻すこともできるので、指定しないか、Noneを指定でも大丈夫です。
connectivity 8連結の場合は 8、4連結の場合は 4 を指定します。
ltype 出力されるラベリング画像の型を指定します。
cv2.CV_16U もしくは cv2.CV_32S(初期値)
(戻り値)retval ラベルの個数が返されます。
ただし、背景も1つとしてカウントされます。
(戻り値)labels 出力されるラベリング画像

connectedComponentsWithStats()関数

connectedComponentsWithStats( image[, labels[, stats[, centroids[, connectivity[, ltype]]]]] ) -> retval, labels, stats, centroids
引数 説明
image ラベリング処理を行う入力画像
8bit1chである必要があります。
labels ラベリング結果を格納する画像データ
戻り値で戻すこともできるので、指定しないか、Noneを指定でも大丈夫です。
connectivity 8連結の場合は 8、4連結の場合は 4 を指定します。
ltype 出力されるラベリング画像の型を指定します。
cv2.CV_16U もしくは cv2.CV_32S(初期値)
(戻り値)retval ラベルの個数が返されます。
ただし、背景も1つとしてカウントされます。
(戻り値)labels 出力されるラベリング画像
(戻り値)stats 各ラベルごとの領域の情報が格納されます。
[領域の左上のx座標, 領域の左上のy座標, 領域の幅, 領域の高さ, 面積]
面積は領域の画素数です。
(戻り値)centroids 各ラベルごとの重心の情報が格納されます。

※labels, stats, centroidsの最初のデータは背景の情報となります。

 

connectedComponentsWithStats()関数

connectedComponentsWithStatsWithAlgorithm( image, connectivity, ltype, ccltype[, labels[, stats[, centroids]]] ) -> retval, labels, stats, centroids
引数 説明
image ラベリング処理を行う入力画像
8bit1chである必要があります。
labels ラベリング結果を格納する画像データ
戻り値で戻すこともできるので、指定しないか、Noneを指定でも大丈夫です。
connectivity 8連結の場合は 8、4連結の場合は 4 を指定します。
ltype 出力されるラベリング画像の型を指定します。
cv2.CV_16U もしくは cv2.CV_32S(初期値)
ccltype ラベリング処理アルゴリズムを指定します。
cv2.CCL_DEFAULT, cv2.CCL_WU, cv2.CCL_GRANA, cv2.CCL_BOLELLI, cv2.CCL_SAUF, cv2.CCL_BBDT, cv2.CCL_SPAGHETTI
(参考)
https://docs.opencv.org/4.6.0/d3/dc0/group__imgproc__shape.html#ga5ed7784614678adccb699c70fb841075
(戻り値)retval ラベルの個数が返されます。
ただし、背景も1つとしてカウントされます。
(戻り値)labels 出力されるラベリング画像
(戻り値)stats 各ラベルごとの領域の情報が格納されます。
[領域の左上のx座標, 領域の左上のy座標, 領域の幅, 領域の高さ, 面積]
面積は領域の画素数です。
(戻り値)centroids 各ラベルごとの重心の情報が格納されます。

※labels, stats, centroidsの最初のデータは背景の情報となります。

connectedComponentsWithStats()関数は、おそらく下記のようにするのと同じです。(アルゴリズムの指定がちょっと怪しい。。)

retval, labels, stats, centroids = cv2.connectedComponentsWithStatsWithAlgorithm(img, 8, cv2.CV_32S, cv2.CCL_DEFAULT)

 

ラベリング処理例

最初にも触れましたが、ラベリングは画像中のキズや打痕、汚れなどの検出に使われる場合が多いです。

下図のように画像中にキズ、打痕、ホコリのようなものがあるときに、黒い部分の面積が大きい部分を検出する例のサンプルプログラムを作成しました。

import cv2
import numpy as np

# 8ビット1チャンネルのグレースケールとして画像を読み込む
img = cv2.imread("image.jpg", cv2.IMREAD_GRAYSCALE) 

# 画像表示用に入力画像をカラーデータに変換する
img_disp = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)

# 二値化(二値化して白黒反転)
retval, img = cv2.threshold(img, 200, 255, cv2.THRESH_BINARY_INV)

# クロージング処理(細切れ状態を防ぐため)
kernel = np.ones((3, 3), np.uint8)
img = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel, iterations=15)

# ラベリング
retval, labels, stats, centroids = cv2.connectedComponentsWithStats(img)

# 結果表示
for i in range(1, retval):
    x, y, width, height, area = stats[i] # x座標, y座標, 幅, 高さ, 面積

    if area > 10: # 面積が10画素以上の部分
        cv2.rectangle(img_disp,(x,y),(x+width,y+height),(0,0,255),2)
        cv2.putText(img_disp, f"[{i}]:{area}", (x, y-10), cv2.FONT_HERSHEY_PLAIN, 1, (255, 0, 0), 1, cv2.LINE_AA)

cv2.imshow("image", img_disp)
cv2.waitKey()

実行結果

 

connectedComponentsWithStatsとfindContoursの違い

ブロブの幅や高さ、面積などを解析することをブロブ解析と言いますが、connectedComponentsWithStats()関数で取得できる情報は、ブロブの領域(傾いていない矩形領域)、面積、重心だけですが、輪郭処理ベースのfindContours()関数では、ブロブの傾いた領域や外接円、楕円近似、直線近似、周囲長、凸法など出来る事が多くあるので、必要に応じて使い分けるといいと思います。

ただし、connectedComponentsWithStats()関数はブロブの画素に関する情報を取得しますが、findContours()関数は、あくまでの輪郭の情報を取得するので注意してください。

例えば、下図のような画像のとき

connectedComponentsWithStats()関数では面積(画素数)は8となりますが、findContours()関数では面積(輪郭の内側の面積、上図の赤線の四角の面積)は4となり、さらに輪郭の内側に白では無い画素があっても考慮されません。

 

関連記事

【OpenCV-Python】ラベリング(connectedComponents)

【OpenCV-Python】findContoursによる輪郭検出

【OpenCV-Python】輪郭(contour)の面積(contourArea)

【OpenCV-Python】輪郭(contour)の矩形領域の取得

膨張・収縮・オープニング・クロージング

【OpenCV-Python】円近似(疑似逆行列を用いた方法)

OpenCVには座標を楕円で近似する関数(fitEllipse)はあるものの、円で近似するfitCircle()のような関数はありません。

そこで、最小二乗法的に座標を円で近似するfitCircle()関数を作ってみました。

円の最小二乗法については、以前、書きました。

一般式による最小二乗法(円の最小二乗法)

この記事で行っている最小二乗法はベタに式を2乗して、未知数で偏微分する必要があるので、計算が少々面倒です。

そこで、今回は、疑似逆行列を使って円近似を行いたいと思います。

 

疑似逆行列を用いた円近似

円上の座標を(x, y)、円の中心座標を(a, b)、半径を r  とすると、円の公式は

$$(x-a)^{2}+(y-b)^{2}=r^{2}$$

この式を展開すると

$$x^{2}-2ax+a^{2}+y^{2}-2by+b^{2}=r^{2}$$

となります。

ここで、

$$\begin{cases}A = -2a\\B = -2b\\C = a^{2}+b^{2}-r^{2}\end{cases}$$

と置き、式を整理すると、

$$Ax+By+C=-x^{2}-y^{2}$$

となります。

近似に用いる点の座標を \((x_{1},y_{1}), (x_{2},y_{2}), (x_{3},y_{3})・・・\)とすると

$$\begin{cases}Ax_{1}+By_{1}+C=-x_{1}^{2}-y_{1}^{2}\\Ax_{2}+By_{2}+C=-x_{2}^{2}-y_{2}^{2}\\Ax_{3}+By_{3}+C=-x_{3}^{2}-y_{3}^{2}\\      :\end{cases}$$

のように、近似する座標の点数(円近似の場合は3点以上必要)ぶんだけの式が成り立ちます。

この連立方程式を行列で表すと、

$$\left(\begin{array}{c}x_{1}&y_{1}&1\\ x_{2}&y_{2}&1\\x_{3}&y_{3}&1\\ &:\end{array}\right)\left(\begin{array}{c}A\\ B\\C\end{array}\right)=\left(\begin{array}{c}-x_{1}^{2}-y_{1}^{2}\\ -x_{2}^{2}-y_{2}^{2}\\ -x_{3}^{2}-y_{3}^{2}\\ :\end{array}\right)$$

となるので、あとは疑似逆行列を使えば、A, B, C が求まるので、A, B, Cの値から円の中心座標の(a, b)、半径の r  が求まります。

$$\begin{cases}a=-\frac{A}{2}\\b=-\frac{B}{2}\\r=\sqrt{a^{2}+b^{2}-C}\end{cases}$$

疑似逆行列を求める部分についはnumpylinalg.pinvという関数があるので、これを用います。

 

サンプルプログラム

上記の疑似逆行列を用いた円近似の処理をまとめました。

円近似の fitCircle()関数を作成しています。

import cv2
import numpy as np

def fitCircle(contour):
    '''
    座標の円近似(最小二乗円)
    '''

    matA = []
    matB = []

    for point in contour:
        x = point[0,0]
        y = point[0,1]
        matA.append([x,y,1])
        matB.append([-x*x -y*y])

    # listからndarrayへ変換
    matA = np.array(matA, np.float32)
    matB = np.array(matB, np.float32)

    # 疑似逆行列を求める
    matA_pinv = np.linalg.pinv(matA)

    # 行列の解
    matX = matA_pinv.dot(matB)

    # 中心座標
    a = -matX[0,0]/2
    b = -matX[1,0]/2
    # 半径
    r = np.sqrt(a*a+b*b-matX[2,0])

    # 中心座標, 半径
    return (a, b), r  

##########################################################################

# 8ビット1チャンネルのグレースケールとして画像を読み込む
img = cv2.imread("image.jpg", cv2.IMREAD_GRAYSCALE) 

# 白黒反転して二値化
ret, img = cv2.threshold(img, 128, 255, cv2.THRESH_BINARY_INV)

# 一番外側の輪郭のみを取得
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE ) 

# 画像表示用に入力画像をカラーデータに変換する
img_disp = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)

# 全ての輪郭を描画
cv2.drawContours(img_disp, contours, -1, (0, 0, 255), 2)

# 輪郭の点の描画
for contour in contours:
    # 円近似
    center, r = fitCircle(contour)
    # 近似円を描画
    cv2.circle(img_disp, np.intp(center), int(r), (255, 0, 0), 2)

cv2.imshow("Image", img_disp)

# キー入力待ち(ここで画像が表示される)
cv2.waitKey()

実行結果

 

関連記事

【OpenCV-Python】findContoursによる輪郭検出

一般式による最小二乗法(円の最小二乗法)

疑似逆行列(一般逆行列)の計算と使用方法

【OpenCV-Python】円形度

円形度とは、図形の面積と周囲長の関係から、円らしさの値を求めます。

円形度の詳細は以下のページを参照ください。

円形度

 

OpenCV的には、図形の面積はcontourArea()関数で、周囲長はarcLength()関数で求める事ができるので、これを使って円形度を求めます。

 

今回は、下図のように手書きで書いた図形の中から円らしい部分を見つけたいと思います。

 

サンプルプログラム

contourArea()関数と、周囲長はarcLength()関数を使って円らしい部分を抽出するサンプルプログラムを作成しました。
円形度を求める部分は関数(circularity)にしてあります。

import cv2
import numpy as np

def circularity(contour):
    '''
    円形度を求める

    Parameters
    ----------
    contour : ndarray
        輪郭の(x,y)座標の配列

    Returns
    -------
        円形度

    '''
    # 面積
    area = cv2.contourArea(contour)
    # 周囲長
    length = cv2.arcLength(contour, True)

    # 円形度を返す
    return 4*np.pi*area/length/length

# 8ビット1チャンネルのグレースケールとして画像を読み込む
img = cv2.imread("image.jpg", cv2.IMREAD_GRAYSCALE) 

# 白黒反転して二値化
ret, img = cv2.threshold(img, 128, 255, cv2.THRESH_BINARY_INV)

# 一番外側の輪郭のみを取得
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) 

# 画像表示用に入力画像をカラーデータに変換する
img_disp = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)

# 全ての輪郭を描画
cv2.drawContours(img_disp, contours, -1, (0, 0, 255), 2)

# 輪郭の点の描画
for contour in contours:
    # 円形度の計算
    val = circularity(contour)
    # 輪郭の矩形領域
    x,y,w,h = cv2.boundingRect(contour)
    # 円形度の描画
    cv2.putText(img_disp, f"{val:.3f}", (x, y-10), cv2.FONT_HERSHEY_PLAIN, 2, (0, 255, 0), 1, cv2.LINE_AA)
    # 円らしい領域(円形度が0.85以上)を囲う
    if val > 0.85:
        cv2.rectangle(img_disp,(x-5,y-5),(x+w+5,y+h+5),(255,0,0),2) # 少し外側を囲う

cv2.imshow("Image", img_disp)

# キー入力待ち(ここで画像が表示される)
cv2.waitKey()

実行結果

 

実行結果を見ると、だいたい円っぽい部分は円形度を使うと抽出することが出来ますが、真円度的な円の正確さを求めるのには、円形度は不向きな感じがします。

 

関連記事

【OpenCV-Python】findContoursによる輪郭検出

【OpenCV-Python】輪郭(contour)の面積(contourArea)

【OpenCV-Python】輪郭の周囲長(arcLength)

【OpenCV-Python】輪郭の周囲長(arcLength)

findContours()関数などで取得した輪郭の座標から輪郭の長さを求めるにはarcLength()関数を用います。

構文

arcLength(curve, closed) ->retval
curve 輪郭を構成する輪郭のxy座標(x, y)の配列
closed 始点と終点が閉じた周囲長を求めるときはTrue
閉じていない周囲長を求めるときはFalse
(戻り値)retval 周囲長

 

サンプルプログラム

通常はfindContours()関数で取得した輪郭の座標を用いる場合が多いかと思いますが、今回は簡単のため、4点からなる周囲長を求めます。

この輪郭座標の閉じた周囲長(cloased = True)と、閉じていない周囲長(cloased = False)を求めます。

閉じた周囲長(cloased = True)

閉じていない周囲長(cloased = False)

import cv2
import numpy as np

# 輪郭の座標
contour = np.array(
    [
     [1, 2],
     [1, 4],
     [5, 4],
     [5, 2]
    ],
    dtype = np.int32
    )

# 閉じた周囲長
perimeter = cv2.arcLength(contour, True) 
print(perimeter) # 12.0

# 閉じていない周囲長
perimeter = cv2.arcLength(contour, False) 
print(perimeter) # 8.0

関連記事

【OpenCV-Python】findContoursによる輪郭検出

【OpenCV-Python】輪郭(contour)の矩形領域の取得

OpenCVのfindContours関数などで得られた点の座標から、点を囲う矩形領域(四角形の領域)を取得するにはboundingRect関数を用います。

さらに、傾きを考慮した矩形領域を取得するにはminAreaRect関数を用います。

まずは、両方の関数を用いたサンプルを示します。

import cv2
import numpy as np

# 8ビット1チャンネルのグレースケールとして画像を読み込む
img = cv2.imread("sample.bmp", cv2.IMREAD_GRAYSCALE) 

contours, hierarchy = cv2.findContours(
    img, 
    cv2.RETR_EXTERNAL,      # 一番外側の輪郭のみを取得する 
    cv2.CHAIN_APPROX_NONE   # 輪郭座標の省略なし
    ) 

# 画像表示用に入力画像をカラーデータに変換する
img_disp = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)

# 輪郭の点の描画
for i, contour in enumerate(contours):
    # 輪郭を描画
    cv2.drawContours(img_disp, contours, i, (255, 0, 0), 2)

    # 傾いていない外接する矩形領域
    x,y,w,h = cv2.boundingRect(contour)
    cv2.rectangle(img_disp,(x,y),(x+w-1,y+h-1),(0,255,0),2)

    # 傾いた外接する矩形領域
    rect = cv2.minAreaRect(contour)
    box = cv2.boxPoints(rect)
    box = np.intp(box)
    cv2.drawContours(img_disp,[box],0,(0,0,255), 2)

# 画像の表示
cv2.imshow("Image", img_disp)

# キー入力待ち(ここで画像が表示される)
cv2.waitKey()

実行結果

緑の線がboundingRect関数を用いて取得した領域、赤の線がminAreaRect関数を用いた領域となります。

 

矩形領域の取得(boundingRect関数)

boundingRect( array ) -> x, y, width, height
array (x,y)座標の配列 もしくは グレースケールの画像を指定します。
(戻り値) x 取得した矩形領域の左上のx座標を取得します。
(戻り値) y 取得した矩形領域の左上のy座標を取得します。
(戻り値) width 取得した矩形領域の幅を取得します。
(戻り値) height 取得した矩形領域の高さを取得します。

注意点

戻り値で取得した幅(width)および高さ(height)の値は、実際の幅、高さ(2点間の距離)の +1 の値を取得します。

例えば、下図のように、(1, 2), (5, 2), (5, 4), (1, 4)の点を囲う矩形領域を取得すると、

x = 1

y = 2

width = 5

height = 3

となります。

import cv2
import numpy as np

contour = np.array(
    [
     [1, 2],
     [5, 2],
     [5, 4],
     [1, 4]
    ],
    dtype = np.int32
    )

# 傾いていない外接する矩形領域
ret = cv2.boundingRect(contour)

print(ret) # (1, 2, 5, 3)

傾きを考慮した矩形領域の取得(minAreaRect関数)

minAreaRect( points) -> rect
points (x,y)座標の配列
(戻り値) rect 傾いた矩形領域の
中心のxy座標 (x, y)
矩形の幅、高さ(width, height)
矩形の回転角度
のタプルで取得します。

 

点(1, 1), (0, 2), (4, 4), (5, 3) を囲う傾きを考慮したときの最小となる矩形領域の取得のサンプルを以下に示します。

import cv2
import numpy as np

contour = np.array(
    [
     [1, 1],
     [0, 2],
     [4, 4],
     [5, 3]
    ],
    dtype = np.int32
    )

# 傾いた外接する矩形領域
rect = cv2.minAreaRect(contour) # rectは中心座標(x,y), (width, height), 回転角度
cnter, size, rot = rect

print(rect) # ((2.500000238418579, 2.5), (4.919349670410156, 1.3416407108306885), 26.56505012512207)

関連記事

【OpenCV-Python】findContoursによる輪郭検出

【OpenCV-Python】矩形抽出(矩形度)

【OpenCV-Python】輪郭(contour)の面積(contourArea)

OpenCVで二値化された領域の輪郭座標は、findContours関数を使えば取得することができます。

このときの戻り値である輪郭情報(contours)をcontourArea関数へ渡し面積を求めます。

ただし、求まる面積は、あくまでも輪郭線の内側の面積(画素数ではない)となります。
(下図の例では、面積は 35 となります。)

さらに、白の領域の内側に黒の領域があっても、考慮されません。

構文

contourArea(contour[, oriented]) ->retval
contour 輪郭座標
oriented 輪郭の向きを考慮するか(True)、しないか(False)を指定します。
初期値)False
Trueのとき、輪郭座標の向きが時計周りのときは正、反時計周りのときは負となります。
Falseのとき、向きに関係なく、常に正となります。
下図参照
(戻り値)retval 輪郭の内側の面積

サンプルプログラム

import cv2
import numpy as np

# 画像データ
img = np.array(
    [[0,   0,   0,   0,   0,   0,   0,   0,   0, 0],
     [0, 255, 255, 255, 255, 255, 255, 255, 255, 0],
     [0, 255, 255, 255, 255, 255, 255, 255, 255, 0],
     [0, 255, 255,   0,   0,   0,   0, 255, 255, 0],
     [0, 255, 255,   0,   0,   0,   0, 255, 255, 0],
     [0, 255, 255, 255, 255, 255, 255, 255, 255, 0],
     [0, 255, 255, 255, 255, 255, 255, 255, 255, 0],
     [0,   0,   0,   0,   0,   0,   0,   0,   0, 0],
    ],
    dtype = np.uint8
    )

# 輪郭情報の取得
contours, hierarchy = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE) 

# 画像表示用に入力画像をカラーデータに変換する
img_disp = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)

# 輪郭ごとの処理
for i, contour in enumerate(contours):
    # 輪郭の面積を求める
    area = cv2.contourArea(contour, True)
    print(f"面積[{i}]: {area}")

    # 輪郭座標
    for point in contour:
        print(point[0])

cv2.namedWindow("Image", cv2.WINDOW_NORMAL)
cv2.imshow("Image", img_disp)

# キー入力待ち(ここで画像が表示される)
cv2.waitKey()

実行結果

このサンプルでは、画像データはnumpyの配列で疑似的に作りましたが、画像にすると下図の用な画像に対して輪郭処理をしています。

この画像では、白の外側の輪郭の面積は 35 となります。

白の内側の輪郭の面積は 13 となります。

内側の輪郭の座標は、黒の画素の上下左右方向(4近傍)に隣接している白の画素の座標となります。

 

関連記事

【OpenCV-Python】findContoursによる輪郭検出