techno_memo

個人用の技術メモ。python・ROS・AI系のソフトウェア・ツールなどの情報を記載

matplotlibを用いたグラフ描画

やりたいこと

 pythonのライブラリ matplotlibを用いて数値データをグラフ描画する。

  • pythonのグラフ描画ライブラリの紹介

  • matplotlibの基本 (オブジェクト(figure/axes/axis)の考え方とグラフ設定方法)

  • 各種グラフ形式の描画方法 (折れ線グラフ/散布図/棒グラフと設定方法)

f:id:sd08419ttic:20190324134031p:plain

pythonのグラフ描画ライブラリの紹介

pythonでは多数のグラフ描画ライブラリが利用できる。

www.st-hakky-blog.com

ライブラリ名 リンク 特徴
matplotlib リンク pythonで最も標準的なグラフ描画ライブラリ
ネット上の実装事例も多い
seaborn リンク matplotlibの描画を見やすくすることができるライブラリ
matplotlibより優れたデータ分析機能を利用できる
plotly リンク Javaスクリプト製可視化ライブラリをpythonに対応させたもの
3D描画がきれいでインタラクティブなグラフ生成が可能
altair リンク JSON形式のファイルを扱うためのライブラリ
描画処理の記述が簡単でWEB系のデータ描画がしやすい

今回の記事では最も標準的なライブラリであるmatplotlibの使い方についてまとめる

matplotlibの基本 (オブジェクト(figure/axes/axis)の考え方とグラフ設定方法)

下記サイトの解説が非常に参考になる。

qiita.com

matplotlibではplt関数で描画する方法とsubplotでfigureを生成して各種設定を変更するオブジェクト指向型の描画方法がサポートされている。

このページでは描画設定のやりやすさなどを考慮して。後者のオブジェクト指向型の方法で描画を行う方法について説明する。

matplotlibでは下記画像のように、描画オブジェクトを構造化している。

f:id:sd08419ttic:20190324135639j:plain

オブジェクト名 内容 使用方法
figure プロット画面全体/ウィンドウ plt.figure関数で生成し、オブジェクトの各種設定を変更
axes 各グラフの画面 figureに対してsubplot関数で生成し、、オブジェクトの各種設定を変更
axis グラフのデータ axesに対してデータ描画関数(plot/scatter等)で生成する
(描画調整はaxesへの操作で行う)

figureとaxesの描画および色や軸説明文を設定する処理は下記のように記述できる

f:id:sd08419ttic:20190325211612p:plain

def matplotlib_gui_fig_sub():

    #plot関数のテスト
    fig = plt.figure()              #新規フィギュア(Window)の描画
    fig.canvas.set_window_title('My title')     #Windowタイトルの設定
    fig.suptitle("Figure Title")                #Figureタイトルの設定
    fig.patch.set_facecolor('xkcd:mint green')  #Figure背景色の設定

    #折れ線グラフの表示
    ax = fig.add_subplot(2,2,1)     #subplotの追加 (行/列/描画対象インデックス)
    ax.set_title('First plot')  #サブプロットタイトルの表示
    ax.set_xlabel('X')          #X軸説明文の表示
    ax.set_ylabel('Y1')    #Y軸説明文の表示
    ax.grid(True)   #Gridの表示

    #散布図の描画例
    ax2 = fig.add_subplot(2,2,2)     #subplotの追加 (行/列/描画対象インデックス)
    ax2.set_title('Second plot')    #サブプロットタイトルの表示
    ax2.set_xlabel('X')             #X軸説明文の表示
    ax2.set_ylabel('Y2')       #Y軸説明文の表示
    ax2.legend(loc="upper right")   #凡例の表示 (locで表示位置を設定可)
    ax2.set_facecolor('lightyellow')
    fig.subplots_adjust(top=0.9)

    #棒グラフの描画
    ax3 = fig.add_subplot(2,2,3)     #subplotの追加 (行/列/描画対象インデックス)
    ax3.set_title('Third plot')  #サブプロットタイトルの表示
    ax3.set_xlabel('X')          #X軸説明文の表示
    ax3.set_ylabel('Y3')    #Y軸説明文の表示
    fig.tight_layout()              #subplot表示位置の調整
    plt.show()

各種グラフ形式の描画方法 (折れ線グラフ/散布図/棒グラフと設定方法)

上記で設定したaxesに対して描画関数でグラフを追加する。

代表的な描画関数は下記になる。

関数名 内容 説明
plot 折れ線グラフ 入力した点を折れ線で結ぶグラフを描画する
scatter 散布図 入力した各点をマーカーで表現するグラフを描画する
bar 棒グラフ 入力した点を下から棒状に表すグラフを描画する

f:id:sd08419ttic:20190325213423p:plain

上記を利用して、sin/cos/tanをそれぞれのグラフ形式で表示するサンプルを下記に示す。

