Python 輪郭の検出とその座標の抽出「OpenCV」

 本記事では、画像ファイルの物体の輪郭を抽出する雛形コードを載せました。検出手法を関数として4つ載せました。いずれも処理の大まかな流れは、2値化してしきい値で判別します。その2値化が画像に依っては難しいのです。
 そのため、毛色の異なる次の2つのリンク先の画像とコードを参考にさせて頂きました。更に、自前で準備した画像2つを合わせた計4つに対して、処理の過程と共に結果例を順番に載せます。

▼チューリップの花の輪郭を検出する

(参考リンク)オブジェクト輪郭検出 | OpenCV / findContours を使用して画像中のオブジェクトの輪郭を検出する方法

画像の特徴1:花が沢山あって、それぞれの形状が複雑
画像の特徴2:花とそれ以外の葉っぱなどの背景との色度が明確にわかれている

f:id:HK29:20200201144740j:plain

処理1. 色調RGBをHSVへ変更
→茎と土の輪郭をぼかす

f:id:HK29:20200201150706j:plain

処理2. ガウシアンによるスムージング処理
→更に、茎や土周りの輪郭をぼかす

f:id:HK29:20200201150921j:plain

処理3. HSV色空間から、H(色相)のデータを抽出する
→ほぼ、花とそれ以外で色が分かれた。

f:id:HK29:20200201151841j:plain

処理4. マスクの作成
しきい値が明確になったため、明確にマスクを掛ける

f:id:HK29:20200201151958j:plain

処理5. オリジナル画像を元に、チューリップ花の輪郭を描画する
→ほぼ、間違いなく花を検出している。

f:id:HK29:20200201145449j:plain

検出した輪郭の座標をcsvファイルに保存する仕様にしてるため、下図のようにグラフ化できるし、必要箇所の形だけ抽出したりといったことも可能になる。

f:id:HK29:20200201152854p:plain

▼微生物(カイミジンコ?)の外形を検出する

(参考リンク)Python - opencv python 輪郭の特徴点の座標|teratail

画像の特徴1:色は黄土色の一色(顕微鏡の光によると思われる)
画像の特徴2:そのため、線だけ構成されている

f:id:HK29:20200201145212j:plain

処理1. 白黒化の処理

f:id:HK29:20200201154504j:plain

処理2. 大津処理

f:id:HK29:20200201154521j:plain

処理3. 膨張縮小によるぼかし

f:id:HK29:20200201154829j:plain

処理4. 微生物の輪郭を描写

f:id:HK29:20200201154946p:plain

座標もcsvファイルへ出力する仕様

f:id:HK29:20200201155424p:plain

▼以降は、自前で準備した画像

下図はパワポの図形で作成した画像です。

f:id:HK29:20200201160022p:plain

輪郭が明確なため、下図のようにほぼ忠実に検出しています。

f:id:HK29:20200201160038p:plain

下図は、ビールとおつまみの写真です。上記1~3とは異なり輪郭を識別しづらいため、物体の境界を明確化する処理に更なる工夫が必要になることがわかる。

f:id:HK29:20200201160427p:plain

■本プログラム
本コードの仕様を次に3つ示します。
・仕様1.  輪郭検出の設定(手法の選択と条件の組み合わせ)を4つの関数として、順番に全て実行する
・仕様2. 本コード実行前に、カレントディレクトリにある「.jpg」ファイルをリストで取得して、順場に実行する
・仕様3.  途中の処理で識別できない場合でもエラーで落ちずに、次のループへ回す

import glob
import cv2
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from matplotlib.patches import Polygon
import datetime
now = datetime.datetime.now()
now = now.strftime("%y%m%d")

##### 輪郭抽出の手法A
##### 色調に明確に差があり、それが輪郭になり得る場合
##### HSVに変換後に2値化して判別
def draw_contours_A(file_name, img):
    height, width = img.shape[:2]

    # 色調変換
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    cv2.imwrite(now + '_1_' + file_name + '_hsv_A.jpg', hsv)

    # ガウス変換
    gauss = cv2.GaussianBlur(hsv,(9, 9),3)    
    cv2.imwrite(now + '_2_' + file_name + '_gauss_A.jpg', gauss)
    
    # 色調分割
    img_H, img_S, img_V = cv2.split(gauss)
    cv2.imwrite(now + '_3_' + file_name + '_H_of_HSV_A.jpg', img_H)
    _thre, img_mask = cv2.threshold(img_H, 140, 255, cv2.THRESH_BINARY)
    cv2.imwrite(now + '_4_' + file_name + '_mask_A.jpg', img_mask)

    # 輪郭抽出
    contours, hierarchy = cv2.findContours(img_mask, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)
    x_list = []
    y_list = []
    for i in range(0, len(contours)):
        if len(contours[i]) > 0:
            if cv2.contourArea(contours[i]) < 500:
                continue
            cv2.polylines(img, contours[i], True, (255, 255, 255), 5)
            buf_np = contours[i].flatten() # numpyの多重配列になっているため、一旦展開する。
            #print(buf_np)
            for i, elem in enumerate(buf_np):
                if i%2==0:
                    x_list.append(elem)
                else:
                    y_list.append(elem*(-1))
    
    cv2.imwrite(now + '_5_' + file_name + '_boundingbox_A.jpg', img)
    # pandasのSeries型へ一旦変換
    x_df = pd.Series(x_list)
    y_df = pd.Series(y_list)
    # pandasのDataFrame型へ結合と共に、列名も加えて変換
    DF = pd.concat((x_df.rename(r'#X'), y_df.rename('Y')), axis=1, sort=False)
    #print(DF)
    DF.to_csv(now + '_' + file_name + "_target_contour_A.csv", encoding="utf-8", index=False)

