techno_memo

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

電子工作プロトタイピングのソフトウェアアーキテクチャについて

この記事の目的

 電子工作のプロトタイピング時のソフトウェアアーキテクチャについて、どのように作るべきかを整理する

電子工作プロトタイピング時の参考書

電子工作でRaspberry piやArduinoを用いたプロトタイピングを行う場合、下記の参考書が便利である。

Raspberry Piで学ぶ電子工作

Raspberry Piの環境構築や各種電子部品を用いた実装まで手を動かして学ぶことができる本

LED点灯からラジコンの操作用ソフトまで多種のセンサ・アクチュエータを取り扱っておりとてもわかりやすい。

Arduinoをはじめよう

Arduinoをはじめよう 第3版 (Make:PROJECTS)

Arduinoをはじめよう 第3版 (Make:PROJECTS)

Arduinoの開発環境構築・使い方や各種電子部品のソフトの基本的なサンプルを取り揃えている。入門書としてとっつきやすい。

実用的なプロトタイプのためのアーキテクチャ

一方で上記の解説書では記載されているサンプルコードをそのまま実行させることで満足してしまい、実用的な機能を作るのが難しいのではないかと感じた。

ユーザーがPCからGUIで処理要求・センサデータの確認をしつつ、アクチュエータを動作させるプロトタイプを実現するためには、下記のような構成が適している。

f:id:sd08419ttic:20190425225053p:plain

上記の構成において、個々のセンサ・アクチュエータは参考書やWebのサンプルページを流用することで動作させることができる。

しかし、一連の機能として連携させて1つのソフトとして動作させるためは下記の要素を理解する必要がある。

項目 説明
プロジェクト構築(Python) 機能ごとにモジュール化/クラス化して整理した複数のPythonScriptから成り立つプロジェクトを構築する
スケジューリング(Python) 機能ごとに適切な周期で処理を呼び出す(GUI/通信などを含む。プロセス処理)
プロジェクト構築(Arduino) 機能ごとにモジュール化/クラス化して整理した複数のCファイルから成り立つプロジェクトを構築する
スケジューリング(Arduino) Arduinoプロジェクトで機能ごとに適切な周期で処理を呼び出す

プロトタイピングの参考書では上記のような点にページを割かず単一機能・mainループの処理のみに記載しているものが多い。

しかし、組込みソフト開発で開発をする場合などには上記概念を理解しておくことが重要であると感じている。

今後の記事で上記について実例を挙げながら説明する。

画像認識①テンプレートマッチング/色に基づく物体認識/エッジ形状に基づく形状認識

やりたいこと

 画像から下記の手段で物体を認識する

  • テンプレートマッチング (正解画像との類似度比較)

  • 色に基づく物体検出 (HSV色空間マスクと輪郭抽出)

  • エッジ形状に基づく物体検出 (cannyエッジ検出とハフ変換)

f:id:sd08419ttic:20190331220930p:plain

画像認識の簡単な説明

『画像認識』として扱われるタスクは、主に下記のようなものがある。

タスク名 説明 応用例
クラス分類(classification) 画像全体を表すラベルを識別する 画像のWEB検索システム
位置推定(localization) 物体のラベルを分類し、それが画像中のどこにあるかを特定する 工場の生産設備(部品の位置決めなど)
物体検出(detection) 画面に映るすべての物体のラベルを分類し、それが画像中のどこにあるかを特定する 自動運転の物体認識(歩行者・対向車など)・防犯カメラの画像認識
領域分割(segmentation) 物体検出した結果をピクセル単位で切り分ける 自動運転の道路領域認識/医療画像の分析(異常箇所の切り分け)

参考サイト

starpentagon.net

大量かつ複雑な問題を解くためには機械学習/DeepLearningを用いた学習・検出が不可欠となるが、本記事ではそれを使うまでもない簡易的なパターン検出アルゴリズムの基本手法を紹介する。

テンプレートマッチング

*正解画像(テンプレート)と入力画像の画素値を直接比較し、類似度の高い箇所を特定する手法

*各画素について絶対値差分2乗和(SQDIFF)、相互相関(CCORR)などで類似度を計算し、閾値で同一とみなせるかを判定する

詳細は下記サイトが詳しい

実装例

labs.eecs.tottori-u.ac.jp

数式の説明

isl.sist.chukyo-u.ac.jp

実装例は下記のようになる。 テンプレート画像はあらかじめ画像ファイルとして用意しておく。 下記例では相関係数の正規化された値を指標として使っている。

def templete_matching(src_img,temp_img,threshold):
    '''
    テンプレートマッチング関数
    src_img:入力画像, temp_img:検出対象画像, threshold:検出敷居値(0-1)
    '''
    img_gray = cv2.cvtColor(src_img, cv2.COLOR_BGR2GRAY)        #入力画像をグレースケール変換する
    temp_gray = cv2.cvtColor(temp_img, cv2.COLOR_BGR2GRAY)      #テンプレート画像をグレースケール変換する
    w= template.shape[1]    #テンプレート画像幅
    h= template.shape[0]    #テンプレート画像高さ

    res = cv2.matchTemplate(img_gray,temp_gray,cv2.TM_CCOEFF_NORMED)    #テンプレートマッチング,相関係数の正規化指標を利用

    res_vis = res.copy()
    res_vis[res_vis<0] = 0.0
    res_vis = np.uint8((res_vis)*100)
    cv2.imshow("score_map",res_vis)    #スコア表示
    cv2.waitKey(0)
    loc = np.where( res >= threshold)   #閾値判定
    for pt in zip(*loc[::-1]):
        cv2.rectangle(src_img, pt, (pt[0] + w, pt[1] + h), (0,0,255), 2)   #結果の描画
    cv2.imshow("result",src_img)    #画面表示
    cv2.waitKey(0)