#波形データのGUI(Matplotlib)
def matplotlib_gui_example():
    #sns.set()
    X = np.linspace(-10, 10, 100)
    Y1 = np.sin(X) # サインの値を計算する
    Y2 = np.cos(X)
    Y3 = np.tan(X)

    #plot関数のテスト
    fig = plt.figure()              #新規フィギュア(Window)の描画
    fig.canvas.set_window_title('My title')     #Windowタイトルの設定
    fig.suptitle("Figure Title")                #Figureタイトルの設定
    fig.patch.set_facecolor('xkcd:mint green')  #Figure背景色の設定

    #折れ線グラフの表示
    #https://pythondatascience.plavox.info/matplotlib/%E6%8A%98%E3%82%8C%E7%B7%9A%E3%82%B0%E3%83%A9%E3%83%95
    ax = fig.add_subplot(2,2,1)     #subplotの追加 (行/列/描画対象インデックス)
    ax.plot(X,Y1, color='black',  linestyle='solid')        #サブプロット1の描画 (折れ線グラフ colorは色,linestyleは線のタイプ)
    ax.plot(X,Y1+0.5, color='black',  linestyle='dashed')   #サブプロット1の描画 (折れ線グラフ colorは色,linestyleは線のタイプ)
    ax.plot(X,Y1+1, color='black', linestyle='dashdot')     #サブプロット1の描画 (折れ線グラフ colorは色,linestyleは線のタイプ)
    ax.set_title('First plot')  #サブプロットタイトルの表示
    ax.set_xlabel('X')          #X軸説明文の表示
    ax.set_ylabel('Y1(sin)')    #Y軸説明文の表示
    ax.grid(True)   #Gridの表示

    #散布図の描画例
    #https://pythondatascience.plavox.info/matplotlib/%E6%95%A3%E5%B8%83%E5%9B%B3
    ax2 = fig.add_subplot(2,2,2)     #subplotの追加 (行/列/描画対象インデックス)
    ax2.scatter(X,Y2,marker="*",s=5.0,label='Y2(cos)')          #散布図1の描画 (sは点の大きさ、labelは凡例用)
    ax2.scatter(X,Y2+0.5,marker="o",s=10.0,label='Y2(cos)+0.5') #散布図2の描画 (sは点の大きさ、labelは凡例用)
    ax2.scatter(X,Y2+1,marker=".",s=1.0,label='Y2(cos)+1.0')    #散布図3の描画 (sは点の大きさ、labelは凡例用)
    ax2.set_title('Second plot')    #サブプロットタイトルの表示
    ax2.set_xlabel('X')             #X軸説明文の表示
    ax2.set_ylabel('Y2(cos)')       #Y軸説明文の表示
    ax2.legend(loc="upper right")   #凡例の表示 (locで表示位置を設定可)
    ax2.set_facecolor('lightyellow')
    fig.tight_layout()              #subplot表示位置の調整
    fig.subplots_adjust(top=0.9)

    #棒グラフの描画
    ax3 = fig.add_subplot(2,2,3)     #subplotの追加 (行/列/描画対象インデックス)
    ax3.bar(X, Y3,width=0.2)
    ax3.set_title('Third plot')  #サブプロットタイトルの表示
    ax3.set_xlabel('X')          #X軸説明文の表示
    ax3.set_ylabel('Y3(tan)')    #Y軸説明文の表示
    #データラベルの描画
    for indx in range(0,Y3.shape[0],20):
            print( Y3[indx])
            ax3.annotate('{:.2f}'.format(Y3[indx]), xy=(X[indx], Y3[indx]))

    plt.tight_layout()
    plt.show()

ソースコード

github.com

上記より細かい設定については下記サイトに非常に役立つ解説がされている。

qiita.com

その他、matplotlibについてはプロットをアニメーション描画する機能、3Dプロットなどが便利なので別記事で取り上げる。

電子工作プロトタイピングの開発プラットフォームについて

この記事の目的

 電子工作のプロトタイピング用開発プラットフォーム下記3つについて、それぞれの特徴・どう使い分けるべきかを整理する。

f:id:sd08419ttic:20190321222121p:plain

Raspberry pi

特徴

IoT用プロトタイピング環境として最もよく用いられるコンピュータである (Amazonなどで容易に入手できる)

2019年3月現在で初代~Raspberry pi 3 までリリースされている。5000円程度で購入可能。

参考:最新のラインナップ

ja.wikipedia.org

f:id:sd08419ttic:20190321225731p:plain

  • SDカードにOS(様々な種類が公開されている。大多数はLinux系)をインストールし、本体のUSB端子にキーボード・マウス・ディスプレイを接続して操作する。 (上記以外でも別PCからのリモート接続による開発が可能)

  • ネットワークは有線EthernetWifiモジュール・Bluetooth (Raspberry pi2以降)を利用できる

  • GPIOにジャンパーワイヤ・ブレッドボードを用いて各種センサ・アクチュエータをつなげて開発するのが一般的である。

  • 給電はUSB type B端子から行う。スマートフォン用のモバイルバッテリからの給電もできる。

長所

  • ネット上に公開されている情報/サンプルコードが多くトラブルシューティングがしやすい

  • 無線ネットワークに標準で対応しており、バッテリ・本体ともに小型であるためリモートでの開発・移動機器でのデータセンシングなどの用途で利用しやすい。

  • python/scratchなどプログラム初心者でも扱いやすい開発環境が提供されている。

短所

  • リアルタイム制御には向かない (OSなし・リアルタイムOSの開発環境も構築できるらしいが、後述のArduinoのほうが開発しやすい)

  • 画像処理/DeepLearningに関する試作をしたい場合は計算能力が不足している。

Arduino

特徴

電子工作教育用の汎用マイコンとして、Raspberry pi と並んで最もよく用いられるプラットフォームの1つである。

(ハードだけでなくプログラム言語・統合開発環境なども含めたプロジェクト全体がArduinoと呼称されている)

2019年3月現在でIOやネットワーク機能の違いを含む多数のバリエーションのArduino対応ハードが発売されている。また、Arduino開発環境に対応した互換機も多数発売されている。

最も標準的な構成であるArduino uno は公式であれば3000円、互換機であれば1200円程度で購入できる。

参考:最新のラインナップ

www.arduino.cc

f:id:sd08419ttic:20190321233651p:plain

  • 開発用のPC (Arduino統合開発環境をインストール)からUSBケーブルで接続したArduinoのFlashROMにソフトを書きこんで動作させる

  • 開発はArduino言語で行う (C言語互換)。公式サイトが公開しているIDEの他に、Visual Studio Codeを用いた開発プラグインなども提供されている。

  • Raspberry pi と異なりIDE・デバッガを利用したソフトウェアの動作状況確認ができない。RAM値を確認するためにはシリアル通信で出力された値のモニタなどが必要となる

  • センサ・アクチュエータはGPIOに接続して利用する。 (Raspberry pi と同様。GPIOの数はArduinoの種類によって異なる。最も標準的なArduino unoで20本)

  • 給電はUSB or ACアダプタ。 USBからの給電(モバイルバッテリなど)もできる。

長所

  • 非常に多くのArduino対応センサ・アクチュエータが世界中で公開されており、ライブラリの流用性が高い。 (OSがないため開発環境違いでハマることもほぼない)

  • OSがないため、リアルタイム性の高い組込み制御(モーター制御など)が制度よく実現できる

短所

  • Raspberry pi と比較すると計算能力・ROM・RAM容量ともに小さく、作成するアプリケーションの制約が大きい

  • 作成したソフトのデバッグがしにくく、C言語に慣れていない人にとってのハードルが高い

Jetson

特徴

