techno_memo

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

ROS入門 (概要/用語/コマンド)

この記事の目的

 ROSを使った開発の考え方、手順(チュートリアルを通じたコマンドの使い方)についてまとめる

ROSの考え方/用語の整理

ROSの特徴や利点については下記サイトに細かく記載されている。

myenigma.hatenablog.com

特に、自動運転・ロボット関連の試作に便利なライブラリ(自己位置推定など)やデバッグ用のツールが充実しており、それらに関する試作を行う場合には非常に便利なフレームワークといえる。

ROSのプロセス/通信の概念・用語

上記のようにROSは便利なフレームワークではあるが使い方や固有の用語がかなり多い。 公式チュートリアル (http://wiki.ros.org/ja/ROS/Tutorials)を参考にしても良いが、 初級の時点でファイルシステムやパッケージファイルの詳細の説明をしており、直観的な 理解が難しいと感じた。そこで、今後説明しやすいように下記にROSでアプリケーションを 構築する際のプロセス/通信に関する概念と用語をまとめてみた。

参考 http://forestofazumino.web.fc2.com/ros/ros_basics.html

用語 内容
Node プロセス(処理)
Message ノード間の送受信
Master ノード・メッセージの名称管理をする
Topic 非同期で送受信するノード間信号
Subscriber Topicを受信する側のノード
Publisher Topicを送信する側のノード
Service 同期型(受信してすぐ処理を実行する)のノード間通信
Service Server Serviceを受けて実行結果を出力するノード
Service Client Serviceを要求して結果を受理するノード

上記については下記参考書の解説を読むとわかりやすい。


ROSではじめるロボットプログラミング―フリーのロボット用「フレームワーク」 (I・O BOOKS)

ROSコマンドの使い方 (チュートリアルを利用)

ROSではノードの起動や通信内容の確認、可視化をROSコマンドを用いて行う。 基本的には、下記の順で使用することが多い。 公式のチュートリアルにあるturtlesimを用いて使い方をまとめる。

1.roscoreの起動

roscore

各ノード・送受信信号の名前の管理、標準入出力処理を行う ROS上で処理を動作させる場合は必ず起動する必要がある (起動していない状態で別ノードを起動させようとするとroscoreの起動待ちになる)

2.ノードの起動

下記の2つの方法で起動できる。 オプションが多い場合はlaunchファイルを作っておくと良い。

rosrunコマンド

rosrun turtlesim turtlesim_node
# rosrun [package_name] [node_name]
rosrun turtlesim turtle_teleop_key
# turtlesim_nodeとは別のターミナルで起動する

コマンド rosrunを使いパッケージ名とノード名を指定して起動する apt-getで入れたライブラリでは問題ないが、自作で作ったパッケージの 場合パスを通しておく必要がある。

複数のノードを立ち上げる場合はそれぞれターミナルを立ち上げて実行する。

roslaunch

roslaunch beginner_tutorials turtlemimic.launch
#事前に下記チュートリアルに従った準備が必要
#roslaunch beginner_tutorials turtlemimic.launch

launch ファイル内に立ち上げたいノードと起動時のオプション (パラメータの有効/無効など)を渡すと1つのターミナルでノードをまとめて起動できる。ライブラリによっては最初からlaunchファイルで起動できるように提供されているものがある。(turtlesimはないのでチュートリアルに従って記述する必要がある)

3.動作結果の確認

ノードを立ち上げたあと、各ノードの状態・通信内容は下記のコマンドで確認する。

コマンドによる確認

送受信中の信号一覧の表示には下記を利用する。 rostopic list では信号の送受信が成立していなくても、 受信待ちの状態となるだけでリストに出力されることに注意する必要がある。 信号が狙いの周期で更新できているかは rostopic hzなどを用いたほうが良い。

# ROSで通信している全信号リスト
rostopic list

# ROSで通信している信号の出力
# rostopic echo [topic名]
rostopic echo /turtle1/pose

#表示例
---
x: 5.544444561
y: 5.544444561
theta: 0.0
linear_velocity: 0.0
angular_velocity: 0.0
---


# ROSで通信している信号の更新周期の出力
# rostopic hz  [topic名]
rostopic hz /turtle1/pose

#表示例
subscribed to [/turtle1/pose]
average rate: 62.515
    min: 0.015s max: 0.017s std dev: 0.00047s window: 61
average rate: 62.519
    min: 0.014s max: 0.017s std dev: 0.00052s window: 123

GUIによる確認

コマンドによる確認の他に、rosのGUI機能rqt_graphでノードとトピックの関係を 可視化して接続関係を確認することができる。

# GUIツール rqt_graphを用いたノードの通信状態の可視化
rosrun rqt_graph rqt_graph

f:id:sd08419ttic:20190619225134p:plain

また、各トピックの波形 (時系列変化)を表示することもできる。

# GUIツール rqt_graphを用いた波形の時間変化の可視化
rosrun rqt_plot rqt_plot

f:id:sd08419ttic:20190619225554p:plain

rvizによる確認

ROSで用意された形式に従って位置や速度情報を出力するとロボットが空間でどのような動きをしているかをrvizを使って確認することができる。 (tfの発行なども必要なので下記チュートリアルに従う)

wiki.ros.org

rviz

f:id:sd08419ttic:20190619231201p:plain

4.動作結果の保存

動作中のノード間でどのような信号の送受信が行われたのかを保存することができる。この機能を用いることで同じカメラ信号のrosbag信号を流しながら自己位置推定処理のデバッグをシミュレーションで行うなどの使い方が可能になる。

#全ての送受信信号の保存 (ターミナルのパスにbagファイルが生成される)
rosbag record -a
#bagファイルを再生する
#再生中は記録時と同じタイムスタンプでトピックの送受信が行われる
rosbag play recorded1.bag  

よく使うコマンドリストのまとめ

https://qiita.com/tomoyafujita/items/13076d37bcac05a83530

https://raspimouse-sim-tutorial.gitbook.io/project/ros_tutorial

用語 内容
roscore マスターノードの起動 (rosを使用する際は必須)
rosrun [ノード名] ノードの起動
roslaunch [launchファイル名] launchファイルを用いたノード起動
rostopic list ros上で通信しているtopicのリスト出力
rostopic echo [topic名] topic名の内容の出力
rostopic hz [topic名] topicの更新周期の出力
rosbag record -a ros上で通信しているtopicをbagファイルに保存
rosrun rqt_graph rqt_graph rqt_graphの起動
rviz rvizの起動

次回はpythonを使ったノードの開発・トピックの通信例についてまとめる。

Raspberry pi 開発環境について(Ubuntu Mate設定/ROS導入)

この記事の目的

 RaspberryPi3(with ROS/Python) を使った電子工作の環境構築手順についてまとめる

 ROSを使ったロボット制御のソフトウェアを実装するため下記を導入する。

  • Ubuntu Mate 16.04

  • ROS Kinetic

必要なハード

下記記事のハードを用い、Raspberry pi 3 model B を使用する。

sd08419ttic.hatenablog.com

下記の手順はWindows PC で必要なソフトのダウンロードやmicroSDカードへの書き込みなどを行うことを前提とする。

環境構築手順

1.SDカードの初期化

Raspberry pimicroSDカードにOSを書き込む必要がある。

公式サイトではツールSDCardFormatterでFAT32に初期化する手順を推奨しているが、SDカードのサイズが64GB以上の場合exFATに自動的に変換してしまうので注意が必要。

下記のIOデータのハードディスクフォーマッタではフォーマット時にFAT32を明示的に選択することができる。

www.iodata.jp

2.OSイメージの焼き込み

現在(2019.6)時点で、公式サイトのUbuntu Mate は 18.04に更新されている。

Ubuntu Mate 18.04 に対してROSを入れることも可能なようであるが、まだリリースされてから日が浅くトラブルも多いと予想できるので、 ここではUbuntu 16.04を導入する。Ubuntu 16.04はすでに公式サイトからはダウンロードできなくなっているが、下記サイトからダウンロード可能である。 (torrent形式なのでBitTorrentなどを用いてダウンロードする必要がある)

ubuntu-pi-flavour-maker.org

ダウンロードしたubuntu-mate-16.04.2-desktop-armhf-raspberry-pi.img.xz は7.zipでimgファイルに展開する

sevenzip.osdn.jp

展開したimgファイルを下記のWin32 Disk ImagerでmicroSDカードに焼きこむ

sourceforge.net

3.初期設定

上記までの手順でUbuntu Mateを焼きこんだmicro SDカードを、Raspberry pi の SDカードスロットに差してmicro USBから電源供給すると Raspberry pi 3が起動する。

Wifi/地域設定/キーボード設定をインストーラーはインストーラーの手順に従って実施すればできる。

システムの更新

Mate Terminalを開いて下記コマンドを実行

sudo apt-get update 
sudo apt-get upgrade

その他初期設定に関する記事(画面の最大化なども便利)

pinky-blog.com

上記を実施するとfirefoxがクラッシュする。(バージョンによって起動しないとのこと)ブラウザにこだわりがなければ下記でchromiumを導入してしまった方が早い。

sudo apt install chromium-browser

日本語入力対応

地域設定をしておくとGUI表示はデフォルトで日本語になる 言語設定が必要なのでシステム⇒設定⇒ユーザー向け⇒言語サポートからインストールし、入力形式をfcitxに変更する 下記に画像付きの解説がされている。

deviceplus.jp

IP固定

Windows PCからリモートでのログインやデータの受け渡しのために、IPを固定しておくと便利である。 Wifiであれば、画面右上のネットワークアイコン以下のGUIを使って設定が可能である

https://qiita.com/dendensho/items/ab63e4b343607d832f21

ファイル共有設定

sambaのインストールと設定行い、Raspberry pi のファイルをWindows PC からアクセス可能にできる

sudo apt-get -y install samba

上記を実施後、下記サイトにある『UI を使って samba を設定する(おまけ)』に従うとGUIを使ってsambaの共有設定ができる。

https://pinky-blog.com/raspberry-pi/nas-samba-server-ubuntu-mate/

リモートデストップの有効化

下記コマンドでxrdpをインストールする

sudo apt-get -y install xrdp

その後、下記サイトに従って暗号化レベルの変更と日本語キーボード設定を追加する (日本語キーボード設定がないと円マークなどが正しく入力できなくなる)

algorithm.joho.info

4. ROSのインストール

公式の手順に従いros-kineticをインストールする。

sudo sh -c 'echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/ros-latest.list'
sudo apt-key adv --keyserver hkp://ha.pool.sks-keyservers.net:80 --recv-key 421C365BD9FF1F717815A3895523BAEEB01FA116
sudo apt-get update
sudo apt-get upgrade
sudo apt-get install ros-kinetic-desktop-full
sudo apt install python-roslaunch

次にrosのパッケージ管理機能の初期化/更新を行う

rosdep init
rosdep update

最後に環境変数にROSのパスを通す (ROSのコマンドを利用するために必要)

echo "source /opt/ros/kinetic/setup.bash" >> ~/.bashrc
source ~/.bashrc

上記まででRaspberry pi でROSを使う準備が整う。 ROSのcatkin_workspaceの作り方などについては今後別記事で説明する。

Raspberry pi 開発環境について(ハードウェア)

この記事の目的

 Raspberry pi の電子工作の開発環境について必要なハードウェアに関する情報を忘備録として整理する。

必須ハード

Raspberry pi 本体

カメラを使った画像認識などをすることを考慮すると、なるべく高スペックなものが望ましい。

2019年6月現在、下記のRaspberry pi 3 model B+が、最新かつ高スペックなRaspberry piである。


Raspberry Pi3 Model B+ ボード&ケースセット

ただし、1つ前のモデルであるRaspberry pi 3 model Bとのスペック差はさほどない。(CPUクロック数のわずかな上昇と通信速度の上昇程度)のと、Pi3の方が発売されてからの期間が長く各種開発環境の動作報告も多いので 以降の記事ではRaspberry pi 3の利用を前提として記載する。


Raspberry Pi3 Model B ボード&ケースセット

AC電源アダプタ

Raspberry Pi 3は micro USB type B端子での電力供給が必要となる。ただし、消費電流が2.5Aとなることからスマホ等のACアダプタでは対応できない可能性が高いため動作確認できているACアダプタを利用することを推奨する。 また、電源ボタンがなくケーブルからの電力供給の有無で起動を判定するため、頻繁な起動/終了の繰り返しで本体の端子が痛む可能性がある。スイッチボタンによる電力の遮断ができる下記のようなACアダプタを使うと便利である。


Raspberry Pi用電源セット(5V 3.0A)-Pi3フル負荷検証済

micro SDカード

Raspberry PiではOS等のソフトウェアをmicroSDカードに保存する必要がある。OSイメージ+基本的なツールを導入するための最低限の必要容量は4GB程度以上だが、今回の用途ではROS環境一式やカメラでの計測データを保存する必要 があるため32GBのものを利用する。 (なお、32GBを上回る容量の場合、SDカードをFAT32でフォーマットする使うツールに気を付ける必要がある。別途記載予定)


東芝 Toshiba microSDHC 32GB + SD アダプター + 保管用クリアケース [バルク品]

また、インストール手順でPCからmicro SDカードへの書き込みをする必要がある。PCにmicro SDを読み込む手段がない場合にはUSBのSDカードリーダーが必要となる。 (速度にこだわらなければ、最近は100均などでも購入可能)


Transcend USB 3.0 Super Speed カードリーダー (SD/SDHC UHS-I/SDXC UHS-I/microSDXC UHS-I 対応) ブラック 2年保証 TS-RDF5K

マウス/キーボード/HDMIケーブル

Raspberry pi の操作に利用する。マウス/キーボードはUSB接続のものであればよほど問題はないが、無線接続したい場合は動作実績があるものを利用する方が無難。

初期設定を済ませると、デスクトップPCからリモート接続して利用することも可能となるためあまりこだわらなくてもよい。

モバイルバッテリ

前述したように、Raspberry pi 3 は micro USB type Bによる給電が可能なのでモバイルバッテリからの電源供給ができる。ただし、最近のモバイルバッテリは消費電流が低い場合に省電力モードになってしまったり、 Raspberry pi のピーク時の2.5Aを出力できないなどの場合があるため、動作実績があるものを選んだほうが良い。下記サイトで実機での確認がされている。実際に私が試したところAnker PowerCore 10000、 cheero Power Plus 10050mAhともに問題はなかった。

消費電力が大きなRaspberry Pi3に最適のモバイルバッテリーは? - 猿まね電子工作

カメラ

Raspberry pi では専用のカメラモジュールによる画像取得とPC等で使われるUSB Webカメラによる画像取得が可能である。

カメラモジュールは小型・軽量で省電力ではあるが、取り付けなどに苦労する。下記のような台座とセットになっているものの方が良い。


For raspberry pi カメラモジュール 5MP Raspberry Pi 3 b+ / Pi Zero Camera とケース500W画素 感光チップOV5647センサー

Webカメラは、台座が安定してHD撮影可能なものとして下記などが良い。


LOGICOOL HDウェブカム フルHD動画対応 C615

ブレッドボード/ジャンパハーネス

Raspberry pi のGPIOに各種の電子部品を接続する際に必要となる。ジャンパハーネスは端子にオス-メスの組み合わせがあるので一定数まとめ買いしておいた方が良い。


ELEGOO 120pcs多色デュポンワイヤー、arduino用ワイヤ—ゲ—ジ28AWG オス-メス オス-オス メス –メス ブレッドボードジャンパーワイヤー

ブレッドボードはいろいろな場所への取り付けに対応できるよう小型/横長タイプで複数用意しておいた方が良い。


ELEGOO 400タイポイント ブレッドボード3PCS 、Arduino用ジャンパーワイヤ 4電源レール

次回はRaspberry pi の環境構築 (OS設定/ROSの導入/開発ツール類の設定)について記述する。

C言語経験者がpythonで開発する際のポイントまとめ

この記事の目的

 C言語(特に組込みソフト)経験者がpythonで開発する際のポイントをまとめる (よく聞かれたこと、聞かれることの整理用)

 * C言語開発者がpythonを開発する際のポイント・・・階層構造をもつpythonスクリプトのインポート方法

 * 別ファイルのimport方法・・・階層構造をもつpythonスクリプトのインポート方法

 * 変数/定数の定義・・・const・#define・グローバル変数・ローカル変数の扱い

C言語開発者がpythonを開発する際のポイント

 最近のデータ処理・AI関連の技術・オープンソースの発展により、組込み制御ソフト・C/C++など他言語の開発者であってもpythonによるプロトタイピングやデータ分析に手を出すケースが多くなっている。 (私もその一人であるが)

 C言語で組み込み等のソフトの開発経験者がpythonを開発する際のポイントは下記参考記事にまとめられている

qiita.com

 特に留意しておく点は

 インデント・・・C言語と異なりインデント(字下げ)が明示的に意味を持つ。if文、for文などはインデントでどこまでが対象かを規定

 型定義・・・動的型付けがされる言語であり、変数宣言時に型は不要。作る際は楽だが意図せぬ代入がされることが多いためデバッグによる確認が必要

 参照渡し・・・pythonにおける代入は基本的に参照渡しとなる (C言語でいうポインタ的な扱い)

 その他上記の参考サイトにはpythonの動作の仕組み(インタプリタ)、型定義、if文など制御文の書式などに一通り触れられているが、C言語でよくある複数ファイル/フォルダからなる構造の構築方法やC言語でよく使うconst/defineなどについては記載されていないので追加でまとめておく。

別ファイルのimport方法

 C言語の制御ソフトでは、機能別に複数ファイル/フォルダに分けて実装する際、include文とmake環境へのインクルードパス設定を記述する。pythonで同様のことをしたい場合、import文で別ファイルの記述内容を参照することが可能である。

f:id:sd08419ttic:20190602183346p:plain

上記の図でルートフォルダにあるスクリプトroot_func.pyから各フォルダのサブファンクションを直接参照する場合は下記のように記述できる。

#1階層下のファイルを呼び出し
from SubA.sub_funcA import func_sub_funcA
from SubB.sub_funcB import func_sub_funcB

#2階層下のファイルを呼び出し
from SubA.Sub01.sub_funcA1a import func_sub_funcA1a
from SubA.Sub01.sub_funcA1b import func_sub_funcA1b
from SubA.Sub02.sub_funcA2 import func_sub_funcA2

#3階層下のファイルを呼び出し
from SubA.Sub01.Sub03.sub_funcA3 import func_sub_funcA3

if __name__ == '__main__':
    func_sub_funcA()
    func_sub_funcB()
    func_sub_funcA1a()
    func_sub_funcA1b()
    func_sub_funcA2()
    func_sub_funcA3()

一方で、サブフォルダSub01にあるsub_funcA1a.pyから上位階層にある関数をインポートしたい場合、相対パスを通すだけだとエラーが出力されてしまう。

(実行フォルダよりも上位パスにあるファイルのインポートを禁止するpythonの仕様のため)

そこで、サブディレクトリからのインポート時にpythonの探索パスにルートディレクトリのパスを加える方法を用いる。

上図でサブフォルダ01から上位階層や他フォルダを参照するためにシステムパスを変更する。

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

#prj_config.pyまでのパスを追加する

#os.path.abspath(__file__) : pythonファイルが存在する絶対パスを取得
#os.path.dirname(): 入力したパスの一つ上の階層のパスを取得

ROOTPATH = os.path.dirname(os.path.abspath(__file__))
CFGPATH = os.path.dirname(os.path.dirname(ROOTPATH))

#sys.path.append() :入力したパスをpythonファイルの探索パスに追加する

sys.path.append(CFGPATH)

#from にルートフォルダからのフォルダ階層/ファイル名を記載する
from SubA.Sub01.sub_funcA1b import func_sub_funcA1b
from SubA.Sub02.sub_funcA2 import func_sub_funcA2
from SubA.Sub01.Sub03.sub_funcA3 import func_sub_funcA3

def func_sub_funcA1a():
    print("sub_funcA1a is called!!")

if __name__ == '__main__':
    func_sub_funcA1b()
    func_sub_funcA2()
    func_sub_funcA3()
    pass

システムパスにルートフォルダを追加することで、開発時に自分のファイルからの相対的なフォルダ関係ではなくルートフォルダからの階層を考慮した記述で実装することが可能である。

変数/定数の定義

const (定数)について

C言語では変数宣言時にconst修飾子を追加することで書き込み不可な定数を利用することができる。pythonには同様の機能はない (完全に書き込みを防ぐ変数を定義できない)

慣習として、全文大文字の変数を定数として扱うことが多いらしい。

 参考サイト www.tohoho-web.com

defineについて

C言語では#defineを用いてよく使うパラメータやコンパイル時に切り替えたい機能のフラグなどを記述することが一般的である。 (特に組込みソフトでは込み入った実装がされることが多い)

pythonでは#defineにあたる機能はないため、上記のような実装は通常の関数/変数を用いて実現する必要がある。

複数ファイルにまたがる機能の開発を行う場合においては、プロジェクト共通で使う定義(機能の切り替え、主要な値の定義など)設定用のコンフィグを記載したpythonファイルをルートフォルダに作成し、他ファイルからimportするようにつくるとC言語に近いスタイルで開発することができる。

global変数・static変数について

pythonではモジュール内のglobal変数を定義し、他関数から参照することができる。しかし、下記の方法では複数ファイルからの更新はできずC言語のような他ファイルの変数をexternで参照して更新という使い方ではない。

ama-ch.hatenablog.com

他機能から利用する変数はクラス内にまとめて記載したほうが開発後の再利用性やデバッグなどの効率が良い。下記のようなクラスを用いるとC言語のイメージに近い実装ができると考えている。

class sub_function_X():

    #初期化用クラス (変数の宣言/初期化)
    def __init__(self):

        #グローバル変数 (他機能からの参照がある変数)
        self.glb_var1 = 0
        self.glb_flg = True

        #スタティック変数 (公開範囲をクラス内だけにしたい変数)
        self.__sts_var1 = 0

    #クラス内ローカル関数
    def __static_func(self):
        self.__sts_var1 = self.__sts_var1 + 1
        print ("This is Private function")

    #グローバル関数
    def global_func(self):
        self.__static_func()
        print ("This is Global function")
        print(self.__sts_var1)

if __name__ == '__main__':
    test_instance = sub_function_X()
    test_instance.global_func()

Arduinoのソフト構造・スケジューリングについて

この記事の目的

 Arduinoで複数の機能を効率よく開発するため、ファイル構造・スケジューリング設定がしやすいプロジェクトの構築方法を記載する

 特に下記に重点を当てて説明する

  • ファイル構造・・・1ファイルを機能単位で構成し、複数のCファイル/Hファイルを扱えるようにする

  • スケジューリング・・・各機能を順序性を保ちながら一定周期で呼び出しやすいソフト構造を取り入れる

Arduinoのソフトの実装方法と課題

 Arduino向けのソフトを開発する場合に最も基本的な方法は公式サイトで配布されているArduino IDEを用いて.inoファイルに処理を記載する方法である。

公式サイトへのリンク

www.arduino.cc

簡易的なスクリプトの開発

n.mtng.org

小規模なプロジェクトであれば上記ツールで記載しても問題はないが、複数人数で開発する処理内容が多いプロジェクト(外部通信/センシング/アクチュエータ制御を同時に実施するなど)の場合

下記のような課題がある。

  • 1ファイル内の処理内容が長くなり複数人同時開発がしにくい

  • 動作タイミングが処理の長さに依存し、一定周期で呼び出す処理などが記載しにくい。 (たとえばPCとの通信が発生するときだけ、センサ処理が遅れるなど)

そこで、下記に示すように複数ファイルに独立した処理を記載して、スケジューラで周期的に呼び出すことが可能なソフトウェアアーキテクチャの構築を試みる。

f:id:sd08419ttic:20190527220839p:plain

機能別ファイル構造の構築

公式IDEを使う場合には気づきにくいが、ArduinoではC言語の書式に従い複数のcpp/hファイルに記載されたファイルのコンパイル・リンクが可能である。

下記サイトに従いVisual Studio CodeC言語の記述に従って開発可能な環境を構築するとintellisenseによるコード補間やジャンプも利用できるため効率が良い。

qiita.com

ポイントは下記となる。

  • "プロジェクト名.ino"ファイルはarduinoプロジェクトのコンパイルに必須である。(処理は記述せずダミーファイルとして残す)

  • 関数setup(初期化関数)、loop(周期関数)は必須関数となる。また、ヘッダArduino.hをインクルードする。(下記のように標準ヘッダにまとめると管理しやすい)

  • その他の機能は任意のファイル名.cpp上に開発し、ヘッダのインクルードによって機能を呼び出す。

  • 関数loopに後述のスケジューリング処理を入れると周期呼び出しがしやすい環境が構築できる。

#ifndef __STDAFX_H__
#define __STDAFX_H__
 
#include "Arduino.h"


void setup();
void loop();

#endif /* __STDAFX_H__ */

処理の周期呼び出しのスケジューリング

 複数の処理を一定周期(数ミリ秒など)で順序に従い呼び出すためには、一定周期の経過をカウントして呼び出すスケジューラを使うと効率化できる。

 下記サイトに公開されてるintervalクラスを用いると上記機能に容易に対応できる。

lowreal.net

 ただしそのまま使うとタイマの単位がミリ秒とやや長い間隔なので、millis()をmicros()にしてlong型でカウンタを保持するとより短い周期にも対応できる。

template <uint16_t time>
class interval {
    unsigned long next_run = 0;

    template <class T>
    void _run(T func) {
        unsigned long now = micros();
        if (next_run < now) {
            func();
            next_run = now + time;
        }
    }

    interval() {}
public:

    template <class T>
    static void run(T func) {
        static interval<time> instance;
        instance._run(func);
    }
};

 上記クラスを使って生成した周期的な動作タイミングに加え、スケジューラ内でも分散カウンタ (switch case文で実装)をすると通信/センシング/アクチュエータなどの各機能が周期条件以内で呼び出されることを確認しながら処理を実装することが可能になる。(下記は5msec毎に種類の違う処理を順版に呼び出す例)

#include "stdafx.h"
#include "communication.h"
#include "sensor.h"
#include "actuator.h"

void setup()
{
    com_init();
    sensor_init();
    actuator_init();
}

void loop()
{
    interval<5000>::run([]{
    switch (cyc_count)
    {
       case 0:
        com_main();
        break;
       case 1:
        sensor_main();
        break;
       case 2:
        actuator_main();
        break;
    }
    if (cyc_count>=2)
    {
            cyc_count = 0;
    }
    else
    {
            cyc_count++;
    }
}

上記をまとめたarduinoの開発用テンプレートをgithubに保存する。

github.com

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

この記事の目的

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

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

電子工作で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を用いたものについても今後記載する。