##### 輪郭抽出の手法B
##### 色調に差があり、それが輪郭になり得る場合
##### 2値化後に閾値で判別
def draw_contours_B(file_name, img):
    # 2値化
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    cv2.imwrite(now + '_1_' + file_name + '_gray_B.jpg', gray)
    
    ret,th1 = cv2.threshold(gray,200,255,cv2.THRESH_BINARY)
    cv2.imwrite(now + '_2_' + file_name + '_th1_B.jpg', gray)
    # 輪郭抽出
    contours, hierarchy = cv2.findContours(th1, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    areas = []
    for cnt in contours:
        area = cv2.contourArea(cnt)
        if area > 10000:
            epsilon = 0.1*cv2.arcLength(cnt,True)
            approx = cv2.approxPolyDP(cnt,epsilon,True)
            areas.append(approx)
    #cv2.drawContours(img, areas, -1, (0,255,0), 3)
    cv2.imwrite(now + '_3_' + file_name + '_boundingbox_B.jpg', img)

##### 輪郭抽出の手法C
##### 色調に明確な差がなく、線で輪郭になり得る場合
##### 2値化後に閾値で判別
def draw_contours_C(file_name, img):
    # 2値化
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    cv2.imwrite(now + '_1_' + file_name + '_gray_C.jpg', gray)

    ret, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
    cv2.imwrite(now + '_2_' + file_name + '_otsu_C.jpg', binary)

    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
    binary = cv2.dilate(binary, kernel)
    cv2.imwrite(now + '_3_' + file_name + '_dilate_C.jpg', binary)
    # 輪郭抽出
    contours, hierarchy = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    # 一番面積が大きい輪郭を抽出
    target_contour = max(contours, key=lambda x: cv2.contourArea(x))

    fig, ax = plt.subplots(figsize=(8, 8))
    ax.imshow(img)
    ax.set_axis_off()

    for i, cnt in enumerate(contours):
        if cv2.contourArea(cnt) < 500:
            continue
        else:
            cnt = cnt.squeeze(axis=1)
            ax.add_patch(Polygon(cnt, color="b", fill=None, lw=2))
            ax.plot(cnt[:, 0], cnt[:, 1], "ro", mew=0, ms=4)
            ax.text(cnt[0][0], cnt[0][1], i, color="orange", size="20")
    plt.savefig(now + '_4_' + file_name + '_boundingbox_C.png')
    plt.close()
    # 輪郭を構成する点を CSV に保存する。
    buf_np = target_contour.squeeze(axis=1).flatten()
    x_list = []
    y_list = []
    for i, elem in enumerate(buf_np):
        if i%2==0:
            x_list.append(elem)
        else:
            y_list.append(elem*(-1))    
    # pandasのSeries型へ一旦変換  
    x_df = pd.Series(x_list)
    y_df = pd.Series(y_list)
    # pandasのDataFrame型へ結合と共に、列名も加えて変換
    DF = pd.concat((x_df.rename(r'#X'), y_df.rename('Y')), axis=1, sort=False)
    #print(DF)
    DF.to_csv(now + '_' + file_name + "_target_contour_B.csv", encoding="utf-8", index=False)

##### 輪郭抽出の手法D
##### 色調に明確な差がなく、線で輪郭になり得る場合
##### Canny(エッジ検出アルゴリズム)で判別
def draw_contours_D(file_name, img):
    edges = cv2.Canny(img, 100, 200)
  
    cv2.imwrite(now + '_' + file_name + '_boundingbox_D.jpg', edges)

##### csvファイルから散布図を作成する関数
def plot_scatter(myfile):
    file_name = myfile[:-4]

    df = pd.read_csv(myfile, header=0)
    #print("df ->" + str(df))
    myX = df.iloc[:, 0].values.tolist()
    myY = df.iloc[:, 1].values.tolist()

    ax = plt.figure(num=0, dpi=120).gca() 
    ax.set_title(file_name, fontsize=14)
    ax.set_xlabel('X Axis', fontsize=16)
    ax.set_ylabel('Y Axis', fontsize=16)
    ax.scatter(myX, myY, s=20, color="red") #, label=file_name)
    ax.plot(myX, myY)
    ax.set_aspect('equal', adjustable='box')
    plt.grid(True)
    #plt.legend(loc='auto', fontsize=15)
    #plt.tick_params(labelsize=15) 
    plt.savefig(now + '_' + file_name + "_contours.png")
    plt.close()

##### メイン関数
def main(my_file):
    file_name = my_file[:-4]
    img = cv2.imread(my_file)

    # 輪郭描写(自作関数呼び出し)
    draw_contours_A(file_name, img) # HSVに変換後に2値化して判別
    draw_contours_B(file_name, img) # 2値化後に閾値で判別
    draw_contours_C(file_name, img) # 2値化後に閾値で判別 大津アルゴリズム
    draw_contours_D(file_name, img) # Canny(エッジ検出アルゴリズム)で判別

if __name__ == '__main__':
    my_jpg_list = glob.glob("*.jpg")
    for my_file in my_jpg_list:
        try: # 例外処理により、何かしらエラーが出ても次のループへ進む
            main(my_file)
        except:
            print('somthing error')

    # 輪郭座標のcsvファイルを読み込みグラフ化
    my_csv_list = glob.glob("*.csv")
    for my_file in my_csv_list:
        try:
            plot_scatter(my_file)
        except:
            print('somthing error')
     

以上

<広告>