エッジ用のAI(特にDeep Learning)に特化した小型コンピュータとしてnvidiaが販売している。

Deep Learningで多く用いられているnvidiaGPU/CUDAに対応したライブラリを小型コンピュータで動かせるのが最大の特徴。

2019年3月現在で Jetson TX1 ~ Xavierまでが発売済み。価格は8万(TX1) ~ 15万(Xavier)とPC並みに高額。

OSはUbuntu16系をインストールする。内蔵のeMMC (TX1 16GB ~ TX2・Xavier 32GB) + SSDによる拡張も可能。

標準の開発ボードはAC電源接続であり、コンセントがない移動機器への取り付けはできない。ただし、開発者ボードを使えば可能。(ただし一般的な電圧のモバイルバッテリは使えないので苦労する)

f:id:sd08419ttic:20190322003459p:plain

長所

  • Deep Learningフレームワークで一般的であるCUDAを使った処理を組込み機器でそのまま動かすことができる。(画像認識系の処理で非常に強力)

  • 上記に限らず、演算能力が高くRaspberry pi では厳しいROSの画面描画などもかなり高速に動かすことができる

  • Ubuntu16 を使った開発ができる。 PCで構築している開発環境に近いツールを利用できる。

短所

  • Raspberry pi/ Arduinoと比較すると参考とできるWebサイトが少ない。 (ハードウェアの追加やライブラリがインストールできない場合のトラブルシュートに苦労する可能性あり)

  • Raspberry pi/ Arduinoと比較すると非常に高額

  • モバイルバッテリでの運用が難しく(キャリアボード・12Vモバイルバッテリなどが必要)移動機器で気軽に利用できない。(次期モデルJetson Nanoで改善されるとの情報あり)

どう使い分けるべきか

処理能力・価格などを総合的に判断すると、汎用かつ手軽に利用できるのはRaspberry pi である。ただし、下記用途ではArduinoやJetsonを利用したほうが良い。

  • Arduinoを使うほうが良い場合

    モーター制御などリアルタイム性の高い制御が必要

    組込み機器で使うC言語での開発を体験したい

  • Jetsonを使うほうが良い場合

    電子工作より、画像処理・物体検出などに特化したアルゴリズムを作りこみたい

    Deep Learning用に作ったpythonロジックを組込み機器で最短で推論させたい

Arduinoについては、単体で利用してもネットワークと連携したアプリケーションを作ることが難しいので、Raspberry piと併用してリアルタイム性の高い処理だけを行うアーキテクチャとしたほうが良い。

また、Jetsonの利点であるDeepLearningについては、GoogleのTPUがRaspberry piでつかるようになるようなので、

Raspberry piであっても十分な処理速度のDeep Learningの推論/画像処理などができる可能性がある。

前処理② 数値データに対する前処理 (データの選定・欠損値・不正値の補間・上下限ガード)

やりたいこと

 下記のような数値データに前処理を実施して、解析に適する形式に修正したい

 (統計処理ではなく、センサーでの値取得においてよくあるデータ取得時の不備などを想定)

  • データの選定:大量のデータ項目から必要なものだけを選定する

  • 欠損値:データがない、正しい形式で取得できていない (数値を期待しているのに文字列が含まれるなど)

  • 上下限ガード:信号の上下限や前後の信号の値に対して、異常な数値となるデータを取り除きたい

f:id:sd08419ttic:20190320231623p:plain

実装

データの選定

大量に選択したデータから必要なものだけを選定したい。 どのデータが必要であるかは、データ項目名をユーザーが文字列のリストとして設定する。

実装方法は、①データ読み込み時に対象とするデータ列名を設定、②DataFrameから特定の列名のみ残す の2種類ある。

データ読み込み時に指定する場合、読み込み時間・読み込み後のメモリ削減にも効果的である。

#数値データの前処理用クラス
class Class_Wave_PreProcessing():
    '''
    各種波形データに前処理をするクラス
    '''
    def __init__(self):
        '''
        コンストラクタ
        '''
        pass

    def get_only_required_columns(self,df_arg,req_columns):
        '''
        pandas dataframeからユーザーが指定取得対象とする列のみを取得する関数
        df_arg:データフレーム入力値、req_columns:取得対象とする列の名前を格納したlist
        '''
        df_new = df_arg[req_columns]

        return df_new

if __name__ == '__main__':
    
    #参考としたデータ (kaggle datasetで公開されている車両センサー情報) https://www.kaggle.com/zhaopengyun/driving-data
    input_path = "D:\\WS\\VS_project\\BlogProject\\BlogProject\\data\\101a.csv"
    #引数usecolsで読み込み対象とするデータを選択 (1つでも存在しない場合はエラー)
    test_data = pd.read_csv(input_path, encoding="shift-jis", usecols=["position X", "position Y","position Z","yawAngle","pitchAngle","rollAngle"])

    wave_inst = Class_Wave_PreProcessing()
    #データから特定の列のみを選定する関数
    df_new = wave_inst.get_only_required_columns(test_data,["position X","yawAngle"])

欠損値・不正値処理

データが正しく取得できなかった(値なし、文字列など)を削除 もしくは補間したい。

pandasでは標準でNanの除去処理があるが、これをそのまま使うだけでは誤って文字列が入力された場合に対処できない。

各列について数値変換処理to_numericをかけて数値変換(非数字の場合はNanになる)後に、Nanを除去もしくは補間する処理をすることで不正文字列入力時にも対処する。

Nanを含む行ごと削除する処理は下記のように記述できる。

    def elim_error_data_from_df(self,df_arg):
        '''
        DataFrameから無効値を除去する
        '''

        #列数
        orig_columns = df_arg.columns   #入力したDFの列名
        col_num = len(df_arg.columns)   #入力したDFの列数
        df_new = df_arg.copy()          #出力用のDF

        #数値データできない値をNanに変換する処理
        for indx in range(col_num):
            temp_series = df_arg.iloc[:,indx].copy()
            temp_series =  pd.to_numeric(temp_series, errors='coerce')
            df_new = df_new.drop(orig_columns[indx],axis=1)
            df_new[orig_columns[indx]] = temp_series
            df_new[orig_columns[indx]] = temp_series.astype(float)

        #Nanの除去(列ごと)
        df_new = df_new.dropna(how='any')   #any:1つでもNanが含まれる場合に行ごと除去、all:1行全てNanである場合に行ごと除去

        return df_new 