色に基づく物体検出 (HSV色空間マスクと輪郭抽出)

*検出したい物体の色がわかっているという前提であれば、特定の色の塊を抽出することで物体を検出できる

*通常の画像ファイルはRGB系で扱われることが多いが、色による検出をしやすくすためHSV系に変換して特定の色を閾値判定して抽出する

*色の抽出結果に輪郭検出アルゴリズムを適用してある程度の大きさを持つ物体領域を検出する。

RGBとHSVの変換については下記サイトを利用できる。 (検出用の閾値設定など)

www.peko-step.com

輪郭抽出用にopencvのfindContours関数を利用している。関数のオプションなどについては下記サイトが詳しい。

今回のサンプルコードでは最も外側の輪郭を抽出する設定を用いている。

また、そのまま適用するとノイズが含まれるため抽出できた輪郭から一定の長さ以下のものを削除し、各輪郭の頂点を 検出して長方形で表示するようにしている。

pynote.hatenablog.com

def color_cluster(src_img):
    '''
    色に基づく物体検出
    '''
    hsv = cv2.cvtColor(src_img, cv2.COLOR_BGR2HSV_FULL)             #hsv座標系への変換
    mask = np.zeros((hsv.shape[0],hsv.shape[1],1), dtype=np.uint8)  #画像マスクの生成
    h = hsv[:, :, 0]    #色相
    s = hsv[:, :, 1]    #彩度
    mask[((h < 20) | (h > 200)) & (s > 128)] = 255  #hsv座標系での色マスク(赤色)
    #他の色を設定する場合下記サイトなどで閾値設定する
    #https://www.peko-step.com/tool/hsvrgb.html

    #色マスクに対する輪郭抽出
    contours, hierarchy =cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    filtered_contour = []

    result_img = src_img.copy()

    #輪郭抽出結果のフィルタ (非常に小さい輪郭はノイズとみなして除去))
    for indx in range(len(contours)):
        if (len(contours[indx])>20):    #輪郭が20pixel異常の長さとなる場合
            filtered_contour.append(contours[indx])
            np_contour = np.array(contours[indx]).reshape(len(contours[indx]),2)
            left_x = min(np_contour[:,0])    #輪郭の一番左となるX座標
            right_x = max(np_contour[:,0])   #輪郭の一番右となるX座標
            top_y = min(np_contour[:,1])     #輪郭の一番上となるY座標
            bottom_y = max(np_contour[:,1])  #輪郭の一番下となるY座標
            cv2.rectangle(result_img, (left_x,top_y), (right_x, bottom_y), (0,0,255), 2)    #長方形で物体の領域を表示
            pass
    result_img = cv2.drawContours(result_img, filtered_contour, -1, (0,255,0), 3)
    cv2.imshow("result",result_img)
    cv2.waitKey(0)

エッジ形状に基づく物体検出 (cannyエッジ検出とハフ変換)

検出したい物体の形状が決まっている(線・円など)場合、エッジ検出結果(Canny エッジ検出)+ハフ変換を利用できる。

*元画像に対してエッジ検出フィルタ(Canny エッジ検出など)でエッジ特徴量を抽出する。

*エッジ特徴に対してハフ変換を用いて特定の形状となる成分を抽出する。

cannyエッジ検出フィルタのアルゴリズムについては下記のopencvリファレンスに説明が記載されている。

labs.eecs.tottori-u.ac.jp

上記で抽出したcannyエッジに対してハフ変換を利用して形状を検出する。

ハフ変換については下記のopencvリファレンスに説明が記載されている。

opencvでは直線検出はcv2.HoughLines()、円検出はcv2.HoughCircles()で容易に行うことができる。

labs.eecs.tottori-u.ac.jp

def edge_cluster(src_img):
    '''
    エッジ形状に基づく物体認識
    src_img:入力画像
    '''

    #canny edge http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_imgproc/py_canny/py_canny.html
    edges = cv2.Canny(src_img,100,200)

    #ハフ変換


    #線の検出と描画
    lines = cv2.HoughLines(edges,1,np.pi/180,50)
    for indx in range(len(lines)):
        for rho,theta in lines[indx]:
            a = np.cos(theta)
            b = np.sin(theta)
            x0 = a*rho
            y0 = b*rho
            x1 = int(x0 + 1000*(-b))
            y1 = int(y0 + 1000*(a))
            x2 = int(x0 - 1000*(-b))
            y2 = int(y0 - 1000*(a))
            cv2.line(src_img,(x1,y1),(x2,y2),(0,255,0),2)

    #円の検出と描画
    circles = cv2.HoughCircles(edges,cv2.HOUGH_GRADIENT,1,20,param1=50,param2=30,minRadius=0,maxRadius=0)
    circles = np.uint16(np.around(circles))
    
    for i in circles[0,:]:
        # draw the outer circle
        cv2.circle(src_img,(i[0],i[1]),i[2],(255,0,0),2)
        # draw the center of the circle
        cv2.circle(src_img,(i[0],i[1]),2,(0,0,255),3)

    cv2.imshow("result",src_img)
    cv2.waitKey(0)

実装結果のまとめは下記のgithubに保存している。

github.com

より複雑な特徴量計算(HOG/harr-likeなど)によるもの、DeepLearningを用いたものについても今後記載する。

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)
'''