Pyhotn 線図の細線化「cv2.ximgproc.thinning」とsvgファイルの作成「svgwrite」

 本記事では、下図左のような線図の線を検出して、下図右のようにCADで読み込めるsvgファイルを作成する雛形コードを載せました。

線図の線は、領域ごとに閉じたポリゴンとして作成します。そのため、押し出すことができます。下図は、オープンソフトのFreeCADでsvgを読み込み、Extrudeでソリッド(立体)を作成した例です。

 本プログラムでは、OpenCVのcv2.ximgproc.thinningメソッドを使用します。もし使用しない場合、線図の線が枝分かれしてる箇所で2重線となり、領域が分かれてしまいます(下図)。

■ライブラリのインストール

pip install opencv-contrib-python
pip install svgwrite

 

■本プログラム

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

# In[1]:


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

# 画像ファイルのパス
file_path = './pictures/tubo.png'
file_name = os.path.basename(file_path)
fname, ext = os.path.splitext(file_name)
print(fname, ext)

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

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


# In[2]:


# グレースケールに変換する
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
plt.imshow(gray)


# In[3]:


# 2値反転
gray_inv = cv2.bitwise_not(gray)
plt.imshow(gray_inv)


# In[4]:


# 上下反転(座標抽出する際に反転するため)
gray_flip = cv2.flip(gray_inv, 0)
plt.imshow(gray_flip)


# In[5]:


# 細線化(スケルトン化)
skeleton = cv2.ximgproc.thinning(gray_flip, thinningType=cv2.ximgproc.THINNING_GUOHALL)
plt.imshow(skeleton)


# In[6]:


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

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


# In[7]:


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

for i in range(len(contours)):
    # 階層が0の場合に処理する
    if hierarchy[0][i][-1] == 0: 
        
        # 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)

# 抽出した輪郭をチェック
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}_03_scatter.jpg')
#plt.close()


# In[8]:


# 画像の縁の座標を削除する場合 True, 削除しない場合False
if False:
    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}_04.xlsx', index=False)

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


# In[9]:


# SVGを作成する
# .svgファイルを出力するファイルパス
outfile_name = f'{save_dir}{fname}_05_Draw.svg'

# インスタンス生成
dwg = svgwrite.Drawing(outfile_name,
                       profile = 'tiny', #'tiny', 'full'
                      )

for num, df_buf in enumerate(df_group_list, start=1):
    print(num, 'before', len(df_buf))
    
    # データを間引く場合はTrueを設定
    if False:
        df_buf = df_buf[::10]
        #print(num, df_buf, color)
    print(num, 'after', len(df_buf))
    
    # データ数が少ない場合はノイズとして飛ばす
    if len(df_buf) < 10:
        continue

    # 座標データをpandasデータフレームから、リストへ変換
    points = df_buf.to_numpy().tolist()
    #print(points)

    # パスメソッドで生成する場合(データ間を直線で結ぶ座標を作る)
    p = ""
    for i in range(len(points)):
        if i == 0:
            p = "M " + str(points[i][0]) + " " + str(points[i][1]) 
        elif i == len( points ) - 1:
            p += " L " + str(points[i][0]) + " " + str(points[i][1]) + " Z"
        else:
            p += " L " + str(points[i][0]) + " " + str(points[i][1]) 
    path = dwg.path(p).stroke(width = 1).fill('none')
    dwg.add(path)  
    
#     # ポリゴンメソッドで生成する場合
#     dwg.add(dwg.polygon(points = points,
#                         stroke = color,
#                         stroke_width = 0,
#                         fill = 'none', #'none', 'red', color
#                         id = 'id_' + '{0:03}'.format(num),
#                        ))

dwg.save()
del dwg
print('save', outfile_name)

(参考)線を閉じたポリゴン出なくて、pathとして作成したい場合の例は次のリンク先を参照下さい。

hk29.hatenablog.jp

以上

<広告>