Nanを含む要素を固定値や前後のデータで補間したい場合は下記のように記述できる

interpolate処理は複雑な補間にも対応している。詳細は下記サイトに解説がある。

note.nkmk.me

    def fill_error_data_from_df(self,df_arg,mode="fixed",fixed_val=0):
        '''
        DataFrameから無効値を補間する mode:方式 ("fixed":固定値代入,fixed_val:固定値(デフォルト:0),"below":下の値,"above":上の値,"interpolate":上下平均)
        '''
        #列数
        orig_columns = df_arg.columns   #入力したDFの列名
        col_num = len(df_arg.columns)   #入力したDFの列数
        df_new = df_arg.copy()          #出力用のDF

        #数値データできない値をNanに変換する処理
        for indx in range(col_num):
            temp_series = df_arg.iloc[:,indx].copy()
            temp_series =  pd.to_numeric(temp_series, errors='coerce')
            df_new = df_new.drop(orig_columns[indx],axis=1)
            df_new[orig_columns[indx]] = temp_series

        if mode== "fixed":          #Nanの穴埋め(固定値)
            df_new = df_new.fillna(fixed_val)
        elif mode== "below":        #Nanの穴埋め(Nanの下にある値を代入) ※一番上のデータがNanの場合はそのまま残るので注意 2つ以上続くと続いた先の値を取得して代入
            df_new = df_new.fillna(method='ffill') 
        elif mode== "above":        #Nanの穴埋め(Nanの上にある値を代入) ※一番下のデータがNanの場合はそのまま残るので注意 2つ以上続くと続いた先の値を取得して代入
            df_new = df_new.fillna(method='bfill')
        else:                       #補間処理(Nanの下にある値を代入) ※一番上or下のデータがNanの場合はそのまま残るので注意
            df_new = df_new.interpolate()
        
        return df_new

上下限ガード

各列について、信号の仕様から取りうる値の上下限範囲が定まっている場合、clip関数を用いてそれ以上の値とならないようにガードをかけることができる。

clip関数の詳細は下記サイトに記載されている。

pandas.pydata.org

DataFrame全体にかける場合と、指定した列のみにかける場合でそれぞれ下記のように記述できる。

    def clip_data_from_df_all(self,df_arg,min_val=None,max_val=None):
        '''
        DataFrameの上下限ガード (全体) min_val:最小値, max_val:最大値
        '''

        df_new = df_arg.clip(lower=min_val,upper  = max_val)

        return df_new

    def clip_data_from_df_col(self,df_arg,col_name=None,min_val=None,max_val=None):
        '''
        DataFrameの上下限ガード (特定の列のみ) col_name:列名, min_val:最小値, max_val:最大値
        '''
        df_new = df_arg

        if col_name is not None:
            temp_series = df_arg[col_name]
            temp_series = temp_series.clip(lower=min_val,upper  = max_val)
            df_new[col_name] = temp_series

        return df_new

実装結果のまとめ

下記サイトに上記を1クラスにまとめた実装・使用例を記載

github.com

pythonスクリプトのexeファイル化 (pyinstaller)

やりたいこと

 pythonスクリプトをexeファイル化したい

  • 別PCへの配布 (python環境がないPC/開発者以外のPCで動作させたいツールなど)

  • 簡単に起動させたい便利ツール (ドラッグアンドドロップでファイル情報を取得して起動など)

f:id:sd08419ttic:20190316002444p:plain

exe化の方法

pythonのexe化用ライブラリは複数存在するが、今回は最も手軽にできるpyinstallerを紹介する

github.com

環境

  • Windows10 (64bit)

  • Anaconda (python 3.5 64bit)

手順

  1. pip install pyinstaller で pyinstallerをインストール

    (初回のみ。anaconda 仮想環境を作っている場合はactivate コマンドで移動後に実行すること)

  2. pyinstaller XXX.py -F でexeファイルを生成

    exeファイルはpyinstallerを実行したカレントディレクトリに生成される『dist』フォルダ以下に出力される

 -Fオプションをつけることで実行時に必要なファイルが1つのexeファイルに集約される

複数ファイルが含まれるpythonファイルをexe化したい場合、mainが記載されているpythonスクリプトを指定するとその他のファイルは自動で読み込まれる。

注意点

pythonのバージョン

pyinstallerはpython 2.X~3.7まで対応しているが、利用するライブラリによってはexeの生成や実行時にエラーがおきることが多い (特にpandas/matplotlib関連)

筆者の環境ではpython3.5が比較的安定していた。(pandas/matplotlibの処理に対しても問題なく生成できた)

ファイルの容量

生成されるexe容量は読み込むライブラリに依存し、数十MB~百MBになる。小さくしたい場合にはpython仮想環境に含まれるライブラリを必要最低限にするなどの工夫が必要となる

ファイルパスの取得

pythonスクリプトファイルが存在するパスを取得する処理を記述したい場合によく記述する下記ではパスを取得できない。

   import os
   iDir = os.path.abspath(os.path.dirname(__file__))

上記の代わりにsysモジュールを使ってファイルパスを取得できる

   import os
   import sys
   dpath = os.path.dirname(sys.argv[0])

ドラッグアンドドロップによるファイルパスの受け渡し

生成したexeファイルにファイルをドラッグアンドドロップすることで、外部パスを簡単に受け渡すことができる。

exeにドラッグアンドドロップされたファイルはsys.argv[1]で読み込める。 (複数ファイル渡す場合にはarg[2]以降で取得)

参考として、exeファイルにドラッグアンドドロップすると画像の横方向反転+グレースケール化をして表示するスクリプトを実装する。

f:id:sd08419ttic:20190316145707p:plain

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import cv2
import numpy as np
import sys

