Python 2値化フィルターによる輪郭の強調。そして座標を抽出する「OpenCV」

'22/07/03更新:グレースケールと輪郭強調だけでは対応できない場合の対策例として、その間の画像加工のコードの記述例を追記しました。
しきい値調整(トライアングル, 大津処理)
ヒストグラム平坦化
・白黒反転
・パイラテラルフィルタ
・ノイズ除去
 

 本記事では、下図のような図形の境界の座標を抽出する雛形コードを載せました。

本プログラムの処理についての説明をします。まず、OpenCVで画像をオブジェクトで読み込み後にグレースケールに変換します。

次に、cv2.adaptiveThreshold()メソッドを用いることで輪郭を検出できます(下図)。

そして、cv2.findContours()メソッドを使用して座標を抽出します。この時、下図の赤枠のように画像の縁の座標も取得する仕様になっています。

この画面サイズを表す赤枠は、原点(x, y)=(0, 0)を必ず通ります。これを判定基準に赤枠データを削除したのが下図です。

下図は、上図の〇、△、□の図形の(x, y)座標データです。列で識別できるようにしました。

▼本プログラム

スクリプト内で画像加工方法を3つに分けています。それはフラグで選択して分岐して処理する仕様にしています。flag_image_process = 'simple_gray' # simple_gray, triangle, multy 

#!/usr/bin/env python
# coding: utf-8

# In[1]:


import os, sys
import cv2
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# 画像ファイルのパス
file_path = './pictures/shape.jpg'

# 保存フォルダパス
save_dir = './save_dir/'

# 画像処理のフラグ
flag_image_process = 'simple_gray' # simple_gray, triangle, multy


# In[2]:


file_name = os.path.basename(file_path)
fname, ext = os.path.splitext(file_name)
print(fname, ext)


# In[3]:


# 画像をOpenCVで読み込む
img = cv2.imread(file_path)


# In[4]:


# グレースケールに変換
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
cv2.imwrite(f'{save_dir}{fname}_01_gray_src{ext}', gray)


# In[5]:


# 画像処理方法
if flag_image_process == 'simple_gray':
    dst = gray.copy()

elif flag_image_process == 'triangle':
    # しきい値調整(トライアングル)
    retval, dst = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_TRIANGLE)
    cv2.imwrite(f'{save_dir}{fname}_02_thresh_triangle_src{ext}', dst)

elif flag_image_process == 'multy':
    # ヒストグラム平坦化
    dst = cv2.equalizeHist(gray)
    cv2.imwrite(f'{save_dir}{fname}_02_contra_src{ext}', dst)
    
    # 白黒反転
    dst = 255 - dst
    cv2.imwrite(f'{save_dir}{fname}_03_re_gray_src{ext}', dst)
    
    # パイラテラルフィルタ
    dst = cv2.bilateralFilter(dst, 5, 100, 100)
    cv2.imwrite(f'{save_dir}{fname}_04_bilateral_src{ext}', dst)
    
    # ノイズ除去
    dst = cv2.fastNlMeansDenoising(dst, h = 20)
    #dst = cv2.fastNlMeansDenoising(dst, None, 30, 10, 7)
    cv2.imwrite(f'{save_dir}{fname}_05_noise_removal_src{ext}', dst)
    
    # しきい値調整(大津処理)
    retval, dst = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    cv2.imwrite(f'{save_dir}{fname}_06_thresh_otsu_src{ext}', dst)

else:
    print('check flag')
    sys.exit()


# In[6]:


# 2値化フィルターによる輪郭の強調
block_size = 5
#contour = cv2.adaptiveThreshold(dst, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, block_size, 4)
contour = cv2.adaptiveThreshold(dst, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, block_size, 4)
cv2.imwrite(f'{save_dir}{fname}_07_contour_src{ext}', contour)


# In[7]:


# 輪郭の座標を読み取る
contours, hierarchy = cv2.findContours(contour, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)

# 取得したデータと階層をプリント出力で確認
print(len(contours))
print(hierarchy)


# In[8]:


# 必要な輪郭の(x,y)座標データを取得する
x_list = []
y_list = []
df_group_list = []

for i in range(len(contours)):
    # 階層が-1の場合に処理する
    if hierarchy[0][i][-1] == -1: 
        
        # numpyの多重配列になっているため、一旦展開する。
        buf_np = contours[i].flatten() 
        #print(buf_np)
        
        # (x, y)座標の取得
        for i, elem in enumerate(buf_np):
            if i%2 == 0: # 偶数の場合はx座標
                x_list.append(elem)
            else: # 奇数の場合はy座標
                y_list.append(elem * (-1))
        
        # pandasデータフレームへ変換
        mylist = list(zip(x_list, y_list))
        df_buf = pd.DataFrame(mylist, columns = ['x', 'y'])
        
        # データフレームをリストへ格納
        df_group_list.append(df_buf)
        
        # リストを空にして、次のオブジェクトの取得へ
        x_list.clear()
        y_list.clear()
        
print('df_group_list', len(df_group_list))
#print(df_group_list)


# In[9]:


# 抽出した輪郭をチェック
for i, df_buf in enumerate(df_group_list, start=1):
    plt.scatter(df_buf['x'], df_buf['y'], s = 0.5)
    plt.xlabel('x')
    plt.ylabel('y')
    plt.grid()
plt.savefig(f'{save_dir}{fname}_08_scatter.jpg')
plt.close()


# In[10]:


# 画像の縁の座標を削除する場合 True, 削除しない場合False
if True:
    for i, df_buf in enumerate(df_group_list):
        if not df_buf[(df_buf['x'] == 0) & (df_buf['y'] == 0)].empty:
            #print(i, df_buf)
            df_group_list.pop(i)

# 抽出した輪郭をチェック
DF_name_list = []
DF_list = []
for i, df_buf in enumerate(df_group_list, start=1):
    plt.scatter(df_buf['x'], df_buf['y'], s = 0.5)
    plt.xlabel('x')
    plt.ylabel('y')
    plt.grid()
    
    # 列名にid番号を付与して、DF_listリストへ格納
    DF_name_list.extend(['id_'+ '{0:03}'.format(i) + '_x', 'id_'+ '{0:03}'.format(i) + '_y'])
    DF_list.append(df_buf)

# データフレームを結合
DF = pd.concat(DF_list, axis=1)

# カラム名をリネーム
DF.columns = DF_name_list

# 座標をcsvファイルへ出力する
#DF.to_csv(f'{save_dir}04_{fname}.csv', index=False)

# 座標をexcelファイルへ出力する。openpyxlのインストールが必要。
DF.to_excel(f'{save_dir}{fname}_09.xlsx', index=False)

# 画像でも出力
plt.savefig(f'{save_dir}{fname}_09_scatter_fix.jpg')    
plt.close()

以上

<広告>