#データ読み込みクラス
class Class_Img_PreProcessing():
    '''
    画像データの前処理をまとめたクラス
    '''

    def __init__(self,filepath="",cv2img = None):
        '''
        コンストラクタ (filepath=指定したパス の画像の読み込み,もしくはcv2img=numpy形式データを代入)

        '''
        if filepath !="" :  #ファイルパスが指定された場合
            self.img = cv2.imread(filepath)
        elif cv2img != None: #numpy形式データが入力された場合
            self.img = cv2img
        else:
            self,img = None

    def get_gray_img(self,color_im= "None"):
        '''
        グレースケール画像を取得する (color_im:カラー画像データ(option))
        '''
        if color_im is "None":
            if self.img is None:
                return None
            else:
                return cv2.cvtColor(self.img, cv2.COLOR_BGR2GRAY)
        else:
            return cv2.cvtColor(color_im, cv2.COLOR_BGR2GRAY)

    def flip_img(self,mode="x"):
        '''
        画像を反転させる
        x: 横方向反転、y:縦方向反転 xy:両方向反転
        '''
        if self.img is None:
            return None
        else:
            if mode == "x": #X軸方向反転
                result = cv2.flip(self.img,1)
            elif mode == "y": #Y軸方向反転
                result = cv2.flip(self.img,0)
            elif mode == "xy": #両方向反転
                result = cv2.flip(self.img,-1)
            else:
                result = self.img
        return result

if __name__ == '__main__':

    target_ext = ['.jpg','.png','.tif']

    for indx in range(1,len(sys.argv)):
        filename = sys.argv[indx]
        root, ext = os.path.splitext(filename)
        print(ext)
        for indx2 in range(len(target_ext)):
            if ext == target_ext[indx2]:
                img_inst = Class_Img_PreProcessing(filename)
                flip_img = img_inst.flip_img(mode="x")
                gray_img = img_inst.get_gray_img(color_im = flip_img)
                cv2.imshow("Windowname",gray_img)
                cv2.waitKey(0)

実装結果は下記サイトに保存済み

github.com

GUI機能の実装(tkinter)

やりたいこと

 pythonGUI機能をお手軽に実装したい

  • 画面の表示 (Window/メニューバー/ラベル/画像など)

  • ユーザー入力情報処理(ボタン・テキストボックス・シークバー)

  • GUIを使ったフォルダ・ファイルの指定

実装にはpython標準ライブラリでるtkinterを利用する。細かい機能が多いライブラリでるため、使う頻度が多い機能に重点を絞る。

実装後のイメージ

f:id:sd08419ttic:20190313234844p:plain

実装

最終的なソフトは下記に記載

github.com

Window・メニューバーの表示

tkinterではroot windowの中にframe(部品置き場となる領域)を配置してwidget(ラベル・ボタンなどの部品)を配置する。

(windo/frame/widgetなどの概念は下記ページが参考となる)

qiita.com

空のwindow(あるサイズで指定した色で塗りつぶされたWindow)は下記のように記述できる。

title(Windowタイトル)/geometry(画面サイズ)/bg(背景色)は実現したいGUI機能に応じて設定する。

class TkRoot(tk.Tk):
    '''
    Tkinterウィンドウ画面クラス (画面設定/メニューバー設定)
    '''
    def __init__(self):
        '''
        Tkinterウィンドウ画面コンストラクタ
        '''
        super().__init__()
        self.title("GUI_title")     #Windowタイトルの描画
        self.geometry("400x300")    #画面サイズ
        self.config(bg="black")     #背景色

このWindowには、Menuバーを設定することができる。(下記がメニューバーの実装例)

下記関数をコンストラクタで呼び出すとWindow描画初期化時にメニューを描画できる。

label(メニュータイトル)/cmd(押されたときに実行する関数)は必要に応じて変更する。

f:id:sd08419ttic:20190314001046p:plain

    def create_menu(self):
        '''
        メニューバーの描画
        '''
        self.menu_bar = Menu(self)
        self.config(menu = self.menu_bar)
        self.file_menu = Menu(self.menu_bar,tearoff=0)
        self.file_menu.add_command(label='Open Existing File',command=self.callback_GUI_file_select)
        self.file_menu.add_separator()
        self.file_menu.add_command(label='Open Existing Directry',command=self.callback_GUI_folder_select)
        self.menu_bar.add_cascade(label='Files', menu=self.file_menu)

ラベル/画像などの画面表示

冒頭に記載したようにtkinterではwindowにframe(部品置き場となる領域)を配置する。 (下記がフレームのみを描画したイメージ)

f:id:sd08419ttic:20190314002544p:plain

下記にframeを描画するコードを記載する。

width/heightでフレームサイズ、bgで色設定ができる (propagate設定を明示的にFalseにしないとサイズが反映されないので注意)

フレームは設定後pack()関数を呼び出したときに初めて描画される。

class TkFrame(tk.Frame):
    def __init__(self, master=None):
        '''
        Tkinterウィンドウ画面コンストラクタ
        '''
        super().__init__(master)
        self.root = master
        self.config(width=200)  #フレームサイズの設定(幅)
        self.config(height=200) #フレームサイズの設定(高さ)
        self.config(bg="gray")  #フレームの設定(色)
        self.propagate(False)   #フレームのpropagate設定 (この設定がTrueだと内側のwidgetに合わせたフレームサイズになる)
        self.pack()             #フレーム描画

上記で作成したフレームに対する画面描画 (labelによる文字列描画、OpenCVで読み込んだ画像の描画)は下記のようになる。

f:id:sd08419ttic:20190314004812p:plain

    def create_widgets(self):
        '''
        Widget描画用処理
        '''
        #ラベルの描画
        #ラベル表示するテキストを設定 font(フォント名/サイズ/太文字),fg:文字色,bg:背景色
        self.label1 = tk.Label(self,text='label_test',font=("Helvetica", 20, "bold"),fg = "red",bg = "blue") 
        self.label1.grid(row=0,column =0,sticky=tk.W)

        #キャンバスの描画 (Opencvで読み込んだ画像の表示)
        image_bgr = cv2.imread("lena.jpg")
        image_bgr = cv2.resize(image_bgr,(100,100))            # opencv画像をresize
        image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB) # imreadはBGRなのでRGBに変換
        image_pil = Image.fromarray(image_rgb)                 # RGBをPILフォーマットに変換
        self.image_tk  = ImageTk.PhotoImage(image_pil)         # ImageTkフォーマットへ変換
        self.canvas = tk.Canvas(self, bg="blue", width=image_bgr.shape[0], height=image_bgr.shape[1])
        self.canvas.create_image(0, 0, image=self.image_tk, anchor='nw') # ImageTk 画像配置
        self.canvas.grid(row=1,column =0,columnspan=2,sticky=tk.W)

上記実装では各widgetの配置にgridという関数を利用している。gridの機能によりFrameを縦・横に区切って指定した場所にwidgetを配置する。

凝った配置をしようとする場合、設定方法は下記サイトが参考になる。

www.shido.info

フォント設定についてさらに凝った設定をしたい場合は下記サイトが参考になる。

memopy.hatenadiary.jp

ユーザー入力ボタンの表示

ユーザーの操作を受け付けるボタン/テキストボックス/チェックボックス/ラジオボックス/シークバーは下記のように実装できる

下記において、チェックボックスは選択式ボタン、ラジオボックスは択一選択式ボタンとしている。

選択されたチェックボックスの状態はgetメソッドで取得できる。

ボタンが押された場合の処理はコールバック関数として任意の処理に紐づけることができる。

同様に、値が変わったときに処理を動かしたい場合にはtraceを使ってコールバック関数と紐づけることができる。

f:id:sd08419ttic:20190314005542p:plain

    def create_widgets(self):
        '''
        Widget描画用処理
        '''
        #ボタンの描画
        self.button1 = tk.Button(self,text="Hello World",command=self.func_callback_button) #ボタン文字列・コールバック関数の設定
        self.button1.grid(row=2,column =0,sticky=tk.W)
        ##http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/anchors.html

        #テキストボックスの描画
        self.textbox1 = tk.Entry(self,width=20)
        self.textbox1.insert(tk.END,"default") #テキストボックス初期文字列の設定
        self.textbox1.grid(row=3,column =0,sticky=tk.W)

        #チェックボタンの描画
        self.chval = tk.BooleanVar(False) #チェックボタン初期値設定
        self.chbox1 = tk.Checkbutton(self, text = 'check1', variable = self.chval) #チェックボタンテキスト/変数紐づけ設定
        self.chbox1.grid(row=4,column =0,sticky=tk.W)

        #ラジオボタンの描画 (択一式)
        self.radval = tk.IntVar(0)  #ラジオボタンテキスト/変数紐づけ設定
        self.radbut1 = tk.Radiobutton(self, text = 'radio0', variable = self.radval, value = 0) #ラジオボタンテキスト/変数紐づけ設定
        self.radbut1.grid(row=5,column =0,sticky=tk.W)
        self.radbut2 = tk.Radiobutton(self, text = 'radio1', variable = self.radval, value = 1) #ラジオボタンテキスト/変数紐づけ設定
        self.radbut2.grid(row=5,column =1,sticky=tk.W)

        #シークバーの表示
        self.seekbarval = tk.DoubleVar() #シークバー変数設定
        self.seekbarval.trace("w", self.func_callback_seekbar) #シークバー変数変動時コールバック関数設定
        self.sc = ttk.Scale(self,variable=self.seekbarval,orient=tk.HORIZONTAL,from_=0,to=255)  #シークバー描画
        self.sc.grid(row=6, column=0,columnspan=2, sticky=(tk.N,tk.E,tk.S,tk.W))

    def func_callback_seekbar(self,*args):
        '''
        バーを動かして値が変化したときのコールバック
        '''
        print('value = %d' % self.seekbarval.get())

    def func_callback_button(self):
        '''
        ボタンを押したときのコールバック関数
        '''
        print("テキストボックス:",self.textbox1.get())
        print("チェックボックス:",self.chval.get())

GUIを使ったフォルダ・ファイルの指定

tkinterではGUIを使ってファイルやフォルダを指定する機能を利用することができる。(下記の例)

f:id:sd08419ttic:20190314010830p:plain

以下にに関数化して実装する例を示す。

メニューバーやボタンのコールバック関数に指定するとファイル入力を伴うGUIツールで利用しやすい。

    def callback_GUI_file_select(self):
        '''
        GUIを用いたファイル指定
        '''
        # ファイル選択ダイアログの表示
        fTyp = [("","*")]   #fTypeで拡張子を限定可能
        iDir = os.path.abspath(os.path.dirname(__file__))   #GUI表示でのデフォルトパスを指定
        file = filedialog.askopenfilename(filetypes = fTyp,initialdir = iDir)   #指定したファイル名を取得する (キャンセル時は空文字)
        if file != "":
            messagebox.showinfo('folder',file)

    def callback_GUI_folder_select(self):
        '''
        GUIを用いたフォルダ指定
        '''
        # ファイル選択ダイアログの表示
        iDir = os.path.abspath(os.path.dirname(__file__))     #GUI表示でのデフォルトパスを指定
        folder = filedialog.askdirectory(initialdir = iDir)   #指定したファイル名を取得する (キャンセル時は空文字)
        if folder != "":
            messagebox.showinfo('folder',folder)
'''

ファイル命名規則/更新日/ファイルサイズなどを反映した探索機能

やりたいこと

 ファイル命名規則/更新日/ファイルサイズなどを反映したファイル探索機能を実装したい

  • ファイル命名規則の判定

  • ファイル作成・更新・アクセス日時・ファイルサイズの取得と条件判定

f:id:sd08419ttic:20190307213548p:plain

実装

ファイル命名規則の判定

フォルダ中に存在するファイル名に対して、ユーザーが指定した正規表現パターンにマッチするかを判定する。

正規表現ライブラリreのmatch関数を用いてファイル名を判定する。

    def check_filename_regexp(self,filefullpathlist,pattern=None):
        '''
        ファイル命名規則のチェック (正規表現)
        filefullpathlist:ファイルパスのリスト、pattern:正規表現パターン
        '''
        result_path_list = []

        for pathname in filefullpathlist:
            #ファイル名の抽出
            fname, ext = os.path.splitext( os.path.basename(pathname) )    #ファイル名/拡張子の取得

            if pattern is not None:
                result = re.match(pattern,fname)   #re.search:途中も含めた検索、re.match:先頭からの完全一致
            else:
                result = True                       #正規表現パターンを何も設定していないときは元のパスをすべて出力する
            if result:
                result_path_list.append(pathname)
        return result_path_list

よく使いそうなpatternの条件は下記のようなものがある

説明
^XX ファイル先頭がXXから始まる
[0-9]+ ファイル名がすべて数字
.*0 ファイル名に0を含む

その他、使用可能な条件は下記サイトなどが参考となる

qiita.com

ファイル作成・更新・アクセス日時の取得と条件判定

ファイル情報の取得はos.pathで提供される各種関数を使って取得する

datetime形式のファイル作成日時などはstrftimeで文字列変換して表示することができる。

下記にフルパスリストからファイル情報を取得してdataframe形式で保存するスクリプトを示す。

    def get_file_detai_info(self,filefullpathlist):
        '''
        各ファイルの詳細情報(タイムスタンプ、サイズ)情報を取得 (結果をDataFrame形式で返す)
        filefullpathlist:ファイルのフルパスリスト
        '''
        result_df = pd.DataFrame(np.zeros([len(filefullpathlist), 5]), columns=['path', 'create_date', 'access_date','update_date','size'])
        indx_num =0
        for pathname in filefullpathlist:
            #result_df['path'][indx_num] = pathname
            result_df.loc[indx_num,'path'] = pathname

            #作成日時
            create_time = os.path.getctime(pathname)
            create_time_dt = datetime.datetime.fromtimestamp(create_time)
            create_time_str = create_time_dt.strftime('%Y/%m/%d  %H:%M:%S')
            result_df.loc[indx_num,'create_date'] = create_time_str

            #アクセス日時の取得
            acces_time = os.path.getatime(pathname)
            acces_time_dt = datetime.datetime.fromtimestamp(acces_time)
            acces_time_str = acces_time_dt.strftime('%Y/%m/%d  %H:%M:%S')
            result_df.loc[indx_num,'access_date'] = acces_time_str

            #更新日時の取得
            update_time = os.path.getmtime(pathname)
            update_time_dt = datetime.datetime.fromtimestamp(update_time)
            update_time_str = update_time_dt.strftime('%Y/%m/%d  %H:%M:%S')
            result_df.loc[indx_num,'update_date'] = update_time_str

            #サイズの取得 (KB)
            file_size  = os.path.getsize(pathname)
            result_df.loc[indx_num,'size'] = file_size

            #出力結果の表示
            print(create_time_str,acces_time_str,acces_time_str,file_size)
            indx_num = indx_num + 1
        return result_df

日付を条件判定する場合はdatetime形式に変換して比較演算子を利用すると効率が良い

(ユーザーは文字列型のほうが入力しやすいため)

下記にファイル作成日時がユーザーが入力した日付以前か以後かを判定して条件が満たされればそのパスを返す処理を実装する。

    def check_file_create_date(self,filefullpathlist,mode="A", cdate = 0):
        '''
        各ファイルの作成日時が指定した日以前であるかをチェックする
        mode:A (検索日時以降に作成) :B (検索日時以前に作成))、cdate:日時 例:2018/06/06
        '''

        result_path_list = []
        conditon_dt = datetime.datetime.strptime(cdate, '%Y/%m/%d')

        for pathname in filefullpathlist:
            #作成日時
            create_time = os.path.getctime(pathname)
            create_time_dt = datetime.datetime.fromtimestamp(create_time)
            create_time_str = create_time_dt.strftime('%Y/%m/%d  %H:%M:%S')

            if mode == "A":   #ファイル作成日時の比較 (検索条件日時以前に作成)
                if conditon_dt < create_time_dt:
                    result_path_list.append(pathname)
            elif mode == "B": #ファイル作成日時の比較 (検索条件日時以後に作成)
                if conditon_dt > create_time_dt:
                    result_path_list.append(pathname)
        return result_path_list

その他、ファイルサイズ比較などの機能についても1クラスにして下記のように実装した。

github.com

GUIで検索条件を指定できるように作るといろいろな場面で使えるのでtkinterでの条件設定などをする機能を実装していく。

前処理① 画像に対する前処理(リサイズ/色補正など)

やりたいこと

 画像に対して下記のような前処理を実施する

  • リサイズ

  • 反転

  • トリミング/パディング

  • グレースケール変換/2値化/ノイズ除去

  • 色補正

 opencvではpandasのようなチートシートがあれば参考としたかったが、見つからなかったので下記のようにまとめてみた。  (ひとまず画像読み込みなどの基本機能から前処理まで。エッジ検出による特徴抽出、物体検出などは別途取り扱う)

f:id:sd08419ttic:20190302203240p:plain

実装

上記機能を1つのクラスにまとめて取り扱えるように実装した。

クラスのインスタンス生成時にファイルから画像を self.imgに取り込み、他関数呼び出しで変換後の画像を出力する仕組みとしている。

リサイズ処理

OpenCV機能を使い、サイズ直接入力・比率の両方で指定できるように実装

    def resize_img_aspect_ratio(self,ratio=1.0):
        '''
        画像を拡大縮小する (比率指定)
        '''
        if self.img is None:
            return None
        else:
            result = cv2.resize(self.img , (int(self.img.shape[0]*ratio), int(self.img.shape[1]* ratio)))
        return result

    def resize_img_pix_size(self,height = 200, width = 150):
        '''
        画像を拡大縮小する (ピクセルサイズ指定)
        '''
        if self.img is None:
            return None
        else:
            result = cv2.resize(self.img , (int(width), int(height)))
        return result

反転処理

横・縦・両方の軸をパラメータ指定して反転できるように実装した。

    def flip_img(self,mode="x"):
        '''
        画像を反転させる
        x: 横方向反転、y:縦方向反転 xy:両方向反転
        '''
        if self.img is None:
            return None
        else:
            if mode == "x": #X軸方向反転
                result = cv2.flip(self.img,1)
            elif mode == "y": #Y軸方向反転
                result = cv2.flip(self.img,0)
            elif mode == "xy": #両方向反転
                result = cv2.flip(self.img,-1)
            else:
                result = self.img
        return result

トリミング

比率ベースのトリミング/座標直接指定のトリミングの両方ができるように実装した。

    def trim_img_aspect_ratio(self,left=0.10, right = 0.10, up=0.10, down = 0.10):
        '''
        画像をトリミングする (左右上下それぞれでカットする部分の比率を0-1.0の間で指定)
        '''
        if self.img is None:
            return None
        else:
            left = max(min(0.5,left),0.0)
            right = max(min(0.5,right),0.0)
            up = max(min(0.5,up),0.0)
            down = max(min(0.5,down),0.0)
            left_edge = int(self.img.shape[0]*left)
            right_edge = int(self.img.shape[0] - self.img.shape[0]*right)
            up_edge = int(self.img.shape[1]*up)
            down_edge = int(self.img.shape[1] - self.img.shape[1]*down)
            result = self.img[up_edge:down_edge,left_edge:right_edge]
        return result

    def trim_img_pix_size(self,left_edge = 0, right_edge = 0,  up_edge = 0, down_edge = 0):
        '''
        画像をトリミングする (ピクセル指定。画像サイズ以上の場合は無視)
        left_edge,right_edge,up_edge,down_edge
        '''
        if self.img is None:
            return None
        else:
            left = max(min(self.img.shape[0],left_edge),0.0)
            right = max(min(self.img.shape[0],right_edge),0.0)
            up = max(min(self.img.shape[1],up_edge),0.0)
            down = max(min(self.img.shape[1],down_edge),0.0)
            result = self.img[up:down,left:right]
        return result

パディング

指定幅だけ上下左右同じ幅ゼロ埋めする関数とした。 (その他反転するパディングなども可能。opencv公式リファレンスを参照のこと)

    def zero_padding_img(self,pad_width = 10):
        '''
        画像の境界領域をゼロ埋めする
        pad_width:パディング幅サイズ
        '''
        if self.img is None:
            return None
        else:
            PAD_COL = [0,0,0]   #ゼロパディング
            result= cv2.copyMakeBorder(self.img,pad_width,pad_width,pad_width,pad_width,cv2.BORDER_CONSTANT,value=PAD_COL)
        return result

グレースケール・2値化処理

入力画像をグレースケース変換し、2値化アルゴリズムで0 or 255の2値に変換する処理を作成した。 2値化アルゴリズムはパラメータ切り替えで変更できる (デフォルトは大津の二値化)

    def get_gray_img(self,color_im= "None"):
        '''
        グレースケール画像を取得する (color_im:カラー画像データ(option))
        '''
        if color_im is "None":
            if self.img is None:
                return None
            else:
                return cv2.cvtColor(self.img, cv2.COLOR_BGR2GRAY)
        else:
            return cv2.cvtColor(color_im, cv2.COLOR_BGR2GRAY)

    def get_binary_img(self,prm="OTSU"):
        '''
        2値化処理 prm: "OTSU":大津の2値化、"BIN":静的な2値化、"ADAPT":ローカル閾値

        '''
        if self.img is None:
            return None

        gray_img = cv2.cvtColor(self.img, cv2.COLOR_BGR2GRAY)
        if prm == "OTSU":
            _, result = cv2.threshold(gray_img, 0, 255, cv2.THRESH_OTSU)
        elif prm == "BIN":
            _, result = cv2.threshold(gray_img, 128, 255, cv2.THRESH_BINARY)
        elif prm == "ADAPT":
            result = cv2.adaptiveThreshold(gray_img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 55, 20)
        else:
            result = self.img
        return result

モルフォジー変換によるノイズ除去

2値データに対して膨張・収縮・オープニング・クロージング (膨張と収縮を組み合わせる)ことでノイズを消去する手法を実装した。

(手書き文字認識などのスキャンデータ、ROSの障害物地図データなどの処理などに利用できる)

パラメータは公式のままなので画像サイズや特性に応じて設定が必要

    def get_morphology_img(self,prm="Ero",binimg="None"):
        '''
        モルフォロジー変換 prm: "Ero":収縮、"Dil":膨張、"Ope":オープニング(膨張+縮小) "Clo":"クロージング(縮小+膨張)"

        '''
        if binimg is "None":
            if self.img is None:
                return None
            gray_img = cv2.cvtColor(self.img, cv2.COLOR_BGR2GRAY)
            _, bin_img = cv2.threshold(gray_img, 0, 255, cv2.THRESH_OTSU)
        else:
            bin_img = binimg

        kernel = np.ones((3,3),np.uint8)

        if prm == "Ero":
            result = cv2.erode(bin_img, kernel,iterations = 2)
        elif prm == "Dil":
            result = cv2.dilate(bin_img, kernel,iterations = 2)
        elif prm == "Ope":
            result = cv2.morphologyEx(bin_img, cv2.MORPH_OPEN, kernel)
        elif prm == "Clo":
            result = cv2.morphologyEx(bin_img, cv2.MORPH_CLOSE, kernel)
        else:
            result = self.img
        return result

色補正フィルタ

画像の色を補正するためガンマ補正(色濃淡・明るさ)、ガウシアンフィルタ(ジャギー低減)、鮮鋭化フィルタ(エッジ強調)を実装した。

あくまで一般的な例なので、パラメータや他フィルタについても必要に応じて実装する必要がある。

    def get_gamma_img(self,gamma=1.0):
        '''
        ガンマ補正変換 (濃淡の補正)
        gamma: 1.0がデフォルト 小さくするほど薄く白っぽく、高くするほど濃く黒っぽい画像になる

        '''
        if self.img is None:
            return None
        LTB = np.empty((1,256), np.uint8)
        for i in range(256):
            LTB[0,i] = np.clip(pow( float(i) / 255.0, gamma) * 255.0, 0, 255)   #Look Up Tableの生成
 
        result = cv2.LUT(self.img, LTB) #輝度値のガンマ補正
        return result

    def get_gausian_filter_img(self,filtsize=5):
        '''
        ガウシアンフィルタ画像:ぼかしフィルタ (ジャギーなどの低減に有効)
        filtsize:フィルタサイズ(整数型かつ奇数)
        '''
        if self.img is None:
            return None
        # Look up tableを使って画像の輝度値を変更
        result = cv2.GaussianBlur(self.img,(filtsize,filtsize),0)
        return result


    def get_sharp_filter_img(self):
        '''
        鮮鋭化フィルタ画像:ボケた画像のエッジ・テクスチャ協調に有効
        '''
        if self.img is None:
            return None
        kernel = np.array([[-1,-1,-1], [-1,9,-1], [-1,-1,-1]], np.float32)
        result = cv2.filter2D(self.img, -1, kernel)
        return result

ソースコードは下記で公開している。

github.com

参考サイト

labs.eecs.tottori-u.ac.jp

labs.eecs.tottori-u.ac.jp

labs.eecs.tottori-u.ac.jp